From f623764b2eb5496055a23dafb39ecafa6aada635 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 11 Apr 2023 09:11:56 -0400 Subject: [PATCH 001/172] Pin pygments in CI (#9938) The recent release of pygments 2.15.0 [1] has started emitting an error in the custom qasm pygments lexer included in qiskit in OpenQASMLexer. In the interest of not blocking CI this commit pins the pygments version we're installing in CI. Longer term, we should probably look at just deprecating the qiskit version and just using openqasm-pygments which is the pygments tools for OpenQASM maintained by the openqasm community. [1] https://pypi.org/project/Pygments/2.15.0/ --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index dd814caf7641..20ada530e452 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,7 +19,7 @@ reno>=3.4.0 Sphinx>=5.0 qiskit-sphinx-theme>=1.6 sphinx-design>=0.2.0 -pygments>=2.4 +pygments>=2.4,<2.15 scikit-learn>=0.20.0 scikit-quant<=0.7;platform_system != 'Windows' jax;platform_system != 'Windows' From 912ba0783420463b4adf7f53bee7cba04bd915f8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 11 Apr 2023 09:39:19 -0400 Subject: [PATCH 002/172] Fix CheckMap pass target handling (#9929) In the recently merged 5672c70649b4f83a6c7146c68a6a543d4a3908fd the CheckMap transpiler pass had issues when handling some Target inputs (which was added as a supported type in that PR). These issues were actually pointed out in code review during #9263 but the follow up slipped through. This commit makes the required fixes and also updates the signature of the CheckMap to match the other changes made in #9263. --- qiskit/transpiler/passes/utils/check_map.py | 27 +++++++---------- .../transpiler/preset_passmanagers/common.py | 5 +++- .../transpiler/preset_passmanagers/level1.py | 2 +- test/python/transpiler/test_check_map.py | 4 +-- .../references/pass_manager_standard.dot | 30 +++++++++---------- .../references/pass_manager_style.dot | 30 +++++++++---------- 6 files changed, 46 insertions(+), 52 deletions(-) diff --git a/qiskit/transpiler/passes/utils/check_map.py b/qiskit/transpiler/passes/utils/check_map.py index f014bdb78203..69bc9707d318 100644 --- a/qiskit/transpiler/passes/utils/check_map.py +++ b/qiskit/transpiler/passes/utils/check_map.py @@ -13,6 +13,7 @@ """Check if a DAG circuit is already mapped to a coupling map.""" from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.transpiler.target import Target from qiskit.circuit.controlflow import ControlFlowOp @@ -27,30 +28,24 @@ class CheckMap(AnalysisPass): for a target use the :class:`~.CheckGateDirection` pass instead. """ - def __init__(self, coupling_map=None, target=None): + def __init__(self, coupling_map): """CheckMap initializer. Args: - coupling_map (CouplingMap): Directed graph representing a coupling map. - target (Target): A target representing the target backend, if both - ``coupling_map`` and this are specified then this argument will take - precedence and ``coupling_map`` will be ignored. + coupling_map (Union[CouplingMap, Target]): Directed graph representing a coupling map. """ super().__init__() - if coupling_map is None and target is None: + if isinstance(coupling_map, Target): + cmap = coupling_map.build_coupling_map() + else: + cmap = coupling_map + if cmap is None: self.qargs = None else: self.qargs = set() - if target is not None: - if target.qargs is not None: - for edge in target.qargs: - if len(edge) == 2: - self.qargs.add(edge) - self.qargs.add((edge[1], edge[0])) - else: - for edge in coupling_map.get_edges(): - self.qargs.add(edge) - self.qargs.add((edge[1], edge[0])) + for edge in cmap.get_edges(): + self.qargs.add(edge) + self.qargs.add((edge[1], edge[0])) def run(self, dag): """Run the CheckMap pass on `dag`. diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 04a1347a761a..a340aee45d8d 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -273,7 +273,10 @@ def _run_post_layout_condition(property_set): return False routing = PassManager() - routing.append(CheckMap(coupling_map, target=target)) + if target is not None: + routing.append(CheckMap(target)) + else: + routing.append(CheckMap(coupling_map)) def _swap_condition(property_set): return not property_set["is_swap_mapped"] diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index b5d435c9b6e3..0a1b857691b1 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -121,7 +121,7 @@ def _vf2_match_not_found(property_set): _choose_layout_0 = ( [] if pass_manager_config.layout_method - else [TrivialLayout(coupling_map_layout), CheckMap(coupling_map, target=target)] + else [TrivialLayout(coupling_map_layout), CheckMap(coupling_map_layout)] ) _choose_layout_1 = ( diff --git a/test/python/transpiler/test_check_map.py b/test/python/transpiler/test_check_map.py index 329eb7edc7cc..f5283b240366 100644 --- a/test/python/transpiler/test_check_map.py +++ b/test/python/transpiler/test_check_map.py @@ -59,7 +59,7 @@ def test_trivial_nop_map_target(self): circuit.h(qr) target = Target() dag = circuit_to_dag(circuit) - pass_ = CheckMap(target=target) + pass_ = CheckMap(target) pass_.run(dag) self.assertTrue(pass_.property_set["is_swap_mapped"]) @@ -120,7 +120,7 @@ def test_swap_mapped_false_target(self): target.add_instruction(CXGate(), {(0, 2): None, (2, 1): None}) dag = circuit_to_dag(circuit) - pass_ = CheckMap(target=target) + pass_ = CheckMap(target) pass_.run(dag) self.assertFalse(pass_.property_set["is_swap_mapped"]) diff --git a/test/python/visualization/references/pass_manager_standard.dot b/test/python/visualization/references/pass_manager_standard.dot index c3af9aaa37af..d4b1d30422d5 100644 --- a/test/python/visualization/references/pass_manager_standard.dot +++ b/test/python/visualization/references/pass_manager_standard.dot @@ -53,39 +53,37 @@ fontname=helvetica; label="[5] "; labeljust=l; 16 [color=red, fontname=helvetica, label=CheckMap, shape=rectangle]; -17 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=dashed]; +17 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; 17 -> 16; -18 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; -18 -> 16; 12 -> 16; } -subgraph cluster_19 { +subgraph cluster_18 { fontname=helvetica; label="[6] do_while"; labeljust=l; -20 [color=blue, fontname=helvetica, label=BarrierBeforeFinalMeasurements, shape=rectangle]; -16 -> 20; +19 [color=blue, fontname=helvetica, label=BarrierBeforeFinalMeasurements, shape=rectangle]; +16 -> 19; } -subgraph cluster_21 { +subgraph cluster_20 { fontname=helvetica; label="[7] "; labeljust=l; -22 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; -23 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; -23 -> 22; -24 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; -24 -> 22; -20 -> 22; +21 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; +22 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; +22 -> 21; +23 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; +23 -> 21; +19 -> 21; } -subgraph cluster_25 { +subgraph cluster_24 { fontname=helvetica; label="[8] "; labeljust=l; -26 [color=blue, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; -22 -> 26; +25 [color=blue, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; +21 -> 25; } } diff --git a/test/python/visualization/references/pass_manager_style.dot b/test/python/visualization/references/pass_manager_style.dot index 54815259b050..5cfd2a95ea56 100644 --- a/test/python/visualization/references/pass_manager_style.dot +++ b/test/python/visualization/references/pass_manager_style.dot @@ -53,39 +53,37 @@ fontname=helvetica; label="[5] "; labeljust=l; 16 [color=green, fontname=helvetica, label=CheckMap, shape=rectangle]; -17 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=dashed]; +17 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; 17 -> 16; -18 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; -18 -> 16; 12 -> 16; } -subgraph cluster_19 { +subgraph cluster_18 { fontname=helvetica; label="[6] do_while"; labeljust=l; -20 [color=blue, fontname=helvetica, label=BarrierBeforeFinalMeasurements, shape=rectangle]; -16 -> 20; +19 [color=blue, fontname=helvetica, label=BarrierBeforeFinalMeasurements, shape=rectangle]; +16 -> 19; } -subgraph cluster_21 { +subgraph cluster_20 { fontname=helvetica; label="[7] "; labeljust=l; -22 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; -23 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; -23 -> 22; -24 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; -24 -> 22; -20 -> 22; +21 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; +22 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; +22 -> 21; +23 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; +23 -> 21; +19 -> 21; } -subgraph cluster_25 { +subgraph cluster_24 { fontname=helvetica; label="[8] "; labeljust=l; -26 [color=grey, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; -22 -> 26; +25 [color=grey, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; +21 -> 25; } } From 47e7c4c881bba1e7445c875f50bab8a29fc25a00 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Tue, 11 Apr 2023 12:05:29 -0600 Subject: [PATCH 003/172] Pin Sphinx theme to minor release (#9949) --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 20ada530e452..d7befa86a240 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,7 +17,7 @@ ddt>=1.2.0,!=1.4.0,!=1.4.3 seaborn>=0.9.0 reno>=3.4.0 Sphinx>=5.0 -qiskit-sphinx-theme>=1.6 +qiskit-sphinx-theme~=1.10.3 sphinx-design>=0.2.0 pygments>=2.4,<2.15 scikit-learn>=0.20.0 From 0a50118355f43690f6f0bb41c5108c09c6449a19 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 11 Apr 2023 15:20:21 -0400 Subject: [PATCH 004/172] Ensure we run VF2PostLayout when needed in optimization level 1 (#9941) * Ensure we run VF2PostLayout when needed in optimization level 1 This commit fixes an issue where we were incorrectly not running VF2PostLayout in the one case where it should be run. Optimization level 1 is more involved than higher optimization levels because for backwards compatibility it runs multiple different passes to try and find a perfect layout, first starting with TrivialLayout, then VF2Layout, and only if those 2 fail to find a perfect match do we run SabreLayout (or DenseLayout and StochasticSwap if control flow are present). We only should run VF2PostLayout if we're running the heuristic layout pass. However, when we integrated routing into SabreLayout the checking for whether we found a pefect layout in the layout stage stopped working as expected. To fix this issue a new flag is added to the CheckMap pass to specify an alternative property set field to store the results of the check in. The underlying issue was that the property set field between the earlier check that we found a perfect layout and the later check whether we should run a standalone routing pass. To fix this the new flag is used to spearate the results from the multiple CheckMap calls. and then we only condition the VF2PostLayout condition on the results of the perfect layout check and not the later routing check. Fixes #9936 * Fix remove out of date comments --- qiskit/transpiler/passes/utils/check_map.py | 24 +- .../transpiler/preset_passmanagers/common.py | 6 +- .../vf2postlayout-fix-16bb54d9bdf3aaf6.yaml | 23 ++ .../transpiler/test_preset_passmanagers.py | 212 +++++++++++++++++- .../references/pass_manager_standard.dot | 28 +-- .../references/pass_manager_style.dot | 28 +-- 6 files changed, 272 insertions(+), 49 deletions(-) create mode 100644 releasenotes/notes/vf2postlayout-fix-16bb54d9bdf3aaf6.yaml diff --git a/qiskit/transpiler/passes/utils/check_map.py b/qiskit/transpiler/passes/utils/check_map.py index 69bc9707d318..f71e5640f163 100644 --- a/qiskit/transpiler/passes/utils/check_map.py +++ b/qiskit/transpiler/passes/utils/check_map.py @@ -22,19 +22,27 @@ class CheckMap(AnalysisPass): Check if a DAGCircuit is mapped to `coupling_map` by checking that all 2-qubit interactions are laid out to be on adjacent qubits in the global coupling - map of the device, setting the property ``is_swap_mapped`` to ``True`` or ``False`` - accordingly. Note this does not validate directionality of the connectivity between - qubits. If you need to check gates are implemented in a native direction - for a target use the :class:`~.CheckGateDirection` pass instead. + map of the device, setting the property set field (either specified with ``property_set_field`` + or the default ``is_swap_mapped``) to ``True`` or ``False`` accordingly. Note this does not + validate directionality of the connectivity between qubits. If you need to check gates are + implemented in a native direction for a target use the :class:`~.CheckGateDirection` pass + instead. """ - def __init__(self, coupling_map): + def __init__(self, coupling_map, property_set_field=None): """CheckMap initializer. Args: coupling_map (Union[CouplingMap, Target]): Directed graph representing a coupling map. + property_set_field (str): An optional string to specify the property set field to + store the result of the check. If not default the result is stored in + ``"is_swap_mapped"``. """ super().__init__() + if property_set_field is None: + self.property_set_field = "is_swap_mapped" + else: + self.property_set_field = property_set_field if isinstance(coupling_map, Target): cmap = coupling_map.build_coupling_map() else: @@ -58,7 +66,7 @@ def run(self, dag): """ from qiskit.converters import circuit_to_dag - self.property_set["is_swap_mapped"] = True + self.property_set[self.property_set_field] = True if not self.qargs: return @@ -77,7 +85,7 @@ def run(self, dag): physical_q0, physical_q1, ) - self.property_set["is_swap_mapped"] = False + self.property_set[self.property_set_field] = False return elif is_controlflow_op: order = [qubit_indices[bit] for bit in node.qargs] @@ -86,5 +94,5 @@ def run(self, dag): mapped_dag = dag.copy_empty_like() mapped_dag.compose(dag_block, qubits=order) self.run(mapped_dag) - if not self.property_set["is_swap_mapped"]: + if not self.property_set[self.property_set_field]: return diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index a340aee45d8d..02654016f4d6 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -274,12 +274,12 @@ def _run_post_layout_condition(property_set): routing = PassManager() if target is not None: - routing.append(CheckMap(target)) + routing.append(CheckMap(target, property_set_field="routing_not_needed")) else: - routing.append(CheckMap(coupling_map)) + routing.append(CheckMap(coupling_map, property_set_field="routing_not_needed")) def _swap_condition(property_set): - return not property_set["is_swap_mapped"] + return not property_set["routing_not_needed"] if use_barrier_before_measurement: routing.append([BarrierBeforeFinalMeasurements(), routing_pass], condition=_swap_condition) diff --git a/releasenotes/notes/vf2postlayout-fix-16bb54d9bdf3aaf6.yaml b/releasenotes/notes/vf2postlayout-fix-16bb54d9bdf3aaf6.yaml new file mode 100644 index 000000000000..9ff099b927f7 --- /dev/null +++ b/releasenotes/notes/vf2postlayout-fix-16bb54d9bdf3aaf6.yaml @@ -0,0 +1,23 @@ +--- +features: + - | + The :class:~.CheckMap` transpiler pass has a new keyword argument on its + constructor, ``property_set_field``. This argument can be used to specify + a field in the property set to store the results of the analysis. + Previously, it was only possible to store the result in the field + ``"is_swap_mapped"`` (which is the default). This enables you to store the + result of multiple instances of the pass in a :class:`~.PassManager` in + different fields. +fixes: + - | + Fixed an issue in :func:`~.transpile` with ``optimization_level=1`` (as + well as in the preset pass managers returned by + :func:`~.generate_preset_pass_manager` and :func:`~.level_1_pass_manager`) + where previously if the ``routing_method`` and ``layout_method`` arguments + were not set and no control flow operations were present in the circuit + then in cases where routing was required the + :class:`~.VF2PostLayout` transpiler pass would not be run. This was the + opposite of the expected behavior because :class:`~.VF2PostLayout` is + intended to find a potentially better performing layout after a heuristic + layout pass and routing are run. + Fixed `#9936 `__ diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 019750c76fc6..4d5eb4f45049 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -20,7 +20,7 @@ import numpy as np from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister -from qiskit.circuit import Qubit, Gate, ControlFlowOp +from qiskit.circuit import Qubit, Gate, ControlFlowOp, ForLoopOp from qiskit.compiler import transpile, assemble from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspilerError from qiskit.circuit.library import U2Gate, U3Gate, QuantumVolume, CXGate, CZGate, XGate @@ -445,6 +445,194 @@ def get_translation_stage_plugin(self): self.assertIn("PadDynamicalDecoupling", self.passes) self.assertIn("RemoveResetInZeroState", self.passes) + def test_level1_runs_vf2post_layout_when_routing_required(self): + """Test that if we run routing as part of sabre layout VF2PostLayout runs.""" + target = FakeLagosV2() + qc = QuantumCircuit(5) + qc.h(0) + qc.cy(0, 1) + qc.cy(0, 2) + qc.cy(0, 3) + qc.cy(0, 4) + qc.measure_all() + _ = transpile(qc, target, optimization_level=1, callback=self.callback) + # Expected call path for layout and routing is: + # 1. TrivialLayout (no perfect match) + # 2. VF2Layout (no perfect match) + # 3. SabreLayout (heuristic layout and also runs routing) + # 4. VF2PostLayout (applies a better layout) + self.assertIn("TrivialLayout", self.passes) + self.assertIn("VF2Layout", self.passes) + self.assertIn("SabreLayout", self.passes) + self.assertIn("VF2PostLayout", self.passes) + # Assert we don't run standalone sabre swap + self.assertNotIn("SabreSwap", self.passes) + + def test_level1_runs_vf2post_layout_when_routing_method_set_and_required(self): + """Test that if we run routing as part of sabre layout VF2PostLayout runs.""" + target = FakeLagosV2() + qc = QuantumCircuit(5) + qc.h(0) + qc.cy(0, 1) + qc.cy(0, 2) + qc.cy(0, 3) + qc.cy(0, 4) + qc.measure_all() + _ = transpile( + qc, target, optimization_level=1, routing_method="stochastic", callback=self.callback + ) + # Expected call path for layout and routing is: + # 1. TrivialLayout (no perfect match) + # 2. VF2Layout (no perfect match) + # 3. SabreLayout (heuristic layout and also runs routing) + # 4. VF2PostLayout (applies a better layout) + self.assertIn("TrivialLayout", self.passes) + self.assertIn("VF2Layout", self.passes) + self.assertIn("SabreLayout", self.passes) + self.assertIn("VF2PostLayout", self.passes) + self.assertIn("StochasticSwap", self.passes) + + def test_level1_not_runs_vf2post_layout_when_layout_method_set(self): + """Test that if we don't run VF2PostLayout with custom layout_method.""" + target = FakeLagosV2() + qc = QuantumCircuit(5) + qc.h(0) + qc.cy(0, 1) + qc.cy(0, 2) + qc.cy(0, 3) + qc.cy(0, 4) + qc.measure_all() + _ = transpile( + qc, target, optimization_level=1, layout_method="dense", callback=self.callback + ) + self.assertNotIn("TrivialLayout", self.passes) + self.assertNotIn("VF2Layout", self.passes) + self.assertNotIn("SabreLayout", self.passes) + self.assertNotIn("VF2PostLayout", self.passes) + self.assertIn("DenseLayout", self.passes) + self.assertIn("SabreSwap", self.passes) + + def test_level1_not_run_vf2post_layout_when_trivial_is_perfect(self): + """Test that if we find a trivial perfect layout we don't run vf2post.""" + target = FakeLagosV2() + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + _ = transpile(qc, target, optimization_level=1, callback=self.callback) + self.assertIn("TrivialLayout", self.passes) + self.assertNotIn("VF2Layout", self.passes) + self.assertNotIn("SabreLayout", self.passes) + self.assertNotIn("VF2PostLayout", self.passes) + # Assert we don't run standalone sabre swap + self.assertNotIn("SabreSwap", self.passes) + + def test_level1_not_run_vf2post_layout_when_vf2layout_is_perfect(self): + """Test that if we find a vf2 perfect layout we don't run vf2post.""" + target = FakeLagosV2() + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.measure_all() + _ = transpile(qc, target, optimization_level=1, callback=self.callback) + self.assertIn("TrivialLayout", self.passes) + self.assertIn("VF2Layout", self.passes) + self.assertNotIn("SabreLayout", self.passes) + self.assertNotIn("VF2PostLayout", self.passes) + # Assert we don't run standalone sabre swap + self.assertNotIn("SabreSwap", self.passes) + + def test_level1_runs_vf2post_layout_when_routing_required_control_flow(self): + """Test that if we run routing as part of sabre layout VF2PostLayout runs.""" + target = FakeLagosV2() + _target = target.target + target._target.add_instruction(ForLoopOp, name="for_loop") + qc = QuantumCircuit(5) + qc.h(0) + qc.cy(0, 1) + qc.cy(0, 2) + qc.cy(0, 3) + qc.cy(0, 4) + with qc.for_loop((1,)): + qc.cx(0, 1) + qc.measure_all() + _ = transpile(qc, target, optimization_level=1, callback=self.callback) + # Expected call path for layout and routing is: + # 1. TrivialLayout (no perfect match) + # 2. VF2Layout (no perfect match) + # 3. DenseLayout (heuristic layout) + # 4. StochasticSwap + # 4. VF2PostLayout (applies a better layout) + self.assertIn("TrivialLayout", self.passes) + self.assertIn("VF2Layout", self.passes) + self.assertIn("DenseLayout", self.passes) + self.assertIn("StochasticSwap", self.passes) + self.assertIn("VF2PostLayout", self.passes) + + def test_level1_not_runs_vf2post_layout_when_layout_method_set_control_flow(self): + """Test that if we don't run VF2PostLayout with custom layout_method.""" + target = FakeLagosV2() + _target = target.target + target._target.add_instruction(ForLoopOp, name="for_loop") + qc = QuantumCircuit(5) + qc.h(0) + qc.cy(0, 1) + qc.cy(0, 2) + qc.cy(0, 3) + qc.cy(0, 4) + with qc.for_loop((1,)): + qc.cx(0, 1) + qc.measure_all() + _ = transpile( + qc, target, optimization_level=1, layout_method="dense", callback=self.callback + ) + self.assertNotIn("TrivialLayout", self.passes) + self.assertNotIn("VF2Layout", self.passes) + self.assertNotIn("SabreLayout", self.passes) + self.assertNotIn("VF2PostLayout", self.passes) + self.assertIn("DenseLayout", self.passes) + self.assertIn("StochasticSwap", self.passes) + + def test_level1_not_run_vf2post_layout_when_trivial_is_perfect_control_flow(self): + """Test that if we find a trivial perfect layout we don't run vf2post.""" + target = FakeLagosV2() + _target = target.target + target._target.add_instruction(ForLoopOp, name="for_loop") + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + with qc.for_loop((1,)): + qc.cx(0, 1) + qc.measure_all() + _ = transpile(qc, target, optimization_level=1, callback=self.callback) + self.assertIn("TrivialLayout", self.passes) + self.assertNotIn("VF2Layout", self.passes) + self.assertNotIn("DenseLayout", self.passes) + self.assertNotIn("StochasticSwap", self.passes) + self.assertNotIn("VF2PostLayout", self.passes) + + def test_level1_not_run_vf2post_layout_when_vf2layout_is_perfect_control_flow(self): + """Test that if we find a vf2 perfect layout we don't run vf2post.""" + target = FakeLagosV2() + _target = target.target + target._target.add_instruction(ForLoopOp, name="for_loop") + qc = QuantumCircuit(4) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + with qc.for_loop((1,)): + qc.cx(0, 1) + qc.measure_all() + _ = transpile(qc, target, optimization_level=1, callback=self.callback) + self.assertIn("TrivialLayout", self.passes) + self.assertIn("VF2Layout", self.passes) + self.assertNotIn("DenseLayout", self.passes) + self.assertNotIn("VF2PostLayout", self.passes) + self.assertNotIn("StochasticSwap", self.passes) + @ddt class TestInitialLayouts(QiskitTestCase): @@ -717,18 +905,18 @@ def test_layout_tokyo_fully_connected_cx(self, level): sabre_layout = { 0: ancilla[0], - 1: ancilla[1], - 2: ancilla[2], - 3: ancilla[3], - 4: ancilla[4], + 1: qr[4], + 2: ancilla[1], + 3: ancilla[2], + 4: ancilla[3], 5: qr[1], - 6: qr[2], - 7: ancilla[5], - 8: ancilla[6], - 9: ancilla[7], - 10: qr[3], - 11: qr[0], - 12: qr[4], + 6: qr[0], + 7: ancilla[4], + 8: ancilla[5], + 9: ancilla[6], + 10: qr[2], + 11: qr[3], + 12: ancilla[7], 13: ancilla[8], 14: ancilla[9], 15: ancilla[10], diff --git a/test/python/visualization/references/pass_manager_standard.dot b/test/python/visualization/references/pass_manager_standard.dot index d4b1d30422d5..642f51b0a24f 100644 --- a/test/python/visualization/references/pass_manager_standard.dot +++ b/test/python/visualization/references/pass_manager_standard.dot @@ -55,35 +55,37 @@ labeljust=l; 16 [color=red, fontname=helvetica, label=CheckMap, shape=rectangle]; 17 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; 17 -> 16; +18 [color=black, fontname=helvetica, fontsize=10, label=property_set_field, shape=ellipse, style=dashed]; +18 -> 16; 12 -> 16; } -subgraph cluster_18 { +subgraph cluster_19 { fontname=helvetica; label="[6] do_while"; labeljust=l; -19 [color=blue, fontname=helvetica, label=BarrierBeforeFinalMeasurements, shape=rectangle]; -16 -> 19; +20 [color=blue, fontname=helvetica, label=BarrierBeforeFinalMeasurements, shape=rectangle]; +16 -> 20; } -subgraph cluster_20 { +subgraph cluster_21 { fontname=helvetica; label="[7] "; labeljust=l; -21 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; -22 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; -22 -> 21; -23 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; -23 -> 21; -19 -> 21; +22 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; +23 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; +23 -> 22; +24 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; +24 -> 22; +20 -> 22; } -subgraph cluster_24 { +subgraph cluster_25 { fontname=helvetica; label="[8] "; labeljust=l; -25 [color=blue, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; -21 -> 25; +26 [color=blue, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; +22 -> 26; } } diff --git a/test/python/visualization/references/pass_manager_style.dot b/test/python/visualization/references/pass_manager_style.dot index 5cfd2a95ea56..64eebf64c08e 100644 --- a/test/python/visualization/references/pass_manager_style.dot +++ b/test/python/visualization/references/pass_manager_style.dot @@ -55,35 +55,37 @@ labeljust=l; 16 [color=green, fontname=helvetica, label=CheckMap, shape=rectangle]; 17 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; 17 -> 16; +18 [color=black, fontname=helvetica, fontsize=10, label=property_set_field, shape=ellipse, style=dashed]; +18 -> 16; 12 -> 16; } -subgraph cluster_18 { +subgraph cluster_19 { fontname=helvetica; label="[6] do_while"; labeljust=l; -19 [color=blue, fontname=helvetica, label=BarrierBeforeFinalMeasurements, shape=rectangle]; -16 -> 19; +20 [color=blue, fontname=helvetica, label=BarrierBeforeFinalMeasurements, shape=rectangle]; +16 -> 20; } -subgraph cluster_20 { +subgraph cluster_21 { fontname=helvetica; label="[7] "; labeljust=l; -21 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; -22 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; -22 -> 21; -23 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; -23 -> 21; -19 -> 21; +22 [color=blue, fontname=helvetica, label=GateDirection, shape=rectangle]; +23 [color=black, fontname=helvetica, fontsize=10, label=coupling_map, shape=ellipse, style=solid]; +23 -> 22; +24 [color=black, fontname=helvetica, fontsize=10, label=target, shape=ellipse, style=dashed]; +24 -> 22; +20 -> 22; } -subgraph cluster_24 { +subgraph cluster_25 { fontname=helvetica; label="[8] "; labeljust=l; -25 [color=grey, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; -21 -> 25; +26 [color=grey, fontname=helvetica, label=RemoveResetInZeroState, shape=rectangle]; +22 -> 26; } } From 6950efd470f942bc046327e174465e8adc825b7e Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Wed, 12 Apr 2023 05:22:12 +0900 Subject: [PATCH 005/172] Add support for pulse reference to QPY (#9890) * Add support pulse reference to QPY * Review comments Co-authored-by: Matthew Treinish --------- Co-authored-by: Matthew Treinish --- qiskit/qpy/__init__.py | 59 +++++++++++++++ qiskit/qpy/binary_io/schedules.py | 74 ++++++++++++++++++- qiskit/qpy/common.py | 2 +- qiskit/qpy/type_keys.py | 14 ++++ ...rt-for-qpy-reference-70478baa529fff8c.yaml | 19 +++++ test/python/qpy/test_block_load_from_qpy.py | 44 +++++++++++ test/qpy_compat/test_qpy.py | 27 +++++++ 7 files changed, 235 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/add-support-for-qpy-reference-70478baa529fff8c.yaml diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 0b0ea88184d6..986e36a3a7fe 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -126,6 +126,46 @@ by ``num_circuits`` in the file header). There is no padding between the circuits in the data. +.. _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:: + + from qiskit import pulse + from qiskit import qpy + + with pulse.build() as schedule: + pulse.reference("cr45p", "q0", "q1") + pulse.reference("x", "q0") + pulse.reference("cr45p", "q0", "q1") + + with open('template_ecr.qpy', 'wb') as fd: + qpy.dump(schedule, fd) + +The conventional :ref:`qpy_schedule_block` data model is preserved, but in +version 7 it is immediately followed by an extra :ref:`qpy_mapping` utf8 bytes block +representing the data of the referenced subroutines. + +New type key character is added to the :ref:`qpy_schedule_instructions` group +for the :class:`.~Reference` instruction. + +- ``y``: :class:`~qiskit.pulse.instructions.Reference` instruction + +New type key character is added to the :ref:`qpy_schedule_operands` group +for the operands of :class:`.~Reference` instruction, +which is a tuple of strings, e.g. ("cr45p", "q0", "q1"). + +- ``o``: string (operand string) + +Note that this is the same encoding with the built-in Python string, however, +the standard value encoding in QPY uses ``s`` type character for string data, +which conflicts with the :class:`~qiskit.pulse.library.SymbolicPulse` in the scope of +pulse instruction operands. A special type character ``o`` is reserved for +the string data that appears in the pulse instruction operands. + .. _qpy_version_6: Version 6 @@ -213,6 +253,8 @@ the same QPY interface. Input data type is implicitly analyzed and no extra option is required to save the schedule block. +.. _qpy_schedule_block_header: + SCHEDULE_BLOCK_HEADER --------------------- @@ -230,6 +272,11 @@ ``metadata_size`` utf8 bytes of the JSON serialized metadata dictionary attached to the schedule. +.. _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 the parameters associated with the alignment context :attr:`AlignmentKind._context_params`. @@ -243,6 +290,11 @@ Note that :class:`~.AlignFunc` context is not supported becasue of the callback function stored in the context parameters. +.. _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. Each schedule instruction starts with ``char`` representing the instruction type @@ -261,6 +313,12 @@ - ``r``: :class:`~qiskit.pulse.instructions.ShiftPhase` instruction - ``b``: :class:`~qiskit.pulse.instructions.RelativeBarrier` instruction - ``t``: :class:`~qiskit.pulse.instructions.TimeBlockade` instruction +- ``y``: :class:`~qiskit.pulse.instructions.Reference` instruction (new in version 0.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. @@ -272,6 +330,7 @@ - ``c``: :class:`~qiskit.pulse.channels.Channel` - ``w``: :class:`~qiskit.pulse.library.Waveform` - ``s``: :class:`~qiskit.pulse.library.SymbolicPulse` +- ``o``: string (operand string, new in version 0.7) .. _qpy_schedule_channel: diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 41c045f0a431..3ca41722694d 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -19,10 +19,11 @@ import numpy as np from qiskit.exceptions import QiskitError -from qiskit.pulse import library, channels +from qiskit.pulse import library, channels, instructions from qiskit.pulse.schedule import ScheduleBlock from qiskit.qpy import formats, common, type_keys from qiskit.qpy.binary_io import value +from qiskit.qpy.exceptions import QpyError from qiskit.utils import optionals as _optional if _optional.HAS_SYMENGINE: @@ -238,6 +239,8 @@ def _loads_operand(type_key, data_bytes, version): return common.data_from_binary(data_bytes, _read_symbolic_pulse_v6, version=version) if type_key == type_keys.ScheduleOperand.CHANNEL: return common.data_from_binary(data_bytes, _read_channel, version=version) + if type_key == type_keys.ScheduleOperand.OPERAND_STR: + return data_bytes.decode(common.ENCODE) return value.loads_value(type_key, data_bytes, version, {}) @@ -259,6 +262,24 @@ def _read_element(file_obj, version, metadata_deserializer): return instance +def _loads_reference_item(type_key, data_bytes, version, metadata_deserializer): + if type_key == type_keys.Value.NULL: + return None + if type_key == type_keys.Program.SCHEDULE_BLOCK: + return common.data_from_binary( + data_bytes, + deserializer=read_schedule_block, + version=version, + metadata_deserializer=metadata_deserializer, + ) + + raise QpyError( + f"Loaded schedule reference item is neither None nor ScheduleBlock. " + f"Type key {type_key} is not valid data type for a reference items. " + "This data cannot be loaded. Please check QPY version." + ) + + def _write_channel(file_obj, data): type_key = type_keys.ScheduleChannel.assign(data) common.write_type_key(file_obj, type_key) @@ -340,6 +361,9 @@ def _dumps_operand(operand): elif isinstance(operand, channels.Channel): type_key = type_keys.ScheduleOperand.CHANNEL data_bytes = common.data_to_binary(operand, _write_channel) + elif isinstance(operand, str): + type_key = type_keys.ScheduleOperand.OPERAND_STR + data_bytes = operand.encode(common.ENCODE) else: type_key, data_bytes = value.dumps_value(operand) @@ -361,6 +385,20 @@ def _write_element(file_obj, element, metadata_serializer): value.write_value(file_obj, element.name) +def _dumps_reference_item(schedule, metadata_serializer): + if schedule is None: + type_key = type_keys.Value.NULL + data_bytes = b"" + else: + type_key = type_keys.Program.SCHEDULE_BLOCK + data_bytes = common.data_to_binary( + obj=schedule, + serializer=write_schedule_block, + metadata_serializer=metadata_serializer, + ) + return type_key, data_bytes + + def read_schedule_block(file_obj, version, metadata_deserializer=None): """Read a single ScheduleBlock from the file like object. @@ -382,7 +420,6 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None): TypeError: If any of the instructions is invalid data format. QiskitError: QPY version is earlier than block support. """ - if version < 5: QiskitError(f"QPY version {version} does not support ScheduleBlock.") @@ -406,6 +443,22 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None): block_elm = _read_element(file_obj, version, metadata_deserializer) block.append(block_elm, inplace=True) + # Load references + if version >= 7: + flat_key_refdict = common.read_mapping( + file_obj=file_obj, + deserializer=_loads_reference_item, + version=version, + metadata_deserializer=metadata_deserializer, + ) + ref_dict = {} + for key_str, schedule in flat_key_refdict.items(): + if schedule is not None: + composite_key = tuple(key_str.split(instructions.Reference.key_delimiter)) + ref_dict[composite_key] = schedule + if ref_dict: + block.assign_references(ref_dict, inplace=True) + return block @@ -440,5 +493,20 @@ def write_schedule_block(file_obj, block, metadata_serializer=None): file_obj.write(metadata) _write_alignment_context(file_obj, block.alignment_context) - for block_elm in block.blocks: + 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) + + # Write references + flat_key_refdict = {} + for ref_keys, schedule in block._reference_manager.items(): + # Do not call block.reference. This returns the reference of most outer program by design. + key_str = instructions.Reference.key_delimiter.join(ref_keys) + flat_key_refdict[key_str] = schedule + common.write_mapping( + file_obj=file_obj, + mapping=flat_key_refdict, + serializer=_dumps_reference_item, + metadata_serializer=metadata_serializer, + ) diff --git a/qiskit/qpy/common.py b/qiskit/qpy/common.py index 456738ba5ae5..174b299f7b8e 100644 --- a/qiskit/qpy/common.py +++ b/qiskit/qpy/common.py @@ -20,7 +20,7 @@ from qiskit.qpy import formats -QPY_VERSION = 6 +QPY_VERSION = 7 ENCODE = "utf8" diff --git a/qiskit/qpy/type_keys.py b/qiskit/qpy/type_keys.py index 53093b96dbb7..f20003a17533 100644 --- a/qiskit/qpy/type_keys.py +++ b/qiskit/qpy/type_keys.py @@ -45,6 +45,7 @@ ShiftPhase, RelativeBarrier, TimeBlockade, + Reference, ) from qiskit.pulse.library import Waveform, SymbolicPulse from qiskit.pulse.schedule import ScheduleBlock @@ -233,6 +234,7 @@ class ScheduleInstruction(TypeKeyBase): SHIFT_PHASE = b"r" BARRIER = b"b" TIME_BLOCKADE = b"t" + REFERENCE = b"y" # 's' is reserved by ScheduleBlock, i.e. block can be nested as an element. # Call instructon is not supported by QPY. @@ -261,6 +263,8 @@ def assign(cls, obj): return cls.BARRIER if isinstance(obj, TimeBlockade): return cls.TIME_BLOCKADE + if isinstance(obj, Reference): + return cls.REFERENCE raise exceptions.QpyError( f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." @@ -286,6 +290,8 @@ def retrieve(cls, type_key): return RelativeBarrier if type_key == cls.TIME_BLOCKADE: return TimeBlockade + if type_key == cls.REFERENCE: + return Reference raise exceptions.QpyError( f"A class corresponding to type key '{type_key}' is not found in {cls.__name__} namespace." @@ -303,6 +309,12 @@ class ScheduleOperand(TypeKeyBase): # Data format of these object is somewhat opaque and not defiend well. # It's rarely used in the Qiskit experiements. Of course these can be added later. + # We need to have own string type definition for operands of schedule instruction. + # Note that string type is already defined in the Value namespace, + # but its key "s" conflicts with the SYMBOLIC_PULSE in the ScheduleOperand namespace. + # New in QPY version 7. + OPERAND_STR = b"o" + @classmethod def assign(cls, obj): if isinstance(obj, Waveform): @@ -311,6 +323,8 @@ def assign(cls, obj): return cls.SYMBOLIC_PULSE if isinstance(obj, Channel): return cls.CHANNEL + if isinstance(obj, str): + return cls.OPERAND_STR raise exceptions.QpyError( f"Object type '{type(obj)}' is not supported in {cls.__name__} namespace." diff --git a/releasenotes/notes/add-support-for-qpy-reference-70478baa529fff8c.yaml b/releasenotes/notes/add-support-for-qpy-reference-70478baa529fff8c.yaml new file mode 100644 index 000000000000..8e54179772f2 --- /dev/null +++ b/releasenotes/notes/add-support-for-qpy-reference-70478baa529fff8c.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + QPY supports pulse :class:`~.ScheduleBlock` with unassigned reference, + and preserves the data structure for the reference to subroutines. + This feature allows to save a template pulse program for tasks such as pulse calibration. + + .. code-block:: python + + from qiskit import pulse + from qiskit import qpy + + with pulse.build() as schedule: + pulse.reference("cr45p", "q0", "q1") + pulse.reference("x", "q0") + pulse.reference("cr45p", "q0", "q1") + + with open('template_ecr.qpy', 'wb') as fd: + qpy.dump(schedule, fd) diff --git a/test/python/qpy/test_block_load_from_qpy.py b/test/python/qpy/test_block_load_from_qpy.py index 59a635eb127b..9831d6ba6951 100644 --- a/test/python/qpy/test_block_load_from_qpy.py +++ b/test/python/qpy/test_block_load_from_qpy.py @@ -207,6 +207,50 @@ def test_called_schedule(self): builder.call(refsched, name="test_ref") self.assert_roundtrip_equal(test_sched) + def test_unassigned_reference(self): + """Test schedule with unassigned reference.""" + with builder.build() as test_sched: + builder.reference("custom1", "q0") + builder.reference("custom1", "q1") + + self.assert_roundtrip_equal(test_sched) + + def test_partly_assigned_reference(self): + """Test schedule with partly assigned reference.""" + with builder.build() as test_sched: + builder.reference("custom1", "q0") + builder.reference("custom1", "q1") + + with builder.build() as sub_q0: + builder.delay(Parameter("duration"), DriveChannel(0)) + + test_sched.assign_references( + {("custom1", "q0"): sub_q0}, + inplace=True, + ) + + self.assert_roundtrip_equal(test_sched) + + def test_nested_assigned_reference(self): + """Test schedule with assigned reference for nested schedule.""" + with builder.build() as test_sched: + with builder.align_left(): + builder.reference("custom1", "q0") + builder.reference("custom1", "q1") + + with builder.build() as sub_q0: + builder.delay(Parameter("duration"), DriveChannel(0)) + + with builder.build() as sub_q1: + builder.delay(Parameter("duration"), DriveChannel(1)) + + test_sched.assign_references( + {("custom1", "q0"): sub_q0, ("custom1", "q1"): sub_q1}, + inplace=True, + ) + + self.assert_roundtrip_equal(test_sched) + def test_bell_schedule(self): """Test complex schedule to create a Bell state.""" with builder.build() as test_sched: diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index e932897b475c..09dddcd43e13 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -443,6 +443,31 @@ def generate_schedule_blocks(): return schedule_blocks +def generate_referenced_schedule(): + """Test for QPY serialization of unassigned reference schedules.""" + from qiskit.pulse import builder, channels, library + + schedule_blocks = [] + + # Completely unassigned schedule + with builder.build() as block: + builder.reference("cr45p", "q0", "q1") + builder.reference("x", "q0") + builder.reference("cr45m", "q0", "q1") + schedule_blocks.append(block) + + # Partly assigned schedule + with builder.build() as x_q0: + builder.play(library.Constant(100, 0.1), channels.DriveChannel(0)) + with builder.build() as block: + builder.reference("cr45p", "q0", "q1") + builder.call(x_q0) + builder.reference("cr45m", "q0", "q1") + schedule_blocks.append(block) + + return schedule_blocks + + def generate_calibrated_circuits(): """Test for QPY serialization with calibrations.""" from qiskit.pulse import builder, Constant, DriveChannel @@ -551,6 +576,8 @@ def generate_circuits(version_parts): output_circuits["pulse_gates.qpy"] = generate_calibrated_circuits() if version_parts >= (0, 21, 2): output_circuits["open_controlled_gates.qpy"] = generate_open_controlled_gates() + if version_parts >= (0, 24, 0): + output_circuits["referenced_schedule_blocks.qpy"] = generate_referenced_schedule() return output_circuits From e5985930204a6836d70f188ff81edec841927807 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 11 Apr 2023 22:26:24 +0100 Subject: [PATCH 006/172] Fix docs build with Pygments 2.15 (#9948) The problem was because the latest version of Pygments included a docstring in `Lexer.__init__` that includes a crossreference to an object that Qiskit naturally doesn't have. We don't need to inherit the `__init__` docstring for this object; we're only interested in documenting the existence of object itself. --- qiskit/qasm/__init__.py | 11 ++++++----- requirements-dev.txt | 2 +- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/qiskit/qasm/__init__.py b/qiskit/qasm/__init__.py index 9010df392331..d828581d5f72 100644 --- a/qiskit/qasm/__init__.py +++ b/qiskit/qasm/__init__.py @@ -30,13 +30,14 @@ Pygments ======== -.. autosummary:: - :toctree: ../stubs/ +.. autoclass:: OpenQASMLexer + :class-doc-from: class - OpenQASMLexer - QasmHTMLStyle - QasmTerminalStyle +.. autoclass:: QasmHTMLStyle + :class-doc-from: class +.. autoclass:: QasmTerminalStyle + :class-doc-from: class """ from numpy import pi diff --git a/requirements-dev.txt b/requirements-dev.txt index d7befa86a240..1403ea993ac3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -19,7 +19,7 @@ reno>=3.4.0 Sphinx>=5.0 qiskit-sphinx-theme~=1.10.3 sphinx-design>=0.2.0 -pygments>=2.4,<2.15 +pygments>=2.4 scikit-learn>=0.20.0 scikit-quant<=0.7;platform_system != 'Windows' jax;platform_system != 'Windows' From 3cf4d77636bc4629aa16aa61c395fdc05f7b3fdf Mon Sep 17 00:00:00 2001 From: Shivalee RK Shah <77015285+shivalee12@users.noreply.github.com> Date: Wed, 12 Apr 2023 04:07:04 +0530 Subject: [PATCH 007/172] fixed issue 9453 by removing "_v2" from name in BackendV2 class (#9465) * fixed issue 9453 by stripping _v2 from name in BackendV2 class * Add release note --------- Co-authored-by: Junye Huang Co-authored-by: Matthew Treinish --- .../fake_provider/backends/almaden/fake_almaden.py | 2 +- .../fake_provider/backends/armonk/fake_armonk.py | 2 +- .../fake_provider/backends/athens/fake_athens.py | 2 +- .../providers/fake_provider/backends/belem/fake_belem.py | 2 +- .../fake_provider/backends/boeblingen/fake_boeblingen.py | 2 +- .../fake_provider/backends/bogota/fake_bogota.py | 2 +- .../fake_provider/backends/brooklyn/fake_brooklyn.py | 2 +- .../fake_provider/backends/burlington/fake_burlington.py | 2 +- .../providers/fake_provider/backends/cairo/fake_cairo.py | 2 +- .../fake_provider/backends/cambridge/fake_cambridge.py | 2 +- .../fake_provider/backends/casablanca/fake_casablanca.py | 2 +- .../providers/fake_provider/backends/essex/fake_essex.py | 2 +- .../fake_provider/backends/guadalupe/fake_guadalupe.py | 2 +- .../providers/fake_provider/backends/hanoi/fake_hanoi.py | 2 +- .../fake_provider/backends/jakarta/fake_jakarta.py | 2 +- .../backends/johannesburg/fake_johannesburg.py | 2 +- .../fake_provider/backends/kolkata/fake_kolkata.py | 2 +- .../providers/fake_provider/backends/lagos/fake_lagos.py | 2 +- .../fake_provider/backends/london/fake_london.py | 2 +- .../fake_provider/backends/manhattan/fake_manhattan.py | 2 +- .../fake_provider/backends/manila/fake_manila.py | 2 +- .../fake_provider/backends/melbourne/fake_melbourne.py | 2 +- .../fake_provider/backends/montreal/fake_montreal.py | 2 +- .../fake_provider/backends/mumbai/fake_mumbai.py | 2 +- .../fake_provider/backends/nairobi/fake_nairobi.py | 2 +- .../fake_provider/backends/ourense/fake_ourense.py | 2 +- .../providers/fake_provider/backends/paris/fake_paris.py | 2 +- .../backends/poughkeepsie/fake_poughkeepsie.py | 2 +- .../providers/fake_provider/backends/quito/fake_quito.py | 2 +- .../fake_provider/backends/rochester/fake_rochester.py | 2 +- .../providers/fake_provider/backends/rome/fake_rome.py | 2 +- .../fake_provider/backends/santiago/fake_santiago.py | 2 +- .../fake_provider/backends/singapore/fake_singapore.py | 2 +- .../fake_provider/backends/sydney/fake_sydney.py | 2 +- .../fake_provider/backends/toronto/fake_toronto.py | 2 +- .../fake_provider/backends/valencia/fake_valencia.py | 2 +- .../providers/fake_provider/backends/vigo/fake_vigo.py | 2 +- .../fake_provider/backends/washington/fake_washington.py | 2 +- .../fake_provider/backends/yorktown/fake_yorktown.py | 2 +- .../notes/rename-fake-backends-b08f8a66bc19088b.yaml | 9 +++++++++ 40 files changed, 48 insertions(+), 39 deletions(-) create mode 100644 releasenotes/notes/rename-fake-backends-b08f8a66bc19088b.yaml diff --git a/qiskit/providers/fake_provider/backends/almaden/fake_almaden.py b/qiskit/providers/fake_provider/backends/almaden/fake_almaden.py index 5869914f8d0e..f1c344d68855 100644 --- a/qiskit/providers/fake_provider/backends/almaden/fake_almaden.py +++ b/qiskit/providers/fake_provider/backends/almaden/fake_almaden.py @@ -35,7 +35,7 @@ class FakeAlmadenV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_almaden.json" props_filename = "props_almaden.json" - backend_name = "fake_almaden_v2" + backend_name = "fake_almaden" class FakeAlmaden(fake_qasm_backend.FakeQasmBackend): diff --git a/qiskit/providers/fake_provider/backends/armonk/fake_armonk.py b/qiskit/providers/fake_provider/backends/armonk/fake_armonk.py index 68f9e9d445be..4c3b0e327c66 100644 --- a/qiskit/providers/fake_provider/backends/armonk/fake_armonk.py +++ b/qiskit/providers/fake_provider/backends/armonk/fake_armonk.py @@ -30,7 +30,7 @@ class FakeArmonkV2(fake_backend.FakeBackendV2): conf_filename = "conf_armonk.json" props_filename = "props_armonk.json" defs_filename = "defs_armonk.json" - backend_name = "fake_armonk_v2" + backend_name = "fake_armonk" class FakeArmonk(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/athens/fake_athens.py b/qiskit/providers/fake_provider/backends/athens/fake_athens.py index 09c7edb2d1e3..01e84b75c4ab 100644 --- a/qiskit/providers/fake_provider/backends/athens/fake_athens.py +++ b/qiskit/providers/fake_provider/backends/athens/fake_athens.py @@ -25,7 +25,7 @@ class FakeAthensV2(fake_backend.FakeBackendV2): conf_filename = "conf_athens.json" props_filename = "props_athens.json" defs_filename = "defs_athens.json" - backend_name = "fake_athens_v2" + backend_name = "fake_athens" class FakeAthens(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/belem/fake_belem.py b/qiskit/providers/fake_provider/backends/belem/fake_belem.py index 19ea06481d58..3e50a05576d9 100644 --- a/qiskit/providers/fake_provider/backends/belem/fake_belem.py +++ b/qiskit/providers/fake_provider/backends/belem/fake_belem.py @@ -25,7 +25,7 @@ class FakeBelemV2(fake_backend.FakeBackendV2): conf_filename = "conf_belem.json" props_filename = "props_belem.json" defs_filename = "defs_belem.json" - backend_name = "fake_belem_v2" + backend_name = "fake_belem" class FakeBelem(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/boeblingen/fake_boeblingen.py b/qiskit/providers/fake_provider/backends/boeblingen/fake_boeblingen.py index b01c690b3071..be7ee88b8aed 100644 --- a/qiskit/providers/fake_provider/backends/boeblingen/fake_boeblingen.py +++ b/qiskit/providers/fake_provider/backends/boeblingen/fake_boeblingen.py @@ -36,7 +36,7 @@ class FakeBoeblingenV2(fake_backend.FakeBackendV2): conf_filename = "conf_boeblingen.json" props_filename = "props_boeblingen.json" defs_filename = "defs_boeblingen.json" - backend_name = "fake_boeblingen_v2" + backend_name = "fake_boeblingen" class FakeBoeblingen(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/bogota/fake_bogota.py b/qiskit/providers/fake_provider/backends/bogota/fake_bogota.py index d178c2dcbe7c..02f4b3e5ff7a 100644 --- a/qiskit/providers/fake_provider/backends/bogota/fake_bogota.py +++ b/qiskit/providers/fake_provider/backends/bogota/fake_bogota.py @@ -25,7 +25,7 @@ class FakeBogotaV2(fake_backend.FakeBackendV2): conf_filename = "conf_bogota.json" props_filename = "props_bogota.json" defs_filename = "defs_bogota.json" - backend_name = "fake_bogota_v2" + backend_name = "fake_bogota" class FakeBogota(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/brooklyn/fake_brooklyn.py b/qiskit/providers/fake_provider/backends/brooklyn/fake_brooklyn.py index 446b9bb922d9..8692129ed799 100644 --- a/qiskit/providers/fake_provider/backends/brooklyn/fake_brooklyn.py +++ b/qiskit/providers/fake_provider/backends/brooklyn/fake_brooklyn.py @@ -25,7 +25,7 @@ class FakeBrooklynV2(fake_backend.FakeBackendV2): conf_filename = "conf_brooklyn.json" props_filename = "props_brooklyn.json" defs_filename = "defs_brooklyn.json" - backend_name = "fake_brooklyn_v2" + backend_name = "fake_brooklyn" class FakeBrooklyn(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/burlington/fake_burlington.py b/qiskit/providers/fake_provider/backends/burlington/fake_burlington.py index f2b091eec16c..fc3b02f68c55 100644 --- a/qiskit/providers/fake_provider/backends/burlington/fake_burlington.py +++ b/qiskit/providers/fake_provider/backends/burlington/fake_burlington.py @@ -31,7 +31,7 @@ class FakeBurlingtonV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_burlington.json" props_filename = "props_burlington.json" - backend_name = "fake_burlington_v2" + backend_name = "fake_burlington" class FakeBurlington(fake_qasm_backend.FakeQasmBackend): diff --git a/qiskit/providers/fake_provider/backends/cairo/fake_cairo.py b/qiskit/providers/fake_provider/backends/cairo/fake_cairo.py index 0511fa17ed4f..ed171cd682c8 100644 --- a/qiskit/providers/fake_provider/backends/cairo/fake_cairo.py +++ b/qiskit/providers/fake_provider/backends/cairo/fake_cairo.py @@ -25,7 +25,7 @@ class FakeCairoV2(fake_backend.FakeBackendV2): conf_filename = "conf_cairo.json" props_filename = "props_cairo.json" defs_filename = "defs_cairo.json" - backend_name = "fake_cairo_v2" + backend_name = "fake_cairo" class FakeCairo(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/cambridge/fake_cambridge.py b/qiskit/providers/fake_provider/backends/cambridge/fake_cambridge.py index 46758406b468..81b99bb071b4 100644 --- a/qiskit/providers/fake_provider/backends/cambridge/fake_cambridge.py +++ b/qiskit/providers/fake_provider/backends/cambridge/fake_cambridge.py @@ -37,7 +37,7 @@ class FakeCambridgeV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_cambridge.json" props_filename = "props_cambridge.json" - backend_name = "fake_cambridge_v2" + backend_name = "fake_cambridge" class FakeCambridge(fake_qasm_backend.FakeQasmBackend): diff --git a/qiskit/providers/fake_provider/backends/casablanca/fake_casablanca.py b/qiskit/providers/fake_provider/backends/casablanca/fake_casablanca.py index 08b606663a32..30bd220b7f99 100644 --- a/qiskit/providers/fake_provider/backends/casablanca/fake_casablanca.py +++ b/qiskit/providers/fake_provider/backends/casablanca/fake_casablanca.py @@ -25,7 +25,7 @@ class FakeCasablancaV2(fake_backend.FakeBackendV2): conf_filename = "conf_casablanca.json" props_filename = "props_casablanca.json" defs_filename = "defs_casablanca.json" - backend_name = "fake_casablanca_v2" + backend_name = "fake_casablanca" class FakeCasablanca(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/essex/fake_essex.py b/qiskit/providers/fake_provider/backends/essex/fake_essex.py index 6029a63393c2..6ad1d4957764 100644 --- a/qiskit/providers/fake_provider/backends/essex/fake_essex.py +++ b/qiskit/providers/fake_provider/backends/essex/fake_essex.py @@ -33,7 +33,7 @@ class FakeEssexV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_essex.json" props_filename = "props_essex.json" - backend_name = "fake_essex_v2" + backend_name = "fake_essex" class FakeEssex(fake_qasm_backend.FakeQasmBackend): diff --git a/qiskit/providers/fake_provider/backends/guadalupe/fake_guadalupe.py b/qiskit/providers/fake_provider/backends/guadalupe/fake_guadalupe.py index 80750f55c9f6..9f37b84c349e 100644 --- a/qiskit/providers/fake_provider/backends/guadalupe/fake_guadalupe.py +++ b/qiskit/providers/fake_provider/backends/guadalupe/fake_guadalupe.py @@ -26,7 +26,7 @@ class FakeGuadalupeV2(fake_backend.FakeBackendV2): conf_filename = "conf_guadalupe.json" props_filename = "props_guadalupe.json" defs_filename = "defs_guadalupe.json" - backend_name = "fake_guadalupe_v2" + backend_name = "fake_guadalupe" class FakeGuadalupe(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/hanoi/fake_hanoi.py b/qiskit/providers/fake_provider/backends/hanoi/fake_hanoi.py index 9e75b6f4c4b3..dba2d4476fee 100644 --- a/qiskit/providers/fake_provider/backends/hanoi/fake_hanoi.py +++ b/qiskit/providers/fake_provider/backends/hanoi/fake_hanoi.py @@ -25,7 +25,7 @@ class FakeHanoiV2(fake_backend.FakeBackendV2): conf_filename = "conf_hanoi.json" props_filename = "props_hanoi.json" defs_filename = "defs_hanoi.json" - backend_name = "fake_hanoi_v2" + backend_name = "fake_hanoi" class FakeHanoi(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/jakarta/fake_jakarta.py b/qiskit/providers/fake_provider/backends/jakarta/fake_jakarta.py index 92a4335a20e2..c7f138511159 100644 --- a/qiskit/providers/fake_provider/backends/jakarta/fake_jakarta.py +++ b/qiskit/providers/fake_provider/backends/jakarta/fake_jakarta.py @@ -25,7 +25,7 @@ class FakeJakartaV2(fake_backend.FakeBackendV2): conf_filename = "conf_jakarta.json" props_filename = "props_jakarta.json" defs_filename = "defs_jakarta.json" - backend_name = "fake_jakarta_v2" + backend_name = "fake_jakarta" class FakeJakarta(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/johannesburg/fake_johannesburg.py b/qiskit/providers/fake_provider/backends/johannesburg/fake_johannesburg.py index bcb9bde5c9e7..1b62b507d7be 100644 --- a/qiskit/providers/fake_provider/backends/johannesburg/fake_johannesburg.py +++ b/qiskit/providers/fake_provider/backends/johannesburg/fake_johannesburg.py @@ -35,7 +35,7 @@ class FakeJohannesburgV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_johannesburg.json" props_filename = "props_johannesburg.json" - backend_name = "fake_johannesburg_v2" + backend_name = "fake_johannesburg" class FakeJohannesburg(fake_qasm_backend.FakeQasmBackend): diff --git a/qiskit/providers/fake_provider/backends/kolkata/fake_kolkata.py b/qiskit/providers/fake_provider/backends/kolkata/fake_kolkata.py index 95c9d04d4503..601d05d6130c 100644 --- a/qiskit/providers/fake_provider/backends/kolkata/fake_kolkata.py +++ b/qiskit/providers/fake_provider/backends/kolkata/fake_kolkata.py @@ -25,7 +25,7 @@ class FakeKolkataV2(fake_backend.FakeBackendV2): conf_filename = "conf_kolkata.json" props_filename = "props_kolkata.json" defs_filename = "defs_kolkata.json" - backend_name = "fake_kolkata_v2" + backend_name = "fake_kolkata" class FakeKolkata(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/lagos/fake_lagos.py b/qiskit/providers/fake_provider/backends/lagos/fake_lagos.py index fd72b6a22168..12ff3c54d2c1 100644 --- a/qiskit/providers/fake_provider/backends/lagos/fake_lagos.py +++ b/qiskit/providers/fake_provider/backends/lagos/fake_lagos.py @@ -25,7 +25,7 @@ class FakeLagosV2(fake_backend.FakeBackendV2): conf_filename = "conf_lagos.json" props_filename = "props_lagos.json" defs_filename = "defs_lagos.json" - backend_name = "fake_lagos_v2" + backend_name = "fake_lagos" class FakeLagos(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/london/fake_london.py b/qiskit/providers/fake_provider/backends/london/fake_london.py index ad07196ff56f..ea6693cf4a90 100644 --- a/qiskit/providers/fake_provider/backends/london/fake_london.py +++ b/qiskit/providers/fake_provider/backends/london/fake_london.py @@ -33,7 +33,7 @@ class FakeLondonV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_london.json" props_filename = "props_london.json" - backend_name = "fake_london_v2" + backend_name = "fake_london" class FakeLondon(fake_qasm_backend.FakeQasmBackend): diff --git a/qiskit/providers/fake_provider/backends/manhattan/fake_manhattan.py b/qiskit/providers/fake_provider/backends/manhattan/fake_manhattan.py index 13e39360ad17..1d114c43bfa6 100644 --- a/qiskit/providers/fake_provider/backends/manhattan/fake_manhattan.py +++ b/qiskit/providers/fake_provider/backends/manhattan/fake_manhattan.py @@ -25,7 +25,7 @@ class FakeManhattanV2(fake_backend.FakeBackendV2): conf_filename = "conf_manhattan.json" props_filename = "props_manhattan.json" defs_filename = "defs_manhattan.json" - backend_name = "fake_manhattan_v2" + backend_name = "fake_manhattan" class FakeManhattan(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/manila/fake_manila.py b/qiskit/providers/fake_provider/backends/manila/fake_manila.py index 6bcadc827ad9..941fc068d010 100644 --- a/qiskit/providers/fake_provider/backends/manila/fake_manila.py +++ b/qiskit/providers/fake_provider/backends/manila/fake_manila.py @@ -25,7 +25,7 @@ class FakeManilaV2(fake_backend.FakeBackendV2): conf_filename = "conf_manila.json" props_filename = "props_manila.json" defs_filename = "defs_manila.json" - backend_name = "fake_manila_v2" + backend_name = "fake_manila" class FakeManila(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/melbourne/fake_melbourne.py b/qiskit/providers/fake_provider/backends/melbourne/fake_melbourne.py index a72a5ca66b17..b300cbbbad84 100644 --- a/qiskit/providers/fake_provider/backends/melbourne/fake_melbourne.py +++ b/qiskit/providers/fake_provider/backends/melbourne/fake_melbourne.py @@ -28,7 +28,7 @@ class FakeMelbourneV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_melbourne.json" props_filename = "props_melbourne.json" - backend_name = "fake_melbourne_v2" + backend_name = "fake_melbourne" class FakeMelbourne(FakeBackend): diff --git a/qiskit/providers/fake_provider/backends/montreal/fake_montreal.py b/qiskit/providers/fake_provider/backends/montreal/fake_montreal.py index 3a86ac6f1ee4..f6fffa879954 100644 --- a/qiskit/providers/fake_provider/backends/montreal/fake_montreal.py +++ b/qiskit/providers/fake_provider/backends/montreal/fake_montreal.py @@ -25,7 +25,7 @@ class FakeMontrealV2(fake_backend.FakeBackendV2): conf_filename = "conf_montreal.json" props_filename = "props_montreal.json" defs_filename = "defs_montreal.json" - backend_name = "fake_montreal_v2" + backend_name = "fake_montreal" class FakeMontreal(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/mumbai/fake_mumbai.py b/qiskit/providers/fake_provider/backends/mumbai/fake_mumbai.py index 74cf90ba97b3..e39108fa852e 100644 --- a/qiskit/providers/fake_provider/backends/mumbai/fake_mumbai.py +++ b/qiskit/providers/fake_provider/backends/mumbai/fake_mumbai.py @@ -25,7 +25,7 @@ class FakeMumbaiV2(fake_backend.FakeBackendV2): conf_filename = "conf_mumbai.json" props_filename = "props_mumbai.json" defs_filename = "defs_mumbai.json" - backend_name = "fake_mumbai_v2" + backend_name = "fake_mumbai" class FakeMumbai(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/nairobi/fake_nairobi.py b/qiskit/providers/fake_provider/backends/nairobi/fake_nairobi.py index c4dcd0954659..7c114926ee95 100644 --- a/qiskit/providers/fake_provider/backends/nairobi/fake_nairobi.py +++ b/qiskit/providers/fake_provider/backends/nairobi/fake_nairobi.py @@ -25,7 +25,7 @@ class FakeNairobiV2(fake_backend.FakeBackendV2): conf_filename = "conf_nairobi.json" props_filename = "props_nairobi.json" defs_filename = "defs_nairobi.json" - backend_name = "fake_nairobi_v2" + backend_name = "fake_nairobi" class FakeNairobi(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/ourense/fake_ourense.py b/qiskit/providers/fake_provider/backends/ourense/fake_ourense.py index 6e4fa15ba165..6c2b59a1dec5 100644 --- a/qiskit/providers/fake_provider/backends/ourense/fake_ourense.py +++ b/qiskit/providers/fake_provider/backends/ourense/fake_ourense.py @@ -31,7 +31,7 @@ class FakeOurenseV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_ourense.json" props_filename = "props_ourense.json" - backend_name = "fake_ourense_v2" + backend_name = "fake_ourense" class FakeOurense(fake_qasm_backend.FakeQasmBackend): diff --git a/qiskit/providers/fake_provider/backends/paris/fake_paris.py b/qiskit/providers/fake_provider/backends/paris/fake_paris.py index 3df1a4efbdce..3307d0713dcc 100644 --- a/qiskit/providers/fake_provider/backends/paris/fake_paris.py +++ b/qiskit/providers/fake_provider/backends/paris/fake_paris.py @@ -38,7 +38,7 @@ class FakeParisV2(fake_backend.FakeBackendV2): conf_filename = "conf_paris.json" props_filename = "props_paris.json" defs_filename = "defs_paris.json" - backend_name = "fake_paris_v2" + backend_name = "fake_paris" class FakeParis(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/poughkeepsie/fake_poughkeepsie.py b/qiskit/providers/fake_provider/backends/poughkeepsie/fake_poughkeepsie.py index 528042c75b29..91e2b3a50e65 100644 --- a/qiskit/providers/fake_provider/backends/poughkeepsie/fake_poughkeepsie.py +++ b/qiskit/providers/fake_provider/backends/poughkeepsie/fake_poughkeepsie.py @@ -29,7 +29,7 @@ class FakePoughkeepsieV2(fake_backend.FakeBackendV2): conf_filename = "conf_poughkeepsie.json" props_filename = "props_poughkeepsie.json" defs_filename = "defs_poughkeepsie.json" - backend_name = "fake_poughkeepsie_v2" + backend_name = "fake_poughkeepsie" class FakePoughkeepsie(FakeBackend): diff --git a/qiskit/providers/fake_provider/backends/quito/fake_quito.py b/qiskit/providers/fake_provider/backends/quito/fake_quito.py index 3eb73ea10396..5086a33ca7a2 100644 --- a/qiskit/providers/fake_provider/backends/quito/fake_quito.py +++ b/qiskit/providers/fake_provider/backends/quito/fake_quito.py @@ -25,7 +25,7 @@ class FakeQuitoV2(fake_backend.FakeBackendV2): conf_filename = "conf_quito.json" props_filename = "props_quito.json" defs_filename = "defs_quito.json" - backend_name = "fake_quito_v2" + backend_name = "fake_quito" class FakeQuito(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/rochester/fake_rochester.py b/qiskit/providers/fake_provider/backends/rochester/fake_rochester.py index 4a9b31901166..9fbfd76d6944 100644 --- a/qiskit/providers/fake_provider/backends/rochester/fake_rochester.py +++ b/qiskit/providers/fake_provider/backends/rochester/fake_rochester.py @@ -24,7 +24,7 @@ class FakeRochesterV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_rochester.json" props_filename = "props_rochester.json" - backend_name = "fake_rochester_v2" + backend_name = "fake_rochester" class FakeRochester(fake_qasm_backend.FakeQasmBackend): diff --git a/qiskit/providers/fake_provider/backends/rome/fake_rome.py b/qiskit/providers/fake_provider/backends/rome/fake_rome.py index 51b180422eb5..6cf9a50fe4ee 100644 --- a/qiskit/providers/fake_provider/backends/rome/fake_rome.py +++ b/qiskit/providers/fake_provider/backends/rome/fake_rome.py @@ -25,7 +25,7 @@ class FakeRomeV2(fake_backend.FakeBackendV2): conf_filename = "conf_rome.json" props_filename = "props_rome.json" defs_filename = "defs_rome.json" - backend_name = "fake_rome_v2" + backend_name = "fake_rome" class FakeRome(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/santiago/fake_santiago.py b/qiskit/providers/fake_provider/backends/santiago/fake_santiago.py index 3e9be068c298..5743e2353770 100644 --- a/qiskit/providers/fake_provider/backends/santiago/fake_santiago.py +++ b/qiskit/providers/fake_provider/backends/santiago/fake_santiago.py @@ -25,7 +25,7 @@ class FakeSantiagoV2(fake_backend.FakeBackendV2): conf_filename = "conf_santiago.json" props_filename = "props_santiago.json" defs_filename = "defs_santiago.json" - backend_name = "fake_santiago_v2" + backend_name = "fake_santiago" class FakeSantiago(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/singapore/fake_singapore.py b/qiskit/providers/fake_provider/backends/singapore/fake_singapore.py index c28664f96da8..3e4185194f20 100644 --- a/qiskit/providers/fake_provider/backends/singapore/fake_singapore.py +++ b/qiskit/providers/fake_provider/backends/singapore/fake_singapore.py @@ -35,7 +35,7 @@ class FakeSingaporeV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_singapore.json" props_filename = "props_singapore.json" - backend_name = "fake_singapore_v2" + backend_name = "fake_singapore" class FakeSingapore(fake_qasm_backend.FakeQasmBackend): diff --git a/qiskit/providers/fake_provider/backends/sydney/fake_sydney.py b/qiskit/providers/fake_provider/backends/sydney/fake_sydney.py index 7a54f657d5c8..ed4fd2a062aa 100644 --- a/qiskit/providers/fake_provider/backends/sydney/fake_sydney.py +++ b/qiskit/providers/fake_provider/backends/sydney/fake_sydney.py @@ -25,7 +25,7 @@ class FakeSydneyV2(fake_backend.FakeBackendV2): conf_filename = "conf_sydney.json" props_filename = "props_sydney.json" defs_filename = "defs_sydney.json" - backend_name = "fake_sydney_v2" + backend_name = "fake_sydney" class FakeSydney(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/toronto/fake_toronto.py b/qiskit/providers/fake_provider/backends/toronto/fake_toronto.py index 95c25afa7225..07ee51c18368 100644 --- a/qiskit/providers/fake_provider/backends/toronto/fake_toronto.py +++ b/qiskit/providers/fake_provider/backends/toronto/fake_toronto.py @@ -25,7 +25,7 @@ class FakeTorontoV2(fake_backend.FakeBackendV2): conf_filename = "conf_toronto.json" props_filename = "props_toronto.json" defs_filename = "defs_toronto.json" - backend_name = "fake_toronto_v2" + backend_name = "fake_toronto" class FakeToronto(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/valencia/fake_valencia.py b/qiskit/providers/fake_provider/backends/valencia/fake_valencia.py index b8e9fe8e22b5..cb0fd6e92285 100644 --- a/qiskit/providers/fake_provider/backends/valencia/fake_valencia.py +++ b/qiskit/providers/fake_provider/backends/valencia/fake_valencia.py @@ -25,7 +25,7 @@ class FakeValenciaV2(fake_backend.FakeBackendV2): conf_filename = "conf_valencia.json" props_filename = "props_valencia.json" defs_filename = "defs_valencia.json" - backend_name = "fake_valencia_v2" + backend_name = "fake_valencia" class FakeValencia(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/vigo/fake_vigo.py b/qiskit/providers/fake_provider/backends/vigo/fake_vigo.py index 6ae3aeeaaf94..95ea5df421ff 100644 --- a/qiskit/providers/fake_provider/backends/vigo/fake_vigo.py +++ b/qiskit/providers/fake_provider/backends/vigo/fake_vigo.py @@ -31,7 +31,7 @@ class FakeVigoV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_vigo.json" props_filename = "props_vigo.json" - backend_name = "fake_vigo_v2" + backend_name = "fake_vigo" class FakeVigo(fake_qasm_backend.FakeQasmBackend): diff --git a/qiskit/providers/fake_provider/backends/washington/fake_washington.py b/qiskit/providers/fake_provider/backends/washington/fake_washington.py index b09e739e60ca..7985baa17fd2 100644 --- a/qiskit/providers/fake_provider/backends/washington/fake_washington.py +++ b/qiskit/providers/fake_provider/backends/washington/fake_washington.py @@ -25,7 +25,7 @@ class FakeWashingtonV2(fake_backend.FakeBackendV2): conf_filename = "conf_washington.json" props_filename = "props_washington.json" defs_filename = "defs_washington.json" - backend_name = "fake_washington_v2" + backend_name = "fake_washington" class FakeWashington(fake_pulse_backend.FakePulseBackend): diff --git a/qiskit/providers/fake_provider/backends/yorktown/fake_yorktown.py b/qiskit/providers/fake_provider/backends/yorktown/fake_yorktown.py index 4d6f529c5308..277b1d78a55b 100644 --- a/qiskit/providers/fake_provider/backends/yorktown/fake_yorktown.py +++ b/qiskit/providers/fake_provider/backends/yorktown/fake_yorktown.py @@ -33,7 +33,7 @@ class FakeYorktownV2(fake_backend.FakeBackendV2): dirname = os.path.dirname(__file__) conf_filename = "conf_yorktown.json" props_filename = "props_yorktown.json" - backend_name = "fake_yorktown_v2" + backend_name = "fake_yorktown" class FakeYorktown(fake_qasm_backend.FakeQasmBackend): diff --git a/releasenotes/notes/rename-fake-backends-b08f8a66bc19088b.yaml b/releasenotes/notes/rename-fake-backends-b08f8a66bc19088b.yaml new file mode 100644 index 000000000000..337bf6392840 --- /dev/null +++ b/releasenotes/notes/rename-fake-backends-b08f8a66bc19088b.yaml @@ -0,0 +1,9 @@ +--- +upgrade: + - | + The :attr:`~.BackendV2.name` attribute on the :class:`~.BackendV2` based + fake backend classes in :mod:`qiskit.providers.fake_provider` have changed + from earlier releases. Previously, the names had a suffix ``"_v2"`` to + differentiate the class from the :class:`~.BackendV1` version. This suffix + has been removed as having the suffix could lead to inconsistencies with + other snapshotted data used to construct the backend object. From 6392665026d03fc6efffcc5a1601bf40e83eb379 Mon Sep 17 00:00:00 2001 From: Toshinari Itoko <15028342+itoko@users.noreply.github.com> Date: Wed, 12 Apr 2023 19:06:16 +0900 Subject: [PATCH 008/172] Add Clifford.from_matrix (#9475) * Add Clifford.from_matrix * Add tests * Add reno * Faster Clifford.from_matrix O(16^n)->O(4^n) * Add infinite recursion test case * Change to try append with definition first * Add u gate handling for speed * Improve implematation following review comments * Add Clifford.from_operator * Update reno * Lint * more accurate reno --------- Co-authored-by: Ikko Hamamura --- .../operators/symplectic/clifford.py | 136 +++++++++++++++++- .../operators/symplectic/clifford_circuits.py | 116 ++++++++++++--- ...clifford-from-matrix-3184822cc559e0b7.yaml | 9 ++ .../operators/symplectic/test_clifford.py | 105 +++++++++++++- 4 files changed, 339 insertions(+), 27 deletions(-) create mode 100644 releasenotes/notes/add-clifford-from-matrix-3184822cc559e0b7.yaml diff --git a/qiskit/quantum_info/operators/symplectic/clifford.py b/qiskit/quantum_info/operators/symplectic/clifford.py index 131115c83e30..d0268989328d 100644 --- a/qiskit/quantum_info/operators/symplectic/clifford.py +++ b/qiskit/quantum_info/operators/symplectic/clifford.py @@ -12,6 +12,7 @@ """ Clifford operator class. """ +import functools import itertools import re @@ -552,10 +553,50 @@ def to_matrix(self): """Convert operator to Numpy matrix.""" return self.to_operator().data + @classmethod + def from_matrix(cls, matrix): + """Create a Clifford from a unitary matrix. + + Note that this function takes exponentially long time w.r.t. the number of qubits. + + Args: + matrix (np.array): A unitary matrix representing a Clifford to be converted. + + Returns: + Clifford: the Clifford object for the unitary matrix. + + Raises: + QiskitError: if the input is not a Clifford matrix. + """ + tableau = cls._unitary_matrix_to_tableau(matrix) + if tableau is None: + raise QiskitError("Non-Clifford matrix is not convertible") + return cls(tableau) + def to_operator(self): """Convert to an Operator object.""" return Operator(self.to_instruction()) + @classmethod + def from_operator(cls, operator): + """Create a Clifford from a operator. + + Note that this function takes exponentially long time w.r.t. the number of qubits. + + Args: + operator (Operator): An operator representing a Clifford to be converted. + + Returns: + Clifford: the Clifford object for the operator. + + Raises: + QiskitError: if the input is not a Clifford operator. + """ + tableau = cls._unitary_matrix_to_tableau(operator.to_matrix()) + if tableau is None: + raise QiskitError("Non-Clifford operator is not convertible") + return cls(tableau) + def to_circuit(self): """Return a QuantumCircuit implementing the Clifford. @@ -604,9 +645,9 @@ def from_circuit(circuit): # Initialize an identity Clifford clifford = Clifford(np.eye(2 * circuit.num_qubits), validate=False) if isinstance(circuit, QuantumCircuit): - _append_circuit(clifford, circuit) + clifford = _append_circuit(clifford, circuit) else: - _append_operation(clifford, circuit) + clifford = _append_operation(clifford, circuit) return clifford @staticmethod @@ -662,7 +703,7 @@ def from_label(label): num_qubits = len(label) op = Clifford(np.eye(2 * num_qubits, dtype=bool)) for qubit, char in enumerate(reversed(label)): - _append_operation(op, label_gates[char], qargs=[qubit]) + op = _append_operation(op, label_gates[char], qargs=[qubit]) return op def to_labels(self, array=False, mode="B"): @@ -849,6 +890,95 @@ def _from_label(label): symp[-1] = phase return symp + @staticmethod + def _pauli_matrix_to_row(mat, num_qubits): + """Generate a binary vector (a row of tableau representation) from a Pauli matrix. + Return None if the non-Pauli matrix is supplied.""" + # pylint: disable=too-many-return-statements + + def find_one_index(x, decimals=6): + indices = np.where(np.round(np.abs(x), decimals) == 1) + return indices[0][0] if len(indices[0]) == 1 else None + + def bitvector(n, num_bits): + return np.array([int(digit) for digit in format(n, f"0{num_bits}b")], dtype=bool)[::-1] + + # compute x-bits + xint = find_one_index(mat[0, :]) + if xint is None: + return None + xbits = bitvector(xint, num_qubits) + + # extract non-zero elements from matrix (rounded to 1, -1, 1j or -1j) + entries = np.empty(len(mat), dtype=complex) + for i, row in enumerate(mat): + index = find_one_index(row) + if index is None: + return None + expected = xint ^ i + if index != expected: + return None + entries[i] = np.round(mat[i, index]) + + # compute z-bits + zbits = np.empty(num_qubits, dtype=bool) + for k in range(num_qubits): + sign = np.round(entries[2**k] / entries[0]) + if sign == 1: + zbits[k] = False + elif sign == -1: + zbits[k] = True + else: + return None + + # compute phase + phase = None + num_y = sum(xbits & zbits) + positive_phase = (-1j) ** num_y + if entries[0] == positive_phase: + phase = False + elif entries[0] == -1 * positive_phase: + phase = True + if phase is None: + return None + + # validate all non-zero elements + coef = ((-1) ** phase) * positive_phase + ivec, zvec = np.ones(2), np.array([1, -1]) + expected = coef * functools.reduce(np.kron, [zvec if z else ivec for z in zbits[::-1]]) + if not np.allclose(entries, expected): + return None + + return np.hstack([xbits, zbits, phase]) + + @staticmethod + def _unitary_matrix_to_tableau(matrix): + # pylint: disable=invalid-name + num_qubits = int(np.log2(len(matrix))) + + stab = np.empty((num_qubits, 2 * num_qubits + 1), dtype=bool) + for i in range(num_qubits): + label = "I" * (num_qubits - i - 1) + "X" + "I" * i + Xi = Operator.from_label(label).to_matrix() + target = matrix @ Xi @ np.conj(matrix).T + row = Clifford._pauli_matrix_to_row(target, num_qubits) + if row is None: + return None + stab[i] = row + + destab = np.empty((num_qubits, 2 * num_qubits + 1), dtype=bool) + for i in range(num_qubits): + label = "I" * (num_qubits - i - 1) + "Z" + "I" * i + Zi = Operator.from_label(label).to_matrix() + target = matrix @ Zi @ np.conj(matrix).T + row = Clifford._pauli_matrix_to_row(target, num_qubits) + if row is None: + return None + destab[i] = row + + tableau = np.vstack([stab, destab]) + return tableau + # Update docstrings for API docs generate_apidocs(Clifford) diff --git a/qiskit/quantum_info/operators/symplectic/clifford_circuits.py b/qiskit/quantum_info/operators/symplectic/clifford_circuits.py index facab603dd34..a75b599cc076 100644 --- a/qiskit/quantum_info/operators/symplectic/clifford_circuits.py +++ b/qiskit/quantum_info/operators/symplectic/clifford_circuits.py @@ -12,19 +12,22 @@ """ Circuit simulation for the Clifford class. """ +import copy +import numpy as np -from qiskit.circuit.barrier import Barrier -from qiskit.circuit.delay import Delay +from qiskit.circuit import Barrier, Delay, Gate +from qiskit.circuit.exceptions import CircuitError from qiskit.exceptions import QiskitError -def _append_circuit(clifford, circuit, qargs=None): +def _append_circuit(clifford, circuit, qargs=None, recursion_depth=0): """Update Clifford inplace by applying a Clifford circuit. Args: - clifford (Clifford): the Clifford to update. - circuit (QuantumCircuit): the circuit to apply. + clifford (Clifford): The Clifford to update. + circuit (QuantumCircuit): The circuit to apply. qargs (list or None): The qubits to apply circuit to. + recursion_depth (int): The depth of mutual recursion with _append_operation Returns: Clifford: the updated Clifford. @@ -42,24 +45,26 @@ def _append_circuit(clifford, circuit, qargs=None): ) # Get the integer position of the flat register new_qubits = [qargs[circuit.find_bit(bit).index] for bit in instruction.qubits] - _append_operation(clifford, instruction.operation, new_qubits) + clifford = _append_operation(clifford, instruction.operation, new_qubits, recursion_depth) return clifford -def _append_operation(clifford, operation, qargs=None): +def _append_operation(clifford, operation, qargs=None, recursion_depth=0): """Update Clifford inplace by applying a Clifford operation. Args: - clifford (Clifford): the Clifford to update. - operation (Instruction or str): the operation or composite operation to apply. + clifford (Clifford): The Clifford to update. + operation (Instruction or Clifford or str): The operation or composite operation to apply. qargs (list or None): The qubits to apply operation to. + recursion_depth (int): The depth of mutual recursion with _append_circuit Returns: Clifford: the updated Clifford. Raises: - QiskitError: if input operation cannot be decomposed into Clifford operations. + QiskitError: if input operation cannot be converted into Clifford operations. """ + # pylint: disable=too-many-return-statements if isinstance(operation, (Barrier, Delay)): return clifford @@ -91,6 +96,28 @@ def _append_operation(clifford, operation, qargs=None): raise QiskitError("Invalid qubits for 2-qubit gate.") return _BASIS_2Q[name](clifford, qargs[0], qargs[1]) + # If u gate, check if it is a Clifford, and if so, apply it + if isinstance(gate, Gate) and name == "u" and len(qargs) == 1: + try: + theta, phi, lambd = tuple(_n_half_pis(par) for par in gate.params) + except ValueError as err: + raise QiskitError("U gate angles must be multiples of pi/2 to be a Clifford") from err + if theta == 0: + clifford = _append_rz(clifford, qargs[0], lambd + phi) + elif theta == 1: + clifford = _append_rz(clifford, qargs[0], lambd - 2) + clifford = _append_h(clifford, qargs[0]) + clifford = _append_rz(clifford, qargs[0], phi) + elif theta == 2: + clifford = _append_rz(clifford, qargs[0], lambd - 1) + clifford = _append_x(clifford, qargs[0]) + clifford = _append_rz(clifford, qargs[0], phi + 1) + elif theta == 3: + clifford = _append_rz(clifford, qargs[0], lambd) + clifford = _append_h(clifford, qargs[0]) + clifford = _append_rz(clifford, qargs[0], phi + 2) + return clifford + # If gate is a Clifford, we can either unroll the gate using the "to_circuit" # method, or we can compose the Cliffords directly. Experimentally, for large # cliffords the second method is considerably faster. @@ -103,19 +130,72 @@ def _append_operation(clifford, operation, qargs=None): clifford.tableau = composed_clifford.tableau return clifford - # If not a Clifford basis gate we try to unroll the gate and - # raise an exception if unrolling reaches a non-Clifford gate. - # TODO: We could also check u3 params to see if they - # are a single qubit Clifford gate rather than raise an exception. - if gate.definition is None: - raise QiskitError(f"Cannot apply Instruction: {gate.name}") - - return _append_circuit(clifford, gate.definition, qargs) + # If the gate is not directly appendable, we try to unroll the gate with its definition. + # This succeeds only if the gate has all-Clifford definition (decomposition). + # If fails, we need to restore the clifford that was before attempting to unroll and append. + if gate.definition is not None: + if recursion_depth > 0: + return _append_circuit(clifford, gate.definition, qargs, recursion_depth + 1) + else: # recursion_depth == 0 + # clifford may be updated in _append_circuit + org_clifford = copy.deepcopy(clifford) + try: + return _append_circuit(clifford, gate.definition, qargs, 1) + except (QiskitError, RecursionError): + # discard incompletely updated clifford and continue + clifford = org_clifford + + # As a final attempt, if the gate is up to 3 qubits, + # we try to construct a Clifford to be appended from its matrix representation. + if isinstance(gate, Gate) and len(qargs) <= 3: + try: + matrix = gate.to_matrix() + gate_cliff = Clifford.from_matrix(matrix) + return _append_operation(clifford, gate_cliff, qargs=qargs) + except TypeError as err: + raise QiskitError(f"Cannot apply {gate.name} gate with unbounded parameters") from err + except CircuitError as err: + raise QiskitError(f"Cannot apply {gate.name} gate without to_matrix defined") from err + except QiskitError as err: + raise QiskitError(f"Cannot apply non-Clifford gate: {gate.name}") from err + + raise QiskitError(f"Cannot apply {gate}") + + +def _n_half_pis(param) -> int: + try: + param = float(param) + epsilon = (abs(param) + 0.5 * 1e-10) % (np.pi / 2) + if epsilon > 1e-10: + raise ValueError(f"{param} is not to a multiple of pi/2") + multiple = int(np.round(param / (np.pi / 2))) + return multiple % 4 + except TypeError as err: + raise ValueError(f"{param} is not bounded") from err # --------------------------------------------------------------------- # Helper functions for applying basis gates # --------------------------------------------------------------------- +def _append_rz(clifford, qubit, multiple): + """Apply an Rz gate to a Clifford. + + Args: + clifford (Clifford): a Clifford. + qubit (int): gate qubit index. + multiple (int): z-rotation angle in a multiple of pi/2 + + Returns: + Clifford: the updated Clifford. + """ + if multiple % 4 == 1: + return _append_s(clifford, qubit) + if multiple % 4 == 2: + return _append_z(clifford, qubit) + if multiple % 4 == 3: + return _append_sdg(clifford, qubit) + + return clifford def _append_i(clifford, qubit): diff --git a/releasenotes/notes/add-clifford-from-matrix-3184822cc559e0b7.yaml b/releasenotes/notes/add-clifford-from-matrix-3184822cc559e0b7.yaml new file mode 100644 index 000000000000..fd4322d215d6 --- /dev/null +++ b/releasenotes/notes/add-clifford-from-matrix-3184822cc559e0b7.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added :meth:`.Clifford.from_matrix` and :meth:`.Clifford.from_operator` method that + creates a ``Clifford`` object from its unitary matrix and operator representation respectively. + - | + The constructor of :class:`.Clifford` now can take any Clifford gate object up to 3 qubits + as long it supports :meth:`to_matrix` method, + including parameterized gates such as ``Rz(pi/2)``, which were not convertible before. diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index db3f8a2945fe..4c162c8753cc 100644 --- a/test/python/quantum_info/operators/symplectic/test_clifford.py +++ b/test/python/quantum_info/operators/symplectic/test_clifford.py @@ -21,28 +21,42 @@ from qiskit.circuit import Gate, QuantumCircuit, QuantumRegister from qiskit.circuit.library import ( + CPhaseGate, + CRXGate, + CRYGate, + CRZGate, CXGate, - CZGate, CYGate, + CZGate, + DCXGate, + ECRGate, HGate, IGate, + RXGate, + RYGate, + RZGate, + RXXGate, + RYYGate, + RZZGate, + RZXGate, SdgGate, SGate, SXGate, SXdgGate, SwapGate, - iSwapGate, - ECRGate, - DCXGate, XGate, + XXMinusYYGate, + XXPlusYYGate, YGate, ZGate, + iSwapGate, LinearFunction, PauliGate, ) from qiskit.exceptions import QiskitError from qiskit.quantum_info import random_clifford 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, @@ -540,12 +554,46 @@ def test_from_circuit_with_all_types(self): expected_clifford = Clifford.from_dict(expected_clifford_dict) self.assertEqual(combined_clifford, expected_clifford) + def test_from_gate_with_cyclic_definition(self): + """Test if a Clifford can be created from gate with cyclic definition""" + + class MyHGate(HGate): + """Custom HGate class for test""" + + def __init__(self): + super().__init__() + self.name = "my_h" + + def _define(self): + qc = QuantumCircuit(1, name=self.name) + qc.s(0) + qc.append(MySXGate(), [0]) + qc.s(0) + self.definition = qc + + class MySXGate(SXGate): + """Custom SXGate class for test""" + + def __init__(self): + super().__init__() + self.name = "my_sx" + + def _define(self): + qc = QuantumCircuit(1, name=self.name) + qc.sdg(0) + qc.append(MyHGate(), [0]) + qc.sdg(0) + self.definition = qc + + Clifford(MyHGate()) + @ddt class TestCliffordSynthesis(QiskitTestCase): """Test Clifford synthesis methods.""" - def _cliffords_1q(self): + @staticmethod + def _cliffords_1q(): clifford_dicts = [ {"stabilizer": ["+Z"], "destabilizer": ["-X"]}, {"stabilizer": ["-Z"], "destabilizer": ["+X"]}, @@ -989,13 +1037,58 @@ def test_instruction_name(self, num_qubits): clifford = random_clifford(num_qubits, seed=777) self.assertEqual(clifford.to_instruction().name, str(clifford)) - def visualize_does_not_throw_error(self): + def test_visualize_does_not_throw_error(self): """Test to verify that drawing Clifford does not throw an error""" # An error may be thrown if visualization code calls op.condition instead # of getattr(op, "condition", None) clifford = random_clifford(3, seed=0) print(clifford) + @combine(num_qubits=[1, 2, 3, 4]) + def test_from_matrix_round_trip(self, num_qubits): + """Test round trip conversion to and from matrix""" + for i in range(10): + expected = random_clifford(num_qubits, seed=42 + i) + actual = Clifford.from_matrix(expected.to_matrix()) + self.assertEqual(expected, actual) + + @combine(num_qubits=[1, 2, 3, 4]) + def test_from_operator_round_trip(self, num_qubits): + """Test round trip conversion to and from operator""" + for i in range(10): + expected = random_clifford(num_qubits, seed=777 + i) + actual = Clifford.from_operator(expected.to_operator()) + self.assertEqual(expected, actual) + + @combine( + gate=[ + RXGate(theta=np.pi / 2), + RYGate(theta=np.pi / 2), + RZGate(phi=np.pi / 2), + CPhaseGate(theta=np.pi), + CRXGate(theta=np.pi), + CRYGate(theta=np.pi), + CRZGate(theta=np.pi), + CXGate(), + CYGate(), + CZGate(), + ECRGate(), + RXXGate(theta=np.pi / 2), + RYYGate(theta=np.pi / 2), + RZZGate(theta=np.pi / 2), + RZXGate(theta=np.pi / 2), + SwapGate(), + iSwapGate(), + XXMinusYYGate(theta=np.pi), + XXPlusYYGate(theta=-np.pi), + ] + ) + def test_create_from_gates(self, gate): + """Test if matrix of Clifford created from gate equals the gate matrix up to global phase""" + self.assertTrue( + matrix_equal(Clifford(gate).to_matrix(), gate.to_matrix(), ignore_phase=True) + ) + if __name__ == "__main__": unittest.main() From 602d3400bc45d9d3682d5db84124ccfed07e46c5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Apr 2023 06:38:18 -0400 Subject: [PATCH 009/172] Deprecate the BIPMapping transpiler pass in favor of external plugin (#9924) * Deprecate the BIPMapping transpiler pass in favor of external plugin This commit deprecates the BIPMapping transpiler pass. Since its introduction the pass has been in a weird state since it's introduction. It is a standalone transpiler pass that we never integrated it into transpile() because it has an external dependency on cplex which is a proprietary software package that most people don't have access too. With the introduction of the transpiler stage plugin interface the pass has been turned into an external package: https://github.com/qiskit-community/qiskit-bip-mapper By using the plugin interface the pass can now be cleanly integrates into the transpile() function and also makes the requirement to have cplex installed much more explicit. For users with cplex it's much easier to run the BIPMapping pass as part of a transpilation workflow with `transpile(..., routing_method="bip")`. Closes #8662 * Catch deprecation warnings in tests * Fix lint --- .../passes/routing/algorithms/bip_model.py | 7 ++ .../transpiler/passes/routing/bip_mapping.py | 7 ++ ...eprecate-bip-mapping-f0025c4c724e1ec8.yaml | 14 ++++ test/python/transpiler/test_bip_mapping.py | 64 ++++++++++++------- 4 files changed, 68 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/deprecate-bip-mapping-f0025c4c724e1ec8.yaml diff --git a/qiskit/transpiler/passes/routing/algorithms/bip_model.py b/qiskit/transpiler/passes/routing/algorithms/bip_model.py index f0b7d576d4c3..7a37ff6a949b 100644 --- a/qiskit/transpiler/passes/routing/algorithms/bip_model.py +++ b/qiskit/transpiler/passes/routing/algorithms/bip_model.py @@ -26,6 +26,7 @@ trace_to_fid, ) from qiskit.utils import optionals as _optionals +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) @@ -41,6 +42,12 @@ class BIPMappingModel: the solution will be stored in :attr:`solution`). None if it's not yet set. """ + @deprecate_func( + since="0.24.0", + additional_msg="This has been replaced by a new transpiler plugin package: " + "qiskit-bip-mapper. More details can be found here: " + "https://github.com/qiskit-community/qiskit-bip-mapper", + ) # pylint: disable=bad-docstring-quotes def __init__(self, dag, coupling_map, qubit_subset, dummy_timesteps=None): """ Args: diff --git a/qiskit/transpiler/passes/routing/bip_mapping.py b/qiskit/transpiler/passes/routing/bip_mapping.py index 9f6fc177eece..58b040a9821c 100644 --- a/qiskit/transpiler/passes/routing/bip_mapping.py +++ b/qiskit/transpiler/passes/routing/bip_mapping.py @@ -23,6 +23,7 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes.routing.algorithms.bip_model import BIPMappingModel from qiskit.transpiler.target import target_to_backend_properties, Target +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) @@ -63,6 +64,12 @@ class BIPMapping(TransformationPass): `arXiv:2106.06446 `_ """ + @deprecate_func( + since="0.24.0", + additional_msg="This has been replaced by a new transpiler plugin package: " + "qiskit-bip-mapper. More details can be found here: " + "https://github.com/qiskit-community/qiskit-bip-mapper", + ) # pylint: disable=bad-docstring-quotes def __init__( self, coupling_map, diff --git a/releasenotes/notes/deprecate-bip-mapping-f0025c4c724e1ec8.yaml b/releasenotes/notes/deprecate-bip-mapping-f0025c4c724e1ec8.yaml new file mode 100644 index 000000000000..93a0523397bd --- /dev/null +++ b/releasenotes/notes/deprecate-bip-mapping-f0025c4c724e1ec8.yaml @@ -0,0 +1,14 @@ +--- +deprecations: + - | + The transpiler routing pass, :class:`~.BIPMapping` has been deprecated + and will be removed in a future release. It has been replaced by an external + plugin package: ``qiskit-bip-mapper``. Details for this new package can + be found at the package's github repository: + + https://github.com/qiskit-community/qiskit-bip-mapper + + 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`. + diff --git a/test/python/transpiler/test_bip_mapping.py b/test/python/transpiler/test_bip_mapping.py index 60283abec750..a95ca727f79a 100644 --- a/test/python/transpiler/test_bip_mapping.py +++ b/test/python/transpiler/test_bip_mapping.py @@ -36,7 +36,8 @@ def test_empty(self): """Returns the original circuit if the circuit is empty.""" coupling = CouplingMap([[0, 1]]) circuit = QuantumCircuit(2) - actual = BIPMapping(coupling)(circuit) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling)(circuit) self.assertEqual(circuit, actual) def test_no_two_qubit_gates(self): @@ -49,8 +50,8 @@ def test_no_two_qubit_gates(self): circuit = QuantumCircuit(2) circuit.h(0) - - actual = BIPMapping(coupling)(circuit) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling)(circuit) self.assertEqual(circuit, actual) @@ -70,7 +71,8 @@ def test_trivial_case(self): circuit.h(0) circuit.cx(2, 0) - actual = BIPMapping(coupling)(circuit) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling)(circuit) self.assertEqual(3, len(actual)) for inst, _, _ in actual.data: # there are no swaps self.assertFalse(isinstance(inst, SwapGate)) @@ -82,7 +84,8 @@ def test_no_swap(self): circuit = QuantumCircuit(3) circuit.cx(1, 2) - actual = BIPMapping(coupling)(circuit) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling)(circuit) q = QuantumRegister(3, name="q") expected = QuantumCircuit(q) @@ -98,7 +101,8 @@ def test_ignore_initial_layout(self): circuit.cx(1, 2) property_set = {"layout": Layout.generate_trivial_layout(*circuit.qubits)} - actual = BIPMapping(coupling)(circuit, property_set) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling)(circuit, property_set) q = QuantumRegister(3, name="q") expected = QuantumCircuit(q) @@ -117,7 +121,8 @@ def test_can_map_measurements_correctly(self): circuit.measure(qr[1], cr[0]) circuit.measure(qr[2], cr[1]) - actual = BIPMapping(coupling)(circuit) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling)(circuit) q = QuantumRegister(3, "q") expected = QuantumCircuit(q, cr) @@ -139,7 +144,8 @@ def test_can_map_measurements_correctly_with_target(self): circuit.measure(qr[1], cr[0]) circuit.measure(qr[2], cr[1]) - actual = BIPMapping(target)(circuit) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(target)(circuit) q = QuantumRegister(3, "q") expected = QuantumCircuit(q, cr) @@ -162,8 +168,9 @@ def test_never_modify_mapped_circuit(self): circuit.measure(2, 1) dag = circuit_to_dag(circuit) - mapped_dag = BIPMapping(coupling).run(dag) - remapped_dag = BIPMapping(coupling).run(mapped_dag) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + mapped_dag = BIPMapping(coupling).run(dag) + remapped_dag = BIPMapping(coupling).run(mapped_dag) self.assertEqual(mapped_dag, remapped_dag) @@ -177,7 +184,8 @@ def test_no_swap_multi_layer(self): circuit.cx(qr[0], qr[3]) property_set = {} - actual = BIPMapping(coupling, objective="depth")(circuit, property_set) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling, objective="depth")(circuit, property_set) self.assertEqual(2, actual.depth()) CheckMap(coupling)(actual, property_set) @@ -194,7 +202,8 @@ def test_unmappable_cnots_in_a_layer(self): circuit.measure(qr, cr) coupling = CouplingMap([[0, 1], [1, 2], [1, 3]]) # {0: [1], 1: [2, 3]} - actual = BIPMapping(coupling)(circuit) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling)(circuit) # Fails to map and returns the original circuit self.assertEqual(circuit, actual) @@ -233,7 +242,8 @@ def test_multi_cregs(self): coupling = CouplingMap([[0, 1], [0, 2], [2, 3]]) # linear [1, 0, 2, 3] property_set = {} - actual = BIPMapping(coupling, objective="depth")(circuit, property_set) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling, objective="depth")(circuit, property_set) self.assertEqual(5, actual.depth()) CheckMap(coupling)(actual, property_set) @@ -264,7 +274,8 @@ def test_swaps_in_dummy_steps(self): coupling = CouplingMap.from_line(4) property_set = {} - actual = BIPMapping(coupling, objective="depth")(circuit, property_set) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling, objective="depth")(circuit, property_set) self.assertEqual(7, actual.depth()) CheckMap(coupling)(actual, property_set) @@ -295,7 +306,8 @@ def test_different_number_of_virtual_and_physical_qubits(self): coupling = CouplingMap.from_line(5) with self.assertRaises(TranspilerError): - BIPMapping(coupling)(circuit) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + BIPMapping(coupling)(circuit) def test_qubit_subset(self): """Test if `qubit_subset` option works as expected.""" @@ -306,7 +318,8 @@ def test_qubit_subset(self): coupling = CouplingMap([(0, 1), (1, 3), (3, 2)]) qubit_subset = [0, 1, 3] - actual = BIPMapping(coupling, qubit_subset=qubit_subset)(circuit) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + actual = BIPMapping(coupling, qubit_subset=qubit_subset)(circuit) # all used qubits are in qubit_subset bit_indices = {bit: index for index, bit in enumerate(actual.qubits)} for _, qargs, _ in actual.data: @@ -323,7 +336,8 @@ def test_unconnected_qubit_subset(self): coupling = CouplingMap([(0, 1), (1, 3), (3, 2)]) with self.assertRaises(TranspilerError): - BIPMapping(coupling, qubit_subset=[0, 1, 2])(circuit) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + BIPMapping(coupling, qubit_subset=[0, 1, 2])(circuit) def test_objective_function(self): """Test if ``objective`` functions prioritize metrics correctly.""" @@ -345,13 +359,15 @@ def test_objective_function(self): qc.dcx(0, 1) qc.cx(2, 3) coupling = CouplingMap(FakeLima().configuration().coupling_map) - dep_opt = BIPMapping(coupling, objective="depth", qubit_subset=[0, 1, 3, 4])(qc) - err_opt = BIPMapping( - coupling, - objective="gate_error", - qubit_subset=[0, 1, 3, 4], - backend_prop=FakeLima().properties(), - )(qc) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + dep_opt = BIPMapping(coupling, objective="depth", qubit_subset=[0, 1, 3, 4])(qc) + with self.assertWarnsRegex(DeprecationWarning, r"^The class.*is deprecated"): + err_opt = BIPMapping( + coupling, + objective="gate_error", + qubit_subset=[0, 1, 3, 4], + backend_prop=FakeLima().properties(), + )(qc) # depth = number of su4 layers (mirrored gates have to be consolidated as single su4 gates) pm_ = PassManager([Collect2qBlocks(), ConsolidateBlocks(basis_gates=["cx", "u"])]) dep_opt = pm_.run(dep_opt) From d814ad43c81e2b285ea91db4f4bba2234bc2b7db Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 12 Apr 2023 13:08:16 +0200 Subject: [PATCH 010/172] Leverage ``EvolutionSynthesis`` in the ``EvolvedOperatorAnsatz`` (#9537) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * v0 of EvolvedOpAnsatz * Fix tests * keep supporting old import location * move hamiltonian gate * fix imports * fix test * add reno * add unittest, rm pending deprecation test * Update qiskit/circuit/library/evolved_operator_ansatz.py Co-authored-by: Max Rossmannek * don't move hamiltonian gate for now * Don't touch HamiltonianGate for now * lintttttttttt! * Apply suggestions from code review Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * remove redundant deprecation warning * fix lint and test * add checks for identity, update QAOA docs * Update test/python/circuit/library/test_qaoa_ansatz.py --------- Co-authored-by: Max Rossmannek Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> --- .../library/evolved_operator_ansatz.py | 89 ++++++++++++++----- qiskit/circuit/library/n_local/qaoa_ansatz.py | 24 ++--- .../synthesis/evolution/matrix_synthesis.py | 12 ++- .../circuit/library/test_evolution_gate.py | 5 ++ .../circuit/library/test_evolved_op_ansatz.py | 41 +++++++-- .../circuit/library/test_qaoa_ansatz.py | 9 +- 6 files changed, 128 insertions(+), 52 deletions(-) diff --git a/qiskit/circuit/library/evolved_operator_ansatz.py b/qiskit/circuit/library/evolved_operator_ansatz.py index 7d7df10c7664..f5e891220665 100644 --- a/qiskit/circuit/library/evolved_operator_ansatz.py +++ b/qiskit/circuit/library/evolved_operator_ansatz.py @@ -12,12 +12,19 @@ """The evolved operator ansatz.""" -from typing import Optional, Union, List +from __future__ import annotations +from collections.abc import Sequence import numpy as np -from qiskit.circuit import Parameter, QuantumRegister, QuantumCircuit +from qiskit.circuit.parameter import Parameter +from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.exceptions import QiskitError +from qiskit.quantum_info import Operator, Pauli, SparsePauliOp +from qiskit.synthesis.evolution import LieTrotter +from .pauli_evolution import PauliEvolutionGate from .n_local.n_local import NLocal @@ -31,31 +38,28 @@ def __init__( evolution=None, insert_barriers: bool = False, name: str = "EvolvedOps", - parameter_prefix: Union[str, List[str]] = "t", - initial_state: Optional[QuantumCircuit] = None, + parameter_prefix: str | Sequence[str] = "t", + initial_state: QuantumCircuit | None = None, ): """ Args: - operators (Optional[Union[OperatorBase, QuantumCircuit, list]): The operators to evolve. - If a circuit is passed, we assume it implements an already evolved operator and thus - the circuit is not evolved again. Can be a single operator (circuit) or a list of - operators (and circuits). + operators (BaseOperator | OperatorBase | QuantumCircuit | list | None): The operators + to evolve. If a circuit is passed, we assume it implements an already evolved + operator and thus the circuit is not evolved again. Can be a single operator + (circuit) or a list of operators (and circuits). reps: The number of times to repeat the evolved operators. - evolution (Optional[EvolutionBase]): An opflow converter object to construct the evolution. - Defaults to Trotterization. + evolution (EvolutionBase | EvolutionSynthesis | None): + A specification of which evolution synthesis to use for the + :class:`.PauliEvolutionGate`, if the operator is from :mod:`qiskit.quantum_info` + or an opflow converter object if the operator is from :mod:`qiskit.opflow`. + Defaults to first order Trotterization. insert_barriers: Whether to insert barriers in between each evolution. name: The name of the circuit. parameter_prefix: Set the names of the circuit parameters. If a string, the same prefix will be used for each parameters. Can also be a list to specify a prefix per operator. - initial_state: A `QuantumCircuit` object to prepend to the circuit. + initial_state: A :class:`.QuantumCircuit` object to prepend to the circuit. """ - if evolution is None: - # pylint: disable=cyclic-import - from qiskit.opflow import PauliTrotterEvolution - - evolution = PauliTrotterEvolution() - super().__init__( initial_state=initial_state, parameter_prefix=parameter_prefix, @@ -64,6 +68,7 @@ def __init__( name=name, ) self._operators = None + if operators is not None: self.operators = operators @@ -99,8 +104,14 @@ def evolution(self): """The evolution converter used to compute the evolution. Returns: - EvolutionBase: The evolution converter used to compute the evolution. + EvolutionBase or EvolutionSynthesis: The evolution converter used to compute the evolution. """ + if self._evolution is None: + # pylint: disable=cyclic-import + from qiskit.opflow import PauliTrotterEvolution + + return PauliTrotterEvolution() + return self._evolution @evolution.setter @@ -108,7 +119,8 @@ def evolution(self, evol) -> None: """Sets the evolution converter used to compute the evolution. Args: - evol (EvolutionBase): An opflow converter object to construct the evolution. + evol (EvolutionBase | EvolutionSynthesis): An evolution synthesis object or + opflow converter object to construct the evolution. """ self._invalidate() self._evolution = evol @@ -152,6 +164,32 @@ def preferred_init_points(self): self._build() return np.zeros(self.reps * len(self.operators), dtype=float) + def _evolve_operator(self, operator, time): + from qiskit.opflow import OperatorBase, EvolutionBase + from qiskit.extensions import HamiltonianGate + + if isinstance(operator, OperatorBase): + if not isinstance(self.evolution, EvolutionBase): + raise QiskitError( + "If qiskit.opflow operators are evolved the evolution must be a " + f"qiskit.opflow.EvolutionBase, not a {type(self.evolution)}." + ) + + evolved = self.evolution.convert((time * operator).exp_i()) + return evolved.reduce().to_circuit() + + # if the operator is specified as matrix use exact matrix exponentiation + if isinstance(operator, Operator): + gate = HamiltonianGate(operator, time) + # otherwise, use the PauliEvolutionGate + else: + evolution = LieTrotter() if self._evolution is None else self._evolution + gate = PauliEvolutionGate(operator, time, synthesis=evolution) + + evolved = QuantumCircuit(operator.num_qubits) + evolved.append(gate, evolved.qubits) + return evolved + def _build(self): if self._is_built: return @@ -171,8 +209,8 @@ def _build(self): if _is_pauli_identity(op): continue - evolved_op = self.evolution.convert((coeff * op).exp_i()).reduce() - circuits.append(evolved_op.to_circuit()) + evolved = self._evolve_operator(op, coeff) + circuits.append(evolved) self.rotation_blocks = [] self.entanglement_blocks = circuits @@ -206,6 +244,13 @@ def _is_pauli_identity(operator): if isinstance(operator, PauliSumOp): operator = operator.to_pauli_op() + if isinstance(operator, SparsePauliOp): + if len(operator.paulis) == 1: + operator = operator.paulis[0] # check if the single Pauli is identity below + else: + return False if isinstance(operator, PauliOp): - return not np.any(np.logical_or(operator.primitive.x, operator.primitive.z)) + operator = operator.primitive + if isinstance(operator, Pauli): + return not np.any(np.logical_or(operator.x, operator.z)) return False diff --git a/qiskit/circuit/library/n_local/qaoa_ansatz.py b/qiskit/circuit/library/n_local/qaoa_ansatz.py index 33b880caf4ff..c3aa8fc37adf 100644 --- a/qiskit/circuit/library/n_local/qaoa_ansatz.py +++ b/qiskit/circuit/library/n_local/qaoa_ansatz.py @@ -41,16 +41,16 @@ def __init__( ): r""" Args: - cost_operator (OperatorBase, optional): The operator representing the cost of - the optimization problem, denoted as :math:`U(C, \gamma)` in the original paper. - Must be set either in the constructor or via property setter. + cost_operator (BaseOperator or OperatorBase, optional): The operator + representing the cost of the optimization problem, denoted as :math:`U(C, \gamma)` + in the original paper. Must be set either in the constructor or via property setter. reps (int): The integer parameter p, which determines the depth of the circuit, as specified in the original paper, default is 1. initial_state (QuantumCircuit, optional): An optional initial state to use. If `None` is passed then a set of Hadamard gates is applied as an initial state to all qubits. - mixer_operator (OperatorBase or QuantumCircuit, optional): An optional custom mixer - to use instead of the global X-rotations, denoted as :math:`U(B, \beta)` + mixer_operator (BaseOperator or OperatorBase or QuantumCircuit, optional): An optional + custom mixer to use instead of the global X-rotations, denoted as :math:`U(B, \beta)` in the original paper. Can be an operator or an optionally parameterized quantum circuit. name (str): A name of the circuit, default 'qaoa' @@ -147,8 +147,8 @@ def operators(self) -> list: """The operators that are evolved in this circuit. Returns: - List[Union[OperatorBase, QuantumCircuit]]: The operators to be evolved (and circuits) - in this ansatz. + List[Union[BaseOperator, OperatorBase, QuantumCircuit]]: The operators to be evolved + (and circuits) in this ansatz. """ return [self.cost_operator, self.mixer_operator] @@ -157,7 +157,7 @@ def cost_operator(self): """Returns an operator representing the cost of the optimization problem. Returns: - OperatorBase: cost operator. + BaseOperator or OperatorBase: cost operator. """ return self._cost_operator @@ -166,7 +166,7 @@ def cost_operator(self, cost_operator) -> None: """Sets cost operator. Args: - cost_operator (OperatorBase, optional): cost operator to set. + cost_operator (BaseOperator or OperatorBase, optional): cost operator to set. """ self._cost_operator = cost_operator self.qregs = [QuantumRegister(self.num_qubits, name="q")] @@ -211,7 +211,7 @@ def mixer_operator(self): """Returns an optional mixer operator expressed as an operator or a quantum circuit. Returns: - OperatorBase or QuantumCircuit, optional: mixer operator or circuit. + BaseOperator or OperatorBase or QuantumCircuit, optional: mixer operator or circuit. """ if self._mixer is not None: return self._mixer @@ -239,8 +239,8 @@ def mixer_operator(self, mixer_operator) -> None: """Sets mixer operator. Args: - mixer_operator (OperatorBase or QuantumCircuit, optional): mixer operator or circuit - to set. + mixer_operator (BaseOperator or OperatorBase or QuantumCircuit, optional): mixer + operator or circuit to set. """ self._mixer = mixer_operator self._invalidate() diff --git a/qiskit/synthesis/evolution/matrix_synthesis.py b/qiskit/synthesis/evolution/matrix_synthesis.py index 3af9d04b6195..97d0167a3ac4 100644 --- a/qiskit/synthesis/evolution/matrix_synthesis.py +++ b/qiskit/synthesis/evolution/matrix_synthesis.py @@ -27,22 +27,20 @@ class MatrixExponential(EvolutionSynthesis): """ def synthesize(self, evolution): - from scipy.linalg import expm + from qiskit.extensions import HamiltonianGate # get operators and time to evolve operators = evolution.operator time = evolution.time - # construct the evolution circuit - evolution_circuit = QuantumCircuit(operators[0].num_qubits) - if not isinstance(operators, list): matrix = operators.to_matrix() else: matrix = sum(op.to_matrix() for op in operators) - exp = expm(-1j * time * matrix) - - evolution_circuit.unitary(exp, evolution_circuit.qubits) + # construct the evolution circuit + evolution_circuit = QuantumCircuit(operators[0].num_qubits) + gate = HamiltonianGate(matrix, time) + evolution_circuit.append(gate, evolution_circuit.qubits) return evolution_circuit diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index 8b5c491af088..92926a077986 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -12,6 +12,7 @@ """Test the evolution gate.""" +import unittest import numpy as np import scipy from ddt import ddt, data, unpack @@ -313,3 +314,7 @@ def test_labels_and_name(self): evo = PauliEvolutionGate(op) self.assertEqual(evo.name, "PauliEvolution") self.assertEqual(evo.label, f"exp(-it {label})") + + +if __name__ == "__main__": + unittest.main() diff --git a/test/python/circuit/library/test_evolved_op_ansatz.py b/test/python/circuit/library/test_evolved_op_ansatz.py index 93b489c7dc82..3fc0161eac30 100644 --- a/test/python/circuit/library/test_evolved_op_ansatz.py +++ b/test/python/circuit/library/test_evolved_op_ansatz.py @@ -12,22 +12,32 @@ """Test the evolved operator ansatz.""" +from ddt import ddt, data +import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.opflow import X, Y, Z, I, MatrixEvolution +from qiskit.quantum_info import SparsePauliOp, Operator, Pauli from qiskit.circuit.library import EvolvedOperatorAnsatz +from qiskit.synthesis.evolution import MatrixExponential from qiskit.test import QiskitTestCase +@ddt class TestEvolvedOperatorAnsatz(QiskitTestCase): """Test the evolved operator ansatz.""" - def test_evolved_op_ansatz(self): + @data(True, False) + def test_evolved_op_ansatz(self, use_opflow): """Test the default evolution.""" num_qubits = 3 - ops = [Z ^ num_qubits, Y ^ num_qubits, X ^ num_qubits] + if use_opflow: + ops = [Z ^ num_qubits, Y ^ num_qubits, X ^ num_qubits] + else: + ops = [Pauli("Z" * num_qubits), Pauli("Y" * num_qubits), Pauli("X" * num_qubits)] + strings = ["z" * num_qubits, "y" * num_qubits, "x" * num_qubits] * 2 evo = EvolvedOperatorAnsatz(ops, 2) @@ -39,18 +49,29 @@ def test_evolved_op_ansatz(self): self.assertEqual(evo.decompose().decompose(), reference) - def test_custom_evolution(self): + @data(True, False) + def test_custom_evolution(self, use_opflow): """Test using another evolution than the default (e.g. matrix evolution).""" + if use_opflow: + op = X ^ I ^ Z + matrix = op.to_matrix() + evolution = MatrixEvolution() + else: + op = SparsePauliOp(["ZIX"]) + matrix = np.array(op) + evolution = MatrixExponential() - op = X ^ I ^ Z - matrix = op.to_matrix() - evo = EvolvedOperatorAnsatz(op, evolution=MatrixEvolution()) + evo = EvolvedOperatorAnsatz(op, evolution=evolution) parameters = evo.parameters reference = QuantumCircuit(3) reference.hamiltonian(matrix, parameters[0], [0, 1, 2]) - self.assertEqual(evo.decompose(), reference) + decomposed = evo.decompose() + if not use_opflow: + decomposed = decomposed.decompose() + + self.assertEqual(decomposed, reference) def test_changing_operators(self): """Test rebuilding after the operators changed.""" @@ -87,6 +108,12 @@ def test_empty_build_fails(self): with self.assertRaises(ValueError): _ = evo.draw() + def test_matrix_operator(self): + """Test passing a quantum_info.Operator uses the HamiltonianGate.""" + unitary = Operator([[0, 1], [1, 0]]) + evo = EvolvedOperatorAnsatz(unitary, reps=3).decompose() + self.assertEqual(evo.count_ops()["hamiltonian"], 3) + def evolve(pauli_string, time): """Get the reference evolution circuit for a single Pauli string.""" diff --git a/test/python/circuit/library/test_qaoa_ansatz.py b/test/python/circuit/library/test_qaoa_ansatz.py index 1bde60570bdb..9a05b2c420c2 100644 --- a/test/python/circuit/library/test_qaoa_ansatz.py +++ b/test/python/circuit/library/test_qaoa_ansatz.py @@ -18,7 +18,8 @@ from qiskit.circuit import QuantumCircuit, Parameter from qiskit.circuit.library import HGate, RXGate, YGate, RYGate, RZGate from qiskit.circuit.library.n_local.qaoa_ansatz import QAOAAnsatz -from qiskit.opflow import I, Y, Z, PauliSumOp +from qiskit.opflow import I, Y, Z +from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.test import QiskitTestCase @@ -156,15 +157,15 @@ def test_empty_op(self): def test_num_qubits(self, num_qubits): """Test num_qubits with {num_qubits} qubits""" - circuit = QAOAAnsatz(cost_operator=I ^ num_qubits, reps=5) + circuit = QAOAAnsatz(cost_operator=Pauli("I" * num_qubits), reps=5) self.assertEqual(circuit.num_qubits, num_qubits) def test_identity(self): """Test construction with identity""" reps = 4 num_qubits = 3 - pauli_sum_op = PauliSumOp.from_list([("I" * num_qubits, 1)]) - pauli_op = I ^ num_qubits + pauli_sum_op = SparsePauliOp.from_list([("I" * num_qubits, 1)]) + pauli_op = Pauli("I" * num_qubits) for cost in [pauli_op, pauli_sum_op]: for mixer in [None, pauli_op, pauli_sum_op]: with self.subTest(f"cost: {type(cost)}, mixer:{type(mixer)}"): From 75673c7867222a9d8048d5264053162c5cfbe802 Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Thu, 13 Apr 2023 00:49:41 +0900 Subject: [PATCH 011/172] Remove deprecated methods and args in Primitives (#9480) * Remove deprecated methods and args in Primitives * rm unnecessary comments * add reno * Update qiskit/primitives/base/base_sampler.py * From review comments --- .../diagonal_estimator.py | 2 + qiskit/primitives/backend_estimator.py | 19 +- qiskit/primitives/backend_sampler.py | 14 +- qiskit/primitives/base/base_estimator.py | 281 +------------ qiskit/primitives/base/base_primitive.py | 6 - qiskit/primitives/base/base_sampler.py | 200 +-------- qiskit/primitives/estimator.py | 43 +- qiskit/primitives/sampler.py | 34 +- ...ethods-in-primitives-d89d444ec0217ae6.yaml | 6 + .../gradients/logging_primitives.py | 11 +- test/python/primitives/test_estimator.py | 389 ++---------------- test/python/primitives/test_sampler.py | 383 ++--------------- 12 files changed, 106 insertions(+), 1282 deletions(-) create mode 100644 releasenotes/notes/delete-args-and-methods-in-primitives-d89d444ec0217ae6.yaml diff --git a/qiskit/algorithms/minimum_eigensolvers/diagonal_estimator.py b/qiskit/algorithms/minimum_eigensolvers/diagonal_estimator.py index 6af526f5ae85..40354c884d98 100644 --- a/qiskit/algorithms/minimum_eigensolvers/diagonal_estimator.py +++ b/qiskit/algorithms/minimum_eigensolvers/diagonal_estimator.py @@ -65,6 +65,8 @@ def __init__( self.aggregation = aggregation self.callback = callback + self._circuit_ids = {} + self._observable_ids = {} def _run( self, diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index 4b97b0ba1838..35d6cd2eaced 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -118,12 +118,7 @@ def __init__( of the input circuits is skipped and the circuit objects will be directly executed when this object is called. """ - super().__init__( - circuits=None, - observables=None, - parameters=None, - options=options, - ) + super().__init__(options=options) self._abelian_grouping = abelian_grouping @@ -138,16 +133,8 @@ def __init__( self._grouping = list(zip(range(len(self._circuits)), range(len(self._observables)))) self._skip_transpilation = skip_transpilation - def __new__( # pylint: disable=signature-differs - cls, - backend: BackendV1 | BackendV2, # pylint: disable=unused-argument - **kwargs, - ): - self = super().__new__(cls) - return self - - def __getnewargs__(self): - return (self._backend,) + self._circuit_ids = {} + self._observable_ids = {} @property def transpile_options(self) -> Options: diff --git a/qiskit/primitives/backend_sampler.py b/qiskit/primitives/backend_sampler.py index b06d552c2cc4..c5d66ebd11cd 100644 --- a/qiskit/primitives/backend_sampler.py +++ b/qiskit/primitives/backend_sampler.py @@ -68,24 +68,14 @@ def __init__( ValueError: If backend is not provided """ - super().__init__(None, None, options) + super().__init__(options=options) self._backend = backend self._transpile_options = Options() self._bound_pass_manager = bound_pass_manager self._preprocessed_circuits: list[QuantumCircuit] | None = None self._transpiled_circuits: list[QuantumCircuit] = [] self._skip_transpilation = skip_transpilation - - def __new__( # pylint: disable=signature-differs - cls, - backend: BackendV1 | BackendV2, # pylint: disable=unused-argument - **kwargs, - ): - self = super().__new__(cls) - return self - - def __getnewargs__(self): - return (self._backend,) + self._circuit_ids = {} @property def preprocessed_circuits(self) -> list[QuantumCircuit]: diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index e5d396884fdb..1cac61331b86 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -81,24 +81,18 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Iterable, Sequence +from collections.abc import Sequence from copy import copy -from typing import cast -from warnings import warn -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit import QuantumCircuit from qiskit.circuit.parametertable import ParameterView from qiskit.opflow import PauliSumOp from qiskit.providers import JobV1 as Job from qiskit.quantum_info.operators import SparsePauliOp from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.utils.deprecation import deprecate_arguments, deprecate_function -from ..utils import _circuit_key, _observable_key, init_observable +from ..utils import init_observable from .base_primitive import BasePrimitive -from .estimator_result import EstimatorResult class BaseEstimator(BasePrimitive): @@ -111,9 +105,7 @@ class BaseEstimator(BasePrimitive): def __init__( self, - circuits: Iterable[QuantumCircuit] | QuantumCircuit | None = None, - observables: Iterable[SparsePauliOp] | SparsePauliOp | None = None, - parameters: Iterable[Iterable[Parameter]] | None = None, + *, options: dict | None = None, ): """ @@ -121,58 +113,13 @@ def __init__( holds resources until the instance is ``close()`` ed or the context is exited. Args: - circuits: Quantum circuits that represent quantum states. - observables: Observables. - parameters: Parameters of quantum circuits, specifying the order in which values - will be bound. Defaults to ``[circ.parameters for circ in circuits]`` - The indexing is such that ``parameters[i, j]`` is the j-th formal parameter of - ``circuits[i]``. options: Default options. - - Raises: - ValueError: For mismatch of circuits and parameters list. """ - if circuits is not None or observables is not None or parameters is not None: - warn( - "The BaseEstimator `circuits`, `observables`, `parameters` kwarg are deprecated " - "as of Qiskit Terra 0.22.0 and will be removed no earlier than 3 months after " - "the release date. You can use the 'run' method to append objects.", - DeprecationWarning, - 2, - ) - if isinstance(circuits, QuantumCircuit): - circuits = (circuits,) - self._circuits = [] if circuits is None else list(circuits) - - if isinstance(observables, SparsePauliOp): - observables = (observables,) - self._observables = [] if observables is None else list(observables) - - # To guarantee that they exist as instance variable. - # With only dynamic set, the python will not know if the attribute exists or not. - self._circuit_ids: dict[tuple, int] = self._circuit_ids - self._observable_ids: dict[tuple, int] = self._observable_ids - - if parameters is None: - self._parameters = [circ.parameters for circ in self._circuits] - else: - self._parameters = [ParameterView(par) for par in parameters] - if len(self._parameters) != len(self._circuits): - raise ValueError( - f"Different number of parameters ({len(self._parameters)}) and " - f"circuits ({len(self._circuits)})" - ) - for i, (circ, params) in enumerate(zip(self._circuits, self._parameters)): - if circ.num_parameters != len(params): - raise ValueError( - f"Different numbers of parameters of {i}-th circuit: " - f"expected {circ.num_parameters}, actual {len(params)}." - ) + self._circuits = [] + self._observables = [] + self._parameters = [] super().__init__(options) - ################################################################################ - ## METHODS - ################################################################################ def run( self, circuits: Sequence[QuantumCircuit] | QuantumCircuit, @@ -239,8 +186,7 @@ def run( **run_opts.__dict__, ) - # This will be comment out after 0.22. (This is necessary for the compatibility.) - # @abstractmethod + @abstractmethod def _run( self, circuits: tuple[QuantumCircuit, ...], @@ -248,13 +194,8 @@ def _run( parameter_values: tuple[tuple[float, ...], ...], **run_options, ) -> Job: - raise NotImplementedError( - "_run method is not implemented. This method will be @abstractmethod after 0.22." - ) + raise NotImplementedError("The subclass of BaseEstimator must implment `_run` method.") - ################################################################################ - ## VALIDATION - ################################################################################ @staticmethod def _validate_observables( observables: Sequence[BaseOperator | PauliSumOp | str] | BaseOperator | PauliSumOp | str, @@ -282,36 +223,6 @@ def _cross_validate_circuits_observables( f"({observable.num_qubits})." ) - ################################################################################ - ## DEPRECATED - ################################################################################ - def __new__( - cls, - circuits: Iterable[QuantumCircuit] | QuantumCircuit | None = None, - observables: Iterable[SparsePauliOp] | SparsePauliOp | None = None, - parameters: Iterable[Iterable[Parameter]] | None = None, # pylint: disable=unused-argument - **kwargs, # pylint: disable=unused-argument - ): - self = super().__new__(cls) - if circuits is None: - self._circuit_ids = {} - elif isinstance(circuits, Iterable): - circuits = copy(circuits) - self._circuit_ids = {_circuit_key(circuit): i for i, circuit in enumerate(circuits)} - else: - self._circuit_ids = {_circuit_key(circuits): 0} - if observables is None: - self._observable_ids = {} - elif isinstance(observables, Iterable): - observables = copy(observables) - self._observable_ids = { - _observable_key(init_observable(observable)): i - for i, observable in enumerate(observables) - } - else: - self._observable_ids = {_observable_key(init_observable(observables)): 0} - return self - @property def circuits(self) -> tuple[QuantumCircuit, ...]: """Quantum circuits that represents quantum states. @@ -338,177 +249,3 @@ def parameters(self) -> tuple[ParameterView, ...]: Parameters, where ``parameters[i][j]`` is the j-th parameter of the i-th circuit. """ return tuple(self._parameters) - - @deprecate_function( - "The BaseEstimator.__enter__ method is deprecated as of Qiskit Terra 0.22.0 " - "and will be removed no sooner than 3 months after the releasedate. " - "BaseEstimator should be initialized directly.", - since="0.22.0", - ) - def __enter__(self): - return self - - @deprecate_function( - "The BaseEstimator.__call__ method is deprecated as of Qiskit Terra 0.22.0 " - "and will be removed no sooner than 3 months after the releasedate. " - "BaseEstimator should be initialized directly.", - since="0.22.0", - ) - def __exit__(self, *exc_info): - self.close() - - def close(self): - """Close the session and free resources""" - ... - - @deprecate_function( - "The BaseEstimator.__call__ method is deprecated as of Qiskit Terra 0.22.0 " - "and will be removed no sooner than 3 months after the releasedate. " - "Use the 'run' method instead.", - since="0.22.0", - ) - @deprecate_arguments( - {"circuit_indices": "circuits", "observable_indices": "observables"}, - since="0.21.0", - ) - def __call__( - self, - circuits: Sequence[int | QuantumCircuit], - observables: Sequence[int | SparsePauliOp], - parameter_values: Sequence[Sequence[float]] | None = None, - **run_options, - ) -> EstimatorResult: - """Run the estimation of expectation value(s). - - ``circuits``, ``observables``, and ``parameter_values`` should have the same - length. The i-th element of the result is the expectation of observable - - .. code-block:: python - - obs = self.observables[observables[i]] - - for the state prepared by - - .. code-block:: python - - circ = self.circuits[circuits[i]] - - with bound parameters - - .. code-block:: python - - values = parameter_values[i]. - - Args: - circuits: the list of circuit indices or circuit objects. - observables: the list of observable indices or observable objects. - parameter_values: concrete parameters to be bound. - run_options: Default runtime options used for circuit execution. - - Returns: - EstimatorResult: The result of the estimator. - - Raises: - ValueError: For mismatch of object id. - ValueError: For mismatch of length of Sequence. - """ - - # Support ndarray - if isinstance(parameter_values, np.ndarray): - parameter_values = parameter_values.tolist() - - # Allow objects - circuits = [ - self._circuit_ids.get(_circuit_key(circuit)) - if not isinstance(circuit, (int, np.integer)) - else circuit - for circuit in circuits - ] - if any(circuit is None for circuit in circuits): - raise ValueError( - "The circuits passed when calling estimator is not one of the circuits used to " - "initialize the session." - ) - observables = [ - self._observable_ids.get(_observable_key(observable)) - if not isinstance(observable, (int, np.integer)) - else observable - for observable in observables - ] - if any(observable is None for observable in observables): - raise ValueError( - "The observables passed when calling estimator is not one of the observables used to " - "initialize the session." - ) - - circuits = cast("list[int]", circuits) - observables = cast("list[int]", observables) - - # Allow optional - if parameter_values is None: - for i in circuits: - if len(self._circuits[i].parameters) != 0: - raise ValueError( - f"The {i}-th circuit is parameterised," - "but parameter values are not given." - ) - parameter_values = [[]] * len(circuits) - - # Validation - if len(circuits) != len(observables): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of observables ({len(observables)})." - ) - if len(circuits) != len(parameter_values): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of parameter value sets ({len(parameter_values)})." - ) - - for i, value in zip(circuits, parameter_values): - if len(value) != len(self._parameters[i]): - raise ValueError( - f"The number of values ({len(value)}) does not match " - f"the number of parameters ({len(self._parameters[i])}) for the {i}-th circuit." - ) - - for circ_i, obs_i in zip(circuits, observables): - circuit_num_qubits = self.circuits[circ_i].num_qubits - observable_num_qubits = self.observables[obs_i].num_qubits - if circuit_num_qubits != observable_num_qubits: - raise ValueError( - f"The number of qubits of the {circ_i}-th circuit ({circuit_num_qubits}) does " - f"not match the number of qubits of the {obs_i}-th observable " - f"({observable_num_qubits})." - ) - - if max(circuits) >= len(self.circuits): - raise ValueError( - f"The number of circuits is {len(self.circuits)}, " - f"but the index {max(circuits)} is given." - ) - if max(observables) >= len(self.observables): - raise ValueError( - f"The number of circuits is {len(self.observables)}, " - f"but the index {max(observables)} is given." - ) - run_opts = copy(self.options) - run_opts.update_options(**run_options) - - return self._call( - circuits=circuits, - observables=observables, - parameter_values=parameter_values, - **run_opts.__dict__, - ) - - @abstractmethod - def _call( - self, - circuits: Sequence[int], - observables: Sequence[int], - parameter_values: Sequence[Sequence[float]], - **run_options, - ) -> EstimatorResult: - ... diff --git a/qiskit/primitives/base/base_primitive.py b/qiskit/primitives/base/base_primitive.py index b9daf0132a77..a2f8dacdb828 100644 --- a/qiskit/primitives/base/base_primitive.py +++ b/qiskit/primitives/base/base_primitive.py @@ -31,9 +31,6 @@ def __init__(self, options: dict | None = None): if options is not None: self._run_options.update_options(**options) - ################################################################################ - ## PROPERTIES - ################################################################################ @property def options(self) -> Options: """Return options values for the estimator. @@ -51,9 +48,6 @@ def set_options(self, **fields): """ self._run_options.update_options(**fields) - ################################################################################ - ## VALIDATION - ################################################################################ @staticmethod def _validate_circuits( circuits: Sequence[QuantumCircuit] | QuantumCircuit, diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 28b717e45b1a..769343477ffe 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -76,21 +76,14 @@ from __future__ import annotations from abc import abstractmethod -from collections.abc import Iterable, Sequence +from collections.abc import Sequence from copy import copy -from typing import cast -from warnings import warn -import numpy as np - -from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit import QuantumCircuit from qiskit.circuit.parametertable import ParameterView from qiskit.providers import JobV1 as Job -from qiskit.utils.deprecation import deprecate_arguments, deprecate_function -from ..utils import _circuit_key from .base_primitive import BasePrimitive -from .sampler_result import SamplerResult class BaseSampler(BasePrimitive): @@ -103,50 +96,17 @@ class BaseSampler(BasePrimitive): def __init__( self, - circuits: Iterable[QuantumCircuit] | QuantumCircuit | None = None, - parameters: Iterable[Iterable[Parameter]] | None = None, + *, options: dict | None = None, ): """ Args: - circuits: Quantum circuits to be executed. - parameters: Parameters of each of the quantum circuits. - Defaults to ``[circ.parameters for circ in circuits]``. options: Default options. - - Raises: - ValueError: For mismatch of circuits and parameters list. """ - if circuits is not None or parameters is not None: - warn( - "The BaseSampler 'circuits', and `parameters` kwarg are deprecated " - "as of 0.22.0 and will be removed no earlier than 3 months after the " - "release date. You can use 'run' method to append objects.", - DeprecationWarning, - 2, - ) - if isinstance(circuits, QuantumCircuit): - circuits = (circuits,) - self._circuits = [] if circuits is None else list(circuits) - - # To guarantee that they exist as instance variable. - # With only dynamic set, the python will not know if the attribute exists or not. - self._circuit_ids: dict[tuple, int] = self._circuit_ids - - if parameters is None: - self._parameters = [circ.parameters for circ in self._circuits] - else: - self._parameters = [ParameterView(par) for par in parameters] - if len(self._parameters) != len(self._circuits): - raise ValueError( - f"Different number of parameters ({len(self._parameters)}) " - f"and circuits ({len(self._circuits)})" - ) + self._circuits = [] + self._parameters = [] super().__init__(options) - ################################################################################ - ## METHODS - ################################################################################ def run( self, circuits: QuantumCircuit | Sequence[QuantumCircuit], @@ -187,21 +147,15 @@ def run( **run_opts.__dict__, ) - # This will be comment out after 0.22. (This is necessary for the compatibility.) - # @abstractmethod + @abstractmethod def _run( self, circuits: tuple[QuantumCircuit, ...], parameter_values: tuple[tuple[float, ...], ...], **run_options, ) -> Job: - raise NotImplementedError( - "_run method is not implemented. This method will be @abstractmethod after 0.22." - ) + raise NotImplementedError("The subclass of BaseSampler must implment `_run` method.") - ################################################################################ - ## VALIDATION - ################################################################################ # TODO: validate measurement gates are present @classmethod def _validate_circuits( @@ -218,26 +172,6 @@ def _validate_circuits( ) return circuits - ################################################################################ - ## DEPRECATED - ################################################################################ - def __new__( - cls, - circuits: Iterable[QuantumCircuit] | QuantumCircuit | None = None, - parameters: Iterable[Iterable[Parameter]] | None = None, # pylint: disable=unused-argument - **kwargs, # pylint: disable=unused-argument - ): - - self = super().__new__(cls) - if circuits is None: - self._circuit_ids = {} - elif isinstance(circuits, Iterable): - circuits = copy(circuits) - self._circuit_ids = {_circuit_key(circuit): i for i, circuit in enumerate(circuits)} - else: - self._circuit_ids = {_circuit_key(circuits): 0} - return self - @property def circuits(self) -> tuple[QuantumCircuit, ...]: """Quantum circuits to be sampled. @@ -255,123 +189,3 @@ def parameters(self) -> tuple[ParameterView, ...]: List of the parameters in each quantum circuit. """ return tuple(self._parameters) - - @deprecate_function( - "The BaseSampler.__enter__ method is deprecated as of Qiskit Terra 0.22.0 " - "and will be removed no sooner than 3 months after the releasedate. " - "BaseSampler should be initialized directly.", - since="0.22.0", - ) - def __enter__(self): - return self - - @deprecate_function( - "The BaseSampler.__exit__ method is deprecated as of Qiskit Terra 0.22.0 " - "and will be removed no sooner than 3 months after the releasedate. " - "BaseSampler should be initialized directly.", - since="0.22.0", - ) - def __exit__(self, *exc_info): - self.close() - - def close(self): - """Close the session and free resources""" - ... - - @deprecate_function( - "The BaseSampler.__call__ method is deprecated as of Qiskit Terra 0.22.0 " - "and will be removed no sooner than 3 months after the releasedate. " - "Use run method instead.", - since="0.22.0", - ) - @deprecate_arguments( - {"circuit_indices": "circuits"}, - since="0.21.0", - ) - def __call__( - self, - circuits: Sequence[int | QuantumCircuit], - parameter_values: Sequence[Sequence[float]] | None = None, - **run_options, - ) -> SamplerResult: - """Run the sampling of bitstrings. - - Args: - circuits: the list of circuit indices or circuit objects. - parameter_values: Parameters to be bound to the circuit. - run_options: Backend runtime options used for circuit execution. - - Returns: - The result of the sampler. The i-th result corresponds to - ``self.circuits[circuits[i]]`` evaluated with parameters bound as - ``parameter_values[i]``. - - Raises: - ValueError: For mismatch of object id. - ValueError: For mismatch of length of Sequence. - """ - # Support ndarray - if isinstance(parameter_values, np.ndarray): - parameter_values = parameter_values.tolist() - - # Allow objects - circuits = [ - self._circuit_ids.get(_circuit_key(circuit)) - if not isinstance(circuit, (int, np.integer)) - else circuit - for circuit in circuits - ] - if any(circuit is None for circuit in circuits): - raise ValueError( - "The circuits passed when calling sampler is not one of the circuits used to " - "initialize the session." - ) - - circuits = cast("list[int]", circuits) - - # Allow optional - if parameter_values is None: - for i in circuits: - if len(self._circuits[i].parameters) != 0: - raise ValueError( - f"The {i}-th circuit ({len(circuits)}) is parameterised," - "but parameter values are not given." - ) - parameter_values = [[]] * len(circuits) - - # Validation - if len(circuits) != len(parameter_values): - raise ValueError( - f"The number of circuits ({len(circuits)}) does not match " - f"the number of parameter value sets ({len(parameter_values)})." - ) - - for i, value in zip(circuits, parameter_values): - if len(value) != len(self._parameters[i]): - raise ValueError( - f"The number of values ({len(value)}) does not match " - f"the number of parameters ({len(self._parameters[i])}) for the {i}-th circuit." - ) - - if max(circuits) >= len(self.circuits): - raise ValueError( - f"The number of circuits is {len(self.circuits)}, " - f"but the index {max(circuits)} is given." - ) - run_opts = copy(self.options) - run_opts.update_options(**run_options) - - return self._call( - circuits=circuits, - parameter_values=parameter_values, - **run_opts.__dict__, - ) - - @abstractmethod - def _call( - self, - circuits: Sequence[int], - parameter_values: Sequence[Sequence[float]], - **run_options, - ) -> SamplerResult: - ... diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index 5961781dbd11..ba251ce294d7 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -15,12 +15,12 @@ from __future__ import annotations -from collections.abc import Iterable, Sequence +from collections.abc import Sequence from typing import Any import numpy as np -from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit import QuantumCircuit from qiskit.exceptions import QiskitError from qiskit.opflow import PauliSumOp from qiskit.quantum_info import Statevector @@ -32,7 +32,6 @@ _circuit_key, _observable_key, bound_circuit_to_instruction, - init_circuit, init_observable, ) @@ -53,41 +52,17 @@ class Estimator(BaseEstimator): this option is ignored. """ - def __init__( - self, - circuits: QuantumCircuit | Iterable[QuantumCircuit] | None = None, - observables: BaseOperator | PauliSumOp | Iterable[BaseOperator | PauliSumOp] | None = None, - parameters: Iterable[Iterable[Parameter]] | None = None, - options: dict | None = None, - ): + def __init__(self, *, options: dict | None = None): """ Args: - circuits: circuits that represent quantum states. - observables: observables to be estimated. - parameters: Parameters of each of the quantum circuits. - Defaults to ``[circ.parameters for circ in circuits]``. options: Default options. Raises: QiskitError: if some classical bits are not used for measurements. """ - if isinstance(circuits, QuantumCircuit): - circuits = (circuits,) - if circuits is not None: - circuits = tuple(init_circuit(circuit) for circuit in circuits) - - if isinstance(observables, (PauliSumOp, BaseOperator)): - observables = (observables,) - if observables is not None: - observables = tuple(init_observable(observable) for observable in observables) - - super().__init__( - circuits=circuits, - observables=observables, # type: ignore - parameters=parameters, - options=options, - ) - self._is_closed = False + super().__init__(options=options) + self._circuit_ids = {} + self._observable_ids = {} def _call( self, @@ -96,9 +71,6 @@ def _call( parameter_values: Sequence[Sequence[float]], **run_options, ) -> EstimatorResult: - if self._is_closed: - raise QiskitError("The primitive has been closed.") - shots = run_options.pop("shots", None) seed = run_options.pop("seed", None) if seed is None: @@ -149,9 +121,6 @@ def _call( return EstimatorResult(np.real_if_close(expectation_values), metadata) - def close(self): - self._is_closed = True - def _run( self, circuits: tuple[QuantumCircuit, ...], diff --git a/qiskit/primitives/sampler.py b/qiskit/primitives/sampler.py index ede3747d0111..534f1605afe3 100644 --- a/qiskit/primitives/sampler.py +++ b/qiskit/primitives/sampler.py @@ -15,12 +15,12 @@ from __future__ import annotations -from collections.abc import Iterable, Sequence +from collections.abc import Sequence from typing import Any import numpy as np -from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.circuit import QuantumCircuit from qiskit.exceptions import QiskitError from qiskit.quantum_info import Statevector from qiskit.result import QuasiDistribution @@ -52,35 +52,17 @@ class Sampler(BaseSampler): option is ignored. """ - def __init__( - self, - circuits: QuantumCircuit | Iterable[QuantumCircuit] | None = None, - parameters: Iterable[Iterable[Parameter]] | None = None, - options: dict | None = None, - ): + def __init__(self, *, options: dict | None = None): """ Args: - circuits: circuits to be executed - parameters: Parameters of each of the quantum circuits. - Defaults to ``[circ.parameters for circ in circuits]``. options: Default options. Raises: QiskitError: if some classical bits are not used for measurements. """ - if isinstance(circuits, QuantumCircuit): - circuits = (circuits,) + super().__init__(options=options) self._qargs_list = [] - if circuits is not None: - preprocessed_circuits = [] - for circuit in circuits: - circuit, qargs = self._preprocess_circuit(circuit) - self._qargs_list.append(qargs) - preprocessed_circuits.append(circuit) - else: - preprocessed_circuits = None - super().__init__(preprocessed_circuits, parameters, options) - self._is_closed = False + self._circuit_ids = {} def _call( self, @@ -88,9 +70,6 @@ def _call( parameter_values: Sequence[Sequence[float]], **run_options, ) -> SamplerResult: - if self._is_closed: - raise QiskitError("The primitive has been closed.") - shots = run_options.pop("shots", None) seed = run_options.pop("seed", None) if seed is None: @@ -135,9 +114,6 @@ def _call( return SamplerResult(quasis, metadata) - def close(self): - self._is_closed = True - def _run( self, circuits: tuple[QuantumCircuit, ...], diff --git a/releasenotes/notes/delete-args-and-methods-in-primitives-d89d444ec0217ae6.yaml b/releasenotes/notes/delete-args-and-methods-in-primitives-d89d444ec0217ae6.yaml new file mode 100644 index 000000000000..b904c6bf0d3d --- /dev/null +++ b/releasenotes/notes/delete-args-and-methods-in-primitives-d89d444ec0217ae6.yaml @@ -0,0 +1,6 @@ +--- + upgrade: + - | + Removed the usage of primitives with the context manager and the initialization with circuits, + (observables only for Estimator), and parameters + which was deprecated in the Qiskit Terra 0.22.0 release in October 2022. diff --git a/test/python/algorithms/gradients/logging_primitives.py b/test/python/algorithms/gradients/logging_primitives.py index efd062f4b92e..ef2cad9e2cc0 100644 --- a/test/python/algorithms/gradients/logging_primitives.py +++ b/test/python/algorithms/gradients/logging_primitives.py @@ -18,15 +18,8 @@ class LoggingEstimator(Estimator): """An estimator checking what operations were in the circuits it executed.""" - def __init__( - self, - circuits=None, - observables=None, - parameters=None, - options=None, - operations_callback=None, - ): - super().__init__(circuits, observables, parameters, options) + def __init__(self, options=None, operations_callback=None): + super().__init__(options=options) self.operations_callback = operations_callback def _run(self, circuits, observables, parameter_values, **run_options): diff --git a/test/python/primitives/test_estimator.py b/test/python/primitives/test_estimator.py index a7dac4cd23d7..4a9209e7fa8a 100644 --- a/test/python/primitives/test_estimator.py +++ b/test/python/primitives/test_estimator.py @@ -24,7 +24,7 @@ from qiskit.primitives import BaseEstimator, Estimator, EstimatorResult from qiskit.primitives.utils import _observable_key from qiskit.providers import JobV1 -from qiskit.quantum_info import Operator, SparsePauliOp, Pauli, PauliList +from qiskit.quantum_info import Operator, Pauli, PauliList, SparsePauliOp from qiskit.test import QiskitTestCase @@ -58,358 +58,6 @@ def setUp(self): [1, 2, 3, 4, 5, 6], ) - def test_estimator(self): - """test for a simple use case""" - lst = [("XX", 1), ("YY", 2), ("ZZ", 3)] - with self.subTest("PauliSumOp"): - observable = PauliSumOp.from_list(lst) - ansatz = RealAmplitudes(num_qubits=2, reps=2) - with self.assertWarns(DeprecationWarning): - est = Estimator([ansatz], [observable]) - result = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1.84209213]) - - with self.subTest("SparsePauliOp"): - observable = SparsePauliOp.from_list(lst) - ansatz = RealAmplitudes(num_qubits=2, reps=2) - with self.assertWarns(DeprecationWarning): - est = Estimator([ansatz], [observable]) - result = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1.84209213]) - - def test_estimator_param_reverse(self): - """test for the reverse parameter""" - observable = PauliSumOp.from_list([("XX", 1), ("YY", 2), ("ZZ", 3)]) - ansatz = RealAmplitudes(num_qubits=2, reps=2) - with self.assertWarns(DeprecationWarning): - est = Estimator([ansatz], [observable], [ansatz.parameters[::-1]]) - result = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5][::-1]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1.84209213]) - - def test_init_observable_from_operator(self): - """test for evaluate without parameters""" - circuit = self.ansatz.bind_parameters([0, 1, 1, 2, 3, 5]) - matrix = Operator( - [ - [-1.06365335, 0.0, 0.0, 0.1809312], - [0.0, -1.83696799, 0.1809312, 0.0], - [0.0, 0.1809312, -0.24521829, 0.0], - [0.1809312, 0.0, 0.0, -1.06365335], - ] - ) - with self.assertWarns(DeprecationWarning): - est = Estimator([circuit], [matrix]) - result = est([0], [0]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-1.284366511861733]) - - def test_evaluate(self): - """test for evaluate""" - with self.assertWarns(DeprecationWarning): - est = Estimator([self.ansatz], [self.observable]) - result = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-1.284366511861733]) - - def test_evaluate_multi_params(self): - """test for evaluate with multiple parameters""" - with self.assertWarns(DeprecationWarning): - est = Estimator([self.ansatz], [self.observable]) - result = est( - [0] * 2, [0] * 2, parameter_values=[[0, 1, 1, 2, 3, 5], [1, 1, 2, 3, 5, 8]] - ) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-1.284366511861733, -1.3187526349078742]) - - def test_evaluate_no_params(self): - """test for evaluate without parameters""" - circuit = self.ansatz.bind_parameters([0, 1, 1, 2, 3, 5]) - with self.assertWarns(DeprecationWarning): - est = Estimator([circuit], [self.observable]) - result = est([0], [0]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-1.284366511861733]) - - def test_run_with_multiple_observables_and_none_parameters(self): - """test for evaluate without parameters""" - circuit = QuantumCircuit(3) - circuit.h(0) - circuit.cx(0, 1) - circuit.cx(1, 2) - with self.assertWarns(DeprecationWarning): - est = Estimator(circuit, ["ZZZ", "III"]) - result = est(circuits=[0, 0], observables=[0, 1]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [0.0, 1.0]) - - def test_estimator_example(self): - """test for Estimator example""" - psi1 = RealAmplitudes(num_qubits=2, reps=2) - psi2 = RealAmplitudes(num_qubits=2, reps=3) - - params1 = psi1.parameters - params2 = psi2.parameters - - op1 = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)]) - op2 = SparsePauliOp.from_list([("IZ", 1)]) - op3 = SparsePauliOp.from_list([("ZI", 1), ("ZZ", 1)]) - - with self.assertWarns(DeprecationWarning): - est = Estimator([psi1, psi2], [op1, op2, op3], [params1, params2]) - theta1 = [0, 1, 1, 2, 3, 5] - theta2 = [0, 1, 1, 2, 3, 5, 8, 13] - theta3 = [1, 2, 3, 4, 5, 6] - - # calculate [ ] - with self.assertWarns(DeprecationWarning): - result = est([0], [0], [theta1]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1.5555572817900956]) - self.assertEqual(len(result.metadata), 1) - - # calculate [ , ] - with self.assertWarns(DeprecationWarning): - result = est([0, 0], [1, 2], [theta1] * 2) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-0.5516530027638437, 0.07535238795415422]) - self.assertEqual(len(result.metadata), 2) - - # calculate [ ] - with self.assertWarns(DeprecationWarning): - result = est([1], [1], [theta2]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [0.17849238433885167]) - self.assertEqual(len(result.metadata), 1) - - # calculate [ , ] - with self.assertWarns(DeprecationWarning): - result = est([0, 0], [0, 0], [theta1, theta3]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1.5555572817900956, 1.0656325933346835]) - self.assertEqual(len(result.metadata), 2) - - # calculate [ , - # , - # ] - with self.assertWarns(DeprecationWarning): - result = est([0, 1, 0], [0, 1, 2], [theta1, theta2, theta3]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose( - result.values, [1.5555572817900956, 0.17849238433885167, -1.0876631752254926] - ) - self.assertEqual(len(result.metadata), 3) - - # It is possible to pass objects. - # calculate [ ] - with self.assertWarns(DeprecationWarning): - result = est([psi2], [op2], [theta2]) - np.testing.assert_allclose(result.values, [0.17849238433885167]) - self.assertEqual(len(result.metadata), 1) - - def test_1qubit(self): - """Test for 1-qubit cases""" - qc = QuantumCircuit(1) - qc2 = QuantumCircuit(1) - qc2.x(0) - - op = SparsePauliOp.from_list([("I", 1)]) - op2 = SparsePauliOp.from_list([("Z", 1)]) - - with self.assertWarns(DeprecationWarning): - est = Estimator([qc, qc2], [op, op2], [[]] * 2) - result = est([0], [0], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) - - with self.assertWarns(DeprecationWarning): - result = est([0], [1], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) - - with self.assertWarns(DeprecationWarning): - result = est([1], [0], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) - - with self.assertWarns(DeprecationWarning): - result = est([1], [1], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-1]) - - def test_2qubits(self): - """Test for 2-qubit cases (to check endian)""" - qc = QuantumCircuit(2) - qc2 = QuantumCircuit(2) - qc2.x(0) - - op = SparsePauliOp.from_list([("II", 1)]) - op2 = SparsePauliOp.from_list([("ZI", 1)]) - op3 = SparsePauliOp.from_list([("IZ", 1)]) - - with self.assertWarns(DeprecationWarning): - est = Estimator([qc, qc2], [op, op2, op3], [[]] * 2) - result = est([0], [0], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) - - with self.assertWarns(DeprecationWarning): - result = est([1], [0], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) - - with self.assertWarns(DeprecationWarning): - result = est([0], [1], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) - - with self.assertWarns(DeprecationWarning): - result = est([1], [1], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) - - with self.assertWarns(DeprecationWarning): - result = est([0], [2], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [1]) - - with self.assertWarns(DeprecationWarning): - result = est([1], [2], [[]]) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-1]) - - def test_errors(self): - """Test for errors""" - qc = QuantumCircuit(1) - qc2 = QuantumCircuit(2) - - op = SparsePauliOp.from_list([("I", 1)]) - op2 = SparsePauliOp.from_list([("II", 1)]) - - with self.assertWarns(DeprecationWarning): - est = Estimator([qc, qc2], [op, op2], [[]] * 2) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - est([0], [1], [[]]) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - est([1], [0], [[]]) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - est([0], [0], [[1e4]]) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - est([1], [1], [[1, 2]]) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - est([0, 1], [1], [[1]]) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - est([0], [0, 1], [[1]]) - - def test_empty_parameter(self): - """Test for empty parameter""" - n = 2 - qc = QuantumCircuit(n) - op = SparsePauliOp.from_list([("I" * n, 1)]) - with self.assertWarns(DeprecationWarning): - estimator = Estimator(circuits=[qc] * 10, observables=[op] * 10) - with self.subTest("one circuit"): - with self.assertWarns(DeprecationWarning): - result = estimator([0], [1], shots=1000) - np.testing.assert_allclose(result.values, [1]) - self.assertEqual(len(result.metadata), 1) - - with self.subTest("two circuits"): - with self.assertWarns(DeprecationWarning): - result = estimator([2, 4], [3, 5], shots=1000) - np.testing.assert_allclose(result.values, [1, 1]) - self.assertEqual(len(result.metadata), 2) - - def test_numpy_params(self): - """Test for numpy array as parameter values""" - qc = RealAmplitudes(num_qubits=2, reps=2) - op = SparsePauliOp.from_list([("IZ", 1), ("XI", 2), ("ZY", -1)]) - k = 5 - params_array = np.random.rand(k, qc.num_parameters) - params_list = params_array.tolist() - params_list_array = list(params_array) - with self.assertWarns(DeprecationWarning): - estimator = Estimator(circuits=qc, observables=op) - target = estimator([0] * k, [0] * k, params_list) - - with self.subTest("ndarrary"): - with self.assertWarns(DeprecationWarning): - result = estimator([0] * k, [0] * k, params_array) - self.assertEqual(len(result.metadata), k) - np.testing.assert_allclose(result.values, target.values) - - with self.subTest("list of ndarray"): - with self.assertWarns(DeprecationWarning): - result = estimator([0] * k, [0] * k, params_list_array) - self.assertEqual(len(result.metadata), k) - np.testing.assert_allclose(result.values, target.values) - - def test_passing_objects(self): - """Test passsing object for Estimator.""" - - with self.subTest("Valid test"): - with self.assertWarns(DeprecationWarning): - estimator = Estimator([self.ansatz], [self.observable]) - result = estimator( - circuits=[self.ansatz, self.ansatz], - observables=[self.observable, self.observable], - parameter_values=[list(range(6)), [0, 1, 1, 2, 3, 5]], - ) - self.assertAlmostEqual(result.values[0], self.expvals[0]) - self.assertAlmostEqual(result.values[1], self.expvals[1]) - - with self.subTest("Invalid circuit test"): - circuit = QuantumCircuit(2) - with self.assertWarns(DeprecationWarning): - estimator = Estimator([self.ansatz], [self.observable]) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - result = estimator( - circuits=[self.ansatz, circuit], - observables=[self.observable, self.observable], - parameter_values=[list(range(6)), [0, 1, 1, 2, 3, 5]], - ) - - with self.subTest("Invalid observable test"): - observable = SparsePauliOp(["ZX"]) - with self.assertWarns(DeprecationWarning): - estimator = Estimator([self.ansatz], [self.observable]) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - result = estimator( - circuits=[self.ansatz, self.ansatz], - observables=[observable, self.observable], - parameter_values=[list(range(6)), [0, 1, 1, 2, 3, 5]], - ) - - def test_deprecated_arguments(self): - """test for deprecated arguments""" - with self.assertWarns(DeprecationWarning): - est = Estimator([self.ansatz], [self.observable]) - result = est( - circuit_indices=[0], - observable_indices=[0], - parameter_values=[[0, 1, 1, 2, 3, 5]], - ) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-1.284366511861733]) - - def test_with_shots_option(self): - """test with shots option.""" - with self.assertWarns(DeprecationWarning): - est = Estimator([self.ansatz], [self.observable]) - result = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=1024, seed=15) - self.assertIsInstance(result, EstimatorResult) - np.testing.assert_allclose(result.values, [-1.307397243478641]) - - def test_with_shots_option_none(self): - """test with shots=None option. Seed is ignored then.""" - with self.assertWarns(DeprecationWarning): - est = Estimator([self.ansatz], [self.observable]) - result_42 = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=None, seed=42) - result_15 = est([0], [0], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=None, seed=15) - np.testing.assert_allclose(result_42.values, result_15.values) - def test_estimator_run(self): """Test Estimator.run()""" psi1, psi2 = self.psi @@ -609,6 +257,22 @@ def test_run_numpy_params(self): self.assertEqual(len(result.metadata), k) np.testing.assert_allclose(result.values, target.values) + def test_run_with_operator(self): + """test for run with Operator as an observable""" + circuit = self.ansatz.bind_parameters([0, 1, 1, 2, 3, 5]) + matrix = Operator( + [ + [-1.06365335, 0.0, 0.0, 0.1809312], + [0.0, -1.83696799, 0.1809312, 0.0], + [0.0, 0.1809312, -0.24521829, 0.0], + [0.1809312, 0.0, 0.0, -1.06365335], + ] + ) + est = Estimator() + result = est.run([circuit], [matrix]).result() + self.assertIsInstance(result, EstimatorResult) + np.testing.assert_allclose(result.values, [-1.284366511861733]) + def test_run_with_shots_option(self): """test with shots option.""" est = Estimator() @@ -622,6 +286,25 @@ def test_run_with_shots_option(self): self.assertIsInstance(result, EstimatorResult) np.testing.assert_allclose(result.values, [-1.307397243478641]) + def test_run_with_shots_option_none(self): + """test with shots=None option. Seed is ignored then.""" + est = Estimator() + result_42 = est.run( + [self.ansatz], + [self.observable], + parameter_values=[[0, 1, 1, 2, 3, 5]], + shots=None, + seed=42, + ).result() + result_15 = est.run( + [self.ansatz], + [self.observable], + parameter_values=[[0, 1, 1, 2, 3, 5]], + shots=None, + seed=15, + ).result() + np.testing.assert_allclose(result_42.values, result_15.values) + def test_options(self): """Test for options""" with self.subTest("init"): diff --git a/test/python/primitives/test_sampler.py b/test/python/primitives/test_sampler.py index 47dd120d8c76..0e04b06aa0db 100644 --- a/test/python/primitives/test_sampler.py +++ b/test/python/primitives/test_sampler.py @@ -13,10 +13,8 @@ """Tests for Sampler.""" import unittest -from test import combine import numpy as np -from ddt import ddt from qiskit import QuantumCircuit from qiskit.circuit import Parameter @@ -28,7 +26,6 @@ from qiskit.test import QiskitTestCase -@ddt class TestSampler(QiskitTestCase): """Test Sampler""" @@ -90,358 +87,6 @@ def _compare_probs(self, prob, target): else: self.assertAlmostEqual(t_val, 0, places=1) - @combine(indices=[[0], [1], [0, 1]]) - def test_sampler(self, indices): - """test for sampler""" - circuits, target = self._generate_circuits_target(indices) - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=circuits) - result = sampler(list(range(len(indices))), parameter_values=[[] for _ in indices]) - self._compare_probs(result.quasi_dists, target) - - @combine(indices=[[0], [1], [0, 1]]) - def test_sampler_pqc(self, indices): - """test for sampler with a parametrized circuit""" - params, target = self._generate_params_target(indices) - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=self._pqc) - result = sampler([0] * len(params), params) - self._compare_probs(result.quasi_dists, target) - - @combine(indices=[[0, 0], [0, 1], [1, 1]]) - def test_evaluate_two_pqcs(self, indices): - """test for sampler with two parametrized circuits""" - circs = [self._pqc, self._pqc] - params, target = self._generate_params_target(indices) - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=circs) - result = sampler(indices, parameter_values=params) - self._compare_probs(result.quasi_dists, target) - - def test_sampler_example(self): - """test for Sampler example""" - - bell = QuantumCircuit(2) - bell.h(0) - bell.cx(0, 1) - bell.measure_all() - - # executes a Bell circuit - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=[bell], parameters=[[]]) - result = sampler(parameter_values=[[]], circuits=[0]) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 1) - keys, values = zip(*sorted(result.quasi_dists[0].items())) - self.assertTupleEqual(keys, (0, 3)) - np.testing.assert_allclose(values, [0.5, 0.5]) - self.assertEqual(len(result.metadata), 1) - - # executes three Bell circuits - with self.assertWarns(DeprecationWarning): - sampler = Sampler([bell] * 3, [[]] * 3) - result = sampler([0, 1, 2], [[]] * 3) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 3) - self.assertEqual(len(result.metadata), 3) - for dist in result.quasi_dists: - keys, values = zip(*sorted(dist.items())) - self.assertTupleEqual(keys, (0, 3)) - np.testing.assert_allclose(values, [0.5, 0.5]) - - with self.assertWarns(DeprecationWarning): - sampler = Sampler([bell]) - result = sampler([bell, bell, bell]) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 3) - self.assertEqual(len(result.metadata), 3) - for dist in result.quasi_dists: - keys, values = zip(*sorted(dist.items())) - self.assertTupleEqual(keys, (0, 3)) - np.testing.assert_allclose(values, [0.5, 0.5]) - - # parametrized circuit - pqc = RealAmplitudes(num_qubits=2, reps=2) - pqc.measure_all() - pqc2 = RealAmplitudes(num_qubits=2, reps=3) - pqc2.measure_all() - - theta1 = [0, 1, 1, 2, 3, 5] - theta2 = [1, 2, 3, 4, 5, 6] - theta3 = [0, 1, 2, 3, 4, 5, 6, 7] - - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=[pqc, pqc2], parameters=[pqc.parameters, pqc2.parameters]) - result = sampler([0, 0, 1], [theta1, theta2, theta3]) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 3) - self.assertEqual(len(result.metadata), 3) - - keys, values = zip(*sorted(result.quasi_dists[0].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose( - values, - [0.13092484629757767, 0.3608720796028449, 0.09324865232050054, 0.414954421779077], - ) - - keys, values = zip(*sorted(result.quasi_dists[1].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose( - values, - [0.06282290651933871, 0.02877144385576703, 0.606654494132085, 0.3017511554928095], - ) - - keys, values = zip(*sorted(result.quasi_dists[2].items())) - self.assertTupleEqual(keys, tuple(range(4))) - np.testing.assert_allclose( - values, - [ - 0.18802639943804164, - 0.6881971261189544, - 0.09326232720582446, - 0.030514147237179882, - ], - ) - - def test_sampler_param_order(self): - """test for sampler with different parameter orders""" - x = Parameter("x") - y = Parameter("y") - - qc = QuantumCircuit(3, 3) - qc.rx(x, 0) - qc.rx(y, 1) - qc.x(2) - qc.measure(0, 0) - qc.measure(1, 1) - qc.measure(2, 2) - - with self.assertWarns(DeprecationWarning): - sampler = Sampler([qc, qc], [[x, y], [y, x]]) - result = sampler([0, 1, 0, 1], [[0, 0], [0, 0], [np.pi / 2, 0], [np.pi / 2, 0]]) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 4) - - # qc({x: 0, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[0].items())) - self.assertTupleEqual(keys, (4,)) - np.testing.assert_allclose(values, [1]) - - # qc({x: 0, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[1].items())) - self.assertTupleEqual(keys, (4,)) - np.testing.assert_allclose(values, [1]) - - # qc({x: pi/2, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[2].items())) - self.assertTupleEqual(keys, (4, 5)) - np.testing.assert_allclose(values, [0.5, 0.5]) - - # qc({x: 0, y: pi/2}) - keys, values = zip(*sorted(result.quasi_dists[3].items())) - self.assertTupleEqual(keys, (4, 6)) - np.testing.assert_allclose(values, [0.5, 0.5]) - - def test_sampler_reverse_meas_order(self): - """test for sampler with reverse measurement order""" - x = Parameter("x") - y = Parameter("y") - - qc = QuantumCircuit(3, 3) - qc.rx(x, 0) - qc.rx(y, 1) - qc.x(2) - qc.measure(0, 2) - qc.measure(1, 1) - qc.measure(2, 0) - - with self.assertWarns(DeprecationWarning): - sampler = Sampler([qc, qc], [[x, y], [y, x]]) - result = sampler([0, 1, 0, 1], [[0, 0], [0, 0], [np.pi / 2, 0], [np.pi / 2, 0]]) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 4) - - # qc({x: 0, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[0].items())) - self.assertTupleEqual(keys, (1,)) - np.testing.assert_allclose(values, [1]) - - # qc({x: 0, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[1].items())) - self.assertTupleEqual(keys, (1,)) - np.testing.assert_allclose(values, [1]) - - # qc({x: pi/2, y: 0}) - keys, values = zip(*sorted(result.quasi_dists[2].items())) - self.assertTupleEqual(keys, (1, 5)) - np.testing.assert_allclose(values, [0.5, 0.5]) - - # qc({x: 0, y: pi/2}) - keys, values = zip(*sorted(result.quasi_dists[3].items())) - self.assertTupleEqual(keys, (1, 3)) - np.testing.assert_allclose(values, [0.5, 0.5]) - - def test_1qubit(self): - """test for 1-qubit cases""" - qc = QuantumCircuit(1) - qc.measure_all() - qc2 = QuantumCircuit(1) - qc2.x(0) - qc2.measure_all() - - with self.assertWarns(DeprecationWarning): - sampler = Sampler([qc, qc2], [qc.parameters, qc2.parameters]) - result = sampler([0, 1], [[]] * 2) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 2) - - for i in range(2): - keys, values = zip(*sorted(result.quasi_dists[i].items())) - self.assertTupleEqual(keys, (i,)) - np.testing.assert_allclose(values, [1]) - - def test_2qubit(self): - """test for 2-qubit cases""" - qc0 = QuantumCircuit(2) - qc0.measure_all() - qc1 = QuantumCircuit(2) - qc1.x(0) - qc1.measure_all() - qc2 = QuantumCircuit(2) - qc2.x(1) - qc2.measure_all() - qc3 = QuantumCircuit(2) - qc3.x([0, 1]) - qc3.measure_all() - - with self.assertWarns(DeprecationWarning): - sampler = Sampler( - [qc0, qc1, qc2, qc3], - [qc0.parameters, qc1.parameters, qc2.parameters, qc3.parameters], - ) - result = sampler([0, 1, 2, 3], [[]] * 4) - self.assertIsInstance(result, SamplerResult) - self.assertEqual(len(result.quasi_dists), 4) - - for i in range(4): - keys, values = zip(*sorted(result.quasi_dists[i].items())) - self.assertTupleEqual(keys, (i,)) - np.testing.assert_allclose(values, [1]) - - def test_errors(self): - """Test for errors""" - qc1 = QuantumCircuit(1) - qc1.measure_all() - qc2 = RealAmplitudes(num_qubits=1, reps=1) - qc2.measure_all() - - with self.assertWarns(DeprecationWarning): - sampler = Sampler([qc1, qc2], [qc1.parameters, qc2.parameters]) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - sampler([0], [[1e2]]) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - sampler([1], [[]]) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - sampler([1], [[1e2]]) - - def test_empty_parameter(self): - """Test for empty parameter""" - n = 5 - qc = QuantumCircuit(n, n - 1) - qc.measure(range(n - 1), range(n - 1)) - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=[qc] * 10) - with self.subTest("one circuit"): - with self.assertWarns(DeprecationWarning): - result = sampler([0], shots=1000) - self.assertEqual(len(result.quasi_dists), 1) - for q_d in result.quasi_dists: - quasi_dist = {k: v for k, v in q_d.items() if v != 0.0} - self.assertDictEqual(quasi_dist, {0: 1.0}) - self.assertEqual(len(result.metadata), 1) - - with self.subTest("two circuits"): - with self.assertWarns(DeprecationWarning): - result = sampler([2, 4], shots=1000) - self.assertEqual(len(result.quasi_dists), 2) - for q_d in result.quasi_dists: - quasi_dist = {k: v for k, v in q_d.items() if v != 0.0} - self.assertDictEqual(quasi_dist, {0: 1.0}) - self.assertEqual(len(result.metadata), 2) - - def test_numpy_params(self): - """Test for numpy array as parameter values""" - qc = RealAmplitudes(num_qubits=2, reps=2) - qc.measure_all() - k = 5 - params_array = np.random.rand(k, qc.num_parameters) - params_list = params_array.tolist() - params_list_array = list(params_array) - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=qc) - target = sampler([0] * k, params_list) - - with self.subTest("ndarrary"): - with self.assertWarns(DeprecationWarning): - result = sampler([0] * k, params_array) - self.assertEqual(len(result.metadata), k) - for i in range(k): - self.assertDictEqual(result.quasi_dists[i], target.quasi_dists[i]) - - with self.subTest("list of ndarray"): - with self.assertWarns(DeprecationWarning): - result = sampler([0] * k, params_list_array) - self.assertEqual(len(result.metadata), k) - for i in range(k): - self.assertDictEqual(result.quasi_dists[i], target.quasi_dists[i]) - - def test_passing_objects(self): - """Test passing objects for Sampler.""" - - params, target = self._generate_params_target([0]) - - with self.subTest("Valid test"): - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=self._pqc) - result = sampler(circuits=[self._pqc], parameter_values=params) - self._compare_probs(result.quasi_dists, target) - - with self.subTest("Invalid circuit test"): - circuit = QuantumCircuit(2) - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=self._pqc) - with self.assertRaises(ValueError), self.assertWarns(DeprecationWarning): - result = sampler(circuits=[circuit], parameter_values=params) - - @combine(indices=[[0], [1], [0, 1]]) - def test_deprecated_circuit_indices(self, indices): - """Test for deprecated arguments""" - circuits, target = self._generate_circuits_target(indices) - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=circuits) - result = sampler( - circuit_indices=list(range(len(indices))), - parameter_values=[[] for _ in indices], - ) - self._compare_probs(result.quasi_dists, target) - - def test_with_shots_option(self): - """test with shots option.""" - params, target = self._generate_params_target([1]) - with self.assertWarns(DeprecationWarning): - sampler = Sampler(circuits=self._pqc) - result = sampler(circuits=[0], parameter_values=params, shots=1024, seed=15) - self._compare_probs(result.quasi_dists, target) - self.assertEqual(result.quasi_dists[0].shots, 1024) - - def test_with_shots_option_none(self): - """test with shots=None option. Seed is ignored then.""" - with self.assertWarns(DeprecationWarning): - sampler = Sampler([self._pqc]) - result_42 = sampler([0], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=None, seed=42) - result_15 = sampler([0], parameter_values=[[0, 1, 1, 2, 3, 5]], shots=None, seed=15) - self.assertDictAlmostEqual(result_42.quasi_dists, result_15.quasi_dists) - def test_sampler_run(self): """Test Sampler.run().""" bell = self._circuit[1] @@ -595,6 +240,34 @@ def test_run_single_circuit(self): self._compare_probs(result.quasi_dists, target) self.assertEqual(len(result.metadata), 1) + def test_run_reverse_meas_order(self): + """test for sampler with reverse measurement order""" + x = Parameter("x") + y = Parameter("y") + + qc = QuantumCircuit(3, 3) + qc.rx(x, 0) + qc.rx(y, 1) + qc.x(2) + qc.measure(0, 2) + qc.measure(1, 1) + qc.measure(2, 0) + + sampler = Sampler() + result = sampler.run([qc] * 2, [[0, 0], [np.pi / 2, 0]]).result() + self.assertIsInstance(result, SamplerResult) + self.assertEqual(len(result.quasi_dists), 2) + + # qc({x: 0, y: 0}) + keys, values = zip(*sorted(result.quasi_dists[0].items())) + self.assertTupleEqual(keys, (1,)) + np.testing.assert_allclose(values, [1]) + + # qc({x: pi/2, y: 0}) + keys, values = zip(*sorted(result.quasi_dists[1].items())) + self.assertTupleEqual(keys, (1, 5)) + np.testing.assert_allclose(values, [0.5, 0.5]) + def test_run_errors(self): """Test for errors with run method""" qc1 = QuantumCircuit(1) From 73602b06d281f339f660633d91ea9d475b87a445 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 12 Apr 2023 17:00:54 +0100 Subject: [PATCH 012/172] Add Rust-based OpenQASM 2 converter (#9784) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Rust-based OpenQASM 2 converter This is a vendored version of qiskit-qasm2 (https://pypi.org/project/qiskit-qasm2), with this initial commit being equivalent (barring some naming / documentation / testing conversions to match Qiskit's style) to version 0.5.3 of that package. This adds a new translation layer from OpenQASM 2 to Qiskit, which is around an order of magnitude faster than the existing version in Python, while being more type safe (in terms of disallowing invalid OpenQASM 2 programs rather than attempting to construction `QuantumCircuit`s that are not correct) and more extensible. The core logic is a hand-written lexer and parser combination written in Rust, which emits a bytecode stream across the PyO3 boundary to a small Python interpreter loop. The main bulk of the parsing logic is a simple LL(1) recursive-descent algorithm, which delegates to more specific recursive Pratt-based algorithm for handling classical expressions. Many of the design decisions made (including why the lexer is written by hand) are because the project originally started life as a way for me to learn about implementations of the different parts of a parser stack; this is the principal reason there are very few external crates used. There are a few inefficiencies in this implementation, for example: - the string interner in the lexer allocates twice for each stored string (but zero times for a lookup). It may be possible to completely eliminate allocations when parsing a string (or a file if it's read into memory as a whole), but realistically there's only a fairly small number of different tokens seen in most OpenQASM 2 programs, so it shouldn't be too big a deal. - the hand-off from Rust to Python transfers small objects frequently. It might be more efficient to have a secondary buffered iterator in Python space, transferring more bytecode instructions at a time and letting Python resolve them. This form could also be made asynchronous, since for the most part, the Rust components only need to acquire the CPython GIL at the API boundary. - there are too many points within the lexer that can return a failure result that needs unwrapping at every site. Since there are no tokens that can span multiple lines, it should be possible to refactor so that almost all of the byte-getter and -peeker routines cannot return error statuses, at the cost of the main lexer loop becoming responsible for advancing the line buffer, and moving the non-ASCII error handling into each token constructor. I'll probably keep playing with some of those in the `qiskit-qasm2` package itself when I have free time, but at some point I needed to draw the line and vendor the package. It's still ~10x faster than the existing one: In [1]: import qiskit.qasm2 ...: prog = """ ...: OPENQASM 2.0; ...: include "qelib1.inc"; ...: qreg q[2]; ...: """ ...: prog += "rz(pi * 2) q[0];\ncx q[0], q[1];\n"*100_000 ...: %timeit qiskit.qasm2.loads(prog) ...: %timeit qiskit.QuantumCircuit.from_qasm_str(prog) 2.26 s ± 39.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) 22.5 s ± 106 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) `cx`-heavy programs like this one are actually the ones that the new parser is (comparatively) slowest on, because the construction time of `CXGate` is higher than most gates, and this dominates the execution time for the Rust-based parser. * Work around docs failure on Sphinx 5.3, Python 3.9 The version of Sphinx that we're constrained to use in the docs build can't handle the `Unpack` operator, so as a temporary measure we can just relax the type hint a little. * Remove unused import * Tweak documentation * More specific PyO3 usage * Use PathBuf directly for paths * Format * Freeze dataclass * Use type-safe id types This should have no impact on runtime or on memory usage, since each of the new types has the same bit width and alignment as the `usize` values they replace. * Documentation tweaks * Fix comments in lexer * Fix lexing version number with separating comments * Add test of pathological formatting * Fixup release note * Fix handling of u0 gate * Credit reviewers Co-authored-by: Luciano Bello Co-authored-by: Kevin Hartman Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> * Add test of invalid gate-body statements * Refactor custom built-in gate definitions The previous system was quite confusing, and required all accesses to the global symbol table to know that the `Gate` symbol could be present but overridable. This led to confusing logic, various bugs and unnecessary constraints, such as it previously being (erroneously) possible to provide re-definitions for any "built-in" gate. Instead, we keep a separate store of instructions that may be redefined. This allows the logic to be centralised to only to the place responsible for performing those overrides, and remains accessible for error-message builders to query in order to provide better diagnostics. * Credit Sasha Co-authored-by: Alexander Ivrii * Credit Matthew Co-authored-by: Matthew Treinish * Remove dependency on `lazy_static` For a hashset of only 6 elements that is only checked once, there's not really any point to pull in an extra dependency or use a hash set at all. * Update PyO3 version --------- Co-authored-by: Luciano Bello Co-authored-by: Kevin Hartman Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Co-authored-by: Alexander Ivrii Co-authored-by: Matthew Treinish --- Cargo.lock | 12 +- crates/qasm2/Cargo.toml | 16 + crates/qasm2/README.md | 6 + crates/qasm2/src/bytecode.rs | 353 ++++ crates/qasm2/src/error.rs | 84 + crates/qasm2/src/expr.rs | 704 +++++++ crates/qasm2/src/lex.rs | 745 +++++++ crates/qasm2/src/lib.rs | 132 ++ crates/qasm2/src/parse.rs | 1801 +++++++++++++++++ docs/apidocs/qasm2.rst | 6 + docs/apidocs/terra.rst | 1 + pyproject.toml | 10 +- qiskit/__init__.py | 8 +- qiskit/qasm2/__init__.py | 525 +++++ qiskit/qasm2/exceptions.py | 27 + qiskit/qasm2/parse.py | 405 ++++ .../qasm2-parser-rust-ecf6570e2d445a94.yaml | 23 + setup.py | 14 +- test/python/qasm2/__init__.py | 38 + test/python/qasm2/test_arxiv_examples.py | 450 ++++ test/python/qasm2/test_expression.py | 259 +++ test/python/qasm2/test_lexer.py | 171 ++ test/python/qasm2/test_parse_errors.py | 862 ++++++++ test/python/qasm2/test_structure.py | 1711 ++++++++++++++++ 24 files changed, 8352 insertions(+), 11 deletions(-) create mode 100644 crates/qasm2/Cargo.toml create mode 100644 crates/qasm2/README.md create mode 100644 crates/qasm2/src/bytecode.rs create mode 100644 crates/qasm2/src/error.rs create mode 100644 crates/qasm2/src/expr.rs create mode 100644 crates/qasm2/src/lex.rs create mode 100644 crates/qasm2/src/lib.rs create mode 100644 crates/qasm2/src/parse.rs create mode 100644 docs/apidocs/qasm2.rst create mode 100644 qiskit/qasm2/__init__.py create mode 100644 qiskit/qasm2/exceptions.py create mode 100644 qiskit/qasm2/parse.py create mode 100644 releasenotes/notes/qasm2-parser-rust-ecf6570e2d445a94.yaml create mode 100644 test/python/qasm2/__init__.py create mode 100644 test/python/qasm2/test_arxiv_examples.py create mode 100644 test/python/qasm2/test_expression.py create mode 100644 test/python/qasm2/test_lexer.py create mode 100644 test/python/qasm2/test_parse_errors.py create mode 100644 test/python/qasm2/test_structure.py diff --git a/Cargo.lock b/Cargo.lock index 78d7df5d5261..8d0de02b1f39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,9 +163,9 @@ checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" [[package]] name = "libc" -version = "0.2.139" +version = "0.2.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "201de327520df007757c1f0adce6e827fe8562fbc28bfd9c15571c66ca1f5f79" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" [[package]] name = "libm" @@ -408,6 +408,14 @@ dependencies = [ "syn", ] +[[package]] +name = "qiskit-qasm2" +version = "0.24.0" +dependencies = [ + "hashbrown 0.13.2", + "pyo3", +] + [[package]] name = "qiskit_accelerate" version = "0.24.0" diff --git a/crates/qasm2/Cargo.toml b/crates/qasm2/Cargo.toml new file mode 100644 index 000000000000..1d7215f477ac --- /dev/null +++ b/crates/qasm2/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "qiskit-qasm2" +# The following options can be inherited with (e.g.) `version.workspace = true` once we hit Rust +# 1.64. Until then, keep in sync with the root `Cargo.toml`. +version = "0.24.0" +edition = "2021" +rust-version = "1.61" +license = "Apache-2.0" + +[lib] +name = "qiskit_qasm2" +crate-type = ["cdylib"] + +[dependencies] +hashbrown = "0.13.2" +pyo3 = { version = "0.18.2", features = ["extension-module"] } diff --git a/crates/qasm2/README.md b/crates/qasm2/README.md new file mode 100644 index 000000000000..05efd24407f9 --- /dev/null +++ b/crates/qasm2/README.md @@ -0,0 +1,6 @@ +# `qiskit._qasm2` + +This crate is the bulk of the OpenQASM 2 parser. Since OpenQASM 2 is a simple language, it doesn't +bother with an AST construction step, but produces a simple linear bytecode stream to pass to a +small Python interpreter (in `qiskit.qasm2`). This started off life as a vendored version of [the +package `qiskit-qasm2`](https://pypi.org/project/qiskit-qasm2). diff --git a/crates/qasm2/src/bytecode.rs b/crates/qasm2/src/bytecode.rs new file mode 100644 index 000000000000..f586824696a6 --- /dev/null +++ b/crates/qasm2/src/bytecode.rs @@ -0,0 +1,353 @@ +// 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 pyo3::prelude::*; + +use crate::error::QASM2ParseError; +use crate::expr::Expr; +use crate::lex; +use crate::parse; +use crate::parse::{ClbitId, CregId, GateId, ParamId, QubitId}; +use crate::{CustomClassical, CustomInstruction}; + +/// The Rust parser produces an iterator of these `Bytecode` instructions, which comprise an opcode +/// integer for operation distinction, and a free-form tuple containing the operands. +#[pyclass(module = "qiskit._qasm2", frozen)] +#[derive(Clone)] +pub struct Bytecode { + #[pyo3(get)] + opcode: OpCode, + #[pyo3(get)] + operands: PyObject, +} + +/// The operations that are represented by the "bytecode" passed to Python. +#[pyclass(module = "qiskit._qasm2", frozen)] +#[derive(Clone)] +pub enum OpCode { + // There is only a `Gate` here, not a `GateInBasis`, because in Python space we don't have the + // same strict typing requirements to satisfy. + Gate, + ConditionedGate, + Measure, + ConditionedMeasure, + Reset, + ConditionedReset, + Barrier, + DeclareQreg, + DeclareCreg, + DeclareGate, + EndDeclareGate, + DeclareOpaque, + SpecialInclude, +} + +// The following structs, with `Expr` or `OpCode` in the name (but not the top-level `OpCode` +// above) build up the tree of symbolic expressions for the parameter applications within gate +// bodies. We choose to store this in the gate classes that the Python component emits, so it can +// lazily create definitions as required, rather than eagerly binding them as the file is parsed. +// +// In Python space we would usually have the classes inherit from some shared subclass, but doing +// that makes things a little fiddlier with PyO3, and there's no real benefit for our uses. + +/// A (potentially folded) floating-point constant value as part of an expression. +#[pyclass(module = "qiskit._qasm2", frozen)] +#[derive(Clone)] +pub struct ExprConstant { + #[pyo3(get)] + pub value: f64, +} + +/// A reference to one of the arguments to the gate. +#[pyclass(module = "qiskit._qasm2", frozen)] +#[derive(Clone)] +pub struct ExprArgument { + #[pyo3(get)] + pub index: ParamId, +} + +/// A unary operation acting on some other part of the expression tree. This includes the `+` and +/// `-` unary operators, but also any of the built-in scientific-calculator functions. +#[pyclass(module = "qiskit._qasm2", frozen)] +#[derive(Clone)] +pub struct ExprUnary { + #[pyo3(get)] + pub opcode: UnaryOpCode, + #[pyo3(get)] + pub argument: PyObject, +} + +/// A binary operation acting on two other parts of the expression tree. +#[pyclass(module = "qiskit._qasm2", frozen)] +#[derive(Clone)] +pub struct ExprBinary { + #[pyo3(get)] + pub opcode: BinaryOpCode, + #[pyo3(get)] + pub left: PyObject, + #[pyo3(get)] + pub right: PyObject, +} + +/// Some custom callable Python function that the user told us about. +#[pyclass(module = "qiskit._qasm2", frozen)] +#[derive(Clone)] +pub struct ExprCustom { + #[pyo3(get)] + pub callable: PyObject, + #[pyo3(get)] + pub arguments: Vec, +} + +/// Discriminator for the different types of unary operator. We could have a separate class for +/// each of these, but this way involves fewer imports in Python, and also serves to split up the +/// option tree at the top level, so we don't have to test every unary operator before testing +/// other operations. +#[pyclass(module = "qiskit._qasm2", frozen)] +#[derive(Clone)] +pub enum UnaryOpCode { + Negate, + Cos, + Exp, + Ln, + Sin, + Sqrt, + Tan, +} + +/// Discriminator for the different types of binary operator. We could have a separate class for +/// each of these, but this way involves fewer imports in Python, and also serves to split up the +/// option tree at the top level, so we don't have to test every binary operator before testing +/// other operations. +#[pyclass(module = "qiskit._qasm2", frozen)] +#[derive(Clone)] +pub enum BinaryOpCode { + Add, + Subtract, + Multiply, + Divide, + Power, +} + +/// An internal representation of the bytecode that will later be converted to the more free-form +/// [Bytecode] Python-space objects. This is fairly tightly coupled to Python space; the intent is +/// just to communicate to Python as concisely as possible what it needs to do. We want to have as +/// little work to do in Python space as possible, since everything is slower there. +/// +/// In various enumeration items, we use zero-indexed numeric keys to identify the object rather +/// than its name. This is much more efficient in Python-space; rather than needing to build and +/// lookup things in a hashmap, we can just build Python lists and index them directly, which also +/// has the advantage of not needing to pass strings to Python for each gate. It also gives us +/// consistency with how qubits and clbits are tracked; there is no need to track both the register +/// name and the index separately when we can use a simple single index. +pub enum InternalBytecode { + Gate { + id: GateId, + arguments: Vec, + qubits: Vec, + }, + ConditionedGate { + id: GateId, + arguments: Vec, + qubits: Vec, + creg: CregId, + value: usize, + }, + Measure { + qubit: QubitId, + clbit: ClbitId, + }, + ConditionedMeasure { + qubit: QubitId, + clbit: ClbitId, + creg: CregId, + value: usize, + }, + Reset { + qubit: QubitId, + }, + ConditionedReset { + qubit: QubitId, + creg: CregId, + value: usize, + }, + Barrier { + qubits: Vec, + }, + DeclareQreg { + name: String, + size: usize, + }, + DeclareCreg { + name: String, + size: usize, + }, + DeclareGate { + name: String, + num_qubits: usize, + }, + GateInBody { + id: GateId, + arguments: Vec, + qubits: Vec, + }, + EndDeclareGate {}, + DeclareOpaque { + name: String, + num_qubits: usize, + }, + SpecialInclude { + indices: Vec, + }, +} + +impl IntoPy for InternalBytecode { + /// Convert the internal bytecode representation to a Python-space one. + fn into_py(self, py: Python<'_>) -> Bytecode { + match self { + InternalBytecode::Gate { + id, + arguments, + qubits, + } => Bytecode { + opcode: OpCode::Gate, + operands: (id, arguments, qubits).into_py(py), + }, + InternalBytecode::ConditionedGate { + id, + arguments, + qubits, + creg, + value, + } => Bytecode { + opcode: OpCode::ConditionedGate, + operands: (id, arguments, qubits, creg, value).into_py(py), + }, + InternalBytecode::Measure { qubit, clbit } => Bytecode { + opcode: OpCode::Measure, + operands: (qubit, clbit).into_py(py), + }, + InternalBytecode::ConditionedMeasure { + qubit, + clbit, + creg, + value, + } => Bytecode { + opcode: OpCode::ConditionedMeasure, + operands: (qubit, clbit, creg, value).into_py(py), + }, + InternalBytecode::Reset { qubit } => Bytecode { + opcode: OpCode::Reset, + operands: (qubit,).into_py(py), + }, + InternalBytecode::ConditionedReset { qubit, creg, value } => Bytecode { + opcode: OpCode::ConditionedReset, + operands: (qubit, creg, value).into_py(py), + }, + InternalBytecode::Barrier { qubits } => Bytecode { + opcode: OpCode::Barrier, + operands: (qubits,).into_py(py), + }, + InternalBytecode::DeclareQreg { name, size } => Bytecode { + opcode: OpCode::DeclareQreg, + operands: (name, size).into_py(py), + }, + InternalBytecode::DeclareCreg { name, size } => Bytecode { + opcode: OpCode::DeclareCreg, + operands: (name, size).into_py(py), + }, + InternalBytecode::DeclareGate { name, num_qubits } => Bytecode { + opcode: OpCode::DeclareGate, + operands: (name, num_qubits).into_py(py), + }, + InternalBytecode::GateInBody { + id, + arguments, + qubits, + } => Bytecode { + // In Python space, we don't have to be worried about the types of the + // parameters changing here, so we can just use `OpCode::Gate` unlike in the + // internal bytecode. + opcode: OpCode::Gate, + operands: (id, arguments.into_py(py), qubits).into_py(py), + }, + InternalBytecode::EndDeclareGate {} => Bytecode { + opcode: OpCode::EndDeclareGate, + operands: ().into_py(py), + }, + InternalBytecode::DeclareOpaque { name, num_qubits } => Bytecode { + opcode: OpCode::DeclareOpaque, + operands: (name, num_qubits).into_py(py), + }, + InternalBytecode::SpecialInclude { indices } => Bytecode { + opcode: OpCode::SpecialInclude, + operands: (indices,).into_py(py), + }, + } + } +} + +/// The custom iterator object that is returned up to Python space for iteration through the +/// bytecode stream. This is never constructed on the Python side; it is built in Rust space +/// by Python calls to [bytecode_from_string] and [bytecode_from_file]. +#[pyclass] +pub struct BytecodeIterator { + parser_state: parse::State, + buffer: Vec>, + buffer_used: usize, +} + +impl BytecodeIterator { + pub fn new( + tokens: lex::TokenStream, + include_path: Vec, + custom_instructions: &[CustomInstruction], + custom_classical: &[CustomClassical], + strict: bool, + ) -> PyResult { + Ok(BytecodeIterator { + parser_state: parse::State::new( + tokens, + include_path, + custom_instructions, + custom_classical, + strict, + ) + .map_err(QASM2ParseError::new_err)?, + buffer: vec![], + buffer_used: 0, + }) + } +} + +#[pymethods] +impl BytecodeIterator { + fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> { + slf + } + + fn __next__(&mut self, py: Python<'_>) -> PyResult> { + if self.buffer_used >= self.buffer.len() { + self.buffer.clear(); + self.buffer_used = 0; + self.parser_state.parse_next(&mut self.buffer)?; + } + if self.buffer.is_empty() { + Ok(None) + } else { + self.buffer_used += 1; + Ok(self.buffer[self.buffer_used - 1] + .take() + .map(|bytecode| bytecode.into_py(py))) + } + } +} diff --git a/crates/qasm2/src/error.rs b/crates/qasm2/src/error.rs new file mode 100644 index 000000000000..4cfe6bc42636 --- /dev/null +++ b/crates/qasm2/src/error.rs @@ -0,0 +1,84 @@ +// 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 pyo3::import_exception; + +use crate::lex::Token; + +pub struct Position<'a> { + filename: &'a std::ffi::OsStr, + line: usize, + col: usize, +} + +impl<'a> Position<'a> { + pub fn new(filename: &'a std::ffi::OsStr, line: usize, col: usize) -> Self { + Self { + filename, + line, + col, + } + } +} + +impl<'a> std::fmt::Display for &Position<'a> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}:{},{}", + self.filename.to_string_lossy(), + self.line, + self.col + ) + } +} + +/// Create an error message that includes span data from the given [token][Token]. The base of the +/// message is `message`, and `filename` is the file the triggering OpenQASM 2 code came from. For +/// string inputs, this can be a placeholder. +pub fn message_generic(position: Option<&Position>, message: &str) -> String { + if let Some(position) = position { + format!("{}: {}", position, message) + } else { + message.to_owned() + } +} + +/// Shorthand form for creating an error message when a particular type of token was required, but +/// something else was `received`. +pub fn message_incorrect_requirement( + required: &str, + received: &Token, + filename: &std::ffi::OsStr, +) -> String { + message_generic( + Some(&Position::new(filename, received.line, received.col)), + &format!( + "needed {}, but instead saw {}", + required, + received.ttype.describe() + ), + ) +} + +/// Shorthand form for creating an error message when a particular type of token was required, but +/// the input ended unexpectedly. +pub fn message_bad_eof(position: Option<&Position>, required: &str) -> String { + message_generic( + position, + &format!("unexpected end-of-file when expecting to see {}", required), + ) +} + +// We define the exception in Python space so it can inherit from QiskitError; it's easier to do +// that from Python and wrap rather than also needing to import QiskitError to Rust to wrap. +import_exception!(qiskit.qasm2.exceptions, QASM2ParseError); diff --git a/crates/qasm2/src/expr.rs b/crates/qasm2/src/expr.rs new file mode 100644 index 000000000000..59a3ebf3c221 --- /dev/null +++ b/crates/qasm2/src/expr.rs @@ -0,0 +1,704 @@ +// 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. + +//! An operator-precedence subparser used by the main parser for handling parameter expressions. +//! Instances of this subparser are intended to only live for as long as it takes to parse a single +//! parameter. + +use core::f64; + +use hashbrown::HashMap; +use pyo3::prelude::*; +use pyo3::types::PyTuple; + +use crate::bytecode; +use crate::error::{ + message_bad_eof, message_generic, message_incorrect_requirement, Position, QASM2ParseError, +}; +use crate::lex::{Token, TokenContext, TokenStream, TokenType}; +use crate::parse::{GateSymbol, GlobalSymbol, ParamId}; + +/// Enum representation of the builtin OpenQASM 2 functions. The built-in Qiskit parser adds the +/// inverse trigonometric functions, but these are an extension to the version as given in the +/// arXiv paper describing OpenQASM 2. This enum is essentially just a subset of the [TokenType] +/// enum, to allow for better pattern-match checking in the Rust compiler. +pub enum Function { + Cos, + Exp, + Ln, + Sin, + Sqrt, + Tan, +} + +impl From for Function { + fn from(value: TokenType) -> Self { + match value { + TokenType::Cos => Function::Cos, + TokenType::Exp => Function::Exp, + TokenType::Ln => Function::Ln, + TokenType::Sin => Function::Sin, + TokenType::Sqrt => Function::Sqrt, + TokenType::Tan => Function::Tan, + _ => panic!(), + } + } +} + +impl From for bytecode::UnaryOpCode { + fn from(value: Function) -> Self { + match value { + Function::Cos => Self::Cos, + Function::Exp => Self::Exp, + Function::Ln => Self::Ln, + Function::Sin => Self::Sin, + Function::Sqrt => Self::Sqrt, + Function::Tan => Self::Tan, + } + } +} + +/// An operator symbol used in the expression parsing. This is essentially just a subset of the +/// [TokenType] enum (albeit with resolved names) to allow for better pattern-match semantics in +/// the Rust compiler. +#[derive(Clone, Copy)] +enum Op { + Plus, + Minus, + Multiply, + Divide, + Power, +} + +impl Op { + fn text(&self) -> &'static str { + match self { + Self::Plus => "+", + Self::Minus => "-", + Self::Multiply => "*", + Self::Divide => "/", + Self::Power => "^", + } + } +} + +impl From for Op { + fn from(value: TokenType) -> Self { + match value { + TokenType::Plus => Op::Plus, + TokenType::Minus => Op::Minus, + TokenType::Asterisk => Op::Multiply, + TokenType::Slash => Op::Divide, + TokenType::Caret => Op::Power, + _ => panic!(), + } + } +} + +/// An atom of the operator-precendence 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 +/// +/// ```rust +/// match atom { +/// Atom::Const(val) => (), +/// Atom::Parameter(index) => (), +/// // ... +/// } +/// ``` +/// +/// where required. +enum Atom { + LParen, + RParen, + Function(Function), + CustomFunction(PyObject, usize), + Op(Op), + Const(f64), + Parameter(ParamId), +} + +/// A tree representation of parameter expressions in OpenQASM 2. The expression +/// operator-precedence parser will do complete constant folding on operations that only involve +/// floating-point numbers, so these will simply be evaluated into a `Constant` variant rather than +/// represented in full tree form. For references to the gate parameters, we just store the index +/// of which parameter it is. +pub enum Expr { + Constant(f64), + Parameter(ParamId), + Negate(Box), + Add(Box, Box), + Subtract(Box, Box), + Multiply(Box, Box), + Divide(Box, Box), + Power(Box, Box), + Function(Function, Box), + CustomFunction(PyObject, Vec), +} + +impl IntoPy for Expr { + fn into_py(self, py: Python<'_>) -> PyObject { + match self { + Expr::Constant(value) => bytecode::ExprConstant { value }.into_py(py), + Expr::Parameter(index) => bytecode::ExprArgument { index }.into_py(py), + Expr::Negate(expr) => bytecode::ExprUnary { + opcode: bytecode::UnaryOpCode::Negate, + argument: expr.into_py(py), + } + .into_py(py), + Expr::Add(left, right) => bytecode::ExprBinary { + opcode: bytecode::BinaryOpCode::Add, + left: left.into_py(py), + right: right.into_py(py), + } + .into_py(py), + Expr::Subtract(left, right) => bytecode::ExprBinary { + opcode: bytecode::BinaryOpCode::Subtract, + left: left.into_py(py), + right: right.into_py(py), + } + .into_py(py), + Expr::Multiply(left, right) => bytecode::ExprBinary { + opcode: bytecode::BinaryOpCode::Multiply, + left: left.into_py(py), + right: right.into_py(py), + } + .into_py(py), + Expr::Divide(left, right) => bytecode::ExprBinary { + opcode: bytecode::BinaryOpCode::Divide, + left: left.into_py(py), + right: right.into_py(py), + } + .into_py(py), + Expr::Power(left, right) => bytecode::ExprBinary { + opcode: bytecode::BinaryOpCode::Power, + left: left.into_py(py), + right: right.into_py(py), + } + .into_py(py), + Expr::Function(func, expr) => bytecode::ExprUnary { + opcode: func.into(), + argument: expr.into_py(py), + } + .into_py(py), + Expr::CustomFunction(func, exprs) => bytecode::ExprCustom { + callable: func, + arguments: exprs.into_iter().map(|expr| expr.into_py(py)).collect(), + } + .into_py(py), + } + } +} + +/// Calculate the binding power of an [Op] when used in a prefix position. Returns [None] if the +/// operation cannot be used in the prefix position. The binding power is on the same scale as +/// those returned by [binary_power]. +fn prefix_power(op: Op) -> Option { + match op { + Op::Plus | Op::Minus => Some(5), + _ => None, + } +} + +/// Calculate the binding power of an [Op] when used in an infix position. The differences between +/// left- and right-binding powers represent the associativity of the operation. +fn binary_power(op: Op) -> (u8, u8) { + // For new binding powers, use the odd number as the "base" and the even number one larger than + // it to represent the associativity. Left-associative operators bind more strongly to the + // operand on their right (i.e. in `a + b + c`, the first `+` binds to the `b` more tightly + // than the second, so we get the left-associative form), and right-associative operators bind + // more strongly to the operand of their left. The separation of using the odd--even pair is + // so there's no clash between different operator levels, even accounting for the associativity + // distinction. + // + // All powers should be greater than zero; we need zero free to be the base case in the + // entry-point to the precedence parser. + match op { + Op::Plus | Op::Minus => (1, 2), + Op::Multiply | Op::Divide => (3, 4), + Op::Power => (8, 7), + } +} + +/// 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. +pub struct ExprParser<'a> { + pub tokens: &'a mut Vec, + pub context: &'a mut TokenContext, + pub gate_symbols: &'a HashMap, + pub global_symbols: &'a HashMap, + pub strict: bool, +} + +impl<'a> ExprParser<'a> { + /// Get the next token available in the stack of token streams, popping and removing any + /// complete streams, except the base case. Will only return `None` once all streams are + /// exhausted. + fn next_token(&mut self) -> PyResult> { + let mut pointer = self.tokens.len() - 1; + while pointer > 1 { + let out = self.tokens[pointer].next(self.context)?; + if out.is_some() { + return Ok(out); + } + self.tokens.pop(); + pointer -= 1; + } + self.tokens[0].next(self.context) + } + + /// Peek the next token in the stack of token streams. This does not remove any complete + /// streams yet. Will only return `None` once all streams are exhausted. + fn peek_token(&mut self) -> PyResult> { + let mut pointer = self.tokens.len() - 1; + while pointer > 1 && self.tokens[pointer].peek(self.context)?.is_none() { + pointer -= 1; + } + self.tokens[pointer].peek(self.context) + } + + /// Get the filename associated with the currently active token stream. + fn current_filename(&self) -> &std::ffi::OsStr { + &self.tokens[self.tokens.len() - 1].filename + } + + /// Expect a token of the correct [TokenType]. This is a direct analogue of + /// [parse::State::expect]. The error variant of the result contains a suitable error message + /// if the expectation is violated. + fn expect(&mut self, expected: TokenType, required: &str, cause: &Token) -> PyResult { + let token = match self.next_token()? { + None => { + return Err(QASM2ParseError::new_err(message_bad_eof( + Some(&Position::new( + self.current_filename(), + cause.line, + cause.col, + )), + required, + ))) + } + Some(token) => token, + }; + if token.ttype == expected { + Ok(token) + } else { + Err(QASM2ParseError::new_err(message_incorrect_requirement( + required, + &token, + self.current_filename(), + ))) + } + } + + /// Peek the next token from the stream, and consume and return it only if it has the correct + /// type. + fn accept(&mut self, acceptable: TokenType) -> PyResult> { + match self.peek_token()? { + Some(Token { ttype, .. }) if *ttype == acceptable => self.next_token(), + _ => Ok(None), + } + } + + /// Apply a prefix [Op] to the current [expression][Expr]. If the current expression is a + /// constant floating-point value the application will be eagerly constant-folded, otherwise + /// the resulting [Expr] will have a tree structure. + fn apply_prefix(&mut self, prefix: Op, expr: Expr) -> PyResult { + match prefix { + Op::Plus => Ok(expr), + Op::Minus => match expr { + Expr::Constant(val) => Ok(Expr::Constant(-val)), + _ => Ok(Expr::Negate(Box::new(expr))), + }, + _ => panic!(), + } + } + + /// Apply a binary infix [Op] to the current [expression][Expr]. If both operands have + /// constant floating-point values the application will be eagerly constant-folded, otherwise + /// the resulting [Expr] will have a tree structure. + fn apply_infix(&mut self, infix: Op, lhs: Expr, rhs: Expr, op_token: &Token) -> PyResult { + if let (Expr::Constant(val), Op::Divide) = (&rhs, infix) { + if *val == 0.0 { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + op_token.line, + op_token.col, + )), + "cannot divide by zero", + ))); + } + }; + if let (Expr::Constant(val_l), Expr::Constant(val_r)) = (&lhs, &rhs) { + // Eagerly constant-fold if possible. + match infix { + Op::Plus => Ok(Expr::Constant(val_l + val_r)), + Op::Minus => Ok(Expr::Constant(val_l - val_r)), + Op::Multiply => Ok(Expr::Constant(val_l * val_r)), + Op::Divide => Ok(Expr::Constant(val_l / val_r)), + Op::Power => Ok(Expr::Constant(val_l.powf(*val_r))), + } + } else { + // If not, we have to build a tree. + let id_l = Box::new(lhs); + let id_r = Box::new(rhs); + match infix { + Op::Plus => Ok(Expr::Add(id_l, id_r)), + Op::Minus => Ok(Expr::Subtract(id_l, id_r)), + Op::Multiply => Ok(Expr::Multiply(id_l, id_r)), + Op::Divide => Ok(Expr::Divide(id_l, id_r)), + Op::Power => Ok(Expr::Power(id_l, id_r)), + } + } + } + + /// Apply a "scientific calculator" built-in function to an [expression][Expr]. If the operand + /// is a constant, the function will be constant-folded to produce a new constant expression, + /// otherwise a tree-form [Expr] is returned. + fn apply_function(&mut self, func: Function, expr: Expr, token: &Token) -> PyResult { + match expr { + Expr::Constant(val) => match func { + Function::Cos => Ok(Expr::Constant(val.cos())), + Function::Exp => Ok(Expr::Constant(val.exp())), + Function::Ln => { + if val > 0.0 { + Ok(Expr::Constant(val.ln())) + } else { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + &format!( + "failure in constant folding: cannot take ln of non-positive {}", + val + ), + ))) + } + } + Function::Sin => Ok(Expr::Constant(val.sin())), + Function::Sqrt => { + if val >= 0.0 { + Ok(Expr::Constant(val.sqrt())) + } else { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + &format!( + "failure in constant folding: cannot take sqrt of negative {}", + val + ), + ))) + } + } + Function::Tan => Ok(Expr::Constant(val.tan())), + }, + _ => Ok(Expr::Function(func, Box::new(expr))), + } + } + + fn apply_custom_function( + &mut self, + callable: PyObject, + exprs: Vec, + token: &Token, + ) -> PyResult { + if exprs.iter().all(|x| matches!(x, Expr::Constant(_))) { + // We can still do constant folding with custom user classical functions, we're just + // going to have to acquire the GIL and call the Python object the user gave us right + // now. We need to explicitly handle any exceptions that might occur from that. + Python::with_gil(|py| { + let args = PyTuple::new( + py, + exprs.iter().map(|x| { + if let Expr::Constant(val) = x { + *val + } else { + unreachable!() + } + }), + ); + match callable.call1(py, args) { + Ok(retval) => { + match retval.extract::(py) { + Ok(fval) => Ok(Expr::Constant(fval)), + Err(inner) => { + let error = QASM2ParseError::new_err(message_generic( + Some(&Position::new(self.current_filename(), token.line, token.col)), + "user-defined function returned non-float during constant folding", + )); + error.set_cause(py, Some(inner)); + Err(error) + } + } + } + Err(inner) => { + let error = QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + "caught exception when constant folding with user-defined function", + )); + error.set_cause(py, Some(inner)); + Err(error) + } + } + }) + } else { + Ok(Expr::CustomFunction(callable, exprs)) + } + } + + /// If in `strict` mode, and we have a trailing comma, emit a suitable error message. + fn check_trailing_comma(&self, comma: Option<&Token>) -> PyResult<()> { + match (self.strict, comma) { + (true, Some(token)) => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + "[strict] trailing commas in parameter and qubit lists are forbidden", + ))), + _ => Ok(()), + } + } + + /// Convert the given general [Token] into the expression-parser-specific [Atom], if possible. + /// Not all [Token]s have a corresponding [Atom]; if this is the case, the return value is + /// `Ok(None)`. The error variant is returned if the next token is grammatically valid, but + /// not semantically, such as an identifier for a value of an incorrect type. + fn try_atom_from_token(&self, token: &Token) -> PyResult> { + match token.ttype { + TokenType::LParen => Ok(Some(Atom::LParen)), + TokenType::RParen => Ok(Some(Atom::RParen)), + TokenType::Minus + | TokenType::Plus + | TokenType::Asterisk + | TokenType::Slash + | TokenType::Caret => Ok(Some(Atom::Op(token.ttype.into()))), + TokenType::Cos + | TokenType::Exp + | TokenType::Ln + | 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))), + TokenType::Pi => Ok(Some(Atom::Const(f64::consts::PI))), + TokenType::Id => { + let id = token.text(self.context); + match self.gate_symbols.get(id) { + Some(GateSymbol::Parameter { index }) => Ok(Some(Atom::Parameter(*index))), + Some(GateSymbol::Qubit { .. }) => { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(self.current_filename(), token.line, token.col)), + &format!("'{}' is a gate qubit, not a parameter", id), + ))) + } + None => { + match self.global_symbols.get(id) { + Some(GlobalSymbol::Classical { callable, num_params }) => { + Ok(Some(Atom::CustomFunction(callable.clone(), *num_params))) + } + _ => { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(self.current_filename(), token.line, token.col)), + &format!( + "'{}' is not a parameter or custom instruction defined in this scope", + id, + )))) + } + } + } + } + } + _ => Ok(None), + } + } + + /// Peek at the next [Atom] (and backing [Token]) if the next token exists and can be converted + /// into a valid [Atom]. If it can't, or if we are at the end of the input, the `None` variant + /// is returned. + fn peek_atom(&mut self) -> PyResult> { + if let Some(&token) = self.peek_token()? { + if let Ok(Some(atom)) = self.try_atom_from_token(&token) { + Ok(Some((atom, token))) + } else { + Ok(None) + } + } else { + Ok(None) + } + } + + /// The main recursive worker routing of the operator-precedence parser. This evaluates a + /// series of binary infix operators that have binding powers greater than the input + /// `power_min`, and unary prefixes on the left-hand operand. For example, if `power_min` + /// starts out at `2` (such as it would when evaluating the right-hand side of a binary `+` + /// expression), then as many `*` and `^` operations as appear would be evaluated by this loop, + /// and its parsing would finish when it saw the next `+` binary operation. For initial entry, + /// the `power_min` should be zero. + fn eval_expression(&mut self, power_min: u8, cause: &Token) -> PyResult { + let token = self.next_token()?.ok_or_else(|| { + QASM2ParseError::new_err(message_bad_eof( + Some(&Position::new( + self.current_filename(), + cause.line, + cause.col, + )), + if power_min == 0 { + "an expression" + } else { + "a missing operand" + }, + )) + })?; + let atom = self.try_atom_from_token(&token)?.ok_or_else(|| { + QASM2ParseError::new_err(message_incorrect_requirement( + if power_min == 0 { + "an expression" + } else { + "a missing operand" + }, + &token, + self.current_filename(), + )) + })?; + // First evaluate the "left-hand side" of a (potential) sequence of binary infix operators. + // This might be a simple value, a unary operator acting on a value, or a bracketed + // expression (either the operand of a function, or just plain parentheses). This can also + // invoke a recursive call; the parenthesis components feel naturally recursive, and the + // unary operator component introduces a new precedence level that requires a recursive + // call to evaluate. + let mut lhs = match atom { + Atom::LParen => { + let out = self.eval_expression(0, cause)?; + self.expect(TokenType::RParen, "a closing parenthesis", &token)?; + Ok(out) + } + Atom::RParen => { + if power_min == 0 { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + "did not find an expected expression", + ))) + } else { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + "the parenthesis closed, but there was a missing operand", + ))) + } + } + Atom::Function(func) => { + let lparen_token = + self.expect(TokenType::LParen, "an opening parenthesis", &token)?; + let argument = self.eval_expression(0, &token)?; + let comma = self.accept(TokenType::Comma)?; + self.check_trailing_comma(comma.as_ref())?; + self.expect(TokenType::RParen, "a closing parenthesis", &lparen_token)?; + Ok(self.apply_function(func, argument, &token)?) + } + Atom::CustomFunction(callable, num_params) => { + let lparen_token = + self.expect(TokenType::LParen, "an opening parenthesis", &token)?; + let mut arguments = Vec::::with_capacity(num_params); + let mut comma = None; + loop { + // There are breaks at the start and end of this loop, because we might be + // breaking because there are _no_ parameters, because there's a trailing + // comma before the closing parenthesis, or because we didn't see a comma after + // an expression so we _need_ a closing parenthesis. + if let Some((Atom::RParen, _)) = self.peek_atom()? { + break; + } + arguments.push(self.eval_expression(0, &token)?); + comma = self.accept(TokenType::Comma)?; + if comma.is_none() { + break; + } + } + self.check_trailing_comma(comma.as_ref())?; + self.expect(TokenType::RParen, "a closing parenthesis", &lparen_token)?; + if arguments.len() == num_params { + Ok(self.apply_custom_function(callable, arguments, &token)?) + } else { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + &format!( + "custom function argument-count mismatch: expected {}, saw {}", + num_params, + arguments.len(), + ), + ))) + } + } + Atom::Op(op) => match prefix_power(op) { + Some(power) => { + let expr = self.eval_expression(power, &token)?; + Ok(self.apply_prefix(op, expr)?) + } + None => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + &format!("'{}' is not a valid unary operator", op.text()), + ))), + }, + Atom::Const(val) => Ok(Expr::Constant(val)), + Atom::Parameter(val) => Ok(Expr::Parameter(val)), + }?; + // Now loop over a series of infix operators. We can continue as long as we're just + // looking at operators that bind more tightly than the `power_min` passed to this + // function. Once they're the same power or less, we have to return, because the calling + // evaluator needs to bind its operator before we move on to the next infix operator. + while let Some((Atom::Op(op), peeked_token)) = self.peek_atom()? { + let (power_l, power_r) = binary_power(op); + if power_l < power_min { + break; + } + self.next_token()?; // Skip peeked operator. + let rhs = self.eval_expression(power_r, &peeked_token)?; + lhs = self.apply_infix(op, lhs, rhs, &peeked_token)?; + } + Ok(lhs) + } + + /// Parse a single expression completely. This is the only public entry point to the + /// operator-precedence parser. + 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 new file mode 100644 index 000000000000..a6b3a6abac90 --- /dev/null +++ b/crates/qasm2/src/lex.rs @@ -0,0 +1,745 @@ +// 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. + +//! The lexing logic for OpenQASM 2, responsible for turning a sequence of bytes into a +//! lexed [TokenStream] for consumption by the parsing machinery. The general strategy here is +//! quite simple; for all the symbol-like tokens, the lexer can use a very simple single-byte +//! lookahead to determine what token it needs to emit. For keywords and identifiers, we just read +//! the identifier in completely, then produce the right token once we see the end of the +//! identifier characters. +//! +//! We effectively use a custom lexing mode to handle the version information after the `OPENQASM` +//! 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. + +use hashbrown::HashMap; +use pyo3::prelude::PyResult; + +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 +/// the specification. +#[derive(Clone, Debug)] +pub struct Version { + pub major: usize, + pub minor: Option, +} + +/// The context that is necessary to fully extract the information from a [Token]. This owns, for +/// example, the text of each token (where a token does not have a static text representation), +/// from which the other properties can later be derived. This struct is effectively entirely +/// opaque outside this module; the associated functions on [Token] take this context object, +/// however, and extract the information from it. +#[derive(Clone, Debug)] +pub struct TokenContext { + text: Vec, + lookup: HashMap, usize>, +} + +impl TokenContext { + /// Create a new context for tokens. Nothing is heap-allocated until required. + pub fn new() -> Self { + TokenContext { + text: vec![], + lookup: HashMap::new(), + } + } + + /// Intern the given `ascii_text` of a [Token], and return an index into the [TokenContext]. + /// This will not store strings that are already present in the context; instead, the previous + /// index is transparently returned. + fn index(&mut self, ascii_text: &[u8]) -> usize { + match self.lookup.get(ascii_text) { + Some(index) => *index, + None => { + let index = self.text.len(); + self.lookup.insert(ascii_text.to_vec(), index); + self.text + .push(std::str::from_utf8(ascii_text).unwrap().to_owned()); + index + } + } + } +} + +// Clippy complains without this. +impl Default for TokenContext { + fn default() -> Self { + Self::new() + } +} + +/// An enumeration of the different types of [Token] that can be created during lexing. This is +/// deliberately not a data enum, to make various abstract `expect` (and so on) methods more +/// ergonomic to use; one does not need to completely define the pattern match each time, but can +/// simply pass the type identifier. This also saves memory, since the static variants do not need +/// to be aligned to include the space necessary for text pointers that would be in the non-static +/// forms, and allows strings to be shared between many tokens (using the [TokenContext] store). +#[derive(PartialEq, Eq, Clone, Copy, Debug)] +pub enum TokenType { + // Keywords + OpenQASM, + Barrier, + Cos, + Creg, + Exp, + Gate, + If, + Include, + Ln, + Measure, + Opaque, + Qreg, + Reset, + Sin, + Sqrt, + Tan, + Pi, + // Symbols + Plus, + Minus, + Arrow, + Asterisk, + Equals, + Slash, + Caret, + Semicolon, + Comma, + LParen, + RParen, + LBracket, + RBracket, + LBrace, + RBrace, + // Content + Id, + Real, + Integer, + Filename, + Version, +} + +impl TokenType { + pub fn variable_text(&self) -> bool { + match self { + TokenType::OpenQASM + | TokenType::Barrier + | TokenType::Cos + | TokenType::Creg + | TokenType::Exp + | TokenType::Gate + | TokenType::If + | TokenType::Include + | TokenType::Ln + | TokenType::Measure + | TokenType::Opaque + | TokenType::Qreg + | TokenType::Reset + | TokenType::Sin + | TokenType::Sqrt + | TokenType::Tan + | TokenType::Pi + | TokenType::Plus + | TokenType::Minus + | TokenType::Arrow + | TokenType::Asterisk + | TokenType::Equals + | TokenType::Slash + | TokenType::Caret + | TokenType::Semicolon + | TokenType::Comma + | TokenType::LParen + | TokenType::RParen + | TokenType::LBracket + | TokenType::RBracket + | TokenType::LBrace + | TokenType::RBrace => false, + TokenType::Id + | TokenType::Real + | TokenType::Integer + | TokenType::Filename + | TokenType::Version => true, + } + } + + /// Get a static description of the token type. This is useful for producing messages when the + /// full token context isn't available, or isn't important. + pub fn describe(&self) -> &'static str { + match self { + TokenType::OpenQASM => "OPENQASM", + TokenType::Barrier => "barrier", + TokenType::Cos => "cos", + TokenType::Creg => "creg", + TokenType::Exp => "exp", + TokenType::Gate => "gate", + TokenType::If => "if", + TokenType::Include => "include", + TokenType::Ln => "ln", + TokenType::Measure => "measure", + TokenType::Opaque => "opaque", + TokenType::Qreg => "qreg", + TokenType::Reset => "reset", + TokenType::Sin => "sin", + TokenType::Sqrt => "sqrt", + TokenType::Tan => "tan", + TokenType::Pi => "pi", + TokenType::Plus => "+", + TokenType::Minus => "-", + TokenType::Arrow => "->", + TokenType::Asterisk => "*", + TokenType::Equals => "==", + TokenType::Slash => "/", + TokenType::Caret => "^", + TokenType::Semicolon => ";", + TokenType::Comma => ",", + TokenType::LParen => "(", + TokenType::RParen => ")", + TokenType::LBracket => "[", + TokenType::RBracket => "]", + TokenType::LBrace => "{", + TokenType::RBrace => "}", + TokenType::Id => "an identifier", + TokenType::Real => "a real number", + TokenType::Integer => "an integer", + TokenType::Filename => "a filename string", + TokenType::Version => "a '.' version", + } + } +} + +/// A representation of a token, including its type, span information and pointer to where its text +/// is stored in the context object. These are relatively lightweight objects (though of course +/// not as light as the single type information). +#[derive(Clone, Copy, Debug)] +pub struct Token { + pub ttype: TokenType, + // The `line` and `col` refer only to the start of the token. There are no tokens that span + // more than one line (we don't tokenise comments), but the ending column offset can be + // calculated by asking the associated `TokenContext` for the text associated with this token, + // and inspecting the length of the returned value. + pub line: usize, + pub col: usize, + // Index into the TokenContext object, to retrieve the text that makes up the token. We don't + // resolve this into a value during lexing; that comes with annoying typing issues or storage + // wastage. Instead, we only convert the text into a value type when asked to by calling a + // relevant method on the token. + index: usize, +} + +impl Token { + /// Get a reference to the string that was seen to generate this token. + pub fn text<'a>(&self, context: &'a TokenContext) -> &'a str { + match self.ttype { + TokenType::Id + | TokenType::Real + | TokenType::Integer + | TokenType::Filename + | TokenType::Version => &context.text[self.index], + _ => self.ttype.describe(), + } + } + + /// If the token is an identifier, this method can be called to get an owned string containing + /// the text of the identifier. Panics if the token is not an identifier. + pub fn id(&self, context: &TokenContext) -> String { + if self.ttype != TokenType::Id { + panic!() + } + (&context.text[self.index]).into() + } + + /// 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. + pub fn real(&self, context: &TokenContext) -> f64 { + if self.ttype != TokenType::Real { + panic!() + } + context.text[self.index].parse().unwrap() + } + + /// If the token is an integer (by type, not just by value), this method can be called to + /// evaluate its value. Panics if the token is not an integer type. + pub fn int(&self, context: &TokenContext) -> usize { + if self.ttype != TokenType::Integer { + panic!() + } + context.text[self.index].parse().unwrap() + } + + /// If the token is a filename path, this method can be called to get a (regular) string + /// representing it. Panics if the token type was not a filename. + pub fn filename(&self, context: &TokenContext) -> String { + if self.ttype != TokenType::Filename { + panic!() + } + let out = &context.text[self.index]; + // String slicing is fine to assume bytes here, because the characters we're slicing out + // must both be the ASCII '"', which is a single-byte UTF-8 character. + out[1..out.len() - 1].into() + } + + /// If the token is a version-information token, this method can be called to evaluate the + /// version information. Panics if the token was not of the correct type. + pub fn version(&self, context: &TokenContext) -> Version { + if self.ttype != TokenType::Version { + panic!() + } + // Everything in the version token is a valid ASCII character, so must be a one-byte token. + let text = &context.text[self.index]; + match text.chars().position(|c| c == '.') { + Some(pos) => Version { + major: text[0..pos].parse().unwrap(), + minor: Some(text[pos + 1..text.len()].parse().unwrap()), + }, + None => Version { + major: text.parse().unwrap(), + minor: None, + }, + } + } +} + +/// The workhouse struct of the lexer. This represents a peekable iterable object that is abstract +/// over some buffered reader. The struct itself essentially represents the mutable state of the +/// lexer, with its main public associated functions being the iterable method [Self::next()] and +/// the [std::iter::Peekable]-like function [Self::peek()]. +/// +/// The stream exposes one public attributes directly: the [filename] that this stream comes from +/// (set to some placeholder value for streams that do not have a backing file). The associated +/// `TokenContext` object is managed separately to the stream and is passed in each call to `next`; +/// this allows for multiple streams to operate on the same context, such as when a new stream +/// begins in order to handle an `include` statement. +pub struct TokenStream { + /// The filename from which this stream is derived. May be a placeholder if there is no + /// backing file or other named resource. + pub filename: std::ffi::OsString, + strict: bool, + source: Box, + line_buffer: Vec, + done: bool, + line: usize, + col: usize, + try_version: bool, + // This is a manual peekable structure (rather than using the `peekable` method of `Iterator`) + // because we still want to be able to access the other members of the struct at the same time. + peeked: Option>, +} + +impl TokenStream { + /// Create and initialise a generic [TokenStream], given a source that implements + /// [std::io::BufRead] and a filename (or resource path) that describes its source. + fn new( + source: Box, + filename: std::ffi::OsString, + strict: bool, + ) -> Self { + TokenStream { + filename, + strict, + source, + 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 + // needs to extract the next line. + line: 0, + col: 0, + try_version: false, + peeked: None, + } + } + + /// Create a [TokenStream] from a string containing the OpenQASM 2 program. + pub fn from_string(string: String, strict: bool) -> Self { + TokenStream::new( + Box::new(std::io::Cursor::new(string)), + "".into(), + strict, + ) + } + + /// Create a [TokenStream] from a path containing the OpenQASM 2 program. + pub fn from_path>(path: P, strict: bool) -> Result { + let file = std::fs::File::open(path.as_ref())?; + Ok(TokenStream::new( + Box::new(std::io::BufReader::new(file)), + Path::file_name(path.as_ref()).unwrap().into(), + strict, + )) + } + + /// Read the next line into the managed buffer in the struct, updating the tracking information + /// of the position, and the `done` state of the iterator. + fn advance_line(&mut self) -> PyResult { + if self.done { + Ok(0) + } else { + self.line += 1; + self.col = 0; + self.line_buffer.clear(); + // We can assume that nobody's running this on ancient Mac software that uses only '\r' + // as its linebreak character. + match self.source.read_until(b'\n', &mut self.line_buffer) { + Ok(count) => { + if count == 0 || self.line_buffer[count - 1] != b'\n' { + self.done = true; + } + Ok(count) + } + Err(err) => { + self.done = true; + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, self.col)), + &format!("lexer failed to read stream: {}", err), + ))) + } + } + } + } + + /// Get the next character in the stream. This updates the line and column information for the + /// current byte as well. + fn next_byte(&mut self) -> PyResult> { + if self.col >= self.line_buffer.len() && self.advance_line()? == 0 { + return Ok(None); + } + let out = self.line_buffer[self.col]; + self.col += 1; + match out { + b @ 0x80..=0xff => { + self.done = true; + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, self.col)), + &format!("encountered a non-ASCII byte: {:02X?}", b), + ))) + } + b => Ok(Some(b)), + } + } + + /// Peek at the next byte in the stream without consuming it. This still returns an error if + /// the next byte isn't in the valid range for OpenQASM 2, or if the file/stream has failed to + /// read into the buffer for some reason. + fn peek_byte(&mut self) -> PyResult> { + if self.col >= self.line_buffer.len() && self.advance_line()? == 0 { + return Ok(None); + } + match self.line_buffer[self.col] { + b @ 0x80..=0xff => { + self.done = true; + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, self.col)), + &format!("encountered a non-ASCII byte: {:02X?}", b), + ))) + } + b => Ok(Some(b)), + } + } + + /// Expect that the next byte is not a word continuation, providing a suitable error message if + /// it is. + fn expect_word_boundary(&mut self, after: &str, start_col: usize) -> PyResult<()> { + match self.peek_byte()? { + Some(c @ (b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_')) => { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, start_col)), + &format!( + "expected a word boundary after {}, but saw '{}'", + after, c as char + ), + ))) + } + _ => Ok(()), + } + } + + /// Complete the lexing of a floating-point value from the position of maybe accepting an + /// exponent. The previous part of the token must be a valid stand-alone float, or the next + /// byte must already have been peeked and known to be `b'e' | b'E'`. + fn lex_float_exponent(&mut self, start_col: usize) -> PyResult { + if !matches!(self.peek_byte()?, Some(b'e' | b'E')) { + self.expect_word_boundary("a float", start_col)?; + return Ok(TokenType::Real); + } + // Consume the rest of the exponent. + self.next_byte()?; + if let Some(b'+' | b'-') = self.peek_byte()? { + self.next_byte()?; + } + // Exponents must have at least one digit in them. + if !matches!(self.peek_byte()?, Some(b'0'..=b'9')) { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, start_col)), + "needed to see an integer exponent for this float", + ))); + } + while let Some(b'0'..=b'9') = self.peek_byte()? { + self.next_byte()?; + } + self.expect_word_boundary("a float", start_col)?; + Ok(TokenType::Real) + } + + /// Lex a numeric token completely. This can return a successful integer or a real number; the + /// function distinguishes based on what it sees. If `self.try_version`, this can also be a + /// version identifier (will take precedence over either other type, if possible). + fn lex_numeric(&mut self, start_col: usize) -> PyResult { + let first = self.line_buffer[start_col]; + if first == b'.' { + return match self.next_byte()? { + // In the case of a float that begins with '.', we require at least one digit, so + // just force consume it and then loop over the rest. + Some(b'0'..=b'9') => { + while let Some(b'0'..=b'9') = self.peek_byte()? { + self.next_byte()?; + } + self.lex_float_exponent(start_col) + } + _ => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, start_col)), + "expected a numeric fractional part after the bare decimal point", + ))), + }; + } + while let Some(b'0'..=b'9') = self.peek_byte()? { + self.next_byte()?; + } + match self.peek_byte()? { + Some(b'.') => { + self.next_byte()?; + let mut has_fractional = false; + while let Some(b'0'..=b'9') = self.peek_byte()? { + has_fractional = true; + self.next_byte()?; + } + if self.try_version + && has_fractional + && !matches!(self.peek_byte()?, Some(b'e' | b'E')) + { + self.expect_word_boundary("a version identifier", start_col)?; + return Ok(TokenType::Version); + } + return self.lex_float_exponent(start_col); + } + // In this situation, what we've lexed so far is an integer (maybe with leading + // zeroes), but it can still be a float if it's followed by an exponent. This + // particular path is not technically within the spec (so should be subject to `strict` + // mode), but pragmatically that's more just a nuisance for OQ2 generators, since many + // languages will happily spit out something like `5e-5` when formatting floats. + Some(b'e' | b'E') => { + return if self.strict { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, start_col)), + "[strict] all floats must include a decimal point", + ))) + } else { + self.lex_float_exponent(start_col) + } + } + _ => (), + } + if first == b'0' && self.col - start_col > 1 { + // Integers can't start with a leading zero unless they are only the single '0', but we + // didn't see a decimal point. + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, start_col)), + "integers cannot have leading zeroes", + ))) + } else if self.try_version { + self.expect_word_boundary("a version identifier", start_col)?; + Ok(TokenType::Version) + } else { + self.expect_word_boundary("an integer", start_col)?; + Ok(TokenType::Integer) + } + } + + /// Lex a text-like token into a complete token. This can return any of the keyword-like + /// tokens (e.g. [TokenType::Pi]), or a [TokenType::Id] if the token is not a built-in keyword. + fn lex_textlike(&mut self, start_col: usize) -> PyResult { + let first = self.line_buffer[start_col]; + while let Some(b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9' | b'_') = self.peek_byte()? { + self.next_byte()?; + } + // No need to expect the word boundary after this, because it's the same check as above. + let text = &self.line_buffer[start_col..self.col]; + if let b'A'..=b'Z' = first { + match text { + b"OPENQASM" => Ok(TokenType::OpenQASM), + b"U" | b"CX" => Ok(TokenType::Id), + _ => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, start_col)), + "identifiers cannot start with capital letters except for the builtins 'U' and 'CX'"))), + } + } else { + match text { + b"barrier" => Ok(TokenType::Barrier), + b"cos" => Ok(TokenType::Cos), + b"creg" => Ok(TokenType::Creg), + b"exp" => Ok(TokenType::Exp), + b"gate" => Ok(TokenType::Gate), + b"if" => Ok(TokenType::If), + b"include" => Ok(TokenType::Include), + b"ln" => Ok(TokenType::Ln), + b"measure" => Ok(TokenType::Measure), + b"opaque" => Ok(TokenType::Opaque), + b"qreg" => Ok(TokenType::Qreg), + b"reset" => Ok(TokenType::Reset), + b"sin" => Ok(TokenType::Sin), + b"sqrt" => Ok(TokenType::Sqrt), + b"tan" => Ok(TokenType::Tan), + b"pi" => Ok(TokenType::Pi), + _ => Ok(TokenType::Id), + } + } + } + + /// Lex a filename token completely. This is always triggered by seeing a `b'"'` byte in the + /// input stream. + fn lex_filename(&mut self, terminator: u8, start_col: usize) -> PyResult { + loop { + match self.next_byte()? { + None => { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, start_col)), + "unexpected end-of-file while lexing string literal", + ))) + } + Some(b'\n' | b'\r') => { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, start_col)), + "unexpected line break while lexing string literal", + ))) + } + Some(c) if c == terminator => { + return Ok(TokenType::Filename); + } + Some(_) => (), + } + } + } + + /// The actual core of the iterator. Read from the stream (ignoring preceding whitespace) + /// until a complete [Token] has been constructed, or the end of the iterator is reached. This + /// returns `Some` for all tokens, including the error token, and only returns `None` if there + /// are no more tokens left to take. + fn next_inner(&mut self, context: &mut TokenContext) -> PyResult> { + // Consume preceding whitespace. Beware that this can still exhaust the underlying stream, + // or scan through an invalid token in the encoding. + loop { + match self.peek_byte()? { + Some(b' ' | b'\t' | b'\r' | b'\n') => { + self.next_byte()?; + } + None => return Ok(None), + _ => break, + } + } + let start_col = self.col; + // The whitespace loop (or [Self::try_lex_version]) has already peeked the next token, so + // we know it's going to be the `Some` variant. + let ttype = match self.next_byte()?.unwrap() { + b'+' => TokenType::Plus, + b'*' => TokenType::Asterisk, + b'^' => TokenType::Caret, + b';' => TokenType::Semicolon, + b',' => TokenType::Comma, + b'(' => TokenType::LParen, + b')' => TokenType::RParen, + b'[' => TokenType::LBracket, + b']' => TokenType::RBracket, + b'{' => TokenType::LBrace, + b'}' => TokenType::RBrace, + b'/' => { + if let Some(b'/') = self.peek_byte()? { + self.advance_line()?; + return self.next(context); + } else { + TokenType::Slash + } + } + b'-' => { + if let Ok(Some(b'>')) = self.peek_byte() { + self.col += 1; + TokenType::Arrow + } else { + TokenType::Minus + } + } + b'=' => { + if let Ok(Some(b'=')) = self.peek_byte() { + self.col += 1; + TokenType::Equals + } else { + return Err(QASM2ParseError::new_err( + "single equals '=' is never valid".to_owned(), + )); + } + } + b'0'..=b'9' | b'.' => self.lex_numeric(start_col)?, + b'a'..=b'z' | b'A'..=b'Z' => self.lex_textlike(start_col)?, + c @ (b'"' | b'\'') => { + if self.strict && c != b'"' { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, start_col)), + "[strict] paths must be in double quotes (\"\")", + ))); + } else { + self.lex_filename(c, start_col)? + } + } + c => { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new(&self.filename, self.line, start_col)), + &format!( + "encountered '{}', which doesn't match any valid tokens", + // Non-ASCII bytes should already have been rejected by `next_byte()`. + c as char, + ), + ))); + } + }; + self.try_version = ttype == TokenType::OpenQASM; + Ok(Some(Token { + ttype, + line: self.line, + col: start_col, + index: if ttype.variable_text() { + context.index(&self.line_buffer[start_col..self.col]) + } else { + usize::MAX + }, + })) + } + + /// Get an optional reference to the next token in the iterator stream without consuming it. + /// This is a direct analogue of the same method on the [std::iter::Peekable] struct, except it + /// is manually defined here to avoid hiding the rest of the public fields of the [TokenStream] + /// struct itself. + pub fn peek(&mut self, context: &mut TokenContext) -> PyResult> { + if self.peeked.is_none() { + self.peeked = Some(self.next_inner(context)?); + } + Ok(self.peeked.as_ref().unwrap().as_ref()) + } + + pub fn next(&mut self, context: &mut TokenContext) -> PyResult> { + match self.peeked.take() { + Some(token) => Ok(token), + None => self.next_inner(context), + } + } +} diff --git a/crates/qasm2/src/lib.rs b/crates/qasm2/src/lib.rs new file mode 100644 index 000000000000..1ae817364d11 --- /dev/null +++ b/crates/qasm2/src/lib.rs @@ -0,0 +1,132 @@ +// 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 pyo3::prelude::*; +use pyo3::Python; + +mod bytecode; +mod error; +mod expr; +mod lex; +mod parse; + +/// Information about a custom instruction that Python space is able to construct to pass down to +/// us. +#[pyclass] +#[derive(Clone)] +pub struct CustomInstruction { + pub name: String, + pub num_params: usize, + pub num_qubits: usize, + pub builtin: bool, +} + +#[pymethods] +impl CustomInstruction { + #[new] + fn __new__(name: String, num_params: usize, num_qubits: usize, builtin: bool) -> Self { + Self { + name, + num_params, + num_qubits, + builtin, + } + } +} + +/// Information about a custom classical function that should be defined in mathematical +/// expressions. +/// +/// The given `callable` must be a Python function that takes `num_params` floats, and returns a +/// float. The `name` is the identifier that refers to it in the OpenQASM 2 program. This cannot +/// clash with any defined gates. +#[pyclass(text_signature = "(name, num_params, callable, /)")] +#[derive(Clone)] +pub struct CustomClassical { + pub name: String, + pub num_params: usize, + pub callable: PyObject, +} + +#[pymethods] +impl CustomClassical { + #[new] + fn __new__(name: String, num_params: usize, callable: PyObject) -> Self { + Self { + name, + num_params, + callable, + } + } +} + +/// Create a bytecode iterable from a string containing an OpenQASM 2 program. The iterable will +/// lex and parse the source lazily; evaluating OpenQASM 2 statements as required, without loading +/// the entire token and parse tree into memory at once. +#[pyfunction] +fn bytecode_from_string( + string: String, + include_path: Vec, + custom_instructions: Vec, + custom_classical: Vec, + strict: bool, +) -> PyResult { + bytecode::BytecodeIterator::new( + lex::TokenStream::from_string(string, strict), + include_path, + &custom_instructions, + &custom_classical, + strict, + ) +} + +/// Create a bytecode iterable from a path to a file containing an OpenQASM 2 program. The +/// iterable will lex and parse the source lazily; evaluating OpenQASM 2 statements as required, +/// without loading the entire token and parse tree into memory at once. +#[pyfunction] +fn bytecode_from_file( + path: std::ffi::OsString, + include_path: Vec, + custom_instructions: Vec, + custom_classical: Vec, + strict: bool, +) -> PyResult { + bytecode::BytecodeIterator::new( + lex::TokenStream::from_path(path, strict)?, + include_path, + &custom_instructions, + &custom_classical, + strict, + ) +} + +/// An interface to the Rust components of the parser stack, and the types it uses to represent the +/// output. The principal entry points for Python are :func:`bytecode_from_string` and +/// :func:`bytecode_from_file`, which produce iterables of :class:`Bytecode` objects. +#[pymodule] +fn _qasm2(py: Python<'_>, module: &PyModule) -> PyResult<()> { + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add_class::()?; + module.add("QASM2ParseError", py.get_type::())?; + module.add_function(wrap_pyfunction!(bytecode_from_string, module)?)?; + module.add_function(wrap_pyfunction!(bytecode_from_file, module)?)?; + Ok(()) +} diff --git a/crates/qasm2/src/parse.rs b/crates/qasm2/src/parse.rs new file mode 100644 index 000000000000..9960258074a0 --- /dev/null +++ b/crates/qasm2/src/parse.rs @@ -0,0 +1,1801 @@ +// 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. + +//! The core of the parsing algorithm. This module contains the core logic for the +//! recursive-descent parser, which handles all statements of OpenQASM 2. In places where we have +//! to evaluate a mathematical expression on parameters, we instead swap to a short-lived +//! operator-precedence parser. + +use hashbrown::{HashMap, HashSet}; +use pyo3::prelude::{PyObject, PyResult, Python}; + +use crate::bytecode::InternalBytecode; +use crate::error::{ + message_bad_eof, message_generic, message_incorrect_requirement, Position, QASM2ParseError, +}; +use crate::expr::{Expr, ExprParser}; +use crate::lex::{Token, TokenContext, TokenStream, TokenType, Version}; +use crate::{CustomClassical, CustomInstruction}; + +/// The number of gates that are built in to the OpenQASM 2 language. This is U and CX. +const N_BUILTIN_GATES: usize = 2; +/// The "qelib1.inc" special include. The elements of the tuple are the gate name, the number of +/// parameters it takes, and the number of qubits it acts on. +const QELIB1: [(&str, usize, usize); 23] = [ + ("u3", 3, 1), + ("u2", 2, 1), + ("u1", 1, 1), + ("cx", 0, 2), + ("id", 0, 1), + ("x", 0, 1), + ("y", 0, 1), + ("z", 0, 1), + ("h", 0, 1), + ("s", 0, 1), + ("sdg", 0, 1), + ("t", 0, 1), + ("tdg", 0, 1), + ("rx", 1, 1), + ("ry", 1, 1), + ("rz", 1, 1), + ("cz", 0, 2), + ("cy", 0, 2), + ("ch", 0, 2), + ("ccx", 0, 3), + ("crz", 1, 2), + ("cu1", 1, 2), + ("cu3", 3, 2), +]; + +const BUILTIN_CLASSICAL: [&str; 6] = ["cos", "exp", "ln", "sin", "sqrt", "tan"]; + +/// Define a simple newtype that just has a single non-public `usize` field, has a `new` +/// constructor, and implements `Copy` and `IntoPy`. The first argument is the name of the type, +/// the second is whether to also define addition to make offsetting the newtype easier. +macro_rules! newtype_id { + ($id:ident, false) => { + #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] + pub struct $id(usize); + + impl $id { + pub fn new(value: usize) -> Self { + Self(value) + } + } + + impl pyo3::IntoPy for $id { + fn into_py(self, py: Python<'_>) -> PyObject { + self.0.into_py(py) + } + } + }; + + ($id:ident, true) => { + newtype_id!($id, false); + + impl std::ops::Add for $id { + type Output = Self; + + fn add(self, rhs: usize) -> Self { + Self::new(self.0 + rhs) + } + } + }; +} + +newtype_id!(GateId, false); +newtype_id!(CregId, false); +newtype_id!(ParamId, false); +newtype_id!(QubitId, true); +newtype_id!(ClbitId, true); + +/// A symbol in the global symbol table. Parameters and individual qubits can't be in the global +/// symbol table, as there is no way for them to be defined. +pub enum GlobalSymbol { + Qreg { + size: usize, + start: QubitId, + }, + Creg { + size: usize, + start: ClbitId, + index: CregId, + }, + Gate { + num_params: usize, + num_qubits: usize, + index: GateId, + }, + Classical { + callable: PyObject, + num_params: usize, + }, +} + +impl GlobalSymbol { + pub fn describe(&self) -> &'static str { + match self { + Self::Qreg { .. } => "a quantum register", + Self::Creg { .. } => "a classical register", + Self::Gate { .. } => "a gate", + Self::Classical { .. } => "a custom classical function", + } + } +} + +/// Information about a gate that permits a new definition in the OQ3 file only in the case that +/// the number of parameters and qubits matches. This is used when the user specifies custom gates +/// that are not +struct OverridableGate { + num_params: usize, + num_qubits: usize, + index: GateId, +} + +impl From for GlobalSymbol { + fn from(value: OverridableGate) -> Self { + Self::Gate { + num_params: value.num_params, + num_qubits: value.num_qubits, + index: value.index, + } + } +} + +/// A symbol in the scope of a single gate definition. This only includes the symbols that are +/// specifically gate-scoped. The rest are part of [GlobalSymbol]. +pub enum GateSymbol { + Qubit { index: QubitId }, + Parameter { index: ParamId }, +} + +impl GateSymbol { + pub fn describe(&self) -> &'static str { + match self { + Self::Qubit { .. } => "a qubit", + Self::Parameter { .. } => "a parameter", + } + } +} + +/// An operand for an instruction. This can be both quantum or classical. Classical operands only +/// occur in the `measure` operation. `Single` variants are what we mostly expect to see; these +/// happen in gate definitions (always), and in regular applications when registers are indexed. +/// The `Range` operand only occurs when a register is used as an operation target. +enum Operand { + Single(T), + Range(usize, T), +} + +/// The available types for the arrays of parameters in a gate call. The `Constant` variant is for +/// general applications, whereas the more general `Expression` variant is for gate bodies, where +/// there might be mathematics occurring on symbolic parameters. We have the special case for the +/// far more common `Constant` form; in this case we immediately unwrap the result of the +/// `ExprParser`, and we won't have to make a new vector with the conversion later. +enum GateParameters { + Constant(Vec), + Expression(Vec), +} + +/// An equality condition from an `if` statement. These can condition gate applications, measures +/// and resets, although in practice they're basically only ever used on gates. +struct Condition { + creg: CregId, + value: usize, +} + +/// Find the first match for the partial [filename] in the directories along [path]. Returns +/// `None` if the cannot be found. +fn find_include_path( + filename: &std::path::Path, + path: &[std::path::PathBuf], +) -> Option { + for directory in path.iter() { + let mut absolute_path = directory.clone(); + absolute_path.push(filename); + if absolute_path.is_file() { + return Some(absolute_path); + } + } + None +} + +/// The state of the parser (but not its output). This struct is opaque to the rest of the +/// program; only its associated functions ever need to modify its internals. The counts of +/// qubits, clbits, classical registers and gates are necessary to efficiently assign index keys to +/// new symbols as they arise. We don't need to track quantum registers like this because no part +/// of the output instruction set requires a reference to a quantum register, since we resolve any +/// broadcast gate applications from within Rust. +pub struct State { + tokens: Vec, + /// The context object that owns all the text strings that back the tokens seen so far. This + /// needs to be given as a read-only reference to the [Token] methods that extract information + /// based on the text they came from. + context: TokenContext, + include_path: Vec, + /// Mapping of name to global-scoped symbols. + symbols: HashMap, + /// Mapping of name to gate-scoped symbols. This object only logically lasts for the duration + /// of the parsing of one gate definition, but we can save allocations by re-using the same + /// object between calls. + gate_symbols: HashMap, + /// Gate names that can accept a definition, even if they are already in the symbol table as + /// gates. For example, if a user defines a custom gate as a `builtin`, we don't error out if + /// we see a compatible definition later. Regardless of whether the symbol was already in the + /// symbol table or not, any gate-based definition of this symbol must match the signature we + /// expect for it. + overridable_gates: HashMap, + num_qubits: usize, + num_clbits: usize, + num_cregs: usize, + num_gates: usize, + /// Whether a version statement is allowed in this position. + allow_version: bool, + /// Whether we're in strict mode or (the default) more permissive parse. + strict: bool, +} + +impl State { + /// Create and initialise a state for the parser. + pub fn new( + tokens: TokenStream, + include_path: Vec, + custom_instructions: &[CustomInstruction], + custom_classical: &[CustomClassical], + strict: bool, + ) -> PyResult { + let mut state = State { + tokens: vec![tokens], + context: TokenContext::new(), + include_path, + // For Qiskit-created circuits, all files will have the builtin gates and `qelib1.inc`, + // so we allocate with that in mind. There may well be overlap between libraries and + // custom instructions, but this is small-potatoes allocation and we'd rather not have + // to reallocate. + symbols: HashMap::with_capacity( + N_BUILTIN_GATES + QELIB1.len() + custom_instructions.len() + custom_classical.len(), + ), + gate_symbols: HashMap::new(), + overridable_gates: HashMap::new(), + num_qubits: 0, + num_clbits: 0, + num_cregs: 0, + num_gates: 0, + allow_version: true, + strict, + }; + for inst in custom_instructions { + if state.symbols.contains_key(&inst.name) + || state.overridable_gates.contains_key(&inst.name) + { + return Err(QASM2ParseError::new_err(message_generic( + None, + &format!("duplicate custom instruction '{}'", inst.name), + ))); + } + state.overridable_gates.insert( + inst.name.clone(), + OverridableGate { + num_params: inst.num_params, + num_qubits: inst.num_qubits, + index: GateId::new(state.num_gates), + }, + ); + if inst.builtin { + state.symbols.insert( + inst.name.clone(), + GlobalSymbol::Gate { + num_params: inst.num_params, + num_qubits: inst.num_qubits, + index: GateId::new(state.num_gates), + }, + ); + } + state.num_gates += 1; + } + state.define_gate(None, "U".to_owned(), 3, 1)?; + state.define_gate(None, "CX".to_owned(), 0, 2)?; + for classical in custom_classical { + if BUILTIN_CLASSICAL.contains(&&*classical.name) { + return Err(QASM2ParseError::new_err(message_generic( + None, + &format!( + "cannot override builtin classical function '{}'", + &classical.name + ), + ))); + } + match state.symbols.insert( + classical.name.clone(), + GlobalSymbol::Classical { + num_params: classical.num_params, + callable: classical.callable.clone(), + }, + ) { + Some(GlobalSymbol::Gate { .. }) => { + let message = match classical.name.as_str() { + "U" | "CX" => format!( + "custom classical instructions cannot shadow built-in gates, but got '{}'", + &classical.name, + ), + _ => format!( + "custom classical instruction '{}' has a naming clash with a custom gate", + &classical.name, + ), + }; + return Err(QASM2ParseError::new_err(message_generic(None, &message))); + } + Some(GlobalSymbol::Classical { .. }) => { + return Err(QASM2ParseError::new_err(message_generic( + None, + &format!("duplicate custom classical function '{}'", &classical.name,), + ))); + } + _ => (), + } + } + Ok(state) + } + + /// Get the next token available in the stack of token streams, popping and removing any + /// complete streams, except the base case. Will only return `None` once all streams are + /// exhausted. + fn next_token(&mut self) -> PyResult> { + let mut pointer = self.tokens.len() - 1; + while pointer > 0 { + let out = self.tokens[pointer].next(&mut self.context)?; + if out.is_some() { + return Ok(out); + } + self.tokens.pop(); + pointer -= 1; + } + self.tokens[0].next(&mut self.context) + } + + /// Peek the next token in the stack of token streams. This does not remove any complete + /// streams yet. Will only return `None` once all streams are exhausted. + fn peek_token(&mut self) -> PyResult> { + let mut pointer = self.tokens.len() - 1; + while pointer > 0 && self.tokens[pointer].peek(&mut self.context)?.is_none() { + pointer -= 1; + } + self.tokens[pointer].peek(&mut self.context) + } + + /// Get the filename associated with the currently active token stream. + fn current_filename(&self) -> &std::ffi::OsStr { + &self.tokens[self.tokens.len() - 1].filename + } + + /// Take a token from the stream that is known to be present and correct, generally because it + /// has already been peeked. Panics if the token type is not correct. + fn expect_known(&mut self, expected: TokenType) -> Token { + let out = self.next_token().unwrap().unwrap(); + if out.ttype != expected { + panic!( + "expected '{}' but got '{}'", + expected.describe(), + out.ttype.describe() + ) + } + out + } + + /// Take the next token from the stream, expecting that it is of a particular type because it + /// is required to be in order for the input program to be valid OpenQASM 2. This returns the + /// token if successful, and a suitable error message if the token type is incorrect, or the + /// end of the file is reached. + fn expect(&mut self, expected: TokenType, required: &str, cause: &Token) -> PyResult { + let token = match self.next_token()? { + None => { + return Err(QASM2ParseError::new_err(message_bad_eof( + Some(&Position::new( + self.current_filename(), + cause.line, + cause.col, + )), + required, + ))) + } + Some(token) => token, + }; + if token.ttype == expected { + Ok(token) + } else { + Err(QASM2ParseError::new_err(message_incorrect_requirement( + required, + &token, + self.current_filename(), + ))) + } + } + + /// Take the next token from the stream, if it is of the correct type. Returns `None` and + /// leaves the next token in the underlying iterator if it does not match. + fn accept(&mut self, expected: TokenType) -> PyResult> { + let peeked = self.peek_token()?; + if peeked.is_some() && peeked.unwrap().ttype == expected { + self.next_token() + } else { + Ok(None) + } + } + + /// True if the next token in the stream matches the given type, and false if it doesn't. + fn next_is(&mut self, expected: TokenType) -> PyResult { + let peeked = self.peek_token()?; + Ok(peeked.is_some() && peeked.unwrap().ttype == expected) + } + + /// If in `strict` mode, and we have a trailing comma, emit a suitable error message. + fn check_trailing_comma(&self, comma: Option<&Token>) -> PyResult<()> { + match (self.strict, comma) { + (true, Some(token)) => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + "[strict] trailing commas in parameter and qubit lists are forbidden", + ))), + _ => Ok(()), + } + } + + /// Take a complete quantum argument from the token stream, if the next token is an identifier. + /// This includes resolving any following subscript operation. Returns an error variant if the + /// next token _is_ an identifier, but the symbol represents something other than a quantum + /// register, or isn't defined. This can also be an error if the subscript is opened, but + /// cannot be completely resolved due to a typing error or other invalid parse. `Ok(None)` is + /// returned if the next token in the stream does not match a possible quantum argument. + fn accept_qarg(&mut self) -> PyResult>> { + let (name, name_token) = match self.accept(TokenType::Id)? { + None => return Ok(None), + Some(token) => (token.id(&self.context), token), + }; + let (register_size, register_start) = match self.symbols.get(&name) { + Some(GlobalSymbol::Qreg { size, start }) => (*size, *start), + Some(symbol) => { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!( + "'{}' is {}, not a quantum register", + name, + symbol.describe() + ), + ))) + } + None => { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!("'{}' is not defined in this scope", name), + ))) + } + }; + self.complete_operand(&name, register_size, register_start) + .map(Some) + } + + /// Take a complete quantum argument from the stream, if it matches. This is for use within + /// gates, and so the only valid type of quantum argument is a single qubit. + fn accept_qarg_gate(&mut self) -> PyResult>> { + let (name, name_token) = match self.accept(TokenType::Id)? { + None => return Ok(None), + Some(token) => (token.id(&self.context), token), + }; + match self.gate_symbols.get(&name) { + Some(GateSymbol::Qubit { index }) => Ok(Some(Operand::Single(*index))), + Some(GateSymbol::Parameter { .. }) => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!("'{}' is a parameter, not a qubit", name), + ))), + None => { + if let Some(symbol) = self.symbols.get(&name) { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!("'{}' is {}, not a qubit", name, symbol.describe()), + ))) + } else { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!("'{}' is not defined in this scope", name), + ))) + } + } + } + } + + /// Take a complete quantum argument from the token stream, returning an error message if one + /// is not present. + fn require_qarg(&mut self, instruction: &Token) -> PyResult> { + match self.peek_token()?.map(|tok| tok.ttype) { + Some(TokenType::Id) => self.accept_qarg().map(Option::unwrap), + Some(_) => { + let token = self.next_token()?; + Err(QASM2ParseError::new_err(message_incorrect_requirement( + "a quantum argument", + &token.unwrap(), + self.current_filename(), + ))) + } + None => Err(QASM2ParseError::new_err(message_bad_eof( + Some(&Position::new( + self.current_filename(), + instruction.line, + instruction.col, + )), + "a quantum argument", + ))), + } + } + + /// Take a complete classical argument from the token stream, if the next token is an + /// identifier. This includes resolving any following subscript operation. Returns an error + /// variant if the next token _is_ an identifier, but the symbol represents something other + /// than a classical register, or isn't defined. This can also be an error if the subscript is + /// opened, but cannot be completely resolved due to a typing error or other invalid parse. + /// `Ok(None)` is returned if the next token in the stream does not match a possible classical + /// argument. + fn accept_carg(&mut self) -> PyResult>> { + let (name, name_token) = match self.accept(TokenType::Id)? { + None => return Ok(None), + Some(token) => (token.id(&self.context), token), + }; + let (register_size, register_start) = match self.symbols.get(&name) { + Some(GlobalSymbol::Creg { size, start, .. }) => (*size, *start), + Some(symbol) => { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!( + "'{}' is {}, not a classical register", + name, + symbol.describe() + ), + ))) + } + None => { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!("'{}' is not defined in this scope", name), + ))) + } + }; + self.complete_operand(&name, register_size, register_start) + .map(Some) + } + + /// Take a complete classical argument from the token stream, returning an error message if one + /// is not present. + fn require_carg(&mut self, instruction: &Token) -> PyResult> { + match self.peek_token()?.map(|tok| tok.ttype) { + Some(TokenType::Id) => self.accept_carg().map(Option::unwrap), + Some(_) => { + let token = self.next_token()?; + Err(QASM2ParseError::new_err(message_incorrect_requirement( + "a classical argument", + &token.unwrap(), + self.current_filename(), + ))) + } + None => Err(QASM2ParseError::new_err(message_bad_eof( + Some(&Position::new( + self.current_filename(), + instruction.line, + instruction.col, + )), + "a classical argument", + ))), + } + } + + /// Evaluate a possible subscript on a register into a final [Operand], consuming the tokens + /// (if present) from the stream. Can return error variants if the subscript cannot be + /// completed or if there is a parse error while reading the subscript. + fn complete_operand( + &mut self, + name: &str, + register_size: usize, + register_start: T, + ) -> PyResult> + where + T: std::ops::Add, + { + let lbracket_token = match self.accept(TokenType::LBracket)? { + Some(token) => token, + None => return Ok(Operand::Range(register_size, register_start)), + }; + let index_token = self.expect(TokenType::Integer, "an integer index", &lbracket_token)?; + let index = index_token.int(&self.context); + self.expect(TokenType::RBracket, "a closing bracket", &lbracket_token)?; + if index < register_size { + Ok(Operand::Single(register_start + index)) + } else { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + index_token.line, + index_token.col, + )), + &format!( + "index {} is out-of-range for register '{}' of size {}", + index, name, register_size + ), + ))) + } + } + + /// Parse an `OPENQASM ;` statement completely. This function does not need to take + /// the bytecode stream because the version information has no actionable effects for Qiskit + /// to care about. We simply error if the version supplied by the file is not the version of + /// OpenQASM that we are able to support. This assumes that the `OPENQASM` token is still in + /// the stream. + fn parse_version(&mut self) -> PyResult { + let openqasm_token = self.expect_known(TokenType::OpenQASM); + let version_token = self.expect(TokenType::Version, "version number", &openqasm_token)?; + match version_token.version(&self.context) { + Version { + major: 2, + minor: Some(0) | None, + } => Ok(()), + _ => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + version_token.line, + version_token.col, + )), + &format!( + "can only handle OpenQASM 2.0, but given {}", + version_token.text(&self.context), + ), + ))), + }?; + self.expect(TokenType::Semicolon, ";", &openqasm_token)?; + Ok(0) + } + + /// Parse a complete gate definition (including the body of the definition). This assumes that + /// the `gate` token is still in the scheme. This function will likely result in many + /// instructions being pushed onto the bytecode stream; one for the start and end of the gate + /// definition, and then one instruction each for the gate applications in the body. + fn parse_gate_definition(&mut self, bc: &mut Vec>) -> PyResult { + let gate_token = self.expect_known(TokenType::Gate); + let name_token = self.expect(TokenType::Id, "an identifier", &gate_token)?; + let name = name_token.id(&self.context); + // Parse the gate parameters (if any) into the symbol take. + let mut num_params = 0usize; + if let Some(lparen_token) = self.accept(TokenType::LParen)? { + let mut comma = None; + while let Some(param_token) = self.accept(TokenType::Id)? { + let param_name = param_token.id(&self.context); + if let Some(symbol) = self.gate_symbols.insert( + param_name.to_owned(), + GateSymbol::Parameter { + index: ParamId::new(num_params), + }, + ) { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + param_token.line, + param_token.col, + )), + &format!( + "'{}' is already defined as {}", + param_name, + symbol.describe() + ), + ))); + } + num_params += 1; + comma = self.accept(TokenType::Comma)?; + if comma.is_none() { + break; + } + } + self.check_trailing_comma(comma.as_ref())?; + self.expect(TokenType::RParen, "a closing parenthesis", &lparen_token)?; + } + // Parse the quantum parameters into the symbol table. + let mut num_qubits = 0usize; + let mut comma = None; + while let Some(qubit_token) = self.accept(TokenType::Id)? { + let qubit_name = qubit_token.id(&self.context).to_owned(); + if let Some(symbol) = self.gate_symbols.insert( + qubit_name.to_owned(), + GateSymbol::Qubit { + index: QubitId::new(num_qubits), + }, + ) { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + qubit_token.line, + qubit_token.col, + )), + &format!( + "'{}' is already defined as {}", + qubit_name, + symbol.describe() + ), + ))); + } + num_qubits += 1; + comma = self.accept(TokenType::Comma)?; + if comma.is_none() { + break; + } + } + self.check_trailing_comma(comma.as_ref())?; + if num_qubits == 0 { + let eof = self.peek_token()?.is_none(); + let position = Position::new(self.current_filename(), gate_token.line, gate_token.col); + return if eof { + Err(QASM2ParseError::new_err(message_bad_eof( + Some(&position), + "a qubit identifier", + ))) + } else { + Err(QASM2ParseError::new_err(message_generic( + Some(&position), + "gates must act on at least one qubit", + ))) + }; + } + let lbrace_token = self.expect(TokenType::LBrace, "a gate body", &gate_token)?; + bc.push(Some(InternalBytecode::DeclareGate { + name: name.clone(), + num_qubits, + })); + // The actual body of the gate. Most of this is devolved to [Self::parse_gate_application] + // to do the right thing. + let mut statements = 0usize; + loop { + match self.peek_token()?.map(|tok| tok.ttype) { + Some(TokenType::Id) => statements += self.parse_gate_application(bc, None, true)?, + Some(TokenType::Barrier) => { + statements += self.parse_barrier(bc, Some(num_qubits))? + } + Some(TokenType::RBrace) => { + self.expect_known(TokenType::RBrace); + break; + } + Some(_) => { + let token = self.next_token()?.unwrap(); + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + &format!( + "only gate applications are valid within a 'gate' body, but saw {}", + token.text(&self.context) + ), + ))); + } + None => { + return Err(QASM2ParseError::new_err(message_bad_eof( + Some(&Position::new( + self.current_filename(), + lbrace_token.line, + lbrace_token.col, + )), + "a closing brace '}' of the gate body", + ))) + } + } + } + bc.push(Some(InternalBytecode::EndDeclareGate {})); + self.gate_symbols.clear(); + self.define_gate(Some(&gate_token), name, num_params, num_qubits)?; + Ok(statements + 2) + } + + /// Parse an `opaque` statement. This assumes that the `opaque` token is still in the token + /// stream we are reading from. + fn parse_opaque_definition( + &mut self, + bc: &mut Vec>, + ) -> PyResult { + let opaque_token = self.expect_known(TokenType::Opaque); + let name = self + .expect(TokenType::Id, "an identifier", &opaque_token)? + .text(&self.context) + .to_owned(); + let mut num_params = 0usize; + if let Some(lparen_token) = self.accept(TokenType::LParen)? { + let mut comma = None; + while self.accept(TokenType::Id)?.is_some() { + num_params += 1; + comma = self.accept(TokenType::Comma)?; + if comma.is_none() { + break; + } + } + self.check_trailing_comma(comma.as_ref())?; + self.expect(TokenType::RParen, "closing parenthesis", &lparen_token)?; + } + let mut num_qubits = 0usize; + let mut comma = None; + while self.accept(TokenType::Id)?.is_some() { + num_qubits += 1; + comma = self.accept(TokenType::Comma)?; + if comma.is_none() { + break; + } + } + self.check_trailing_comma(comma.as_ref())?; + self.expect(TokenType::Semicolon, ";", &opaque_token)?; + if num_qubits == 0 { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + opaque_token.line, + opaque_token.col, + )), + "gates must act on at least one qubit", + ))); + } + bc.push(Some(InternalBytecode::DeclareOpaque { + name: name.clone(), + num_qubits, + })); + self.define_gate(Some(&opaque_token), name, num_params, num_qubits)?; + Ok(1) + } + + /// Parse a gate application into the bytecode stream. This resolves any broadcast over + /// registers into a series of bytecode instructions, rather than leaving Qiskit to do it, + /// which would involve much slower Python execution. This assumes that the identifier token + /// is still in the token stream. + fn parse_gate_application( + &mut self, + bc: &mut Vec>, + condition: Option, + in_gate: bool, + ) -> PyResult { + let name_token = self.expect_known(TokenType::Id); + let name = name_token.id(&self.context); + let (index, num_params, num_qubits) = match self.symbols.get(&name) { + Some(GlobalSymbol::Gate { + num_params, + num_qubits, + index, + }) => Ok((*index, *num_params, *num_qubits)), + Some(symbol) => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!("'{}' is {}, not a gate", name, symbol.describe()), + ))), + None => { + let pos = Position::new(self.current_filename(), name_token.line, name_token.col); + let message = if self.overridable_gates.contains_key(&name) { + format!( + "cannot use non-builtin custom instruction '{}' before definition", + name, + ) + } else { + format!("'{}' is not defined in this scope", name) + }; + Err(QASM2ParseError::new_err(message_generic( + Some(&pos), + &message, + ))) + } + }?; + let parameters = self.expect_gate_parameters(&name_token, num_params, in_gate)?; + let mut qargs = Vec::>::with_capacity(num_qubits); + let mut comma = None; + if in_gate { + while let Some(qarg) = self.accept_qarg_gate()? { + qargs.push(qarg); + comma = self.accept(TokenType::Comma)?; + if comma.is_none() { + break; + } + } + } else { + while let Some(qarg) = self.accept_qarg()? { + qargs.push(qarg); + comma = self.accept(TokenType::Comma)?; + if comma.is_none() { + break; + } + } + } + self.check_trailing_comma(comma.as_ref())?; + if qargs.len() != num_qubits { + return match self.peek_token()?.map(|tok| tok.ttype) { + Some(TokenType::Semicolon) => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!( + "'{}' takes {} quantum argument{}, but got {}", + name, + num_qubits, + if num_qubits == 1 { "" } else { "s" }, + qargs.len() + ), + ))), + Some(_) => Err(QASM2ParseError::new_err(message_incorrect_requirement( + "the end of the argument list", + &name_token, + self.current_filename(), + ))), + None => Err(QASM2ParseError::new_err(message_bad_eof( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + "the end of the argument list", + ))), + }; + } + self.expect(TokenType::Semicolon, "';'", &name_token)?; + self.emit_gate_application(bc, &name_token, index, parameters, &qargs, condition) + } + + /// Parse the parameters (if any) from a gate application. + fn expect_gate_parameters( + &mut self, + name_token: &Token, + num_params: usize, + in_gate: bool, + ) -> PyResult { + let lparen_token = match self.accept(TokenType::LParen)? { + Some(lparen_token) => lparen_token, + None => { + return Ok(if in_gate { + GateParameters::Expression(vec![]) + } else { + GateParameters::Constant(vec![]) + }); + } + }; + let mut seen_params = 0usize; + let mut comma = None; + // This code duplication is to avoid duplication of allocation when parsing the far more + // common case of expecting constant parameters for a gate application in the body of the + // OQ2 file. + let parameters = if in_gate { + let mut parameters = Vec::::with_capacity(num_params); + while !self.next_is(TokenType::RParen)? { + let mut expr_parser = ExprParser { + tokens: &mut self.tokens, + context: &mut self.context, + gate_symbols: &self.gate_symbols, + global_symbols: &self.symbols, + strict: self.strict, + }; + parameters.push(expr_parser.parse_expression(&lparen_token)?); + seen_params += 1; + comma = self.accept(TokenType::Comma)?; + if comma.is_none() { + break; + } + } + self.expect(TokenType::RParen, "')'", &lparen_token)?; + GateParameters::Expression(parameters) + } else { + let mut parameters = Vec::::with_capacity(num_params); + while !self.next_is(TokenType::RParen)? { + let mut expr_parser = ExprParser { + tokens: &mut self.tokens, + context: &mut self.context, + gate_symbols: &self.gate_symbols, + global_symbols: &self.symbols, + strict: self.strict, + }; + match expr_parser.parse_expression(&lparen_token)? { + Expr::Constant(value) => parameters.push(value), + _ => { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + lparen_token.line, + lparen_token.col, + )), + "non-constant expression in program body", + ))) + } + } + seen_params += 1; + comma = self.accept(TokenType::Comma)?; + if comma.is_none() { + break; + } + } + self.expect(TokenType::RParen, "')'", &lparen_token)?; + GateParameters::Constant(parameters) + }; + self.check_trailing_comma(comma.as_ref())?; + if seen_params != num_params { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!( + "'{}' takes {} parameter{}, but got {}", + &name_token.text(&self.context), + num_params, + if num_params == 1 { "" } else { "s" }, + seen_params + ), + ))); + } + Ok(parameters) + } + + /// Emit the bytecode for the application of a gate. This involves resolving any broadcasts + /// in the operands of the gate (i.e. if one or more of them is a register). + fn emit_gate_application( + &self, + bc: &mut Vec>, + instruction: &Token, + gate_id: GateId, + parameters: GateParameters, + qargs: &[Operand], + condition: Option, + ) -> PyResult { + // Fast path for most common gate patterns that don't need broadcasting. + if let Some(qubits) = match qargs { + [Operand::Single(index)] => Some(vec![*index]), + [Operand::Single(left), Operand::Single(right)] => { + if *left == *right { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + instruction.line, + instruction.col, + )), + "duplicate qubits in gate application", + ))); + } + Some(vec![*left, *right]) + } + [] => Some(vec![]), + _ => None, + } { + return match parameters { + GateParameters::Constant(parameters) => { + self.emit_single_global_gate(bc, gate_id, parameters, qubits, &condition) + } + GateParameters::Expression(parameters) => { + self.emit_single_gate_gate(bc, gate_id, parameters, qubits) + } + }; + }; + // If we're here we either have to broadcast or it's a 3+q gate - either way, we're not as + // worried about performance. + let mut qubits = HashSet::::with_capacity(qargs.len()); + let mut broadcast_length = 0usize; + for qarg in qargs { + match qarg { + Operand::Single(index) => { + if !qubits.insert(*index) { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + instruction.line, + instruction.col, + )), + "duplicate qubits in gate application", + ))); + } + } + Operand::Range(size, start) => { + if broadcast_length != 0 && broadcast_length != *size { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + instruction.line, + instruction.col, + )), + "cannot resolve broadcast in gate application", + ))); + } + for offset in 0..*size { + if !qubits.insert(*start + offset) { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + instruction.line, + instruction.col, + )), + "duplicate qubits in gate application", + ))); + } + } + broadcast_length = *size; + } + } + } + if broadcast_length == 0 { + let qubits = qargs + .iter() + .filter_map(|qarg| { + if let Operand::Single(index) = qarg { + Some(*index) + } else { + None + } + }) + .collect::>(); + if qubits.len() < qargs.len() { + // We're broadcasting against at least one empty register. + return Ok(0); + } + return match parameters { + GateParameters::Constant(parameters) => { + self.emit_single_global_gate(bc, gate_id, parameters, qubits, &condition) + } + GateParameters::Expression(parameters) => { + self.emit_single_gate_gate(bc, gate_id, parameters, qubits) + } + }; + } + for i in 0..broadcast_length { + let qubits = qargs + .iter() + .map(|qarg| match qarg { + Operand::Single(index) => *index, + Operand::Range(_, start) => *start + i, + }) + .collect::>(); + match parameters { + GateParameters::Constant(ref parameters) => { + self.emit_single_global_gate( + bc, + gate_id, + parameters.clone(), + qubits, + &condition, + )?; + } + // Gates used in gate-body definitions can't ever broadcast, because their only + // operands are single qubits. + _ => unreachable!(), + } + } + Ok(broadcast_length) + } + + /// Emit the bytecode for a single gate application in the global scope. This could + /// potentially have a classical condition. + fn emit_single_global_gate( + &self, + bc: &mut Vec>, + gate_id: GateId, + arguments: Vec, + qubits: Vec, + condition: &Option, + ) -> PyResult { + if let Some(condition) = condition { + bc.push(Some(InternalBytecode::ConditionedGate { + id: gate_id, + arguments, + qubits, + creg: condition.creg, + value: condition.value, + })); + } else { + bc.push(Some(InternalBytecode::Gate { + id: gate_id, + arguments, + qubits, + })); + } + Ok(1) + } + + /// Emit the bytecode for a single gate application in a gate-definition body. These are not + /// allowed to be conditioned, because otherwise the containing `gate` would not be unitary. + fn emit_single_gate_gate( + &self, + bc: &mut Vec>, + gate_id: GateId, + arguments: Vec, + qubits: Vec, + ) -> PyResult { + bc.push(Some(InternalBytecode::GateInBody { + id: gate_id, + arguments, + qubits, + })); + Ok(1) + } + + /// Parse a complete conditional statement, including the operation that follows the condition + /// (though this work is delegated to the requisite other grammar rule). This assumes that the + /// `if` token is still on the token stream. + fn parse_conditional(&mut self, bc: &mut Vec>) -> PyResult { + let if_token = self.expect_known(TokenType::If); + let lparen_token = self.expect(TokenType::LParen, "'('", &if_token)?; + let name_token = self.expect(TokenType::Id, "classical register", &if_token)?; + self.expect(TokenType::Equals, "'=='", &if_token)?; + let value = self + .expect(TokenType::Integer, "an integer", &if_token)? + .int(&self.context); + self.expect(TokenType::RParen, "')'", &lparen_token)?; + let name = name_token.id(&self.context); + let creg = match self.symbols.get(&name) { + Some(GlobalSymbol::Creg { index, .. }) => Ok(*index), + Some(symbol) => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!( + "'{}' is {}, not a classical register", + name, + symbol.describe() + ), + ))), + None => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!("'{}' is not defined in this scope", name), + ))), + }?; + let condition = Some(Condition { creg, value }); + match self.peek_token()?.map(|tok| tok.ttype) { + Some(TokenType::Id) => self.parse_gate_application(bc, condition, false), + Some(TokenType::Measure) => self.parse_measure(bc, condition), + Some(TokenType::Reset) => self.parse_reset(bc, condition), + Some(_) => { + let token = self.next_token()?; + Err(QASM2ParseError::new_err(message_incorrect_requirement( + "a gate application, measurement or reset", + &token.unwrap(), + self.current_filename(), + ))) + } + None => Err(QASM2ParseError::new_err(message_bad_eof( + Some(&Position::new( + self.current_filename(), + if_token.line, + if_token.col, + )), + "a gate, measurement or reset to condition", + ))), + } + } + + /// Parse a barrier statement. This assumes that the `barrier` token is still in the token + /// stream. + fn parse_barrier( + &mut self, + bc: &mut Vec>, + num_gate_qubits: Option, + ) -> PyResult { + let barrier_token = self.expect_known(TokenType::Barrier); + let qubits = if !self.next_is(TokenType::Semicolon)? { + let mut qubits = Vec::new(); + let mut used = HashSet::::new(); + let mut comma = None; + while let Some(qarg) = if num_gate_qubits.is_some() { + self.accept_qarg_gate()? + } else { + self.accept_qarg()? + } { + match qarg { + Operand::Single(index) => { + if used.insert(index) { + qubits.push(index) + } + } + Operand::Range(size, start) => qubits.extend((0..size).filter_map(|offset| { + let index = start + offset; + if used.insert(index) { + Some(index) + } else { + None + } + })), + } + comma = self.accept(TokenType::Comma)?; + if comma.is_none() { + break; + } + } + self.check_trailing_comma(comma.as_ref())?; + qubits + } else if let Some(num_gate_qubits) = num_gate_qubits { + (0..num_gate_qubits).map(QubitId::new).collect::>() + } else { + (0..self.num_qubits).map(QubitId::new).collect::>() + }; + self.expect(TokenType::Semicolon, "';'", &barrier_token)?; + if qubits.is_empty() { + Ok(0) + } else { + // The qubits are empty iff the only operands are zero-sized registers. If there's no + // quantum arguments at all, then `qubits` will contain everything. + bc.push(Some(InternalBytecode::Barrier { qubits })); + Ok(1) + } + } + + /// Parse a measurement operation into bytecode. This resolves any broadcast in the + /// measurement statement into a series of bytecode instructions, so we do more of the work in + /// Rust space than in Python space. + fn parse_measure( + &mut self, + bc: &mut Vec>, + condition: Option, + ) -> PyResult { + let measure_token = self.expect_known(TokenType::Measure); + let qarg = self.require_qarg(&measure_token)?; + self.expect(TokenType::Arrow, "'->'", &measure_token)?; + let carg = self.require_carg(&measure_token)?; + self.expect(TokenType::Semicolon, "';'", &measure_token)?; + if let Some(Condition { creg, value }) = condition { + match (qarg, carg) { + (Operand::Single(qubit), Operand::Single(clbit)) => { + bc.push(Some(InternalBytecode::ConditionedMeasure { + qubit, + clbit, + creg, + value, + })); + Ok(1) + } + (Operand::Range(q_size, q_start), Operand::Range(c_size, c_start)) + if q_size == c_size => + { + bc.extend((0..q_size).map(|i| { + Some(InternalBytecode::ConditionedMeasure { + qubit: q_start + i, + clbit: c_start + i, + creg, + value, + }) + })); + Ok(q_size) + } + _ => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + measure_token.line, + measure_token.col, + )), + "cannot resolve broadcast in measurement", + ))), + } + } else { + match (qarg, carg) { + (Operand::Single(qubit), Operand::Single(clbit)) => { + bc.push(Some(InternalBytecode::Measure { qubit, clbit })); + Ok(1) + } + (Operand::Range(q_size, q_start), Operand::Range(c_size, c_start)) + if q_size == c_size => + { + bc.extend((0..q_size).map(|i| { + Some(InternalBytecode::Measure { + qubit: q_start + i, + clbit: c_start + i, + }) + })); + Ok(q_size) + } + _ => Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + measure_token.line, + measure_token.col, + )), + "cannot resolve broadcast in measurement", + ))), + } + } + } + + /// Parse a single reset statement. This resolves any broadcast in the statement, i.e. if the + /// target is a register rather than a single qubit. This assumes that the `reset` token is + /// still in the token stream. + fn parse_reset( + &mut self, + bc: &mut Vec>, + condition: Option, + ) -> PyResult { + let reset_token = self.expect_known(TokenType::Reset); + let qarg = self.require_qarg(&reset_token)?; + self.expect(TokenType::Semicolon, "';'", &reset_token)?; + if let Some(Condition { creg, value }) = condition { + match qarg { + Operand::Single(qubit) => { + bc.push(Some(InternalBytecode::ConditionedReset { + qubit, + creg, + value, + })); + Ok(1) + } + Operand::Range(size, start) => { + bc.extend((0..size).map(|offset| { + Some(InternalBytecode::ConditionedReset { + qubit: start + offset, + creg, + value, + }) + })); + Ok(size) + } + } + } else { + match qarg { + Operand::Single(qubit) => { + bc.push(Some(InternalBytecode::Reset { qubit })); + Ok(0) + } + Operand::Range(size, start) => { + bc.extend((0..size).map(|offset| { + Some(InternalBytecode::Reset { + qubit: start + offset, + }) + })); + Ok(size) + } + } + } + } + + /// Parse a declaration of a classical register, emitting the relevant bytecode and adding the + /// definition to the relevant parts of the internal symbol tables in the parser state. This + /// assumes that the `creg` token is still in the token stream. + fn parse_creg(&mut self, bc: &mut Vec>) -> PyResult { + let creg_token = self.expect_known(TokenType::Creg); + let name_token = self.expect(TokenType::Id, "a valid identifier", &creg_token)?; + let name = name_token.id(&self.context); + let lbracket_token = self.expect(TokenType::LBracket, "'['", &creg_token)?; + let size = self + .expect(TokenType::Integer, "an integer", &lbracket_token)? + .int(&self.context); + self.expect(TokenType::RBracket, "']'", &lbracket_token)?; + self.expect(TokenType::Semicolon, "';'", &creg_token)?; + let symbol = GlobalSymbol::Creg { + size, + start: ClbitId::new(self.num_clbits), + index: CregId::new(self.num_cregs), + }; + if self.symbols.insert(name.clone(), symbol).is_none() { + self.num_clbits += size; + self.num_cregs += 1; + bc.push(Some(InternalBytecode::DeclareCreg { name, size })); + Ok(1) + } else { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!("'{}' is already defined", name_token.id(&self.context)), + ))) + } + } + + /// Parse a declaration of a quantum register, emitting the relevant bytecode and adding the + /// definition to the relevant parts of the internal symbol tables in the parser state. This + /// assumes that the `qreg` token is still in the token stream. + fn parse_qreg(&mut self, bc: &mut Vec>) -> PyResult { + let qreg_token = self.expect_known(TokenType::Qreg); + let name_token = self.expect(TokenType::Id, "a valid identifier", &qreg_token)?; + let name = name_token.id(&self.context); + let lbracket_token = self.expect(TokenType::LBracket, "'['", &qreg_token)?; + let size = self + .expect(TokenType::Integer, "an integer", &lbracket_token)? + .int(&self.context); + self.expect(TokenType::RBracket, "']'", &lbracket_token)?; + self.expect(TokenType::Semicolon, "';'", &qreg_token)?; + let symbol = GlobalSymbol::Qreg { + size, + start: QubitId::new(self.num_qubits), + }; + if self.symbols.insert(name.clone(), symbol).is_none() { + self.num_qubits += size; + bc.push(Some(InternalBytecode::DeclareQreg { name, size })); + Ok(1) + } else { + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + name_token.line, + name_token.col, + )), + &format!("'{}' is already defined", name_token.id(&self.context)), + ))) + } + } + + /// Parse an include statement. This currently only actually handles includes of `qelib1.inc`, + /// which aren't actually parsed; the parser has a built-in version of the file that it simply + /// updates its state with (and the Python side of the parser does the same) rather than + /// re-parsing the same file every time. This assumes that the `include` token is still in the + /// token stream. + fn parse_include(&mut self, bc: &mut Vec>) -> PyResult { + let include_token = self.expect_known(TokenType::Include); + let filename_token = + self.expect(TokenType::Filename, "a filename string", &include_token)?; + self.expect(TokenType::Semicolon, "';'", &include_token)?; + let filename = filename_token.filename(&self.context); + if filename == "qelib1.inc" { + self.symbols.reserve(QELIB1.len()); + let mut indices = Vec::with_capacity(QELIB1.len()); + for (i, (name, num_params, num_qubits)) in QELIB1.iter().enumerate() { + if self.define_gate( + Some(&include_token), + name.to_string(), + *num_params, + *num_qubits, + )? { + indices.push(i); + } + } + bc.push(Some(InternalBytecode::SpecialInclude { indices })); + Ok(1) + } else { + let base_filename = std::path::PathBuf::from(&filename); + let absolute_filename = find_include_path(&base_filename, &self.include_path) + .ok_or_else(|| { + QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + filename_token.line, + filename_token.col, + )), + &format!( + "unable to find '{}' in the include search path", + base_filename.display() + ), + )) + })?; + let new_stream = + TokenStream::from_path(absolute_filename, self.strict).map_err(|err| { + QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + filename_token.line, + filename_token.col, + )), + &format!("unable to open file '{}' for reading: {}", &filename, err), + )) + })?; + self.tokens.push(new_stream); + self.allow_version = true; + Ok(0) + } + } + + /// 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 + /// handles the `qelib1.inc` include specially as well. + fn define_gate( + &mut self, + owner: Option<&Token>, + name: String, + num_params: usize, + num_qubits: usize, + ) -> PyResult { + let already_defined = |state: &Self, name: String| { + let pos = owner.map(|tok| Position::new(state.current_filename(), tok.line, tok.col)); + Err(QASM2ParseError::new_err(message_generic( + pos.as_ref(), + &format!("'{}' is already defined", name), + ))) + }; + let mismatched_definitions = |state: &Self, name: String, previous: OverridableGate| { + let plural = |count: usize, singular: &str| { + let mut out = format!("{} {}", count, singular); + if count != 1 { + out.push('s'); + } + out + }; + let from_custom = format!( + "{} and {}", + plural(previous.num_params, "parameter"), + plural(previous.num_qubits, "qubit") + ); + let from_program = format!( + "{} and {}", + plural(num_params, "parameter"), + plural(num_qubits, "qubit") + ); + let pos = owner.map(|tok| Position::new(state.current_filename(), tok.line, tok.col)); + Err(QASM2ParseError::new_err(message_generic( + pos.as_ref(), + &format!( + concat!( + "custom instruction '{}' is mismatched with its definition: ", + "OpenQASM program has {}, custom has {}", + ), + name, from_program, from_custom + ), + ))) + }; + + if let Some(symbol) = self.overridable_gates.remove(&name) { + if num_params != symbol.num_params || num_qubits != symbol.num_qubits { + return mismatched_definitions(self, name, symbol); + } + match self.symbols.get(&name) { + None => { + self.symbols.insert(name, symbol.into()); + self.num_gates += 1; + Ok(true) + } + Some(GlobalSymbol::Gate { .. }) => { + self.symbols.insert(name, symbol.into()); + Ok(false) + } + _ => already_defined(self, name), + } + } else if self.symbols.contains_key(&name) { + already_defined(self, name) + } else { + self.symbols.insert( + name, + GlobalSymbol::Gate { + num_params, + num_qubits, + index: GateId::new(self.num_gates), + }, + ); + self.num_gates += 1; + Ok(true) + } + } + + /// Parse the next OpenQASM 2 statement in the program into a series of bytecode instructions. + /// + /// This is the principal public worker function of the parser. One call to this function + /// parses a single OpenQASM 2 statement, which may expand to several bytecode instructions if + /// there is any broadcasting to resolve, or if the statement is a gate definition. A call to + /// this function that returns `Some` will always have pushed at least one instruction to the + /// bytecode stream (the number is included). A return of `None` signals the end of the + /// iterator. + pub fn parse_next( + &mut self, + bc: &mut Vec>, + ) -> PyResult> { + if self.strict && self.allow_version { + match self.peek_token()?.map(|tok| tok.ttype) { + Some(TokenType::OpenQASM) => self.parse_version(), + Some(_) => { + let token = self.next_token()?.unwrap(); + Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + "[strict] the first statement must be 'OPENQASM 2.0;'", + ))) + } + None => Err(QASM2ParseError::new_err(message_generic( + None, + "[strict] saw an empty token stream, but needed a version statement", + ))), + }?; + self.allow_version = false; + } + let allow_version = self.allow_version; + self.allow_version = false; + while let Some(ttype) = self.peek_token()?.map(|tok| tok.ttype) { + let emitted = match ttype { + TokenType::Id => self.parse_gate_application(bc, None, false)?, + TokenType::Creg => self.parse_creg(bc)?, + TokenType::Qreg => self.parse_qreg(bc)?, + TokenType::Include => self.parse_include(bc)?, + TokenType::Measure => self.parse_measure(bc, None)?, + TokenType::Reset => self.parse_reset(bc, None)?, + TokenType::Barrier => self.parse_barrier(bc, None)?, + TokenType::If => self.parse_conditional(bc)?, + TokenType::Opaque => self.parse_opaque_definition(bc)?, + TokenType::Gate => self.parse_gate_definition(bc)?, + TokenType::OpenQASM => { + if allow_version { + self.parse_version()? + } else { + let token = self.next_token()?.unwrap(); + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + "only the first statement may be a version declaration", + ))); + } + } + TokenType::Semicolon => { + let token = self.next_token()?.unwrap(); + if self.strict { + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + "[strict] empty statements and/or extra semicolons are forbidden", + ))); + } else { + 0 + } + } + _ => { + let token = self.next_token()?.unwrap(); + return Err(QASM2ParseError::new_err(message_generic( + Some(&Position::new( + self.current_filename(), + token.line, + token.col, + )), + &format!( + "needed a start-of-statement token, but instead got {}", + token.text(&self.context) + ), + ))); + } + }; + if emitted > 0 { + return Ok(Some(emitted)); + } + } + Ok(None) + } +} diff --git a/docs/apidocs/qasm2.rst b/docs/apidocs/qasm2.rst new file mode 100644 index 000000000000..1f03ffc4d171 --- /dev/null +++ b/docs/apidocs/qasm2.rst @@ -0,0 +1,6 @@ +.. _qiskit-qasm2: + +.. automodule:: qiskit.qasm2 + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/apidocs/terra.rst b/docs/apidocs/terra.rst index bac338f2c3ac..48e8fb3f27b0 100644 --- a/docs/apidocs/terra.rst +++ b/docs/apidocs/terra.rst @@ -25,6 +25,7 @@ Qiskit Terra API Reference scheduler synthesis primitives + qasm2 qasm3 qasm qobj diff --git a/pyproject.toml b/pyproject.toml index ec54165cf952..d8eebc130298 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,15 @@ before-all = "yum install -y wget && {package}/tools/install_rust.sh" environment = 'PATH="$PATH:$HOME/.cargo/bin" CARGO_NET_GIT_FETCH_WITH_CLI="true" RUSTUP_TOOLCHAIN="stable"' [tool.pylint.main] -extension-pkg-allow-list = ["retworkx", "numpy", "tweedledum", "qiskit._accelerate", "rustworkx"] +extension-pkg-allow-list = [ + "numpy", + "qiskit._accelerate", + # We can't allow pylint to load qiskit._qasm2 because it's not able to + # statically resolve the cyclical load of the exception and it bugs out. + "retworkx", + "rustworkx", + "tweedledum", +] load-plugins = ["pylint.extensions.docparams", "pylint.extensions.docstyle"] py-version = "3.7" # update it when bumping minimum supported python version diff --git a/qiskit/__init__.py b/qiskit/__init__.py index c597a0124d9c..76bb5dace4c5 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -30,10 +30,10 @@ ) -# Globally define compiled modules. The normal import mechanism will not -# find compiled submodules in _accelerate because it relies on file paths -# manually define them on import so people can directly import -# qiskit._accelerate.* submodules and not have to rely on attribute access +# 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.nlayout"] = qiskit._accelerate.nlayout sys.modules["qiskit._accelerate.stochastic_swap"] = qiskit._accelerate.stochastic_swap sys.modules["qiskit._accelerate.sabre_swap"] = qiskit._accelerate.sabre_swap diff --git a/qiskit/qasm2/__init__.py b/qiskit/qasm2/__init__.py new file mode 100644 index 000000000000..349321539435 --- /dev/null +++ b/qiskit/qasm2/__init__.py @@ -0,0 +1,525 @@ +# 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. + +r""" +================================ +OpenQASM 2 (:mod:`qiskit.qasm2`) +================================ + +Qiskit has support for interoperation with OpenQASM 2.0 programs, both parsing into Qiskit formats +and exporting back to OpenQASM 2. The parsing components live in this module, while currently the +export capabilities are limited to being the :meth:`.QuantumCircuit.qasm` method. + +.. note:: + + OpenQASM 2 is a simple language, and not suitable for general serialisation of Qiskit objects. + See :ref:`some discussion of alternatives below `, if that is what you are + looking for. + +Parsing API +=========== + +This module contains two public functions, both of which create a :class:`.QuantumCircuit` from an +OpenQASM 2 program. :func:`load` takes a filename, while :func:`loads` takes the program itself as a +string. Their internals are very similar, so both offer almost the same API. + +.. autofunction:: load + +.. autofunction:: loads + +Both of these loading functions also take an argument ``include_path``, which is an iterable of +directory names to use when searching for files in ``include`` statements. The directories are +tried from index 0 onwards, and the first match is used. The import ``qelib1.inc`` is treated +specially; it is always found before looking in the include path, and contains exactly the content +of the `paper describing the OpenQASM 2 language `__. The gates +in this include file are mapped to circuit-library gate objects defined by Qiskit. + +.. _qasm2-custom-instructions: + +Specifying custom instructions +------------------------------ + +You can extend the quantum components of the OpenQASM 2 language by passing an iterable of +information on custom instructions as the argument ``custom_instructions``. In files that have +compatible definitions for these instructions, the given ``constructor`` will be used in place of +whatever other handling :mod:`qiskit.qasm2` would have done. These instructions may optionally be +marked as ``builtin``, which causes them to not require an ``opaque`` or ``gate`` declaration, but +they will silently ignore a compatible declaration. Either way, it is an error to provide a custom +instruction that has a different number of parameters or qubits as a defined instruction in a parsed +program. Each element of the argument iterable should be a particular data class: + +.. autoclass:: CustomInstruction + +.. _qasm2-custom-classical: + +Specifying custom classical functions +------------------------------------- + +Similar to the quantum extensions above, you can also extend the processing done to classical +expressions (arguments to gates) by passing an iterable to the argument ``custom_classical`` to either +loader. This needs the ``name`` (a valid OpenQASM 2 identifier), the number ``num_params`` of +parameters it takes, and a Python callable that implements the function. The Python callable must +be able to accept ``num_params`` positional floating-point arguments, and must return a float or +integer (which will be converted to a float). Builtin functions cannot be overridden. + +.. autoclass:: CustomClassical + +.. _qasm2-strict-mode: + +Strict mode +----------- + +Both of the loader functions have an optional "strict" mode. By default, this parser is a little +bit more relaxed than the official specification: it allows trailing commas in parameter lists; +unnecessary (empty-statement) semicolons; the ``OPENQASM 2.0;`` version statement to be omitted; and +a couple of other quality-of-life improvements without emitting any errors. You can use the +letter-of-the-spec mode with ``strict=True``. + +Errors +====== + +This module defines a generic error type that derives from :exc:`.QiskitError` that can be used as a +catch when you care about failures emitted by the interoperation layer specifically. + +.. autoexception:: QASM2Error + +In cases where the lexer or parser fails due to an invalid OpenQASM 2 file, the conversion functions +will raise a more specific error with a message explaining what the failure is, and where in the +file it occurred. + +.. autoexception:: QASM2ParseError + +.. _qasm2-examples: + +Examples +======== + +Use :func:`loads` to import an OpenQASM 2 program in a string into a :class:`.QuantumCircuit`: + +.. code-block:: python + + import qiskit.qasm2 + program = ''' + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + creg c[2]; + + h q[0]; + cx q[0], q[1]; + + measure q -> c; + ''' + circuit = qiskit.qasm2.loads(program) + circuit.draw() + +.. code-block:: text + + ┌───┐ ┌─┐ + q_0: ┤ H ├──■──┤M├─── + └───┘┌─┴─┐└╥┘┌─┐ + q_1: ─────┤ X ├─╫─┤M├ + └───┘ ║ └╥┘ + c: 2/═══════════╩══╩═ + 0 1 + +You can achieve the same thing if the program is stored in a file by using :func:`load` instead, +passing the filename as an argument: + +.. code-block:: python + + import qiskit.qasm2 + circuit = qiskit.qasm2.load("myfile.qasm") + +OpenQASM 2 files can include other OpenQASM 2 files via the ``include`` statement. You can +influence the search path used for finding these files with the ``include_path`` argument to both +:func:`load` and :func:`loads`. By default, only the current working directory is searched. + +.. code-block:: python + + import qiskit.qasm2 + program = ''' + include "other.qasm"; + // ... and so on + ''' + circuit = qiskit.qasm2.loads(program, include_path=("/path/to/a", "/path/to/b", ".")) + +For :func:`load` only, there is an extra argument ``include_input_directory``, which can be used to +either ``'append'``, ``'prepend'`` or ignore (``None``) the directory of the loaded file in the +include path. By default, this directory is appended to the search path, so it is tried last, but +you can change this. + +.. code-block:: python + + import qiskit.qasm2 + filenames = ["./subdirectory/a.qasm", "/path/to/b.qasm", "~/my.qasm"] + # Search the directory of each file before other parts of the include path. + circuits = [ + qiskit.qasm2.load(filename, include_input_directory="prepend") for filename in filenames + ] + # Override the include path, and don't search the directory of each file unless it's in the + # absolute path list. + circuits = [ + qiskit.qasm2.load( + filename, + include_path=("/usr/include/qasm", "~/qasm/include"), + include_input_directory=None, + ) + for filename in filenames + ] + +Sometimes you may want to influence the :class:`.Gate` objects that the importer emits for given +named instructions. Gates that are defined by the statement ``include "qelib1.inc";`` will +automatically be associated with a suitable Qiskit circuit-library gate, but you can extend this: + +.. code-block:: python + + from qiskit.circuit import Gate + from qiskit.qasm2 import loads, CustomInstruction + + class MyGate(Gate): + def __init__(self, theta): + super().__init__("my", 2, [theta]) + + class Builtin(Gate): + def __init__(self): + super().__init__("builtin", 1, []) + + program = ''' + opaque my(theta) q1, q2; + qreg q[2]; + my(0.5) q[0], q[1]; + builtin q[0]; + ''' + customs = [ + CustomInstruction(name="my", num_params=1, num_qubits=2, constructor=MyGate), + # Setting 'builtin=True' means the instruction doesn't require a declaration to be usable. + CustomInstruction("builtin", 0, 1, Builtin, builtin=True), + ] + circuit = loads(program, custom_instructions=customs) + + +Similarly, you can add new classical functions used during the description of arguments to gates, +both in the main body of the program (which come out constant-folded) and within the bodies of +defined gates (which are computed on demand). Here we provide a Python version of ``atan2(y, x)``, +which mathematically is :math:`\atan(y/x)` but correctly handling angle quadrants and infinities, +and a custom ``add_one`` function: + +.. code-block:: python + + import math + from qiskit.qasm2 import loads, CustomClassical + + program = ''' + include "qelib1.inc"; + qreg q[2]; + rx(atan2(pi, 3 + add_one(0.2))) q[0]; + cx q[0], q[1]; + ''' + + def add_one(x): + return x + 1 + + customs = [ + # `atan2` takes two parameters, and `math.atan2` implements it. + CustomClassical("atan2", 2, math.atan2), + # Our `add_one` takes only one parameter. + CustomClassical("add_one", 1, add_one), + ] + circuit = loads(program, custom_classical=customs) + + +.. _qasm2-legacy-compatibility: + +Legacy Compatibility +==================== + +: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 +defaults implements the specification more strictly. + +The complete legacy code-paths are + +.. code-block:: python + + from qiskit.converters import ast_to_dag, dag_to_circuit + from qiskit.qasm import Qasm + + def from_qasm_file(path: str): + dag_to_circuit(ast_to_dag(Qasm(filename=path).parse())) + + def from_qasm_str(qasm_str: str): + dag_to_circuit(ast_to_dag(Qasm(data=qasm_str).parse())) + +In particular, in the legacy importers: + +* the `include_path` is effectively: + 1. ``/qasm/libs``, where ```` is the root of the installed ``qiskit`` package; + 2. the current working directory. + +* there are additional instructions defined in ``qelib1.inc``: + ``csx a, b`` + Controlled :math:`\sqrt X` gate, corresponding to :class:`.CSXGate`. + + ``cu(theta, phi, lambda, gamma) c, t`` + The four-parameter version of a controlled-:math:`U`, corresponding to :class:`.CUGate`. + + ``rxx(theta) a, b`` + Two-qubit rotation arond the :math:`XX` axis, corresponding to :class:`.RXXGate`. + + ``rzz(theta) a, b`` + Two-qubit rotation arond the :math:`ZZ` axis, corresponding to :class:`.RZZGate`. + + ``rccx a, b, c`` + The double-controlled :math:`X` gate, but with relative phase differences over the standard + Toffoli gate. This *should* correspond to the Qiskit gate :class:`~.RCCXGate`, but the legacy + converter wouldn't actually output this type. + + ``rc3x a, b, c, d`` + The triple-controlled :math:`X` gate, but with relative phase differences over the standard + definition. Corresponds to :class:`.RC3XGate`. + + ``c3x a, b, c, d`` + The triple-controlled :math:`X` gate, corresponding to :class:`.C3XGate`. + + ``c3sqrtx a, b, c, d`` + The triple-controlled :math:`\sqrt X` gate, corresponding to :class:`.C3SXGate`. + + ``c4x a, b, c, d, e`` + The quadruple-controlled :math:`X` gate., corresponding to :class:`.C4XGate`. + +* if *any* ``opaque`` or ``gate`` definition was given for the name ``delay``, they attempt to + output a :class:`~qiskit.circuit.Delay` instruction at each call. To function, this expects a + definition compatible with ``opaque delay(t) q;``, where the time ``t`` is given in units of + ``dt``. The importer will raise errors on construction if there was not exactly one parameter + and one qubit, or if the parameter is not integer-valued. + +* the additional scientific-calculator functions ``asin``, ``acos`` and ``atan`` are available. + +* 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` +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. + +.. py:data:: LEGACY_CUSTOM_INSTRUCTIONS + + A tuple containing the extra `custom_instructions` that Qiskit's legacy built-in converters used + if ``qelib1.inc`` is included, and there is any definition of a ``delay`` instruction. The gates + in the paper version of ``qelib1.inc`` and ``delay`` all require a compatible declaration + statement to be present within the OpenQASM 2 program, but Qiskit's legacy additions are all + marked as builtins since they are not actually present in any include file this parser sees. + +.. py:data:: LEGACY_CUSTOM_CLASSICAL + + A tuple containing the extra `custom_classical` functions that Qiskit's legacy built-in + converters use beyond those specified by the paper. This is the three basic inverse + trigonometric functions: :math:`\asin`, :math:`\acos` and :math:`\atan`. + +.. py:data:: LEGACY_INCLUDE_PATH + + A tuple containing the exact `include_path` used by the legacy Qiskit converter. + +On *all* the gates defined in Qiskit's legacy version of ``qelib1.inc`` and the ``delay`` +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 +`custom_instructions` argument can cause built-in Qiskit instructions to be used, and the signatures +of these match each other. + +.. note:: + + Circuits imported with :func:`load` and :func:`loads` with the above legacy-compability settings + should compare equal to those created by Qiskit's legacy importer, provided no non-``qelib1.inc`` + user gates are defined. User-defined gates are handled slightly differently in the new importer, + and while they should have equivalent :attr:`~.Instruction.definition` fields on inspection, this + module uses a custom class to lazily load the definition when it is requested (like most Qiskit + objects), rather than eagerly creating it during the parse. Qiskit's comparison rules for gates + will see these two objects as unequal, although any pass through :func:`.transpile` for a + particular backend should produce the same output circuits. + + +.. _qasm2-alternatives: + +Alternatives +============ + +The parser components of this module started off as a separate PyPI package: `qiskit-qasm2 +`__. This package at version 0.5.3 was vendored into Qiskit +Terra 0.24. Any subsequent changes between the two packages may not necessarily be kept in sync. + +There is a newer version of the OpenQASM specification, version 3.0, which is described at +https://openqasm.com. This includes far more facilities for high-level classical programming. +Qiskit has some rudimentary support for OpenQASM 3 already; see :mod:`qiskit.qasm3` for that. + +OpenQASM 2 is not a suitable serialization language for Qiskit's :class:`.QuantumCircuit`. This +module is provided for interoperability purposes, not as a general serialization format. If that is +what you need, consider using :mod:`qiskit.qpy` instead. +""" + +__all__ = [ + "load", + "loads", + "CustomInstruction", + "CustomClassical", + "LEGACY_CUSTOM_INSTRUCTIONS", + "LEGACY_CUSTOM_CLASSICAL", + "LEGACY_INCLUDE_PATH", + "QASM2Error", + "QASM2ParseError", + "QASM2ExportError", +] + +import os +import sys +from pathlib import Path +from typing import Iterable, Union, Optional + +# Pylint can't handle the C-extension introspection of `_qasm2` because there's a re-import through +# to `qiskit.qasm2.exceptions`, and pylint ends up trying to import `_qasm2` twice, which PyO3 +# hates. If that gets fixed, this disable can be removed and `qiskit._qasm2` added to the allowed C +# extensions for loadings in the `pyproject.toml`. +# pylint: disable=c-extension-no-member +from qiskit import _qasm2 +from qiskit.circuit import QuantumCircuit +from . import parse as _parse +from .exceptions import QASM2Error, QASM2ParseError, QASM2ExportError +from .parse import ( + CustomInstruction, + CustomClassical, + LEGACY_CUSTOM_INSTRUCTIONS, + LEGACY_CUSTOM_CLASSICAL, +) + +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + + +LEGACY_INCLUDE_PATH = ( + Path(__file__).parents[1] / "qasm" / "libs", + # This is deliberately left as a relative current-directory import until call time, so it + # respects changes the user might make from within the interpreter. + Path("."), +) + + +def _normalize_path(path: Union[str, os.PathLike]) -> str: + """Normalise 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. + """ + path = Path(path).expanduser().absolute() + if not path.exists(): + raise FileNotFoundError(str(path)) + return str(path) + + +def loads( + string: str, + *, + include_path: Iterable[Union[str, os.PathLike]] = (".",), + custom_instructions: Iterable[CustomInstruction] = (), + custom_classical: Iterable[CustomClassical] = (), + strict: bool = False, +) -> QuantumCircuit: + """Parse an OpenQASM 2 program from a string into a :class:`.QuantumCircuit`. + + Args: + string: The OpenQASM 2 program in a string. + include_path: order of directories to search when evluating ``include`` statements. + custom_instructions: any custom constructors that should be used for specific gates or + opaque instructions during circuit construction. See :ref:`qasm2-custom-instructions` + for more. + custom_classical: any custom classical functions that should be used during the parsing of + classical expressions. See :ref:`qasm2-custom-classical` for more. + strict: whether to run in :ref:`strict mode `. + + Returns: + A circuit object representing the same OpenQASM 2 program. + """ + custom_instructions = list(custom_instructions) + return _parse.from_bytecode( + _qasm2.bytecode_from_string( + string, + [_normalize_path(path) for path in include_path], + [ + _qasm2.CustomInstruction(x.name, x.num_params, x.num_qubits, x.builtin) + for x in custom_instructions + ], + tuple(custom_classical), + strict, + ), + custom_instructions, + ) + + +def load( + filename: Union[str, os.PathLike], + *, + include_path: Iterable[Union[str, os.PathLike]] = (".",), + include_input_directory: Optional[Literal["append", "prepend"]] = "append", + custom_instructions: Iterable[CustomInstruction] = (), + custom_classical: Iterable[CustomClassical] = (), + strict: bool = False, +) -> QuantumCircuit: + """Parse an OpenQASM 2 program from a file into a :class:`.QuantumCircuit`. The given path + should be ASCII or UTF-8 encoded, and contain the OpenQASM 2 program. + + Args: + filename: The OpenQASM 2 program in a string. + include_path: order of directories to search when evluating ``include`` statements. + include_input_directory: Whether to add the directory of the input file to the + ``include_path``, and if so, whether to *append* it to search last, or *prepend* it to + search first. Pass ``None`` to suppress adding this directory entirely. + custom_instructions: any custom constructors that should be used for specific gates or + opaque instructions during circuit construction. See :ref:`qasm2-custom-instructions` + for more. + custom_classical: any custom classical functions that should be used during the parsing of + classical expressions. See :ref:`qasm2-custom-classical` for more. + strict: whether to run in :ref:`strict mode `. + + Returns: + A circuit object representing the same OpenQASM 2 program. + """ + filename = Path(filename) + include_path = [_normalize_path(path) for path in include_path] + if include_input_directory == "append": + include_path.append(str(filename.parent)) + elif include_input_directory == "prepend": + include_path.insert(0, str(filename.parent)) + elif include_input_directory is not None: + raise ValueError( + f"unknown value for include_input_directory: '{include_input_directory}'." + " Valid values are '\"append\"', '\"prepend\"' and 'None'." + ) + custom_instructions = tuple(custom_instructions) + return _parse.from_bytecode( + _qasm2.bytecode_from_file( + _normalize_path(filename), + include_path, + [ + _qasm2.CustomInstruction(x.name, x.num_params, x.num_qubits, x.builtin) + for x in custom_instructions + ], + tuple(custom_classical), + strict, + ), + custom_instructions, + ) diff --git a/qiskit/qasm2/exceptions.py b/qiskit/qasm2/exceptions.py new file mode 100644 index 000000000000..a2b592052588 --- /dev/null +++ b/qiskit/qasm2/exceptions.py @@ -0,0 +1,27 @@ +# 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. + +"""Exception definitions for the OQ2 module.""" + +from qiskit.exceptions import QiskitError + + +class QASM2Error(QiskitError): + """A general error raised by the OpenQASM 2 interoperation layer.""" + + +class QASM2ParseError(QASM2Error): + """An error raised because of a failure to parse an OpenQASM 2 file.""" + + +class QASM2ExportError(QASM2Error): + """An error raised because of a failure to convert a Qiskit object to an OpenQASM 2 form.""" diff --git a/qiskit/qasm2/parse.py b/qiskit/qasm2/parse.py new file mode 100644 index 000000000000..6f56d31b0a02 --- /dev/null +++ b/qiskit/qasm2/parse.py @@ -0,0 +1,405 @@ +# 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. + +"""Python-space bytecode interpreter for the output of the main Rust parser logic.""" + +import dataclasses +import math +from typing import Iterable, Callable + +from qiskit.circuit import ( + Barrier, + CircuitInstruction, + ClassicalRegister, + Delay, + Gate, + Instruction, + Measure, + QuantumCircuit, + QuantumRegister, + Qubit, + Reset, + library as lib, +) + +# This is the same C-extension problems as described in the `__init__.py` disable near the +# `_qasm2` import. +from qiskit._qasm2 import ( # pylint: disable=no-name-in-module + OpCode, + UnaryOpCode, + BinaryOpCode, + CustomClassical, + ExprConstant, + ExprArgument, + ExprUnary, + ExprBinary, + ExprCustom, +) +from .exceptions import QASM2ParseError + +# Constructors of the form `*params -> Gate` for the special 'qelib1.inc' include. This is +# essentially a pre-parsed state for the file as given in the arXiv paper defining OQ2. +QELIB1 = ( + lib.U3Gate, + lib.U2Gate, + lib.U1Gate, + lib.CXGate, + # IGate in Terra < 0.24 is defined as a single-cycle delay, so is not strictly an identity. + # Instead we use a true no-op stand-in. + lambda: lib.UGate(0, 0, 0), + lib.XGate, + lib.YGate, + lib.ZGate, + lib.HGate, + lib.SGate, + lib.SdgGate, + lib.TGate, + lib.TdgGate, + lib.RXGate, + lib.RYGate, + lib.RZGate, + lib.CZGate, + lib.CYGate, + lib.CHGate, + lib.CCXGate, + lib.CRZGate, + lib.CU1Gate, + lib.CU3Gate, +) + + +@dataclasses.dataclass(frozen=True) +class CustomInstruction: + """Information about a custom instruction that should be defined during the parse. + + The ``name``, ``num_params`` and ``num_qubits`` fields are self-explanatory. The + ``constructor`` field should be a callable object with signature ``*args -> Instruction``, where + each of the ``num_params`` ``args`` is a floating-point value. Most of the built-in Qiskit gate + classes have this form. + + There is a final ``builtin`` field. This is optional, and if set true will cause the + instruction to be defined and available within the parsing, even if there is no definition in + any included OpenQASM 2 file. + """ + + name: str + num_params: int + num_qubits: int + # This should be `(float*) -> Instruction`, but the older version of Sphinx we're constrained to + # use in the Python 3.9 docs build chokes on it, so relax the hint. + constructor: Callable[..., Instruction] + builtin: bool = False + + +def _generate_delay(time: float): + # This wrapper is just to ensure that the correct type of exception gets emitted; it would be + # unnecessarily spaghetti-ish to check every emitted instruction in Rust for integer + # compatibility, but only if its name is `delay` _and_ its constructor wraps Qiskit's `Delay`. + if int(time) != time: + raise QASM2ParseError("the custom 'delay' instruction can only accept an integer parameter") + return Delay(int(time), unit="dt") + + +class _U0Gate(Gate): + def __init__(self, count): + if int(count) != count: + raise QASM2ParseError("the number of single-qubit delay lengths must be an integer") + super().__init__("u0", 1, [int(count)]) + + def _define(self): + self._definition = QuantumCircuit(1) + for _ in [None] * self.params[0]: + self._definition.id(0) + + +LEGACY_CUSTOM_INSTRUCTIONS = ( + CustomInstruction("u3", 3, 1, lib.U3Gate), + CustomInstruction("u2", 2, 1, lib.U2Gate), + CustomInstruction("u1", 1, 1, lib.U1Gate), + CustomInstruction("cx", 0, 2, lib.CXGate), + # The Qiskit parser emits IGate for 'id', even if that is not strictly accurate in Terra < 0.24. + CustomInstruction("id", 0, 1, lib.IGate), + CustomInstruction("u0", 1, 1, _U0Gate, builtin=True), + CustomInstruction("u", 3, 1, lib.UGate, builtin=True), + CustomInstruction("p", 1, 1, lib.PhaseGate, builtin=True), + CustomInstruction("x", 0, 1, lib.XGate), + CustomInstruction("y", 0, 1, lib.YGate), + CustomInstruction("z", 0, 1, lib.ZGate), + CustomInstruction("h", 0, 1, lib.HGate), + CustomInstruction("s", 0, 1, lib.SGate), + CustomInstruction("sdg", 0, 1, lib.SdgGate), + CustomInstruction("t", 0, 1, lib.TGate), + CustomInstruction("tdg", 0, 1, lib.TdgGate), + CustomInstruction("rx", 1, 1, lib.RXGate), + CustomInstruction("ry", 1, 1, lib.RYGate), + CustomInstruction("rz", 1, 1, lib.RZGate), + CustomInstruction("sx", 0, 1, lib.SXGate, builtin=True), + CustomInstruction("sxdg", 0, 1, lib.SXdgGate, builtin=True), + CustomInstruction("cz", 0, 2, lib.CZGate), + CustomInstruction("cy", 0, 2, lib.CYGate), + CustomInstruction("swap", 0, 2, lib.SwapGate, builtin=True), + CustomInstruction("ch", 0, 2, lib.CHGate), + CustomInstruction("ccx", 0, 3, lib.CCXGate), + CustomInstruction("cswap", 0, 3, lib.CSwapGate, builtin=True), + CustomInstruction("crx", 1, 2, lib.CRXGate, builtin=True), + CustomInstruction("cry", 1, 2, lib.CRYGate, builtin=True), + CustomInstruction("crz", 1, 2, lib.CRZGate), + CustomInstruction("cu1", 1, 2, lib.CU1Gate), + CustomInstruction("cp", 1, 2, lib.CPhaseGate, builtin=True), + CustomInstruction("cu3", 3, 2, lib.CU3Gate), + CustomInstruction("csx", 0, 2, lib.CSXGate, builtin=True), + CustomInstruction("cu", 4, 2, lib.CUGate, builtin=True), + CustomInstruction("rxx", 1, 2, lib.RXXGate, builtin=True), + CustomInstruction("rzz", 1, 2, lib.RZZGate, builtin=True), + CustomInstruction("rccx", 0, 3, lib.RCCXGate, builtin=True), + CustomInstruction("rc3x", 0, 4, lib.RC3XGate, builtin=True), + CustomInstruction("c3x", 0, 4, lib.C3XGate, builtin=True), + CustomInstruction("c3sqrtx", 0, 4, lib.C3SXGate, builtin=True), + CustomInstruction("c4x", 0, 5, lib.C4XGate, builtin=True), + CustomInstruction("delay", 1, 1, _generate_delay), +) + +LEGACY_CUSTOM_CLASSICAL = ( + CustomClassical("asin", 1, math.asin), + CustomClassical("acos", 1, math.acos), + CustomClassical("atan", 1, math.atan), +) + + +def from_bytecode(bytecode, custom_instructions: Iterable[CustomInstruction]): + """Loop through the Rust bytecode iterator `bytecode` producing a + :class:`~qiskit.circuit.QuantumCircuit` instance from it. All the hard work is done in Rust + space where operations are faster; here, we're just about looping through the instructions as + fast as possible, doing as little calculation as we can in Python space. The Python-space + components are the vast majority of the runtime. + + The "bytecode", and so also this Python function, is very tightly coupled to the output of the + Rust parser. The bytecode itself is largely defined by Rust; from Python space, the iterator is + over essentially a 2-tuple of `(opcode, operands)`. The `operands` are fixed by Rust, and + assumed to be correct by this function. + + The Rust code is responsible for all validation. If this function causes any errors to be + raised by Qiskit (except perhaps for some symbolic manipulations of `Parameter` objects), we + should consider that a bug in the Rust code.""" + # The method `QuantumCircuit._append` is a semi-public method, so isn't really subject to + # "protected access". + # pylint: disable=protected-access + qc = QuantumCircuit() + qubits = [] + clbits = [] + gates = [] + has_u, has_cx = False, False + for custom in custom_instructions: + gates.append(custom.constructor) + if custom.name == "U": + has_u = True + elif custom.name == "CX": + has_cx = True + if not has_u: + gates.append(lib.UGate) + if not has_cx: + gates.append(lib.CXGate) + # Pull this out as an explicit iterator so we can manually advance the loop in `DeclareGate` + # contexts easily. + bc = iter(bytecode) + for op in bc: + # We have to check `op.opcode` so many times, it's worth pulling out the extra attribute + # access. We should check the opcodes in order of their likelihood to be in the OQ2 program + # for speed. Gate applications are by far the most common for long programs. This function + # is deliberately long and does not use hashmaps or function lookups for speed in + # Python-space. + opcode = op.opcode + # `OpCode` is an `enum` in Rust, but its instances don't have the same singleton property as + # Python `enum.Enum` objects. + if opcode == OpCode.Gate: + gate_id, parameters, op_qubits = op.operands + qc._append( + CircuitInstruction(gates[gate_id](*parameters), [qubits[q] for q in op_qubits]) + ) + elif opcode == OpCode.ConditionedGate: + gate_id, parameters, op_qubits, creg, value = op.operands + gate = gates[gate_id](*parameters) + gate.condition = (qc.cregs[creg], value) + qc._append(CircuitInstruction(gate, [qubits[q] for q in op_qubits])) + elif opcode == OpCode.Measure: + qubit, clbit = op.operands + qc._append(CircuitInstruction(Measure(), (qubits[qubit],), (clbits[clbit],))) + elif opcode == OpCode.ConditionedMeasure: + qubit, clbit, creg, value = op.operands + measure = Measure() + measure.condition = (qc.cregs[creg], value) + qc._append(CircuitInstruction(measure, (qubits[qubit],), (clbits[clbit],))) + elif opcode == OpCode.Reset: + qc._append(CircuitInstruction(Reset(), (qubits[op.operands[0]],))) + elif opcode == OpCode.ConditionedReset: + qubit, creg, value = op.operands + reset = Reset() + reset.condition = (qc.cregs[creg], value) + qc._append(CircuitInstruction(reset, (qubits[qubit],))) + elif opcode == OpCode.Barrier: + op_qubits = op.operands[0] + qc._append(CircuitInstruction(Barrier(len(op_qubits)), [qubits[q] for q in op_qubits])) + elif opcode == OpCode.DeclareQreg: + name, size = op.operands + register = QuantumRegister(size, name) + qubits += register[:] + qc.add_register(register) + elif opcode == OpCode.DeclareCreg: + name, size = op.operands + register = ClassicalRegister(size, name) + clbits += register[:] + qc.add_register(register) + elif opcode == OpCode.SpecialInclude: + # Including `qelib1.inc` is pretty much universal, and we treat its gates as having + # special relationships to the Qiskit ones, so we don't actually parse it; we just + # short-circuit to add its pre-calculated content to our state. + (indices,) = op.operands + for index in indices: + gates.append(QELIB1[index]) + elif opcode == OpCode.DeclareGate: + name, num_qubits = op.operands + # This inner loop advances the iterator of the outer loop as well, since `bc` is a + # manually created iterator, rather than an implicit one from the first loop. + inner_bc = [] + for inner_op in bc: + if inner_op.opcode == OpCode.EndDeclareGate: + break + inner_bc.append(inner_op) + # Technically there's a quadratic dependency in the number of gates here, which could be + # fixed by just sharing a reference to `gates` rather than recreating a new object. + # Gates can't ever be removed from the list, so it wouldn't get out-of-date, though + # there's a minor risk of somewhere accidentally mutating it instead, and in practice + # the cost shouldn't really matter. + gates.append(_gate_builder(name, num_qubits, tuple(gates), inner_bc)) + elif opcode == OpCode.DeclareOpaque: + name, num_qubits = op.operands + gates.append(_opaque_builder(name, num_qubits)) + else: + raise ValueError(f"invalid operation: {op}") + return qc + + +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.""" + + def __init__(self, name, num_qubits, params, gates, bytecode): + self._gates = gates + self._bytecode = bytecode + super().__init__(name, num_qubits, list(params)) + + def _define(self): + # This is a stripped-down version of the bytecode interpreter; there's very few opcodes that + # we actually need to handle within gate bodies. + # pylint: disable=protected-access + qubits = [Qubit() for _ in [None] * self.num_qubits] + qc = QuantumCircuit(qubits) + for op in self._bytecode: + if op.opcode == OpCode.Gate: + gate_id, args, op_qubits = op.operands + qc._append( + CircuitInstruction( + self._gates[gate_id](*(_evaluate_argument(a, self.params) for a in args)), + [qubits[q] for q in op_qubits], + ) + ) + elif op.opcode == OpCode.Barrier: + op_qubits = op.operands[0] + qc._append( + CircuitInstruction(Barrier(len(op_qubits)), [qubits[q] for q in op_qubits]) + ) + else: + raise ValueError(f"received invalid bytecode to build gate: {op}") + self._definition = qc + + # 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. + + def __getstate__(self): + return (self.name, self.num_qubits, self.params, self.definition) + + def __setstate__(self, state): + name, num_qubits, params, definition = state + super().__init__(name, num_qubits, params) + self._gates = () + self._bytecode = () + self._definition = definition + + +def _gate_builder(name, num_qubits, known_gates, bytecode): + """Create a gate-builder function of the signature `*params -> Gate` for a gate with a given + `name`. This produces a `_DefinedGate` class, whose `_define` method runs through the given + `bytecode` using the current list of `known_gates` to interpret the gate indices. + + The indirection here is mostly needed to correctly close over `known_gates` and `bytecode`.""" + + def definer(*params): + return _DefinedGate(name, num_qubits, params, known_gates, tuple(bytecode)) + + return definer + + +def _opaque_builder(name, num_qubits): + """Create a gate-builder function of the signature `*params -> Gate` for an opaque gate with a + given `name`, which takes the given numbers of qubits.""" + + def definer(*params): + return Gate(name, num_qubits, params) + + return definer + + +# The natural way to reduce returns in this function would be to use a lookup table for the opcodes, +# but the PyO3 enum entities aren't (currently) hashable. +def _evaluate_argument(expr, parameters): # pylint: disable=too-many-return-statements + """Inner recursive function to calculate the value of a mathematical expression given the + concrete values in the `parameters` field.""" + if isinstance(expr, ExprConstant): + return expr.value + if isinstance(expr, ExprArgument): + return parameters[expr.index] + if isinstance(expr, ExprUnary): + inner = _evaluate_argument(expr.argument, parameters) + opcode = expr.opcode + if opcode == UnaryOpCode.Negate: + return -inner + if opcode == UnaryOpCode.Cos: + return math.cos(inner) + if opcode == UnaryOpCode.Exp: + return math.exp(inner) + if opcode == UnaryOpCode.Ln: + return math.log(inner) + if opcode == UnaryOpCode.Sin: + return math.sin(inner) + if opcode == UnaryOpCode.Sqrt: + return math.sqrt(inner) + if opcode == UnaryOpCode.Tan: + return math.tan(inner) + raise ValueError(f"unhandled unary opcode: {opcode}") + if isinstance(expr, ExprBinary): + left = _evaluate_argument(expr.left, parameters) + right = _evaluate_argument(expr.right, parameters) + opcode = expr.opcode + if opcode == BinaryOpCode.Add: + return left + right + if opcode == BinaryOpCode.Subtract: + return left - right + if opcode == BinaryOpCode.Multiply: + return left * right + if opcode == BinaryOpCode.Divide: + return left / right + if opcode == BinaryOpCode.Power: + return left**right + raise ValueError(f"unhandled binary opcode: {opcode}") + if isinstance(expr, ExprCustom): + return expr.callable(*(_evaluate_argument(x, parameters) for x in expr.arguments)) + raise ValueError(f"unhandled expression type: {expr}") diff --git a/releasenotes/notes/qasm2-parser-rust-ecf6570e2d445a94.yaml b/releasenotes/notes/qasm2-parser-rust-ecf6570e2d445a94.yaml new file mode 100644 index 000000000000..f38d694e77ad --- /dev/null +++ b/releasenotes/notes/qasm2-parser-rust-ecf6570e2d445a94.yaml @@ -0,0 +1,23 @@ +--- +features: + - | + A new OpenQASM 2 parser is available in :mod:`qiskit.qasm2`. This has two entry points: + :func:`.qasm2.load` and :func:`.qasm2.loads`, for reading the source code from a file and from a + string, respectively:: + + import qiskit.qasm2 + program = """ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + h q[0]; + cx q[0], q[1]; + """ + bell = qiskit.qasm2.loads(program) + + 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 + 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/setup.py b/setup.py index 1025480078f7..ec447e6d250c 100755 --- a/setup.py +++ b/setup.py @@ -32,6 +32,11 @@ ) +# If RUST_DEBUG is set, force compiling in debug mode. Else, use the default behavior of whether +# it's an editable installation. +rust_debug = True if os.getenv("RUST_DEBUG") == "1" else None + + qasm3_import_extras = [ "qiskit-qasm3-import>=0.1.0", ] @@ -103,10 +108,11 @@ "qiskit._accelerate", "crates/accelerate/Cargo.toml", binding=Binding.PyO3, - # If RUST_DEBUG is set, force compiling in debug mode. Else, use the default behavior - # of whether it's an editable installation. - debug=True if os.getenv("RUST_DEBUG") == "1" else None, - ) + debug=rust_debug, + ), + RustExtension( + "qiskit._qasm2", "crates/qasm2/Cargo.toml", binding=Binding.PyO3, debug=rust_debug + ), ], zip_safe=False, entry_points={ diff --git a/test/python/qasm2/__init__.py b/test/python/qasm2/__init__.py new file mode 100644 index 000000000000..f4604807ff33 --- /dev/null +++ b/test/python/qasm2/__init__.py @@ -0,0 +1,38 @@ +# 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. + +"""Common utility function.""" + +from typing import Iterable +from qiskit.circuit import QuantumCircuit, Parameter + +import qiskit.qasm2 + + +def gate_builder(name: str, parameters: Iterable[Parameter], definition: QuantumCircuit): + """Get a builder for a custom gate. + + Ideally we would just use an eagerly defined `Gate` instance here, but limitations in how + `QuantumCircuit.__eq__` and `Instruction.__eq__` work mean that we have to ensure we're using + the same class as the parser for equality checks to work.""" + + # Ideally we wouldn't have this at all, but hiding it away in one function is likely the safest + # and easiest to update if the Python component of the library changes. + + def definer(*arguments): + # We can supply empty lists for the gates and the bytecode, because we're going to override + # the definition manually ourselves. + gate = qiskit.qasm2.parse._DefinedGate(name, definition.num_qubits, arguments, (), ()) + gate._definition = definition.assign_parameters(dict(zip(parameters, arguments))) + return gate + + return definer diff --git a/test/python/qasm2/test_arxiv_examples.py b/test/python/qasm2/test_arxiv_examples.py new file mode 100644 index 000000000000..46d7c1e84663 --- /dev/null +++ b/test/python/qasm2/test_arxiv_examples.py @@ -0,0 +1,450 @@ +# 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. + +"""These tests are the examples given in the arXiv paper describing OpenQASM 2. Specifically, there +is a test for each subsection (except the description of 'qelib1.inc') in section 3 of +https://arxiv.org/abs/1707.03429v2. The examples are copy/pasted from the source files there.""" + +# pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring + +import math +import os +import tempfile + +import ddt + +from qiskit import qasm2 +from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister, Qubit +from qiskit.circuit.library import U1Gate, U3Gate, CU1Gate +from qiskit.test import QiskitTestCase + +from . import gate_builder + + +def load(string, *args, **kwargs): + # We're deliberately not using the context-manager form here because we need to use it in a + # slightly odd pattern. + # pylint: disable=consider-using-with + temp = tempfile.NamedTemporaryFile(mode="w", delete=False) + try: + temp.write(string) + # NamedTemporaryFile claims not to be openable a second time on Windows, so close it + # (without deletion) so Rust can open it again. + temp.close() + return qasm2.load(temp.name, *args, **kwargs) + finally: + # Now actually clean up after ourselves. + os.unlink(temp.name) + + +@ddt.ddt +class TestArxivExamples(QiskitTestCase): + @ddt.data(qasm2.loads, load) + def test_teleportation(self, parser): + example = """\ +// quantum teleportation example +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[3]; +creg c0[1]; +creg c1[1]; +creg c2[1]; +// optional post-rotation for state tomography +gate post q { } +u3(0.3,0.2,0.1) q[0]; +h q[1]; +cx q[1],q[2]; +barrier q; +cx q[0],q[1]; +h q[0]; +measure q[0] -> c0[0]; +measure q[1] -> c1[0]; +if(c0==1) z q[2]; +if(c1==1) x q[2]; +post q[2]; +measure q[2] -> c2[0];""" + parsed = parser(example) + + post = gate_builder("post", [], QuantumCircuit([Qubit()])) + + q = QuantumRegister(3, "q") + c0 = ClassicalRegister(1, "c0") + c1 = ClassicalRegister(1, "c1") + c2 = ClassicalRegister(1, "c2") + + qc = QuantumCircuit(q, c0, c1, c2) + qc.append(U3Gate(0.3, 0.2, 0.1), [q[0]], []) + qc.h(q[1]) + qc.cx(q[1], q[2]) + qc.barrier(q) + qc.cx(q[0], q[1]) + qc.h(q[0]) + qc.measure(q[0], c0[0]) + qc.measure(q[1], c1[0]) + qc.z(q[2]).c_if(c0, 1) + qc.x(q[2]).c_if(c1, 1) + qc.append(post(), [q[2]], []) + qc.measure(q[2], c2[0]) + + self.assertEqual(parsed, qc) + + @ddt.data(qasm2.loads, load) + def test_qft(self, parser): + example = """\ +// quantum Fourier transform +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[4]; +creg c[4]; +x q[0]; +x q[2]; +barrier q; +h q[0]; +cu1(pi/2) q[1],q[0]; +h q[1]; +cu1(pi/4) q[2],q[0]; +cu1(pi/2) q[2],q[1]; +h q[2]; +cu1(pi/8) q[3],q[0]; +cu1(pi/4) q[3],q[1]; +cu1(pi/2) q[3],q[2]; +h q[3]; +measure q -> c;""" + parsed = parser(example) + + qc = QuantumCircuit(QuantumRegister(4, "q"), ClassicalRegister(4, "c")) + qc.x(0) + qc.x(2) + qc.barrier(range(4)) + qc.h(0) + qc.append(CU1Gate(math.pi / 2), [1, 0]) + qc.h(1) + qc.append(CU1Gate(math.pi / 4), [2, 0]) + qc.append(CU1Gate(math.pi / 2), [2, 1]) + qc.h(2) + qc.append(CU1Gate(math.pi / 8), [3, 0]) + qc.append(CU1Gate(math.pi / 4), [3, 1]) + qc.append(CU1Gate(math.pi / 2), [3, 2]) + qc.h(3) + qc.measure(range(4), range(4)) + + self.assertEqual(parsed, qc) + + @ddt.data(qasm2.loads, load) + def test_inverse_qft_1(self, parser): + example = """\ +// QFT and measure, version 1 +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[4]; +creg c[4]; +h q; +barrier q; +h q[0]; +measure q[0] -> c[0]; +if(c==1) u1(pi/2) q[1]; +h q[1]; +measure q[1] -> c[1]; +if(c==1) u1(pi/4) q[2]; +if(c==2) u1(pi/2) q[2]; +if(c==3) u1(pi/2+pi/4) q[2]; +h q[2]; +measure q[2] -> c[2]; +if(c==1) u1(pi/8) q[3]; +if(c==2) u1(pi/4) q[3]; +if(c==3) u1(pi/4+pi/8) q[3]; +if(c==4) u1(pi/2) q[3]; +if(c==5) u1(pi/2+pi/8) q[3]; +if(c==6) u1(pi/2+pi/4) q[3]; +if(c==7) u1(pi/2+pi/4+pi/8) q[3]; +h q[3]; +measure q[3] -> c[3];""" + parsed = parser(example) + + q = QuantumRegister(4, "q") + c = ClassicalRegister(4, "c") + qc = QuantumCircuit(q, c) + qc.h(q) + qc.barrier(q) + qc.h(q[0]) + qc.measure(q[0], c[0]) + qc.append(U1Gate(math.pi / 2).c_if(c, 1), [q[1]]) + qc.h(q[1]) + qc.measure(q[1], c[1]) + qc.append(U1Gate(math.pi / 4).c_if(c, 1), [q[2]]) + qc.append(U1Gate(math.pi / 2).c_if(c, 2), [q[2]]) + qc.append(U1Gate(math.pi / 4 + math.pi / 2).c_if(c, 3), [q[2]]) + qc.h(q[2]) + qc.measure(q[2], c[2]) + qc.append(U1Gate(math.pi / 8).c_if(c, 1), [q[3]]) + qc.append(U1Gate(math.pi / 4).c_if(c, 2), [q[3]]) + qc.append(U1Gate(math.pi / 8 + math.pi / 4).c_if(c, 3), [q[3]]) + qc.append(U1Gate(math.pi / 2).c_if(c, 4), [q[3]]) + qc.append(U1Gate(math.pi / 8 + math.pi / 2).c_if(c, 5), [q[3]]) + qc.append(U1Gate(math.pi / 4 + math.pi / 2).c_if(c, 6), [q[3]]) + qc.append(U1Gate(math.pi / 8 + math.pi / 4 + math.pi / 2).c_if(c, 7), [q[3]]) + qc.h(q[3]) + qc.measure(q[3], c[3]) + + self.assertEqual(parsed, qc) + + @ddt.data(qasm2.loads, load) + def test_inverse_qft_2(self, parser): + example = """\ +// QFT and measure, version 2 +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[4]; +creg c0[1]; +creg c1[1]; +creg c2[1]; +creg c3[1]; +h q; +barrier q; +h q[0]; +measure q[0] -> c0[0]; +if(c0==1) u1(pi/2) q[1]; +h q[1]; +measure q[1] -> c1[0]; +if(c0==1) u1(pi/4) q[2]; +if(c1==1) u1(pi/2) q[2]; +h q[2]; +measure q[2] -> c2[0]; +if(c0==1) u1(pi/8) q[3]; +if(c1==1) u1(pi/4) q[3]; +if(c2==1) u1(pi/2) q[3]; +h q[3]; +measure q[3] -> c3[0];""" + parsed = parser(example) + + q = QuantumRegister(4, "q") + c0 = ClassicalRegister(1, "c0") + c1 = ClassicalRegister(1, "c1") + c2 = ClassicalRegister(1, "c2") + c3 = ClassicalRegister(1, "c3") + qc = QuantumCircuit(q, c0, c1, c2, c3) + qc.h(q) + qc.barrier(q) + qc.h(q[0]) + qc.measure(q[0], c0[0]) + qc.append(U1Gate(math.pi / 2).c_if(c0, 1), [q[1]]) + qc.h(q[1]) + qc.measure(q[1], c1[0]) + qc.append(U1Gate(math.pi / 4).c_if(c0, 1), [q[2]]) + qc.append(U1Gate(math.pi / 2).c_if(c1, 1), [q[2]]) + qc.h(q[2]) + qc.measure(q[2], c2[0]) + qc.append(U1Gate(math.pi / 8).c_if(c0, 1), [q[3]]) + qc.append(U1Gate(math.pi / 4).c_if(c1, 1), [q[3]]) + qc.append(U1Gate(math.pi / 2).c_if(c2, 1), [q[3]]) + qc.h(q[3]) + qc.measure(q[3], c3[0]) + + self.assertEqual(parsed, qc) + + @ddt.data(qasm2.loads, load) + def test_ripple_carry_adder(self, parser): + example = """\ +// quantum ripple-carry adder from Cuccaro et al, quant-ph/0410184 +OPENQASM 2.0; +include "qelib1.inc"; +gate majority a,b,c +{ + cx c,b; + cx c,a; + ccx a,b,c; +} +gate unmaj a,b,c +{ + ccx a,b,c; + cx c,a; + cx a,b; +} +qreg cin[1]; +qreg a[4]; +qreg b[4]; +qreg cout[1]; +creg ans[5]; +// set input states +x a[0]; // a = 0001 +x b; // b = 1111 +// add a to b, storing result in b +majority cin[0],b[0],a[0]; +majority a[0],b[1],a[1]; +majority a[1],b[2],a[2]; +majority a[2],b[3],a[3]; +cx a[3],cout[0]; +unmaj a[2],b[3],a[3]; +unmaj a[1],b[2],a[2]; +unmaj a[0],b[1],a[1]; +unmaj cin[0],b[0],a[0]; +measure b[0] -> ans[0]; +measure b[1] -> ans[1]; +measure b[2] -> ans[2]; +measure b[3] -> ans[3]; +measure cout[0] -> ans[4];""" + parsed = parser(example) + + majority_definition = QuantumCircuit([Qubit(), Qubit(), Qubit()]) + majority_definition.cx(2, 1) + majority_definition.cx(2, 0) + majority_definition.ccx(0, 1, 2) + majority = gate_builder("majority", [], majority_definition) + + unmaj_definition = QuantumCircuit([Qubit(), Qubit(), Qubit()]) + unmaj_definition.ccx(0, 1, 2) + unmaj_definition.cx(2, 0) + unmaj_definition.cx(0, 1) + unmaj = gate_builder("unmaj", [], unmaj_definition) + + cin = QuantumRegister(1, "cin") + a = QuantumRegister(4, "a") + b = QuantumRegister(4, "b") + cout = QuantumRegister(1, "cout") + ans = ClassicalRegister(5, "ans") + + qc = QuantumCircuit(cin, a, b, cout, ans) + qc.x(a[0]) + qc.x(b) + qc.append(majority(), [cin[0], b[0], a[0]]) + qc.append(majority(), [a[0], b[1], a[1]]) + qc.append(majority(), [a[1], b[2], a[2]]) + qc.append(majority(), [a[2], b[3], a[3]]) + qc.cx(a[3], cout[0]) + qc.append(unmaj(), [a[2], b[3], a[3]]) + qc.append(unmaj(), [a[1], b[2], a[2]]) + qc.append(unmaj(), [a[0], b[1], a[1]]) + qc.append(unmaj(), [cin[0], b[0], a[0]]) + qc.measure(b, ans[:4]) + qc.measure(cout[0], ans[4]) + + self.assertEqual(parsed, qc) + + @ddt.data(qasm2.loads, load) + def test_randomised_benchmarking(self, parser): + example = """\ +// One randomized benchmarking sequence +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[2]; +creg c[2]; +h q[0]; +barrier q; +cz q[0],q[1]; +barrier q; +s q[0]; +cz q[0],q[1]; +barrier q; +s q[0]; +z q[0]; +h q[0]; +barrier q; +measure q -> c; + """ + parsed = parser(example) + + q = QuantumRegister(2, "q") + c = ClassicalRegister(2, "c") + qc = QuantumCircuit(q, c) + qc.h(q[0]) + qc.barrier(q) + qc.cz(q[0], q[1]) + qc.barrier(q) + qc.s(q[0]) + qc.cz(q[0], q[1]) + qc.barrier(q) + qc.s(q[0]) + qc.z(q[0]) + qc.h(q[0]) + qc.barrier(q) + qc.measure(q, c) + + self.assertEqual(parsed, qc) + + @ddt.data(qasm2.loads, load) + def test_process_tomography(self, parser): + example = """\ +OPENQASM 2.0; +include "qelib1.inc"; +gate pre q { } // pre-rotation +gate post q { } // post-rotation +qreg q[1]; +creg c[1]; +pre q[0]; +barrier q; +h q[0]; +barrier q; +post q[0]; +measure q[0] -> c[0];""" + parsed = parser(example) + + pre = gate_builder("pre", [], QuantumCircuit([Qubit()])) + post = gate_builder("post", [], QuantumCircuit([Qubit()])) + + qc = QuantumCircuit(QuantumRegister(1, "q"), ClassicalRegister(1, "c")) + qc.append(pre(), [0]) + qc.barrier(qc.qubits) + qc.h(0) + qc.barrier(qc.qubits) + qc.append(post(), [0]) + qc.measure(0, 0) + + self.assertEqual(parsed, qc) + + @ddt.data(qasm2.loads, load) + def test_error_correction(self, parser): + example = """\ +// Repetition code syndrome measurement +OPENQASM 2.0; +include "qelib1.inc"; +qreg q[3]; +qreg a[2]; +creg c[3]; +creg syn[2]; +gate syndrome d1,d2,d3,a1,a2 +{ + cx d1,a1; cx d2,a1; + cx d2,a2; cx d3,a2; +} +x q[0]; // error +barrier q; +syndrome q[0],q[1],q[2],a[0],a[1]; +measure a -> syn; +if(syn==1) x q[0]; +if(syn==2) x q[2]; +if(syn==3) x q[1]; +measure q -> c;""" + parsed = parser(example) + + syndrome_definition = QuantumCircuit([Qubit() for _ in [None] * 5]) + syndrome_definition.cx(0, 3) + syndrome_definition.cx(1, 3) + syndrome_definition.cx(1, 4) + syndrome_definition.cx(2, 4) + syndrome = gate_builder("syndrome", [], syndrome_definition) + + q = QuantumRegister(3, "q") + a = QuantumRegister(2, "a") + c = ClassicalRegister(3, "c") + syn = ClassicalRegister(2, "syn") + + qc = QuantumCircuit(q, a, c, syn) + qc.x(q[0]) + qc.barrier(q) + qc.append(syndrome(), [q[0], q[1], q[2], a[0], a[1]]) + qc.measure(a, syn) + qc.x(q[0]).c_if(syn, 1) + qc.x(q[2]).c_if(syn, 2) + qc.x(q[1]).c_if(syn, 3) + qc.measure(q, c) + + self.assertEqual(parsed, qc) diff --git a/test/python/qasm2/test_expression.py b/test/python/qasm2/test_expression.py new file mode 100644 index 000000000000..9906d1574f9d --- /dev/null +++ b/test/python/qasm2/test_expression.py @@ -0,0 +1,259 @@ +# 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-module-docstring,missing-class-docstring,missing-function-docstring + +import itertools +import math +import sys + +import ddt + +import qiskit.qasm2 +from qiskit.test import QiskitTestCase + + +@ddt.ddt +class TestSimple(QiskitTestCase): + def test_unary_constants(self): + program = "qreg q[1]; U(-(0.5 + 0.5), +(+1 - 2), -+-+2) q[0];" + parsed = qiskit.qasm2.loads(program) + expected = [-1.0, -1.0, 2.0] + self.assertEqual(list(parsed.data[0].operation.params), expected) + + def test_unary_symbolic(self): + program = """ + gate u(a, b, c) q { + U(-(a + a), +(+b - c), -+-+c) q; + } + qreg q[1]; + u(0.5, 1.0, 2.0) q[0]; + """ + parsed = qiskit.qasm2.loads(program) + expected = [-1.0, -1.0, 2.0] + actual = [float(x) for x in parsed.data[0].operation.definition.data[0].operation.params] + self.assertEqual(list(actual), expected) + + @ddt.data( + ("+", lambda a, b: a + b), + ("-", lambda a, b: a - b), + ("*", lambda a, b: a * b), + ("/", lambda a, b: a / b), + ("^", lambda a, b: a**b), + ) + @ddt.unpack + def test_binary_constants(self, str_op, py_op): + program = f"qreg q[1]; U(0.25{str_op}0.5, 1.0{str_op}0.5, 3.2{str_op}-0.8) q[0];" + parsed = qiskit.qasm2.loads(program) + expected = [py_op(0.25, 0.5), py_op(1.0, 0.5), py_op(3.2, -0.8)] + # These should be bit-for-bit exact. + self.assertEqual(list(parsed.data[0].operation.params), expected) + + @ddt.data( + ("+", lambda a, b: a + b), + ("-", lambda a, b: a - b), + ("*", lambda a, b: a * b), + ("/", lambda a, b: a / b), + ("^", lambda a, b: a**b), + ) + @ddt.unpack + def test_binary_symbolic(self, str_op, py_op): + program = f""" + gate u(a, b, c) q {{ + U(a {str_op} b, a {str_op} (b {str_op} c), 0.0) q; + }} + qreg q[1]; + u(1.0, 2.0, 3.0) q[0]; + """ + parsed = qiskit.qasm2.loads(program) + outer = [1.0, 2.0, 3.0] + abstract_op = parsed.data[0].operation + self.assertEqual(list(abstract_op.params), outer) + expected = [py_op(1.0, 2.0), py_op(1.0, py_op(2.0, 3.0)), 0.0] + actual = [float(x) for x in abstract_op.definition.data[0].operation.params] + self.assertEqual(list(actual), expected) + + @ddt.data( + ("cos", math.cos), + ("exp", math.exp), + ("ln", math.log), + ("sin", math.sin), + ("sqrt", math.sqrt), + ("tan", math.tan), + ) + @ddt.unpack + def test_function_constants(self, function_str, function_py): + program = f"qreg q[1]; U({function_str}(0.5),{function_str}(1.0),{function_str}(pi)) q[0];" + parsed = qiskit.qasm2.loads(program) + expected = [function_py(0.5), function_py(1.0), function_py(math.pi)] + # These should be bit-for-bit exact. + self.assertEqual(list(parsed.data[0].operation.params), expected) + + @ddt.data( + ("cos", math.cos), + ("exp", math.exp), + ("ln", math.log), + ("sin", math.sin), + ("sqrt", math.sqrt), + ("tan", math.tan), + ) + @ddt.unpack + def test_function_symbolic(self, function_str, function_py): + program = f""" + gate u(a, b, c) q {{ + U({function_str}(a), {function_str}(b), {function_str}(c)) q; + }} + qreg q[1]; + u(0.5, 1.0, pi) q[0]; + """ + parsed = qiskit.qasm2.loads(program) + outer = [0.5, 1.0, math.pi] + abstract_op = parsed.data[0].operation + self.assertEqual(list(abstract_op.params), outer) + expected = [function_py(x) for x in outer] + actual = [float(x) for x in abstract_op.definition.data[0].operation.params] + self.assertEqual(list(actual), expected) + + +class TestPrecedenceAssociativity(QiskitTestCase): + def test_precedence(self): + # OQ3's precedence rules are the same as Python's, so we can effectively just eval. + expr = " 1.0 + 2.0 * -3.0 ^ 1.5 - 0.5 / +0.25" + expected = 1.0 + 2.0 * -(3.0**1.5) - 0.5 / +0.25 + + program = f"qreg q[1]; U({expr}, 0, 0) q[0];" + parsed = qiskit.qasm2.loads(program) + self.assertEqual(parsed.data[0].operation.params[0], expected) + + def test_addition_left(self): + # `eps` is the smallest floating-point value such that `1 + eps != 1`. That means that if + # addition is correctly parsed and resolved as left-associative, then the first parameter + # should first calculate `1 + (eps / 2)`, which will be 1, and then the same again, whereas + # the second will do `(eps / 2) + (eps / 2) = eps`, then `eps + 1` will be different. + eps = sys.float_info.epsilon + program = f"qreg q[1]; U(1 + {eps / 2} + {eps / 2}, {eps / 2} + {eps / 2} + 1, 0) q[0];" + parsed = qiskit.qasm2.loads(program) + self.assertNotEqual(1.0 + eps, 1.0) # Sanity check for the test. + self.assertEqual(list(parsed.data[0].operation.params), [1.0, 1.0 + eps, 0.0]) + + def test_multiplication_left(self): + # A similar principle to the epsilon test for addition; if multiplication associates right, + # then `(0.5 * 2.0 * fmax)` is `inf`, otherwise it's `fmax`. + fmax = sys.float_info.max + program = f"qreg q[1]; U({fmax} * 0.5 * 2.0, 2.0 * 0.5 * {fmax}, 2.0 * {fmax} * 0.5) q[0];" + parsed = qiskit.qasm2.loads(program) + self.assertEqual(list(parsed.data[0].operation.params), [fmax, fmax, math.inf]) + + def test_subtraction_left(self): + # If subtraction associated right, we'd accidentally get 2. + program = "qreg q[1]; U(2.0 - 1.0 - 1.0, 0, 0) q[0];" + parsed = qiskit.qasm2.loads(program) + self.assertEqual(list(parsed.data[0].operation.params), [0.0, 0.0, 0.0]) + + def test_division_left(self): + # If division associated right, we'd accidentally get 4. + program = "qreg q[1]; U(4.0 / 2.0 / 2.0, 0, 0) q[0];" + parsed = qiskit.qasm2.loads(program) + self.assertEqual(list(parsed.data[0].operation.params), [1.0, 0.0, 0.0]) + + def test_power_right(self): + # If the power operator associated left, we'd accidentally get 64 instead. + program = "qreg q[1]; U(2.0 ^ 3.0 ^ 2.0, 0, 0) q[0];" + parsed = qiskit.qasm2.loads(program) + self.assertEqual(list(parsed.data[0].operation.params), [512.0, 0.0, 0.0]) + + +class TestCustomClassical(QiskitTestCase): + def test_evaluation_order(self): + """We should be evaluating all functions, including custom user ones the exact number of + times we expect, and left-to-right in parameter lists.""" + # pylint: disable=invalid-name + + order = itertools.count() + + def f(): + return next(order) + + program = """ + qreg q[1]; + U(f(), 2 * f() + f(), atan2(f(), f()) - f()) q[0]; + """ + parsed = qiskit.qasm2.loads( + program, + custom_classical=[ + qiskit.qasm2.CustomClassical("f", 0, f), + qiskit.qasm2.CustomClassical("atan2", 2, math.atan2), + ], + ) + self.assertEqual( + list(parsed.data[0].operation.params), [0, 2 * 1 + 2, math.atan2(3, 4) - 5] + ) + self.assertEqual(next(order), 6) + + +@ddt.ddt +class TestErrors(QiskitTestCase): + @ddt.data("0.0", "(1.0 - 1.0)") + def test_refuses_to_divide_by_zero(self, denom): + program = f"qreg q[1]; U(2.0 / {denom}, 0.0, 0.0) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "divide by zero"): + qiskit.qasm2.loads(program) + + program = f"gate rx(a) q {{ U(a / {denom}, 0.0, 0.0) q; }}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "divide by zero"): + qiskit.qasm2.loads(program) + + @ddt.data("0.0", "1.0 - 1.0", "-2.0", "2.0 - 3.0") + def test_refuses_to_ln_non_positive(self, operand): + program = f"qreg q[1]; U(ln({operand}), 0.0, 0.0) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "ln of non-positive"): + qiskit.qasm2.loads(program) + + program = f"gate rx(a) q {{ U(a + ln({operand}), 0.0, 0.0) q; }}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "ln of non-positive"): + qiskit.qasm2.loads(program) + + @ddt.data("-2.0", "2.0 - 3.0") + def test_refuses_to_sqrt_negative(self, operand): + program = f"qreg q[1]; U(sqrt({operand}), 0.0, 0.0) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "sqrt of negative"): + qiskit.qasm2.loads(program) + + program = f"gate rx(a) q {{ U(a + sqrt({operand}), 0.0, 0.0) q; }}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "sqrt of negative"): + qiskit.qasm2.loads(program) + + @ddt.data("*", "/", "^") + def test_cannot_use_nonunary_operators_in_unary_position(self, operator): + program = f"qreg q[1]; U({operator}1.0, 0.0, 0.0) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "not a valid unary operator"): + qiskit.qasm2.loads(program) + + @ddt.data("+", "-", "*", "/", "^") + def test_missing_binary_operand_errors(self, operator): + program = f"qreg q[1]; U(1.0 {operator}, 0.0, 0.0) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "missing operand"): + qiskit.qasm2.loads(program) + + program = f"qreg q[1]; U((1.0 {operator}), 0.0, 0.0) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "missing operand"): + qiskit.qasm2.loads(program) + + def test_parenthesis_must_be_closed(self): + program = "qreg q[1]; U((1 + 1 2), 3, 2) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "needed a closing parenthesis"): + qiskit.qasm2.loads(program) + + def test_premature_right_parenthesis(self): + program = "qreg q[1]; U(sin(), 0.0, 0.0) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "did not find an .* expression"): + qiskit.qasm2.loads(program) diff --git a/test/python/qasm2/test_lexer.py b/test/python/qasm2/test_lexer.py new file mode 100644 index 000000000000..6d3c4a52d588 --- /dev/null +++ b/test/python/qasm2/test_lexer.py @@ -0,0 +1,171 @@ +# 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-module-docstring,missing-class-docstring,missing-function-docstring + +import ddt + +import qiskit.qasm2 +from qiskit.test import QiskitTestCase + + +@ddt.ddt +class TestLexer(QiskitTestCase): + # Most of the lexer is fully exercised in the parser tests. These tests here are really mopping + # up some error messages and whatnot that might otherwise be missed. + + def test_pathological_formatting(self): + # This is deliberately _terribly_ formatted, included multiple blanks lines in quick + # succession and comments in places you really wouldn't expect to see comments. + program = """ + OPENQASM + + + // do we really need a comment here? + + 2.0//and another comment very squished up + ; + + include // this line introduces a file import + "qelib1.inc" // this is the file imported + ; // this is a semicolon + + gate // we're making a gate + bell( // void, with loose parenthesis in comment ) + ) a,// +b{h a;cx a //,,,, +,b;} + + qreg // a quantum register + q + [ // a square bracket + + + + + 2];bell q[0],// +q[1];creg c[2];measure q->c;""" + parsed = qiskit.qasm2.loads(program) + expected_unrolled = qiskit.QuantumCircuit( + qiskit.QuantumRegister(2, "q"), qiskit.ClassicalRegister(2, "c") + ) + expected_unrolled.h(0) + expected_unrolled.cx(0, 1) + expected_unrolled.measure([0, 1], [0, 1]) + self.assertEqual(parsed.decompose(), expected_unrolled) + + @ddt.data("0.25", "00.25", "2.5e-1", "2.5e-01", "0.025E+1", ".25", ".025e1", "25e-2") + def test_float_lexes(self, number): + program = f"qreg q[1]; U({number}, 0, 0) q[0];" + parsed = qiskit.qasm2.loads(program) + self.assertEqual(list(parsed.data[0].operation.params), [0.25, 0, 0]) + + def test_no_decimal_float_rejected_in_strict_mode(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, + r"\[strict\] all floats must include a decimal point", + ): + qiskit.qasm2.loads("OPENQASM 2.0; qreg q[1]; U(25e-2, 0, 0) q[0];", strict=True) + + @ddt.data("", "qre", "cre", ".") + def test_non_ascii_bytes_error(self, prefix): + token = f"{prefix}\xff" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "encountered a non-ASCII byte"): + qiskit.qasm2.loads(token) + + def test_integers_cannot_start_with_zero(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "integers cannot have leading zeroes" + ): + qiskit.qasm2.loads("0123") + + @ddt.data("", "+", "-") + def test_float_exponents_must_have_a_digit(self, sign): + token = f"12.34e{sign}" + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "needed to see an integer exponent" + ): + qiskit.qasm2.loads(token) + + def test_non_builtins_cannot_be_capitalised(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "identifiers cannot start with capital" + ): + qiskit.qasm2.loads("Qubit") + + def test_unterminated_filename_is_invalid(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "unexpected end-of-file while lexing string literal" + ): + qiskit.qasm2.loads('include "qelib1.inc') + + def test_filename_with_linebreak_is_invalid(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "unexpected line break while lexing string literal" + ): + qiskit.qasm2.loads('include "qe\nlib1.inc";') + + def test_strict_single_quoted_path_rejected(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"\[strict\] paths must be in double quotes" + ): + qiskit.qasm2.loads("OPENQASM 2.0; include 'qelib1.inc';", strict=True) + + def test_version_must_have_word_boundary_after(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"expected a word boundary after a version" + ): + qiskit.qasm2.loads("OPENQASM 2a;") + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"expected a word boundary after a version" + ): + qiskit.qasm2.loads("OPENQASM 2.0a;") + + def test_no_boundary_float_in_version_position(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"expected a word boundary after a float" + ): + qiskit.qasm2.loads("OPENQASM .5a;") + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"expected a word boundary after a float" + ): + qiskit.qasm2.loads("OPENQASM 0.2e1a;") + + def test_integers_must_have_word_boundaries_after(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"expected a word boundary after an integer" + ): + qiskit.qasm2.loads("OPENQASM 2.0; qreg q[2a];") + + def test_floats_must_have_word_boundaries_after(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"expected a word boundary after a float" + ): + qiskit.qasm2.loads("OPENQASM 2.0; qreg q[1]; U(2.0a, 0, 0) q[0];") + + def test_single_equals_is_rejected(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"single equals '=' is never valid" + ): + qiskit.qasm2.loads("if (a = 2) U(0, 0, 0) q[0];") + + def test_bare_dot_is_not_valid_float(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"expected a numeric fractional part" + ): + qiskit.qasm2.loads("qreg q[0]; U(2 + ., 0, 0) q[0];") + + def test_invalid_token(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"encountered '!', which doesn't match" + ): + qiskit.qasm2.loads("!") diff --git a/test/python/qasm2/test_parse_errors.py b/test/python/qasm2/test_parse_errors.py new file mode 100644 index 000000000000..801559aac7a1 --- /dev/null +++ b/test/python/qasm2/test_parse_errors.py @@ -0,0 +1,862 @@ +# 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-module-docstring,missing-class-docstring,missing-function-docstring + +import enum +import math + +import ddt + +import qiskit.qasm2 +from qiskit.circuit import Gate, library as lib +from qiskit.test import QiskitTestCase + +from test import combine # pylint: disable=wrong-import-order + + +# We need to use this enum a _bunch_ of times, so let's not give it a long name. +# pylint: disable=invalid-name +class T(enum.Enum): + # This is a deliberately stripped-down list that doesn't include most of the expression-specific + # tokens, because we don't want to complicate matters with those in tests of the general parser + # errors. We test the expression subparser elsewhere. + OPENQASM = "OPENQASM" + BARRIER = "barrier" + CREG = "creg" + GATE = "gate" + IF = "if" + INCLUDE = "include" + MEASURE = "measure" + OPAQUE = "opaque" + QREG = "qreg" + RESET = "reset" + PI = "pi" + ARROW = "->" + EQUALS = "==" + SEMICOLON = ";" + COMMA = "," + LPAREN = "(" + RPAREN = ")" + LBRACKET = "[" + RBRACKET = "]" + LBRACE = "{" + RBRACE = "}" + ID = "q" + REAL = "1.5" + INTEGER = "1" + FILENAME = '"qelib1.inc"' + + +def bad_token_parametrisation(): + """Generate the test cases for the "bad token" tests; this makes a sequence of OpenQASM 2 + statements, then puts various invalid tokens after them to verify that the parser correctly + throws an error on them.""" + + token_set = frozenset(T) + + def without(*tokens): + return token_set - set(tokens) + + # ddt isn't a particularly great parametriser - it'll only correctly unpack tuples and lists in + # the way we really want, but if we want to control the test id, we also have to set `__name__` + # which isn't settable on either of those. We can't use unpack, then, so we just need a class + # to pass. + class BadTokenCase: + def __init__(self, statement, disallowed, name=None): + self.statement = statement + self.disallowed = disallowed + self.__name__ = name + + for statement, disallowed in [ + # This should only include stopping points where the next token is somewhat fixed; in + # places where there's a real decision to be made (such as number of qubits in a gate, + # or the statement type in a gate body), there should be a better error message. + # + # There's a large subset of OQ2 that's reducible to a regular language, so we _could_ + # define that, build a DFA for it, and use that to very quickly generate a complete set + # of tests. That would be more complex to read and verify for correctness, though. + ( + "", + without( + T.OPENQASM, + T.ID, + T.INCLUDE, + T.OPAQUE, + T.GATE, + T.QREG, + T.CREG, + T.IF, + T.RESET, + T.BARRIER, + T.MEASURE, + T.SEMICOLON, + ), + ), + ("OPENQASM", without(T.REAL, T.INTEGER)), + ("OPENQASM 2.0", without(T.SEMICOLON)), + ("include", without(T.FILENAME)), + ('include "qelib1.inc"', without(T.SEMICOLON)), + ("opaque", without(T.ID)), + ("opaque bell", without(T.LPAREN, T.ID, T.SEMICOLON)), + ("opaque bell (", without(T.ID, T.RPAREN)), + ("opaque bell (a", without(T.COMMA, T.RPAREN)), + ("opaque bell (a,", without(T.ID, T.RPAREN)), + ("opaque bell (a, b", without(T.COMMA, T.RPAREN)), + ("opaque bell (a, b)", without(T.ID, T.SEMICOLON)), + ("opaque bell (a, b) q1", without(T.COMMA, T.SEMICOLON)), + ("opaque bell (a, b) q1,", without(T.ID, T.SEMICOLON)), + ("opaque bell (a, b) q1, q2", without(T.COMMA, T.SEMICOLON)), + ("gate", without(T.ID)), + ("gate bell (", without(T.ID, T.RPAREN)), + ("gate bell (a", without(T.COMMA, T.RPAREN)), + ("gate bell (a,", without(T.ID, T.RPAREN)), + ("gate bell (a, b", without(T.COMMA, T.RPAREN)), + ("gate bell (a, b) q1", without(T.COMMA, T.LBRACE)), + ("gate bell (a, b) q1,", without(T.ID, T.LBRACE)), + ("gate bell (a, b) q1, q2", without(T.COMMA, T.LBRACE)), + ("qreg", without(T.ID)), + ("qreg reg", without(T.LBRACKET)), + ("qreg reg[", without(T.INTEGER)), + ("qreg reg[5", without(T.RBRACKET)), + ("qreg reg[5]", without(T.SEMICOLON)), + ("creg", without(T.ID)), + ("creg reg", without(T.LBRACKET)), + ("creg reg[", without(T.INTEGER)), + ("creg reg[5", without(T.RBRACKET)), + ("creg reg[5]", without(T.SEMICOLON)), + ("CX", without(T.LPAREN, T.ID, T.SEMICOLON)), + ("CX(", without(T.PI, T.INTEGER, T.REAL, T.ID, T.LPAREN, T.RPAREN)), + ("CX()", without(T.ID, T.SEMICOLON)), + ("CX q", without(T.LBRACKET, T.COMMA, T.SEMICOLON)), + ("CX q[", without(T.INTEGER)), + ("CX q[0", without(T.RBRACKET)), + ("CX q[0]", without(T.COMMA, T.SEMICOLON)), + ("CX q[0],", without(T.ID, T.SEMICOLON)), + ("CX q[0], q", without(T.LBRACKET, T.COMMA, T.SEMICOLON)), + # No need to repeatedly "every" possible number of arguments. + ("measure", without(T.ID)), + ("measure q", without(T.LBRACKET, T.ARROW)), + ("measure q[", without(T.INTEGER)), + ("measure q[0", without(T.RBRACKET)), + ("measure q[0]", without(T.ARROW)), + ("measure q[0] ->", without(T.ID)), + ("measure q[0] -> c", without(T.LBRACKET, T.SEMICOLON)), + ("measure q[0] -> c[", without(T.INTEGER)), + ("measure q[0] -> c[0", without(T.RBRACKET)), + ("measure q[0] -> c[0]", without(T.SEMICOLON)), + ("reset", without(T.ID)), + ("reset q", without(T.LBRACKET, T.SEMICOLON)), + ("reset q[", without(T.INTEGER)), + ("reset q[0", without(T.RBRACKET)), + ("reset q[0]", without(T.SEMICOLON)), + ("barrier", without(T.ID, T.SEMICOLON)), + ("barrier q", without(T.LBRACKET, T.COMMA, T.SEMICOLON)), + ("barrier q[", without(T.INTEGER)), + ("barrier q[0", without(T.RBRACKET)), + ("barrier q[0]", without(T.COMMA, T.SEMICOLON)), + ("if", without(T.LPAREN)), + ("if (", without(T.ID)), + ("if (cond", without(T.EQUALS)), + ("if (cond ==", without(T.INTEGER)), + ("if (cond == 0", without(T.RPAREN)), + ("if (cond == 0)", without(T.ID, T.RESET, T.MEASURE)), + ]: + for token in disallowed: + yield BadTokenCase(statement, token.value, name=f"'{statement}'-{token.name.lower()}") + + +def eof_parametrisation(): + for tokens in [ + ("OPENQASM", "2.0", ";"), + ("include", '"qelib1.inc"', ";"), + ("opaque", "bell", "(", "a", ",", "b", ")", "q1", ",", "q2", ";"), + ("gate", "bell", "(", "a", ",", "b", ")", "q1", ",", "q2", "{", "}"), + ("qreg", "qr", "[", "5", "]", ";"), + ("creg", "cr", "[", "5", "]", ";"), + ("CX", "(", ")", "q", "[", "0", "]", ",", "q", "[", "1", "]", ";"), + ("measure", "q", "[", "0", "]", "->", "c", "[", "0", "]", ";"), + ("reset", "q", "[", "0", "]", ";"), + ("barrier", "q", ";"), + # No need to test every combination of `if`, really. + ("if", "(", "cond", "==", "0", ")", "CX q[0], q[1];"), + ]: + prefix = "" + for token in tokens[:-1]: + prefix = f"{prefix} {token}".strip() + yield prefix + + +@ddt.ddt +class TestIncompleteStructure(QiskitTestCase): + PRELUDE = "OPENQASM 2.0; qreg q[5]; creg c[5]; creg cond[1];" + + @ddt.idata(bad_token_parametrisation()) + def test_bad_token(self, case): + """Test that the parser raises an error when an incorrect token is given.""" + statement = case.statement + disallowed = case.disallowed + + prelude = "" if statement.startswith("OPENQASM") else self.PRELUDE + full = f"{prelude} {statement} {disallowed}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "needed .*, but instead"): + qiskit.qasm2.loads(full) + + @ddt.idata(eof_parametrisation()) + def test_eof(self, statement): + """Test that the parser raises an error when the end-of-file is reached instead of a token + that is required.""" + prelude = "" if statement.startswith("OPENQASM") else self.PRELUDE + full = f"{prelude} {statement}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "unexpected end-of-file"): + qiskit.qasm2.loads(full) + + +class TestVersion(QiskitTestCase): + def test_invalid_version(self): + program = "OPENQASM 3.0;" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "can only handle OpenQASM 2.0"): + qiskit.qasm2.loads(program) + + program = "OPENQASM 2.1;" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "can only handle OpenQASM 2.0"): + qiskit.qasm2.loads(program) + + program = "OPENQASM 20.e-1;" + with self.assertRaises(qiskit.qasm2.QASM2ParseError): + qiskit.qasm2.loads(program) + + def test_openqasm_must_be_first_statement(self): + program = "qreg q[0]; OPENQASM 2.0;" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "only the first statement"): + qiskit.qasm2.loads(program) + + +@ddt.ddt +class TestScoping(QiskitTestCase): + def test_register_use_before_definition(self): + program = "CX after[0], after[1]; qreg after[2];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "not defined in this scope"): + qiskit.qasm2.loads(program) + + program = "qreg q[2]; measure q[0] -> c[0]; creg c[2];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "not defined in this scope"): + qiskit.qasm2.loads(program) + + @combine( + definer=["qreg reg[2];", "creg reg[2];", "gate reg a {}", "opaque reg a;"], + bad_definer=["qreg reg[2];", "creg reg[2];"], + ) + def test_register_already_defined(self, definer, bad_definer): + program = f"{definer} {bad_definer}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "already defined"): + qiskit.qasm2.loads(program) + + def test_qelib1_not_implicit(self): + program = """ + OPENQASM 2.0; + qreg q[2]; + cx q[0], q[1]; + """ + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'cx' is not defined"): + qiskit.qasm2.loads(program) + + def test_cannot_access_gates_before_definition(self): + program = """ + qreg q[2]; + cx q[0], q[1]; + gate cx a, b { + CX a, b; + } + """ + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'cx' is not defined"): + qiskit.qasm2.loads(program) + + def test_cannot_access_gate_recursively(self): + program = """ + gate cx a, b { + cx a, b; + } + """ + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'cx' is not defined"): + qiskit.qasm2.loads(program) + + def test_cannot_access_qubits_from_previous_gate(self): + program = """ + gate cx a, b { + CX a, b; + } + gate other c { + CX a, b; + } + """ + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'a' is not defined"): + qiskit.qasm2.loads(program) + + def test_cannot_access_parameters_from_previous_gate(self): + program = """ + gate first(a, b) q { + U(a, 0, b) q; + } + gate second q { + U(a, 0, b) q; + } + """ + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "'a' is not a parameter.*defined" + ): + qiskit.qasm2.loads(program) + + def test_cannot_access_quantum_registers_within_gate(self): + program = """ + qreg q[2]; + gate my_gate a { + CX a, q; + } + """ + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'q' is a quantum register"): + qiskit.qasm2.loads(program) + + def test_parameters_not_defined_outside_gate(self): + program = """ + gate my_gate(a) q {} + qreg qr[2]; + U(a, 0, 0) qr; + """ + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "'a' is not a parameter.*defined" + ): + qiskit.qasm2.loads(program) + + def test_qubits_not_defined_outside_gate(self): + program = """ + gate my_gate(a) q {} + U(0, 0, 0) q; + """ + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'q' is not defined"): + qiskit.qasm2.loads(program) + + @ddt.data('include "qelib1.inc";', "gate h q { }") + def test_gates_cannot_redefine(self, definer): + program = f"{definer} gate h q {{ }}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "already defined"): + qiskit.qasm2.loads(program) + + def test_cannot_use_undeclared_register_conditional(self): + program = "qreg q[1]; if (c == 0) U(0, 0, 0) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "not defined"): + qiskit.qasm2.loads(program) + + +@ddt.ddt +class TestTyping(QiskitTestCase): + @ddt.data( + "CX q[0], U;", + "measure U -> c[0];", + "measure q[0] -> U;", + "reset U;", + "barrier U;", + "if (U == 0) CX q[0], q[1];", + "gate my_gate a { U(0, 0, 0) U; }", + ) + def test_cannot_use_gates_incorrectly(self, usage): + program = f"qreg q[2]; creg c[2]; {usage}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'U' is a gate"): + qiskit.qasm2.loads(program) + + @ddt.data( + "measure q[0] -> q[1];", + "if (q == 0) CX q[0], q[1];", + "q q[0], q[1];", + "gate my_gate a { U(0, 0, 0) q; }", + ) + def test_cannot_use_qregs_incorrectly(self, usage): + program = f"qreg q[2]; creg c[2]; {usage}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'q' is a quantum register"): + qiskit.qasm2.loads(program) + + @ddt.data( + "CX q[0], c[1];", + "measure c[0] -> c[1];", + "reset c[0];", + "barrier c[0];", + "c q[0], q[1];", + "gate my_gate a { U(0, 0, 0) c; }", + ) + def test_cannot_use_cregs_incorrectly(self, usage): + program = f"qreg q[2]; creg c[2]; {usage}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'c' is a classical register"): + qiskit.qasm2.loads(program) + + def test_cannot_use_parameters_incorrectly(self): + program = "gate my_gate(p) q { CX p, q; }" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'p' is a parameter"): + qiskit.qasm2.loads(program) + + def test_cannot_use_qubits_incorrectly(self): + program = "gate my_gate(p) q { U(q, q, q) q; }" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'q' is a gate qubit"): + qiskit.qasm2.loads(program) + + @ddt.data(("h", 0), ("h", 2), ("CX", 0), ("CX", 1), ("CX", 3), ("ccx", 2), ("ccx", 4)) + @ddt.unpack + def test_gates_accept_only_valid_number_qubits(self, gate, bad_count): + arguments = ", ".join(f"q[{i}]" for i in range(bad_count)) + program = f'include "qelib1.inc"; qreg q[5];\n{gate} {arguments};' + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "takes .* quantum arguments?"): + qiskit.qasm2.loads(program) + + @ddt.data(("U", 2), ("U", 4), ("rx", 0), ("rx", 2), ("u3", 1)) + @ddt.unpack + def test_gates_accept_only_valid_number_parameters(self, gate, bad_count): + arguments = ", ".join("0" for _ in [None] * bad_count) + program = f'include "qelib1.inc"; qreg q[5];\n{gate}({arguments}) q[0];' + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "takes .* parameters?"): + qiskit.qasm2.loads(program) + + +@ddt.ddt +class TestGateDefinition(QiskitTestCase): + def test_no_zero_qubit(self): + program = "gate zero {}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "gates must act on at least one"): + qiskit.qasm2.loads(program) + + program = "gate zero(a) {}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "gates must act on at least one"): + qiskit.qasm2.loads(program) + + def test_no_zero_qubit_opaque(self): + program = "opaque zero;" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "gates must act on at least one"): + qiskit.qasm2.loads(program) + + program = "opaque zero(a);" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "gates must act on at least one"): + qiskit.qasm2.loads(program) + + def test_cannot_subscript_qubit(self): + program = """ + gate my_gate a { + CX a[0], a[1]; + } + """ + with self.assertRaises(qiskit.qasm2.QASM2ParseError): + qiskit.qasm2.loads(program) + + def test_cannot_repeat_parameters(self): + program = "gate my_gate(a, a) q {}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "already defined"): + qiskit.qasm2.loads(program) + + def test_cannot_repeat_qubits(self): + program = "gate my_gate a, a {}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "already defined"): + qiskit.qasm2.loads(program) + + def test_qubit_cannot_shadow_parameter(self): + program = "gate my_gate(a) a {}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "already defined"): + qiskit.qasm2.loads(program) + + @ddt.data("measure q -> c;", "reset q", "if (c == 0) U(0, 0, 0) q;", "gate my_x q {}") + def test_definition_cannot_contain_nonunitary(self, statement): + program = f"OPENQASM 2.0; creg c[5]; gate my_gate q {{ {statement} }}" + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "only gate applications are valid" + ): + qiskit.qasm2.loads(program) + + def test_cannot_redefine_u(self): + program = "gate U(a, b, c) q {}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "already defined"): + qiskit.qasm2.loads(program) + + def test_cannot_redefine_cx(self): + program = "gate CX a, b {}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "already defined"): + qiskit.qasm2.loads(program) + + +@ddt.ddt +class TestBitResolution(QiskitTestCase): + def test_disallow_out_of_range(self): + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "out-of-range"): + qiskit.qasm2.loads("qreg q[2]; U(0, 0, 0) q[2];") + + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "out-of-range"): + qiskit.qasm2.loads("qreg q[2]; creg c[2]; measure q[2] -> c[0];") + + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "out-of-range"): + qiskit.qasm2.loads("qreg q[2]; creg c[2]; measure q[0] -> c[2];") + + @combine( + conditional=[True, False], + call=[ + "CX q1[0], q1[0];", + "CX q1, q1[0];", + "CX q1[0], q1;", + "CX q1, q1;", + "ccx q1[0], q1[1], q1[0];", + "ccx q2, q1, q2[0];", + ], + ) + def test_disallow_duplicate_qubits(self, call, conditional): + program = """ + include "qelib1.inc"; + qreg q1[3]; + qreg q2[3]; + qreg q3[3]; + """ + if conditional: + program += "creg cond[1]; if (cond == 0) " + program += call + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "duplicate qubit"): + qiskit.qasm2.loads(program) + + @ddt.data( + (("q1[1]", "q2[2]"), "CX q1, q2"), + (("q1[1]", "q2[2]"), "CX q2, q1"), + (("q1[3]", "q2[2]"), "CX q1, q2"), + (("q1[2]", "q2[3]", "q3[3]"), "ccx q1, q2, q3"), + (("q1[2]", "q2[3]", "q3[3]"), "ccx q2, q3, q1"), + (("q1[2]", "q2[3]", "q3[3]"), "ccx q3, q1, q2"), + (("q1[2]", "q2[3]", "q3[3]"), "ccx q1, q2[0], q3"), + (("q1[2]", "q2[3]", "q3[3]"), "ccx q2[0], q3, q1"), + (("q1[2]", "q2[3]", "q3[3]"), "ccx q3, q1, q2[0]"), + ) + @ddt.unpack + def test_incorrect_gate_broadcast_lengths(self, registers, call): + setup = 'include "qelib1.inc";\n' + "\n".join(f"qreg {reg};" for reg in registers) + program = f"{setup}\n{call};" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "cannot resolve broadcast"): + qiskit.qasm2.loads(program) + + cond = "creg cond[1];\nif (cond == 0)" + program = f"{setup}\n{cond} {call};" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "cannot resolve broadcast"): + qiskit.qasm2.loads(program) + + @ddt.data( + ("qreg q[2]; creg c[2];", "q[0] -> c"), + ("qreg q[2]; creg c[2];", "q -> c[0]"), + ("qreg q[1]; creg c[2];", "q -> c[0]"), + ("qreg q[2]; creg c[1];", "q[0] -> c"), + ("qreg q[2]; creg c[3];", "q -> c"), + ) + @ddt.unpack + def test_incorrect_measure_broadcast_lengths(self, setup, operands): + program = f"{setup}\nmeasure {operands};" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "cannot resolve broadcast"): + qiskit.qasm2.loads(program) + + program = f"{setup}\ncreg cond[1];\nif (cond == 0) measure {operands};" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "cannot resolve broadcast"): + qiskit.qasm2.loads(program) + + +@ddt.ddt +class TestCustomInstructions(QiskitTestCase): + def test_cannot_use_custom_before_definition(self): + program = "qreg q[2]; my_gate q[0], q[1];" + + class MyGate(Gate): + def __init__(self): + super().__init__("my_gate", 2, []) + + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "cannot use .* before definition" + ): + qiskit.qasm2.loads( + program, + custom_instructions=[qiskit.qasm2.CustomInstruction("my_gate", 0, 2, MyGate)], + ) + + def test_cannot_misdefine_u(self): + program = "qreg q[1]; U(0.5, 0.25) q[0]" + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "custom instruction .* mismatched" + ): + qiskit.qasm2.loads( + program, custom_instructions=[qiskit.qasm2.CustomInstruction("U", 2, 1, lib.U2Gate)] + ) + + def test_cannot_misdefine_cx(self): + program = "qreg q[1]; CX q[0]" + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "custom instruction .* mismatched" + ): + qiskit.qasm2.loads( + program, custom_instructions=[qiskit.qasm2.CustomInstruction("CX", 0, 1, lib.XGate)] + ) + + def test_builtin_is_typechecked(self): + program = "qreg q[1]; my(0.5) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'my' takes 2 quantum arguments"): + qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("my", 1, 2, lib.RXXGate, builtin=True) + ], + ) + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'my' takes 2 parameters"): + qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("my", 2, 1, lib.U2Gate, builtin=True) + ], + ) + + def test_cannot_define_builtin_twice(self): + program = "gate builtin q {}; gate builtin q {};" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'builtin' is already defined"): + qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("builtin", 0, 1, lambda: Gate("builtin", 1, [])) + ], + ) + + def test_cannot_redefine_custom_u(self): + program = "gate U(a, b, c) q {}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "already defined"): + qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("U", 3, 1, lib.UGate, builtin=True) + ], + ) + + def test_cannot_redefine_custom_cx(self): + program = "gate CX a, b {}" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "already defined"): + qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("CX", 0, 2, lib.CXGate, builtin=True) + ], + ) + + @combine( + program=["gate my(a) q {}", "opaque my(a) q;"], + builtin=[True, False], + ) + def test_custom_definition_must_match_gate(self, program, builtin): + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'my' is mismatched"): + qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("my", 1, 2, lib.RXXGate, builtin=builtin) + ], + ) + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "'my' is mismatched"): + qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("my", 2, 1, lib.U2Gate, builtin=builtin) + ], + ) + + def test_cannot_have_duplicate_customs(self): + customs = [ + qiskit.qasm2.CustomInstruction("my", 1, 2, lib.RXXGate), + qiskit.qasm2.CustomInstruction("x", 0, 1, lib.XGate), + qiskit.qasm2.CustomInstruction("my", 1, 2, lib.RZZGate), + ] + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "duplicate custom instruction"): + qiskit.qasm2.loads("", custom_instructions=customs) + + def test_qiskit_delay_float_input_wraps_exception(self): + program = "opaque delay(t) q; qreg q[1]; delay(1.5) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "can only accept an integer"): + qiskit.qasm2.loads(program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS) + + def test_u0_float_input_wraps_exception(self): + program = "opaque u0(n) q; qreg q[1]; u0(1.1) q[0];" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "must be an integer"): + qiskit.qasm2.loads(program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS) + + +@ddt.ddt +class TestCustomClassical(QiskitTestCase): + @ddt.data("cos", "exp", "sin", "sqrt", "tan", "ln") + def test_cannot_override_builtin(self, builtin): + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, r"cannot override builtin"): + qiskit.qasm2.loads( + "", + custom_classical=[qiskit.qasm2.CustomClassical(builtin, 1, math.exp)], + ) + + def test_duplicate_names_disallowed(self): + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, r"duplicate custom classical"): + qiskit.qasm2.loads( + "", + custom_classical=[ + qiskit.qasm2.CustomClassical("f", 1, math.exp), + qiskit.qasm2.CustomClassical("f", 1, math.exp), + ], + ) + + def test_cannot_shadow_custom_instruction(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"custom classical.*naming clash" + ): + qiskit.qasm2.loads( + "", + custom_instructions=[ + qiskit.qasm2.CustomInstruction("f", 0, 1, lib.RXGate, builtin=True) + ], + custom_classical=[qiskit.qasm2.CustomClassical("f", 1, math.exp)], + ) + + def test_cannot_shadow_builtin_instruction(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"custom classical.*cannot shadow" + ): + qiskit.qasm2.loads( + "", + custom_classical=[qiskit.qasm2.CustomClassical("U", 1, math.exp)], + ) + + def test_cannot_shadow_with_gate_definition(self): + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, r"'f' is already defined"): + qiskit.qasm2.loads( + "gate f q {}", + custom_classical=[qiskit.qasm2.CustomClassical("f", 1, math.exp)], + ) + + @ddt.data("qreg", "creg") + def test_cannot_shadow_with_register_definition(self, regtype): + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, r"'f' is already defined"): + qiskit.qasm2.loads( + f"{regtype} f[2];", + custom_classical=[qiskit.qasm2.CustomClassical("f", 1, math.exp)], + ) + + @ddt.data((0, 1), (0, 2), (1, 0), (1, 2), (2, 0), (2, 1)) + @ddt.unpack + def test_mismatched_argument_count(self, n_good, n_bad): + arg_string = ", ".join(["0" for _ in [None] * n_bad]) + program = f""" + qreg q[1]; + U(f({arg_string}), 0, 0) q[0]; + """ + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"custom function argument-count mismatch" + ): + qiskit.qasm2.loads( + program, custom_classical=[qiskit.qasm2.CustomClassical("f", n_good, lambda *_: 0)] + ) + + def test_output_type_error_is_caught(self): + program = """ + qreg q[1]; + U(f(), 0, 0) q[0]; + """ + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, r"user.*returned non-float"): + qiskit.qasm2.loads( + program, + custom_classical=[qiskit.qasm2.CustomClassical("f", 0, lambda: "not a float")], + ) + + def test_inner_exception_is_wrapped(self): + inner_exception = Exception("custom exception") + + def raises(): + raise inner_exception + + program = """ + qreg q[1]; + U(raises(), 0, 0) q[0]; + """ + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, "caught exception when constant folding" + ) as excinfo: + qiskit.qasm2.loads( + program, custom_classical=[qiskit.qasm2.CustomClassical("raises", 0, raises)] + ) + assert excinfo.exception.__cause__ is inner_exception + + def test_cannot_be_used_as_gate(self): + program = """ + qreg q[1]; + f(0) q[0]; + """ + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"'f' is a custom classical function" + ): + qiskit.qasm2.loads( + program, custom_classical=[qiskit.qasm2.CustomClassical("f", 1, lambda x: x)] + ) + + def test_cannot_be_used_as_qarg(self): + program = """ + U(0, 0, 0) f; + """ + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"'f' is a custom classical function" + ): + qiskit.qasm2.loads( + program, custom_classical=[qiskit.qasm2.CustomClassical("f", 1, lambda x: x)] + ) + + def test_cannot_be_used_as_carg(self): + program = """ + qreg q[1]; + measure q[0] -> f; + """ + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"'f' is a custom classical function" + ): + qiskit.qasm2.loads( + program, custom_classical=[qiskit.qasm2.CustomClassical("f", 1, lambda x: x)] + ) + + +@ddt.ddt +class TestStrict(QiskitTestCase): + @ddt.data( + "gate my_gate(p0, p1,) q0, q1 {}", + "gate my_gate(p0, p1) q0, q1, {}", + "opaque my_gate(p0, p1,) q0, q1;", + "opaque my_gate(p0, p1) q0, q1,;", + 'include "qelib1.inc"; qreg q[2]; cu3(0.5, 0.25, 0.125,) q[0], q[1];', + 'include "qelib1.inc"; qreg q[2]; cu3(0.5, 0.25, 0.125) q[0], q[1],;', + "qreg q[2]; barrier q[0], q[1],;", + 'include "qelib1.inc"; qreg q[1]; rx(sin(pi,)) q[0];', + ) + def test_trailing_comma(self, program): + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, r"\[strict\] .*trailing comma"): + qiskit.qasm2.loads("OPENQASM 2.0;\n" + program, strict=True) + + def test_trailing_semicolon_after_gate(self): + program = "OPENQASM 2.0; gate my_gate q {};" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, r"\[strict\] .*extra semicolon"): + qiskit.qasm2.loads(program, strict=True) + + def test_empty_statement(self): + program = "OPENQASM 2.0; ;" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, r"\[strict\] .*empty statement"): + qiskit.qasm2.loads(program, strict=True) + + def test_required_version_regular(self): + program = "qreg q[1];" + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"\[strict\] the first statement" + ): + qiskit.qasm2.loads(program, strict=True) + + def test_required_version_empty(self): + with self.assertRaisesRegex( + qiskit.qasm2.QASM2ParseError, r"\[strict\] .*needed a version statement" + ): + qiskit.qasm2.loads("", strict=True) diff --git a/test/python/qasm2/test_structure.py b/test/python/qasm2/test_structure.py new file mode 100644 index 000000000000..813dbd1e96d2 --- /dev/null +++ b/test/python/qasm2/test_structure.py @@ -0,0 +1,1711 @@ +# 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-module-docstring,missing-class-docstring,missing-function-docstring + +import io +import math +import os +import pathlib +import pickle +import shutil +import tempfile +import unittest + +import ddt + +import qiskit.qasm2 +from qiskit import qpy +from qiskit.circuit import ( + ClassicalRegister, + Gate, + Parameter, + QuantumCircuit, + QuantumRegister, + Qubit, + library as lib, +) +from qiskit.test import QiskitTestCase + +from . import gate_builder + + +class TestEmpty(QiskitTestCase): + def test_allows_empty(self): + self.assertEqual(qiskit.qasm2.loads(""), QuantumCircuit()) + + +class TestVersion(QiskitTestCase): + def test_complete_version(self): + program = "OPENQASM 2.0;" + parsed = qiskit.qasm2.loads(program) + self.assertEqual(parsed, QuantumCircuit()) + + def test_incomplete_version(self): + program = "OPENQASM 2;" + parsed = qiskit.qasm2.loads(program) + self.assertEqual(parsed, QuantumCircuit()) + + def test_after_comment(self): + program = """ + // hello, world + OPENQASM 2.0; + qreg q[2]; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q")) + self.assertEqual(parsed, qc) + + +class TestRegisters(QiskitTestCase): + def test_qreg(self): + program = "qreg q1[2]; qreg q2[1]; qreg q3[4];" + parsed = qiskit.qasm2.loads(program) + regs = [QuantumRegister(2, "q1"), QuantumRegister(1, "q2"), QuantumRegister(4, "q3")] + self.assertEqual(list(parsed.qregs), regs) + self.assertEqual(list(parsed.cregs), []) + + def test_creg(self): + program = "creg c1[2]; creg c2[1]; creg c3[4];" + parsed = qiskit.qasm2.loads(program) + regs = [ClassicalRegister(2, "c1"), ClassicalRegister(1, "c2"), ClassicalRegister(4, "c3")] + self.assertEqual(list(parsed.cregs), regs) + self.assertEqual(list(parsed.qregs), []) + + def test_interleaved_registers(self): + program = "qreg q1[3]; creg c1[2]; qreg q2[1]; creg c2[1];" + parsed = qiskit.qasm2.loads(program) + qregs = [QuantumRegister(3, "q1"), QuantumRegister(1, "q2")] + cregs = [ClassicalRegister(2, "c1"), ClassicalRegister(1, "c2")] + self.assertEqual(list(parsed.qregs), qregs) + self.assertEqual(list(parsed.cregs), cregs) + + def test_registers_after_gate(self): + program = "qreg before[2]; CX before[0], before[1]; qreg after[2]; CX after[0], after[1];" + parsed = qiskit.qasm2.loads(program) + before = QuantumRegister(2, "before") + after = QuantumRegister(2, "after") + qc = QuantumCircuit(before, after) + qc.cx(before[0], before[1]) + qc.cx(after[0], after[1]) + self.assertEqual(parsed, qc) + + def test_empty_registers(self): + program = "qreg q[0]; creg c[0];" + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(0, "q"), ClassicalRegister(0, "c")) + self.assertEqual(parsed, qc) + + +@ddt.ddt +class TestGateApplication(QiskitTestCase): + def test_builtin_single(self): + program = """ + qreg q[2]; + U(0, 0, 0) q[0]; + CX q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.u(0, 0, 0, 0) + qc.cx(0, 1) + self.assertEqual(parsed, qc) + + def test_builtin_1q_broadcast(self): + program = "qreg q[2]; U(0, 0, 0) q;" + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.u(0, 0, 0, 0) + qc.u(0, 0, 0, 1) + self.assertEqual(parsed, qc) + + def test_builtin_2q_broadcast(self): + program = """ + qreg q1[2]; + qreg q2[2]; + CX q1[0], q2; + barrier; + CX q1, q2[1]; + barrier; + CX q1, q2; + """ + parsed = qiskit.qasm2.loads(program) + q1 = QuantumRegister(2, "q1") + q2 = QuantumRegister(2, "q2") + qc = QuantumCircuit(q1, q2) + qc.cx(q1[0], q2[0]) + qc.cx(q1[0], q2[1]) + qc.barrier() + qc.cx(q1[0], q2[1]) + qc.cx(q1[1], q2[1]) + qc.barrier() + qc.cx(q1[0], q2[0]) + qc.cx(q1[1], q2[1]) + self.assertEqual(parsed, qc) + + def test_3q_broadcast(self): + program = """ + include "qelib1.inc"; + qreg q1[2]; + qreg q2[2]; + qreg q3[2]; + + ccx q1, q2[0], q3[1]; + ccx q1[1], q2, q3[0]; + ccx q1[0], q2[1], q3; + barrier; + + ccx q1, q2, q3[1]; + ccx q1[1], q2, q3; + ccx q1, q2[1], q3; + barrier; + + ccx q1, q2, q3; + """ + parsed = qiskit.qasm2.loads(program) + q1 = QuantumRegister(2, "q1") + q2 = QuantumRegister(2, "q2") + q3 = QuantumRegister(2, "q3") + qc = QuantumCircuit(q1, q2, q3) + qc.ccx(q1[0], q2[0], q3[1]) + qc.ccx(q1[1], q2[0], q3[1]) + qc.ccx(q1[1], q2[0], q3[0]) + qc.ccx(q1[1], q2[1], q3[0]) + qc.ccx(q1[0], q2[1], q3[0]) + qc.ccx(q1[0], q2[1], q3[1]) + qc.barrier() + qc.ccx(q1[0], q2[0], q3[1]) + qc.ccx(q1[1], q2[1], q3[1]) + qc.ccx(q1[1], q2[0], q3[0]) + qc.ccx(q1[1], q2[1], q3[1]) + qc.ccx(q1[0], q2[1], q3[0]) + qc.ccx(q1[1], q2[1], q3[1]) + qc.barrier() + qc.ccx(q1[0], q2[0], q3[0]) + qc.ccx(q1[1], q2[1], q3[1]) + self.assertEqual(parsed, qc) + + @ddt.data(True, False) + def test_broadcast_against_empty_register(self, conditioned): + cond = "if (cond == 0) " if conditioned else "" + program = f""" + OPENQASM 2; + include "qelib1.inc"; + qreg q1[1]; + qreg q2[1]; + qreg empty1[0]; + qreg empty2[0]; + qreg empty3[0]; + creg cond[1]; + + // None of the following statements should produce any gate applications. + {cond}h empty1; + + {cond}cx q1[0], empty1; + {cond}cx empty1, q2[0]; + {cond}cx empty1, empty2; + + {cond}ccx empty1, q1[0], q2[0]; + {cond}ccx q1[0], empty2, q2[0]; + {cond}ccx q1[0], q2[0], empty3; + + {cond}ccx empty1, empty2, q1[0]; + {cond}ccx empty1, q1[0], empty2; + {cond}ccx q1[0], empty1, empty2; + + {cond}ccx empty1, empty2, empty3; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit( + QuantumRegister(1, "q1"), + QuantumRegister(1, "q2"), + QuantumRegister(0, "empty1"), + QuantumRegister(0, "empty2"), + QuantumRegister(0, "empty3"), + ClassicalRegister(1, "cond"), + ) + self.assertEqual(parsed, qc) + + def test_conditioned(self): + program = """ + qreg q[2]; + creg cond[1]; + if (cond == 0) U(0, 0, 0) q[0]; + if (cond == 1) CX q[1], q[0]; + """ + parsed = qiskit.qasm2.loads(program) + cond = ClassicalRegister(1, "cond") + qc = QuantumCircuit(QuantumRegister(2, "q"), cond) + qc.u(0, 0, 0, 0).c_if(cond, 0) + qc.cx(1, 0).c_if(cond, 1) + self.assertEqual(parsed, qc) + + def test_conditioned_broadcast(self): + program = """ + qreg q1[2]; + qreg q2[2]; + creg cond[1]; + if (cond == 0) U(0, 0, 0) q1; + if (cond == 1) CX q1[0], q2; + """ + parsed = qiskit.qasm2.loads(program) + cond = ClassicalRegister(1, "cond") + q1 = QuantumRegister(2, "q1") + q2 = QuantumRegister(2, "q2") + qc = QuantumCircuit(q1, q2, cond) + qc.u(0, 0, 0, q1[0]).c_if(cond, 0) + qc.u(0, 0, 0, q1[1]).c_if(cond, 0) + qc.cx(q1[0], q2[0]).c_if(cond, 1) + qc.cx(q1[0], q2[1]).c_if(cond, 1) + self.assertEqual(parsed, qc) + + def test_constant_folding(self): + # Most expression-related things are tested in `test_expression.py` instead. + program = """ + qreg q[1]; + U(4 + 3 * 2 ^ 2, cos(pi) * (1 - ln(1)), 2 ^ 3 ^ 2) q[0]; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.u(16.0, -1.0, 512.0, 0) + self.assertEqual(parsed, qc) + + def test_call_defined_gate(self): + program = """ + gate my_gate a { + U(0, 0, 0) a; + } + qreg q[2]; + my_gate q[0]; + my_gate q; + """ + parsed = qiskit.qasm2.loads(program) + my_gate_def = QuantumCircuit([Qubit()]) + my_gate_def.u(0, 0, 0, 0) + my_gate = gate_builder("my_gate", [], my_gate_def) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(my_gate(), [0]) + qc.append(my_gate(), [0]) + qc.append(my_gate(), [1]) + self.assertEqual(parsed, qc) + + def test_parameterless_gates_accept_parentheses(self): + program = """ + qreg q[2]; + CX q[0], q[1]; + CX() q[1], q[0]; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.cx(0, 1) + qc.cx(1, 0) + self.assertEqual(parsed, qc) + + +class TestGateDefinition(QiskitTestCase): + def test_simple_definition(self): + program = """ + gate not_bell a, b { + U(0, 0, 0) a; + CX a, b; + } + qreg q[2]; + not_bell q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + not_bell_def = QuantumCircuit([Qubit(), Qubit()]) + not_bell_def.u(0, 0, 0, 0) + not_bell_def.cx(0, 1) + not_bell = gate_builder("not_bell", [], not_bell_def) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(not_bell(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_conditioned(self): + program = """ + gate not_bell a, b { + U(0, 0, 0) a; + CX a, b; + } + qreg q[2]; + creg cond[1]; + if (cond == 0) not_bell q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + not_bell_def = QuantumCircuit([Qubit(), Qubit()]) + not_bell_def.u(0, 0, 0, 0) + not_bell_def.cx(0, 1) + not_bell = gate_builder("not_bell", [], not_bell_def) + cond = ClassicalRegister(1, "cond") + qc = QuantumCircuit(QuantumRegister(2, "q"), cond) + qc.append(not_bell().c_if(cond, 0), [0, 1]) + self.assertEqual(parsed, qc) + + def test_constant_folding_in_definition(self): + program = """ + gate bell a, b { + U(pi/2, 0, pi) a; + CX a, b; + } + qreg q[2]; + bell q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + bell_def = QuantumCircuit([Qubit(), Qubit()]) + bell_def.u(math.pi / 2, 0, math.pi, 0) + bell_def.cx(0, 1) + bell = gate_builder("bell", [], bell_def) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(bell(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_parameterised_gate(self): + # Most of the tests of deep parameter expressions are in `test_expression.py`. + program = """ + gate my_gate(a, b) c { + U(a, b, a + 2 * b) c; + } + qreg q[1]; + my_gate(0.25, 0.5) q[0]; + my_gate(0.5, 0.25) q[0]; + """ + parsed = qiskit.qasm2.loads(program) + a, b = Parameter("a"), Parameter("b") + my_gate_def = QuantumCircuit([Qubit()]) + my_gate_def.u(a, b, a + 2 * b, 0) + my_gate = gate_builder("my_gate", [a, b], my_gate_def) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.append(my_gate(0.25, 0.5), [0]) + qc.append(my_gate(0.5, 0.25), [0]) + self.assertEqual(parsed, qc) + + # Also check the decomposition has come out exactly as expected. The floating-point + # assertions are safe as exact equality checks because there are no lossy operations with + # these parameters, and the answer should be exact. + decomposed = qc.decompose() + self.assertEqual(decomposed.data[0].operation.name, "u") + self.assertEqual(list(decomposed.data[0].operation.params), [0.25, 0.5, 1.25]) + self.assertEqual(decomposed.data[1].operation.name, "u") + self.assertEqual(list(decomposed.data[1].operation.params), [0.5, 0.25, 1.0]) + + def test_parameterless_gate_with_parentheses(self): + program = """ + gate my_gate() a { + U(0, 0, 0) a; + } + qreg q[1]; + my_gate q[0]; + my_gate() q[0]; + """ + parsed = qiskit.qasm2.loads(program) + my_gate_def = QuantumCircuit([Qubit()]) + my_gate_def.u(0, 0, 0, 0) + my_gate = gate_builder("my_gate", [], my_gate_def) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.append(my_gate(), [0]) + qc.append(my_gate(), [0]) + self.assertEqual(parsed, qc) + + def test_access_includes_in_definition(self): + program = """ + include "qelib1.inc"; + gate bell a, b { + h a; + cx a, b; + } + qreg q[2]; + bell q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + bell_def = QuantumCircuit([Qubit(), Qubit()]) + bell_def.h(0) + bell_def.cx(0, 1) + bell = gate_builder("bell", [], bell_def) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(bell(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_access_previous_defined_gate(self): + program = """ + include "qelib1.inc"; + gate bell a, b { + h a; + cx a, b; + } + gate second_bell a, b { + bell b, a; + } + qreg q[2]; + second_bell q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + bell_def = QuantumCircuit([Qubit(), Qubit()]) + bell_def.h(0) + bell_def.cx(0, 1) + bell = gate_builder("bell", [], bell_def) + + second_bell_def = QuantumCircuit([Qubit(), Qubit()]) + second_bell_def.append(bell(), [1, 0]) + second_bell = gate_builder("second_bell", [], second_bell_def) + + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(second_bell(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_qubits_lookup_differently_to_gates(self): + # The spec is somewhat unclear on this, and this leads to super weird text, but it's + # technically unambiguously resolvable and this is more permissive. + program = """ + include "qelib1.inc"; + gate bell h, cx { + h h; + cx h, cx; + } + qreg q[2]; + bell q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + bell_def = QuantumCircuit([Qubit(), Qubit()]) + bell_def.h(0) + bell_def.cx(0, 1) + bell = gate_builder("bell", [], bell_def) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(bell(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_parameters_lookup_differently_to_gates(self): + # The spec is somewhat unclear on this, and this leads to super weird text, but it's + # technically unambiguously resolvable and this is more permissive. + program = """ + include "qelib1.inc"; + gate shadow(rx, rz) a { + rz(rz) a; + rx(rx) a; + } + qreg q[1]; + shadow(0.5, 2.0) q[0]; + """ + parsed = qiskit.qasm2.loads(program) + rx, rz = Parameter("rx"), Parameter("rz") + shadow_def = QuantumCircuit([Qubit()]) + shadow_def.rz(rz, 0) + shadow_def.rx(rx, 0) + shadow = gate_builder("shadow", [rx, rz], shadow_def) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.append(shadow(0.5, 2.0), [0]) + self.assertEqual(parsed, qc) + + def test_unused_parameters_convert_correctly(self): + # The main risk here is that there might be lazy application in the gate definition + # bindings, and we might accidentally try and bind parameters that aren't actually in the + # definition. + program = """ + gate my_gate(p) q { + U(0, 0, 0) q; + } + qreg q[1]; + my_gate(0.5) q[0]; + """ + parsed = qiskit.qasm2.loads(program) + # No top-level circuit equality test here, because all the internals of gate application are + # an implementation detail, and we don't want to tie the tests and implementation together + # too closely. + self.assertEqual(list(parsed.qregs), [QuantumRegister(1, "q")]) + self.assertEqual(list(parsed.cregs), []) + self.assertEqual(len(parsed.data), 1) + self.assertEqual(parsed.data[0].qubits, (parsed.qubits[0],)) + self.assertEqual(parsed.data[0].clbits, ()) + self.assertEqual(parsed.data[0].operation.name, "my_gate") + self.assertEqual(list(parsed.data[0].operation.params), [0.5]) + + decomposed = QuantumCircuit(QuantumRegister(1, "q")) + decomposed.u(0, 0, 0, 0) + self.assertEqual(parsed.decompose(), decomposed) + + def test_qubit_barrier_in_definition(self): + program = """ + gate my_gate a, b { + barrier a; + barrier b; + barrier a, b; + } + qreg q[2]; + my_gate q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + my_gate_def = QuantumCircuit([Qubit(), Qubit()]) + my_gate_def.barrier(0) + my_gate_def.barrier(1) + my_gate_def.barrier([0, 1]) + my_gate = gate_builder("my_gate", [], my_gate_def) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(my_gate(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_bare_barrier_in_definition(self): + program = """ + gate my_gate a, b { + barrier; + } + qreg q[2]; + my_gate q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + my_gate_def = QuantumCircuit([Qubit(), Qubit()]) + my_gate_def.barrier(my_gate_def.qubits) + my_gate = gate_builder("my_gate", [], my_gate_def) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(my_gate(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_duplicate_barrier_in_definition(self): + program = """ + gate my_gate a, b { + barrier a, a; + barrier b, a, b; + } + qreg q[2]; + my_gate q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + my_gate_def = QuantumCircuit([Qubit(), Qubit()]) + my_gate_def.barrier(0) + my_gate_def.barrier([1, 0]) + my_gate = gate_builder("my_gate", [], my_gate_def) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(my_gate(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_pickleable(self): + program = """ + include "qelib1.inc"; + gate my_gate(a) b, c { + rz(2 * a) b; + h b; + cx b, c; + } + qreg q[2]; + my_gate(0.5) q[0], q[1]; + my_gate(0.25) q[1], q[0]; + """ + parsed = qiskit.qasm2.loads(program) + a = Parameter("a") + my_gate_def = QuantumCircuit([Qubit(), Qubit()]) + my_gate_def.rz(2 * a, 0) + my_gate_def.h(0) + my_gate_def.cx(0, 1) + my_gate = gate_builder("my_gate", [a], my_gate_def) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(my_gate(0.5), [0, 1]) + qc.append(my_gate(0.25), [1, 0]) + self.assertEqual(parsed, qc) + with io.BytesIO() as fptr: + pickle.dump(parsed, fptr) + fptr.seek(0) + loaded = pickle.load(fptr) + self.assertEqual(parsed, loaded) + + def test_qpy_single_call_roundtrip(self): + program = """ + include "qelib1.inc"; + gate my_gate(a) b, c { + rz(2 * a) b; + h b; + cx b, c; + } + qreg q[2]; + my_gate(0.5) q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + + # QPY won't persist custom gates by design choice, so instead let us check against the + # explicit form it uses. + my_gate_def = QuantumCircuit([Qubit(), Qubit()]) + my_gate_def.rz(1.0, 0) + my_gate_def.h(0) + my_gate_def.cx(0, 1) + my_gate = Gate("my_gate", 2, [0.5]) + my_gate.definition = my_gate_def + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(my_gate, [0, 1]) + + with io.BytesIO() as fptr: + qpy.dump(parsed, fptr) + fptr.seek(0) + loaded = qpy.load(fptr)[0] + self.assertEqual(loaded, qc) + + # See https://github.com/Qiskit/qiskit-terra/issues/8941 + @unittest.expectedFailure + def test_qpy_double_call_roundtrip(self): + program = """ + include "qelib1.inc"; + gate my_gate(a) b, c { + rz(2 * a) b; + h b; + cx b, c; + } + qreg q[2]; + my_gate(0.5) q[0], q[1]; + my_gate(0.25) q[1], q[0]; + """ + parsed = qiskit.qasm2.loads(program) + + my_gate1_def = QuantumCircuit([Qubit(), Qubit()]) + my_gate1_def.rz(1.0, 0) + my_gate1_def.h(0) + my_gate1_def.cx(0, 1) + my_gate1 = Gate("my_gate", 2, [0.5]) + my_gate1.definition = my_gate1_def + + my_gate2_def = QuantumCircuit([Qubit(), Qubit()]) + my_gate2_def.rz(0.5, 0) + my_gate2_def.h(0) + my_gate2_def.cx(0, 1) + my_gate2 = Gate("my_gate", 2, [0.25]) + my_gate2.definition = my_gate2_def + + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(my_gate1, [0, 1]) + qc.append(my_gate2, [1, 0]) + + with io.BytesIO() as fptr: + qpy.dump(parsed, fptr) + fptr.seek(0) + loaded = qpy.load(fptr)[0] + self.assertEqual(loaded, qc) + + +class TestOpaque(QiskitTestCase): + def test_simple(self): + program = """ + opaque my_gate a; + opaque my_gate2() a; + qreg q[2]; + my_gate q[0]; + my_gate() q[1]; + my_gate2 q[0]; + my_gate2() q[1]; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(Gate("my_gate", 1, []), [0]) + qc.append(Gate("my_gate", 1, []), [1]) + qc.append(Gate("my_gate2", 1, []), [0]) + qc.append(Gate("my_gate2", 1, []), [1]) + self.assertEqual(parsed, qc) + + def test_parameterised(self): + program = """ + opaque my_gate(a, b) c, d; + qreg q[2]; + my_gate(0.5, 0.25) q[1], q[0]; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(Gate("my_gate", 2, [0.5, 0.25]), [1, 0]) + self.assertEqual(parsed, qc) + + +class TestBarrier(QiskitTestCase): + def test_single_register_argument(self): + program = """ + qreg first[3]; + qreg second[3]; + barrier first; + barrier second; + """ + parsed = qiskit.qasm2.loads(program) + first = QuantumRegister(3, "first") + second = QuantumRegister(3, "second") + qc = QuantumCircuit(first, second) + qc.barrier(first) + qc.barrier(second) + self.assertEqual(parsed, qc) + + def test_single_qubit_argument(self): + program = """ + qreg first[3]; + qreg second[3]; + barrier first[1]; + barrier second[0]; + """ + parsed = qiskit.qasm2.loads(program) + first = QuantumRegister(3, "first") + second = QuantumRegister(3, "second") + qc = QuantumCircuit(first, second) + qc.barrier(first[1]) + qc.barrier(second[0]) + self.assertEqual(parsed, qc) + + def test_empty_circuit_empty_arguments(self): + program = "barrier;" + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit() + self.assertEqual(parsed, qc) + + def test_one_register_circuit_empty_arguments(self): + program = "qreg q1[2]; barrier;" + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q1")) + qc.barrier(qc.qubits) + self.assertEqual(parsed, qc) + + def test_multi_register_circuit_empty_arguments(self): + program = "qreg q1[2]; qreg q2[3]; qreg q3[1]; barrier;" + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit( + QuantumRegister(2, "q1"), QuantumRegister(3, "q2"), QuantumRegister(1, "q3") + ) + qc.barrier(qc.qubits) + self.assertEqual(parsed, qc) + + def test_include_empty_register(self): + program = """ + qreg q[2]; + qreg empty[0]; + barrier empty; + barrier q, empty; + barrier; + """ + parsed = qiskit.qasm2.loads(program) + q = QuantumRegister(2, "q") + qc = QuantumCircuit(q, QuantumRegister(0, "empty")) + qc.barrier(q) + qc.barrier(qc.qubits) + self.assertEqual(parsed, qc) + + def test_allows_duplicate_arguments(self): + # There's nothing in the paper that implies this should be forbidden. + program = """ + qreg q1[3]; + qreg q2[2]; + barrier q1, q1; + barrier q1[0], q1; + barrier q1, q1[0]; + barrier q1, q2, q1; + """ + parsed = qiskit.qasm2.loads(program) + q1 = QuantumRegister(3, "q1") + q2 = QuantumRegister(2, "q2") + qc = QuantumCircuit(q1, q2) + qc.barrier(q1) + qc.barrier(q1) + qc.barrier(q1) + qc.barrier(q1, q2) + self.assertEqual(parsed, qc) + + +class TestMeasure(QiskitTestCase): + def test_single(self): + program = """ + qreg q[1]; + creg c[1]; + measure q[0] -> c[0]; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(1, "q"), ClassicalRegister(1, "c")) + qc.measure(0, 0) + self.assertEqual(parsed, qc) + + def test_broadcast(self): + program = """ + qreg q[2]; + creg c[2]; + measure q -> c; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q"), ClassicalRegister(2, "c")) + qc.measure(0, 0) + qc.measure(1, 1) + self.assertEqual(parsed, qc) + + def test_conditioned(self): + program = """ + qreg q[2]; + creg c[2]; + creg cond[1]; + if (cond == 0) measure q[0] -> c[0]; + if (cond == 1) measure q -> c; + """ + parsed = qiskit.qasm2.loads(program) + cond = ClassicalRegister(1, "cond") + qc = QuantumCircuit(QuantumRegister(2, "q"), ClassicalRegister(2, "c"), cond) + qc.measure(0, 0).c_if(cond, 0) + qc.measure(0, 0).c_if(cond, 1) + qc.measure(1, 1).c_if(cond, 1) + self.assertEqual(parsed, qc) + + def test_broadcast_against_empty_register(self): + program = """ + qreg q_empty[0]; + creg c_empty[0]; + measure q_empty -> c_empty; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(0, "q_empty"), ClassicalRegister(0, "c_empty")) + self.assertEqual(parsed, qc) + + def test_conditioned_broadcast_against_empty_register(self): + program = """ + qreg q_empty[0]; + creg c_empty[0]; + creg cond[1]; + if (cond == 0) measure q_empty -> c_empty; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit( + QuantumRegister(0, "q_empty"), + ClassicalRegister(0, "c_empty"), + ClassicalRegister(1, "cond"), + ) + self.assertEqual(parsed, qc) + + +class TestReset(QiskitTestCase): + def test_single(self): + program = """ + qreg q[1]; + reset q[0]; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.reset(0) + self.assertEqual(parsed, qc) + + def test_broadcast(self): + program = """ + qreg q[2]; + reset q; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.reset(0) + qc.reset(1) + self.assertEqual(parsed, qc) + + def test_conditioned(self): + program = """ + qreg q[2]; + creg cond[1]; + if (cond == 0) reset q[0]; + if (cond == 1) reset q; + """ + parsed = qiskit.qasm2.loads(program) + cond = ClassicalRegister(1, "cond") + qc = QuantumCircuit(QuantumRegister(2, "q"), cond) + qc.reset(0).c_if(cond, 0) + qc.reset(0).c_if(cond, 1) + qc.reset(1).c_if(cond, 1) + self.assertEqual(parsed, qc) + + def test_broadcast_against_empty_register(self): + program = """ + qreg empty[0]; + reset empty; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(0, "empty")) + self.assertEqual(parsed, qc) + + def test_conditioned_broadcast_against_empty_register(self): + program = """ + qreg empty[0]; + creg cond[1]; + if (cond == 0) reset empty; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(0, "empty"), ClassicalRegister(1, "cond")) + self.assertEqual(parsed, qc) + + +class TestInclude(QiskitTestCase): + def setUp(self): + super().setUp() + self.tmp_dir = pathlib.Path(tempfile.mkdtemp()) + + def tearDown(self): + # Doesn't really matter if the removal fails, since this was a tempdir anyway; it'll get + # cleaned up by the OS at some point. + shutil.rmtree(self.tmp_dir, ignore_errors=True) + super().tearDown() + + def test_qelib1_include(self): + program = """ + include "qelib1.inc"; + qreg q[3]; + u3(0.5, 0.25, 0.125) q[0]; + u2(0.5, 0.25) q[0]; + u1(0.5) q[0]; + cx q[0], q[1]; + id q[0]; + x q[0]; + y q[0]; + z q[0]; + h q[0]; + s q[0]; + sdg q[0]; + t q[0]; + tdg q[0]; + rx(0.5) q[0]; + ry(0.5) q[0]; + rz(0.5) q[0]; + cz q[0], q[1]; + cy q[0], q[1]; + ch q[0], q[1]; + ccx q[0], q[1], q[2]; + crz(0.5) q[0], q[1]; + cu1(0.5) q[0], q[1]; + cu3(0.5, 0.25, 0.125) q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(3, "q")) + qc.append(lib.U3Gate(0.5, 0.25, 0.125), [0]) + qc.append(lib.U2Gate(0.5, 0.25), [0]) + qc.append(lib.U1Gate(0.5), [0]) + qc.append(lib.CXGate(), [0, 1]) + qc.append(lib.UGate(0, 0, 0), [0]) # Stand-in for id. + qc.append(lib.XGate(), [0]) + qc.append(lib.YGate(), [0]) + qc.append(lib.ZGate(), [0]) + qc.append(lib.HGate(), [0]) + qc.append(lib.SGate(), [0]) + qc.append(lib.SdgGate(), [0]) + qc.append(lib.TGate(), [0]) + qc.append(lib.TdgGate(), [0]) + qc.append(lib.RXGate(0.5), [0]) + qc.append(lib.RYGate(0.5), [0]) + qc.append(lib.RZGate(0.5), [0]) + qc.append(lib.CZGate(), [0, 1]) + qc.append(lib.CYGate(), [0, 1]) + qc.append(lib.CHGate(), [0, 1]) + qc.append(lib.CCXGate(), [0, 1, 2]) + qc.append(lib.CRZGate(0.5), [0, 1]) + qc.append(lib.CU1Gate(0.5), [0, 1]) + qc.append(lib.CU3Gate(0.5, 0.25, 0.125), [0, 1]) + self.assertEqual(parsed, qc) + + def test_qelib1_after_gate_definition(self): + program = """ + gate bell a, b { + U(pi/2, 0, pi) a; + CX a, b; + } + include "qelib1.inc"; + qreg q[2]; + bell q[0], q[1]; + rx(0.5) q[0]; + bell q[1], q[0]; + """ + parsed = qiskit.qasm2.loads(program) + bell_def = QuantumCircuit([Qubit(), Qubit()]) + bell_def.u(math.pi / 2, 0, math.pi, 0) + bell_def.cx(0, 1) + bell = gate_builder("bell", [], bell_def) + + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(bell(), [0, 1]) + qc.rx(0.5, 0) + qc.append(bell(), [1, 0]) + self.assertEqual(parsed, qc) + + def test_include_can_define_version(self): + include = """ + OPENQASM 2.0; + qreg inner_q[2]; + """ + with open(self.tmp_dir / "include.qasm", "w") as fp: + fp.write(include) + program = """ + OPENQASM 2.0; + include "include.qasm"; + """ + parsed = qiskit.qasm2.loads(program, include_path=(self.tmp_dir,)) + qc = QuantumCircuit(QuantumRegister(2, "inner_q")) + self.assertEqual(parsed, qc) + + def test_can_define_gates(self): + include = """ + gate bell a, b { + h a; + cx a, b; + } + """ + with open(self.tmp_dir / "include.qasm", "w") as fp: + fp.write(include) + program = """ + OPENQASM 2.0; + include "qelib1.inc"; + include "include.qasm"; + qreg q[2]; + bell q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program, include_path=(self.tmp_dir,)) + bell_def = QuantumCircuit([Qubit(), Qubit()]) + bell_def.h(0) + bell_def.cx(0, 1) + bell = gate_builder("bell", [], bell_def) + + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(bell(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_nested_include(self): + inner = "creg c[2];" + with open(self.tmp_dir / "inner.qasm", "w") as fp: + fp.write(inner) + outer = """ + qreg q[2]; + include "inner.qasm"; + """ + with open(self.tmp_dir / "outer.qasm", "w") as fp: + fp.write(outer) + program = """ + OPENQASM 2.0; + include "outer.qasm"; + """ + parsed = qiskit.qasm2.loads(program, include_path=(self.tmp_dir,)) + qc = QuantumCircuit(QuantumRegister(2, "q"), ClassicalRegister(2, "c")) + self.assertEqual(parsed, qc) + + def test_first_hit_is_used(self): + empty = self.tmp_dir / "empty" + empty.mkdir() + first = self.tmp_dir / "first" + first.mkdir() + with open(first / "include.qasm", "w") as fp: + fp.write("qreg q[1];") + second = self.tmp_dir / "second" + second.mkdir() + with open(second / "include.qasm", "w") as fp: + fp.write("qreg q[2];") + program = 'include "include.qasm";' + parsed = qiskit.qasm2.loads(program, include_path=(empty, first, second)) + qc = QuantumCircuit(QuantumRegister(1, "q")) + self.assertEqual(parsed, qc) + + def test_qelib1_ignores_search_path(self): + with open(self.tmp_dir / "qelib1.inc", "w") as fp: + fp.write("qreg not_used[2];") + program = 'include "qelib1.inc";' + parsed = qiskit.qasm2.loads(program, include_path=(self.tmp_dir,)) + qc = QuantumCircuit() + self.assertEqual(parsed, qc) + + def test_include_from_current_directory(self): + include = """ + qreg q[2]; + """ + with open(self.tmp_dir / "include.qasm", "w") as fp: + fp.write(include) + program = """ + OPENQASM 2.0; + include "include.qasm"; + """ + prevdir = os.getcwd() + os.chdir(self.tmp_dir) + try: + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q")) + self.assertEqual(parsed, qc) + finally: + os.chdir(prevdir) + + def test_load_searches_source_directory(self): + with open(self.tmp_dir / "include.qasm", "w") as fp: + fp.write("qreg q[2];") + program = 'include "include.qasm";' + with open(self.tmp_dir / "program.qasm", "w") as fp: + fp.write(program) + parsed = qiskit.qasm2.load(self.tmp_dir / "program.qasm") + qc = QuantumCircuit(QuantumRegister(2, "q")) + self.assertEqual(parsed, qc) + + def test_load_searches_source_directory_last(self): + first = self.tmp_dir / "first" + first.mkdir() + with open(first / "include.qasm", "w") as fp: + fp.write("qreg q[2];") + with open(self.tmp_dir / "include.qasm", "w") as fp: + fp.write("qreg not_used[2];") + program = 'include "include.qasm";' + with open(self.tmp_dir / "program.qasm", "w") as fp: + fp.write(program) + parsed = qiskit.qasm2.load(self.tmp_dir / "program.qasm", include_path=(first,)) + qc = QuantumCircuit(QuantumRegister(2, "q")) + self.assertEqual(parsed, qc) + + def test_load_searches_source_directory_prepend(self): + first = self.tmp_dir / "first" + first.mkdir() + with open(first / "include.qasm", "w") as fp: + fp.write("qreg not_used[2];") + with open(self.tmp_dir / "include.qasm", "w") as fp: + fp.write("qreg q[2];") + program = 'include "include.qasm";' + with open(self.tmp_dir / "program.qasm", "w") as fp: + fp.write(program) + parsed = qiskit.qasm2.load( + self.tmp_dir / "program.qasm", include_path=(first,), include_input_directory="prepend" + ) + qc = QuantumCircuit(QuantumRegister(2, "q")) + self.assertEqual(parsed, qc) + + def test_load_can_ignore_source_directory(self): + with open(self.tmp_dir / "include.qasm", "w") as fp: + fp.write("qreg q[2];") + program = 'include "include.qasm";' + with open(self.tmp_dir / "program.qasm", "w") as fp: + fp.write(program) + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "unable to find 'include.qasm'"): + qiskit.qasm2.load(self.tmp_dir / "program.qasm", include_input_directory=None) + + +@ddt.ddt +class TestCustomInstructions(QiskitTestCase): + def test_qelib1_include_overridden(self): + program = """ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[3]; + u3(0.5, 0.25, 0.125) q[0]; + u2(0.5, 0.25) q[0]; + u1(0.5) q[0]; + cx q[0], q[1]; + id q[0]; + x q[0]; + y q[0]; + z q[0]; + h q[0]; + s q[0]; + sdg q[0]; + t q[0]; + tdg q[0]; + rx(0.5) q[0]; + ry(0.5) q[0]; + rz(0.5) q[0]; + cz q[0], q[1]; + cy q[0], q[1]; + ch q[0], q[1]; + ccx q[0], q[1], q[2]; + crz(0.5) q[0], q[1]; + cu1(0.5) q[0], q[1]; + cu3(0.5, 0.25, 0.125) q[0], q[1]; + """ + parsed = qiskit.qasm2.loads( + program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS + ) + qc = QuantumCircuit(QuantumRegister(3, "q")) + qc.append(lib.U3Gate(0.5, 0.25, 0.125), [0]) + qc.append(lib.U2Gate(0.5, 0.25), [0]) + qc.append(lib.U1Gate(0.5), [0]) + qc.append(lib.CXGate(), [0, 1]) + qc.append(lib.IGate(), [0]) + qc.append(lib.XGate(), [0]) + qc.append(lib.YGate(), [0]) + qc.append(lib.ZGate(), [0]) + qc.append(lib.HGate(), [0]) + qc.append(lib.SGate(), [0]) + qc.append(lib.SdgGate(), [0]) + qc.append(lib.TGate(), [0]) + qc.append(lib.TdgGate(), [0]) + qc.append(lib.RXGate(0.5), [0]) + qc.append(lib.RYGate(0.5), [0]) + qc.append(lib.RZGate(0.5), [0]) + qc.append(lib.CZGate(), [0, 1]) + qc.append(lib.CYGate(), [0, 1]) + qc.append(lib.CHGate(), [0, 1]) + qc.append(lib.CCXGate(), [0, 1, 2]) + qc.append(lib.CRZGate(0.5), [0, 1]) + qc.append(lib.CU1Gate(0.5), [0, 1]) + qc.append(lib.CU3Gate(0.5, 0.25, 0.125), [0, 1]) + self.assertEqual(parsed, qc) + + # Also test that the output matches what Qiskit puts out. + from_qiskit = QuantumCircuit.from_qasm_str(program) + self.assertEqual(parsed, from_qiskit) + + def test_qelib1_sparse_overrides(self): + """Test that the qelib1 special import still works as expected when a couple of gates in the + middle of it are custom. As long as qelib1 is handled specially, there is a risk that this + handling will break in weird ways when custom instructions overlap it.""" + program = """ + include "qelib1.inc"; + qreg q[3]; + u3(0.5, 0.25, 0.125) q[0]; + u2(0.5, 0.25) q[0]; + u1(0.5) q[0]; + cx q[0], q[1]; + id q[0]; + x q[0]; + y q[0]; + z q[0]; + h q[0]; + s q[0]; + sdg q[0]; + t q[0]; + tdg q[0]; + rx(0.5) q[0]; + ry(0.5) q[0]; + rz(0.5) q[0]; + cz q[0], q[1]; + cy q[0], q[1]; + ch q[0], q[1]; + ccx q[0], q[1], q[2]; + crz(0.5) q[0], q[1]; + cu1(0.5) q[0], q[1]; + cu3(0.5, 0.25, 0.125) q[0], q[1]; + """ + parsed = qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("id", 0, 1, lib.IGate), + qiskit.qasm2.CustomInstruction("h", 0, 1, lib.HGate), + qiskit.qasm2.CustomInstruction("crz", 1, 2, lib.CRZGate), + ], + ) + qc = QuantumCircuit(QuantumRegister(3, "q")) + qc.append(lib.U3Gate(0.5, 0.25, 0.125), [0]) + qc.append(lib.U2Gate(0.5, 0.25), [0]) + qc.append(lib.U1Gate(0.5), [0]) + qc.append(lib.CXGate(), [0, 1]) + qc.append(lib.IGate(), [0]) + qc.append(lib.XGate(), [0]) + qc.append(lib.YGate(), [0]) + qc.append(lib.ZGate(), [0]) + qc.append(lib.HGate(), [0]) + qc.append(lib.SGate(), [0]) + qc.append(lib.SdgGate(), [0]) + qc.append(lib.TGate(), [0]) + qc.append(lib.TdgGate(), [0]) + qc.append(lib.RXGate(0.5), [0]) + qc.append(lib.RYGate(0.5), [0]) + qc.append(lib.RZGate(0.5), [0]) + qc.append(lib.CZGate(), [0, 1]) + qc.append(lib.CYGate(), [0, 1]) + qc.append(lib.CHGate(), [0, 1]) + qc.append(lib.CCXGate(), [0, 1, 2]) + qc.append(lib.CRZGate(0.5), [0, 1]) + qc.append(lib.CU1Gate(0.5), [0, 1]) + qc.append(lib.CU3Gate(0.5, 0.25, 0.125), [0, 1]) + self.assertEqual(parsed, qc) + + def test_user_gate_after_overidden_qelib1(self): + program = """ + include "qelib1.inc"; + qreg q[1]; + opaque my_gate q; + my_gate q[0]; + """ + parsed = qiskit.qasm2.loads( + program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.append(Gate("my_gate", 1, []), [0]) + self.assertEqual(parsed, qc) + + def test_qiskit_extra_builtins(self): + program = """ + qreg q[5]; + u(0.5 ,0.25, 0.125) q[0]; + p(0.5) q[0]; + sx q[0]; + sxdg q[0]; + swap q[0], q[1]; + cswap q[0], q[1], q[2]; + crx(0.5) q[0], q[1]; + cry(0.5) q[0], q[1]; + cp(0.5) q[0], q[1]; + csx q[0], q[1]; + cu(0.5, 0.25, 0.125, 0.0625) q[0], q[1]; + rxx(0.5) q[0], q[1]; + rzz(0.5) q[0], q[1]; + rccx q[0], q[1], q[2]; + rc3x q[0], q[1], q[2], q[3]; + c3x q[0], q[1], q[2], q[3]; + c3sqrtx q[0], q[1], q[2], q[3]; + c4x q[0], q[1], q[2], q[3], q[4]; + """ + parsed = qiskit.qasm2.loads( + program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS + ) + qc = QuantumCircuit(QuantumRegister(5, "q")) + qc.append(lib.UGate(0.5, 0.25, 0.125), [0]) + qc.append(lib.PhaseGate(0.5), [0]) + qc.append(lib.SXGate(), [0]) + qc.append(lib.SXdgGate(), [0]) + qc.append(lib.SwapGate(), [0, 1]) + qc.append(lib.CSwapGate(), [0, 1, 2]) + qc.append(lib.CRXGate(0.5), [0, 1]) + qc.append(lib.CRYGate(0.5), [0, 1]) + qc.append(lib.CPhaseGate(0.5), [0, 1]) + qc.append(lib.CSXGate(), [0, 1]) + qc.append(lib.CUGate(0.5, 0.25, 0.125, 0.0625), [0, 1]) + qc.append(lib.RXXGate(0.5), [0, 1]) + qc.append(lib.RZZGate(0.5), [0, 1]) + qc.append(lib.RCCXGate(), [0, 1, 2]) + qc.append(lib.RC3XGate(), [0, 1, 2, 3]) + qc.append(lib.C3XGate(), [0, 1, 2, 3]) + qc.append(lib.C3SXGate(), [0, 1, 2, 3]) + qc.append(lib.C4XGate(), [0, 1, 2, 3, 4]) + self.assertEqual(parsed, qc) + + # There's also the 'u0' gate, but this is weird so we don't wildly care what its definition + # is and it has no Qiskit equivalent, so we'll just test that it using it doesn't produce an + # error. + parsed = qiskit.qasm2.loads( + "qreg q[1]; u0(1) q[0];", custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS + ) + self.assertEqual(parsed.data[0].operation.name, "u0") + + def test_qiskit_override_delay_opaque(self): + program = """ + opaque delay(t) q; + qreg q[1]; + delay(1) q[0]; + """ + parsed = qiskit.qasm2.loads( + program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.delay(1, 0, unit="dt") + self.assertEqual(parsed, qc) + + def test_qiskit_override_u0_opaque(self): + program = """ + opaque u0(n) q; + qreg q[1]; + u0(2) q[0]; + """ + parsed = qiskit.qasm2.loads( + program, custom_instructions=qiskit.qasm2.LEGACY_CUSTOM_INSTRUCTIONS + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.id(0) + qc.id(0) + self.assertEqual(parsed.decompose(), qc) + + def test_can_override_u(self): + program = """ + qreg q[1]; + U(0.5, 0.25, 0.125) q[0]; + """ + + class MyGate(Gate): + def __init__(self, a, b, c): + super().__init__("u", 1, [a, b, c]) + + parsed = qiskit.qasm2.loads( + program, + custom_instructions=[qiskit.qasm2.CustomInstruction("U", 3, 1, MyGate, builtin=True)], + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.append(MyGate(0.5, 0.25, 0.125), [0]) + self.assertEqual(parsed, qc) + + def test_can_override_cx(self): + program = """ + qreg q[2]; + CX q[0], q[1]; + """ + + class MyGate(Gate): + def __init__(self): + super().__init__("cx", 2, []) + + parsed = qiskit.qasm2.loads( + program, + custom_instructions=[qiskit.qasm2.CustomInstruction("CX", 0, 2, MyGate, builtin=True)], + ) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(MyGate(), [0, 1]) + self.assertEqual(parsed, qc) + + @ddt.data(lambda x: x, reversed) + def test_can_override_both_builtins_with_other_gates(self, order): + program = """ + gate unimportant q {} + qreg q[2]; + U(0.5, 0.25, 0.125) q[0]; + CX q[0], q[1]; + """ + + class MyUGate(Gate): + def __init__(self, a, b, c): + super().__init__("u", 1, [a, b, c]) + + class MyCXGate(Gate): + def __init__(self): + super().__init__("cx", 2, []) + + custom = [ + qiskit.qasm2.CustomInstruction("unused", 0, 1, lambda: Gate("unused", 1, [])), + qiskit.qasm2.CustomInstruction("U", 3, 1, MyUGate, builtin=True), + qiskit.qasm2.CustomInstruction("CX", 0, 2, MyCXGate, builtin=True), + ] + custom = order(custom) + parsed = qiskit.qasm2.loads(program, custom_instructions=custom) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(MyUGate(0.5, 0.25, 0.125), [0]) + qc.append(MyCXGate(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_custom_builtin_gate(self): + program = """ + qreg q[1]; + builtin(0.5) q[0]; + """ + + class MyGate(Gate): + def __init__(self, a): + super().__init__("builtin", 1, [a]) + + parsed = qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("builtin", 1, 1, MyGate, builtin=True) + ], + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.append(MyGate(0.5), [0]) + self.assertEqual(parsed, qc) + + def test_can_define_builtin_as_gate(self): + program = """ + qreg q[1]; + gate builtin(t) q {} + builtin(0.5) q[0]; + """ + + class MyGate(Gate): + def __init__(self, a): + super().__init__("builtin", 1, [a]) + + parsed = qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("builtin", 1, 1, MyGate, builtin=True) + ], + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.append(MyGate(0.5), [0]) + self.assertEqual(parsed, qc) + + def test_can_define_builtin_as_opaque(self): + program = """ + qreg q[1]; + opaque builtin(t) q; + builtin(0.5) q[0]; + """ + + class MyGate(Gate): + def __init__(self, a): + super().__init__("builtin", 1, [a]) + + parsed = qiskit.qasm2.loads( + program, + custom_instructions=[ + qiskit.qasm2.CustomInstruction("builtin", 1, 1, MyGate, builtin=True) + ], + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.append(MyGate(0.5), [0]) + self.assertEqual(parsed, qc) + + def test_can_define_custom_as_gate(self): + program = """ + qreg q[1]; + gate my_gate(t) q {} + my_gate(0.5) q[0]; + """ + + class MyGate(Gate): + def __init__(self, a): + super().__init__("my_gate", 1, [a]) + + parsed = qiskit.qasm2.loads( + program, custom_instructions=[qiskit.qasm2.CustomInstruction("my_gate", 1, 1, MyGate)] + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.append(MyGate(0.5), [0]) + self.assertEqual(parsed, qc) + + def test_can_define_custom_as_opaque(self): + program = """ + qreg q[1]; + opaque my_gate(t) q; + my_gate(0.5) q[0]; + """ + + class MyGate(Gate): + def __init__(self, a): + super().__init__("my_gate", 1, [a]) + + parsed = qiskit.qasm2.loads( + program, custom_instructions=[qiskit.qasm2.CustomInstruction("my_gate", 1, 1, MyGate)] + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.append(MyGate(0.5), [0]) + self.assertEqual(parsed, qc) + + +class TestCustomClassical(QiskitTestCase): + def test_qiskit_extensions(self): + program = """ + include "qelib1.inc"; + qreg q[1]; + rx(asin(0.3)) q[0]; + ry(acos(0.3)) q[0]; + rz(atan(0.3)) q[0]; + """ + parsed = qiskit.qasm2.loads(program, custom_classical=qiskit.qasm2.LEGACY_CUSTOM_CLASSICAL) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.rx(math.asin(0.3), 0) + qc.ry(math.acos(0.3), 0) + qc.rz(math.atan(0.3), 0) + self.assertEqual(parsed, qc) + + def test_zero_parameter_custom(self): + program = """ + qreg q[1]; + U(f(), 0, 0) q[0]; + """ + parsed = qiskit.qasm2.loads( + program, custom_classical=[qiskit.qasm2.CustomClassical("f", 0, lambda: 0.2)] + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.u(0.2, 0, 0, 0) + self.assertEqual(parsed, qc) + + def test_multi_parameter_custom(self): + program = """ + qreg q[1]; + U(f(0.2), g(0.4, 0.1), h(1, 2, 3)) q[0]; + """ + parsed = qiskit.qasm2.loads( + program, + custom_classical=[ + qiskit.qasm2.CustomClassical("f", 1, lambda x: 1 + x), + qiskit.qasm2.CustomClassical("g", 2, math.atan2), + qiskit.qasm2.CustomClassical("h", 3, lambda x, y, z: z - y + x), + ], + ) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.u(1.2, math.atan2(0.4, 0.1), 2, 0) + self.assertEqual(parsed, qc) + + def test_use_in_gate_definition(self): + # pylint: disable=invalid-name + program = """ + gate my_gate(a, b) q { + U(f(a, b), g(f(b, f(b, a))), b) q; + } + qreg q[1]; + my_gate(0.5, 0.25) q[0]; + my_gate(0.25, 0.5) q[0]; + """ + f = lambda x, y: x - y + g = lambda x: 2 * x + parsed = qiskit.qasm2.loads( + program, + custom_classical=[ + qiskit.qasm2.CustomClassical("f", 2, f), + qiskit.qasm2.CustomClassical("g", 1, g), + ], + ) + first_gate = parsed.data[0].operation + second_gate = parsed.data[1].operation + self.assertEqual(list(first_gate.params), [0.5, 0.25]) + self.assertEqual(list(second_gate.params), [0.25, 0.5]) + + self.assertEqual( + list(first_gate.definition.data[0].operation.params), + [ + f(0.5, 0.25), + g(f(0.25, f(0.25, 0.5))), + 0.25, + ], + ) + self.assertEqual( + list(second_gate.definition.data[0].operation.params), + [ + f(0.25, 0.5), + g(f(0.5, f(0.5, 0.25))), + 0.5, + ], + ) + + +@ddt.ddt +class TestStrict(QiskitTestCase): + @ddt.data( + "gate my_gate(p0, p1$) q0, q1 {}", + "gate my_gate(p0, p1) q0, q1$ {}", + "opaque my_gate(p0, p1$) q0, q1;", + "opaque my_gate(p0, p1) q0, q1$;", + 'include "qelib1.inc"; qreg q[2]; cu3(0.5, 0.25, 0.125$) q[0], q[1];', + 'include "qelib1.inc"; qreg q[2]; cu3(0.5, 0.25, 0.125) q[0], q[1]$;', + "qreg q[2]; barrier q[0], q[1]$;", + 'include "qelib1.inc"; qreg q[1]; rx(sin(pi$)) q[0];', + ) + def test_trailing_comma(self, program): + without = qiskit.qasm2.loads("OPENQASM 2.0;\n" + program.replace("$", ""), strict=True) + with_ = qiskit.qasm2.loads(program.replace("$", ","), strict=False) + self.assertEqual(with_, without) + + def test_trailing_semicolon_after_gate(self): + program = """ + include "qelib1.inc"; + gate bell a, b { + h a; + cx a, b; + }; // <- the important bit of the test + qreg q[2]; + bell q[0], q[1]; + """ + parsed = qiskit.qasm2.loads(program) + bell_def = QuantumCircuit([Qubit(), Qubit()]) + bell_def.h(0) + bell_def.cx(0, 1) + bell = gate_builder("bell", [], bell_def) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.append(bell(), [0, 1]) + self.assertEqual(parsed, qc) + + def test_empty_statement(self): + # This is allowed more as a side-effect of allowing the trailing semicolon after gate + # definitions. + program = """ + OPENQASM 2.0; + include "qelib1.inc"; + qreg q[2]; + h q[0]; + ; + cx q[0], q[1]; + ;;;; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(2, "q")) + qc.h(0) + qc.cx(0, 1) + self.assertEqual(parsed, qc) + + def test_single_quoted_path(self): + program = """ + include 'qelib1.inc'; + qreg q[1]; + h q[0]; + """ + parsed = qiskit.qasm2.loads(program) + qc = QuantumCircuit(QuantumRegister(1, "q")) + qc.h(0) + self.assertEqual(parsed, qc) From b5056fedc3daa63d1995c809a7eb434354143eea Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 12 Apr 2023 12:43:41 -0400 Subject: [PATCH 013/172] Remove faulty qubits and gates sections from transpile() (#9900) * Remove faulty qubits and gates sections from transpile() This commit removes the "faulty qubits" support from the transpile(). Previously the transpile() function would attempt to filter out any faulty qubits or gates reported in a BackendV1's BackendProperties object and remap the qubits based on that. However, this code was never actually functioning as the first BackendV1 based backend to attempt to set these flags is causing an internal panic in rustworkx trying to created the remapped coupling map. This code path was mildly controversial at the time of introduction (and was already reverted once in the past) and since it provably isn't valid anymore (and I wasn't clear it ever functioned as expected) as the only backend to leverage the feature crashes transpile() this commit just opts to remove it as a potential breaking change. * Fix release note typos --- qiskit/compiler/transpiler.py | 184 +------- ...aulty-qubits-support-00850f69185c365e.yaml | 16 + test/python/transpiler/test_faulty_backend.py | 392 ------------------ 3 files changed, 18 insertions(+), 574 deletions(-) create mode 100644 releasenotes/notes/remove-faulty-qubits-support-00850f69185c365e.yaml delete mode 100644 test/python/transpiler/test_faulty_backend.py diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index f2918c78b5a4..7475ea4cf1e2 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -27,18 +27,16 @@ from qiskit import user_config from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import Qubit -from qiskit.converters import isinstanceint, isinstancelist, dag_to_circuit, circuit_to_dag +from qiskit.converters import isinstanceint, isinstancelist from qiskit.dagcircuit import DAGCircuit from qiskit.providers.backend import Backend from qiskit.providers.models import BackendProperties -from qiskit.providers.models.backendproperties import Gate from qiskit.pulse import Schedule, InstructionScheduleMap from qiskit.tools import parallel from qiskit.transpiler import Layout, CouplingMap, PropertySet from qiskit.transpiler.basepasses import BasePass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations, InstructionDurationsType -from qiskit.transpiler.passes import ApplyLayout from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.preset_passmanagers import ( @@ -386,9 +384,6 @@ def callback_func(**kwargs): pass_manager, transpile_config["callback"], transpile_config["output_name"], - transpile_config["backend_num_qubits"], - transpile_config["faulty_qubits_map"], - transpile_config["pass_manager_config"].backend_properties, ) ) circuits = output_circuits @@ -446,11 +441,6 @@ def _combine_args(shared_transpiler_args, unique_config): transpile_config = unique_config transpile_config["pass_manager_config"] = pass_manager_config - if transpile_config["faulty_qubits_map"]: - pass_manager_config.initial_layout = _remap_layout_faulty_backend( - pass_manager_config.initial_layout, transpile_config["faulty_qubits_map"] - ) - # we choose an appropriate one based on desired optimization level if level == 0: pass_manager = level_0_pass_manager(pass_manager_config) @@ -470,19 +460,8 @@ def _serial_transpile_circuit( pass_manager, callback, output_name, - num_qubits, - faulty_qubits_map=None, - backend_prop=None, ): result = pass_manager.run(circuit, callback=callback, output_name=output_name) - if faulty_qubits_map: - return _remap_circuit_faulty_backend( - result, - num_qubits, - backend_prop, - faulty_qubits_map, - ) - return result @@ -508,78 +487,14 @@ def _transpile_circuit(circuit_config_tuple: Tuple[QuantumCircuit, str, Dict]) - existing_shm.close() transpile_config, pass_manager = _combine_args(shared_transpiler_args, unique_config) - pass_manager_config = transpile_config["pass_manager_config"] result = pass_manager.run( circuit, callback=transpile_config["callback"], output_name=transpile_config["output_name"] ) - if transpile_config["faulty_qubits_map"]: - return _remap_circuit_faulty_backend( - result, - transpile_config["backend_num_qubits"], - pass_manager_config.backend_properties, - transpile_config["faulty_qubits_map"], - ) - return result -def _remap_circuit_faulty_backend(circuit, num_qubits, backend_prop, faulty_qubits_map): - faulty_qubits = backend_prop.faulty_qubits() if backend_prop else [] - disconnected_qubits = {k for k, v in faulty_qubits_map.items() if v is None}.difference( - faulty_qubits - ) - faulty_qubits_map_reverse = {v: k for k, v in faulty_qubits_map.items()} - if faulty_qubits: - faulty_qreg = circuit._create_qreg(len(faulty_qubits), "faulty") - else: - faulty_qreg = [] - if disconnected_qubits: - disconnected_qreg = circuit._create_qreg(len(disconnected_qubits), "disconnected") - else: - disconnected_qreg = [] - - new_layout = Layout() - faulty_qubit = 0 - disconnected_qubit = 0 - - for real_qubit in range(num_qubits): - if faulty_qubits_map[real_qubit] is not None: - new_layout[real_qubit] = circuit._layout.initial_layout[faulty_qubits_map[real_qubit]] - else: - if real_qubit in faulty_qubits: - new_layout[real_qubit] = faulty_qreg[faulty_qubit] - faulty_qubit += 1 - else: - new_layout[real_qubit] = disconnected_qreg[disconnected_qubit] - disconnected_qubit += 1 - physical_layout_dict = {} - for index, qubit in enumerate(circuit.qubits): - physical_layout_dict[qubit] = faulty_qubits_map_reverse[index] - for qubit in faulty_qreg[:] + disconnected_qreg[:]: - physical_layout_dict[qubit] = new_layout[qubit] - dag_circuit = circuit_to_dag(circuit) - apply_layout_pass = ApplyLayout() - apply_layout_pass.property_set["layout"] = Layout(physical_layout_dict) - circuit = dag_to_circuit(apply_layout_pass.run(dag_circuit), copy_operations=False) - circuit._layout = new_layout - return circuit - - -def _remap_layout_faulty_backend(layout, faulty_qubits_map): - if layout is None: - return layout - new_layout = Layout() - for virtual, physical in layout.get_virtual_bits().items(): - if faulty_qubits_map[physical] is None: - raise TranspilerError( - "The initial_layout parameter refers to faulty or disconnected qubits" - ) - new_layout[virtual] = faulty_qubits_map[physical] - return new_layout - - def _parse_transpile_args( circuits, backend, @@ -657,7 +572,6 @@ def _parse_transpile_args( basis_gates = _parse_basis_gates(basis_gates, backend) initial_layout = _parse_initial_layout(initial_layout, circuits) inst_map = _parse_inst_map(inst_map, backend) - faulty_qubits_map = _parse_faulty_qubits_map(backend, num_circuits) coupling_map = _parse_coupling_map(coupling_map, backend) backend_properties = _parse_backend_properties(backend_properties, backend) backend_num_qubits = _parse_backend_num_qubits(backend, num_circuits) @@ -678,7 +592,6 @@ def _parse_transpile_args( unique_dict = { "callback": callback, "output_name": output_name, - "faulty_qubits_map": faulty_qubits_map, "backend_num_qubits": backend_num_qubits, } shared_dict = { @@ -757,7 +670,6 @@ def _parse_transpile_args( transpile_args = { "output_name": kwargs.pop("output_name"), "callback": kwargs.pop("callback"), - "faulty_qubits_map": kwargs.pop("faulty_qubits_map"), "backend_num_qubits": kwargs.pop("backend_num_qubits"), "pass_manager_config": kwargs, } @@ -766,42 +678,6 @@ def _parse_transpile_args( return list_transpile_args, shared_dict -def _create_faulty_qubits_map(backend): - """If the backend has faulty qubits, those should be excluded. A faulty_qubit_map is a map - from working qubit in the backend to dummy qubits that are consecutive and connected.""" - faulty_qubits_map = None - if backend is not None: - backend_version = getattr(backend, "version", 0) - if backend_version > 1: - return None - if backend.properties(): - faulty_qubits = backend.properties().faulty_qubits() - faulty_edges = [gates.qubits for gates in backend.properties().faulty_gates()] - else: - faulty_qubits = [] - faulty_edges = [] - - if faulty_qubits or faulty_edges: - faulty_qubits_map = {} - configuration = backend.configuration() - full_coupling_map = configuration.coupling_map - functional_cm_list = [ - edge - for edge in full_coupling_map - if (set(edge).isdisjoint(faulty_qubits) and edge not in faulty_edges) - ] - - connected_working_qubits = CouplingMap(functional_cm_list).largest_connected_component() - dummy_qubit_counter = 0 - for qubit in range(configuration.n_qubits): - if qubit in connected_working_qubits: - faulty_qubits_map[qubit] = dummy_qubit_counter - dummy_qubit_counter += 1 - else: - faulty_qubits_map[qubit] = None - return faulty_qubits_map - - def _parse_basis_gates(basis_gates, backend): # try getting basis_gates from user, else backend if basis_gates is None: @@ -834,25 +710,7 @@ def _parse_coupling_map(coupling_map, backend): if getattr(backend, "configuration", None): configuration = backend.configuration() if hasattr(configuration, "coupling_map") and configuration.coupling_map: - faulty_map = _create_faulty_qubits_map(backend) - if faulty_map: - faulty_edges = [gate.qubits for gate in backend.properties().faulty_gates()] - functional_gates = [ - edge for edge in configuration.coupling_map if edge not in faulty_edges - ] - coupling_map = CouplingMap() - for qubit1, qubit2 in functional_gates: - if faulty_map[qubit1] is not None and faulty_map[qubit2] is not None: - coupling_map.add_edge(faulty_map[qubit1], faulty_map[qubit2]) - if configuration.n_qubits != coupling_map.size(): - warnings.warn( - "The backend has currently some qubits/edges out of service." - " This temporarily reduces the backend size from " - f"{configuration.n_qubits} to {coupling_map.size()}", - UserWarning, - ) - else: - coupling_map = CouplingMap(configuration.coupling_map) + coupling_map = CouplingMap(configuration.coupling_map) else: coupling_map = backend.coupling_map @@ -875,33 +733,6 @@ def _parse_backend_properties(backend_properties, backend): if backend_version <= 1: if getattr(backend, "properties", None): backend_properties = backend.properties() - if backend_properties and ( - backend_properties.faulty_qubits() or backend_properties.faulty_gates() - ): - faulty_qubits = sorted(backend_properties.faulty_qubits(), reverse=True) - faulty_edges = [gates.qubits for gates in backend_properties.faulty_gates()] - # remove faulty qubits in backend_properties.qubits - for faulty_qubit in faulty_qubits: - del backend_properties.qubits[faulty_qubit] - - gates = [] - for gate in backend_properties.gates: - # remove gates using faulty edges or with faulty qubits (and remap the - # gates in terms of faulty_qubits_map) - faulty_qubits_map = _create_faulty_qubits_map(backend) - if ( - any(faulty_qubits_map[qubits] is not None for qubits in gate.qubits) - or gate.qubits in faulty_edges - ): - continue - gate_dict = gate.to_dict() - replacement_gate = Gate.from_dict(gate_dict) - gate_dict["qubits"] = [faulty_qubits_map[qubit] for qubit in gate.qubits] - args = "_".join([str(qubit) for qubit in gate_dict["qubits"]]) - gate_dict["name"] = "{}{}".format(gate_dict["gate"], args) - gates.append(replacement_gate) - - backend_properties.gates = gates else: backend_properties = target_to_backend_properties(backend.target) return backend_properties @@ -1020,17 +851,6 @@ def _parse_callback(callback, num_circuits): return callback -def _parse_faulty_qubits_map(backend, num_circuits): - if backend is None: - return [None] * num_circuits - if not isinstance(backend, list): - return [_create_faulty_qubits_map(backend)] * num_circuits - faulty_qubits_map = [] - for a_backend in backend: - faulty_qubits_map.append(_create_faulty_qubits_map(a_backend)) - return faulty_qubits_map - - def _parse_output_name(output_name, circuits): # naming and returning circuits # output_name could be either a string or a list diff --git a/releasenotes/notes/remove-faulty-qubits-support-00850f69185c365e.yaml b/releasenotes/notes/remove-faulty-qubits-support-00850f69185c365e.yaml new file mode 100644 index 000000000000..283bf3ccf249 --- /dev/null +++ b/releasenotes/notes/remove-faulty-qubits-support-00850f69185c365e.yaml @@ -0,0 +1,16 @@ +--- +upgrade: + - | + When running the :func:`~.transpile` function with a :class:`~.BackendV1` + based backend or a :class:`~.BackendProperties` via the ``backend_properties`` + keyword argument that has any qubits or gates flagged as faulty the function + will no longer try to automatically remap the qubits based on this information. + The method by which :func:`~.transpile` attempted to do this remapping was + fundamentally flawed and in most cases of such a backend it would result + an internal error being raised. In practice very few backends ever set the + fields in :class:`~.BackendProperties` to flag a qubit or gate as faulty. + If you were somehow successfully relying on :func:`~.transpile to do this + re-mapping for you will now need to manually do that and pass a + mapped input to the ``coupling_map`` and ``backend_properties`` arguments + which has filtered out the faulty qubits and gates and then manually re-map + the output. diff --git a/test/python/transpiler/test_faulty_backend.py b/test/python/transpiler/test_faulty_backend.py deleted file mode 100644 index 80fed1716d01..000000000000 --- a/test/python/transpiler/test_faulty_backend.py +++ /dev/null @@ -1,392 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2020. -# -# 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. - -"""Tests preset pass manager with faulty backends""" - -from ddt import ddt, data - -from qiskit import QuantumCircuit, QuantumRegister, BasicAer, execute -from qiskit.compiler import transpile -from qiskit.test import QiskitTestCase -from qiskit.converters import circuit_to_dag -from qiskit.circuit.library import CXGate -from qiskit.dagcircuit import DAGOpNode -from qiskit.transpiler import TranspilerError -from ..providers.faulty_backends import ( - FakeOurenseFaultyQ1, - FakeOurenseFaultyCX13, - FakeOurenseFaultyCX01CX10, - FakeOurenseFaultyCX13CX31, -) - - -class TestFaultyBackendCase(QiskitTestCase): - """Base TestCase for testing transpilation if faulty backends.""" - - def assertEqualCount(self, circuit1, circuit2): - """Asserts circuit1 and circuit2 has the same result counts after execution in BasicAer""" - backend = BasicAer.get_backend("qasm_simulator") - shots = 2048 - - result1 = ( - execute( - circuit1, - backend, - basis_gates=["u1", "u2", "u3", "id", "cx"], - seed_simulator=0, - seed_transpiler=0, - shots=shots, - ) - .result() - .get_counts() - ) - - result2 = ( - execute( - circuit2, - backend, - basis_gates=["u1", "u2", "u3", "id", "cx"], - seed_simulator=0, - seed_transpiler=0, - shots=shots, - ) - .result() - .get_counts() - ) - - for key in set(result1.keys()).union(result2.keys()): - with self.subTest(key=key): - diff = abs(result1.get(key, 0) - result2.get(key, 0)) - self.assertLess(diff / shots * 100, 2.5) - - -@ddt -class TestFaultyCX01CX10(TestFaultyBackendCase): - """Test preset passmanagers with FakeOurenseFaultyCX01CX10 - A fake 5 qubit backend, with a faulty CX(Q0, Q1) (and symmetric). - 0 (↔) 1 ↔ 3 ↔ 4 - ↕ - 2 - """ - - def assertIdleCX01(self, circuit): - """Asserts the CX(0, 1) (and symmetric) is not used in the circuit""" - physical_qubits = QuantumRegister(5, "q") - cx_nodes = circuit_to_dag(circuit).op_nodes(CXGate) - for node in cx_nodes: - if set(node.qargs) == {physical_qubits[0], physical_qubits[1]}: - raise AssertionError("Faulty CX(Q0, Q1) (or symmetric) is being used.") - - @data(0, 1, 2, 3) - def test_level(self, level): - """Test level {level} Ourense backend with faulty CX(Q0, Q1) and CX(Q1, Q0)""" - circuit = QuantumCircuit(QuantumRegister(4, "qr")) - circuit.h(range(4)) - circuit.ccx(0, 1, 2) - circuit.measure_all() - result = transpile( - circuit, - backend=FakeOurenseFaultyCX01CX10(), - optimization_level=level, - seed_transpiler=42, - ) - - self.assertIdleCX01(result) - self.assertEqualCount(circuit, result) - - @data(0, 1, 2, 3) - def test_layout_level(self, level): - """Test level {level} with a faulty CX(Q0, Q1) with a working initial layout""" - circuit = QuantumCircuit(QuantumRegister(4, "qr")) - circuit.h(range(4)) - circuit.ccx(0, 1, 2) - circuit.measure_all() - result = transpile( - circuit, - backend=FakeOurenseFaultyCX01CX10(), - optimization_level=level, - initial_layout=[3, 4, 1, 2], - seed_transpiler=42, - ) - - self.assertIdleCX01(result) - self.assertEqualCount(circuit, result) - - @data(0, 1, 2, 3) - def test_failing_layout_level(self, level): - """Test level {level} with a faulty CX(Q0, Q1) with a failing initial layout. Raises.""" - circuit = QuantumCircuit(QuantumRegister(4, "qr")) - circuit.h(range(4)) - circuit.ccx(0, 1, 2) - circuit.measure_all() - - message = "The initial_layout parameter refers to faulty or disconnected qubits" - - with self.assertRaises(TranspilerError) as context: - transpile( - circuit, - backend=FakeOurenseFaultyCX01CX10(), - optimization_level=level, - initial_layout=[0, 4, 1, 2], - seed_transpiler=42, - ) - - self.assertEqual(context.exception.message, message) - - -@ddt -class TestFaultyCX13CX31(TestFaultyBackendCase): - """Test preset passmanagers with FakeOurenseFaultyCX13CX31 - A fake 5 qubit backend, with a faulty CX(Q1, Q3) (and symmetric). - 0 ↔ 1 (↔) 3 ↔ 4 - ↕ - 2 - """ - - def assertIdleCX13(self, circuit): - """Asserts the CX(1, 3) (and symmetric) is not used in the circuit""" - physical_qubits = QuantumRegister(5, "q") - cx_nodes = circuit_to_dag(circuit).op_nodes(CXGate) - for node in cx_nodes: - if set(node.qargs) == {physical_qubits[1], physical_qubits[3]}: - raise AssertionError("Faulty CX(Q1, Q3) (or symmetric) is being used.") - - @data(0, 1, 2, 3) - def test_level(self, level): - """Test level {level} Ourense backend with faulty CX(Q1, Q3) and CX(Q3, Q1)""" - circuit = QuantumCircuit(QuantumRegister(3, "qr")) - circuit.h(range(3)) - circuit.ccx(0, 1, 2) - circuit.measure_all() - result = transpile( - circuit, - backend=FakeOurenseFaultyCX13CX31(), - optimization_level=level, - seed_transpiler=42, - ) - - self.assertIdleCX13(result) - self.assertEqualCount(circuit, result) - - @data(0, 1, 2, 3) - def test_layout_level(self, level): - """Test level {level} with a faulty CX(Q1, Q3) with a working initial layout""" - circuit = QuantumCircuit(QuantumRegister(3, "qr")) - circuit.h(range(3)) - circuit.ccx(0, 1, 2) - circuit.measure_all() - result = transpile( - circuit, - backend=FakeOurenseFaultyCX13CX31(), - optimization_level=level, - initial_layout=[0, 2, 1], - seed_transpiler=42, - ) - - self.assertIdleCX13(result) - self.assertEqualCount(circuit, result) - - @data(0, 1, 2, 3) - def test_failing_layout_level(self, level): - """Test level {level} with a faulty CX(Q1, Q3) with a failing initial layout. Raises.""" - circuit = QuantumCircuit(QuantumRegister(3, "qr")) - circuit.h(range(3)) - circuit.ccx(0, 1, 2) - circuit.measure_all() - - message = "The initial_layout parameter refers to faulty or disconnected qubits" - - with self.assertRaises(TranspilerError) as context: - transpile( - circuit, - backend=FakeOurenseFaultyCX13CX31(), - optimization_level=level, - initial_layout=[0, 1, 3], - seed_transpiler=42, - ) - - self.assertEqual(context.exception.message, message) - - -@ddt -class TestFaultyCX13(TestFaultyBackendCase): - """Test preset passmanagers with FakeOurenseFaultyCX13 - A fake 5 qubit backend, with a faulty CX(Q1, Q3). Notice that CX(Q3, Q1) is operational - 0 ↔ 1 <- 3 ↔ 4 - ↕ - 2 - """ - - def assertIdleCX13(self, circuit): - """Asserts the CX(1, 3) is not used in the circuit""" - physical_qubits = QuantumRegister(5, "q") - cx_nodes = circuit_to_dag(circuit).op_nodes(CXGate) - for node in cx_nodes: - if node.qargs == [physical_qubits[1], physical_qubits[3]]: - raise AssertionError("Faulty CX(Q1, Q3) is being used.") - - @data(0, 1, 2) # TODO: add 3 once https://github.com/Qiskit/qiskit-terra/issues/6406 is fixed - def test_level(self, level): - """Test level {level} Ourense backend with a faulty CX(Q1, Q3)""" - circuit = QuantumCircuit(QuantumRegister(5, "qr")) - circuit.h(range(5)) - circuit.ccx(0, 1, 2) - circuit.ccx(2, 3, 4) - circuit.measure_all() - result = transpile( - circuit, - backend=FakeOurenseFaultyCX13(), - optimization_level=level, - initial_layout=range(5), - seed_transpiler=42, - ) - self.assertIdleCX13(result) - self.assertEqualCount(circuit, result) - - @data(0, 1, 2) # TODO: add 3 once https://github.com/Qiskit/qiskit-terra/issues/6406 is fixed - def test_layout_level(self, level): - """Test level {level} with a faulty CX(Q1, Q3) with a working initial layout""" - - # ┌───┐ ┌───┐ ░ ┌─┐ - # qr_0: ┤ H ├──■──┤ X ├───────────────────────────────░─┤M├──────────── - # ├───┤┌─┴─┐└─┬─┘ ┌───┐ ┌───┐ ░ └╥┘┌─┐ - # qr_1: ┤ H ├┤ X ├──■────■──┤ X ├──■──┤ X ├───────────░──╫─┤M├───────── - # ├───┤└───┘ ┌─┴─┐└─┬─┘ │ └─┬─┘ ░ ║ └╥┘┌─┐ - # qr_2: ┤ H ├──────────┤ X ├──■────┼────┼─────────────░──╫──╫─┤M├────── - # └───┘ └───┘ ┌─┴─┐ │ ┌───┐ ░ ║ ║ └╥┘┌─┐ - # qr_3: ─────────────────────────┤ X ├──■────■──┤ X ├─░──╫──╫──╫─┤M├─── - # └───┘ ┌─┴─┐└─┬─┘ ░ ║ ║ ║ └╥┘┌─┐ - # qr_4: ───────────────────────────────────┤ X ├──■───░──╫──╫──╫──╫─┤M├ - # └───┘ ░ ║ ║ ║ ║ └╥┘ - # meas: 5/═══════════════════════════════════════════════╩══╩══╩══╩══╩═ - # 0 1 2 3 4 - circuit = QuantumCircuit(QuantumRegister(5, "qr")) - circuit.h(range(3)) - circuit.cx(0, 1) - circuit.cx(1, 0) - circuit.cx(1, 2) - circuit.cx(2, 1) - circuit.cx(1, 3) - circuit.cx(3, 1) - circuit.cx(3, 4) - circuit.cx(4, 3) - circuit.measure_all() - result = transpile( - circuit, - backend=FakeOurenseFaultyCX13(), - optimization_level=level, - initial_layout=[0, 1, 2, 3, 4], - seed_transpiler=42, - ) - - self.assertIdleCX13(result) - self.assertEqualCount(circuit, result) - - -@ddt -class TestFaultyQ1(TestFaultyBackendCase): - """Test preset passmanagers with FakeOurenseFaultyQ1. - A 5 qubit backend, with a faulty q1 - 0 ↔ (1) ↔ 3 ↔ 4 - ↕ - 2 - """ - - def assertIdleQ1(self, circuit): - """Asserts the Q1 in circuit is not used with operations""" - physical_qubits = QuantumRegister(5, "q") - nodes = circuit_to_dag(circuit).nodes_on_wire(physical_qubits[1]) - for node in nodes: - if isinstance(node, DAGOpNode): - raise AssertionError("Faulty Qubit Q1 not totally idle") - - @data(0, 1, 2, 3) - def test_level(self, level): - """Test level {level} Ourense backend with a faulty Q1""" - circuit = QuantumCircuit(QuantumRegister(2, "qr")) - circuit.h(range(2)) - circuit.cz(0, 1) - circuit.measure_all() - result = transpile( - circuit, backend=FakeOurenseFaultyQ1(), optimization_level=level, seed_transpiler=42 - ) - - self.assertIdleQ1(result) - self.assertEqualCount(circuit, result) - - @data(0, 1, 2, 3) - def test_layout_level(self, level): - """Test level {level} with a faulty Q1 with a working initial layout""" - circuit = QuantumCircuit(QuantumRegister(2, "qr")) - circuit.h(range(2)) - circuit.cz(0, 1) - circuit.measure_all() - result = transpile( - circuit, - backend=FakeOurenseFaultyQ1(), - optimization_level=level, - initial_layout=[4, 3], - seed_transpiler=42, - ) - - self.assertIdleQ1(result) - self.assertEqualCount(circuit, result) - - @data(0, 1, 2, 3) - def test_failing_layout_level(self, level): - """Test level {level} with a faulty Q1 with a failing initial layout. Raises.""" - circuit = QuantumCircuit(QuantumRegister(2, "qr")) - circuit.h(range(2)) - circuit.cz(0, 1) - circuit.measure_all() - - message = "The initial_layout parameter refers to faulty or disconnected qubits" - - with self.assertRaises(TranspilerError) as context: - transpile( - circuit, - backend=FakeOurenseFaultyQ1(), - optimization_level=level, - initial_layout=[4, 0], - seed_transpiler=42, - ) - - self.assertEqual(context.exception.message, message) - - -class TestFaultyQ1Unpickable(TestFaultyBackendCase): - """See: - https://github.com/Qiskit/qiskit-terra/pull/4723 - https://github.com/Qiskit/qiskit-terra/pull/4782 - """ - - def setUp(self): - """Creates a FakeBackend that is unpickable""" - super().setUp() - backend = FakeOurenseFaultyQ1() - backend.unpickable_prop = lambda x: x - self.unpickable_backend = backend - - def test_unpickable_backend(self): - """Test Ourense unpickable backend with a faulty Q1 in parallel""" - circuit = QuantumCircuit(QuantumRegister(2, "qr")) - circuit.h(range(2)) - circuit.cz(0, 1) - circuit.measure_all() - result = transpile( - [circuit, circuit], - backend=self.unpickable_backend, - optimization_level=1, - seed_transpiler=42, - ) - self.assertEqualCount(circuit, result[0]) - self.assertEqualCount(circuit, result[1]) From 58aceb59e9bc1e16f29b09402e1984b16d211da3 Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Thu, 13 Apr 2023 02:29:22 +0900 Subject: [PATCH 014/172] Deprecate PauliTable and StabilizerTable (#9547) * Deprecate PauliTable and StabilizerTable * Update qiskit/quantum_info/operators/symplectic/pauli_table.py Co-authored-by: Julien Gacon * use deprecate_func instead of deprecate_function --------- Co-authored-by: Julien Gacon --- .../operators/symplectic/clifford.py | 42 +- .../operators/symplectic/pauli_table.py | 4 +- .../operators/symplectic/stabilizer_table.py | 6 +- ...eprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml | 9 + .../operators/symplectic/test_clifford.py | 11 +- .../operators/symplectic/test_pauli_list.py | 8 +- .../operators/symplectic/test_pauli_table.py | 653 +++++++++----- .../symplectic/test_stabilizer_table.py | 842 +++++++++++------- .../quantum_info/operators/test_random.py | 20 +- 9 files changed, 992 insertions(+), 603 deletions(-) create mode 100644 releasenotes/notes/deprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml diff --git a/qiskit/quantum_info/operators/symplectic/clifford.py b/qiskit/quantum_info/operators/symplectic/clifford.py index d0268989328d..3990d4dd280f 100644 --- a/qiskit/quantum_info/operators/symplectic/clifford.py +++ b/qiskit/quantum_info/operators/symplectic/clifford.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017--2022 +# (C) Copyright IBM 2017--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 @@ -27,6 +27,7 @@ from qiskit.quantum_info.operators.operator import Operator from qiskit.quantum_info.operators.scalar_op import ScalarOp from qiskit.quantum_info.operators.symplectic.base_pauli import _count_y +from qiskit.utils.deprecation import deprecate_func from .base_pauli import BasePauli from .clifford_circuits import _append_circuit, _append_operation @@ -145,7 +146,7 @@ def __init__(self, data, validate=True, copy=True): num_qubits = data.num_qubits self.tableau = Clifford.from_circuit(data).tableau - # DEPRECATE in the future: data is StabilizerTable + # DEPRECATED: data is StabilizerTable elif isinstance(data, StabilizerTable): self.tableau = self._stack_table_phase(data.array, data.phase) num_qubits = data.num_qubits @@ -212,20 +213,37 @@ def copy(self): # Attributes # --------------------------------------------------------------------- + # pylint: disable=bad-docstring-quotes + + @deprecate_func( + since="0.24.0", + additional_msg="Instead, index or iterate through the Clifford.tableau attribute.", + ) def __getitem__(self, key): """Return a stabilizer Pauli row""" return self.table.__getitem__(key) + @deprecate_func(since="0.24.0", additional_msg="Use Clifford.tableau property instead.") def __setitem__(self, key, value): """Set a stabilizer Pauli row""" self.tableau.__setitem__(key, self._stack_table_phase(value.array, value.phase)) @property + @deprecate_func( + since="0.24.0", + additional_msg="Use Clifford.stab and Clifford.destab properties instead.", + is_property=True, + ) def table(self): """Return StabilizerTable""" return StabilizerTable(self.symplectic_matrix, phase=self.phase) @table.setter + @deprecate_func( + since="0.24.0", + additional_msg="Use Clifford.stab and Clifford.destab properties instead.", + is_property=True, + ) def table(self, value): """Set the stabilizer table""" # Note this setter cannot change the size of the Clifford @@ -237,6 +255,11 @@ def table(self, value): self.phase = value._table._phase @property + @deprecate_func( + since="0.24.0", + additional_msg="Use Clifford.stab properties instead.", + is_property=True, + ) def stabilizer(self): """Return the stabilizer block of the StabilizerTable.""" array = self.tableau[self.num_qubits : 2 * self.num_qubits, :-1] @@ -244,6 +267,11 @@ def stabilizer(self): return StabilizerTable(array, phase) @stabilizer.setter + @deprecate_func( + since="0.24.0", + additional_msg="Use Clifford.stab properties instead.", + is_property=True, + ) def stabilizer(self, value): """Set the value of stabilizer block of the StabilizerTable""" if not isinstance(value, StabilizerTable): @@ -251,6 +279,11 @@ def stabilizer(self, value): self.tableau[self.num_qubits : 2 * self.num_qubits, :-1] = value.array @property + @deprecate_func( + since="0.24.0", + additional_msg="Use Clifford.destab properties instead.", + is_property=True, + ) def destabilizer(self): """Return the destabilizer block of the StabilizerTable.""" array = self.tableau[0 : self.num_qubits, :-1] @@ -258,6 +291,11 @@ def destabilizer(self): return StabilizerTable(array, phase) @destabilizer.setter + @deprecate_func( + since="0.24.0", + additional_msg="Use Clifford.destab properties instead.", + is_property=True, + ) def destabilizer(self, value): """Set the value of destabilizer block of the StabilizerTable""" if not isinstance(value, StabilizerTable): diff --git a/qiskit/quantum_info/operators/symplectic/pauli_table.py b/qiskit/quantum_info/operators/symplectic/pauli_table.py index e792f807c516..92faac92f753 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli_table.py +++ b/qiskit/quantum_info/operators/symplectic/pauli_table.py @@ -28,7 +28,7 @@ class PauliTable(BaseOperator, AdjointMixin): - r"""Symplectic representation of a list Pauli matrices. + r"""DEPRECATED: Symplectic representation of a list Pauli matrices. **Symplectic Representation** @@ -127,7 +127,7 @@ class PauliTable(BaseOperator, AdjointMixin): `arXiv:quant-ph/0406196 `_ """ - @deprecate_func(additional_msg="Instead, use the class PauliList", since="0.23.0", pending=True) + @deprecate_func(additional_msg="Instead, use the class PauliList", since="0.24.0") def __init__(self, data): """Initialize the PauliTable. diff --git a/qiskit/quantum_info/operators/symplectic/stabilizer_table.py b/qiskit/quantum_info/operators/symplectic/stabilizer_table.py index 2b162f15f1b4..00e717b7a20a 100644 --- a/qiskit/quantum_info/operators/symplectic/stabilizer_table.py +++ b/qiskit/quantum_info/operators/symplectic/stabilizer_table.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2020 +# (C) Copyright IBM 2017, 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 @@ -23,7 +23,7 @@ class StabilizerTable(PauliTable, AdjointMixin): - r"""Symplectic representation of a list Stabilizer matrices. + r"""DEPRECATED: Symplectic representation of a list Stabilizer matrices. **Symplectic Representation** @@ -169,7 +169,7 @@ class StabilizerTable(PauliTable, AdjointMixin): `arXiv:quant-ph/0406196 `_ """ - @deprecate_func(additional_msg="Instead, use the class PauliList", since="0.23.0", pending=True) + @deprecate_func(additional_msg="Instead, use the class PauliList", since="0.24.0") def __init__(self, data, phase=None): """Initialize the StabilizerTable. diff --git a/releasenotes/notes/deprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml b/releasenotes/notes/deprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml new file mode 100644 index 000000000000..a10b8c87bdd1 --- /dev/null +++ b/releasenotes/notes/deprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml @@ -0,0 +1,9 @@ +--- +deprecations: + - | + The :class:`~qiskit.quantum_info.PauliTable` and :class:`~qiskit.quantum_info.StabilizerTable` + are deprecated and will be removed in a future release. + Instead, the :class:`~qiskit.quantum_info.PauliList` should be used. + With this change, :meth:`~qiskit.quantum_info.Clifford.table` has been deprecated + so that you should operate directly from :meth:`~qiskit.quantum_info.Clifford.tableau` + without it. diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py index 4c162c8753cc..b1b606ea12d7 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, 2020. +# (C) Copyright IBM 2017, 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 @@ -241,10 +241,11 @@ def test_append_1_qubit_gate(self): with self.subTest(msg="append gate %s" % gate_name): cliff = Clifford([[1, 0], [0, 1]]) cliff = _append_operation(cliff, gate_name, [0]) - value_table = cliff.table._array - value_phase = cliff.table._phase - value_stabilizer = cliff.stabilizer.to_labels() - value_destabilizer = cliff.destabilizer.to_labels() + with self.assertWarns(DeprecationWarning): + value_table = cliff.table._array + value_phase = cliff.table._phase + value_stabilizer = cliff.stabilizer.to_labels() + value_destabilizer = cliff.destabilizer.to_labels() self.assertTrue(np.all(np.array(value_table == target_table[gate_name]))) self.assertTrue(np.all(np.array(value_phase == target_phase[gate_name]))) self.assertTrue( 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 0f3d0dbb00fa..28e4d30b215f 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli_list.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli_list.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2020. +# (C) Copyright IBM 2017, 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 @@ -231,12 +231,14 @@ def test_pauli_table_init(self): def test_stabilizer_table_init(self): """Test table initialization.""" with self.subTest(msg="PauliTable"): - target = StabilizerTable.from_labels(["+II", "-XZ"]) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["+II", "-XZ"]) value = PauliList(target) self.assertEqual(value, target) with self.subTest(msg="PauliTable no copy"): - target = StabilizerTable.from_labels(["+YY", "-XZ", "XI"]) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["+YY", "-XZ", "XI"]) value = PauliList(target) value[0] = "II" self.assertEqual(value, target) diff --git a/test/python/quantum_info/operators/symplectic/test_pauli_table.py b/test/python/quantum_info/operators/symplectic/test_pauli_table.py index 03f0b8f534bd..5d51aa2071ae 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli_table.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli_table.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2020. +# (C) Copyright IBM 2017, 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 @@ -14,13 +14,14 @@ import unittest from test import combine -from ddt import ddt + import numpy as np +from ddt import ddt from scipy.sparse import csr_matrix from qiskit import QiskitError -from qiskit.test import QiskitTestCase from qiskit.quantum_info.operators.symplectic import PauliTable +from qiskit.test import QiskitTestCase def pauli_mat(label): @@ -48,30 +49,35 @@ def test_array_init(self): # Matrix array initialization with self.subTest(msg="bool array"): target = np.array([[False, False], [True, True]]) - value = PauliTable(target)._array + with self.assertWarns(DeprecationWarning): + value = PauliTable(target)._array self.assertTrue(np.all(value == target)) with self.subTest(msg="bool array no copy"): target = np.array([[False, True], [True, True]]) - value = PauliTable(target)._array + with self.assertWarns(DeprecationWarning): + value = PauliTable(target)._array value[0, 0] = not value[0, 0] self.assertTrue(np.all(value == target)) with self.subTest(msg="bool array raises"): array = np.array([[False, False, False], [True, True, True]]) - self.assertRaises(QiskitError, PauliTable, array) + with self.assertWarns(DeprecationWarning): + self.assertRaises(QiskitError, PauliTable, array) def test_vector_init(self): """Test vector initialization.""" # Vector array initialization with self.subTest(msg="bool vector"): target = np.array([False, False, False, False]) - value = PauliTable(target)._array + with self.assertWarns(DeprecationWarning): + value = PauliTable(target)._array self.assertTrue(np.all(value == target)) with self.subTest(msg="bool vector no copy"): target = np.array([False, True, True, False]) - value = PauliTable(target)._array + with self.assertWarns(DeprecationWarning): + value = PauliTable(target)._array value[0, 0] = not value[0, 0] self.assertTrue(np.all(value == target)) @@ -79,42 +85,50 @@ def test_string_init(self): """Test string initialization.""" # String initialization with self.subTest(msg='str init "I"'): - value = PauliTable("I")._array + with self.assertWarns(DeprecationWarning): + value = PauliTable("I")._array target = np.array([[False, False]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "X"'): - value = PauliTable("X")._array + with self.assertWarns(DeprecationWarning): + value = PauliTable("X")._array target = np.array([[True, False]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "Y"'): - value = PauliTable("Y")._array + with self.assertWarns(DeprecationWarning): + value = PauliTable("Y")._array target = np.array([[True, True]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "Z"'): - value = PauliTable("Z")._array + with self.assertWarns(DeprecationWarning): + value = PauliTable("Z")._array target = np.array([[False, True]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "IX"'): - value = PauliTable("IX")._array + with self.assertWarns(DeprecationWarning): + value = PauliTable("IX")._array target = np.array([[True, False, False, False]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "XI"'): - value = PauliTable("XI")._array + with self.assertWarns(DeprecationWarning): + value = PauliTable("XI")._array target = np.array([[False, True, False, False]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "YZ"'): - value = PauliTable("YZ")._array + with self.assertWarns(DeprecationWarning): + value = PauliTable("YZ")._array target = np.array([[False, True, True, True]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "XIZ"'): - value = PauliTable("XIZ")._array + with self.assertWarns(DeprecationWarning): + value = PauliTable("XIZ")._array target = np.array([[False, False, True, True, False, False]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) @@ -122,13 +136,15 @@ def test_table_init(self): """Test table initialization.""" # Pauli Table initialization with self.subTest(msg="PauliTable"): - target = PauliTable.from_labels(["XI", "IX", "IZ"]) - value = PauliTable(target) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["XI", "IX", "IZ"]) + value = PauliTable(target) self.assertEqual(value, target) with self.subTest(msg="PauliTable no copy"): - target = PauliTable.from_labels(["XI", "IX", "IZ"]) - value = PauliTable(target) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["XI", "IX", "IZ"]) + value = PauliTable(target) value[0] = "II" self.assertEqual(value, target) @@ -140,12 +156,14 @@ def test_array_propertiy(self): """Test array property""" with self.subTest(msg="array"): - pauli = PauliTable("II") + with self.assertWarns(DeprecationWarning): + pauli = PauliTable("II") array = np.zeros([2, 4], dtype=bool) self.assertTrue(np.all(pauli.array == array)) with self.subTest(msg="set array"): - pauli = PauliTable("XX") + with self.assertWarns(DeprecationWarning): + pauli = PauliTable("XX") array = np.zeros([1, 4], dtype=bool) pauli.array = array self.assertTrue(np.all(pauli.array == array)) @@ -153,7 +171,8 @@ def test_array_propertiy(self): with self.subTest(msg="set array raises"): def set_array_raise(): - pauli = PauliTable("XXX") + with self.assertWarns(DeprecationWarning): + pauli = PauliTable("XXX") pauli.array = np.eye(4) return pauli @@ -162,20 +181,24 @@ def set_array_raise(): def test_x_propertiy(self): """Test X property""" with self.subTest(msg="X"): - pauli = PauliTable.from_labels(["XI", "IZ", "YY"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["XI", "IZ", "YY"]) array = np.array([[False, True], [False, False], [True, True]], dtype=bool) self.assertTrue(np.all(pauli.X == array)) with self.subTest(msg="set X"): - pauli = PauliTable.from_labels(["XI", "IZ"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["XI", "IZ"]) val = np.array([[False, False], [True, True]], dtype=bool) pauli.X = val - self.assertEqual(pauli, PauliTable.from_labels(["II", "XY"])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pauli, PauliTable.from_labels(["II", "XY"])) with self.subTest(msg="set X raises"): def set_x(): - pauli = PauliTable.from_labels(["XI", "IZ"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["XI", "IZ"]) val = np.array([[False, False, False], [True, True, True]], dtype=bool) pauli.X = val return pauli @@ -185,20 +208,24 @@ def set_x(): def test_z_propertiy(self): """Test Z property""" with self.subTest(msg="Z"): - pauli = PauliTable.from_labels(["XI", "IZ", "YY"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["XI", "IZ", "YY"]) array = np.array([[False, False], [True, False], [True, True]], dtype=bool) self.assertTrue(np.all(pauli.Z == array)) with self.subTest(msg="set Z"): - pauli = PauliTable.from_labels(["XI", "IZ"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["XI", "IZ"]) val = np.array([[False, False], [True, True]], dtype=bool) pauli.Z = val - self.assertEqual(pauli, PauliTable.from_labels(["XI", "ZZ"])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pauli, PauliTable.from_labels(["XI", "ZZ"])) with self.subTest(msg="set Z raises"): def set_z(): - pauli = PauliTable.from_labels(["XI", "IZ"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["XI", "IZ"]) val = np.array([[False, False, False], [True, True, True]], dtype=bool) pauli.Z = val return pauli @@ -208,7 +235,8 @@ def set_z(): def test_shape_propertiy(self): """Test shape property""" shape = (3, 8) - pauli = PauliTable(np.zeros(shape)) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable(np.zeros(shape)) self.assertEqual(pauli.shape, shape) def test_size_propertiy(self): @@ -216,7 +244,8 @@ def test_size_propertiy(self): with self.subTest(msg="size"): for j in range(1, 10): shape = (j, 8) - pauli = PauliTable(np.zeros(shape)) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable(np.zeros(shape)) self.assertEqual(pauli.size, j) def test_n_qubit_propertiy(self): @@ -224,13 +253,15 @@ def test_n_qubit_propertiy(self): with self.subTest(msg="num_qubits"): for j in range(1, 10): shape = (5, 2 * j) - pauli = PauliTable(np.zeros(shape)) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable(np.zeros(shape)) self.assertEqual(pauli.num_qubits, j) def test_eq(self): """Test __eq__ method.""" - pauli1 = PauliTable.from_labels(["II", "XI"]) - pauli2 = PauliTable.from_labels(["XI", "II"]) + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(["II", "XI"]) + pauli2 = PauliTable.from_labels(["XI", "II"]) self.assertEqual(pauli1, pauli1) self.assertNotEqual(pauli1, pauli2) @@ -238,70 +269,86 @@ def test_len_methods(self): """Test __len__ method.""" for j in range(1, 10): labels = j * ["XX"] - pauli = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) self.assertEqual(len(pauli), j) def test_add_methods(self): """Test __add__ method.""" labels1 = ["XXI", "IXX"] labels2 = ["XXI", "ZZI", "ZYZ"] - pauli1 = PauliTable.from_labels(labels1) - pauli2 = PauliTable.from_labels(labels2) - target = PauliTable.from_labels(labels1 + labels2) + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(labels1) + pauli2 = PauliTable.from_labels(labels2) + target = PauliTable.from_labels(labels1 + labels2) self.assertEqual(target, pauli1 + pauli2) def test_add_qargs(self): """Test add method with qargs.""" - pauli1 = PauliTable.from_labels(["IIII", "YYYY"]) - pauli2 = PauliTable.from_labels(["XY", "YZ"]) + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(["IIII", "YYYY"]) + pauli2 = PauliTable.from_labels(["XY", "YZ"]) with self.subTest(msg="qargs=[0, 1]"): - target = PauliTable.from_labels(["IIII", "YYYY", "IIXY", "IIYZ"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IIII", "YYYY", "IIXY", "IIYZ"]) self.assertEqual(pauli1 + pauli2([0, 1]), target) with self.subTest(msg="qargs=[0, 3]"): - target = PauliTable.from_labels(["IIII", "YYYY", "XIIY", "YIIZ"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IIII", "YYYY", "XIIY", "YIIZ"]) self.assertEqual(pauli1 + pauli2([0, 3]), target) with self.subTest(msg="qargs=[2, 1]"): - target = PauliTable.from_labels(["IIII", "YYYY", "IYXI", "IZYI"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IIII", "YYYY", "IYXI", "IZYI"]) self.assertEqual(pauli1 + pauli2([2, 1]), target) with self.subTest(msg="qargs=[3, 1]"): - target = PauliTable.from_labels(["IIII", "YYYY", "YIXI", "ZIYI"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IIII", "YYYY", "YIXI", "ZIYI"]) self.assertEqual(pauli1 + pauli2([3, 1]), target) def test_getitem_methods(self): """Test __getitem__ method.""" with self.subTest(msg="__getitem__ single"): labels = ["XI", "IY"] - pauli = PauliTable.from_labels(labels) - self.assertEqual(pauli[0], PauliTable(labels[0])) - self.assertEqual(pauli[1], PauliTable(labels[1])) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) + self.assertEqual(pauli[0], PauliTable(labels[0])) + self.assertEqual(pauli[1], PauliTable(labels[1])) with self.subTest(msg="__getitem__ array"): labels = np.array(["XI", "IY", "IZ", "XY", "ZX"]) - pauli = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) inds = [0, 3] - self.assertEqual(pauli[inds], PauliTable.from_labels(labels[inds])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pauli[inds], PauliTable.from_labels(labels[inds])) inds = np.array([4, 1]) - self.assertEqual(pauli[inds], PauliTable.from_labels(labels[inds])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pauli[inds], PauliTable.from_labels(labels[inds])) with self.subTest(msg="__getitem__ slice"): labels = np.array(["XI", "IY", "IZ", "XY", "ZX"]) - pauli = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) self.assertEqual(pauli[:], pauli) - self.assertEqual(pauli[1:3], PauliTable.from_labels(labels[1:3])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pauli[1:3], PauliTable.from_labels(labels[1:3])) def test_setitem_methods(self): """Test __setitem__ method.""" with self.subTest(msg="__setitem__ single"): labels = ["XI", "IY"] - pauli = PauliTable.from_labels(["XI", "IY"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["XI", "IY"]) pauli[0] = "II" - self.assertEqual(pauli[0], PauliTable("II")) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pauli[0], PauliTable("II")) pauli[1] = "XX" - self.assertEqual(pauli[1], PauliTable("XX")) + with self.assertWarns(DeprecationWarning): + self.assertEqual(pauli[1], PauliTable("XX")) def raises_single(): # Wrong size Pauli @@ -311,24 +358,28 @@ def raises_single(): with self.subTest(msg="__setitem__ array"): labels = np.array(["XI", "IY", "IZ"]) - pauli = PauliTable.from_labels(labels) - target = PauliTable.from_labels(["II", "ZZ"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) + target = PauliTable.from_labels(["II", "ZZ"]) inds = [2, 0] pauli[inds] = target self.assertEqual(pauli[inds], target) def raises_array(): - pauli[inds] = PauliTable.from_labels(["YY", "ZZ", "XX"]) + with self.assertWarns(DeprecationWarning): + pauli[inds] = PauliTable.from_labels(["YY", "ZZ", "XX"]) self.assertRaises(Exception, raises_array) with self.subTest(msg="__setitem__ slice"): labels = np.array(5 * ["III"]) - pauli = PauliTable.from_labels(labels) - target = PauliTable.from_labels(5 * ["XXX"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) + target = PauliTable.from_labels(5 * ["XXX"]) pauli[:] = target self.assertEqual(pauli[:], target) - target = PauliTable.from_labels(2 * ["ZZZ"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(2 * ["ZZZ"]) pauli[1:3] = target self.assertEqual(pauli[1:3], target) @@ -342,8 +393,9 @@ def test_from_labels_1q(self): array = np.array( [[False, False], [False, True], [False, True], [True, False], [True, True]], dtype=bool ) - target = PauliTable(array) - value = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + target = PauliTable(array) + value = PauliTable.from_labels(labels) self.assertEqual(target, value) def test_from_labels_2q(self): @@ -353,8 +405,9 @@ def test_from_labels_2q(self): [[False, False, False, False], [True, True, True, True], [False, True, True, False]], dtype=bool, ) - target = PauliTable(array) - value = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + target = PauliTable(array) + value = PauliTable.from_labels(labels) self.assertEqual(target, value) def test_from_labels_5q(self): @@ -364,30 +417,33 @@ def test_from_labels_5q(self): [10 * [False], 5 * [True] + 5 * [False], 10 * [True], 5 * [False] + 5 * [True]], dtype=bool, ) - target = PauliTable(array) - value = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + target = PauliTable(array) + value = PauliTable.from_labels(labels) self.assertEqual(target, value) def test_to_labels_1q(self): """Test 1-qubit to_labels method.""" - pauli = PauliTable( - np.array( - [[False, False], [False, True], [False, True], [True, False], [True, True]], - dtype=bool, + with self.assertWarns(DeprecationWarning): + pauli = PauliTable( + np.array( + [[False, False], [False, True], [False, True], [True, False], [True, True]], + dtype=bool, + ) ) - ) target = ["I", "Z", "Z", "X", "Y"] value = pauli.to_labels() self.assertEqual(value, target) def test_to_labels_1q_array(self): """Test 1-qubit to_labels method w/ array=True.""" - pauli = PauliTable( - np.array( - [[False, False], [False, True], [False, True], [True, False], [True, True]], - dtype=bool, + with self.assertWarns(DeprecationWarning): + pauli = PauliTable( + np.array( + [[False, False], [False, True], [False, True], [True, False], [True, True]], + dtype=bool, + ) ) - ) target = np.array(["I", "Z", "Z", "X", "Y"]) value = pauli.to_labels(array=True) self.assertTrue(np.all(value == target)) @@ -395,14 +451,16 @@ def test_to_labels_1q_array(self): def test_labels_round_trip(self): """Test from_labels and to_labels round trip.""" target = ["III", "IXZ", "XYI", "ZZZ"] - value = PauliTable.from_labels(target).to_labels() + with self.assertWarns(DeprecationWarning): + value = PauliTable.from_labels(target).to_labels() self.assertEqual(value, target) def test_labels_round_trip_array(self): """Test from_labels and to_labels round trip w/ array=True.""" labels = ["III", "IXZ", "XYI", "ZZZ"] target = np.array(labels) - value = PauliTable.from_labels(labels).to_labels(array=True) + with self.assertWarns(DeprecationWarning): + value = PauliTable.from_labels(labels).to_labels(array=True) self.assertTrue(np.all(value == target)) @@ -413,7 +471,8 @@ def test_to_matrix_1q(self): """Test 1-qubit to_matrix method.""" labels = ["X", "I", "Z", "Y"] targets = [pauli_mat(i) for i in labels] - values = PauliTable.from_labels(labels).to_matrix() + with self.assertWarns(DeprecationWarning): + values = PauliTable.from_labels(labels).to_matrix() self.assertTrue(isinstance(values, list)) for target, value in zip(targets, values): self.assertTrue(np.all(value == target)) @@ -422,7 +481,8 @@ def test_to_matrix_1q_array(self): """Test 1-qubit to_matrix method w/ array=True.""" labels = ["Z", "I", "Y", "X"] target = np.array([pauli_mat(i) for i in labels]) - value = PauliTable.from_labels(labels).to_matrix(array=True) + with self.assertWarns(DeprecationWarning): + value = PauliTable.from_labels(labels).to_matrix(array=True) self.assertTrue(isinstance(value, np.ndarray)) self.assertTrue(np.all(value == target)) @@ -430,7 +490,8 @@ def test_to_matrix_1q_sparse(self): """Test 1-qubit to_matrix method w/ sparse=True.""" labels = ["X", "I", "Z", "Y"] targets = [pauli_mat(i) for i in labels] - values = PauliTable.from_labels(labels).to_matrix(sparse=True) + with self.assertWarns(DeprecationWarning): + values = PauliTable.from_labels(labels).to_matrix(sparse=True) for mat, targ in zip(values, targets): self.assertTrue(isinstance(mat, csr_matrix)) self.assertTrue(np.all(targ == mat.toarray())) @@ -439,7 +500,8 @@ def test_to_matrix_2q(self): """Test 2-qubit to_matrix method.""" labels = ["IX", "YI", "II", "ZZ"] targets = [pauli_mat(i) for i in labels] - values = PauliTable.from_labels(labels).to_matrix() + with self.assertWarns(DeprecationWarning): + values = PauliTable.from_labels(labels).to_matrix() self.assertTrue(isinstance(values, list)) for target, value in zip(targets, values): self.assertTrue(np.all(value == target)) @@ -448,7 +510,8 @@ def test_to_matrix_2q_array(self): """Test 2-qubit to_matrix method w/ array=True.""" labels = ["ZZ", "XY", "YX", "IZ"] target = np.array([pauli_mat(i) for i in labels]) - value = PauliTable.from_labels(labels).to_matrix(array=True) + with self.assertWarns(DeprecationWarning): + value = PauliTable.from_labels(labels).to_matrix(array=True) self.assertTrue(isinstance(value, np.ndarray)) self.assertTrue(np.all(value == target)) @@ -456,7 +519,8 @@ def test_to_matrix_2q_sparse(self): """Test 2-qubit to_matrix method w/ sparse=True.""" labels = ["IX", "II", "ZY", "YZ"] targets = [pauli_mat(i) for i in labels] - values = PauliTable.from_labels(labels).to_matrix(sparse=True) + with self.assertWarns(DeprecationWarning): + values = PauliTable.from_labels(labels).to_matrix(sparse=True) for mat, targ in zip(values, targets): self.assertTrue(isinstance(mat, csr_matrix)) self.assertTrue(np.all(targ == mat.toarray())) @@ -465,7 +529,8 @@ def test_to_matrix_5q(self): """Test 5-qubit to_matrix method.""" labels = ["IXIXI", "YZIXI", "IIXYZ"] targets = [pauli_mat(i) for i in labels] - values = PauliTable.from_labels(labels).to_matrix() + with self.assertWarns(DeprecationWarning): + values = PauliTable.from_labels(labels).to_matrix() self.assertTrue(isinstance(values, list)) for target, value in zip(targets, values): self.assertTrue(np.all(value == target)) @@ -474,7 +539,8 @@ def test_to_matrix_5q_sparse(self): """Test 5-qubit to_matrix method w/ sparse=True.""" labels = ["XXXYY", "IXIZY", "ZYXIX"] targets = [pauli_mat(i) for i in labels] - values = PauliTable.from_labels(labels).to_matrix(sparse=True) + with self.assertWarns(DeprecationWarning): + values = PauliTable.from_labels(labels).to_matrix(sparse=True) for mat, targ in zip(values, targets): self.assertTrue(isinstance(mat, csr_matrix)) self.assertTrue(np.all(targ == mat.toarray())) @@ -486,42 +552,51 @@ class TestPauliTableIteration(QiskitTestCase): def test_enumerate(self): """Test enumerate with PauliTable.""" labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"] - pauli = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) for idx, i in enumerate(pauli): - self.assertEqual(i, PauliTable(labels[idx])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(i, PauliTable(labels[idx])) def test_iter(self): """Test iter with PauliTable.""" labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"] - pauli = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) for idx, i in enumerate(iter(pauli)): - self.assertEqual(i, PauliTable(labels[idx])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(i, PauliTable(labels[idx])) def test_zip(self): """Test zip with PauliTable.""" labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"] - pauli = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) for label, i in zip(labels, pauli): - self.assertEqual(i, PauliTable(label)) + with self.assertWarns(DeprecationWarning): + self.assertEqual(i, PauliTable(label)) def test_label_iter(self): """Test PauliTable label_iter method.""" labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"] - pauli = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) for idx, i in enumerate(pauli.label_iter()): self.assertEqual(i, labels[idx]) def test_matrix_iter(self): """Test PauliTable dense matrix_iter method.""" labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"] - pauli = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) for idx, i in enumerate(pauli.matrix_iter()): self.assertTrue(np.all(i == pauli_mat(labels[idx]))) def test_matrix_iter_sparse(self): """Test PauliTable sparse matrix_iter method.""" labels = ["III", "IXI", "IYY", "YIZ", "XYZ", "III"] - pauli = PauliTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(labels) for idx, i in enumerate(pauli.matrix_iter(sparse=True)): self.assertTrue(isinstance(i, csr_matrix)) self.assertTrue(np.all(i.toarray() == pauli_mat(labels[idx]))) @@ -536,11 +611,13 @@ def test_tensor(self, j): """Test tensor method j={j}.""" labels1 = ["XX", "YY"] labels2 = [j * "I", j * "Z"] - pauli1 = PauliTable.from_labels(labels1) - pauli2 = PauliTable.from_labels(labels2) + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(labels1) + pauli2 = PauliTable.from_labels(labels2) value = pauli1.tensor(pauli2) - target = PauliTable.from_labels([i + j for i in labels1 for j in labels2]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels([i + j for i in labels1 for j in labels2]) self.assertEqual(value, target) @combine(j=range(1, 10)) @@ -548,206 +625,246 @@ def test_expand(self, j): """Test expand method j={j}.""" labels1 = ["XX", "YY"] labels2 = [j * "I", j * "Z"] - pauli1 = PauliTable.from_labels(labels1) - pauli2 = PauliTable.from_labels(labels2) + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(labels1) + pauli2 = PauliTable.from_labels(labels2) value = pauli1.expand(pauli2) - target = PauliTable.from_labels([j + i for j in labels2 for i in labels1]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels([j + i for j in labels2 for i in labels1]) self.assertEqual(value, target) def test_compose_1q(self): """Test 1-qubit compose methods.""" # Test single qubit Pauli dot products - pauli = PauliTable.from_labels(["I", "X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["I", "X", "Y", "Z"]) with self.subTest(msg="compose single I"): - target = PauliTable.from_labels(["I", "X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["I", "X", "Y", "Z"]) value = pauli.compose("I") self.assertEqual(target, value) with self.subTest(msg="compose single X"): - target = PauliTable.from_labels(["X", "I", "Z", "Y"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["X", "I", "Z", "Y"]) value = pauli.compose("X") self.assertEqual(target, value) with self.subTest(msg="compose single Y"): - target = PauliTable.from_labels(["Y", "Z", "I", "X"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["Y", "Z", "I", "X"]) value = pauli.compose("Y") self.assertEqual(target, value) with self.subTest(msg="compose single Z"): - target = PauliTable.from_labels(["Z", "Y", "X", "I"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["Z", "Y", "X", "I"]) value = pauli.compose("Z") self.assertEqual(target, value) def test_dot_1q(self): """Test 1-qubit dot method.""" # Test single qubit Pauli dot products - pauli = PauliTable.from_labels(["I", "X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["I", "X", "Y", "Z"]) with self.subTest(msg="dot single I"): - target = PauliTable.from_labels(["I", "X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["I", "X", "Y", "Z"]) value = pauli.dot("I") self.assertEqual(target, value) with self.subTest(msg="dot single X"): - target = PauliTable.from_labels(["X", "I", "Z", "Y"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["X", "I", "Z", "Y"]) value = pauli.dot("X") self.assertEqual(target, value) with self.subTest(msg="dot single Y"): - target = PauliTable.from_labels(["Y", "Z", "I", "X"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["Y", "Z", "I", "X"]) value = pauli.dot("Y") self.assertEqual(target, value) with self.subTest(msg="dot single Z"): - target = PauliTable.from_labels(["Z", "Y", "X", "I"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["Z", "Y", "X", "I"]) value = pauli.dot("Z") self.assertEqual(target, value) def test_qargs_compose_1q(self): """Test 1-qubit compose method with qargs.""" - pauli1 = PauliTable.from_labels(["III", "XXX"]) - pauli2 = PauliTable("Z") + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(["III", "XXX"]) + pauli2 = PauliTable("Z") with self.subTest(msg="compose 1-qubit qargs=[0]"): - target = PauliTable.from_labels(["IIZ", "XXY"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IIZ", "XXY"]) value = pauli1.compose(pauli2, qargs=[0]) self.assertEqual(value, target) with self.subTest(msg="compose 1-qubit qargs=[1]"): - target = PauliTable.from_labels(["IZI", "XYX"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IZI", "XYX"]) value = pauli1.compose(pauli2, qargs=[1]) self.assertEqual(value, target) with self.subTest(msg="compose 1-qubit qargs=[2]"): - target = PauliTable.from_labels(["ZII", "YXX"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["ZII", "YXX"]) value = pauli1.compose(pauli2, qargs=[2]) self.assertEqual(value, target) def test_qargs_dot_1q(self): """Test 1-qubit dot method with qargs.""" - pauli1 = PauliTable.from_labels(["III", "XXX"]) - pauli2 = PauliTable("Z") + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(["III", "XXX"]) + pauli2 = PauliTable("Z") with self.subTest(msg="dot 1-qubit qargs=[0]"): - target = PauliTable.from_labels(["IIZ", "XXY"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IIZ", "XXY"]) value = pauli1.dot(pauli2, qargs=[0]) self.assertEqual(value, target) with self.subTest(msg="dot 1-qubit qargs=[1]"): - target = PauliTable.from_labels(["IZI", "XYX"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IZI", "XYX"]) value = pauli1.dot(pauli2, qargs=[1]) self.assertEqual(value, target) with self.subTest(msg="dot 1-qubit qargs=[2]"): - target = PauliTable.from_labels(["ZII", "YXX"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["ZII", "YXX"]) value = pauli1.dot(pauli2, qargs=[2]) self.assertEqual(value, target) def test_qargs_compose_2q(self): """Test 2-qubit compose method with qargs.""" - pauli1 = PauliTable.from_labels(["III", "XXX"]) - pauli2 = PauliTable("ZY") + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(["III", "XXX"]) + pauli2 = PauliTable("ZY") with self.subTest(msg="compose 2-qubit qargs=[0, 1]"): - target = PauliTable.from_labels(["IZY", "XYZ"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IZY", "XYZ"]) value = pauli1.compose(pauli2, qargs=[0, 1]) self.assertEqual(value, target) with self.subTest(msg="compose 2-qubit qargs=[1, 0]"): - target = PauliTable.from_labels(["IYZ", "XZY"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IYZ", "XZY"]) value = pauli1.compose(pauli2, qargs=[1, 0]) self.assertEqual(value, target) with self.subTest(msg="compose 2-qubit qargs=[0, 2]"): - target = PauliTable.from_labels(["ZIY", "YXZ"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["ZIY", "YXZ"]) value = pauli1.compose(pauli2, qargs=[0, 2]) self.assertEqual(value, target) with self.subTest(msg="compose 2-qubit qargs=[2, 0]"): - target = PauliTable.from_labels(["YIZ", "ZXY"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["YIZ", "ZXY"]) value = pauli1.compose(pauli2, qargs=[2, 0]) self.assertEqual(value, target) def test_qargs_dot_2q(self): """Test 2-qubit dot method with qargs.""" - pauli1 = PauliTable.from_labels(["III", "XXX"]) - pauli2 = PauliTable("ZY") + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(["III", "XXX"]) + pauli2 = PauliTable("ZY") with self.subTest(msg="dot 2-qubit qargs=[0, 1]"): - target = PauliTable.from_labels(["IZY", "XYZ"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IZY", "XYZ"]) value = pauli1.dot(pauli2, qargs=[0, 1]) self.assertEqual(value, target) with self.subTest(msg="dot 2-qubit qargs=[1, 0]"): - target = PauliTable.from_labels(["IYZ", "XZY"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IYZ", "XZY"]) value = pauli1.dot(pauli2, qargs=[1, 0]) self.assertEqual(value, target) with self.subTest(msg="dot 2-qubit qargs=[0, 2]"): - target = PauliTable.from_labels(["ZIY", "YXZ"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["ZIY", "YXZ"]) value = pauli1.dot(pauli2, qargs=[0, 2]) self.assertEqual(value, target) with self.subTest(msg="dot 2-qubit qargs=[2, 0]"): - target = PauliTable.from_labels(["YIZ", "ZXY"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["YIZ", "ZXY"]) value = pauli1.dot(pauli2, qargs=[2, 0]) self.assertEqual(value, target) def test_qargs_compose_3q(self): """Test 3-qubit compose method with qargs.""" - pauli1 = PauliTable.from_labels(["III", "XXX"]) - pauli2 = PauliTable("XYZ") + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(["III", "XXX"]) + pauli2 = PauliTable("XYZ") with self.subTest(msg="compose 3-qubit qargs=None"): - target = PauliTable.from_labels(["XYZ", "IZY"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["XYZ", "IZY"]) value = pauli1.compose(pauli2) self.assertEqual(value, target) with self.subTest(msg="compose 3-qubit qargs=[0, 1, 2]"): - target = PauliTable.from_labels(["XYZ", "IZY"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["XYZ", "IZY"]) value = pauli1.compose(pauli2, qargs=[0, 1, 2]) self.assertEqual(value, target) with self.subTest(msg="compose 3-qubit qargs=[2, 1, 0]"): - target = PauliTable.from_labels(["ZYX", "YZI"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["ZYX", "YZI"]) value = pauli1.compose(pauli2, qargs=[2, 1, 0]) self.assertEqual(value, target) with self.subTest(msg="compose 3-qubit qargs=[1, 0, 2]"): - target = PauliTable.from_labels(["XZY", "IYZ"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["XZY", "IYZ"]) value = pauli1.compose(pauli2, qargs=[1, 0, 2]) self.assertEqual(value, target) def test_qargs_dot_3q(self): """Test 3-qubit dot method with qargs.""" - pauli1 = PauliTable.from_labels(["III", "XXX"]) - pauli2 = PauliTable("XYZ") + with self.assertWarns(DeprecationWarning): + pauli1 = PauliTable.from_labels(["III", "XXX"]) + pauli2 = PauliTable("XYZ") with self.subTest(msg="dot 3-qubit qargs=None"): - target = PauliTable.from_labels(["XYZ", "IZY"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["XYZ", "IZY"]) value = pauli1.dot(pauli2, qargs=[0, 1, 2]) self.assertEqual(value, target) with self.subTest(msg="dot 3-qubit qargs=[0, 1, 2]"): - target = PauliTable.from_labels(["XYZ", "IZY"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["XYZ", "IZY"]) value = pauli1.dot(pauli2, qargs=[0, 1, 2]) self.assertEqual(value, target) with self.subTest(msg="dot 3-qubit qargs=[2, 1, 0]"): - target = PauliTable.from_labels(["ZYX", "YZI"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["ZYX", "YZI"]) value = pauli1.dot(pauli2, qargs=[2, 1, 0]) self.assertEqual(value, target) with self.subTest(msg="dot 3-qubit qargs=[1, 0, 2]"): - target = PauliTable.from_labels(["XZY", "IYZ"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["XZY", "IYZ"]) value = pauli1.dot(pauli2, qargs=[1, 0, 2]) self.assertEqual(value, target) @@ -760,15 +877,17 @@ def test_sort(self): with self.subTest(msg="1 qubit standard order"): unsrt = ["X", "Z", "I", "Y", "X", "Z"] srt = ["I", "X", "X", "Y", "Z", "Z"] - target = PauliTable.from_labels(srt) - value = PauliTable.from_labels(unsrt).sort() + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(srt) + value = PauliTable.from_labels(unsrt).sort() self.assertEqual(target, value) with self.subTest(msg="1 qubit weight order"): unsrt = ["X", "Z", "I", "Y", "X", "Z"] srt = ["I", "X", "X", "Y", "Z", "Z"] - target = PauliTable.from_labels(srt) - value = PauliTable.from_labels(unsrt).sort(weight=True) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(srt) + value = PauliTable.from_labels(unsrt).sort(weight=True) self.assertEqual(target, value) with self.subTest(msg="2 qubit standard order"): @@ -794,8 +913,9 @@ def test_sort(self): ] unsrt = srt.copy() np.random.shuffle(unsrt) - target = PauliTable.from_labels(srt) - value = PauliTable.from_labels(unsrt).sort() + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(srt) + value = PauliTable.from_labels(unsrt).sort() self.assertEqual(target, value) with self.subTest(msg="2 qubit weight order"): @@ -824,8 +944,9 @@ def test_sort(self): ] unsrt = srt.copy() np.random.shuffle(unsrt) - target = PauliTable.from_labels(srt) - value = PauliTable.from_labels(unsrt).sort(weight=True) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(srt) + value = PauliTable.from_labels(unsrt).sort(weight=True) self.assertEqual(target, value) with self.subTest(msg="3 qubit standard order"): @@ -905,8 +1026,9 @@ def test_sort(self): ] unsrt = srt.copy() np.random.shuffle(unsrt) - target = PauliTable.from_labels(srt) - value = PauliTable.from_labels(unsrt).sort() + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(srt) + value = PauliTable.from_labels(unsrt).sort() self.assertEqual(target, value) with self.subTest(msg="3 qubit weight order"): @@ -981,8 +1103,9 @@ def test_sort(self): ] unsrt = srt.copy() np.random.shuffle(unsrt) - target = PauliTable.from_labels(srt) - value = PauliTable.from_labels(unsrt).sort(weight=True) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(srt) + value = PauliTable.from_labels(unsrt).sort(weight=True) self.assertEqual(target, value) def test_unique(self): @@ -990,70 +1113,84 @@ def test_unique(self): with self.subTest(msg="1 qubit"): labels = ["X", "Z", "X", "X", "I", "Y", "I", "X", "Z", "Z", "X", "I"] unique = ["X", "Z", "I", "Y"] - target = PauliTable.from_labels(unique) - value = PauliTable.from_labels(labels).unique() + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(unique) + value = PauliTable.from_labels(labels).unique() self.assertEqual(target, value) with self.subTest(msg="2 qubit"): labels = ["XX", "IX", "XX", "II", "IZ", "ZI", "YX", "YX", "ZZ", "IX", "XI"] unique = ["XX", "IX", "II", "IZ", "ZI", "YX", "ZZ", "XI"] - target = PauliTable.from_labels(unique) - value = PauliTable.from_labels(labels).unique() + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(unique) + value = PauliTable.from_labels(labels).unique() self.assertEqual(target, value) with self.subTest(msg="10 qubit"): labels = [10 * "X", 10 * "I", 10 * "X"] unique = [10 * "X", 10 * "I"] - target = PauliTable.from_labels(unique) - value = PauliTable.from_labels(labels).unique() + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(unique) + value = PauliTable.from_labels(labels).unique() self.assertEqual(target, value) def test_delete(self): """Test delete method.""" with self.subTest(msg="single row"): for j in range(1, 6): - pauli = PauliTable.from_labels([j * "X", j * "Y"]) - self.assertEqual(pauli.delete(0), PauliTable(j * "Y")) - self.assertEqual(pauli.delete(1), PauliTable(j * "X")) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels([j * "X", j * "Y"]) + self.assertEqual(pauli.delete(0), PauliTable(j * "Y")) + self.assertEqual(pauli.delete(1), PauliTable(j * "X")) with self.subTest(msg="multiple rows"): for j in range(1, 6): - pauli = PauliTable.from_labels([j * "X", j * "Y", j * "Z"]) - self.assertEqual(pauli.delete([0, 2]), PauliTable(j * "Y")) - self.assertEqual(pauli.delete([1, 2]), PauliTable(j * "X")) - self.assertEqual(pauli.delete([0, 1]), PauliTable(j * "Z")) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels([j * "X", j * "Y", j * "Z"]) + self.assertEqual(pauli.delete([0, 2]), PauliTable(j * "Y")) + self.assertEqual(pauli.delete([1, 2]), PauliTable(j * "X")) + self.assertEqual(pauli.delete([0, 1]), PauliTable(j * "Z")) with self.subTest(msg="single qubit"): - pauli = PauliTable.from_labels(["IIX", "IYI", "ZII"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["IIX", "IYI", "ZII"]) value = pauli.delete(0, qubit=True) - target = PauliTable.from_labels(["II", "IY", "ZI"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["II", "IY", "ZI"]) self.assertEqual(value, target) value = pauli.delete(1, qubit=True) - target = PauliTable.from_labels(["IX", "II", "ZI"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IX", "II", "ZI"]) self.assertEqual(value, target) value = pauli.delete(2, qubit=True) - target = PauliTable.from_labels(["IX", "YI", "II"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["IX", "YI", "II"]) self.assertEqual(value, target) with self.subTest(msg="multiple qubits"): - pauli = PauliTable.from_labels(["IIX", "IYI", "ZII"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["IIX", "IYI", "ZII"]) value = pauli.delete([0, 1], qubit=True) - target = PauliTable.from_labels(["I", "I", "Z"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["I", "I", "Z"]) self.assertEqual(value, target) value = pauli.delete([1, 2], qubit=True) - target = PauliTable.from_labels(["X", "I", "I"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["X", "I", "I"]) self.assertEqual(value, target) value = pauli.delete([0, 2], qubit=True) - target = PauliTable.from_labels(["I", "Y", "I"]) + with self.assertWarns(DeprecationWarning): + target = PauliTable.from_labels(["I", "Y", "I"]) self.assertEqual(value, target) def test_insert(self): """Test insert method.""" # Insert single row for j in range(1, 10): - pauli = PauliTable(j * "X") - target0 = PauliTable.from_labels([j * "I", j * "X"]) - target1 = PauliTable.from_labels([j * "X", j * "I"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable(j * "X") + target0 = PauliTable.from_labels([j * "I", j * "X"]) + target1 = PauliTable.from_labels([j * "X", j * "I"]) with self.subTest(msg=f"single row from str ({j})"): value0 = pauli.insert(0, j * "I") @@ -1062,21 +1199,26 @@ def test_insert(self): self.assertEqual(value1, target1) with self.subTest(msg=f"single row from PauliTable ({j})"): - value0 = pauli.insert(0, PauliTable(j * "I")) + with self.assertWarns(DeprecationWarning): + value0 = pauli.insert(0, PauliTable(j * "I")) self.assertEqual(value0, target0) - value1 = pauli.insert(1, PauliTable(j * "I")) + with self.assertWarns(DeprecationWarning): + value1 = pauli.insert(1, PauliTable(j * "I")) self.assertEqual(value1, target1) with self.subTest(msg=f"single row from array ({j})"): - value0 = pauli.insert(0, PauliTable(j * "I").array) + with self.assertWarns(DeprecationWarning): + value0 = pauli.insert(0, PauliTable(j * "I").array) self.assertEqual(value0, target0) - value1 = pauli.insert(1, PauliTable(j * "I").array) + with self.assertWarns(DeprecationWarning): + value1 = pauli.insert(1, PauliTable(j * "I").array) self.assertEqual(value1, target1) # Insert multiple rows for j in range(1, 10): - pauli = PauliTable(j * "X") - insert = PauliTable.from_labels([j * "I", j * "Y", j * "Z"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable(j * "X") + insert = PauliTable.from_labels([j * "I", j * "Y", j * "Z"]) target0 = insert + pauli target1 = pauli + insert @@ -1095,8 +1237,9 @@ def test_insert(self): # Insert single column pauli = PauliTable.from_labels(["X", "Y", "Z"]) for i in ["I", "X", "Y", "Z"]: - target0 = PauliTable.from_labels(["X" + i, "Y" + i, "Z" + i]) - target1 = PauliTable.from_labels([i + "X", i + "Y", i + "Z"]) + with self.assertWarns(DeprecationWarning): + target0 = PauliTable.from_labels(["X" + i, "Y" + i, "Z" + i]) + target1 = PauliTable.from_labels([i + "X", i + "Y", i + "Z"]) with self.subTest(msg="single-column single-val from str"): value = pauli.insert(0, i, qubit=True) @@ -1105,41 +1248,53 @@ def test_insert(self): self.assertEqual(value, target1) with self.subTest(msg="single-column single-val from PauliTable"): - value = pauli.insert(0, PauliTable(i), qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(0, PauliTable(i), qubit=True) self.assertEqual(value, target0) - value = pauli.insert(1, PauliTable(i), qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(1, PauliTable(i), qubit=True) self.assertEqual(value, target1) with self.subTest(msg="single-column single-val from array"): - value = pauli.insert(0, PauliTable(i).array, qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(0, PauliTable(i).array, qubit=True) self.assertEqual(value, target0) - value = pauli.insert(1, PauliTable(i).array, qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(1, PauliTable(i).array, qubit=True) self.assertEqual(value, target1) # Insert single column with multiple values - pauli = PauliTable.from_labels(["X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["X", "Y", "Z"]) for i in [("I", "X", "Y"), ("X", "Y", "Z"), ("Y", "Z", "I")]: - target0 = PauliTable.from_labels(["X" + i[0], "Y" + i[1], "Z" + i[2]]) - target1 = PauliTable.from_labels([i[0] + "X", i[1] + "Y", i[2] + "Z"]) + with self.assertWarns(DeprecationWarning): + target0 = PauliTable.from_labels(["X" + i[0], "Y" + i[1], "Z" + i[2]]) + target1 = PauliTable.from_labels([i[0] + "X", i[1] + "Y", i[2] + "Z"]) with self.subTest(msg="single-column multiple-vals from PauliTable"): - value = pauli.insert(0, PauliTable.from_labels(i), qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(0, PauliTable.from_labels(i), qubit=True) self.assertEqual(value, target0) - value = pauli.insert(1, PauliTable.from_labels(i), qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(1, PauliTable.from_labels(i), qubit=True) self.assertEqual(value, target1) with self.subTest(msg="single-column multiple-vals from array"): - value = pauli.insert(0, PauliTable.from_labels(i).array, qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(0, PauliTable.from_labels(i).array, qubit=True) self.assertEqual(value, target0) - value = pauli.insert(1, PauliTable.from_labels(i).array, qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(1, PauliTable.from_labels(i).array, qubit=True) self.assertEqual(value, target1) # Insert multiple columns from single - pauli = PauliTable.from_labels(["X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["X", "Y", "Z"]) for j in range(1, 5): for i in [j * "I", j * "X", j * "Y", j * "Z"]: - target0 = PauliTable.from_labels(["X" + i, "Y" + i, "Z" + i]) - target1 = PauliTable.from_labels([i + "X", i + "Y", i + "Z"]) + with self.assertWarns(DeprecationWarning): + target0 = PauliTable.from_labels(["X" + i, "Y" + i, "Z" + i]) + target1 = PauliTable.from_labels([i + "X", i + "Y", i + "Z"]) with self.subTest(msg="multiple-columns single-val from str"): value = pauli.insert(0, i, qubit=True) @@ -1148,44 +1303,55 @@ def test_insert(self): self.assertEqual(value, target1) with self.subTest(msg="multiple-columns single-val from PauliTable"): - value = pauli.insert(0, PauliTable(i), qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(0, PauliTable(i), qubit=True) self.assertEqual(value, target0) - value = pauli.insert(1, PauliTable(i), qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(1, PauliTable(i), qubit=True) self.assertEqual(value, target1) with self.subTest(msg="multiple-columns single-val from array"): - value = pauli.insert(0, PauliTable(i).array, qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(0, PauliTable(i).array, qubit=True) self.assertEqual(value, target0) - value = pauli.insert(1, PauliTable(i).array, qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(1, PauliTable(i).array, qubit=True) self.assertEqual(value, target1) # Insert multiple columns multiple row values - pauli = PauliTable.from_labels(["X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["X", "Y", "Z"]) for j in range(1, 5): for i in [ (j * "I", j * "X", j * "Y"), (j * "X", j * "Z", j * "Y"), (j * "Y", j * "Z", j * "I"), ]: - target0 = PauliTable.from_labels(["X" + i[0], "Y" + i[1], "Z" + i[2]]) - target1 = PauliTable.from_labels([i[0] + "X", i[1] + "Y", i[2] + "Z"]) + with self.assertWarns(DeprecationWarning): + target0 = PauliTable.from_labels(["X" + i[0], "Y" + i[1], "Z" + i[2]]) + target1 = PauliTable.from_labels([i[0] + "X", i[1] + "Y", i[2] + "Z"]) with self.subTest(msg="multiple-column multiple-vals from PauliTable"): - value = pauli.insert(0, PauliTable.from_labels(i), qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(0, PauliTable.from_labels(i), qubit=True) self.assertEqual(value, target0) - value = pauli.insert(1, PauliTable.from_labels(i), qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(1, PauliTable.from_labels(i), qubit=True) self.assertEqual(value, target1) with self.subTest(msg="multiple-column multiple-vals from array"): - value = pauli.insert(0, PauliTable.from_labels(i).array, qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(0, PauliTable.from_labels(i).array, qubit=True) self.assertEqual(value, target0) - value = pauli.insert(1, PauliTable.from_labels(i).array, qubit=True) + with self.assertWarns(DeprecationWarning): + value = pauli.insert(1, PauliTable.from_labels(i).array, qubit=True) self.assertEqual(value, target1) def test_commutes(self): """Test commutes method.""" # Single qubit Pauli - pauli = PauliTable.from_labels(["I", "X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["I", "X", "Y", "Z"]) with self.subTest(msg="commutes single-Pauli I"): value = list(pauli.commutes("I")) target = [True, True, True, True] @@ -1207,7 +1373,8 @@ def test_commutes(self): self.assertEqual(value, target) # 2-qubit Pauli - pauli = PauliTable.from_labels(["II", "IX", "YI", "XY", "ZZ"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["II", "IX", "YI", "XY", "ZZ"]) with self.subTest(msg="commutes single-Pauli II"): value = list(pauli.commutes("II")) target = [True, True, True, True, True] @@ -1251,7 +1418,8 @@ def test_commutes(self): def test_commutes_with_all(self): """Test commutes_with_all method.""" # 1-qubit - pauli = PauliTable.from_labels(["I", "X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["I", "X", "Y", "Z"]) with self.subTest(msg="commutes_with_all [I]"): value = list(pauli.commutes_with_all("I")) target = [0, 1, 2, 3] @@ -1273,40 +1441,47 @@ def test_commutes_with_all(self): self.assertEqual(value, target) # 2-qubit Pauli - pauli = PauliTable.from_labels(["II", "IX", "YI", "XY", "ZZ"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["II", "IX", "YI", "XY", "ZZ"]) with self.subTest(msg="commutes_with_all [IX, YI]"): - other = PauliTable.from_labels(["IX", "YI"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["IX", "YI"]) value = list(pauli.commutes_with_all(other)) target = [0, 1, 2] self.assertEqual(value, target) with self.subTest(msg="commutes_with_all [XY, ZZ]"): - other = PauliTable.from_labels(["XY", "ZZ"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["XY", "ZZ"]) value = list(pauli.commutes_with_all(other)) target = [0, 3, 4] self.assertEqual(value, target) with self.subTest(msg="commutes_with_all [YX, ZZ]"): - other = PauliTable.from_labels(["YX", "ZZ"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["YX", "ZZ"]) value = list(pauli.commutes_with_all(other)) target = [0, 3, 4] self.assertEqual(value, target) with self.subTest(msg="commutes_with_all [XY, YX]"): - other = PauliTable.from_labels(["XY", "YX"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["XY", "YX"]) value = list(pauli.commutes_with_all(other)) target = [0, 3, 4] self.assertEqual(value, target) with self.subTest(msg="commutes_with_all [XY, IX]"): - other = PauliTable.from_labels(["XY", "IX"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["XY", "IX"]) value = list(pauli.commutes_with_all(other)) target = [0] self.assertEqual(value, target) with self.subTest(msg="commutes_with_all [YX, IX]"): - other = PauliTable.from_labels(["YX", "IX"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["YX", "IX"]) value = list(pauli.commutes_with_all(other)) target = [0, 1, 2] self.assertEqual(value, target) @@ -1314,7 +1489,8 @@ def test_commutes_with_all(self): def test_anticommutes_with_all(self): """Test anticommutes_with_all method.""" # 1-qubit - pauli = PauliTable.from_labels(["I", "X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["I", "X", "Y", "Z"]) with self.subTest(msg="anticommutes_with_all [I]"): value = list(pauli.anticommutes_with_all("I")) target = [] @@ -1336,40 +1512,47 @@ def test_anticommutes_with_all(self): self.assertEqual(value, target) # 2-qubit Pauli - pauli = PauliTable.from_labels(["II", "IX", "YI", "XY", "ZZ"]) + with self.assertWarns(DeprecationWarning): + pauli = PauliTable.from_labels(["II", "IX", "YI", "XY", "ZZ"]) with self.subTest(msg="anticommutes_with_all [IX, YI]"): - other = PauliTable.from_labels(["IX", "YI"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["IX", "YI"]) value = list(pauli.anticommutes_with_all(other)) target = [3, 4] self.assertEqual(value, target) with self.subTest(msg="anticommutes_with_all [XY, ZZ]"): - other = PauliTable.from_labels(["XY", "ZZ"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["XY", "ZZ"]) value = list(pauli.anticommutes_with_all(other)) target = [1, 2] self.assertEqual(value, target) with self.subTest(msg="anticommutes_with_all [YX, ZZ]"): - other = PauliTable.from_labels(["YX", "ZZ"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["YX", "ZZ"]) value = list(pauli.anticommutes_with_all(other)) target = [] self.assertEqual(value, target) with self.subTest(msg="anticommutes_with_all [XY, YX]"): - other = PauliTable.from_labels(["XY", "YX"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["XY", "YX"]) value = list(pauli.anticommutes_with_all(other)) target = [] self.assertEqual(value, target) with self.subTest(msg="anticommutes_with_all [XY, IX]"): - other = PauliTable.from_labels(["XY", "IX"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["XY", "IX"]) value = list(pauli.anticommutes_with_all(other)) target = [] self.assertEqual(value, target) with self.subTest(msg="anticommutes_with_all [YX, IX]"): - other = PauliTable.from_labels(["YX", "IX"]) + with self.assertWarns(DeprecationWarning): + other = PauliTable.from_labels(["YX", "IX"]) value = list(pauli.anticommutes_with_all(other)) target = [] self.assertEqual(value, target) diff --git a/test/python/quantum_info/operators/symplectic/test_stabilizer_table.py b/test/python/quantum_info/operators/symplectic/test_stabilizer_table.py index 05d2d9f59a9b..a77ff14276d7 100644 --- a/test/python/quantum_info/operators/symplectic/test_stabilizer_table.py +++ b/test/python/quantum_info/operators/symplectic/test_stabilizer_table.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2020. +# (C) Copyright IBM 2017, 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 @@ -51,30 +51,35 @@ def test_array_init(self): with self.subTest(msg="bool array"): target = np.array([[False, False], [True, True]]) - value = StabilizerTable(target)._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable(target)._array self.assertTrue(np.all(value == target)) with self.subTest(msg="bool array no copy"): target = np.array([[False, True], [True, True]]) - value = StabilizerTable(target)._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable(target)._array value[0, 0] = not value[0, 0] self.assertTrue(np.all(value == target)) with self.subTest(msg="bool array raises"): array = np.array([[False, False, False], [True, True, True]]) - self.assertRaises(QiskitError, StabilizerTable, array) + with self.assertWarns(DeprecationWarning): + self.assertRaises(QiskitError, StabilizerTable, array) def test_vector_init(self): """Test vector initialization.""" with self.subTest(msg="bool vector"): target = np.array([False, False, False, False]) - value = StabilizerTable(target)._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable(target)._array self.assertTrue(np.all(value == target)) with self.subTest(msg="bool vector no copy"): target = np.array([False, True, True, False]) - value = StabilizerTable(target)._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable(target)._array value[0, 0] = not value[0, 0] self.assertTrue(np.all(value == target)) @@ -82,42 +87,50 @@ def test_string_init(self): """Test string initialization.""" with self.subTest(msg='str init "I"'): - value = StabilizerTable("I")._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable("I")._array target = np.array([[False, False]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "X"'): - value = StabilizerTable("X")._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable("X")._array target = np.array([[True, False]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "Y"'): - value = StabilizerTable("Y")._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable("Y")._array target = np.array([[True, True]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "Z"'): - value = StabilizerTable("Z")._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable("Z")._array target = np.array([[False, True]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "IX"'): - value = StabilizerTable("IX")._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable("IX")._array target = np.array([[True, False, False, False]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "XI"'): - value = StabilizerTable("XI")._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable("XI")._array target = np.array([[False, True, False, False]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "YZ"'): - value = StabilizerTable("YZ")._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable("YZ")._array target = np.array([[False, True, True, True]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) with self.subTest(msg='str init "XIZ"'): - value = StabilizerTable("XIZ")._array + with self.assertWarns(DeprecationWarning): + value = StabilizerTable("XIZ")._array target = np.array([[False, False, True, True, False, False]], dtype=bool) self.assertTrue(np.all(np.array(value == target))) @@ -125,15 +138,17 @@ def test_table_init(self): """Test StabilizerTable initialization.""" with self.subTest(msg="StabilizerTable"): - target = StabilizerTable.from_labels(["XI", "IX", "IZ"]) - value = StabilizerTable(target) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["XI", "IX", "IZ"]) + value = StabilizerTable(target) + self.assertEqual(value, target) with self.subTest(msg="StabilizerTable no copy"): - target = StabilizerTable.from_labels(["XI", "IX", "IZ"]) - value = StabilizerTable(target) - value[0] = "II" - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["XI", "IX", "IZ"]) + value = StabilizerTable(target) + value[0] = "II" + self.assertEqual(value, target) class TestStabilizerTableProperties(QiskitTestCase): @@ -143,14 +158,16 @@ def test_array_property(self): """Test array property""" with self.subTest(msg="array"): - stab = StabilizerTable("II") + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable("II") array = np.zeros([2, 4], dtype=bool) self.assertTrue(np.all(stab.array == array)) with self.subTest(msg="set array"): def set_array(): - stab = StabilizerTable("XXX") + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable("XXX") stab.array = np.eye(4) return stab @@ -160,20 +177,24 @@ def test_x_property(self): """Test X property""" with self.subTest(msg="X"): - stab = StabilizerTable.from_labels(["XI", "IZ", "YY"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["XI", "IZ", "YY"]) array = np.array([[False, True], [False, False], [True, True]], dtype=bool) self.assertTrue(np.all(stab.X == array)) with self.subTest(msg="set X"): - stab = StabilizerTable.from_labels(["XI", "IZ"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["XI", "IZ"]) val = np.array([[False, False], [True, True]], dtype=bool) stab.X = val - self.assertEqual(stab, StabilizerTable.from_labels(["II", "XY"])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(stab, StabilizerTable.from_labels(["II", "XY"])) with self.subTest(msg="set X raises"): def set_x(): - stab = StabilizerTable.from_labels(["XI", "IZ"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["XI", "IZ"]) val = np.array([[False, False, False], [True, True, True]], dtype=bool) stab.X = val return stab @@ -183,20 +204,24 @@ def set_x(): def test_z_property(self): """Test Z property""" with self.subTest(msg="Z"): - stab = StabilizerTable.from_labels(["XI", "IZ", "YY"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["XI", "IZ", "YY"]) array = np.array([[False, False], [True, False], [True, True]], dtype=bool) self.assertTrue(np.all(stab.Z == array)) with self.subTest(msg="set Z"): - stab = StabilizerTable.from_labels(["XI", "IZ"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["XI", "IZ"]) val = np.array([[False, False], [True, True]], dtype=bool) stab.Z = val - self.assertEqual(stab, StabilizerTable.from_labels(["XI", "ZZ"])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(stab, StabilizerTable.from_labels(["XI", "ZZ"])) with self.subTest(msg="set Z raises"): def set_z(): - stab = StabilizerTable.from_labels(["XI", "IZ"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["XI", "IZ"]) val = np.array([[False, False, False], [True, True, True]], dtype=bool) stab.Z = val return stab @@ -206,7 +231,8 @@ def set_z(): def test_shape_property(self): """Test shape property""" shape = (3, 8) - stab = StabilizerTable(np.zeros(shape)) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(np.zeros(shape)) self.assertEqual(stab.shape, shape) def test_size_property(self): @@ -214,7 +240,8 @@ def test_size_property(self): with self.subTest(msg="size"): for j in range(1, 10): shape = (j, 8) - stab = StabilizerTable(np.zeros(shape)) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(np.zeros(shape)) self.assertEqual(stab.size, j) def test_num_qubits_property(self): @@ -222,7 +249,8 @@ def test_num_qubits_property(self): with self.subTest(msg="num_qubits"): for j in range(1, 10): shape = (5, 2 * j) - stab = StabilizerTable(np.zeros(shape)) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(np.zeros(shape)) self.assertEqual(stab.num_qubits, j) def test_phase_property(self): @@ -230,20 +258,23 @@ def test_phase_property(self): with self.subTest(msg="phase"): phase = np.array([False, True, True, False]) array = np.eye(4, dtype=bool) - stab = StabilizerTable(array, phase) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(array, phase) self.assertTrue(np.all(stab.phase == phase)) with self.subTest(msg="set phase"): phase = np.array([False, True, True, False]) array = np.eye(4, dtype=bool) - stab = StabilizerTable(array) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(array) stab.phase = phase self.assertTrue(np.all(stab.phase == phase)) with self.subTest(msg="set phase raises"): phase = np.array([False, True, False]) array = np.eye(4, dtype=bool) - stab = StabilizerTable(array) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(array) def set_phase_raise(): """Raise exception""" @@ -256,15 +287,17 @@ def test_pauli_property(self): with self.subTest(msg="pauli"): phase = np.array([False, True, True, False]) array = np.eye(4, dtype=bool) - stab = StabilizerTable(array, phase) - pauli = PauliTable(array) - self.assertEqual(stab.pauli, pauli) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(array, phase) + pauli = PauliTable(array) + self.assertEqual(stab.pauli, pauli) with self.subTest(msg="set pauli"): phase = np.array([False, True, True, False]) array = np.zeros((4, 4), dtype=bool) - stab = StabilizerTable(array, phase) - pauli = PauliTable(np.eye(4, dtype=bool)) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(array, phase) + pauli = PauliTable(np.eye(4, dtype=bool)) stab.pauli = pauli self.assertTrue(np.all(stab.array == pauli.array)) self.assertTrue(np.all(stab.phase == phase)) @@ -272,8 +305,9 @@ def test_pauli_property(self): with self.subTest(msg="set pauli"): phase = np.array([False, True, True, False]) array = np.zeros((4, 4), dtype=bool) - stab = StabilizerTable(array, phase) - pauli = PauliTable(np.eye(4, dtype=bool)[1:]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(array, phase) + pauli = PauliTable(np.eye(4, dtype=bool)[1:]) def set_pauli_raise(): """Raise exception""" @@ -283,79 +317,93 @@ def set_pauli_raise(): def test_eq(self): """Test __eq__ method.""" - stab1 = StabilizerTable.from_labels(["II", "XI"]) - stab2 = StabilizerTable.from_labels(["XI", "II"]) - self.assertEqual(stab1, stab1) - self.assertNotEqual(stab1, stab2) + with self.assertWarns(DeprecationWarning): + stab1 = StabilizerTable.from_labels(["II", "XI"]) + stab2 = StabilizerTable.from_labels(["XI", "II"]) + self.assertEqual(stab1, stab1) + self.assertNotEqual(stab1, stab2) def test_len_methods(self): """Test __len__ method.""" for j in range(1, 10): labels = j * ["XX"] - stab = StabilizerTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(labels) self.assertEqual(len(stab), j) def test_add_methods(self): """Test __add__ method.""" labels1 = ["+XXI", "-IXX"] labels2 = ["+XXI", "-ZZI", "+ZYZ"] - stab1 = StabilizerTable.from_labels(labels1) - stab2 = StabilizerTable.from_labels(labels2) - target = StabilizerTable.from_labels(labels1 + labels2) - self.assertEqual(target, stab1 + stab2) + with self.assertWarns(DeprecationWarning): + stab1 = StabilizerTable.from_labels(labels1) + stab2 = StabilizerTable.from_labels(labels2) + target = StabilizerTable.from_labels(labels1 + labels2) + self.assertEqual(target, stab1 + stab2) def test_add_qargs(self): """Test add method with qargs.""" - stab1 = StabilizerTable.from_labels(["+IIII", "-YYYY"]) - stab2 = StabilizerTable.from_labels(["-XY", "+YZ"]) + with self.assertWarns(DeprecationWarning): + stab1 = StabilizerTable.from_labels(["+IIII", "-YYYY"]) + stab2 = StabilizerTable.from_labels(["-XY", "+YZ"]) with self.subTest(msg="qargs=[0, 1]"): - target = StabilizerTable.from_labels(["+IIII", "-YYYY", "-IIXY", "+IIYZ"]) - self.assertEqual(stab1 + stab2([0, 1]), target) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["+IIII", "-YYYY", "-IIXY", "+IIYZ"]) + self.assertEqual(stab1 + stab2([0, 1]), target) with self.subTest(msg="qargs=[0, 3]"): - target = StabilizerTable.from_labels(["+IIII", "-YYYY", "-XIIY", "+YIIZ"]) - self.assertEqual(stab1 + stab2([0, 3]), target) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["+IIII", "-YYYY", "-XIIY", "+YIIZ"]) + self.assertEqual(stab1 + stab2([0, 3]), target) with self.subTest(msg="qargs=[2, 1]"): - target = StabilizerTable.from_labels(["+IIII", "-YYYY", "-IYXI", "+IZYI"]) - self.assertEqual(stab1 + stab2([2, 1]), target) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["+IIII", "-YYYY", "-IYXI", "+IZYI"]) + self.assertEqual(stab1 + stab2([2, 1]), target) with self.subTest(msg="qargs=[3, 1]"): - target = StabilizerTable.from_labels(["+IIII", "-YYYY", "-YIXI", "+ZIYI"]) - self.assertEqual(stab1 + stab2([3, 1]), target) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["+IIII", "-YYYY", "-YIXI", "+ZIYI"]) + self.assertEqual(stab1 + stab2([3, 1]), target) def test_getitem_methods(self): """Test __getitem__ method.""" with self.subTest(msg="__getitem__ single"): labels = ["+XI", "-IY"] - stab = StabilizerTable.from_labels(labels) - self.assertEqual(stab[0], StabilizerTable(labels[0])) - self.assertEqual(stab[1], StabilizerTable(labels[1])) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(labels) + self.assertEqual(stab[0], StabilizerTable(labels[0])) + self.assertEqual(stab[1], StabilizerTable(labels[1])) with self.subTest(msg="__getitem__ array"): labels = np.array(["+XI", "-IY", "+IZ", "-XY", "+ZX"]) - stab = StabilizerTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(labels) inds = [0, 3] - self.assertEqual(stab[inds], StabilizerTable.from_labels(labels[inds])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(stab[inds], StabilizerTable.from_labels(labels[inds])) inds = np.array([4, 1]) - self.assertEqual(stab[inds], StabilizerTable.from_labels(labels[inds])) + with self.assertWarns(DeprecationWarning): + self.assertEqual(stab[inds], StabilizerTable.from_labels(labels[inds])) with self.subTest(msg="__getitem__ slice"): labels = np.array(["+XI", "-IY", "+IZ", "-XY", "+ZX"]) - stab = StabilizerTable.from_labels(labels) - self.assertEqual(stab[:], stab) - self.assertEqual(stab[1:3], StabilizerTable.from_labels(labels[1:3])) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(labels) + self.assertEqual(stab[:], stab) + self.assertEqual(stab[1:3], StabilizerTable.from_labels(labels[1:3])) def test_setitem_methods(self): """Test __setitem__ method.""" with self.subTest(msg="__setitem__ single"): labels = ["+XI", "IY"] - stab = StabilizerTable.from_labels(["+XI", "IY"]) - stab[0] = "+II" - self.assertEqual(stab[0], StabilizerTable("+II")) - stab[1] = "-XX" - self.assertEqual(stab[1], StabilizerTable("-XX")) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["+XI", "IY"]) + stab[0] = "+II" + self.assertEqual(stab[0], StabilizerTable("+II")) + stab[1] = "-XX" + self.assertEqual(stab[1], StabilizerTable("-XX")) def raises_single(): # Wrong size Pauli @@ -365,26 +413,31 @@ def raises_single(): with self.subTest(msg="__setitem__ array"): labels = np.array(["+XI", "-IY", "+IZ"]) - stab = StabilizerTable.from_labels(labels) - target = StabilizerTable.from_labels(["+II", "-ZZ"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(labels) + target = StabilizerTable.from_labels(["+II", "-ZZ"]) inds = [2, 0] stab[inds] = target - self.assertEqual(stab[inds], target) + with self.assertWarns(DeprecationWarning): + self.assertEqual(stab[inds], target) def raises_array(): - stab[inds] = StabilizerTable.from_labels(["+YY", "-ZZ", "+XX"]) + with self.assertWarns(DeprecationWarning): + stab[inds] = StabilizerTable.from_labels(["+YY", "-ZZ", "+XX"]) self.assertRaises(Exception, raises_array) with self.subTest(msg="__setitem__ slice"): labels = np.array(5 * ["+III"]) - stab = StabilizerTable.from_labels(labels) - target = StabilizerTable.from_labels(5 * ["-XXX"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(labels) + target = StabilizerTable.from_labels(5 * ["-XXX"]) stab[:] = target - self.assertEqual(stab[:], target) - target = StabilizerTable.from_labels(2 * ["+ZZZ"]) - stab[1:3] = target - self.assertEqual(stab[1:3], target) + with self.assertWarns(DeprecationWarning): + self.assertEqual(stab[:], target) + target = StabilizerTable.from_labels(2 * ["+ZZZ"]) + stab[1:3] = target + self.assertEqual(stab[1:3], target) class TestStabilizerTableLabels(QiskitTestCase): @@ -397,9 +450,10 @@ def test_from_labels_1q(self): 3 * [np.array([[False, False], [True, False], [True, True], [False, True]], dtype=bool)] ) phase = np.array(8 * [False] + 4 * [True], dtype=bool) - target = StabilizerTable(array, phase) - value = StabilizerTable.from_labels(labels) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable(array, phase) + value = StabilizerTable.from_labels(labels) + self.assertEqual(target, value) def test_from_labels_2q(self): """Test 2-qubit from_labels method.""" @@ -409,9 +463,10 @@ def test_from_labels_2q(self): dtype=bool, ) phase = np.array([False, True, False]) - target = StabilizerTable(array, phase) - value = StabilizerTable.from_labels(labels) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable(array, phase) + value = StabilizerTable.from_labels(labels) + self.assertEqual(target, value) def test_from_labels_5q(self): """Test 5-qubit from_labels method.""" @@ -421,9 +476,10 @@ def test_from_labels_5q(self): dtype=bool, ) phase = np.array([False, True, False, False]) - target = StabilizerTable(array, phase) - value = StabilizerTable.from_labels(labels) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable(array, phase) + value = StabilizerTable.from_labels(labels) + self.assertEqual(target, value) def test_to_labels_1q(self): """Test 1-qubit to_labels method.""" @@ -431,7 +487,8 @@ def test_to_labels_1q(self): 2 * [np.array([[False, False], [True, False], [True, True], [False, True]], dtype=bool)] ) phase = np.array(4 * [False] + 4 * [True], dtype=bool) - value = StabilizerTable(array, phase).to_labels() + with self.assertWarns(DeprecationWarning): + value = StabilizerTable(array, phase).to_labels() target = ["+I", "+X", "+Y", "+Z", "-I", "-X", "-Y", "-Z"] self.assertEqual(value, target) @@ -441,21 +498,24 @@ def test_to_labels_1q_array(self): 2 * [np.array([[False, False], [True, False], [True, True], [False, True]], dtype=bool)] ) phase = np.array(4 * [False] + 4 * [True], dtype=bool) - value = StabilizerTable(array, phase).to_labels(array=True) + with self.assertWarns(DeprecationWarning): + value = StabilizerTable(array, phase).to_labels(array=True) target = np.array(["+I", "+X", "+Y", "+Z", "-I", "-X", "-Y", "-Z"]) self.assertTrue(np.all(value == target)) def test_labels_round_trip(self): """Test from_labels and to_labels round trip.""" target = ["+III", "-IXZ", "-XYI", "+ZZZ"] - value = StabilizerTable.from_labels(target).to_labels() + with self.assertWarns(DeprecationWarning): + value = StabilizerTable.from_labels(target).to_labels() self.assertEqual(value, target) def test_labels_round_trip_array(self): """Test from_labels and to_labels round trip w/ array=True.""" labels = ["+III", "-IXZ", "-XYI", "+ZZZ"] target = np.array(labels) - value = StabilizerTable.from_labels(labels).to_labels(array=True) + with self.assertWarns(DeprecationWarning): + value = StabilizerTable.from_labels(labels).to_labels(array=True) self.assertTrue(np.all(value == target)) @@ -466,7 +526,8 @@ def test_to_matrix_1q(self): """Test 1-qubit to_matrix method.""" labels = ["+I", "+X", "+Y", "+Z", "-I", "-X", "-Y", "-Z"] targets = [stab_mat(i) for i in labels] - values = StabilizerTable.from_labels(labels).to_matrix() + with self.assertWarns(DeprecationWarning): + values = StabilizerTable.from_labels(labels).to_matrix() self.assertTrue(isinstance(values, list)) for target, value in zip(targets, values): self.assertTrue(np.all(value == target)) @@ -475,7 +536,8 @@ def test_to_matrix_1q_array(self): """Test 1-qubit to_matrix method w/ array=True.""" labels = ["+I", "+X", "+Y", "+Z", "-I", "-X", "-Y", "-Z"] target = np.array([stab_mat(i) for i in labels]) - value = StabilizerTable.from_labels(labels).to_matrix(array=True) + with self.assertWarns(DeprecationWarning): + value = StabilizerTable.from_labels(labels).to_matrix(array=True) self.assertTrue(isinstance(value, np.ndarray)) self.assertTrue(np.all(value == target)) @@ -483,7 +545,8 @@ def test_to_matrix_1q_sparse(self): """Test 1-qubit to_matrix method w/ sparse=True.""" labels = ["+I", "+X", "+Y", "+Z", "-I", "-X", "-Y", "-Z"] targets = [stab_mat(i) for i in labels] - values = StabilizerTable.from_labels(labels).to_matrix(sparse=True) + with self.assertWarns(DeprecationWarning): + values = StabilizerTable.from_labels(labels).to_matrix(sparse=True) for mat, targ in zip(values, targets): self.assertTrue(isinstance(mat, csr_matrix)) self.assertTrue(np.all(targ == mat.toarray())) @@ -492,7 +555,8 @@ def test_to_matrix_2q(self): """Test 2-qubit to_matrix method.""" labels = ["+IX", "-YI", "-II", "+ZZ"] targets = [stab_mat(i) for i in labels] - values = StabilizerTable.from_labels(labels).to_matrix() + with self.assertWarns(DeprecationWarning): + values = StabilizerTable.from_labels(labels).to_matrix() self.assertTrue(isinstance(values, list)) for target, value in zip(targets, values): self.assertTrue(np.all(value == target)) @@ -501,7 +565,8 @@ def test_to_matrix_2q_array(self): """Test 2-qubit to_matrix method w/ array=True.""" labels = ["-ZZ", "-XY", "+YX", "-IZ"] target = np.array([stab_mat(i) for i in labels]) - value = StabilizerTable.from_labels(labels).to_matrix(array=True) + with self.assertWarns(DeprecationWarning): + value = StabilizerTable.from_labels(labels).to_matrix(array=True) self.assertTrue(isinstance(value, np.ndarray)) self.assertTrue(np.all(value == target)) @@ -509,7 +574,8 @@ def test_to_matrix_2q_sparse(self): """Test 2-qubit to_matrix method w/ sparse=True.""" labels = ["+IX", "+II", "-ZY", "-YZ"] targets = [stab_mat(i) for i in labels] - values = StabilizerTable.from_labels(labels).to_matrix(sparse=True) + with self.assertWarns(DeprecationWarning): + values = StabilizerTable.from_labels(labels).to_matrix(sparse=True) for mat, targ in zip(values, targets): self.assertTrue(isinstance(mat, csr_matrix)) self.assertTrue(np.all(targ == mat.toarray())) @@ -518,7 +584,8 @@ def test_to_matrix_5q(self): """Test 5-qubit to_matrix method.""" labels = ["IXIXI", "YZIXI", "IIXYZ"] targets = [stab_mat(i) for i in labels] - values = StabilizerTable.from_labels(labels).to_matrix() + with self.assertWarns(DeprecationWarning): + values = StabilizerTable.from_labels(labels).to_matrix() self.assertTrue(isinstance(values, list)) for target, value in zip(targets, values): self.assertTrue(np.all(value == target)) @@ -527,7 +594,8 @@ def test_to_matrix_5q_sparse(self): """Test 5-qubit to_matrix method w/ sparse=True.""" labels = ["-XXXYY", "IXIZY", "-ZYXIX", "+ZXIYZ"] targets = [stab_mat(i) for i in labels] - values = StabilizerTable.from_labels(labels).to_matrix(sparse=True) + with self.assertWarns(DeprecationWarning): + values = StabilizerTable.from_labels(labels).to_matrix(sparse=True) for mat, targ in zip(values, targets): self.assertTrue(isinstance(mat, csr_matrix)) self.assertTrue(np.all(targ == mat.toarray())) @@ -541,16 +609,18 @@ def test_sort(self): with self.subTest(msg="1 qubit"): unsrt = ["X", "-Z", "I", "Y", "-X", "Z"] srt = ["I", "X", "-X", "Y", "-Z", "Z"] - target = StabilizerTable.from_labels(srt) - value = StabilizerTable.from_labels(unsrt).sort() - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(srt) + value = StabilizerTable.from_labels(unsrt).sort() + self.assertEqual(target, value) with self.subTest(msg="1 qubit weight order"): unsrt = ["X", "-Z", "I", "Y", "-X", "Z"] srt = ["I", "X", "-X", "Y", "-Z", "Z"] - target = StabilizerTable.from_labels(srt) - value = StabilizerTable.from_labels(unsrt).sort(weight=True) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(srt) + value = StabilizerTable.from_labels(unsrt).sort(weight=True) + self.assertEqual(target, value) with self.subTest(msg="2 qubit standard order"): srt_p = [ @@ -580,16 +650,18 @@ def test_sort(self): # Sort with + cases all first in shuffled list srt = [val for pair in zip(srt_p, srt_m) for val in pair] unsrt = unsrt_p + unsrt_m - target = StabilizerTable.from_labels(srt) - value = StabilizerTable.from_labels(unsrt).sort() - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(srt) + value = StabilizerTable.from_labels(unsrt).sort() + self.assertEqual(target, value) # Sort with - cases all first in shuffled list srt = [val for pair in zip(srt_m, srt_p) for val in pair] unsrt = unsrt_m + unsrt_p - target = StabilizerTable.from_labels(srt) - value = StabilizerTable.from_labels(unsrt).sort() - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(srt) + value = StabilizerTable.from_labels(unsrt).sort() + self.assertEqual(target, value) with self.subTest(msg="2 qubit weight order"): srt_p = [ @@ -620,25 +692,28 @@ def test_sort(self): # Sort with + cases all first in shuffled list srt = [val for pair in zip(srt_p, srt_m) for val in pair] unsrt = unsrt_p + unsrt_m - target = StabilizerTable.from_labels(srt) - value = StabilizerTable.from_labels(unsrt).sort(weight=True) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(srt) + value = StabilizerTable.from_labels(unsrt).sort(weight=True) + self.assertEqual(target, value) # Sort with - cases all first in shuffled list srt = [val for pair in zip(srt_m, srt_p) for val in pair] unsrt = unsrt_m + unsrt_p - target = StabilizerTable.from_labels(srt) - value = StabilizerTable.from_labels(unsrt).sort(weight=True) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(srt) + value = StabilizerTable.from_labels(unsrt).sort(weight=True) + self.assertEqual(target, value) def test_unique(self): """Test unique method.""" with self.subTest(msg="1 qubit"): labels = ["X", "Z", "-I", "-X", "X", "I", "Y", "-I", "-X", "-Z", "Z", "X", "I"] unique = ["X", "Z", "-I", "-X", "I", "Y", "-Z"] - target = StabilizerTable.from_labels(unique) - value = StabilizerTable.from_labels(labels).unique() - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(unique) + value = StabilizerTable.from_labels(labels).unique() + self.assertEqual(target, value) with self.subTest(msg="2 qubit"): labels = [ @@ -657,55 +732,61 @@ def test_unique(self): "XI", ] unique = ["XX", "IX", "-XX", "-IZ", "II", "IZ", "ZI", "YX", "ZZ", "XI"] - target = StabilizerTable.from_labels(unique) - value = StabilizerTable.from_labels(labels).unique() - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(unique) + value = StabilizerTable.from_labels(labels).unique() + self.assertEqual(target, value) with self.subTest(msg="10 qubit"): labels = [10 * "X", "-" + 10 * "X", "-" + 10 * "X", 10 * "I", 10 * "X"] unique = [10 * "X", "-" + 10 * "X", 10 * "I"] - target = StabilizerTable.from_labels(unique) - value = StabilizerTable.from_labels(labels).unique() - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(unique) + value = StabilizerTable.from_labels(labels).unique() + self.assertEqual(target, value) def test_delete(self): """Test delete method.""" with self.subTest(msg="single row"): for j in range(1, 6): - stab = StabilizerTable.from_labels([j * "X", "-" + j * "Y"]) - self.assertEqual(stab.delete(0), StabilizerTable("-" + j * "Y")) - self.assertEqual(stab.delete(1), StabilizerTable(j * "X")) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels([j * "X", "-" + j * "Y"]) + self.assertEqual(stab.delete(0), StabilizerTable("-" + j * "Y")) + self.assertEqual(stab.delete(1), StabilizerTable(j * "X")) with self.subTest(msg="multiple rows"): for j in range(1, 6): - stab = StabilizerTable.from_labels([j * "X", "-" + j * "Y", j * "Z"]) - self.assertEqual(stab.delete([0, 2]), StabilizerTable("-" + j * "Y")) - self.assertEqual(stab.delete([1, 2]), StabilizerTable(j * "X")) - self.assertEqual(stab.delete([0, 1]), StabilizerTable(j * "Z")) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels([j * "X", "-" + j * "Y", j * "Z"]) + self.assertEqual(stab.delete([0, 2]), StabilizerTable("-" + j * "Y")) + self.assertEqual(stab.delete([1, 2]), StabilizerTable(j * "X")) + self.assertEqual(stab.delete([0, 1]), StabilizerTable(j * "Z")) with self.subTest(msg="single qubit"): - stab = StabilizerTable.from_labels(["IIX", "IYI", "ZII"]) - value = stab.delete(0, qubit=True) - target = StabilizerTable.from_labels(["II", "IY", "ZI"]) - self.assertEqual(value, target) - value = stab.delete(1, qubit=True) - target = StabilizerTable.from_labels(["IX", "II", "ZI"]) - self.assertEqual(value, target) - value = stab.delete(2, qubit=True) - target = StabilizerTable.from_labels(["IX", "YI", "II"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["IIX", "IYI", "ZII"]) + value = stab.delete(0, qubit=True) + target = StabilizerTable.from_labels(["II", "IY", "ZI"]) + self.assertEqual(value, target) + value = stab.delete(1, qubit=True) + target = StabilizerTable.from_labels(["IX", "II", "ZI"]) + self.assertEqual(value, target) + value = stab.delete(2, qubit=True) + target = StabilizerTable.from_labels(["IX", "YI", "II"]) + self.assertEqual(value, target) with self.subTest(msg="multiple qubits"): - stab = StabilizerTable.from_labels(["IIX", "IYI", "ZII"]) - value = stab.delete([0, 1], qubit=True) - target = StabilizerTable.from_labels(["I", "I", "Z"]) - self.assertEqual(value, target) - value = stab.delete([1, 2], qubit=True) - target = StabilizerTable.from_labels(["X", "I", "I"]) - self.assertEqual(value, target) - value = stab.delete([0, 2], qubit=True) - target = StabilizerTable.from_labels(["I", "Y", "I"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["IIX", "IYI", "ZII"]) + value = stab.delete([0, 1], qubit=True) + target = StabilizerTable.from_labels(["I", "I", "Z"]) + self.assertEqual(value, target) + value = stab.delete([1, 2], qubit=True) + target = StabilizerTable.from_labels(["X", "I", "I"]) + self.assertEqual(value, target) + value = stab.delete([0, 2], qubit=True) + target = StabilizerTable.from_labels(["I", "Y", "I"]) + self.assertEqual(value, target) def test_insert(self): """Test insert method.""" @@ -713,137 +794,167 @@ def test_insert(self): for j in range(1, 10): l_px = j * "X" l_mi = "-" + j * "I" - stab = StabilizerTable(l_px) - target0 = StabilizerTable.from_labels([l_mi, l_px]) - target1 = StabilizerTable.from_labels([l_px, l_mi]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(l_px) + target0 = StabilizerTable.from_labels([l_mi, l_px]) + target1 = StabilizerTable.from_labels([l_px, l_mi]) with self.subTest(msg=f"single row from str ({j})"): - value0 = stab.insert(0, l_mi) - self.assertEqual(value0, target0) - value1 = stab.insert(1, l_mi) - self.assertEqual(value1, target1) + with self.assertWarns(DeprecationWarning): + value0 = stab.insert(0, l_mi) + self.assertEqual(value0, target0) + value1 = stab.insert(1, l_mi) + self.assertEqual(value1, target1) with self.subTest(msg=f"single row from StabilizerTable ({j})"): - value0 = stab.insert(0, StabilizerTable(l_mi)) - self.assertEqual(value0, target0) - value1 = stab.insert(1, StabilizerTable(l_mi)) - self.assertEqual(value1, target1) + with self.assertWarns(DeprecationWarning): + value0 = stab.insert(0, StabilizerTable(l_mi)) + self.assertEqual(value0, target0) + value1 = stab.insert(1, StabilizerTable(l_mi)) + self.assertEqual(value1, target1) # Insert multiple rows for j in range(1, 10): - stab = StabilizerTable(j * "X") - insert = StabilizerTable.from_labels(["-" + j * "I", j * "Y", "-" + j * "Z"]) - target0 = insert + stab - target1 = stab + insert + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable(j * "X") + insert = StabilizerTable.from_labels(["-" + j * "I", j * "Y", "-" + j * "Z"]) + target0 = insert + stab + target1 = stab + insert with self.subTest(msg=f"multiple-rows from StabilizerTable ({j})"): - value0 = stab.insert(0, insert) - self.assertEqual(value0, target0) - value1 = stab.insert(1, insert) - self.assertEqual(value1, target1) + with self.assertWarns(DeprecationWarning): + value0 = stab.insert(0, insert) + self.assertEqual(value0, target0) + value1 = stab.insert(1, insert) + self.assertEqual(value1, target1) # Insert single column - stab = StabilizerTable.from_labels(["X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["X", "Y", "Z"]) for sgn in ["+", "-"]: for i in ["I", "X", "Y", "Z"]: - target0 = StabilizerTable.from_labels([sgn + "X" + i, sgn + "Y" + i, sgn + "Z" + i]) - target1 = StabilizerTable.from_labels([sgn + i + "X", sgn + i + "Y", sgn + i + "Z"]) + with self.assertWarns(DeprecationWarning): + target0 = StabilizerTable.from_labels( + [sgn + "X" + i, sgn + "Y" + i, sgn + "Z" + i] + ) + target1 = StabilizerTable.from_labels( + [sgn + i + "X", sgn + i + "Y", sgn + i + "Z"] + ) with self.subTest(msg=f"single-column single-val from str {sgn + i}"): - value = stab.insert(0, sgn + i, qubit=True) - self.assertEqual(value, target0) - value = stab.insert(1, sgn + i, qubit=True) - self.assertEqual(value, target1) + with self.assertWarns(DeprecationWarning): + value = stab.insert(0, sgn + i, qubit=True) + self.assertEqual(value, target0) + value = stab.insert(1, sgn + i, qubit=True) + self.assertEqual(value, target1) with self.subTest(msg=f"single-column single-val from StabilizerTable {sgn + i}"): - value = stab.insert(0, StabilizerTable(sgn + i), qubit=True) - self.assertEqual(value, target0) - value = stab.insert(1, StabilizerTable(sgn + i), qubit=True) - self.assertEqual(value, target1) + with self.assertWarns(DeprecationWarning): + value = stab.insert(0, StabilizerTable(sgn + i), qubit=True) + self.assertEqual(value, target0) + value = stab.insert(1, StabilizerTable(sgn + i), qubit=True) + self.assertEqual(value, target1) # Insert single column with multiple values - stab = StabilizerTable.from_labels(["X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["X", "Y", "Z"]) for i in [("I", "X", "Y"), ("X", "Y", "Z"), ("Y", "Z", "I")]: - target0 = StabilizerTable.from_labels(["X" + i[0], "Y" + i[1], "Z" + i[2]]) - target1 = StabilizerTable.from_labels([i[0] + "X", i[1] + "Y", i[2] + "Z"]) + with self.assertWarns(DeprecationWarning): + target0 = StabilizerTable.from_labels(["X" + i[0], "Y" + i[1], "Z" + i[2]]) + target1 = StabilizerTable.from_labels([i[0] + "X", i[1] + "Y", i[2] + "Z"]) with self.subTest(msg="single-column multiple-vals from StabilizerTable"): - value = stab.insert(0, StabilizerTable.from_labels(i), qubit=True) - self.assertEqual(value, target0) - value = stab.insert(1, StabilizerTable.from_labels(i), qubit=True) - self.assertEqual(value, target1) + with self.assertWarns(DeprecationWarning): + value = stab.insert(0, StabilizerTable.from_labels(i), qubit=True) + self.assertEqual(value, target0) + value = stab.insert(1, StabilizerTable.from_labels(i), qubit=True) + self.assertEqual(value, target1) with self.subTest(msg="single-column multiple-vals from array"): - value = stab.insert(0, StabilizerTable.from_labels(i).array, qubit=True) - self.assertEqual(value, target0) - value = stab.insert(1, StabilizerTable.from_labels(i).array, qubit=True) - self.assertEqual(value, target1) + with self.assertWarns(DeprecationWarning): + value = stab.insert(0, StabilizerTable.from_labels(i).array, qubit=True) + self.assertEqual(value, target0) + value = stab.insert(1, StabilizerTable.from_labels(i).array, qubit=True) + self.assertEqual(value, target1) # Insert multiple columns from single - stab = StabilizerTable.from_labels(["X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["X", "Y", "Z"]) for j in range(1, 5): for i in [j * "I", j * "X", j * "Y", j * "Z"]: - target0 = StabilizerTable.from_labels(["X" + i, "Y" + i, "Z" + i]) - target1 = StabilizerTable.from_labels([i + "X", i + "Y", i + "Z"]) + with self.assertWarns(DeprecationWarning): + target0 = StabilizerTable.from_labels(["X" + i, "Y" + i, "Z" + i]) + target1 = StabilizerTable.from_labels([i + "X", i + "Y", i + "Z"]) with self.subTest(msg="multiple-columns single-val from str"): - value = stab.insert(0, i, qubit=True) - self.assertEqual(value, target0) - value = stab.insert(1, i, qubit=True) - self.assertEqual(value, target1) + with self.assertWarns(DeprecationWarning): + value = stab.insert(0, i, qubit=True) + self.assertEqual(value, target0) + value = stab.insert(1, i, qubit=True) + self.assertEqual(value, target1) with self.subTest(msg="multiple-columns single-val from StabilizerTable"): - value = stab.insert(0, StabilizerTable(i), qubit=True) - self.assertEqual(value, target0) - value = stab.insert(1, StabilizerTable(i), qubit=True) - self.assertEqual(value, target1) + with self.assertWarns(DeprecationWarning): + value = stab.insert(0, StabilizerTable(i), qubit=True) + self.assertEqual(value, target0) + value = stab.insert(1, StabilizerTable(i), qubit=True) + self.assertEqual(value, target1) with self.subTest(msg="multiple-columns single-val from array"): - value = stab.insert(0, StabilizerTable(i).array, qubit=True) - self.assertEqual(value, target0) - value = stab.insert(1, StabilizerTable(i).array, qubit=True) - self.assertEqual(value, target1) + with self.assertWarns(DeprecationWarning): + value = stab.insert(0, StabilizerTable(i).array, qubit=True) + self.assertEqual(value, target0) + value = stab.insert(1, StabilizerTable(i).array, qubit=True) + self.assertEqual(value, target1) # Insert multiple columns multiple row values - stab = StabilizerTable.from_labels(["X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["X", "Y", "Z"]) for j in range(1, 5): for i in [ (j * "I", j * "X", j * "Y"), (j * "X", j * "Z", j * "Y"), (j * "Y", j * "Z", j * "I"), ]: - target0 = StabilizerTable.from_labels(["X" + i[0], "Y" + i[1], "Z" + i[2]]) - target1 = StabilizerTable.from_labels([i[0] + "X", i[1] + "Y", i[2] + "Z"]) + with self.assertWarns(DeprecationWarning): + target0 = StabilizerTable.from_labels(["X" + i[0], "Y" + i[1], "Z" + i[2]]) + target1 = StabilizerTable.from_labels([i[0] + "X", i[1] + "Y", i[2] + "Z"]) with self.subTest(msg="multiple-column multiple-vals from StabilizerTable"): - value = stab.insert(0, StabilizerTable.from_labels(i), qubit=True) - self.assertEqual(value, target0) - value = stab.insert(1, StabilizerTable.from_labels(i), qubit=True) - self.assertEqual(value, target1) + with self.assertWarns(DeprecationWarning): + value = stab.insert(0, StabilizerTable.from_labels(i), qubit=True) + self.assertEqual(value, target0) + value = stab.insert(1, StabilizerTable.from_labels(i), qubit=True) + self.assertEqual(value, target1) with self.subTest(msg="multiple-column multiple-vals from array"): - value = stab.insert(0, StabilizerTable.from_labels(i).array, qubit=True) - self.assertEqual(value, target0) - value = stab.insert(1, StabilizerTable.from_labels(i).array, qubit=True) - self.assertEqual(value, target1) + with self.assertWarns(DeprecationWarning): + value = stab.insert(0, StabilizerTable.from_labels(i).array, qubit=True) + self.assertEqual(value, target0) + value = stab.insert(1, StabilizerTable.from_labels(i).array, qubit=True) + self.assertEqual(value, target1) def test_iteration(self): """Test iteration methods.""" labels = ["+III", "+IXI", "-IYY", "+YIZ", "-ZIZ", "+XYZ", "-III"] - stab = StabilizerTable.from_labels(labels) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(labels) with self.subTest(msg="enumerate"): - for idx, i in enumerate(stab): - self.assertEqual(i, StabilizerTable(labels[idx])) + with self.assertWarns(DeprecationWarning): + for idx, i in enumerate(stab): + self.assertEqual(i, StabilizerTable(labels[idx])) with self.subTest(msg="iter"): - for idx, i in enumerate(iter(stab)): - self.assertEqual(i, StabilizerTable(labels[idx])) + with self.assertWarns(DeprecationWarning): + for idx, i in enumerate(iter(stab)): + self.assertEqual(i, StabilizerTable(labels[idx])) with self.subTest(msg="zip"): - for label, i in zip(labels, stab): - self.assertEqual(i, StabilizerTable(label)) + with self.assertWarns(DeprecationWarning): + for label, i in zip(labels, stab): + self.assertEqual(i, StabilizerTable(label)) with self.subTest(msg="label_iter"): for idx, i in enumerate(stab.label_iter()): @@ -862,199 +973,240 @@ def test_tensor(self): """Test tensor method.""" labels1 = ["-XX", "YY"] labels2 = ["III", "-ZZZ"] - stab1 = StabilizerTable.from_labels(labels1) - stab2 = StabilizerTable.from_labels(labels2) + with self.assertWarns(DeprecationWarning): + stab1 = StabilizerTable.from_labels(labels1) + stab2 = StabilizerTable.from_labels(labels2) - target = StabilizerTable.from_labels(["-XXIII", "XXZZZ", "YYIII", "-YYZZZ"]) - value = stab1.tensor(stab2) - self.assertEqual(value, target) + target = StabilizerTable.from_labels(["-XXIII", "XXZZZ", "YYIII", "-YYZZZ"]) + value = stab1.tensor(stab2) + self.assertEqual(value, target) def test_expand(self): """Test expand method.""" labels1 = ["-XX", "YY"] labels2 = ["III", "-ZZZ"] - stab1 = StabilizerTable.from_labels(labels1) - stab2 = StabilizerTable.from_labels(labels2) + with self.assertWarns(DeprecationWarning): + stab1 = StabilizerTable.from_labels(labels1) + stab2 = StabilizerTable.from_labels(labels2) - target = StabilizerTable.from_labels(["-IIIXX", "IIIYY", "ZZZXX", "-ZZZYY"]) - value = stab1.expand(stab2) - self.assertEqual(value, target) + target = StabilizerTable.from_labels(["-IIIXX", "IIIYY", "ZZZXX", "-ZZZYY"]) + value = stab1.expand(stab2) + self.assertEqual(value, target) def test_compose(self): """Test compose and dot methods.""" # Test single qubit Pauli dot products - stab = StabilizerTable.from_labels(["I", "X", "Y", "Z"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["I", "X", "Y", "Z"]) # Test single qubit Pauli dot products - stab = StabilizerTable.from_labels(["I", "X", "Y", "Z", "-I", "-X", "-Y", "-Z"]) + with self.assertWarns(DeprecationWarning): + stab = StabilizerTable.from_labels(["I", "X", "Y", "Z", "-I", "-X", "-Y", "-Z"]) with self.subTest(msg="dot single I"): - value = stab.compose("I") - target = StabilizerTable.from_labels(["I", "X", "Y", "Z", "-I", "-X", "-Y", "-Z"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.compose("I") + target = StabilizerTable.from_labels(["I", "X", "Y", "Z", "-I", "-X", "-Y", "-Z"]) + self.assertEqual(target, value) with self.subTest(msg="dot single -I"): - value = stab.compose("-I") - target = StabilizerTable.from_labels(["-I", "-X", "-Y", "-Z", "I", "X", "Y", "Z"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.compose("-I") + target = StabilizerTable.from_labels(["-I", "-X", "-Y", "-Z", "I", "X", "Y", "Z"]) + self.assertEqual(target, value) with self.subTest(msg="dot single I"): - value = stab.dot("I") - target = StabilizerTable.from_labels(["I", "X", "Y", "Z", "-I", "-X", "-Y", "-Z"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.dot("I") + target = StabilizerTable.from_labels(["I", "X", "Y", "Z", "-I", "-X", "-Y", "-Z"]) + self.assertEqual(target, value) with self.subTest(msg="dot single -I"): - value = stab.dot("-I") - target = StabilizerTable.from_labels(["-I", "-X", "-Y", "-Z", "I", "X", "Y", "Z"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.dot("-I") + target = StabilizerTable.from_labels(["-I", "-X", "-Y", "-Z", "I", "X", "Y", "Z"]) + self.assertEqual(target, value) with self.subTest(msg="compose single X"): - value = stab.compose("X") - target = StabilizerTable.from_labels(["X", "I", "-Z", "Y", "-X", "-I", "Z", "-Y"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.compose("X") + target = StabilizerTable.from_labels(["X", "I", "-Z", "Y", "-X", "-I", "Z", "-Y"]) + self.assertEqual(target, value) with self.subTest(msg="compose single -X"): - value = stab.compose("-X") - target = StabilizerTable.from_labels(["-X", "-I", "Z", "-Y", "X", "I", "-Z", "Y"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.compose("-X") + target = StabilizerTable.from_labels(["-X", "-I", "Z", "-Y", "X", "I", "-Z", "Y"]) + self.assertEqual(target, value) with self.subTest(msg="dot single X"): - value = stab.dot("X") - target = StabilizerTable.from_labels(["X", "I", "Z", "-Y", "-X", "-I", "-Z", "Y"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.dot("X") + target = StabilizerTable.from_labels(["X", "I", "Z", "-Y", "-X", "-I", "-Z", "Y"]) + self.assertEqual(target, value) with self.subTest(msg="dot single -X"): - value = stab.dot("-X") - target = StabilizerTable.from_labels(["-X", "-I", "-Z", "Y", "X", "I", "Z", "-Y"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.dot("-X") + target = StabilizerTable.from_labels(["-X", "-I", "-Z", "Y", "X", "I", "Z", "-Y"]) + self.assertEqual(target, value) with self.subTest(msg="compose single Y"): - value = stab.compose("Y") - target = StabilizerTable.from_labels(["Y", "Z", "-I", "-X", "-Y", "-Z", "I", "X"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.compose("Y") + target = StabilizerTable.from_labels(["Y", "Z", "-I", "-X", "-Y", "-Z", "I", "X"]) + self.assertEqual(target, value) with self.subTest(msg="compose single -Y"): - value = stab.compose("-Y") - target = StabilizerTable.from_labels(["-Y", "-Z", "I", "X", "Y", "Z", "-I", "-X"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.compose("-Y") + target = StabilizerTable.from_labels(["-Y", "-Z", "I", "X", "Y", "Z", "-I", "-X"]) + self.assertEqual(target, value) with self.subTest(msg="dot single Y"): - value = stab.dot("Y") - target = StabilizerTable.from_labels(["Y", "-Z", "-I", "X", "-Y", "Z", "I", "-X"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.dot("Y") + target = StabilizerTable.from_labels(["Y", "-Z", "-I", "X", "-Y", "Z", "I", "-X"]) + self.assertEqual(target, value) with self.subTest(msg="dot single -Y"): - value = stab.dot("-Y") - target = StabilizerTable.from_labels(["-Y", "Z", "I", "-X", "Y", "-Z", "-I", "X"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.dot("-Y") + target = StabilizerTable.from_labels(["-Y", "Z", "I", "-X", "Y", "-Z", "-I", "X"]) + self.assertEqual(target, value) with self.subTest(msg="compose single Z"): - value = stab.compose("Z") - target = StabilizerTable.from_labels(["Z", "-Y", "X", "I", "-Z", "Y", "-X", "-I"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.compose("Z") + target = StabilizerTable.from_labels(["Z", "-Y", "X", "I", "-Z", "Y", "-X", "-I"]) + self.assertEqual(target, value) with self.subTest(msg="compose single -Z"): - value = stab.compose("-Z") - target = StabilizerTable.from_labels(["-Z", "Y", "-X", "-I", "Z", "-Y", "X", "I"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.compose("-Z") + target = StabilizerTable.from_labels(["-Z", "Y", "-X", "-I", "Z", "-Y", "X", "I"]) + self.assertEqual(target, value) with self.subTest(msg="dot single Z"): - value = stab.dot("Z") - target = StabilizerTable.from_labels(["Z", "Y", "-X", "I", "-Z", "-Y", "X", "-I"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.dot("Z") + target = StabilizerTable.from_labels(["Z", "Y", "-X", "I", "-Z", "-Y", "X", "-I"]) + self.assertEqual(target, value) with self.subTest(msg="dot single -Z"): - value = stab.dot("-Z") - target = StabilizerTable.from_labels(["-Z", "-Y", "X", "-I", "Z", "Y", "-X", "I"]) - self.assertEqual(target, value) + with self.assertWarns(DeprecationWarning): + value = stab.dot("-Z") + target = StabilizerTable.from_labels(["-Z", "-Y", "X", "-I", "Z", "Y", "-X", "I"]) + self.assertEqual(target, value) def test_compose_qargs(self): """Test compose and dot methods with qargs.""" # Dot product with qargs - stab1 = StabilizerTable.from_labels(["III", "-XXX", "YYY", "-ZZZ"]) + with self.assertWarns(DeprecationWarning): + stab1 = StabilizerTable.from_labels(["III", "-XXX", "YYY", "-ZZZ"]) # 1-qubit qargs - stab2 = StabilizerTable("-Z") + with self.assertWarns(DeprecationWarning): + stab2 = StabilizerTable("-Z") with self.subTest(msg="dot 1-qubit qargs=[0]"): - target = StabilizerTable.from_labels(["-IIZ", "XXY", "YYX", "ZZI"]) - value = stab1.dot(stab2, qargs=[0]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["-IIZ", "XXY", "YYX", "ZZI"]) + value = stab1.dot(stab2, qargs=[0]) + self.assertEqual(value, target) with self.subTest(msg="compose 1-qubit qargs=[0]"): - target = StabilizerTable.from_labels(["-IIZ", "-XXY", "-YYX", "ZZI"]) - value = stab1.compose(stab2, qargs=[0]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["-IIZ", "-XXY", "-YYX", "ZZI"]) + value = stab1.compose(stab2, qargs=[0]) + self.assertEqual(value, target) with self.subTest(msg="dot 1-qubit qargs=[1]"): - target = StabilizerTable.from_labels(["-IZI", "XYX", "YXY", "ZIZ"]) - value = stab1.dot(stab2, qargs=[1]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["-IZI", "XYX", "YXY", "ZIZ"]) + value = stab1.dot(stab2, qargs=[1]) + self.assertEqual(value, target) with self.subTest(msg="compose 1-qubit qargs=[1]"): - value = stab1.compose(stab2, qargs=[1]) - target = StabilizerTable.from_labels(["-IZI", "-XYX", "-YXY", "ZIZ"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.compose(stab2, qargs=[1]) + target = StabilizerTable.from_labels(["-IZI", "-XYX", "-YXY", "ZIZ"]) + self.assertEqual(value, target) - target = StabilizerTable.from_labels(["ZII", "YXX"]) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["ZII", "YXX"]) with self.subTest(msg="dot 1-qubit qargs=[2]"): - value = stab1.dot(stab2, qargs=[2]) - target = StabilizerTable.from_labels(["-ZII", "YXX", "XYY", "IZZ"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.dot(stab2, qargs=[2]) + target = StabilizerTable.from_labels(["-ZII", "YXX", "XYY", "IZZ"]) + self.assertEqual(value, target) with self.subTest(msg="compose 1-qubit qargs=[2]"): - value = stab1.compose(stab2, qargs=[2]) - target = StabilizerTable.from_labels(["-ZII", "-YXX", "-XYY", "IZZ"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.compose(stab2, qargs=[2]) + target = StabilizerTable.from_labels(["-ZII", "-YXX", "-XYY", "IZZ"]) + self.assertEqual(value, target) # 2-qubit qargs - stab2 = StabilizerTable("-ZY") + with self.assertWarns(DeprecationWarning): + stab2 = StabilizerTable("-ZY") with self.subTest(msg="dot 2-qubit qargs=[0, 1]"): - value = stab1.dot(stab2, qargs=[0, 1]) - target = StabilizerTable.from_labels(["-IZY", "-XYZ", "-YXI", "ZIX"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.dot(stab2, qargs=[0, 1]) + target = StabilizerTable.from_labels(["-IZY", "-XYZ", "-YXI", "ZIX"]) + self.assertEqual(value, target) with self.subTest(msg="compose 2-qubit qargs=[0, 1]"): - value = stab1.compose(stab2, qargs=[0, 1]) - target = StabilizerTable.from_labels(["-IZY", "-XYZ", "YXI", "-ZIX"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.compose(stab2, qargs=[0, 1]) + target = StabilizerTable.from_labels(["-IZY", "-XYZ", "YXI", "-ZIX"]) + self.assertEqual(value, target) - target = StabilizerTable.from_labels(["YIZ", "ZXY"]) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["YIZ", "ZXY"]) with self.subTest(msg="dot 2-qubit qargs=[2, 0]"): - value = stab1.dot(stab2, qargs=[2, 0]) - target = StabilizerTable.from_labels(["-YIZ", "-ZXY", "-IYX", "XZI"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.dot(stab2, qargs=[2, 0]) + target = StabilizerTable.from_labels(["-YIZ", "-ZXY", "-IYX", "XZI"]) + self.assertEqual(value, target) with self.subTest(msg="compose 2-qubit qargs=[2, 0]"): - value = stab1.compose(stab2, qargs=[2, 0]) - target = StabilizerTable.from_labels(["-YIZ", "-ZXY", "IYX", "-XZI"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.compose(stab2, qargs=[2, 0]) + target = StabilizerTable.from_labels(["-YIZ", "-ZXY", "IYX", "-XZI"]) + self.assertEqual(value, target) # 3-qubit qargs - stab2 = StabilizerTable("-XYZ") + with self.assertWarns(DeprecationWarning): + stab2 = StabilizerTable("-XYZ") - target = StabilizerTable.from_labels(["XYZ", "IZY"]) + with self.assertWarns(DeprecationWarning): + target = StabilizerTable.from_labels(["XYZ", "IZY"]) with self.subTest(msg="dot 3-qubit qargs=None"): - value = stab1.dot(stab2, qargs=[0, 1, 2]) - target = StabilizerTable.from_labels(["-XYZ", "-IZY", "-ZIX", "-YXI"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.dot(stab2, qargs=[0, 1, 2]) + target = StabilizerTable.from_labels(["-XYZ", "-IZY", "-ZIX", "-YXI"]) + self.assertEqual(value, target) with self.subTest(msg="dot 3-qubit qargs=[0, 1, 2]"): - value = stab1.dot(stab2, qargs=[0, 1, 2]) - target = StabilizerTable.from_labels(["-XYZ", "-IZY", "-ZIX", "-YXI"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.dot(stab2, qargs=[0, 1, 2]) + target = StabilizerTable.from_labels(["-XYZ", "-IZY", "-ZIX", "-YXI"]) + self.assertEqual(value, target) with self.subTest(msg="dot 3-qubit qargs=[2, 1, 0]"): - value = stab1.dot(stab2, qargs=[2, 1, 0]) - target = StabilizerTable.from_labels(["-ZYX", "-YZI", "-XIZ", "-IXY"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.dot(stab2, qargs=[2, 1, 0]) + target = StabilizerTable.from_labels(["-ZYX", "-YZI", "-XIZ", "-IXY"]) + self.assertEqual(value, target) with self.subTest(msg="compose 3-qubit qargs=[2, 1, 0]"): - value = stab1.compose(stab2, qargs=[2, 1, 0]) - target = StabilizerTable.from_labels(["-ZYX", "-YZI", "-XIZ", "-IXY"]) - self.assertEqual(value, target) + with self.assertWarns(DeprecationWarning): + value = stab1.compose(stab2, qargs=[2, 1, 0]) + target = StabilizerTable.from_labels(["-ZYX", "-YZI", "-XIZ", "-IXY"]) + self.assertEqual(value, target) if __name__ == "__main__": diff --git a/test/python/quantum_info/operators/test_random.py b/test/python/quantum_info/operators/test_random.py index 1b9c21f5119f..2837711a020f 100644 --- a/test/python/quantum_info/operators/test_random.py +++ b/test/python/quantum_info/operators/test_random.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2020. +# (C) Copyright IBM 2017, 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 @@ -203,12 +203,13 @@ def test_not_global_seed(self): @ddt class TestPauliTwoDesignTable(QiskitTestCase): - """Testing random_pauli_table function.""" + """DEPRECATED: Testing random_pauli_table function.""" @combine(num_qubits=[1, 2, 3, 4, 5, 10, 50, 100, 200, 250], size=[1, 10, 100]) def test_valid(self, num_qubits, size): """Test random_pauli_table {num_qubits}-qubits, size {size}.""" - value = random_pauli_table(num_qubits, size=size) + with self.assertWarns(DeprecationWarning): + value = random_pauli_table(num_qubits, size=size) with self.subTest(msg="Test type"): self.assertIsInstance(value, PauliTable) with self.subTest(msg="Test num_qubits"): @@ -219,17 +220,20 @@ def test_valid(self, num_qubits, size): def test_fixed_seed(self): """Test fixing seed fixes output""" seed = 1532 - value1 = random_pauli_table(10, size=10, seed=seed) - value2 = random_pauli_table(10, size=10, seed=seed) + with self.assertWarns(DeprecationWarning): + value1 = random_pauli_table(10, size=10, seed=seed) + value2 = random_pauli_table(10, size=10, seed=seed) self.assertEqual(value1, value2) def test_not_global_seed(self): """Test fixing random_hermitian seed is locally scoped.""" seed = 314159 test_cases = 100 - random_pauli_table(10, size=10, seed=seed) + with self.assertWarns(DeprecationWarning): + random_pauli_table(10, size=10, seed=seed) rng_before = np.random.randint(1000, size=test_cases) - random_pauli_table(10, seed=seed) + with self.assertWarns(DeprecationWarning): + random_pauli_table(10, seed=seed) rng_after = np.random.randint(1000, size=test_cases) self.assertFalse(np.all(rng_before == rng_after)) @@ -302,7 +306,7 @@ def test_fixed_seed(self): with self.assertWarns(DeprecationWarning): value1 = random_stabilizer_table(10, size=10, seed=seed) value2 = random_stabilizer_table(10, size=10, seed=seed) - self.assertEqual(value1, value2) + self.assertEqual(value1, value2) def test_not_global_seed(self): """Test fixing random_hermitian seed is locally scoped.""" From f2518cbd2f33dc73b79b6f7661a808f1785825f5 Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Thu, 13 Apr 2023 19:11:32 +0900 Subject: [PATCH 015/172] Generic PrimitiveJob and BaseSampler/BaseEstimator (#9920) * Generic PrimitiveJob * Generic BaseSampler/BaseEstimator * remove type hint in _run --- qiskit/primitives/backend_estimator.py | 4 ++-- qiskit/primitives/backend_sampler.py | 4 ++-- qiskit/primitives/base/base_estimator.py | 9 ++++++--- qiskit/primitives/base/base_sampler.py | 9 ++++++--- qiskit/primitives/estimator.py | 4 ++-- qiskit/primitives/primitive_job.py | 9 +++++++-- qiskit/primitives/sampler.py | 4 ++-- 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index 35d6cd2eaced..329805bc319e 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -80,7 +80,7 @@ def _prepare_counts(results: list[Result]): return counts -class BackendEstimator(BaseEstimator): +class BackendEstimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): """Evaluates expectation value using Pauli rotation gates. The :class:`~.BackendEstimator` class is a generic implementation of the @@ -250,7 +250,7 @@ def _run( observables: tuple[BaseOperator | PauliSumOp, ...], parameter_values: tuple[tuple[float, ...], ...], **run_options, - ) -> PrimitiveJob: + ): circuit_indices = [] for circuit in circuits: index = self._circuit_ids.get(_circuit_key(circuit)) diff --git a/qiskit/primitives/backend_sampler.py b/qiskit/primitives/backend_sampler.py index c5d66ebd11cd..6d7f16173980 100644 --- a/qiskit/primitives/backend_sampler.py +++ b/qiskit/primitives/backend_sampler.py @@ -30,7 +30,7 @@ from .utils import _circuit_key -class BackendSampler(BaseSampler): +class BackendSampler(BaseSampler[PrimitiveJob[SamplerResult]]): """A :class:`~.BaseSampler` implementation that provides an interface for leveraging the sampler interface from any backend. @@ -192,7 +192,7 @@ def _run( circuits: tuple[QuantumCircuit, ...], parameter_values: tuple[tuple[float, ...], ...], **run_options, - ) -> PrimitiveJob: + ): circuit_indices = [] for circuit in circuits: index = self._circuit_ids.get(_circuit_key(circuit)) diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index 1cac61331b86..f091bc0d335c 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -83,6 +83,7 @@ from abc import abstractmethod from collections.abc import Sequence from copy import copy +from typing import Generic, TypeVar from qiskit.circuit import QuantumCircuit from qiskit.circuit.parametertable import ParameterView @@ -94,8 +95,10 @@ from ..utils import init_observable from .base_primitive import BasePrimitive +T = TypeVar("T", bound=Job) -class BaseEstimator(BasePrimitive): + +class BaseEstimator(BasePrimitive, Generic[T]): """Estimator base class. Base class for Estimator that estimates expectation values of quantum circuits and observables. @@ -126,7 +129,7 @@ def run( observables: Sequence[BaseOperator | PauliSumOp | str] | BaseOperator | PauliSumOp | str, parameter_values: Sequence[Sequence[float]] | Sequence[float] | float | None = None, **run_options, - ) -> Job: + ) -> T: """Run the job of the estimation of expectation value(s). ``circuits``, ``observables``, and ``parameter_values`` should have the same @@ -193,7 +196,7 @@ def _run( observables: tuple[SparsePauliOp, ...], parameter_values: tuple[tuple[float, ...], ...], **run_options, - ) -> Job: + ) -> T: raise NotImplementedError("The subclass of BaseEstimator must implment `_run` method.") @staticmethod diff --git a/qiskit/primitives/base/base_sampler.py b/qiskit/primitives/base/base_sampler.py index 769343477ffe..6a78c0767fd7 100644 --- a/qiskit/primitives/base/base_sampler.py +++ b/qiskit/primitives/base/base_sampler.py @@ -78,6 +78,7 @@ from abc import abstractmethod from collections.abc import Sequence from copy import copy +from typing import Generic, TypeVar from qiskit.circuit import QuantumCircuit from qiskit.circuit.parametertable import ParameterView @@ -85,8 +86,10 @@ from .base_primitive import BasePrimitive +T = TypeVar("T", bound=Job) -class BaseSampler(BasePrimitive): + +class BaseSampler(BasePrimitive, Generic[T]): """Sampler base class Base class of Sampler that calculates quasi-probabilities of bitstrings from quantum circuits. @@ -112,7 +115,7 @@ def run( circuits: QuantumCircuit | Sequence[QuantumCircuit], parameter_values: Sequence[float] | Sequence[Sequence[float]] | None = None, **run_options, - ) -> Job: + ) -> T: """Run the job of the sampling of bitstrings. Args: @@ -153,7 +156,7 @@ def _run( circuits: tuple[QuantumCircuit, ...], parameter_values: tuple[tuple[float, ...], ...], **run_options, - ) -> Job: + ) -> T: raise NotImplementedError("The subclass of BaseSampler must implment `_run` method.") # TODO: validate measurement gates are present diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index ba251ce294d7..7333e44332c9 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -36,7 +36,7 @@ ) -class Estimator(BaseEstimator): +class Estimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): """ Reference implementation of :class:`BaseEstimator`. @@ -127,7 +127,7 @@ def _run( observables: tuple[BaseOperator | PauliSumOp, ...], parameter_values: tuple[tuple[float, ...], ...], **run_options, - ) -> PrimitiveJob: + ): circuit_indices = [] for circuit in circuits: key = _circuit_key(circuit) diff --git a/qiskit/primitives/primitive_job.py b/qiskit/primitives/primitive_job.py index f8d295b662b5..95cc9629e0cd 100644 --- a/qiskit/primitives/primitive_job.py +++ b/qiskit/primitives/primitive_job.py @@ -15,11 +15,16 @@ import uuid from concurrent.futures import ThreadPoolExecutor +from typing import Generic, TypeVar from qiskit.providers import JobError, JobStatus, JobV1 +from .base.base_result import BasePrimitiveResult -class PrimitiveJob(JobV1): +T = TypeVar("T", bound=BasePrimitiveResult) + + +class PrimitiveJob(JobV1, Generic[T]): """ PrimitiveJob class for the reference implemetations of Primitives. """ @@ -44,7 +49,7 @@ def submit(self): future = executor.submit(self._function, *self._args, **self._kwargs) self._future = future - def result(self): + def result(self) -> T: """Return the results of the job.""" self._check_submitted() return self._future.result() diff --git a/qiskit/primitives/sampler.py b/qiskit/primitives/sampler.py index 534f1605afe3..82c637b5d2ba 100644 --- a/qiskit/primitives/sampler.py +++ b/qiskit/primitives/sampler.py @@ -35,7 +35,7 @@ ) -class Sampler(BaseSampler): +class Sampler(BaseSampler[PrimitiveJob[SamplerResult]]): """ Sampler class. @@ -119,7 +119,7 @@ def _run( circuits: tuple[QuantumCircuit, ...], parameter_values: tuple[tuple[float, ...], ...], **run_options, - ) -> PrimitiveJob: + ): circuit_indices = [] for circuit in circuits: key = _circuit_key(circuit) From 8fadfc7774fdd219e2fad14b26eaa18d2cca103c Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Thu, 13 Apr 2023 16:13:07 +0300 Subject: [PATCH 016/172] Bug fix in template optimization, and minor cleanup (#9541) * replacing computation of transitive predecessors and successors by rustworkx versions * bug fix: adding missing add_predecessors computation * adding test and formatting * different seeds * reno * fix docstrings and comments * pylint: changing import order * adding an extra test * release notes * adding a simple testcase which would have also caught the bug --- qiskit/dagcircuit/dagdependency.py | 78 ++----------- .../template_substitution.py | 1 + .../fix-template-opt-bd3c40382e9a993b.yaml | 6 + .../transpiler/test_template_matching.py | 104 +++++++++++++++++- 4 files changed, 117 insertions(+), 72 deletions(-) create mode 100644 releasenotes/notes/fix-template-opt-bd3c40382e9a993b.yaml diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py index b7071748d2fa..d1ad623bdc6d 100644 --- a/qiskit/dagcircuit/dagdependency.py +++ b/qiskit/dagcircuit/dagdependency.py @@ -35,7 +35,7 @@ # should investigate the possibility of using rx.descendants() instead of caching). # - We should rethink the API of DAGDependency: # Currently, most of the functions (such as "add_op_node", "_update_edges", etc.) -# are only used when creating a new DAGDependency from another representation of a circuit. +# are only used when creating a new DAGDependency. # On the other hand, replace_block_with_op is only used at the very end, # just before DAGDependency is converted into QuantumCircuit or DAGCircuit. # A part of the reason is that doing local changes to DAGDependency is tricky: @@ -430,61 +430,6 @@ def add_op_node(self, operation, qargs, cargs): self._add_multi_graph_node(new_node) self._update_edges() - def _gather_pred(self, node_id, direct_pred): - """Function set an attribute predecessors and gather multiple lists - of direct predecessors into a single one. - - Args: - node_id (int): label of the considered node in the DAG - direct_pred (list): list of direct successors for the given node - - Returns: - DAGDependency: A multigraph with update of the attribute ['predecessors'] - the lists of direct successors are put into a single one - """ - gather = self._multi_graph - gather.get_node_data(node_id).predecessors = [] - for d_pred in direct_pred: - gather.get_node_data(node_id).predecessors.append([d_pred]) - pred = self._multi_graph.get_node_data(d_pred).predecessors - gather.get_node_data(node_id).predecessors.append(pred) - return gather - - def _gather_succ(self, node_id, direct_succ): - """ - Function set an attribute successors and gather multiple lists - of direct successors into a single one. - - Args: - node_id (int): label of the considered node in the DAG - direct_succ (list): list of direct successors for the given node - - Returns: - MultiDiGraph: with update of the attribute ['predecessors'] - the lists of direct successors are put into a single one - """ - gather = self._multi_graph - gather.get_node_data(node_id).successors = [] - for d_succ in direct_succ: - gather.get_node_data(node_id).successors.append([d_succ]) - succ = gather.get_node_data(d_succ).successors - gather.get_node_data(node_id).successors.append(succ) - return gather - - def _list_pred(self, node_id): - """ - Use _gather_pred function and merge_no_duplicates to construct - the list of predecessors for a given node. - - Args: - node_id (int): label of the considered node - """ - direct_pred = self.direct_predecessors(node_id) - self._multi_graph = self._gather_pred(node_id, direct_pred) - self._multi_graph.get_node_data(node_id).predecessors = list( - merge_no_duplicates(*(self._multi_graph.get_node_data(node_id).predecessors)) - ) - def _update_edges(self): """ Updates DagDependency by adding edges to the newly added node (max_node) @@ -536,32 +481,23 @@ def _update_edges(self): def _add_successors(self): """ - Use _gather_succ and merge_no_duplicates to create the list of successors - for each node. Update DAGDependency 'successors' attribute. It has to + Create the list of successors. Update DAGDependency 'successors' attribute. It has to be used when the DAGDependency() object is complete (i.e. converters). """ for node_id in range(len(self._multi_graph) - 1, -1, -1): - direct_successors = self.direct_successors(node_id) - - self._multi_graph = self._gather_succ(node_id, direct_successors) - self._multi_graph.get_node_data(node_id).successors = list( - merge_no_duplicates(*self._multi_graph.get_node_data(node_id).successors) + rx.descendants(self._multi_graph, node_id) ) def _add_predecessors(self): """ - Use _gather_pred and merge_no_duplicates to create the list of predecessors - for each node. Update DAGDependency 'predecessors' attribute. It has to - be used when the DAGDependency() object is complete (i.e. converters). + Create the list of predecessors for each node. Update DAGDependency + 'predecessors' attribute. It has to be used when the DAGDependency() object + is complete (i.e. converters). """ for node_id in range(0, len(self._multi_graph)): - direct_predecessors = self.direct_predecessors(node_id) - - self._multi_graph = self._gather_pred(node_id, direct_predecessors) - self._multi_graph.get_node_data(node_id).predecessors = list( - merge_no_duplicates(*self._multi_graph.get_node_data(node_id).predecessors) + rx.ancestors(self._multi_graph, node_id) ) def copy(self): diff --git a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py index 89c688f8f7d4..5e020fd03a30 100644 --- a/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py +++ b/qiskit/transpiler/passes/optimization/template_matching/template_substitution.py @@ -433,6 +433,7 @@ def run_dag_opt(self): inst = node.op.copy() dag_dep_opt.add_op_node(inst, node.qargs, node.cargs) + dag_dep_opt._add_predecessors() dag_dep_opt._add_successors() # If there is no valid match, it returns the original dag. else: diff --git a/releasenotes/notes/fix-template-opt-bd3c40382e9a993b.yaml b/releasenotes/notes/fix-template-opt-bd3c40382e9a993b.yaml new file mode 100644 index 000000000000..5f47436cc94e --- /dev/null +++ b/releasenotes/notes/fix-template-opt-bd3c40382e9a993b.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixed a bug when constructing :class:`~qiskit.dagcircuit.DAGDependency` from + within the :class:`~qiskit.transpiler.passes.TemplateOptimization` transpiler pass, + which could lead to incorrect optimizations. diff --git a/test/python/transpiler/test_template_matching.py b/test/python/transpiler/test_template_matching.py index dae16075122a..e6d8ed2cff7a 100644 --- a/test/python/transpiler/test_template_matching.py +++ b/test/python/transpiler/test_template_matching.py @@ -14,11 +14,21 @@ """Test the TemplateOptimization pass.""" import unittest +from test.python.quantum_info.operators.symplectic.test_clifford import random_clifford_circuit import numpy as np from qiskit import QuantumRegister, QuantumCircuit from qiskit.circuit import Parameter from qiskit.quantum_info import Operator -from qiskit.circuit.library.templates import template_nct_2a_2, template_nct_5a_3 +from qiskit.circuit.library.templates.nct import template_nct_2a_2, template_nct_5a_3 +from qiskit.circuit.library.templates.clifford import ( + clifford_2_1, + clifford_2_2, + clifford_2_3, + clifford_2_4, + clifford_3_1, + clifford_4_1, + clifford_4_2, +) from qiskit.converters.circuit_to_dag import circuit_to_dag from qiskit.converters.circuit_to_dagdependency import circuit_to_dagdependency from qiskit.transpiler import PassManager @@ -647,6 +657,98 @@ def test_naming_clash_multiparameter(self): self.assertEqual(circuit_out, expected) + def test_consecutive_templates_apply(self): + """Test the scenario where one template optimization creates an opportunity for + another template optimization. + + This is the original circuit: + + ┌───┐ + q_0: ┤ X ├──■───X───────■─ + └─┬─┘┌─┴─┐ │ ┌───┐ │ + q_1: ──■──┤ X ├─X─┤ H ├─■─ + └───┘ └───┘ + + The clifford_4_1 template allows to replace the two CNOTs followed by the SWAP by a + single CNOT: + + q_0: ──■────────■─ + ┌─┴─┐┌───┐ │ + q_1: ┤ X ├┤ H ├─■─ + └───┘└───┘ + + At these point, the clifford_4_2 template allows to replace the circuit by a single + Hadamard gate: + + q_0: ───── + ┌───┐ + q_1: ┤ H ├ + └───┘ + + The second optimization would not have been possible without the applying the first + optimization. + """ + qc = QuantumCircuit(2) + qc.cx(1, 0) + qc.cx(0, 1) + qc.swap(0, 1) + qc.h(1) + qc.cz(0, 1) + + qc_expected = QuantumCircuit(2) + qc_expected.h(1) + + costs = {"h": 1, "cx": 2, "cz": 2, "swap": 3} + + # Check that consecutively applying both templates leads to the expected circuit. + qc_opt = TemplateOptimization( + template_list=[clifford_4_1(), clifford_4_2()], user_cost_dict=costs + )(qc) + self.assertEqual(qc_opt, qc_expected) + + # Also check that applying the second template by itself does not do anything. + qc_non_opt = TemplateOptimization(template_list=[clifford_4_2()], user_cost_dict=costs)(qc) + self.assertEqual(qc, qc_non_opt) + + def test_consecutive_templates_do_not_apply(self): + """Test that applying one template optimization does not allow incorrectly + applying other templates (which could happen if the DagDependency graph is + not constructed correctly after the optimization). + """ + template_list = [ + clifford_2_2(), + clifford_2_3(), + ] + pm = PassManager(TemplateOptimization(template_list=template_list)) + qc = QuantumCircuit(2) + qc.cx(0, 1) + qc.cx(0, 1) + qc.h(0) + qc.swap(0, 1) + qc.h(0) + qc_opt = pm.run(qc) + self.assertTrue(Operator(qc) == Operator(qc_opt)) + + def test_clifford_templates(self): + """Tests TemplateOptimization pass on several larger examples.""" + template_list = [ + clifford_2_1(), + clifford_2_2(), + clifford_2_3(), + clifford_2_4(), + clifford_3_1(), + ] + pm = PassManager(TemplateOptimization(template_list=template_list)) + for seed in range(10): + qc = random_clifford_circuit( + num_qubits=5, + num_gates=100, + gates=["x", "y", "z", "h", "s", "sdg", "cx", "cz", "swap"], + seed=seed, + ) + qc_opt = pm.run(qc) + self.assertTrue(Operator(qc) == Operator(qc_opt)) + if __name__ == "__main__": unittest.main() From dec20b0139c3e8ebf045f71326a2818661c09bbe Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 13 Apr 2023 15:57:40 +0100 Subject: [PATCH 017/172] Wrap io::Error in QASM2ParseError on failed file read (#9958) Previously, if Python thought the target file for `qasm2.load` existed, but Rust was unable to open it, the result `io:Error` would be propagated up to Python space verbatim, and these situations usually have confusing error messages. This could happen, for example, if the target was a directory on Windows. --- crates/qasm2/src/lib.rs | 12 +++++++++++- test/python/qasm2/test_parse_errors.py | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/crates/qasm2/src/lib.rs b/crates/qasm2/src/lib.rs index 1ae817364d11..fab26c5b8e93 100644 --- a/crates/qasm2/src/lib.rs +++ b/crates/qasm2/src/lib.rs @@ -13,6 +13,8 @@ use pyo3::prelude::*; use pyo3::Python; +use crate::error::QASM2ParseError; + mod bytecode; mod error; mod expr; @@ -94,6 +96,7 @@ fn bytecode_from_string( /// without loading the entire token and parse tree into memory at once. #[pyfunction] fn bytecode_from_file( + py: Python<'_>, path: std::ffi::OsString, include_path: Vec, custom_instructions: Vec, @@ -101,7 +104,14 @@ fn bytecode_from_file( strict: bool, ) -> PyResult { bytecode::BytecodeIterator::new( - lex::TokenStream::from_path(path, strict)?, + lex::TokenStream::from_path(&path, strict).map_err(|err| { + let exc = QASM2ParseError::new_err(format!( + "failed to read a token stream from file '{}'", + path.to_string_lossy() + )); + exc.set_cause(py, Some(err.into())); + exc + })?, include_path, &custom_instructions, &custom_classical, diff --git a/test/python/qasm2/test_parse_errors.py b/test/python/qasm2/test_parse_errors.py index 801559aac7a1..1cc90cdaef3c 100644 --- a/test/python/qasm2/test_parse_errors.py +++ b/test/python/qasm2/test_parse_errors.py @@ -220,6 +220,11 @@ def test_eof(self, statement): with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "unexpected end-of-file"): qiskit.qasm2.loads(full) + def test_loading_directory(self): + """Test that the correct error is raised when a file fails to open.""" + with self.assertRaisesRegex(qiskit.qasm2.QASM2ParseError, "failed to read"): + qiskit.qasm2.load(".") + class TestVersion(QiskitTestCase): def test_invalid_version(self): From 9157b40e886f127c93d1bc5ad5bff36b8c27f980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Thu, 13 Apr 2023 18:37:46 +0200 Subject: [PATCH 018/172] Add Migration Guide for `QuantumInstance` (#9554) * Add draft * Remove qiskit * Add qi migration file * Add content * Finish content * Update provider syntax * Finish content * Apply auto-suggestions * Add doctests * Change to dropdowns * Add EM results * Apply suggestions from code review Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Apply review comments * Apply suggestions from Declan's code review Co-authored-by: Declan Millar * Apply suggestions from Guillermo's code review Co-authored-by: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> * Change from doctest to codeblocks * Review follow-up edits * Replace statevector with quantum info * Reorder index * Add assemble alternative * Apply suggestions from Julien's code review Co-authored-by: Julien Gacon * Change transpilation example * Apply suggestions from Juliencode review Co-authored-by: Julien Gacon --------- Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: Declan Millar Co-authored-by: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Co-authored-by: Julien Gacon --- docs/migration_guides/index.rst | 3 +- docs/migration_guides/qi_migration.rst | 667 +++++++++++++++++++++++++ 2 files changed, 669 insertions(+), 1 deletion(-) create mode 100644 docs/migration_guides/qi_migration.rst diff --git a/docs/migration_guides/index.rst b/docs/migration_guides/index.rst index 302f6882ce1d..2c2e1a0c79be 100644 --- a/docs/migration_guides/index.rst +++ b/docs/migration_guides/index.rst @@ -5,5 +5,6 @@ Qiskit Migration Guides .. toctree:: :maxdepth: 1 - opflow_migration algorithms_migration + opflow_migration + qi_migration diff --git a/docs/migration_guides/qi_migration.rst b/docs/migration_guides/qi_migration.rst new file mode 100644 index 000000000000..09708f6848c8 --- /dev/null +++ b/docs/migration_guides/qi_migration.rst @@ -0,0 +1,667 @@ +################################ +Quantum Instance Migration Guide +################################ + +The :class:`~qiskit.utils.QuantumInstance` is a utility class that allows the joint +configuration of the circuit transpilation and execution steps, and provides functions +at a higher level of abstraction for a more convenient integration with algorithms. +These include measurement error mitigation, splitting/combining execution to +conform to job limits, +and ensuring reliable execution of circuits with additional job management tools. + +The :class:`~qiskit.utils.QuantumInstance` is being deprecated for several reasons: +On one hand, the functionality of :meth:`~qiskit.utils.QuantumInstance.execute` has +now been delegated to the different implementations of the :mod:`~qiskit.primitives` base classes. +On the other hand, with the direct implementation of transpilation at the primitives level, +the algorithms no longer +need to manage that aspect of execution, and thus :meth:`~qiskit.utils.QuantumInstance.transpile` is no longer +required by the workflow. If desired, custom transpilation routines can still be performed at the +user level through the :mod:`~qiskit.transpiler` module (see table below). + + +The following table summarizes the migration alternatives for the :class:`~qiskit.utils.QuantumInstance` class: + +.. list-table:: + :header-rows: 1 + + * - QuantumInstance method + - Alternative + * - :meth:`.QuantumInstance.execute` + - :meth:`qiskit.primitives.Sampler.run` or :meth:`qiskit.primitives.Estimator.run` + * - :meth:`.QuantumInstance.transpile` + - :meth:`qiskit.compiler.transpile` + * - :meth:`.QuantumInstance.assemble` + - :meth:`qiskit.compiler.assemble` + +The remainder of this guide will focus on the :meth:`.QuantumInstance.execute` to +:mod:`~qiskit.primitives` migration path. + +Contents +======== + +* `Choosing the right primitive for your task`_ +* `Choosing the right primitive for your settings`_ +* `Code examples`_ + +.. attention:: + + **Background on the Qiskit Primitives** + + The Qiskit Primitives are algorithmic abstractions that encapsulate the access to backends or simulators + for an easy integration into algorithm workflows. + + The current pool of primitives includes **two** different **classes** (:class:`~qiskit.primitives.Sampler` and + :class:`~qiskit.primitives.Estimator`) that can be imported from **three** different locations ( + :mod:`qiskit.primitives`, :mod:`qiskit_aer.primitives` and :mod:`qiskit_ibm_runtime` ). In addition to the + reference Sampler and Estimator, :mod:`qiskit.primitives` also contains a + :class:`~qiskit.primitives.BackendSampler` and a :class:`~qiskit.primitives.BackendEstimator` class. These are + wrappers for ``backend.run()`` that follow the primitives interface. + + This guide uses the following naming standard to refer to the primitives: + + - *Primitives* - Any Sampler/Estimator implementation + - *Reference Primitives* - The Sampler and Estimator in :mod:`qiskit.primitives` --> ``from qiskit.primitives import Sampler/Estimator`` + - *Aer Primitives* - The Sampler and Estimator in :mod:`qiskit_aer.primitives` --> ``from qiskit_aer.primitives import Sampler/Estimator`` + - *Runtime Primitives* - The Sampler and Estimator in :mod:`qiskit_ibm_runtime` --> ``from qiskit_ibm_runtime import Sampler/Estimator`` + - *Backend Primitives* - The BackendSampler and BackendEstimator in :mod:`qiskit.primitives` --> ``from qiskit import BackendSampler/BackendEstimator`` + + For guidelines on which primitives to choose for your task, please continue reading. + +Choosing the right primitive for your task +=========================================== + +The :class:`~qiskit.utils.QuantumInstance` was designed to be an abstraction over transpile/run. +It took inspiration from :func:`~qiskit.execute_function.execute`, but retained config information that could be set +at the algorithm level, to save the user from defining the same parameters for every transpile/execute call. + +The :mod:`qiskit.primitives` share some of these features, but unlike the :class:`~qiskit.utils.QuantumInstance`, +there are multiple primitive classes, and each is optimized for a specific +purpose. Selecting the right primitive (``Sampler`` or ``Estimator``) requires some knowledge about +**what** it is expected to do and **where/how** it is expected to run. + +.. note:: + + The role of the primitives is two-fold. On one hand, they act as access points to backends and simulators. + On the other hand, they are **algorithmic** abstractions with defined tasks: + + * The ``Estimator`` takes in circuits and observables and returns **expectation values**. + * The ``Sampler`` takes in circuits, measures them, and returns their **quasi-probability distributions**. + +In order to know which primitive to use instead of :class:`~qiskit.utils.QuantumInstance`, you should ask +yourself two questions: + +1. What is the minimal unit of information used by your algorithm? + a. **Expectation value** - you will need an ``Estimator`` + b. **Probability distribution** (from sampling the device) - you will need a ``Sampler`` + +2. How do you want to execute your circuits? + + This question is not new. In the legacy algorithm workflow, you would have to decide to set up a + :class:`~qiskit.utils.QuantumInstance` with either a real backend from a provider, or a simulator. + Now, this "backend selection" process is translated to **where** do you import your primitives + from: + + a. Using **local** statevector simulators for quick prototyping: **Reference Primitives** + b. Using **local** noisy simulations for finer algorithm tuning: **Aer Primitives** + c. Accessing **runtime-enabled backends** (or cloud simulators): **Runtime Primitives** + d. Accessing **non runtime-enabled backends** : **Backend Primitives** + +Arguably, the ``Sampler`` is the closest primitive to :class:`~qiskit.utils.QuantumInstance`, as they +both execute circuits and provide a result back. However, with the :class:`~qiskit.utils.QuantumInstance`, +the result data was backend dependent (it could be a counts ``dict``, a :class:`numpy.array` for +statevector simulations, etc), while the ``Sampler`` normalizes its ``SamplerResult`` to +return a :class:`~qiskit.result.QuasiDistribution` object with the resulting quasi-probability distribution. + +The ``Estimator`` provides a specific abstraction for the expectation value calculation that can replace +the use of :class:`.QuantumInstance` as well as the associated pre- and post-processing steps, usually performed +with an additional library such as :mod:`qiskit.opflow`. + +Choosing the right primitive for your settings +============================================== + +Certain :class:`~qiskit.utils.QuantumInstance` features are only available in certain primitive implementations. +The following table summarizes the most common :class:`~qiskit.utils.QuantumInstance` settings and which +primitives **expose a similar setting through their interface**: + +.. attention:: + + In some cases, a setting might not be exposed through the interface, but there might an alternative path to make + it work. This is the case for custom transpiler passes, which cannot be set through the primitives interface, + but pre-transpiled circuits can be sent if setting the option ``skip_transpilation=True``. For more information, + please refer to the API reference or source code of the desired primitive implementation. + +.. list-table:: + :header-rows: 1 + + * - QuantumInstance + - Reference Primitives + - Aer Primitives + - Runtime Primitives + - Backend Primitives + * - Select ``backend`` + - No + - No + - Yes + - Yes + * - Set ``shots`` + - Yes + - Yes + - Yes + - Yes + * - Simulator settings: ``basis_gates``, ``coupling_map``, ``initial_layout``, ``noise_model``, ``backend_options`` + - No + - Yes + - Yes + - No (inferred from internal ``backend``) + * - Transpiler settings: ``seed_transpiler``, ``optimization_level`` + - No + - No + - Yes (via ``options``) (*) + - Yes (via ``.set_transpile_options()``) + * - Set unbound ``pass_manager`` + - No + - No + - No (but can ``skip_transpilation``) + - No (but can ``skip_transpilation``) + * - Set ``bound_pass_manager`` + - No + - No + - No + - Yes + * - Set ``backend_options``: common ones were ``memory`` and ``meas_level`` + - No + - No + - No (only ``qubit_layout``) + - No + * - Measurement error mitigation: ``measurement_error_mitigation_cls``, ``cals_matrix_refresh_period``, + ``measurement_error_mitigation_shots``, ``mit_pattern`` + - No + - No + - Sampler default -> M3 (*) + - No + * - Job management: ``job_callback``, ``max_job_retries``, ``timeout``, ``wait`` + - Does not apply + - Does not apply + - Sessions, callback (**) + - No + + +(*) For more information on error mitigation and setting options on Runtime Primitives, visit +`this link `_. + +(**) For more information on Runtime sessions, visit `this how-to `_. + +Code examples +============= + +.. dropdown:: Example 1: Circuit Sampling with Local Simulation + :animate: fade-in-slide-down + + **Using Quantum Instance** + + The only alternative for local simulations using the quantum instance was using an Aer simulator backend. + If no simulation method is specified, the Aer simulator will default to an exact simulation + (statevector/stabilizer), if shots are specified, it will add shot noise. + Please note that ``QuantumInstance.execute()`` returned the counts in hexadecimal format. + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit_aer import AerSimulator + from qiskit.utils import QuantumInstance + + circuit = QuantumCircuit(2) + circuit.x(0) + circuit.x(1) + circuit.measure_all() + + simulator = AerSimulator() + qi = QuantumInstance(backend=simulator, shots=200) + result = qi.execute(circuit).results[0] + data = result.data + counts = data.counts + + print("Counts: ", counts) + print("Data: ", data) + print("Result: ", result) + + .. code-block:: text + + Counts: {'0x3': 200} + Data: ExperimentResultData(counts={'0x3': 200}) + Result: ExperimentResult(shots=200, success=True, meas_level=2, data=ExperimentResultData(counts={'0x3': 200}), header=QobjExperimentHeader(clbit_labels=[['meas', 0], ['meas', 1]], creg_sizes=[['meas', 2]], global_phase=0.0, memory_slots=2, metadata={}, n_qubits=2, name='circuit-99', qreg_sizes=[['q', 2]], qubit_labels=[['q', 0], ['q', 1]]), status=DONE, seed_simulator=2846213898, metadata={'parallel_state_update': 16, 'parallel_shots': 1, 'sample_measure_time': 0.00025145, 'noise': 'ideal', 'batched_shots_optimization': False, 'remapped_qubits': False, 'device': 'CPU', 'active_input_qubits': [0, 1], 'measure_sampling': True, 'num_clbits': 2, 'input_qubit_map': [[1, 1], [0, 0]], 'num_qubits': 2, 'method': 'stabilizer', 'fusion': {'enabled': False}}, time_taken=0.000672166) + + **Using Primitives** + + The primitives offer two alternatives for local simulation, one with the Reference primitives + and one with the Aer primitives. As mentioned above the closest alternative to ``QuantumInstance.execute()`` + for sampling is the ``Sampler`` primitive. + + **a. Using the Reference Primitives** + + Basic simulation implemented using the :mod:`qiskit.quantum_info` module. If shots are + specified, the results will include shot noise. Please note that + the resulting quasi-probability distribution does not use bitstrings but **integers** to identify the states. + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit.primitives import Sampler + + circuit = QuantumCircuit(2) + circuit.x(0) + circuit.x(1) + circuit.measure_all() + + sampler = Sampler() + result = sampler.run(circuit, shots=200).result() + quasi_dists = result.quasi_dists + + print("Quasi-dists: ", quasi_dists) + print("Result: ", result) + + .. code-block:: text + + Quasi-dists: [{3: 1.0}] + Result: SamplerResult(quasi_dists=[{3: 1.0}], metadata=[{'shots': 200}]) + + **b. Using the Aer Primitives** + + Aer simulation following the statevector method. This would be the closer replacement of the + :class:`~qiskit.utils.QuantumInstance` + example, as they are both accessing the same simulator. For this reason, the output metadata is + closer to the Quantum Instance's output. Please note that + the resulting quasi-probability distribution does not use bitstrings but **integers** to identify the states. + + .. note:: + + The :class:`qiskit.result.QuasiDistribution` class returned as part of the :class:`qiskit.primitives.SamplerResult` + exposes two methods to convert the result keys from integer to binary strings/hexadecimal: + + - :meth:`qiskit.result.QuasiDistribution.binary_probabilities` + - :meth:`qiskit.result.QuasiDistribution.hex_probabilities` + + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit_aer.primitives import Sampler + + circuit = QuantumCircuit(2) + circuit.x(0) + circuit.x(1) + circuit.measure_all() + + # if no Noise Model provided, the aer primitives + # perform an exact (statevector) simulation + sampler = Sampler() + result = sampler.run(circuit, shots=200).result() + quasi_dists = result.quasi_dists + # convert keys to binary bitstrings + binary_dist = quasi_dists[0].binary_probabilities() + + print("Quasi-dists: ", quasi_dists) + print("Result: ", result) + print("Binary quasi-dist: ", binary_dist) + + .. code-block:: text + + Quasi-dists: [{3: 1.0}] + Result: SamplerResult(quasi_dists=[{3: 1.0}], metadata=[{'shots': 200, 'simulator_metadata': {'parallel_state_update': 16, 'parallel_shots': 1, 'sample_measure_time': 9.016e-05, 'noise': 'ideal', 'batched_shots_optimization': False, 'remapped_qubits': False, 'device': 'CPU', 'active_input_qubits': [0, 1], 'measure_sampling': True, 'num_clbits': 2, 'input_qubit_map': [[1, 1], [0, 0]], 'num_qubits': 2, 'method': 'statevector', 'fusion': {'applied': False, 'max_fused_qubits': 5, 'threshold': 14, 'enabled': True}}}]) + Binary quasi-dist: {'11': 1.0} + +.. dropdown:: Example 2: Expectation Value Calculation with Local Noisy Simulation + :animate: fade-in-slide-down + + While this example does not include a direct call to ``QuantumInstance.execute()``, it shows + how to migrate from a :class:`~qiskit.utils.QuantumInstance`-based to a :mod:`~qiskit.primitives`-based + workflow. + + **Using Quantum Instance** + + The most common use case for computing expectation values with the Quantum Instance was as in combination with the + :mod:`~qiskit.opflow` library. You can see more information in the `opflow migration guide `_. + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit.opflow import StateFn, PauliSumOp, PauliExpectation, CircuitSampler + from qiskit.utils import QuantumInstance + from qiskit_aer import AerSimulator + from qiskit_aer.noise import NoiseModel + from qiskit_ibm_provider import IBMProvider + + # Define problem using opflow + op = PauliSumOp.from_list([("XY",1)]) + qc = QuantumCircuit(2) + qc.x(0) + qc.x(1) + + state = StateFn(qc) + measurable_expression = StateFn(op, is_measurement=True).compose(state) + expectation = PauliExpectation().convert(measurable_expression) + + # Define Quantum Instance with noisy simulator + provider = IBMProvider() + device = provider.get_backend("ibmq_manila") + noise_model = NoiseModel.from_backend(device) + coupling_map = device.configuration().coupling_map + + backend = AerSimulator() + qi = QuantumInstance(backend=backend, shots=1024, + seed_simulator=42, seed_transpiler=42, + coupling_map=coupling_map, noise_model=noise_model) + + # Run + sampler = CircuitSampler(qi).convert(expectation) + expectation_value = sampler.eval().real + + print(expectation_value) + + .. code-block:: text + + -0.04687500000000008 + + **Using Primitives** + + The primitives now allow the combination of the opflow and quantum instance functionality in a single ``Estimator``. + In this case, for local noisy simulation, this will be the Aer Estimator. + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit.quantum_info import SparsePauliOp + from qiskit_aer.noise import NoiseModel + from qiskit_aer.primitives import Estimator + from qiskit_ibm_provider import IBMProvider + + # Define problem + op = SparsePauliOp("XY") + qc = QuantumCircuit(2) + qc.x(0) + qc.x(1) + + # Define Aer Estimator with noisy simulator + device = provider.get_backend("ibmq_manila") + noise_model = NoiseModel.from_backend(device) + coupling_map = device.configuration().coupling_map + + # if Noise Model provided, the aer primitives + # perform a "qasm" simulation + estimator = Estimator( + backend_options={ # method chosen automatically to match options + "coupling_map": coupling_map, + "noise_model": noise_model, + }, + run_options={"seed": 42, "shots": 1024}, + transpile_options={"seed_transpiler": 42}, + ) + + # Run + expectation_value = estimator.run(qc, op).result().values + + print(expectation_value) + + .. code-block:: text + + [-0.04101562] + +.. dropdown:: Example 3: Circuit Sampling on IBM Backend with Error Mitigation + :animate: fade-in-slide-down + + **Using Quantum Instance** + + The ``QuantumInstance`` interface allowed the configuration of measurement error mitigation settings such as the method, the + matrix refresh period or the mitigation pattern. This configuration is no longer available in the primitives + interface. + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit.utils import QuantumInstance + from qiskit.utils.mitigation import CompleteMeasFitter + from qiskit_ibm_provider import IBMProvider + + circuit = QuantumCircuit(2) + circuit.x(0) + circuit.x(1) + circuit.measure_all() + + provider = IBMProvider() + backend = provider.get_backend("ibmq_montreal") + + qi = QuantumInstance( + backend=backend, + shots=4000, + measurement_error_mitigation_cls=CompleteMeasFitter, + cals_matrix_refresh_period=0, + ) + + result = qi.execute(circuit).results[0].data + print(result) + + .. code-block:: text + + ExperimentResultData(counts={'11': 4000}) + + + **Using Primitives** + + The Runtime Primitives offer a suite of error mitigation methods that can be easily turned on with the + ``resilience_level`` option. These are, however, not configurable. The sampler's ``resilience_level=1`` + is the closest alternative to the Quantum Instance's measurement error mitigation implementation, but this + is not a 1-1 replacement. + + For more information on the error mitigation options in the Runtime Primitives, you can check out the following + `link `_. + + .. code-block:: python + + from qiskit import QuantumCircuit + from qiskit_ibm_runtime import QiskitRuntimeService, Sampler, Options + + circuit = QuantumCircuit(2) + circuit.x(0) + circuit.x(1) + circuit.measure_all() + + service = QiskitRuntimeService(channel="ibm_quantum") + backend = service.backend("ibmq_montreal") + + options = Options(resilience_level = 1) # 1 = measurement error mitigation + sampler = Sampler(session=backend, options=options) + + # Run + result = sampler.run(circuit, shots=4000).result() + quasi_dists = result.quasi_dists + + print("Quasi dists: ", quasi_dists) + + .. code-block:: text + + Quasi dists: [{2: 0.0008492371522941081, 3: 0.9968874384378738, 0: -0.0003921227905920063, + 1: 0.002655447200424097}] + +.. dropdown:: Example 4: Circuit Sampling with Custom Bound and Unbound Pass Managers + :animate: fade-in-slide-down + + The management of transpilation is different between the ``QuantumInstance`` and the Primitives. + + The Quantum Instance allowed you to: + + * Define bound and unbound pass managers that will be called during ``.execute()``. + * Explicitly call its ``.transpile()`` method with a specific pass manager. + + However: + + * The Quantum Instance **did not** manage parameter bindings on parametrized quantum circuits. This would + mean that if a ``bound_pass_manager`` was set, the circuit sent to ``QuantumInstance.execute()`` could + not have any free parameters. + + On the other hand, when using the primitives: + + * You cannot explicitly access their transpilation routine. + * The mechanism to apply custom transpilation passes to the Aer, Runtime and Backend primitives is to pre-transpile + locally and set ``skip_transpilation=True`` in the corresponding primitive. + * The only primitives that currently accept a custom **bound** transpiler pass manager are the **Backend Primitives**. + If a ``bound_pass_manager`` is defined, the ``skip_transpilation=True`` option will **not** skip this bound pass. + + .. attention:: + + Care is needed when setting ``skip_transpilation=True`` with the ``Estimator`` primitive. + Since operator and circuit size need to match for the Estimator, should the custom transpilation change + the circuit size, then the operator must be adapted before sending it + to the Estimator, as there is no currently no mechanism to identify the active qubits it should consider. + + .. + In opflow, the ansatz would always have the basis change and measurement gates added before transpilation, + so if the circuit ended up on more qubits it did not matter. + + Note that the primitives **do** handle parameter bindings, meaning that even if a ``bound_pass_manager`` is defined in a + Backend Primitive, you do not have to manually assign parameters as expected in the Quantum Instance workflow. + + The use-case that motivated the addition of the two-stage transpilation to the ``QuantumInstance`` was to allow + running pulse-efficient transpilation passes with the :class:`~qiskit.opflow.CircuitSampler` class. The following + example shows to migrate this particular use-case, where the ``QuantumInstance.execute()`` method is called + under the hood by the :class:`~qiskit.opflow.CircuitSampler`. + + **Using Quantum Instance** + + .. code-block:: python + + from qiskit.circuit.library.standard_gates.equivalence_library import StandardEquivalenceLibrary as std_eqlib + from qiskit.circuit.library import RealAmplitudes + from qiskit.opflow import CircuitSampler, StateFn + from qiskit.providers.fake_provider import FakeBelem + from qiskit.transpiler import PassManager, PassManagerConfig, CouplingMap + from qiskit.transpiler.preset_passmanagers import level_1_pass_manager + from qiskit.transpiler.passes import ( + Collect2qBlocks, ConsolidateBlocks, Optimize1qGatesDecomposition, + RZXCalibrationBuilderNoEcho, UnrollCustomDefinitions, BasisTranslator + ) + from qiskit.transpiler.passes.optimization.echo_rzx_weyl_decomposition import EchoRZXWeylDecomposition + from qiskit.utils import QuantumInstance + + # Define backend + backend = FakeBelem() + + # Build the pass manager for the parameterized circuit + rzx_basis = ['rzx', 'rz', 'x', 'sx'] + coupling_map = CouplingMap(backend.configuration().coupling_map) + config = PassManagerConfig(basis_gates=rzx_basis, coupling_map=coupling_map) + pre = level_1_pass_manager(config) + inst_map = backend.defaults().instruction_schedule_map + + # Build a pass manager for the CX decomposition (works only on bound circuits) + post = PassManager([ + # Consolidate consecutive two-qubit operations. + Collect2qBlocks(), + ConsolidateBlocks(basis_gates=['rz', 'sx', 'x', 'rxx']), + + # Rewrite circuit in terms of Weyl-decomposed echoed RZX gates. + EchoRZXWeylDecomposition(inst_map), + + # Attach scaled CR pulse schedules to the RZX gates. + RZXCalibrationBuilderNoEcho(inst_map), + + # Simplify single-qubit gates. + UnrollCustomDefinitions(std_eqlib, rzx_basis), + BasisTranslator(std_eqlib, rzx_basis), + Optimize1qGatesDecomposition(rzx_basis), + ]) + + # Instantiate qi + quantum_instance = QuantumInstance(backend, pass_manager=pre, bound_pass_manager=post) + + # Define parametrized circuit and parameter values + qc = RealAmplitudes(2) + print(qc.decompose()) + param_dict = {p: 0.5 for p in qc.parameters} + + # Instantiate CircuitSampler + sampler = CircuitSampler(quantum_instance) + + # Run + quasi_dists = sampler.convert(StateFn(qc), params=param_dict).sample() + print("Quasi-dists: ", quasi_dists) + + .. code-block:: text + + ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ + q_0: ┤ Ry(θ[0]) ├──■──┤ Ry(θ[2]) ├──■──┤ Ry(θ[4]) ├──■──┤ Ry(θ[6]) ├ + ├──────────┤┌─┴─┐├──────────┤┌─┴─┐├──────────┤┌─┴─┐├──────────┤ + q_1: ┤ Ry(θ[1]) ├┤ X ├┤ Ry(θ[3]) ├┤ X ├┤ Ry(θ[5]) ├┤ X ├┤ Ry(θ[7]) ├ + └──────────┘└───┘└──────────┘└───┘└──────────┘└───┘└──────────┘ + Quasi-dists: {'11': 0.443359375, '10': 0.21875, '01': 0.189453125, '00': 0.1484375} + + **Using Primitives** + + Let's see how the workflow changes with the Backend Sampler: + + .. code-block:: python + + from qiskit.circuit.library.standard_gates.equivalence_library import StandardEquivalenceLibrary as std_eqlib + from qiskit.circuit.library import RealAmplitudes + from qiskit.primitives import BackendSampler + from qiskit.providers.fake_provider import FakeBelem + from qiskit.transpiler import PassManager, PassManagerConfig, CouplingMap + from qiskit.transpiler.preset_passmanagers import level_1_pass_manager + from qiskit.transpiler.passes import ( + Collect2qBlocks, ConsolidateBlocks, Optimize1qGatesDecomposition, + RZXCalibrationBuilderNoEcho, UnrollCustomDefinitions, BasisTranslator + ) + from qiskit.transpiler.passes.optimization.echo_rzx_weyl_decomposition import EchoRZXWeylDecomposition + + # Define backend + backend = FakeBelem() + + # Build the pass manager for the parameterized circuit + rzx_basis = ['rzx', 'rz', 'x', 'sx'] + coupling_map = CouplingMap(backend.configuration().coupling_map) + config = PassManagerConfig(basis_gates=rzx_basis, coupling_map=coupling_map) + pre = level_1_pass_manager(config) + + # Build a pass manager for the CX decomposition (works only on bound circuits) + inst_map = backend.defaults().instruction_schedule_map + post = PassManager([ + # Consolidate consecutive two-qubit operations. + Collect2qBlocks(), + ConsolidateBlocks(basis_gates=['rz', 'sx', 'x', 'rxx']), + + # Rewrite circuit in terms of Weyl-decomposed echoed RZX gates. + EchoRZXWeylDecomposition(inst_map), + + # Attach scaled CR pulse schedules to the RZX gates. + RZXCalibrationBuilderNoEcho(inst_map), + + # Simplify single-qubit gates. + UnrollCustomDefinitions(std_eqlib, rzx_basis), + BasisTranslator(std_eqlib, rzx_basis), + Optimize1qGatesDecomposition(rzx_basis), + ]) + + # Define parametrized circuit and parameter values + qc = RealAmplitudes(2) + qc.measure_all() # add measurements! + print(qc.decompose()) + + # Instantiate backend sampler with skip_transpilation + sampler = BackendSampler(backend=backend, skip_transpilation=True, bound_pass_manager=post) + + # Run unbound transpiler pass + transpiled_circuit = pre.run(qc) + + # Run sampler + quasi_dists = sampler.run(transpiled_circuit, [[0.5] * len(qc.parameters)]).result().quasi_dists + print("Quasi-dists: ", quasi_dists) + + .. code-block:: text + + ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ░ ┌─┐ + q_0: ┤ Ry(θ[0]) ├──■──┤ Ry(θ[2]) ├──■──┤ Ry(θ[4]) ├──■──┤ Ry(θ[6]) ├─░─┤M├─── + ├──────────┤┌─┴─┐├──────────┤┌─┴─┐├──────────┤┌─┴─┐├──────────┤ ░ └╥┘┌─┐ + q_1: ┤ Ry(θ[1]) ├┤ X ├┤ Ry(θ[3]) ├┤ X ├┤ Ry(θ[5]) ├┤ X ├┤ Ry(θ[7]) ├─░──╫─┤M├ + └──────────┘└───┘└──────────┘└───┘└──────────┘└───┘└──────────┘ ░ ║ └╥┘ + meas: 2/═══════════════════════════════════════════════════════════════════╩══╩═ + 0 1 + Quasi-dists: [{1: 0.18359375, 2: 0.2333984375, 0: 0.1748046875, 3: 0.408203125}] From d07c5cc9a4b14be942e6b032c6954ea81844fd00 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 13 Apr 2023 12:39:55 -0400 Subject: [PATCH 019/172] Add constructor to build target from legacy transpiler model (#9255) * Add constructor to build target from legacy transpiler model This commit adds a new constructor method from_configuration() to the Target class. This can be used to construct a new Target object from the legacy input types: basis_gates, coupling_map, etc. This can then be used to provide a conversion layer in transpile() in support of migrating to using the target everywhere internally. * Add more test coverage * Apply suggestions from code review Co-authored-by: Kevin Krsulich * Make num_qubits optional * Explain arg precedence in docstring * Use assertRaisesRegex * Fix lint * Fix rebase name mismatch * Fix lint * Update error message with multiqubit gates * Give instruction durations higher priority * Improve handling of instruction schedule map * Add comment about how connectivity is defined * Update test exception assertion regex for new error message * Update qiskit/transpiler/target.py Co-authored-by: Naoki Kanazawa --------- Co-authored-by: Kevin Krsulich Co-authored-by: Naoki Kanazawa --- qiskit/transpiler/target.py | 241 +++++++++++++++++- ...t-from-configuration-91f7eb569d95b330.yaml | 17 ++ test/python/transpiler/test_target.py | 109 +++++++- 3 files changed, 365 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/new-constructor-target-from-configuration-91f7eb569d95b330.yaml diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 27b9f8fa57af..2187ff014db8 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -16,8 +16,13 @@ A target object represents the minimum set of information the transpiler needs from a backend """ + +from __future__ import annotations + + import warnings -from typing import Tuple, Union + +from typing import Tuple, Union, Optional, Dict, List, Any from collections.abc import Mapping from collections import defaultdict import datetime @@ -38,6 +43,8 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations from qiskit.transpiler.timing_constraints import TimingConstraints +from qiskit.providers.exceptions import BackendPropertyError +from qiskit.pulse.exceptions import PulseError from qiskit.utils.deprecation import deprecate_arguments from qiskit.exceptions import QiskitError @@ -46,6 +53,7 @@ from qiskit.providers.backend import QubitProperties # pylint: disable=unused-import from qiskit.providers.models.backendproperties import BackendProperties + logger = logging.getLogger(__name__) @@ -1176,6 +1184,237 @@ def __str__(self): output.write("".join(prop_str_pieces)) return output.getvalue() + @classmethod + def from_configuration( + cls, + basis_gates: List[str], + num_qubits: Optional[int] = None, + coupling_map: Optional[CouplingMap] = None, + inst_map: Optional[InstructionScheduleMap] = None, + backend_properties: Optional[BackendProperties] = None, + instruction_durations: Optional[InstructionDurations] = None, + dt: Optional[float] = None, + timing_constraints: Optional[TimingConstraints] = None, + custom_name_mapping: Optional[Dict[str, Any]] = None, + ) -> Target: + """Create a target object from the individual global configuration + + Prior to the creation of the :class:`~.Target` class, the constraints + of a backend were represented by a collection of different objects + which combined represent a subset of the information contained in + the :class:`~.Target`. This function provides a simple interface + to convert those separate objects to a :class:`~.Target`. + + This constructor will use the input from ``basis_gates``, ``num_qubits``, + and ``coupling_map`` to build a base model of the backend and the + ``instruction_durations``, ``backend_properties``, and ``inst_map`` inputs + are then queried (in that order) based on that model to look up the properties + of each instruction and qubit. If there is an inconsistency between the inputs + any extra or conflicting information present in ``instruction_durations``, + ``backend_properties``, or ``inst_map`` will be ignored. + + Args: + basis_gates: The list of basis gate names for the backend. For the + target to be created these names must either be in the output + from :func:~.get_standard_gate_name_mapping` or present in the + specified ``custom_name_mapping`` argument. + num_qubits: The number of qubits supported on the backend. + coupling_map: The coupling map representing connectivity constraints + on the backend. If specified all gates from ``basis_gates`` will + be supported on all qubits (or pairs of qubits). + inst_map: The instruction schedule map representing the pulse + :class:`~.Schedule` definitions for each instruction. If this + is specified ``coupling_map`` must be specified. The + ``coupling_map`` is used as the source of truth for connectivity + and if ``inst_map`` is used the schedule is looked up based + on the instuctions from the pair of ``basis_gates`` and + ``coupling_map``. If you want to define a custom gate for + a particular qubit or qubit pair, you can manually build :class:`.Target`. + backend_properties: The :class:`~.BackendProperties` object which is + used for instruction properties and qubit properties. + If specified and instruction properties are intended to be used + then the ``coupling_map`` argument must be specified. This is + only used to lookup error rates and durations (unless + ``instruction_durations`` is specified which would take + precedence) for instructions specified via ``coupling_map`` and + ``basis_gates``. + instruction_durations: Optional instruction durations for instructions. If specified + it will take priority for setting the ``duration`` field in the + :class:`~InstructionProperties` objects for the instructions in the target. + dt: The system time resolution of input signals in seconds + timing_constraints: Optional timing constraints to include in the + :class:`~.Target` + custom_name_mapping: An optional dictionary that maps custom gate/operation names in + ``basis_gates`` to an :class:`~.Operation` object representing that + gate/operation. By default most standard gates names are mapped to the + standard gate object from :mod:`qiskit.circuit.library` this only needs + to be specified if the input ``basis_gates`` defines gates in names outside + that set. + + Returns: + Target: the target built from the input configuration + + Raises: + TranspilerError: If the input basis gates contain > 2 qubits and ``coupling_map`` is + specified. + KeyError: If no mappign is available for a specified ``basis_gate``. + """ + granularity = 1 + min_length = 1 + pulse_alignment = 1 + acquire_alignment = 1 + if timing_constraints is not None: + granularity = timing_constraints.granularity + min_length = timing_constraints.min_length + pulse_alignment = timing_constraints.pulse_alignment + acquire_alignment = timing_constraints.acquire_alignment + + qubit_properties = None + if backend_properties is not None: + # pylint: disable=cyclic-import + from qiskit.providers.backend_compat import qubit_props_list_from_props + + qubit_properties = qubit_props_list_from_props(properties=backend_properties) + + target = cls( + num_qubits=num_qubits, + dt=dt, + granularity=granularity, + min_length=min_length, + pulse_alignment=pulse_alignment, + acquire_alignment=acquire_alignment, + qubit_properties=qubit_properties, + ) + name_mapping = get_standard_gate_name_mapping() + if custom_name_mapping is not None: + name_mapping.update(custom_name_mapping) + + # While BackendProperties can also contain coupling information we + # rely solely on CouplingMap to determin connectivity. This is because + # in legacy transpiler usage (and implicitly in the BackendV1 data model) + # the coupling map is used to define connecitivity constraints and + # the properties is only used for error rate and duration population. + # If coupling map is not specified we ignore the backend_properties + if coupling_map is None: + for gate in basis_gates: + if gate not in name_mapping: + raise KeyError( + f"The specified basis gate: {gate} is not present in the standard gate " + "names or a provided custom_name_mapping" + ) + target.add_instruction(name_mapping[gate], name=gate) + else: + one_qubit_gates = [] + two_qubit_gates = [] + global_ideal_variable_width_gates = [] # pylint: disable=invalid-name + if num_qubits is None: + num_qubits = len(coupling_map.graph) + for gate in basis_gates: + if gate not in name_mapping: + raise KeyError( + f"The specified basis gate: {gate} is not present in the standard gate " + "names or a provided custom_name_mapping" + ) + gate_obj = name_mapping[gate] + if gate_obj.num_qubits == 1: + one_qubit_gates.append(gate) + elif gate_obj.num_qubits == 2: + two_qubit_gates.append(gate) + elif inspect.isclass(gate_obj): + global_ideal_variable_width_gates.append(gate) + else: + raise TranspilerError( + f"The specified basis gate: {gate} has {gate_obj.num_qubits} " + "qubits. This constructor method only supports fixed width operations " + "with <= 2 qubits (because connectivity is defined on a CouplingMap)." + ) + for gate in one_qubit_gates: + gate_properties = {} + for qubit in range(num_qubits): + error = None + duration = None + calibration = None + if backend_properties is not None: + if duration is None: + try: + duration = backend_properties.gate_length(gate, qubit) + except BackendPropertyError: + duration = None + try: + error = backend_properties.gate_error(gate, qubit) + except BackendPropertyError: + error = None + if inst_map is not None: + try: + calibration = inst_map._get_calibration_entry(gate, qubit) + # If we have dt defined and there is a custom calibration which is user + # generate use that custom pulse schedule for the duration. If it is + # not user generated than we assume it's the same duration as what is + # defined in the backend properties + if dt and calibration.user_provided: + duration = calibration.get_schedule().duration * dt + except PulseError: + calibration = None + # Durations if specified manually should override model objects + if instruction_durations is not None: + try: + duration = instruction_durations.get(gate, qubit, unit="s") + except TranspilerError: + duration = None + + if error is None and duration is None and calibration is None: + gate_properties[(qubit,)] = None + else: + gate_properties[(qubit,)] = InstructionProperties( + duration=duration, error=error, calibration=calibration + ) + target.add_instruction(name_mapping[gate], properties=gate_properties, name=gate) + edges = list(coupling_map.get_edges()) + for gate in two_qubit_gates: + gate_properties = {} + for edge in edges: + error = None + duration = None + calibration = None + if backend_properties is not None: + if duration is None: + try: + duration = backend_properties.gate_length(gate, edge) + except BackendPropertyError: + duration = None + try: + error = backend_properties.gate_error(gate, edge) + except BackendPropertyError: + error = None + if inst_map is not None: + try: + calibration = inst_map._get_calibration_entry(gate, edge) + # If we have dt defined and there is a custom calibration which is user + # generate use that custom pulse schedule for the duration. If it is + # not user generated than we assume it's the same duration as what is + # defined in the backend properties + if dt and calibration.user_provided: + duration = calibration.get_schedule().duration * dt + except PulseError: + calibration = None + # Durations if specified manually should override model objects + if instruction_durations is not None: + try: + duration = instruction_durations.get(gate, edge, unit="s") + except TranspilerError: + duration = None + + if error is None and duration is None and calibration is None: + gate_properties[edge] = None + else: + gate_properties[edge] = InstructionProperties( + duration=duration, error=error, calibration=calibration + ) + target.add_instruction(name_mapping[gate], properties=gate_properties, name=gate) + for gate in global_ideal_variable_width_gates: + target.add_instruction(name_mapping[gate], name=gate) + return target + def target_to_backend_properties(target: Target): """Convert a :class:`~.Target` object into a legacy :class:`~.BackendProperties`""" diff --git a/releasenotes/notes/new-constructor-target-from-configuration-91f7eb569d95b330.yaml b/releasenotes/notes/new-constructor-target-from-configuration-91f7eb569d95b330.yaml new file mode 100644 index 000000000000..c5ca0523782d --- /dev/null +++ b/releasenotes/notes/new-constructor-target-from-configuration-91f7eb569d95b330.yaml @@ -0,0 +1,17 @@ +--- +features: + - | + Added a new constructor for the :class:`~.Target` class, + :meth:`.Target.from_configuration`, which lets you construct a + :class:`~.Target` object from the separate object types for describing + the constraints of a backend (e.g. basis gates, :class:`~.CouplingMap`, + :class:`~.BackendProperties`, etc). For example:: + + target = Target.from_configuration( + basis_gates=["u", "cx", "measure"], + coupling_map=CouplingMap.from_line(25), + ) + + This will construct a :class:`~.Target` object that has :class:`~.UGate`, + :class:`~.CXGate`, and :class:`~.Measure` globally available on 25 qubits + which are connected in a line. diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 980daef3d9bd..3316ed93b1f7 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -43,7 +43,13 @@ from qiskit.transpiler import Target from qiskit.transpiler import InstructionProperties from qiskit.test import QiskitTestCase -from qiskit.providers.fake_provider import FakeBackendV2, FakeMumbaiFractionalCX, FakeGeneva +from qiskit.providers.fake_provider import ( + FakeBackendV2, + FakeMumbaiFractionalCX, + FakeVigo, + FakeNairobi, + FakeGeneva, +) class TestTarget(QiskitTestCase): @@ -1738,3 +1744,104 @@ def test_empty_repr(self): repr(properties), "InstructionProperties(duration=None, error=None, calibration=None)", ) + + +class TestTargetFromConfiguration(QiskitTestCase): + """Test the from_configuration() constructor.""" + + def test_basis_gates_qubits_only(self): + """Test construction with only basis gates.""" + target = Target.from_configuration(["u", "cx"], 3) + self.assertEqual(target.operation_names, {"u", "cx"}) + + def test_basis_gates_no_qubits(self): + target = Target.from_configuration(["u", "cx"]) + self.assertEqual(target.operation_names, {"u", "cx"}) + + def test_basis_gates_coupling_map(self): + """Test construction with only basis gates.""" + target = Target.from_configuration( + ["u", "cx"], 3, CouplingMap.from_ring(3, bidirectional=False) + ) + self.assertEqual(target.operation_names, {"u", "cx"}) + self.assertEqual({(0,), (1,), (2,)}, target["u"].keys()) + self.assertEqual({(0, 1), (1, 2), (2, 0)}, target["cx"].keys()) + + def test_properties(self): + fake_backend = FakeVigo() + config = fake_backend.configuration() + properties = fake_backend.properties() + target = Target.from_configuration( + basis_gates=config.basis_gates, + num_qubits=config.num_qubits, + coupling_map=CouplingMap(config.coupling_map), + backend_properties=properties, + ) + self.assertEqual(0, target["rz"][(0,)].error) + self.assertEqual(0, target["rz"][(0,)].duration) + + def test_properties_with_durations(self): + fake_backend = FakeVigo() + config = fake_backend.configuration() + properties = fake_backend.properties() + durations = InstructionDurations([("rz", 0, 0.5)], dt=1.0) + target = Target.from_configuration( + basis_gates=config.basis_gates, + num_qubits=config.num_qubits, + coupling_map=CouplingMap(config.coupling_map), + backend_properties=properties, + instruction_durations=durations, + dt=config.dt, + ) + self.assertEqual(0.5, target["rz"][(0,)].duration) + + def test_inst_map(self): + fake_backend = FakeNairobi() + config = fake_backend.configuration() + properties = fake_backend.properties() + defaults = fake_backend.defaults() + constraints = TimingConstraints(**config.timing_constraints) + target = Target.from_configuration( + basis_gates=config.basis_gates, + num_qubits=config.num_qubits, + coupling_map=CouplingMap(config.coupling_map), + backend_properties=properties, + dt=config.dt, + inst_map=defaults.instruction_schedule_map, + timing_constraints=constraints, + ) + self.assertIsNotNone(target["sx"][(0,)].calibration) + self.assertEqual(target.granularity, constraints.granularity) + self.assertEqual(target.min_length, constraints.min_length) + self.assertEqual(target.pulse_alignment, constraints.pulse_alignment) + self.assertEqual(target.acquire_alignment, constraints.acquire_alignment) + + def test_custom_basis_gates(self): + basis_gates = ["my_x", "cx"] + custom_name_mapping = {"my_x": XGate()} + target = Target.from_configuration( + basis_gates=basis_gates, num_qubits=2, custom_name_mapping=custom_name_mapping + ) + self.assertEqual(target.operation_names, {"my_x", "cx"}) + + def test_missing_custom_basis_no_coupling(self): + basis_gates = ["my_X", "cx"] + with self.assertRaisesRegex(KeyError, "is not present in the standard gate names"): + Target.from_configuration(basis_gates, num_qubits=4) + + def test_missing_custom_basis_with_coupling(self): + basis_gates = ["my_X", "cx"] + cmap = CouplingMap.from_line(3) + with self.assertRaisesRegex(KeyError, "is not present in the standard gate names"): + Target.from_configuration(basis_gates, 3, cmap) + + def test_over_two_qubit_gate_without_coupling(self): + basis_gates = ["ccx", "cx", "swap", "u"] + target = Target.from_configuration(basis_gates, 15) + self.assertEqual(target.operation_names, {"ccx", "cx", "swap", "u"}) + + def test_over_two_qubits_with_coupling(self): + basis_gates = ["ccx", "cx", "swap", "u"] + cmap = CouplingMap.from_line(15) + with self.assertRaisesRegex(TranspilerError, "This constructor method only supports"): + Target.from_configuration(basis_gates, 15, cmap) From 2534efb7023179253cad13e3be933a006c09e446 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 13 Apr 2023 15:22:56 -0400 Subject: [PATCH 020/172] Add support for disjoint coupling maps to SabreLayout (#9802) * Add support for disjoint coupling maps to SabreLayout This commit adds support to the SabreLayout pass for targeting disjoint CouplingMap objects. The SABRE algorithm is written assuming a connected graph, so to enable targeting a disjoint coupling graph this commit decomposes the circuit into it's weakly connected components and then maps those to the connected components of the coupling map and runs the rust portion of the sabre code on each connected component of the coupling graph. The results for each subgraph of the target backend is then combined the pass is building the output dag. In general the biggest potential issue for output quality right now is the mapping function does not take into account the relative connectivity of the dag component and the coupling map component. The mapping is just done in order and it tries every component until it finds one that has enough qubits. In the future we should use a heuristic to try and pack the components based on what we expect will require the least amount of swaps, but we can attempt that in a future PR. * Fix handling of barriers across separating components This commit fixes the handling of the barrier directive as the input DAG is split across multiple components. It is entirely valid for an input dag to have a multi-qubit barrier that spans multiple connected components. However, such barriers shouldn't be treated as multi-qubit operation for computing the connected components (as this result in the components being treated larger than they otherwise would be). To handle this edge case the logic introduced into this commit is prior to computing the connected components in the input dag we split each multi-qubit barrier into n single qubit barriers and assign a UUID label to each of the single qubit barriers. Then after we create each subgraph DAGCircuit object for each connected component we find all the barriers with a UUID and recombine them into a multi qubit barrier. This will retain the barriers characteristics for each connected component (as if it were in the original disjoint dag). Then in `SabreLayout` after we've built the output mapped dagcircuit we run the combination function one more time to combine the multiqubit dags across the different components, and in that process the UUID labels are removed. It is worth pointing out the downside with this approach is that it precludes preserving barriers with labels through transpile(). * Fix compatibility with Python < 3.9 * Adjust sorting order for mapping components * Add full path transpile() tests * Tag six component test as a slow test * Fix handling of splitting dags with shared classical bits This commit fixes the handling of the separate_dag() function when the input DAGCircuit has shared classical bits between quantum connected components. In previous commits on this branch these would incorrectly be treated as a single connected component because from the perspective of rustworkx's weakly_connected_components() function there is no difference between the type of wires when computing the connected components of the graph. This commit fixes this by instead of computing the connected components of the DAG itself we create an interaction graph of just the quantum component and use that to find the connected components of qubits. From that we build subgraphs of the DAGCircuit for each connected component DAGCircuit. This ends up being less efficient, but produces the correct result by ignoring the classical component for computing the connected components. * Add more test coverage and release notes * Don't route in SabreLayout with > 1 layout component with shared clbits When we run SabreLayout normally we're running both the layout and routing stage together. This is done for performance because the rust component is running routing internally so we avoid multiple back and forths between Python and Rust this way and it can lead to noticeable runtime improvements when doing this. However, in the case of > 1 circuit component that have any shared clbits we can not safetly route from the split dag components because the data dependency is lost for the classical component of the circuit is lost between the split components. To do routing without potentially reordering the data dependencies for the classical bits we need to run it on the combined dag all at once. For SabreLayout the easiest way to do that is to just have it work as a normal layout pass and let `SabreSwap` do the routing as it will have the full context and won't cause things to reorder incorrectly. This commit makes the change by checking if we have more than one component and any shared clbits between any components. If we do then we skip routing in SabreLayout and only return layout information. * Apply suggestions from code review Co-authored-by: Kevin Hartman * Fix return type hint for map_components() * Cleanup variables and set usage in separate_dag * Remove duplicate lines in SabreLayout * Update error message text for routing_pass arg and disjoint cmap Co-authored-by: Kevin Hartman * Improve docstring for map_components * Add comment explaining dag composition in run_pass_over_connected_components --------- Co-authored-by: Kevin Hartman --- .../passes/layout/disjoint_utils.py | 158 +++++++ .../transpiler/passes/layout/sabre_layout.py | 134 ++++-- .../transpiler/passes/routing/sabre_swap.py | 16 +- ...-disjoint-cmap-sabre-551ae4295131a449.yaml | 11 + test/python/compiler/test_transpiler.py | 444 +++++++++++++++++- test/python/transpiler/test_sabre_layout.py | 139 ++++++ 6 files changed, 858 insertions(+), 44 deletions(-) create mode 100644 qiskit/transpiler/passes/layout/disjoint_utils.py create mode 100644 releasenotes/notes/support-disjoint-cmap-sabre-551ae4295131a449.yaml diff --git a/qiskit/transpiler/passes/layout/disjoint_utils.py b/qiskit/transpiler/passes/layout/disjoint_utils.py new file mode 100644 index 000000000000..f4f55a352e1e --- /dev/null +++ b/qiskit/transpiler/passes/layout/disjoint_utils.py @@ -0,0 +1,158 @@ +# 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. + +"""This module contains common utils for disjoint coupling maps.""" + +from collections import defaultdict +from typing import List, Callable, TypeVar, Dict +import uuid + +import rustworkx as rx + +from qiskit.circuit import Qubit, Barrier, Clbit +from qiskit.dagcircuit.dagcircuit import DAGCircuit +from qiskit.dagcircuit.dagnode import DAGOutNode +from qiskit.transpiler.coupling import CouplingMap +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.layout import vf2_utils + +T = TypeVar("T") + + +def run_pass_over_connected_components( + dag: DAGCircuit, + coupling_map: CouplingMap, + run_func: Callable[[DAGCircuit, CouplingMap], T], +) -> List[T]: + """Run a transpiler pass inner function over mapped components.""" + cmap_components = coupling_map.connected_components() + # If graph is connected we only need to run the pass once + if len(cmap_components) == 1: + return [run_func(dag, cmap_components[0])] + dag_components = separate_dag(dag) + mapped_components = map_components(dag_components, cmap_components) + out_component_pairs = [] + for cmap_index, dags in mapped_components.items(): + # Take the first dag from the mapped dag components and then + # compose it with any other dag components that are operating on the + # same coupling map connected component. This results in a subcircuit + # of possibly disjoint circuit components which we will run the layout + # pass on. + out_dag = dag_components[dags.pop()] + for dag_index in dags: + dag = dag_components[dag_index] + out_dag.add_qubits(dag.qubits) + out_dag.add_clbits(dag.clbits) + for qreg in dag.qregs: + out_dag.add_qreg(qreg) + for creg in dag.cregs: + out_dag.add_cregs(creg) + out_dag.compose(dag, qubits=dag.qubits, clbits=dag.clbits) + out_component_pairs.append((out_dag, cmap_components[cmap_index])) + res = [run_func(out_dag, cmap) for out_dag, cmap in out_component_pairs] + return res + + +def map_components( + dag_components: List[DAGCircuit], cmap_components: List[CouplingMap] +) -> Dict[int, List[int]]: + """Returns a map where the key is the index of each connected component in cmap_components and + the value is a list of indices from dag_components which should be placed onto it.""" + free_qubits = {index: len(cmap.graph) for index, cmap in enumerate(cmap_components)} + out_mapping = defaultdict(list) + + for dag_index, dag in sorted( + enumerate(dag_components), key=lambda x: x[1].num_qubits(), reverse=True + ): + for cmap_index in sorted( + range(len(cmap_components)), key=lambda index: free_qubits[index], reverse=True + ): + # TODO: Improve heuristic to involve connectivity and estimate + # swap cost + if dag.num_qubits() <= free_qubits[cmap_index]: + out_mapping[cmap_index].append(dag_index) + free_qubits[cmap_index] -= dag.num_qubits() + break + else: + raise TranspilerError( + "A connected component of the DAGCircuit is too large for any of the connected " + "components in the coupling map." + ) + return out_mapping + + +def split_barriers(dag: DAGCircuit): + """Mutate an input dag to split barriers into single qubit barriers.""" + for node in dag.op_nodes(Barrier): + num_qubits = len(node.qargs) + if num_qubits == 1: + continue + barrier_uuid = uuid.uuid4() + split_dag = DAGCircuit() + split_dag.add_qubits([Qubit() for _ in range(num_qubits)]) + for i in range(num_qubits): + split_dag.apply_operation_back( + Barrier(1, label=barrier_uuid), qargs=[split_dag.qubits[i]] + ) + dag.substitute_node_with_dag(node, split_dag) + + +def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True): + """Mutate input dag to combine barriers with UUID labels into a single barrier.""" + qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} + uuid_map = {} + for node in dag.op_nodes(Barrier): + if isinstance(node.op.label, uuid.UUID): + barrier_uuid = node.op.label + if barrier_uuid in uuid_map: + other_node = uuid_map[node.op.label] + num_qubits = len(other_node.qargs) + len(node.qargs) + new_op = Barrier(num_qubits, label=barrier_uuid) + new_node = dag.replace_block_with_op([node, other_node], new_op, qubit_indices) + uuid_map[barrier_uuid] = new_node + else: + uuid_map[barrier_uuid] = node + if not retain_uuid: + for node in dag.op_nodes(Barrier): + if isinstance(node.op.label, uuid.UUID): + node.op.label = None + + +def separate_dag(dag: DAGCircuit) -> List[DAGCircuit]: + """Separate a dag circuit into it's connected components.""" + # Split barriers into single qubit barriers before splitting connected components + split_barriers(dag) + im_graph, _, qubit_map, __ = vf2_utils.build_interaction_graph(dag) + connected_components = rx.weakly_connected_components(im_graph) + component_qubits = [] + for component in connected_components: + component_qubits.append(set(qubit_map[x] for x in component)) + + qubits = set(dag.qubits) + + decomposed_dags = [] + for dag_qubits in component_qubits: + new_dag = dag.copy_empty_like() + new_dag.remove_qubits(*qubits - dag_qubits) + new_dag.global_phase = 0 + for node in dag.topological_op_nodes(): + if dag_qubits.issuperset(node.qargs): + new_dag.apply_operation_back(node.op, node.qargs, node.cargs) + idle_clbits = [] + for bit, node in new_dag.input_map.items(): + succ_node = next(new_dag.successors(node)) + if isinstance(succ_node, DAGOutNode) and isinstance(succ_node.wire, Clbit): + idle_clbits.append(bit) + new_dag.remove_clbits(*idle_clbits) + combine_barriers(new_dag) + decomposed_dags.append(new_dag) + return decomposed_dags diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 26db9ddecb43..749751fdba5e 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -23,6 +23,7 @@ from qiskit.transpiler.passes.layout.full_ancilla_allocation import FullAncillaAllocation from qiskit.transpiler.passes.layout.enlarge_with_ancilla import EnlargeWithAncilla from qiskit.transpiler.passes.layout.apply_layout import ApplyLayout +from qiskit.transpiler.passes.layout import disjoint_utils from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.layout import Layout from qiskit.transpiler.basepasses import TransformationPass @@ -171,13 +172,13 @@ def run(self, dag): """ if len(dag.qubits) > self.coupling_map.size(): raise TranspilerError("More virtual qubits exist than physical.") - if not self.coupling_map.is_connected(): - raise TranspilerError( - "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " - "map." - ) + # Choose a random initial_layout. if self.routing_pass is not None: + if not self.coupling_map.is_connected(): + raise TranspilerError( + "The routing_pass argument cannot be used with disjoint coupling maps." + ) if self.seed is None: seed = np.random.randint(0, np.iinfo(np.int32).max) else: @@ -215,7 +216,90 @@ def run(self, dag): self.property_set["layout"] = initial_layout self.routing_pass.fake_run = False return dag - dist_matrix = self.coupling_map.distance_matrix + # Combined + layout_components = disjoint_utils.run_pass_over_connected_components( + dag, self.coupling_map, self._inner_run + ) + initial_layout_dict = {} + final_layout_dict = {} + shared_clbits = False + seen_clbits = set() + for ( + layout_dict, + final_dict, + component_map, + _gate_order, + _swap_map, + local_dag, + ) in layout_components: + initial_layout_dict.update({k: component_map[v] for k, v in layout_dict.items()}) + final_layout_dict.update({component_map[k]: component_map[v] for k, v in final_dict}) + if not shared_clbits: + for clbit in local_dag.clbits: + if clbit in seen_clbits: + shared_clbits = True + break + seen_clbits.add(clbit) + self.property_set["layout"] = Layout(initial_layout_dict) + # If skip_routing is set then return the layout in the property set + # and throwaway the extra work we did to compute the swap map. + # We also skip routing here if the input circuit is split over multiple + # connected components and there is a shared clbit between any + # components. We can only reliably route the full dag if there is any + # shared classical data. + if self.skip_routing or shared_clbits: + return dag + # After this point the pass is no longer an analysis pass and the + # output circuit returned is transformed with the layout applied + # and swaps inserted + dag = self._apply_layout_no_pass_manager(dag) + mapped_dag = dag.copy_empty_like() + self.property_set["final_layout"] = Layout( + {dag.qubits[k]: v for (k, v) in final_layout_dict.items()} + ) + canonical_register = dag.qregs["q"] + qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} + original_layout = NLayout.generate_trivial_layout(self.coupling_map.size()) + for ( + _layout_dict, + _final_layout_dict, + component_map, + gate_order, + swap_map, + local_dag, + ) in layout_components: + for node_id in gate_order: + node = local_dag._multi_graph[node_id] + process_swaps( + swap_map, + node, + mapped_dag, + original_layout, + canonical_register, + False, + qubit_indices, + component_map, + ) + apply_gate( + mapped_dag, + node, + original_layout, + canonical_register, + False, + initial_layout_dict, + ) + disjoint_utils.combine_barriers(mapped_dag, retain_uuid=False) + return mapped_dag + + def _inner_run(self, dag, coupling_map): + if not coupling_map.is_symmetric: + # deepcopy is needed here to avoid modifications updating + # shared references in passes which require directional + # constraints + coupling_map = copy.deepcopy(coupling_map) + coupling_map.make_symmetric() + neighbor_table = NeighborTable(rx.adjacency_matrix(coupling_map.graph)) + dist_matrix = coupling_map.distance_matrix original_qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} original_clbit_indices = {bit: index for index, bit in enumerate(dag.clbits)} @@ -236,7 +320,7 @@ def run(self, dag): ((initial_layout, final_layout), swap_map, gate_order) = sabre_layout_and_routing( len(dag.clbits), dag_list, - self._neighbor_table, + neighbor_table, dist_matrix, Heuristic.Decay, self.max_iterations, @@ -245,44 +329,14 @@ def run(self, dag): self.seed, ) # Apply initial layout selected. - original_dag = dag layout_dict = {} num_qubits = len(dag.qubits) for k, v in initial_layout.layout_mapping(): if k < num_qubits: layout_dict[dag.qubits[k]] = v - initital_layout = Layout(layout_dict) - self.property_set["layout"] = initital_layout - # If skip_routing is set then return the layout in the property set - # and throwaway the extra work we did to compute the swap map - if self.skip_routing: - return dag - # After this point the pass is no longer an analysis pass and the - # output circuit returned is transformed with the layout applied - # and swaps inserted - dag = self._apply_layout_no_pass_manager(dag) - # Apply sabre swap ontop of circuit with sabre layout - final_layout_mapping = final_layout.layout_mapping() - self.property_set["final_layout"] = Layout( - {dag.qubits[k]: v for (k, v) in final_layout_mapping} - ) - mapped_dag = dag.copy_empty_like() - canonical_register = dag.qregs["q"] - qubit_indices = {bit: idx for idx, bit in enumerate(canonical_register)} - original_layout = NLayout.generate_trivial_layout(self.coupling_map.size()) - for node_id in gate_order: - node = original_dag._multi_graph[node_id] - process_swaps( - swap_map, - node, - mapped_dag, - original_layout, - canonical_register, - False, - qubit_indices, - ) - apply_gate(mapped_dag, node, original_layout, canonical_register, False, layout_dict) - return mapped_dag + final_layout_dict = final_layout.layout_mapping() + component_mapping = {x: coupling_map.graph[x] for x in coupling_map.graph.node_indices()} + return layout_dict, final_layout_dict, component_mapping, gate_order, swap_map, dag def _apply_layout_no_pass_manager(self, dag): """Apply and embed a layout into a dagcircuit without using a ``PassManager`` to diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 04e15cbce94b..3fef57f16f66 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -291,11 +291,18 @@ def process_swaps( canonical_register, fake_run, qubit_indices, + swap_qubit_mapping=None, ): """Process swaps from SwapMap.""" if node._node_id in swap_map: for swap in swap_map[node._node_id]: - swap_qargs = [canonical_register[swap[0]], canonical_register[swap[1]]] + if swap_qubit_mapping: + swap_qargs = [ + canonical_register[swap_qubit_mapping[swap[0]]], + canonical_register[swap_qubit_mapping[swap[1]]], + ] + else: + swap_qargs = [canonical_register[swap[0]], canonical_register[swap[1]]] apply_gate( mapped_dag, DAGOpNode(op=SwapGate(), qargs=swap_qargs), @@ -304,7 +311,12 @@ def process_swaps( fake_run, qubit_indices, ) - current_layout.swap_logical(*swap) + if swap_qubit_mapping: + current_layout.swap_logical( + swap_qubit_mapping[swap[0]], swap_qubit_mapping[swap[1]] + ) + else: + current_layout.swap_logical(*swap) def apply_gate(mapped_dag, node, current_layout, canonical_register, fake_run, qubit_indices): diff --git a/releasenotes/notes/support-disjoint-cmap-sabre-551ae4295131a449.yaml b/releasenotes/notes/support-disjoint-cmap-sabre-551ae4295131a449.yaml new file mode 100644 index 000000000000..806a4e7a12ac --- /dev/null +++ b/releasenotes/notes/support-disjoint-cmap-sabre-551ae4295131a449.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + The :class:`~.SabreLayout` pass now supports running against a target + with a disjoint :class:`~.CouplingMap`. When targeting a disjoint coupling + the input :class:`.DAGCircuit` is split into its connected components of + virtual qubits, each component is mapped to the connected components + of the :class:`~.CouplingMap`, layout is run on each connected + component in isolation, and then all layouts are combined and returned. + Note when the ``routing_pass`` argument is set the pass doesn't + support running with disjoint connectivity. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index a2efb497ec83..0dcc5ba9ba81 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -24,13 +24,14 @@ from test import combine # pylint: disable=wrong-import-order import numpy as np +import rustworkx as rx from qiskit.exceptions import QiskitError from qiskit import BasicAer from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, pulse, qpy, qasm3 from qiskit.circuit import Parameter, Gate, Qubit, Clbit from qiskit.compiler import transpile -from qiskit.dagcircuit import DAGOutNode +from qiskit.dagcircuit import DAGOutNode, DAGOpNode from qiskit.converters import circuit_to_dag from qiskit.circuit.library import ( CXGate, @@ -42,9 +43,12 @@ RZGate, UGate, CZGate, + XGate, + SXGate, ) from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp from qiskit.circuit.measure import Measure +from qiskit.circuit.delay import Delay from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import ( FakeMelbourne, @@ -55,7 +59,7 @@ ) from qiskit.transpiler import Layout, CouplingMap from qiskit.transpiler import PassManager, TransformationPass -from qiskit.transpiler.target import Target +from qiskit.transpiler.target import Target, InstructionProperties from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection from qiskit.quantum_info import Operator, random_unitary @@ -63,6 +67,9 @@ from qiskit.transpiler.preset_passmanagers import level_0_pass_manager from qiskit.tools import parallel from qiskit.pulse import InstructionScheduleMap +from qiskit.providers.backend import BackendV2 +from qiskit.providers.options import Options +from qiskit.test import slow_test class CustomCX(Gate): @@ -2026,3 +2033,436 @@ def test_backend_and_custom_gate(self, opt_level): 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) + + +@ddt +class TestTranspileMultiChipTarget(QiskitTestCase): + """Test transpile() with a disjoint coupling map.""" + + def setUp(self): + super().setUp() + + class FakeMultiChip(BackendV2): + """Fake multi chip backend.""" + + def __init__(self): + super().__init__() + graph = rx.generators.directed_heavy_hex_graph(3) + num_qubits = len(graph) * 3 + rng = np.random.default_rng(seed=12345678942) + rz_props = {} + x_props = {} + sx_props = {} + measure_props = {} + delay_props = {} + self._target = Target("Fake multi-chip backend", num_qubits=num_qubits) + for i in range(num_qubits): + qarg = (i,) + rz_props[qarg] = InstructionProperties(error=0.0, duration=0.0) + x_props[qarg] = InstructionProperties( + error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7) + ) + sx_props[qarg] = InstructionProperties( + error=rng.uniform(1e-6, 1e-4), duration=rng.uniform(1e-8, 9e-7) + ) + measure_props[qarg] = InstructionProperties( + error=rng.uniform(1e-3, 1e-1), duration=rng.uniform(1e-8, 9e-7) + ) + delay_props[qarg] = None + self._target.add_instruction(XGate(), x_props) + self._target.add_instruction(SXGate(), sx_props) + self._target.add_instruction(RZGate(Parameter("theta")), rz_props) + self._target.add_instruction(Measure(), measure_props) + self._target.add_instruction(Delay(Parameter("t")), delay_props) + cz_props = {} + for i in range(3): + for root_edge in graph.edge_list(): + offset = i * len(graph) + edge = (root_edge[0] + offset, root_edge[1] + offset) + cz_props[edge] = InstructionProperties( + error=rng.uniform(1e-5, 5e-3), duration=rng.uniform(1e-8, 9e-7) + ) + self._target.add_instruction(CZGate(), cz_props) + + @property + def target(self): + return self._target + + @property + def max_circuits(self): + return None + + @classmethod + def _default_options(cls): + return Options(shots=1024) + + def run(self, circuit, **kwargs): + raise NotImplementedError + + self.backend = FakeMultiChip() + + # Add level 0 and 1 when TrivialLayout supports disjoint coupling maps + @data(2, 3) + def test_basic_connected_circuit(self, opt_level): + """Test basic connected circuit on disjoint backend""" + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.measure_all() + tqc = transpile(qc, self.backend, optimization_level=opt_level) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + + # Add level 0 and 1 when TrivialLayout supports disjoint coupling maps + @data(2, 3) + def test_triple_circuit(self, opt_level): + """Test a split circuit with one circuit component per chip.""" + qc = QuantumCircuit(30) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.measure_all() + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=42) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + + # Add level 0 and 1 when TrivialLayout supports disjoint coupling maps + # Tagged as slow until #9834 is fixed + @slow_test + @data(2, 3) + def test_six_component_circuit(self, opt_level): + """Test input circuit with more than 1 component per backend component.""" + qc = QuantumCircuit(42) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.h(30) + qc.cx(30, 31) + qc.cx(30, 32) + qc.cx(30, 33) + qc.h(34) + qc.cx(34, 35) + qc.cx(34, 36) + qc.cx(34, 37) + qc.h(38) + qc.cx(38, 39) + qc.cx(39, 40) + qc.cx(39, 41) + qc.measure_all() + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=42) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + + @data(2, 3) + def test_shared_classical_between_components_condition(self, opt_level): + """Test a condition sharing classical bits between components.""" + creg = ClassicalRegister(19) + qc = QuantumCircuit(25) + qc.add_register(creg) + qc.h(0) + for i in range(18): + qc.cx(0, i + 1) + for i in range(18): + qc.measure(i, creg[i]) + + qc.ecr(20, 21).c_if(creg, 0) + tqc = transpile(qc, self.backend, optimization_level=opt_level) + + def _visit_block(circuit, qubit_mapping=None): + for instruction in circuit: + if instruction.operation.name == "barrier": + continue + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue( + self.backend.target.instruction_supported(instruction.operation.name, qargs) + ) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + + _visit_block( + tqc, + qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, + ) + + @data(2, 3) + def test_shared_classical_between_components_condition_large_to_small(self, opt_level): + """Test a condition sharing classical bits between components.""" + creg = ClassicalRegister(2) + qc = QuantumCircuit(25) + qc.add_register(creg) + qc.h(24) + qc.cx(24, 23) + qc.measure(24, creg[0]) + qc.measure(23, creg[1]) + qc.h(0).c_if(creg, 0) + for i in range(18): + qc.ecr(0, i + 1).c_if(creg, 0) + tqc = transpile(qc, self.backend, optimization_level=opt_level) + + def _visit_block(circuit, qubit_mapping=None): + for instruction in circuit: + if instruction.operation.name == "barrier": + continue + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue( + self.backend.target.instruction_supported(instruction.operation.name, qargs) + ) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + + _visit_block( + tqc, + qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, + ) + # Check clbits are in order + # Traverse the output dag over the sole clbit. Checking that the qubits of the ops + # go in order between the components. This is a sanity check to ensure that routing + # doesn't reorder a classical data dependency between components. Inside a component + # we have the dag ordering so nothing should be out of order within a component. + initial_layout = tqc.layout.initial_layout + first_component = {qc.qubits[23], qc.qubits[24]} + second_component = {qc.qubits[i] for i in range(19)} + tqc_dag = circuit_to_dag(tqc) + qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} + input_node = tqc_dag.input_map[tqc_dag.clbits[0]] + first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + # The first node should be a measurement + self.assertIsInstance(first_meas_node.op, Measure) + # This shoulde be in the first ocmponent + self.assertIn(initial_layout._p2v[qubit_map[first_meas_node.qargs[0]]], first_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + while isinstance(op_node, DAGOpNode): + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + + @data(2, 3) + def test_shared_classical_between_components_condition_large_to_small_reverse_index( + self, opt_level + ): + """Test a condition sharing classical bits between components.""" + creg = ClassicalRegister(2) + qc = QuantumCircuit(25) + qc.add_register(creg) + qc.h(0) + qc.cx(0, 1) + qc.measure(0, creg[0]) + qc.measure(1, creg[1]) + qc.h(24).c_if(creg, 0) + for i in range(23, 5, -1): + qc.ecr(24, i).c_if(creg, 0) + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=2023) + + def _visit_block(circuit, qubit_mapping=None): + for instruction in circuit: + if instruction.operation.name == "barrier": + continue + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue( + self.backend.target.instruction_supported(instruction.operation.name, qargs) + ) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + + _visit_block( + tqc, + qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, + ) + # Check clbits are in order + # Traverse the output dag over the sole clbit. Checking that the qubits of the ops + # go in order between the components. This is a sanity check to ensure that routing + # doesn't reorder a classical data dependency between components. Inside a component + # we have the dag ordering so nothing should be out of order within a component. + initial_layout = tqc.layout.initial_layout + first_component = {qc.qubits[i] for i in range(2)} + second_component = {qc.qubits[i] for i in range(6, 25)} + tqc_dag = circuit_to_dag(tqc) + qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} + input_node = tqc_dag.input_map[tqc_dag.clbits[0]] + first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + # The first node should be a measurement + self.assertIsInstance(first_meas_node.op, Measure) + # This shoulde be in the first ocmponent + self.assertIn(initial_layout._p2v[qubit_map[first_meas_node.qargs[0]]], first_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + while isinstance(op_node, DAGOpNode): + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + + @data(2, 3) + def test_chained_data_dependency(self, opt_level): + """Test 3 component circuit with shared clbits between each component.""" + creg = ClassicalRegister(1) + qc = QuantumCircuit(30) + qc.add_register(creg) + # Component 1 + qc.h(0) + for i in range(9): + qc.cx(0, i + 1) + measure_op = Measure() + qc.append(measure_op, [9], [creg[0]]) + # Component 2 + qc.h(10).c_if(creg, 0) + for i in range(11, 20): + qc.ecr(10, i).c_if(creg, 0) + measure_op = Measure() + qc.append(measure_op, [19], [creg[0]]) + # Component 3 + qc.h(20).c_if(creg, 0) + for i in range(21, 30): + qc.cz(20, i).c_if(creg, 0) + measure_op = Measure() + qc.append(measure_op, [29], [creg[0]]) + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=2023) + + def _visit_block(circuit, qubit_mapping=None): + for instruction in circuit: + if instruction.operation.name == "barrier": + continue + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue( + self.backend.target.instruction_supported(instruction.operation.name, qargs) + ) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + + _visit_block( + tqc, + qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, + ) + # Check clbits are in order + # Traverse the output dag over the sole clbit. Checking that the qubits of the ops + # go in order between the components. This is a sanity check to ensure that routing + # doesn't reorder a classical data dependency between components. Inside a component + # we have the dag ordering so nothing should be incompatible there. + + initial_layout = tqc.layout.initial_layout + first_component = {qc.qubits[i] for i in range(10)} + second_component = {qc.qubits[i] for i in range(10, 20)} + third_component = {qc.qubits[i] for i in range(20, 30)} + tqc_dag = circuit_to_dag(tqc) + qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} + input_node = tqc_dag.input_map[tqc_dag.clbits[0]] + first_meas_node = tqc_dag._multi_graph.find_successors_by_edge( + input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + self.assertIsInstance(first_meas_node.op, Measure) + self.assertIn(initial_layout._p2v[qubit_map[first_meas_node.qargs[0]]], first_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + while not isinstance(op_node.op, Measure): + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + while not isinstance(op_node.op, Measure): + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], third_component) + op_node = tqc_dag._multi_graph.find_successors_by_edge( + op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) + )[0] + self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], third_component) diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 7a0ebbfba692..2dc639439308 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -17,6 +17,7 @@ from qiskit import QuantumRegister, QuantumCircuit from qiskit.transpiler import CouplingMap from qiskit.transpiler.passes import SabreLayout +from qiskit.transpiler.exceptions import TranspilerError from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase from qiskit.compiler.transpiler import transpile @@ -217,5 +218,143 @@ def test_layout_many_search_trials(self): ) +class TestDisjointDeviceSabreLayout(QiskitTestCase): + """Test SabreLayout with a disjoint coupling map.""" + + def setUp(self): + super().setUp() + self.dual_grid_cmap = CouplingMap( + [[0, 1], [0, 2], [1, 3], [2, 3], [4, 5], [4, 6], [5, 7], [5, 8]] + ) + + def test_dual_ghz(self): + """Test a basic example with 2 circuit components and 2 cmap components.""" + qc = QuantumCircuit(8, name="double dhz") + qc.h(0) + qc.cz(0, 1) + qc.cz(0, 2) + qc.h(3) + qc.cx(3, 4) + qc.cx(3, 5) + qc.cx(3, 6) + qc.cx(3, 7) + layout_routing_pass = SabreLayout( + self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 + ) + out = layout_routing_pass(qc) + layout = layout_routing_pass.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual(1, out.count_ops()["swap"]) + edge_set = set(self.dual_grid_cmap.graph.edge_list()) + for gate in out.data: + if len(gate.qubits) == 2: + qubits = tuple(out.find_bit(x).index for x in gate.qubits) + # Handle reverse edges which will be fixed by gate direction + # later + if qubits not in edge_set: + self.assertIn((qubits[1], qubits[0]), edge_set) + + def test_dual_ghz_with_wide_barrier(self): + """Test a basic example with 2 circuit components and 2 cmap components.""" + qc = QuantumCircuit(8, name="double dhz") + qc.h(0) + qc.cz(0, 1) + qc.cz(0, 2) + qc.h(3) + qc.cx(3, 4) + qc.cx(3, 5) + qc.cx(3, 6) + qc.cx(3, 7) + qc.measure_all() + layout_routing_pass = SabreLayout( + self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 + ) + out = layout_routing_pass(qc) + layout = layout_routing_pass.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual(1, out.count_ops()["swap"]) + edge_set = set(self.dual_grid_cmap.graph.edge_list()) + for gate in out.data: + if len(gate.qubits) == 2: + qubits = tuple(out.find_bit(x).index for x in gate.qubits) + # Handle reverse edges which will be fixed by gate direction + # later + if qubits not in edge_set: + self.assertIn((qubits[1], qubits[0]), edge_set) + + def test_dual_ghz_with_intermediate_barriers(self): + """Test dual ghz circuit with intermediate barriers local to each componennt.""" + qc = QuantumCircuit(8, name="double dhz") + qc.h(0) + qc.cz(0, 1) + qc.cz(0, 2) + qc.barrier(0, 1, 2) + qc.h(3) + qc.cx(3, 4) + qc.cx(3, 5) + qc.barrier(4, 5, 6) + qc.cx(3, 6) + qc.cx(3, 7) + qc.measure_all() + layout_routing_pass = SabreLayout( + self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 + ) + out = layout_routing_pass(qc) + layout = layout_routing_pass.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual(1, out.count_ops()["swap"]) + edge_set = set(self.dual_grid_cmap.graph.edge_list()) + for gate in out.data: + if len(gate.qubits) == 2: + qubits = tuple(out.find_bit(x).index for x in gate.qubits) + # Handle reverse edges which will be fixed by gate direction + # later + if qubits not in edge_set: + self.assertIn((qubits[1], qubits[0]), edge_set) + + def test_dual_ghz_with_intermediate_spanning_barriers(self): + """Test dual ghz circuit with barrier in the middle across components.""" + qc = QuantumCircuit(8, name="double dhz") + qc.h(0) + qc.cz(0, 1) + qc.cz(0, 2) + qc.barrier(0, 1, 2, 4, 5) + qc.h(3) + qc.cx(3, 4) + qc.cx(3, 5) + qc.cx(3, 6) + qc.cx(3, 7) + qc.measure_all() + layout_routing_pass = SabreLayout( + self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 + ) + out = layout_routing_pass(qc) + layout = layout_routing_pass.property_set["layout"] + self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) + self.assertEqual(1, out.count_ops()["swap"]) + edge_set = set(self.dual_grid_cmap.graph.edge_list()) + for gate in out.data: + if len(gate.qubits) == 2: + qubits = tuple(out.find_bit(x).index for x in gate.qubits) + # Handle reverse edges which will be fixed by gate direction + # later + if qubits not in edge_set: + self.assertIn((qubits[1], qubits[0]), edge_set) + + def test_too_large_components(self): + """Assert trying to run a circuit with too large a connected component raises.""" + qc = QuantumCircuit(8) + qc.h(0) + for i in range(1, 6): + qc.cx(0, i) + qc.h(7) + qc.cx(7, 6) + layout_routing_pass = SabreLayout( + self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 + ) + with self.assertRaises(TranspilerError): + layout_routing_pass(qc) + + if __name__ == "__main__": unittest.main() From 7b147a022010c2b64b7119b7e70f67e96db9c9b3 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Thu, 13 Apr 2023 22:14:10 +0200 Subject: [PATCH 021/172] new unroll for-loops transpilation pass (#9670) * dynamic circuit optimization: unroll for loops * exceptions * check inside conditional blocks * docs * reno * Update qiskit/transpiler/passes/optimization/unroll_forloops.py Co-authored-by: Jake Lishman * parameterless support * moved to utils * no classmethod, but function * docstring and __init__ * Update qiskit/transpiler/passes/optimization/unroll_forloops.py Co-authored-by: Jake Lishman * Update test/python/transpiler/test_unroll_forloops.py Co-authored-by: Jake Lishman * nested for-loops test * docstring note * new test with c_if * Remove commented-out code --------- Co-authored-by: Jake Lishman --- qiskit/transpiler/passes/__init__.py | 2 + qiskit/transpiler/passes/utils/__init__.py | 1 + .../passes/utils/unroll_forloops.py | 79 +++++++++ .../unroll-forloops-7bf8000620f738e7.yaml | 6 + .../python/transpiler/test_unroll_forloops.py | 166 ++++++++++++++++++ 5 files changed, 254 insertions(+) create mode 100644 qiskit/transpiler/passes/utils/unroll_forloops.py create mode 100644 releasenotes/notes/unroll-forloops-7bf8000620f738e7.yaml create mode 100644 test/python/transpiler/test_unroll_forloops.py diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 662eb6c3324c..73e6ea055165 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -176,6 +176,7 @@ ContainsInstruction GatesInBasis ConvertConditionsToIfOps + UnrollForLoops """ # layout selection (placement) @@ -290,3 +291,4 @@ from .utils import ContainsInstruction from .utils import GatesInBasis from .utils import ConvertConditionsToIfOps +from .utils import UnrollForLoops diff --git a/qiskit/transpiler/passes/utils/__init__.py b/qiskit/transpiler/passes/utils/__init__.py index c69244b20b4a..fd2d00bbc4b6 100644 --- a/qiskit/transpiler/passes/utils/__init__.py +++ b/qiskit/transpiler/passes/utils/__init__.py @@ -27,6 +27,7 @@ from .contains_instruction import ContainsInstruction from .gates_basis import GatesInBasis from .convert_conditions_to_if_ops import ConvertConditionsToIfOps +from .unroll_forloops import UnrollForLoops from .minimum_point import MinimumPoint # Utility functions diff --git a/qiskit/transpiler/passes/utils/unroll_forloops.py b/qiskit/transpiler/passes/utils/unroll_forloops.py new file mode 100644 index 000000000000..c83a47f72eec --- /dev/null +++ b/qiskit/transpiler/passes/utils/unroll_forloops.py @@ -0,0 +1,79 @@ +# 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. + +""" UnrollForLoops transpilation pass """ + +from qiskit.circuit import ForLoopOp, ContinueLoopOp, BreakLoopOp, IfElseOp +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.passes.utils import control_flow +from qiskit.converters import circuit_to_dag + + +class UnrollForLoops(TransformationPass): + """``UnrollForLoops`` transpilation pass unrolls for-loops when possible.""" + + def __init__(self, max_target_depth=-1): + """Things like `for x in {0, 3, 4} {rx(x) qr[1];}` will turn into + `rx(0) qr[1]; rx(3) qr[1]; rx(4) qr[1];`. + + .. note:: + The ``UnrollForLoops`` unrolls only one level of block depth. No inner loop will + be considered by ``max_target_depth``. + + Args: + max_target_depth (int): Optional. Checks if the unrolled block is over a particular + subcircuit depth. To disable the check, use ``-1`` (Default). + """ + super().__init__() + self.max_target_depth = max_target_depth + + @control_flow.trivial_recurse + def run(self, dag): + """Run the UnrollForLoops pass on `dag`. + + Args: + dag (DAGCircuit): the directed acyclic graph to run on. + + Returns: + DAGCircuit: Transformed DAG. + """ + for forloop_op in dag.op_nodes(ForLoopOp): + (indexset, loop_param, body) = forloop_op.op.params + + # skip unrolling if it results in bigger than max_target_depth + if 0 < self.max_target_depth < len(indexset) * body.depth(): + continue + + # skip unroll when break_loop or continue_loop inside body + if _body_contains_continue_or_break(body): + continue + + unrolled_dag = circuit_to_dag(body).copy_empty_like() + for index_value in indexset: + bound_body = body.bind_parameters({loop_param: index_value}) if loop_param else body + unrolled_dag.compose(circuit_to_dag(bound_body), inplace=True) + dag.substitute_node_with_dag(forloop_op, unrolled_dag) + + return dag + + +def _body_contains_continue_or_break(circuit): + """Checks if a circuit contains ``continue``s or ``break``s. Conditional bodies are inspected.""" + for inst in circuit.data: + operation = inst.operation + if isinstance(operation, (ContinueLoopOp, BreakLoopOp)): + return True + if isinstance(operation, IfElseOp): + for block in operation.params: + if _body_contains_continue_or_break(block): + return True + return False diff --git a/releasenotes/notes/unroll-forloops-7bf8000620f738e7.yaml b/releasenotes/notes/unroll-forloops-7bf8000620f738e7.yaml new file mode 100644 index 000000000000..d224c6705fb4 --- /dev/null +++ b/releasenotes/notes/unroll-forloops-7bf8000620f738e7.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The transpiler pass :class:`~.UnrollForLoops` was added. It unrolls for-loops when possible (if + no :class:`~.ContinueLoopOp` or a :class:`~.BreakLoopOp` is inside the body block). + For example ``for x in {0, 3, 4} {rx(x) qr[1];}`` gets converted into ``rx(0) qr[1]; rx(3) qr[1]; rx(4) qr[1];``. diff --git a/test/python/transpiler/test_unroll_forloops.py b/test/python/transpiler/test_unroll_forloops.py new file mode 100644 index 000000000000..ffff53761a72 --- /dev/null +++ b/test/python/transpiler/test_unroll_forloops.py @@ -0,0 +1,166 @@ +# 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 the UnrollForLoops pass""" + +import unittest + +from qiskit.circuit import QuantumCircuit, Parameter, QuantumRegister, ClassicalRegister +from qiskit.transpiler import PassManager +from qiskit.test import QiskitTestCase +from qiskit.transpiler.passes.utils.unroll_forloops import UnrollForLoops + + +class TestUnrollForLoops(QiskitTestCase): + """Test UnrollForLoops pass""" + + def test_range(self): + """Check simples unrolling case""" + qreg, creg = QuantumRegister(5, "q"), ClassicalRegister(2, "c") + + body = QuantumCircuit(3, 1) + loop_parameter = Parameter("foo") + indexset = range(0, 10, 2) + + body.rx(loop_parameter, [0, 1, 2]) + + circuit = QuantumCircuit(qreg, creg) + circuit.for_loop(indexset, loop_parameter, body, [1, 2, 3], [1]) + + expected = QuantumCircuit(qreg, creg) + for index_loop in indexset: + expected.rx(index_loop, [1, 2, 3]) + + passmanager = PassManager() + passmanager.append(UnrollForLoops()) + result = passmanager.run(circuit) + + self.assertEqual(result, expected) + + def test_parameterless_range(self): + """Check simples unrolling case when there is not parameter""" + qreg, creg = QuantumRegister(5, "q"), ClassicalRegister(2, "c") + + body = QuantumCircuit(3, 1) + indexset = range(0, 10, 2) + + body.h([0, 1, 2]) + + circuit = QuantumCircuit(qreg, creg) + circuit.for_loop(indexset, None, body, [1, 2, 3], [1]) + + expected = QuantumCircuit(qreg, creg) + for _ in indexset: + expected.h([1, 2, 3]) + + passmanager = PassManager() + passmanager.append(UnrollForLoops()) + result = passmanager.run(circuit) + + self.assertEqual(result, expected) + + def test_nested_forloop(self): + """Test unrolls only one level of nested for-loops""" + circuit = QuantumCircuit(1) + twice = range(2) + with circuit.for_loop(twice): + with circuit.for_loop(twice): + circuit.h(0) + + expected = QuantumCircuit(1) + for _ in twice: + for _ in twice: + expected.h(0) + + passmanager = PassManager() + passmanager.append(UnrollForLoops()) + result = passmanager.run(circuit) + + self.assertEqual(result, expected) + + def test_skip_continue_loop(self): + """Unrolling should not be done when a `continue;` in the body""" + parameter = Parameter("x") + loop_body = QuantumCircuit(1) + loop_body.rx(parameter, 0) + loop_body.continue_loop() + + qc = QuantumCircuit(2) + qc.for_loop([0, 3, 4], parameter, loop_body, [1], []) + qc.x(0) + + passmanager = PassManager() + passmanager.append(UnrollForLoops()) + result = passmanager.run(qc) + + self.assertEqual(result, qc) + + def test_skip_continue_in_conditional(self): + """Unrolling should not be done when a `continue;` is in a nested condition""" + parameter = Parameter("x") + + true_body = QuantumCircuit(1) + true_body.continue_loop() + false_body = QuantumCircuit(1) + false_body.rx(parameter, 0) + + qr = QuantumRegister(2, name="qr") + cr = ClassicalRegister(2, name="cr") + loop_body = QuantumCircuit(qr, cr) + loop_body.if_else((cr, 0), true_body, false_body, [1], []) + loop_body.x(0) + + qc = QuantumCircuit(qr, cr) + qc.for_loop([0, 3, 4], parameter, loop_body, qr, cr) + + passmanager = PassManager() + passmanager.append(UnrollForLoops()) + result = passmanager.run(qc) + + self.assertEqual(result, qc) + + def test_skip_continue_c_if(self): + """Unrolling should not be done when a break in the c_if in the body""" + circuit = QuantumCircuit(2, 1) + with circuit.for_loop(range(2)): + circuit.h(0) + circuit.cx(0, 1) + circuit.measure(0, 0) + circuit.break_loop().c_if(0, True) + + passmanager = PassManager() + passmanager.append(UnrollForLoops()) + result = passmanager.run(circuit) + + self.assertEqual(result, circuit) + + def test_max_target_depth(self): + """Unrolling should not be done when results over `max_target_depth`""" + + loop_parameter = Parameter("foo") + indexset = range(0, 10, 2) + body = QuantumCircuit(3, 1) + body.rx(loop_parameter, [0, 1, 2]) + + qreg, creg = QuantumRegister(5, "q"), ClassicalRegister(2, "c") + circuit = QuantumCircuit(qreg, creg) + circuit.for_loop(indexset, loop_parameter, body, [1, 2, 3], [1]) + + passmanager = PassManager() + passmanager.append(UnrollForLoops(max_target_depth=2)) + result = passmanager.run(circuit) + + self.assertEqual(result, circuit) + + +if __name__ == "__main__": + unittest.main() From 65b4e3e3af2f89a32370887414c067b13dea2359 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 13 Apr 2023 22:01:28 +0100 Subject: [PATCH 022/172] Correctly error on incorrect clbits in `QuantumCircuit.append` (#9386) * Correctly error on incorrect clbits in `QuantumCircuit.append` Previously, the clbits were unchecked, which could allow incorrect use of `append` that might later explode in an unrelated place. Making this change has turned up a few places in the Terra tests that also build invalid assumptions. * Fix incorrect tests * Fix lint * Apply suggestions from code review Co-authored-by: Julien Gacon --------- Co-authored-by: Julien Gacon --- qiskit/circuit/instruction.py | 5 +++++ ...d-bad-argument-error-cc9eafe94cc39033.yaml | 5 +++++ .../python/circuit/test_circuit_operations.py | 14 +++++++++++-- .../circuit/test_control_flow_builders.py | 2 +- test/python/transpiler/test_check_map.py | 20 +++++++++---------- .../python/transpiler/test_clifford_passes.py | 10 ++-------- .../test_linear_functions_passes.py | 2 +- .../test_optimize_1q_decomposition.py | 4 ++-- .../test_optimize_swap_before_measure.py | 6 ++++-- .../python/transpiler/test_remove_barriers.py | 6 ++++-- ...st_remove_diagonal_gates_before_measure.py | 6 ++++-- ...test_reset_after_measure_simplification.py | 6 ++++-- .../transpiler/test_unitary_synthesis.py | 2 +- .../test_unroll_custom_definitions.py | 8 ++++---- 14 files changed, 59 insertions(+), 37 deletions(-) create mode 100644 releasenotes/notes/append-bad-argument-error-cc9eafe94cc39033.yaml diff --git a/qiskit/circuit/instruction.py b/qiskit/circuit/instruction.py index 7623892a6124..6721983d3bc9 100644 --- a/qiskit/circuit/instruction.py +++ b/qiskit/circuit/instruction.py @@ -481,6 +481,11 @@ def broadcast_arguments(self, qargs, cargs): f"The amount of qubit arguments {len(qargs)} does not match" f" the instruction expectation ({self.num_qubits})." ) + if len(cargs) != self.num_clbits: + raise CircuitError( + f"The amount of clbit arguments {len(cargs)} does not match" + f" the instruction expectation ({self.num_clbits})." + ) # [[q[0], q[1]], [c[0], c[1]]] -> [q[0], c[0]], [q[1], c[1]] flat_qargs = [qarg for sublist in qargs for qarg in sublist] diff --git a/releasenotes/notes/append-bad-argument-error-cc9eafe94cc39033.yaml b/releasenotes/notes/append-bad-argument-error-cc9eafe94cc39033.yaml new file mode 100644 index 000000000000..c9addba7ac45 --- /dev/null +++ b/releasenotes/notes/append-bad-argument-error-cc9eafe94cc39033.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + :meth:`.QuantumCircuit.append` will now correctly raise an error if given an incorrect number of + classical bits to apply to an operation. Fix `#9385 `__. diff --git a/test/python/circuit/test_circuit_operations.py b/test/python/circuit/test_circuit_operations.py index 2612f2cfb8c9..fe17bdeb2836 100644 --- a/test/python/circuit/test_circuit_operations.py +++ b/test/python/circuit/test_circuit_operations.py @@ -147,6 +147,16 @@ def test_append_rejects_wrong_types(self, specifier): with self.subTest("c list"), self.assertRaisesRegex(CircuitError, "Invalid bit index"): test.append(opaque, [[0]], [[specifier]]) + @data([], [0], [0, 1, 2]) + def test_append_rejects_bad_arguments_opaque(self, bad_arg): + """Test that a suitable exception is raised when there is an argument mismatch.""" + inst = QuantumCircuit(2, 2).to_instruction() + qc = QuantumCircuit(3, 3) + with self.assertRaisesRegex(CircuitError, "The amount of qubit arguments"): + qc.append(inst, bad_arg, [0, 1]) + with self.assertRaisesRegex(CircuitError, "The amount of clbit arguments"): + qc.append(inst, [0, 1], bad_arg) + def test_anding_self(self): """Test that qc &= qc finishes, which can be prone to infinite while-loops. @@ -1123,9 +1133,9 @@ def test_from_instructions(self): a, b, c, d = qreg x, y, z = creg - circuit_1 = QuantumCircuit(2) + circuit_1 = QuantumCircuit(2, 1) circuit_1.x(0) - circuit_2 = QuantumCircuit(2) + circuit_2 = QuantumCircuit(2, 1) circuit_2.y(0) def instructions(): diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index 58da7c84ee2b..151249b652a0 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -91,7 +91,7 @@ def test_if_register(self): expected = QuantumCircuit(qr, cr) expected.measure(qr, cr) - expected.if_test((cr, 0), if_true0, [qr[0]], [cr[:]]) + expected.if_test((cr, 0), if_true0, [qr[0]], cr) self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) diff --git a/test/python/transpiler/test_check_map.py b/test/python/transpiler/test_check_map.py index f5283b240366..8368e3cc94eb 100644 --- a/test/python/transpiler/test_check_map.py +++ b/test/python/transpiler/test_check_map.py @@ -132,7 +132,7 @@ def test_swap_mapped_cf_true(self): qr = QuantumRegister(3) cr = ClassicalRegister(3) circuit = QuantumCircuit(qr, cr) - true_body = QuantumCircuit(qr) + true_body = QuantumCircuit(qr, cr) true_body.swap(0, 1) true_body.cx(2, 1) circuit.if_else((cr[0], 0), true_body, None, qr, cr) @@ -150,7 +150,7 @@ def test_swap_mapped_cf_false(self): circuit = QuantumCircuit(qr, cr) true_body = QuantumCircuit(qr) true_body.cx(0, 2) - circuit.if_else((cr[0], 0), true_body, None, qr, cr) + circuit.if_else((cr[0], 0), true_body, None, qr, []) dag = circuit_to_dag(circuit) pass_ = CheckMap(coupling) pass_.run(dag) @@ -163,7 +163,7 @@ def test_swap_mapped_cf_layout_change_false(self): qr = QuantumRegister(3) cr = ClassicalRegister(3) circuit = QuantumCircuit(qr, cr) - true_body = QuantumCircuit(qr) + true_body = QuantumCircuit(qr, cr) true_body.cx(1, 2) circuit.if_else((cr[0], 0), true_body, None, qr[[1, 0, 2]], cr) dag = circuit_to_dag(circuit) @@ -180,7 +180,7 @@ def test_swap_mapped_cf_layout_change_true(self): circuit = QuantumCircuit(qr, cr) true_body = QuantumCircuit(qr) true_body.cx(0, 2) - circuit.if_else((cr[0], 0), true_body, None, qr[[1, 0, 2]], cr) + circuit.if_else((cr[0], 0), true_body, None, qr[[1, 0, 2]], []) dag = circuit_to_dag(circuit) pass_ = CheckMap(coupling) pass_.run(dag) @@ -193,9 +193,9 @@ def test_swap_mapped_cf_different_bits(self): qr = QuantumRegister(3) cr = ClassicalRegister(3) circuit = QuantumCircuit(qr, cr) - true_body = QuantumCircuit(3) + true_body = QuantumCircuit(3, 1) true_body.cx(0, 2) - circuit.if_else((cr[0], 0), true_body, None, qr[[1, 0, 2]], cr) + circuit.if_else((cr[0], 0), true_body, None, qr[[1, 0, 2]], [cr[0]]) dag = circuit_to_dag(circuit) pass_ = CheckMap(coupling) pass_.run(dag) @@ -209,9 +209,9 @@ def test_disjoint_controlflow_bits(self): qr2 = QuantumRegister(3, "qrif") cr = ClassicalRegister(3) circuit = QuantumCircuit(qr1, cr) - true_body = QuantumCircuit(qr2) + true_body = QuantumCircuit(qr2, [cr[0]]) true_body.cx(0, 2) - circuit.if_else((cr[0], 0), true_body, None, qr1[[1, 0, 2]], cr) + circuit.if_else((cr[0], 0), true_body, None, qr1[[1, 0, 2]], [cr[0]]) dag = circuit_to_dag(circuit) pass_ = CheckMap(coupling) pass_.run(dag) @@ -229,7 +229,7 @@ def test_nested_controlflow_true(self): true_body = QuantumCircuit(qr2, cr2) for_body = QuantumCircuit(3) for_body.cx(0, 2) - true_body.for_loop(range(5), body=for_body, qubits=qr2, clbits=cr2) + true_body.for_loop(range(5), body=for_body, qubits=qr2, clbits=[]) circuit.if_else((cr1[0], 0), true_body, None, qr1[[1, 0, 2]], cr1) dag = circuit_to_dag(circuit) pass_ = CheckMap(coupling) @@ -248,7 +248,7 @@ def test_nested_controlflow_false(self): true_body = QuantumCircuit(qr2, cr2) for_body = QuantumCircuit(3) for_body.cx(0, 2) - true_body.for_loop(range(5), body=for_body, qubits=qr2, clbits=cr2) + true_body.for_loop(range(5), body=for_body, qubits=qr2, clbits=[]) circuit.if_else((cr1[0], 0), true_body, None, qr1[[0, 1, 2]], cr1) dag = circuit_to_dag(circuit) pass_ = CheckMap(coupling) diff --git a/test/python/transpiler/test_clifford_passes.py b/test/python/transpiler/test_clifford_passes.py index b610f59fa1ff..8b0600059f60 100644 --- a/test/python/transpiler/test_clifford_passes.py +++ b/test/python/transpiler/test_clifford_passes.py @@ -244,18 +244,12 @@ def test_if_else(self): test = QuantumCircuit(cliff1.num_qubits, 1) test.measure(0, 0) - test.if_else( - (test.clbits[0], True), inner_test.copy(), inner_test.copy(), test.qubits, test.clbits - ) + test.if_else((test.clbits[0], True), inner_test.copy(), inner_test.copy(), test.qubits, []) expected = QuantumCircuit(combined.num_qubits, 1) expected.measure(0, 0) expected.if_else( - (expected.clbits[0], True), - inner_expected, - inner_expected, - expected.qubits, - expected.clbits, + (expected.clbits[0], True), inner_expected, inner_expected, expected.qubits, [] ) self.assertEqual(OptimizeCliffords()(test), expected) diff --git a/test/python/transpiler/test_linear_functions_passes.py b/test/python/transpiler/test_linear_functions_passes.py index e6e7336e799f..5c09f6d46ea5 100644 --- a/test/python/transpiler/test_linear_functions_passes.py +++ b/test/python/transpiler/test_linear_functions_passes.py @@ -417,7 +417,7 @@ def test_if_else(self, do_commutative_analysis): """Test that collection recurses into a simple if-else.""" pass_ = CollectLinearFunctions(do_commutative_analysis=do_commutative_analysis) - circuit = QuantumCircuit(4) + circuit = QuantumCircuit(4, 1) circuit.cx(0, 1) circuit.cx(1, 0) circuit.cx(2, 3) diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index a65f4c8e2beb..f9b9759934c8 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -684,7 +684,7 @@ def test_if_else(self): test = QuantumCircuit(qr, cr) test.h(0) test.measure(0, 0) - test_true = QuantumCircuit(qr) + test_true = QuantumCircuit(qr, cr) test_true.h(qr[0]) test_true.h(qr[0]) test_true.h(qr[0]) @@ -693,7 +693,7 @@ def test_if_else(self): expected = QuantumCircuit(qr, cr) expected.u(np.pi / 2, 0, -np.pi, 0) expected.measure(0, 0) - expected_true = QuantumCircuit(qr) + expected_true = QuantumCircuit(qr, cr) expected_true.u(np.pi / 2, 0, -np.pi, qr[0]) expected.if_else((0, True), expected_true, None, range(num_qubits), [0]) diff --git a/test/python/transpiler/test_optimize_swap_before_measure.py b/test/python/transpiler/test_optimize_swap_before_measure.py index 4daea21aab1f..0fe4f3937795 100644 --- a/test/python/transpiler/test_optimize_swap_before_measure.py +++ b/test/python/transpiler/test_optimize_swap_before_measure.py @@ -199,10 +199,12 @@ def test_nested_control_flow(self): base_expected.measure(1, 0) body_test = QuantumCircuit(2, 1) - body_test.for_loop((0,), None, base_expected.copy(), body_test.qubits, []) + body_test.for_loop((0,), None, base_expected.copy(), body_test.qubits, body_test.clbits) body_expected = QuantumCircuit(2, 1) - body_expected.for_loop((0,), None, base_expected.copy(), body_expected.qubits, []) + body_expected.for_loop( + (0,), None, base_expected.copy(), body_expected.qubits, body_expected.clbits + ) test = QuantumCircuit(2, 1) test.while_loop((test.clbits[0], True), body_test, test.qubits, test.clbits) diff --git a/test/python/transpiler/test_remove_barriers.py b/test/python/transpiler/test_remove_barriers.py index c04bf3db4773..95efc9f79eb6 100644 --- a/test/python/transpiler/test_remove_barriers.py +++ b/test/python/transpiler/test_remove_barriers.py @@ -91,10 +91,12 @@ def test_nested_control_flow(self): base_expected.measure(0, 0) body_test = QuantumCircuit(1, 1) - body_test.for_loop((0,), None, base_expected.copy(), body_test.qubits, []) + body_test.for_loop((0,), None, base_expected.copy(), body_test.qubits, body_test.clbits) body_expected = QuantumCircuit(1, 1) - body_expected.for_loop((0,), None, base_expected.copy(), body_expected.qubits, []) + body_expected.for_loop( + (0,), None, base_expected.copy(), body_expected.qubits, body_expected.clbits + ) test = QuantumCircuit(1, 1) test.while_loop((test.clbits[0], True), body_test, test.qubits, test.clbits) diff --git a/test/python/transpiler/test_remove_diagonal_gates_before_measure.py b/test/python/transpiler/test_remove_diagonal_gates_before_measure.py index eb225a19c841..d58bc47bf57d 100644 --- a/test/python/transpiler/test_remove_diagonal_gates_before_measure.py +++ b/test/python/transpiler/test_remove_diagonal_gates_before_measure.py @@ -251,10 +251,12 @@ def test_nested_control_flow(self): base_expected.measure(1, 0) body_test = QuantumCircuit(2, 1) - body_test.for_loop((0,), None, base_expected.copy(), body_test.qubits, []) + body_test.for_loop((0,), None, base_expected.copy(), body_test.qubits, body_test.clbits) body_expected = QuantumCircuit(2, 1) - body_expected.for_loop((0,), None, base_expected.copy(), body_expected.qubits, []) + body_expected.for_loop( + (0,), None, base_expected.copy(), body_expected.qubits, body_expected.clbits + ) test = QuantumCircuit(2, 1) test.while_loop((test.clbits[0], True), body_test, test.qubits, test.clbits) diff --git a/test/python/transpiler/test_reset_after_measure_simplification.py b/test/python/transpiler/test_reset_after_measure_simplification.py index d0ab0913ada8..64afd8d74838 100644 --- a/test/python/transpiler/test_reset_after_measure_simplification.py +++ b/test/python/transpiler/test_reset_after_measure_simplification.py @@ -180,10 +180,12 @@ def test_nested_control_flow(self): base_expected.x(0).c_if(0, True) body_test = QuantumCircuit(1, 1) - body_test.for_loop((0,), None, base_expected.copy(), body_test.qubits, []) + body_test.for_loop((0,), None, base_expected.copy(), body_test.qubits, body_test.clbits) body_expected = QuantumCircuit(1, 1) - body_expected.for_loop((0,), None, base_expected.copy(), body_expected.qubits, []) + body_expected.for_loop( + (0,), None, base_expected.copy(), body_expected.qubits, body_expected.clbits + ) test = QuantumCircuit(1, 1) test.while_loop((test.clbits[0], True), body_test, test.qubits, test.clbits) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 0fba49dc3861..9731f84214ee 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -775,7 +775,7 @@ def test_if_simple(self): qc_true_body.unitary(qc_uni_mat, [0, 1]) qc = QuantumCircuit(qr, cr) - qc.if_test((cr, 1), qc_true_body, [0, 1], [0, 1]) + qc.if_test((cr, 1), qc_true_body, [0, 1], []) dag = circuit_to_dag(qc) cdag = UnitarySynthesis(basis_gates=basis_gates).run(dag) cqc = dag_to_circuit(cdag) diff --git a/test/python/transpiler/test_unroll_custom_definitions.py b/test/python/transpiler/test_unroll_custom_definitions.py index 509c2732c0e4..bad9c9a53047 100644 --- a/test/python/transpiler/test_unroll_custom_definitions.py +++ b/test/python/transpiler/test_unroll_custom_definitions.py @@ -194,16 +194,16 @@ def test_nested_control_flow(self): while_body.append(TestCompositeGate(), [0], []) true_body = QuantumCircuit([qubit, clbit]) - true_body.while_loop((clbit, True), while_body, [0], [0]) + true_body.while_loop((clbit, True), while_body, [0], []) test = QuantumCircuit([qubit, clbit]) - test.for_loop(range(2), None, for_body, [0], [0]) + test.for_loop(range(2), None, for_body, [0], []) test.if_else((clbit, True), true_body, None, [0], [0]) expected_if_body = QuantumCircuit([qubit, clbit]) - expected_if_body.while_loop((clbit, True), pass_(while_body), [0], [0]) + expected_if_body.while_loop((clbit, True), pass_(while_body), [0], []) expected = QuantumCircuit([qubit, clbit]) - expected.for_loop(range(2), None, pass_(for_body), [0], [0]) + expected.for_loop(range(2), None, pass_(for_body), [0], []) expected.if_else(range(2), pass_(expected_if_body), None, [0], [0]) self.assertEqual(pass_(test), expected) From 9e54f1386b9ca406cc06fa7fa861944cb300c178 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Thu, 13 Apr 2023 19:02:13 -0400 Subject: [PATCH 023/172] Add `max_trials` parameter to `VF2PostLayout`. (#9963) * Add max_trials parameter to VF2PostLayout. * Fix formatting. * Fix spacing. * Update release note based on review. * Revert vf2_layout.py. --- .../passes/layout/vf2_post_layout.py | 9 +++++ ...st-layout-max-trials-98b1c736e2e33861.yaml | 12 +++++++ .../python/transpiler/test_vf2_post_layout.py | 33 +++++++++++++++++++ 3 files changed, 54 insertions(+) create mode 100644 releasenotes/notes/vf2-post-layout-max-trials-98b1c736e2e33861.yaml diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py index 1e874affa7c9..19570d6c4908 100644 --- a/qiskit/transpiler/passes/layout/vf2_post_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py @@ -106,6 +106,7 @@ def __init__( call_limit=None, time_limit=None, strict_direction=True, + max_trials=0, ): """Initialize a ``VF2PostLayout`` pass instance @@ -131,6 +132,8 @@ def __init__( However, if ``strict_direction=True`` the pass expects the input :class:`~.DAGCircuit` object to :meth:`~.VF2PostLayout.run` to be in the target set of instructions. + max_trials (int): The maximum number of trials to run VF2 to find + a layout. A value of ``0`` (the default) means 'unlimited'. Raises: TypeError: At runtime, if neither ``coupling_map`` or ``target`` are provided. @@ -141,6 +144,7 @@ def __init__( self.properties = properties self.call_limit = call_limit self.time_limit = time_limit + self.max_trials = max_trials self.seed = seed self.strict_direction = strict_direction self.avg_error_map = None @@ -310,6 +314,11 @@ def run(self, dag): ) chosen_layout = layout chosen_layout_score = layout_score + + if self.max_trials and trials >= self.max_trials: + logger.debug("Trial %s is >= configured max trials %s", trials, self.max_trials) + break + elapsed_time = time.time() - start_time if self.time_limit is not None and elapsed_time >= self.time_limit: logger.debug( diff --git a/releasenotes/notes/vf2-post-layout-max-trials-98b1c736e2e33861.yaml b/releasenotes/notes/vf2-post-layout-max-trials-98b1c736e2e33861.yaml new file mode 100644 index 000000000000..c4c047871de4 --- /dev/null +++ b/releasenotes/notes/vf2-post-layout-max-trials-98b1c736e2e33861.yaml @@ -0,0 +1,12 @@ +--- +features: + - | + Added a new parameter ``max_trials`` to pass :class:`~.VF2PostLayout` + which, when specified, limits the number of layouts discovered and + compared when searching for the best layout. + This differs from existing parameters ``call_limit`` and ``time_limit`` + (which are used to limit the number of state visits performed by the VF2 + algorithm and the total time spent by pass :class:`~.VF2PostLayout`, + respectively) in that it is used to place an upper bound on the time + spent scoring potential layouts, which may be useful for larger + devices. diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index 9b48ecbb445e..d06e46a229bd 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -239,6 +239,39 @@ def test_2q_circuit_5q_backend_controlflow(self): self.assertLayout(dag, cmap, pass_.property_set) self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + def test_2q_circuit_5q_backend_max_trials(self): + """A simple example, without considering the direction + 0 - 1 + qr1 - qr0 + """ + max_trials = 11 + backend = FakeYorktown() + + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) # qr1 -> qr0 + tqc = transpile(circuit, backend, layout_method="dense") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + cmap = CouplingMap(backend.configuration().coupling_map) + props = backend.properties() + pass_ = VF2PostLayout( + coupling_map=cmap, properties=props, seed=self.seed, max_trials=max_trials + ) + + with self.assertLogs( + "qiskit.transpiler.passes.layout.vf2_post_layout", level="DEBUG" + ) as cm: + pass_.run(dag) + self.assertIn( + f"DEBUG:qiskit.transpiler.passes.layout.vf2_post_layout:Trial {max_trials} " + f"is >= configured max trials {max_trials}", + cm.output, + ) + + self.assertLayout(dag, cmap, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + def test_best_mapping_ghz_state_full_device_multiple_qregs_v2(self): """Test best mappings with multiple registers""" backend = FakeLimaV2() From 28829987f3e0e911c9f55e44aa040a1469b41957 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 14 Apr 2023 12:07:37 +0100 Subject: [PATCH 024/172] Remove circular dependency on `qiskit.qasm.pi` (#9959) Prior to 4a6eb32337 (gh-3156), the value `qiskit.qasm.pi` was a re-export of a Sympy object, so it had some value. Since then, we no longer use Sympy at all, let alone as the default type for parameters, and the export served no real purpose. It did, however, cause a bunch of problematic circular dependencies between `qiskit.circuit` and `qiskit.qasm`, since `qiskit.qasm` is logically higher in the module hierarchy than `qiskit.circuit`. Solving this circular dependency is another step down the path of being able to fully deprecate and remove the legacy `qiskit.qasm` code. --- qiskit/circuit/library/standard_gates/equivalence_library.py | 4 +++- qiskit/circuit/library/standard_gates/h.py | 3 +-- qiskit/circuit/library/standard_gates/r.py | 2 +- qiskit/circuit/library/standard_gates/rx.py | 2 +- qiskit/circuit/library/standard_gates/ry.py | 2 +- qiskit/circuit/library/standard_gates/s.py | 2 +- qiskit/circuit/library/standard_gates/sx.py | 2 +- qiskit/circuit/library/standard_gates/t.py | 2 +- qiskit/circuit/library/standard_gates/u2.py | 3 +-- qiskit/circuit/library/standard_gates/x.py | 3 +-- qiskit/circuit/library/standard_gates/xx_minus_yy.py | 2 +- qiskit/circuit/library/standard_gates/xx_plus_yy.py | 2 +- qiskit/circuit/library/standard_gates/y.py | 2 +- qiskit/circuit/library/standard_gates/z.py | 2 +- test/python/circuit/test_extensions_standard.py | 2 +- test/python/qasm3/test_export.py | 2 +- 16 files changed, 18 insertions(+), 19 deletions(-) diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index c9fa73894884..0269dc0b4910 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -13,7 +13,9 @@ """Standard gates.""" from __future__ import annotations -from qiskit.qasm import pi + +from math import pi + from qiskit.circuit import ( EquivalenceLibrary, Parameter, diff --git a/qiskit/circuit/library/standard_gates/h.py b/qiskit/circuit/library/standard_gates/h.py index 114b9008834e..225f8666fb8c 100644 --- a/qiskit/circuit/library/standard_gates/h.py +++ b/qiskit/circuit/library/standard_gates/h.py @@ -11,13 +11,12 @@ # that they have been altered from the originals. """Hadamard gate.""" -from math import sqrt +from math import sqrt, pi from typing import Optional, Union import numpy from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.qasm import pi from .t import TGate, TdgGate from .s import SGate, SdgGate diff --git a/qiskit/circuit/library/standard_gates/r.py b/qiskit/circuit/library/standard_gates/r.py index 292df90264d8..28ca03385efc 100644 --- a/qiskit/circuit/library/standard_gates/r.py +++ b/qiskit/circuit/library/standard_gates/r.py @@ -14,9 +14,9 @@ import math from cmath import exp +from math import pi from typing import Optional import numpy -from qiskit.qasm import pi from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index d26a9df60408..7eddd05e9510 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -13,10 +13,10 @@ """Rotation around the X axis.""" import math +from math import pi from typing import Optional, Union import numpy -from qiskit.qasm import pi from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index a552cff5d087..8cba2ed43252 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -13,9 +13,9 @@ """Rotation around the Y axis.""" import math +from math import pi from typing import Optional, Union import numpy -from qiskit.qasm import pi from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister diff --git a/qiskit/circuit/library/standard_gates/s.py b/qiskit/circuit/library/standard_gates/s.py index eaef481d5961..0fc0566b83d8 100644 --- a/qiskit/circuit/library/standard_gates/s.py +++ b/qiskit/circuit/library/standard_gates/s.py @@ -12,6 +12,7 @@ """The S, Sdg, CS and CSdg gates.""" +from math import pi from typing import Optional, Union import numpy @@ -20,7 +21,6 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.library.standard_gates.p import CPhaseGate, PhaseGate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.qasm import pi class SGate(Gate): diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 84a8fc52272c..7bcc28b62861 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -12,9 +12,9 @@ """Sqrt(X) and C-Sqrt(X) gates.""" +from math import pi from typing import Optional, Union import numpy -from qiskit.qasm import pi from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister diff --git a/qiskit/circuit/library/standard_gates/t.py b/qiskit/circuit/library/standard_gates/t.py index 5ca423e54220..8f4895d9272f 100644 --- a/qiskit/circuit/library/standard_gates/t.py +++ b/qiskit/circuit/library/standard_gates/t.py @@ -12,6 +12,7 @@ """T and Tdg gate.""" import math +from math import pi from typing import Optional import numpy @@ -19,7 +20,6 @@ from qiskit.circuit.gate import Gate from qiskit.circuit.library.standard_gates.p import PhaseGate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.qasm import pi class TGate(Gate): diff --git a/qiskit/circuit/library/standard_gates/u2.py b/qiskit/circuit/library/standard_gates/u2.py index fa0f00f790c5..a13d03cd40af 100644 --- a/qiskit/circuit/library/standard_gates/u2.py +++ b/qiskit/circuit/library/standard_gates/u2.py @@ -11,11 +11,10 @@ # that they have been altered from the originals. """One-pulse single-qubit gate.""" -from math import sqrt +from math import sqrt, pi from cmath import exp from typing import Optional import numpy -from qiskit.qasm import pi from qiskit.circuit.gate import Gate from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumregister import QuantumRegister diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 01a867f8ca38..659b6ffb4942 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -13,13 +13,12 @@ """X, CX, CCX and multi-controlled X gates.""" from __future__ import annotations from typing import Optional, Union, Type -from math import ceil +from math import ceil, pi import numpy from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit._utils import _compute_control_matrix, _ctrl_state_to_int -from qiskit.qasm import pi from .h import HGate from .t import TGate, TdgGate from .u1 import U1Gate diff --git a/qiskit/circuit/library/standard_gates/xx_minus_yy.py b/qiskit/circuit/library/standard_gates/xx_minus_yy.py index df7fb1f8504f..9e6c3f2c6943 100644 --- a/qiskit/circuit/library/standard_gates/xx_minus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_minus_yy.py @@ -13,6 +13,7 @@ """Two-qubit XX-YY gate.""" import math from cmath import exp +from math import pi from typing import Optional import numpy as np @@ -26,7 +27,6 @@ from qiskit.circuit.parameterexpression import ParameterValueType from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.qasm import pi class XXMinusYYGate(Gate): diff --git a/qiskit/circuit/library/standard_gates/xx_plus_yy.py b/qiskit/circuit/library/standard_gates/xx_plus_yy.py index 85d63ee625f9..5e0a8f184778 100644 --- a/qiskit/circuit/library/standard_gates/xx_plus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_plus_yy.py @@ -13,8 +13,8 @@ """Two-qubit XX+YY gate.""" import math from cmath import exp +from math import pi from typing import Optional -from qiskit.qasm import pi from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister from qiskit.circuit.parameterexpression import ParameterValueType diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index 57c48ee6aad9..6240a70338ad 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -12,9 +12,9 @@ """Y and CY gates.""" +from math import pi from typing import Optional, Union import numpy -from qiskit.qasm import pi # pylint: disable=cyclic-import from qiskit.circuit.controlledgate import ControlledGate diff --git a/qiskit/circuit/library/standard_gates/z.py b/qiskit/circuit/library/standard_gates/z.py index 7362317ec289..c724fddb38b3 100644 --- a/qiskit/circuit/library/standard_gates/z.py +++ b/qiskit/circuit/library/standard_gates/z.py @@ -12,6 +12,7 @@ """Z, CZ and CCZ gates.""" +from math import pi from typing import Optional, Union import numpy @@ -20,7 +21,6 @@ from qiskit.circuit.controlledgate import ControlledGate from qiskit.circuit.gate import Gate from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.qasm import pi from .p import PhaseGate diff --git a/test/python/circuit/test_extensions_standard.py b/test/python/circuit/test_extensions_standard.py index 5dfb678fda59..79f1d08b7bb0 100644 --- a/test/python/circuit/test_extensions_standard.py +++ b/test/python/circuit/test_extensions_standard.py @@ -14,13 +14,13 @@ import unittest from inspect import signature +from math import pi import numpy as np from scipy.linalg import expm from ddt import data, ddt, unpack from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, execute -from qiskit.qasm import pi from qiskit.exceptions import QiskitError from qiskit.circuit.exceptions import CircuitError from qiskit.test import QiskitTestCase diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 7efcb6a0ac28..ace1bb3fdd34 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -16,6 +16,7 @@ # pylint: disable=line-too-long from io import StringIO +from math import pi import re import unittest @@ -27,7 +28,6 @@ from qiskit.qasm3 import Exporter, dumps, dump, QASM3ExporterError from qiskit.qasm3.exporter import QASM3Builder from qiskit.qasm3.printer import BasicPrinter -from qiskit.qasm import pi # Tests marked with this decorator should be restored after gate definition with parameters is fixed From 714e521ee38121692aa4c03f6b96660773c18c36 Mon Sep 17 00:00:00 2001 From: Max Rossmannek Date: Fri, 14 Apr 2023 13:40:16 +0200 Subject: [PATCH 025/172] `AdaptVQE` threshold improvements (#9921) * Pending-deprecate AdaptVQE.threshold in favor of gradient_threshold * Adds the AdaptVQE.eigenvalue_threshold attribute * Add release note * Apply suggestions from code review Co-authored-by: Julien Gacon * Leverage deprecate_func on AdaptVQE.threshold property * Test pending deprecation of AdaptVQE.threshold * Add missing deprecate_func decorator to property setter * Improve documentation of AdaptVQE.eigenvalue_threshold w.r.t. final iteration * Adds a unittest for SparsePauliOp support in AdaptVQE * Attempt to fix test flakiness * Pick better operator pool for gradients in flaky unittest --------- Co-authored-by: Julien Gacon Co-authored-by: ElePT --- .../minimum_eigensolvers/adapt_vqe.py | 101 +++++++++++++++--- ...adapt-vqe-thresholds-239ed9f250c63e71.yaml | 10 ++ .../minimum_eigensolvers/test_adapt_vqe.py | 59 +++++++++- 3 files changed, 152 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/adapt-vqe-thresholds-239ed9f250c63e71.yaml diff --git a/qiskit/algorithms/minimum_eigensolvers/adapt_vqe.py b/qiskit/algorithms/minimum_eigensolvers/adapt_vqe.py index 59674ab7b617..e05f07cf3607 100644 --- a/qiskit/algorithms/minimum_eigensolvers/adapt_vqe.py +++ b/qiskit/algorithms/minimum_eigensolvers/adapt_vqe.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -27,6 +27,7 @@ from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.opflow import OperatorBase, PauliSumOp from qiskit.circuit.library import EvolvedOperatorAnsatz +from qiskit.utils.deprecation import deprecate_arg, deprecate_func from qiskit.utils.validation import validate_min from .minimum_eigensolver import MinimumEigensolver @@ -84,39 +85,86 @@ class AdaptVQE(VariationalAlgorithm, MinimumEigensolver): Attributes: solver: a :class:`~.VQE` instance used internally to compute the minimum eigenvalues. It is a requirement that the :attr:`~.VQE.ansatz` of this solver is of type - :class:`qiskit.circuit.library.EvolvedOperatorAnsatz`. - threshold: the convergence threshold for the algorithm. Once all gradients have an absolute - value smaller than this threshold, the algorithm terminates. + :class:`~qiskit.circuit.library.EvolvedOperatorAnsatz`. + gradient_threshold: once all gradients have an absolute value smaller than this threshold, + the algorithm has converged and terminates. + eigenvalue_threshold: once the eigenvalue has changed by less than this threshold from one + iteration to the next, the algorithm has converged and terminates. When this case + occurs, the excitation included in the final iteration did not result in a significant + improvement of the eigenvalue and, thus, the results from this iteration are not + considered. max_iterations: the maximum number of iterations for the adaptive loop. If ``None``, the algorithm is not bound in its number of iterations. """ + @deprecate_arg( + "threshold", + since="0.24.0", + pending=True, + new_alias="gradient_threshold", + ) def __init__( self, solver: VQE, *, - threshold: float = 1e-5, + gradient_threshold: float = 1e-5, + eigenvalue_threshold: float = 1e-5, max_iterations: int | None = None, + threshold: float | None = None, # pylint: disable=unused-argument ) -> None: """ Args: solver: a :class:`~.VQE` instance used internally to compute the minimum eigenvalues. It is a requirement that the :attr:`~.VQE.ansatz` of this solver is of type - :class:`qiskit.circuit.library.EvolvedOperatorAnsatz`. - threshold: the convergence threshold for the algorithm. Once all gradients have an - absolute value smaller than this threshold, the algorithm terminates. + :class:`~qiskit.circuit.library.EvolvedOperatorAnsatz`. + gradient_threshold: once all gradients have an absolute value smaller than this + threshold, the algorithm has converged and terminates. + eigenvalue_threshold: once the eigenvalue has changed by less than this threshold from + one iteration to the next, the algorithm has converged and terminates. When this + case occurs, the excitation included in the final iteration did not result in a + significant improvement of the eigenvalue and, thus, the results from this iteration + are not considered. max_iterations: the maximum number of iterations for the adaptive loop. If ``None``, the algorithm is not bound in its number of iterations. + threshold: once all gradients have an absolute value smaller than this threshold, the + algorithm has converged and terminates. Defaults to ``1e-5``. """ - validate_min("threshold", threshold, 1e-15) + validate_min("gradient_threshold", gradient_threshold, 1e-15) + validate_min("eigenvalue_threshold", eigenvalue_threshold, 1e-15) self.solver = solver - self.threshold = threshold + self.gradient_threshold = gradient_threshold + self.eigenvalue_threshold = eigenvalue_threshold self.max_iterations = max_iterations self._tmp_ansatz: EvolvedOperatorAnsatz | None = None self._excitation_pool: list[OperatorBase] = [] self._excitation_list: list[OperatorBase] = [] + @property + @deprecate_func( + since="0.24.0", + pending=True, + is_property=True, + additional_msg="Instead, use the gradient_threshold attribute.", + ) + def threshold(self) -> float: + """The threshold for the gradients. + + Once all gradients have an absolute value smaller than this threshold, the algorithm has + converged and terminates. + """ + return self.gradient_threshold + + @threshold.setter + @deprecate_func( + since="0.24.0", + pending=True, + is_property=True, + additional_msg="Instead, use the gradient_threshold attribute.", + ) + def threshold(self, threshold: float) -> None: + self.gradient_threshold = threshold + @property def initial_point(self) -> Sequence[float] | None: """Returns the initial point of the internal :class:`~.VQE` solver.""" @@ -134,7 +182,7 @@ def supports_aux_operators(cls) -> bool: def _compute_gradients( self, theta: list[float], - operator: OperatorBase, + operator: BaseOperator | OperatorBase, ) -> list[tuple[complex, dict[str, Any]]]: """ Computes the gradients for all available excitation operators. @@ -208,6 +256,8 @@ def compute_minimum_eigenvalue( self.solver.ansatz = self._tmp_ansatz.initial_state prev_op_indices: list[int] = [] + prev_raw_vqe_result: VQEResult | None = None + raw_vqe_result: VQEResult | None = None theta: list[float] = [] max_grad: tuple[complex, dict[str, Any] | None] = (0.0, None) self._excitation_list = [] @@ -229,7 +279,7 @@ def compute_minimum_eigenvalue( str(max_grad_index), ) # log gradients - if np.abs(max_grad[0]) < self.threshold: + if np.abs(max_grad[0]) < self.gradient_threshold: if iteration == 1: raise QiskitError( "All gradients have been evaluated to lie below the convergence threshold " @@ -256,12 +306,35 @@ def compute_minimum_eigenvalue( ) self._excitation_list.append(self._excitation_pool[max_grad_index]) theta.append(0.0) - # run VQE on current Ansatz + # setting up the ansatz for the VQE iteration self._tmp_ansatz.operators = self._excitation_list self.solver.ansatz = self._tmp_ansatz self.solver.initial_point = theta + # evaluating the eigenvalue with the internal VQE + prev_raw_vqe_result = raw_vqe_result raw_vqe_result = self.solver.compute_minimum_eigenvalue(operator) theta = raw_vqe_result.optimal_point.tolist() + # checking convergence based on the change in eigenvalue + if iteration > 1: + eigenvalue_diff = np.abs(raw_vqe_result.eigenvalue - history[-1]) + if eigenvalue_diff < self.eigenvalue_threshold: + logger.info( + "AdaptVQE terminated successfully with a final change in eigenvalue: %s", + str(eigenvalue_diff), + ) + termination_criterion = TerminationCriterion.CONVERGED + logger.debug( + "Reverting the addition of the last excitation to the ansatz since it " + "resulted in a change of the eigenvalue below the configured threshold." + ) + self._excitation_list.pop() + theta.pop() + self._tmp_ansatz.operators = self._excitation_list + self.solver.ansatz = self._tmp_ansatz + self.solver.initial_point = theta + raw_vqe_result = prev_raw_vqe_result + break + # appending the computed eigenvalue to the tracking history history.append(raw_vqe_result.eigenvalue) logger.info("Current eigenvalue: %s", str(raw_vqe_result.eigenvalue)) else: @@ -284,7 +357,7 @@ def compute_minimum_eigenvalue( ) result.aux_operators_evaluated = aux_values - logger.info("The final energy is: %s", str(result.eigenvalue)) + logger.info("The final eigenvalue is: %s", str(result.eigenvalue)) self.solver.ansatz.operators = self._excitation_pool return result diff --git a/releasenotes/notes/adapt-vqe-thresholds-239ed9f250c63e71.yaml b/releasenotes/notes/adapt-vqe-thresholds-239ed9f250c63e71.yaml new file mode 100644 index 000000000000..2ced892e5e78 --- /dev/null +++ b/releasenotes/notes/adapt-vqe-thresholds-239ed9f250c63e71.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Adds the new :attr:`~qiskit.algorithms.minimum_eigensolvers.AdaptVQE.eigenvalue_threshold` + setting which allows configuring a new kind of threshold to terminate the algorithm once + the eigenvalue changes less than a set value. + - | + Adds the new :attr:`~qiskit.algorithms.minimum_eigensolvers.AdaptVQE.gradient_threshold` + setting which will replace the :attr:`~qiskit.algorithms.minimum_eigensolvers.AdaptVQE.threshold` + in the future. diff --git a/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py index c4764eef62a9..2b5d899c7a1b 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -56,7 +56,7 @@ def setUp(self): ("ZXZX", -0.04523279994605788), ] ) - excitation_pool = [ + self.excitation_pool = [ PauliSumOp( SparsePauliOp(["IIIY", "IIZY"], coeffs=[0.5 + 0.0j, -0.5 + 0.0j]), coeff=1.0 ), @@ -83,7 +83,7 @@ def setUp(self): self.initial_state = QuantumCircuit(QuantumRegister(4)) self.initial_state.x(0) self.initial_state.x(1) - self.ansatz = EvolvedOperatorAnsatz(excitation_pool, initial_state=self.initial_state) + self.ansatz = EvolvedOperatorAnsatz(self.excitation_pool, initial_state=self.initial_state) self.optimizer = SLSQP() def test_default(self): @@ -96,11 +96,26 @@ def test_default(self): self.assertAlmostEqual(res.eigenvalue, expected_eigenvalue, places=6) np.testing.assert_allclose(res.eigenvalue_history, [expected_eigenvalue], rtol=1e-6) + def test_with_quantum_info(self): + """Test behavior with quantum_info-based operators.""" + ansatz = EvolvedOperatorAnsatz( + [op.primitive for op in self.excitation_pool], + initial_state=self.initial_state, + ) + + calc = AdaptVQE(VQE(Estimator(), ansatz, self.optimizer)) + res = calc.compute_minimum_eigenvalue(operator=self.h2_op.primitive) + + expected_eigenvalue = -1.85727503 + + self.assertAlmostEqual(res.eigenvalue, expected_eigenvalue, places=6) + np.testing.assert_allclose(res.eigenvalue_history, [expected_eigenvalue], rtol=1e-6) + def test_converged(self): """Test to check termination criteria""" calc = AdaptVQE( VQE(Estimator(), self.ansatz, self.optimizer), - threshold=1e-3, + gradient_threshold=1e-3, ) res = calc.compute_minimum_eigenvalue(operator=self.h2_op) @@ -116,6 +131,42 @@ def test_maximum(self): self.assertEqual(res.termination_criterion, TerminationCriterion.MAXIMUM) + def test_eigenvalue_threshold(self): + """Test for the eigenvalue_threshold attribute.""" + operator = PauliSumOp.from_list( + [ + ("XX", 1.0), + ("ZX", -0.5), + ("XZ", -0.5), + ] + ) + ansatz = EvolvedOperatorAnsatz( + [ + PauliSumOp.from_list([("YZ", 0.4)]), + PauliSumOp.from_list([("ZY", 0.5)]), + ], + initial_state=QuantumCircuit(2), + ) + + calc = AdaptVQE( + VQE(Estimator(), ansatz, self.optimizer), + eigenvalue_threshold=1, + ) + res = calc.compute_minimum_eigenvalue(operator) + + self.assertEqual(res.termination_criterion, TerminationCriterion.CONVERGED) + + def test_threshold_attribute(self): + """Test the (pending deprecated) threshold attribute""" + with self.assertWarns(PendingDeprecationWarning): + calc = AdaptVQE( + VQE(Estimator(), self.ansatz, self.optimizer), + threshold=1e-3, + ) + res = calc.compute_minimum_eigenvalue(operator=self.h2_op) + + self.assertEqual(res.termination_criterion, TerminationCriterion.CONVERGED) + @data( ([1, 1], True), ([1, 11], False), From b1df607a3e7f62f5dd20285a9a507fff4b78dd3a Mon Sep 17 00:00:00 2001 From: Arianna Crippa <65536419+ariannacrippa@users.noreply.github.com> Date: Fri, 14 Apr 2023 14:37:18 +0200 Subject: [PATCH 026/172] Allow list of optimizers+initial points in ``VQD`` (#9151) * test commit * added optional lists of init ial point and optimizer * fixed initial points and add more compact expressions * fixed bug initial_point input * fix initial points * formatting * fix none initial point * Added releasenote * Ensure initial_point sampled once only if None * moved release note * Update typehints * Update typehints * Update typehints * Update typehints * Update typehints * Apply suggestions from code review * Fix conflict, add test * Fix black * Update releasenotes/notes/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml --------- Co-authored-by: Julien Gacon Co-authored-by: ElePT <57907331+ElePT@users.noreply.github.com> Co-authored-by: ElePT --- qiskit/algorithms/eigensolvers/vqd.py | 68 +++++++++++++------ ...ints-list-optimizers-033d7439f86bbb71.yaml | 10 +++ .../algorithms/eigensolvers/test_vqd.py | 40 +++++++++++ 3 files changed, 96 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml diff --git a/qiskit/algorithms/eigensolvers/vqd.py b/qiskit/algorithms/eigensolvers/vqd.py index 99a0fb3076d1..6a0c8a0842b4 100644 --- a/qiskit/algorithms/eigensolvers/vqd.py +++ b/qiskit/algorithms/eigensolvers/vqd.py @@ -18,9 +18,9 @@ from __future__ import annotations from collections.abc import Callable, Sequence +from typing import Any import logging from time import time -from typing import Any import numpy as np @@ -61,7 +61,7 @@ class VQD(VariationalAlgorithm, Eigensolver): An instance of VQD requires defining three algorithmic sub-components: an integer k denoting the number of eigenstates to calculate, a trial state (a.k.a. ansatz) which is a :class:`QuantumCircuit`, - and one of the classical :mod:`~qiskit.algorithms.optimizers`. + and one instance (or list of) classical :mod:`~qiskit.algorithms.optimizers`. The optimizer varies the circuit parameters The trial state :math:`|\psi(\vec\theta)\rangle` is varied by the optimizer, which modifies the set of ansatz parameters :math:`\vec\theta` @@ -79,6 +79,7 @@ class VQD(VariationalAlgorithm, Eigensolver): of ``None``, then VQD will look to the ansatz for a preferred value, based on its given initial state. If the ansatz returns ``None``, then a random point will be generated within the parameter bounds set, as per above. + It is also possible to give a list of initial points, one for every kth eigenvalue. If the ansatz provides ``None`` as the lower bound, then VQD will default it to :math:`-2\pi`; similarly, if the ansatz returns ``None`` as the upper bound, the default value will be :math:`2\pi`. @@ -92,7 +93,8 @@ class VQD(VariationalAlgorithm, Eigensolver): fidelity (BaseStateFidelity): The fidelity class instance used to compute the overlap estimation as indicated in the VQD paper. ansatz (QuantumCircuit): A parameterized circuit used as ansatz for the wave function. - optimizer(Optimizer): A classical optimizer. Can either be a Qiskit optimizer or a callable + optimizer(Optimizer | Sequence[Optimizer]): A classical optimizer or a list of optimizers, + one for every k-th eigenvalue. Can either be a Qiskit optimizer or a callable that takes an array as input and returns a Qiskit or SciPy optimization result. k (int): the number of eigenvalues to return. Returns the lowest k eigenvalues. betas (list[float]): Beta parameters in the VQD paper. @@ -100,10 +102,12 @@ class VQD(VariationalAlgorithm, Eigensolver): These hyper-parameters balance the contribution of each overlap term to the cost function and have a default value computed as the mean square sum of the coefficients of the observable. - initial point (list[float]): An optional initial point (i.e. initial parameter values) - for the optimizer. If ``None`` then VQD will look to the ansatz for a preferred - point and if not will simply compute a random one. - callback (Callable[[int, np.ndarray, float, dict[str, Any], int], None] | None): + initial point (Sequence[float] | Sequence[Sequence[float]] | None): An optional initial + point (i.e. initial parameter values) or a list of initial points + (one for every k-th eigenvalue) for the optimizer. + If ``None`` then VQD will look to the ansatz for a + preferred point and if not will simply compute a random one. + callback (Callable[[int, np.ndarray, float, dict[str, Any]], None] | None): A callback that can access the intermediate data during the optimization. Four parameter values are passed to the callback as follows during each evaluation by the optimizer: the evaluation count, @@ -116,12 +120,12 @@ def __init__( estimator: BaseEstimator, fidelity: BaseStateFidelity, ansatz: QuantumCircuit, - optimizer: Optimizer | Minimizer, + optimizer: Optimizer | Minimizer | Sequence[Optimizer | Minimizer], *, k: int = 2, betas: Sequence[float] | None = None, - initial_point: Sequence[float] | None = None, - callback: Callable[[int, np.ndarray, float, dict[str, Any], int], None] | None = None, + initial_point: Sequence[float] | Sequence[Sequence[float]] | None = None, + callback: Callable[[int, np.ndarray, float, dict[str, Any]], None] | None = None, ) -> None: """ @@ -129,7 +133,8 @@ def __init__( estimator: The estimator primitive. fidelity: The fidelity class using primitives. ansatz: A parameterized circuit used as ansatz for the wave function. - optimizer: A classical optimizer. Can either be a Qiskit optimizer or a callable + optimizer: A classical optimizer or a list of optimizers, one for every k-th eigenvalue. + Can either be a Qiskit optimizer or a callable that takes an array as input and returns a Qiskit or SciPy optimization result. k: The number of eigenvalues to return. Returns the lowest k eigenvalues. betas: Beta parameters in the VQD paper. @@ -138,7 +143,9 @@ def __init__( function and have a default value computed as the mean square sum of the coefficients of the observable. initial_point: An optional initial point (i.e. initial parameter values) - for the optimizer. If ``None`` then VQD will look to the ansatz for a preferred + or a list of initial points (one for every k-th eigenvalue) + for the optimizer. + If ``None`` then VQD will look to the ansatz for a preferred point and if not will simply compute a random one. callback: A callback that can access the intermediate data during the optimization. Four parameter values are passed to the callback as @@ -161,12 +168,12 @@ def __init__( self._eval_count = 0 @property - def initial_point(self) -> Sequence[float] | None: + def initial_point(self) -> Sequence[float] | Sequence[Sequence[float]] | None: """Returns initial point.""" return self._initial_point @initial_point.setter - def initial_point(self, initial_point: Sequence[float]): + def initial_point(self, initial_point: Sequence[float] | Sequence[Sequence[float]] | None): """Sets initial point""" self._initial_point = initial_point @@ -199,8 +206,6 @@ def compute_eigenvalues( # validation self._check_operator_ansatz(operator) - initial_point = validate_initial_point(self.initial_point, self.ansatz) - bounds = validate_bounds(self.ansatz) # We need to handle the array entries being zero or Optional i.e. having value None @@ -251,8 +256,20 @@ def compute_eigenvalues( # the same parameters to the ansatz if we do multiple steps prev_states = [] + num_initial_points = 0 + if self.initial_point is not None: + initial_points = np.reshape(self.initial_point, (-1, self.ansatz.num_parameters)) + num_initial_points = len(initial_points) + + # 0 just means the initial point is ``None`` and ``validate_initial_point`` + # will select a random point + if num_initial_points <= 1: + initial_point = validate_initial_point(self.initial_point, self.ansatz) + for step in range(1, self.k + 1): - # update list of optimal circuits + if num_initial_points > 1: + initial_point = validate_initial_point(initial_points[step - 1], self.ansatz) + if step > 1: prev_states.append(self.ansatz.bind_parameters(result.optimal_points[-1])) @@ -264,20 +281,27 @@ def compute_eigenvalues( start_time = time() # TODO: add gradient support after FidelityGradients are implemented - if callable(self.optimizer): - opt_result = self.optimizer(fun=energy_evaluation, x0=initial_point, bounds=bounds) + if isinstance(self.optimizer, Sequence): + optimizer = self.optimizer[step - 1] + else: + optimizer = self.optimizer # fall back to single optimizer if not list + + if callable(optimizer): + opt_result = optimizer( # pylint: disable=not-callable + fun=energy_evaluation, x0=initial_point, bounds=bounds + ) else: # we always want to submit as many estimations per job as possible for minimal # overhead on the hardware - was_updated = _set_default_batchsize(self.optimizer) + was_updated = _set_default_batchsize(optimizer) - opt_result = self.optimizer.minimize( + opt_result = optimizer.minimize( fun=energy_evaluation, x0=initial_point, bounds=bounds ) # reset to original value if was_updated: - self.optimizer.set_max_evals_grouped(None) + optimizer.set_max_evals_grouped(None) eval_time = time() - start_time diff --git a/releasenotes/notes/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml b/releasenotes/notes/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml new file mode 100644 index 000000000000..fadd99c80234 --- /dev/null +++ b/releasenotes/notes/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Added extensions to the :class:`~.eigensolvers.VQD` algorithm, which allow + 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. + + diff --git a/test/python/algorithms/eigensolvers/test_vqd.py b/test/python/algorithms/eigensolvers/test_vqd.py index ffc8441f370b..16652a259ebd 100644 --- a/test/python/algorithms/eigensolvers/test_vqd.py +++ b/test/python/algorithms/eigensolvers/test_vqd.py @@ -239,6 +239,46 @@ def run_check(): result = vqd.compute_eigenvalues(operator=op) self.assertIsInstance(result, VQDResult) + @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) + def test_optimizer_list(self, op): + """Test sending an optimizer list""" + + optimizers = [SLSQP(), L_BFGS_B()] + initial_point_1 = [ + 1.70256666, + -5.34843975, + -0.39542903, + 5.99477786, + -2.74374986, + -4.85284669, + 0.2442925, + -1.51638917, + ] + initial_point_2 = [ + 0.5, + 0.5, + 0.5, + 0.5, + 0.5, + 0.5, + 0.5, + 0.5, + ] + vqd = VQD( + estimator=self.estimator, + fidelity=self.fidelity, + ansatz=RealAmplitudes(), + optimizer=optimizers, + initial_point=[initial_point_1, initial_point_2], + k=2, + betas=self.betas, + ) + + result = vqd.compute_eigenvalues(operator=op) + np.testing.assert_array_almost_equal( + result.eigenvalues.real, self.h2_energy_excited[:2], decimal=3 + ) + @data(H2_PAULI, H2_OP, H2_SPARSE_PAULI) def test_aux_operators_list(self, op): """Test list-based aux_operators.""" From ccf78ae59203c5b0388ec8a187d166e97c750b60 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Apr 2023 12:54:29 +0000 Subject: [PATCH 027/172] Bump pyo3 from 0.18.2 to 0.18.3 (#9964) Bumps [pyo3](https://github.com/pyo3/pyo3) from 0.18.2 to 0.18.3. - [Release notes](https://github.com/pyo3/pyo3/releases) - [Changelog](https://github.com/PyO3/pyo3/blob/main/CHANGELOG.md) - [Commits](https://github.com/pyo3/pyo3/compare/v0.18.2...v0.18.3) --- updated-dependencies: - dependency-name: pyo3 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 | 20 ++++++++++---------- crates/accelerate/Cargo.toml | 2 +- crates/qasm2/Cargo.toml | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8d0de02b1f39..2035c1a9e5a5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -346,9 +346,9 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfb848f80438f926a9ebddf0a539ed6065434fd7aae03a89312a9821f81b8501" +checksum = "e3b1ac5b3731ba34fdaa9785f8d74d17448cd18f30cf19e0c7e7b1fdb5272109" dependencies = [ "cfg-if", "hashbrown 0.13.2", @@ -367,9 +367,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98a42e7f42e917ce6664c832d5eee481ad514c98250c49e0b03b20593e2c7ed0" +checksum = "9cb946f5ac61bb61a5014924910d936ebd2b23b705f7a4a3c40b05c720b079a3" dependencies = [ "once_cell", "target-lexicon", @@ -377,9 +377,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0707f0ab26826fe4ccd59b69106e9df5e12d097457c7b8f9c0fd1d2743eec4d" +checksum = "fd4d7c5337821916ea2a1d21d1092e8443cf34879e53a0ac653fbb98f44ff65c" dependencies = [ "libc", "pyo3-build-config", @@ -387,9 +387,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978d18e61465ecd389e1f235ff5a467146dc4e3c3968b90d274fe73a5dd4a438" +checksum = "a9d39c55dab3fc5a4b25bbd1ac10a2da452c4aca13bb450f22818a002e29648d" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -399,9 +399,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e0e1128f85ce3fca66e435e08aa2089a2689c1c48ce97803e13f63124058462" +checksum = "97daff08a4c48320587b5224cc98d609e3c27b6d437315bd40b605c98eeb5918" dependencies = [ "proc-macro2", "quote", diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 65d1fe9cbcd6..602574ab0b55 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -25,7 +25,7 @@ rustworkx-core = "0.12" # The base version of PyO3 and setting a minimum feature set (e.g. probably just 'extension-module') # can be done in the workspace and inherited once we hit Rust 1.64. [dependencies.pyo3] -version = "0.18.2" +version = "0.18.3" features = ["extension-module", "hashbrown", "indexmap", "num-complex", "num-bigint"] [dependencies.ndarray] diff --git a/crates/qasm2/Cargo.toml b/crates/qasm2/Cargo.toml index 1d7215f477ac..cc27943133af 100644 --- a/crates/qasm2/Cargo.toml +++ b/crates/qasm2/Cargo.toml @@ -13,4 +13,4 @@ crate-type = ["cdylib"] [dependencies] hashbrown = "0.13.2" -pyo3 = { version = "0.18.2", features = ["extension-module"] } +pyo3 = { version = "0.18.3", features = ["extension-module"] } From 159898fac56827fe529bc260f8067fc0ba036b72 Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Sat, 15 Apr 2023 01:12:49 +0900 Subject: [PATCH 028/172] Fix PrimitiveJob.submit() so it no longer blocks on execution (#9216) * remove context manager * forgotten equal... * fix tests * fix litn * Apply suggestions from code review Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> * Update qiskit/primitives/primitive_job.py * fix CI * fix the bug introduced by resolving merge conflict * add releasenote * Apply suggestions from code review --------- Co-authored-by: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> --- qiskit/algorithms/gradients/qfi.py | 3 ++- qiskit/primitives/primitive_job.py | 6 +++--- .../notes/primitive-job-submit-a7633872e2ae3c7b.yaml | 6 ++++++ test/python/primitives/test_backend_sampler.py | 1 + test/python/primitives/test_sampler.py | 1 + 5 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/primitive-job-submit-a7633872e2ae3c7b.yaml diff --git a/qiskit/algorithms/gradients/qfi.py b/qiskit/algorithms/gradients/qfi.py index 5a0e083a633c..1dcaa70d31e9 100644 --- a/qiskit/algorithms/gradients/qfi.py +++ b/qiskit/algorithms/gradients/qfi.py @@ -120,13 +120,14 @@ def _run( DerivativeType.REAL, ) job = self._qgt.run(circuits, parameter_values, parameters, **options) - self._qgt.derivative_type = temp_derivative_type try: result = job.result() except AlgorithmError as exc: raise AlgorithmError("Estimator job or gradient job failed.") from exc + self._qgt.derivative_type = temp_derivative_type + return QFIResult( qfis=[4 * qgt.real for qgt in result.qgts], metadata=result.metadata, diff --git a/qiskit/primitives/primitive_job.py b/qiskit/primitives/primitive_job.py index 95cc9629e0cd..7c87fd34994d 100644 --- a/qiskit/primitives/primitive_job.py +++ b/qiskit/primitives/primitive_job.py @@ -45,9 +45,9 @@ def submit(self): if self._future is not None: raise JobError("Primitive job has already been submitted.") - with ThreadPoolExecutor(max_workers=1) as executor: - future = executor.submit(self._function, *self._args, **self._kwargs) - self._future = future + executor = ThreadPoolExecutor(max_workers=1) # pylint: disable=consider-using-with + self._future = executor.submit(self._function, *self._args, **self._kwargs) + executor.shutdown(wait=False) def result(self) -> T: """Return the results of the job.""" diff --git a/releasenotes/notes/primitive-job-submit-a7633872e2ae3c7b.yaml b/releasenotes/notes/primitive-job-submit-a7633872e2ae3c7b.yaml new file mode 100644 index 000000000000..d1485ee766c9 --- /dev/null +++ b/releasenotes/notes/primitive-job-submit-a7633872e2ae3c7b.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + :meth:`.PrimitiveJob.submit` no longer blocks on execution finishing. + As a result, :meth:`.Sampler.run`, :meth:`.BackendSampler.run`, :meth:`.Estimator.run` + and :meth:`.BaseEstimator.run` do not block until :meth:`.PrimitiveJob.result` method is called. diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index 61ec3449de04..9627dfdbea7a 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -282,6 +282,7 @@ def test_primitive_job_status_done(self, backend): bell = self._circuit[1] sampler = BackendSampler(backend=backend) job = sampler.run(circuits=[bell]) + _ = job.result() self.assertEqual(job.status(), JobStatus.DONE) def test_primitive_job_size_limit_backend_v2(self): diff --git a/test/python/primitives/test_sampler.py b/test/python/primitives/test_sampler.py index 0e04b06aa0db..030ccf7c26ad 100644 --- a/test/python/primitives/test_sampler.py +++ b/test/python/primitives/test_sampler.py @@ -382,6 +382,7 @@ def test_primitive_job_status_done(self): bell = self._circuit[1] sampler = Sampler() job = sampler.run(circuits=[bell]) + _ = job.result() self.assertEqual(job.status(), JobStatus.DONE) def test_options(self): From 2c67f663df888090f144cb0a36b8dbaf0a4807cd Mon Sep 17 00:00:00 2001 From: Etienne Wodey <44871469+airwoodix@users.noreply.github.com> Date: Fri, 14 Apr 2023 20:05:47 +0200 Subject: [PATCH 029/172] providers/options: implement Mapping (#9704) * providers/options: implement MutableMapping Resolves #9686 * Update qiskit/providers/options.py Reword docstring. Co-authored-by: Matthew Treinish * Add release notes. * providers/options: implement Mapping + setitem --------- Co-authored-by: Matthew Treinish --- qiskit/providers/options.py | 88 +++++++++++++++---- ...ment-mutable-mapping-b11f4e2c6df4cf31.yaml | 13 +++ test/python/providers/test_options.py | 34 ++++++- 3 files changed, 117 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/options-implement-mutable-mapping-b11f4e2c6df4cf31.yaml diff --git a/qiskit/providers/options.py b/qiskit/providers/options.py index d985fde4a195..b3c83a0dbdf2 100644 --- a/qiskit/providers/options.py +++ b/qiskit/providers/options.py @@ -13,17 +13,52 @@ """Container class for backend options.""" import io +from collections.abc import Mapping -class Options: +class Options(Mapping): """Base options object - This class is the abstract class that all backend options are based + This class is what all backend options are based on. The properties of the class are intended to be all dynamically adjustable so that a user can reconfigure the backend on demand. If a property is immutable to the user (eg something like number of qubits) that should be a configuration of the backend class itself instead of the options. + + Instances of this class behave like dictionaries. Accessing an + option with a default value can be done with the `get()` method: + + >>> options = Options(opt1=1, opt2=2) + >>> options.get("opt1") + 1 + >>> options.get("opt3", default="hello") + 'hello' + + Key-value pairs for all options can be retrieved using the `items()` method: + + >>> list(options.items()) + [('opt1', 1), ('opt2', 2)] + + Options can be updated by name: + + >>> options["opt1"] = 3 + >>> options.get("opt1") + 3 + + Runtime validators can be registered. See `set_validator`. + Updates through `update_options` and indexing (`__setitem__`) validate + the new value before peforming the update and raise `ValueError` if + the new value is invalid. + + >>> options.set_validator("opt1", (1, 5)) + >>> options["opt1"] = 4 + >>> options["opt1"] + 4 + >>> options["opt1"] = 10 # doctest: +ELLIPSIS + Traceback (most recent call last): + ... + ValueError: ... """ # Here there are dragons. @@ -65,13 +100,47 @@ class Options: __slots__ = ("_fields", "validator") + # implementation of the Mapping ABC: + + def __getitem__(self, key): + return self._fields[key] + + def __iter__(self): + return iter(self._fields) + + def __len__(self): + return len(self._fields) + + # Allow modifying the options (validated) + + def __setitem__(self, key, value): + self.update_options(**{key: value}) + + # backwards-compatibilty with Qiskit Experiments: + @property def __dict__(self): return self._fields + # SimpleNamespace-like access to options: + + def __getattr__(self, name): + # This does not interrupt the normal lookup of things like methods or `_fields`, because + # those are successfully resolved by the normal Python lookup apparatus. If we are here, + # then lookup has failed, so we must be looking for an option. If the user has manually + # called `self.__getattr__("_fields")` then they'll get the option not the full dict, but + # that's not really our fault. `getattr(self, "_fields")` will still find the dict. + try: + return self._fields[name] + except KeyError as ex: + raise AttributeError(f"Option {name} is not defined") from ex + + # setting options with the namespace interface is not validated def __setattr__(self, key, value): self._fields[key] = value + # custom pickling: + def __getstate__(self): return (self._fields, self.validator) @@ -186,21 +255,6 @@ def update_options(self, **fields): self._fields.update(fields) - def __getattr__(self, name): - # This does not interrupt the normal lookup of things like methods or `_fields`, because - # those are successfully resolved by the normal Python lookup apparatus. If we are here, - # then lookup has failed, so we must be looking for an option. If the user has manually - # called `self.__getattr__("_fields")` then they'll get the option not the full dict, but - # that's not really our fault. `getattr(self, "_fields")` will still find the dict. - try: - return self._fields[name] - except KeyError as ex: - raise AttributeError(f"Option {name} is not defined") from ex - - def get(self, field, default=None): - """Get an option value for a given key.""" - return self._fields.get(field, default) - def __str__(self): no_validator = super().__str__() if not self.validator: diff --git a/releasenotes/notes/options-implement-mutable-mapping-b11f4e2c6df4cf31.yaml b/releasenotes/notes/options-implement-mutable-mapping-b11f4e2c6df4cf31.yaml new file mode 100644 index 000000000000..bff02d1b7449 --- /dev/null +++ b/releasenotes/notes/options-implement-mutable-mapping-b11f4e2c6df4cf31.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + The :class:`qiskit.providers.options.Options` class now implements the + :class`Mapping` and `__setitem__`. :class:`Options` instances therefore + now offer the same interface as standard dictionaries, except for the + deletion methods (`__delitem__`, `pop`, `clear`). + Key assignments are validated by the registered validators, if any. + See `#9686 `. +upgrade: + - | + Instances of `options.__dict__.items()` and related calls can be replaced + by the same call without the `__dict__` proxy (e.g. `options.items()`). diff --git a/test/python/providers/test_options.py b/test/python/providers/test_options.py index 7ea860dfffbc..bbc2aaf13253 100644 --- a/test/python/providers/test_options.py +++ b/test/python/providers/test_options.py @@ -18,7 +18,6 @@ from qiskit.providers import Options from qiskit.qobj.utils import MeasLevel - from qiskit.test import QiskitTestCase @@ -120,6 +119,39 @@ def test_copy(self): self.assertEqual(cpy.opt2, 2) self.assertEqual(cpy.opt3, 20) + def test_iterate(self): + options = Options(opt1=1, opt2=2, opt3="abc") + options_dict = dict(options) + + self.assertEqual(options_dict, {"opt1": 1, "opt2": 2, "opt3": "abc"}) + + def test_iterate_items(self): + options = Options(opt1=1, opt2=2, opt3="abc") + items = list(options.items()) + + self.assertEqual(items, [("opt1", 1), ("opt2", 2), ("opt3", "abc")]) + + def test_mutate_mapping(self): + options = Options(opt1=1, opt2=2, opt3="abc") + + options["opt4"] = "def" + self.assertEqual(options.opt4, "def") + + options_dict = dict(options) + self.assertEqual(options_dict, {"opt1": 1, "opt2": 2, "opt3": "abc", "opt4": "def"}) + + def test_mutate_mapping_validator(self): + options = Options(shots=1024) + options.set_validator("shots", (1, 2048)) + + options["shots"] = 512 + self.assertEqual(options.shots, 512) + + with self.assertRaises(ValueError): + options["shots"] = 3096 + + self.assertEqual(options.shots, 512) + class TestOptionsSimpleNamespaceBackwardCompatibility(QiskitTestCase): """Tests that SimpleNamespace-like functionality that qiskit-experiments relies on for Options From 3ebef17322c7714aedaa71967dee74fd455a83a0 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Fri, 14 Apr 2023 14:59:29 -0600 Subject: [PATCH 030/172] Change algorithms from pending deprecation to deprecated (#9883) * Change algorithms from pending deprecation to deprecated Co-authored-by: Manoel Marques * Review feedback & fix fmt * Try to fix the tests * Another round of test fixes * Last round of test fixes? * Actual last test fix! * Use HTTPS * Refer to migration guide in deprecation changelog --------- Co-authored-by: Manoel Marques --- .../algorithms/amplitude_amplifiers/grover.py | 28 +- qiskit/algorithms/amplitude_estimators/ae.py | 26 +- qiskit/algorithms/amplitude_estimators/fae.py | 26 +- qiskit/algorithms/amplitude_estimators/iae.py | 26 +- .../algorithms/amplitude_estimators/mlae.py | 26 +- qiskit/algorithms/aux_ops_evaluator.py | 8 +- .../algorithms/eigen_solvers/eigen_solver.py | 18 +- .../eigen_solvers/numpy_eigen_solver.py | 8 +- qiskit/algorithms/eigen_solvers/vqd.py | 20 +- .../algorithms/evolvers/evolution_problem.py | 8 +- .../algorithms/evolvers/evolution_result.py | 8 +- .../algorithms/evolvers/imaginary_evolver.py | 8 +- qiskit/algorithms/evolvers/real_evolver.py | 8 +- .../evolvers/trotterization/trotter_qrte.py | 6 +- .../minimum_eigen_solver.py | 16 +- .../numpy_minimum_eigen_solver.py | 8 +- .../algorithms/minimum_eigen_solvers/qaoa.py | 10 +- .../algorithms/minimum_eigen_solvers/vqe.py | 18 +- qiskit/algorithms/optimizers/qnspsa.py | 22 +- .../hamiltonian_phase_estimation.py | 25 +- qiskit/algorithms/phase_estimators/ipe.py | 10 +- .../phase_estimators/phase_estimation.py | 10 +- ...deprecate-algorithms-c6e1e28b6091c507.yaml | 38 ++ .../evolvers/test_evolution_problem.py | 37 +- .../evolvers/test_evolution_result.py | 6 +- .../trotterization/test_trotter_qrte.py | 48 +-- .../optimizers/test_optimizer_aqgd.py | 35 +- .../optimizers/test_optimizer_nft.py | 21 +- .../optimizers/test_optimizers_scikitquant.py | 5 +- .../python/algorithms/optimizers/test_spsa.py | 4 +- .../algorithms/test_amplitude_estimators.py | 33 +- .../algorithms/test_aux_ops_evaluator.py | 7 +- test/python/algorithms/test_backendv1.py | 25 +- test/python/algorithms/test_backendv2.py | 20 +- test/python/algorithms/test_grover.py | 2 +- .../test_measure_error_mitigation.py | 35 +- .../algorithms/test_numpy_eigen_solver.py | 46 ++- .../test_numpy_minimum_eigen_solver.py | 96 +++-- .../python/algorithms/test_phase_estimator.py | 31 +- test/python/algorithms/test_qaoa.py | 89 +++-- test/python/algorithms/test_vqd.py | 308 ++++++++------- test/python/algorithms/test_vqe.py | 359 ++++++++++-------- .../algorithms/time_evolvers/test_pvqd.py | 18 +- 43 files changed, 947 insertions(+), 659 deletions(-) create mode 100644 releasenotes/notes/deprecate-algorithms-c6e1e28b6091c507.yaml diff --git a/qiskit/algorithms/amplitude_amplifiers/grover.py b/qiskit/algorithms/amplitude_amplifiers/grover.py index 60d00f62d5dc..f8e85918a136 100644 --- a/qiskit/algorithms/amplitude_amplifiers/grover.py +++ b/qiskit/algorithms/amplitude_amplifiers/grover.py @@ -116,9 +116,11 @@ class Grover(AmplitudeAmplifier): @deprecate_arg( "quantum_instance", - additional_msg="Instead, use the ``sampler`` argument.", - since="0.22.0", - pending=True, + additional_msg=( + "Instead, use the ``sampler`` argument. " + "See https://qisk.it/algo_migration for a migration guide." + ), + since="0.24.0", ) def __init__( self, @@ -145,7 +147,7 @@ def __init__( sample_from_iterations: If True, instead of taking the values in ``iterations`` as powers of the Grover operator, a random integer sample between 0 and smaller value than the iteration is used as a power, see [1], Section 4. - quantum_instance: Pending deprecation: A Quantum Instance or Backend to run the circuits. + quantum_instance: Deprecated: A Quantum Instance or Backend to run the circuits. sampler: A Sampler to use for sampling the results of the circuits. Raises: @@ -185,7 +187,7 @@ def __init__( self._quantum_instance: QuantumInstance | None = None if quantum_instance is not None: with warnings.catch_warnings(): - warnings.simplefilter("ignore", category=PendingDeprecationWarning) + warnings.simplefilter("ignore", DeprecationWarning) self.quantum_instance = quantum_instance self._sampler = sampler @@ -194,9 +196,13 @@ def __init__( self._iterations_arg = iterations @property - @deprecate_func(since="0.23.0", pending=True, is_property=True) + @deprecate_func( + since="0.24.0", + is_property=True, + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + ) def quantum_instance(self) -> QuantumInstance | None: - r"""Pending deprecation\; Get the quantum instance. + r"""Deprecated. Get the quantum instance. Returns: The quantum instance used to run this algorithm. @@ -204,9 +210,13 @@ def quantum_instance(self) -> QuantumInstance | None: return self._quantum_instance @quantum_instance.setter - @deprecate_func(since="0.23.0", pending=True, is_property=True) + @deprecate_func( + since="0.24.0", + is_property=True, + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + ) def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - r"""Pending deprecation\; Set quantum instance. + r"""Deprecated. Set quantum instance. Args: quantum_instance: The quantum instance used to run this algorithm. diff --git a/qiskit/algorithms/amplitude_estimators/ae.py b/qiskit/algorithms/amplitude_estimators/ae.py index a14e8f70a7e9..d9aced5d9fe2 100644 --- a/qiskit/algorithms/amplitude_estimators/ae.py +++ b/qiskit/algorithms/amplitude_estimators/ae.py @@ -66,9 +66,11 @@ class AmplitudeEstimation(AmplitudeEstimator): @deprecate_arg( "quantum_instance", - additional_msg="Instead, use the ``sampler`` argument.", - since="0.22.0", - pending=True, + additional_msg=( + "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " + "migration guide." + ), + since="0.24.0", ) def __init__( self, @@ -86,7 +88,7 @@ def __init__( `qiskit.circuit.library.PhaseEstimation` when None. iqft: The inverse quantum Fourier transform component, defaults to using a standard implementation from `qiskit.circuit.library.QFT` when None. - quantum_instance: Pending deprecation\: The backend (or `QuantumInstance`) to execute + quantum_instance: Deprecated: The backend (or `QuantumInstance`) to execute the circuits on. sampler: A sampler primitive to evaluate the circuits. @@ -130,9 +132,13 @@ def sampler(self, sampler: BaseSampler) -> None: self._sampler = sampler @property - @deprecate_func(since="0.23.0", pending=True, is_property=True) + @deprecate_func( + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + since="0.24.0", + is_property=True, + ) def quantum_instance(self) -> QuantumInstance | None: - """Pending deprecation; Get the quantum instance. + """Deprecated: Get the quantum instance. Returns: The quantum instance used to run this algorithm. @@ -140,9 +146,13 @@ def quantum_instance(self) -> QuantumInstance | None: return self._quantum_instance @quantum_instance.setter - @deprecate_func(since="0.23.0", pending=True, is_property=True) + @deprecate_func( + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + since="0.24.0", + is_property=True, + ) def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - """Pending deprecation; Set quantum instance. + """Deprecated: Set quantum instance. Args: quantum_instance: The quantum instance used to run this algorithm. diff --git a/qiskit/algorithms/amplitude_estimators/fae.py b/qiskit/algorithms/amplitude_estimators/fae.py index cbfeecad499b..08fedaffa34a 100644 --- a/qiskit/algorithms/amplitude_estimators/fae.py +++ b/qiskit/algorithms/amplitude_estimators/fae.py @@ -50,9 +50,11 @@ class FasterAmplitudeEstimation(AmplitudeEstimator): @deprecate_arg( "quantum_instance", - additional_msg="Instead, use the ``sampler`` argument.", - since="0.22.0", - pending=True, + additional_msg=( + "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " + "migration guide." + ), + since="0.24.0", ) def __init__( self, @@ -67,7 +69,7 @@ def __init__( delta: The probability that the true value is outside of the final confidence interval. maxiter: The number of iterations, the maximal power of Q is `2 ** (maxiter - 1)`. rescale: Whether to rescale the problem passed to `estimate`. - quantum_instance: Pending deprecation\: The quantum instance or backend + quantum_instance: Deprecated: The quantum instance or backend to run the circuits. sampler: A sampler primitive to evaluate the circuits. @@ -108,9 +110,13 @@ def sampler(self, sampler: BaseSampler) -> None: self._sampler = sampler @property - @deprecate_func(since="0.23.0", pending=True, is_property=True) + @deprecate_func( + since="0.24.0", + is_property=True, + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + ) def quantum_instance(self) -> QuantumInstance | None: - """Pending deprecation; Get the quantum instance. + """Deprecated. Get the quantum instance. Returns: The quantum instance used to run this algorithm. @@ -118,9 +124,13 @@ def quantum_instance(self) -> QuantumInstance | None: return self._quantum_instance @quantum_instance.setter - @deprecate_func(since="0.23.0", pending=True, is_property=True) + @deprecate_func( + since="0.24.0", + is_property=True, + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + ) def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - """Pending deprecation; Set quantum instance. + """Deprecated. Set quantum instance. Args: quantum_instance: The quantum instance used to run this algorithm. diff --git a/qiskit/algorithms/amplitude_estimators/iae.py b/qiskit/algorithms/amplitude_estimators/iae.py index f5aa04c926c6..e420b76e6a4e 100644 --- a/qiskit/algorithms/amplitude_estimators/iae.py +++ b/qiskit/algorithms/amplitude_estimators/iae.py @@ -52,9 +52,11 @@ class IterativeAmplitudeEstimation(AmplitudeEstimator): @deprecate_arg( "quantum_instance", - additional_msg="Instead, use the ``sampler`` argument.", - since="0.22.0", - pending=True, + additional_msg=( + "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " + "migration guide." + ), + since="0.24.0", ) def __init__( self, @@ -77,7 +79,7 @@ def __init__( each iteration, can be 'chernoff' for the Chernoff intervals or 'beta' for the Clopper-Pearson intervals (default) min_ratio: Minimal q-ratio (:math:`K_{i+1} / K_i`) for FindNextK - quantum_instance: Pending deprecation\: Quantum Instance or Backend + quantum_instance: Deprecated: Quantum Instance or Backend sampler: A sampler primitive to evaluate the circuits. Raises: @@ -131,9 +133,13 @@ def sampler(self, sampler: BaseSampler) -> None: self._sampler = sampler @property - @deprecate_func(since="0.23.0", pending=True, is_property=True) + @deprecate_func( + since="0.24.0", + is_property=True, + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + ) def quantum_instance(self) -> QuantumInstance | None: - """Pending deprecation; Get the quantum instance. + """Deprecated. Get the quantum instance. Returns: The quantum instance used to run this algorithm. @@ -141,9 +147,13 @@ def quantum_instance(self) -> QuantumInstance | None: return self._quantum_instance @quantum_instance.setter - @deprecate_func(since="0.23.0", pending=True, is_property=True) + @deprecate_func( + since="0.24.0", + is_property=True, + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + ) def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - """Pending deprecation; Set quantum instance. + """Deprecated. Set quantum instance. Args: quantum_instance: The quantum instance used to run this algorithm. diff --git a/qiskit/algorithms/amplitude_estimators/mlae.py b/qiskit/algorithms/amplitude_estimators/mlae.py index 469dcb09a97a..aa9f600b700e 100644 --- a/qiskit/algorithms/amplitude_estimators/mlae.py +++ b/qiskit/algorithms/amplitude_estimators/mlae.py @@ -55,9 +55,11 @@ class in named ``MaximumLikelihoodAmplitudeEstimation``. @deprecate_arg( "quantum_instance", - additional_msg="Instead, use the ``sampler`` argument.", - since="0.22.0", - pending=True, + additional_msg=( + "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " + "migration guide." + ), + since="0.24.0", ) def __init__( self, @@ -77,7 +79,7 @@ def __init__( according to ``evaluation_schedule``. The minimizer takes a function as first argument and a list of (float, float) tuples (as bounds) as second argument and returns a single float which is the found minimum. - quantum_instance: Pending deprecation\: Quantum Instance or Backend + quantum_instance: Deprecated: Quantum Instance or Backend sampler: A sampler primitive to evaluate the circuits. Raises: @@ -135,9 +137,13 @@ def sampler(self, sampler: BaseSampler) -> None: self._sampler = sampler @property - @deprecate_func(since="0.23.0", pending=True, is_property=True) + @deprecate_func( + since="0.24.0", + is_property=True, + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + ) def quantum_instance(self) -> QuantumInstance | None: - """Pending deprecation; Get the quantum instance. + """Deprecated. Get the quantum instance. Returns: The quantum instance used to run this algorithm. @@ -145,9 +151,13 @@ def quantum_instance(self) -> QuantumInstance | None: return self._quantum_instance @quantum_instance.setter - @deprecate_func(since="0.23.0", pending=True, is_property=True) + @deprecate_func( + since="0.24.0", + is_property=True, + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + ) def quantum_instance(self, quantum_instance: QuantumInstance | Backend) -> None: - """Pending deprecation; Set quantum instance. + """Deprecated. Set quantum instance. Args: quantum_instance: The quantum instance used to run this algorithm. diff --git a/qiskit/algorithms/aux_ops_evaluator.py b/qiskit/algorithms/aux_ops_evaluator.py index d0348f381d19..788b66d6e43f 100644 --- a/qiskit/algorithms/aux_ops_evaluator.py +++ b/qiskit/algorithms/aux_ops_evaluator.py @@ -34,10 +34,10 @@ @deprecate_func( additional_msg=( "Instead, use the function " - "``qiskit.algorithms.observables_evaluator.estimate_observables``." + "``qiskit.algorithms.observables_evaluator.estimate_observables``. See " + "https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def eval_observables( quantum_instance: QuantumInstance | Backend, @@ -47,7 +47,7 @@ def eval_observables( threshold: float = 1e-12, ) -> ListOrDict[tuple[complex, complex]]: """ - Pending deprecation: Accepts a list or a dictionary of operators and calculates + Deprecated: Accepts a list or a dictionary of operators and calculates their expectation values - means and standard deviations. They are calculated with respect to a quantum state provided. A user can optionally provide a threshold value which filters mean values falling below the threshold. diff --git a/qiskit/algorithms/eigen_solvers/eigen_solver.py b/qiskit/algorithms/eigen_solvers/eigen_solver.py index 2fc42a509627..5fd59b3e023c 100644 --- a/qiskit/algorithms/eigen_solvers/eigen_solver.py +++ b/qiskit/algorithms/eigen_solvers/eigen_solver.py @@ -23,7 +23,7 @@ class Eigensolver(ABC): - """Pending deprecation: Eigensolver Interface. + """Deprecated: Eigensolver Interface. The Eigensolver interface has been superseded by the :class:`qiskit.algorithms.eigensolvers.Eigensolver` interface. @@ -36,9 +36,11 @@ class Eigensolver(ABC): """ @deprecate_func( - additional_msg="Instead, use the interface ``qiskit.algorithms.eigensolvers.Eigensolver``", - since="0.23.0", - pending=True, + additional_msg=( + "Instead, use the interface ``qiskit.algorithms.eigensolvers.Eigensolver``. See " + "https://qisk.it/algo_migration for a migration guide." + ), + since="0.24.0", ) def __init__(self) -> None: pass @@ -76,7 +78,7 @@ def supports_aux_operators(cls) -> bool: class EigensolverResult(AlgorithmResult): - """Pending deprecation: Eigensolver Result. + """Deprecated: Eigensolver Result. The EigensolverResult class has been superseded by the :class:`qiskit.algorithms.eigensolvers.EigensolverResult` class. @@ -87,10 +89,10 @@ class EigensolverResult(AlgorithmResult): @deprecate_func( additional_msg=( - "Instead, use the class ``qiskit.algorithms.eigensolvers.EigensolverResult``." + "Instead, use the class ``qiskit.algorithms.eigensolvers.EigensolverResult``. " + "See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__(self) -> None: super().__init__() diff --git a/qiskit/algorithms/eigen_solvers/numpy_eigen_solver.py b/qiskit/algorithms/eigen_solvers/numpy_eigen_solver.py index 98da79cfff79..6b0536330441 100755 --- a/qiskit/algorithms/eigen_solvers/numpy_eigen_solver.py +++ b/qiskit/algorithms/eigen_solvers/numpy_eigen_solver.py @@ -32,7 +32,7 @@ class NumPyEigensolver(Eigensolver): r""" - Pending deprecation: NumPy Eigensolver algorithm. + Deprecated: NumPy Eigensolver algorithm. The NumPyEigensolver class has been superseded by the :class:`qiskit.algorithms.eigensolvers.NumPyEigensolver` class. @@ -50,10 +50,10 @@ class NumPyEigensolver(Eigensolver): @deprecate_func( additional_msg=( - "Instead, use the class ``qiskit.algorithms.eigensolvers.NumPyEigensolver``." + "Instead, use the class ``qiskit.algorithms.eigensolvers.NumPyEigensolver``. " + "See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__( self, diff --git a/qiskit/algorithms/eigen_solvers/vqd.py b/qiskit/algorithms/eigen_solvers/vqd.py index 90297a7dc972..72e3dd306647 100644 --- a/qiskit/algorithms/eigen_solvers/vqd.py +++ b/qiskit/algorithms/eigen_solvers/vqd.py @@ -53,7 +53,7 @@ class VQD(VariationalAlgorithm, Eigensolver): - r"""Pending deprecation: Variational Quantum Deflation algorithm. + r"""Deprecated: Variational Quantum Deflation algorithm. The VQD class has been superseded by the :class:`qiskit.algorithms.eigensolvers.VQD` class. @@ -98,9 +98,11 @@ class VQD(VariationalAlgorithm, Eigensolver): """ @deprecate_func( - additional_msg="Instead, use the class ``qiskit.algorithms.eigensolvers.VQD``", - since="0.23.0", - pending=True, + additional_msg=( + "Instead, use the class ``qiskit.algorithms.eigensolvers.VQD``." + "See https://qisk.it/algo_migration for a migration guide." + ), + since="0.24.0", ) def __init__( self, @@ -760,7 +762,7 @@ def _get_eigenstate(self, optimal_parameters) -> list[float] | dict[str, int]: class VQDResult(VariationalResult, EigensolverResult): - """Pending deprecation: VQD Result. + """Deprecated: VQD Result. The VQDResult class has been superseded by the :class:`qiskit.algorithms.eigensolvers.VQDResult` class. @@ -770,9 +772,11 @@ class VQDResult(VariationalResult, EigensolverResult): """ @deprecate_func( - additional_msg="Instead, use the class ``qiskit.algorithms.eigensolvers.VQDResult``.", - since="0.23.0", - pending=True, + additional_msg=( + "Instead, use the class ``qiskit.algorithms.eigensolvers.VQDResult``." + "See https://qisk.it/algo_migration for a migration guide." + ), + since="0.24.0", ) def __init__(self) -> None: super().__init__() diff --git a/qiskit/algorithms/evolvers/evolution_problem.py b/qiskit/algorithms/evolvers/evolution_problem.py index 856d62e19039..8d87dbd9ae6a 100644 --- a/qiskit/algorithms/evolvers/evolution_problem.py +++ b/qiskit/algorithms/evolvers/evolution_problem.py @@ -22,7 +22,7 @@ class EvolutionProblem: - """Pending deprecation: Evolution problem class. + """Deprecated: Evolution problem class. The EvolutionProblem class has been superseded by the :class:`qiskit.algorithms.time_evolvers.TimeEvolutionProblem` class. @@ -35,10 +35,10 @@ class EvolutionProblem: @deprecate_func( additional_msg=( - "Instead, use the class ``qiskit.algorithms.time_evolvers.TimeEvolutionProblem``." + "Instead, use the class ``qiskit.algorithms.time_evolvers.TimeEvolutionProblem``. " + "See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__( self, diff --git a/qiskit/algorithms/evolvers/evolution_result.py b/qiskit/algorithms/evolvers/evolution_result.py index f5513923ac9e..5dd9e103f669 100644 --- a/qiskit/algorithms/evolvers/evolution_result.py +++ b/qiskit/algorithms/evolvers/evolution_result.py @@ -22,7 +22,7 @@ class EvolutionResult(AlgorithmResult): - """Pending deprecation: Class for holding evolution result. + """Deprecated: Class for holding evolution result. The EvolutionResult class has been superseded by the :class:`qiskit.algorithms.time_evolvers.TimeEvolutionResult` class. @@ -33,10 +33,10 @@ class EvolutionResult(AlgorithmResult): @deprecate_func( additional_msg=( - "Instead, use the class ``qiskit.algorithms.time_evolvers.TimeEvolutionResult``." + "Instead, use the class ``qiskit.algorithms.time_evolvers.TimeEvolutionResult``. " + "See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__( self, diff --git a/qiskit/algorithms/evolvers/imaginary_evolver.py b/qiskit/algorithms/evolvers/imaginary_evolver.py index f6c0624f1b74..74b301d3e539 100644 --- a/qiskit/algorithms/evolvers/imaginary_evolver.py +++ b/qiskit/algorithms/evolvers/imaginary_evolver.py @@ -20,7 +20,7 @@ class ImaginaryEvolver(ABC): - """Pending deprecation: Interface for Quantum Imaginary Time Evolution. + """Deprecated: Interface for Quantum Imaginary Time Evolution. The ImaginaryEvolver interface has been superseded by the :class:`qiskit.algorithms.time_evolvers.ImaginaryTimeEvolver` interface. @@ -31,10 +31,10 @@ class ImaginaryEvolver(ABC): @deprecate_func( additional_msg=( - "Instead, use the interface ``qiskit.algorithms.time_evolvers.ImaginaryTimeEvolver``." + "Instead, use the interface ``qiskit.algorithms.time_evolvers.ImaginaryTimeEvolver``. " + "See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__(self) -> None: pass diff --git a/qiskit/algorithms/evolvers/real_evolver.py b/qiskit/algorithms/evolvers/real_evolver.py index 6dfd2d7c119b..5024143b59b6 100644 --- a/qiskit/algorithms/evolvers/real_evolver.py +++ b/qiskit/algorithms/evolvers/real_evolver.py @@ -20,7 +20,7 @@ class RealEvolver(ABC): - """Pending deprecation: Interface for Quantum Real Time Evolution. + """Deprecated: Interface for Quantum Real Time Evolution. The RealEvolver interface has been superseded by the :class:`qiskit.algorithms.time_evolvers.RealTimeEvolver` interface. @@ -31,10 +31,10 @@ class RealEvolver(ABC): @deprecate_func( additional_msg=( - "Instead, use the interface ``qiskit.algorithms.time_evolvers.RealTimeEvolver``" + "Instead, use the interface ``qiskit.algorithms.time_evolvers.RealTimeEvolver``. " + "See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__(self) -> None: pass diff --git a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py index 10c5c107efcf..4c4af495e6ba 100644 --- a/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py +++ b/qiskit/algorithms/evolvers/trotterization/trotter_qrte.py @@ -38,7 +38,7 @@ class TrotterQRTE(RealEvolver): - """Pending deprecation: Quantum Real Time Evolution using Trotterization. + """Deprecated: Quantum Real Time Evolution using Trotterization. The TrotterQRTE class has been superseded by the :class:`qiskit.algorithms.time_evolvers.trotterization.TrotterQRTE` class. @@ -68,9 +68,9 @@ class TrotterQRTE(RealEvolver): @deprecate_func( additional_msg=( "Instead, use the class ``qiskit.algorithms.time_evolvers.trotterization.TrotterQRTE``." + " See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__( self, diff --git a/qiskit/algorithms/minimum_eigen_solvers/minimum_eigen_solver.py b/qiskit/algorithms/minimum_eigen_solvers/minimum_eigen_solver.py index da0d084c20e3..6625cf30eaeb 100644 --- a/qiskit/algorithms/minimum_eigen_solvers/minimum_eigen_solver.py +++ b/qiskit/algorithms/minimum_eigen_solvers/minimum_eigen_solver.py @@ -24,7 +24,7 @@ class MinimumEigensolver(ABC): - """Pending deprecation: Minimum Eigensolver Interface. + """Deprecated: Minimum Eigensolver Interface. The Minimum Eigensolver interface has been superseded by the :class:`qiskit.algorithms.minimum_eigensolvers.MinimumEigensolver` interface. @@ -39,10 +39,10 @@ class MinimumEigensolver(ABC): @deprecate_func( additional_msg=( "Instead, use the interface " - "``qiskit.algorithms.minimum_eigensolvers.MinimumEigensolver``." + "``qiskit.algorithms.minimum_eigensolvers.MinimumEigensolver``. " + "See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__(self) -> None: pass @@ -84,7 +84,7 @@ def supports_aux_operators(cls) -> bool: class MinimumEigensolverResult(AlgorithmResult): - """Pending deprecation: Minimum Eigensolver Result. + """Deprecated: Minimum Eigensolver Result. The MinimumEigensolverResult class has been superseded by the :class:`qiskit.algorithms.minimum_eigensolvers.MinimumEigensolverResult` class. @@ -96,10 +96,10 @@ class MinimumEigensolverResult(AlgorithmResult): @deprecate_func( additional_msg=( "Instead, use the class " - "``qiskit.algorithms.minimum_eigensolvers.MinimumEigensolverResult``." + "``qiskit.algorithms.minimum_eigensolvers.MinimumEigensolverResult``. " + "See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__(self) -> None: super().__init__() diff --git a/qiskit/algorithms/minimum_eigen_solvers/numpy_minimum_eigen_solver.py b/qiskit/algorithms/minimum_eigen_solvers/numpy_minimum_eigen_solver.py index 31438445b796..83623666b715 100644 --- a/qiskit/algorithms/minimum_eigen_solvers/numpy_minimum_eigen_solver.py +++ b/qiskit/algorithms/minimum_eigen_solvers/numpy_minimum_eigen_solver.py @@ -30,7 +30,7 @@ class NumPyMinimumEigensolver(MinimumEigensolver): """ - Pending deprecation: Numpy Minimum Eigensolver algorithm. + Deprecated: Numpy Minimum Eigensolver algorithm. The NumPyMinimumEigensolver class has been superseded by the :class:`qiskit.algorithms.minimum_eigensolvers.NumPyMinimumEigensolver` class. @@ -42,10 +42,10 @@ class NumPyMinimumEigensolver(MinimumEigensolver): @deprecate_func( additional_msg=( "Instead, use the class " - "``qiskit.algorithms.minimum_eigensolvers.NumPyMinimumEigensolver``." + "``qiskit.algorithms.minimum_eigensolvers.NumPyMinimumEigensolver``. " + "See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__( self, diff --git a/qiskit/algorithms/minimum_eigen_solvers/qaoa.py b/qiskit/algorithms/minimum_eigen_solvers/qaoa.py index db31c22694df..fc18be860218 100644 --- a/qiskit/algorithms/minimum_eigen_solvers/qaoa.py +++ b/qiskit/algorithms/minimum_eigen_solvers/qaoa.py @@ -32,7 +32,7 @@ class QAOA(VQE): """ - Pending deprecation: Quantum Approximate Optimization Algorithm. + Deprecated: Quantum Approximate Optimization Algorithm. The QAOA class has been superseded by the :class:`qiskit.algorithms.minimum_eigensolvers.QAOA` class. @@ -62,9 +62,11 @@ class QAOA(VQE): """ @deprecate_func( - additional_msg="Instead, use the class ``qiskit.algorithms.minimum_eigensolvers.QAOA``.", - since="0.23.0", - pending=True, + additional_msg=( + "Instead, use the class ``qiskit.algorithms.minimum_eigensolvers.QAOA``. " + "See https://qisk.it/algo_migration for a migration guide." + ), + since="0.24.0", ) def __init__( self, diff --git a/qiskit/algorithms/minimum_eigen_solvers/vqe.py b/qiskit/algorithms/minimum_eigen_solvers/vqe.py index 2742edace2ef..bf9304e4d6b7 100755 --- a/qiskit/algorithms/minimum_eigen_solvers/vqe.py +++ b/qiskit/algorithms/minimum_eigen_solvers/vqe.py @@ -54,7 +54,7 @@ class VQE(VariationalAlgorithm, MinimumEigensolver): - r"""Pending deprecation: Variational Quantum Eigensolver algorithm. + r"""Deprecated: Variational Quantum Eigensolver algorithm. The VQE class has been superseded by the :class:`qiskit.algorithms.minimum_eigensolvers.VQE` class. @@ -128,9 +128,11 @@ def my_minimizer(fun, x0, jac=None, bounds=None) -> OptimizerResult: """ @deprecate_func( - additional_msg="Instead, use the class ``qiskit.algorithms.minimum_eigensolvers.VQE``.", - since="0.23.0", - pending=True, + additional_msg=( + "Instead, use the class ``qiskit.algorithms.minimum_eigensolvers.VQE``. " + "See https://qisk.it/algo_migration for a migration guide." + ), + since="0.24.0", ) def __init__( self, @@ -657,7 +659,7 @@ def _get_eigenstate(self, optimal_parameters) -> list[float] | dict[str, int]: class VQEResult(VariationalResult, MinimumEigensolverResult): - """Pending deprecation: VQE Result. + """Deprecated: VQE Result. The VQEResult class has been superseded by the :class:`qiskit.algorithms.minimum_eigensolvers.VQEResult` class. @@ -668,10 +670,10 @@ class VQEResult(VariationalResult, MinimumEigensolverResult): @deprecate_func( additional_msg=( - "Instead, use the class ``qiskit.algorithms.minimum_eigensolvers.VQEResult``." + "Instead, use the class ``qiskit.algorithms.minimum_eigensolvers.VQEResult``. " + "See https://qisk.it/algo_migration for a migration guide." ), - since="0.23.0", - pending=True, + since="0.24.0", ) def __init__(self) -> None: with warnings.catch_warnings(): diff --git a/qiskit/algorithms/optimizers/qnspsa.py b/qiskit/algorithms/optimizers/qnspsa.py index 93f79b18d1ce..21b902353e44 100644 --- a/qiskit/algorithms/optimizers/qnspsa.py +++ b/qiskit/algorithms/optimizers/qnspsa.py @@ -91,7 +91,7 @@ def loss(x): result = qnspsa.optimize(ansatz.num_parameters, loss, initial_point=initial_point) This is a legacy version solving the same problem but using Qiskit Opflow instead - of the Qiskit Primitives. Note however, that this usage is pending deprecation. + of the Qiskit Primitives. Note however, that this usage is deprecated. .. code-block:: python @@ -255,8 +255,16 @@ def settings(self) -> dict[str, Any]: return settings @staticmethod - @deprecate_arg("backend", since="0.22", pending=True) - @deprecate_arg("expectation", since="0.22", pending=True) + @deprecate_arg( + "backend", + since="0.24.0", + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + ) + @deprecate_arg( + "expectation", + since="0.24.0", + additional_msg="See https://qisk.it/algo_migration for a migration guide.", + ) def get_fidelity( circuit: QuantumCircuit, backend: Backend | QuantumInstance | None = None, @@ -285,9 +293,9 @@ def get_fidelity( Args: circuit: The circuit preparing the parameterized ansatz. - backend: *Pending deprecation.* A backend of quantum instance to evaluate the circuits. + backend: Deprecated. A backend of quantum instance to evaluate the circuits. If None, plain matrix multiplication will be used. - expectation: *Pending deprecation.* An expectation converter to specify how the expected + expectation: Deprecated. An expectation converter to specify how the expected value is computed. If a shot-based readout is used this should be set to ``PauliExpectation``. sampler: A sampler primitive to sample from a quantum state. @@ -331,11 +339,11 @@ def _legacy_get_fidelity( backend: Backend | QuantumInstance | None = None, expectation: ExpectationBase | None = None, ) -> Callable[[np.ndarray, np.ndarray], float]: - r"""PENDING DEPRECATION. Get a function to compute the fidelity of ``circuit`` with itself. + r"""Deprecated. Get a function to compute the fidelity of ``circuit`` with itself. .. note:: - This method is pending deprecation. Instead use the :class:`~.ComputeUncompute` + This method is deprecated. Instead use the :class:`~.ComputeUncompute` class which implements the fidelity calculation in the same fashion as this method. Let ``circuit`` be a parameterized quantum circuit performing the operation diff --git a/qiskit/algorithms/phase_estimators/hamiltonian_phase_estimation.py b/qiskit/algorithms/phase_estimators/hamiltonian_phase_estimation.py index ee06e2b31e26..87907c39e304 100644 --- a/qiskit/algorithms/phase_estimators/hamiltonian_phase_estimation.py +++ b/qiskit/algorithms/phase_estimators/hamiltonian_phase_estimation.py @@ -14,6 +14,8 @@ from __future__ import annotations +import warnings + from qiskit import QuantumCircuit from qiskit.utils import QuantumInstance from qiskit.utils.deprecation import deprecate_arg @@ -93,9 +95,11 @@ class HamiltonianPhaseEstimation: @deprecate_arg( "quantum_instance", - additional_msg="Instead, use the ``sampler`` argument.", - since="0.22.0", - pending=True, + additional_msg=( + "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " + "migration guide." + ), + since="0.24.0", ) def __init__( self, @@ -107,15 +111,18 @@ def __init__( Args: num_evaluation_qubits: The number of qubits used in estimating the phase. The phase will be estimated as a binary string with this many bits. - quantum_instance: Pending deprecation\: The quantum instance on which + quantum_instance: Deprecated: The quantum instance on which the circuit will be run. sampler: The sampler primitive on which the circuit will be sampled. """ - self._phase_estimation = PhaseEstimation( - num_evaluation_qubits=num_evaluation_qubits, - quantum_instance=quantum_instance, - sampler=sampler, - ) + # Avoid double warning on deprecated used of `quantum_instance`. + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + self._phase_estimation = PhaseEstimation( + num_evaluation_qubits=num_evaluation_qubits, + quantum_instance=quantum_instance, + sampler=sampler, + ) def _get_scale(self, hamiltonian, bound=None) -> PhaseEstimationScale: if bound is None: diff --git a/qiskit/algorithms/phase_estimators/ipe.py b/qiskit/algorithms/phase_estimators/ipe.py index 9d2f6e1417ca..e8e583027a92 100644 --- a/qiskit/algorithms/phase_estimators/ipe.py +++ b/qiskit/algorithms/phase_estimators/ipe.py @@ -41,9 +41,11 @@ class IterativePhaseEstimation(PhaseEstimator): @deprecate_arg( "quantum_instance", - additional_msg="Instead, use the ``sampler`` argument.", - since="0.22.0", - pending=True, + additional_msg=( + "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " + "migration guide." + ), + since="0.24.0", ) def __init__( self, @@ -54,7 +56,7 @@ def __init__( r""" Args: num_iterations: The number of iterations (rounds) of the phase estimation to run. - quantum_instance: Pending deprecation\: The quantum instance on which the + quantum_instance: Deprecated: The quantum instance on which the circuit will be run. sampler: The sampler primitive on which the circuit will be sampled. diff --git a/qiskit/algorithms/phase_estimators/phase_estimation.py b/qiskit/algorithms/phase_estimators/phase_estimation.py index ff8e92dd11d2..a3bc1d59c60d 100644 --- a/qiskit/algorithms/phase_estimators/phase_estimation.py +++ b/qiskit/algorithms/phase_estimators/phase_estimation.py @@ -83,9 +83,11 @@ class PhaseEstimation(PhaseEstimator): @deprecate_arg( "quantum_instance", - additional_msg="Instead, use the ``sampler`` argument.", - since="0.22.0", - pending=True, + additional_msg=( + "Instead, use the ``sampler`` argument. See https://qisk.it/algo_migration for a " + "migration guide." + ), + since="0.24.0", ) def __init__( self, @@ -97,7 +99,7 @@ def __init__( Args: num_evaluation_qubits: The number of qubits used in estimating the phase. The phase will be estimated as a binary string with this many bits. - quantum_instance: Pending deprecation\: The quantum instance on which the + quantum_instance: Deprecated: The quantum instance on which the circuit will be run. sampler: The sampler primitive on which the circuit will be sampled. diff --git a/releasenotes/notes/deprecate-algorithms-c6e1e28b6091c507.yaml b/releasenotes/notes/deprecate-algorithms-c6e1e28b6091c507.yaml new file mode 100644 index 000000000000..fdcbcf8890e5 --- /dev/null +++ b/releasenotes/notes/deprecate-algorithms-c6e1e28b6091c507.yaml @@ -0,0 +1,38 @@ +--- +deprecations: + - | + All of the following features are now deprecated, after having been made pending deprecation + since 0.22.0. More information is available at https://qisk.it/algo_migration. + + Module :mod:`qiskit.algorithms.minimum_eigen_solvers` is deprecated and + superseded by :mod:`qiskit.algorithms.minimum_eigensolvers`. + + Module :mod:`qiskit.algorithms.eigen_solvers` is deprecated and + superseded by :mod:`qiskit.algorithms.eigensolvers`. + + Module :mod:`qiskit.algorithms.evolvers` is deprecated and + superseded by :mod:`qiskit.algorithms.time_evolvers`. + + Class :class:`qiskit.algorithms.TrotterQRTE` is deprecated and superseded by + :class:`qiskit.algorithms.time_evolvers.trotterization.TrotterQRTE`. + + Using :class:`~qiskit.utils.QuantumInstance` or :class:`~qiskit.providers.Backend` + is deprecated and superseded by + :class:`~qiskit.primitives.BaseSampler` in the following classes: + :class:`~qiskit.algorithms.amplitude_amplifiers.Grover`, + :class:`~qiskit.algorithms.amplitude_estimators.AmplitudeEstimation`, + :class:`~qiskit.algorithms.amplitude_estimators.FasterAmplitudeEstimation`, + :class:`~qiskit.algorithms.amplitude_estimators.IterativePhaseEstimation`, + :class:`~qiskit.algorithms.amplitude_estimators.MaximumLikelihoodAmplitudeEstimation`, + :class:`~qiskit.algorithms.phase_estimators.HamiltonianPhaseEstimation`, + :class:`~qiskit.algorithms.phase_estimators.IterativePhaseEstimation`, + :class:`~qiskit.algorithms.phase_estimators.PhaseEstimation` + + Using :class:`~qiskit.utils.QuantumInstance` or :class:`~qiskit.providers.Backend` + or :class:`~qiskit.opflow.ExpectationBase` is deprecated and superseded by + :class:`~qiskit.primitives.BaseSampler` in the following static method: + :meth:`~qiskit.algorithms.optimizers.QNSPSA.get_fidelity` + + Function :func:`~qiskit.algorithms.aux_ops_evaluator.eval_observables` is deprecated + and superseded by + :func:`~qiskit.algorithms.observables_evaluator.estimate_observables` function. diff --git a/test/python/algorithms/evolvers/test_evolution_problem.py b/test/python/algorithms/evolvers/test_evolution_problem.py index d3dd2f7bc9b2..6f0cf4e7fd83 100644 --- a/test/python/algorithms/evolvers/test_evolution_problem.py +++ b/test/python/algorithms/evolvers/test_evolution_problem.py @@ -31,7 +31,8 @@ def test_init_default(self): time = 2.5 initial_state = One - evo_problem = EvolutionProblem(hamiltonian, time, initial_state) + with self.assertWarns(DeprecationWarning): + evo_problem = EvolutionProblem(hamiltonian, time, initial_state) expected_hamiltonian = Y expected_time = 2.5 @@ -56,14 +57,15 @@ def test_init_all(self): aux_operators = [X, Y] param_value_dict = {t_parameter: 3.2} - evo_problem = EvolutionProblem( - hamiltonian, - time, - initial_state, - aux_operators, - t_param=t_parameter, - param_value_dict=param_value_dict, - ) + with self.assertWarns(DeprecationWarning): + evo_problem = EvolutionProblem( + hamiltonian, + time, + initial_state, + aux_operators, + t_param=t_parameter, + param_value_dict=param_value_dict, + ) expected_hamiltonian = Y + t_parameter * Z expected_time = 2 @@ -83,7 +85,7 @@ def test_init_all(self): @unpack def test_init_errors(self, hamiltonian, time, initial_state): """Tests expected errors are thrown on invalid time argument.""" - with assert_raises(ValueError): + with self.assertWarns(DeprecationWarning), assert_raises(ValueError): _ = EvolutionProblem(hamiltonian, time, initial_state) def test_validate_params(self): @@ -93,21 +95,30 @@ def test_validate_params(self): with self.subTest(msg="Parameter missing in dict."): hamiltonian = param_x * X + param_y * Y param_dict = {param_y: 2} - evolution_problem = EvolutionProblem(hamiltonian, 2, Zero, param_value_dict=param_dict) + with self.assertWarns(DeprecationWarning): + evolution_problem = EvolutionProblem( + hamiltonian, 2, Zero, param_value_dict=param_dict + ) with assert_raises(ValueError): evolution_problem.validate_params() with self.subTest(msg="Empty dict."): hamiltonian = param_x * X + param_y * Y param_dict = {} - evolution_problem = EvolutionProblem(hamiltonian, 2, Zero, param_value_dict=param_dict) + with self.assertWarns(DeprecationWarning): + evolution_problem = EvolutionProblem( + hamiltonian, 2, Zero, param_value_dict=param_dict + ) with assert_raises(ValueError): evolution_problem.validate_params() with self.subTest(msg="Extra parameter in dict."): hamiltonian = param_x * X + param_y * Y param_dict = {param_y: 2, param_x: 1, Parameter("z"): 1} - evolution_problem = EvolutionProblem(hamiltonian, 2, Zero, param_value_dict=param_dict) + with self.assertWarns(DeprecationWarning): + evolution_problem = EvolutionProblem( + hamiltonian, 2, Zero, param_value_dict=param_dict + ) with assert_raises(ValueError): evolution_problem.validate_params() diff --git a/test/python/algorithms/evolvers/test_evolution_result.py b/test/python/algorithms/evolvers/test_evolution_result.py index 5500b283a1cb..2fe40d8c66f8 100644 --- a/test/python/algorithms/evolvers/test_evolution_result.py +++ b/test/python/algorithms/evolvers/test_evolution_result.py @@ -23,7 +23,8 @@ class TestEvolutionResult(QiskitAlgorithmsTestCase): def test_init_state(self): """Tests that a class is initialized correctly with an evolved_state.""" evolved_state = Zero - evo_result = EvolutionResult(evolved_state=evolved_state) + with self.assertWarns(DeprecationWarning): + evo_result = EvolutionResult(evolved_state=evolved_state) expected_state = Zero expected_aux_ops_evaluated = None @@ -35,7 +36,8 @@ def test_init_observable(self): """Tests that a class is initialized correctly with an evolved_observable.""" evolved_state = Zero evolved_aux_ops_evaluated = [(5j, 5j), (1.0, 8j), (5 + 1j, 6 + 1j)] - evo_result = EvolutionResult(evolved_state, evolved_aux_ops_evaluated) + with self.assertWarns(DeprecationWarning): + evo_result = EvolutionResult(evolved_state, evolved_aux_ops_evaluated) expected_state = Zero expected_aux_ops_evaluated = [(5j, 5j), (1.0, 8j), (5 + 1j, 6 + 1j)] diff --git a/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py b/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py index 47e4940b2f07..7f7f5e797c82 100644 --- a/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py +++ b/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py @@ -92,10 +92,10 @@ def test_trotter_qrte_trotter_single_qubit(self, product_formula, expected_state operator = SummedOp([X, Z]) initial_state = StateFn([1, 0]) time = 1 - evolution_problem = EvolutionProblem(operator, time, initial_state) - - trotter_qrte = TrotterQRTE(product_formula=product_formula) - evolution_result_state_circuit = trotter_qrte.evolve(evolution_problem).evolved_state + with self.assertWarns(DeprecationWarning): + evolution_problem = EvolutionProblem(operator, time, initial_state) + trotter_qrte = TrotterQRTE(product_formula=product_formula) + evolution_result_state_circuit = trotter_qrte.evolve(evolution_problem).evolved_state np.testing.assert_equal(evolution_result_state_circuit.eval(), expected_state) @@ -107,7 +107,8 @@ def test_trotter_qrte_trotter_single_qubit_aux_ops(self): initial_state = Zero time = 3 - evolution_problem = EvolutionProblem(operator, time, initial_state, aux_ops) + with self.assertWarns(DeprecationWarning): + evolution_problem = EvolutionProblem(operator, time, initial_state, aux_ops) expected_evolved_state = VectorStateFn( Statevector([0.98008514 + 0.13970775j, 0.01991486 + 0.13970775j], dims=(2,)) @@ -126,8 +127,9 @@ def test_trotter_qrte_trotter_single_qubit_aux_ops(self): operator=operator, backend=backend, ) - trotter_qrte = TrotterQRTE(quantum_instance=backend, expectation=expectation) - evolution_result = trotter_qrte.evolve(evolution_problem) + with self.assertWarns(DeprecationWarning): + trotter_qrte = TrotterQRTE(quantum_instance=backend, expectation=expectation) + evolution_result = trotter_qrte.evolve(evolution_problem) np.testing.assert_equal( evolution_result.evolved_state.eval(), expected_evolved_state @@ -169,10 +171,10 @@ def test_trotter_qrte_trotter_two_qubits(self, operator, expected_state): """Test for TrotterQRTE on two qubits with various types of a Hamiltonian.""" # LieTrotter with 1 rep initial_state = StateFn([1, 0, 0, 0]) - evolution_problem = EvolutionProblem(operator, 1, initial_state) - - trotter_qrte = TrotterQRTE() - evolution_result = trotter_qrte.evolve(evolution_problem) + with self.assertWarns(DeprecationWarning): + evolution_problem = EvolutionProblem(operator, 1, initial_state) + trotter_qrte = TrotterQRTE() + evolution_result = trotter_qrte.evolve(evolution_problem) np.testing.assert_equal(evolution_result.evolved_state.eval(), expected_state) def test_trotter_qrte_trotter_two_qubits_with_params(self): @@ -184,14 +186,15 @@ def test_trotter_qrte_trotter_two_qubits_with_params(self): params_dict = {w_param: 2.0, u_param: 3.0} operator = w_param * (Z ^ Z) / 2.0 + (Z ^ I) + u_param * (I ^ Z) / 3.0 time = 1 - evolution_problem = EvolutionProblem( - operator, time, initial_state, param_value_dict=params_dict - ) expected_state = VectorStateFn( Statevector([-0.9899925 - 0.14112001j, 0.0 + 0.0j, 0.0 + 0.0j, 0.0 + 0.0j], dims=(2, 2)) ) - trotter_qrte = TrotterQRTE() - evolution_result = trotter_qrte.evolve(evolution_problem) + with self.assertWarns(DeprecationWarning): + evolution_problem = EvolutionProblem( + operator, time, initial_state, param_value_dict=params_dict + ) + trotter_qrte = TrotterQRTE() + evolution_result = trotter_qrte.evolve(evolution_problem) np.testing.assert_equal(evolution_result.evolved_state.eval(), expected_state) @data( @@ -213,11 +216,11 @@ def test_trotter_qrte_qdrift(self, initial_state, expected_state): """Test for TrotterQRTE with QDrift.""" operator = SummedOp([X, Z]) time = 1 - evolution_problem = EvolutionProblem(operator, time, initial_state) - algorithm_globals.random_seed = 0 - trotter_qrte = TrotterQRTE(product_formula=QDrift()) - evolution_result = trotter_qrte.evolve(evolution_problem) + with self.assertWarns(DeprecationWarning): + evolution_problem = EvolutionProblem(operator, time, initial_state) + trotter_qrte = TrotterQRTE(product_formula=QDrift()) + evolution_result = trotter_qrte.evolve(evolution_problem) np.testing.assert_equal(evolution_result.evolved_state.eval(), expected_state) @data((Parameter("t"), {}), (None, {Parameter("x"): 2}), (None, None)) @@ -228,8 +231,8 @@ def test_trotter_qrte_trotter_errors(self, t_param, param_value_dict): initial_state = Zero time = 1 algorithm_globals.random_seed = 0 - trotter_qrte = TrotterQRTE() - with assert_raises(ValueError): + with self.assertWarns(DeprecationWarning): + trotter_qrte = TrotterQRTE() evolution_problem = EvolutionProblem( operator, time, @@ -237,6 +240,7 @@ def test_trotter_qrte_trotter_errors(self, t_param, param_value_dict): t_param=t_param, param_value_dict=param_value_dict, ) + with assert_raises(ValueError): _ = trotter_qrte.evolve(evolution_problem) diff --git a/test/python/algorithms/optimizers/test_optimizer_aqgd.py b/test/python/algorithms/optimizers/test_optimizer_aqgd.py index d4923b7cb18b..da1c6e8db889 100644 --- a/test/python/algorithms/optimizers/test_optimizer_aqgd.py +++ b/test/python/algorithms/optimizers/test_optimizer_aqgd.py @@ -53,13 +53,14 @@ def test_simple(self): ) aqgd = AQGD(momentum=0.0) - vqe = VQE( - ansatz=RealAmplitudes(), - optimizer=aqgd, - gradient=Gradient("lin_comb"), - quantum_instance=q_instance, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=RealAmplitudes(), + optimizer=aqgd, + gradient=Gradient("lin_comb"), + quantum_instance=q_instance, + ) + result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=3) @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") @@ -74,8 +75,9 @@ def test_list(self): ) aqgd = AQGD(maxiter=[1000, 1000, 1000], eta=[1.0, 0.5, 0.3], momentum=[0.0, 0.5, 0.75]) - vqe = VQE(ansatz=RealAmplitudes(), optimizer=aqgd, quantum_instance=q_instance) - result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE(ansatz=RealAmplitudes(), optimizer=aqgd, quantum_instance=q_instance) + result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=3) def test_raises_exception(self): @@ -95,13 +97,14 @@ def test_int_values(self): ) aqgd = AQGD(maxiter=1000, eta=1, momentum=0) - vqe = VQE( - ansatz=RealAmplitudes(), - optimizer=aqgd, - gradient=Gradient("lin_comb"), - quantum_instance=q_instance, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=RealAmplitudes(), + optimizer=aqgd, + gradient=Gradient("lin_comb"), + quantum_instance=q_instance, + ) + result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=3) diff --git a/test/python/algorithms/optimizers/test_optimizer_nft.py b/test/python/algorithms/optimizers/test_optimizer_nft.py index b3f813188c42..765c0ad3d880 100644 --- a/test/python/algorithms/optimizers/test_optimizer_nft.py +++ b/test/python/algorithms/optimizers/test_optimizer_nft.py @@ -42,16 +42,17 @@ def setUp(self): def test_nft(self): """Test NFT optimizer by using it""" - vqe = VQE( - ansatz=RealAmplitudes(), - optimizer=NFT(), - quantum_instance=QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ), - ) - result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=RealAmplitudes(), + optimizer=NFT(), + quantum_instance=QuantumInstance( + BasicAer.get_backend("statevector_simulator"), + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ), + ) + result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) self.assertAlmostEqual(result.eigenvalue.real, -1.857275, places=6) diff --git a/test/python/algorithms/optimizers/test_optimizers_scikitquant.py b/test/python/algorithms/optimizers/test_optimizers_scikitquant.py index 503451de2394..f07a0238f7de 100644 --- a/test/python/algorithms/optimizers/test_optimizers_scikitquant.py +++ b/test/python/algorithms/optimizers/test_optimizers_scikitquant.py @@ -52,8 +52,9 @@ def _optimize(self, optimizer): seed_simulator=algorithm_globals.random_seed, seed_transpiler=algorithm_globals.random_seed, ) - vqe = VQE(ansatz=RealAmplitudes(), optimizer=optimizer, quantum_instance=qe) - result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE(ansatz=RealAmplitudes(), optimizer=optimizer, quantum_instance=qe) + result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=1) def test_bobyqa(self): diff --git a/test/python/algorithms/optimizers/test_spsa.py b/test/python/algorithms/optimizers/test_spsa.py index 6204198c02ba..394bee0ba81d 100644 --- a/test/python/algorithms/optimizers/test_spsa.py +++ b/test/python/algorithms/optimizers/test_spsa.py @@ -201,9 +201,9 @@ def objective(x): def test_qnspsa_fidelity_deprecation(self): """Test using a backend and expectation converter in get_fidelity warns.""" ansatz = PauliTwoDesign(2, reps=1, seed=2) - with self.assertWarns(PendingDeprecationWarning): + with self.assertWarns(DeprecationWarning): QNSPSA.get_fidelity(ansatz, backend=StatevectorSimulatorPy()) - with self.assertWarns(PendingDeprecationWarning): + with self.assertWarns(DeprecationWarning): QNSPSA.get_fidelity(ansatz, expectation=MatrixExpectation()) # No warning when used correctly. QNSPSA.get_fidelity(ansatz) diff --git a/test/python/algorithms/test_amplitude_estimators.py b/test/python/algorithms/test_amplitude_estimators.py index 2c4b7b792171..4868f2643788 100644 --- a/test/python/algorithms/test_amplitude_estimators.py +++ b/test/python/algorithms/test_amplitude_estimators.py @@ -128,7 +128,8 @@ def sampler_shots(shots=100): @unpack def test_statevector(self, prob, qae, expect): """statevector test""" - qae.quantum_instance = self._statevector + with self.assertWarns(DeprecationWarning): + qae.quantum_instance = self._statevector problem = EstimationProblem(BernoulliStateIn(prob), 0, BernoulliGrover(prob)) result = qae.estimate(problem) @@ -185,7 +186,8 @@ def test_sampler(self, prob, qae, expect): @unpack def test_qasm(self, prob, shots, qae, expect): """qasm test""" - qae.quantum_instance = self._qasm(shots) + with self.assertWarns(DeprecationWarning): + qae.quantum_instance = self._qasm(shots) problem = EstimationProblem(BernoulliStateIn(prob), [0], BernoulliGrover(prob)) result = qae.estimate(problem) @@ -409,7 +411,8 @@ def test_statevector(self, n, qae, expect): """Statevector end-to-end test""" # construct factories for A and Q # qae.state_preparation = SineIntegral(n) - qae.quantum_instance = self._statevector + with self.assertWarns(DeprecationWarning): + qae.quantum_instance = self._statevector estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) # result = qae.run(self._statevector) @@ -454,7 +457,8 @@ def test_sampler(self, n, qae, expect): def test_qasm(self, n, shots, qae, expect): """QASM simulator end-to-end test.""" # construct factories for A and Q - qae.quantum_instance = self._qasm(shots) + with self.assertWarns(DeprecationWarning): + qae.quantum_instance = self._qasm(shots) estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) result = qae.estimate(estimation_problem) @@ -510,7 +514,8 @@ def test_sampler_with_shots(self, n, shots, qae, expect): def test_confidence_intervals(self, qae, key, expect): """End-to-end test for all confidence intervals.""" n = 3 - qae.quantum_instance = self._statevector + with self.assertWarns(DeprecationWarning): + qae.quantum_instance = self._statevector estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) # statevector simulator @@ -527,7 +532,8 @@ def test_confidence_intervals(self, qae, key, expect): # qasm simulator shots = 100 alpha = 0.01 - qae.quantum_instance = self._qasm(shots) + with self.assertWarns(DeprecationWarning): + qae.quantum_instance = self._qasm(shots) result = qae.estimate(estimation_problem) for method, expected_confint in expect.items(): confint = qae.compute_confidence_interval(result, alpha, method) @@ -537,7 +543,8 @@ def test_confidence_intervals(self, qae, key, expect): def test_iqae_confidence_intervals(self): """End-to-end test for the IQAE confidence interval.""" n = 3 - qae = IterativeAmplitudeEstimation(0.1, 0.01, quantum_instance=self._statevector) + with self.assertWarns(DeprecationWarning): + qae = IterativeAmplitudeEstimation(0.1, 0.01, quantum_instance=self._statevector) expected_confint = (0.1984050, 0.3511015) estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) @@ -551,7 +558,8 @@ def test_iqae_confidence_intervals(self): # qasm simulator shots = 100 - qae.quantum_instance = self._qasm(shots) + with self.assertWarns(DeprecationWarning): + qae.quantum_instance = self._qasm(shots) result = qae.estimate(estimation_problem) confint = result.confidence_interval np.testing.assert_array_almost_equal(confint, expected_confint) @@ -603,7 +611,8 @@ def test_run_without_rescaling(self): # construct algo without rescaling backend = BasicAer.get_backend("statevector_simulator") - fae = FasterAmplitudeEstimation(0.1, 1, rescale=False, quantum_instance=backend) + with self.assertWarns(DeprecationWarning): + fae = FasterAmplitudeEstimation(0.1, 1, rescale=False, quantum_instance=backend) # run the algo result = fae.estimate(problem) @@ -647,7 +656,8 @@ def test_rescaling_with_custom_grover_raises(self): # construct algo without rescaling backend = BasicAer.get_backend("statevector_simulator") - fae = FasterAmplitudeEstimation(0.1, 1, quantum_instance=backend) + with self.assertWarns(DeprecationWarning): + fae = FasterAmplitudeEstimation(0.1, 1, quantum_instance=backend) # run the algo with self.assertWarns(Warning): @@ -682,7 +692,8 @@ def is_good_state(bitstr): BasicAer.get_backend(backend_str), seed_simulator=2, seed_transpiler=2 ) # cannot use rescaling with a custom grover operator - fae = FasterAmplitudeEstimation(0.01, 5, rescale=False, quantum_instance=backend) + with self.assertWarns(DeprecationWarning): + fae = FasterAmplitudeEstimation(0.01, 5, rescale=False, quantum_instance=backend) # run the algo result = fae.estimate(problem) diff --git a/test/python/algorithms/test_aux_ops_evaluator.py b/test/python/algorithms/test_aux_ops_evaluator.py index 0f956bbb9adf..5a75843eeac9 100644 --- a/test/python/algorithms/test_aux_ops_evaluator.py +++ b/test/python/algorithms/test_aux_ops_evaluator.py @@ -85,9 +85,10 @@ def _run_test( observables: ListOrDict[OperatorBase], quantum_instance: Union[QuantumInstance, Backend], ): - result = eval_observables( - quantum_instance, quantum_state, observables, expectation, self.threshold - ) + with self.assertWarns(DeprecationWarning): + result = eval_observables( + quantum_instance, quantum_state, observables, expectation, self.threshold + ) if isinstance(observables, dict): np.testing.assert_equal(list(result.keys()), list(expected_result.keys())) diff --git a/test/python/algorithms/test_backendv1.py b/test/python/algorithms/test_backendv1.py index ed4bf878495a..2ed807d3d8ce 100644 --- a/test/python/algorithms/test_backendv1.py +++ b/test/python/algorithms/test_backendv1.py @@ -47,14 +47,14 @@ def test_vqe_qasm(self): qasm_simulator = QuantumInstance( self._qasm, shots=1024, seed_simulator=self.seed, seed_transpiler=self.seed ) - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=qasm_simulator, - ) - - result = vqe.compute_minimum_eigenvalue(operator=h2_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + optimizer=optimizer, + max_evals_grouped=1, + quantum_instance=qasm_simulator, + ) + result = vqe.compute_minimum_eigenvalue(operator=h2_op) self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) def test_run_circuit_oracle(self): @@ -65,7 +65,7 @@ def test_run_circuit_oracle(self): qi = QuantumInstance( self._provider.get_backend("fake_vigo"), seed_simulator=12, seed_transpiler=32 ) - with self.assertWarns(PendingDeprecationWarning): + with self.assertWarns(DeprecationWarning): grover = Grover(quantum_instance=qi) result = grover.amplify(problem) self.assertIn(result.top_measurement, ["11"]) @@ -78,7 +78,7 @@ def test_run_circuit_oracle_single_experiment_backend(self): backend = self._provider.get_backend("fake_vigo") backend._configuration.max_experiments = 1 qi = QuantumInstance(backend, seed_simulator=12, seed_transpiler=32) - with self.assertWarns(PendingDeprecationWarning): + with self.assertWarns(DeprecationWarning): grover = Grover(quantum_instance=qi) result = grover.amplify(problem) self.assertIn(result.top_measurement, ["11"]) @@ -118,8 +118,9 @@ def test_measurement_error_mitigation_with_vqe(self): optimizer = SPSA(maxiter=200) ansatz = EfficientSU2(2, reps=1) - vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) - result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) + with self.assertWarns(DeprecationWarning): + vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) + result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) self.assertGreater(quantum_instance.time_taken, 0.0) quantum_instance.reset_execution_results() self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) diff --git a/test/python/algorithms/test_backendv2.py b/test/python/algorithms/test_backendv2.py index 71712a4a5c7e..83bbf848f211 100644 --- a/test/python/algorithms/test_backendv2.py +++ b/test/python/algorithms/test_backendv2.py @@ -47,14 +47,14 @@ def test_vqe_qasm(self): qasm_simulator = QuantumInstance( self._qasm, shots=1024, seed_simulator=self.seed, seed_transpiler=self.seed ) - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=qasm_simulator, - ) - - result = vqe.compute_minimum_eigenvalue(operator=h2_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + optimizer=optimizer, + max_evals_grouped=1, + quantum_instance=qasm_simulator, + ) + result = vqe.compute_minimum_eigenvalue(operator=h2_op) self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) def test_run_circuit_oracle(self): @@ -65,7 +65,7 @@ def test_run_circuit_oracle(self): qi = QuantumInstance( self._provider.get_backend("fake_yorktown"), seed_simulator=12, seed_transpiler=32 ) - with self.assertWarns(PendingDeprecationWarning): + with self.assertWarns(DeprecationWarning): grover = Grover(quantum_instance=qi) result = grover.amplify(problem) self.assertIn(result.top_measurement, ["11"]) @@ -80,7 +80,7 @@ def test_run_circuit_oracle_single_experiment_backend(self): qi = QuantumInstance( self._provider.get_backend("fake_yorktown"), seed_simulator=12, seed_transpiler=32 ) - with self.assertWarns(PendingDeprecationWarning): + with self.assertWarns(DeprecationWarning): grover = Grover(quantum_instance=qi) result = grover.amplify(problem) self.assertIn(result.top_measurement, ["11"]) diff --git a/test/python/algorithms/test_grover.py b/test/python/algorithms/test_grover.py index 5658ad5a2323..ae9e627b411f 100644 --- a/test/python/algorithms/test_grover.py +++ b/test/python/algorithms/test_grover.py @@ -326,7 +326,7 @@ def _prepare_grover( sample_from_iterations=sample_from_iterations, ) else: - with self.assertWarns(PendingDeprecationWarning): + with self.assertWarns(DeprecationWarning): grover = Grover( quantum_instance=self.qasm, iterations=iterations, diff --git a/test/python/algorithms/test_measure_error_mitigation.py b/test/python/algorithms/test_measure_error_mitigation.py index b53b6fd797d0..8a53708ec5f4 100644 --- a/test/python/algorithms/test_measure_error_mitigation.py +++ b/test/python/algorithms/test_measure_error_mitigation.py @@ -152,8 +152,9 @@ def test_measurement_error_mitigation_with_vqe(self, config): optimizer = SPSA(maxiter=200) ansatz = EfficientSU2(2, reps=1) - vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) - result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) + with self.assertWarns(DeprecationWarning): + vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) + result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) self.assertGreater(quantum_instance.time_taken, 0.0) quantum_instance.reset_execution_results() self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) @@ -201,12 +202,13 @@ def test_measurement_error_mitigation_qaoa(self): seed_simulator=algorithm_globals.random_seed, seed_transpiler=algorithm_globals.random_seed, ) - qaoa = QAOA( - optimizer=COBYLA(maxiter=3), - quantum_instance=quantum_instance, - initial_point=initial_point, - ) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + qaoa = QAOA( + optimizer=COBYLA(maxiter=3), + quantum_instance=quantum_instance, + initial_point=initial_point, + ) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) ref_eigenvalue = result.eigenvalue.real # compute with noise @@ -224,12 +226,13 @@ def test_measurement_error_mitigation_qaoa(self): shots=10000, ) - qaoa = QAOA( - optimizer=COBYLA(maxiter=3), - quantum_instance=quantum_instance, - initial_point=initial_point, - ) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + qaoa = QAOA( + optimizer=COBYLA(maxiter=3), + quantum_instance=quantum_instance, + initial_point=initial_point, + ) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) self.assertAlmostEqual(result.eigenvalue.real, ref_eigenvalue, delta=0.05) @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") @@ -330,8 +333,8 @@ def test_measurement_error_mitigation_with_vqe_ignis(self, config): optimizer = SPSA(maxiter=200) ansatz = EfficientSU2(2, reps=1) - vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) - with self.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): + with self.assertWarnsRegex(DeprecationWarning): + vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) self.assertGreater(quantum_instance.time_taken, 0.0) quantum_instance.reset_execution_results() diff --git a/test/python/algorithms/test_numpy_eigen_solver.py b/test/python/algorithms/test_numpy_eigen_solver.py index d26b6e202feb..6b2b41b796a7 100644 --- a/test/python/algorithms/test_numpy_eigen_solver.py +++ b/test/python/algorithms/test_numpy_eigen_solver.py @@ -40,8 +40,9 @@ def setUp(self): def test_ce(self): """Test basics""" - algo = NumPyEigensolver() - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) + with self.assertWarns(DeprecationWarning): + algo = NumPyEigensolver() + result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) self.assertEqual(len(result.eigenvalues), 1) self.assertEqual(len(result.eigenstates), 1) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -49,8 +50,9 @@ def test_ce(self): def test_ce_k4(self): """Test for k=4 eigenvalues""" - algo = NumPyEigensolver(k=4) - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) + with self.assertWarns(DeprecationWarning): + algo = NumPyEigensolver(k=4) + result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) self.assertEqual(len(result.eigenvalues), 4) self.assertEqual(len(result.eigenstates), 4) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -66,8 +68,9 @@ def test_ce_k4_filtered(self): def criterion(x, v, a_v): return v >= -1 - algo = NumPyEigensolver(k=4, filter_criterion=criterion) - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) + with self.assertWarns(DeprecationWarning): + algo = NumPyEigensolver(k=4, filter_criterion=criterion) + result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) self.assertEqual(len(result.eigenvalues), 2) self.assertEqual(len(result.eigenstates), 2) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -81,23 +84,26 @@ def test_ce_k4_filtered_empty(self): def criterion(x, v, a_v): return False - algo = NumPyEigensolver(k=4, filter_criterion=criterion) - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) + with self.assertWarns(DeprecationWarning): + algo = NumPyEigensolver(k=4, filter_criterion=criterion) + result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) self.assertEqual(len(result.eigenvalues), 0) self.assertEqual(len(result.eigenstates), 0) @data(X, Y, Z) def test_ce_k1_1q(self, op): """Test for 1 qubit operator""" - algo = NumPyEigensolver(k=1) - result = algo.compute_eigenvalues(operator=op) + with self.assertWarns(DeprecationWarning): + algo = NumPyEigensolver(k=1) + result = algo.compute_eigenvalues(operator=op) np.testing.assert_array_almost_equal(result.eigenvalues, [-1]) @data(X, Y, Z) def test_ce_k2_1q(self, op): """Test for 1 qubit operator""" - algo = NumPyEigensolver(k=2) - result = algo.compute_eigenvalues(operator=op) + with self.assertWarns(DeprecationWarning): + algo = NumPyEigensolver(k=2) + result = algo.compute_eigenvalues(operator=op) np.testing.assert_array_almost_equal(result.eigenvalues, [-1, 1]) def test_aux_operators_list(self): @@ -105,8 +111,9 @@ def test_aux_operators_list(self): aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] - algo = NumPyEigensolver() - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + algo = NumPyEigensolver() + result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=aux_ops) self.assertEqual(len(result.eigenvalues), 1) self.assertEqual(len(result.eigenstates), 1) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -122,7 +129,8 @@ def test_aux_operators_list(self): # Go again with additional None and zero operators extra_ops = [*aux_ops, None, 0] - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=extra_ops) + with self.assertWarns(DeprecationWarning): + result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=extra_ops) self.assertEqual(len(result.eigenvalues), 1) self.assertEqual(len(result.eigenstates), 1) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -144,8 +152,9 @@ def test_aux_operators_dict(self): aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - algo = NumPyEigensolver() - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + algo = NumPyEigensolver() + result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=aux_ops) self.assertEqual(len(result.eigenvalues), 1) self.assertEqual(len(result.eigenstates), 1) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -161,7 +170,8 @@ def test_aux_operators_dict(self): # Go again with additional None and zero operators extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=extra_ops) + with self.assertWarns(DeprecationWarning): + result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=extra_ops) self.assertEqual(len(result.eigenvalues), 1) self.assertEqual(len(result.eigenstates), 1) self.assertEqual(result.eigenvalues.dtype, np.float64) diff --git a/test/python/algorithms/test_numpy_minimum_eigen_solver.py b/test/python/algorithms/test_numpy_minimum_eigen_solver.py index 160e2796de4e..c4d5a3b43ddd 100644 --- a/test/python/algorithms/test_numpy_minimum_eigen_solver.py +++ b/test/python/algorithms/test_numpy_minimum_eigen_solver.py @@ -45,10 +45,11 @@ def setUp(self): def test_cme(self): """Basic test""" - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) + with self.assertWarns(DeprecationWarning): + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) @@ -57,38 +58,45 @@ def test_cme(self): def test_cme_reuse(self): """Test reuse""" # Start with no operator or aux_operators, give via compute method - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op) + with self.assertWarns(DeprecationWarning): + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op) self.assertEqual(result.eigenvalue.dtype, np.float64) self.assertAlmostEqual(result.eigenvalue, -1.85727503) self.assertIsNone(result.aux_operator_eigenvalues) # Add aux_operators and go again - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) + with self.assertWarns(DeprecationWarning): + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) # "Remove" aux_operators and go again - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=[]) + with self.assertWarns(DeprecationWarning): + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=[]) self.assertEqual(result.eigenvalue.dtype, np.float64) self.assertAlmostEqual(result.eigenvalue, -1.85727503) self.assertIsNone(result.aux_operator_eigenvalues) # Set aux_operators and go again - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) + with self.assertWarns(DeprecationWarning): + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) # Finally just set one of aux_operators and main operator, remove aux_operators - result = algo.compute_minimum_eigenvalue(operator=self.aux_ops_list[0], aux_operators=[]) + with self.assertWarns(DeprecationWarning): + result = algo.compute_minimum_eigenvalue( + operator=self.aux_ops_list[0], aux_operators=[] + ) self.assertAlmostEqual(result.eigenvalue, 2 + 0j) self.assertIsNone(result.aux_operator_eigenvalues) @@ -100,10 +108,11 @@ def test_cme_filter(self): def criterion(x, v, a_v): return v >= -0.5 - algo = NumPyMinimumEigensolver(filter_criterion=criterion) - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) + with self.assertWarns(DeprecationWarning): + algo = NumPyMinimumEigensolver(filter_criterion=criterion) + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) self.assertAlmostEqual(result.eigenvalue, -0.22491125 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) @@ -117,10 +126,11 @@ def test_cme_filter_empty(self): def criterion(x, v, a_v): return False - algo = NumPyMinimumEigensolver(filter_criterion=criterion) - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_list - ) + with self.assertWarns(DeprecationWarning): + algo = NumPyMinimumEigensolver(filter_criterion=criterion) + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_list + ) self.assertEqual(result.eigenvalue, None) self.assertEqual(result.eigenstate, None) self.assertEqual(result.aux_operator_eigenvalues, None) @@ -128,22 +138,25 @@ def criterion(x, v, a_v): @data(X, Y, Z) def test_cme_1q(self, op): """Test for 1 qubit operator""" - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=op) + with self.assertWarns(DeprecationWarning): + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue(operator=op) self.assertAlmostEqual(result.eigenvalue, -1) def test_cme_aux_ops_dict(self): """Test dictionary compatibility of aux_operators""" # Start with an empty dictionary - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators={}) + with self.assertWarns(DeprecationWarning): + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators={}) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertIsNone(result.aux_operator_eigenvalues) # Add aux_operators dictionary and go again - result = algo.compute_minimum_eigenvalue( - operator=self.qubit_op, aux_operators=self.aux_ops_dict - ) + with self.assertWarns(DeprecationWarning): + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=self.aux_ops_dict + ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) @@ -151,7 +164,10 @@ def test_cme_aux_ops_dict(self): # Add None and zero operators and go again extra_ops = {"None_op": None, "zero_op": 0, **self.aux_ops_dict} - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + with self.assertWarns(DeprecationWarning): + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=extra_ops + ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 3) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) @@ -163,8 +179,9 @@ def test_aux_operators_list(self): aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values @@ -176,7 +193,10 @@ def test_aux_operators_list(self): # Go again with additional None and zero operators extra_ops = [*aux_ops, None, 0] - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + with self.assertWarns(DeprecationWarning): + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=extra_ops + ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 4) # expectation values @@ -194,8 +214,9 @@ def test_aux_operators_dict(self): aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - algo = NumPyMinimumEigensolver() - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + algo = NumPyMinimumEigensolver() + result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values @@ -207,7 +228,10 @@ def test_aux_operators_dict(self): # Go again with additional None and zero operators extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=extra_ops) + with self.assertWarns(DeprecationWarning): + result = algo.compute_minimum_eigenvalue( + operator=self.qubit_op, aux_operators=extra_ops + ) self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 3) # expectation values diff --git a/test/python/algorithms/test_phase_estimator.py b/test/python/algorithms/test_phase_estimator.py index 3153d87d7b79..2af6b902a7fc 100644 --- a/test/python/algorithms/test_phase_estimator.py +++ b/test/python/algorithms/test_phase_estimator.py @@ -61,9 +61,10 @@ def hamiltonian_pe( if backend is None: backend = qiskit.BasicAer.get_backend("statevector_simulator") quantum_instance = qiskit.utils.QuantumInstance(backend=backend, shots=10000) - phase_est = HamiltonianPhaseEstimation( - num_evaluation_qubits=num_evaluation_qubits, quantum_instance=quantum_instance - ) + with self.assertWarns(DeprecationWarning): + phase_est = HamiltonianPhaseEstimation( + num_evaluation_qubits=num_evaluation_qubits, quantum_instance=quantum_instance + ) result = phase_est.estimate( hamiltonian=hamiltonian, state_preparation=state_preparation, @@ -160,7 +161,8 @@ def _setup_from_bound(self, evolution, op_class): hamiltonian = hamiltonian.to_matrix_op() backend = qiskit.BasicAer.get_backend("statevector_simulator") qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) - phase_est = HamiltonianPhaseEstimation(num_evaluation_qubits=6, quantum_instance=qi) + with self.assertWarns(DeprecationWarning): + phase_est = HamiltonianPhaseEstimation(num_evaluation_qubits=6, quantum_instance=qi) result = phase_est.estimate( hamiltonian=hamiltonian, bound=bound, @@ -328,12 +330,14 @@ def one_phase( qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) if phase_estimator is None: phase_estimator = IterativePhaseEstimation - if phase_estimator == IterativePhaseEstimation: - p_est = IterativePhaseEstimation(num_iterations=num_iterations, quantum_instance=qi) - elif phase_estimator == PhaseEstimation: - p_est = PhaseEstimation(num_evaluation_qubits=6, quantum_instance=qi) - else: - raise ValueError("Unrecognized phase_estimator") + + with self.assertWarns(DeprecationWarning): + if phase_estimator == IterativePhaseEstimation: + p_est = IterativePhaseEstimation(num_iterations=num_iterations, quantum_instance=qi) + elif phase_estimator == PhaseEstimation: + p_est = PhaseEstimation(num_evaluation_qubits=6, quantum_instance=qi) + else: + raise ValueError("Unrecognized phase_estimator") result = p_est.estimate(unitary=unitary_circuit, state_preparation=state_preparation) phase = result.phase return phase @@ -407,9 +411,10 @@ def phase_estimation( if backend is None: backend = qiskit.BasicAer.get_backend("statevector_simulator") qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) - phase_est = PhaseEstimation( - num_evaluation_qubits=num_evaluation_qubits, quantum_instance=qi - ) + with self.assertWarns(DeprecationWarning): + phase_est = PhaseEstimation( + num_evaluation_qubits=num_evaluation_qubits, quantum_instance=qi + ) if construct_circuit: pe_circuit = phase_est.construct_circuit(unitary_circuit, state_preparation) result = phase_est.estimate_from_pe_circuit(pe_circuit, unitary_circuit.num_qubits) diff --git a/test/python/algorithms/test_qaoa.py b/test/python/algorithms/test_qaoa.py index 87e5a6050038..15dcc61ad753 100644 --- a/test/python/algorithms/test_qaoa.py +++ b/test/python/algorithms/test_qaoa.py @@ -92,9 +92,10 @@ def test_qaoa(self, w, prob, m, solutions, convert_to_matrix_op): if convert_to_matrix_op: qubit_op = qubit_op.to_matrix_op() - qaoa = QAOA(COBYLA(), prob, mixer=m, quantum_instance=self.statevector_simulator) + with self.assertWarns(DeprecationWarning): + qaoa = QAOA(COBYLA(), prob, mixer=m, quantum_instance=self.statevector_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) self.assertIn(graph_solution, solutions) @@ -126,9 +127,10 @@ def test_qaoa_qc_mixer(self, w, prob, solutions, convert_to_matrix_op): theta = Parameter("θ") mixer.rx(theta, range(num_qubits)) - qaoa = QAOA(optimizer, prob, mixer=mixer, quantum_instance=self.statevector_simulator) + with self.assertWarns(DeprecationWarning): + qaoa = QAOA(optimizer, prob, mixer=mixer, quantum_instance=self.statevector_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) self.assertIn(graph_solution, solutions) @@ -144,8 +146,9 @@ def test_qaoa_qc_mixer_many_parameters(self): theta = Parameter("θ" + str(i)) mixer.rx(theta, range(num_qubits)) - qaoa = QAOA(optimizer, reps=2, mixer=mixer, quantum_instance=self.statevector_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + qaoa = QAOA(optimizer, reps=2, mixer=mixer, quantum_instance=self.statevector_simulator) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) self.log.debug(x) graph_solution = self._get_graph_solution(x) @@ -160,8 +163,9 @@ def test_qaoa_qc_mixer_no_parameters(self): # just arbitrary circuit mixer.rx(np.pi / 2, range(num_qubits)) - qaoa = QAOA(COBYLA(), reps=1, mixer=mixer, quantum_instance=self.statevector_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + qaoa = QAOA(COBYLA(), reps=1, mixer=mixer, quantum_instance=self.statevector_simulator) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) # we just assert that we get a result, it is not meaningful. self.assertIsNotNone(result.eigenstate) @@ -170,8 +174,9 @@ def test_change_operator_size(self): qubit_op, _ = self._get_operator( np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) ) - qaoa = QAOA(COBYLA(), 1, quantum_instance=self.statevector_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + qaoa = QAOA(COBYLA(), 1, quantum_instance=self.statevector_simulator) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) with self.subTest(msg="QAOA 4x4"): @@ -190,7 +195,8 @@ def test_change_operator_size(self): ) ) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) with self.subTest(msg="QAOA 6x6"): @@ -209,14 +215,15 @@ def cb_callback(eval_count, parameters, mean, std): if eval_count == 1: first_pt = list(parameters) - qaoa = QAOA( - COBYLA(), - initial_point=init_pt, - callback=cb_callback, - quantum_instance=self.statevector_simulator, - ) + with self.assertWarns(DeprecationWarning): + qaoa = QAOA( + COBYLA(), + initial_point=init_pt, + callback=cb_callback, + quantum_instance=self.statevector_simulator, + ) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) @@ -246,18 +253,19 @@ def test_qaoa_initial_state(self, w, init_state): initial_state.initialize(init_state, initial_state.qubits) zero_init_state = QuantumCircuit(QuantumRegister(qubit_op.num_qubits, "q")) - qaoa_zero_init_state = QAOA( - optimizer=optimizer, - initial_state=zero_init_state, - initial_point=init_pt, - quantum_instance=self.statevector_simulator, - ) - qaoa = QAOA( - optimizer=optimizer, - initial_state=initial_state, - initial_point=init_pt, - quantum_instance=self.statevector_simulator, - ) + with self.assertWarns(DeprecationWarning): + qaoa_zero_init_state = QAOA( + optimizer=optimizer, + initial_state=zero_init_state, + initial_point=init_pt, + quantum_instance=self.statevector_simulator, + ) + qaoa = QAOA( + optimizer=optimizer, + initial_state=initial_state, + initial_point=init_pt, + quantum_instance=self.statevector_simulator, + ) zero_circuits = qaoa_zero_init_state.construct_circuit(init_pt, qubit_op) custom_circuits = qaoa.construct_circuit(init_pt, qubit_op) @@ -295,14 +303,18 @@ def test_qaoa_random_initial_point(self): rx.undirected_gnp_random_graph(5, 0.5, seed=algorithm_globals.random_seed) ) qubit_op, _ = self._get_operator(w) - qaoa = QAOA(optimizer=NELDER_MEAD(disp=True), reps=1, quantum_instance=self.qasm_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + qaoa = QAOA( + optimizer=NELDER_MEAD(disp=True), reps=1, quantum_instance=self.qasm_simulator + ) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) self.assertLess(result.eigenvalue, -0.97) def test_qaoa_construct_circuit_update(self): """Test updating operators with QAOA construct_circuit""" - qaoa = QAOA() + with self.assertWarns(DeprecationWarning): + qaoa = QAOA() ref = qaoa.construct_circuit([0, 0], I ^ Z)[0] circ2 = qaoa.construct_circuit([0, 0], I ^ Z)[0] self.assertEqual(circ2, ref) @@ -313,11 +325,12 @@ def test_qaoa_construct_circuit_update(self): def test_optimizer_scipy_callable(self): """Test passing a SciPy optimizer directly as callable.""" - qaoa = QAOA( - optimizer=partial(scipy_minimize, method="Nelder-Mead", options={"maxiter": 2}), - quantum_instance=self.statevector_simulator, - ) - result = qaoa.compute_minimum_eigenvalue(Z) + with self.assertWarns(DeprecationWarning): + qaoa = QAOA( + optimizer=partial(scipy_minimize, method="Nelder-Mead", options={"maxiter": 2}), + quantum_instance=self.statevector_simulator, + ) + result = qaoa.compute_minimum_eigenvalue(Z) self.assertEqual(result.cost_function_evals, 4) def _get_operator(self, weight_matrix): diff --git a/test/python/algorithms/test_vqd.py b/test/python/algorithms/test_vqd.py index 2fb2a573bc98..8079a063ddf6 100644 --- a/test/python/algorithms/test_vqd.py +++ b/test/python/algorithms/test_vqd.py @@ -90,20 +90,21 @@ def setUp(self): def test_basic_aer_statevector(self): """Test the VQD on BasicAer's statevector simulator.""" wavefunction = self.ryrz_wavefunction - vqd = VQD( - k=2, - ansatz=wavefunction, - optimizer=COBYLA(), - quantum_instance=QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - basis_gates=["u1", "u2", "u3", "cx", "id"], - coupling_map=[[0, 1]], - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ), - ) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + k=2, + ansatz=wavefunction, + optimizer=COBYLA(), + quantum_instance=QuantumInstance( + BasicAer.get_backend("statevector_simulator"), + basis_gates=["u1", "u2", "u3", "cx", "id"], + coupling_map=[[0, 1]], + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ), + ) - result = vqd.compute_eigenvalues(operator=self.h2_op) + result = vqd.compute_eigenvalues(operator=self.h2_op) with self.subTest(msg="test eigenvalue"): np.testing.assert_array_almost_equal( @@ -123,14 +124,15 @@ def test_mismatching_num_qubits(self): """Ensuring circuit and operator mismatch is caught""" wavefunction = QuantumCircuit(1) optimizer = SLSQP(maxiter=50) - vqd = VQD( - k=1, - ansatz=wavefunction, - optimizer=optimizer, - quantum_instance=self.statevector_simulator, - ) - with self.assertRaises(AlgorithmError): - _ = vqd.compute_eigenvalues(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + k=1, + ansatz=wavefunction, + optimizer=optimizer, + quantum_instance=self.statevector_simulator, + ) + with self.assertRaises(AlgorithmError): + _ = vqd.compute_eigenvalues(operator=self.h2_op) @data( (MatrixExpectation(), 1), @@ -142,7 +144,8 @@ def test_construct_circuit(self, expectation, num_circuits): """Test construct circuits returns QuantumCircuits and the right number of them.""" try: wavefunction = EfficientSU2(2, reps=1) - vqd = VQD(k=2, ansatz=wavefunction, expectation=expectation) + with self.assertWarns(DeprecationWarning): + vqd = VQD(k=2, ansatz=wavefunction, expectation=expectation) params = [0] * wavefunction.num_parameters circuits = vqd.construct_circuit(parameter=params, operator=self.h2_op) @@ -156,26 +159,28 @@ def test_construct_circuit(self, expectation, num_circuits): def test_missing_varform_params(self): """Test specifying a variational form with no parameters raises an error.""" circuit = QuantumCircuit(self.h2_op.num_qubits) - vqd = VQD( - k=1, ansatz=circuit, quantum_instance=BasicAer.get_backend("statevector_simulator") - ) - with self.assertRaises(RuntimeError): - vqd.compute_eigenvalues(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + k=1, ansatz=circuit, quantum_instance=BasicAer.get_backend("statevector_simulator") + ) + with self.assertRaises(RuntimeError): + vqd.compute_eigenvalues(operator=self.h2_op) def test_basic_aer_qasm(self): """Test the VQD on BasicAer's QASM simulator.""" optimizer = COBYLA(maxiter=1000) wavefunction = self.ry_wavefunction - vqd = VQD( - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=self.qasm_simulator, - ) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + ansatz=wavefunction, + optimizer=optimizer, + max_evals_grouped=1, + quantum_instance=self.qasm_simulator, + ) - # TODO benchmark this later. - result = vqd.compute_eigenvalues(operator=self.h2_op) + # TODO benchmark this later. + result = vqd.compute_eigenvalues(operator=self.h2_op) np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=1 ) @@ -192,15 +197,16 @@ def test_with_aer_statevector(self): seed_simulator=algorithm_globals.random_seed, seed_transpiler=algorithm_globals.random_seed, ) - vqd = VQD( - k=2, - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=quantum_instance, - ) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + k=2, + ansatz=wavefunction, + optimizer=optimizer, + max_evals_grouped=1, + quantum_instance=quantum_instance, + ) + result = vqd.compute_eigenvalues(operator=self.h2_op) - result = vqd.compute_eigenvalues(operator=self.h2_op) np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=2 ) @@ -218,15 +224,15 @@ def test_with_aer_qasm(self): seed_transpiler=algorithm_globals.random_seed, ) - vqd = VQD( - k=2, - ansatz=wavefunction, - optimizer=optimizer, - expectation=PauliExpectation(), - quantum_instance=quantum_instance, - ) - - result = vqd.compute_eigenvalues(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + k=2, + ansatz=wavefunction, + optimizer=optimizer, + expectation=PauliExpectation(), + quantum_instance=quantum_instance, + ) + result = vqd.compute_eigenvalues(operator=self.h2_op) np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=1 @@ -246,15 +252,16 @@ def test_with_aer_qasm_snapshot_mode(self): seed_simulator=algorithm_globals.random_seed, seed_transpiler=algorithm_globals.random_seed, ) - vqd = VQD( - k=2, - ansatz=wavefunction, - optimizer=optimizer, - expectation=AerPauliExpectation(), - quantum_instance=quantum_instance, - ) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + k=2, + ansatz=wavefunction, + optimizer=optimizer, + expectation=AerPauliExpectation(), + quantum_instance=quantum_instance, + ) - result = vqd.compute_eigenvalues(operator=self.test_op) + result = vqd.compute_eigenvalues(operator=self.test_op) np.testing.assert_array_almost_equal(result.eigenvalues.real, self.test_results, decimal=1) def test_callback(self): @@ -271,13 +278,14 @@ def store_intermediate_result(eval_count, parameters, mean, std, step): optimizer = COBYLA(maxiter=3) wavefunction = self.ry_wavefunction - vqd = VQD( - ansatz=wavefunction, - optimizer=optimizer, - callback=store_intermediate_result, - quantum_instance=self.qasm_simulator, - ) - vqd.compute_eigenvalues(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + ansatz=wavefunction, + optimizer=optimizer, + callback=store_intermediate_result, + quantum_instance=self.qasm_simulator, + ) + vqd.compute_eigenvalues(operator=self.h2_op) self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) @@ -298,39 +306,44 @@ def store_intermediate_result(eval_count, parameters, mean, std, step): def test_reuse(self): """Test re-using a VQD algorithm instance.""" - vqd = VQD(k=1) + with self.assertWarns(DeprecationWarning): + vqd = VQD(k=1) with self.subTest(msg="assert running empty raises AlgorithmError"): - with self.assertRaises(AlgorithmError): + with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): _ = vqd.compute_eigenvalues(operator=self.h2_op) ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") vqd.ansatz = ansatz with self.subTest(msg="assert missing operator raises AlgorithmError"): - with self.assertRaises(AlgorithmError): + with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): _ = vqd.compute_eigenvalues(operator=self.h2_op) vqd.expectation = MatrixExpectation() vqd.quantum_instance = self.statevector_simulator with self.subTest(msg="assert VQE works once all info is available"): - result = vqd.compute_eigenvalues(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(operator=self.h2_op) np.testing.assert_array_almost_equal(result.eigenvalues.real, self.h2_energy, decimal=2) operator = PrimitiveOp(np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]])) with self.subTest(msg="assert minimum eigensolver interface works"): - result = vqd.compute_eigenvalues(operator=operator) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(operator=operator) self.assertAlmostEqual(result.eigenvalues.real[0], -1.0, places=5) def test_vqd_optimizer(self): """Test running same VQD twice to re-use optimizer, then switch optimizer""" - vqd = VQD( - k=2, - optimizer=SLSQP(), - quantum_instance=QuantumInstance(BasicAer.get_backend("statevector_simulator")), - ) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + k=2, + optimizer=SLSQP(), + quantum_instance=QuantumInstance(BasicAer.get_backend("statevector_simulator")), + ) def run_check(): - result = vqd.compute_eigenvalues(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(operator=self.h2_op) np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=3 ) @@ -347,14 +360,16 @@ def run_check(): @data(MatrixExpectation(), None) def test_backend_change(self, user_expectation): """Test that VQE works when backend changes.""" - vqd = VQD( - k=1, - ansatz=TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz"), - optimizer=SLSQP(maxiter=2), - expectation=user_expectation, - quantum_instance=BasicAer.get_backend("statevector_simulator"), - ) - result0 = vqd.compute_eigenvalues(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + k=1, + ansatz=TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz"), + optimizer=SLSQP(maxiter=2), + expectation=user_expectation, + quantum_instance=BasicAer.get_backend("statevector_simulator"), + ) + with self.assertWarns(DeprecationWarning): + result0 = vqd.compute_eigenvalues(operator=self.h2_op) if user_expectation is not None: with self.subTest("User expectation kept."): self.assertEqual(vqd.expectation, user_expectation) @@ -362,7 +377,8 @@ def test_backend_change(self, user_expectation): vqd.quantum_instance = BasicAer.get_backend("qasm_simulator") # works also if no expectation is set, since it will be determined automatically - result1 = vqd.compute_eigenvalues(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + result1 = vqd.compute_eigenvalues(operator=self.h2_op) if user_expectation is not None: with self.subTest("Change backend with user expectation, it is kept."): @@ -373,33 +389,37 @@ def test_backend_change(self, user_expectation): def test_set_ansatz_to_none(self): """Tests that setting the ansatz to None results in the default behavior""" - vqd = VQD( - k=1, - ansatz=self.ryrz_wavefunction, - optimizer=L_BFGS_B(), - quantum_instance=self.statevector_simulator, - ) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + k=1, + ansatz=self.ryrz_wavefunction, + optimizer=L_BFGS_B(), + quantum_instance=self.statevector_simulator, + ) vqd.ansatz = None self.assertIsInstance(vqd.ansatz, RealAmplitudes) def test_set_optimizer_to_none(self): """Tests that setting the optimizer to None results in the default behavior""" - vqd = VQD( - k=1, - ansatz=self.ryrz_wavefunction, - optimizer=L_BFGS_B(), - quantum_instance=self.statevector_simulator, - ) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + k=1, + ansatz=self.ryrz_wavefunction, + optimizer=L_BFGS_B(), + quantum_instance=self.statevector_simulator, + ) vqd.optimizer = None self.assertIsInstance(vqd.optimizer, SLSQP) def test_aux_operators_list(self): """Test list-based aux_operators.""" wavefunction = self.ry_wavefunction - vqd = VQD(k=2, ansatz=wavefunction, quantum_instance=self.statevector_simulator) + with self.assertWarns(DeprecationWarning): + vqd = VQD(k=2, ansatz=wavefunction, quantum_instance=self.statevector_simulator) # Start with an empty list - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=[]) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(self.h2_op, aux_operators=[]) np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=2 ) @@ -409,7 +429,8 @@ def test_aux_operators_list(self): aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=2 ) @@ -423,7 +444,8 @@ def test_aux_operators_list(self): # Go again with additional None and zero operators extra_ops = [*aux_ops, None, 0] - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=extra_ops) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(self.h2_op, aux_operators=extra_ops) np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=2 ) @@ -441,10 +463,12 @@ def test_aux_operators_list(self): def test_aux_operators_dict(self): """Test dictionary compatibility of aux_operators""" wavefunction = self.ry_wavefunction - vqd = VQD(ansatz=wavefunction, quantum_instance=self.statevector_simulator) + with self.assertWarns(DeprecationWarning): + vqd = VQD(ansatz=wavefunction, quantum_instance=self.statevector_simulator) # Start with an empty dictionary - result = vqd.compute_eigenvalues(self.h2_op, aux_operators={}) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(self.h2_op, aux_operators={}) np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=2 ) @@ -454,7 +478,8 @@ def test_aux_operators_dict(self): aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) self.assertEqual(len(result.eigenvalues), 2) self.assertEqual(len(result.eigenstates), 2) self.assertEqual(result.eigenvalues.dtype, np.complex128) @@ -470,7 +495,8 @@ def test_aux_operators_dict(self): # Go again with additional None and zero operators extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=extra_ops) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(self.h2_op, aux_operators=extra_ops) self.assertEqual(len(result.eigenvalues), 2) self.assertEqual(len(result.eigenstates), 2) self.assertEqual(result.eigenvalues.dtype, np.complex128) @@ -490,28 +516,30 @@ def test_aux_operators_dict(self): def test_aux_operator_std_dev_pauli(self): """Test non-zero standard deviations of aux operators with PauliExpectation.""" wavefunction = self.ry_wavefunction - vqd = VQD( - ansatz=wavefunction, - expectation=PauliExpectation(), - initial_point=[ - 1.70256666, - -5.34843975, - -0.39542903, - 5.99477786, - -2.74374986, - -4.85284669, - 0.2442925, - -1.51638917, - ], - optimizer=COBYLA(maxiter=0), - quantum_instance=self.qasm_simulator, - ) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + ansatz=wavefunction, + expectation=PauliExpectation(), + initial_point=[ + 1.70256666, + -5.34843975, + -0.39542903, + 5.99477786, + -2.74374986, + -4.85284669, + 0.2442925, + -1.51638917, + ], + optimizer=COBYLA(maxiter=0), + quantum_instance=self.qasm_simulator, + ) # Go again with two auxiliary operators aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1) @@ -526,7 +554,8 @@ def test_aux_operator_std_dev_pauli(self): # Go again with additional None and zero operators aux_ops = [*aux_ops, None, 0] - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) self.assertEqual(len(result.aux_operator_eigenvalues[0]), 4) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1) @@ -547,23 +576,25 @@ def test_aux_operator_std_dev_pauli(self): def test_aux_operator_std_dev_aer_pauli(self): """Test non-zero standard deviations of aux operators with AerPauliExpectation.""" wavefunction = self.ry_wavefunction - vqd = VQD( - ansatz=wavefunction, - expectation=AerPauliExpectation(), - optimizer=COBYLA(maxiter=0), - quantum_instance=QuantumInstance( - backend=Aer.get_backend("qasm_simulator"), - shots=1, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ), - ) + with self.assertWarns(DeprecationWarning): + vqd = VQD( + ansatz=wavefunction, + expectation=AerPauliExpectation(), + optimizer=COBYLA(maxiter=0), + quantum_instance=QuantumInstance( + backend=Aer.get_backend("qasm_simulator"), + shots=1, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ), + ) # Go again with two auxiliary operators aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1) @@ -576,7 +607,8 @@ def test_aux_operator_std_dev_aer_pauli(self): # Go again with additional None and zero operators aux_ops = [*aux_ops, None, 0] - result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) self.assertEqual(len(result.aux_operator_eigenvalues[-1]), 4) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=6) diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py index 3e2d4f248955..c0711e408b7c 100644 --- a/test/python/algorithms/test_vqe.py +++ b/test/python/algorithms/test_vqe.py @@ -117,19 +117,19 @@ def setUp(self): def test_basic_aer_statevector(self): """Test the VQE on BasicAer's statevector simulator.""" wavefunction = self.ryrz_wavefunction - vqe = VQE( - ansatz=wavefunction, - optimizer=L_BFGS_B(), - quantum_instance=QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - basis_gates=["u1", "u2", "u3", "cx", "id"], - coupling_map=[[0, 1]], - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ), - ) - - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + optimizer=L_BFGS_B(), + quantum_instance=QuantumInstance( + BasicAer.get_backend("statevector_simulator"), + basis_gates=["u1", "u2", "u3", "cx", "id"], + coupling_map=[[0, 1]], + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ), + ) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) with self.subTest(msg="test eigenvalue"): self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy) @@ -147,10 +147,13 @@ def test_circuit_input(self): """Test running the VQE on a plain QuantumCircuit object.""" wavefunction = QuantumCircuit(2).compose(EfficientSU2(2)) optimizer = SLSQP(maxiter=50) - vqe = VQE( - ansatz=wavefunction, optimizer=optimizer, quantum_instance=self.statevector_simulator - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + optimizer=optimizer, + quantum_instance=self.statevector_simulator, + ) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) @data( @@ -163,7 +166,8 @@ def test_construct_circuit(self, expectation, num_circuits): """Test construct circuits returns QuantumCircuits and the right number of them.""" try: wavefunction = EfficientSU2(2, reps=1) - vqe = VQE(ansatz=wavefunction, expectation=expectation) + with self.assertWarns(DeprecationWarning): + vqe = VQE(ansatz=wavefunction, expectation=expectation) params = [0] * wavefunction.num_parameters circuits = vqe.construct_circuit(parameter=params, operator=self.h2_op) @@ -177,9 +181,12 @@ def test_construct_circuit(self, expectation, num_circuits): def test_missing_varform_params(self): """Test specifying a variational form with no parameters raises an error.""" circuit = QuantumCircuit(self.h2_op.num_qubits) - vqe = VQE(ansatz=circuit, quantum_instance=BasicAer.get_backend("statevector_simulator")) - with self.assertRaises(RuntimeError): - vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=circuit, quantum_instance=BasicAer.get_backend("statevector_simulator") + ) + with self.assertRaises(RuntimeError): + vqe.compute_minimum_eigenvalue(operator=self.h2_op) @data( (SLSQP(maxiter=50), 5, 4), @@ -188,13 +195,14 @@ def test_missing_varform_params(self): @unpack def test_max_evals_grouped(self, optimizer, places, max_evals_grouped): """VQE Optimizers test""" - vqe = VQE( - ansatz=self.ryrz_wavefunction, - optimizer=optimizer, - max_evals_grouped=max_evals_grouped, - quantum_instance=self.statevector_simulator, - ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=self.ryrz_wavefunction, + optimizer=optimizer, + max_evals_grouped=max_evals_grouped, + quantum_instance=self.statevector_simulator, + ) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=places) def test_basic_aer_qasm(self): @@ -202,22 +210,24 @@ def test_basic_aer_qasm(self): optimizer = SPSA(maxiter=300, last_avg=5) wavefunction = self.ry_wavefunction - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=self.qasm_simulator, - ) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + optimizer=optimizer, + max_evals_grouped=1, + quantum_instance=self.qasm_simulator, + ) - # TODO benchmark this later. - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + # TODO benchmark this later. + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, -1.86823, places=2) def test_qasm_eigenvector_normalized(self): """Test VQE with qasm_simulator returns normalized eigenvector.""" wavefunction = self.ry_wavefunction - vqe = VQE(ansatz=wavefunction, quantum_instance=self.qasm_simulator) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE(ansatz=wavefunction, quantum_instance=self.qasm_simulator) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) amplitudes = list(result.eigenstate.values()) self.assertAlmostEqual(np.linalg.norm(amplitudes), 1.0, places=4) @@ -234,14 +244,15 @@ def test_with_aer_statevector(self): seed_simulator=algorithm_globals.random_seed, seed_transpiler=algorithm_globals.random_seed, ) - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - max_evals_grouped=1, - quantum_instance=quantum_instance, - ) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + optimizer=optimizer, + max_evals_grouped=1, + quantum_instance=quantum_instance, + ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") @@ -257,14 +268,15 @@ def test_with_aer_qasm(self): seed_transpiler=algorithm_globals.random_seed, ) - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - expectation=PauliExpectation(), - quantum_instance=quantum_instance, - ) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + optimizer=optimizer, + expectation=PauliExpectation(), + quantum_instance=quantum_instance, + ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, -1.86305, places=2) @@ -282,14 +294,15 @@ def test_with_aer_qasm_snapshot_mode(self): seed_simulator=algorithm_globals.random_seed, seed_transpiler=algorithm_globals.random_seed, ) - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - expectation=AerPauliExpectation(), - quantum_instance=quantum_instance, - ) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + optimizer=optimizer, + expectation=AerPauliExpectation(), + quantum_instance=quantum_instance, + ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") @@ -308,15 +321,16 @@ def test_with_gradient(self, optimizer): seed_simulator=algorithm_globals.random_seed, seed_transpiler=algorithm_globals.random_seed, ) - vqe = VQE( - ansatz=self.ry_wavefunction, - optimizer=optimizer, - gradient=Gradient(), - expectation=AerPauliExpectation(), - quantum_instance=quantum_instance, - max_evals_grouped=1000, - ) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=self.ry_wavefunction, + optimizer=optimizer, + gradient=Gradient(), + expectation=AerPauliExpectation(), + quantum_instance=quantum_instance, + max_evals_grouped=1000, + ) + vqe.compute_minimum_eigenvalue(operator=self.h2_op) def test_with_two_qubit_reduction(self): """Test the VQE using TwoQubitReduction.""" @@ -341,7 +355,7 @@ def test_with_two_qubit_reduction(self): ) tapered_qubit_op = TwoQubitReduction(num_particles=2).convert(qubit_op) for simulator in [self.qasm_simulator, self.statevector_simulator]: - with self.subTest(f"Test for {simulator}."): + with self.subTest(f"Test for {simulator}."), self.assertWarns(DeprecationWarning): vqe = VQE( self.ry_wavefunction, SPSA(maxiter=300, last_avg=5), @@ -364,13 +378,14 @@ def store_intermediate_result(eval_count, parameters, mean, std): optimizer = COBYLA(maxiter=3) wavefunction = self.ry_wavefunction - vqe = VQE( - ansatz=wavefunction, - optimizer=optimizer, - callback=store_intermediate_result, - quantum_instance=self.qasm_simulator, - ) - vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + optimizer=optimizer, + callback=store_intermediate_result, + quantum_instance=self.qasm_simulator, + ) + vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertTrue(all(isinstance(count, int) for count in history["eval_count"])) self.assertTrue(all(isinstance(mean, float) for mean in history["mean"])) @@ -380,38 +395,45 @@ def store_intermediate_result(eval_count, parameters, mean, std): def test_reuse(self): """Test re-using a VQE algorithm instance.""" - vqe = VQE() + with self.assertWarns(DeprecationWarning): + vqe = VQE() with self.subTest(msg="assert running empty raises AlgorithmError"): - with self.assertRaises(AlgorithmError): + with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") vqe.ansatz = ansatz with self.subTest(msg="assert missing operator raises AlgorithmError"): - with self.assertRaises(AlgorithmError): + with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) vqe.expectation = MatrixExpectation() vqe.quantum_instance = self.statevector_simulator - with self.subTest(msg="assert VQE works once all info is available"): + with self.subTest(msg="assert VQE works once all info is available"), self.assertWarns( + DeprecationWarning + ): result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) operator = PrimitiveOp(np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]])) - with self.subTest(msg="assert minimum eigensolver interface works"): + with self.subTest(msg="assert minimum eigensolver interface works"), self.assertWarns( + DeprecationWarning + ): result = vqe.compute_minimum_eigenvalue(operator=operator) self.assertAlmostEqual(result.eigenvalue.real, -1.0, places=5) def test_vqe_optimizer(self): """Test running same VQE twice to re-use optimizer, then switch optimizer""" - vqe = VQE( - optimizer=SLSQP(), - quantum_instance=QuantumInstance(BasicAer.get_backend("statevector_simulator")), - ) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + optimizer=SLSQP(), + quantum_instance=QuantumInstance(BasicAer.get_backend("statevector_simulator")), + ) def run_check(): - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, -1.85727503, places=5) run_check() @@ -426,13 +448,14 @@ def run_check(): @data(MatrixExpectation(), None) def test_backend_change(self, user_expectation): """Test that VQE works when backend changes.""" - vqe = VQE( - ansatz=TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz"), - optimizer=SLSQP(maxiter=2), - expectation=user_expectation, - quantum_instance=BasicAer.get_backend("statevector_simulator"), - ) - result0 = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz"), + optimizer=SLSQP(maxiter=2), + expectation=user_expectation, + quantum_instance=BasicAer.get_backend("statevector_simulator"), + ) + result0 = vqe.compute_minimum_eigenvalue(operator=self.h2_op) if user_expectation is not None: with self.subTest("User expectation kept."): self.assertEqual(vqe.expectation, user_expectation) @@ -440,7 +463,8 @@ def test_backend_change(self, user_expectation): vqe.quantum_instance = BasicAer.get_backend("qasm_simulator") # works also if no expectation is set, since it will be determined automatically - result1 = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + result1 = vqe.compute_minimum_eigenvalue(operator=self.h2_op) if user_expectation is not None: with self.subTest("Change backend with user expectation, it is kept."): @@ -464,16 +488,18 @@ def wrapped_run(circuits, **kwargs): wrapped_backend.run = partial(wrapped_run, callcount=callcount) - fidelity = QNSPSA.get_fidelity(ansatz, backend=wrapped_backend) + with self.assertWarns(DeprecationWarning): + fidelity = QNSPSA.get_fidelity(ansatz, backend=wrapped_backend) qnspsa = QNSPSA(fidelity, maxiter=5) - vqe = VQE( - ansatz=ansatz, - optimizer=qnspsa, - max_evals_grouped=100, - quantum_instance=wrapped_backend, - ) - _ = vqe.compute_minimum_eigenvalue(Z ^ Z) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=ansatz, + optimizer=qnspsa, + max_evals_grouped=100, + quantum_instance=wrapped_backend, + ) + _ = vqe.compute_minimum_eigenvalue(Z ^ Z) # 1 calibration + 1 stddev estimation + 1 initial blocking # + 5 (1 loss + 1 fidelity + 1 blocking) + 1 return loss + 1 VQE eval @@ -483,49 +509,56 @@ def wrapped_run(circuits, **kwargs): def test_set_ansatz_to_none(self): """Tests that setting the ansatz to None results in the default behavior""" - vqe = VQE( - ansatz=self.ryrz_wavefunction, - optimizer=L_BFGS_B(), - quantum_instance=self.statevector_simulator, - ) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=self.ryrz_wavefunction, + optimizer=L_BFGS_B(), + quantum_instance=self.statevector_simulator, + ) vqe.ansatz = None self.assertIsInstance(vqe.ansatz, RealAmplitudes) def test_set_optimizer_to_none(self): """Tests that setting the optimizer to None results in the default behavior""" - vqe = VQE( - ansatz=self.ryrz_wavefunction, - optimizer=L_BFGS_B(), - quantum_instance=self.statevector_simulator, - ) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=self.ryrz_wavefunction, + optimizer=L_BFGS_B(), + quantum_instance=self.statevector_simulator, + ) vqe.optimizer = None self.assertIsInstance(vqe.optimizer, SLSQP) def test_optimizer_scipy_callable(self): """Test passing a SciPy optimizer directly as callable.""" - vqe = VQE( - optimizer=partial(scipy_minimize, method="L-BFGS-B", options={"maxiter": 2}), - quantum_instance=self.statevector_simulator, - ) - result = vqe.compute_minimum_eigenvalue(Z) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + optimizer=partial(scipy_minimize, method="L-BFGS-B", options={"maxiter": 2}), + quantum_instance=self.statevector_simulator, + ) + result = vqe.compute_minimum_eigenvalue(Z) self.assertEqual(result.cost_function_evals, 20) def test_optimizer_callable(self): """Test passing a optimizer directly as callable.""" ansatz = RealAmplitudes(1, reps=1) - vqe = VQE( - ansatz=ansatz, optimizer=_mock_optimizer, quantum_instance=self.statevector_simulator - ) - result = vqe.compute_minimum_eigenvalue(Z) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=ansatz, + optimizer=_mock_optimizer, + quantum_instance=self.statevector_simulator, + ) + result = vqe.compute_minimum_eigenvalue(Z) self.assertTrue(np.all(result.optimal_point == np.zeros(ansatz.num_parameters))) def test_aux_operators_list(self): """Test list-based aux_operators.""" wavefunction = self.ry_wavefunction - vqe = VQE(ansatz=wavefunction, quantum_instance=self.statevector_simulator) + with self.assertWarns(DeprecationWarning): + vqe = VQE(ansatz=wavefunction, quantum_instance=self.statevector_simulator) - # Start with an empty list - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) + # Start with an empty list + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertIsNone(result.aux_operator_eigenvalues) @@ -533,7 +566,8 @@ def test_aux_operators_list(self): aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values @@ -545,7 +579,8 @@ def test_aux_operators_list(self): # Go again with additional None and zero operators extra_ops = [*aux_ops, None, 0] - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operator_eigenvalues), 4) # expectation values @@ -562,10 +597,12 @@ def test_aux_operators_list(self): def test_aux_operators_dict(self): """Test dictionary compatibility of aux_operators""" wavefunction = self.ry_wavefunction - vqe = VQE(ansatz=wavefunction, quantum_instance=self.statevector_simulator) + with self.assertWarns(DeprecationWarning): + vqe = VQE(ansatz=wavefunction, quantum_instance=self.statevector_simulator) # Start with an empty dictionary - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertIsNone(result.aux_operator_eigenvalues) @@ -573,7 +610,8 @@ def test_aux_operators_dict(self): aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values @@ -585,7 +623,8 @@ def test_aux_operators_dict(self): # Go again with additional None and zero operators extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operator_eigenvalues), 3) # expectation values @@ -601,18 +640,20 @@ def test_aux_operators_dict(self): def test_aux_operator_std_dev_pauli(self): """Test non-zero standard deviations of aux operators with PauliExpectation.""" wavefunction = self.ry_wavefunction - vqe = VQE( - ansatz=wavefunction, - expectation=PauliExpectation(), - optimizer=COBYLA(maxiter=0), - quantum_instance=self.qasm_simulator, - ) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + expectation=PauliExpectation(), + optimizer=COBYLA(maxiter=0), + quantum_instance=self.qasm_simulator, + ) # Go again with two auxiliary operators aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) @@ -623,7 +664,8 @@ def test_aux_operator_std_dev_pauli(self): # Go again with additional None and zero operators aux_ops = [*aux_ops, None, 0] - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) self.assertEqual(len(result.aux_operator_eigenvalues), 4) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) @@ -642,23 +684,25 @@ def test_aux_operator_std_dev_pauli(self): def test_aux_operator_std_dev_aer_pauli(self): """Test non-zero standard deviations of aux operators with AerPauliExpectation.""" wavefunction = self.ry_wavefunction - vqe = VQE( - ansatz=wavefunction, - expectation=AerPauliExpectation(), - optimizer=COBYLA(maxiter=0), - quantum_instance=QuantumInstance( - backend=Aer.get_backend("qasm_simulator"), - shots=1, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ), - ) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, + expectation=AerPauliExpectation(), + optimizer=COBYLA(maxiter=0), + quantum_instance=QuantumInstance( + backend=Aer.get_backend("qasm_simulator"), + shots=1, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ), + ) # Go again with two auxiliary operators aux_op1 = PauliSumOp.from_list([("II", 2.0)]) aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) @@ -669,7 +713,8 @@ def test_aux_operator_std_dev_aer_pauli(self): # Go again with additional None and zero operators aux_ops = [*aux_ops, None, 0] - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) self.assertEqual(len(result.aux_operator_eigenvalues), 4) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) @@ -703,10 +748,11 @@ def test_2step_transpile(self): optimizer = SPSA(maxiter=5, learning_rate=0.01, perturbation=0.01) - vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) - _ = vqe.compute_minimum_eigenvalue(Z) + with self.assertWarns(DeprecationWarning): + vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) + _ = vqe.compute_minimum_eigenvalue(Z) - with self.assertLogs(logger, level="INFO") as cm: + with self.assertLogs(logger, level="INFO") as cm, self.assertWarns(DeprecationWarning): _ = vqe.compute_minimum_eigenvalue(Z) expected = [ @@ -736,8 +782,9 @@ def test_construct_eigenstate_from_optpoint(self): quantum_instance = QuantumInstance( backend=BasicAer.get_backend("statevector_simulator"), basis_gates=["u3", "cx"] ) - vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) - result = vqe.compute_minimum_eigenvalue(hamiltonian) + with self.assertWarns(DeprecationWarning): + vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) + result = vqe.compute_minimum_eigenvalue(hamiltonian) optimal_circuit = vqe.ansatz.bind_parameters(result.optimal_point) self.assertTrue(Statevector(result.eigenstate).equiv(optimal_circuit)) diff --git a/test/python/algorithms/time_evolvers/test_pvqd.py b/test/python/algorithms/time_evolvers/test_pvqd.py index 70cb12520e84..55c7c0f45ba7 100644 --- a/test/python/algorithms/time_evolvers/test_pvqd.py +++ b/test/python/algorithms/time_evolvers/test_pvqd.py @@ -18,7 +18,7 @@ from ddt import data, ddt, unpack from qiskit import QiskitError -from qiskit.algorithms.evolvers import EvolutionProblem +from qiskit.algorithms.time_evolvers import TimeEvolutionProblem from qiskit.algorithms.optimizers import L_BFGS_B, SPSA, GradientDescent, OptimizerResult from qiskit.algorithms.state_fidelities import ComputeUncompute from qiskit.algorithms.time_evolvers.pvqd import PVQD @@ -96,7 +96,9 @@ def test_pvqd(self, hamiltonian_type, gradient, num_timesteps): optimizer=optimizer, num_timesteps=num_timesteps, ) - problem = EvolutionProblem(hamiltonian, time, aux_operators=[hamiltonian, self.observable]) + problem = TimeEvolutionProblem( + hamiltonian, time, aux_operators=[hamiltonian, self.observable] + ) result = pvqd.evolve(problem) self.assertTrue(len(result.fidelities) == 3) @@ -176,7 +178,7 @@ def test_invalid_num_timestep(self): optimizer=L_BFGS_B(), num_timesteps=0, ) - problem = EvolutionProblem( + problem = TimeEvolutionProblem( self.hamiltonian, time=0.01, aux_operators=[self.hamiltonian, self.observable] ) @@ -199,7 +201,7 @@ def test_initial_guess_and_observables(self): num_timesteps=10, initial_guess=initial_guess, ) - problem = EvolutionProblem( + problem = TimeEvolutionProblem( self.hamiltonian, time=0.1, aux_operators=[self.hamiltonian, self.observable] ) @@ -211,7 +213,7 @@ def test_initial_guess_and_observables(self): def test_zero_parameters(self): """Test passing an ansatz with zero parameters raises an error.""" - problem = EvolutionProblem(self.hamiltonian, time=0.02) + problem = TimeEvolutionProblem(self.hamiltonian, time=0.02) sampler = Sampler() fidelity_primitive = ComputeUncompute(sampler) @@ -230,7 +232,7 @@ def test_initial_state_raises(self): initial_state = QuantumCircuit(2) initial_state.x(0) - problem = EvolutionProblem( + problem = TimeEvolutionProblem( self.hamiltonian, time=0.02, initial_state=initial_state, @@ -252,7 +254,7 @@ def test_initial_state_raises(self): def test_aux_ops_raises(self): """Test passing auxiliary operators with no estimator raises an error.""" - problem = EvolutionProblem( + problem = TimeEvolutionProblem( self.hamiltonian, time=0.02, aux_operators=[self.hamiltonian, self.observable] ) @@ -321,7 +323,7 @@ def test_gradient_supported(self): estimator=estimator, optimizer=optimizer, ) - problem = EvolutionProblem(self.hamiltonian, time=0.01) + problem = TimeEvolutionProblem(self.hamiltonian, time=0.01) for circuit, expected_support in tests: with self.subTest(circuit=circuit, expected_support=expected_support): pvqd.ansatz = circuit From 51fdb3c8653d9da7fd2b7150f1be89d5ba179db0 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 17 Apr 2023 11:56:12 +0100 Subject: [PATCH 031/172] Invoke `pip install` by module not executable (#9972) On some OSes and configurations (usually Windows), there can be problems when a binary attempts to one that is in use, especially itself. For this reason, it is more reliable to use `python -m pip install ...` than `pip install`. We have seen some CI failures on Windows due to `pip` failing to update itself because of the direct-executable `pip install` form. --- .azure/docs-linux.yml | 2 +- .azure/lint-linux.yml | 4 ++-- .azure/test-linux.yml | 14 +++++++------- .azure/test-macos.yml | 6 +++--- .azure/test-windows.yml | 6 +++--- .azure/tutorials-linux.yml | 2 +- .azure/wheels.yml | 4 ++-- .github/workflows/coverage.yml | 6 +++--- .github/workflows/randomized_tests.yml | 12 ++++++------ .github/workflows/slow.yml | 10 +++++----- 10 files changed, 33 insertions(+), 33 deletions(-) diff --git a/.azure/docs-linux.yml b/.azure/docs-linux.yml index 8c44558ffa7a..24c539b5e769 100644 --- a/.azure/docs-linux.yml +++ b/.azure/docs-linux.yml @@ -22,7 +22,7 @@ jobs: - bash: | set -e python -m pip install --upgrade pip setuptools wheel - pip install -U "tox<4.4.0" + python -m pip install -U "tox<4.4.0" sudo apt-get update sudo apt-get install -y graphviz displayName: 'Install dependencies' diff --git a/.azure/lint-linux.yml b/.azure/lint-linux.yml index e904c4806636..aac89bc005ce 100644 --- a/.azure/lint-linux.yml +++ b/.azure/lint-linux.yml @@ -21,8 +21,8 @@ jobs: python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job source test-job/bin/activate - pip install -U pip setuptools wheel - pip install -U \ + python -m pip install -U pip setuptools wheel + python -m pip install -U \ -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml index abbdc0cc7ed1..807246a85191 100644 --- a/.azure/test-linux.yml +++ b/.azure/test-linux.yml @@ -60,11 +60,11 @@ jobs: # Use stable Rust, rather than MSRV, to spot-check that stable builds properly. rustup override set stable source test-job/bin/activate - pip install -U pip setuptools wheel + python -m pip install -U pip setuptools wheel # Install setuptools-rust for building sdist - pip install -U -c constraints.txt setuptools-rust + python -m pip install -U -c constraints.txt setuptools-rust python setup.py sdist - pip install -U \ + python -m pip install -U \ -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ @@ -75,7 +75,7 @@ jobs: - bash: | set -e source test-job/bin/activate - pip install -U \ + python -m pip install -U \ -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ @@ -87,7 +87,7 @@ jobs: - bash: | set -e source test-job/bin/activate - pip install -U \ + python -m pip install -U \ -c constraints.txt \ "cplex ; python_version < '3.11'" \ "qiskit-aer" \ @@ -119,7 +119,7 @@ jobs: set -e source test-job/bin/activate cp tools/subunit_to_junit.py /tmp/terra-tests/. - pip install -U junitxml + python -m pip install -U junitxml pushd /tmp/terra-tests mkdir -p junit stestr last --subunit | ./subunit_to_junit.py -o junit/test-results.xml @@ -174,7 +174,7 @@ jobs: - bash: | set -e virtualenv image_tests - image_tests/bin/pip install -U \ + image_tests/bin/python -m pip install -U \ -c constraints.txt \ -r requirements.txt \ -e ".[visualization]" diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml index 475d009b8ed4..6799d70fdc4e 100644 --- a/.azure/test-macos.yml +++ b/.azure/test-macos.yml @@ -34,8 +34,8 @@ jobs: python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job source test-job/bin/activate - pip install -U pip setuptools wheel - pip install -U \ + python -m pip install -U pip setuptools wheel + python -m pip install -U \ -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ @@ -59,7 +59,7 @@ jobs: - bash: | set -e source test-job/bin/activate - pip install -U junitxml + python -m pip install -U junitxml mkdir -p junit stestr last --subunit | tools/subunit_to_junit.py -o junit/test-results.xml pushd .stestr diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml index fda470e9a92c..e4d4c5b4065b 100644 --- a/.azure/test-windows.yml +++ b/.azure/test-windows.yml @@ -33,8 +33,8 @@ jobs: python -m pip install --upgrade pip setuptools wheel virtualenv virtualenv test-job source test-job/Scripts/activate - pip install -U pip setuptools wheel - pip install -U \ + python -m pip install -U pip setuptools wheel + python -m pip install -U \ -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ @@ -63,7 +63,7 @@ jobs: set -e chcp.com 65001 source test-job/Scripts/activate - pip install -U junitxml + python -m pip install -U junitxml mkdir -p junit stestr last --subunit | python tools/subunit_to_junit.py -o junit/test-results.xml pushd .stestr diff --git a/.azure/tutorials-linux.yml b/.azure/tutorials-linux.yml index 2aa2a851dcd9..72beadc37b7d 100644 --- a/.azure/tutorials-linux.yml +++ b/.azure/tutorials-linux.yml @@ -22,7 +22,7 @@ jobs: set -e git clone https://github.com/Qiskit/qiskit-tutorials --depth=1 python -m pip install --upgrade pip setuptools wheel - pip install -U \ + python -m pip install -U \ -c constraints.txt \ -r requirements.txt \ -r requirements-dev.txt \ diff --git a/.azure/wheels.yml b/.azure/wheels.yml index 63604a40382c..5b106747fd2d 100644 --- a/.azure/wheels.yml +++ b/.azure/wheels.yml @@ -25,8 +25,8 @@ jobs: - bash: | set -e python -m pip install --upgrade pip - pip install cibuildwheel==2.11.2 - pip install -U twine + python -m pip install cibuildwheel==2.11.2 + python -m pip install -U twine cibuildwheel --output-dir wheelhouse . - task: PublishBuildArtifacts@1 diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7bef242ba019..40e4d9332773 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -37,10 +37,10 @@ jobs: # Modern pip (23.1+) can error out if it doesn't have `wheel` and we ask for one # of these legacy packages. - name: Ensure basic build requirements - run: pip install --upgrade pip setuptools wheel + run: python -m pip install --upgrade pip setuptools wheel - name: Build and install qiskit-terra - run: pip install -e . + run: python -m pip install -e . env: CARGO_INCREMENTAL: 0 RUSTFLAGS: "-Cinstrument-coverage" @@ -50,7 +50,7 @@ jobs: - name: Generate unittest coverage report run: | set -e - pip install -r requirements-dev.txt qiskit-aer + python -m pip install -r requirements-dev.txt qiskit-aer stestr run # We set the --source-dir to '.' because we want all paths to appear relative to the repo # root (we need to combine them with the Python ones), but we only care about `grcov` diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index 74fbd8ed39ba..03af53519ff6 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -15,12 +15,12 @@ jobs: python-version: '3.8' - name: Install dependencies run: | - pip install -U pip setuptools wheel - pip install -U -r requirements.txt -c constraints.txt - pip install -U -r requirements-dev.txt coveralls -c constraints.txt - pip install -c constraints.txt -e . - pip install "qiskit-ibmq-provider" -c constraints.txt - pip install "qiskit-aer" + python -m pip install -U pip setuptools wheel + python -m pip install -U -r requirements.txt -c constraints.txt + python -m pip install -U -r requirements-dev.txt coveralls -c constraints.txt + python -m pip install -c constraints.txt -e . + python -m pip install "qiskit-ibmq-provider" -c constraints.txt + python -m pip install "qiskit-aer" env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - name: Run randomized tests diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml index d41398915839..319318e62028 100644 --- a/.github/workflows/slow.yml +++ b/.github/workflows/slow.yml @@ -15,11 +15,11 @@ jobs: python-version: '3.10' - name: Install dependencies run: | - pip install -U pip setuptools wheel - pip install -U -r requirements.txt -c constraints.txt - pip install -U -r requirements-dev.txt -c constraints.txt - pip install -c constraints.txt -e . - pip install "qiskit-aer" "z3-solver" "cplex" -c constraints.txt + python -m pip install -U pip setuptools wheel + 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 . + python -m pip install "qiskit-aer" "z3-solver" "cplex" -c constraints.txt env: SETUPTOOLS_ENABLE_FEATURES: "legacy-editable" - name: Run all tests including slow From 5dbac1ed8a48d35d3eb1588d1bcdb62c5431bea9 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 17 Apr 2023 06:58:27 -0400 Subject: [PATCH 032/172] Add full path transpile() support for disjoint backends (#9840) * Add full path transpile() support for disjoint backends This commit finalizes the last piece of the full path transpile() support at all optimization levels. The primary piece to accomplish this was adding DenseLayout support for targeting a disjoint CouplingMap. With this piece added then all the default transpiler paths in the preset pass managers are able to support running with a disjoint CouplingMap. The biggest exception is the TrivialLayout pass which can result in an invalid layout being selected (where 2q connectivity crosses connected components). To handle this edge case a check is added to each routing stage to ensure the selected layout is valid for the coupling map and it is routable. This enables all the default paths at all 4 optimization levels for transpile() to be usable with disjoint connectivity. For optimization_level=0/TrivialLayout if the trivial layout is invalid the routing pass will raise an error saying as much. It's worth pointing out that NoiseAdaptiveLayout still doesn't support disjoint connectivity. However, since it's performance is poor and it's not used by default in any preset passmanager we can add this at a later date, but all other combinations of layout and routing methods should work (given the caveats with TrivialLayout) above. * Fix test determinism * Add future facing test for shared classical bits between components * Enable shared control flow test Now that #9802 supports shared classical bits correctly this commit re-enables the tests disabled in the previous commit. * Remove unused condition for faulty qubits backnedv1 path * Remove opt level 1 variant of test_chained_data_dependency * Use enumerate() in check_layout_isolated_to_component() Co-authored-by: Kevin Hartman * Expand level 0 test coverage * Update test/python/compiler/test_transpiler.py Co-authored-by: Kevin Hartman * s/check_layout_isolated_to_component/require_layout_isolated_to_component/g --------- Co-authored-by: Kevin Hartman --- .../transpiler/passes/layout/dense_layout.py | 85 ++--- .../passes/layout/disjoint_utils.py | 15 + .../passes/layout/trivial_layout.py | 5 - .../transpiler/passes/routing/basic_swap.py | 2 + .../transpiler/passes/routing/bip_mapping.py | 2 + .../passes/routing/lookahead_swap.py | 2 + .../transpiler/passes/routing/sabre_swap.py | 2 + .../passes/routing/stochastic_swap.py | 3 + test/python/compiler/test_transpiler.py | 291 +++++++++++++++++- test/python/transpiler/test_dense_layout.py | 14 +- 10 files changed, 363 insertions(+), 58 deletions(-) diff --git a/qiskit/transpiler/passes/layout/dense_layout.py b/qiskit/transpiler/passes/layout/dense_layout.py index cd87fad134ca..be29c28e519d 100644 --- a/qiskit/transpiler/passes/layout/dense_layout.py +++ b/qiskit/transpiler/passes/layout/dense_layout.py @@ -19,6 +19,7 @@ from qiskit.transpiler.layout import Layout from qiskit.transpiler.basepasses import AnalysisPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.passes.layout import disjoint_utils from qiskit._accelerate.dense_layout import best_subset @@ -47,21 +48,9 @@ def __init__(self, coupling_map=None, backend_prop=None, target=None): self.coupling_map = coupling_map self.backend_prop = backend_prop self.target = target - num_qubits = 0 self.adjacency_matrix = None if target is not None: - num_qubits = target.num_qubits self.coupling_map = target.build_coupling_map() - if self.coupling_map is not None: - self.adjacency_matrix = rustworkx.adjacency_matrix(self.coupling_map.graph) - self.error_mat, self._use_error = _build_error_matrix(num_qubits, target=target) - else: - if self.coupling_map: - num_qubits = self.coupling_map.size() - self.adjacency_matrix = rustworkx.adjacency_matrix(self.coupling_map.graph) - self.error_mat, self._use_error = _build_error_matrix( - num_qubits, backend_prop=self.backend_prop, coupling_map=self.coupling_map - ) def run(self, dag): """Run the DenseLayout pass on `dag`. @@ -79,13 +68,20 @@ def run(self, dag): raise TranspilerError( "A coupling_map or target with constrained qargs is necessary to run the pass." ) - if dag.num_qubits() > len(self.coupling_map.largest_connected_component()): - raise TranspilerError( - "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " - "map for a circuit this wide." - ) + layout_components = disjoint_utils.run_pass_over_connected_components( + dag, self.coupling_map, self._inner_run + ) + layout_mapping = {} + for component in layout_components: + layout_mapping.update(component) + layout = Layout(layout_mapping) + for qreg in dag.qregs.values(): + layout.add_register(qreg) + self.property_set["layout"] = layout + + def _inner_run(self, dag, coupling_map): num_dag_qubits = len(dag.qubits) - if num_dag_qubits > self.coupling_map.size(): + if num_dag_qubits > coupling_map.size(): raise TranspilerError("Number of qubits greater than device.") num_cx = 0 num_meas = 0 @@ -101,15 +97,13 @@ def run(self, dag): if "measure" in ops.keys(): num_meas = ops["measure"] - best_sub = self._best_subset(num_dag_qubits, num_meas, num_cx) - layout = Layout() - for i, qubit in enumerate(dag.qubits): - layout.add(qubit, int(best_sub[i])) - for qreg in dag.qregs.values(): - layout.add_register(qreg) - self.property_set["layout"] = layout + best_sub = self._best_subset(num_dag_qubits, num_meas, num_cx, coupling_map) + layout_mapping = { + qubit: coupling_map.graph[int(best_sub[i])] for i, qubit in enumerate(dag.qubits) + } + return layout_mapping - def _best_subset(self, num_qubits, num_meas, num_cx): + def _best_subset(self, num_qubits, num_meas, num_cx, coupling_map): """Computes the qubit mapping with the best connectivity. Args: @@ -125,14 +119,25 @@ def _best_subset(self, num_qubits, num_meas, num_cx): if num_qubits == 0: return [] + adjacency_matrix = rustworkx.adjacency_matrix(coupling_map.graph) + reverse_index_map = {v: k for k, v in enumerate(coupling_map.graph.nodes())} + + error_mat, use_error = _build_error_matrix( + coupling_map.size(), + reverse_index_map, + backend_prop=self.backend_prop, + coupling_map=self.coupling_map, + target=self.target, + ) + rows, cols, best_map = best_subset( num_qubits, - self.adjacency_matrix, + adjacency_matrix, num_meas, num_cx, - self._use_error, - self.coupling_map.is_symmetric, - self.error_mat, + use_error, + coupling_map.is_symmetric, + error_mat, ) data = [1] * len(rows) sp_sub_graph = coo_matrix((data, (rows, cols)), shape=(num_qubits, num_qubits)).tocsr() @@ -141,7 +146,7 @@ def _best_subset(self, num_qubits, num_meas, num_cx): return best_map -def _build_error_matrix(num_qubits, target=None, coupling_map=None, backend_prop=None): +def _build_error_matrix(num_qubits, qubit_map, target=None, coupling_map=None, backend_prop=None): error_mat = np.zeros((num_qubits, num_qubits)) use_error = False if target is not None and target.qargs is not None: @@ -161,13 +166,15 @@ def _build_error_matrix(num_qubits, target=None, coupling_map=None, backend_prop # the possible worst case. error = max(error, props.error) max_error = error + if any(qubit not in qubit_map for qubit in qargs): + continue # TODO: Factor in T1 and T2 to error matrix after #7736 if len(qargs) == 1: - qubit = qargs[0] + qubit = qubit_map[qargs[0]] error_mat[qubit][qubit] = max_error use_error = True elif len(qargs) == 2: - error_mat[qargs[0]][qargs[1]] = max_error + error_mat[qubit_map[qargs[0]]][qubit_map[qargs[1]]] = max_error use_error = True elif backend_prop and coupling_map: error_dict = { @@ -178,14 +185,16 @@ def _build_error_matrix(num_qubits, target=None, coupling_map=None, backend_prop for edge in coupling_map.get_edges(): gate_error = error_dict.get(edge) if gate_error is not None: - error_mat[edge[0]][edge[1]] = gate_error + if edge[0] not in qubit_map or edge[1] not in qubit_map: + continue + error_mat[qubit_map[edge[0]]][qubit_map[edge[1]]] = gate_error use_error = True for index, qubit_data in enumerate(backend_prop.qubits): - # Handle faulty qubits edge case - if index >= num_qubits: - break + if index not in qubit_map: + continue for item in qubit_data: if item.name == "readout_error": - error_mat[index][index] = item.value + mapped_index = qubit_map[index] + error_mat[mapped_index][mapped_index] = item.value use_error = True return error_mat, use_error diff --git a/qiskit/transpiler/passes/layout/disjoint_utils.py b/qiskit/transpiler/passes/layout/disjoint_utils.py index f4f55a352e1e..e7d7c9756193 100644 --- a/qiskit/transpiler/passes/layout/disjoint_utils.py +++ b/qiskit/transpiler/passes/layout/disjoint_utils.py @@ -127,6 +127,21 @@ def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True): node.op.label = None +def require_layout_isolated_to_component(dag: DAGCircuit, coupling_map: CouplingMap) -> bool: + """Check that the layout of the dag does not require connectivity across connected components + in the CouplingMap""" + qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} + component_sets = [set(x.graph.nodes()) for x in coupling_map.connected_components()] + for inst in dag.two_qubit_ops(): + component_index = None + for i, component_set in enumerate(component_sets): + if qubit_indices[inst.qargs[0]] in component_set: + component_index = i + break + if qubit_indices[inst.qargs[1]] not in component_sets[component_index]: + raise TranspilerError("Chosen layout is not valid for the target disjoint connectivity") + + def separate_dag(dag: DAGCircuit) -> List[DAGCircuit]: """Separate a dag circuit into it's connected components.""" # Split barriers into single qubit barriers before splitting connected components diff --git a/qiskit/transpiler/passes/layout/trivial_layout.py b/qiskit/transpiler/passes/layout/trivial_layout.py index 5f1457d094bc..74fdc5b4ec30 100644 --- a/qiskit/transpiler/passes/layout/trivial_layout.py +++ b/qiskit/transpiler/passes/layout/trivial_layout.py @@ -61,11 +61,6 @@ def run(self, dag): raise TranspilerError("Number of qubits greater than device.") elif dag.num_qubits() > self.coupling_map.size(): raise TranspilerError("Number of qubits greater than device.") - if not self.coupling_map.is_connected(): - raise TranspilerError( - "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " - "map." - ) self.property_set["layout"] = Layout.generate_trivial_layout( *(dag.qubits + list(dag.qregs.values())) ) diff --git a/qiskit/transpiler/passes/routing/basic_swap.py b/qiskit/transpiler/passes/routing/basic_swap.py index 91a4d4ded611..f286015b7b3d 100644 --- a/qiskit/transpiler/passes/routing/basic_swap.py +++ b/qiskit/transpiler/passes/routing/basic_swap.py @@ -18,6 +18,7 @@ from qiskit.transpiler.layout import Layout from qiskit.circuit.library.standard_gates import SwapGate from qiskit.transpiler.target import Target +from qiskit.transpiler.passes.layout import disjoint_utils class BasicSwap(TransformationPass): @@ -71,6 +72,7 @@ def run(self, dag): if len(dag.qubits) > len(self.coupling_map.physical_qubits): raise TranspilerError("The layout does not match the amount of qubits in the DAG") + disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map) canonical_register = dag.qregs["q"] trivial_layout = Layout.generate_trivial_layout(canonical_register) diff --git a/qiskit/transpiler/passes/routing/bip_mapping.py b/qiskit/transpiler/passes/routing/bip_mapping.py index 58b040a9821c..e2e87f1761ca 100644 --- a/qiskit/transpiler/passes/routing/bip_mapping.py +++ b/qiskit/transpiler/passes/routing/bip_mapping.py @@ -24,6 +24,7 @@ from qiskit.transpiler.passes.routing.algorithms.bip_model import BIPMappingModel from qiskit.transpiler.target import target_to_backend_properties, Target from qiskit.utils.deprecation import deprecate_func +from qiskit.transpiler.passes.layout import disjoint_utils logger = logging.getLogger(__name__) @@ -166,6 +167,7 @@ def run(self, dag): "BIPMapping requires the number of virtual and physical qubits to be the same. " "Supply 'qubit_subset' to specify physical qubits to use." ) + disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map) original_dag = dag diff --git a/qiskit/transpiler/passes/routing/lookahead_swap.py b/qiskit/transpiler/passes/routing/lookahead_swap.py index bb0476ee4f41..92c0fc5adf35 100644 --- a/qiskit/transpiler/passes/routing/lookahead_swap.py +++ b/qiskit/transpiler/passes/routing/lookahead_swap.py @@ -23,6 +23,7 @@ from qiskit.transpiler.layout import Layout from qiskit.dagcircuit import DAGOpNode from qiskit.transpiler.target import Target +from qiskit.transpiler.passes.layout import disjoint_utils logger = logging.getLogger(__name__) @@ -129,6 +130,7 @@ def run(self, dag): f"The number of DAG qubits ({len(dag.qubits)}) is greater than the number of " f"available device qubits ({number_of_available_qubits})." ) + disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map) register = dag.qregs["q"] current_state = _SystemState( diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 3fef57f16f66..54693b68984d 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -22,6 +22,7 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout from qiskit.transpiler.target import Target +from qiskit.transpiler.passes.layout import disjoint_utils from qiskit.dagcircuit import DAGOpNode from qiskit.tools.parallel import CPU_COUNT @@ -212,6 +213,7 @@ def run(self, dag): heuristic = Heuristic.Decay else: raise TranspilerError("Heuristic %s not recognized." % self.heuristic) + disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map) self.dist_matrix = self.coupling_map.distance_matrix diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index df549ef848bc..2e65c0e90168 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -28,6 +28,7 @@ from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp, Instruction from qiskit._accelerate import stochastic_swap as stochastic_swap_rs from qiskit._accelerate import nlayout +from qiskit.transpiler.passes.layout import disjoint_utils from .utils import get_swap_map_dag @@ -104,6 +105,8 @@ def run(self, dag): if len(dag.qubits) > len(self.coupling_map.physical_qubits): raise TranspilerError("The layout does not match the amount of qubits in the DAG") + disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map) + self.rng = np.random.default_rng(self.seed) canonical_register = dag.qregs["q"] diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 0dcc5ba9ba81..b3b9f08411aa 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -29,7 +29,7 @@ from qiskit.exceptions import QiskitError from qiskit import BasicAer from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, pulse, qpy, qasm3 -from qiskit.circuit import Parameter, Gate, Qubit, Clbit +from qiskit.circuit import Parameter, Gate, Qubit, Clbit, Reset from qiskit.compiler import transpile from qiskit.dagcircuit import DAGOutNode, DAGOpNode from qiskit.converters import circuit_to_dag @@ -2101,8 +2101,7 @@ def run(self, circuit, **kwargs): self.backend = FakeMultiChip() - # Add level 0 and 1 when TrivialLayout supports disjoint coupling maps - @data(2, 3) + @data(0, 1, 2, 3) def test_basic_connected_circuit(self, opt_level): """Test basic connected circuit on disjoint backend""" qc = QuantumCircuit(5) @@ -2120,8 +2119,7 @@ def test_basic_connected_circuit(self, opt_level): continue self.assertIn(qubits, self.backend.target[op_name]) - # Add level 0 and 1 when TrivialLayout supports disjoint coupling maps - @data(2, 3) + @data(0, 1, 2, 3) def test_triple_circuit(self, opt_level): """Test a split circuit with one circuit component per chip.""" qc = QuantumCircuit(30) @@ -2156,6 +2154,12 @@ def test_triple_circuit(self, opt_level): qc.cy(20, 28) qc.cy(20, 29) qc.measure_all() + + if opt_level == 0: + with self.assertRaises(TranspilerError): + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=42) + return + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=42) for inst in tqc.data: qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) @@ -2164,10 +2168,89 @@ def test_triple_circuit(self, opt_level): continue self.assertIn(qubits, self.backend.target[op_name]) - # Add level 0 and 1 when TrivialLayout supports disjoint coupling maps - # Tagged as slow until #9834 is fixed + def test_disjoint_control_flow(self): + """Test control flow circuit on disjoint coupling map.""" + qc = QuantumCircuit(6, 1) + qc.h(0) + qc.ecr(0, 1) + qc.cx(0, 2) + qc.measure(0, 0) + with qc.if_test((qc.clbits[0], True)): + qc.reset(0) + qc.cz(1, 0) + qc.h(3) + qc.cz(3, 4) + qc.cz(3, 5) + target = self.backend.target + target.add_instruction(Reset(), {(i,): None for i in range(target.num_qubits)}) + target.add_instruction(IfElseOp, name="if_else") + tqc = transpile(qc, target=target) + edges = set(target.build_coupling_map().graph.edge_list()) + + def _visit_block(circuit, qubit_mapping=None): + for instruction in circuit: + if instruction.operation.name == "barrier": + continue + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue(target.instruction_supported(instruction.operation.name, qargs)) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + elif len(qargs) == 2: + self.assertIn(qargs, edges) + self.assertIn(instruction.operation.name, target) + + _visit_block( + tqc, + qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, + ) + + def test_disjoint_control_flow_shared_classical(self): + """Test circuit with classical data dependency between connected components.""" + creg = ClassicalRegister(19) + qc = QuantumCircuit(25) + qc.add_register(creg) + qc.h(0) + for i in range(18): + qc.cx(0, i + 1) + for i in range(18): + qc.measure(i, creg[i]) + with qc.if_test((creg, 0)): + qc.h(20) + qc.ecr(20, 21) + qc.ecr(20, 22) + qc.ecr(20, 23) + qc.ecr(20, 24) + target = self.backend.target + target.add_instruction(Reset(), {(i,): None for i in range(target.num_qubits)}) + target.add_instruction(IfElseOp, name="if_else") + tqc = transpile(qc, target=target) + + def _visit_block(circuit, qubit_mapping=None): + for instruction in circuit: + if instruction.operation.name == "barrier": + continue + qargs = tuple(qubit_mapping[x] for x in instruction.qubits) + self.assertTrue(target.instruction_supported(instruction.operation.name, qargs)) + if isinstance(instruction.operation, ControlFlowOp): + for block in instruction.operation.blocks: + new_mapping = { + inner: qubit_mapping[outer] + for outer, inner in zip(instruction.qubits, block.qubits) + } + _visit_block(block, new_mapping) + + _visit_block( + tqc, + qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, + ) + @slow_test - @data(2, 3) + @data(1, 2, 3) def test_six_component_circuit(self, opt_level): """Test input circuit with more than 1 component per backend component.""" qc = QuantumCircuit(42) @@ -2222,7 +2305,7 @@ def test_six_component_circuit(self, opt_level): continue self.assertIn(qubits, self.backend.target[op_name]) - @data(2, 3) + @data(0, 1, 2, 3) def test_shared_classical_between_components_condition(self, opt_level): """Test a condition sharing classical bits between components.""" creg = ClassicalRegister(19) @@ -2258,7 +2341,7 @@ def _visit_block(circuit, qubit_mapping=None): qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, ) - @data(2, 3) + @data(0, 1, 2, 3) def test_shared_classical_between_components_condition_large_to_small(self, opt_level): """Test a condition sharing classical bits between components.""" creg = ClassicalRegister(2) @@ -2271,7 +2354,7 @@ def test_shared_classical_between_components_condition_large_to_small(self, opt_ qc.h(0).c_if(creg, 0) for i in range(18): qc.ecr(0, i + 1).c_if(creg, 0) - tqc = transpile(qc, self.backend, optimization_level=opt_level) + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=123456789) def _visit_block(circuit, qubit_mapping=None): for instruction in circuit: @@ -2320,7 +2403,7 @@ def _visit_block(circuit, qubit_mapping=None): op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] - @data(2, 3) + @data(1, 2, 3) def test_shared_classical_between_components_condition_large_to_small_reverse_index( self, opt_level ): @@ -2384,6 +2467,10 @@ def _visit_block(circuit, qubit_mapping=None): op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] + # Level 1 skipped in this test for now because routing inserts more swaps + # and tricking the intermediate layout permutation to validate ordering + # will be different compared to higher optimization levels. We have similar + # coverage provided by above tests for level 1. @data(2, 3) def test_chained_data_dependency(self, opt_level): """Test 3 component circuit with shared clbits between each component.""" @@ -2466,3 +2553,183 @@ def _visit_block(circuit, qubit_mapping=None): op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], third_component) + + @data("sabre", "stochastic", "basic", "lookahead") + def test_basic_connected_circuit_dense_layout(self, routing_method): + """Test basic connected circuit on disjoint backend""" + qc = QuantumCircuit(5) + qc.h(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.measure_all() + tqc = transpile( + qc, + self.backend, + layout_method="dense", + routing_method=routing_method, + seed_transpiler=42, + ) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + + # Lookahead swap skipped for performance + @data("sabre", "stochastic", "basic") + def test_triple_circuit_dense_layout(self, routing_method): + """Test a split circuit with one circuit component per chip.""" + qc = QuantumCircuit(30) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.measure_all() + tqc = transpile( + qc, + self.backend, + layout_method="dense", + routing_method=routing_method, + seed_transpiler=42, + ) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + + @data("sabre", "stochastic", "basic", "lookahead") + def test_triple_circuit_invalid_layout(self, routing_method): + """Test a split circuit with one circuit component per chip.""" + qc = QuantumCircuit(30) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.measure_all() + with self.assertRaises(TranspilerError): + transpile( + qc, + self.backend, + layout_method="trivial", + routing_method=routing_method, + seed_transpiler=42, + ) + + # Lookahead swap skipped for performance reasons + @data("sabre", "stochastic", "basic") + def test_six_component_circuit_dense_layout(self, routing_method): + """Test input circuit with more than 1 component per backend component.""" + qc = QuantumCircuit(42) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.h(30) + qc.cx(30, 31) + qc.cx(30, 32) + qc.cx(30, 33) + qc.h(34) + qc.cx(34, 35) + qc.cx(34, 36) + qc.cx(34, 37) + qc.h(38) + qc.cx(38, 39) + qc.cx(39, 40) + qc.cx(39, 41) + qc.measure_all() + tqc = transpile( + qc, + self.backend, + layout_method="dense", + routing_method=routing_method, + seed_transpiler=42, + ) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) diff --git a/test/python/transpiler/test_dense_layout.py b/test/python/transpiler/test_dense_layout.py index 10e5bbed9252..01642b4036d1 100644 --- a/test/python/transpiler/test_dense_layout.py +++ b/test/python/transpiler/test_dense_layout.py @@ -24,6 +24,7 @@ from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase from qiskit.providers.fake_provider import FakeTokyo +from qiskit.transpiler.passes.layout.dense_layout import _build_error_matrix class TestDenseLayout(QiskitTestCase): @@ -150,12 +151,16 @@ def test_target_too_small_for_circuit(self): def test_19q_target_with_noise_error_matrix(self): """Test the error matrix construction works for a just cx target.""" - pass_ = DenseLayout(target=self.target_19) expected_error_mat = np.zeros((19, 19)) for qargs, props in self.target_19["cx"].items(): error = props.error expected_error_mat[qargs[0]][qargs[1]] = error - np.testing.assert_array_equal(expected_error_mat, pass_.error_mat) + error_mat = _build_error_matrix( + self.target_19.num_qubits, + {i: i for i in range(self.target_19.num_qubits)}, + target=self.target_19, + )[0] + np.testing.assert_array_equal(expected_error_mat, error_mat) def test_multiple_gate_error_matrix(self): """Test error matrix ona small target with multiple gets on each qubit generates""" @@ -192,7 +197,10 @@ def test_multiple_gate_error_matrix(self): [2e-2, 1e-3, 1e-2], ] ) - np.testing.assert_array_equal(expected_error_matrix, DenseLayout(target=target).error_mat) + error_mat = _build_error_matrix( + target.num_qubits, {i: i for i in range(target.num_qubits)}, target=target + )[0] + np.testing.assert_array_equal(expected_error_matrix, error_mat) def test_5q_circuit_20q_with_if_else(self): """Test layout works finds a dense 5q subgraph in a 19q heavy hex target.""" From 580590f66881adf705af22e792e92d81b5afbd2f Mon Sep 17 00:00:00 2001 From: "Payal D. Solanki" Date: Mon, 17 Apr 2023 16:28:38 +0530 Subject: [PATCH 033/172] Add partial transpose function in quantum_info (#9566) * partial_transpose included * included partial_transpose * included partial_transpose * included partial_transpose * included partial_transpose * included partial_transpose * included partial_transpose * included partial_transpose * included partial_transpose * included partial_transpose * included partial_transpose * included partial_transpose * included test for partial transpose * included test for partial transpose * included test for partial transpose * added docstring * included DensityMatrix(rho1) * changed rho1 * addition of release note * fix * fix * fir partial_transpose * Update utils.py * refactor and add tests --------- Co-authored-by: Ikko Hamamura --- qiskit/quantum_info/states/densitymatrix.py | 19 ++++++++++ ...ng-partial-transpose-040a6ff00228841b.yaml | 5 +++ .../quantum_info/states/test_densitymatrix.py | 35 ++++++++++++++----- 3 files changed, 50 insertions(+), 9 deletions(-) create mode 100644 releasenotes/notes/adding-partial-transpose-040a6ff00228841b.yaml diff --git a/qiskit/quantum_info/states/densitymatrix.py b/qiskit/quantum_info/states/densitymatrix.py index ac7b672b77ef..124a382f01d1 100644 --- a/qiskit/quantum_info/states/densitymatrix.py +++ b/qiskit/quantum_info/states/densitymatrix.py @@ -814,3 +814,22 @@ def to_statevector(self, atol=None, rtol=None): psi = evecs[:, np.argmax(evals)] # eigenvectors returned in columns. return Statevector(psi) + + def partial_transpose(self, qargs): + """Return partially transposed density matrix. + + Args: + qargs (list): The subsystems to be transposed. + + Returns: + DensityMatrix: The partially transposed density matrix. + """ + arr = self._data.reshape(self._op_shape.tensor_shape) + qargs = len(self._op_shape.dims_l()) - 1 - np.array(qargs) + n = len(self.dims()) + lst = list(range(2 * n)) + for i in qargs: + lst[i], lst[i + n] = lst[i + n], lst[i] + rho = np.transpose(arr, lst) + rho = np.reshape(rho, self._op_shape.shape) + return DensityMatrix(rho) diff --git a/releasenotes/notes/adding-partial-transpose-040a6ff00228841b.yaml b/releasenotes/notes/adding-partial-transpose-040a6ff00228841b.yaml new file mode 100644 index 000000000000..6744ea4bcfcc --- /dev/null +++ b/releasenotes/notes/adding-partial-transpose-040a6ff00228841b.yaml @@ -0,0 +1,5 @@ + +features: + - | + The partial transpose operation has been integrated into the quantum_info module, allowing for partial transposition of matrices. + This operation is key in detecting entanglement between bipartite quantum system. diff --git a/test/python/quantum_info/states/test_densitymatrix.py b/test/python/quantum_info/states/test_densitymatrix.py index 17f2b22f174e..8886e35cc288 100644 --- a/test/python/quantum_info/states/test_densitymatrix.py +++ b/test/python/quantum_info/states/test_densitymatrix.py @@ -12,21 +12,20 @@ """Tests for DensityMatrix quantum state class.""" -import unittest import logging -from ddt import ddt, data +import unittest + import numpy as np +from ddt import data, ddt from numpy.testing import assert_allclose -from qiskit.test import QiskitTestCase -from qiskit import QiskitError -from qiskit import QuantumRegister, QuantumCircuit -from qiskit.circuit.library import HGate, QFT - -from qiskit.quantum_info.random import random_unitary, random_density_matrix, random_pauli -from qiskit.quantum_info.states import DensityMatrix, Statevector +from qiskit import QiskitError, QuantumCircuit, QuantumRegister +from qiskit.circuit.library import QFT, HGate from qiskit.quantum_info.operators.operator import Operator from qiskit.quantum_info.operators.symplectic import Pauli, SparsePauliOp +from qiskit.quantum_info.random import random_density_matrix, random_pauli, random_unitary +from qiskit.quantum_info.states import DensityMatrix, Statevector +from qiskit.test import QiskitTestCase logger = logging.getLogger(__name__) @@ -1202,6 +1201,24 @@ def test_drawings(self): with self.subTest(msg=f"draw('{drawtype}')"): dm.draw(drawtype) + def test_density_matrix_partial_transpose(self): + """Test partial_transpose function on density matrices""" + with self.subTest(msg="separable"): + rho = DensityMatrix.from_label("10+") + rho1 = np.zeros((8, 8), complex) + rho1[4, 4] = 0.5 + rho1[4, 5] = 0.5 + rho1[5, 4] = 0.5 + rho1[5, 5] = 0.5 + self.assertEqual(rho.partial_transpose([0, 1]), DensityMatrix(rho1)) + self.assertEqual(rho.partial_transpose([0, 2]), DensityMatrix(rho1)) + + with self.subTest(msg="entangled"): + rho = DensityMatrix([[0, 0, 0, 0], [0, 0.5, -0.5, 0], [0, -0.5, 0.5, 0], [0, 0, 0, 0]]) + rho1 = DensityMatrix([[0, 0, 0, -0.5], [0, 0.5, 0, 0], [0, 0, 0.5, 0], [-0.5, 0, 0, 0]]) + self.assertEqual(rho.partial_transpose([0]), DensityMatrix(rho1)) + self.assertEqual(rho.partial_transpose([1]), DensityMatrix(rho1)) + def test_clip_probabilities(self): """Test probabilities are clipped to [0, 1].""" dm = DensityMatrix([[1.1, 0], [0, 0]]) From 272c0cf79b54c1f2943175032ead092424f66c19 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Mon, 17 Apr 2023 13:32:38 +0200 Subject: [PATCH 034/172] Remove argument max_credits from execute and assemble (#9322) * Remove argument max_credits in qiskit.execute_function.execute * remove max_credits * remove test * Update releasenotes/notes/remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman Co-authored-by: Jake Lishman --- qiskit/assembler/run_config.py | 16 ----- qiskit/compiler/assembler.py | 16 ----- qiskit/execute_function.py | 14 ---- qiskit/qobj/pulse_qobj.py | 16 ----- qiskit/qobj/qasm_qobj.py | 16 ----- qiskit/utils/quantum_instance.py | 17 +---- ...function-max_credits-8c822b8b4b3d84ba.yaml | 8 +++ test/python/qobj/test_qobj_deprecated.py | 69 ------------------- 8 files changed, 10 insertions(+), 162 deletions(-) create mode 100644 releasenotes/notes/remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml delete mode 100644 test/python/qobj/test_qobj_deprecated.py diff --git a/qiskit/assembler/run_config.py b/qiskit/assembler/run_config.py index 78da74f4b08e..b83f07ec08e1 100644 --- a/qiskit/assembler/run_config.py +++ b/qiskit/assembler/run_config.py @@ -13,7 +13,6 @@ """Models for RunConfig and its related components.""" from types import SimpleNamespace -import warnings class RunConfig(SimpleNamespace): @@ -21,9 +20,6 @@ class RunConfig(SimpleNamespace): Attributes: shots (int): the number of shots - max_credits (int): DEPRECATED This parameter is deprecated as of - Qiskit Terra 0.20.0, and will be removed in a future release. This parameter has - no effect on modern IBM Quantum systems, and no alternative is necessary. seed_simulator (int): the seed to use in the simulator memory (bool): whether to request memory from backend (per-shot readouts) @@ -33,7 +29,6 @@ class RunConfig(SimpleNamespace): def __init__( self, shots=None, - max_credits=None, seed_simulator=None, memory=None, parameter_binds=None, @@ -43,8 +38,6 @@ def __init__( Args: shots (int): the number of shots - max_credits (int): DEPRECATED the max_credits to use on the IBM Q public - devices seed_simulator (int): the seed to use in the simulator memory (bool): whether to request memory from backend (per-shot readouts) @@ -53,15 +46,6 @@ def __init__( """ if shots is not None: self.shots = shots - if max_credits is not None: - self.max_credits = max_credits - warnings.warn( - "The `max_credits` parameter is deprecated as of Qiskit Terra 0.20.0, " - "and will be removed in a future release. This parameter has no effect on " - "modern IBM Quantum systems, and no alternative is necessary.", - DeprecationWarning, - stacklevel=2, - ) if seed_simulator is not None: self.seed_simulator = seed_simulator if memory is not None: diff --git a/qiskit/compiler/assembler.py b/qiskit/compiler/assembler.py index 640d23076b0e..5a9e15c33f0e 100644 --- a/qiskit/compiler/assembler.py +++ b/qiskit/compiler/assembler.py @@ -53,7 +53,6 @@ def assemble( qobj_header: Optional[Union[QobjHeader, Dict]] = None, shots: Optional[int] = None, memory: Optional[bool] = False, - max_credits: Optional[int] = None, seed_simulator: Optional[int] = None, qubit_lo_freq: Optional[List[float]] = None, meas_lo_freq: Optional[List[float]] = None, @@ -102,9 +101,6 @@ def assemble( memory: If ``True``, per-shot measurement bitstrings are returned as well (provided the backend supports it). For OpenPulse jobs, only measurement level 2 supports this option. - max_credits: DEPRECATED This parameter is deprecated as of - Qiskit Terra 0.20.0, and will be removed in a future release. This parameter has - no effect on modern IBM Quantum systems, and no alternative is necessary. seed_simulator: Random seed to control sampling, for when backend is a simulator qubit_lo_freq: List of job level qubit drive LO frequencies in Hz. Overridden by ``schedule_los`` if specified. Must have length ``n_qubits.`` @@ -157,15 +153,6 @@ def assemble( Raises: QiskitError: if the input cannot be interpreted as either circuits or schedules """ - if max_credits is not None: - max_credits = None - warnings.warn( - "The `max_credits` parameter is deprecated as of Qiskit Terra 0.20.0, " - "and will be removed in a future release. This parameter has no effect on " - "modern IBM Quantum systems, and no alternative is necessary.", - DeprecationWarning, - stacklevel=2, - ) start_time = time() experiments = experiments if isinstance(experiments, list) else [experiments] pulse_qobj = any(isinstance(exp, (ScheduleBlock, Schedule, Instruction)) for exp in experiments) @@ -175,7 +162,6 @@ def assemble( qobj_header, shots, memory, - max_credits, seed_simulator, init_qubits, rep_delay, @@ -241,7 +227,6 @@ def _parse_common_args( qobj_header, shots, memory, - max_credits, seed_simulator, init_qubits, rep_delay, @@ -368,7 +353,6 @@ def _parse_common_args( run_config_dict = dict( shots=shots, memory=memory, - max_credits=max_credits, seed_simulator=seed_simulator, init_qubits=init_qubits, rep_delay=rep_delay, diff --git a/qiskit/execute_function.py b/qiskit/execute_function.py index 45cb585be24e..e79399064e34 100644 --- a/qiskit/execute_function.py +++ b/qiskit/execute_function.py @@ -36,14 +36,6 @@ def _log_submission_time(start_time, end_time): logger.info(log_msg) -@deprecate_arg( - "max_credits", - since="0.20.0", - additional_msg=( - "This argument has no effect on modern IBM Quantum systems, and no alternative is" - "necessary." - ), -) @deprecate_arg("qobj_id", since="0.21.0", additional_msg="This argument has no effect anymore.") @deprecate_arg("qobj_header", since="0.21.0", additional_msg="This argument has no effect anymore.") def execute( @@ -60,7 +52,6 @@ def execute( qobj_header=None, shots=None, # common run options memory=None, - max_credits=None, seed_simulator=None, default_qubit_los=None, default_meas_los=None, # schedule run options @@ -183,10 +174,6 @@ def execute( (provided the backend supports it). For OpenPulse jobs, only measurement level 2 supports this option. Default: False - max_credits (int): DEPRECATED This parameter is deprecated as of Qiskit Terra 0.20.0 - and will be removed in a future release. This parameter has no effect on modern - IBM Quantum systems, no alternative is necessary. - seed_simulator (int): Random seed to control sampling, for when backend is a simulator default_qubit_los (Optional[List[float]]): List of job level qubit drive LO frequencies @@ -293,7 +280,6 @@ def execute( """ del qobj_id del qobj_header - del max_credits if isinstance(experiments, (Schedule, ScheduleBlock)) or ( isinstance(experiments, list) and isinstance(experiments[0], (Schedule, ScheduleBlock)) ): diff --git a/qiskit/qobj/pulse_qobj.py b/qiskit/qobj/pulse_qobj.py index caa4d8738436..e5f45b4d2acb 100644 --- a/qiskit/qobj/pulse_qobj.py +++ b/qiskit/qobj/pulse_qobj.py @@ -23,7 +23,6 @@ from qiskit.qobj.common import QobjDictField from qiskit.qobj.common import QobjHeader from qiskit.qobj.common import QobjExperimentHeader -from qiskit.utils.deprecation import deprecate_arg class QobjMeasurementOption: @@ -283,14 +282,6 @@ def _to_complex(value: Union[List[float], complex]) -> complex: class PulseQobjConfig(QobjDictField): """A configuration for a Pulse Qobj.""" - @deprecate_arg( - "max_credits", - since="0.20.0", - additional_msg=( - "This argument has no effect on modern IBM Quantum systems, and no alternative is" - "necessary." - ), - ) def __init__( self, meas_level, @@ -302,7 +293,6 @@ def __init__( rep_time=None, rep_delay=None, shots=None, - max_credits=None, seed_simulator=None, memory_slots=None, **kwargs, @@ -329,9 +319,6 @@ def __init__( supplied by the backend (``backend.configuration().rep_delay_range``). Default is ``backend.configuration().default_rep_delay``. shots (int): The number of shots - max_credits (int): DEPRECATED This parameter is deprecated as of - Qiskit Terra 0.20.0, and will be removed in a future release. This parameter has - no effect on modern IBM Quantum systems, and no alternative is necessary. seed_simulator (int): the seed to use in the simulator memory_slots (list): The number of memory slots on the device kwargs: Additional free form key value fields to add to the @@ -351,9 +338,6 @@ def __init__( if shots is not None: self.shots = int(shots) - if max_credits is not None: - self.max_credits = int(max_credits) - if seed_simulator is not None: self.seed_simulator = int(seed_simulator) diff --git a/qiskit/qobj/qasm_qobj.py b/qiskit/qobj/qasm_qobj.py index 69422ace83d1..47554a2a875a 100644 --- a/qiskit/qobj/qasm_qobj.py +++ b/qiskit/qobj/qasm_qobj.py @@ -18,7 +18,6 @@ from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.qobj.pulse_qobj import PulseQobjInstruction, PulseLibraryItem from qiskit.qobj.common import QobjDictField, QobjHeader -from qiskit.utils.deprecation import deprecate_arg class QasmQobjInstruction: @@ -279,18 +278,9 @@ def __eq__(self, other): class QasmQobjConfig(SimpleNamespace): """A configuration for a QASM Qobj.""" - @deprecate_arg( - "max_credits", - since="0.20.0", - additional_msg=( - "This argument has no effect on modern IBM Quantum systems, and no alternative is" - "necessary." - ), - ) def __init__( self, shots=None, - max_credits=None, seed_simulator=None, memory=None, parameter_binds=None, @@ -309,9 +299,6 @@ def __init__( Args: shots (int): the number of shots. - max_credits (int): DEPRECATED This parameter is deprecated as of - Qiskit Terra 0.20.0, and will be removed in a future release. This parameter has - no effect on modern IBM Quantum systems, and no alternative is necessary. seed_simulator (int): the seed to use in the simulator memory (bool): whether to request memory from backend (per-shot readouts) parameter_binds (list[dict]): List of parameter bindings @@ -334,9 +321,6 @@ def __init__( if shots is not None: self.shots = int(shots) - if max_credits is not None: - self.max_credits = int(max_credits) - if seed_simulator is not None: self.seed_simulator = int(seed_simulator) diff --git a/qiskit/utils/quantum_instance.py b/qiskit/utils/quantum_instance.py index cbc27b091aab..3a563e273d8a 100644 --- a/qiskit/utils/quantum_instance.py +++ b/qiskit/utils/quantum_instance.py @@ -34,7 +34,6 @@ _get_backend_provider, _get_backend_interface_version, ) -from qiskit.utils.deprecation import deprecate_arg from qiskit.utils.mitigation import ( CompleteMeasFitter, TensoredMeasFitter, @@ -129,7 +128,7 @@ class QuantumInstance: _BACKEND_CONFIG = ["basis_gates", "coupling_map"] _COMPILE_CONFIG = ["initial_layout", "seed_transpiler", "optimization_level"] - _RUN_CONFIG = ["shots", "max_credits", "memory", "seed_simulator"] + _RUN_CONFIG = ["shots", "memory", "seed_simulator"] _QJOB_CONFIG = ["timeout", "wait"] _NOISE_CONFIG = ["noise_model"] @@ -144,21 +143,12 @@ class QuantumInstance: "statevector_hpc_gate_opt", ] + _BACKEND_OPTIONS_QASM_ONLY - @deprecate_arg( - "max_credits", - since="0.20.0", - additional_msg=( - "This parameter has no effect on modern IBM Quantum systems, and no " - "alternative is necessary." - ), - ) def __init__( self, backend, # run config shots: Optional[int] = None, seed_simulator: Optional[int] = None, - max_credits: int = None, # backend properties basis_gates: Optional[List[str]] = None, coupling_map=None, @@ -193,9 +183,6 @@ def __init__( shots: Number of repetitions of each circuit, for sampling. If None, the shots are extracted from the backend. If the backend has none set, the default is 1024. seed_simulator: Random seed for simulators - max_credits: DEPRECATED This parameter is deprecated as of - Qiskit Terra 0.20.0, and will be removed in a future release. This parameter has - no effect on modern IBM Quantum systems, and no alternative is necessary. basis_gates: List of basis gate names supported by the target. Defaults to basis gates of the backend. coupling_map (Optional[Union['CouplingMap', List[List]]]): @@ -274,7 +261,7 @@ def __init__( # pylint: disable=cyclic-import from qiskit.assembler.run_config import RunConfig - run_config = RunConfig(shots=shots, max_credits=max_credits) + run_config = RunConfig(shots=shots) if seed_simulator is not None: run_config.seed_simulator = seed_simulator diff --git a/releasenotes/notes/remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml b/releasenotes/notes/remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml new file mode 100644 index 000000000000..f754f8e85254 --- /dev/null +++ b/releasenotes/notes/remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + The ``max_credits`` argument to :func:`~.execute_function.execute`, :func:`~.compiler.assemble` and all + of the ``Qobj`` configurations (e.g. :class:`.QasmQobjConfig` and + :class:`.PulseQobjConfig`) has been removed. + The credit system has not been in use on IBM Quantum backends for + nearly three years, and the option has no effect. No alternative is necessary. diff --git a/test/python/qobj/test_qobj_deprecated.py b/test/python/qobj/test_qobj_deprecated.py deleted file mode 100644 index be1f57b3068d..000000000000 --- a/test/python/qobj/test_qobj_deprecated.py +++ /dev/null @@ -1,69 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017, 2018. -# -# 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. - - -"""Qobj tests.""" - - -from qiskit import QuantumCircuit, execute, BasicAer -from qiskit.compiler import assemble -from qiskit.qobj import PulseQobjConfig, PulseLibraryItem, QasmQobjConfig -from qiskit.test import QiskitTestCase -from qiskit.utils import QuantumInstance - - -class TestMaxCreditsDeprecated(QiskitTestCase): - """Tests for max_credits deprecation.""" - - def test_assemble_warns(self): - """Test that assemble function displays a deprecation warning if max_credits is used""" - circuit = QuantumCircuit(1, 1) - with self.assertWarns(DeprecationWarning): - assemble(circuit, max_credits=10) - - def test_execute_warns(self): - """Test that execute function displays a deprecation warning if max_credits is used""" - backend = BasicAer.get_backend("statevector_simulator") - circuit = QuantumCircuit(1, 1) - with self.assertWarns(DeprecationWarning): - execute(circuit, backend, max_credits=10) - - def test_qasm_obj_config_warns(self): - """Test that QasmQobjConfig constructor displays a deprecation - warning if max_credits is used""" - with self.assertWarns(DeprecationWarning): - QasmQobjConfig(shots=1024, memory_slots=2, max_credits=10) - - def test_quantum_instance_warns(self): - """Test that QuantumInstance constructor displays a deprecation - warning if max_credits is used""" - with self.assertWarns(DeprecationWarning): - QuantumInstance(BasicAer.get_backend("statevector_simulator"), max_credits=10) - - def test_pulse_obj_config_warns(self): - """Test that PulseQobjConfig constructor displays a deprecation - warning if max_credits is used""" - with self.assertWarns(DeprecationWarning): - PulseQobjConfig( - shots=1024, - memory_slots=2, - max_credits=10, - meas_level=1, - memory_slot_size=8192, - meas_return="avg", - pulse_library=[ - PulseLibraryItem(name="pulse0", samples=[0.0 + 0.0j, 0.5 + 0.0j, 0.0 + 0.0j]) - ], - qubit_lo_freq=[4.9], - meas_lo_freq=[6.9], - rep_time=1000, - ) From c34caa83596b5513d2018a696b303707a64c7b6e Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 17 Apr 2023 14:37:28 +0100 Subject: [PATCH 035/172] Add builder interface for new switch statement (#9919) * Add builder interface for new switch statement With the addition of `switch`, we now have a second control-flow construct that - is not a loop, so can have lazily constructed blocks because of contained `break` and `continue` statements - can have multiple blocks that need their resources unifying For the first part, we largely just have to handle things separately to `if`, though the placeholder instructions are much simpler because the placeholder will never need mutating and replacing (c.f. the `else` block). For the second point, we move a bunch of the previously if-specific code into a shared location in order to reuse it. Unlike the `if` case, the `switch` builder uses nested context managers for its cases, because this mirrors a common indentation pattern for switch statements (at least if you're not a Linux kernel developer). We need new special logic to reject statements that are loose in the `switch` context but not a `case` context. We _could_ have just ignored that, but it feels like an easy potential mistake to make, so much better to loudly fail than silently do the wrong thing. * Rename inner placeholder_resources calculation * Add missing tests of loops inside switches --- qiskit/circuit/controlflow/_builder_utils.py | 114 +++ qiskit/circuit/controlflow/builder.py | 27 +- qiskit/circuit/controlflow/if_else.py | 129 +--- qiskit/circuit/controlflow/switch_case.py | 221 ++++++ qiskit/circuit/quantumcircuit.py | 86 ++- .../circuit/test_control_flow_builders.py | 692 +++++++++++++++++- 6 files changed, 1121 insertions(+), 148 deletions(-) create mode 100644 qiskit/circuit/controlflow/_builder_utils.py diff --git a/qiskit/circuit/controlflow/_builder_utils.py b/qiskit/circuit/controlflow/_builder_utils.py new file mode 100644 index 000000000000..49950a6655b2 --- /dev/null +++ b/qiskit/circuit/controlflow/_builder_utils.py @@ -0,0 +1,114 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022. +# +# 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. + +"""Private utility functions that are used by the builder interfaces.""" + +from typing import Iterable, Tuple, Set + +from qiskit.circuit.exceptions import CircuitError +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.register import Register +from qiskit.circuit.classicalregister import ClassicalRegister +from qiskit.circuit.quantumregister import QuantumRegister + + +def partition_registers( + registers: Iterable[Register], +) -> Tuple[Set[QuantumRegister], Set[ClassicalRegister]]: + """Partition a sequence of registers into its quantum and classical registers.""" + qregs = set() + cregs = set() + for register in registers: + if isinstance(register, QuantumRegister): + qregs.add(register) + elif isinstance(register, ClassicalRegister): + cregs.add(register) + else: + # Purely defensive against Terra expansion. + raise CircuitError(f"Unknown register: {register}.") + return qregs, cregs + + +def unify_circuit_resources(circuits: Iterable[QuantumCircuit]) -> Iterable[QuantumCircuit]: + """ + Ensure that all the given ``circuits`` have all the same qubits, clbits and registers, and + that they are defined in the same order. The order is important for binding when the bodies are + used in the 3-tuple :obj:`.Instruction` context. + + 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 + are very liable to change. No matter the method used, circuits with unified bits and registers + are returned. + """ + circuits = tuple(circuits) + if len(circuits) < 2: + return circuits + qubits = [] + clbits = [] + for circuit in circuits: + if circuit.qubits[: len(qubits)] != qubits: + return _unify_circuit_resources_rebuild(circuits) + if circuit.clbits[: len(qubits)] != clbits: + return _unify_circuit_resources_rebuild(circuits) + if circuit.num_qubits > len(qubits): + qubits = list(circuit.qubits) + if circuit.num_clbits > len(clbits): + clbits = list(circuit.clbits) + for circuit in circuits: + circuit.add_bits(qubits[circuit.num_qubits :]) + circuit.add_bits(clbits[circuit.num_clbits :]) + return _unify_circuit_registers(circuits) + + +def _unify_circuit_resources_rebuild( # pylint: disable=invalid-name # (it's too long?!) + circuits: Tuple[QuantumCircuit, ...] +) -> Tuple[QuantumCircuit, QuantumCircuit]: + """ + Ensure that all the given circuits have all the same qubits and clbits, and that they + are defined in the same order. The order is important for binding when the bodies are used in + the 3-tuple :obj:`.Instruction` context. + + This function will always rebuild the objects into new :class:`.QuantumCircuit` instances. + """ + qubits, clbits = set(), set() + for circuit in circuits: + qubits.update(circuit.qubits) + clbits.update(circuit.clbits) + qubits, clbits = list(qubits), list(clbits) + + # We use the inner `_append` method because everything is already resolved in the builders. + out_circuits = [] + for circuit in circuits: + out = QuantumCircuit(qubits, clbits, *circuit.qregs, *circuit.cregs) + for instruction in circuit.data: + out._append(instruction) + out_circuits.append(out) + return _unify_circuit_registers(out_circuits) + + +def _unify_circuit_registers(circuits: Iterable[QuantumCircuit]) -> Iterable[QuantumCircuit]: + """ + Ensure that ``true_body`` and ``false_body`` have the same registers defined within them. These + do not need to be in the same order between circuits. The two input circuits are returned, + mutated to have the same registers. + """ + circuits = tuple(circuits) + total_registers = set() + for circuit in circuits: + total_registers.update(circuit.qregs) + total_registers.update(circuit.cregs) + for circuit in circuits: + for register in total_registers - set(circuit.qregs) - set(circuit.cregs): + circuit.add_register(register) + return circuits diff --git a/qiskit/circuit/controlflow/builder.py b/qiskit/circuit/controlflow/builder.py index ee1ee58e2f6e..0d929f70d44b 100644 --- a/qiskit/circuit/controlflow/builder.py +++ b/qiskit/circuit/controlflow/builder.py @@ -21,7 +21,7 @@ import abc import itertools import typing -from typing import Callable, Collection, Iterable, List, FrozenSet, Tuple, Union +from typing import Callable, Collection, Iterable, List, FrozenSet, Tuple, Union, Optional from qiskit.circuit.classicalregister import Clbit, ClassicalRegister from qiskit.circuit.exceptions import CircuitError @@ -95,8 +95,13 @@ def concrete_instruction( The returned resources may not be the full width of the given resources, but will certainly be a subset of them; this can occur if (for example) a placeholder ``if`` statement is present, but does not itself contain any placeholder instructions. For resource efficiency, - the returned :obj:`.IfElseOp` will not unnecessarily span all resources, but only the ones - that it needs. + the returned :class:`.ControlFlowOp` will not unnecessarily span all resources, but only the + ones that it needs. + + .. note:: + + The caller of this function is responsible for ensuring that the inputs to this function + are non-strict supersets of the bits returned by :meth:`placeholder_resources`. Any condition added in by a call to :obj:`.Instruction.c_if` will be propagated through, but set properties like ``duration`` will not; it doesn't make sense for control-flow operations @@ -202,6 +207,7 @@ class ControlFlowBuilderBlock: "_allow_jumps", "_resource_requester", "_built", + "_forbidden_message", ) def __init__( @@ -212,6 +218,7 @@ def __init__( registers: Iterable[Register] = (), resource_requester: Callable, allow_jumps: bool = True, + forbidden_message: Optional[str] = None, ): """ Args: @@ -238,6 +245,11 @@ def __init__( :meth:`.QuantumCircuit._resolve_classical_resource` for the normal expected input here, and the documentation of :obj:`.InstructionSet`, which uses this same callback. + forbidden_message: If a string is given here, a :exc:`.CircuitError` will be raised on + any attempts to append instructions to the scope with this message. This is used by + pseudo scopes where the state machine of the builder scopes has changed into a + position where no instructions should be accepted, such as when inside a ``switch`` + but outside any cases. """ self.instructions: List[CircuitInstruction] = [] self.qubits = set(qubits) @@ -246,6 +258,7 @@ def __init__( self._allow_jumps = allow_jumps self._resource_requester = resource_requester self._built = False + self._forbidden_message = forbidden_message @property def allow_jumps(self): @@ -264,6 +277,9 @@ def allow_jumps(self): def append(self, instruction: CircuitInstruction) -> CircuitInstruction: """Add an instruction into the scope, keeping track of the qubits and clbits that have been used in total.""" + if self._forbidden_message is not None: + raise CircuitError(self._forbidden_message) + if not self._allow_jumps: # pylint: disable=cyclic-import from .break_loop import BreakLoopOp, BreakLoopPlaceholder @@ -393,6 +409,10 @@ def build( # that may have been built into other objects. self._built = True + if self._forbidden_message is not None: + # Reaching this implies a logic error in the builder interface. + raise RuntimeError("Cannot build a forbidden scope. Please report this as a bug.") + potential_qubits = all_qubits - self.qubits potential_clbits = all_clbits - self.clbits @@ -452,4 +472,5 @@ def copy(self) -> "ControlFlowBuilderBlock": out.clbits = self.clbits.copy() out.registers = self.registers.copy() out._allow_jumps = self._allow_jumps + out._forbidden_message = self._forbidden_message return out diff --git a/qiskit/circuit/controlflow/if_else.py b/qiskit/circuit/controlflow/if_else.py index 2a207565c684..23dc3a1d9ddd 100644 --- a/qiskit/circuit/controlflow/if_else.py +++ b/qiskit/circuit/controlflow/if_else.py @@ -13,17 +13,17 @@ "Circuit operation representing an ``if/else`` statement." -from typing import Optional, Tuple, Union, Iterable, Set +from typing import Optional, Tuple, Union, Iterable import itertools from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit from qiskit.circuit.instructionset import InstructionSet from qiskit.circuit.exceptions import CircuitError -from qiskit.circuit.quantumregister import QuantumRegister -from qiskit.circuit.register import Register + from .builder import ControlFlowBuilderBlock, InstructionPlaceholder, InstructionResources from .condition import validate_condition, condition_bits, condition_registers from .control_flow import ControlFlowOp +from ._builder_utils import partition_registers, unify_circuit_resources # This is just an indication of what's actually meant to be the public API. @@ -194,7 +194,7 @@ def __init__( # These are protected names because we're not trying to clash with parent attributes. self.__true_block = true_block self.__false_block: Optional[ControlFlowBuilderBlock] = false_block - self.__resources = self._placeholder_resources() + self.__resources = self._calculate_placeholder_resources() super().__init__( "if_else", len(self.__resources.qubits), len(self.__resources.clbits), [], label=label ) @@ -232,7 +232,7 @@ def registers(self): return self.__true_block.registers.copy() return self.__true_block.registers | self.__false_block.registers - def _placeholder_resources(self) -> InstructionResources: + 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 @@ -240,15 +240,15 @@ def _placeholder_resources(self) -> InstructionResources: public version as a cache access for efficiency. """ if self.__false_block is None: - qregs, cregs = _partition_registers(self.__true_block.registers) + qregs, cregs = partition_registers(self.__true_block.registers) return InstructionResources( qubits=tuple(self.__true_block.qubits), clbits=tuple(self.__true_block.clbits), qregs=tuple(qregs), cregs=tuple(cregs), ) - true_qregs, true_cregs = _partition_registers(self.__true_block.registers) - false_qregs, false_cregs = _partition_registers(self.__false_block.registers) + true_qregs, true_cregs = partition_registers(self.__true_block.registers) + false_qregs, false_cregs = partition_registers(self.__false_block.registers) return InstructionResources( qubits=tuple(self.__true_block.qubits | self.__false_block.qubits), clbits=tuple(self.__true_block.clbits | self.__false_block.clbits), @@ -276,13 +276,15 @@ def concrete_instruction(self, qubits, clbits): f" {current_bits - all_bits!r}" ) true_body = self.__true_block.build(qubits, clbits) - false_body = ( - None if self.__false_block is None else self.__false_block.build(qubits, clbits) - ) - # The bodies are not compelled to use all the resources that the - # ControlFlowBuilderBlock.build calls get passed, but they do need to be as wide as each - # other. Now we ensure that they are. - true_body, false_body = _unify_circuit_resources(true_body, false_body) + if self.__false_block is None: + false_body = None + else: + # The bodies are not compelled to use all the resources that the + # ControlFlowBuilderBlock.build calls get passed, but they do need to be as wide as each + # other. Now we ensure that they are. + true_body, false_body = unify_circuit_resources( + (true_body, self.__false_block.build(qubits, clbits)) + ) return ( self._copy_mutable_properties( IfElseOp(self.condition, true_body, false_body, label=self.label) @@ -485,7 +487,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): # bits onto the circuits at the end. true_body = self._if_instruction.operation.blocks[0] false_body = false_block.build(false_block.qubits, false_block.clbits) - true_body, false_body = _unify_circuit_resources(true_body, false_body) + true_body, false_body = unify_circuit_resources((true_body, false_body)) circuit.append( IfElseOp( self._if_context.condition, @@ -497,98 +499,3 @@ def __exit__(self, exc_type, exc_val, exc_tb): tuple(true_body.clbits), ) return False - - -def _partition_registers( - registers: Iterable[Register], -) -> Tuple[Set[QuantumRegister], Set[ClassicalRegister]]: - """Partition a sequence of registers into its quantum and classical registers.""" - qregs = set() - cregs = set() - for register in registers: - if isinstance(register, QuantumRegister): - qregs.add(register) - elif isinstance(register, ClassicalRegister): - cregs.add(register) - else: - # Purely defensive against Terra expansion. - raise CircuitError(f"Unknown register: {register}.") - return qregs, cregs - - -def _unify_circuit_resources( - true_body: QuantumCircuit, false_body: Optional[QuantumCircuit] -) -> Tuple[QuantumCircuit, Union[QuantumCircuit, None]]: - """ - Ensure that ``true_body`` and ``false_body`` have all the same qubits, clbits and registers, and - that they are defined in the same order. The order is important for binding when the bodies are - used in the 3-tuple :obj:`.Instruction` context. - - This function will preferentially try to mutate ``true_body`` and ``false_body`` 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 are very liable to change. No matter the method used, two circuits - with unified bits and registers are returned. - """ - if false_body is None: - return true_body, false_body - # These may be returned as inner lists, so take care to avoid mutation. - true_qubits, true_clbits = true_body.qubits, true_body.clbits - n_true_qubits, n_true_clbits = len(true_qubits), len(true_clbits) - false_qubits, false_clbits = false_body.qubits, false_body.clbits - n_false_qubits, n_false_clbits = len(false_qubits), len(false_clbits) - # Attempt to determine if the two resource lists can simply be extended to be equal. The - # messiness with comparing lengths first is to avoid doing multiple full-list comparisons. - if n_true_qubits <= n_false_qubits and true_qubits == false_qubits[:n_true_qubits]: - true_body.add_bits(false_qubits[n_true_qubits:]) - elif n_false_qubits < n_true_qubits and false_qubits == true_qubits[:n_false_qubits]: - false_body.add_bits(true_qubits[n_false_qubits:]) - else: - return _unify_circuit_resources_rebuild(true_body, false_body) - if n_true_clbits <= n_false_clbits and true_clbits == false_clbits[:n_true_clbits]: - true_body.add_bits(false_clbits[n_true_clbits:]) - elif n_false_clbits < n_true_clbits and false_clbits == true_clbits[:n_false_clbits]: - false_body.add_bits(true_clbits[n_false_clbits:]) - else: - return _unify_circuit_resources_rebuild(true_body, false_body) - return _unify_circuit_registers(true_body, false_body) - - -def _unify_circuit_resources_rebuild( - true_body: QuantumCircuit, false_body: QuantumCircuit -) -> Tuple[QuantumCircuit, QuantumCircuit]: - """ - Ensure that ``true_body`` and ``false_body`` have all the same qubits and clbits, and that they - are defined in the same order. The order is important for binding when the bodies are used in - the 3-tuple :obj:`.Instruction` context. - - This function will always rebuild the two parameters into new :obj:`.QuantumCircuit` instances. - """ - qubits = list(set(true_body.qubits).union(false_body.qubits)) - clbits = list(set(true_body.clbits).union(false_body.clbits)) - # We use the inner `_append` method because everything is already resolved. - true_out = QuantumCircuit(qubits, clbits, *true_body.qregs, *true_body.cregs) - for instruction in true_body.data: - true_out._append(instruction) - false_out = QuantumCircuit(qubits, clbits, *false_body.qregs, *false_body.cregs) - for instruction in false_body.data: - false_out._append(instruction) - return _unify_circuit_registers(true_out, false_out) - - -def _unify_circuit_registers( - true_body: QuantumCircuit, false_body: QuantumCircuit -) -> Tuple[QuantumCircuit, QuantumCircuit]: - """ - Ensure that ``true_body`` and ``false_body`` have the same registers defined within them. These - do not need to be in the same order between circuits. The two input circuits are returned, - mutated to have the same registers. - """ - true_registers = set(true_body.qregs) | set(true_body.cregs) - false_registers = set(false_body.qregs) | set(false_body.cregs) - for register in false_registers - true_registers: - true_body.add_register(register) - for register in true_registers - false_registers: - false_body.add_register(register) - return true_body, false_body diff --git a/qiskit/circuit/controlflow/switch_case.py b/qiskit/circuit/controlflow/switch_case.py index edb190c0ad02..30586f25d9bf 100644 --- a/qiskit/circuit/controlflow/switch_case.py +++ b/qiskit/circuit/controlflow/switch_case.py @@ -14,13 +14,16 @@ __all__ = ("SwitchCaseOp", "CASE_DEFAULT") +import contextlib import sys from typing import Union, Iterable, Any, Tuple, Optional, List from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit from qiskit.circuit.exceptions import CircuitError +from .builder import InstructionPlaceholder, InstructionResources, ControlFlowBuilderBlock from .control_flow import ControlFlowOp +from ._builder_utils import unify_circuit_resources, partition_registers if sys.version_info >= (3, 8): from typing import Literal @@ -176,3 +179,221 @@ def c_if(self, classical, val): "SwitchCaseOp cannot be classically controlled through Instruction.c_if. " "Please nest it in an IfElseOp instead." ) + + +class SwitchCasePlaceholder(InstructionPlaceholder): + """A placeholder instruction to use in control-flow context managers, when calculating the + number of resources this instruction should block is deferred until the construction of the + outer loop. + + This generally should not be instantiated manually; only :obj:`.SwitchContext` should do it when + it needs to defer creation of the concrete instruction. + + .. warning:: + + This is an internal interface and no part of it should be relied upon outside of Qiskit + Terra. + """ + + def __init__( + self, + target: Union[Clbit, ClassicalRegister], + cases: List[Tuple[Any, ControlFlowBuilderBlock]], + *, + label: Optional[str] = None, + ): + self.__target = target + self.__cases = cases + self.__resources = self._calculate_placeholder_resources() + super().__init__( + "switch_case", + len(self.__resources.qubits), + len(self.__resources.clbits), + [], + label=label, + ) + + def _calculate_placeholder_resources(self): + qubits = set() + clbits = set() + qregs = set() + cregs = set() + if isinstance(self.__target, Clbit): + clbits.add(self.__target) + else: + clbits.update(self.__target) + cregs.add(self.__target) + for _, body in self.__cases: + qubits |= body.qubits + clbits |= body.clbits + body_qregs, body_cregs = partition_registers(body.registers) + qregs |= body_qregs + cregs |= body_cregs + return InstructionResources( + qubits=tuple(qubits), + clbits=tuple(clbits), + qregs=tuple(qregs), + cregs=tuple(cregs), + ) + + def placeholder_resources(self): + return self.__resources + + def concrete_instruction(self, qubits, clbits): + cases = [ + (labels, unified_body) + for (labels, _), unified_body in zip( + self.__cases, + unify_circuit_resources(body.build(qubits, clbits) for _, body in self.__cases), + ) + ] + if cases: + resources = InstructionResources( + qubits=tuple(cases[0][1].qubits), + clbits=tuple(cases[0][1].clbits), + qregs=tuple(cases[0][1].qregs), + cregs=tuple(cases[0][1].cregs), + ) + else: + resources = self.__resources + return ( + self._copy_mutable_properties(SwitchCaseOp(self.__target, cases, label=self.label)), + resources, + ) + + +class SwitchContext: + """A context manager for building up ``switch`` statements onto circuits in a natural order, + without having to construct the case bodies first. + + The return value of this context manager can be used within the created context to build up the + individual ``case`` statements. No other instructions should be appended to the circuit during + the `switch` context. + + This context should almost invariably be created by a :meth:`.QuantumCircuit.switch_case` call, + and the resulting instance is a "friend" of the calling circuit. The context will manipulate + the circuit's defined scopes when it is entered (by pushing a new scope onto the stack) and + exited (by popping its scope, building it, and appending the resulting :obj:`.SwitchCaseOp`). + + .. warning:: + + This is an internal interface and no part of it should be relied upon outside of Qiskit + Terra. + """ + + def __init__( + self, + circuit: QuantumCircuit, + target: Union[Clbit, ClassicalRegister], + *, + in_loop: bool, + label: Optional[str] = None, + ): + self.circuit = circuit + self.target = target + self.in_loop = in_loop + self.complete = False + self._op_label = label + self._cases: List[Tuple[Tuple[Any, ...], ControlFlowBuilderBlock]] = [] + self._label_set = set() + + def label_in_use(self, label): + """Return whether a case label is already accounted for in the switch statement.""" + return label in self._label_set + + def add_case( + self, labels: Tuple[Union[int, Literal[CASE_DEFAULT]], ...], block: ControlFlowBuilderBlock + ): + """Add a sequence of conditions and the single block that should be run if they are + triggered to the context. The labels are assumed to have already been validated using + :meth:`label_in_use`.""" + # The labels were already validated when the case scope was entered, so we don't need to do + # it again. + self._label_set.update(labels) + self._cases.append((labels, block)) + + def __enter__(self): + self.circuit._push_scope(forbidden_message="Cannot have instructions outside a case") + return CaseBuilder(self) + + def __exit__(self, exc_type, exc_val, exc_tb): + self.complete = True + # The popped scope should be the forbidden scope. + self.circuit._pop_scope() + if exc_type is not None: + return False + # If we're in a loop-builder context, we need to emit a placeholder so that any `break` or + # `continue`s in any of our cases can be expanded when the loop-builder. If we're not, we + # need to emit a concrete instruction immediately. + placeholder = SwitchCasePlaceholder(self.target, self._cases, label=self._op_label) + initial_resources = placeholder.placeholder_resources() + if self.in_loop: + self.circuit.append(placeholder, initial_resources.qubits, initial_resources.clbits) + else: + operation, resources = placeholder.concrete_instruction( + set(initial_resources.qubits), set(initial_resources.clbits) + ) + self.circuit.append(operation, resources.qubits, resources.clbits) + return False + + +class CaseBuilder: + """A child context manager for building up the ``case`` blocks of ``switch`` statements onto + circuits in a natural order, without having to construct the case bodies first. + + This context should never need to be created manually by a user; it is the return value of the + :class:`.SwitchContext` context manager, which in turn should only be created by suitable + :meth:`.QuantumCircuit.switch_case` calls. + + .. warning:: + + This is an internal interface and no part of it should be relied upon outside of Qiskit + Terra. + """ + + DEFAULT = CASE_DEFAULT + """Convenient re-exposure of the :data:`.CASE_DEFAULT` constant.""" + + def __init__(self, parent: SwitchContext): + self.switch = parent + self.entered = False + + @contextlib.contextmanager + def __call__(self, *values): + if self.entered: + raise CircuitError( + "Cannot enter more than one case at once." + " If you want multiple labels to point to the same block," + " pass them all to a single case context," + " such as `with case(1, 2, 3):`." + ) + if self.switch.complete: + raise CircuitError("Cannot add a new case to a completed switch statement.") + if not all(value is CASE_DEFAULT or isinstance(value, int) for value in values): + raise CircuitError("Case values must be integers or `CASE_DEFAULT`") + seen = set() + for value in values: + if self.switch.label_in_use(value) or value in seen: + raise CircuitError(f"duplicate case label: '{value}'") + seen.add(value) + if isinstance(self.switch.target, Clbit): + target_clbits = [self.switch.target] + target_registers = [] + else: + target_clbits = list(self.switch.target) + target_registers = [self.switch.target] + self.switch.circuit._push_scope( + clbits=target_clbits, registers=target_registers, allow_jumps=self.switch.in_loop + ) + + try: + self.entered = True + yield + finally: + self.entered = False + block = self.switch.circuit._pop_scope() + + # This is outside the `finally` because we only want to add the case to the switch if we're + # leaving it under normal circumstances. If there was an exception in the case block, we + # should discard anything happened during its construction. + self.switch.add_case(values, block) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index f35864d2d60d..d248fbc81d6e 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -4179,6 +4179,7 @@ def _push_scope( clbits: Iterable[Clbit] = (), registers: Iterable[Register] = (), allow_jumps: bool = True, + forbidden_message: Optional[str] = None, ): """Add a scope for collecting instructions into this circuit. @@ -4189,6 +4190,8 @@ def _push_scope( qubits: Any qubits that this scope should automatically use. clbits: Any clbits that this scope should automatically use. allow_jumps: Whether this scope allows jumps to be used within it. + forbidden_message: If given, all attempts to add instructions to this scope will raise a + :exc:`.CircuitError` with this message. """ # pylint: disable=cyclic-import from qiskit.circuit.controlflow.builder import ControlFlowBuilderBlock @@ -4207,6 +4210,7 @@ def _push_scope( resource_requester=resource_requester, registers=registers, allow_jumps=allow_jumps, + forbidden_message=forbidden_message, ) ) @@ -4615,6 +4619,19 @@ def if_else( condition = (self._resolve_classical_resource(condition[0]), condition[1]) return self.append(IfElseOp(condition, true_body, false_body, label), qubits, clbits) + @typing.overload + def switch( + self, + target: Union[ClbitSpecifier, ClassicalRegister], + cases: None, + qubits: None, + clbits: None, + *, + label: Optional[str], + ) -> "qiskit.circuit.controlflow.switch_case.SwitchContext": + ... + + @typing.overload def switch( self, target: Union[ClbitSpecifier, ClassicalRegister], @@ -4622,30 +4639,75 @@ def switch( qubits: Sequence[QubitSpecifier], clbits: Sequence[ClbitSpecifier], *, - label: Optional[str] = None, + label: Optional[str], ) -> InstructionSet: + ... + + def switch(self, target, cases=None, qubits=None, clbits=None, *, label=None): """Create a ``switch``/``case`` structure on this circuit. + There are two forms for calling this function. If called with all its arguments (with the + possible exception of ``label``), it will create a :class:`.SwitchCaseOp` with the given + case structure. If ``cases`` (and ``qubits`` and ``clbits``) are *not* passed, then this + acts as a context manager, which will automatically build a :class:`.SwitchCaseOp` when the + scope finishes. In this form, you do not need to keep track of the qubits or clbits you are + using, because the scope will handle it for you. + + Example usage:: + + from qiskit.circuit import QuantumCircuit, ClassicalRegister, QuantumRegister + qreg = QuantumRegister(3) + creg = ClassicalRegister(3) + qc = QuantumCircuit(qreg, creg) + qc.h([0, 1, 2]) + qc.measure([0, 1, 2], [0, 1, 2]) + + with qc.switch(creg) as case: + with case(0): + qc.x(0) + with case(1, 2): + qc.z(1) + with case(case.DEFAULT): + qc.cx(0, 1) + Args: target (Union[ClassicalRegister, Clbit]): The classical value to switch one. This must - be integer valued. - cases (Iterable[Tuple[typing.Any, QuantumCircuit]]): A sequence of case specifiers. Each - tuple defines one case body (the second item). The first item of the tuple can be - either a single integer value, the special value :data:`.CASE_DEFAULT`, or a tuple - of several integer values. Each of the integer values will be tried in turn; control - will then pass to the body corresponding to the first match. :data:`.CASE_DEFAULT` - matches all possible values. - qubits (Sequence[Qubit]): The circuit qubits over which all case bodies execute. - clbits (Sequence[Clbit]): The circuit clbits over which all case bodies execute. + be integer-like. + cases (Iterable[Tuple[typing.Any, QuantumCircuit]]): A sequence of case specifiers. + Each tuple defines one case body (the second item). The first item of the tuple can + be either a single integer value, the special value :data:`.CASE_DEFAULT`, or a + tuple of several integer values. Each of the integer values will be tried in turn; + control will then pass to the body corresponding to the first match. + :data:`.CASE_DEFAULT` matches all possible values. Omit in context-manager form. + qubits (Sequence[Qubit]): The circuit qubits over which all case bodies execute. Omit in + context-manager form. + clbits (Sequence[Clbit]): The circuit clbits over which all case bodies execute. Omit in + context-manager form. label (Optional[str]): The string label of the instruction in the circuit. Returns: - InstructionSet: A handle to the instruction created. + InstructionSet or SwitchCaseContext: If used in context-manager mode, then this should + be used as a ``with`` resource, which will return an object that can be repeatedly + entered to produce cases for the switch statement. If the full form is used, then this + returns a handle to the instructions created. + + Raises: + CircuitError: if an incorrect calling convention is used. """ # pylint: disable=cyclic-import - from qiskit.circuit.controlflow.switch_case import SwitchCaseOp + from qiskit.circuit.controlflow.switch_case import SwitchCaseOp, SwitchContext target = self._resolve_classical_resource(target) + if cases is None: + if qubits is not None or clbits is not None: + raise CircuitError( + "When using 'switch' as a context manager, you cannot pass qubits or clbits." + ) + in_loop = bool(self._control_flow_scopes and self._control_flow_scopes[-1].allow_jumps) + return SwitchContext(self, target, in_loop=in_loop, label=label) + + if qubits is None or clbits is None: + raise CircuitError("When using 'switch' with cases, you must pass qubits and clbits.") return self.append(SwitchCaseOp(target, cases, label=label), qubits, clbits) def break_loop(self) -> InstructionSet: diff --git a/test/python/circuit/test_control_flow_builders.py b/test/python/circuit/test_control_flow_builders.py index 151249b652a0..79dda75bd16f 100644 --- a/test/python/circuit/test_control_flow_builders.py +++ b/test/python/circuit/test_control_flow_builders.py @@ -26,7 +26,7 @@ QuantumRegister, Qubit, ) -from qiskit.circuit.controlflow import ForLoopOp, IfElseOp, WhileLoopOp +from qiskit.circuit.controlflow import ForLoopOp, IfElseOp, WhileLoopOp, SwitchCaseOp, CASE_DEFAULT from qiskit.circuit.controlflow.builder import ControlFlowBuilderBlock from qiskit.circuit.controlflow.if_else import IfElsePlaceholder from qiskit.circuit.exceptions import CircuitError @@ -197,6 +197,88 @@ def test_register_condition_in_nested_block(self): self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + with self.subTest("switch/if"): + test = QuantumCircuit(qr, clbits, cr1, cr2, cr3, cr4) + with test.switch(cr1) as case_: + with case_(0): + with test.if_test((cr2, 0)): + test.x(0) + with case_(1, 2): + with test.if_test((cr3, 0)): + test.y(0) + with case_(case_.DEFAULT): + with test.if_test((cr4, 0)): + test.z(0) + + true_body1 = QuantumCircuit([qr[0]], cr2) + true_body1.x(0) + case_body1 = QuantumCircuit([qr[0]], clbits, cr1, cr2, cr3, cr4) + case_body1.if_test((cr2, 0), true_body1, [qr[0]], cr2) + + true_body2 = QuantumCircuit([qr[0]], cr3) + true_body2.y(0) + case_body2 = QuantumCircuit([qr[0]], clbits, cr1, cr2, cr3, cr4) + case_body2.if_test((cr3, 0), true_body2, [qr[0]], cr3) + + true_body3 = QuantumCircuit([qr[0]], cr4) + true_body3.z(0) + case_body3 = QuantumCircuit([qr[0]], clbits, cr1, cr2, cr3, cr4) + case_body3.if_test((cr4, 0), true_body3, [qr[0]], cr4) + + expected = QuantumCircuit(qr, clbits, cr1, cr2, cr3, cr4) + expected.switch( + cr1, + [ + (0, case_body1), + ((1, 2), case_body2), + (CASE_DEFAULT, case_body3), + ], + [qr[0]], + clbits + list(cr1), + ) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + + with self.subTest("switch/while"): + test = QuantumCircuit(qr, clbits, cr1, cr2, cr3, cr4) + with test.switch(cr1) as case_: + with case_(0): + with test.while_loop((cr2, 0)): + test.x(0) + with case_(1, 2): + with test.while_loop((cr3, 0)): + test.y(0) + with case_(case_.DEFAULT): + with test.while_loop((cr4, 0)): + test.z(0) + + loop_body1 = QuantumCircuit([qr[0]], cr2) + loop_body1.x(0) + case_body1 = QuantumCircuit([qr[0]], clbits, cr1, cr2, cr3, cr4) + case_body1.while_loop((cr2, 0), loop_body1, [qr[0]], cr2) + + loop_body2 = QuantumCircuit([qr[0]], cr3) + loop_body2.y(0) + case_body2 = QuantumCircuit([qr[0]], clbits, cr1, cr2, cr3, cr4) + case_body2.while_loop((cr3, 0), loop_body2, [qr[0]], cr3) + + loop_body3 = QuantumCircuit([qr[0]], cr4) + loop_body3.z(0) + case_body3 = QuantumCircuit([qr[0]], clbits, cr1, cr2, cr3, cr4) + case_body3.while_loop((cr4, 0), loop_body3, [qr[0]], cr4) + + expected = QuantumCircuit(qr, clbits, cr1, cr2, cr3, cr4) + expected.switch( + cr1, + [ + (0, case_body1), + ((1, 2), case_body2), + (CASE_DEFAULT, case_body3), + ], + [qr[0]], + clbits + list(cr1), + ) + def test_if_else_simple(self): """Test a simple if/else statement builds correctly, in the midst of other instructions. This test has paired if and else blocks the same natural width.""" @@ -455,6 +537,78 @@ def test_if_else_nested(self): expected.if_else(outer_cond, outer_true, outer_false, qubits, [clbits[0], clbits[2]]) self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_switch_simple(self): + """Individual labels switch test.""" + qubits = [Qubit(), Qubit(), Qubit()] + creg = ClassicalRegister(2) + test = QuantumCircuit(qubits, creg) + with test.switch(creg) as case: + with case(0): + test.x(0) + with case(1): + test.x(2) + with case(2): + test.h(0) + with case(3): + test.h(2) + + body0 = QuantumCircuit([qubits[0], qubits[2]], creg) + body0.x(qubits[0]) + body1 = QuantumCircuit([qubits[0], qubits[2]], creg) + body1.x(qubits[2]) + body2 = QuantumCircuit([qubits[0], qubits[2]], creg) + body2.h(qubits[0]) + body3 = QuantumCircuit([qubits[0], qubits[2]], creg) + body3.h(qubits[2]) + expected = QuantumCircuit(qubits, creg) + expected.switch( + creg, + [(0, body0), (1, body1), (2, body2), (3, body3)], + [qubits[0], qubits[2]], + list(creg), + ) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + + def test_switch_several_labels(self): + """Several labels pointing to the same body.""" + qubits = [Qubit(), Qubit(), Qubit()] + creg = ClassicalRegister(2) + test = QuantumCircuit(qubits, creg) + with test.switch(creg) as case: + with case(0, 1): + test.x(0) + with case(2): + test.h(0) + + body0 = QuantumCircuit([qubits[0]], creg) + body0.x(qubits[0]) + body1 = QuantumCircuit([qubits[0]], creg) + body1.h(qubits[0]) + expected = QuantumCircuit(qubits, creg) + expected.switch(creg, [((0, 1), body0), (2, body1)], [qubits[0]], list(creg)) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + + def test_switch_default(self): + """Allow a default case.""" + qubits = [Qubit(), Qubit(), Qubit()] + creg = ClassicalRegister(2) + test = QuantumCircuit(qubits, creg) + with test.switch(creg) as case: + with case(case.DEFAULT): + test.x(0) + # Additional test that the exposed `case.DEFAULT` object is referentially identical to + # the general `CASE_DEFAULT` as well, to avoid subtle equality bugs. + self.assertIs(case.DEFAULT, CASE_DEFAULT) + + body = QuantumCircuit([qubits[0]], creg) + body.x(qubits[0]) + expected = QuantumCircuit(qubits, creg) + expected.switch(creg, [(CASE_DEFAULT, body)], [qubits[0]], list(creg)) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_break_continue_expand_to_match_arguments_simple(self): """Test that ``break`` and ``continue`` statements expand to include all resources in the containing loop for simple cases with unconditional breaks.""" @@ -825,16 +979,98 @@ def test_break_continue_nested_in_if(self, loop_operation): self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) @ddt.data(QuantumCircuit.break_loop, QuantumCircuit.continue_loop) - def test_break_continue_deeply_nested_in_if(self, loop_operation): - """Test that ``break`` and ``continue`` work correctly when inside more than one ``if`` - block within a loop. This includes testing that multiple different ``if`` statements with - and without ``break`` expand to the correct number of arguments. + def test_break_continue_nested_in_switch(self, loop_operation): + """Similar to the nested-in-if case, we have to ensure that `break` and `continue` inside a + `switch` expand in size to the containing loop.""" + qubits = [Qubit(), Qubit(), Qubit()] + clbits = [Clbit(), Clbit(), Clbit(), Clbit()] + + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + with test.switch(clbits[0]) as case: + with case(0): + loop_operation(test) + with case(1): + pass + # The second empty `switch` is to test that only blocks that _need_ to expand to be the + # full width of the loop do so. + with test.switch(clbits[0]) as case: + with case(case.DEFAULT): + pass + test.h(0).c_if(clbits[2], 0) + + body0 = QuantumCircuit([qubits[0], clbits[0], clbits[2]]) + loop_operation(body0) + body1 = QuantumCircuit([qubits[0], clbits[0], clbits[2]]) + + body2 = QuantumCircuit([clbits[0]]) + + loop_body = QuantumCircuit([qubits[0], clbits[0], clbits[2]]) + loop_body.switch(clbits[0], [(0, body0), (1, body1)], [qubits[0]], [clbits[0], clbits[2]]) + loop_body.switch(clbits[0], [(CASE_DEFAULT, body2)], [], [clbits[0]]) + loop_body.h(qubits[0]).c_if(clbits[2], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, loop_body, [qubits[0]], [clbits[0], clbits[2]]) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + + @ddt.data(QuantumCircuit.break_loop, QuantumCircuit.continue_loop) + def test_break_continue_nested_in_multiple_switch(self, loop_operation): + """Similar to the nested-in-if case, we have to ensure that `break` and `continue` inside + more than one `switch` in a loop expand in size to the containing loop.""" + qubits = [Qubit(), Qubit(), Qubit()] + clbits = [Clbit(), Clbit(), Clbit()] + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)): + test.measure(1, 1) + with test.switch(1) as case: + with case(False): + test.h(0) + loop_operation(test) + with case(True): + pass + with test.switch(1) as case: + with case(False): + pass + with case(True): + test.h(2) + loop_operation(test) + loop_operation(test) + + case1_f = QuantumCircuit([qubits[0], qubits[1], qubits[2], clbits[1]]) + case1_f.h(qubits[0]) + loop_operation(case1_f) + case1_t = QuantumCircuit([qubits[0], qubits[1], qubits[2], clbits[1]]) + + case2_f = QuantumCircuit([qubits[0], qubits[1], qubits[2], clbits[1]]) + case2_t = QuantumCircuit([qubits[0], qubits[1], qubits[2], clbits[1]]) + case2_t.h(qubits[2]) + loop_operation(case2_t) - These are the deepest tests, hitting all parts of the deferred builder scopes. We test both - ``if`` and ``if/else`` 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, 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. + body = QuantumCircuit([qubits[0], qubits[1], qubits[2], clbits[1]]) + body.measure(qubits[1], clbits[1]) + body.switch(clbits[1], [(False, case1_f), (True, case1_t)], body.qubits, body.clbits) + body.switch(clbits[1], [(False, case2_f), (True, case2_t)], body.qubits, body.clbits) + loop_operation(body) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop(range(2), None, body, qubits, [clbits[1]]) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + + @ddt.data(QuantumCircuit.break_loop, QuantumCircuit.continue_loop) + def test_break_continue_deeply_nested(self, loop_operation): + """Test that ``break`` and ``continue`` work correctly when inside more than one block + within a loop. This includes testing that multiple different statements with and without + ``break`` expand to the correct number of arguments. + + 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, + 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. There are several tests that build up in complexity to aid debugging if something goes wrong; the aim is that there will be more information available depending on which of the @@ -960,7 +1196,7 @@ def test_break_continue_deeply_nested_in_if(self, loop_operation): self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) - with self.subTest("for/else/else"): + with self.subTest("for/else/else/switch"): # Look on my works, ye Mighty, and despair! # (but also hopefully this is less hubristic pretension and more a useful stress test) @@ -991,14 +1227,21 @@ def test_break_continue_deeply_nested_in_if(self, loop_operation): with inner22_else: loop_operation(test) + # inner 23 + with test.switch(cond_inner[0]) as inner23_case: + with inner23_case(True): + test.h(5).c_if(8, 0) + with inner23_case(False): + loop_operation(test) + test.h(6).c_if(9, 0) with outer2_else: test.h(7).c_if(10, 0) - # inner 23 - with test.if_test(cond_inner) as inner23_else: + # inner 24 + with test.if_test(cond_inner) as inner24_else: test.h(8).c_if(11, 0) - with inner23_else: + with inner24_else: test.h(9).c_if(12, 0) # outer 3 (nesting the inner condition in an 'else' branch) @@ -1048,22 +1291,33 @@ def test_break_continue_deeply_nested_in_if(self, loop_operation): inner22_false = QuantumCircuit(loop_bits) loop_operation(inner22_false) - inner23_true = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) - inner23_true.h(qubits[8]).c_if(clbits[11], 0) - inner23_false = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) - inner23_false.h(qubits[9]).c_if(clbits[12], 0) + inner23_true = QuantumCircuit(loop_bits) + inner23_true.h(qubits[5]).c_if(clbits[8], 0) + inner23_false = QuantumCircuit(loop_bits) + loop_operation(inner23_false) + + inner24_true = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) + inner24_true.h(qubits[8]).c_if(clbits[11], 0) + inner24_false = QuantumCircuit(qubits[8:10], [clbits[0], clbits[11], clbits[12]]) + inner24_false.h(qubits[9]).c_if(clbits[12], 0) outer2_true = QuantumCircuit(loop_bits) outer2_true.h(qubits[3]).c_if(clbits[6], 0) outer2_true.if_else(cond_inner, inner21_true, inner21_false, loop_qubits, loop_clbits) outer2_true.if_else(cond_inner, inner22_true, inner22_false, loop_qubits, loop_clbits) + outer2_true.switch( + cond_inner[0], + [(True, inner23_true), (False, inner23_false)], + loop_qubits, + loop_clbits, + ) outer2_true.h(qubits[6]).c_if(clbits[9], 0) outer2_false = QuantumCircuit(loop_bits) outer2_false.h(qubits[7]).c_if(clbits[10], 0) outer2_false.if_else( cond_inner, - inner23_true, - inner23_false, + inner24_true, + inner24_false, [qubits[8], qubits[9]], [clbits[0], clbits[11], clbits[12]], ) @@ -1380,6 +1634,123 @@ def test_break_continue_deeply_nested_in_if(self, loop_operation): self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + with self.subTest("if/while/if/switch"): + test = QuantumCircuit(qubits, clbits) + with test.if_test(cond_outer): # outer_t + test.h(0).c_if(3, 0) + with test.while_loop(cond_loop): # loop + test.h(1).c_if(4, 0) + with test.if_test(cond_inner): # inner_t + test.h(2).c_if(5, 0) + with test.switch(5) as case_: + with case_(False): # case_f + test.h(3).c_if(6, 0) + with case_(True): # case_t + loop_operation(test) + test.h(4).c_if(7, 0) + # exit inner_t + test.h(5).c_if(8, 0) + # exit loop + test.h(6).c_if(9, 0) + # exit outer_t + test.h(7).c_if(10, 0) + + case_f = QuantumCircuit(qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) + case_f.h(qubits[3]).c_if(clbits[6], 0) + case_t = QuantumCircuit(qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) + loop_operation(case_t) + + inner_t = QuantumCircuit(qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) + inner_t.h(qubits[2]).c_if(clbits[5], 0) + inner_t.switch( + clbits[5], + [(False, case_f), (True, case_t)], + qubits[1:6], + [clbits[0], clbits[2]] + clbits[4:9], + ) + inner_t.h(qubits[4]).c_if(clbits[7], 0) + + loop = QuantumCircuit(qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) + loop.h(qubits[1]).c_if(clbits[4], 0) + loop.if_test(cond_inner, inner_t, qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) + loop.h(qubits[5]).c_if(clbits[8], 0) + + outer_t = QuantumCircuit(qubits[:7], clbits[:10]) + outer_t.h(qubits[0]).c_if(clbits[3], 0) + outer_t.while_loop(cond_loop, loop, qubits[1:6], [clbits[0], clbits[2]] + clbits[4:9]) + outer_t.h(qubits[6]).c_if(clbits[9], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.if_test(cond_outer, outer_t, qubits[:7], clbits[:10]) + expected.h(qubits[7]).c_if(clbits[10], 0) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + + with self.subTest("switch/for/switch/else"): + test = QuantumCircuit(qubits, clbits) + with test.switch(0) as case_outer: + with case_outer(False): # outer_case_f + test.h(0).c_if(3, 0) + with test.for_loop(range(2)): # loop + test.h(1).c_if(4, 0) + with test.switch(1) as case_inner: + with case_inner(False): # inner_case_f + test.h(2).c_if(5, 0) + with test.if_test((2, True)) as else_: # if_t + test.h(3).c_if(6, 0) + with else_: # if_f + loop_operation(test) + test.h(4).c_if(7, 0) + with case_inner(True): # inner_case_t + loop_operation(test) + test.h(5).c_if(8, 0) + # exit loop1 + test.h(6).c_if(9, 0) + with case_outer(True): # outer_case_t + test.h(7).c_if(10, 0) + test.h(8).c_if(11, 0) + + if_t = QuantumCircuit(qubits[1:6], clbits[1:3] + clbits[4:9]) + if_t.h(qubits[3]).c_if(clbits[6], 0) + if_f = QuantumCircuit(qubits[1:6], clbits[1:3] + clbits[4:9]) + loop_operation(if_f) + + inner_case_f = QuantumCircuit(qubits[1:6], clbits[1:3] + clbits[4:9]) + inner_case_f.h(qubits[2]).c_if(clbits[5], 0) + inner_case_f.if_else( + (clbits[2], True), if_t, if_f, qubits[1:6], clbits[1:3] + clbits[4:9] + ) + inner_case_f.h(qubits[4]).c_if(clbits[7], 0) + + inner_case_t = QuantumCircuit(qubits[1:6], clbits[1:3] + clbits[4:9]) + loop_operation(inner_case_t) + + loop = QuantumCircuit(qubits[1:6], clbits[1:3] + clbits[4:9]) + loop.h(qubits[1]).c_if(clbits[4], 0) + loop.switch( + clbits[1], + [(False, inner_case_f), (True, inner_case_t)], + qubits[1:6], + clbits[1:3] + clbits[4:9], + ) + loop.h(qubits[5]).c_if(clbits[8], 0) + + outer_case_f = QuantumCircuit(qubits[:8], clbits[:11]) + outer_case_f.h(qubits[0]).c_if(clbits[3], 0) + outer_case_f.for_loop(range(2), None, loop, qubits[1:6], clbits[1:3] + clbits[4:9]) + outer_case_f.h(qubits[6]).c_if(clbits[9], 0) + + outer_case_t = QuantumCircuit(qubits[:8], clbits[:11]) + outer_case_t.h(qubits[7]).c_if(clbits[10], 0) + + expected = QuantumCircuit(qubits, clbits) + expected.switch( + clbits[0], [(False, outer_case_f), (True, outer_case_t)], qubits[:8], clbits[:11] + ) + expected.h(qubits[8]).c_if(clbits[11], 0) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_for_handles_iterables_correctly(self): """Test that the ``indexset`` in ``for`` loops is handled the way we expect. In general, this means all iterables are consumed into a tuple on first access, except for ``range`` @@ -1585,6 +1956,19 @@ def test_access_of_resources_from_direct_append(self): self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + with self.subTest("switch"): + test = QuantumCircuit(qubits, clbits) + with test.switch(cond[0]) as case: + with case(0): + test.append(Measure(), [qubits[1]], [clbits[1]]) + + body = QuantumCircuit([qubits[1]], clbits) + body.measure(qubits[1], clbits[1]) + expected = QuantumCircuit(qubits, clbits) + expected.switch(cond[0], [(0, body)], [qubits[1]], clbits) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_access_of_clbit_from_c_if(self): """Test that resources added from a call to :meth:`.InstructionSet.c_if` propagate through the context managers correctly.""" @@ -1636,6 +2020,16 @@ def test_access_of_clbit_from_c_if(self): expected = QuantumCircuit(bits) expected.while_loop(cond, body, [qubits[0]], clbits) + with self.subTest("switch"): + test = QuantumCircuit(bits) + with test.switch(cond[0]) as case, case(False): + test.h(0).c_if(1, 0) + + body = QuantumCircuit([qubits[0]], clbits) + body.h(qubits[0]).c_if(clbits[1], 0) + expected = QuantumCircuit(bits) + expected.switch(cond[0], [(False, body)], [qubits[0]], clbits) + with self.subTest("if inside for"): test = QuantumCircuit(bits) with test.for_loop(range(2)): @@ -1649,6 +2043,18 @@ def test_access_of_clbit_from_c_if(self): expected = QuantumCircuit(bits) expected.for_loop(range(2), None, body, [qubits[0]], clbits) + with self.subTest("switch inside for"): + test = QuantumCircuit(bits) + with test.for_loop(range(2)), test.switch(cond[0]) as case, case(False): + test.h(0).c_if(1, 0) + + body = QuantumCircuit([qubits[0]], clbits) + body.h(qubits[0]).c_if(clbits[1], 0) + body = QuantumCircuit([qubits[0]], clbits) + body.switch(cond[0], [(False, body)], [qubits[0]], clbits) + expected = QuantumCircuit(bits) + expected.for_loop(range(2), None, body, [qubits[0]], clbits) + def test_access_of_classicalregister_from_c_if(self): """Test that resources added from a call to :meth:`.InstructionSet.c_if` propagate through the context managers correctly.""" @@ -1701,6 +2107,16 @@ def test_access_of_classicalregister_from_c_if(self): expected = QuantumCircuit(qubits, clbits, creg) expected.while_loop(cond, body, [qubits[0]], all_clbits) + with self.subTest("switch"): + test = QuantumCircuit(qubits, clbits, creg) + with test.switch(cond[0]) as case, case(False): + test.h(0).c_if(creg, 0) + + body = QuantumCircuit([qubits[0]], clbits, creg) + body.h(qubits[0]).c_if(creg, 0) + expected = QuantumCircuit(qubits, clbits, creg) + expected.switch(cond[0], [(False, body)], [qubits[0]], all_clbits) + with self.subTest("if inside for"): test = QuantumCircuit(qubits, clbits, creg) with test.for_loop(range(2)): @@ -1714,6 +2130,19 @@ def test_access_of_classicalregister_from_c_if(self): expected = QuantumCircuit(qubits, clbits, creg) expected.for_loop(range(2), None, body, [qubits[0]], all_clbits) + with self.subTest("switch inside for"): + test = QuantumCircuit(qubits, clbits, creg) + with test.for_loop(range(2)): + with test.switch(cond[0]) as case, case(False): + test.h(0).c_if(creg, 0) + + case = QuantumCircuit([qubits[0]], clbits, creg) + case.h(qubits[0]).c_if(creg, 0) + body = QuantumCircuit([qubits[0]], clbits, creg) + body.switch(cond[0], [(False, case)], [qubits[0]], all_clbits) + expected = QuantumCircuit(qubits, clbits, creg) + expected.for_loop(range(2), None, body, [qubits[0]], all_clbits) + def test_accept_broadcast_gates(self): """Test that the context managers accept gates that are broadcast during their addition to the scope.""" @@ -1782,6 +2211,20 @@ def test_accept_broadcast_gates(self): self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + with self.subTest("switch"): + test = QuantumCircuit(qubits, clbits) + with test.switch(cond[0]) as case, case(True): + test.measure([0, 1], [0, 1]) + + body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + body.measure(qubits[0], clbits[0]) + body.measure(qubits[1], clbits[1]) + + expected = QuantumCircuit(qubits, clbits) + expected.switch(cond[0], [(True, body)], [qubits[0], qubits[1]], [clbits[0], clbits[1]]) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + with self.subTest("if inside for"): test = QuantumCircuit(qubits, clbits) with test.for_loop(range(2)): @@ -1802,6 +2245,25 @@ def test_accept_broadcast_gates(self): self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + with self.subTest("switch inside for"): + test = QuantumCircuit(qubits, clbits) + with test.for_loop(range(2)), test.switch(cond[0]) as case, case(True): + test.measure([0, 1], [0, 1]) + + case_body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + case_body.measure(qubits[0], clbits[0]) + case_body.measure(qubits[1], clbits[1]) + + for_body = QuantumCircuit([qubits[0], qubits[1], clbits[0], clbits[1]]) + for_body.switch(cond[0], [(True, body)], [qubits[0], qubits[1]], [clbits[0], clbits[1]]) + + expected = QuantumCircuit(qubits, clbits) + expected.for_loop( + range(2), None, for_body, [qubits[0], qubits[1]], [clbits[0], clbits[1]] + ) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_labels_propagated_to_instruction(self): """Test that labels given to the circuit-builder interface are passed through.""" bits = [Qubit(), Clbit()] @@ -1842,8 +2304,17 @@ def test_labels_propagated_to_instruction(self): self.assertIsInstance(instruction, WhileLoopOp) self.assertEqual(instruction.label, label) - # The tests of 'if' and 'else' inside 'for' are to ensure we're hitting the paths where the - # 'if' scope is built lazily at the completion of the 'for'. + with self.subTest("switch"): + test = QuantumCircuit(bits) + with test.switch(cond[0], label=label) as case: + with case(False): + pass + instruction = test.data[-1].operation + self.assertIsInstance(instruction, SwitchCaseOp) + self.assertEqual(instruction.label, label) + + # The tests of blocks inside 'for' are to ensure we're hitting the paths where the scope is + # built lazily at the completion of the 'for'. with self.subTest("if inside for"): test = QuantumCircuit(bits) with test.for_loop(range(2)): @@ -1868,6 +2339,18 @@ def test_labels_propagated_to_instruction(self): self.assertIsInstance(instruction, IfElseOp) self.assertEqual(instruction.label, label) + with self.subTest("switch inside for"): + test = QuantumCircuit(bits) + with test.for_loop(range(2)): + with test.switch(cond[0], label=label) as case: + with case(False): + # Use break to ensure that we're triggering the lazy building + test.break_loop() + + instruction = test.data[-1].operation.blocks[0].data[-1].operation + self.assertIsInstance(instruction, SwitchCaseOp) + self.assertEqual(instruction.label, label) + def test_copy_of_circuits(self): """Test that various methods of copying a circuit made with the builder interface works.""" test = QuantumCircuit(5, 5) @@ -1935,6 +2418,22 @@ def test_copy_of_instructions(self): self.assertEqual(while_instruction, copy.copy(while_instruction)) self.assertEqual(while_instruction, copy.deepcopy(while_instruction)) + with self.subTest("switch"): + creg = ClassicalRegister(4) + test = QuantumCircuit(qubits, creg) + with test.switch(creg) as case: + with case(0): + test.h(0) + with case(1, 2, 3): + test.z(1) + with case(case.DEFAULT): + test.cx(0, 1) + test.measure(2, 2) + switch_instruction = test.data[0].operation + self.assertEqual(switch_instruction, switch_instruction.copy()) + self.assertEqual(switch_instruction, copy.copy(switch_instruction)) + self.assertEqual(switch_instruction, copy.deepcopy(switch_instruction)) + def test_copy_of_instruction_parameters(self): """Test that various methods of copying the parameters inside instructions created by the builder interface work. Regression test of gh-7367.""" @@ -1992,6 +2491,17 @@ def test_copy_of_instruction_parameters(self): self.assertEqual(while_body, copy.copy(while_body)) self.assertEqual(while_body, copy.deepcopy(while_body)) + with self.subTest("switch"): + test = QuantumCircuit(qubits, clbits) + with test.switch(cond[0]) as case, case(0): + test.cx(0, 1) + test.measure(2, 2) + case_instruction = test.data[0].operation + (case_body,) = case_instruction.blocks + self.assertEqual(case_body, case_body.copy()) + self.assertEqual(case_body, copy.copy(case_body)) + self.assertEqual(case_body, copy.deepcopy(case_body)) + def test_inplace_compose_within_builder(self): """Test that QuantumCircuit.compose used in-place works as expected within control-flow scopes.""" @@ -2050,6 +2560,17 @@ def test_inplace_compose_within_builder(self): self.assertEqual(canonicalize_control_flow(outer), canonicalize_control_flow(expected)) + with self.subTest("switch"): + outer = base.copy() + with outer.switch(outer.clbits[0]) as case, case(False): + outer.compose(inner, inplace=True) + + expected = base.copy() + with expected.switch(outer.clbits[0]) as case, case(False): + expected.x(0) + + self.assertEqual(canonicalize_control_flow(outer), canonicalize_control_flow(expected)) + @ddt.ddt class TestControlFlowBuildersFailurePaths(QiskitTestCase): @@ -2206,6 +2727,90 @@ def test_if_placeholder_rejects_c_if(self): ): placeholder.c_if(bits[1], 0) + def test_switch_rejects_operations_outside_cases(self): + """It shouldn't be permissible to try and put instructions inside a switch but outside a + case.""" + circuit = QuantumCircuit(1, 1) + with circuit.switch(0) as case: + with case(0): + pass + with self.assertRaisesRegex(CircuitError, r"Cannot have instructions outside a case"): + circuit.x(0) + + def test_switch_rejects_entering_case_after_close(self): + """It shouldn't be possible to enter a case within another case.""" + circuit = QuantumCircuit(1, 1) + with circuit.switch(0) as case, case(0): + pass + with self.assertRaisesRegex(CircuitError, r"Cannot add .* to a completed switch"), case(1): + pass + + def test_switch_rejects_reentering_case(self): + """It shouldn't be possible to enter a case within another case.""" + circuit = QuantumCircuit(1, 1) + with circuit.switch(0) as case, case(0), self.assertRaisesRegex( + CircuitError, r"Cannot enter more than one case at once" + ), case(1): + pass + + @ddt.data("1", 1.0, None, (1, 2)) + def test_switch_rejects_bad_case_value(self, value): + """Only well-typed values should be accepted.""" + circuit = QuantumCircuit(1, 1) + with circuit.switch(0) as case: + with case(0): + pass + with self.assertRaisesRegex(CircuitError, "Case values must be"), case(value): + pass + + def test_case_rejects_duplicate_labels(self): + """Using duplicates in the same `case` should raise an error.""" + circuit = QuantumCircuit(1, 2) + with circuit.switch(circuit.cregs[0]) as case: + with case(0): + pass + with self.assertRaisesRegex(CircuitError, "duplicate"), case(1, 1): + pass + with self.assertRaisesRegex(CircuitError, "duplicate"), case(1, 2, 3, 1): + pass + + def test_switch_rejects_duplicate_labels(self): + """Using duplicates in different `case`s should raise an error.""" + circuit = QuantumCircuit(1, 2) + with circuit.switch(circuit.cregs[0]) as case: + with case(0): + pass + with case(1): + pass + with self.assertRaisesRegex(CircuitError, "duplicate"), case(1): + pass + + def test_switch_accepts_label_after_failure(self): + """If one case causes an exception that's caught, subsequent cases should still be possible + using labels that were "used" by the failing case.""" + qreg = QuantumRegister(1, "q") + creg = ClassicalRegister(2, "c") + + test = QuantumCircuit(qreg, creg) + with test.switch(creg) as case: + with case(0): + pass + # assertRaises here is an extra test that the exception is propagated through the + # context manager, and acts as an `except` clause for the exception so control will + # continue beyond. + with self.assertRaises(SentinelException), case(1): + raise SentinelException + with case(1): + test.x(0) + + expected = QuantumCircuit(qreg, creg) + with expected.switch(creg) as case: + with case(0): + pass + with case(1): + expected.x(0) + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_reject_c_if_from_outside_scope(self): """Test that the context managers reject :meth:`.InstructionSet.c_if` calls if they occur after their scope has completed.""" @@ -2250,6 +2855,15 @@ def test_reject_c_if_from_outside_scope(self): ): instructions.c_if(*cond) + with self.subTest("switch"): + test = QuantumCircuit(bits) + with test.switch(bits[1]) as case, case(0): + instructions = test.h(0) + with self.assertRaisesRegex( + CircuitError, r"Cannot add resources after the scope has been built\." + ): + instructions.c_if(*cond) + with self.subTest("if inside for"): # 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 @@ -2262,7 +2876,19 @@ def test_reject_c_if_from_outside_scope(self): with self.assertRaisesRegex( CircuitError, r"Cannot add resources after the scope has been built\." ): + instructions.c_if(*cond) + with self.subTest("switch inside for"): + # `switch` has the same lazy building as `if`, so is subject to the same considerations + # as the above subtest. + test = QuantumCircuit(bits) + with test.for_loop(range(2)): + with test.switch(bits[1]) as case: + with case(0): + instructions = test.h(0) + with self.assertRaisesRegex( + CircuitError, r"Cannot add resources after the scope has been built\." + ): instructions.c_if(*cond) def test_raising_inside_context_manager_leave_circuit_usable(self): @@ -2351,6 +2977,19 @@ def test_raising_inside_context_manager_leave_circuit_usable(self): self.assertEqual(test, expected) self.assertEqual({x}, set(test.parameters)) + with self.subTest("switch"): + test = QuantumCircuit(1, 1) + with self.assertRaises(SentinelException), test.switch(0) as case: + with case(False): + pass + with case(True): + pass + raise SentinelException + test.h(0) + expected = QuantumCircuit(1, 1) + expected.h(0) + self.assertEqual(test, expected) + def test_can_reuse_else_manager_after_exception(self): """Test that the "else" context manager is usable after a first attempt to construct it raises an exception. Normally you cannot re-enter an "else" block, but we want the user to @@ -2403,6 +3042,9 @@ def test_context_managers_reject_passing_qubits(self, resources): r"When using 'if_test' as a context manager, you cannot pass qubits or clbits\.", ): test.if_test((test.clbits[0], 0), true_body=None, qubits=qubits, clbits=clbits) + with self.subTest("switch"): + with self.assertRaisesRegex(CircuitError, r"When using 'switch' as a context manager"): + test.switch(test.clbits[0], cases=None, qubits=qubits, clbits=clbits) @ddt.data((None, [0]), ([0], None), (None, None)) def test_non_context_manager_calling_states_reject_missing_resources(self, resources): @@ -2429,6 +3071,12 @@ def test_non_context_manager_calling_states_reject_missing_resources(self, resou r"When using 'if_test' with a body, you must pass qubits and clbits\.", ): test.if_test((test.clbits[0], 0), true_body=body, qubits=qubits, clbits=clbits) + with self.subTest("switch"): + with self.assertRaisesRegex( + CircuitError, + r"When using 'switch' with cases, you must pass qubits and clbits\.", + ): + test.switch(test.clbits[0], [(False, body)], qubits=qubits, clbits=clbits) @ddt.data(None, [Clbit()], 0) def test_builder_block_add_bits_reject_bad_bits(self, bit): From 4d4d292c8391f9400a7a5607e6d03f98a366d387 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 17 Apr 2023 15:45:25 +0100 Subject: [PATCH 036/172] Add experimental OpenQASM 3 support for switch (#9916) * Add experimental OpenQASM 3 support for switch This changes Terra's OpenQASM 3 exporter to add support for the IBM QSS `switch` extension to the OpenQASM 3 language. This is currently only a pull request to the OpenQASM 3 specification, so its syntax might be subject to some amount of change, and it is hidden behind an "experimental" flag in the exporter. * Fix lint * Decolonise spelling Co-authored-by: Matthew Treinish * Update documentation * Spelling The OED can claim that "-ize" is more correct etymologically all they like, but I don't see them re-erasing the "l" from "could". Why does "-ize" get special etymological treatment? Noah Webster wanted to go further and spell "is" as "iz", so hiz choices clearly can't be trusted either. --------- Co-authored-by: Matthew Treinish --- qiskit/qasm3/__init__.py | 38 +++++ qiskit/qasm3/ast.py | 34 +++- qiskit/qasm3/experimental.py | 30 ++++ qiskit/qasm3/exporter.py | 185 +++++++++++++++------ qiskit/qasm3/printer.py | 65 +++++++- test/python/qasm3/test_export.py | 265 ++++++++++++++++++++++++++++++- 6 files changed, 564 insertions(+), 53 deletions(-) create mode 100644 qiskit/qasm3/experimental.py diff --git a/qiskit/qasm3/__init__.py b/qiskit/qasm3/__init__.py index f9346bfb1ebe..352464cdf037 100644 --- a/qiskit/qasm3/__init__.py +++ b/qiskit/qasm3/__init__.py @@ -42,6 +42,42 @@ .. autoexception:: QASM3ExporterError +Experimental features +--------------------- + +The OpenQASM 3 language is still evolving as hardware capabilities improve, so there is no final +syntax that Qiskit can reliably target. In order to represent the evolving language, we will +sometimes release features before formal standardization, which may need to change as the review +process in the OpenQASM 3 design committees progresses. By default, the exporters will only support +standardised features of the language. To enable these early-release features, use the +``experimental`` keyword argument of :func:`dump` and :func:`dumps`. The available feature flags +are: + +.. autoclass:: ExperimentalFeatures + :members: + +If you want to enable multiple experimental features, you should combine the flags using the ``|`` +operator, such as ``flag1 | flag2``. + +For example, to perform an export using the early semantics of ``switch`` support:: + + from qiskit import qasm3, QuantumCircuit, QuantumRegister, ClassicalRegister + + # Build the circuit + qreg = QuantumRegister(3) + creg = ClassicalRegister(3) + qc = QuantumCircuit(qreg, creg) + with qc.switch(creg) as case: + with case(0): + qc.x(0) + with case(1, 2): + qc.x(1) + with case(case.DEFAULT): + qc.x(2) + + # Export to an OpenQASM 3 string. + qasm_string = qasm3.dumps(qc, experimental=qasm3.ExperimentalFeatures.SWITCH_CASE_V1) + Importing from OpenQASM 3 ========================= @@ -124,6 +160,8 @@ """ from qiskit.utils import optionals as _optionals + +from .experimental import ExperimentalFeatures from .exporter import Exporter from .exceptions import QASM3Error, QASM3ImporterError, QASM3ExporterError diff --git a/qiskit/qasm3/ast.py b/qiskit/qasm3/ast.py index 952aeb1b5ee8..83313668a286 100644 --- a/qiskit/qasm3/ast.py +++ b/qiskit/qasm3/ast.py @@ -15,7 +15,7 @@ """QASM3 AST Nodes""" import enum -from typing import Optional, List, Union +from typing import Optional, List, Union, Iterable, Tuple class ASTNode: @@ -128,6 +128,13 @@ class FloatType(ClassicalType, enum.Enum): OCT = 256 +class IntType(ClassicalType): + """Type information for a signed integer.""" + + def __init__(self, size: Optional[int] = None): + self.size = size + + class BitArrayType(ClassicalType): """Type information for a sized number of classical bits.""" @@ -299,6 +306,14 @@ def __init__(self, type_: ClassicalType, identifier: Identifier, initializer=Non self.initializer = initializer +class AssignmentStatement(Statement): + """Assignment of an expression to an l-value.""" + + def __init__(self, lvalue: SubscriptedIdentifier, rvalue: Expression): + self.lvalue = lvalue + self.rvalue = rvalue + + class QuantumDeclaration(ASTNode): """ quantumDeclaration @@ -629,3 +644,20 @@ class IODeclaration(ClassicalDeclaration): def __init__(self, modifier: IOModifier, type_: ClassicalType, identifier: Identifier): super().__init__(type_, identifier) self.modifier = modifier + + +class DefaultCase(Expression): + """An object representing the `default` special label in switch statements.""" + + def __init__(self): + super().__init__(None) + + +class SwitchStatement(Statement): + """AST node for the proposed 'switch-case' extension to OpenQASM 3.""" + + def __init__( + self, target: Expression, cases: Iterable[Tuple[Iterable[Expression], ProgramBlock]] + ): + self.target = target + self.cases = [(tuple(values), case) for values, case in cases] diff --git a/qiskit/qasm3/experimental.py b/qiskit/qasm3/experimental.py new file mode 100644 index 000000000000..f52bca12c18b --- /dev/null +++ b/qiskit/qasm3/experimental.py @@ -0,0 +1,30 @@ +# 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. + +"""Experimental feature flags.""" + +import enum + +# This enumeration is shared between a bunch of parts of the exporter, so it gets its own file to +# avoid cyclic dependencies. + + +class ExperimentalFeatures(enum.Flag): + """Flags for experimental features that the OpenQASM 3 exporter supports. + + These are experimental and are more liable to change, because the OpenQASM 3 + specification has not formally accepted them yet, so the syntax may not be finalized.""" + + SWITCH_CASE_V1 = enum.auto() + """Support exporting switch-case statements as proposed by + https://github.com/openqasm/openqasm/pull/463 at `commit bfa787aa3078 + `__.""" diff --git a/qiskit/qasm3/exporter.py b/qiskit/qasm3/exporter.py index b6bbca8d4400..2bb1919933e7 100644 --- a/qiskit/qasm3/exporter.py +++ b/qiskit/qasm3/exporter.py @@ -18,7 +18,7 @@ import itertools import numbers from os.path import dirname, join, abspath -from typing import Iterable, List, Sequence, Union +from typing import Iterable, List, Sequence, Union, Generator from qiskit.circuit import ( Barrier, @@ -40,15 +40,18 @@ IfElseOp, ForLoopOp, WhileLoopOp, + SwitchCaseOp, ControlFlowOp, BreakLoopOp, ContinueLoopOp, + CASE_DEFAULT, ) from qiskit.circuit.library import standard_gates from qiskit.circuit.register import Register from qiskit.circuit.tools import pi_check from . import ast +from .experimental import ExperimentalFeatures from .exceptions import QASM3ExporterError from .printer import BasicPrinter @@ -131,6 +134,7 @@ def __init__( disable_constants: bool = False, alias_classical_registers: bool = False, indent: str = " ", + experimental: ExperimentalFeatures = ExperimentalFeatures(0), ): """ Args: @@ -153,12 +157,15 @@ def __init__( :obj:`.ClassicalRegister`\\ s. indent: the indentation string to use for each level within an indented block. Can be set to the empty string to disable indentation. + experimental: any experimental features to enable during the export. See + :class:`ExperimentalFeatures` for more details. """ self.basis_gates = basis_gates self.disable_constants = disable_constants self.alias_classical_registers = alias_classical_registers self.includes = list(includes) self.indent = indent + self.experimental = experimental def dumps(self, circuit): """Convert the circuit to QASM 3, returning the result as a string.""" @@ -174,8 +181,11 @@ def dump(self, circuit, stream): basis_gates=self.basis_gates, disable_constants=self.disable_constants, alias_classical_registers=self.alias_classical_registers, + experimental=self.experimental, + ) + BasicPrinter(stream, indent=self.indent, experimental=self.experimental).visit( + builder.build_program() ) - BasicPrinter(stream, indent=self.indent).visit(builder.build_program()) class GlobalNamespace: @@ -317,6 +327,7 @@ def __init__( basis_gates, disable_constants, alias_classical_registers, + experimental=ExperimentalFeatures(0), ): # This is a stack of stacks; the outer stack is a list of "outer" look-up contexts, and the # inner stack is for scopes within these. A "outer" look-up context in this sense means @@ -325,6 +336,14 @@ def __init__( self._circuit_ctx = [] self.push_context(quantumcircuit) 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 + # 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._subroutine_to_declare = {} self._opaque_to_declare = {} @@ -337,6 +356,14 @@ def __init__( self.disable_constants = disable_constants self.alias_classical_registers = alias_classical_registers self.global_namespace = GlobalNamespace(includeslist, basis_gates) + self.experimental = experimental + + 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: + name = f"{basename}__generated{next(self._counter)}" + return name def _register_gate(self, gate): self.global_namespace.register(gate) @@ -351,8 +378,8 @@ def _register_opaque(self, instruction): self.global_namespace.register(instruction) self._opaque_to_declare[id(instruction)] = instruction - def _register_variable(self, variable, name=None) -> ast.Identifier: - """Register a variable in the symbol table for the current scope, returning the name that + 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 should be used to refer to the variable. The same name will be returned by subsequent calls to :meth:`_lookup_variable` within the same scope. @@ -362,7 +389,7 @@ def _register_variable(self, variable, name=None) -> ast.Identifier: # in the current scope, not just one that's available. This is a rough implementation of # the shadowing proposal currently being drafted for OpenQASM 3, though we expect it to be # expanded and modified in the future (2022-03-07). - table = self.current_scope().symbol_map + table = scope.symbol_map if name is not None: if name in _RESERVED_KEYWORDS: raise QASM3ExporterError(f"cannot reserve the keyword '{name}' as a variable name") @@ -371,16 +398,14 @@ def _register_variable(self, variable, name=None) -> ast.Identifier: f"tried to reserve '{name}', but it is already used by '{table[name]}'" ) else: - name = basename = _escape_invalid_identifier(variable.name) - while name in table: - name = f"{basename}__generated{next(self._counter)}" + name = self._unique_name(variable.name, scope) identifier = ast.Identifier(name) table[identifier.string] = variable table[variable] = identifier return identifier - def _reserve_variable_name(self, name: ast.Identifier) -> ast.Identifier: - """Reserve a variable name in the current scope, raising a :class:`.QASM3ExporterError` if + def _reserve_variable_name(self, name: ast.Identifier, scope: _Scope) -> ast.Identifier: + """Reserve a variable name in the given scope, raising a :class:`.QASM3ExporterError` if the name is already in use. This is useful for autogenerated names that the exporter itself reserves when dealing with @@ -388,7 +413,7 @@ def _reserve_variable_name(self, name: ast.Identifier) -> ast.Identifier: circuit qubits, so cannot be placed into the symbol table by the normal means. Returns the same identifier, for convenience in chaining.""" - table = self.current_scope().symbol_map + table = scope.symbol_map if name.string in table: variable = table[name.string] raise QASM3ExporterError( @@ -535,8 +560,10 @@ def build_global_statements(self) -> List[ast.Statement]: ; """ definitions = self.build_definitions() - inputs, outputs, variables = self.build_variable_declarations() - bit_declarations = self.build_classical_declarations() + # 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 if getattr(context, "_layout", None) is not None: self._physical_qubit = True @@ -549,11 +576,11 @@ def build_global_statements(self) -> List[ast.Statement]: return [ statement for source in ( - inputs, - outputs, + # 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, - variables, - bit_declarations, + self._global_classical_declarations, quantum_declarations, quantum_instructions, ) @@ -604,9 +631,12 @@ def build_subroutine_definition(self, instruction): ) name = self.global_namespace[instruction] self.push_context(instruction.definition) + scope = self.current_scope() quantum_arguments = [ ast.QuantumArgument( - self._reserve_variable_name(ast.Identifier(f"{self.gate_qubit_prefix}_{n_qubit}")) + self._reserve_variable_name( + ast.Identifier(f"{self.gate_qubit_prefix}_{n_qubit}"), scope + ) ) for n_qubit in range(len(instruction.definition.qubits)) ] @@ -630,33 +660,33 @@ def build_gate_signature(self, gate): params = [] definition = gate.definition # Dummy parameters + scope = self.current_scope() for num in range(len(gate.params) - len(definition.parameters)): param_name = f"{self.gate_parameter_prefix}_{num}" - params.append(self._reserve_variable_name(ast.Identifier(param_name))) - params += [self._register_variable(param) for param in definition.parameters] + params.append(self._reserve_variable_name(ast.Identifier(param_name), scope)) + params += [self._register_variable(param, scope) for param in definition.parameters] quantum_arguments = [ - self._reserve_variable_name(ast.Identifier(f"{self.gate_qubit_prefix}_{n_qubit}")) + self._reserve_variable_name( + ast.Identifier(f"{self.gate_qubit_prefix}_{n_qubit}"), scope + ) for n_qubit in range(len(definition.qubits)) ] return ast.QuantumGateSignature(ast.Identifier(name), quantum_arguments, params or None) - def build_variable_declarations(self): + def build_parameter_declarations(self): """Builds lists of the input, output and standard variables used in this program.""" - inputs, outputs, variables = [], [], [] - global_scope = self.global_scope(assert_=True).circuit - for parameter in global_scope.parameters: - parameter_name = self._register_variable(parameter) - declaration = _infer_variable_declaration(global_scope, parameter, parameter_name) + global_scope = self.global_scope(assert_=True) + for parameter in global_scope.circuit.parameters: + parameter_name = self._register_variable(parameter, global_scope) + declaration = _infer_variable_declaration( + global_scope.circuit, parameter, parameter_name + ) if declaration is None: continue if isinstance(declaration, ast.IODeclaration): - if declaration.modifier is ast.IOModifier.INPUT: - inputs.append(declaration) - else: - outputs.append(declaration) + self._global_io_declarations.append(declaration) else: - variables.append(declaration) - return inputs, outputs, variables + self._global_classical_declarations.append(declaration) @property def base_classical_register_name(self): @@ -675,7 +705,8 @@ def base_quantum_register_name(self): return name def build_classical_declarations(self): - """Return a list of AST nodes declaring all the classical bits and registers. + """Extend the global classical declarations with AST nodes declaring all the classical bits + and registers. The behaviour of this function depends on the setting ``alias_classical_registers``. If this is ``True``, then the output will be in the same form as the output of @@ -686,16 +717,22 @@ def build_classical_declarations(self): This function populates the lookup table ``self._loose_clbit_index_lookup``. """ + global_scope = self.global_scope() circuit = self.current_scope().circuit if self.alias_classical_registers: self._loose_clbit_index_lookup = { bit: index for index, bit in enumerate(circuit.clbits) } - flat_declaration = self.build_clbit_declaration( - len(circuit.clbits), - self._reserve_variable_name(ast.Identifier(self.base_classical_register_name)), + self._global_classical_declarations.append( + self.build_clbit_declaration( + len(circuit.clbits), + self._reserve_variable_name( + ast.Identifier(self.base_classical_register_name), global_scope + ), + ) ) - return [flat_declaration] + self.build_aliases(circuit.cregs) + self._global_classical_declarations.extend(self.build_aliases(circuit.cregs)) + return loose_register_size = 0 for index, bit in enumerate(circuit.clbits): found_bit = circuit.find_bit(bit) @@ -708,18 +745,20 @@ def build_classical_declarations(self): self._loose_clbit_index_lookup[bit] = loose_register_size loose_register_size += 1 if loose_register_size > 0: - loose = [ + self._global_classical_declarations.append( self.build_clbit_declaration( loose_register_size, - self._reserve_variable_name(ast.Identifier(self.base_classical_register_name)), + self._reserve_variable_name( + ast.Identifier(self.base_classical_register_name), global_scope + ), ) - ] - else: - loose = [] - return loose + [ - self.build_clbit_declaration(len(register), self._register_variable(register)) + ) + self._global_classical_declarations.extend( + self.build_clbit_declaration( + len(register), self._register_variable(register, global_scope) + ) for register in circuit.cregs - ] + ) def build_clbit_declaration( self, n_clbits: int, name: ast.Identifier @@ -736,9 +775,12 @@ def build_quantum_declarations(self): def build_qubit_declarations(self): """Return a declaration of all the :obj:`.Qubit`\\ s in the current scope.""" + global_scope = self.global_scope(assert_=True) # Base register return ast.QuantumDeclaration( - self._reserve_variable_name(ast.Identifier(self.base_quantum_register_name)), + self._reserve_variable_name( + ast.Identifier(self.base_quantum_register_name), global_scope + ), ast.Designator(self.build_integer(self.current_scope().circuit.num_qubits)), ) @@ -746,6 +788,7 @@ def build_aliases(self, registers: Iterable[Register]) -> List[ast.AliasStatemen """Return a list of alias declarations for the given registers. The registers can be either classical or quantum.""" out = [] + scope = self.current_scope() for register in registers: elements = [] # Greedily consolidate runs of bits into ranges. We don't bother trying to handle @@ -784,7 +827,7 @@ def build_aliases(self, registers: Iterable[Register]) -> List[ast.AliasStatemen ), ) ) - out.append(ast.AliasStatement(self._register_variable(register), elements)) + out.append(ast.AliasStatement(self._register_variable(register, scope), elements)) return out def build_quantum_instructions(self, instructions): @@ -800,6 +843,9 @@ def build_quantum_instructions(self, instructions): 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)) + continue # Build the node, ignoring any condition. if isinstance(instruction.operation, Gate): nodes = [self.build_gate_call(instruction)] @@ -853,6 +899,44 @@ def build_if_statement(self, instruction: CircuitInstruction) -> ast.BranchingSt self.pop_scope() return ast.BranchingStatement(condition, true_body, false_body) + def build_switch_statement( + self, instruction: CircuitInstruction + ) -> Generator[ast.Statement, None, None]: + """Build a :obj:`.SwitchCaseOp` into a :class:`.ast.SwitchStatement`.""" + if ExperimentalFeatures.SWITCH_CASE_V1 not in self.experimental: + raise QASM3ExporterError( + "'switch' statements are not stabilized in OpenQASM 3 yet." + " To enable experimental support, set the flag" + " 'ExperimentalFeatures.SWITCH_CASE_V1' in the 'experimental' keyword" + " argument of the exporter." + ) + if isinstance(instruction.operation.target, Clbit): + target = self.build_single_bit_reference(instruction.operation.target) + else: + real_target = self._lookup_variable(instruction.operation.target) + global_scope = self.global_scope() + target = self._reserve_variable_name( + ast.Identifier(self._unique_name("switch_dummy", global_scope)), global_scope + ) + self._global_classical_declarations.append( + ast.ClassicalDeclaration(ast.IntType(), target, None) + ) + yield ast.AssignmentStatement(target, real_target) + + def case(values, case_block): + values = [ + ast.DefaultCase() if v is CASE_DEFAULT else self.build_integer(v) for v in values + ] + self.push_scope(case_block, instruction.qubits, instruction.clbits) + case_body = self.build_program_block(case_block.data) + self.pop_scope() + return values, case_body + + yield ast.SwitchStatement( + target, + (case(values, block) for values, block in instruction.operation.cases_specifier()), + ) + def build_while_loop(self, instruction: CircuitInstruction) -> ast.WhileLoopStatement: """Build a :obj:`.WhileLoopOp` into a :obj:`.ast.WhileLoopStatement`.""" condition = self.build_eqcondition(instruction.operation.condition) @@ -866,12 +950,13 @@ def build_for_loop(self, instruction: CircuitInstruction) -> ast.ForLoopStatemen """Build a :obj:`.ForLoopOp` into a :obj:`.ast.ForLoopStatement`.""" indexset, loop_parameter, loop_circuit = instruction.operation.params self.push_scope(loop_circuit, instruction.qubits, instruction.clbits) + scope = self.current_scope() if loop_parameter is None: # The loop parameter is implicitly declared by the ``for`` loop (see also # _infer_parameter_declaration), so it doesn't matter that we haven't declared this. - loop_parameter_ast = self._reserve_variable_name(ast.Identifier("_")) + loop_parameter_ast = self._reserve_variable_name(ast.Identifier("_"), scope) else: - loop_parameter_ast = self._register_variable(loop_parameter) + loop_parameter_ast = self._register_variable(loop_parameter, scope) if isinstance(indexset, range): # QASM 3 uses inclusive ranges on both ends, unlike Python. indexset_ast = ast.Range( diff --git a/qiskit/qasm3/printer.py b/qiskit/qasm3/printer.py index 8dc4b3e5f22f..e97403c60b02 100644 --- a/qiskit/qasm3/printer.py +++ b/qiskit/qasm3/printer.py @@ -16,6 +16,8 @@ from typing import Sequence from . import ast +from .experimental import ExperimentalFeatures +from .exceptions import QASM3ExporterError class BasicPrinter: @@ -40,7 +42,14 @@ class BasicPrinter: # The visitor names include the class names, so they mix snake_case with PascalCase. # pylint: disable=invalid-name - def __init__(self, stream: io.TextIOBase, *, indent: str, chain_else_if: bool = False): + def __init__( + self, + stream: io.TextIOBase, + *, + indent: str, + chain_else_if: bool = False, + experimental: ExperimentalFeatures = ExperimentalFeatures(0), + ): """ Args: stream (io.TextIOBase): the stream that the output will be written to. @@ -76,6 +85,7 @@ def __init__(self, stream: io.TextIOBase, *, indent: str, chain_else_if: bool = self.indent = indent self._current_indent = 0 self._chain_else_if = chain_else_if + self._experimental = experimental def visit(self, node: ast.ASTNode) -> None: """Visit this node of the AST, printing it out to the stream in this class instance. @@ -156,6 +166,11 @@ 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_IntType(self, node: ast.IntType) -> None: + self.stream.write("int") + if node.size is not None: + self.stream.write(f"[{node.size}]") + def _visit_BitArrayType(self, node: ast.BitArrayType) -> None: self.stream.write(f"bit[{node.size}]") @@ -237,6 +252,13 @@ def _visit_ClassicalDeclaration(self, node: ast.ClassicalDeclaration) -> None: self.visit(node.initializer) self._end_statement() + def _visit_AssignmentStatement(self, node: ast.AssignmentStatement) -> None: + self._start_line() + self.visit(node.lvalue) + self.stream.write(" = ") + self.visit(node.rvalue) + self._end_statement() + def _visit_IODeclaration(self, node: ast.IODeclaration) -> None: self._start_line() modifier = "input" if node.modifier is ast.IOModifier.INPUT else "output" @@ -430,3 +452,44 @@ def _visit_WhileLoopStatement(self, node: ast.WhileLoopStatement) -> None: self.stream.write(") ") self.visit(node.body) self._end_line() + + def _visit_SwitchStatement(self, node: ast.SwitchStatement) -> None: + if ExperimentalFeatures.SWITCH_CASE_V1 not in self._experimental: + raise QASM3ExporterError( + "'switch' statements are not stabilised in OpenQASM 3 yet." + " To enable experimental support, set the flag" + " 'ExperimentalFeatures.SWITCH_CASE_V1' in the 'experimental' keyword" + " argument of the printer." + ) + self._start_line() + self.stream.write("switch (") + self.visit(node.target) + self.stream.write(") {") + self._end_line() + self._current_indent += 1 + for labels, case in node.cases: + if not labels: + continue + for label in labels[:-1]: + self._start_line() + self.stream.write("case ") + self.visit(label) + self.stream.write(":") + self._end_line() + self._start_line() + if isinstance(labels[-1], ast.DefaultCase): + self.visit(labels[-1]) + else: + self.stream.write("case ") + self.visit(labels[-1]) + self.stream.write(": ") + self.visit(case) + self._end_line() + self._write_statement("break") + self._current_indent -= 1 + self._start_line() + self.stream.write("}") + self._end_line() + + def _visit_DefaultCase(self, _node: ast.DefaultCase) -> None: + self.stream.write("default") diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index ace1bb3fdd34..2f3030ca3ce6 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -24,8 +24,9 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, transpile from qiskit.circuit import Parameter, Qubit, Clbit, Instruction, Gate, Delay, Barrier +from qiskit.circuit.controlflow import CASE_DEFAULT from qiskit.test import QiskitTestCase -from qiskit.qasm3 import Exporter, dumps, dump, QASM3ExporterError +from qiskit.qasm3 import Exporter, dumps, dump, QASM3ExporterError, ExperimentalFeatures from qiskit.qasm3.exporter import QASM3Builder from qiskit.qasm3.printer import BasicPrinter @@ -1776,6 +1777,268 @@ def test_unusual_conditions(self): self.assertEqual(dumps(qc).strip(), expected.strip()) +class TestExperimentalFeatures(QiskitTestCase): + """Tests of features that are hidden behind experimental flags.""" + + maxDiff = None + + def test_switch_forbidden_without_flag(self): + """Omitting the feature flag should raise an error.""" + case = QuantumCircuit(1) + circuit = QuantumCircuit(1, 1) + circuit.switch(circuit.clbits[0], [((True, False), case)], [0], []) + with self.assertRaisesRegex(QASM3ExporterError, "'switch' statements are not stabilized"): + dumps(circuit) + + def test_switch_clbit(self): + """Test that a switch statement can be constructed with a bit as a condition.""" + qubit = Qubit() + clbit = Clbit() + case1 = QuantumCircuit([qubit, clbit]) + case1.x(0) + case2 = QuantumCircuit([qubit, clbit]) + case2.z(0) + circuit = QuantumCircuit([qubit, clbit]) + circuit.switch(clbit, [(True, case1), (False, case2)], [0], [0]) + + test = dumps(circuit, experimental=ExperimentalFeatures.SWITCH_CASE_V1) + expected = """\ +OPENQASM 3; +include "stdgates.inc"; +bit[1] _loose_clbits; +qubit[1] _all_qubits; +switch (_loose_clbits[0]) { + case 1: { + x _all_qubits[0]; + } + break; + case 0: { + z _all_qubits[0]; + } + break; +} +""" + self.assertEqual(test, expected) + + def test_switch_register(self): + """Test that a switch statement can be constructed with a register as a condition.""" + qubit = Qubit() + creg = ClassicalRegister(2, "c") + case1 = QuantumCircuit([qubit], creg) + case1.x(0) + case2 = QuantumCircuit([qubit], creg) + case2.y(0) + case3 = QuantumCircuit([qubit], creg) + case3.z(0) + + circuit = QuantumCircuit([qubit], creg) + circuit.switch(creg, [(0, case1), (1, case2), (2, case3)], [0], circuit.clbits) + + test = dumps(circuit, experimental=ExperimentalFeatures.SWITCH_CASE_V1) + expected = """\ +OPENQASM 3; +include "stdgates.inc"; +bit[2] c; +int switch_dummy; +qubit[1] _all_qubits; +switch_dummy = c; +switch (switch_dummy) { + case 0: { + x _all_qubits[0]; + } + break; + case 1: { + y _all_qubits[0]; + } + break; + case 2: { + z _all_qubits[0]; + } + break; +} +""" + self.assertEqual(test, expected) + + def test_switch_with_default(self): + """Test that a switch statement can be constructed with a default case at the end.""" + qubit = Qubit() + creg = ClassicalRegister(2, "c") + case1 = QuantumCircuit([qubit], creg) + case1.x(0) + case2 = QuantumCircuit([qubit], creg) + case2.y(0) + case3 = QuantumCircuit([qubit], creg) + case3.z(0) + + circuit = QuantumCircuit([qubit], creg) + circuit.switch(creg, [(0, case1), (1, case2), (CASE_DEFAULT, case3)], [0], circuit.clbits) + + test = dumps(circuit, experimental=ExperimentalFeatures.SWITCH_CASE_V1) + expected = """\ +OPENQASM 3; +include "stdgates.inc"; +bit[2] c; +int switch_dummy; +qubit[1] _all_qubits; +switch_dummy = c; +switch (switch_dummy) { + case 0: { + x _all_qubits[0]; + } + break; + case 1: { + y _all_qubits[0]; + } + break; + default: { + z _all_qubits[0]; + } + break; +} +""" + self.assertEqual(test, expected) + + def test_switch_multiple_cases_to_same_block(self): + """Test that it is possible to add multiple cases that apply to the same block, if they are + given as a compound value. This is an allowed special case of block fall-through.""" + qubit = Qubit() + creg = ClassicalRegister(2, "c") + case1 = QuantumCircuit([qubit], creg) + case1.x(0) + case2 = QuantumCircuit([qubit], creg) + case2.y(0) + + circuit = QuantumCircuit([qubit], creg) + circuit.switch(creg, [(0, case1), ((1, 2), case2)], [0], circuit.clbits) + + test = dumps(circuit, experimental=ExperimentalFeatures.SWITCH_CASE_V1) + expected = """\ +OPENQASM 3; +include "stdgates.inc"; +bit[2] c; +int switch_dummy; +qubit[1] _all_qubits; +switch_dummy = c; +switch (switch_dummy) { + case 0: { + x _all_qubits[0]; + } + break; + case 1: + case 2: { + y _all_qubits[0]; + } + break; +} +""" + self.assertEqual(test, expected) + + def test_multiple_switches_dont_clash_on_dummy(self): + """Test that having more than one switch statement in the circuit doesn't cause naming + clashes in the dummy integer value used.""" + qubit = Qubit() + creg = ClassicalRegister(2, "switch_dummy") + case1 = QuantumCircuit([qubit], creg) + case1.x(0) + case2 = QuantumCircuit([qubit], creg) + case2.y(0) + + circuit = QuantumCircuit([qubit], creg) + circuit.switch(creg, [(0, case1), ((1, 2), case2)], [0], circuit.clbits) + circuit.switch(creg, [(0, case1), ((1, 2), case2)], [0], circuit.clbits) + + test = dumps(circuit, experimental=ExperimentalFeatures.SWITCH_CASE_V1) + expected = """\ +OPENQASM 3; +include "stdgates.inc"; +bit[2] switch_dummy; +int switch_dummy__generated0; +int switch_dummy__generated1; +qubit[1] _all_qubits; +switch_dummy__generated0 = switch_dummy; +switch (switch_dummy__generated0) { + case 0: { + x _all_qubits[0]; + } + break; + case 1: + case 2: { + y _all_qubits[0]; + } + break; +} +switch_dummy__generated1 = switch_dummy; +switch (switch_dummy__generated1) { + case 0: { + x _all_qubits[0]; + } + break; + case 1: + case 2: { + y _all_qubits[0]; + } + break; +} +""" + self.assertEqual(test, expected) + + def test_switch_nested_in_if(self): + """Test that the switch statement works when in a nested scope, including the dummy + classical variable being declared globally. This isn't necessary in the OQ3 language, but + it is universally valid and the IBM QSS stack prefers that. They're our primary consumers + of OQ3 strings, so it's best to play nicely with them.""" + qubit = Qubit() + creg = ClassicalRegister(2, "c") + case1 = QuantumCircuit([qubit], creg) + case1.x(0) + case2 = QuantumCircuit([qubit], creg) + case2.y(0) + + body = QuantumCircuit([qubit], creg) + body.switch(creg, [(0, case1), ((1, 2), case2)], [0], body.clbits) + + circuit = QuantumCircuit([qubit], creg) + circuit.if_else((creg, 1), body.copy(), body, [0], body.clbits) + + test = dumps(circuit, experimental=ExperimentalFeatures.SWITCH_CASE_V1) + expected = """\ +OPENQASM 3; +include "stdgates.inc"; +bit[2] c; +int switch_dummy; +int switch_dummy__generated0; +qubit[1] _all_qubits; +if (c == 1) { + switch_dummy = c; + switch (switch_dummy) { + case 0: { + x _all_qubits[0]; + } + break; + case 1: + case 2: { + y _all_qubits[0]; + } + break; + } +} else { + switch_dummy__generated0 = c; + switch (switch_dummy__generated0) { + case 0: { + x _all_qubits[0]; + } + break; + case 1: + case 2: { + y _all_qubits[0]; + } + break; + } +} +""" + self.assertEqual(test, expected) + + @ddt class TestQASM3ExporterFailurePaths(QiskitTestCase): """Tests of the failure paths for the exporter.""" From c4602ea0235eaf2a18f41bf97fb833ca4d3d6022 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 17 Apr 2023 11:30:33 -0400 Subject: [PATCH 037/172] Fix handling of numpy integers in marginal_distribution() (#9976) * Fix handling of numpy integers in marginal_distribution() This commit fixes an oversight in the input type checking for the marginal_distribution() function. The function which is primarily written in rust has some quick input type checking to raise a more descriptive TypeError if the input distribtion/counts object isn't of a known type (and also to dispatch to the correct rust function). This type checking was overly restrictive and would cause an error to be raised if an input counts dictionary was using numpy integer types as a value instead of a python int. This commit updates the type check to treat numpy ints as valid too. * Add support for np float values too --- qiskit/result/utils.py | 4 +- ...distribution-np-ints-ee78859bfda79b60.yaml | 8 ++++ test/python/result/test_counts.py | 30 ++++++++++++++ test/python/result/test_quasi.py | 39 ++++++++++++++++++- 4 files changed, 78 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-marginal-distribution-np-ints-ee78859bfda79b60.yaml diff --git a/qiskit/result/utils.py b/qiskit/result/utils.py index 25f96dc4d77d..b531b1bc65b5 100644 --- a/qiskit/result/utils.py +++ b/qiskit/result/utils.py @@ -231,9 +231,9 @@ def marginal_distribution( res = results_rs.marginal_distribution(counts, indices) else: first_value = next(iter(counts.values())) - if isinstance(first_value, int): + if isinstance(first_value, (int, np.integer)): res = results_rs.marginal_counts(counts, indices) - elif isinstance(first_value, float): + elif isinstance(first_value, (float, np.floating)): res = results_rs.marginal_distribution(counts, indices) else: raise QiskitError("Values of counts must be an int or float") diff --git a/releasenotes/notes/fix-marginal-distribution-np-ints-ee78859bfda79b60.yaml b/releasenotes/notes/fix-marginal-distribution-np-ints-ee78859bfda79b60.yaml new file mode 100644 index 000000000000..91d1c1134261 --- /dev/null +++ b/releasenotes/notes/fix-marginal-distribution-np-ints-ee78859bfda79b60.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed an issue with the :func:`~.marginal_distribution` function where it + would incorrectly raise an error when an input counts dictionary was using + a numpy integer type instead of the Python int type. The underlying function + always would handle the different types correctly, but the input type + checking was previously incorrectly raising a ``TypeError`` in this case. diff --git a/test/python/result/test_counts.py b/test/python/result/test_counts.py index 6fa52b92917a..e68e7718fcc6 100644 --- a/test/python/result/test_counts.py +++ b/test/python/result/test_counts.py @@ -114,6 +114,36 @@ def test_marginal_distribution_int_counts(self): result = utils.marginal_distribution(counts_obj, [0, 1]) self.assertEqual(expected, result) + def test_marginal_distribution_int_counts_numpy_64_bit(self): + raw_counts = { + 0: np.int64(4), + 1: np.int64(7), + 2: np.int64(10), + 6: np.int64(5), + 9: np.int64(11), + 13: np.int64(9), + 14: np.int64(8), + } + expected = {"00": 4, "01": 27, "10": 23} + counts_obj = counts.Counts(raw_counts, creg_sizes=[["c0", 4]], memory_slots=4) + result = utils.marginal_distribution(counts_obj, [0, 1]) + self.assertEqual(expected, result) + + def test_marginal_distribution_int_counts_numpy_8_bit(self): + raw_counts = { + 0: np.int8(4), + 1: np.int8(7), + 2: np.int8(10), + 6: np.int8(5), + 9: np.int8(11), + 13: np.int8(9), + 14: np.int8(8), + } + expected = {"00": 4, "01": 27, "10": 23} + counts_obj = counts.Counts(raw_counts, creg_sizes=[["c0", 4]], memory_slots=4) + result = utils.marginal_distribution(counts_obj, [0, 1]) + self.assertEqual(expected, result) + def test_int_outcomes_with_int_counts(self): raw_counts = {0: 21, 2: 12, 3: 5, 46: 265} counts_obj = counts.Counts(raw_counts) diff --git a/test/python/result/test_quasi.py b/test/python/result/test_quasi.py index 3225e98aaa9b..3c9b7d4c59a8 100644 --- a/test/python/result/test_quasi.py +++ b/test/python/result/test_quasi.py @@ -13,8 +13,10 @@ """Test conversion to probability distribution""" from math import sqrt +import numpy as np + from qiskit.test import QiskitTestCase -from qiskit.result import QuasiDistribution +from qiskit.result import QuasiDistribution, marginal_distribution class TestQuasi(QiskitTestCase): @@ -192,3 +194,38 @@ def test_known_quasi_conversion(self): assert abs(ans[key] - val) < 1e-14 # Check if distance calculation is correct assert abs(dist - sqrt(0.38)) < 1e-14 + + def test_marginal_distribution(self): + """Test marginal_distribution with float value.""" + qprobs = {0: 3 / 5, 1: 1 / 2, 2: 7 / 20, 3: 1 / 10, 4: -11 / 20} + dist = QuasiDistribution(qprobs) + res = marginal_distribution(dist.binary_probabilities(), [0]) + expected = {"0": 0.4, "1": 0.6} + self.assertDictAlmostEqual(expected, res, delta=0.0001) + + def test_marginal_distribution_np_double(self): + """Test marginal_distribution with np double float value.""" + qprobs = {0: 3 / 5, 1: 1 / 2, 2: 7 / 20, 3: 1 / 10, 4: -11 / 20} + qprobs = {k: np.float64(v) for k, v in qprobs.items()} + dist = QuasiDistribution(qprobs) + res = marginal_distribution(dist.binary_probabilities(), [0]) + expected = {"0": 0.4, "1": 0.6} + self.assertDictAlmostEqual(expected, res, delta=0.0001) + + def test_marginal_distribution_np_single(self): + """test marginal_distribution with np single float value.""" + qprobs = {0: 3 / 5, 1: 1 / 2, 2: 7 / 20, 3: 1 / 10, 4: -11 / 20} + qprobs = {k: np.float32(v) for k, v in qprobs.items()} + dist = QuasiDistribution(qprobs) + res = marginal_distribution(dist.binary_probabilities(), [0]) + expected = {"0": 0.4, "1": 0.6} + self.assertDictAlmostEqual(expected, res, delta=0.0001) + + def test_marginal_distribution_np_half(self): + """test marginal_distribution with np half float value.""" + qprobs = {0: 3 / 5, 1: 1 / 2, 2: 7 / 20, 3: 1 / 10, 4: -11 / 20} + qprobs = {k: np.float16(v) for k, v in qprobs.items()} + dist = QuasiDistribution(qprobs) + res = marginal_distribution(dist.binary_probabilities(), [0]) + expected = {"0": 0.4, "1": 0.6} + self.assertDictAlmostEqual(expected, res, delta=0.001) From da924788d8468aab40d1e14f371abb56767f327a Mon Sep 17 00:00:00 2001 From: Sumit Suresh Kale <35228896+sumit-kale@users.noreply.github.com> Date: Mon, 17 Apr 2023 12:17:41 -0400 Subject: [PATCH 038/172] Adding global_phase gate in qiskit-terra (#9251) * created gphase.py file * completed glbalphase file & test_gate_defin * adding GphaseGate in @data in test_gate_defn * added reno add-global-phase * added inv and matrix test in test_exten * test_gphase_inv * tox lint edit * renamed GLobalPhaseGate to Global_Phase_Gate * Changed the name to more competable GlobalPhaseGate class * # pylint: disable=E1101 * # pylint: disable=no-member * # pylint: error fixed * including phase in super * changed float to ParameterExp * modified test_globalphaseGate * phase to params[0] * renamed global_phase.py to gphase * test gphase new in extentions_std * debug 1 * debug 2 * debug 3 instructions set * debug 4 * debug 5 * debug 6 * debug 6 instructions set * debug 7 * debug 7 * added gphase in quantumcircuit.py * added method gphase in quantumcircuit.py * test_gphase_inv * gphase debug1 * gphase debug2 * gphase debug3 * inverse mtd in gphase.py * added _array in gphase.py * gphase.py array debug1 * test_matrix gphase complex dtype * test_matrix in @ddt * test_gphase_matrix * changed gphase to global_phase * removed extra comma l871 * typo in reno fixed * modified reno * Delete .vs directory Deleted .vs directory * Added test to check compatibility of gate with qc.attri * debugged pylint error1 * debugged pylint error2 * debugged pylint error2a * debugged -epy error debug1 * debugged -epy error debug 1 * debugged -epy error debug 2 * restored test_gate_definations * Revert "restored test_gate_definations" This reverts commit 642877b16f3321c087fe7e0f6d99b91a3798c28f. * reverted mistakely edited the file on Github * Delete .vs directory Deleted .vs directory * restored test_gate_definations * Revert "restored test_gate_definations" This reverts commit 642877b16f3321c087fe7e0f6d99b91a3798c28f. * reverted mistakely edited the file on Github * retrived test_gate_definitions * PR modifications new * commit feb 13 * range in append * [] in append * [0] in append * [0]->[] * [0] * [] * Operator(qc) * fixed test_ex_std * resolved changes suggested by Cryolis * Update global_phase.py Typo in year * Added transpiler consistancy test * restored the docstring * prevented cyclic imports and other changes * Fix spacing in docs * Fix lint * Add test of controlling global phase --------- Co-authored-by: Jake Lishman --- qiskit/circuit/library/__init__.py | 1 + .../library/standard_gates/__init__.py | 3 +- .../library/standard_gates/global_phase.py | 62 +++++++++++++++++++ qiskit/quantum_info/operators/operator.py | 2 +- ...dd-global-phase-gate-b52c5b25ab8a3cf6.yaml | 6 ++ test/python/circuit/test_controlled_gate.py | 16 +++++ .../circuit/test_extensions_standard.py | 49 ++++++++++++++- 7 files changed, 135 insertions(+), 4 deletions(-) create mode 100644 qiskit/circuit/library/standard_gates/global_phase.py create mode 100644 releasenotes/notes/add-global-phase-gate-b52c5b25ab8a3cf6.yaml diff --git a/qiskit/circuit/library/__init__.py b/qiskit/circuit/library/__init__.py index a121165d453b..4d530fc79ca5 100644 --- a/qiskit/circuit/library/__init__.py +++ b/qiskit/circuit/library/__init__.py @@ -124,6 +124,7 @@ XGate YGate ZGate + GlobalPhaseGate Standard Directives =================== diff --git a/qiskit/circuit/library/standard_gates/__init__.py b/qiskit/circuit/library/standard_gates/__init__.py index f085c1f3a5b8..49c0b4dde899 100644 --- a/qiskit/circuit/library/standard_gates/__init__.py +++ b/qiskit/circuit/library/standard_gates/__init__.py @@ -42,7 +42,7 @@ from .x import MCXGate, MCXGrayCode, MCXRecursive, MCXVChain from .y import YGate, CYGate from .z import ZGate, CZGate, CCZGate - +from .global_phase import GlobalPhaseGate from .multi_control_rotation_gates import mcrx, mcry, mcrz @@ -80,6 +80,7 @@ def get_standard_gate_name_mapping(): CYGate(), CZGate(), CCZGate(), + GlobalPhaseGate(Parameter("ϴ")), HGate(), PhaseGate(Parameter("ϴ")), RCCXGate(), diff --git a/qiskit/circuit/library/standard_gates/global_phase.py b/qiskit/circuit/library/standard_gates/global_phase.py new file mode 100644 index 000000000000..a92c8d4c7c9d --- /dev/null +++ b/qiskit/circuit/library/standard_gates/global_phase.py @@ -0,0 +1,62 @@ +# 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. + +"""Global Phase Gate""" + +from typing import Optional +import numpy +from qiskit.circuit.gate import Gate +from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.circuit.parameterexpression import ParameterValueType + + +class GlobalPhaseGate(Gate): + r"""The global phase gate (:math:`e^{i\theta}`). + + Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` + + **Mathamatical Representation:** + + .. math:: + \text{GlobalPhaseGate}\ = + \begin{pmatrix} + e^{i\theta} + \end{pmatrix} + """ + + def __init__(self, phase: ParameterValueType, label: Optional[str] = None): + """ + Args: + phase: The value of phase it takes. + label: An optional label for the gate. + """ + super().__init__("global_phase", 0, [phase], label=label) + + def _define(self): + + q = QuantumRegister(0, "q") + qc = QuantumCircuit(q, name=self.name, global_phase=self.params[0]) + + self.definition = qc + + def inverse(self): + r"""Return inverted GLobalPhaseGate gate. + + :math:`\text{GlobalPhaseGate}(\lambda){\dagger} = \text{GlobalPhaseGate}(-\lambda)` + """ + return GlobalPhaseGate(-self.params[0]) + + def __array__(self, dtype=complex): + """Return a numpy.array for the global_phase gate.""" + theta = self.params[0] + return numpy.array([[numpy.exp(1j * theta)]], dtype=dtype) diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index 1b04e7e9f85d..e7d3b962d326 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -345,7 +345,7 @@ def compose(self, other, qargs=None, front=False): tensor = np.reshape(self.data, self._op_shape.tensor_shape) mat = np.reshape(other.data, other._op_shape.tensor_shape) indices = [num_indices - 1 - qubit for qubit in qargs] - final_shape = [np.product(output_dims), np.product(input_dims)] + final_shape = [int(np.product(output_dims)), int(np.product(input_dims))] data = np.reshape( Operator._einsum_matmul(tensor, mat, indices, shift, right_mul), final_shape ) diff --git a/releasenotes/notes/add-global-phase-gate-b52c5b25ab8a3cf6.yaml b/releasenotes/notes/add-global-phase-gate-b52c5b25ab8a3cf6.yaml new file mode 100644 index 000000000000..0ea260e5e693 --- /dev/null +++ b/releasenotes/notes/add-global-phase-gate-b52c5b25ab8a3cf6.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added :class:`.GlobalPhaseGate` which can be applied to add a global phase + on the :class:`.QuantumCircuit`. + diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 636fe1872d14..702a500c1fe7 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -72,6 +72,7 @@ C3SXGate, C4XGate, MCPhaseGate, + GlobalPhaseGate, ) from qiskit.circuit._utils import _compute_control_matrix import qiskit.circuit.library.standard_gates as allGates @@ -223,6 +224,21 @@ def test_special_cases_equivalent_to_controlled_base_gate(self): # CX is treated like a primitive within Terra, and doesn't have a definition. self.assertTrue(Operator(special_case_gate.definition).equiv(naive_operator)) + def test_global_phase_control(self): + """Test creation of a GlobalPhaseGate.""" + base = GlobalPhaseGate(np.pi / 7) + expected_1q = PhaseGate(np.pi / 7) + self.assertEqual(Operator(base.control()), Operator(expected_1q)) + + expected_2q = PhaseGate(np.pi / 7).control() + self.assertEqual(Operator(base.control(2)), Operator(expected_2q)) + + expected_open = QuantumCircuit(1) + expected_open.x(0) + expected_open.p(np.pi / 7, 0) + expected_open.x(0) + self.assertEqual(Operator(base.control(ctrl_state=0)), Operator(expected_open)) + def test_circuit_append(self): """Test appending a controlled gate to a quantum circuit.""" circ = QuantumCircuit(5) diff --git a/test/python/circuit/test_extensions_standard.py b/test/python/circuit/test_extensions_standard.py index 79f1d08b7bb0..368da2114ade 100644 --- a/test/python/circuit/test_extensions_standard.py +++ b/test/python/circuit/test_extensions_standard.py @@ -19,7 +19,6 @@ import numpy as np from scipy.linalg import expm from ddt import data, ddt, unpack - from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister, execute from qiskit.exceptions import QiskitError from qiskit.circuit.exceptions import CircuitError @@ -36,11 +35,14 @@ RZGate, XGate, YGate, + GlobalPhaseGate, ) from qiskit import BasicAer from qiskit.quantum_info import Pauli from qiskit.quantum_info.operators.predicates import matrix_equal, is_unitary_matrix from qiskit.utils.optionals import HAS_TWEEDLEDUM +from qiskit.quantum_info import Operator +from qiskit import transpile class TestStandard1Q(QiskitTestCase): @@ -851,6 +853,50 @@ def test_z_reg_inv(self): self.assertEqual(instruction_set[1].qubits, (self.qr[1],)) self.assertEqual(instruction_set[2].operation.params, []) + def test_global_phase(self): + qc = self.circuit + qc.append(GlobalPhaseGate(0.1), []) + self.assertEqual(self.circuit[0].operation.name, "global_phase") + self.assertEqual(self.circuit[0].operation.params, [0.1]) + self.assertEqual(self.circuit[0].qubits, ()) + + def test_global_phase_inv(self): + instruction_set = self.circuit.append(GlobalPhaseGate(0.1), []).inverse() + self.assertEqual(len(instruction_set), 1) + self.assertEqual(instruction_set[0].operation.params, [-0.1]) + + def test_global_phase_matrix(self): + """Test global_phase matrix.""" + theta = 0.1 + np.testing.assert_allclose( + np.array(GlobalPhaseGate(theta)), + np.array([[np.exp(1j * theta)]], dtype=complex), + atol=1e-7, + ) + + def test_global_phase_consistency(self): + """Tests compatibility of GlobalPhaseGate with QuantumCircuit.global_phase""" + theta = 0.1 + qc1 = QuantumCircuit(0, global_phase=theta) + qc2 = QuantumCircuit(0) + qc2.append(GlobalPhaseGate(theta), []) + np.testing.assert_allclose( + Operator(qc1), + Operator(qc2), + atol=1e-7, + ) + + def test_transpile_global_phase_consistency(self): + """Tests compatibility of transpiled GlobalPhaseGate with QuantumCircuit.global_phase""" + qc1 = QuantumCircuit(0, global_phase=0.3) + qc2 = QuantumCircuit(0, global_phase=0.2) + qc2.append(GlobalPhaseGate(0.1), []) + np.testing.assert_allclose( + Operator(transpile(qc1, basis_gates=["u"])), + Operator(transpile(qc2, basis_gates=["u"])), + atol=1e-7, + ) + @ddt class TestStandard2Q(QiskitTestCase): @@ -1375,7 +1421,6 @@ def test_to_matrix(self): def test_to_matrix_op(self): """test gates implementing to_matrix generate matrix which matches definition using Operator.""" - from qiskit.quantum_info import Operator from qiskit.circuit.library.generalized_gates.gms import MSGate from qiskit.circuit.library.generalized_gates.pauli import PauliGate from qiskit.circuit.library.pauli_evolution import PauliEvolutionGate From f630fcd4e08ea97959eb2013d7b0e92a6fc26770 Mon Sep 17 00:00:00 2001 From: Shelly Garion <46566946+ShellyGarion@users.noreply.github.com> Date: Mon, 17 Apr 2023 20:05:17 +0300 Subject: [PATCH 039/172] Prepare qiskit/transpiler/graysynth.py for deprecation in next release (#9795) * transfer cnot-phase (graysynth) synthesis code to qiskit/synthesis/cnot_phase * prepare qiskit/transpiler/graysynth.py for deprecation * update test_gray_synthesis * add cnot-phase (graysynth) code * update docstring of cnot_phase_synth * add deprecation functions * fix tests following deprecations * style fix * add release notes * remove deprecation functions from graysynth.py * update tests * update release notes as a bug fix * add back graysynth to synthesis/linear * fix link in releaese notes * Add note on re-export --------- Co-authored-by: Jake Lishman --- qiskit/synthesis/__init__.py | 2 +- qiskit/synthesis/linear/__init__.py | 6 +- qiskit/synthesis/linear/cnot_synth.py | 141 +++++++++++++++++ qiskit/synthesis/linear_phase/__init__.py | 1 + .../cnot_phase_synth.py} | 146 ++---------------- qiskit/transpiler/synthesis/__init__.py | 11 +- qiskit/transpiler/synthesis/graysynth.py | 62 +++++++- .../rename-graysynth-3fa4fcb7d096ab35.yaml | 10 ++ .../circuit/library/test_linear_function.py | 2 +- ...thesis.py => test_cnot_phase_synthesis.py} | 23 +-- 10 files changed, 251 insertions(+), 153 deletions(-) create mode 100644 qiskit/synthesis/linear/cnot_synth.py rename qiskit/synthesis/{linear/graysynth.py => linear_phase/cnot_phase_synth.py} (57%) create mode 100644 releasenotes/notes/rename-graysynth-3fa4fcb7d096ab35.yaml rename test/python/synthesis/{test_gray_synthesis.py => test_cnot_phase_synthesis.py} (96%) diff --git a/qiskit/synthesis/__init__.py b/qiskit/synthesis/__init__.py index 33deb78a597a..4d118ddb1fa3 100644 --- a/qiskit/synthesis/__init__.py +++ b/qiskit/synthesis/__init__.py @@ -118,7 +118,7 @@ synth_cnot_count_full_pmh, synth_cnot_depth_line_kms, ) -from .linear_phase import synth_cz_depth_line_mr +from .linear_phase import synth_cz_depth_line_mr, synth_cnot_phase_aam from .clifford import ( synth_clifford_full, synth_clifford_ag, diff --git a/qiskit/synthesis/linear/__init__.py b/qiskit/synthesis/linear/__init__.py index 7300ee8e6f0a..115fc557bfa4 100644 --- a/qiskit/synthesis/linear/__init__.py +++ b/qiskit/synthesis/linear/__init__.py @@ -12,10 +12,14 @@ """Module containing cnot circuits""" -from .graysynth import graysynth, synth_cnot_count_full_pmh +from .cnot_synth import synth_cnot_count_full_pmh from .linear_depth_lnn import synth_cnot_depth_line_kms from .linear_matrix_utils import ( random_invertible_binary_matrix, calc_inverse_matrix, check_invertible_binary_matrix, ) + +# This is re-import is kept for compatibility with Terra 0.23. Eligible for deprecation in 0.25+. +# pylint: disable=cyclic-import,wrong-import-order +from qiskit.synthesis.linear_phase import synth_cnot_phase_aam as graysynth diff --git a/qiskit/synthesis/linear/cnot_synth.py b/qiskit/synthesis/linear/cnot_synth.py new file mode 100644 index 000000000000..f7088fbd2c87 --- /dev/null +++ b/qiskit/synthesis/linear/cnot_synth.py @@ -0,0 +1,141 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +""" +Implementation of the GraySynth algorithm for synthesizing CNOT-Phase +circuits with efficient CNOT cost, and the Patel-Hayes-Markov algorithm +for optimal synthesis of linear (CNOT-only) reversible circuits. +""" + +import copy +import numpy as np +from qiskit.circuit import QuantumCircuit +from qiskit.exceptions import QiskitError + + +def synth_cnot_count_full_pmh(state, section_size=2): + """ + Synthesize linear reversible circuits for all-to-all architecture + using Patel, Markov and Hayes method. + + This function is an implementation of the Patel, Markov and Hayes algorithm from [1] + for optimal synthesis of linear reversible circuits for all-to-all architecture, + as specified by an n x n matrix. + + Args: + state (list[list] or ndarray): n x n boolean invertible matrix, describing the state + of the input circuit + section_size (int): the size of each section, used in the + Patel–Markov–Hayes algorithm [1]. section_size must be a factor of num_qubits. + + Returns: + QuantumCircuit: a CX-only circuit implementing the linear transformation. + + Raises: + QiskitError: when variable "state" isn't of type numpy.ndarray + + References: + 1. Patel, Ketan N., Igor L. Markov, and John P. Hayes, + *Optimal synthesis of linear reversible circuits*, + Quantum Information & Computation 8.3 (2008): 282-294. + `arXiv:quant-ph/0302002 [quant-ph] `_ + """ + 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)) + ) + state = np.array(state) + # Synthesize lower triangular part + [state, circuit_l] = _lwr_cnot_synth(state, section_size) + state = np.transpose(state) + # Synthesize upper triangular part + [state, circuit_u] = _lwr_cnot_synth(state, section_size) + circuit_l.reverse() + for i in circuit_u: + i.reverse() + # Convert the list into a circuit of C-NOT gates + circ = QuantumCircuit(state.shape[0]) + for i in circuit_u + circuit_l: + circ.cx(i[0], i[1]) + return circ + + +def _lwr_cnot_synth(state, section_size): + """ + This function is a helper function of the algorithm for optimal synthesis + of linear reversible circuits (the Patel–Markov–Hayes algorithm). It works + like gaussian elimination, except that it works a lot faster, and requires + fewer steps (and therefore fewer CNOTs). It takes the matrix "state" and + splits it into sections of size section_size. Then it eliminates all non-zero + sub-rows within each section, which are the same as a non-zero sub-row + above. Once this has been done, it continues with normal gaussian elimination. + The benefit is that with small section sizes (m), most of the sub-rows will + be cleared in the first step, resulting in a factor m fewer row row operations + during Gaussian elimination. + + The algorithm is described in detail in the following paper + "Optimal synthesis of linear reversible circuits." + Patel, Ketan N., Igor L. Markov, and John P. Hayes. + Quantum Information & Computation 8.3 (2008): 282-294. + + Note: + This implementation tweaks the Patel, Markov, and Hayes algorithm by adding + a "back reduce" which adds rows below the pivot row with a high degree of + overlap back to it. The intuition is to avoid a high-weight pivot row + increasing the weight of lower rows. + + Args: + state (ndarray): n x n matrix, describing a linear quantum circuit + section_size (int): the section size the matrix columns are divided into + + Returns: + numpy.matrix: n by n matrix, describing the state of the output circuit + list: a k by 2 list of C-NOT operations that need to be applied + """ + circuit = [] + num_qubits = state.shape[0] + cutoff = 1 + + # Iterate over column sections + for sec in range(1, int(np.floor(num_qubits / section_size) + 1)): + # Remove duplicate sub-rows in section sec + patt = {} + for row in range((sec - 1) * section_size, num_qubits): + sub_row_patt = copy.deepcopy(state[row, (sec - 1) * section_size : sec * section_size]) + if np.sum(sub_row_patt) == 0: + continue + if str(sub_row_patt) not in patt: + patt[str(sub_row_patt)] = row + else: + state[row, :] ^= state[patt[str(sub_row_patt)], :] + circuit.append([patt[str(sub_row_patt)], row]) + # Use gaussian elimination for remaining entries in column section + for col in range((sec - 1) * section_size, sec * section_size): + # Check if 1 on diagonal + diag_one = 1 + if state[col, col] == 0: + diag_one = 0 + # Remove ones in rows below column col + for row in range(col + 1, num_qubits): + if state[row, col] == 1: + if diag_one == 0: + state[col, :] ^= state[row, :] + circuit.append([row, col]) + diag_one = 1 + state[row, :] ^= state[col, :] + circuit.append([col, row]) + # Back reduce the pivot row using the current row + if sum(state[col, :] & state[row, :]) > cutoff: + state[col, :] ^= state[row, :] + circuit.append([row, col]) + return [state, circuit] diff --git a/qiskit/synthesis/linear_phase/__init__.py b/qiskit/synthesis/linear_phase/__init__.py index 78ce4433603f..0be6c9c91175 100644 --- a/qiskit/synthesis/linear_phase/__init__.py +++ b/qiskit/synthesis/linear_phase/__init__.py @@ -13,3 +13,4 @@ """Module containing cnot-phase circuits""" from .cz_depth_lnn import synth_cz_depth_line_mr +from .cnot_phase_synth import synth_cnot_phase_aam diff --git a/qiskit/synthesis/linear/graysynth.py b/qiskit/synthesis/linear_phase/cnot_phase_synth.py similarity index 57% rename from qiskit/synthesis/linear/graysynth.py rename to qiskit/synthesis/linear_phase/cnot_phase_synth.py index 9caf7d45ae66..f1b49cc8d8df 100644 --- a/qiskit/synthesis/linear/graysynth.py +++ b/qiskit/synthesis/linear_phase/cnot_phase_synth.py @@ -20,19 +20,21 @@ import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.exceptions import QiskitError +from qiskit.synthesis.linear import synth_cnot_count_full_pmh -def graysynth(cnots, angles, section_size=2): - """This function is an implementation of the GraySynth algorithm. +def synth_cnot_phase_aam(cnots, angles, section_size=2): + """This function is an implementation of the GraySynth algorithm of + Amy, Azimadeh and Mosca. - GraySynth is a heuristic algorithm for synthesizing small parity networks. + GraySynth is a heuristic algorithm from [1] for synthesizing small parity networks. It is inspired by Gray codes. Given a set of binary strings S (called "cnots" bellow), the algorithm synthesizes a parity network for S by repeatedly choosing an index i to expand and then effectively recursing on the co-factors S_0 and S_1, consisting of the strings y in S, with y_i = 0 or 1 respectively. As a subset S is recursively expanded, CNOT gates are applied so that a designated target bit contains the - (partial) parity ksi_y(x) where y_i = 1 if and only if y'_i = 1 for for all + (partial) parity ksi_y(x) where y_i = 1 if and only if y'_i = 1 for all y' in S. If S is a singleton {y'}, then y = y', hence the target bit contains the value ksi_y'(x) as desired. @@ -42,10 +44,7 @@ def graysynth(cnots, angles, section_size=2): of bits. This allows the algorithm to avoid the 'backtracking' inherent in uncomputing-based methods. - The algorithm is described in detail in the following paper in section 4: - "On the controlled-NOT complexity of controlled-NOT–phase circuits." - Amy, Matthew, Parsiad Azimzadeh, and Michele Mosca. - Quantum Science and Technology 4.1 (2018): 015002. + The algorithm is described in detail in section 4 of [1]. Args: cnots (list[list]): a matrix whose columns are the parities to be synthesized @@ -62,17 +61,23 @@ def graysynth(cnots, angles, section_size=2): angles (list): a list containing all the phase-shift gates which are to be applied, in the same order as in "cnots". A number is - interpreted as the angle of u1(angle), otherwise the elements + interpreted as the angle of p(angle), otherwise the elements have to be 't', 'tdg', 's', 'sdg' or 'z'. section_size (int): the size of every section, used in _lwr_cnot_synth(), in the Patel–Markov–Hayes algorithm. section_size must be a factor of num_qubits. Returns: - QuantumCircuit: the quantum circuit + QuantumCircuit: the decomposed quantum circuit. Raises: - QiskitError: when dimensions of cnots and angles don't align + QiskitError: when dimensions of cnots and angles don't align. + + References: + 1. Matthew Amy, Parsiad Azimzadeh, and Michele Mosca. + *On the controlled-NOT complexity of controlled-NOT–phase circuits.*, + Quantum Science and Technology 4.1 (2018): 015002. + `arXiv:1712.01859 `_ """ num_qubits = len(cnots) @@ -180,125 +185,6 @@ def graysynth(cnots, angles, section_size=2): return qcir -def synth_cnot_count_full_pmh(state, section_size=2): - """ - Synthesize linear reversible circuits for all-to-all architecture - using Patel, Markov and Hayes method. - - This function is an implementation of the Patel, Markov and Hayes algorithm from [1] - for optimal synthesis of linear reversible circuits for all-to-all architecture, - as specified by an n x n matrix. - - Args: - state (list[list] or ndarray): n x n boolean invertible matrix, describing the state - of the input circuit - section_size (int): the size of each section, used in the - Patel–Markov–Hayes algorithm [1]. section_size must be a factor of num_qubits. - - Returns: - QuantumCircuit: a CX-only circuit implementing the linear transformation. - - Raises: - QiskitError: when variable "state" isn't of type numpy.ndarray - - References: - 1. Patel, Ketan N., Igor L. Markov, and John P. Hayes, - *Optimal synthesis of linear reversible circuits*, - Quantum Information & Computation 8.3 (2008): 282-294. - `arXiv:quant-ph/0302002 [quant-ph] `_ - """ - 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)) - ) - state = np.array(state) - # Synthesize lower triangular part - [state, circuit_l] = _lwr_cnot_synth(state, section_size) - state = np.transpose(state) - # Synthesize upper triangular part - [state, circuit_u] = _lwr_cnot_synth(state, section_size) - circuit_l.reverse() - for i in circuit_u: - i.reverse() - # Convert the list into a circuit of C-NOT gates - circ = QuantumCircuit(state.shape[0]) - for i in circuit_u + circuit_l: - circ.cx(i[0], i[1]) - return circ - - -def _lwr_cnot_synth(state, section_size): - """ - This function is a helper function of the algorithm for optimal synthesis - of linear reversible circuits (the Patel–Markov–Hayes algorithm). It works - like gaussian elimination, except that it works a lot faster, and requires - fewer steps (and therefore fewer CNOTs). It takes the matrix "state" and - splits it into sections of size section_size. Then it eliminates all non-zero - sub-rows within each section, which are the same as a non-zero sub-row - above. Once this has been done, it continues with normal gaussian elimination. - The benefit is that with small section sizes (m), most of the sub-rows will - be cleared in the first step, resulting in a factor m fewer row row operations - during Gaussian elimination. - - The algorithm is described in detail in the following paper - "Optimal synthesis of linear reversible circuits." - Patel, Ketan N., Igor L. Markov, and John P. Hayes. - Quantum Information & Computation 8.3 (2008): 282-294. - - Note: - This implementation tweaks the Patel, Markov, and Hayes algorithm by adding - a "back reduce" which adds rows below the pivot row with a high degree of - overlap back to it. The intuition is to avoid a high-weight pivot row - increasing the weight of lower rows. - - Args: - state (ndarray): n x n matrix, describing a linear quantum circuit - section_size (int): the section size the matrix columns are divided into - - Returns: - numpy.matrix: n by n matrix, describing the state of the output circuit - list: a k by 2 list of C-NOT operations that need to be applied - """ - circuit = [] - num_qubits = state.shape[0] - cutoff = 1 - - # Iterate over column sections - for sec in range(1, int(np.floor(num_qubits / section_size) + 1)): - # Remove duplicate sub-rows in section sec - patt = {} - for row in range((sec - 1) * section_size, num_qubits): - sub_row_patt = copy.deepcopy(state[row, (sec - 1) * section_size : sec * section_size]) - if np.sum(sub_row_patt) == 0: - continue - if str(sub_row_patt) not in patt: - patt[str(sub_row_patt)] = row - else: - state[row, :] ^= state[patt[str(sub_row_patt)], :] - circuit.append([patt[str(sub_row_patt)], row]) - # Use gaussian elimination for remaining entries in column section - for col in range((sec - 1) * section_size, sec * section_size): - # Check if 1 on diagonal - diag_one = 1 - if state[col, col] == 0: - diag_one = 0 - # Remove ones in rows below column col - for row in range(col + 1, num_qubits): - if state[row, col] == 1: - if diag_one == 0: - state[col, :] ^= state[row, :] - circuit.append([row, col]) - diag_one = 1 - state[row, :] ^= state[col, :] - circuit.append([col, row]) - # Back reduce the pivot row using the current row - if sum(state[col, :] & state[row, :]) > cutoff: - state[col, :] ^= state[row, :] - circuit.append([row, col]) - return [state, circuit] - - def _remove_duplicates(lists): """ Remove duplicates in list diff --git a/qiskit/transpiler/synthesis/__init__.py b/qiskit/transpiler/synthesis/__init__.py index 62dcdfc0c74c..b8ac116d2284 100644 --- a/qiskit/transpiler/synthesis/__init__.py +++ b/qiskit/transpiler/synthesis/__init__.py @@ -10,16 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=undefined-all-variable """Module containing transpiler synthesize.""" -import importlib - -__all__ = ["graysynth", "cnot_synth"] - - -def __getattr__(name): - if name in __all__: - return getattr(importlib.import_module(".graysynth", __name__), name) - raise AttributeError(f"module {__name__!r} has no attribute {name!r}") +from .graysynth import graysynth, cnot_synth diff --git a/qiskit/transpiler/synthesis/graysynth.py b/qiskit/transpiler/synthesis/graysynth.py index 249335d27a73..7f0ab1c08ba0 100644 --- a/qiskit/transpiler/synthesis/graysynth.py +++ b/qiskit/transpiler/synthesis/graysynth.py @@ -21,7 +21,8 @@ # Redirect getattrs to modules new location # TODO: Deprecate in 0.24.0 and remove in 0.26.0 -from qiskit.synthesis.linear.graysynth import * +from qiskit.synthesis.linear.cnot_synth import * +from qiskit.synthesis.linear_phase.cnot_phase_synth import * def cnot_synth(state, section_size=2): @@ -52,3 +53,62 @@ def cnot_synth(state, section_size=2): `arXiv:quant-ph/0302002 [quant-ph] `_ """ return synth_cnot_count_full_pmh(state, section_size=section_size) + + +def graysynth(cnots, angles, section_size=2): + """This function is an implementation of the GraySynth algorithm of + Amy, Azimadeh and Mosca. + + GraySynth is a heuristic algorithm from [1] for synthesizing small parity networks. + It is inspired by Gray codes. Given a set of binary strings S + (called "cnots" bellow), the algorithm synthesizes a parity network for S by + repeatedly choosing an index i to expand and then effectively recursing on + the co-factors S_0 and S_1, consisting of the strings y in S, + with y_i = 0 or 1 respectively. As a subset S is recursively expanded, + CNOT gates are applied so that a designated target bit contains the + (partial) parity ksi_y(x) where y_i = 1 if and only if y'_i = 1 for all + y' in S. If S is a singleton {y'}, then y = y', hence the target bit contains + the value ksi_y'(x) as desired. + + Notably, rather than uncomputing this sequence of CNOT gates when a subset S + is finished being synthesized, the algorithm maintains the invariant + that the remaining parities to be computed are expressed over the current state + of bits. This allows the algorithm to avoid the 'backtracking' inherent in + uncomputing-based methods. + + The algorithm is described in detail in section 4 of [1]. + + Args: + cnots (list[list]): a matrix whose columns are the parities to be synthesized + e.g.:: + + [[0, 1, 1, 1, 1, 1], + [1, 0, 0, 1, 1, 1], + [1, 0, 0, 1, 0, 0], + [0, 0, 1, 0, 1, 0]] + + corresponds to:: + + x1^x2 + x0 + x0^x3 + x0^x1^x2 + x0^x1^x3 + x0^x1 + + angles (list): a list containing all the phase-shift gates which are + to be applied, in the same order as in "cnots". A number is + interpreted as the angle of p(angle), otherwise the elements + have to be 't', 'tdg', 's', 'sdg' or 'z'. + + section_size (int): the size of every section, used in _lwr_cnot_synth(), in the + Patel–Markov–Hayes algorithm. section_size must be a factor of num_qubits. + + Returns: + QuantumCircuit: the decomposed quantum circuit. + + Raises: + QiskitError: when dimensions of cnots and angles don't align. + + References: + 1. Matthew Amy, Parsiad Azimzadeh, and Michele Mosca. + *On the controlled-NOT complexity of controlled-NOT–phase circuits.*, + Quantum Science and Technology 4.1 (2018): 015002. + `arXiv:1712.01859 `_ + """ + return synth_cnot_phase_aam(cnots, angles, section_size=section_size) diff --git a/releasenotes/notes/rename-graysynth-3fa4fcb7d096ab35.yaml b/releasenotes/notes/rename-graysynth-3fa4fcb7d096ab35.yaml new file mode 100644 index 000000000000..dce2d2711c3d --- /dev/null +++ b/releasenotes/notes/rename-graysynth-3fa4fcb7d096ab35.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Added a new function :func:`qiskit.synthesis.synth_cnot_phase_aam` + which is used to synthesize cnot phase circuits for all-to-all architectures + using the Amy, Azimzadeh, and Mosca method. This function is identical to + the available ``qiskit.transpiler.synthesis.graysynth()`` + function but has a more descriptive name and is more logically placed + in the package tree. This new function supersedes the legacy function + which will likely be deprecated in a future release. diff --git a/test/python/circuit/library/test_linear_function.py b/test/python/circuit/library/test_linear_function.py index b7ca592ace80..f9b7e5dce21e 100644 --- a/test/python/circuit/library/test_linear_function.py +++ b/test/python/circuit/library/test_linear_function.py @@ -116,7 +116,7 @@ def test_conversion_to_linear_function_and_back(self, num_qubits): def test_patel_markov_hayes(self): """Checks the explicit example from Patel-Markov-Hayes's paper.""" - # This code is adapted from test_gray_synthesis.py + # This code is adapted from test_cnot_phase_synthesis.py binary_matrix = [ [1, 1, 0, 0, 0, 0], [1, 0, 0, 1, 1, 0], diff --git a/test/python/synthesis/test_gray_synthesis.py b/test/python/synthesis/test_cnot_phase_synthesis.py similarity index 96% rename from test/python/synthesis/test_gray_synthesis.py rename to test/python/synthesis/test_cnot_phase_synthesis.py index 3d2ffbb38ccd..725ad7f84967 100644 --- a/test/python/synthesis/test_gray_synthesis.py +++ b/test/python/synthesis/test_cnot_phase_synthesis.py @@ -19,9 +19,12 @@ from qiskit.circuit import QuantumCircuit, QuantumRegister from qiskit.quantum_info.operators import Operator from qiskit.extensions.unitary import UnitaryGate -from qiskit.synthesis.linear import graysynth, synth_cnot_count_full_pmh -from qiskit.transpiler.synthesis import cnot_synth # pylint: disable=no-name-in-module -from qiskit.transpiler.synthesis.graysynth import graysynth as legacy_graysynth +from qiskit.synthesis.linear import synth_cnot_count_full_pmh +from qiskit.synthesis.linear_phase import synth_cnot_phase_aam +from qiskit.transpiler.synthesis.graysynth import ( + cnot_synth, + graysynth, +) from qiskit.test import QiskitTestCase @@ -29,7 +32,7 @@ class TestGraySynth(QiskitTestCase): """Test the Gray-Synth algorithm.""" - @ddt.data(graysynth, legacy_graysynth) + @ddt.data(synth_cnot_phase_aam, graysynth) def test_gray_synth(self, synth_func): """Test synthesis of a small parity network via gray_synth. @@ -101,7 +104,8 @@ def test_gray_synth(self, synth_func): # Check if the two circuits are equivalent self.assertEqual(unitary_gray, unitary_compare) - def test_paper_example(self): + @ddt.data(synth_cnot_phase_aam, graysynth) + def test_paper_example(self, synth_func): """Test synthesis of a diagonal operator from the paper. The diagonal operator in Example 4.2 @@ -129,7 +133,7 @@ def test_paper_example(self): """ cnots = [[0, 1, 1, 1, 1, 1], [1, 0, 0, 1, 1, 1], [1, 0, 0, 1, 0, 0], [0, 0, 1, 0, 1, 0]] angles = ["t"] * 6 - c_gray = graysynth(cnots, angles) + c_gray = synth_func(cnots, angles) unitary_gray = UnitaryGate(Operator(c_gray)) # Create the circuit displayed above: @@ -155,7 +159,8 @@ def test_paper_example(self): # Check if the two circuits are equivalent self.assertEqual(unitary_gray, unitary_compare) - def test_ccz(self): + @ddt.data(synth_cnot_phase_aam, graysynth) + def test_ccz(self, synth_func): """Test synthesis of the doubly-controlled Z gate. The diagonal operator in Example 4.3 @@ -181,7 +186,7 @@ def test_ccz(self): """ cnots = [[1, 0, 0, 1, 1, 0, 1], [0, 1, 0, 1, 0, 1, 1], [0, 0, 1, 0, 1, 1, 1]] angles = ["t", "t", "t", "tdg", "tdg", "tdg", "t"] - c_gray = graysynth(cnots, angles) + c_gray = synth_func(cnots, angles) unitary_gray = UnitaryGate(Operator(c_gray)) # Create the circuit displayed above: @@ -211,7 +216,7 @@ class TestPatelMarkovHayes(QiskitTestCase): """Test the Patel-Markov-Hayes algorithm for synthesizing linear CNOT-only circuits.""" - @ddt.data(cnot_synth, synth_cnot_count_full_pmh) + @ddt.data(synth_cnot_count_full_pmh, cnot_synth) def test_patel_markov_hayes(self, synth_func): """Test synthesis of a small linear circuit (example from paper, Figure 3). From 639bd58fcbd0ef9505be61ab1c8e4ce95fba6d85 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 17 Apr 2023 20:59:38 +0100 Subject: [PATCH 040/172] Refactor OpenQASM 2 exporter (#9953) * Refactor OpenQASM 2 exporter This rewrites the handling of instructions in the OpenQASM 2 exporter in several places. First, the definition and the call site are handled at the same time. Previously, these were handled at separate points and required two name munging steps, which mostly just needed to hope that they both converged on the same name. Instead, one singular point handles both, so there are no issues with name mismatches. In a similar vein, this now ensures that the two places will not disagree about when a definition has already been seen before; previously, duplicate definitions could be "used" but not defined. Second, the gate-definition pass is now made properly recursive, so the exact same code is responsible for defining gates that appear in the body of the main circuit, and of custom definitions, including sharing the duplication detection. Previously, two custom definitions that both used the same gate could cause duplicate definitions to appear with incorrect names. Unifying this handling also has the natural consequence of correctly ordering _all_ gate definitions, whereas before some inner gates could be defined after their first use. Inside the call-site/definition-site handling, this refactor separates those into two separate objects. This massively simplified: - handling known-good Qiskit standard gates to have special-cased support for always exporting properly parametrised definitions; - handling `UnitaryGate` and `Permutation` specially, so they no longer need to output special OQ2 strings, but can just define a custom object to use as the source for both call-site and definition site; - handling gates with no definitions as `opaque`. * Style tweak Co-authored-by: Luciano Bello --------- Co-authored-by: Luciano Bello --- .../library/generalized_gates/permutation.py | 29 +- qiskit/circuit/quantumcircuit.py | 275 ++++++++---------- qiskit/extensions/unitary.py | 44 +-- ...sm2-exporter-rewrite-8993dd24f930b180.yaml | 34 +++ test/python/circuit/test_circuit_qasm.py | 166 ++++++++++- test/python/circuit/test_unitary.py | 40 +-- 6 files changed, 346 insertions(+), 242 deletions(-) create mode 100644 releasenotes/notes/qasm2-exporter-rewrite-8993dd24f930b180.yaml diff --git a/qiskit/circuit/library/generalized_gates/permutation.py b/qiskit/circuit/library/generalized_gates/permutation.py index e386317c7ebc..44576518d624 100644 --- a/qiskit/circuit/library/generalized_gates/permutation.py +++ b/qiskit/circuit/library/generalized_gates/permutation.py @@ -145,10 +145,6 @@ def __init__( ) pattern = np.array(pattern) - # This is needed to support qasm() - self._qasm_name = "permutation__" + "_".join([str(n) for n in pattern]) + "_" - self._qasm_definition = None - super().__init__(name="permutation", num_qubits=num_qubits, params=[pattern]) def __array__(self, dtype=None): @@ -183,21 +179,12 @@ def inverse(self): return PermutationGate(pattern=_inverse_pattern(self.pattern)) - def qasm(self): - """The qasm for a permutation.""" - - if not self._qasm_definition: - - # pylint: disable=cyclic-import - from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap - - # This qasm should be identical to the one produced when permutation - # was a circuit rather than a gate. - swaps = _get_ordered_swap(self.pattern) - gates_def = "".join(["swap q" + str(i) + ",q" + str(j) + "; " for i, j in swaps]) - qubit_list = ",".join(["q" + str(n) for n in range(len(self.pattern))]) - self._qasm_definition = ( - "gate " + self._qasm_name + " " + qubit_list + " { " + gates_def + "}" - ) + def _qasm2_decomposition(self): + # pylint: disable=cyclic-import + from qiskit.synthesis.permutation.permutation_utils import _get_ordered_swap - return self._qasmif(self._qasm_name) + 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) + return out.to_gate() diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index d248fbc81d6e..cd8062a7d34f 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1620,7 +1620,7 @@ def qasm( if self.num_parameters > 0: raise QasmError("Cannot represent circuits with unbound parameters in OpenQASM 2.") - existing_gate_names = [ + existing_gate_names = { "barrier", "measure", "reset", @@ -1666,12 +1666,11 @@ def qasm( "c3x", "c3sx", # This is the Qiskit gate name, but the qelib1.inc name is 'c3sqrtx'. "c4x", - ] - - existing_composite_circuits: list[Instruction] = [] + } - string_temp = self.header + "\n" - string_temp += self.extension_lib + "\n" + # Mapping of instruction name to a pair of the source for a definition, and an OQ2 string + # that includes the `gate` or `opaque` statement that defines the gate. + gates_to_define: OrderedDict[str, tuple[Instruction, str]] = OrderedDict() regless_qubits = [bit for bit in self.qubits if not self.find_bit(bit).registers] regless_clbits = [bit for bit in self.clbits if not self.find_bit(bit).registers] @@ -1691,72 +1690,51 @@ def qasm( for name, register in register_escaped_names.items() for (idx, bit) in enumerate(register) } - for name, reg in register_escaped_names.items(): - string_temp += ( - f"{'qreg' if isinstance(reg, QuantumRegister) else 'creg'} {name}[{reg.size}];\n" - ) - + register_definitions_qasm = "".join( + f"{'qreg' if isinstance(reg, QuantumRegister) else 'creg'} {name}[{reg.size}];\n" + for name, reg in register_escaped_names.items() + ) + instruction_calls = [] for instruction in self._data: operation = instruction.operation if operation.name == "measure": qubit = instruction.qubits[0] clbit = instruction.clbits[0] - string_temp += "{} {} -> {};\n".format( - operation.qasm(), - bit_labels[qubit], - bit_labels[clbit], - ) + instruction_qasm = f"measure {bit_labels[qubit]} -> {bit_labels[clbit]};" elif operation.name == "reset": - string_temp += f"reset {bit_labels[instruction.qubits[0]]};\n" + instruction_qasm = f"reset {bit_labels[instruction.qubits[0]]};" elif operation.name == "barrier": qargs = ",".join(bit_labels[q] for q in instruction.qubits) - string_temp += "barrier;\n" if not qargs else f"barrier {qargs};\n" + instruction_qasm = "barrier;" if not qargs else f"barrier {qargs};" else: - # Check instructions names or label are valid - escaped = _qasm_escape_name(operation.name, "gate_") - if escaped != operation.name: - operation = operation.copy(name=escaped) - - # decompose gate using definitions if they are not defined in OpenQASM2 - if operation.name not in existing_gate_names: - op_qasm_name = None - if operation.name == "permutation": - op_qasm_name = getattr(operation, "_qasm_name", None) - if op_qasm_name: - operation = operation.copy(name=op_qasm_name) - - if operation not in existing_composite_circuits: - if operation.name in [ - operation.name for operation in existing_composite_circuits - ]: - # append operation id to name of operation copy to make it unique - operation = operation.copy(name=f"{operation.name}_{id(operation)}") - - existing_composite_circuits.append(operation) - - # Strictly speaking, the code below does not work for operations that - # do not have the "definition" method but require a complex (recursive) - # "_qasm_definition". Fortunately, right now we do not have any such operations. - if getattr(operation, "definition", None) is not None: - _add_sub_instruction_to_existing_composite_circuits( - operation, existing_gate_names, existing_composite_circuits - ) + operation = _qasm2_define_custom_operation( + operation, existing_gate_names, gates_to_define + ) # Insert qasm representation of the original instruction - string_temp += "{} {};\n".format( - operation.qasm(), - ",".join([bit_labels[j] for j in instruction.qubits + instruction.clbits]), + bits_qasm = ",".join( + bit_labels[j] for j in itertools.chain(instruction.qubits, instruction.clbits) ) - - # insert gate definitions - string_temp = _insert_composite_gate_definition_qasm( - string_temp, existing_composite_circuits, self.extension_lib + instruction_qasm = f"{operation.qasm()} {bits_qasm};" + instruction_calls.append(instruction_qasm) + instructions_qasm = "".join(f"{call}\n" for call in instruction_calls) + gate_definitions_qasm = "".join(f"{qasm}\n" for _, qasm in gates_to_define.values()) + + out = "".join( + ( + self.header, + "\n", + self.extension_lib, + "\n", + gate_definitions_qasm, + register_definitions_qasm, + instructions_qasm, + ) ) if filename: with open(filename, "w+", encoding=encoding) as file: - file.write(string_temp) - file.close() + file.write(out) if formatted: if not HAS_PYGMENTS: @@ -1766,12 +1744,11 @@ def qasm( pip_install="pip install pygments", ) code = pygments.highlight( - string_temp, OpenQASMLexer(), Terminal256Formatter(style=QasmTerminalStyle) + out, OpenQASMLexer(), Terminal256Formatter(style=QasmTerminalStyle) ) print(code) return None - else: - return string_temp + return out def draw( self, @@ -4948,28 +4925,100 @@ def _compare_parameters(param1: Parameter, param2: Parameter) -> int: return _standard_compare(param1.name, param2.name) -def _add_sub_instruction_to_existing_composite_circuits( - instruction: Instruction, - existing_gate_names: list[str], - existing_composite_circuits: list[Instruction], -) -> None: - """Recursively add undefined sub-instructions in the definition of the given - instruction to existing_composite_circuit list. - """ - for sub_instruction in instruction.definition: - sub_operation = sub_instruction.operation - # Check instructions names are valid - escaped = _qasm_escape_name(sub_operation.name, "gate_") - if escaped != sub_operation.name: - sub_operation = sub_operation.copy(name=escaped) - if ( - sub_operation.name not in existing_gate_names - and sub_operation not in existing_composite_circuits - ): - existing_composite_circuits.insert(0, sub_operation) - _add_sub_instruction_to_existing_composite_circuits( - sub_operation, existing_gate_names, existing_composite_circuits +# Used by the OQ2 exporter. Just needs to have enough parameters to support the largest standard +# (non-controlled) gate in our standard library. We have to use the same `Parameter` instances each +# time so the equality comparisons will work. +_QASM2_FIXED_PARAMETERS = [Parameter("param0"), Parameter("param1"), Parameter("param2")] + + +def _qasm2_define_custom_operation(operation, existing_gate_names, gates_to_define): + """Extract a custom definition from the given operation, and append any necessary additional + subcomponents' definitions to the ``gates_to_define`` ordered dictionary. + + Returns a potentially new :class:`.Instruction`, which should be used for the + :meth:`~.Instruction.qasm` call (it may have been renamed).""" + from qiskit.circuit import library as lib # pylint: disable=cyclic-import + + if operation.name in existing_gate_names: + return operation + + # Check instructions names or label are valid + escaped = _qasm_escape_name(operation.name, "gate_") + if escaped != operation.name: + operation = operation.copy(name=escaped) + + # These are built-in gates that are known to be safe to construct by passing the correct number + # of `Parameter` instances positionally, and have no other information. We can't guarantee that + # if they've been subclassed, though. This is a total hack; ideally we'd be able to inspect the + # "calling" signatures of Qiskit `Gate` objects to know whether they're safe to re-parameterise. + known_good_parameterized = { + lib.PhaseGate, + lib.RGate, + lib.RXGate, + lib.RXXGate, + lib.RYGate, + lib.RYYGate, + lib.RZGate, + lib.RZXGate, + lib.RZZGate, + lib.XXMinusYYGate, + lib.XXPlusYYGate, + lib.UGate, + lib.U1Gate, + lib.U2Gate, + lib.U3Gate, + } + + # In known-good situations we want to use a manually parametrised object as the source of the + # definition, but still continue to return the given object as the call-site object. + if type(operation) in known_good_parameterized: + parameterized_operation = type(operation)(*_QASM2_FIXED_PARAMETERS[: len(operation.params)]) + elif hasattr(operation, "_qasm2_decomposition"): + new_op = operation._qasm2_decomposition() + parameterized_operation = operation = new_op.copy( + name=_qasm_escape_name(new_op.name, "gate_") + ) + else: + parameterized_operation = operation + + # If there's an _equal_ operation in the existing circuits to be defined, then our job is done. + previous_definition_source, _ = gates_to_define.get(operation.name, (None, None)) + if parameterized_operation == previous_definition_source: + return operation + + # Otherwise, if there's a naming clash, we need a unique name. + if operation.name in gates_to_define: + new_name = f"{operation.name}_{id(operation)}" + operation = operation.copy(name=new_name) + else: + new_name = operation.name + + if parameterized_operation.params: + parameters_qasm = ( + "(" + ",".join(f"param{i}" for i in range(len(parameterized_operation.params))) + ")" + ) + else: + parameters_qasm = "" + qubits_qasm = ",".join(f"q{i}" for i in range(parameterized_operation.num_qubits)) + parameterized_definition = getattr(parameterized_operation, "definition", None) + if parameterized_definition is None: + gates_to_define[new_name] = ( + parameterized_operation, + f"opaque {new_name}{parameters_qasm} {qubits_qasm};", + ) + else: + statements = [] + qubit_labels = {bit: f"q{i}" for i, bit in enumerate(parameterized_definition.qubits)} + for instruction in parameterized_definition.data: + new_operation = _qasm2_define_custom_operation( + instruction.operation, existing_gate_names, gates_to_define ) + bits_qasm = ",".join(qubit_labels[q] for q in instruction.qubits) + statements.append(f"{new_operation.qasm()} {bits_qasm};") + body_qasm = " ".join(statements) + definition_qasm = f"gate {new_name}{parameters_qasm} {qubits_qasm} {{ {body_qasm} }}" + gates_to_define[new_name] = (parameterized_operation, definition_qasm) + return operation def _qasm_escape_name(name: str, prefix: str) -> str: @@ -5002,72 +5051,6 @@ def _make_unique(name: str, already_defined: collections.abc.Set[str]) -> str: return name -def _get_composite_circuit_qasm_from_instruction(instruction: Instruction) -> str: - """Returns OpenQASM string composite circuit given an instruction. - The given instruction should be the result of composite_circuit.to_instruction().""" - - if instruction.definition is None: - raise ValueError(f'Instruction "{instruction.name}" is not defined.') - - gate_parameters = ",".join(["param%i" % num for num in range(len(instruction.params))]) - qubit_parameters = ",".join(["q%i" % num for num in range(instruction.num_qubits)]) - composite_circuit_gates = "" - - definition = instruction.definition - definition_bit_labels = { - bit: idx for bits in (definition.qubits, definition.clbits) for idx, bit in enumerate(bits) - } - for sub_instruction in definition: - sub_operation = sub_instruction.operation - escaped = _qasm_escape_name(sub_operation.name, "gate_") - if escaped != sub_operation.name: - sub_operation = sub_operation.copy(name=escaped) - - gate_qargs = ",".join( - "q%i" % index - for index in [definition_bit_labels[qubit] for qubit in sub_instruction.qubits] - ) - composite_circuit_gates += f"{sub_operation.qasm()} {gate_qargs}; " - - if composite_circuit_gates: - composite_circuit_gates = composite_circuit_gates.rstrip(" ") - - if gate_parameters: - qasm_string = "gate {}({}) {} {{ {} }}".format( - instruction.name, - gate_parameters, - qubit_parameters, - composite_circuit_gates, - ) - else: - qasm_string = "gate {} {} {{ {} }}".format( - instruction.name, - qubit_parameters, - composite_circuit_gates, - ) - - return qasm_string - - -def _insert_composite_gate_definition_qasm( - string_temp: str, existing_composite_circuits: list[Instruction], extension_lib: str -) -> str: - """Insert composite gate definition QASM code right after extension library in the header""" - - gate_definition_string = "" - - # Generate gate definition string - for instruction in existing_composite_circuits: - if hasattr(instruction, "_qasm_definition"): - qasm_string = instruction._qasm_definition - else: - qasm_string = _get_composite_circuit_qasm_from_instruction(instruction) - gate_definition_string += "\n" + qasm_string - - string_temp = string_temp.replace(extension_lib, f"{extension_lib}{gate_definition_string}") - return string_temp - - def _bit_argument_conversion(specifier, bit_sequence, bit_set, type_) -> list[Bit]: """Get the list of bits referred to by the specifier ``specifier``. diff --git a/qiskit/extensions/unitary.py b/qiskit/extensions/unitary.py index 859ce2ef7869..cfcdcbf83adc 100644 --- a/qiskit/extensions/unitary.py +++ b/qiskit/extensions/unitary.py @@ -21,7 +21,6 @@ from qiskit.circuit import QuantumRegister, Qubit from qiskit.circuit.exceptions import CircuitError from qiskit.circuit._utils import _compute_control_matrix -from qiskit.circuit.quantumcircuit import _qasm_escape_name from qiskit.circuit.library.standard_gates import U3Gate from qiskit.extensions.quantum_initializer import isometry from qiskit.quantum_info.operators.predicates import matrix_equal @@ -87,8 +86,6 @@ def __init__(self, data, label=None): if input_dim != output_dim or 2**num_qubits != input_dim: raise ExtensionError("Input matrix is not an N-qubit operator.") - self._qasm_name = None - self._qasm_definition = None # Store instruction params super().__init__("unitary", num_qubits, [data], label=label) @@ -170,41 +167,12 @@ def control(self, num_ctrl_qubits=1, label=None, ctrl_state=None): base_gate=self.copy(), ) - def qasm(self): - """The qasm for a custom unitary gate - This is achieved by adding a custom gate that corresponds to the definition - of this gate. It gives the gate a random name if one hasn't been given to it. - """ - - # give this unitary a name - self._qasm_name = ( - _qasm_escape_name(self.label, "gate_") if self.label else "unitary" + str(id(self)) - ) - - qubit_to_qasm = {bit: f"p{i}" for i, bit in enumerate(self.definition.qubits)} - gates_def = "" - for instruction in self.definition.data: - - curr_gate = "\t{} {};\n".format( - instruction.operation.qasm(), - ",".join(qubit_to_qasm[qubit] for qubit in instruction.qubits), - ) - gates_def += curr_gate - - # name of gate + params + {definition} - overall = ( - "gate " - + self._qasm_name - + " " - + ",".join(qubit_to_qasm[qubit] for qubit in self.definition.qubits) - + " {\n" - + gates_def - + "}" - ) - - self._qasm_definition = overall - - return self._qasmif(self._qasm_name) + def _qasm2_decomposition(self): + """Return an unparameterized version of ourselves, so the OQ2 exporter doesn't choke on the + non-standard things in our `params` field.""" + out = self.definition.to_gate() + out.name = self.name + return out def validate_parameter(self, parameter): """Unitary gate parameter has to be an ndarray.""" diff --git a/releasenotes/notes/qasm2-exporter-rewrite-8993dd24f930b180.yaml b/releasenotes/notes/qasm2-exporter-rewrite-8993dd24f930b180.yaml new file mode 100644 index 000000000000..a6573de88e5b --- /dev/null +++ b/releasenotes/notes/qasm2-exporter-rewrite-8993dd24f930b180.yaml @@ -0,0 +1,34 @@ +--- +fixes: + - | + The OpenQASM 2 exporter (:meth:`.QuantumCircuit.qasm`) will no longer emit duplicate definitions + for gates that appear in other gates' definitions. See + `#7771 `__, + `#8086 `__, + `#8402 `__, + `#8558 `__, and + `#9805 `__. + - | + The OpenQASM 2 exporter (:meth:`.QuantumCircuit.qasm`) will now handle multiple and nested + definitions of :class:`.UnitaryGate`. See + `#4623 `__, + `#6712 `__, + `#7772 `__, and + `#8222 `__. + - | + The OpenQASM 2 exporter (:meth:`.QuantumCircuit.qasm`) will now output definitions for gates used + only in other gates' definitions in a correct order. See + `#7769 `__ and + `#7773 `__. + - | + Standard gates defined by Qiskit, such as :class:`.RZXGate`, will now have properly parametrised + definitions when exported using the OpenQASM 2 exporter (:meth:`.QuantumCircuit.qasm`). See + `#7172 `__. + - | + Quantum volume circuits (:class:`.QuantumVolume`) are now supported by the OpenQASM 2 exporter + (:meth:`.QuantumCircuit.qasm`). See + `#6466 `__ and + `#7051 `__. + - | + The OpenQASM 2 exporter will now output gates with no known definition with ``opaque`` statements, + rather than failing. See `#5036 `__. diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index c7685dbd6a79..1d83ab8a024b 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -19,7 +19,7 @@ from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.test import QiskitTestCase from qiskit.circuit import Parameter, Qubit, Clbit, Gate -from qiskit.circuit.library import C3SXGate, CCZGate, CSGate, CSdgGate, RZXGate +from qiskit.circuit.library import C3SXGate, CCZGate, CSGate, CSdgGate, PermutationGate from qiskit.qasm.exceptions import QasmError # Regex pattern to match valid OpenQASM identifiers @@ -308,16 +308,73 @@ def test_csdggate_qasm(self): def test_rzxgate_qasm(self): """Test that RZX dumps definition as a non-qelib1 gate.""" qc = QuantumCircuit(2) - qc.append(RZXGate(0), qc.qubits, []) + qc.rzx(0, 0, 1) + qc.rzx(pi / 2, 1, 0) qasm = qc.qasm() expected = """OPENQASM 2.0; include "qelib1.inc"; -gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(0) q1; cx q0,q1; h q1; } +gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(param0) q1; cx q0,q1; h q1; } qreg q[2]; rzx(0) q[0],q[1]; +rzx(pi/2) q[1],q[0]; """ self.assertEqual(qasm, expected) + def test_ecrgate_qasm(self): + """Test that ECR dumps its definition as a non-qelib1 gate.""" + qc = QuantumCircuit(2) + qc.ecr(0, 1) + qc.ecr(1, 0) + qasm = qc.qasm() + expected = """OPENQASM 2.0; +include "qelib1.inc"; +gate rzx(param0) q0,q1 { h q1; cx q0,q1; rz(param0) q1; cx q0,q1; h q1; } +gate ecr q0,q1 { rzx(pi/4) q0,q1; x q0; rzx(-pi/4) q0,q1; } +qreg q[2]; +ecr q[0],q[1]; +ecr q[1],q[0]; +""" + self.assertEqual(qasm, expected) + + def test_unitary_qasm(self): + """Test that UnitaryGate can be dumped to OQ2 correctly.""" + qc = QuantumCircuit(1) + qc.unitary([[1, 0], [0, 1]], 0) + qasm = qc.qasm() + expected = """OPENQASM 2.0; +include "qelib1.inc"; +gate unitary q0 { u3(0,0,0) q0; } +qreg q[1]; +unitary q[0]; +""" + self.assertEqual(qasm, expected) + + def test_multiple_unitary_qasm(self): + """Test that multiple UnitaryGate instances can all dump successfully.""" + custom = QuantumCircuit(1, name="custom") + custom.unitary([[1, 0], [0, -1]], 0) + + qc = QuantumCircuit(2) + qc.unitary([[1, 0], [0, 1]], 0) + qc.unitary([[0, 1], [1, 0]], 1) + qc.append(custom.to_gate(), [0], []) + qasm = qc.qasm() + expected = re.compile( + r"""OPENQASM 2.0; +include "qelib1.inc"; +gate unitary q0 { u3\(0,0,0\) q0; } +gate (?Punitary_[0-9]*) q0 { u3\(pi,-pi/2,pi/2\) q0; } +gate (?Punitary_[0-9]*) q0 { u3\(0,pi/2,pi/2\) q0; } +gate custom q0 { (?P=u2) q0; } +qreg q\[2\]; +unitary q\[0\]; +(?P=u1) q\[1\]; +custom q\[0\]; +""", + re.MULTILINE, + ) + self.assertRegex(qasm, expected) + def test_unbound_circuit_raises(self): """Test circuits with unbound parameters raises.""" qc = QuantumCircuit(1) @@ -365,9 +422,9 @@ def test_circuit_qasm_with_mcx_gate_variants(self): # param0 for "gate mcuq(param0) is not used inside the definition expected_qasm = """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; } gate mcu1(param0) q0,q1,q2,q3,q4,q5 { cu1(pi/16) q4,q5; cx q4,q3; cu1(-pi/16) q3,q5; cx q4,q3; cu1(pi/16) q3,q5; cx q3,q2; cu1(-pi/16) q2,q5; cx q4,q2; cu1(pi/16) q2,q5; cx q3,q2; cu1(-pi/16) q2,q5; cx q4,q2; cu1(pi/16) q2,q5; cx q2,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q3,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q2,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q3,q1; cu1(-pi/16) q1,q5; cx q4,q1; cu1(pi/16) q1,q5; cx q1,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q2,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q1,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q2,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; cx q3,q0; cu1(-pi/16) q0,q5; cx q4,q0; cu1(pi/16) q0,q5; } gate mcx_gray q0,q1,q2,q3,q4,q5 { h q5; mcu1(pi) q0,q1,q2,q3,q4,q5; h q5; } +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 mcx_recursive q0,q1,q2,q3,q4,q5,q6 { mcx q0,q1,q2,q6; mcx q3,q4,q6,q5; mcx q0,q1,q2,q6; mcx q3,q4,q6,q5; } gate mcx_vchain q0,q1,q2,q3,q4,q5,q6,q7,q8 { rccx q0,q1,q6; rccx q2,q6,q7; rccx q3,q7,q8; ccx q4,q8,q5; rccx q3,q7,q8; rccx q2,q6,q7; rccx q0,q1,q6; } qreg q[9]; @@ -463,9 +520,6 @@ def test_circuit_qasm_with_invalid_identifiers(self): gate2 = custom2.to_gate() gate2.name = "invalid[name]" - # Unitary gate, for which qasm string is produced by internal method - qc.unitary([[0, 1], [1, 0]], 0, label="[valid?]") - # Append gates qc.append(gate, [0]) qc.append(gate2, [1, 0]) @@ -475,13 +529,9 @@ def test_circuit_qasm_with_invalid_identifiers(self): [ "OPENQASM 2.0;", 'include "qelib1.inc";', - "gate gate__valid__ p0 {", - " u3(pi,-pi/2,pi/2) p0;", - "}", "gate gate_A___ q0 { x q0; u(0,0,pi) q0; }", "gate invalid_name_ q0,q1 { x q0; gate_A___ q1; }", "qreg q[2];", - "gate__valid__ q[0];", "gate_A___ q[0];", "invalid_name_ q[1],q[0];", "", @@ -492,7 +542,7 @@ def test_circuit_qasm_with_invalid_identifiers(self): self.assertEqual(expected_qasm, qc.qasm()) # Check instruction names were not changed by qasm() - names = ["unitary", "A[$]", "invalid[name]"] + names = ["A[$]", "invalid[name]"] for idx, instruction in enumerate(qc._data): self.assertEqual(instruction.operation.name, names[idx]) @@ -604,7 +654,6 @@ def test_circuit_raises_on_single_bit_condition(self): def test_circuit_qasm_with_permutations(self): """Test circuit qasm() method with Permutation gates.""" - from qiskit.circuit.library import PermutationGate qc = QuantumCircuit(4) qc.append(PermutationGate([2, 1, 0]), [0, 1, 2]) @@ -616,6 +665,30 @@ def test_circuit_qasm_with_permutations(self): permutation__2_1_0_ q[0],q[1],q[2];\n""" self.assertEqual(qc.qasm(), expected_qasm) + def test_multiple_permutation(self): + """Test that multiple PermutationGates can be added to a circuit.""" + custom = QuantumCircuit(3, name="custom") + custom.append(PermutationGate([2, 1, 0]), [0, 1, 2]) + custom.append(PermutationGate([0, 1, 2]), [0, 1, 2]) + + qc = QuantumCircuit(4) + qc.append(PermutationGate([2, 1, 0]), [0, 1, 2], []) + qc.append(PermutationGate([1, 2, 0]), [0, 1, 2], []) + qc.append(custom.to_gate(), [1, 3, 2], []) + qasm = qc.qasm() + expected = """OPENQASM 2.0; +include "qelib1.inc"; +gate permutation__2_1_0_ q0,q1,q2 { swap q0,q2; } +gate permutation__1_2_0_ q0,q1,q2 { swap q1,q2; swap q0,q2; } +gate permutation__0_1_2_ q0,q1,q2 { } +gate custom q0,q1,q2 { permutation__2_1_0_ q0,q1,q2; permutation__0_1_2_ q0,q1,q2; } +qreg q[4]; +permutation__2_1_0_ q[0],q[1],q[2]; +permutation__1_2_0_ q[0],q[1],q[2]; +custom q[1],q[3],q[2]; +""" + self.assertEqual(qasm, expected) + def test_circuit_qasm_with_reset(self): """Test circuit qasm() method with Reset.""" qc = QuantumCircuit(2) @@ -628,6 +701,73 @@ def test_circuit_qasm_with_reset(self): reset q[1];\n""" self.assertEqual(qc.qasm(), expected_qasm) + def test_nested_gate_naming_clashes(self): + """Test that gates that have naming clashes but only appear in the body of another gate + still get exported correctly.""" + + # pylint: disable=missing-class-docstring + + class Inner(Gate): + def __init__(self, param): + super().__init__("inner", 1, [param]) + + def _define(self): + self._definition = QuantumCircuit(1) + self._definition.rx(self.params[0], 0) + + class Outer(Gate): + def __init__(self, param): + super().__init__("outer", 1, [param]) + + def _define(self): + self._definition = QuantumCircuit(1) + self._definition.append(Inner(self.params[0]), [0], []) + + qc = QuantumCircuit(1) + qc.append(Outer(1.0), [0], []) + qc.append(Outer(2.0), [0], []) + qasm = qc.qasm() + + expected = re.compile( + r"""OPENQASM 2\.0; +include "qelib1\.inc"; +gate inner\(param0\) q0 { rx\(1\.0\) q0; } +gate outer\(param0\) q0 { inner\(1\.0\) q0; } +gate (?Pinner_[0-9]*)\(param0\) q0 { rx\(2\.0\) q0; } +gate (?Pouter_[0-9]*)\(param0\) q0 { (?P=inner1)\(2\.0\) q0; } +qreg q\[1\]; +outer\(1\.0\) q\[0\]; +(?P=outer1)\(2\.0\) q\[0\]; +""", + re.MULTILINE, + ) + self.assertRegex(qasm, expected) + + def test_opaque_output(self): + """Test that gates with no definition are exported as `opaque`.""" + custom = QuantumCircuit(1, name="custom") + custom.append(Gate("my_c", 1, []), [0]) + + qc = QuantumCircuit(2) + qc.append(Gate("my_a", 1, []), [0]) + qc.append(Gate("my_a", 1, []), [1]) + qc.append(Gate("my_b", 2, [1.0]), [1, 0]) + qc.append(custom.to_gate(), [0], []) + qasm = qc.qasm() + expected = """OPENQASM 2.0; +include "qelib1.inc"; +opaque my_a q0; +opaque my_b(param0) q0,q1; +opaque my_c q0; +gate custom q0 { my_c q0; } +qreg q[2]; +my_a q[0]; +my_a q[1]; +my_b(1.0) q[1],q[0]; +custom q[0]; +""" + self.assertEqual(qasm, expected) + if __name__ == "__main__": unittest.main() diff --git a/test/python/circuit/test_unitary.py b/test/python/circuit/test_unitary.py index 4af224fe9b37..b234dc7f7a4f 100644 --- a/test/python/circuit/test_unitary.py +++ b/test/python/circuit/test_unitary.py @@ -208,7 +208,7 @@ def test_qasm_unitary_only_one_def(self): cr = ClassicalRegister(1, "c0") qc = QuantumCircuit(qr, cr) matrix = numpy.array([[1, 0], [0, 1]]) - unitary_gate = UnitaryGate(matrix, label="custom_gate") + unitary_gate = UnitaryGate(matrix) qc.x(qr[0]) qc.append(unitary_gate, [qr[0]]) @@ -217,13 +217,11 @@ def test_qasm_unitary_only_one_def(self): expected_qasm = ( "OPENQASM 2.0;\n" 'include "qelib1.inc";\n' - "gate custom_gate p0 {\n" - "\tu3(0,0,0) p0;\n" - "}\n" + "gate unitary q0 { u3(0,0,0) q0; }\n" "qreg q0[2];\ncreg c0[1];\n" "x q0[0];\n" - "custom_gate q0[0];\n" - "custom_gate q0[1];\n" + "unitary q0[0];\n" + "unitary q0[1];\n" ) self.assertEqual(expected_qasm, qc.qasm()) @@ -234,7 +232,7 @@ def test_qasm_unitary_twice(self): cr = ClassicalRegister(1, "c0") qc = QuantumCircuit(qr, cr) matrix = numpy.array([[1, 0], [0, 1]]) - unitary_gate = UnitaryGate(matrix, label="custom_gate") + unitary_gate = UnitaryGate(matrix) qc.x(qr[0]) qc.append(unitary_gate, [qr[0]]) @@ -243,13 +241,11 @@ def test_qasm_unitary_twice(self): expected_qasm = ( "OPENQASM 2.0;\n" 'include "qelib1.inc";\n' - "gate custom_gate p0 {\n" - "\tu3(0,0,0) p0;\n" - "}\n" + "gate unitary q0 { u3(0,0,0) q0; }\n" "qreg q0[2];\ncreg c0[1];\n" "x q0[0];\n" - "custom_gate q0[0];\n" - "custom_gate q0[1];\n" + "unitary q0[0];\n" + "unitary q0[1];\n" ) self.assertEqual(expected_qasm, qc.qasm()) self.assertEqual(expected_qasm, qc.qasm()) @@ -260,7 +256,7 @@ def test_qasm_2q_unitary(self): cr = ClassicalRegister(1, "c0") qc = QuantumCircuit(qr, cr) matrix = numpy.asarray([[0, 0, 0, 1], [0, 0, 1, 0], [0, 1, 0, 0], [1, 0, 0, 0]]) - unitary_gate = UnitaryGate(matrix, label="custom_gate") + unitary_gate = UnitaryGate(matrix) qc.x(qr[0]) qc.append(unitary_gate, [qr[0], qr[1]]) @@ -269,29 +265,25 @@ def test_qasm_2q_unitary(self): expected_qasm = ( "OPENQASM 2.0;\n" 'include "qelib1.inc";\n' - "gate custom_gate p0,p1 {\n" - "\tu(pi,-pi/2,pi/2) p0;\n" - "\tu(pi,pi/2,-pi/2) p1;\n" - "}\n" + "gate unitary q0,q1 { u(pi,-pi/2,pi/2) q0; u(pi,pi/2,-pi/2) q1; }\n" "qreg q0[2];\n" "creg c0[1];\n" "x q0[0];\n" - "custom_gate q0[0],q0[1];\n" - "custom_gate q0[1],q0[0];\n" + "unitary q0[0],q0[1];\n" + "unitary q0[1],q0[0];\n" ) self.assertEqual(expected_qasm, qc.qasm()) def test_qasm_unitary_noop(self): - """Test that an identifier unitary can be converted to OpenQASM 2""" + """Test that an identity unitary can be converted to OpenQASM 2""" qc = QuantumCircuit(QuantumRegister(3, "q0")) - qc.unitary(numpy.eye(8), qc.qubits, label="unitary_identity") + qc.unitary(numpy.eye(8), qc.qubits) expected_qasm = ( "OPENQASM 2.0;\n" 'include "qelib1.inc";\n' - "gate unitary_identity p0,p1,p2 {\n" - "}\n" + "gate unitary q0,q1,q2 { }\n" "qreg q0[3];\n" - "unitary_identity q0[0],q0[1],q0[2];\n" + "unitary q0[0],q0[1],q0[2];\n" ) self.assertEqual(expected_qasm, qc.qasm()) From 212b1b5a59c12b1027f87bf6e3553b5d01ff584b Mon Sep 17 00:00:00 2001 From: ewinston Date: Mon, 17 Apr 2023 16:35:53 -0400 Subject: [PATCH 041/172] add function to swap connected nodes in DAGCircuit (#9160) * add function to swap connected nodes in DAGCircuit * linting. remove multiplexor test. The multiplexor partial swap wasn't passing. Not sure this is rerlated to this pr. * Update qiskit/dagcircuit/dagcircuit.py Co-authored-by: Jake Lishman * build dagcircuit directly in tests * add spectator ops to swap tests * add release notes * remove dag visualization changes from this pr --------- Co-authored-by: Jake Lishman --- qiskit/dagcircuit/dagcircuit.py | 29 +++++ ...o-dagcircuit-methods-2964426f02251fc4.yaml | 7 ++ test/python/dagcircuit/test_dagcircuit.py | 116 ++++++++++++++++++ 3 files changed, 152 insertions(+) create mode 100644 releasenotes/notes/add-swap_nodes-to-dagcircuit-methods-2964426f02251fc4.yaml diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index ed3dfc1fd618..3f3146299922 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -1393,6 +1393,35 @@ def substitute_node(self, node, op, inplace=False): self._decrement_op(node.op) return new_node + def swap_nodes(self, node1, node2): + """Swap connected nodes e.g. due to commutation. + + Args: + node1 (OpNode): predecessor node + node2 (OpNode): successor node + + Raises: + DAGCircuitError: if either node is not an OpNode or nodes are not connected + """ + if not (isinstance(node1, DAGOpNode) and isinstance(node2, DAGOpNode)): + raise DAGCircuitError("nodes to swap are not both DAGOpNodes") + try: + connected_edges = self._multi_graph.get_all_edge_data(node1._node_id, node2._node_id) + except rx.NoEdgeBetweenNodes as no_common_edge: + raise DAGCircuitError("attempt to swap unconnected nodes") from no_common_edge + node1_id = node1._node_id + node2_id = node2._node_id + for edge in connected_edges[::-1]: + edge_find = lambda x, y=edge: x == y + edge_parent = self._multi_graph.find_predecessors_by_edge(node1_id, edge_find)[0] + self._multi_graph.remove_edge(edge_parent._node_id, node1_id) + self._multi_graph.add_edge(edge_parent._node_id, node2_id, edge) + edge_child = self._multi_graph.find_successors_by_edge(node2_id, edge_find)[0] + self._multi_graph.remove_edge(node1_id, node2_id) + self._multi_graph.add_edge(node2_id, node1_id, edge) + self._multi_graph.remove_edge(node2_id, edge_child._node_id) + self._multi_graph.add_edge(node1_id, edge_child._node_id, edge) + def node(self, node_id): """Get the node in the dag. diff --git a/releasenotes/notes/add-swap_nodes-to-dagcircuit-methods-2964426f02251fc4.yaml b/releasenotes/notes/add-swap_nodes-to-dagcircuit-methods-2964426f02251fc4.yaml new file mode 100644 index 000000000000..103a1b881531 --- /dev/null +++ b/releasenotes/notes/add-swap_nodes-to-dagcircuit-methods-2964426f02251fc4.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added method on :class:`qiskit.dagcircuit.DAGCircuit` to allow swapping + nodes which are partially connected. Partially connected here means that + the two nodes share at least one edge/qubit. If the nodes do not share + any edges a DAGCircuitError is raised. diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index ae6e6afe53d4..26f73d669cb7 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -36,6 +36,7 @@ from qiskit.circuit.library.standard_gates.x import XGate from qiskit.circuit.library.standard_gates.y import YGate from qiskit.circuit.library.standard_gates.u1 import U1Gate +from qiskit.circuit.library.standard_gates.rx import RXGate from qiskit.circuit.barrier import Barrier from qiskit.dagcircuit.exceptions import DAGCircuitError from qiskit.converters import circuit_to_dag @@ -2133,5 +2134,120 @@ def test_clbit_conditional(self): ) +class TestSwapNodes(QiskitTestCase): + """Test Swapping connected nodes.""" + + def test_1q_swap_fully_connected(self): + """Test swapping single qubit gates""" + dag = DAGCircuit() + qreg = QuantumRegister(1) + dag.add_qreg(qreg) + op_node0 = dag.apply_operation_back(RXGate(pi / 2), [qreg[0]], []) + op_node1 = dag.apply_operation_back(RXGate(pi), [qreg[0]], []) + dag.swap_nodes(op_node0, op_node1) + + expected = DAGCircuit() + expected.add_qreg(qreg) + expected.apply_operation_back(RXGate(pi), [qreg[0]], []) + expected.apply_operation_back(RXGate(pi / 2), [qreg[0]], []) + + self.assertEqual(dag, expected) + + def test_2q_swap_fully_connected(self): + """test swaping full connected 2q gates""" + dag = DAGCircuit() + qreg = QuantumRegister(2) + dag.add_qreg(qreg) + op_node0 = dag.apply_operation_back(CXGate(), qreg[[0, 1]], []) + op_node1 = dag.apply_operation_back(CZGate(), qreg[[1, 0]], []) + dag.swap_nodes(op_node0, op_node1) + + expected = DAGCircuit() + expected.add_qreg(qreg) + expected.apply_operation_back(CZGate(), qreg[[1, 0]], []) + expected.apply_operation_back(CXGate(), qreg[[0, 1]], []) + + self.assertEqual(dag, expected) + + def test_2q_swap_partially_connected(self): + """test swapping 2q partially connected gates""" + dag = DAGCircuit() + qreg = QuantumRegister(3) + dag.add_qreg(qreg) + op_node0 = dag.apply_operation_back(CXGate(), qreg[[1, 0]], []) + op_node1 = dag.apply_operation_back(CZGate(), qreg[[1, 2]], []) + dag.swap_nodes(op_node0, op_node1) + + expected = DAGCircuit() + expected.add_qreg(qreg) + expected.apply_operation_back(CZGate(), qreg[[1, 2]], []) + expected.apply_operation_back(CXGate(), qreg[[1, 0]], []) + self.assertEqual(dag, expected) + + def test_4q_swap_partially_connected(self): + """test swapping 4q partially connected gates""" + from qiskit.circuit.library.standard_gates import C3XGate + + dag = DAGCircuit() + qreg = QuantumRegister(5) + dag.add_qreg(qreg) + op_node0 = dag.apply_operation_back(C3XGate(), qreg[[0, 1, 2, 3]], []) + op_node1 = dag.apply_operation_back(C3XGate(), qreg[[0, 1, 2, 4]], []) + dag.swap_nodes(op_node0, op_node1) + + expected = DAGCircuit() + expected.add_qreg(qreg) + expected.apply_operation_back(C3XGate(), qreg[[0, 1, 2, 4]], []) + expected.apply_operation_back(C3XGate(), qreg[[0, 1, 2, 3]], []) + self.assertEqual(dag, expected) + + def test_2q_swap_non_connected_raises(self): + """test swapping 2q non-connected gates raises an exception""" + dag = DAGCircuit() + qreg = QuantumRegister(4) + dag.add_qreg(qreg) + op_node0 = dag.apply_operation_back(CXGate(), qreg[[0, 2]], []) + op_node1 = dag.apply_operation_back(CZGate(), qreg[[1, 3]], []) + self.assertRaises(DAGCircuitError, dag.swap_nodes, op_node0, op_node1) + + def test_2q_swap_partially_connected_pre_spectators(self): + """test swapping 2q partially connected gates when in the presence of pre + spectator ops.""" + dag = DAGCircuit() + qreg = QuantumRegister(5) + dag.add_qreg(qreg) + dag.apply_operation_back(XGate(), qreg[[0]]) + op_node0 = dag.apply_operation_back(CXGate(), qreg[[1, 0]], []) + op_node1 = dag.apply_operation_back(CZGate(), qreg[[1, 2]], []) + dag.swap_nodes(op_node0, op_node1) + + expected = DAGCircuit() + expected.add_qreg(qreg) + expected.apply_operation_back(XGate(), qreg[[0]]) + expected.apply_operation_back(CZGate(), qreg[[1, 2]], []) + expected.apply_operation_back(CXGate(), qreg[[1, 0]], []) + self.assertEqual(dag, expected) + + def test_2q_swap_partially_connected_prepost_spectators(self): + """test swapping 2q partially connected gates when in the presence of pre + and post spectator ops.""" + dag = DAGCircuit() + qreg = QuantumRegister(5) + dag.add_qreg(qreg) + dag.apply_operation_back(XGate(), qreg[[0]]) + op_node0 = dag.apply_operation_back(CXGate(), qreg[[1, 0]], []) + op_node1 = dag.apply_operation_back(CZGate(), qreg[[1, 2]], []) + dag.apply_operation_back(CZGate(), qreg[[1, 2]], []) + dag.swap_nodes(op_node0, op_node1) + + expected = DAGCircuit() + expected.add_qreg(qreg) + expected.apply_operation_back(XGate(), qreg[[0]]) + expected.apply_operation_back(CZGate(), qreg[[1, 2]], []) + expected.apply_operation_back(CXGate(), qreg[[1, 0]], []) + expected.apply_operation_back(CZGate(), qreg[[1, 2]], []) + self.assertEqual(dag, expected) + + if __name__ == "__main__": unittest.main() From f51a93f6e37e78f5803930790c4c881599c0a956 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 17 Apr 2023 21:45:47 +0100 Subject: [PATCH 042/172] Improve error messages on failed control-flow transpilation (#9049) * Improve error messages on failed control-flow transpilation As part of this, it became convenient for the `Error` transpiler pass to be able to accept an arbitrary `property_set -> str` callable function to generate the message, rather than just relying on fixed formatting strings. * Fix lint --- qiskit/transpiler/passes/utils/error.py | 18 ++++-- .../transpiler/preset_passmanagers/common.py | 35 ++++++++++-- .../transpiler/preset_passmanagers/level0.py | 2 + .../transpiler/preset_passmanagers/level1.py | 2 + ...ass-callable-message-3f29f09b9faba736.yaml | 6 ++ test/python/transpiler/test_error.py | 12 ++++ .../transpiler/test_preset_passmanagers.py | 55 ++++++++++++++++++- 7 files changed, 120 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/error-pass-callable-message-3f29f09b9faba736.yaml diff --git a/qiskit/transpiler/passes/utils/error.py b/qiskit/transpiler/passes/utils/error.py index 475456b86d12..445b1d21d565 100644 --- a/qiskit/transpiler/passes/utils/error.py +++ b/qiskit/transpiler/passes/utils/error.py @@ -27,7 +27,9 @@ def __init__(self, msg=None, action="raise"): """Error pass. Args: - msg (str): Error message, if not provided a generic error will be used + msg (str | Callable[[PropertySet], str]): Error message, if not provided a generic error + will be used. This can be either a raw string, or a callback function that accepts + the current ``property_set`` and returns the desired message. action (str): the action to perform. Default: 'raise'. The options are: * 'raise': Raises a `TranspilerError` exception with msg * 'warn': Raises a non-fatal warning with msg @@ -45,10 +47,16 @@ def __init__(self, msg=None, action="raise"): def run(self, _): """Run the Error pass on `dag`.""" - msg = self.msg if self.msg else "An error occurred while the passmanager was running." - prop_names = [tup[1] for tup in string.Formatter().parse(msg) if tup[1] is not None] - properties = {prop_name: self.property_set[prop_name] for prop_name in prop_names} - msg = msg.format(**properties) + if self.msg is None: + msg = "An error occurred while the pass manager was running." + elif isinstance(self.msg, str): + prop_names = [ + tup[1] for tup in string.Formatter().parse(self.msg) if tup[1] is not None + ] + properties = {prop_name: self.property_set[prop_name] for prop_name in prop_names} + msg = self.msg.format(**properties) + else: + msg = self.msg(self.property_set) if self.action == "raise": raise TranspilerError(msg) diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 02654016f4d6..5f1fdbd92e45 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -84,12 +84,39 @@ def _without_control_flow(property_set): return not any(property_set[f"contains_{x}"] for x in _CONTROL_FLOW_OP_NAMES) +class _InvalidControlFlowForBackend: + # Explicitly stateful closure to allow pickling. + + def __init__(self, basis_gates=(), target=None): + if target is not None: + self.unsupported = [op for op in _CONTROL_FLOW_OP_NAMES if op not in target] + else: + basis_gates = set(basis_gates) if basis_gates is not None else set() + self.unsupported = [op for op in _CONTROL_FLOW_OP_NAMES if op not in basis_gates] + + def message(self, property_set): + """Create an error message for the given property set.""" + fails = [x for x in self.unsupported if property_set[f"contains_{x}"]] + if len(fails) == 1: + return f"The control-flow construct '{fails[0]}' is not supported by the backend." + return ( + f"The control-flow constructs [{', '.join(repr(op) for op in fails)}]" + " are not supported by the backend." + ) + + def condition(self, property_set): + """Checkable condition for the given property set.""" + return any(property_set[f"contains_{x}"] for x in self.unsupported) + + def generate_control_flow_options_check( layout_method=None, routing_method=None, translation_method=None, optimization_method=None, scheduling_method=None, + basis_gates=(), + target=None, ): """Generate a pass manager that, when run on a DAG that contains control flow, fails with an error message explaining the invalid options, and what could be used instead. @@ -99,7 +126,6 @@ def generate_control_flow_options_check( control-flow operations, and raises an error if any of the given options do not support control flow, but a circuit with control flow is given. """ - bad_options = [] message = "Some options cannot be used with control flow." for stage, given in [ @@ -123,9 +149,10 @@ def generate_control_flow_options_check( bad_options.append(option) out = PassManager() out.append(ContainsInstruction(_CONTROL_FLOW_OP_NAMES, recurse=False)) - if not bad_options: - return out - out.append(Error(message), condition=_has_control_flow) + if bad_options: + out.append(Error(message), condition=_has_control_flow) + backend_control = _InvalidControlFlowForBackend(basis_gates, target) + out.append(Error(backend_control.message), condition=backend_control.condition) return out diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index ddb9560b0369..36b62d2c22fe 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -176,6 +176,8 @@ def _swap_mapped(property_set): translation_method=translation_method, optimization_method=optimization_method, scheduling_method=scheduling_method, + basis_gates=basis_gates, + target=target, ) if init_method is not None: init += plugin_manager.get_passmanager_stage( diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 0a1b857691b1..bb4413b03219 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -300,6 +300,8 @@ def _unroll_condition(property_set): translation_method=translation_method, optimization_method=optimization_method, scheduling_method=scheduling_method, + basis_gates=basis_gates, + target=target, ) if init_method is not None: init += plugin_manager.get_passmanager_stage( diff --git a/releasenotes/notes/error-pass-callable-message-3f29f09b9faba736.yaml b/releasenotes/notes/error-pass-callable-message-3f29f09b9faba736.yaml new file mode 100644 index 000000000000..d307864cb2ec --- /dev/null +++ b/releasenotes/notes/error-pass-callable-message-3f29f09b9faba736.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The transpiler pass :class:`~.transpiler.passes.Error` now accepts callables + in its ``msg`` parameter. These should be a callable that takes in the + ``property_set`` and returns a string. diff --git a/test/python/transpiler/test_error.py b/test/python/transpiler/test_error.py index c541283d0fb0..e110eaaa4180 100644 --- a/test/python/transpiler/test_error.py +++ b/test/python/transpiler/test_error.py @@ -53,6 +53,18 @@ def test_logger(self): pass_.run(None) self.assertEqual(log.output, ["INFO:qiskit.transpiler.passes.utils.error:a message"]) + def test_message_callable(self): + """Test that the message can be a callable that accepts the property set.""" + + def message(property_set): + self.assertIn("sentinel key", property_set) + return property_set["sentinel key"] + + pass_ = Error(message) + pass_.property_set["sentinel key"] = "sentinel value" + with self.assertRaisesRegex(TranspilerError, "sentinel value"): + pass_.run(None) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 4d5eb4f45049..55eba7eabb13 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -22,7 +22,7 @@ from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister from qiskit.circuit import Qubit, Gate, ControlFlowOp, ForLoopOp from qiskit.compiler import transpile, assemble -from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspilerError +from qiskit.transpiler import CouplingMap, Layout, PassManager, TranspilerError, Target from qiskit.circuit.library import U2Gate, U3Gate, QuantumVolume, CXGate, CZGate, XGate from qiskit.transpiler.passes import ( ALAPScheduleAnalysis, @@ -1561,3 +1561,56 @@ def test_unsupported_levels_raise(self, optimization_level): with self.assertRaisesRegex(TranspilerError, "The optimizations in optimization_level="): transpile(qc, optimization_level=optimization_level) + + @data(0, 1) + def test_unsupported_basis_gates_raise(self, optimization_level): + """Test that trying to transpile a control-flow circuit for a backend that doesn't support + the necessary operations in its `basis_gates` will raise a sensible error.""" + backend = FakeTokyo() + + qc = QuantumCircuit(1, 1) + with qc.for_loop((0,)): + pass + with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"): + transpile(qc, backend, optimization_level=optimization_level) + + qc = QuantumCircuit(1, 1) + with qc.if_test((qc.clbits[0], False)): + pass + with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"): + transpile(qc, backend, optimization_level=optimization_level) + + qc = QuantumCircuit(1, 1) + with qc.while_loop((qc.clbits[0], False)): + pass + with qc.for_loop((0, 1, 2)): + pass + with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"): + transpile(qc, backend, optimization_level=optimization_level) + + @data(0, 1) + def test_unsupported_targets_raise(self, optimization_level): + """Test that trying to transpile a control-flow circuit for a backend that doesn't support + the necessary operations in its `Target` will raise a more sensible error.""" + target = Target(num_qubits=2) + target.add_instruction(CXGate(), {(0, 1): None}) + + qc = QuantumCircuit(1, 1) + with qc.for_loop((0,)): + pass + with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"): + transpile(qc, target=target, optimization_level=optimization_level) + + qc = QuantumCircuit(1, 1) + with qc.if_test((qc.clbits[0], False)): + pass + with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"): + transpile(qc, target=target, optimization_level=optimization_level) + + qc = QuantumCircuit(1, 1) + with qc.while_loop((qc.clbits[0], False)): + pass + with qc.for_loop((0, 1, 2)): + pass + with self.assertRaisesRegex(TranspilerError, "The control-flow construct.*not supported"): + transpile(qc, target=target, optimization_level=optimization_level) From 81e4913036da5572f24dca58c3f15cf12e752f3d Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Mon, 17 Apr 2023 23:13:26 +0200 Subject: [PATCH 043/172] link to `unitary_synthesis_plugin_names` in `unitary_synthesis_method` documentation (#9896) * link to unitary_synthesis_plugin_names in unitary_synthesis_method doc * s/tilde/dot Co-authored-by: Jake Lishman * last one --------- Co-authored-by: Jake Lishman --- qiskit/compiler/transpiler.py | 5 ++--- qiskit/transpiler/passmanager_config.py | 3 ++- qiskit/transpiler/preset_passmanagers/__init__.py | 5 ++--- qiskit/transpiler/preset_passmanagers/common.py | 6 ++++-- 4 files changed, 10 insertions(+), 9 deletions(-) diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 7475ea4cf1e2..40fd9ff52659 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -248,9 +248,8 @@ def callback_func(**kwargs): output_name: A list with strings to identify the output circuits. The length of the list should be exactly the length of the ``circuits`` parameter. unitary_synthesis_method (str): The name of the unitary synthesis - method to use. By default 'default' is used, which is the only - method included with qiskit. If you have installed any unitary - synthesis plugins you can use the name exported by the plugin. + method to use. By default ``'default'`` is used. You can see a list of installed + plugins with :func:`.unitary_synthesis_plugin_names`. unitary_synthesis_plugin_config: An optional configuration dictionary that will be passed directly to the unitary synthesis plugin. By default this setting will have no effect as the default unitary diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index a50db70d3616..5c1720ccb1d6 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -75,7 +75,8 @@ def __init__( timing_constraints (TimingConstraints): Hardware time alignment restrictions. unitary_synthesis_method (str): The string method to use for the :class:`~qiskit.transpiler.passes.UnitarySynthesis` pass. Will - search installed plugins for a valid method. + search installed plugins for a valid method. You can see a list of + installed plugins with :func:`.unitary_synthesis_plugin_names`. target (Target): The backend target hls_config (HLSConfig): An optional configuration class to use for :class:`~qiskit.transpiler.passes.HighLevelSynthesis` pass. diff --git a/qiskit/transpiler/preset_passmanagers/__init__.py b/qiskit/transpiler/preset_passmanagers/__init__.py index a17fe77f4ae6..2e18e8659d38 100644 --- a/qiskit/transpiler/preset_passmanagers/__init__.py +++ b/qiskit/transpiler/preset_passmanagers/__init__.py @@ -172,9 +172,8 @@ def generate_preset_pass_manager( seed_transpiler (int): Sets random seed for the stochastic parts of the transpiler. unitary_synthesis_method (str): The name of the unitary synthesis - method to use. By default 'default' is used, which is the only - method included with qiskit. If you have installed any unitary - synthesis plugins you can use the name exported by the plugin. + method to use. By default ``'default'`` is used. You can see a list of + installed plugins with :func:`.unitary_synthesis_plugin_names`. unitary_synthesis_plugin_config (dict): An optional configuration dictionary that will be passed directly to the unitary synthesis plugin. By default this setting will have no effect as the default unitary diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 5f1fdbd92e45..22eae3829040 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -195,7 +195,8 @@ def generate_unroll_3q( gates on the backend target approximation_degree (Optional[float]): The heuristic approximation degree to use. Can be between 0 and 1. - unitary_synthesis_method (str): The unitary synthesis method to use + unitary_synthesis_method (str): The unitary synthesis method to use. You can see + a list of installed plugins with :func:`.unitary_synthesis_plugin_names`. unitary_synthesis_plugin_config (dict): The optional dictionary plugin configuration, this is plugin specific refer to the specified plugin's documentation for how to use. @@ -388,7 +389,8 @@ def generate_translation_passmanager( documentation for how to use. backend_props (BackendProperties): Properties of a backend to synthesize for (e.g. gate fidelities). - unitary_synthesis_method (str): The unitary synthesis method to use + unitary_synthesis_method (str): The unitary synthesis method to use. You can + see a list of installed plugins with :func:`.unitary_synthesis_plugin_names`. hls_config (HLSConfig): An optional configuration class to use for :class:`~qiskit.transpiler.passes.HighLevelSynthesis` pass. Specifies how to synthesize various high-level objects. From ebf83b620a44d44929f4d17293b79b34d753452d Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Mon, 17 Apr 2023 18:00:11 -0400 Subject: [PATCH 044/172] Update docstring of CouplingMap.connected_components to reflect name change (#9979) This method was renamed. But the docstring was not updated. --- qiskit/transpiler/coupling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index a15d142cf544..fd53e06ed193 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -458,7 +458,7 @@ def connected_components(self) -> List["CouplingMap"]: from qiskit.transpiler import CouplingMap cmap = CouplingMap([[0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3]]) - component_cmaps = cmap.get_component_subgraphs() + component_cmaps = cmap.connected_components() print(component_cmaps[1].graph[0]) will print ``3`` as index ``0`` in the second component is qubit 3 in the original cmap. From c4f1b0bd0b3873bcc5807a3c55b7eeceedd18193 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Tue, 18 Apr 2023 02:45:09 +0200 Subject: [PATCH 045/172] plugin convenience function for getting the entry point object (#9275) * plugin convenience function for getting the entry point object * simpler code, thanks to Jake Co-authored-by: Jake Lishman * typehint and docstring * reno update * thanks Jake * Fix docs build --------- Co-authored-by: Jake Lishman --- .../transpiler/preset_passmanagers/plugin.py | 47 ++++++++++++++++++- .../entry_point_obj-60625d9d797df1d9.yaml | 18 +++++++ test/python/transpiler/test_stage_plugin.py | 12 +++++ 3 files changed, 76 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/entry_point_obj-60625d9d797df1d9.yaml diff --git a/qiskit/transpiler/preset_passmanagers/plugin.py b/qiskit/transpiler/preset_passmanagers/plugin.py index f4d1ae7e4b89..83d66ceae6a2 100644 --- a/qiskit/transpiler/preset_passmanagers/plugin.py +++ b/qiskit/transpiler/preset_passmanagers/plugin.py @@ -161,10 +161,11 @@ def pass_manager(self, pass_manager_config, optimization_level): PassManagerStagePlugin PassManagerStagePluginManager list_stage_plugins + passmanager_stage_plugins """ import abc -from typing import List, Optional +from typing import List, Optional, Dict import stevedore @@ -301,3 +302,47 @@ def list_stage_plugins(stage_name: str) -> List[str]: return plugin_mgr.scheduling_plugins.names() else: raise TranspilerError(f"Invalid stage name: {stage_name}") + + +def passmanager_stage_plugins(stage: str) -> Dict[str, PassManagerStagePlugin]: + """Return a dict with, for each stage name, the class type of the plugin. + + This function is useful for getting more information about a plugin: + + .. code-block:: python + + from qiskit.transpiler.preset_passmanagers.plugin import passmanager_stage_plugins + routing_plugins = passmanager_stage_plugins('routing') + basic_plugin = routing_plugins['basic'] + help(basic_plugin) + + .. code-block:: text + + Help on BasicSwapPassManager in module ...preset_passmanagers.builtin_plugins object: + + class BasicSwapPassManager(...preset_passmanagers.plugin.PassManagerStagePlugin) + | Plugin class for routing stage with :class:`~.BasicSwap` + | + | Method resolution order: + | BasicSwapPassManager + | ...preset_passmanagers.plugin.PassManagerStagePlugin + | abc.ABC + | builtins.object + ... + + Args: + stage: The stage name to get + + Returns: + dict: the key is the name of the plugin and the value is the class type for each. + + Raises: + TranspilerError: If an invalid stage name is specified. + """ + plugin_mgr = PassManagerStagePluginManager() + try: + manager = getattr(plugin_mgr, f"{stage}_plugins") + except AttributeError as exc: + raise TranspilerError(f"Passmanager stage {stage} not found") from exc + + return {name: manager[name].obj for name in manager.names()} diff --git a/releasenotes/notes/entry_point_obj-60625d9d797df1d9.yaml b/releasenotes/notes/entry_point_obj-60625d9d797df1d9.yaml new file mode 100644 index 000000000000..0494a059c644 --- /dev/null +++ b/releasenotes/notes/entry_point_obj-60625d9d797df1d9.yaml @@ -0,0 +1,18 @@ +--- +features: + - | + The function ``passmanager_stage_plugins`` in the module + ``qiskit.transpiler.preset_passmanagers.plugin`` was added to obtain a map between + plugin names and their class type. + This allows to identify and query passmanager plugin documentation. For example:: + + >>> from qiskit.transpiler.preset_passmanagers.plugin import passmanager_stage_plugins + >>> passmanager_stage_plugins('routing')['lookahead'].__class__ + qiskit.transpiler.preset_passmanagers.builtin_plugins.LookaheadSwapPassManager + + >>> help(passmanager_stage_plugins('routing')['lookahead']) + Help on BasicSwapPassManager in module qiskit.transpiler.preset_passmanagers.builtin_plugins object: + + class BasicSwapPassManager(qiskit.transpiler.preset_passmanagers.plugin.PassManagerStagePlugin) + | Plugin class for routing stage with :class:`~.BasicSwap` + ... diff --git a/test/python/transpiler/test_stage_plugin.py b/test/python/transpiler/test_stage_plugin.py index 1242905e9f4e..1279fae467e8 100644 --- a/test/python/transpiler/test_stage_plugin.py +++ b/test/python/transpiler/test_stage_plugin.py @@ -22,9 +22,11 @@ from qiskit.compiler.transpiler import transpile from qiskit.test import QiskitTestCase from qiskit.transpiler import PassManager, PassManagerConfig, CouplingMap +from qiskit.transpiler.preset_passmanagers.builtin_plugins import BasicSwapPassManager from qiskit.transpiler.preset_passmanagers.plugin import ( PassManagerStagePluginManager, list_stage_plugins, + passmanager_stage_plugins, ) from qiskit.transpiler.exceptions import TranspilerError from qiskit.providers.basicaer import QasmSimulatorPy @@ -51,6 +53,16 @@ def test_list_stage_plugins_invalid_stage_name(self): with self.assertRaises(TranspilerError): list_stage_plugins("not_a_stage") + def test_passmanager_stage_plugins(self): + """Test entry_point_obj function.""" + basic_obj = passmanager_stage_plugins("routing") + self.assertIsInstance(basic_obj["basic"], BasicSwapPassManager) + + def test_passmanager_stage_plugins_not_found(self): + """Test entry_point_obj function with nonexistent stage""" + with self.assertRaises(TranspilerError): + passmanager_stage_plugins("foo_stage") + def test_build_pm_invalid_plugin_name_valid_stage(self): """Test get pm from plugin with invalid plugin name and valid stage.""" plugin_manager = PassManagerStagePluginManager() From 971a203deb33bdc309b1467d00e39efc5eda8936 Mon Sep 17 00:00:00 2001 From: Ian Hincks Date: Tue, 18 Apr 2023 11:07:41 -0400 Subject: [PATCH 046/172] Change QuantumCircuit.metadata to always be a dict (#9849) * Fix QuantumCircuit.metadata return annotation The QuantumCircuit.metadata return annotation was previously just ``dict``, whereas ``None`` is a valid value. This means that static type checkers would have no problems with statements like ``circuit.metadata.get("name", 0)``. * Change QuantumCircuit.metadata to always be dictionary Previous to this change, the QuantumCircuit.metadata could be set to, and return None instead of a dictionary. * Fix cases where QuantumCircuit.metadata is set to None by a DAG * Added release notes * Minor updates * Change default metadata value of DAGCircuit/Dependency to {} Previously it was None * fix typos * Fix stacklevel of emitted warning * Add warning suppression for Aer behaviour This will be fixed in Aer in the near future in a backwards-compatible manner, so in order to put the fix out into Terra while Aer's deployment issues are worked out (run out of space on PyPI!), we can add a very targetted warning suppression to the tests. --------- Co-authored-by: Jake Lishman --- qiskit/circuit/quantumcircuit.py | 19 +++++++++++++------ qiskit/dagcircuit/dagcircuit.py | 2 +- qiskit/dagcircuit/dagdependency.py | 2 +- qiskit/test/base.py | 7 +++++++ ...metadata_always_dict-49015896dfa49d33.yaml | 15 +++++++++++++++ .../python/circuit/test_circuit_properties.py | 19 +++++++++++++++++++ test/python/dagcircuit/test_dagcircuit.py | 6 ++++++ test/python/dagcircuit/test_dagdependency.py | 6 ++++++ 8 files changed, 68 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/circuit_metadata_always_dict-49015896dfa49d33.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index cd8062a7d34f..7f2bc7d8beb8 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -22,6 +22,7 @@ import multiprocessing as mp import string import re +import warnings import typing from collections import OrderedDict, defaultdict, namedtuple from typing import ( @@ -300,9 +301,7 @@ def __init__( self.duration = None self.unit = "dt" - if not isinstance(metadata, dict) and metadata is not None: - raise TypeError("Only a dictionary or None is accepted for circuit metadata") - self._metadata = metadata + self.metadata = {} if metadata is None else metadata @staticmethod def from_instructions( @@ -469,7 +468,7 @@ def has_calibration_for(self, instr_context: tuple): @property def metadata(self) -> dict: - """The user provided metadata associated with the circuit + """The user provided metadata associated with the circuit. The metadata for the circuit is a user provided ``dict`` of metadata for the circuit. It will not be used to influence the execution or @@ -483,8 +482,16 @@ def metadata(self) -> dict: @metadata.setter def metadata(self, metadata: dict | None): """Update the circuit metadata""" - if not isinstance(metadata, dict) and metadata is not None: - raise TypeError("Only a dictionary or None is accepted for circuit metadata") + if metadata is None: + metadata = {} + warnings.warn( + "Setting metadata to None was deprecated in Terra 0.24.0 and this ability will be " + "removed in a future release. Instead, set metadata to an empty dictionary.", + DeprecationWarning, + stacklevel=2, + ) + elif not isinstance(metadata, dict): + raise TypeError("Only a dictionary is accepted for circuit metadata") self._metadata = metadata def __str__(self) -> str: diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 3f3146299922..274502f5c0f4 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -60,7 +60,7 @@ def __init__(self): self.name = None # Circuit metadata - self.metadata = None + self.metadata = {} # Set of wires (Register,idx) in the dag self._wires = set() diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py index d1ad623bdc6d..1ed5eb96e113 100644 --- a/qiskit/dagcircuit/dagdependency.py +++ b/qiskit/dagcircuit/dagdependency.py @@ -91,7 +91,7 @@ def __init__(self): self.name = None # Circuit metadata - self.metadata = None + self.metadata = {} # Directed multigraph whose nodes are operations(gates) and edges # represent non-commutativity between two gates. diff --git a/qiskit/test/base.py b/qiskit/test/base.py index e81226cc1b87..ac9da7a02a2d 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -240,6 +240,13 @@ def setUpClass(cls): ] for msg in allow_DeprecationWarning_message: warnings.filterwarnings("default", category=DeprecationWarning, message=msg) + # This warning should be fixed once Qiskit/qiskit-aer#1761 is in a release version of Aer. + warnings.filterwarnings( + "default", + category=DeprecationWarning, + module="qiskit_aer.*", + message="Setting metadata to None.*", + ) class FullQiskitTestCase(QiskitTestCase): diff --git a/releasenotes/notes/circuit_metadata_always_dict-49015896dfa49d33.yaml b/releasenotes/notes/circuit_metadata_always_dict-49015896dfa49d33.yaml new file mode 100644 index 000000000000..772daee423c1 --- /dev/null +++ b/releasenotes/notes/circuit_metadata_always_dict-49015896dfa49d33.yaml @@ -0,0 +1,15 @@ +upgrade: + - | + The :class:`.QuantumCircuit` :attr:`.QuantumCircuit.metadata` attribute now + always returns a dictionary, and can only be set to a dictionary. Previously, + its default value was ``None``, and could be manually set to ``None`` or a + dictionary. + - | + The default value of ``metadata`` in both :class:`.DAGCircuit` and + :class:`.DAGDependency` has been changed from ``None`` to ``{}`` for compatibility + with a similar attribute of :class:`.QuantumCircuit`. +deprecations: + - | + Setting the :class:`.QuantumCircuit` :attr:`.QuantumCircuit.metadata` attribute + to ``None`` has been deprecated. Instead, users should set it to an empty + dictionary if they want it to contain no data. diff --git a/test/python/circuit/test_circuit_properties.py b/test/python/circuit/test_circuit_properties.py index 12c3ecd9b0a2..67c6cca5518b 100644 --- a/test/python/circuit/test_circuit_properties.py +++ b/test/python/circuit/test_circuit_properties.py @@ -1267,6 +1267,25 @@ def test_metadata_copy_does_not_share_state(self): self.assertEqual(qc1.metadata["a"], 0) + def test_metadata_is_dict(self): + """Verify setting metadata to None in the constructor results in an empty dict.""" + qc = QuantumCircuit(1) + metadata1 = qc.metadata + self.assertEqual(metadata1, {}) + + def test_metadata_raises(self): + """Test that we must set metadata to a dict.""" + qc = QuantumCircuit(1) + with self.assertRaises(TypeError): + qc.metadata = 1 + + def test_metdata_deprectation(self): + """Test that setting metadata to None emits a deprecation warning.""" + qc = QuantumCircuit(1) + with self.assertWarns(DeprecationWarning): + qc.metadata = None + self.assertEqual(qc.metadata, {}) + def test_scheduling(self): """Test cannot return schedule information without scheduling.""" qc = QuantumCircuit(2) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 26f73d669cb7..48ec6b7af29f 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -1226,6 +1226,12 @@ def test_circuit_factors(self): """Test number of separable factors in circuit.""" self.assertEqual(self.dag.num_tensor_factors(), 2) + def test_default_metadata_value(self): + """Test that the default DAGCircuit metadata is valid QuantumCircuit metadata.""" + qc = QuantumCircuit(1) + qc.metadata = self.dag.metadata + self.assertEqual(qc.metadata, {}) + class TestCircuitControlFlowProperties(QiskitTestCase): """Properties tests of DAGCircuit with control-flow instructions.""" diff --git a/test/python/dagcircuit/test_dagdependency.py b/test/python/dagcircuit/test_dagdependency.py index 011584dded2b..ca6890bb40a3 100644 --- a/test/python/dagcircuit/test_dagdependency.py +++ b/test/python/dagcircuit/test_dagdependency.py @@ -319,6 +319,12 @@ def test_dag_depth_empty(self): dag = circuit_to_dagdependency(qc) self.assertEqual(dag.depth(), 0) + def test_default_metadata_value(self): + """Test that the default DAGDependency metadata is valid QuantumCircuit metadata.""" + qc = QuantumCircuit(1) + qc.metadata = self.dag.metadata + self.assertEqual(qc.metadata, {}) + if __name__ == "__main__": unittest.main() From f76e96a6432c2340622b18e4ce3e6f814caf9533 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 18 Apr 2023 17:25:40 +0100 Subject: [PATCH 047/172] Add QPY support for `switch` statement (#9926) QPY had a somewhat implicit assumption that most classes can be reconstructed by doing `type(obj)(*obj.params)`, which `SwitchCaseOp` very much does not abide by - to do so would compromise more core parts of how it is meant to function. This commit puts in a special case so that instances of `SwitchCaseOp` present their "params" as the information necessary to reconstruct the op in the above form, rather than actually using the parameters. That didn't feel _entirely_ satisfying, but does work fine. --- qiskit/qpy/__init__.py | 6 ++ qiskit/qpy/binary_io/circuits.py | 69 +++++++++++++------ qiskit/qpy/binary_io/value.py | 5 +- qiskit/qpy/type_keys.py | 16 ++++- .../circuit/test_circuit_load_from_qpy.py | 40 +++++++++++ test/qpy_compat/test_qpy.py | 33 +++++++++ 6 files changed, 147 insertions(+), 22 deletions(-) diff --git a/qiskit/qpy/__init__.py b/qiskit/qpy/__init__.py index 986e36a3a7fe..e56e22efd178 100644 --- a/qiskit/qpy/__init__.py +++ b/qiskit/qpy/__init__.py @@ -166,6 +166,12 @@ pulse instruction operands. A special type character ``o`` is reserved for the string data that appears in the pulse instruction operands. +In addition, version 7 adds two new type keys to the INSTRUCTION_PARM struct. ``"d"`` is followed +by no data and represents the literal value :data:`.CASE_DEFAULT` for switch-statement support. +``"R"`` represents a :class:`.ClassicalRegister` or :class:`.Clbit`, and is followed by the same +format as the description of register or classical bit as used in the first element of :ref:`the +condition of an INSTRUCTION field `. + .. _qpy_version_6: Version 6 diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py index e84ed7ef0eb8..95adba8fdbd1 100644 --- a/qiskit/qpy/binary_io/circuits.py +++ b/qiskit/qpy/binary_io/circuits.py @@ -121,7 +121,7 @@ def _read_registers(file_obj, num_registers): return registers -def _loads_instruction_parameter(type_key, data_bytes, version, vectors): +def _loads_instruction_parameter(type_key, data_bytes, version, vectors, registers, circuit): if type_key == type_keys.Program.CIRCUIT: param = common.data_from_binary(data_bytes, read_circuit, version=version) elif type_key == type_keys.Container.RANGE: @@ -134,6 +134,8 @@ def _loads_instruction_parameter(type_key, data_bytes, version, vectors): _loads_instruction_parameter, version=version, vectors=vectors, + registers=registers, + circuit=circuit, ) ) elif type_key == type_keys.Value.INTEGER: @@ -142,12 +144,22 @@ def _loads_instruction_parameter(type_key, data_bytes, version, vectors): elif type_key == type_keys.Value.FLOAT: # TODO This uses little endian. Should be fixed in the next QPY version. param = struct.unpack("= (0, 24, 0): output_circuits["referenced_schedule_blocks.qpy"] = generate_referenced_schedule() + output_circuits["control_flow_switch.qpy"] = generate_control_flow_switch_circuits() return output_circuits From 3c393ad87a4be984587f4b1f357ed03c0850484f Mon Sep 17 00:00:00 2001 From: Toshinari Itoko <15028342+itoko@users.noreply.github.com> Date: Wed, 19 Apr 2023 06:00:41 +0900 Subject: [PATCH 048/172] Update docstrings on BackendV2Converter and convert_to_target (#9474) * Fix silent drop of inst map during conversion to target * Add docstrings * Remove a raise of warning * Update qiskit/providers/backend_compat.py Co-authored-by: Matthew Treinish * fix indent --------- Co-authored-by: Matthew Treinish --- qiskit/providers/backend_compat.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/qiskit/providers/backend_compat.py b/qiskit/providers/backend_compat.py index 2dc7bdd07b64..524df5498bd2 100644 --- a/qiskit/providers/backend_compat.py +++ b/qiskit/providers/backend_compat.py @@ -40,6 +40,17 @@ def convert_to_target( ): """Uses configuration, properties and pulse defaults to construct and return Target class. + + In order to convert with a ``defaults.instruction_schedule_map``, + which has a custom calibration for an operation, + the operation name must be in ``configuration.basis_gates`` and + ``custom_name_mapping`` must be supplied for the operation. + Otherwise, the operation will be dropped in the resulting ``Target`` object. + + That suggests it is recommended to add custom calibrations **after** creating a target + with this function instead of adding them to ``defaults`` in advance. For example:: + + target.add_instruction(custom_gate, {(0, 1): InstructionProperties(calibration=custom_sched)}) """ # pylint: disable=cyclic-import from qiskit.transpiler.target import ( @@ -206,6 +217,21 @@ class BackendV2Converter(BackendV2): common access patterns between :class:`~.BackendV1` and :class:`~.BackendV2`. This class should only be used if you need a :class:`~.BackendV2` and still need compatibility with :class:`~.BackendV1`. + + When using custom calibrations (or other custom workflows) it is **not** recommended + to mutate the ``BackendV1`` object before applying this converter. For example, in order to + convert a ``BackendV1`` object with a customized ``defaults().instruction_schedule_map``, + which has a custom calibration for an operation, the operation name must be in + ``configuration().basis_gates`` and ``name_mapping`` must be supplied for the operation. + Otherwise, the operation will be dropped in the resulting ``BackendV2`` object. + + Instead it is typically better to add custom calibrations **after** applying this converter + instead of updating ``BackendV1.defaults()`` in advance. For example:: + + backend_v2 = BackendV2Converter(backend_v1) + backend_v2.target.add_instruction( + custom_gate, {(0, 1): InstructionProperties(calibration=custom_sched)} + ) """ def __init__( From e18e4d2929c5d7833c0b14facc775a78600e9fa0 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 18 Apr 2023 18:33:26 -0400 Subject: [PATCH 049/172] Filter/ignore qubits in Target without any operations (#9927) * Filter/ignore qubits in Target without any operations Building off #9840 which adds full path support in all the preset pass managers for targetting backends with a disconnected coupling graph, this commit adds support for ignoring qubits that do not support any operations. When a Target is generated from #9911 with `filter_faulty` set to `True` this will potentially result in qubits being present in the `Target` without any supported operations. In these cases the layout passes in the transpiler might inadvertently use these qubits only to fail in the basis translator because there are no instructions available. This commit adds filtering of connected components from the list of output connected components if the `Target` does have any supported instructions on a qubit. This works by building a copy of the coupling map's internal graph that removes the nodes which do not have any supported operations. Then when we compute the connected components of this graph it will exclude any components of isolated qubits without any operations supported. A similar change is made to the coupling graph we pass to rustworkx.vf2_mapping() inside the vf2 layout family of passes. * Expand testing * Make filtered qubit coupling map a Target.build_coupling_map option This commit reworks the logic to construct a filtered coupling map as an optional argument on `Target.build_coupling_map()`. This makes the filtering a function of the Target object itself, which is where the context/data about which qubits support operations or not lives. The previous versions of this PR had a weird mix of responsibilities where the target would generate a coupling map and then we'd pass the target to that coupling map to do an additional round of filtering on it. * Apply suggestions from code review Co-authored-by: John Lapeyre * Fix incorrect set construction * Expand docstring on build_coupling_map argument * Rework logic in vf2 passes for filtering * Update argument name in disjoint_utils.py * Inline second argument for require_layout_isolated_to_component Co-authored-by: Kevin Hartman * Update qiskit/transpiler/passes/layout/vf2_post_layout.py Co-authored-by: Kevin Hartman * Apply suggestions from code review Co-authored-by: Kevin Hartman * Remove unnecessary len() * Inline second arg for dense_layout too --------- Co-authored-by: John Lapeyre Co-authored-by: Kevin Hartman --- .../transpiler/passes/layout/dense_layout.py | 4 +- .../passes/layout/disjoint_utils.py | 22 ++++- .../transpiler/passes/layout/sabre_layout.py | 12 ++- qiskit/transpiler/passes/layout/vf2_layout.py | 13 +++ .../passes/layout/vf2_post_layout.py | 11 +++ qiskit/transpiler/passes/layout/vf2_utils.py | 2 + .../transpiler/passes/routing/basic_swap.py | 5 +- .../transpiler/passes/routing/bip_mapping.py | 4 +- .../passes/routing/lookahead_swap.py | 4 +- .../transpiler/passes/routing/sabre_swap.py | 4 +- .../passes/routing/stochastic_swap.py | 5 +- qiskit/transpiler/target.py | 26 +++++- ...ter-idle-qubits-cmap-74ac7711fc7476f3.yaml | 6 ++ test/python/compiler/test_transpiler.py | 82 +++++++++++++++++++ 14 files changed, 185 insertions(+), 15 deletions(-) create mode 100644 releasenotes/notes/filter-idle-qubits-cmap-74ac7711fc7476f3.yaml diff --git a/qiskit/transpiler/passes/layout/dense_layout.py b/qiskit/transpiler/passes/layout/dense_layout.py index be29c28e519d..973947420680 100644 --- a/qiskit/transpiler/passes/layout/dense_layout.py +++ b/qiskit/transpiler/passes/layout/dense_layout.py @@ -69,7 +69,9 @@ def run(self, dag): "A coupling_map or target with constrained qargs is necessary to run the pass." ) layout_components = disjoint_utils.run_pass_over_connected_components( - dag, self.coupling_map, self._inner_run + dag, + self.coupling_map if self.target is None else self.target, + self._inner_run, ) layout_mapping = {} for component in layout_components: diff --git a/qiskit/transpiler/passes/layout/disjoint_utils.py b/qiskit/transpiler/passes/layout/disjoint_utils.py index e7d7c9756193..2afef62955a3 100644 --- a/qiskit/transpiler/passes/layout/disjoint_utils.py +++ b/qiskit/transpiler/passes/layout/disjoint_utils.py @@ -13,7 +13,7 @@ """This module contains common utils for disjoint coupling maps.""" from collections import defaultdict -from typing import List, Callable, TypeVar, Dict +from typing import List, Callable, TypeVar, Dict, Union import uuid import rustworkx as rx @@ -22,6 +22,7 @@ from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.dagcircuit.dagnode import DAGOutNode from qiskit.transpiler.coupling import CouplingMap +from qiskit.transpiler.target import Target from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes.layout import vf2_utils @@ -30,13 +31,22 @@ def run_pass_over_connected_components( dag: DAGCircuit, - coupling_map: CouplingMap, + components_source: Union[Target, CouplingMap], run_func: Callable[[DAGCircuit, CouplingMap], T], ) -> List[T]: """Run a transpiler pass inner function over mapped components.""" + if isinstance(components_source, Target): + coupling_map = components_source.build_coupling_map(filter_idle_qubits=True) + else: + coupling_map = components_source cmap_components = coupling_map.connected_components() # If graph is connected we only need to run the pass once if len(cmap_components) == 1: + if dag.num_qubits() > cmap_components[0].size(): + raise TranspilerError( + "A connected component of the DAGCircuit is too large for any of the connected " + "components in the coupling map." + ) return [run_func(dag, cmap_components[0])] dag_components = separate_dag(dag) mapped_components = map_components(dag_components, cmap_components) @@ -127,9 +137,15 @@ def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True): node.op.label = None -def require_layout_isolated_to_component(dag: DAGCircuit, coupling_map: CouplingMap) -> bool: +def require_layout_isolated_to_component( + dag: DAGCircuit, components_source: Union[Target, CouplingMap] +) -> bool: """Check that the layout of the dag does not require connectivity across connected components in the CouplingMap""" + if isinstance(components_source, Target): + coupling_map = components_source.build_coupling_map(filter_idle_qubits=True) + else: + coupling_map = components_source qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} component_sets = [set(x.graph.nodes()) for x in coupling_map.connected_components()] for inst in dag.two_qubit_ops(): diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 749751fdba5e..e4ad778e93ed 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -217,8 +217,18 @@ def run(self, dag): self.routing_pass.fake_run = False return dag # Combined + if self.target is not None: + # This is a special case SABRE only works with a bidirectional coupling graph + # which we explicitly can't create from the target. So do this manually here + # to avoid altering the shared state with the unfiltered indices. + target = self.target.build_coupling_map(filter_idle_qubits=True) + target.make_symmetric() + else: + target = self.coupling_map layout_components = disjoint_utils.run_pass_over_connected_components( - dag, self.coupling_map, self._inner_run + dag, + target, + self._inner_run, ) initial_layout_dict = {} final_layout_dict = {} diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index c5f17d9192b4..9eb2b4d68284 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -14,6 +14,7 @@ """VF2Layout pass to find a layout using subgraph isomorphism""" import os from enum import Enum +import itertools import logging import time @@ -141,6 +142,14 @@ def run(self, dag): cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph( self.coupling_map, self.seed, self.strict_direction ) + # 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: + has_operations = set(itertools.chain.from_iterable(self.target.qargs)) + to_remove = set(range(len(cm_nodes))).difference(has_operations) + if to_remove: + cm_graph.remove_nodes_from([cm_nodes[i] for i in to_remove]) + # To avoid trying to over optimize the result by default limit the number # of trials based on the size of the graphs. For circuits with simple layouts # like an all 1q circuit we don't want to sit forever trying every possible @@ -239,6 +248,10 @@ def mapping_to_layout(layout_mapping): reverse_im_graph_node_map, self.avg_error_map, ) + # No free qubits for free qubit mapping + if chosen_layout is None: + self.property_set["VF2Layout_stop_reason"] = VF2LayoutStopReason.NO_SOLUTION_FOUND + return self.property_set["layout"] = chosen_layout for reg in dag.qregs.values(): self.property_set["layout"].add_register(reg) diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py index 19570d6c4908..96ffc745b451 100644 --- a/qiskit/transpiler/passes/layout/vf2_post_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py @@ -16,6 +16,7 @@ from enum import Enum import logging import inspect +import itertools import time from rustworkx import PyDiGraph, vf2_mapping, PyGraph @@ -215,6 +216,16 @@ def run(self, dag): ops.update(global_ops[2]) cm_graph.add_edge(qargs[0], qargs[1], ops) cm_nodes = list(cm_graph.node_indexes()) + # Filter qubits without any supported operations. If they + # don't support any operations, they're not valid for layout selection. + # This is only needed in the undirected case because in strict direction + # mode the node matcher will not match since none of the circuit ops + # will match the cmap ops. + if not self.strict_direction: + has_operations = set(itertools.chain.from_iterable(self.target.qargs)) + to_remove = set(cm_graph.node_indices()).difference(has_operations) + if to_remove: + cm_graph.remove_nodes_from(list(to_remove)) else: cm_graph, cm_nodes = vf2_utils.shuffle_coupling_graph( self.coupling_map, self.seed, self.strict_direction diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index 75557732fc8b..ba8c250208c4 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -233,6 +233,8 @@ def map_free_qubits( set(range(num_physical_qubits)) - partial_layout.get_physical_bits().keys() ) for im_index in sorted(free_nodes, key=lambda x: sum(free_nodes[x].values())): + if not free_qubits: + return None selected_qubit = free_qubits.pop(0) partial_layout.add(reverse_bit_map[im_index], selected_qubit) return partial_layout diff --git a/qiskit/transpiler/passes/routing/basic_swap.py b/qiskit/transpiler/passes/routing/basic_swap.py index f286015b7b3d..0cae1d8651d3 100644 --- a/qiskit/transpiler/passes/routing/basic_swap.py +++ b/qiskit/transpiler/passes/routing/basic_swap.py @@ -72,8 +72,9 @@ def run(self, dag): if len(dag.qubits) > len(self.coupling_map.physical_qubits): raise TranspilerError("The layout does not match the amount of qubits in the DAG") - disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map) - + disjoint_utils.require_layout_isolated_to_component( + dag, self.coupling_map if self.target is None else self.target + ) canonical_register = dag.qregs["q"] trivial_layout = Layout.generate_trivial_layout(canonical_register) current_layout = trivial_layout.copy() diff --git a/qiskit/transpiler/passes/routing/bip_mapping.py b/qiskit/transpiler/passes/routing/bip_mapping.py index e2e87f1761ca..a658c1c98eaa 100644 --- a/qiskit/transpiler/passes/routing/bip_mapping.py +++ b/qiskit/transpiler/passes/routing/bip_mapping.py @@ -167,7 +167,9 @@ def run(self, dag): "BIPMapping requires the number of virtual and physical qubits to be the same. " "Supply 'qubit_subset' to specify physical qubits to use." ) - disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map) + disjoint_utils.require_layout_isolated_to_component( + dag, self.coupling_map if self.target is None else self.target + ) original_dag = dag diff --git a/qiskit/transpiler/passes/routing/lookahead_swap.py b/qiskit/transpiler/passes/routing/lookahead_swap.py index 92c0fc5adf35..485ea76a8b50 100644 --- a/qiskit/transpiler/passes/routing/lookahead_swap.py +++ b/qiskit/transpiler/passes/routing/lookahead_swap.py @@ -130,7 +130,9 @@ def run(self, dag): f"The number of DAG qubits ({len(dag.qubits)}) is greater than the number of " f"available device qubits ({number_of_available_qubits})." ) - disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map) + disjoint_utils.require_layout_isolated_to_component( + dag, self.coupling_map if self.target is None else self.target + ) register = dag.qregs["q"] current_state = _SystemState( diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index 54693b68984d..a1d2f907f608 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -213,7 +213,9 @@ def run(self, dag): heuristic = Heuristic.Decay else: raise TranspilerError("Heuristic %s not recognized." % self.heuristic) - disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map) + disjoint_utils.require_layout_isolated_to_component( + dag, self.coupling_map if self.target is None else self.target + ) self.dist_matrix = self.coupling_map.distance_matrix diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index 2e65c0e90168..147dfc830b07 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -104,8 +104,9 @@ def run(self, dag): if len(dag.qubits) > len(self.coupling_map.physical_qubits): raise TranspilerError("The layout does not match the amount of qubits in the DAG") - - disjoint_utils.require_layout_isolated_to_component(dag, self.coupling_map) + disjoint_utils.require_layout_isolated_to_component( + dag, self.coupling_map if self.target is None else self.target + ) self.rng = np.random.default_rng(self.seed) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 2187ff014db8..b3306cde80e7 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -19,7 +19,7 @@ from __future__ import annotations - +import itertools import warnings from typing import Tuple, Union, Optional, Dict, List, Any @@ -996,7 +996,7 @@ def _build_coupling_graph(self): if self._coupling_graph.num_edges() == 0 and any(x is None for x in self._qarg_gate_map): self._coupling_graph = None - def build_coupling_map(self, two_q_gate=None): + def build_coupling_map(self, two_q_gate=None, filter_idle_qubits=False): """Get a :class:`~qiskit.transpiler.CouplingMap` from this target. If there is a mix of two qubit operations that have a connectivity @@ -1011,6 +1011,14 @@ def build_coupling_map(self, two_q_gate=None): the Target to generate the coupling map for. If specified the output coupling map will only have edges between qubits where this gate is present. + filter_idle_qubits (bool): If set to ``True`` the output :class:`~.CouplingMap` + will remove any qubits that don't have any operations defined in the + target. Note that using this argument will result in an output + :class:`~.CouplingMap` object which has holes in its indices + which might differ from the assumptions of the class. The typical use + case of this argument is to be paired with with + :meth:`.CouplingMap.connected_components` which will handle the holes + as expected. Returns: CouplingMap: The :class:`~qiskit.transpiler.CouplingMap` object for this target. If there are no connectivity constraints in @@ -1048,11 +1056,23 @@ def build_coupling_map(self, two_q_gate=None): # existing and return if self._coupling_graph is not None: cmap = CouplingMap() - cmap.graph = self._coupling_graph + if filter_idle_qubits: + cmap.graph = self._filter_coupling_graph() + else: + cmap.graph = self._coupling_graph return cmap else: return None + def _filter_coupling_graph(self): + has_operations = set(itertools.chain.from_iterable(self.qargs)) + graph = self._coupling_graph + to_remove = set(graph.node_indices()).difference(has_operations) + if to_remove: + graph = graph.copy() + graph.remove_nodes_from(list(to_remove)) + return graph + @property def physical_qubits(self): """Returns a sorted list of physical_qubits""" diff --git a/releasenotes/notes/filter-idle-qubits-cmap-74ac7711fc7476f3.yaml b/releasenotes/notes/filter-idle-qubits-cmap-74ac7711fc7476f3.yaml new file mode 100644 index 000000000000..c4a76457472f --- /dev/null +++ b/releasenotes/notes/filter-idle-qubits-cmap-74ac7711fc7476f3.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + The :meth:`~.Target.build_coupling_map` method has a new keyword argument, + ``filter_idle_qubits`` which when set to ``True`` will remove any qubits + from the output :class:`~.CouplingMap` that don't support any operations. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index b3b9f08411aa..db2df0db1eaf 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -45,6 +45,7 @@ CZGate, XGate, SXGate, + HGate, ) from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp from qiskit.circuit.measure import Measure @@ -2733,3 +2734,84 @@ def test_six_component_circuit_dense_layout(self, routing_method): if op_name == "barrier": continue self.assertIn(qubits, self.backend.target[op_name]) + + @data(0, 1, 2, 3) + def test_transpile_target_with_qubits_without_ops(self, opt_level): + """Test qubits without operations aren't ever used.""" + target = Target(num_qubits=5) + target.add_instruction(XGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)}) + target.add_instruction(HGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)}) + target.add_instruction( + CXGate(), {edge: InstructionProperties(error=0.5) for edge in [(0, 1), (1, 2), (2, 0)]} + ) + qc = QuantumCircuit(3) + qc.x(0) + qc.cx(0, 1) + qc.cx(0, 2) + tqc = transpile(qc, target=target, optimization_level=opt_level) + invalid_qubits = {3, 4} + self.assertEqual(tqc.num_qubits, 5) + for inst in tqc.data: + for bit in inst.qubits: + self.assertNotIn(tqc.find_bit(bit).index, invalid_qubits) + + @data(0, 1, 2, 3) + def test_transpile_target_with_qubits_without_ops_with_routing(self, opt_level): + """Test qubits without operations aren't ever used.""" + target = Target(num_qubits=5) + target.add_instruction(XGate(), {(i,): InstructionProperties(error=0.5) for i in range(4)}) + target.add_instruction(HGate(), {(i,): InstructionProperties(error=0.5) for i in range(4)}) + target.add_instruction( + CXGate(), + {edge: InstructionProperties(error=0.5) for edge in [(0, 1), (1, 2), (2, 0), (2, 3)]}, + ) + qc = QuantumCircuit(4) + qc.x(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(1, 3) + qc.cx(0, 3) + tqc = transpile(qc, target=target, optimization_level=opt_level) + invalid_qubits = { + 4, + } + self.assertEqual(tqc.num_qubits, 5) + for inst in tqc.data: + for bit in inst.qubits: + self.assertNotIn(tqc.find_bit(bit).index, invalid_qubits) + + @data(0, 1, 2, 3) + def test_transpile_target_with_qubits_without_ops_circuit_too_large(self, opt_level): + """Test qubits without operations aren't ever used and error if circuit needs them.""" + target = Target(num_qubits=5) + target.add_instruction(XGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)}) + target.add_instruction(HGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)}) + target.add_instruction( + CXGate(), {edge: InstructionProperties(error=0.5) for edge in [(0, 1), (1, 2), (2, 0)]} + ) + qc = QuantumCircuit(4) + qc.x(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + with self.assertRaises(TranspilerError): + transpile(qc, target=target, optimization_level=opt_level) + + @data(0, 1, 2, 3) + def test_transpile_target_with_qubits_without_ops_circuit_too_large_disconnected( + self, opt_level + ): + """Test qubits without operations aren't ever used if a disconnected circuit needs them.""" + target = Target(num_qubits=5) + target.add_instruction(XGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)}) + target.add_instruction(HGate(), {(i,): InstructionProperties(error=0.5) for i in range(3)}) + target.add_instruction( + CXGate(), {edge: InstructionProperties(error=0.5) for edge in [(0, 1), (1, 2), (2, 0)]} + ) + qc = QuantumCircuit(5) + qc.x(0) + qc.x(1) + qc.x(3) + qc.x(4) + with self.assertRaises(TranspilerError): + transpile(qc, target=target, optimization_level=opt_level) From 5128c6751fc2909131ab38c72358bb1e91c9fd84 Mon Sep 17 00:00:00 2001 From: Manoel Marques Date: Tue, 18 Apr 2023 18:39:24 -0400 Subject: [PATCH 050/172] Deprecate QuantumInstance and Opflow (#9176) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Deprecate QuantumInstance and Opflow * Add .. deprecated:: to init modules * Add link to url guide * Remove internal use of opflow * Fix black * Discard old changes in deprecation.py * Update decorators * Remove from docstring * Update sphinx deprecations, fix lint, fix tests * Remove hanging Deprecated in docstring * Remove old Deprecation messages * Revert "Remove old Deprecation messages" This reverts commit 6924130e12ffdfde971ff1e25434ee84bf983482. * Revert "Remove hanging Deprecated in docstring" This reverts commit cfb04f50e6a30a33955b57be4ab5bbdb9035923c. * Revert "Remove from docstring" This reverts commit 50e5954ecd4a729e7101e8ea4bf493240ae8b59e. * Change Deprecation to Deprecated * Remove catch warnings in main code * Fix missing decorators * Fix tests * Shorten message * Fix black * Shorten qi message * Remove checks for QDrift's internal use of opflow (changed in this PR) * Add warning catchers to unit tests * Remove opflow from qaoa ansatz tests * Remove opflow from non-related tests * Restore some opflow tests for completion * Fix black * Remove catch warnings * Refactor adaptVQE tests, remove opflow from observables evaluator * Fix tests, lint Fix tests, lint Fix CI failures Fix tests CI Fix unit tests Fix CI again Fix lint Restore vqd test Remove unused import * Delete qobj test to fix CI until #9322 is merged * Refactor gradients test warnings * Remove warning filter * Refactor rest of tests to assert warnings * Fix formatting * Restore unchanged unit tests Restore opflow test case Restore estimator test Restore unchanged tests * Go back to old opflow test setup * Revert "Refactor gradients test warnings", only keep qi assertWarns This reverts commit 9c0a37f3bc3068a9b1ccd122298cfefe556d4135. * Restore opflow unittests * Fix tests * Fix lint * Fix qaoa repeated test * Add module deprecation warning * Divide message in multiple lines * Fix lint * :mod:: -> :mod: * Deprecate Z2 symmetries --------- Co-authored-by: ElePT Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: woodsp-ibm --- qiskit/algorithms/observables_evaluator.py | 3 +- qiskit/circuit/library/n_local/qaoa_ansatz.py | 5 +- qiskit/opflow/__init__.py | 42 ++- qiskit/opflow/converters/__init__.py | 9 +- qiskit/opflow/converters/abelian_grouper.py | 10 +- qiskit/opflow/converters/circuit_sampler.py | 11 +- qiskit/opflow/converters/converter_base.py | 12 +- .../opflow/converters/dict_to_circuit_sum.py | 14 +- .../opflow/converters/pauli_basis_change.py | 10 +- .../opflow/converters/two_qubit_reduction.py | 12 +- qiskit/opflow/evolutions/__init__.py | 14 +- qiskit/opflow/evolutions/evolution_base.py | 12 +- qiskit/opflow/evolutions/evolution_factory.py | 11 +- qiskit/opflow/evolutions/evolved_op.py | 9 +- qiskit/opflow/evolutions/matrix_evolution.py | 12 +- .../evolutions/pauli_trotter_evolution.py | 13 +- .../evolutions/trotterizations/__init__.py | 6 +- .../evolutions/trotterizations/qdrift.py | 9 +- .../evolutions/trotterizations/suzuki.py | 9 +- .../evolutions/trotterizations/trotter.py | 9 +- .../trotterizations/trotterization_base.py | 11 +- .../trotterizations/trotterization_factory.py | 9 +- qiskit/opflow/exceptions.py | 13 +- qiskit/opflow/expectations/__init__.py | 15 +- .../expectations/aer_pauli_expectation.py | 12 +- .../opflow/expectations/cvar_expectation.py | 10 +- .../opflow/expectations/expectation_base.py | 12 +- .../expectations/expectation_factory.py | 20 +- .../opflow/expectations/matrix_expectation.py | 14 +- .../opflow/expectations/pauli_expectation.py | 10 +- qiskit/opflow/gradients/__init__.py | 15 +- .../gradients/circuit_gradients/__init__.py | 5 +- .../circuit_gradients/circuit_gradient.py | 13 +- .../gradients/circuit_gradients/lin_comb.py | 11 +- .../circuit_gradients/param_shift.py | 11 +- .../gradients/circuit_qfis/circuit_qfi.py | 12 +- .../gradients/circuit_qfis/lin_comb_full.py | 11 +- .../circuit_qfis/overlap_block_diag.py | 12 +- .../gradients/circuit_qfis/overlap_diag.py | 13 +- qiskit/opflow/gradients/derivative_base.py | 13 +- qiskit/opflow/gradients/gradient.py | 13 +- qiskit/opflow/gradients/gradient_base.py | 11 +- qiskit/opflow/gradients/hessian.py | 13 +- qiskit/opflow/gradients/hessian_base.py | 11 +- qiskit/opflow/gradients/natural_gradient.py | 9 +- qiskit/opflow/gradients/qfi.py | 13 +- qiskit/opflow/gradients/qfi_base.py | 12 +- qiskit/opflow/list_ops/__init__.py | 9 +- qiskit/opflow/list_ops/composed_op.py | 9 +- qiskit/opflow/list_ops/list_op.py | 11 +- qiskit/opflow/list_ops/summed_op.py | 9 +- qiskit/opflow/list_ops/tensored_op.py | 11 +- qiskit/opflow/mixins/star_algebra.py | 12 +- qiskit/opflow/mixins/tensor.py | 12 +- qiskit/opflow/operator_base.py | 10 +- qiskit/opflow/operator_globals.py | 9 +- qiskit/opflow/primitive_ops/__init__.py | 8 +- qiskit/opflow/primitive_ops/circuit_op.py | 10 +- qiskit/opflow/primitive_ops/matrix_op.py | 11 +- qiskit/opflow/primitive_ops/pauli_op.py | 11 +- qiskit/opflow/primitive_ops/pauli_sum_op.py | 10 +- qiskit/opflow/primitive_ops/primitive_op.py | 9 +- .../primitive_ops/tapered_pauli_sum_op.py | 15 +- qiskit/opflow/state_fns/__init__.py | 14 +- qiskit/opflow/state_fns/circuit_state_fn.py | 9 +- qiskit/opflow/state_fns/cvar_measurement.py | 9 +- qiskit/opflow/state_fns/dict_state_fn.py | 9 +- qiskit/opflow/state_fns/operator_state_fn.py | 9 +- .../state_fns/sparse_vector_state_fn.py | 9 +- qiskit/opflow/state_fns/state_fn.py | 9 +- qiskit/opflow/state_fns/vector_state_fn.py | 9 +- qiskit/opflow/utils.py | 21 +- qiskit/synthesis/evolution/qdrift.py | 3 +- qiskit/utils/backend_utils.py | 47 ++- qiskit/utils/measurement_error_mitigation.py | 25 +- qiskit/utils/mitigation/__init__.py | 7 +- qiskit/utils/mitigation/_filters.py | 15 +- qiskit/utils/mitigation/circuits.py | 21 +- qiskit/utils/mitigation/fitters.py | 13 +- qiskit/utils/quantum_instance.py | 9 +- qiskit/utils/run_circuits.py | 15 +- .../deprecate-opflow-qi-32f7e27884deea3f.yaml | 14 + .../evolvers/test_evolution_problem.py | 23 +- .../trotterization/test_trotter_qrte.py | 37 +- .../minimum_eigensolvers/test_adapt_vqe.py | 126 +++---- .../minimum_eigensolvers/test_qaoa.py | 36 +- .../minimum_eigensolvers/test_vqe.py | 66 ++-- .../optimizers/test_gradient_descent.py | 13 +- .../optimizers/test_optimizer_aqgd.py | 66 ++-- .../optimizers/test_optimizer_nft.py | 26 +- .../optimizers/test_optimizers_scikitquant.py | 33 +- .../python/algorithms/optimizers/test_spsa.py | 13 +- .../algorithms/test_amplitude_estimators.py | 116 ++++--- .../algorithms/test_aux_ops_evaluator.py | 61 ++-- test/python/algorithms/test_backendv1.py | 78 +++-- test/python/algorithms/test_backendv2.py | 50 ++- test/python/algorithms/test_grover.py | 100 ++++-- .../test_measure_error_mitigation.py | 244 +++++++------ .../algorithms/test_numpy_eigen_solver.py | 45 ++- .../test_numpy_minimum_eigen_solver.py | 62 +++- .../algorithms/test_observables_evaluator.py | 7 +- .../python/algorithms/test_phase_estimator.py | 210 ++++++----- test/python/algorithms/test_qaoa.py | 75 ++-- .../algorithms/test_skip_qobj_validation.py | 91 ++--- test/python/algorithms/test_vqd.py | 177 ++++++---- test/python/algorithms/test_vqe.py | 290 +++++++++------- .../test_scipy_imaginary_evolver.py | 17 +- .../algorithms/time_evolvers/test_pvqd.py | 6 +- .../test_time_evolution_problem.py | 14 +- .../time_evolvers/test_trotter_qrte.py | 21 +- .../circuit/library/test_evolution_gate.py | 82 +++-- .../circuit/library/test_evolved_op_ansatz.py | 53 +-- .../circuit/library/test_qaoa_ansatz.py | 43 +-- .../circuit/test_circuit_load_from_qpy.py | 44 ++- test/python/opflow/opflow_test_case.py | 11 +- .../opflow/test_aer_pauli_expectation.py | 139 ++++---- .../python/opflow/test_expectation_factory.py | 30 +- test/python/opflow/test_gradients.py | 140 ++++---- test/python/opflow/test_matrix_expectation.py | 88 +++-- test/python/opflow/test_pauli_expectation.py | 117 ++++--- .../python/opflow/test_state_op_meas_evals.py | 75 ++-- test/python/result/test_sampled_expval.py | 9 +- .../transpiler/test_swap_strategy_router.py | 32 +- test/python/utils/mitigation/test_meas.py | 326 +++++++++--------- 124 files changed, 2710 insertions(+), 1571 deletions(-) create mode 100644 releasenotes/notes/deprecate-opflow-qi-32f7e27884deea3f.yaml diff --git a/qiskit/algorithms/observables_evaluator.py b/qiskit/algorithms/observables_evaluator.py index 3e8f11dfd6e6..6d40239e229e 100644 --- a/qiskit/algorithms/observables_evaluator.py +++ b/qiskit/algorithms/observables_evaluator.py @@ -20,6 +20,7 @@ from qiskit import QuantumCircuit from qiskit.opflow import PauliSumOp +from qiskit.quantum_info import SparsePauliOp from .exceptions import AlgorithmError from .list_or_dict import ListOrDict from ..primitives import BaseEstimator @@ -88,7 +89,7 @@ def _handle_zero_ops( """Replaces all occurrence of operators equal to 0 in the list with an equivalent ``PauliSumOp`` operator.""" if observables_list: - zero_op = PauliSumOp.from_list([("I" * observables_list[0].num_qubits, 0)]) + zero_op = SparsePauliOp.from_list([("I" * observables_list[0].num_qubits, 0)]) for ind, observable in enumerate(observables_list): if observable == 0: observables_list[ind] = zero_op diff --git a/qiskit/circuit/library/n_local/qaoa_ansatz.py b/qiskit/circuit/library/n_local/qaoa_ansatz.py index c3aa8fc37adf..c8da8d271e2f 100644 --- a/qiskit/circuit/library/n_local/qaoa_ansatz.py +++ b/qiskit/circuit/library/n_local/qaoa_ansatz.py @@ -20,6 +20,7 @@ from qiskit.circuit.parametervector import ParameterVector from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import QuantumRegister +from qiskit.quantum_info import SparsePauliOp class QAOAAnsatz(EvolvedOperatorAnsatz): @@ -219,8 +220,6 @@ def mixer_operator(self): # if no mixer is passed and we know the number of qubits, then initialize it. if self.cost_operator is not None: # local imports to avoid circular imports - from qiskit.opflow import PauliSumOp - num_qubits = self.cost_operator.num_qubits # Mixer is just a sum of single qubit X's on each qubit. Evolving by this operator @@ -228,7 +227,7 @@ def mixer_operator(self): mixer_terms = [ ("I" * left + "X" + "I" * (num_qubits - left - 1), 1) for left in range(num_qubits) ] - mixer = PauliSumOp.from_list(mixer_terms) + mixer = SparsePauliOp.from_list(mixer_terms) return mixer # otherwise we cannot provide a default diff --git a/qiskit/opflow/__init__.py b/qiskit/opflow/__init__.py index 35fa212fcc65..9551dd3f318b 100644 --- a/qiskit/opflow/__init__.py +++ b/qiskit/opflow/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2020. +# (C) Copyright IBM 2019, 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 @@ -17,6 +17,12 @@ .. currentmodule:: qiskit.opflow +.. deprecated:: 0.24.0 + + The :mod:`qiskit.opflow` module is deprecated and will be removed no earlier + than 3 months after the release date. For code migration guidelines, + visit https://qisk.it/opflow_migration. + Operators and State functions are the building blocks of Quantum Algorithms. A library for Quantum Algorithms & Applications is more than a collection of @@ -77,8 +83,9 @@ Operator Base Class =================== -The OperatorBase serves as the base class for all Operators, State functions and measurements, and -enforces the presence and consistency of methods to manipulate these objects conveniently. +The OperatorBase serves as the base class for all Operators, State functions +and measurements, and enforces the presence and consistency of methods to manipulate these +objects conveniently. .. autosummary:: :toctree: ../stubs/ @@ -91,8 +98,8 @@ Operator Globals ================ -The :mod:`operator_globals` is a set of immutable Operator instances that are convenient building -blocks to reach for while working with the Operator flow. +The :mod:`operator_globals` is a set of immutable Operator instances that are +convenient building blocks to reach for while working with the Operator flow. One qubit Pauli operators: :attr:`X`, :attr:`Y`, :attr:`Z`, :attr:`I` @@ -109,8 +116,8 @@ Operators --------- -The Operators submodules include the PrimitiveOp, ListOp, and StateFn class groups which -represent the primary Operator modules. +The Operators submodules include the PrimitiveOp, ListOp, and StateFn class +groups which represent the primary Operator modules. .. autosummary:: :toctree: ../stubs/ @@ -123,12 +130,12 @@ Converters ---------- -The Converter submodules include objects which manipulate Operators, usually recursing over an -Operator structure and changing certain Operators' representation. For example, the -:class:`~.expectations.PauliExpectation` traverses an Operator structure, and replaces all of the -:class:`~.state_fns.OperatorStateFn` measurements containing non-diagonal Pauli terms into -diagonalizing circuits following by :class:`~.state_fns.OperatorStateFn` measurement containing -only diagonal Paulis. +The Converter submodules include objects which manipulate Operators, +usually recursing over an Operator structure and changing certain Operators' representation. +For example, the :class:`~.expectations.PauliExpectation` traverses an Operator structure, and +replaces all of the :class:`~.state_fns.OperatorStateFn` measurements containing non-diagonal +Pauli terms into diagonalizing circuits following by :class:`~.state_fns.OperatorStateFn` +measurement containing only diagonal Paulis. .. autosummary:: :toctree: ../stubs/ @@ -158,6 +165,7 @@ OpflowError """ +import warnings # New Operators from .operator_base import OperatorBase @@ -320,3 +328,11 @@ "anti_commutator", "double_commutator", ] + +warnings.warn( + "The ``qiskit.opflow`` module is deprecated as of qiskit-terra 0.24.0. " + "It will be removed no earlier than 3 months after the release date. " + "For code migration guidelines, visit https://qisk.it/opflow_migration.", + category=DeprecationWarning, + stacklevel=2, +) diff --git a/qiskit/opflow/converters/__init__.py b/qiskit/opflow/converters/__init__.py index 14272be076fd..8c2236eaec77 100644 --- a/qiskit/opflow/converters/__init__.py +++ b/qiskit/opflow/converters/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -16,6 +16,12 @@ .. currentmodule:: qiskit.opflow.converters +.. deprecated:: 0.24.0 + + The :mod:`qiskit.opflow` module is deprecated and will be removed no earlier + than 3 months after the release date. For code migration guidelines, + visit https://qisk.it/opflow_migration. + Converters are objects which manipulate Operators, usually traversing an Operator to change certain sub-Operators into a desired representation. Often the converted Operator is isomorphic or approximate to the original Operator in some way, but not always. For example, @@ -31,6 +37,7 @@ exponential in the number of qubits unless a clever trick is known (such as the use of sparse matrices). + Note: Not all converters are in this module, as :mod:`~qiskit.opflow.expectations` and :mod:`~qiskit.opflow.evolutions` are also converters. diff --git a/qiskit/opflow/converters/abelian_grouper.py b/qiskit/opflow/converters/abelian_grouper.py index 1b8b516166a3..fa1d1842a8ed 100644 --- a/qiskit/opflow/converters/abelian_grouper.py +++ b/qiskit/opflow/converters/abelian_grouper.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -27,10 +27,11 @@ from qiskit.opflow.primitive_ops.pauli_op import PauliOp from qiskit.opflow.primitive_ops.pauli_sum_op import PauliSumOp from qiskit.opflow.state_fns.operator_state_fn import OperatorStateFn +from qiskit.utils.deprecation import deprecate_func class AbelianGrouper(ConverterBase): - """The AbelianGrouper converts SummedOps into a sum of Abelian sums. + """Deprecated: The AbelianGrouper converts SummedOps into a sum of Abelian sums. Meaning, it will traverse the Operator, and when it finds a SummedOp, it will evaluate which of the summed sub-Operators commute with one another. It will then convert each of the groups of @@ -41,12 +42,17 @@ class AbelianGrouper(ConverterBase): diagonalized together. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, traverse: bool = True) -> None: """ Args: traverse: Whether to convert only the Operator passed to ``convert``, or traverse down that Operator. """ + super().__init__() self._traverse = traverse def convert(self, operator: OperatorBase) -> OperatorBase: diff --git a/qiskit/opflow/converters/circuit_sampler.py b/qiskit/opflow/converters/circuit_sampler.py index e3641b0c8d6d..e8e5938617e7 100644 --- a/qiskit/opflow/converters/circuit_sampler.py +++ b/qiskit/opflow/converters/circuit_sampler.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -32,13 +32,14 @@ from qiskit.providers import Backend from qiskit.utils.backend_utils import is_aer_provider, is_statevector_backend from qiskit.utils.quantum_instance import QuantumInstance +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) class CircuitSampler(ConverterBase): """ - The CircuitSampler traverses an Operator and converts any CircuitStateFns into + Deprecated: The CircuitSampler traverses an Operator and converts any CircuitStateFns into approximations of the state function by a DictStateFn or VectorStateFn using a quantum backend. Note that in order to approximate the value of the CircuitStateFn, it must 1) send state function through a depolarizing channel, which will destroy all phase information and @@ -51,6 +52,10 @@ class CircuitSampler(ConverterBase): you are better off using a different CircuitSampler for each Operator to avoid cache thrashing. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, backend: Union[Backend, QuantumInstance], @@ -76,6 +81,8 @@ def __init__( Raises: ValueError: Set statevector or param_qobj True when not supported by backend. """ + super().__init__() + self._quantum_instance = ( backend if isinstance(backend, QuantumInstance) else QuantumInstance(backend=backend) ) diff --git a/qiskit/opflow/converters/converter_base.py b/qiskit/opflow/converters/converter_base.py index 996052c872d7..231f3f2daad7 100644 --- a/qiskit/opflow/converters/converter_base.py +++ b/qiskit/opflow/converters/converter_base.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -15,11 +15,12 @@ from abc import ABC, abstractmethod from qiskit.opflow.operator_base import OperatorBase +from qiskit.utils.deprecation import deprecate_func class ConverterBase(ABC): r""" - Converters take an Operator and return a new Operator, generally isomorphic + Deprecated: Converters take an Operator and return a new Operator, generally isomorphic in some way with the first, but with certain desired properties. For example, a converter may accept ``CircuitOp`` and return a ``SummedOp`` of ``PauliOps`` representing the circuit unitary. Converters may not @@ -29,6 +30,13 @@ class ConverterBase(ABC): in the number of qubits unless a clever trick is known (such as the use of sparse matrices).""" + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + pass + @abstractmethod def convert(self, operator: OperatorBase) -> OperatorBase: """Accept the Operator and return the converted Operator diff --git a/qiskit/opflow/converters/dict_to_circuit_sum.py b/qiskit/opflow/converters/dict_to_circuit_sum.py index a4582ad08d66..b52f16f185c6 100644 --- a/qiskit/opflow/converters/dict_to_circuit_sum.py +++ b/qiskit/opflow/converters/dict_to_circuit_sum.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -18,16 +18,21 @@ from qiskit.opflow.state_fns.circuit_state_fn import CircuitStateFn from qiskit.opflow.state_fns.dict_state_fn import DictStateFn from qiskit.opflow.state_fns.vector_state_fn import VectorStateFn +from qiskit.utils.deprecation import deprecate_func class DictToCircuitSum(ConverterBase): r""" - Converts ``DictStateFns`` or ``VectorStateFns`` to equivalent ``CircuitStateFns`` or sums - thereof. The behavior of this class can be mostly replicated by calling ``to_circuit_op`` on - an Operator, but with the added control of choosing whether to convert only ``DictStateFns`` + Deprecated: Converts ``DictStateFns`` or ``VectorStateFns`` to equivalent ``CircuitStateFns`` + or sums thereof. The behavior of this class can be mostly replicated by calling ``to_circuit_op`` + on an Operator, but with the added control of choosing whether to convert only ``DictStateFns`` or ``VectorStateFns``, rather than both. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, traverse: bool = True, convert_dicts: bool = True, convert_vectors: bool = True ) -> None: @@ -38,6 +43,7 @@ def __init__( convert_dicts: Whether to convert VectorStateFn. convert_vectors: Whether to convert DictStateFns. """ + super().__init__() self._traverse = traverse self._convert_dicts = convert_dicts self._convert_vectors = convert_vectors diff --git a/qiskit/opflow/converters/pauli_basis_change.py b/qiskit/opflow/converters/pauli_basis_change.py index 325f3e875992..5e432e70ffd3 100644 --- a/qiskit/opflow/converters/pauli_basis_change.py +++ b/qiskit/opflow/converters/pauli_basis_change.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -30,11 +30,12 @@ from qiskit.opflow.state_fns.operator_state_fn import OperatorStateFn from qiskit.opflow.state_fns.state_fn import StateFn from qiskit.quantum_info import Pauli +from qiskit.utils.deprecation import deprecate_func class PauliBasisChange(ConverterBase): r""" - Converter for changing Paulis into other bases. By default, the diagonal basis + Deprecated: Converter for changing Paulis into other bases. By default, the diagonal basis composed only of Pauli {Z, I}^n is used as the destination basis to which to convert. Meaning, if a Pauli containing X or Y terms is passed in, which cannot be sampled or evolved natively on some Quantum hardware, the Pauli can be replaced by a @@ -55,6 +56,10 @@ class PauliBasisChange(ConverterBase): this method, such as the placement of the CNOT chains. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, destination_basis: Optional[Union[Pauli, PauliOp]] = None, @@ -83,6 +88,7 @@ def __init__( beginning and ending operators are equivalent. """ + super().__init__() if destination_basis is not None: self.destination = destination_basis # type: ignore else: diff --git a/qiskit/opflow/converters/two_qubit_reduction.py b/qiskit/opflow/converters/two_qubit_reduction.py index 971f09cf880b..10f596f10494 100644 --- a/qiskit/opflow/converters/two_qubit_reduction.py +++ b/qiskit/opflow/converters/two_qubit_reduction.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -20,14 +20,15 @@ from qiskit.opflow.primitive_ops.pauli_sum_op import PauliSumOp from qiskit.opflow.primitive_ops.tapered_pauli_sum_op import Z2Symmetries from qiskit.quantum_info import Pauli +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) class TwoQubitReduction(ConverterBase): """ - Two qubit reduction converter which eliminates the central and last qubit in a list of Pauli - that has diagonal operators (Z,I) at those positions. + Deprecated: Two qubit reduction converter which eliminates the central and last + qubit in a list of Pauli that has diagonal operators (Z,I) at those positions. Chemistry specific method: It can be used to taper two qubits in parity and binary-tree mapped @@ -35,12 +36,17 @@ class TwoQubitReduction(ConverterBase): sectors, (block spin order) according to the number of particles in the system. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, num_particles: Union[int, List[int], Tuple[int, int]]): """ Args: num_particles: number of particles, if it is a list, the first number is alpha and the second number if beta. """ + super().__init__() if isinstance(num_particles, (tuple, list)): num_alpha = num_particles[0] num_beta = num_particles[1] diff --git a/qiskit/opflow/evolutions/__init__.py b/qiskit/opflow/evolutions/__init__.py index 907781c0d100..6bfeb7d1490d 100644 --- a/qiskit/opflow/evolutions/__init__.py +++ b/qiskit/opflow/evolutions/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -16,8 +16,15 @@ .. currentmodule:: qiskit.opflow.evolutions -Evolutions are converters which traverse an Operator tree, replacing any :class:`EvolvedOp` `e` -with a Schrodinger equation-style evolution :class:`~qiskit.opflow.primitive_ops.CircuitOp` +.. deprecated:: 0.24.0 + + The :mod:`qiskit.opflow` module is deprecated and will be removed no earlier + than 3 months after the release date. For code migration guidelines, + visit https://qisk.it/opflow_migration. + +Evolutions are converters which traverse an Operator tree, replacing +any :class:`EvolvedOp` `e` with a Schrodinger equation-style evolution +:class:`~qiskit.opflow.primitive_ops.CircuitOp` equalling or approximating the matrix exponential of -i * the Operator contained inside (`e.primitive`). The Evolutions are essentially implementations of Hamiltonian Simulation algorithms, including various methods for Trotterization. @@ -28,6 +35,7 @@ ``.exp_i()`` methods which either return the exponential of the Operator directly, or an :class:`EvolvedOp` containing the Operator. + Note: Evolutions work with parameterized Operator coefficients, so ``my_expectation.convert((t * H).exp_i())``, where t is a scalar or Terra Parameter and H diff --git a/qiskit/opflow/evolutions/evolution_base.py b/qiskit/opflow/evolutions/evolution_base.py index e25edfd91c50..1123c36c34a7 100644 --- a/qiskit/opflow/evolutions/evolution_base.py +++ b/qiskit/opflow/evolutions/evolution_base.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -16,11 +16,12 @@ from qiskit.opflow.operator_base import OperatorBase from qiskit.opflow.converters.converter_base import ConverterBase +from qiskit.utils.deprecation import deprecate_func class EvolutionBase(ConverterBase, ABC): r""" - A base for Evolution converters. + Deprecated: A base for Evolution converters. Evolutions are converters which traverse an Operator tree, replacing any ``EvolvedOp`` `e` with a Schrodinger equation-style evolution ``CircuitOp`` equalling or approximating the matrix exponential of -i * the Operator contained inside (`e.primitive`). The Evolutions are @@ -29,6 +30,13 @@ class EvolutionBase(ConverterBase, ABC): """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + super().__init__() + @abstractmethod def convert(self, operator: OperatorBase) -> OperatorBase: """Traverse the operator, replacing any ``EvolutionOps`` with their equivalent evolution diff --git a/qiskit/opflow/evolutions/evolution_factory.py b/qiskit/opflow/evolutions/evolution_factory.py index a4136edfe0bf..fffad684e816 100644 --- a/qiskit/opflow/evolutions/evolution_factory.py +++ b/qiskit/opflow/evolutions/evolution_factory.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -16,14 +16,19 @@ from qiskit.opflow.evolutions.evolution_base import EvolutionBase from qiskit.opflow.evolutions.pauli_trotter_evolution import PauliTrotterEvolution from qiskit.opflow.evolutions.matrix_evolution import MatrixEvolution +from qiskit.utils.deprecation import deprecate_func class EvolutionFactory: - """A factory class for convenient automatic selection of an Evolution algorithm based on the - Operator to be converted. + """Deprecated: A factory class for convenient automatic selection of an + Evolution algorithm based on the Operator to be converted. """ @staticmethod + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def build(operator: OperatorBase = None) -> EvolutionBase: r""" A factory method for convenient automatic selection of an Evolution algorithm based on the diff --git a/qiskit/opflow/evolutions/evolved_op.py b/qiskit/opflow/evolutions/evolved_op.py index 13a3cb13f77f..03771aa52fa8 100644 --- a/qiskit/opflow/evolutions/evolved_op.py +++ b/qiskit/opflow/evolutions/evolved_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -27,11 +27,12 @@ from qiskit.opflow.primitive_ops.matrix_op import MatrixOp from qiskit.opflow.primitive_ops.primitive_op import PrimitiveOp from qiskit.quantum_info import Statevector +from qiskit.utils.deprecation import deprecate_func class EvolvedOp(PrimitiveOp): r""" - Class for wrapping Operator Evolutions for compilation (``convert``) by an EvolutionBase + Deprecated: Class for wrapping Operator Evolutions for compilation (``convert``) by an EvolutionBase method later, essentially acting as a placeholder. Note that EvolvedOp is a weird case of PrimitiveOp. It happens to be that it fits into the PrimitiveOp interface nearly perfectly, and it essentially represents a placeholder for a PrimitiveOp later, even though it doesn't @@ -39,6 +40,10 @@ class EvolvedOp(PrimitiveOp): but would have ended up copying and pasting a lot of code from PrimitiveOp.""" primitive: PrimitiveOp + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: OperatorBase, coeff: Union[complex, ParameterExpression] = 1.0 ) -> None: diff --git a/qiskit/opflow/evolutions/matrix_evolution.py b/qiskit/opflow/evolutions/matrix_evolution.py index 0d64477079ab..8c18a6a57728 100644 --- a/qiskit/opflow/evolutions/matrix_evolution.py +++ b/qiskit/opflow/evolutions/matrix_evolution.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -20,16 +20,24 @@ from qiskit.opflow.operator_base import OperatorBase from qiskit.opflow.primitive_ops.matrix_op import MatrixOp from qiskit.opflow.primitive_ops.pauli_op import PauliOp +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) class MatrixEvolution(EvolutionBase): r""" - Performs Evolution by classical matrix exponentiation, constructing a circuit with + Deprecated: Performs Evolution by classical matrix exponentiation, constructing a circuit with ``UnitaryGates`` or ``HamiltonianGates`` containing the exponentiation of the Operator. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + super().__init__() + def convert(self, operator: OperatorBase) -> OperatorBase: r""" Traverse the operator, replacing ``EvolvedOps`` with ``CircuitOps`` containing diff --git a/qiskit/opflow/evolutions/pauli_trotter_evolution.py b/qiskit/opflow/evolutions/pauli_trotter_evolution.py index ca7f096a19c8..414ffd5777ca 100644 --- a/qiskit/opflow/evolutions/pauli_trotter_evolution.py +++ b/qiskit/opflow/evolutions/pauli_trotter_evolution.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -32,6 +32,7 @@ from qiskit.opflow.primitive_ops.circuit_op import CircuitOp from qiskit.opflow.primitive_ops.pauli_sum_op import PauliSumOp from qiskit.opflow.primitive_ops.primitive_op import PrimitiveOp +from qiskit.utils.deprecation import deprecate_func # TODO uncomment when we implement Abelian grouped evolution. # from qiskit.opflow.converters.abelian_grouper import AbelianGrouper @@ -41,8 +42,8 @@ class PauliTrotterEvolution(EvolutionBase): r""" - An Evolution algorithm replacing exponentiated sums of Paulis by changing them each to the - Z basis, rotating with an rZ, changing back, and Trotterizing. + Deprecated: An Evolution algorithm replacing exponentiated sums of Paulis by changing + them each to the Z basis, rotating with an rZ, changing back, and Trotterizing. More specifically, we compute basis change circuits for each Pauli into a single-qubit Z, evolve the Z by the desired evolution time with an rZ gate, and change the basis back using @@ -50,6 +51,10 @@ class PauliTrotterEvolution(EvolutionBase): evolution circuits are composed together by Trotterization scheme. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, trotter_mode: Optional[Union[str, TrotterizationBase]] = "trotter", @@ -69,7 +74,7 @@ def __init__( # sub-groups, so a single diagonalization circuit can be used for each group # rather than each Pauli. """ - + super().__init__() if isinstance(trotter_mode, TrotterizationBase): self._trotter = trotter_mode else: diff --git a/qiskit/opflow/evolutions/trotterizations/__init__.py b/qiskit/opflow/evolutions/trotterizations/__init__.py index f177f5ce36c6..a721bec2fee4 100644 --- a/qiskit/opflow/evolutions/trotterizations/__init__.py +++ b/qiskit/opflow/evolutions/trotterizations/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -11,8 +11,8 @@ # that they have been altered from the originals. """ -Trotterization methods - Algorithms for approximating Exponentials of Operator Sums. - +Trotterization methods - Algorithms for +approximating Exponentials of Operator Sums. """ from .trotterization_base import TrotterizationBase diff --git a/qiskit/opflow/evolutions/trotterizations/qdrift.py b/qiskit/opflow/evolutions/trotterizations/qdrift.py index 89c6ced0b144..865cbd1071ae 100644 --- a/qiskit/opflow/evolutions/trotterizations/qdrift.py +++ b/qiskit/opflow/evolutions/trotterizations/qdrift.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 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 @@ -26,16 +26,21 @@ from qiskit.opflow.primitive_ops.pauli_sum_op import PauliSumOp from qiskit.opflow.primitive_ops.primitive_op import PrimitiveOp from qiskit.utils import algorithm_globals +from qiskit.utils.deprecation import deprecate_func # pylint: disable=invalid-name class QDrift(TrotterizationBase): - """The QDrift Trotterization method, which selects each each term in the + """Deprecated: The QDrift Trotterization method, which selects each each term in the Trotterization randomly, with a probability proportional to its weight. Based on the work of Earl Campbell in https://arxiv.org/abs/1811.08017. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, reps: int = 1) -> None: r""" Args: diff --git a/qiskit/opflow/evolutions/trotterizations/suzuki.py b/qiskit/opflow/evolutions/trotterizations/suzuki.py index 964102f6b2d9..cfe59acac5a8 100644 --- a/qiskit/opflow/evolutions/trotterizations/suzuki.py +++ b/qiskit/opflow/evolutions/trotterizations/suzuki.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -23,17 +23,22 @@ from qiskit.opflow.operator_base import OperatorBase from qiskit.opflow.primitive_ops.pauli_sum_op import PauliSumOp from qiskit.opflow.primitive_ops.primitive_op import PrimitiveOp +from qiskit.utils.deprecation import deprecate_func class Suzuki(TrotterizationBase): r""" - Suzuki Trotter expansion, composing the evolution circuits of each Operator in the sum + Deprecated: Suzuki Trotter expansion, composing the evolution circuits of each Operator in the sum together by a recursive "bookends" strategy, repeating the whole composed circuit ``reps`` times. Detailed in https://arxiv.org/pdf/quant-ph/0508139.pdf. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, reps: int = 1, order: int = 2) -> None: """ Args: diff --git a/qiskit/opflow/evolutions/trotterizations/trotter.py b/qiskit/opflow/evolutions/trotterizations/trotter.py index 2fc0f50d4611..eb1c48d7e27d 100644 --- a/qiskit/opflow/evolutions/trotterizations/trotter.py +++ b/qiskit/opflow/evolutions/trotterizations/trotter.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -13,14 +13,19 @@ """Trotter Class""" from qiskit.opflow.evolutions.trotterizations.suzuki import Suzuki +from qiskit.utils.deprecation import deprecate_func class Trotter(Suzuki): r""" - Simple Trotter expansion, composing the evolution circuits of each Operator in the sum + Deprecated: Simple Trotter expansion, composing the evolution circuits of each Operator in the sum together ``reps`` times and dividing the evolution time of each by ``reps``. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, reps: int = 1) -> None: r""" Args: diff --git a/qiskit/opflow/evolutions/trotterizations/trotterization_base.py b/qiskit/opflow/evolutions/trotterizations/trotterization_base.py index c62c431f2b82..222d338dfdce 100644 --- a/qiskit/opflow/evolutions/trotterizations/trotterization_base.py +++ b/qiskit/opflow/evolutions/trotterizations/trotterization_base.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -16,17 +16,22 @@ from qiskit.opflow.evolutions.evolution_base import EvolutionBase from qiskit.opflow.operator_base import OperatorBase +from qiskit.utils.deprecation import deprecate_func # TODO centralize handling of commuting groups class TrotterizationBase(EvolutionBase): - """A base for Trotterization methods, algorithms for approximating exponentiations of + """Deprecated: A base for Trotterization methods, algorithms for approximating exponentiations of operator sums by compositions of exponentiations. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, reps: int = 1) -> None: - + super().__init__() self._reps = reps @property diff --git a/qiskit/opflow/evolutions/trotterizations/trotterization_factory.py b/qiskit/opflow/evolutions/trotterizations/trotterization_factory.py index 9c0d7d830cb3..f8d119140502 100644 --- a/qiskit/opflow/evolutions/trotterizations/trotterization_factory.py +++ b/qiskit/opflow/evolutions/trotterizations/trotterization_factory.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -16,12 +16,17 @@ from qiskit.opflow.evolutions.trotterizations.suzuki import Suzuki from qiskit.opflow.evolutions.trotterizations.trotter import Trotter from qiskit.opflow.evolutions.trotterizations.trotterization_base import TrotterizationBase +from qiskit.utils.deprecation import deprecate_func class TrotterizationFactory: - """A factory for conveniently creating TrotterizationBase instances.""" + """Deprecated: A factory for conveniently creating TrotterizationBase instances.""" @staticmethod + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def build(mode: str = "trotter", reps: int = 1) -> TrotterizationBase: """A factory for conveniently creating TrotterizationBase instances. diff --git a/qiskit/opflow/exceptions.py b/qiskit/opflow/exceptions.py index bf80dd3d22c6..27bc0f6cc14d 100644 --- a/qiskit/opflow/exceptions.py +++ b/qiskit/opflow/exceptions.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2018. +# (C) Copyright IBM 2017, 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 @@ -13,9 +13,16 @@ """Exception for errors raised by Opflow module.""" from qiskit.exceptions import QiskitError +from qiskit.utils.deprecation import deprecate_func class OpflowError(QiskitError): - """For Opflow specific errors.""" + """Deprecated: For Opflow specific errors.""" - pass + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self, *message): + """Set the error message.""" + super().__init__(*message) diff --git a/qiskit/opflow/expectations/__init__.py b/qiskit/opflow/expectations/__init__.py index 8109dc0bad4c..885d4b7e36f6 100644 --- a/qiskit/opflow/expectations/__init__.py +++ b/qiskit/opflow/expectations/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -16,9 +16,15 @@ .. currentmodule:: qiskit.opflow.expectations -Expectations are converters which enable the computation of the expectation value of an -Observable with respect to some state function. They traverse an Operator tree, replacing -:class:`~qiskit.opflow.state_fns.OperatorStateFn` measurements with equivalent +.. deprecated:: 0.24.0 + + The :mod:`qiskit.opflow` module is deprecated and will be removed no earlier + than 3 months after the release date. For code migration guidelines, + visit https://qisk.it/opflow_migration. + +Expectations are converters which enable the computation of the expectation +value of an Observable with respect to some state function. They traverse an Operator tree, +replacing :class:`~qiskit.opflow.state_fns.OperatorStateFn` measurements with equivalent measurements which are more amenable to computation on quantum or classical hardware. For example, if one would like to measure the expectation value of an Operator ``o`` expressed as a sum of Paulis with respect to some state @@ -28,6 +34,7 @@ a :class:`~qiskit.opflow.converters.CircuitSampler`. All in all, this would be: ``my_sampler.convert(my_expect.convert(~StateFn(o)) @ my_state).eval()``. + Expectation Base Class ---------------------- diff --git a/qiskit/opflow/expectations/aer_pauli_expectation.py b/qiskit/opflow/expectations/aer_pauli_expectation.py index 511565a33f5c..44d8f6ebcb01 100644 --- a/qiskit/opflow/expectations/aer_pauli_expectation.py +++ b/qiskit/opflow/expectations/aer_pauli_expectation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -28,16 +28,24 @@ from qiskit.opflow.state_fns.circuit_state_fn import CircuitStateFn from qiskit.opflow.state_fns.operator_state_fn import OperatorStateFn from qiskit.quantum_info import SparsePauliOp +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) class AerPauliExpectation(ExpectationBase): - r"""An Expectation converter for using Aer's operator snapshot to + r"""Deprecated: An Expectation converter for using Aer's operator snapshot to take expectations of quantum state circuits over Pauli observables. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + super().__init__() + def convert(self, operator: OperatorBase) -> OperatorBase: """Accept an Operator and return a new Operator with the Pauli measurements replaced by AerSnapshot-based expectation circuits. diff --git a/qiskit/opflow/expectations/cvar_expectation.py b/qiskit/opflow/expectations/cvar_expectation.py index 641c17f9619f..9a6711c498ae 100644 --- a/qiskit/opflow/expectations/cvar_expectation.py +++ b/qiskit/opflow/expectations/cvar_expectation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -20,10 +20,11 @@ from qiskit.opflow.list_ops import ComposedOp, ListOp from qiskit.opflow.operator_base import OperatorBase from qiskit.opflow.state_fns import CVaRMeasurement, OperatorStateFn +from qiskit.utils.deprecation import deprecate_func class CVaRExpectation(ExpectationBase): - r"""Compute the Conditional Value at Risk (CVaR) expectation value. + r"""Deprecated: Compute the Conditional Value at Risk (CVaR) expectation value. The standard approach to calculating the expectation value of a Hamiltonian w.r.t. a state is to take the sample mean of the measurement outcomes. This corresponds to an estimator @@ -54,6 +55,10 @@ class CVaRExpectation(ExpectationBase): """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, alpha: float, expectation: Optional[ExpectationBase] = None) -> None: """ Args: @@ -64,6 +69,7 @@ def __init__(self, alpha: float, expectation: Optional[ExpectationBase] = None) Raises: NotImplementedError: If the ``expectation`` is an AerPauliExpecation. """ + super().__init__() self.alpha = alpha if isinstance(expectation, AerPauliExpectation): raise NotImplementedError("AerPauliExpecation currently not supported.") diff --git a/qiskit/opflow/expectations/expectation_base.py b/qiskit/opflow/expectations/expectation_base.py index 64a78832ea7f..593c43b83833 100644 --- a/qiskit/opflow/expectations/expectation_base.py +++ b/qiskit/opflow/expectations/expectation_base.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -19,11 +19,12 @@ from qiskit.opflow.converters import ConverterBase from qiskit.opflow.operator_base import OperatorBase +from qiskit.utils.deprecation import deprecate_func class ExpectationBase(ConverterBase): r""" - A base for Expectation value converters. Expectations are converters which enable the + Deprecated: A base for Expectation value converters. Expectations are converters which enable the computation of the expectation value of an Observable with respect to some state function. They traverse an Operator tree, replacing OperatorStateFn measurements with equivalent measurements which are more amenable to computation on quantum or classical hardware. For @@ -37,6 +38,13 @@ class ExpectationBase(ConverterBase): """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + super().__init__() + @abstractmethod def convert(self, operator: OperatorBase) -> OperatorBase: """Accept an Operator and return a new Operator with the measurements replaced by diff --git a/qiskit/opflow/expectations/expectation_factory.py b/qiskit/opflow/expectations/expectation_factory.py index cd3bc262fc20..58a72ed6daca 100644 --- a/qiskit/opflow/expectations/expectation_factory.py +++ b/qiskit/opflow/expectations/expectation_factory.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -22,18 +22,24 @@ from qiskit.opflow.expectations.pauli_expectation import PauliExpectation from qiskit.opflow.operator_base import OperatorBase from qiskit.providers import Backend -from qiskit.utils.backend_utils import has_aer, is_aer_qasm, is_statevector_backend -from qiskit.utils.quantum_instance import QuantumInstance +from qiskit.utils.backend_utils import is_aer_qasm, is_statevector_backend +from qiskit.utils import QuantumInstance, optionals +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) class ExpectationFactory: - """A factory class for convenient automatic selection of an Expectation based on the + + """Deprecated: factory class for convenient automatic selection of an Expectation based on the Operator to be converted and backend used to sample the expectation value. """ @staticmethod + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def build( operator: OperatorBase, backend: Optional[Union[Backend, QuantumInstance]] = None, @@ -68,10 +74,10 @@ def build( if backend_to_check is None: # If user has Aer but didn't specify a backend, use the Aer fast expectation - if has_aer(): - from qiskit import Aer + if optionals.HAS_AER: + from qiskit_aer import AerSimulator - backend_to_check = Aer.get_backend("qasm_simulator") + backend_to_check = AerSimulator() # If user doesn't have Aer, use statevector_simulator # for < 16 qubits, and qasm with warning for more. else: diff --git a/qiskit/opflow/expectations/matrix_expectation.py b/qiskit/opflow/expectations/matrix_expectation.py index b6979f02f96f..dcd2d64adb08 100644 --- a/qiskit/opflow/expectations/matrix_expectation.py +++ b/qiskit/opflow/expectations/matrix_expectation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -18,11 +18,19 @@ from qiskit.opflow.list_ops import ComposedOp, ListOp from qiskit.opflow.operator_base import OperatorBase from qiskit.opflow.state_fns.operator_state_fn import OperatorStateFn +from qiskit.utils.deprecation import deprecate_func class MatrixExpectation(ExpectationBase): - """An Expectation converter which converts Operator measurements to be matrix-based so they - can be evaluated by matrix multiplication.""" + """Deprecated: An Expectation converter which converts Operator measurements to + be matrix-based so they can be evaluated by matrix multiplication.""" + + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + super().__init__() def convert(self, operator: OperatorBase) -> OperatorBase: """Accept an Operator and return a new Operator with the Pauli measurements replaced by diff --git a/qiskit/opflow/expectations/pauli_expectation.py b/qiskit/opflow/expectations/pauli_expectation.py index 0bf31eff2682..0305fc90250a 100644 --- a/qiskit/opflow/expectations/pauli_expectation.py +++ b/qiskit/opflow/expectations/pauli_expectation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -27,13 +27,14 @@ from qiskit.opflow.primitive_ops.primitive_op import PrimitiveOp from qiskit.opflow.state_fns.operator_state_fn import OperatorStateFn from qiskit.opflow.state_fns.state_fn import StateFn +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) class PauliExpectation(ExpectationBase): r""" - An Expectation converter for Pauli-basis observables by changing Pauli measurements to a + Deprecated: An Expectation converter for Pauli-basis observables by changing Pauli measurements to a diagonal ({Z, I}^n) basis and appending circuit post-rotations to the measured state function. Optionally groups the Paulis with the same post-rotations (those that commute with one another, or form Abelian groups) into single measurements to reduce circuit execution @@ -41,6 +42,10 @@ class PauliExpectation(ExpectationBase): """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, group_paulis: bool = True) -> None: """ Args: @@ -48,6 +53,7 @@ def __init__(self, group_paulis: bool = True) -> None: have the same diagonalizing circuit. """ + super().__init__() self._grouper = AbelianGrouper() if group_paulis else None def convert(self, operator: OperatorBase) -> OperatorBase: diff --git a/qiskit/opflow/gradients/__init__.py b/qiskit/opflow/gradients/__init__.py index 01c4b02a62eb..9c2625d078ca 100644 --- a/qiskit/opflow/gradients/__init__.py +++ b/qiskit/opflow/gradients/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -14,13 +14,20 @@ Gradients (:mod:`qiskit.opflow.gradients`) ========================================== -Given an operator that represents either a quantum state resp. an expectation value, the gradient -framework enables the evaluation of gradients, natural gradients, Hessians, as well as the Quantum -Fisher Information. +.. deprecated:: 0.24.0 + + The :mod:`qiskit.opflow` module is deprecated and will be removed no earlier + than 3 months after the release date. For code migration guidelines, + visit https://qisk.it/opflow_migration. + +Given an operator that represents either a quantum state resp. an expectation value, +the gradient framework enables the evaluation of gradients, natural gradients, +Hessians, as well as the Quantum Fisher Information. Suppose a parameterized quantum state `|ψ(θ)〉 = V(θ)|ψ〉` with input state `|ψ〉` and parameterized Ansatz `V(θ)`, and an Operator `O(ω)`. + **Gradients** We want to compute one of: diff --git a/qiskit/opflow/gradients/circuit_gradients/__init__.py b/qiskit/opflow/gradients/circuit_gradients/__init__.py index eae08898049a..16953b57ff21 100644 --- a/qiskit/opflow/gradients/circuit_gradients/__init__.py +++ b/qiskit/opflow/gradients/circuit_gradients/__init__.py @@ -10,8 +10,9 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""The module for Aqua's first order derivatives.""" - +""" +The module for first order derivatives. +""" from .circuit_gradient import CircuitGradient from .lin_comb import LinComb from .param_shift import ParamShift diff --git a/qiskit/opflow/gradients/circuit_gradients/circuit_gradient.py b/qiskit/opflow/gradients/circuit_gradients/circuit_gradient.py index 576094758fb9..284cd7da7eb7 100644 --- a/qiskit/opflow/gradients/circuit_gradients/circuit_gradient.py +++ b/qiskit/opflow/gradients/circuit_gradients/circuit_gradient.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -17,12 +17,13 @@ from qiskit import QuantumCircuit, QiskitError, transpile from qiskit.circuit import ParameterExpression, ParameterVector +from qiskit.utils.deprecation import deprecate_func from ...converters.converter_base import ConverterBase from ...operator_base import OperatorBase class CircuitGradient(ConverterBase): - r"""Circuit to gradient operator converter. + r"""Deprecated: Circuit to gradient operator converter. Converter for changing parameterized circuits into operators whose evaluation yields the gradient with respect to the circuit parameters. @@ -35,6 +36,14 @@ class CircuitGradient(ConverterBase): DerivativeBase - uses classical techniques to differentiate operator flow data structures """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + super().__init__() + + # pylint: disable=arguments-differ @abstractmethod def convert( self, diff --git a/qiskit/opflow/gradients/circuit_gradients/lin_comb.py b/qiskit/opflow/gradients/circuit_gradients/lin_comb.py index 4db33fffb761..361d13112e85 100644 --- a/qiskit/opflow/gradients/circuit_gradients/lin_comb.py +++ b/qiskit/opflow/gradients/circuit_gradients/lin_comb.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 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 @@ -50,6 +50,7 @@ ZGate, ) from qiskit.quantum_info import partial_trace +from qiskit.utils.deprecation import deprecate_func from ...operator_base import OperatorBase from ...list_ops.list_op import ListOp from ...list_ops.composed_op import ComposedOp @@ -67,7 +68,7 @@ class LinComb(CircuitGradient): - """Compute the state gradient d⟨ψ(ω)|O(θ)|ψ(ω)〉/ dω respectively the gradients of the + """Deprecated: Compute the state gradient d⟨ψ(ω)|O(θ)|ψ(ω)〉/ dω respectively the gradients of the sampling probabilities of the basis states of a state |ψ(ω)〉w.r.t. ω. This method employs a linear combination of unitaries, @@ -99,7 +100,11 @@ class LinComb(CircuitGradient): "z", } - # pylint: disable=signature-differs + # pylint: disable=signature-differs, arguments-differ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, aux_meas_op: OperatorBase = Z): """ Args: diff --git a/qiskit/opflow/gradients/circuit_gradients/param_shift.py b/qiskit/opflow/gradients/circuit_gradients/param_shift.py index 435914f9f342..5329cc4c14c0 100644 --- a/qiskit/opflow/gradients/circuit_gradients/param_shift.py +++ b/qiskit/opflow/gradients/circuit_gradients/param_shift.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -22,6 +22,7 @@ from qiskit import QuantumCircuit from qiskit.circuit import Parameter, ParameterExpression, ParameterVector +from qiskit.utils.deprecation import deprecate_func from .circuit_gradient import CircuitGradient from ...operator_base import OperatorBase from ...state_fns.state_fn import StateFn @@ -39,13 +40,17 @@ class ParamShift(CircuitGradient): - """Compute the gradient d⟨ψ(ω)|O(θ)|ψ(ω)〉/ dω respectively the gradients of the sampling + """Deprecated: Compute the gradient d⟨ψ(ω)|O(θ)|ψ(ω)〉/ dω respectively the gradients of the sampling probabilities of the basis states of a state |ψ(ω)〉w.r.t. ω with the parameter shift method. """ SUPPORTED_GATES = {"x", "y", "z", "h", "rx", "ry", "rz", "p", "u", "cx", "cy", "cz"} + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, analytic: bool = True, epsilon: float = 1e-6): r""" Args: @@ -57,7 +62,7 @@ def __init__(self, analytic: bool = True, epsilon: float = 1e-6): Raises: ValueError: If method != ``fin_diff`` and ``epsilon`` is not None. """ - + super().__init__() self._analytic = analytic self._epsilon = epsilon diff --git a/qiskit/opflow/gradients/circuit_qfis/circuit_qfi.py b/qiskit/opflow/gradients/circuit_qfis/circuit_qfi.py index 02058f25e6a0..9a11d619e6c7 100644 --- a/qiskit/opflow/gradients/circuit_qfis/circuit_qfi.py +++ b/qiskit/opflow/gradients/circuit_qfis/circuit_qfi.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -16,12 +16,13 @@ from typing import List, Union from qiskit.circuit import ParameterExpression, ParameterVector +from qiskit.utils.deprecation import deprecate_func from ...converters.converter_base import ConverterBase from ...operator_base import OperatorBase class CircuitQFI(ConverterBase): - r"""Circuit to Quantum Fisher Information operator converter. + r"""Deprecated: Circuit to Quantum Fisher Information operator converter. Converter for changing parameterized circuits into operators whose evaluation yields Quantum Fisher Information metric tensor @@ -35,6 +36,13 @@ class CircuitQFI(ConverterBase): DerivativeBase - uses classical techniques to differentiate opflow data structures """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + super().__init__() + # pylint: disable=arguments-differ @abstractmethod def convert( diff --git a/qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py b/qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py index 81f00eb5cfc0..71f4eea3d8c2 100644 --- a/qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py +++ b/qiskit/opflow/gradients/circuit_qfis/lin_comb_full.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 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 @@ -17,7 +17,7 @@ import numpy as np from qiskit.circuit import QuantumCircuit, QuantumRegister, ParameterVector, ParameterExpression from qiskit.utils.arithmetic import triu_to_dense - +from qiskit.utils.deprecation import deprecate_func from ...operator_base import OperatorBase from ...list_ops.list_op import ListOp from ...list_ops.summed_op import SummedOp @@ -29,12 +29,17 @@ class LinCombFull(CircuitQFI): - r"""Compute the full Quantum Fisher Information (QFI). + r"""Deprecated: Compute the full Quantum Fisher Information (QFI). Given a pure, parameterized quantum state this class uses the linear combination of unitaries See also :class:`~qiskit.opflow.QFI`. """ + # pylint: disable=signature-differs, arguments-differ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, aux_meas_op: OperatorBase = Z, diff --git a/qiskit/opflow/gradients/circuit_qfis/overlap_block_diag.py b/qiskit/opflow/gradients/circuit_qfis/overlap_block_diag.py index 7daf6486685c..86b0bd1094cf 100644 --- a/qiskit/opflow/gradients/circuit_qfis/overlap_block_diag.py +++ b/qiskit/opflow/gradients/circuit_qfis/overlap_block_diag.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -18,6 +18,7 @@ from scipy.linalg import block_diag from qiskit.circuit import Parameter, ParameterVector, ParameterExpression from qiskit.utils.arithmetic import triu_to_dense +from qiskit.utils.deprecation import deprecate_func from ...list_ops.list_op import ListOp from ...primitive_ops.circuit_op import CircuitOp from ...expectations.pauli_expectation import PauliExpectation @@ -32,12 +33,19 @@ class OverlapBlockDiag(CircuitQFI): - r"""Compute the block-diagonal of the QFI given a pure, parameterized quantum state. + r"""Deprecated: Compute the block-diagonal of the QFI given a pure, parameterized quantum state. The blocks are given by all parameterized gates in quantum circuit layer. See also :class:`~qiskit.opflow.QFI`. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + super().__init__() + def convert( self, operator: Union[CircuitOp, CircuitStateFn], diff --git a/qiskit/opflow/gradients/circuit_qfis/overlap_diag.py b/qiskit/opflow/gradients/circuit_qfis/overlap_diag.py index 9efca9f6791b..3f630fe304b9 100644 --- a/qiskit/opflow/gradients/circuit_qfis/overlap_diag.py +++ b/qiskit/opflow/gradients/circuit_qfis/overlap_diag.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -11,6 +11,7 @@ # that they have been altered from the originals. """The module for Quantum the Fisher Information.""" + import copy from typing import List, Union @@ -18,6 +19,7 @@ from qiskit.circuit import ParameterVector, ParameterExpression from qiskit.circuit.library import RZGate, RXGate, RYGate from qiskit.converters import dag_to_circuit, circuit_to_dag +from qiskit.utils.deprecation import deprecate_func from ...list_ops.list_op import ListOp from ...primitive_ops.circuit_op import CircuitOp from ...expectations.pauli_expectation import PauliExpectation @@ -31,11 +33,18 @@ class OverlapDiag(CircuitQFI): - r"""Compute the diagonal of the QFI given a pure, parameterized quantum state. + r"""Deprecated: Compute the diagonal of the QFI given a pure, parameterized quantum state. See also :class:`~qiskit.opflow.QFI`. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + super().__init__() + def convert( self, operator: Union[CircuitOp, CircuitStateFn], diff --git a/qiskit/opflow/gradients/derivative_base.py b/qiskit/opflow/gradients/derivative_base.py index 83b71621b29e..9ffbc6f0cdf0 100644 --- a/qiskit/opflow/gradients/derivative_base.py +++ b/qiskit/opflow/gradients/derivative_base.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -20,7 +20,6 @@ from qiskit.utils.quantum_instance import QuantumInstance from qiskit.circuit import ParameterExpression, ParameterVector from qiskit.providers import Backend - from ..converters.converter_base import ConverterBase from ..expectations import ExpectationBase, PauliExpectation from ..list_ops.composed_op import ComposedOp @@ -34,7 +33,7 @@ class DerivativeBase(ConverterBase): - r"""Base class for differentiating opflow objects. + r"""Deprecated: Base class for differentiating opflow objects. Converter for differentiating opflow objects and handling things like properly differentiating combo_fn's and enforcing product rules @@ -48,6 +47,14 @@ class DerivativeBase(ConverterBase): DerivativeBase - uses classical techniques to differentiate opflow data structures """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + super().__init__() + + # pylint: disable=arguments-differ @abstractmethod def convert( self, diff --git a/qiskit/opflow/gradients/gradient.py b/qiskit/opflow/gradients/gradient.py index 1b7e6fd10333..b65784127a5a 100644 --- a/qiskit/opflow/gradients/gradient.py +++ b/qiskit/opflow/gradients/gradient.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -19,6 +19,8 @@ from qiskit.circuit.quantumcircuit import _compare_parameters from qiskit.circuit import ParameterExpression, ParameterVector from qiskit.utils import optionals as _optionals +from qiskit.utils.deprecation import deprecate_func +from .circuit_gradients.circuit_gradient import CircuitGradient from ..expectations.pauli_expectation import PauliExpectation from .gradient_base import GradientBase from .derivative_base import _coeff_derivative @@ -33,7 +35,14 @@ class Gradient(GradientBase): - """Convert an operator expression to the first-order gradient.""" + """Deprecated: Convert an operator expression to the first-order gradient.""" + + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self, grad_method: Union[str, CircuitGradient] = "param_shift", **kwargs): + super().__init__(grad_method=grad_method, **kwargs) def convert( self, diff --git a/qiskit/opflow/gradients/gradient_base.py b/qiskit/opflow/gradients/gradient_base.py index af38b5150b1c..f8da278acc93 100644 --- a/qiskit/opflow/gradients/gradient_base.py +++ b/qiskit/opflow/gradients/gradient_base.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -14,16 +14,21 @@ from typing import Union +from qiskit.utils.deprecation import deprecate_func from .circuit_gradients.circuit_gradient import CircuitGradient from .derivative_base import DerivativeBase class GradientBase(DerivativeBase): - """Base class for first-order operator gradient. + """Deprecated: Base class for first-order operator gradient. Convert an operator expression to the first-order gradient. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, grad_method: Union[str, CircuitGradient] = "param_shift", **kwargs): r""" Args: @@ -35,7 +40,7 @@ def __init__(self, grad_method: Union[str, CircuitGradient] = "param_shift", **k Raises: ValueError: If method != ``fin_diff`` and ``epsilon`` is not None. """ - + super().__init__() if isinstance(grad_method, CircuitGradient): self._grad_method = grad_method elif grad_method == "param_shift": diff --git a/qiskit/opflow/gradients/hessian.py b/qiskit/opflow/gradients/hessian.py index 60a05d131fb4..d25de4ad4cae 100644 --- a/qiskit/opflow/gradients/hessian.py +++ b/qiskit/opflow/gradients/hessian.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -19,6 +19,7 @@ from qiskit.circuit.quantumcircuit import _compare_parameters from qiskit.circuit import ParameterVector, ParameterExpression from qiskit.utils import optionals as _optionals +from qiskit.utils.deprecation import deprecate_func from ..operator_globals import Zero, One from ..state_fns.circuit_state_fn import CircuitStateFn from ..state_fns.state_fn import StateFn @@ -33,10 +34,18 @@ from .hessian_base import HessianBase from ..exceptions import OpflowError from ...utils.arithmetic import triu_to_dense +from .circuit_gradients.circuit_gradient import CircuitGradient class Hessian(HessianBase): - """Compute the Hessian of an expected value.""" + """Deprecated: Compute the Hessian of an expected value.""" + + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self, hess_method: Union[str, CircuitGradient] = "param_shift", **kwargs): + super().__init__(hess_method=hess_method, **kwargs) def convert( self, diff --git a/qiskit/opflow/gradients/hessian_base.py b/qiskit/opflow/gradients/hessian_base.py index 4ed5b3381821..2230ec28d824 100644 --- a/qiskit/opflow/gradients/hessian_base.py +++ b/qiskit/opflow/gradients/hessian_base.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -14,13 +14,18 @@ from typing import Union +from qiskit.utils.deprecation import deprecate_func from .circuit_gradients.circuit_gradient import CircuitGradient from .derivative_base import DerivativeBase class HessianBase(DerivativeBase): - """Base class for the Hessian of an expected value.""" + """Deprecated: Base class for the Hessian of an expected value.""" + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, hess_method: Union[str, CircuitGradient] = "param_shift", **kwargs): r""" Args: @@ -32,7 +37,7 @@ def __init__(self, hess_method: Union[str, CircuitGradient] = "param_shift", **k Raises: ValueError: If method != ``fin_diff`` and ``epsilon`` is not None. """ - + super().__init__() if isinstance(hess_method, CircuitGradient): self._hess_method = hess_method elif hess_method == "param_shift": diff --git a/qiskit/opflow/gradients/natural_gradient.py b/qiskit/opflow/gradients/natural_gradient.py index d957f3114918..9866b79e943b 100644 --- a/qiskit/opflow/gradients/natural_gradient.py +++ b/qiskit/opflow/gradients/natural_gradient.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 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 @@ -20,6 +20,7 @@ from qiskit.circuit.quantumcircuit import _compare_parameters from qiskit.circuit import ParameterVector, ParameterExpression from qiskit.utils import optionals as _optionals +from qiskit.utils.deprecation import deprecate_func from ..operator_base import OperatorBase from ..list_ops.list_op import ListOp from ..list_ops.composed_op import ComposedOp @@ -37,7 +38,7 @@ class NaturalGradient(GradientBase): - r"""Convert an operator expression to the first-order gradient. + r"""Deprecated: Convert an operator expression to the first-order gradient. Given an ill-posed inverse problem @@ -51,6 +52,10 @@ class NaturalGradient(GradientBase): where R(x) represents the penalization term. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, grad_method: Union[str, CircuitGradient] = "lin_comb", diff --git a/qiskit/opflow/gradients/qfi.py b/qiskit/opflow/gradients/qfi.py index f9bd289aaa0e..64ba631455d2 100644 --- a/qiskit/opflow/gradients/qfi.py +++ b/qiskit/opflow/gradients/qfi.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -17,14 +17,16 @@ from qiskit.circuit.quantumcircuit import _compare_parameters from qiskit.circuit import ParameterExpression, ParameterVector +from qiskit.utils.deprecation import deprecate_func from ..list_ops.list_op import ListOp from ..expectations.pauli_expectation import PauliExpectation from ..state_fns.circuit_state_fn import CircuitStateFn from .qfi_base import QFIBase +from .circuit_qfis import CircuitQFI class QFI(QFIBase): - r"""Compute the Quantum Fisher Information (QFI). + r"""Deprecated: Compute the Quantum Fisher Information (QFI). Computes the QFI given a pure, parameterized quantum state, where QFI is: @@ -35,6 +37,13 @@ class QFI(QFIBase): """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self, qfi_method: Union[str, CircuitQFI] = "lin_comb_full"): + super().__init__(qfi_method=qfi_method) + def convert( self, operator: CircuitStateFn, diff --git a/qiskit/opflow/gradients/qfi_base.py b/qiskit/opflow/gradients/qfi_base.py index b0497de840d3..b09f9170dbad 100644 --- a/qiskit/opflow/gradients/qfi_base.py +++ b/qiskit/opflow/gradients/qfi_base.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -14,12 +14,14 @@ from typing import Union +from qiskit.utils.deprecation import deprecate_func from .derivative_base import DerivativeBase from .circuit_qfis import CircuitQFI class QFIBase(DerivativeBase): - r"""Base class for Quantum Fisher Information (QFI). + + r"""Deprecated: Base class for Quantum Fisher Information (QFI). Compute the Quantum Fisher Information (QFI) given a pure, parameterized quantum state. @@ -28,6 +30,10 @@ class QFIBase(DerivativeBase): [QFI]kl= Re[〈∂kψ|∂lψ〉−〈∂kψ|ψ〉〈ψ|∂lψ〉] * 4. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, qfi_method: Union[str, CircuitQFI] = "lin_comb_full"): r""" Args: @@ -38,7 +44,7 @@ def __init__(self, qfi_method: Union[str, CircuitQFI] = "lin_comb_full"): ValueError: if ``qfi_method`` is neither a ``CircuitQFI`` object nor one of the predefined strings. """ - + super().__init__() if isinstance(qfi_method, CircuitQFI): self._qfi_method = qfi_method diff --git a/qiskit/opflow/list_ops/__init__.py b/qiskit/opflow/list_ops/__init__.py index d2ae28d1f05b..b4e4a45b3d72 100644 --- a/qiskit/opflow/list_ops/__init__.py +++ b/qiskit/opflow/list_ops/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -16,6 +16,12 @@ .. currentmodule:: qiskit.opflow.list_ops +.. deprecated:: 0.24.0 + + The :mod:`qiskit.opflow` module is deprecated and will be removed no earlier + than 3 months after the release date. For code migration guidelines, + visit https://qisk.it/opflow_migration. + List Operators are classes for storing and manipulating lists of Operators, State functions, or Measurements, and include some rule or ``combo_fn`` defining how the Operator functions of the list constituents should be combined to form to cumulative Operator function of the @@ -31,6 +37,7 @@ :mod:`.list_ops` are the basis for constructing large and sophisticated Operators, State Functions, and Measurements. + The base :class:`ListOp` class is particularly interesting, as its ``combo_fn`` is "the identity list Operation". Meaning, if we understand the ``combo_fn`` as a function from a list of complex values to some output, one such function is returning the list as\-is. This is powerful for diff --git a/qiskit/opflow/list_ops/composed_op.py b/qiskit/opflow/list_ops/composed_op.py index 1c2dd46dd434..d47a81fbd796 100644 --- a/qiskit/opflow/list_ops/composed_op.py +++ b/qiskit/opflow/list_ops/composed_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -24,15 +24,20 @@ from qiskit.opflow.list_ops.list_op import ListOp from qiskit.opflow.operator_base import OperatorBase from qiskit.quantum_info import Statevector +from qiskit.utils.deprecation import deprecate_func class ComposedOp(ListOp): - """A class for lazily representing compositions of Operators. Often Operators cannot be + """Deprecated: A class for lazily representing compositions of Operators. Often Operators cannot be efficiently composed with one another, but may be manipulated further so that they can be composed later. This class holds logic to indicate that the Operators in ``oplist`` are meant to be composed, and therefore if they reach a point in which they can be, such as after conversion to QuantumCircuits or matrices, they can be reduced by composition.""" + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, oplist: List[OperatorBase], diff --git a/qiskit/opflow/list_ops/list_op.py b/qiskit/opflow/list_ops/list_op.py index 775f82164eca..abf2d2326d0c 100644 --- a/qiskit/opflow/list_ops/list_op.py +++ b/qiskit/opflow/list_ops/list_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -23,12 +23,13 @@ from qiskit.opflow.operator_base import OperatorBase from qiskit.quantum_info import Statevector from qiskit.utils import arithmetic +from qiskit.utils.deprecation import deprecate_func class ListOp(OperatorBase): """ - A Class for manipulating List Operators, and parent class to ``SummedOp``, ``ComposedOp``, - and ``TensoredOp``. + Deprecated: A Class for manipulating List Operators, and parent class to ``SummedOp``, + ``ComposedOp`` and ``TensoredOp``. List Operators are classes for storing and manipulating lists of Operators, State functions, or Measurements, and include some rule or ``combo_fn`` defining how the Operator functions @@ -52,6 +53,10 @@ class ListOp(OperatorBase): multiple dimensional lists. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, oplist: Sequence[OperatorBase], diff --git a/qiskit/opflow/list_ops/summed_op.py b/qiskit/opflow/list_ops/summed_op.py index 525e973a562a..52f7faa15083 100644 --- a/qiskit/opflow/list_ops/summed_op.py +++ b/qiskit/opflow/list_ops/summed_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -21,15 +21,20 @@ from qiskit.opflow.exceptions import OpflowError from qiskit.opflow.list_ops.list_op import ListOp from qiskit.opflow.operator_base import OperatorBase +from qiskit.utils.deprecation import deprecate_func class SummedOp(ListOp): - """A class for lazily representing sums of Operators. Often Operators cannot be + """Deprecated: A class for lazily representing sums of Operators. Often Operators cannot be efficiently added to one another, but may be manipulated further so that they can be later. This class holds logic to indicate that the Operators in ``oplist`` are meant to be added together, and therefore if they reach a point in which they can be, such as after evaluation or conversion to matrices, they can be reduced by addition.""" + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, oplist: List[OperatorBase], diff --git a/qiskit/opflow/list_ops/tensored_op.py b/qiskit/opflow/list_ops/tensored_op.py index 427703a261d9..e3eb30cfaa41 100644 --- a/qiskit/opflow/list_ops/tensored_op.py +++ b/qiskit/opflow/list_ops/tensored_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -22,15 +22,20 @@ from qiskit.opflow.list_ops.list_op import ListOp from qiskit.opflow.operator_base import OperatorBase from qiskit.quantum_info import Statevector +from qiskit.utils.deprecation import deprecate_func class TensoredOp(ListOp): - """A class for lazily representing tensor products of Operators. Often Operators cannot be - efficiently tensored to one another, but may be manipulated further so that they can be + """Deprecated: A class for lazily representing tensor products of Operators. Often Operators + cannot be efficiently tensored to one another, but may be manipulated further so that they can be later. This class holds logic to indicate that the Operators in ``oplist`` are meant to be tensored together, and therefore if they reach a point in which they can be, such as after conversion to QuantumCircuits, they can be reduced by tensor product.""" + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, oplist: List[OperatorBase], diff --git a/qiskit/opflow/mixins/star_algebra.py b/qiskit/opflow/mixins/star_algebra.py index 30ba75b8ffeb..642086d5ba04 100644 --- a/qiskit/opflow/mixins/star_algebra.py +++ b/qiskit/opflow/mixins/star_algebra.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -16,10 +16,11 @@ from numbers import Integral from qiskit.quantum_info.operators.mixins import MultiplyMixin +from qiskit.utils.deprecation import deprecate_func class StarAlgebraMixin(MultiplyMixin, ABC): - """The star algebra mixin class. + """Deprecated: The star algebra mixin class. Star algebra is an algebra with an adjoint. This class overrides: @@ -39,6 +40,13 @@ class StarAlgebraMixin(MultiplyMixin, ABC): - :meth:`adjoint(self)` """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + pass + # Scalar multiplication @abstractmethod diff --git a/qiskit/opflow/mixins/tensor.py b/qiskit/opflow/mixins/tensor.py index dfec66d5674d..3535db439f09 100644 --- a/qiskit/opflow/mixins/tensor.py +++ b/qiskit/opflow/mixins/tensor.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -14,10 +14,11 @@ from abc import ABC, abstractmethod from numbers import Integral +from qiskit.utils.deprecation import deprecate_func class TensorMixin(ABC): - """The mixin class for tensor operations. + """Deprecated: The mixin class for tensor operations. This class overrides: - ``^``, ``__xor__``, `__rxor__` -> :meth:`tensor` between two operators and @@ -27,6 +28,13 @@ class TensorMixin(ABC): - :meth:``tensorpower(self, other: int)`` """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) + def __init__(self) -> None: + pass + def __xor__(self, other): if isinstance(other, Integral): return self.tensorpower(other) diff --git a/qiskit/opflow/operator_base.py b/qiskit/opflow/operator_base.py index 713cbe38384b..7c1e031d8620 100644 --- a/qiskit/opflow/operator_base.py +++ b/qiskit/opflow/operator_base.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 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 @@ -25,10 +25,11 @@ from qiskit.opflow.mixins import StarAlgebraMixin, TensorMixin from qiskit.quantum_info import Statevector from qiskit.utils import algorithm_globals +from qiskit.utils.deprecation import deprecate_func class OperatorBase(StarAlgebraMixin, TensorMixin, ABC): - """A base class for all Operators: PrimitiveOps, StateFns, ListOps, etc. Operators are + """Deprecated: A base class for all Operators: PrimitiveOps, StateFns, ListOps, etc. Operators are defined as functions which take one complex binary function to another. These complex binary functions are represented by StateFns, which are themselves a special class of Operators taking only the ``Zero`` StateFn to the complex binary function they represent. @@ -44,7 +45,12 @@ class OperatorBase(StarAlgebraMixin, TensorMixin, ABC): _count = itertools.count() + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self) -> None: + super().__init__() self._instance_id = next(self._count) @property diff --git a/qiskit/opflow/operator_globals.py b/qiskit/opflow/operator_globals.py index 7fb3796ea8a9..a5afcd9cf08f 100644 --- a/qiskit/opflow/operator_globals.py +++ b/qiskit/opflow/operator_globals.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -20,6 +20,7 @@ from qiskit.opflow.primitive_ops.pauli_op import PauliOp from qiskit.opflow.primitive_ops.circuit_op import CircuitOp from qiskit.opflow.state_fns.dict_state_fn import DictStateFn +from qiskit.utils.deprecation import deprecate_func # Digits of precision when returning values from eval functions. Without rounding, 1e-17 or 1e-32 # values often show up in place of 0, etc. @@ -32,8 +33,12 @@ # Immutable convenience objects +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", +) def make_immutable(obj): - """Delete the __setattr__ property to make the object mostly immutable.""" + r"""Deprecate\: Delete the __setattr__ property to make the object mostly immutable.""" # TODO figure out how to get correct error message # def throw_immutability_exception(self, *args): diff --git a/qiskit/opflow/primitive_ops/__init__.py b/qiskit/opflow/primitive_ops/__init__.py index 1af655893599..7e5cc72fad6b 100644 --- a/qiskit/opflow/primitive_ops/__init__.py +++ b/qiskit/opflow/primitive_ops/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -16,6 +16,12 @@ .. currentmodule:: qiskit.opflow.primitive_ops +.. deprecated:: 0.24.0 + + The :mod:`qiskit.opflow` module is deprecated and will be removed no earlier + than 3 months after the release date. For code migration guidelines, + visit https://qisk.it/opflow_migration. + Operators are defined to be functions which take State functions to State functions. PrimitiveOps are the classes for representing basic Operators, backed by computational diff --git a/qiskit/opflow/primitive_ops/circuit_op.py b/qiskit/opflow/primitive_ops/circuit_op.py index 916a93c132fb..1a6104a4cac3 100644 --- a/qiskit/opflow/primitive_ops/circuit_op.py +++ b/qiskit/opflow/primitive_ops/circuit_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -13,7 +13,6 @@ """CircuitOp Class""" from typing import Dict, List, Optional, Set, Union, cast - import numpy as np import qiskit @@ -24,13 +23,18 @@ from qiskit.opflow.operator_base import OperatorBase from qiskit.opflow.primitive_ops.primitive_op import PrimitiveOp from qiskit.quantum_info import Statevector +from qiskit.utils.deprecation import deprecate_func class CircuitOp(PrimitiveOp): - """Class for Operators backed by Terra's ``QuantumCircuit`` module.""" + """Deprecated: Class for Operators backed by Terra's ``QuantumCircuit`` module.""" primitive: QuantumCircuit + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: Union[Instruction, QuantumCircuit], diff --git a/qiskit/opflow/primitive_ops/matrix_op.py b/qiskit/opflow/primitive_ops/matrix_op.py index 74d25f32ad82..be8c8ac90eec 100644 --- a/qiskit/opflow/primitive_ops/matrix_op.py +++ b/qiskit/opflow/primitive_ops/matrix_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -13,7 +13,6 @@ """MatrixOp Class""" from typing import Dict, List, Optional, Set, Union, cast, get_type_hints - import numpy as np from scipy.sparse import spmatrix @@ -28,13 +27,19 @@ from qiskit.opflow.primitive_ops.primitive_op import PrimitiveOp from qiskit.quantum_info import Operator, Statevector from qiskit.utils import arithmetic +from qiskit.utils.deprecation import deprecate_func class MatrixOp(PrimitiveOp): - """Class for Operators represented by matrices, backed by Terra's ``Operator`` module.""" + """Deprecated: Class for Operators represented by matrices, + backed by Terra's ``Operator`` module.""" primitive: Operator + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: Union[list, np.ndarray, spmatrix, Operator], diff --git a/qiskit/opflow/primitive_ops/pauli_op.py b/qiskit/opflow/primitive_ops/pauli_op.py index 8bc0878da63a..5720a13357df 100644 --- a/qiskit/opflow/primitive_ops/pauli_op.py +++ b/qiskit/opflow/primitive_ops/pauli_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -14,7 +14,6 @@ from math import pi from typing import Dict, List, Optional, Set, Union, cast - import numpy as np from scipy.sparse import spmatrix @@ -28,13 +27,18 @@ from qiskit.opflow.operator_base import OperatorBase from qiskit.opflow.primitive_ops.primitive_op import PrimitiveOp from qiskit.quantum_info import Pauli, SparsePauliOp, Statevector +from qiskit.utils.deprecation import deprecate_func class PauliOp(PrimitiveOp): - """Class for Operators backed by Terra's ``Pauli`` module.""" + """Deprecated: Class for Operators backed by Terra's ``Pauli`` module.""" primitive: Pauli + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__(self, primitive: Pauli, coeff: Union[complex, ParameterExpression] = 1.0) -> None: """ Args: @@ -46,6 +50,7 @@ def __init__(self, primitive: Pauli, coeff: Union[complex, ParameterExpression] """ if not isinstance(primitive, Pauli): raise TypeError(f"PauliOp can only be instantiated with Paulis, not {type(primitive)}") + super().__init__(primitive, coeff=coeff) def primitive_strings(self) -> Set[str]: diff --git a/qiskit/opflow/primitive_ops/pauli_sum_op.py b/qiskit/opflow/primitive_ops/pauli_sum_op.py index 2cad82f6d696..97ecd8fe23ad 100644 --- a/qiskit/opflow/primitive_ops/pauli_sum_op.py +++ b/qiskit/opflow/primitive_ops/pauli_sum_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -14,7 +14,6 @@ from collections import defaultdict from typing import Dict, List, Optional, Set, Tuple, Union, cast - import numpy as np from scipy.sparse import spmatrix @@ -27,13 +26,18 @@ from qiskit.opflow.primitive_ops.primitive_op import PrimitiveOp from qiskit.quantum_info import Pauli, SparsePauliOp, Statevector from qiskit.quantum_info.operators.custom_iterator import CustomIterator +from qiskit.utils.deprecation import deprecate_func class PauliSumOp(PrimitiveOp): - """Class for Operators backed by Terra's ``SparsePauliOp`` class.""" + """Deprecated: Class for Operators backed by Terra's ``SparsePauliOp`` class.""" primitive: SparsePauliOp + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: SparsePauliOp, diff --git a/qiskit/opflow/primitive_ops/primitive_op.py b/qiskit/opflow/primitive_ops/primitive_op.py index 425a25422671..de3ef06c3add 100644 --- a/qiskit/opflow/primitive_ops/primitive_op.py +++ b/qiskit/opflow/primitive_ops/primitive_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -22,11 +22,12 @@ from qiskit.circuit import Instruction, ParameterExpression from qiskit.opflow.operator_base import OperatorBase from qiskit.quantum_info import Operator, Pauli, SparsePauliOp, Statevector +from qiskit.utils.deprecation import deprecate_func class PrimitiveOp(OperatorBase): r""" - A class for representing basic Operators, backed by Operator primitives from + Deprecated: A class for representing basic Operators, backed by Operator primitives from Terra. This class (and inheritors) primarily serves to allow the underlying primitives to "flow" - i.e. interoperability and adherence to the Operator formalism - while the core computational logic mostly remains in the underlying primitives. @@ -92,6 +93,10 @@ def __new__( "factory constructor".format(type(primitive)) ) + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: Union[QuantumCircuit, Operator, Pauli, SparsePauliOp, OperatorBase], diff --git a/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py b/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py index 60dec6fc54f0..9411afc71f26 100644 --- a/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py +++ b/qiskit/opflow/primitive_ops/tapered_pauli_sum_op.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -27,13 +27,18 @@ from qiskit.opflow.primitive_ops.pauli_sum_op import PauliSumOp from qiskit.opflow.utils import commutator from qiskit.quantum_info import Pauli, SparsePauliOp +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) class TaperedPauliSumOp(PauliSumOp): - """Class for PauliSumOp after tapering""" + """Deprecated: Class for PauliSumOp after tapering""" + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: SparsePauliOp, @@ -81,8 +86,12 @@ def assign_parameters(self, param_dict: dict) -> OperatorBase: class Z2Symmetries: - """Z2 Symmetries""" + """Deprecated: Z2 Symmetries""" + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, symmetries: List[Pauli], diff --git a/qiskit/opflow/state_fns/__init__.py b/qiskit/opflow/state_fns/__init__.py index 6d71298df145..69b7d960bc20 100644 --- a/qiskit/opflow/state_fns/__init__.py +++ b/qiskit/opflow/state_fns/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -14,9 +14,15 @@ State Functions (:mod:`qiskit.opflow.state_fns`) ================================================ -State functions are defined to be complex functions over a single binary string (as -compared to an operator, which is defined as a function over two binary strings, or a -function taking a binary function to another binary function). This function may be +.. deprecated:: 0.24.0 + + The :mod:`qiskit.opflow` module is deprecated and will be removed no earlier + than 3 months after the release date. For code migration guidelines, + visit https://qisk.it/opflow_migration. + +State functions are defined to be complex functions over a single binary +string (as compared to an operator, which is defined as a function over two binary strings, +or a function taking a binary function to another binary function). This function may be called by the eval() method. Measurements are defined to be functionals over StateFns, taking them to real values. diff --git a/qiskit/opflow/state_fns/circuit_state_fn.py b/qiskit/opflow/state_fns/circuit_state_fn.py index 3594a33332d0..3f23f89a1cee 100644 --- a/qiskit/opflow/state_fns/circuit_state_fn.py +++ b/qiskit/opflow/state_fns/circuit_state_fn.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -33,16 +33,21 @@ from qiskit.opflow.state_fns.state_fn import StateFn from qiskit.opflow.state_fns.vector_state_fn import VectorStateFn from qiskit.quantum_info import Statevector +from qiskit.utils.deprecation import deprecate_func class CircuitStateFn(StateFn): r""" - A class for state functions and measurements which are defined by the action of a + Deprecated: A class for state functions and measurements which are defined by the action of a QuantumCircuit starting from \|0⟩, and stored using Terra's ``QuantumCircuit`` class. """ primitive: QuantumCircuit # TODO allow normalization somehow? + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: Union[QuantumCircuit, Instruction] = None, diff --git a/qiskit/opflow/state_fns/cvar_measurement.py b/qiskit/opflow/state_fns/cvar_measurement.py index dda966dbf0d9..858d3b87f671 100644 --- a/qiskit/opflow/state_fns/cvar_measurement.py +++ b/qiskit/opflow/state_fns/cvar_measurement.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -28,10 +28,11 @@ from qiskit.opflow.state_fns.state_fn import StateFn from qiskit.opflow.state_fns.vector_state_fn import VectorStateFn from qiskit.quantum_info import Statevector +from qiskit.utils.deprecation import deprecate_func class CVaRMeasurement(OperatorStateFn): - r"""A specialized measurement class to compute CVaR expectation values. + r"""Deprecated: A specialized measurement class to compute CVaR expectation values. See https://arxiv.org/pdf/1907.04769.pdf for further details. Used in :class:`~qiskit.opflow.CVaRExpectation`, see there for more details. @@ -40,6 +41,10 @@ class CVaRMeasurement(OperatorStateFn): primitive: OperatorBase # TODO allow normalization somehow? + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: OperatorBase = None, diff --git a/qiskit/opflow/state_fns/dict_state_fn.py b/qiskit/opflow/state_fns/dict_state_fn.py index 6bfed225e887..77d5c32d8772 100644 --- a/qiskit/opflow/state_fns/dict_state_fn.py +++ b/qiskit/opflow/state_fns/dict_state_fn.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 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 @@ -27,16 +27,21 @@ from qiskit.quantum_info import Statevector from qiskit.result import Result from qiskit.utils import algorithm_globals +from qiskit.utils.deprecation import deprecate_func class DictStateFn(StateFn): - """A class for state functions and measurements which are defined by a lookup table, + """Deprecated: A class for state functions and measurements which are defined by a lookup table, stored in a dict. """ primitive: Dict[str, complex] # TODO allow normalization somehow? + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: Union[str, dict, Result] = None, diff --git a/qiskit/opflow/state_fns/operator_state_fn.py b/qiskit/opflow/state_fns/operator_state_fn.py index febd57830969..a9cac679deef 100644 --- a/qiskit/opflow/state_fns/operator_state_fn.py +++ b/qiskit/opflow/state_fns/operator_state_fn.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -26,16 +26,21 @@ from qiskit.opflow.state_fns.state_fn import StateFn from qiskit.opflow.state_fns.circuit_state_fn import CircuitStateFn from qiskit.quantum_info import Statevector +from qiskit.utils.deprecation import deprecate_func class OperatorStateFn(StateFn): r""" - A class for state functions and measurements which are defined by a density Operator, + Deprecated: A class for state functions and measurements which are defined by a density Operator, stored using an ``OperatorBase``. """ primitive: OperatorBase # TODO allow normalization somehow? + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: OperatorBase, diff --git a/qiskit/opflow/state_fns/sparse_vector_state_fn.py b/qiskit/opflow/state_fns/sparse_vector_state_fn.py index 935241a2e197..b26c6dff9df1 100644 --- a/qiskit/opflow/state_fns/sparse_vector_state_fn.py +++ b/qiskit/opflow/state_fns/sparse_vector_state_fn.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 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 @@ -26,10 +26,11 @@ from qiskit.opflow.state_fns.vector_state_fn import VectorStateFn from qiskit.quantum_info import Statevector from qiskit.utils import algorithm_globals +from qiskit.utils.deprecation import deprecate_func class SparseVectorStateFn(StateFn): - """A class for sparse state functions and measurements in vector representation. + """Deprecated: A class for sparse state functions and measurements in vector representation. This class uses ``scipy.sparse.spmatrix`` for the internal representation. """ @@ -37,6 +38,10 @@ class SparseVectorStateFn(StateFn): primitive: scipy.sparse.spmatrix # TODO allow normalization somehow? + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: scipy.sparse.spmatrix, diff --git a/qiskit/opflow/state_fns/state_fn.py b/qiskit/opflow/state_fns/state_fn.py index 3a2d4a4eb656..f24592bdaf55 100644 --- a/qiskit/opflow/state_fns/state_fn.py +++ b/qiskit/opflow/state_fns/state_fn.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -21,11 +21,12 @@ from qiskit.opflow.operator_base import OperatorBase from qiskit.quantum_info import Statevector from qiskit.result import Result +from qiskit.utils.deprecation import deprecate_func class StateFn(OperatorBase): r""" - A class for representing state functions and measurements. + Deprecated: A class for representing state functions and measurements. State functions are defined to be complex functions over a single binary string (as compared to an operator, which is defined as a function over two binary strings, or a @@ -112,6 +113,10 @@ def __new__( ) # TODO allow normalization somehow? + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: Union[ diff --git a/qiskit/opflow/state_fns/vector_state_fn.py b/qiskit/opflow/state_fns/vector_state_fn.py index fb3956e46a5a..8391d92a697b 100644 --- a/qiskit/opflow/state_fns/vector_state_fn.py +++ b/qiskit/opflow/state_fns/vector_state_fn.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 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 @@ -26,16 +26,21 @@ from qiskit.opflow.state_fns.state_fn import StateFn from qiskit.quantum_info import Statevector from qiskit.utils import algorithm_globals, arithmetic +from qiskit.utils.deprecation import deprecate_func class VectorStateFn(StateFn): - """A class for state functions and measurements which are defined in vector + """Deprecated: A class for state functions and measurements which are defined in vector representation, and stored using Terra's ``Statevector`` class. """ primitive: Statevector # TODO allow normalization somehow? + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", + ) def __init__( self, primitive: Union[list, np.ndarray, Statevector] = None, diff --git a/qiskit/opflow/utils.py b/qiskit/opflow/utils.py index 752cc89346f7..0979fc0bc3e7 100644 --- a/qiskit/opflow/utils.py +++ b/qiskit/opflow/utils.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -13,11 +13,16 @@ """Utility functions for OperatorFlow""" from qiskit.opflow.operator_base import OperatorBase +from qiskit.utils.deprecation import deprecate_func +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", +) def commutator(op_a: OperatorBase, op_b: OperatorBase) -> OperatorBase: r""" - Compute commutator of `op_a` and `op_b`. + Deprecated: Compute commutator of `op_a` and `op_b`. .. math:: @@ -32,9 +37,13 @@ def commutator(op_a: OperatorBase, op_b: OperatorBase) -> OperatorBase: return (op_a @ op_b - op_b @ op_a).reduce() +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", +) def anti_commutator(op_a: OperatorBase, op_b: OperatorBase) -> OperatorBase: r""" - Compute anti-commutator of `op_a` and `op_b`. + Deprecated: Compute anti-commutator of `op_a` and `op_b`. .. math:: @@ -49,6 +58,10 @@ def anti_commutator(op_a: OperatorBase, op_b: OperatorBase) -> OperatorBase: return (op_a @ op_b + op_b @ op_a).reduce() +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/opflow_migration.", +) def double_commutator( op_a: OperatorBase, op_b: OperatorBase, @@ -56,7 +69,7 @@ def double_commutator( sign: bool = False, ) -> OperatorBase: r""" - Compute symmetric double commutator of `op_a`, `op_b` and `op_c`. + Deprecated: Compute symmetric double commutator of `op_a`, `op_b` and `op_c`. See McWeeny chapter 13.6 Equation of motion methods (page 479) If `sign` is `False`, it returns diff --git a/qiskit/synthesis/evolution/qdrift.py b/qiskit/synthesis/evolution/qdrift.py index 009548653988..6707fa2ec411 100644 --- a/qiskit/synthesis/evolution/qdrift.py +++ b/qiskit/synthesis/evolution/qdrift.py @@ -85,14 +85,13 @@ def synthesize(self, evolution): # pylint: disable=cyclic-import from qiskit.circuit.library.pauli_evolution import PauliEvolutionGate - from qiskit.opflow import PauliOp # Build the evolution circuit using the LieTrotter synthesis with the sampled operators lie_trotter = LieTrotter( insert_barriers=self.insert_barriers, atomic_evolution=self.atomic_evolution ) evolution_circuit = PauliEvolutionGate( - sum(PauliOp(op) for op, coeff in self.sampled_ops), + sum(SparsePauliOp(op) for op, coeff in self.sampled_ops), time=evolution_time, synthesis=lie_trotter, ).definition diff --git a/qiskit/utils/backend_utils.py b/qiskit/utils/backend_utils.py index c87bdbd937a9..7f71a7c91897 100644 --- a/qiskit/utils/backend_utils.py +++ b/qiskit/utils/backend_utils.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 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 @@ -13,6 +13,7 @@ """backend utility functions""" import logging +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) @@ -49,6 +50,10 @@ def _get_backend_provider(backend): return provider +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def has_ibmq(): """Check if IBMQ is installed.""" if not _PROVIDER_CHECK.checked_ibmq: @@ -66,6 +71,10 @@ def has_ibmq(): return _PROVIDER_CHECK.has_ibmq +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def has_aer(): """Check if Aer is installed.""" if not _PROVIDER_CHECK.checked_aer: @@ -82,6 +91,10 @@ def has_aer(): return _PROVIDER_CHECK.has_aer +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def is_aer_provider(backend): """Detect whether or not backend is from Aer provider. @@ -102,6 +115,10 @@ def is_aer_provider(backend): return False +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def is_basicaer_provider(backend): """Detect whether or not backend is from BasicAer provider. @@ -115,6 +132,10 @@ def is_basicaer_provider(backend): return isinstance(_get_backend_provider(backend), BasicAerProvider) +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def is_ibmq_provider(backend): """Detect whether or not backend is from IBMQ provider. @@ -131,6 +152,10 @@ def is_ibmq_provider(backend): return False +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def is_aer_statevector_backend(backend): """ Return True if backend object is statevector and from Aer provider. @@ -143,6 +168,10 @@ def is_aer_statevector_backend(backend): return is_statevector_backend(backend) and is_aer_provider(backend) +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def is_statevector_backend(backend): """ Return True if backend object is statevector. @@ -168,6 +197,10 @@ def is_statevector_backend(backend): return backend.name.startswith("statevector") +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def is_simulator_backend(backend): """ Return True if backend is a simulator. @@ -183,6 +216,10 @@ def is_simulator_backend(backend): return False +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def is_local_backend(backend): """ Return True if backend is a local backend. @@ -198,6 +235,10 @@ def is_local_backend(backend): return False +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def is_aer_qasm(backend): """ Return True if backend is Aer Qasm simulator @@ -214,6 +255,10 @@ def is_aer_qasm(backend): return ret +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def support_backend_options(backend): """ Return True if backend supports backend_options diff --git a/qiskit/utils/measurement_error_mitigation.py b/qiskit/utils/measurement_error_mitigation.py index 378f6c4b21cf..0ef09c997ec5 100644 --- a/qiskit/utils/measurement_error_mitigation.py +++ b/qiskit/utils/measurement_error_mitigation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2021. +# (C) Copyright IBM 2019, 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 @@ -27,13 +27,18 @@ CompleteMeasFitter, TensoredMeasFitter, ) +from qiskit.utils.deprecation import deprecate_func +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def get_measured_qubits( transpiled_circuits: List[QuantumCircuit], ) -> Tuple[List[int], Dict[str, List[int]]]: """ - Retrieve the measured qubits from transpiled circuits. + Deprecated: Retrieve the measured qubits from transpiled circuits. Args: transpiled_circuits: a list of transpiled circuits @@ -73,9 +78,13 @@ def get_measured_qubits( return sorted(qubit_index), qubit_mappings +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def get_measured_qubits_from_qobj(qobj: QasmQobj) -> Tuple[List[int], Dict[str, List[int]]]: """ - Retrieve the measured qubits from transpiled circuits. + Deprecated: Retrieve the measured qubits from transpiled circuits. Args: qobj: qobj @@ -114,6 +123,10 @@ def get_measured_qubits_from_qobj(qobj: QasmQobj) -> Tuple[List[int], Dict[str, return sorted(qubit_index), qubit_mappings +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def build_measurement_error_mitigation_circuits( qubit_list: List[int], fitter_cls: Callable, @@ -122,7 +135,7 @@ def build_measurement_error_mitigation_circuits( compile_config: Optional[Dict] = None, mit_pattern: Optional[List[List[int]]] = None, ) -> Tuple[QuantumCircuit, List[str], List[str]]: - """Build measurement error mitigation circuits + """Deprecated: Build measurement error mitigation circuits Args: qubit_list: list of ordered qubits used in the algorithm fitter_cls: CompleteMeasFitter or TensoredMeasFitter @@ -189,6 +202,10 @@ def build_measurement_error_mitigation_circuits( return t_meas_calibs_circuits, state_labels, circlabel +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def build_measurement_error_mitigation_qobj( qubit_list: List[int], fitter_cls: Callable, diff --git a/qiskit/utils/mitigation/__init__.py b/qiskit/utils/mitigation/__init__.py index 2d2de3040746..c79b107cdc0f 100644 --- a/qiskit/utils/mitigation/__init__.py +++ b/qiskit/utils/mitigation/__init__.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019. +# (C) Copyright IBM 2019, 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 @@ -21,6 +21,11 @@ .. currentmodule:: qiskit.utils.mitigation +.. deprecated:: 0.24.0 + This module is deprecated and will be removed no sooner than 3 months + after the release date. For code migration guidelines, + visit https://qisk.it/qi_migration. + .. warning:: The user-facing API stability of this module is not guaranteed except for diff --git a/qiskit/utils/mitigation/_filters.py b/qiskit/utils/mitigation/_filters.py index 5fff37e230c9..5d566f625116 100644 --- a/qiskit/utils/mitigation/_filters.py +++ b/qiskit/utils/mitigation/_filters.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019. +# (C) Copyright IBM 2019, 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 @@ -31,17 +31,22 @@ from qiskit import QiskitError from qiskit.tools import parallel_map from qiskit.utils.mitigation.circuits import count_keys +from qiskit.utils.deprecation import deprecate_func class MeasurementFilter: """ - Measurement error mitigation filter. + Deprecated: Measurement error mitigation filter. Produced from a measurement calibration fitter and can be applied to data. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", + ) def __init__(self, cal_matrix: np.matrix, state_labels: list): """ Initialize a measurement error mitigation filter using the cal_matrix @@ -214,12 +219,16 @@ def _apply_correction(self, resultidx, raw_data, method): class TensoredFilter: """ - Tensored measurement error mitigation filter. + Deprecated: Tensored measurement error mitigation filter. Produced from a tensored measurement calibration fitter and can be applied to data. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", + ) def __init__(self, cal_matrices: np.matrix, substate_labels_list: list, mit_pattern: list): """ Initialize a tensored measurement error mitigation filter using diff --git a/qiskit/utils/mitigation/circuits.py b/qiskit/utils/mitigation/circuits.py index af62d1c36fa5..2fdeaa6372a6 100644 --- a/qiskit/utils/mitigation/circuits.py +++ b/qiskit/utils/mitigation/circuits.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019. +# (C) Copyright IBM 2019, 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 @@ -19,10 +19,15 @@ use the fitters to produce a filter. """ from typing import List, Tuple, Union +from qiskit.utils.deprecation import deprecate_func +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def count_keys(num_qubits: int) -> List[str]: - """Return ordered count keys. + """Deprecated: Return ordered count keys. Args: num_qubits: The number of qubits in the generated list. @@ -35,6 +40,10 @@ def count_keys(num_qubits: int) -> List[str]: return [bin(j)[2:].zfill(num_qubits) for j in range(2**num_qubits)] +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def complete_meas_cal( qubit_list: List[int] = None, qr: Union[int, List["QuantumRegister"]] = None, @@ -42,7 +51,7 @@ def complete_meas_cal( circlabel: str = "", ) -> Tuple[List["QuantumCircuit"], List[str]]: """ - Return a list of measurement calibration circuits for the full + Deprecated: Return a list of measurement calibration circuits for the full Hilbert space. If the circuit contains :math:`n` qubits, then :math:`2^n` calibration circuits @@ -112,6 +121,10 @@ def complete_meas_cal( return cal_circuits, state_labels +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def tensored_meas_cal( mit_pattern: List[List[int]] = None, qr: Union[int, List["QuantumRegister"]] = None, @@ -119,7 +132,7 @@ def tensored_meas_cal( circlabel: str = "", ) -> Tuple[List["QuantumCircuit"], List[List[int]]]: """ - Return a list of calibration circuits + Deprecated: Return a list of calibration circuits Args: mit_pattern: Qubits on which to perform the diff --git a/qiskit/utils/mitigation/fitters.py b/qiskit/utils/mitigation/fitters.py index 3eff35424b94..65357d5dc5b5 100644 --- a/qiskit/utils/mitigation/fitters.py +++ b/qiskit/utils/mitigation/fitters.py @@ -27,13 +27,18 @@ from qiskit import QiskitError from qiskit.utils.mitigation.circuits import count_keys from qiskit.utils.mitigation._filters import MeasurementFilter, TensoredFilter +from qiskit.utils.deprecation import deprecate_func class CompleteMeasFitter: """ - Measurement correction fitter for a full calibration + Deprecated: Measurement correction fitter for a full calibration """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", + ) def __init__( self, results, @@ -211,9 +216,13 @@ def readout_fidelity(self, label_list=None): class TensoredMeasFitter: """ - Measurement correction fitter for a tensored calibration. + Deprecated: Measurement correction fitter for a tensored calibration. """ + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", + ) def __init__( self, results, diff --git a/qiskit/utils/quantum_instance.py b/qiskit/utils/quantum_instance.py index 3a563e273d8a..a4bbb76e1b00 100644 --- a/qiskit/utils/quantum_instance.py +++ b/qiskit/utils/quantum_instance.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2022. +# (C) Copyright IBM 2018, 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 @@ -38,6 +38,7 @@ CompleteMeasFitter, TensoredMeasFitter, ) +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) @@ -124,7 +125,7 @@ def type_from_instance(meas_instance): class QuantumInstance: - """Quantum Backend including execution setting.""" + """Deprecated: Quantum Backend including execution setting.""" _BACKEND_CONFIG = ["basis_gates", "coupling_map"] _COMPILE_CONFIG = ["initial_layout", "seed_transpiler", "optimization_level"] @@ -143,6 +144,10 @@ class QuantumInstance: "statevector_hpc_gate_opt", ] + _BACKEND_OPTIONS_QASM_ONLY + @deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", + ) def __init__( self, backend, diff --git a/qiskit/utils/run_circuits.py b/qiskit/utils/run_circuits.py index a480123b0ee1..9714234ca694 100644 --- a/qiskit/utils/run_circuits.py +++ b/qiskit/utils/run_circuits.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 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 @@ -23,6 +23,7 @@ from qiskit.providers import Backend, JobStatus, JobError, Job from qiskit.providers.jobstatus import JOB_FINAL_STATES from qiskit.result import Result +from qiskit.utils.deprecation import deprecate_func from ..exceptions import QiskitError, MissingOptionalLibraryError from .backend_utils import ( is_aer_provider, @@ -39,10 +40,14 @@ logger = logging.getLogger(__name__) +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def find_regs_by_name( circuit: QuantumCircuit, name: str, qreg: bool = True ) -> Optional[Union[QuantumRegister, ClassicalRegister]]: - """Find the registers in the circuits. + """Deprecated: Find the registers in the circuits. Args: circuit: the quantum circuit. @@ -100,6 +105,10 @@ def _safe_get_job_status(job: Job, job_id: str, max_job_retries: int, wait: floa return job_status +@deprecate_func( + since="0.24.0", + additional_msg="For code migration guidelines, visit https://qisk.it/qi_migration.", +) def run_circuits( circuits: Union[QuantumCircuit, List[QuantumCircuit]], backend: Backend, @@ -111,7 +120,7 @@ def run_circuits( max_job_retries: int = 50, ) -> Result: """ - An execution wrapper with Qiskit-Terra, with job auto recover capability. + Deprecated: An execution wrapper with Qiskit-Terra, with job auto recover capability. The auto-recovery feature is only applied for non-simulator backend. This wrapper will try to get the result no matter how long it takes. diff --git a/releasenotes/notes/deprecate-opflow-qi-32f7e27884deea3f.yaml b/releasenotes/notes/deprecate-opflow-qi-32f7e27884deea3f.yaml new file mode 100644 index 000000000000..4e55ffccf0d9 --- /dev/null +++ b/releasenotes/notes/deprecate-opflow-qi-32f7e27884deea3f.yaml @@ -0,0 +1,14 @@ +--- +deprecations: + - | + The modules :mod:`qiskit.opflow`, :mod:`qiskit.utils.backend_utils`, + :mod:`qiskit.utils.mitigation`, + :mod:`qiskit.utils.measurement_error_mitigation`, + class :class:`qiskit.utils.QuantumInstance` and methods + :meth:`~qiskit.utils.run_circuits.find_regs_by_name`, + :meth:`~qiskit.utils.run_circuits.run_circuits` + have been deprecated and will be removed in a future release. + Using :class:`~qiskit.utils.QuantumInstance` is superseded by + :class:`~qiskit.primitives.BaseSampler`. + See `Opflow Migration `__. + See `QuantumInstance Migration `__. diff --git a/test/python/algorithms/evolvers/test_evolution_problem.py b/test/python/algorithms/evolvers/test_evolution_problem.py index 6f0cf4e7fd83..dc9d45f548a2 100644 --- a/test/python/algorithms/evolvers/test_evolution_problem.py +++ b/test/python/algorithms/evolvers/test_evolution_problem.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -51,7 +51,8 @@ def test_init_default(self): def test_init_all(self): """Tests that all fields are initialized correctly.""" t_parameter = Parameter("t") - hamiltonian = t_parameter * Z + Y + with self.assertWarns(DeprecationWarning): + hamiltonian = t_parameter * Z + Y time = 2 initial_state = One aux_operators = [X, Y] @@ -66,15 +67,17 @@ def test_init_all(self): t_param=t_parameter, param_value_dict=param_value_dict, ) + expected_hamiltonian = Y + t_parameter * Z - expected_hamiltonian = Y + t_parameter * Z expected_time = 2 expected_initial_state = One expected_aux_operators = [X, Y] expected_t_param = t_parameter expected_param_value_dict = {t_parameter: 3.2} - self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) + with self.assertWarns(DeprecationWarning): + self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) + self.assertEqual(evo_problem.time, expected_time) self.assertEqual(evo_problem.initial_state, expected_initial_state) self.assertEqual(evo_problem.aux_operators, expected_aux_operators) @@ -93,7 +96,9 @@ def test_validate_params(self): param_x = Parameter("x") param_y = Parameter("y") with self.subTest(msg="Parameter missing in dict."): - hamiltonian = param_x * X + param_y * Y + with self.assertWarns(DeprecationWarning): + hamiltonian = param_x * X + param_y * Y + param_dict = {param_y: 2} with self.assertWarns(DeprecationWarning): evolution_problem = EvolutionProblem( @@ -103,7 +108,9 @@ def test_validate_params(self): evolution_problem.validate_params() with self.subTest(msg="Empty dict."): - hamiltonian = param_x * X + param_y * Y + with self.assertWarns(DeprecationWarning): + hamiltonian = param_x * X + param_y * Y + param_dict = {} with self.assertWarns(DeprecationWarning): evolution_problem = EvolutionProblem( @@ -113,7 +120,9 @@ def test_validate_params(self): evolution_problem.validate_params() with self.subTest(msg="Extra parameter in dict."): - hamiltonian = param_x * X + param_y * Y + with self.assertWarns(DeprecationWarning): + hamiltonian = param_x * X + param_y * Y + param_dict = {param_y: 2, param_x: 1, Parameter("z"): 1} with self.assertWarns(DeprecationWarning): evolution_problem = EvolutionProblem( diff --git a/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py b/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py index 7f7f5e797c82..077957d8361d 100644 --- a/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py +++ b/test/python/algorithms/evolvers/trotterization/test_trotter_qrte.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 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 @@ -13,7 +13,6 @@ """Test TrotterQRTE.""" import unittest - from test.python.opflow import QiskitOpflowTestCase from ddt import ddt, data, unpack import numpy as np @@ -52,18 +51,19 @@ def setUp(self): algorithm_globals.random_seed = self.seed backend_statevector = BasicAer.get_backend("statevector_simulator") backend_qasm = BasicAer.get_backend("qasm_simulator") - self.quantum_instance = QuantumInstance( - backend=backend_statevector, - shots=1, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - self.quantum_instance_qasm = QuantumInstance( - backend=backend_qasm, - shots=8000, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) + with self.assertWarns(DeprecationWarning): + self.quantum_instance = QuantumInstance( + backend=backend_statevector, + shots=1, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.quantum_instance_qasm = QuantumInstance( + backend=backend_qasm, + shots=8000, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) self.backends_dict = { "qi_sv": self.quantum_instance, "qi_qasm": self.quantum_instance_qasm, @@ -123,10 +123,11 @@ def test_trotter_qrte_trotter_single_qubit_aux_ops(self): with self.subTest(msg=f"Test {backend_name} backend."): algorithm_globals.random_seed = 0 backend = self.backends_dict[backend_name] - expectation = ExpectationFactory.build( - operator=operator, - backend=backend, - ) + with self.assertWarns(DeprecationWarning): + expectation = ExpectationFactory.build( + operator=operator, + backend=backend, + ) with self.assertWarns(DeprecationWarning): trotter_qrte = TrotterQRTE(quantum_instance=backend, expectation=expectation) evolution_result = trotter_qrte.evolve(evolution_problem) diff --git a/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py index 2b5d899c7a1b..ea0198259560 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_adapt_vqe.py @@ -37,59 +37,65 @@ class TestAdaptVQE(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() algorithm_globals.random_seed = 42 - self.h2_op = PauliSumOp.from_list( - [ - ("IIII", -0.8105479805373266), - ("ZZII", -0.2257534922240251), - ("IIZI", +0.12091263261776641), - ("ZIZI", +0.12091263261776641), - ("IZZI", +0.17218393261915543), - ("IIIZ", +0.17218393261915546), - ("IZIZ", +0.1661454325638243), - ("ZZIZ", +0.1661454325638243), - ("IIZZ", -0.2257534922240251), - ("IZZZ", +0.16892753870087926), - ("ZZZZ", +0.17464343068300464), - ("IXIX", +0.04523279994605788), - ("ZXIX", +0.04523279994605788), - ("IXZX", -0.04523279994605788), - ("ZXZX", -0.04523279994605788), - ] - ) - self.excitation_pool = [ - PauliSumOp( - SparsePauliOp(["IIIY", "IIZY"], coeffs=[0.5 + 0.0j, -0.5 + 0.0j]), coeff=1.0 - ), - PauliSumOp( - SparsePauliOp(["ZYII", "IYZI"], coeffs=[-0.5 + 0.0j, 0.5 + 0.0j]), coeff=1.0 - ), - PauliSumOp( - SparsePauliOp( - ["ZXZY", "IXIY", "IYIX", "ZYZX", "IYZX", "ZYIX", "ZXIY", "IXZY"], - coeffs=[ - -0.125 + 0.0j, - 0.125 + 0.0j, - -0.125 + 0.0j, - 0.125 + 0.0j, - 0.125 + 0.0j, - -0.125 + 0.0j, - 0.125 + 0.0j, - -0.125 + 0.0j, - ], + + with self.assertWarns(DeprecationWarning): + self.h2_op = PauliSumOp.from_list( + [ + ("IIII", -0.8105479805373266), + ("ZZII", -0.2257534922240251), + ("IIZI", +0.12091263261776641), + ("ZIZI", +0.12091263261776641), + ("IZZI", +0.17218393261915543), + ("IIIZ", +0.17218393261915546), + ("IZIZ", +0.1661454325638243), + ("ZZIZ", +0.1661454325638243), + ("IIZZ", -0.2257534922240251), + ("IZZZ", +0.16892753870087926), + ("ZZZZ", +0.17464343068300464), + ("IXIX", +0.04523279994605788), + ("ZXIX", +0.04523279994605788), + ("IXZX", -0.04523279994605788), + ("ZXZX", -0.04523279994605788), + ] + ) + self.excitation_pool = [ + PauliSumOp( + SparsePauliOp(["IIIY", "IIZY"], coeffs=[0.5 + 0.0j, -0.5 + 0.0j]), coeff=1.0 + ), + PauliSumOp( + SparsePauliOp(["ZYII", "IYZI"], coeffs=[-0.5 + 0.0j, 0.5 + 0.0j]), coeff=1.0 ), - coeff=1.0, - ), - ] - self.initial_state = QuantumCircuit(QuantumRegister(4)) - self.initial_state.x(0) - self.initial_state.x(1) - self.ansatz = EvolvedOperatorAnsatz(self.excitation_pool, initial_state=self.initial_state) - self.optimizer = SLSQP() + PauliSumOp( + SparsePauliOp( + ["ZXZY", "IXIY", "IYIX", "ZYZX", "IYZX", "ZYIX", "ZXIY", "IXZY"], + coeffs=[ + -0.125 + 0.0j, + 0.125 + 0.0j, + -0.125 + 0.0j, + 0.125 + 0.0j, + 0.125 + 0.0j, + -0.125 + 0.0j, + 0.125 + 0.0j, + -0.125 + 0.0j, + ], + ), + coeff=1.0, + ), + ] + self.initial_state = QuantumCircuit(QuantumRegister(4)) + self.initial_state.x(0) + self.initial_state.x(1) + self.ansatz = EvolvedOperatorAnsatz( + self.excitation_pool, initial_state=self.initial_state + ) + self.optimizer = SLSQP() def test_default(self): """Default execution""" calc = AdaptVQE(VQE(Estimator(), self.ansatz, self.optimizer)) - res = calc.compute_minimum_eigenvalue(operator=self.h2_op) + + with self.assertWarns(DeprecationWarning): + res = calc.compute_minimum_eigenvalue(operator=self.h2_op) expected_eigenvalue = -1.85727503 @@ -117,7 +123,8 @@ def test_converged(self): VQE(Estimator(), self.ansatz, self.optimizer), gradient_threshold=1e-3, ) - res = calc.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + res = calc.compute_minimum_eigenvalue(operator=self.h2_op) self.assertEqual(res.termination_criterion, TerminationCriterion.CONVERGED) @@ -127,13 +134,14 @@ def test_maximum(self): VQE(Estimator(), self.ansatz, self.optimizer), max_iterations=1, ) - res = calc.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + res = calc.compute_minimum_eigenvalue(operator=self.h2_op) self.assertEqual(res.termination_criterion, TerminationCriterion.MAXIMUM) def test_eigenvalue_threshold(self): """Test for the eigenvalue_threshold attribute.""" - operator = PauliSumOp.from_list( + operator = SparsePauliOp.from_list( [ ("XX", 1.0), ("ZX", -0.5), @@ -142,8 +150,8 @@ def test_eigenvalue_threshold(self): ) ansatz = EvolvedOperatorAnsatz( [ - PauliSumOp.from_list([("YZ", 0.4)]), - PauliSumOp.from_list([("ZY", 0.5)]), + SparsePauliOp.from_list([("YZ", 0.4)]), + SparsePauliOp.from_list([("ZY", 0.5)]), ], initial_state=QuantumCircuit(2), ) @@ -163,7 +171,8 @@ def test_threshold_attribute(self): VQE(Estimator(), self.ansatz, self.optimizer), threshold=1e-3, ) - res = calc.compute_minimum_eigenvalue(operator=self.h2_op) + with self.assertWarns(DeprecationWarning): + res = calc.compute_minimum_eigenvalue(operator=self.h2_op) self.assertEqual(res.termination_criterion, TerminationCriterion.CONVERGED) @@ -203,9 +212,9 @@ def test_vqe_solver(self): """Test to check if the VQE solver remains the same or not""" solver = VQE(Estimator(), self.ansatz, self.optimizer) calc = AdaptVQE(solver) - _ = calc.compute_minimum_eigenvalue(operator=self.h2_op) - - self.assertEqual(solver.ansatz, calc.solver.ansatz) + with self.assertWarns(DeprecationWarning): + _ = calc.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertEqual(solver.ansatz, calc.solver.ansatz) def test_gradient_calculation(self): """Test to check if the gradient calculation""" @@ -219,7 +228,8 @@ def test_gradient_calculation(self): def test_supports_aux_operators(self): """Test that auxiliary operators are supported""" calc = AdaptVQE(VQE(Estimator(), self.ansatz, self.optimizer)) - res = calc.compute_minimum_eigenvalue(operator=self.h2_op, aux_operators=[self.h2_op]) + with self.assertWarns(DeprecationWarning): + res = calc.compute_minimum_eigenvalue(operator=self.h2_op, aux_operators=[self.h2_op]) expected_eigenvalue = -1.85727503 diff --git a/test/python/algorithms/minimum_eigensolvers/test_qaoa.py b/test/python/algorithms/minimum_eigensolvers/test_qaoa.py index de1bbe5e1492..ff3cce47e239 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_qaoa.py +++ b/test/python/algorithms/minimum_eigensolvers/test_qaoa.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -81,8 +81,8 @@ def test_qaoa(self, w, reps, mixer, solutions): qubit_op, _ = self._get_operator(w) qaoa = QAOA(self.sampler, COBYLA(), reps=reps, mixer=mixer) - - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) self.assertIn(graph_solution, solutions) @@ -111,8 +111,8 @@ def test_qaoa_qc_mixer(self, w, prob, solutions): mixer.rx(theta, range(num_qubits)) qaoa = QAOA(self.sampler, optimizer, reps=prob, mixer=mixer) - - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) self.assertIn(graph_solution, solutions) @@ -129,7 +129,8 @@ def test_qaoa_qc_mixer_many_parameters(self): mixer.rx(theta, range(num_qubits)) qaoa = QAOA(self.sampler, optimizer, reps=2, mixer=mixer) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) self.log.debug(x) graph_solution = self._get_graph_solution(x) @@ -145,7 +146,8 @@ def test_qaoa_qc_mixer_no_parameters(self): mixer.rx(np.pi / 2, range(num_qubits)) qaoa = QAOA(self.sampler, COBYLA(), reps=1, mixer=mixer) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) # we just assert that we get a result, it is not meaningful. self.assertIsNotNone(result.eigenstate) @@ -155,7 +157,8 @@ def test_change_operator_size(self): np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) ) qaoa = QAOA(self.sampler, COBYLA(), reps=1) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) with self.subTest(msg="QAOA 4x4"): @@ -173,8 +176,8 @@ def test_change_operator_size(self): ] ) ) - - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) with self.subTest(msg="QAOA 6x6"): @@ -199,8 +202,8 @@ def cb_callback(eval_count, parameters, mean, metadata): initial_point=init_pt, callback=cb_callback, ) - - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) @@ -221,7 +224,8 @@ def test_qaoa_random_initial_point(self): ) qubit_op, _ = self._get_operator(w) qaoa = QAOA(self.sampler, NELDER_MEAD(disp=True), reps=2) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) self.assertLess(result.eigenvalue, -0.97) @@ -235,7 +239,8 @@ def test_optimizer_scipy_callable(self): self.sampler, partial(scipy_minimize, method="Nelder-Mead", options={"maxiter": 2}), ) - result = qaoa.compute_minimum_eigenvalue(qubit_op) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(qubit_op) self.assertEqual(result.cost_function_evals, 5) def _get_operator(self, weight_matrix): @@ -262,7 +267,8 @@ def _get_operator(self, weight_matrix): pauli_list.append([0.5 * weight_matrix[i, j], Pauli((z_p, x_p))]) shift -= 0.5 * weight_matrix[i, j] opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] - return PauliSumOp.from_list(opflow_list), shift + with self.assertWarns(DeprecationWarning): + return PauliSumOp.from_list(opflow_list), shift def _get_graph_solution(self, x: np.ndarray) -> str: """Get graph solution from binary string. diff --git a/test/python/algorithms/minimum_eigensolvers/test_vqe.py b/test/python/algorithms/minimum_eigensolvers/test_vqe.py index 31a55d27d438..4e61aa89f704 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_vqe.py +++ b/test/python/algorithms/minimum_eigensolvers/test_vqe.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -208,26 +208,27 @@ def test_gradient_run(self): def test_with_two_qubit_reduction(self): """Test the VQE using TwoQubitReduction.""" - qubit_op = PauliSumOp.from_list( - [ - ("IIII", -0.8105479805373266), - ("IIIZ", 0.17218393261915552), - ("IIZZ", -0.22575349222402472), - ("IZZI", 0.1721839326191556), - ("ZZII", -0.22575349222402466), - ("IIZI", 0.1209126326177663), - ("IZZZ", 0.16892753870087912), - ("IXZX", -0.045232799946057854), - ("ZXIX", 0.045232799946057854), - ("IXIX", 0.045232799946057854), - ("ZXZX", -0.045232799946057854), - ("ZZIZ", 0.16614543256382414), - ("IZIZ", 0.16614543256382414), - ("ZZZZ", 0.17464343068300453), - ("ZIZI", 0.1209126326177663), - ] - ) - tapered_qubit_op = TwoQubitReduction(num_particles=2).convert(qubit_op) + with self.assertWarns(DeprecationWarning): + qubit_op = PauliSumOp.from_list( + [ + ("IIII", -0.8105479805373266), + ("IIIZ", 0.17218393261915552), + ("IIZZ", -0.22575349222402472), + ("IZZI", 0.1721839326191556), + ("ZZII", -0.22575349222402466), + ("IIZI", 0.1209126326177663), + ("IZZZ", 0.16892753870087912), + ("IXZX", -0.045232799946057854), + ("ZXIX", 0.045232799946057854), + ("IXIX", 0.045232799946057854), + ("ZXZX", -0.045232799946057854), + ("ZZIZ", 0.16614543256382414), + ("IZIZ", 0.16614543256382414), + ("ZZZZ", 0.17464343068300453), + ("ZIZI", 0.1209126326177663), + ] + ) + tapered_qubit_op = TwoQubitReduction(num_particles=2).convert(qubit_op) vqe = VQE( Estimator(), self.ry_wavefunction, @@ -406,10 +407,14 @@ def test_aux_operators_list(self): self.assertEqual(len(result.aux_operators_evaluated), 0) with self.subTest("Test with two auxiliary operators."): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list( + [("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)] + ) + aux_ops = [aux_op1, aux_op2] + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) self.assertEqual(len(result.aux_operators_evaluated), 2) # expectation values @@ -444,10 +449,13 @@ def test_aux_operators_dict(self): self.assertEqual(len(result.aux_operators_evaluated), 0) with self.subTest("Test with two auxiliary operators."): - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} - result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + with self.assertWarns(DeprecationWarning): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list( + [("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)] + ) + aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operators_evaluated), 2) diff --git a/test/python/algorithms/optimizers/test_gradient_descent.py b/test/python/algorithms/optimizers/test_gradient_descent.py index abe15f4b5362..8d2396650176 100644 --- a/test/python/algorithms/optimizers/test_gradient_descent.py +++ b/test/python/algorithms/optimizers/test_gradient_descent.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 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 @@ -18,7 +18,6 @@ from qiskit.algorithms.optimizers.steppable_optimizer import TellData, AskData from qiskit.circuit.library import PauliTwoDesign from qiskit.opflow import I, Z, StateFn -from qiskit.test.decorators import slow_test class TestGradientDescent(QiskitAlgorithmsTestCase): @@ -37,13 +36,13 @@ def grad(self, x): """Gradient of the objective function""" return 2 * (np.linalg.norm(x) - 1) * x / np.linalg.norm(x) - @slow_test def test_pauli_two_design(self): """Test standard gradient descent on the Pauli two-design example.""" circuit = PauliTwoDesign(3, reps=3, seed=2) parameters = list(circuit.parameters) - obs = Z ^ Z ^ I - expr = ~StateFn(obs) @ StateFn(circuit) + with self.assertWarns(DeprecationWarning): + obs = Z ^ Z ^ I + expr = ~StateFn(obs) @ StateFn(circuit) initial_point = np.array( [ @@ -67,8 +66,8 @@ def objective_pauli(x): optimizer = GradientDescent(maxiter=100, learning_rate=0.1, perturbation=0.1) - result = optimizer.minimize(objective_pauli, x0=initial_point) - + with self.assertWarns(DeprecationWarning): + result = optimizer.minimize(objective_pauli, x0=initial_point) self.assertLess(result.fun, -0.95) # final loss self.assertEqual(result.nfev, 1300) # function evaluations diff --git a/test/python/algorithms/optimizers/test_optimizer_aqgd.py b/test/python/algorithms/optimizers/test_optimizer_aqgd.py index da1c6e8db889..7eebf6bc5ce7 100644 --- a/test/python/algorithms/optimizers/test_optimizer_aqgd.py +++ b/test/python/algorithms/optimizers/test_optimizer_aqgd.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2022 +# (C) Copyright IBM 2019, 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 @@ -28,17 +28,17 @@ class TestOptimizerAQGD(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() - self.seed = 50 - algorithm_globals.random_seed = self.seed - self.qubit_op = PauliSumOp.from_list( - [ - ("II", -1.052373245772859), - ("IZ", 0.39793742484318045), - ("ZI", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) + algorithm_globals.random_seed = 50 + with self.assertWarns(DeprecationWarning): + self.qubit_op = PauliSumOp.from_list( + [ + ("II", -1.052373245772859), + ("IZ", 0.39793742484318045), + ("ZI", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) @slow_test @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") @@ -46,13 +46,14 @@ def test_simple(self): """test AQGD optimizer with the parameters as single values.""" from qiskit_aer import Aer - q_instance = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + Aer.get_backend("aer_simulator_statevector"), + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) aqgd = AQGD(momentum=0.0) + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=RealAmplitudes(), @@ -61,6 +62,7 @@ def test_simple(self): quantum_instance=q_instance, ) result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) + self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=3) @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") @@ -68,16 +70,18 @@ def test_list(self): """test AQGD optimizer with the parameters as lists.""" from qiskit_aer import Aer - q_instance = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + Aer.get_backend("aer_simulator_statevector"), + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) aqgd = AQGD(maxiter=[1000, 1000, 1000], eta=[1.0, 0.5, 0.3], momentum=[0.0, 0.5, 0.75]) + with self.assertWarns(DeprecationWarning): vqe = VQE(ansatz=RealAmplitudes(), optimizer=aqgd, quantum_instance=q_instance) result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) + self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=3) def test_raises_exception(self): @@ -90,13 +94,14 @@ def test_int_values(self): """test AQGD with int values passed as eta and momentum.""" from qiskit_aer import Aer - q_instance = QuantumInstance( - Aer.get_backend("aer_simulator_statevector"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) - + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + Aer.get_backend("aer_simulator_statevector"), + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) aqgd = AQGD(maxiter=1000, eta=1, momentum=0) + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=RealAmplitudes(), @@ -105,6 +110,7 @@ def test_int_values(self): quantum_instance=q_instance, ) result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) + self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=3) diff --git a/test/python/algorithms/optimizers/test_optimizer_nft.py b/test/python/algorithms/optimizers/test_optimizer_nft.py index 765c0ad3d880..31a29f4abd41 100644 --- a/test/python/algorithms/optimizers/test_optimizer_nft.py +++ b/test/python/algorithms/optimizers/test_optimizer_nft.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 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 @@ -27,21 +27,20 @@ class TestOptimizerNFT(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() - self.seed = 50 - algorithm_globals.random_seed = self.seed - self.qubit_op = PauliSumOp.from_list( - [ - ("II", -1.052373245772859), - ("IZ", 0.39793742484318045), - ("ZI", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) + algorithm_globals.random_seed = 50 + with self.assertWarns(DeprecationWarning): + self.qubit_op = PauliSumOp.from_list( + [ + ("II", -1.052373245772859), + ("IZ", 0.39793742484318045), + ("ZI", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) def test_nft(self): """Test NFT optimizer by using it""" - with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=RealAmplitudes(), @@ -53,6 +52,7 @@ def test_nft(self): ), ) result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) + self.assertAlmostEqual(result.eigenvalue.real, -1.857275, places=6) diff --git a/test/python/algorithms/optimizers/test_optimizers_scikitquant.py b/test/python/algorithms/optimizers/test_optimizers_scikitquant.py index f07a0238f7de..a980b046e758 100644 --- a/test/python/algorithms/optimizers/test_optimizers_scikitquant.py +++ b/test/python/algorithms/optimizers/test_optimizers_scikitquant.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 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 @@ -35,26 +35,29 @@ def setUp(self): """Set the problem.""" super().setUp() algorithm_globals.random_seed = 50 - self.qubit_op = PauliSumOp.from_list( - [ - ("II", -1.052373245772859), - ("IZ", 0.39793742484318045), - ("ZI", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) + with self.assertWarns(DeprecationWarning): + self.qubit_op = PauliSumOp.from_list( + [ + ("II", -1.052373245772859), + ("IZ", 0.39793742484318045), + ("ZI", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) def _optimize(self, optimizer): """launch vqe""" - qe = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) + with self.assertWarns(DeprecationWarning): + qe = QuantumInstance( + BasicAer.get_backend("statevector_simulator"), + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) with self.assertWarns(DeprecationWarning): vqe = VQE(ansatz=RealAmplitudes(), optimizer=optimizer, quantum_instance=qe) result = vqe.compute_minimum_eigenvalue(operator=self.qubit_op) + self.assertAlmostEqual(result.eigenvalue.real, -1.857, places=1) def test_bobyqa(self): diff --git a/test/python/algorithms/optimizers/test_spsa.py b/test/python/algorithms/optimizers/test_spsa.py index 394bee0ba81d..7204a25e2003 100644 --- a/test/python/algorithms/optimizers/test_spsa.py +++ b/test/python/algorithms/optimizers/test_spsa.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -40,8 +40,9 @@ def test_pauli_two_design(self, method): """Test SPSA on the Pauli two-design example.""" circuit = PauliTwoDesign(3, reps=1, seed=1) parameters = list(circuit.parameters) - obs = Z ^ Z ^ I - expr = ~StateFn(obs) @ StateFn(circuit) + with self.assertWarns(DeprecationWarning): + obs = Z ^ Z ^ I + expr = ~StateFn(obs) @ StateFn(circuit) initial_point = np.array( [0.82311034, 0.02611798, 0.21077064, 0.61842177, 0.09828447, 0.62013131] @@ -201,10 +202,12 @@ def objective(x): def test_qnspsa_fidelity_deprecation(self): """Test using a backend and expectation converter in get_fidelity warns.""" ansatz = PauliTwoDesign(2, reps=1, seed=2) + with self.assertWarns(DeprecationWarning): QNSPSA.get_fidelity(ansatz, backend=StatevectorSimulatorPy()) with self.assertWarns(DeprecationWarning): QNSPSA.get_fidelity(ansatz, expectation=MatrixExpectation()) + # No warning when used correctly. QNSPSA.get_fidelity(ansatz) @@ -230,7 +233,9 @@ def test_qnspsa_max_evals_grouped(self): """Test using max_evals_grouped with QNSPSA.""" circuit = PauliTwoDesign(3, reps=1, seed=1) num_parameters = circuit.num_parameters - obs = Z ^ Z ^ I + + with self.assertWarns(DeprecationWarning): + obs = Z ^ Z ^ I estimator = Estimator(options={"seed": 12}) diff --git a/test/python/algorithms/test_amplitude_estimators.py b/test/python/algorithms/test_amplitude_estimators.py index 4868f2643788..ea0a1af099ee 100644 --- a/test/python/algorithms/test_amplitude_estimators.py +++ b/test/python/algorithms/test_amplitude_estimators.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2022. +# (C) Copyright IBM 2018, 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 @@ -90,21 +90,24 @@ class TestBernoulli(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() - self._statevector = QuantumInstance( - backend=BasicAer.get_backend("statevector_simulator"), - seed_simulator=2, - seed_transpiler=2, - ) + with self.assertWarns(DeprecationWarning): + self._statevector = QuantumInstance( + backend=BasicAer.get_backend("statevector_simulator"), + seed_simulator=2, + seed_transpiler=2, + ) self._sampler = Sampler(options={"seed": 2}) def qasm(shots=100): - return QuantumInstance( - backend=BasicAer.get_backend("qasm_simulator"), - shots=shots, - seed_simulator=2, - seed_transpiler=2, - ) + with self.assertWarns(DeprecationWarning): + qi = QuantumInstance( + backend=BasicAer.get_backend("qasm_simulator"), + shots=shots, + seed_simulator=2, + seed_transpiler=2, + ) + return qi self._qasm = qasm @@ -128,11 +131,13 @@ def sampler_shots(shots=100): @unpack def test_statevector(self, prob, qae, expect): """statevector test""" + + problem = EstimationProblem(BernoulliStateIn(prob), 0, BernoulliGrover(prob)) + with self.assertWarns(DeprecationWarning): qae.quantum_instance = self._statevector - problem = EstimationProblem(BernoulliStateIn(prob), 0, BernoulliGrover(prob)) + result = qae.estimate(problem) - result = qae.estimate(problem) self._statevector.reset_execution_results() for key, value in expect.items(): self.assertAlmostEqual( @@ -186,11 +191,12 @@ def test_sampler(self, prob, qae, expect): @unpack def test_qasm(self, prob, shots, qae, expect): """qasm test""" + with self.assertWarns(DeprecationWarning): qae.quantum_instance = self._qasm(shots) - problem = EstimationProblem(BernoulliStateIn(prob), [0], BernoulliGrover(prob)) + problem = EstimationProblem(BernoulliStateIn(prob), [0], BernoulliGrover(prob)) + result = qae.estimate(problem) - result = qae.estimate(problem) for key, value in expect.items(): self.assertAlmostEqual( value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" @@ -375,21 +381,24 @@ class TestSineIntegral(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() - self._statevector = QuantumInstance( - backend=BasicAer.get_backend("statevector_simulator"), - seed_simulator=123, - seed_transpiler=41, - ) + with self.assertWarns(DeprecationWarning): + self._statevector = QuantumInstance( + backend=BasicAer.get_backend("statevector_simulator"), + seed_simulator=123, + seed_transpiler=41, + ) self._sampler = Sampler(options={"seed": 123}) def qasm(shots=100): - return QuantumInstance( - backend=BasicAer.get_backend("qasm_simulator"), - shots=shots, - seed_simulator=7192, - seed_transpiler=90000, - ) + with self.assertWarns(DeprecationWarning): + qi = QuantumInstance( + backend=BasicAer.get_backend("qasm_simulator"), + shots=shots, + seed_simulator=7192, + seed_transpiler=90000, + ) + return qi self._qasm = qasm @@ -411,12 +420,13 @@ def test_statevector(self, n, qae, expect): """Statevector end-to-end test""" # construct factories for A and Q # qae.state_preparation = SineIntegral(n) + estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) + with self.assertWarns(DeprecationWarning): qae.quantum_instance = self._statevector - estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) + # result = qae.run(self._statevector) + result = qae.estimate(estimation_problem) - # result = qae.run(self._statevector) - result = qae.estimate(estimation_problem) self._statevector.reset_execution_results() for key, value in expect.items(): self.assertAlmostEqual( @@ -456,12 +466,12 @@ def test_sampler(self, n, qae, expect): @unpack def test_qasm(self, n, shots, qae, expect): """QASM simulator end-to-end test.""" - # construct factories for A and Q + estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) + with self.assertWarns(DeprecationWarning): qae.quantum_instance = self._qasm(shots) - estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) + result = qae.estimate(estimation_problem) - result = qae.estimate(estimation_problem) for key, value in expect.items(): self.assertAlmostEqual( value, getattr(result, key), places=3, msg=f"estimate `{key}` failed" @@ -514,12 +524,13 @@ def test_sampler_with_shots(self, n, shots, qae, expect): def test_confidence_intervals(self, qae, key, expect): """End-to-end test for all confidence intervals.""" n = 3 + + estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) with self.assertWarns(DeprecationWarning): qae.quantum_instance = self._statevector - estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) + # statevector simulator + result = qae.estimate(estimation_problem) - # statevector simulator - result = qae.estimate(estimation_problem) self._statevector.reset_execution_results() methods = ["lr", "fi", "oi"] # short for likelihood_ratio, fisher, observed_fisher alphas = [0.1, 0.00001, 0.9] # alpha shouldn't matter in statevector @@ -534,7 +545,8 @@ def test_confidence_intervals(self, qae, key, expect): alpha = 0.01 with self.assertWarns(DeprecationWarning): qae.quantum_instance = self._qasm(shots) - result = qae.estimate(estimation_problem) + result = qae.estimate(estimation_problem) + for method, expected_confint in expect.items(): confint = qae.compute_confidence_interval(result, alpha, method) np.testing.assert_array_almost_equal(confint, expected_confint) @@ -543,13 +555,14 @@ def test_confidence_intervals(self, qae, key, expect): def test_iqae_confidence_intervals(self): """End-to-end test for the IQAE confidence interval.""" n = 3 - with self.assertWarns(DeprecationWarning): - qae = IterativeAmplitudeEstimation(0.1, 0.01, quantum_instance=self._statevector) expected_confint = (0.1984050, 0.3511015) estimation_problem = EstimationProblem(SineIntegral(n), objective_qubits=[n]) - # statevector simulator - result = qae.estimate(estimation_problem) + with self.assertWarns(DeprecationWarning): + qae = IterativeAmplitudeEstimation(0.1, 0.01, quantum_instance=self._statevector) + # statevector simulator + result = qae.estimate(estimation_problem) + self._statevector.reset_execution_results() confint = result.confidence_interval # confidence interval based on statevector should be empty, as we are sure of the result @@ -558,9 +571,11 @@ def test_iqae_confidence_intervals(self): # qasm simulator shots = 100 + with self.assertWarns(DeprecationWarning): qae.quantum_instance = self._qasm(shots) - result = qae.estimate(estimation_problem) + result = qae.estimate(estimation_problem) + confint = result.confidence_interval np.testing.assert_array_almost_equal(confint, expected_confint) self.assertTrue(confint[0] <= result.estimation <= confint[1]) @@ -611,11 +626,12 @@ def test_run_without_rescaling(self): # construct algo without rescaling backend = BasicAer.get_backend("statevector_simulator") + with self.assertWarns(DeprecationWarning): fae = FasterAmplitudeEstimation(0.1, 1, rescale=False, quantum_instance=backend) - # run the algo - result = fae.estimate(problem) + # run the algo + result = fae.estimate(problem) # assert the result is correct self.assertAlmostEqual(result.estimation, prob) @@ -688,15 +704,17 @@ def is_good_state(bitstr): ) # construct algo - backend = QuantumInstance( - BasicAer.get_backend(backend_str), seed_simulator=2, seed_transpiler=2 - ) + with self.assertWarns(DeprecationWarning): + backend = QuantumInstance( + BasicAer.get_backend(backend_str), seed_simulator=2, seed_transpiler=2 + ) # cannot use rescaling with a custom grover operator + with self.assertWarns(DeprecationWarning): fae = FasterAmplitudeEstimation(0.01, 5, rescale=False, quantum_instance=backend) - # run the algo - result = fae.estimate(problem) + # run the algo + result = fae.estimate(problem) # assert the result is correct self.assertAlmostEqual(result.estimation, expect, places=5) diff --git a/test/python/algorithms/test_aux_ops_evaluator.py b/test/python/algorithms/test_aux_ops_evaluator.py index 5a75843eeac9..11e4c8e76fc5 100644 --- a/test/python/algorithms/test_aux_ops_evaluator.py +++ b/test/python/algorithms/test_aux_ops_evaluator.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -45,13 +45,14 @@ def setUp(self): super().setUp() self.seed = 50 algorithm_globals.random_seed = self.seed - self.h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) + with self.assertWarns(DeprecationWarning): + self.h2_op = ( + -1.052373245772859 * (I ^ I) + + 0.39793742484318045 * (I ^ Z) + - 0.39793742484318045 * (Z ^ I) + - 0.01128010425623538 * (Z ^ Z) + + 0.18093119978423156 * (X ^ X) + ) self.threshold = 1e-8 self.backend_names = ["statevector_simulator", "qasm_simulator"] @@ -85,6 +86,7 @@ def _run_test( observables: ListOrDict[OperatorBase], quantum_instance: Union[QuantumInstance, Backend], ): + with self.assertWarns(DeprecationWarning): result = eval_observables( quantum_instance, quantum_state, observables, expectation, self.threshold @@ -133,16 +135,17 @@ def test_eval_observables(self, observables: ListOrDict[OperatorBase]): ) # to accommodate for qasm being imperfect with self.subTest(msg=f"Test {backend_name} backend."): backend = BasicAer.get_backend(backend_name) - quantum_instance = QuantumInstance( - backend=backend, - shots=shots, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - expectation = ExpectationFactory.build( - operator=self.h2_op, - backend=quantum_instance, - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend=backend, + shots=shots, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + expectation = ExpectationFactory.build( + operator=self.h2_op, + backend=quantum_instance, + ) with self.subTest(msg="Test QuantumCircuit."): self._run_test( @@ -174,17 +177,17 @@ def test_eval_observables(self, observables: ListOrDict[OperatorBase]): observables, quantum_instance, ) - - with self.subTest(msg="Test StateFn."): - statefn = StateFn(bound_ansatz) - self._run_test( - expected_result, - statefn, - decimal, - expectation, - observables, - quantum_instance, - ) + with self.assertWarns(DeprecationWarning): + with self.subTest(msg="Test StateFn."): + statefn = StateFn(bound_ansatz) + self._run_test( + expected_result, + statefn, + decimal, + expectation, + observables, + quantum_instance, + ) if __name__ == "__main__": diff --git a/test/python/algorithms/test_backendv1.py b/test/python/algorithms/test_backendv1.py index 2ed807d3d8ce..2dd8ca339e84 100644 --- a/test/python/algorithms/test_backendv1.py +++ b/test/python/algorithms/test_backendv1.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 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 @@ -35,18 +35,21 @@ def setUp(self): def test_vqe_qasm(self): """Test the VQE on QASM simulator.""" - h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) optimizer = SPSA(maxiter=300, last_avg=5) wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") - qasm_simulator = QuantumInstance( - self._qasm, shots=1024, seed_simulator=self.seed, seed_transpiler=self.seed - ) + + with self.assertWarns(DeprecationWarning): + h2_op = ( + -1.052373245772859 * (I ^ I) + + 0.39793742484318045 * (I ^ Z) + - 0.39793742484318045 * (Z ^ I) + - 0.01128010425623538 * (Z ^ Z) + + 0.18093119978423156 * (X ^ X) + ) + qasm_simulator = QuantumInstance( + self._qasm, shots=1024, seed_simulator=self.seed, seed_transpiler=self.seed + ) + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=wavefunction, @@ -55,6 +58,7 @@ def test_vqe_qasm(self): quantum_instance=qasm_simulator, ) result = vqe.compute_minimum_eigenvalue(operator=h2_op) + self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) def test_run_circuit_oracle(self): @@ -62,12 +66,16 @@ def test_run_circuit_oracle(self): oracle = QuantumCircuit(2) oracle.cz(0, 1) problem = AmplificationProblem(oracle, is_good_state=["11"]) - qi = QuantumInstance( - self._provider.get_backend("fake_vigo"), seed_simulator=12, seed_transpiler=32 - ) + + with self.assertWarns(DeprecationWarning): + qi = QuantumInstance( + self._provider.get_backend("fake_vigo"), seed_simulator=12, seed_transpiler=32 + ) + with self.assertWarns(DeprecationWarning): grover = Grover(quantum_instance=qi) - result = grover.amplify(problem) + result = grover.amplify(problem) + self.assertIn(result.top_measurement, ["11"]) def test_run_circuit_oracle_single_experiment_backend(self): @@ -77,10 +85,14 @@ def test_run_circuit_oracle_single_experiment_backend(self): problem = AmplificationProblem(oracle, is_good_state=["11"]) backend = self._provider.get_backend("fake_vigo") backend._configuration.max_experiments = 1 - qi = QuantumInstance(backend, seed_simulator=12, seed_transpiler=32) + + with self.assertWarns(DeprecationWarning): + qi = QuantumInstance(backend, seed_simulator=12, seed_transpiler=32) + with self.assertWarns(DeprecationWarning): grover = Grover(quantum_instance=qi) - result = grover.amplify(problem) + result = grover.amplify(problem) + self.assertIn(result.top_measurement, ["11"]) def test_measurement_error_mitigation_with_vqe(self): @@ -100,27 +112,29 @@ def test_measurement_error_mitigation_with_vqe(self): backend = self._qasm - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=167, - seed_transpiler=167, - noise_model=noise_model, - measurement_error_mitigation_cls=CompleteMeasFitter, - ) - - h2_hamiltonian = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend=backend, + seed_simulator=167, + seed_transpiler=167, + noise_model=noise_model, + measurement_error_mitigation_cls=CompleteMeasFitter, + ) + h2_hamiltonian = ( + -1.052373245772859 * (I ^ I) + + 0.39793742484318045 * (I ^ Z) + - 0.39793742484318045 * (Z ^ I) + - 0.01128010425623538 * (Z ^ Z) + + 0.18093119978423156 * (X ^ X) + ) + optimizer = SPSA(maxiter=200) ansatz = EfficientSU2(2, reps=1) with self.assertWarns(DeprecationWarning): vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) + self.assertGreater(quantum_instance.time_taken, 0.0) quantum_instance.reset_execution_results() self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) diff --git a/test/python/algorithms/test_backendv2.py b/test/python/algorithms/test_backendv2.py index 83bbf848f211..0987a631f690 100644 --- a/test/python/algorithms/test_backendv2.py +++ b/test/python/algorithms/test_backendv2.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021, 2022. +# (C) Copyright IBM 2021, 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 @@ -35,18 +35,21 @@ def setUp(self): def test_vqe_qasm(self): """Test the VQE on QASM simulator.""" - h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) optimizer = SPSA(maxiter=300, last_avg=5) wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") - qasm_simulator = QuantumInstance( - self._qasm, shots=1024, seed_simulator=self.seed, seed_transpiler=self.seed - ) + + with self.assertWarns(DeprecationWarning): + h2_op = ( + -1.052373245772859 * (I ^ I) + + 0.39793742484318045 * (I ^ Z) + - 0.39793742484318045 * (Z ^ I) + - 0.01128010425623538 * (Z ^ Z) + + 0.18093119978423156 * (X ^ X) + ) + qasm_simulator = QuantumInstance( + self._qasm, shots=1024, seed_simulator=self.seed, seed_transpiler=self.seed + ) + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=wavefunction, @@ -55,6 +58,7 @@ def test_vqe_qasm(self): quantum_instance=qasm_simulator, ) result = vqe.compute_minimum_eigenvalue(operator=h2_op) + self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) def test_run_circuit_oracle(self): @@ -62,12 +66,16 @@ def test_run_circuit_oracle(self): oracle = QuantumCircuit(2) oracle.cz(0, 1) problem = AmplificationProblem(oracle, is_good_state=["11"]) - qi = QuantumInstance( - self._provider.get_backend("fake_yorktown"), seed_simulator=12, seed_transpiler=32 - ) + + with self.assertWarns(DeprecationWarning): + qi = QuantumInstance( + self._provider.get_backend("fake_yorktown"), seed_simulator=12, seed_transpiler=32 + ) + with self.assertWarns(DeprecationWarning): grover = Grover(quantum_instance=qi) - result = grover.amplify(problem) + result = grover.amplify(problem) + self.assertIn(result.top_measurement, ["11"]) def test_run_circuit_oracle_single_experiment_backend(self): @@ -77,12 +85,16 @@ def test_run_circuit_oracle_single_experiment_backend(self): problem = AmplificationProblem(oracle, is_good_state=["11"]) backend = self._provider.get_backend("fake_yorktown") backend._configuration.max_experiments = 1 - qi = QuantumInstance( - self._provider.get_backend("fake_yorktown"), seed_simulator=12, seed_transpiler=32 - ) + + with self.assertWarns(DeprecationWarning): + qi = QuantumInstance( + self._provider.get_backend("fake_yorktown"), seed_simulator=12, seed_transpiler=32 + ) + with self.assertWarns(DeprecationWarning): grover = Grover(quantum_instance=qi) - result = grover.amplify(problem) + result = grover.amplify(problem) + self.assertIn(result.top_measurement, ["11"]) diff --git a/test/python/algorithms/test_grover.py b/test/python/algorithms/test_grover.py index ae9e627b411f..8fdeb12a7964 100644 --- a/test/python/algorithms/test_grover.py +++ b/test/python/algorithms/test_grover.py @@ -91,12 +91,13 @@ class TestGrover(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() - self.statevector = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), seed_simulator=12, seed_transpiler=32 - ) - self.qasm = QuantumInstance( - BasicAer.get_backend("qasm_simulator"), seed_simulator=12, seed_transpiler=32 - ) + with self.assertWarns(DeprecationWarning): + self.statevector = QuantumInstance( + BasicAer.get_backend("statevector_simulator"), seed_simulator=12, seed_transpiler=32 + ) + self.qasm = QuantumInstance( + BasicAer.get_backend("qasm_simulator"), seed_simulator=12, seed_transpiler=32 + ) self._sampler = Sampler() self._sampler_with_shots = Sampler(options={"shots": 1024, "seed": 123}) algorithm_globals.random_seed = 123 @@ -108,7 +109,11 @@ def test_implicit_phase_oracle_is_good_state(self, use_sampler): grover = self._prepare_grover(use_sampler) oracle = PhaseOracle("x & y") problem = AmplificationProblem(oracle) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertEqual(result.top_measurement, "11") @idata(itertools.product(["ideal", "shots", False], [[1, 2, 3], None, 2])) @@ -117,7 +122,11 @@ def test_iterations_with_good_state(self, use_sampler, iterations): """Test the algorithm with different iteration types and with good state""" grover = self._prepare_grover(use_sampler, iterations) problem = AmplificationProblem(Statevector.from_label("111"), is_good_state=["111"]) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertEqual(result.top_measurement, "111") @idata(itertools.product(["shots", False], [[1, 2, 3], None, 2])) @@ -126,7 +135,11 @@ def test_iterations_with_good_state_sample_from_iterations(self, use_sampler, it """Test the algorithm with different iteration types and with good state""" grover = self._prepare_grover(use_sampler, iterations, sample_from_iterations=True) problem = AmplificationProblem(Statevector.from_label("111"), is_good_state=["111"]) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertEqual(result.top_measurement, "111") @data("ideal", "shots", False) @@ -134,7 +147,11 @@ def test_fixed_iterations_without_good_state(self, use_sampler): """Test the algorithm with iterations as an int and without good state""" grover = self._prepare_grover(use_sampler, iterations=2) problem = AmplificationProblem(Statevector.from_label("111")) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertEqual(result.top_measurement, "111") @idata(itertools.product(["ideal", "shots", False], [[1, 2, 3], None])) @@ -143,10 +160,15 @@ def test_iterations_without_good_state(self, use_sampler, iterations): """Test the correct error is thrown for none/list of iterations and without good state""" grover = self._prepare_grover(use_sampler, iterations=iterations) problem = AmplificationProblem(Statevector.from_label("111")) + with self.assertRaisesRegex( TypeError, "An is_good_state function is required with the provided oracle" ): - grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + grover.amplify(problem) + else: + grover.amplify(problem) @data("ideal", "shots", False) def test_iterator(self, use_sampler): @@ -163,7 +185,11 @@ def iterator(): grover = self._prepare_grover(use_sampler, iterations=iterator()) problem = AmplificationProblem(Statevector.from_label("111"), is_good_state=["111"]) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertEqual(result.top_measurement, "111") @data("ideal", "shots", False) @@ -171,7 +197,11 @@ def test_growth_rate(self, use_sampler): """Test running the algorithm on a growth rate""" grover = self._prepare_grover(use_sampler, growth_rate=8 / 7) problem = AmplificationProblem(Statevector.from_label("111"), is_good_state=["111"]) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertEqual(result.top_measurement, "111") @data("ideal", "shots", False) @@ -185,7 +215,11 @@ def zero(): grover = self._prepare_grover(use_sampler, iterations=zero()) n = 5 problem = AmplificationProblem(Statevector.from_label("1" * n), is_good_state=["1" * n]) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertEqual(len(result.iterations), 2**n) @data("ideal", "shots", False) @@ -204,7 +238,11 @@ def test_run_circuit_oracle(self, use_sampler): oracle.cz(0, 1) problem = AmplificationProblem(oracle, is_good_state=["11"]) grover = self._prepare_grover(use_sampler) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertIn(result.top_measurement, ["11"]) @data("ideal", "shots", False) @@ -213,7 +251,11 @@ def test_run_state_vector_oracle(self, use_sampler): mark_state = Statevector.from_label("11") problem = AmplificationProblem(mark_state, is_good_state=["11"]) grover = self._prepare_grover(use_sampler) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertIn(result.top_measurement, ["11"]) @data("ideal", "shots", False) @@ -226,8 +268,12 @@ def test_run_custom_grover_operator(self, use_sampler): oracle=oracle, grover_operator=grover_op, is_good_state=["11"] ) grover = self._prepare_grover(use_sampler) - ret = grover.amplify(problem) - self.assertIn(ret.top_measurement, ["11"]) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) + self.assertIn(result.top_measurement, ["11"]) def test_optimal_num_iterations(self): """Test optimal_num_iterations""" @@ -261,7 +307,11 @@ def test_circuit_result(self, use_sampler): # is_good_state=['00'] is intentionally selected to obtain a list of results problem = AmplificationProblem(oracle, is_good_state=["00"]) grover = self._prepare_grover(use_sampler, iterations=[1, 2, 3, 4]) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) if use_sampler: for i, dist in enumerate(result.circuit_results): keys, values = zip(*sorted(dist.items())) @@ -287,7 +337,11 @@ def test_max_probability(self, use_sampler): oracle.cz(0, 1) problem = AmplificationProblem(oracle, is_good_state=["11"]) grover = self._prepare_grover(use_sampler) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertAlmostEqual(result.max_probability, 1.0) @unittest.skipUnless(HAS_TWEEDLEDUM, "tweedledum required for this test") @@ -297,7 +351,11 @@ def test_oracle_evaluation(self, use_sampler): oracle = PhaseOracle("x1 & x2 & (not x3)") problem = AmplificationProblem(oracle, is_good_state=oracle.evaluate_bitstring) grover = self._prepare_grover(use_sampler) - result = grover.amplify(problem) + if not use_sampler: + with self.assertWarns(DeprecationWarning): + result = grover.amplify(problem) + else: + result = grover.amplify(problem) self.assertTrue(result.oracle_evaluation) self.assertEqual("011", result.top_measurement) diff --git a/test/python/algorithms/test_measure_error_mitigation.py b/test/python/algorithms/test_measure_error_mitigation.py index 8a53708ec5f4..a252ab08af7d 100644 --- a/test/python/algorithms/test_measure_error_mitigation.py +++ b/test/python/algorithms/test_measure_error_mitigation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2022. +# (C) Copyright IBM 2019, 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 @@ -13,7 +13,6 @@ """Test Measurement Error Mitigation""" import unittest - from test.python.algorithms import QiskitAlgorithmsTestCase from ddt import ddt, data, unpack import numpy as np @@ -72,16 +71,19 @@ def test_measurement_error_mitigation_with_diff_qubit_order( CompleteMeasFitter if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter ) backend = Aer.get_backend("aer_simulator") - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=1679, - seed_transpiler=167, - shots=1000, - noise_model=noise_model, - measurement_error_mitigation_cls=fitter_cls, - cals_matrix_refresh_period=0, - mit_pattern=mit_pattern, - ) + + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend=backend, + seed_simulator=1679, + seed_transpiler=167, + shots=1000, + noise_model=noise_model, + measurement_error_mitigation_cls=fitter_cls, + cals_matrix_refresh_period=0, + mit_pattern=mit_pattern, + ) + # circuit qc1 = QuantumCircuit(2, 2) qc1.h(0) @@ -94,15 +96,16 @@ def test_measurement_error_mitigation_with_diff_qubit_order( qc2.measure(1, 0) qc2.measure(0, 1) - if fails: - self.assertRaisesRegex( - QiskitError, - "Each element in the mit pattern should have length 1.", - quantum_instance.execute, - [qc1, qc2], - ) - else: - quantum_instance.execute([qc1, qc2]) + with self.assertWarns(DeprecationWarning): + if fails: + self.assertRaisesRegex( + QiskitError, + "Each element in the mit pattern should have length 1.", + quantum_instance.execute, + [qc1, qc2], + ) + else: + quantum_instance.execute([qc1, qc2]) self.assertGreater(quantum_instance.time_taken, 0.0) quantum_instance.reset_execution_results() @@ -114,7 +117,8 @@ def test_measurement_error_mitigation_with_diff_qubit_order( qc3.measure(2, 1) qc3.measure(1, 2) - self.assertRaises(QiskitError, quantum_instance.execute, [qc1, qc3]) + with self.assertWarns(DeprecationWarning): + self.assertRaises(QiskitError, quantum_instance.execute, [qc1, qc3]) @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") @data(("CompleteMeasFitter", None), ("TensoredMeasFitter", [[0], [1]])) @@ -133,28 +137,33 @@ def test_measurement_error_mitigation_with_vqe(self, config): CompleteMeasFitter if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter ) backend = Aer.get_backend("aer_simulator") - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=167, - seed_transpiler=167, - noise_model=noise_model, - measurement_error_mitigation_cls=fitter_cls, - mit_pattern=mit_pattern, - ) - h2_hamiltonian = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend=backend, + seed_simulator=167, + seed_transpiler=167, + noise_model=noise_model, + measurement_error_mitigation_cls=fitter_cls, + mit_pattern=mit_pattern, + ) + optimizer = SPSA(maxiter=200) ansatz = EfficientSU2(2, reps=1) + with self.assertWarns(DeprecationWarning): + h2_hamiltonian = ( + -1.052373245772859 * (I ^ I) + + 0.39793742484318045 * (I ^ Z) + - 0.39793742484318045 * (Z ^ I) + - 0.01128010425623538 * (Z ^ Z) + + 0.18093119978423156 * (X ^ X) + ) + with self.assertWarns(DeprecationWarning): vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) + self.assertGreater(quantum_instance.time_taken, 0.0) quantum_instance.reset_execution_results() self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) @@ -183,7 +192,9 @@ def _get_operator(self, weight_matrix): pauli_list.append([0.5 * weight_matrix[i, j], Pauli((z_p, x_p))]) shift -= 0.5 * weight_matrix[i, j] opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] - return PauliSumOp.from_list(opflow_list), shift + + with self.assertWarns(DeprecationWarning): + return PauliSumOp.from_list(opflow_list), shift @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") def test_measurement_error_mitigation_qaoa(self): @@ -197,11 +208,13 @@ def test_measurement_error_mitigation_qaoa(self): initial_point = np.asarray([0.0, 0.0]) # Compute first without noise - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend=backend, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) + with self.assertWarns(DeprecationWarning): qaoa = QAOA( optimizer=COBYLA(maxiter=3), @@ -209,6 +222,7 @@ def test_measurement_error_mitigation_qaoa(self): initial_point=initial_point, ) result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + ref_eigenvalue = result.eigenvalue.real # compute with noise @@ -217,22 +231,25 @@ def test_measurement_error_mitigation_qaoa(self): read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]]) noise_model.add_all_qubit_readout_error(read_err) - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - noise_model=noise_model, - measurement_error_mitigation_cls=CompleteMeasFitter, - shots=10000, - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend=backend, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + noise_model=noise_model, + measurement_error_mitigation_cls=CompleteMeasFitter, + shots=10000, + ) with self.assertWarns(DeprecationWarning): + qaoa = QAOA( optimizer=COBYLA(maxiter=3), quantum_instance=quantum_instance, initial_point=initial_point, ) result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + self.assertAlmostEqual(result.eigenvalue.real, ref_eigenvalue, delta=0.05) @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") @@ -251,15 +268,18 @@ def test_measurement_error_mitigation_with_diff_qubit_order_ignis(self, fitter_s CompleteMeasFitter_IG if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter_IG ) backend = Aer.get_backend("aer_simulator") - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=1679, - seed_transpiler=167, - shots=1000, - noise_model=noise_model, - measurement_error_mitigation_cls=fitter_cls, - cals_matrix_refresh_period=0, - ) + + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend=backend, + seed_simulator=1679, + seed_transpiler=167, + shots=1000, + noise_model=noise_model, + measurement_error_mitigation_cls=fitter_cls, + cals_matrix_refresh_period=0, + ) + # circuit qc1 = QuantumCircuit(2, 2) qc1.h(0) @@ -314,14 +334,16 @@ def test_measurement_error_mitigation_with_vqe_ignis(self, config): CompleteMeasFitter_IG if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter_IG ) backend = Aer.get_backend("aer_simulator") - quantum_instance = QuantumInstance( - backend=backend, - seed_simulator=167, - seed_transpiler=167, - noise_model=noise_model, - measurement_error_mitigation_cls=fitter_cls, - mit_pattern=mit_pattern, - ) + + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend=backend, + seed_simulator=167, + seed_transpiler=167, + noise_model=noise_model, + measurement_error_mitigation_cls=fitter_cls, + mit_pattern=mit_pattern, + ) h2_hamiltonian = ( -1.052373245772859 * (I ^ I) @@ -336,6 +358,7 @@ def test_measurement_error_mitigation_with_vqe_ignis(self, config): with self.assertWarnsRegex(DeprecationWarning): vqe = VQE(ansatz=ansatz, optimizer=optimizer, quantum_instance=quantum_instance) result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) + self.assertGreater(quantum_instance.time_taken, 0.0) quantum_instance.reset_execution_results() self.assertAlmostEqual(result.eigenvalue.real, -1.86, delta=0.05) @@ -355,24 +378,25 @@ def test_calibration_results(self): counts_array = [None, None] for idx, is_use_mitigation in enumerate([True, False]): - if is_use_mitigation: - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - shots=1024, - measurement_error_mitigation_cls=CompleteMeasFitter_IG, - ) - with self.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): + with self.assertWarns(DeprecationWarning): + if is_use_mitigation: + quantum_instance = QuantumInstance( + backend, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + shots=1024, + measurement_error_mitigation_cls=CompleteMeasFitter_IG, + ) + with self.assertWarnsRegex(DeprecationWarning, r".*ignis.*"): + counts_array[idx] = quantum_instance.execute(qc_meas).get_counts() + else: + quantum_instance = QuantumInstance( + backend, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + shots=1024, + ) counts_array[idx] = quantum_instance.execute(qc_meas).get_counts() - else: - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - shots=1024, - ) - counts_array[idx] = quantum_instance.execute(qc_meas).get_counts() self.assertEqual( counts_array[0], counts_array[1], msg="Counts different with/without fitter." ) @@ -388,19 +412,22 @@ def test_circuit_modified(self): circuit.x(0) circuit.measure_all() - qi = QuantumInstance( - Aer.get_backend("aer_simulator"), - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - shots=1024, - measurement_error_mitigation_cls=CompleteMeasFitter, - ) + with self.assertWarns(DeprecationWarning): + qi = QuantumInstance( + Aer.get_backend("aer_simulator"), + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + shots=1024, + measurement_error_mitigation_cls=CompleteMeasFitter, + ) # The error happens on transpiled circuits since "execute" was changing the input array # Non transpiled circuits didn't have a problem because a new transpiled array was created # internally. circuits_ref = qi.transpile(circuit) # always returns a new array circuits_input = circuits_ref.copy() - _ = qi.execute(circuits_input, had_transpiled=True) + + with self.assertWarns(DeprecationWarning): + _ = qi.execute(circuits_input, had_transpiled=True) self.assertEqual(circuits_ref, circuits_input, msg="Transpiled circuit array modified.") @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required for this test") @@ -422,22 +449,27 @@ def test_tensor_subset_fitter(self): mit_pattern = [[idx] for idx in range(3)] backend = Aer.get_backend("aer_simulator") backend.set_options(seed_simulator=123) - mit_circuits = build_measurement_error_mitigation_circuits( - [0, 1, 2], - TensoredMeasFitter, - backend, - backend_config={}, - compile_config={}, - mit_pattern=mit_pattern, - ) - result = execute(mit_circuits[0], backend, noise_model=noise_model).result() - fitter = TensoredMeasFitter(result, mit_pattern=mit_pattern) + + with self.assertWarns(DeprecationWarning): + mit_circuits = build_measurement_error_mitigation_circuits( + [0, 1, 2], + TensoredMeasFitter, + backend, + backend_config={}, + compile_config={}, + mit_pattern=mit_pattern, + ) + result = execute(mit_circuits[0], backend, noise_model=noise_model).result() + fitter = TensoredMeasFitter(result, mit_pattern=mit_pattern) + cal_matrices = fitter.cal_matrices # Check that permutations and permuted subsets match. for subset in [[1, 0], [1, 2], [0, 2], [2, 0, 1]]: with self.subTest(subset=subset): - new_fitter = fitter.subset_fitter(subset) + with self.assertWarns(DeprecationWarning): + new_fitter = fitter.subset_fitter(subset) + for idx, qubit in enumerate(subset): self.assertTrue(np.allclose(new_fitter.cal_matrices[idx], cal_matrices[qubit])) @@ -458,7 +490,9 @@ def test_tensor_subset_fitter(self): result = execute( circuit, backend, noise_model=noise_model, shots=1000, seed_simulator=0 ).result() - new_result = fitter.subset_fitter([1, 2, 0]).filter.apply(result) + with self.subTest(subset=subset): + with self.assertWarns(DeprecationWarning): + new_result = fitter.subset_fitter([1, 2, 0]).filter.apply(result) # The noisy result should have a poor 111 state, the mit. result should be good. self.assertTrue(result.get_counts()["111"] < 800) diff --git a/test/python/algorithms/test_numpy_eigen_solver.py b/test/python/algorithms/test_numpy_eigen_solver.py index 6b2b41b796a7..36d2b66148d0 100644 --- a/test/python/algorithms/test_numpy_eigen_solver.py +++ b/test/python/algorithms/test_numpy_eigen_solver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 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 @@ -28,21 +28,23 @@ class TestNumPyEigensolver(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() - self.qubit_op = PauliSumOp.from_list( - [ - ("II", -1.052373245772859), - ("ZI", 0.39793742484318045), - ("IZ", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) + with self.assertWarns(DeprecationWarning): + self.qubit_op = PauliSumOp.from_list( + [ + ("II", -1.052373245772859), + ("ZI", 0.39793742484318045), + ("IZ", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) def test_ce(self): """Test basics""" with self.assertWarns(DeprecationWarning): algo = NumPyEigensolver() result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) + self.assertEqual(len(result.eigenvalues), 1) self.assertEqual(len(result.eigenstates), 1) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -53,6 +55,7 @@ def test_ce_k4(self): with self.assertWarns(DeprecationWarning): algo = NumPyEigensolver(k=4) result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) + self.assertEqual(len(result.eigenvalues), 4) self.assertEqual(len(result.eigenstates), 4) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -71,6 +74,7 @@ def criterion(x, v, a_v): with self.assertWarns(DeprecationWarning): algo = NumPyEigensolver(k=4, filter_criterion=criterion) result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=[]) + self.assertEqual(len(result.eigenvalues), 2) self.assertEqual(len(result.eigenstates), 2) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -93,6 +97,7 @@ def criterion(x, v, a_v): @data(X, Y, Z) def test_ce_k1_1q(self, op): """Test for 1 qubit operator""" + with self.assertWarns(DeprecationWarning): algo = NumPyEigensolver(k=1) result = algo.compute_eigenvalues(operator=op) @@ -101,6 +106,7 @@ def test_ce_k1_1q(self, op): @data(X, Y, Z) def test_ce_k2_1q(self, op): """Test for 1 qubit operator""" + with self.assertWarns(DeprecationWarning): algo = NumPyEigensolver(k=2) result = algo.compute_eigenvalues(operator=op) @@ -108,12 +114,16 @@ def test_ce_k2_1q(self, op): def test_aux_operators_list(self): """Test list-based aux_operators.""" - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + + with self.assertWarns(DeprecationWarning): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] + with self.assertWarns(DeprecationWarning): algo = NumPyEigensolver() result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=aux_ops) + self.assertEqual(len(result.eigenvalues), 1) self.assertEqual(len(result.eigenstates), 1) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -129,8 +139,10 @@ def test_aux_operators_list(self): # Go again with additional None and zero operators extra_ops = [*aux_ops, None, 0] + with self.assertWarns(DeprecationWarning): result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=extra_ops) + self.assertEqual(len(result.eigenvalues), 1) self.assertEqual(len(result.eigenstates), 1) self.assertEqual(result.eigenvalues.dtype, np.float64) @@ -149,9 +161,12 @@ def test_aux_operators_list(self): def test_aux_operators_dict(self): """Test dict-based aux_operators.""" - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + + with self.assertWarns(DeprecationWarning): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + with self.assertWarns(DeprecationWarning): algo = NumPyEigensolver() result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=aux_ops) @@ -170,8 +185,10 @@ def test_aux_operators_dict(self): # Go again with additional None and zero operators extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} + with self.assertWarns(DeprecationWarning): result = algo.compute_eigenvalues(operator=self.qubit_op, aux_operators=extra_ops) + self.assertEqual(len(result.eigenvalues), 1) self.assertEqual(len(result.eigenstates), 1) self.assertEqual(result.eigenvalues.dtype, np.float64) diff --git a/test/python/algorithms/test_numpy_minimum_eigen_solver.py b/test/python/algorithms/test_numpy_minimum_eigen_solver.py index c4d5a3b43ddd..8f50d5738338 100644 --- a/test/python/algorithms/test_numpy_minimum_eigen_solver.py +++ b/test/python/algorithms/test_numpy_minimum_eigen_solver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020, 2021. +# (C) Copyright IBM 2020, 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 @@ -28,28 +28,31 @@ class TestNumPyMinimumEigensolver(QiskitAlgorithmsTestCase): def setUp(self): super().setUp() - self.qubit_op = PauliSumOp.from_list( - [ - ("II", -1.052373245772859), - ("ZI", 0.39793742484318045), - ("IZ", -0.39793742484318045), - ("ZZ", -0.01128010425623538), - ("XX", 0.18093119978423156), - ] - ) - - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + with self.assertWarns(DeprecationWarning): + self.qubit_op = PauliSumOp.from_list( + [ + ("II", -1.052373245772859), + ("ZI", 0.39793742484318045), + ("IZ", -0.39793742484318045), + ("ZZ", -0.01128010425623538), + ("XX", 0.18093119978423156), + ] + ) + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + self.aux_ops_list = [aux_op1, aux_op2] self.aux_ops_dict = {"aux_op1": aux_op1, "aux_op2": aux_op2} def test_cme(self): """Basic test""" + with self.assertWarns(DeprecationWarning): algo = NumPyMinimumEigensolver() result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=self.aux_ops_list ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) @@ -61,6 +64,7 @@ def test_cme_reuse(self): with self.assertWarns(DeprecationWarning): algo = NumPyMinimumEigensolver() result = algo.compute_minimum_eigenvalue(operator=self.qubit_op) + self.assertEqual(result.eigenvalue.dtype, np.float64) self.assertAlmostEqual(result.eigenvalue, -1.85727503) self.assertIsNone(result.aux_operator_eigenvalues) @@ -70,6 +74,7 @@ def test_cme_reuse(self): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=self.aux_ops_list ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) @@ -78,6 +83,7 @@ def test_cme_reuse(self): # "Remove" aux_operators and go again with self.assertWarns(DeprecationWarning): result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=[]) + self.assertEqual(result.eigenvalue.dtype, np.float64) self.assertAlmostEqual(result.eigenvalue, -1.85727503) self.assertIsNone(result.aux_operator_eigenvalues) @@ -87,16 +93,19 @@ def test_cme_reuse(self): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=self.aux_ops_list ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[1], [0, 0]) # Finally just set one of aux_operators and main operator, remove aux_operators + with self.assertWarns(DeprecationWarning): result = algo.compute_minimum_eigenvalue( operator=self.aux_ops_list[0], aux_operators=[] ) + self.assertAlmostEqual(result.eigenvalue, 2 + 0j) self.assertIsNone(result.aux_operator_eigenvalues) @@ -113,6 +122,7 @@ def criterion(x, v, a_v): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=self.aux_ops_list ) + self.assertAlmostEqual(result.eigenvalue, -0.22491125 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues[0], [2, 0]) @@ -131,6 +141,7 @@ def criterion(x, v, a_v): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=self.aux_ops_list ) + self.assertEqual(result.eigenvalue, None) self.assertEqual(result.eigenstate, None) self.assertEqual(result.aux_operator_eigenvalues, None) @@ -138,9 +149,11 @@ def criterion(x, v, a_v): @data(X, Y, Z) def test_cme_1q(self, op): """Test for 1 qubit operator""" + with self.assertWarns(DeprecationWarning): algo = NumPyMinimumEigensolver() result = algo.compute_minimum_eigenvalue(operator=op) + self.assertAlmostEqual(result.eigenvalue, -1) def test_cme_aux_ops_dict(self): @@ -149,6 +162,7 @@ def test_cme_aux_ops_dict(self): with self.assertWarns(DeprecationWarning): algo = NumPyMinimumEigensolver() result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators={}) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertIsNone(result.aux_operator_eigenvalues) @@ -157,6 +171,7 @@ def test_cme_aux_ops_dict(self): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=self.aux_ops_dict ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) @@ -168,6 +183,7 @@ def test_cme_aux_ops_dict(self): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=extra_ops ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 3) np.testing.assert_array_almost_equal(result.aux_operator_eigenvalues["aux_op1"], [2, 0]) @@ -176,12 +192,16 @@ def test_cme_aux_ops_dict(self): def test_aux_operators_list(self): """Test list-based aux_operators.""" - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + + with self.assertWarns(DeprecationWarning): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = [aux_op1, aux_op2] + with self.assertWarns(DeprecationWarning): algo = NumPyMinimumEigensolver() result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values @@ -193,10 +213,12 @@ def test_aux_operators_list(self): # Go again with additional None and zero operators extra_ops = [*aux_ops, None, 0] + with self.assertWarns(DeprecationWarning): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=extra_ops ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 4) # expectation values @@ -211,12 +233,16 @@ def test_aux_operators_list(self): def test_aux_operators_dict(self): """Test dict-based aux_operators.""" - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + + with self.assertWarns(DeprecationWarning): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + with self.assertWarns(DeprecationWarning): algo = NumPyMinimumEigensolver() result = algo.compute_minimum_eigenvalue(operator=self.qubit_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values @@ -228,10 +254,12 @@ def test_aux_operators_dict(self): # Go again with additional None and zero operators extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} + with self.assertWarns(DeprecationWarning): result = algo.compute_minimum_eigenvalue( operator=self.qubit_op, aux_operators=extra_ops ) + self.assertAlmostEqual(result.eigenvalue, -1.85727503 + 0j) self.assertEqual(len(result.aux_operator_eigenvalues), 3) # expectation values diff --git a/test/python/algorithms/test_observables_evaluator.py b/test/python/algorithms/test_observables_evaluator.py index 9db5f02b312d..f4df4ba4bdf5 100644 --- a/test/python/algorithms/test_observables_evaluator.py +++ b/test/python/algorithms/test_observables_evaluator.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -165,8 +165,9 @@ def test_estimate_observables_shots(self): bound_ansatz = ansatz.bind_parameters(parameters) state = bound_ansatz estimator = Estimator(options={"shots": 2048}) - observables = [PauliSumOp.from_list([("ZZ", 2.0)])] - result = estimate_observables(estimator, state, observables, None, self.threshold) + with self.assertWarns(DeprecationWarning): + observables = [PauliSumOp.from_list([("ZZ", 2.0)])] + result = estimate_observables(estimator, state, observables, None, self.threshold) exact_result = self.get_exact_expectation(bound_ansatz, observables) expected_result = [(exact_result[0][0], {"variance": 1.0898, "shots": 2048})] diff --git a/test/python/algorithms/test_phase_estimator.py b/test/python/algorithms/test_phase_estimator.py index 2af6b902a7fc..15a5ef879f1b 100644 --- a/test/python/algorithms/test_phase_estimator.py +++ b/test/python/algorithms/test_phase_estimator.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2022. +# (C) Copyright IBM 2018, 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 @@ -13,7 +13,6 @@ """Test phase estimation""" import unittest - from test.python.algorithms import QiskitAlgorithmsTestCase from ddt import ddt, data, unpack import numpy as np @@ -35,7 +34,6 @@ Y, Z, I, - T, StateFn, PauliTrotterEvolution, MatrixEvolution, @@ -60,7 +58,10 @@ def hamiltonian_pe( """Run HamiltonianPhaseEstimation and return result with all phases.""" if backend is None: backend = qiskit.BasicAer.get_backend("statevector_simulator") - quantum_instance = qiskit.utils.QuantumInstance(backend=backend, shots=10000) + + with self.assertWarns(DeprecationWarning): + quantum_instance = qiskit.utils.QuantumInstance(backend=backend, shots=10000) + with self.assertWarns(DeprecationWarning): phase_est = HamiltonianPhaseEstimation( num_evaluation_qubits=num_evaluation_qubits, quantum_instance=quantum_instance @@ -76,10 +77,11 @@ def hamiltonian_pe( @data(MatrixEvolution(), PauliTrotterEvolution("suzuki", 4)) def test_pauli_sum_1(self, evolution): """Two eigenvalues from Pauli sum with X, Z""" - hamiltonian = 0.5 * X + Z - state_preparation = StateFn(H.to_circuit()) + with self.assertWarns(DeprecationWarning): + hamiltonian = 0.5 * X + Z + state_preparation = StateFn(H.to_circuit()) + result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=evolution) - result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=evolution) phase_dict = result.filter_phases(0.162, as_float=True) phases = list(phase_dict.keys()) phases.sort() @@ -90,10 +92,11 @@ def test_pauli_sum_1(self, evolution): @data(MatrixEvolution(), PauliTrotterEvolution("suzuki", 3)) def test_pauli_sum_2(self, evolution): """Two eigenvalues from Pauli sum with X, Y, Z""" - hamiltonian = 0.5 * X + Y + Z - state_preparation = None + with self.assertWarns(DeprecationWarning): + hamiltonian = 0.5 * X + Y + Z + state_preparation = None + result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=evolution) - result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=evolution) phase_dict = result.filter_phases(0.1, as_float=True) phases = list(phase_dict.keys()) phases.sort() @@ -105,15 +108,17 @@ def test_single_pauli_op(self): """Two eigenvalues from Pauli sum with X, Y, Z""" hamiltonian = Z state_preparation = None + with self.assertWarns(DeprecationWarning): + result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=None) - result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=None) eigv = result.most_likely_eigenvalue with self.subTest("First eigenvalue"): self.assertAlmostEqual(eigv, 1.0, delta=0.001) - state_preparation = StateFn(X.to_circuit()) + with self.assertWarns(DeprecationWarning): + state_preparation = StateFn(X.to_circuit()) + result = self.hamiltonian_pe(hamiltonian, state_preparation, bound=1.05) - result = self.hamiltonian_pe(hamiltonian, state_preparation, bound=1.05) eigv = result.most_likely_eigenvalue with self.subTest("Second eigenvalue"): self.assertAlmostEqual(eigv, -0.98, delta=0.01) @@ -121,15 +126,16 @@ def test_single_pauli_op(self): @slow_test def test_H2_hamiltonian(self): """Test H2 hamiltonian""" - hamiltonian = ( - (-1.0523732457728587 * (I ^ I)) - + (0.3979374248431802 * (I ^ Z)) - + (-0.3979374248431802 * (Z ^ I)) - + (-0.011280104256235324 * (Z ^ Z)) - + (0.18093119978423147 * (X ^ X)) - ) - state_preparation = StateFn((I ^ H).to_circuit()) - evo = PauliTrotterEvolution(trotter_mode="suzuki", reps=4) + with self.assertWarns(DeprecationWarning): + hamiltonian = ( + (-1.0523732457728587 * (I ^ I)) + + (0.3979374248431802 * (I ^ Z)) + + (-0.3979374248431802 * (Z ^ I)) + + (-0.011280104256235324 * (Z ^ Z)) + + (0.18093119978423147 * (X ^ X)) + ) + state_preparation = StateFn((I ^ H).to_circuit()) + evo = PauliTrotterEvolution(trotter_mode="suzuki", reps=4) result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=evo) with self.subTest("Most likely eigenvalues"): @@ -145,24 +151,31 @@ def test_H2_hamiltonian(self): def test_matrix_evolution(self): """1Q Hamiltonian with MatrixEvolution""" - hamiltonian = (0.5 * X) + (0.6 * Y) + (0.7 * I) - state_preparation = None - result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=MatrixEvolution()) + with self.assertWarns(DeprecationWarning): + hamiltonian = (0.5 * X) + (0.6 * Y) + (0.7 * I) + state_preparation = None + result = self.hamiltonian_pe( + hamiltonian, state_preparation, evolution=MatrixEvolution() + ) phase_dict = result.filter_phases(0.2, as_float=True) phases = list(phase_dict.keys()) self.assertAlmostEqual(phases[0], 1.490, delta=0.001) self.assertAlmostEqual(phases[1], -0.090, delta=0.001) def _setup_from_bound(self, evolution, op_class): - hamiltonian = 0.5 * X + Y + Z + with self.assertWarns(DeprecationWarning): + hamiltonian = 0.5 * X + Y + Z state_preparation = None bound = 1.2 * sum(abs(hamiltonian.coeff * coeff) for coeff in hamiltonian.coeffs) if op_class == "MatrixOp": hamiltonian = hamiltonian.to_matrix_op() backend = qiskit.BasicAer.get_backend("statevector_simulator") - qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) + + with self.assertWarns(DeprecationWarning): + qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) with self.assertWarns(DeprecationWarning): phase_est = HamiltonianPhaseEstimation(num_evaluation_qubits=6, quantum_instance=qi) + result = phase_est.estimate( hamiltonian=hamiltonian, bound=bound, @@ -173,23 +186,25 @@ def _setup_from_bound(self, evolution, op_class): def test_from_bound(self): """HamiltonianPhaseEstimation with bound""" - for op_class in ("SummedOp", "MatrixOp"): - result = self._setup_from_bound(MatrixEvolution(), op_class) - cutoff = 0.01 - phases = result.filter_phases(cutoff) - with self.subTest(f"test phases has the correct length: {op_class}"): - self.assertEqual(len(phases), 2) - with self.subTest(f"test scaled phases are correct: {op_class}"): - self.assertEqual(list(phases.keys()), [1.5, -1.5]) - phases = result.filter_phases(cutoff, scaled=False) - with self.subTest(f"test unscaled phases are correct: {op_class}"): - self.assertEqual(list(phases.keys()), [0.25, 0.75]) + with self.assertWarns(DeprecationWarning): + for op_class in ("SummedOp", "MatrixOp"): + result = self._setup_from_bound(MatrixEvolution(), op_class) + cutoff = 0.01 + phases = result.filter_phases(cutoff) + with self.subTest(f"test phases has the correct length: {op_class}"): + self.assertEqual(len(phases), 2) + with self.subTest(f"test scaled phases are correct: {op_class}"): + self.assertEqual(list(phases.keys()), [1.5, -1.5]) + phases = result.filter_phases(cutoff, scaled=False) + with self.subTest(f"test unscaled phases are correct: {op_class}"): + self.assertEqual(list(phases.keys()), [0.25, 0.75]) def test_trotter_from_bound(self): """HamiltonianPhaseEstimation with bound and Trotterization""" - result = self._setup_from_bound( - PauliTrotterEvolution(trotter_mode="suzuki", reps=3), op_class="SummedOp" - ) + with self.assertWarns(DeprecationWarning): + result = self._setup_from_bound( + PauliTrotterEvolution(trotter_mode="suzuki", reps=3), op_class="SummedOp" + ) phase_dict = result.filter_phases(0.1) phases = list(phase_dict.keys()) with self.subTest("test phases has the correct length"): @@ -206,24 +221,35 @@ def hamiltonian_pe_sampler( num_evaluation_qubits=6, evolution=None, bound=None, + uses_opflow=True, ): """Run HamiltonianPhaseEstimation and return result with all phases.""" sampler = Sampler() phase_est = HamiltonianPhaseEstimation( num_evaluation_qubits=num_evaluation_qubits, sampler=sampler ) - result = phase_est.estimate( - hamiltonian=hamiltonian, - state_preparation=state_preparation, - evolution=evolution, - bound=bound, - ) + if uses_opflow: + with self.assertWarns(DeprecationWarning): + result = phase_est.estimate( + hamiltonian=hamiltonian, + state_preparation=state_preparation, + evolution=evolution, + bound=bound, + ) + else: + result = phase_est.estimate( + hamiltonian=hamiltonian, + state_preparation=state_preparation, + evolution=evolution, + bound=bound, + ) return result @data(MatrixExponential(), SuzukiTrotter(reps=4)) def test_pauli_sum_1_sampler(self, evolution): """Two eigenvalues from Pauli sum with X, Z""" - hamiltonian = PauliSumOp(SparsePauliOp.from_list([("X", 0.5), ("Z", 1)])) + with self.assertWarns(DeprecationWarning): + hamiltonian = PauliSumOp(SparsePauliOp.from_list([("X", 0.5), ("Z", 1)])) state_preparation = QuantumCircuit(1).compose(HGate()) result = self.hamiltonian_pe_sampler(hamiltonian, state_preparation, evolution=evolution) @@ -237,7 +263,8 @@ def test_pauli_sum_1_sampler(self, evolution): @data(MatrixExponential(), SuzukiTrotter(reps=3)) def test_pauli_sum_2_sampler(self, evolution): """Two eigenvalues from Pauli sum with X, Y, Z""" - hamiltonian = PauliSumOp(SparsePauliOp.from_list([("X", 0.5), ("Z", 1), ("Y", 1)])) + with self.assertWarns(DeprecationWarning): + hamiltonian = PauliSumOp(SparsePauliOp.from_list([("X", 0.5), ("Z", 1), ("Y", 1)])) state_preparation = None result = self.hamiltonian_pe_sampler(hamiltonian, state_preparation, evolution=evolution) @@ -253,14 +280,18 @@ def test_single_pauli_op_sampler(self): hamiltonian = SparsePauliOp(Pauli("Z")) state_preparation = None - result = self.hamiltonian_pe_sampler(hamiltonian, state_preparation, evolution=None) + result = self.hamiltonian_pe_sampler( + hamiltonian, state_preparation, evolution=None, uses_opflow=False + ) eigv = result.most_likely_eigenvalue with self.subTest("First eigenvalue"): self.assertAlmostEqual(eigv, 1.0, delta=0.001) state_preparation = QuantumCircuit(1).compose(XGate()) - result = self.hamiltonian_pe_sampler(hamiltonian, state_preparation, bound=1.05) + result = self.hamiltonian_pe_sampler( + hamiltonian, state_preparation, bound=1.05, uses_opflow=False + ) eigv = result.most_likely_eigenvalue with self.subTest("Second eigenvalue"): self.assertAlmostEqual(eigv, -0.98, delta=0.01) @@ -272,17 +303,19 @@ def test_single_pauli_op_sampler(self): def test_H2_hamiltonian_sampler(self, state_preparation): """Test H2 hamiltonian""" - hamiltonian = PauliSumOp( - SparsePauliOp.from_list( - [ - ("II", -1.0523732457728587), - ("IZ", 0.3979374248431802), - ("ZI", -0.3979374248431802), - ("ZZ", -0.011280104256235324), - ("XX", 0.18093119978423147), - ] + with self.assertWarns(DeprecationWarning): + hamiltonian = PauliSumOp( + SparsePauliOp.from_list( + [ + ("II", -1.0523732457728587), + ("IZ", 0.3979374248431802), + ("ZI", -0.3979374248431802), + ("ZZ", -0.011280104256235324), + ("XX", 0.18093119978423147), + ] + ) ) - ) + evo = SuzukiTrotter(reps=4) result = self.hamiltonian_pe_sampler(hamiltonian, state_preparation, evolution=evo) with self.subTest("Most likely eigenvalues"): @@ -298,7 +331,8 @@ def test_H2_hamiltonian_sampler(self, state_preparation): def test_matrix_evolution_sampler(self): """1Q Hamiltonian with MatrixEvolution""" - hamiltonian = PauliSumOp(SparsePauliOp.from_list([("X", 0.5), ("Y", 0.6), ("I", 0.7)])) + with self.assertWarns(DeprecationWarning): + hamiltonian = PauliSumOp(SparsePauliOp.from_list([("X", 0.5), ("Y", 0.6), ("I", 0.7)])) state_preparation = None result = self.hamiltonian_pe_sampler( hamiltonian, state_preparation, evolution=MatrixExponential() @@ -327,17 +361,21 @@ def one_phase( if backend_type is None: backend_type = "qasm_simulator" backend = qiskit.BasicAer.get_backend(backend_type) - qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) if phase_estimator is None: phase_estimator = IterativePhaseEstimation with self.assertWarns(DeprecationWarning): + qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) + + with self.assertWarns(DeprecationWarning): + if phase_estimator == IterativePhaseEstimation: p_est = IterativePhaseEstimation(num_iterations=num_iterations, quantum_instance=qi) elif phase_estimator == PhaseEstimation: p_est = PhaseEstimation(num_evaluation_qubits=6, quantum_instance=qi) else: raise ValueError("Unrecognized phase_estimator") + result = p_est.estimate(unitary=unitary_circuit, state_preparation=state_preparation) phase = result.phase return phase @@ -354,12 +392,13 @@ def one_phase( def test_qpe_Z(self, state_preparation, expected_phase, backend_type, phase_estimator): """eigenproblem Z, |0> and |1>""" unitary_circuit = Z.to_circuit() - phase = self.one_phase( - unitary_circuit, - state_preparation, - backend_type=backend_type, - phase_estimator=phase_estimator, - ) + with self.assertWarns(DeprecationWarning): + phase = self.one_phase( + unitary_circuit, + state_preparation, + backend_type=backend_type, + phase_estimator=phase_estimator, + ) self.assertEqual(phase, expected_phase) @data( @@ -372,7 +411,10 @@ def test_qpe_Z(self, state_preparation, expected_phase, backend_type, phase_esti def test_qpe_X_plus_minus(self, state_preparation, expected_phase, phase_estimator): """eigenproblem X, (|+>, |->)""" unitary_circuit = X.to_circuit() - phase = self.one_phase(unitary_circuit, state_preparation, phase_estimator=phase_estimator) + with self.assertWarns(DeprecationWarning): + phase = self.one_phase( + unitary_circuit, state_preparation, phase_estimator=phase_estimator + ) self.assertEqual(phase, expected_phase) @data( @@ -387,7 +429,10 @@ def test_qpe_RZ(self, state_preparation, expected_phase, phase_estimator): alpha = np.pi / 2 unitary_circuit = QuantumCircuit(1) unitary_circuit.rz(alpha, 0) - phase = self.one_phase(unitary_circuit, state_preparation, phase_estimator=phase_estimator) + with self.assertWarns(DeprecationWarning): + phase = self.one_phase( + unitary_circuit, state_preparation, phase_estimator=phase_estimator + ) self.assertEqual(phase, expected_phase) def test_check_num_iterations(self): @@ -410,7 +455,9 @@ def phase_estimation( """ if backend is None: backend = qiskit.BasicAer.get_backend("statevector_simulator") - qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) + + with self.assertWarns(DeprecationWarning): + qi = qiskit.utils.QuantumInstance(backend=backend, shots=10000) with self.assertWarns(DeprecationWarning): phase_est = PhaseEstimation( num_evaluation_qubits=num_evaluation_qubits, quantum_instance=qi @@ -422,6 +469,7 @@ def phase_estimation( result = phase_est.estimate( unitary=unitary_circuit, state_preparation=state_preparation ) + return result @data(True, False) @@ -429,12 +477,14 @@ def test_qpe_Zplus(self, construct_circuit): """superposition eigenproblem Z, |+>""" unitary_circuit = Z.to_circuit() state_preparation = H.to_circuit() # prepare |+> - result = self.phase_estimation( - unitary_circuit, - state_preparation, - backend=qiskit.BasicAer.get_backend("statevector_simulator"), - construct_circuit=construct_circuit, - ) + + with self.assertWarns(DeprecationWarning): + result = self.phase_estimation( + unitary_circuit, + state_preparation, + backend=qiskit.BasicAer.get_backend("statevector_simulator"), + construct_circuit=construct_circuit, + ) phases = result.filter_phases(1e-15, as_float=True) with self.subTest("test phases has correct values"): @@ -541,7 +591,9 @@ def test_qpe_RZ_sampler(self, state_preparation, expected_phase, phase_estimator @unpack def test_qpe_two_qubit_unitary(self, state_preparation, expected_phase, phase_estimator): """two qubit unitary T ^ T""" - unitary_circuit = (T ^ T).to_circuit() + unitary_circuit = QuantumCircuit(2) + unitary_circuit.t(0) + unitary_circuit.t(1) phase = self.one_phase_sampler( unitary_circuit, state_preparation, diff --git a/test/python/algorithms/test_qaoa.py b/test/python/algorithms/test_qaoa.py index 15dcc61ad753..7c743af7e263 100644 --- a/test/python/algorithms/test_qaoa.py +++ b/test/python/algorithms/test_qaoa.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 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 @@ -63,17 +63,18 @@ def setUp(self): self.seed = 10598 algorithm_globals.random_seed = self.seed - self.qasm_simulator = QuantumInstance( - BasicAer.get_backend("qasm_simulator"), - shots=4096, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - self.statevector_simulator = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) + with self.assertWarns(DeprecationWarning): + self.qasm_simulator = QuantumInstance( + BasicAer.get_backend("qasm_simulator"), + shots=4096, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.statevector_simulator = QuantumInstance( + BasicAer.get_backend("statevector_simulator"), + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) @idata( [ @@ -89,13 +90,15 @@ def test_qaoa(self, w, prob, m, solutions, convert_to_matrix_op): self.log.debug("Testing %s-step QAOA with MaxCut on graph\n%s", prob, w) qubit_op, _ = self._get_operator(w) + if convert_to_matrix_op: - qubit_op = qubit_op.to_matrix_op() + with self.assertWarns(DeprecationWarning): + qubit_op = qubit_op.to_matrix_op() with self.assertWarns(DeprecationWarning): qaoa = QAOA(COBYLA(), prob, mixer=m, quantum_instance=self.statevector_simulator) - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) self.assertIn(graph_solution, solutions) @@ -120,7 +123,8 @@ def test_qaoa_qc_mixer(self, w, prob, solutions, convert_to_matrix_op): optimizer = COBYLA() qubit_op, _ = self._get_operator(w) if convert_to_matrix_op: - qubit_op = qubit_op.to_matrix_op() + with self.assertWarns(DeprecationWarning): + qubit_op = qubit_op.to_matrix_op() num_qubits = qubit_op.num_qubits mixer = QuantumCircuit(num_qubits) @@ -131,6 +135,7 @@ def test_qaoa_qc_mixer(self, w, prob, solutions, convert_to_matrix_op): qaoa = QAOA(optimizer, prob, mixer=mixer, quantum_instance=self.statevector_simulator) result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) self.assertIn(graph_solution, solutions) @@ -149,6 +154,7 @@ def test_qaoa_qc_mixer_many_parameters(self): with self.assertWarns(DeprecationWarning): qaoa = QAOA(optimizer, reps=2, mixer=mixer, quantum_instance=self.statevector_simulator) result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) self.log.debug(x) graph_solution = self._get_graph_solution(x) @@ -166,6 +172,7 @@ def test_qaoa_qc_mixer_no_parameters(self): with self.assertWarns(DeprecationWarning): qaoa = QAOA(COBYLA(), reps=1, mixer=mixer, quantum_instance=self.statevector_simulator) result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + # we just assert that we get a result, it is not meaningful. self.assertIsNotNone(result.eigenstate) @@ -177,6 +184,7 @@ def test_change_operator_size(self): with self.assertWarns(DeprecationWarning): qaoa = QAOA(COBYLA(), 1, quantum_instance=self.statevector_simulator) result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) with self.subTest(msg="QAOA 4x4"): @@ -194,9 +202,9 @@ def test_change_operator_size(self): ] ) ) - with self.assertWarns(DeprecationWarning): result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) with self.subTest(msg="QAOA 6x6"): @@ -224,6 +232,7 @@ def cb_callback(eval_count, parameters, mean, std): ) result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) @@ -253,6 +262,7 @@ def test_qaoa_initial_state(self, w, init_state): initial_state.initialize(init_state, initial_state.qubits) zero_init_state = QuantumCircuit(QuantumRegister(qubit_op.num_qubits, "q")) + with self.assertWarns(DeprecationWarning): qaoa_zero_init_state = QAOA( optimizer=optimizer, @@ -266,9 +276,8 @@ def test_qaoa_initial_state(self, w, init_state): initial_point=init_pt, quantum_instance=self.statevector_simulator, ) - - zero_circuits = qaoa_zero_init_state.construct_circuit(init_pt, qubit_op) - custom_circuits = qaoa.construct_circuit(init_pt, qubit_op) + zero_circuits = qaoa_zero_init_state.construct_circuit(init_pt, qubit_op) + custom_circuits = qaoa.construct_circuit(init_pt, qubit_op) self.assertEqual(len(zero_circuits), len(custom_circuits)) @@ -289,11 +298,12 @@ def test_qaoa_initial_state(self, w, init_state): else: original_init_qc = initial_state - job_init_state = self.statevector_simulator.execute(original_init_qc) - job_qaoa_init_state = self.statevector_simulator.execute(custom_init_qc) + with self.assertWarns(DeprecationWarning): + job_init_state = self.statevector_simulator.execute(original_init_qc) + job_qaoa_init_state = self.statevector_simulator.execute(custom_init_qc) - statevector_original = job_init_state.get_statevector(original_init_qc) - statevector_custom = job_qaoa_init_state.get_statevector(custom_init_qc) + statevector_original = job_init_state.get_statevector(original_init_qc) + statevector_custom = job_qaoa_init_state.get_statevector(custom_init_qc) self.assertListEqual(statevector_original.tolist(), statevector_custom.tolist()) @@ -303,6 +313,7 @@ def test_qaoa_random_initial_point(self): rx.undirected_gnp_random_graph(5, 0.5, seed=algorithm_globals.random_seed) ) qubit_op, _ = self._get_operator(w) + with self.assertWarns(DeprecationWarning): qaoa = QAOA( optimizer=NELDER_MEAD(disp=True), reps=1, quantum_instance=self.qasm_simulator @@ -315,13 +326,13 @@ def test_qaoa_construct_circuit_update(self): """Test updating operators with QAOA construct_circuit""" with self.assertWarns(DeprecationWarning): qaoa = QAOA() - ref = qaoa.construct_circuit([0, 0], I ^ Z)[0] - circ2 = qaoa.construct_circuit([0, 0], I ^ Z)[0] - self.assertEqual(circ2, ref) - circ3 = qaoa.construct_circuit([0, 0], Z ^ I)[0] - self.assertNotEqual(circ3, ref) - circ4 = qaoa.construct_circuit([0, 0], I ^ Z)[0] - self.assertEqual(circ4, ref) + ref = qaoa.construct_circuit([0, 0], I ^ Z)[0] + circ2 = qaoa.construct_circuit([0, 0], I ^ Z)[0] + self.assertEqual(circ2, ref) + circ3 = qaoa.construct_circuit([0, 0], Z ^ I)[0] + self.assertNotEqual(circ3, ref) + circ4 = qaoa.construct_circuit([0, 0], I ^ Z)[0] + self.assertEqual(circ4, ref) def test_optimizer_scipy_callable(self): """Test passing a SciPy optimizer directly as callable.""" @@ -357,7 +368,9 @@ def _get_operator(self, weight_matrix): pauli_list.append([0.5 * weight_matrix[i, j], Pauli((z_p, x_p))]) shift -= 0.5 * weight_matrix[i, j] opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] - return PauliSumOp.from_list(opflow_list), shift + + with self.assertWarns(DeprecationWarning): + return PauliSumOp.from_list(opflow_list), shift def _get_graph_solution(self, x: np.ndarray) -> str: """Get graph solution from binary string. diff --git a/test/python/algorithms/test_skip_qobj_validation.py b/test/python/algorithms/test_skip_qobj_validation.py index f3ef8827e26f..0474d6cff794 100644 --- a/test/python/algorithms/test_skip_qobj_validation.py +++ b/test/python/algorithms/test_skip_qobj_validation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2020. +# (C) Copyright IBM 2019, 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 @@ -13,7 +13,6 @@ """Test Skip Qobj Validation""" import unittest - from test.python.algorithms import QiskitAlgorithmsTestCase from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister from qiskit import BasicAer @@ -63,18 +62,20 @@ def setUp(self): def test_wo_backend_options(self): """without backend options test""" - quantum_instance = QuantumInstance( - self.backend, - seed_transpiler=self.random_seed, - seed_simulator=self.random_seed, - shots=1024, - ) - # run without backend_options and without noise - res_wo_bo = quantum_instance.execute(self.qc).get_counts(self.qc) - self.assertGreaterEqual(quantum_instance.time_taken, 0.0) - quantum_instance.reset_execution_results() - quantum_instance.skip_qobj_validation = True - res_wo_bo_skip_validation = quantum_instance.execute(self.qc).get_counts(self.qc) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + self.backend, + seed_transpiler=self.random_seed, + seed_simulator=self.random_seed, + shots=1024, + ) + # run without backend_options and without noise + res_wo_bo = quantum_instance.execute(self.qc).get_counts(self.qc) + self.assertGreaterEqual(quantum_instance.time_taken, 0.0) + quantum_instance.reset_execution_results() + quantum_instance.skip_qobj_validation = True + res_wo_bo_skip_validation = quantum_instance.execute(self.qc).get_counts(self.qc) + self.assertGreaterEqual(quantum_instance.time_taken, 0.0) quantum_instance.reset_execution_results() self.assertTrue(_compare_dict(res_wo_bo, res_wo_bo_skip_validation)) @@ -82,18 +83,20 @@ def test_wo_backend_options(self): def test_w_backend_options(self): """with backend options test""" # run with backend_options - quantum_instance = QuantumInstance( - self.backend, - seed_transpiler=self.random_seed, - seed_simulator=self.random_seed, - shots=1024, - backend_options={"initial_statevector": [0.5, 0.5, 0.5, 0.5]}, - ) - res_w_bo = quantum_instance.execute(self.qc).get_counts(self.qc) - self.assertGreaterEqual(quantum_instance.time_taken, 0.0) - quantum_instance.reset_execution_results() - quantum_instance.skip_qobj_validation = True - res_w_bo_skip_validation = quantum_instance.execute(self.qc).get_counts(self.qc) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + self.backend, + seed_transpiler=self.random_seed, + seed_simulator=self.random_seed, + shots=1024, + backend_options={"initial_statevector": [0.5, 0.5, 0.5, 0.5]}, + ) + res_w_bo = quantum_instance.execute(self.qc).get_counts(self.qc) + self.assertGreaterEqual(quantum_instance.time_taken, 0.0) + quantum_instance.reset_execution_results() + quantum_instance.skip_qobj_validation = True + res_w_bo_skip_validation = quantum_instance.execute(self.qc).get_counts(self.qc) + self.assertGreaterEqual(quantum_instance.time_taken, 0.0) quantum_instance.reset_execution_results() self.assertTrue(_compare_dict(res_w_bo, res_w_bo_skip_validation)) @@ -116,26 +119,28 @@ def test_w_noise(self): noise_model = NoiseModel() noise_model.add_readout_error([probs_given0, probs_given1], [0]) - quantum_instance = QuantumInstance( - self.backend, - seed_transpiler=self.random_seed, - seed_simulator=self.random_seed, - shots=1024, - noise_model=noise_model, - ) - res_w_noise = quantum_instance.execute(self.qc).get_counts(self.qc) - - quantum_instance.skip_qobj_validation = True - res_w_noise_skip_validation = quantum_instance.execute(self.qc).get_counts(self.qc) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + self.backend, + seed_transpiler=self.random_seed, + seed_simulator=self.random_seed, + shots=1024, + noise_model=noise_model, + ) + res_w_noise = quantum_instance.execute(self.qc).get_counts(self.qc) + quantum_instance.skip_qobj_validation = True + res_w_noise_skip_validation = quantum_instance.execute(self.qc).get_counts(self.qc) + self.assertTrue(_compare_dict(res_w_noise, res_w_noise_skip_validation)) - # BasicAer should fail: - with self.assertRaises(QiskitError): - _ = QuantumInstance(BasicAer.get_backend("qasm_simulator"), noise_model=noise_model) + with self.assertWarns(DeprecationWarning): + # BasicAer should fail: + with self.assertRaises(QiskitError): + _ = QuantumInstance(BasicAer.get_backend("qasm_simulator"), noise_model=noise_model) - with self.assertRaises(QiskitError): - quantum_instance = QuantumInstance(BasicAer.get_backend("qasm_simulator")) - quantum_instance.set_config(noise_model=noise_model) + with self.assertRaises(QiskitError): + quantum_instance = QuantumInstance(BasicAer.get_backend("qasm_simulator")) + quantum_instance.set_config(noise_model=noise_model) if __name__ == "__main__": diff --git a/test/python/algorithms/test_vqd.py b/test/python/algorithms/test_vqd.py index 8079a063ddf6..10ac4010cdbf 100644 --- a/test/python/algorithms/test_vqd.py +++ b/test/python/algorithms/test_vqd.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 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 @@ -55,17 +55,9 @@ def setUp(self): super().setUp() self.seed = 50 algorithm_globals.random_seed = self.seed - self.h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) self.h2_energy = -1.85727503 self.h2_energy_excited = [-1.85727503, -1.24458455] - self.test_op = MatrixOp(np.diagflat([3, 5, -1, 0.8, 0.2, 2, 1, -3])).to_pauli_op() self.test_results = [-3, -1] self.ryrz_wavefunction = TwoLocal( @@ -73,23 +65,33 @@ def setUp(self): ) self.ry_wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") - self.qasm_simulator = QuantumInstance( - BasicAer.get_backend("qasm_simulator"), - shots=2048, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - self.statevector_simulator = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - shots=1, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) + with self.assertWarns(DeprecationWarning): + self.h2_op = ( + -1.052373245772859 * (I ^ I) + + 0.39793742484318045 * (I ^ Z) + - 0.39793742484318045 * (Z ^ I) + - 0.01128010425623538 * (Z ^ Z) + + 0.18093119978423156 * (X ^ X) + ) + self.test_op = MatrixOp(np.diagflat([3, 5, -1, 0.8, 0.2, 2, 1, -3])).to_pauli_op() + self.qasm_simulator = QuantumInstance( + BasicAer.get_backend("qasm_simulator"), + shots=2048, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.statevector_simulator = QuantumInstance( + BasicAer.get_backend("statevector_simulator"), + shots=1, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) @slow_test def test_basic_aer_statevector(self): """Test the VQD on BasicAer's statevector simulator.""" wavefunction = self.ryrz_wavefunction + with self.assertWarns(DeprecationWarning): vqd = VQD( k=2, @@ -124,6 +126,7 @@ def test_mismatching_num_qubits(self): """Ensuring circuit and operator mismatch is caught""" wavefunction = QuantumCircuit(1) optimizer = SLSQP(maxiter=50) + with self.assertWarns(DeprecationWarning): vqd = VQD( k=1, @@ -144,20 +147,23 @@ def test_construct_circuit(self, expectation, num_circuits): """Test construct circuits returns QuantumCircuits and the right number of them.""" try: wavefunction = EfficientSU2(2, reps=1) + with self.assertWarns(DeprecationWarning): vqd = VQD(k=2, ansatz=wavefunction, expectation=expectation) - params = [0] * wavefunction.num_parameters - circuits = vqd.construct_circuit(parameter=params, operator=self.h2_op) - + params = [0] * wavefunction.num_parameters + circuits = vqd.construct_circuit(parameter=params, operator=self.h2_op) self.assertEqual(len(circuits), num_circuits) + for circuit in circuits: self.assertIsInstance(circuit, QuantumCircuit) + except MissingOptionalLibraryError as ex: self.skipTest(str(ex)) return def test_missing_varform_params(self): """Test specifying a variational form with no parameters raises an error.""" + circuit = QuantumCircuit(self.h2_op.num_qubits) with self.assertWarns(DeprecationWarning): vqd = VQD( @@ -178,9 +184,9 @@ def test_basic_aer_qasm(self): max_evals_grouped=1, quantum_instance=self.qasm_simulator, ) - # TODO benchmark this later. result = vqd.compute_eigenvalues(operator=self.h2_op) + np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=1 ) @@ -192,11 +198,12 @@ def test_with_aer_statevector(self): wavefunction = self.ry_wavefunction optimizer = L_BFGS_B() - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) with self.assertWarns(DeprecationWarning): vqd = VQD( k=2, @@ -218,11 +225,12 @@ def test_with_aer_qasm(self): optimizer = COBYLA(maxiter=1000) wavefunction = self.ry_wavefunction - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) with self.assertWarns(DeprecationWarning): vqd = VQD( @@ -246,12 +254,13 @@ def test_with_aer_qasm_snapshot_mode(self): optimizer = COBYLA(maxiter=400) wavefunction = self.ryrz_wavefunction - quantum_instance = QuantumInstance( - backend, - shots=100, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend, + shots=100, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) with self.assertWarns(DeprecationWarning): vqd = VQD( k=2, @@ -260,8 +269,8 @@ def test_with_aer_qasm_snapshot_mode(self): expectation=AerPauliExpectation(), quantum_instance=quantum_instance, ) - result = vqd.compute_eigenvalues(operator=self.test_op) + np.testing.assert_array_almost_equal(result.eigenvalues.real, self.test_results, decimal=1) def test_callback(self): @@ -306,26 +315,34 @@ def store_intermediate_result(eval_count, parameters, mean, std, step): def test_reuse(self): """Test re-using a VQD algorithm instance.""" + with self.assertWarns(DeprecationWarning): vqd = VQD(k=1) + with self.subTest(msg="assert running empty raises AlgorithmError"): with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): _ = vqd.compute_eigenvalues(operator=self.h2_op) ansatz = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") vqd.ansatz = ansatz + with self.subTest(msg="assert missing operator raises AlgorithmError"): with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): _ = vqd.compute_eigenvalues(operator=self.h2_op) - vqd.expectation = MatrixExpectation() - vqd.quantum_instance = self.statevector_simulator + with self.assertWarns(DeprecationWarning): + vqd.expectation = MatrixExpectation() + vqd.quantum_instance = self.statevector_simulator + with self.subTest(msg="assert VQE works once all info is available"): with self.assertWarns(DeprecationWarning): result = vqd.compute_eigenvalues(operator=self.h2_op) np.testing.assert_array_almost_equal(result.eigenvalues.real, self.h2_energy, decimal=2) - operator = PrimitiveOp(np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]])) + with self.assertWarns(DeprecationWarning): + operator = PrimitiveOp( + np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]]) + ) with self.subTest(msg="assert minimum eigensolver interface works"): with self.assertWarns(DeprecationWarning): @@ -344,6 +361,7 @@ def test_vqd_optimizer(self): def run_check(): with self.assertWarns(DeprecationWarning): result = vqd.compute_eigenvalues(operator=self.h2_op) + np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=3 ) @@ -360,6 +378,7 @@ def run_check(): @data(MatrixExpectation(), None) def test_backend_change(self, user_expectation): """Test that VQE works when backend changes.""" + with self.assertWarns(DeprecationWarning): vqd = VQD( k=1, @@ -368,24 +387,23 @@ def test_backend_change(self, user_expectation): expectation=user_expectation, quantum_instance=BasicAer.get_backend("statevector_simulator"), ) - with self.assertWarns(DeprecationWarning): result0 = vqd.compute_eigenvalues(operator=self.h2_op) - if user_expectation is not None: - with self.subTest("User expectation kept."): - self.assertEqual(vqd.expectation, user_expectation) + if user_expectation is not None: + with self.subTest("User expectation kept."): + self.assertEqual(vqd.expectation, user_expectation) - vqd.quantum_instance = BasicAer.get_backend("qasm_simulator") - - # works also if no expectation is set, since it will be determined automatically with self.assertWarns(DeprecationWarning): + vqd.quantum_instance = BasicAer.get_backend("qasm_simulator") + # works also if no expectation is set, since it will be determined automatically + result1 = vqd.compute_eigenvalues(operator=self.h2_op) - if user_expectation is not None: - with self.subTest("Change backend with user expectation, it is kept."): - self.assertEqual(vqd.expectation, user_expectation) + if user_expectation is not None: + with self.subTest("Change backend with user expectation, it is kept."): + self.assertEqual(vqd.expectation, user_expectation) - with self.subTest("Check results."): - self.assertEqual(len(result0.optimal_point), len(result1.optimal_point)) + with self.subTest("Check results."): + self.assertEqual(len(result0.optimal_point), len(result1.optimal_point)) def test_set_ansatz_to_none(self): """Tests that setting the ansatz to None results in the default behavior""" @@ -401,6 +419,7 @@ def test_set_ansatz_to_none(self): def test_set_optimizer_to_none(self): """Tests that setting the optimizer to None results in the default behavior""" + with self.assertWarns(DeprecationWarning): vqd = VQD( k=1, @@ -414,23 +433,25 @@ def test_set_optimizer_to_none(self): def test_aux_operators_list(self): """Test list-based aux_operators.""" wavefunction = self.ry_wavefunction + with self.assertWarns(DeprecationWarning): vqd = VQD(k=2, ansatz=wavefunction, quantum_instance=self.statevector_simulator) - # Start with an empty list - with self.assertWarns(DeprecationWarning): + # Start with an empty list result = vqd.compute_eigenvalues(self.h2_op, aux_operators=[]) + np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=2 ) self.assertIsNone(result.aux_operator_eigenvalues) # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] with self.assertWarns(DeprecationWarning): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) + np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=2 ) @@ -446,6 +467,7 @@ def test_aux_operators_list(self): extra_ops = [*aux_ops, None, 0] with self.assertWarns(DeprecationWarning): result = vqd.compute_eigenvalues(self.h2_op, aux_operators=extra_ops) + np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=2 ) @@ -463,23 +485,27 @@ def test_aux_operators_list(self): def test_aux_operators_dict(self): """Test dictionary compatibility of aux_operators""" wavefunction = self.ry_wavefunction + with self.assertWarns(DeprecationWarning): vqd = VQD(ansatz=wavefunction, quantum_instance=self.statevector_simulator) # Start with an empty dictionary with self.assertWarns(DeprecationWarning): result = vqd.compute_eigenvalues(self.h2_op, aux_operators={}) + np.testing.assert_array_almost_equal( result.eigenvalues.real, self.h2_energy_excited, decimal=2 ) self.assertIsNone(result.aux_operator_eigenvalues) # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + with self.assertWarns(DeprecationWarning): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} with self.assertWarns(DeprecationWarning): result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) + self.assertEqual(len(result.eigenvalues), 2) self.assertEqual(len(result.eigenstates), 2) self.assertEqual(result.eigenvalues.dtype, np.complex128) @@ -516,6 +542,7 @@ def test_aux_operators_dict(self): def test_aux_operator_std_dev_pauli(self): """Test non-zero standard deviations of aux operators with PauliExpectation.""" wavefunction = self.ry_wavefunction + with self.assertWarns(DeprecationWarning): vqd = VQD( ansatz=wavefunction, @@ -534,12 +561,14 @@ def test_aux_operator_std_dev_pauli(self): quantum_instance=self.qasm_simulator, ) - # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] + with self.assertWarns(DeprecationWarning): + # Go again with two auxiliary operators + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] with self.assertWarns(DeprecationWarning): result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1) @@ -556,6 +585,7 @@ def test_aux_operator_std_dev_pauli(self): aux_ops = [*aux_ops, None, 0] with self.assertWarns(DeprecationWarning): result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) + self.assertEqual(len(result.aux_operator_eigenvalues[0]), 4) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0][0], 2.0, places=1) @@ -589,10 +619,11 @@ def test_aux_operator_std_dev_aer_pauli(self): ), ) - # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] + with self.assertWarns(DeprecationWarning): + # Go again with two auxiliary operators + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] with self.assertWarns(DeprecationWarning): result = vqd.compute_eigenvalues(self.h2_op, aux_operators=aux_ops) self.assertEqual(len(result.aux_operator_eigenvalues), 2) diff --git a/test/python/algorithms/test_vqe.py b/test/python/algorithms/test_vqe.py index c0711e408b7c..c9fee9547d19 100644 --- a/test/python/algorithms/test_vqe.py +++ b/test/python/algorithms/test_vqe.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 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 @@ -52,10 +52,7 @@ from qiskit.quantum_info import Statevector from qiskit.transpiler import PassManager, PassManagerConfig from qiskit.transpiler.preset_passmanagers import level_1_pass_manager -from qiskit.utils import QuantumInstance, algorithm_globals, has_aer - -if has_aer(): - from qiskit import Aer +from qiskit.utils import QuantumInstance, algorithm_globals, optionals logger = "LocalLogger" @@ -89,30 +86,31 @@ def setUp(self): super().setUp() self.seed = 50 algorithm_globals.random_seed = self.seed - self.h2_op = ( - -1.052373245772859 * (I ^ I) - + 0.39793742484318045 * (I ^ Z) - - 0.39793742484318045 * (Z ^ I) - - 0.01128010425623538 * (Z ^ Z) - + 0.18093119978423156 * (X ^ X) - ) self.h2_energy = -1.85727503 self.ryrz_wavefunction = TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz") self.ry_wavefunction = TwoLocal(rotation_blocks="ry", entanglement_blocks="cz") - self.qasm_simulator = QuantumInstance( - BasicAer.get_backend("qasm_simulator"), - shots=1024, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - self.statevector_simulator = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - shots=1, - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) + with self.assertWarns(DeprecationWarning): + self.h2_op = ( + -1.052373245772859 * (I ^ I) + + 0.39793742484318045 * (I ^ Z) + - 0.39793742484318045 * (Z ^ I) + - 0.01128010425623538 * (Z ^ Z) + + 0.18093119978423156 * (X ^ X) + ) + self.qasm_simulator = QuantumInstance( + BasicAer.get_backend("qasm_simulator"), + shots=1024, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + self.statevector_simulator = QuantumInstance( + BasicAer.get_backend("statevector_simulator"), + shots=1, + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) def test_basic_aer_statevector(self): """Test the VQE on BasicAer's statevector simulator.""" @@ -147,6 +145,7 @@ def test_circuit_input(self): """Test running the VQE on a plain QuantumCircuit object.""" wavefunction = QuantumCircuit(2).compose(EfficientSU2(2)) optimizer = SLSQP(maxiter=50) + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=wavefunction, @@ -154,6 +153,7 @@ def test_circuit_input(self): quantum_instance=self.statevector_simulator, ) result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) @data( @@ -166,10 +166,11 @@ def test_construct_circuit(self, expectation, num_circuits): """Test construct circuits returns QuantumCircuits and the right number of them.""" try: wavefunction = EfficientSU2(2, reps=1) + with self.assertWarns(DeprecationWarning): vqe = VQE(ansatz=wavefunction, expectation=expectation) - params = [0] * wavefunction.num_parameters - circuits = vqe.construct_circuit(parameter=params, operator=self.h2_op) + params = [0] * wavefunction.num_parameters + circuits = vqe.construct_circuit(parameter=params, operator=self.h2_op) self.assertEqual(len(circuits), num_circuits) for circuit in circuits: @@ -181,6 +182,7 @@ def test_construct_circuit(self, expectation, num_circuits): def test_missing_varform_params(self): """Test specifying a variational form with no parameters raises an error.""" circuit = QuantumCircuit(self.h2_op.num_qubits) + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=circuit, quantum_instance=BasicAer.get_backend("statevector_simulator") @@ -203,6 +205,7 @@ def test_max_evals_grouped(self, optimizer, places, max_evals_grouped): quantum_instance=self.statevector_simulator, ) result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=places) def test_basic_aer_qasm(self): @@ -217,9 +220,8 @@ def test_basic_aer_qasm(self): max_evals_grouped=1, quantum_instance=self.qasm_simulator, ) - - # TODO benchmark this later. result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, -1.86823, places=2) def test_qasm_eigenvector_normalized(self): @@ -232,18 +234,22 @@ def test_qasm_eigenvector_normalized(self): amplitudes = list(result.eigenstate.values()) self.assertAlmostEqual(np.linalg.norm(amplitudes), 1.0, places=4) - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") + @unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") def test_with_aer_statevector(self): """Test VQE with Aer's statevector_simulator.""" - backend = Aer.get_backend("aer_simulator_statevector") + from qiskit_aer import AerSimulator + + backend = AerSimulator(method="statevector") wavefunction = self.ry_wavefunction optimizer = L_BFGS_B() - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=wavefunction, @@ -251,22 +257,25 @@ def test_with_aer_statevector(self): max_evals_grouped=1, quantum_instance=quantum_instance, ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") + @unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") def test_with_aer_qasm(self): """Test VQE with Aer's qasm_simulator.""" - backend = Aer.get_backend("aer_simulator") + from qiskit_aer import AerSimulator + + backend = AerSimulator() optimizer = SPSA(maxiter=200, last_avg=5) wavefunction = self.ry_wavefunction - quantum_instance = QuantumInstance( - backend, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) with self.assertWarns(DeprecationWarning): vqe = VQE( @@ -275,25 +284,27 @@ def test_with_aer_qasm(self): expectation=PauliExpectation(), quantum_instance=quantum_instance, ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, -1.86305, places=2) - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") + @unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") def test_with_aer_qasm_snapshot_mode(self): """Test the VQE using Aer's qasm_simulator snapshot mode.""" + from qiskit_aer import AerSimulator - backend = Aer.get_backend("aer_simulator") + backend = AerSimulator() optimizer = L_BFGS_B() wavefunction = self.ry_wavefunction - quantum_instance = QuantumInstance( - backend, - shots=1, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend, + shots=1, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=wavefunction, @@ -301,11 +312,11 @@ def test_with_aer_qasm_snapshot_mode(self): expectation=AerPauliExpectation(), quantum_instance=quantum_instance, ) - result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") + @unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") @data( CG(maxiter=1), L_BFGS_B(maxfun=1), @@ -315,12 +326,15 @@ def test_with_aer_qasm_snapshot_mode(self): ) def test_with_gradient(self, optimizer): """Test VQE using Gradient().""" - quantum_instance = QuantumInstance( - backend=Aer.get_backend("qasm_simulator"), - shots=1, - seed_simulator=algorithm_globals.random_seed, - seed_transpiler=algorithm_globals.random_seed, - ) + from qiskit_aer import AerSimulator + + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend=AerSimulator(), + shots=1, + seed_simulator=algorithm_globals.random_seed, + seed_transpiler=algorithm_globals.random_seed, + ) with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=self.ry_wavefunction, @@ -334,26 +348,29 @@ def test_with_gradient(self, optimizer): def test_with_two_qubit_reduction(self): """Test the VQE using TwoQubitReduction.""" - qubit_op = PauliSumOp.from_list( - [ - ("IIII", -0.8105479805373266), - ("IIIZ", 0.17218393261915552), - ("IIZZ", -0.22575349222402472), - ("IZZI", 0.1721839326191556), - ("ZZII", -0.22575349222402466), - ("IIZI", 0.1209126326177663), - ("IZZZ", 0.16892753870087912), - ("IXZX", -0.045232799946057854), - ("ZXIX", 0.045232799946057854), - ("IXIX", 0.045232799946057854), - ("ZXZX", -0.045232799946057854), - ("ZZIZ", 0.16614543256382414), - ("IZIZ", 0.16614543256382414), - ("ZZZZ", 0.17464343068300453), - ("ZIZI", 0.1209126326177663), - ] - ) - tapered_qubit_op = TwoQubitReduction(num_particles=2).convert(qubit_op) + + with self.assertWarns(DeprecationWarning): + qubit_op = PauliSumOp.from_list( + [ + ("IIII", -0.8105479805373266), + ("IIIZ", 0.17218393261915552), + ("IIZZ", -0.22575349222402472), + ("IZZI", 0.1721839326191556), + ("ZZII", -0.22575349222402466), + ("IIZI", 0.1209126326177663), + ("IZZZ", 0.16892753870087912), + ("IXZX", -0.045232799946057854), + ("ZXIX", 0.045232799946057854), + ("IXIX", 0.045232799946057854), + ("ZXZX", -0.045232799946057854), + ("ZZIZ", 0.16614543256382414), + ("IZIZ", 0.16614543256382414), + ("ZZZZ", 0.17464343068300453), + ("ZIZI", 0.1209126326177663), + ] + ) + tapered_qubit_op = TwoQubitReduction(num_particles=2).convert(qubit_op) + for simulator in [self.qasm_simulator, self.statevector_simulator]: with self.subTest(f"Test for {simulator}."), self.assertWarns(DeprecationWarning): vqe = VQE( @@ -395,6 +412,7 @@ def store_intermediate_result(eval_count, parameters, mean, std): def test_reuse(self): """Test re-using a VQE algorithm instance.""" + with self.assertWarns(DeprecationWarning): vqe = VQE() with self.subTest(msg="assert running empty raises AlgorithmError"): @@ -407,15 +425,20 @@ def test_reuse(self): with self.assertWarns(DeprecationWarning), self.assertRaises(AlgorithmError): _ = vqe.compute_minimum_eigenvalue(operator=self.h2_op) - vqe.expectation = MatrixExpectation() - vqe.quantum_instance = self.statevector_simulator + with self.assertWarns(DeprecationWarning): + vqe.expectation = MatrixExpectation() + vqe.quantum_instance = self.statevector_simulator + with self.subTest(msg="assert VQE works once all info is available"), self.assertWarns( DeprecationWarning ): result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=5) - operator = PrimitiveOp(np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]])) + with self.assertWarns(DeprecationWarning): + operator = PrimitiveOp( + np.array([[1, 0, 0, 0], [0, -1, 0, 0], [0, 0, 2, 0], [0, 0, 0, 3]]) + ) with self.subTest(msg="assert minimum eigensolver interface works"), self.assertWarns( DeprecationWarning @@ -434,6 +457,7 @@ def test_vqe_optimizer(self): def run_check(): with self.assertWarns(DeprecationWarning): result = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + self.assertAlmostEqual(result.eigenvalue.real, -1.85727503, places=5) run_check() @@ -448,6 +472,7 @@ def run_check(): @data(MatrixExpectation(), None) def test_backend_change(self, user_expectation): """Test that VQE works when backend changes.""" + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=TwoLocal(rotation_blocks=["ry", "rz"], entanglement_blocks="cz"), @@ -456,14 +481,14 @@ def test_backend_change(self, user_expectation): quantum_instance=BasicAer.get_backend("statevector_simulator"), ) result0 = vqe.compute_minimum_eigenvalue(operator=self.h2_op) + if user_expectation is not None: with self.subTest("User expectation kept."): self.assertEqual(vqe.expectation, user_expectation) - vqe.quantum_instance = BasicAer.get_backend("qasm_simulator") - # works also if no expectation is set, since it will be determined automatically with self.assertWarns(DeprecationWarning): + vqe.quantum_instance = BasicAer.get_backend("qasm_simulator") result1 = vqe.compute_minimum_eigenvalue(operator=self.h2_op) if user_expectation is not None: @@ -509,34 +534,40 @@ def wrapped_run(circuits, **kwargs): def test_set_ansatz_to_none(self): """Tests that setting the ansatz to None results in the default behavior""" + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=self.ryrz_wavefunction, optimizer=L_BFGS_B(), quantum_instance=self.statevector_simulator, ) + vqe.ansatz = None self.assertIsInstance(vqe.ansatz, RealAmplitudes) def test_set_optimizer_to_none(self): """Tests that setting the optimizer to None results in the default behavior""" + with self.assertWarns(DeprecationWarning): vqe = VQE( ansatz=self.ryrz_wavefunction, optimizer=L_BFGS_B(), quantum_instance=self.statevector_simulator, ) + vqe.optimizer = None self.assertIsInstance(vqe.optimizer, SLSQP) def test_optimizer_scipy_callable(self): """Test passing a SciPy optimizer directly as callable.""" + with self.assertWarns(DeprecationWarning): vqe = VQE( optimizer=partial(scipy_minimize, method="L-BFGS-B", options={"maxiter": 2}), quantum_instance=self.statevector_simulator, ) result = vqe.compute_minimum_eigenvalue(Z) + self.assertEqual(result.cost_function_evals, 20) def test_optimizer_callable(self): @@ -554,20 +585,26 @@ def test_optimizer_callable(self): def test_aux_operators_list(self): """Test list-based aux_operators.""" wavefunction = self.ry_wavefunction + + # Start with an empty list with self.assertWarns(DeprecationWarning): vqe = VQE(ansatz=wavefunction, quantum_instance=self.statevector_simulator) # Start with an empty list result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=[]) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertIsNone(result.aux_operator_eigenvalues) # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] + with self.assertWarns(DeprecationWarning): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] + with self.assertWarns(DeprecationWarning): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values @@ -581,6 +618,7 @@ def test_aux_operators_list(self): extra_ops = [*aux_ops, None, 0] with self.assertWarns(DeprecationWarning): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operator_eigenvalues), 4) # expectation values @@ -597,21 +635,26 @@ def test_aux_operators_list(self): def test_aux_operators_dict(self): """Test dictionary compatibility of aux_operators""" wavefunction = self.ry_wavefunction + with self.assertWarns(DeprecationWarning): vqe = VQE(ansatz=wavefunction, quantum_instance=self.statevector_simulator) # Start with an empty dictionary with self.assertWarns(DeprecationWarning): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators={}) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertIsNone(result.aux_operator_eigenvalues) # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + with self.assertWarns(DeprecationWarning): + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = {"aux_op1": aux_op1, "aux_op2": aux_op2} + with self.assertWarns(DeprecationWarning): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values @@ -625,6 +668,7 @@ def test_aux_operators_dict(self): extra_ops = {**aux_ops, "None_operator": None, "zero_operator": 0} with self.assertWarns(DeprecationWarning): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=extra_ops) + self.assertAlmostEqual(result.eigenvalue.real, self.h2_energy, places=6) self.assertEqual(len(result.aux_operator_eigenvalues), 3) # expectation values @@ -648,12 +692,15 @@ def test_aux_operator_std_dev_pauli(self): quantum_instance=self.qasm_simulator, ) - # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] + with self.assertWarns(DeprecationWarning): + # Go again with two auxiliary operators + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] + with self.assertWarns(DeprecationWarning): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) @@ -664,8 +711,10 @@ def test_aux_operator_std_dev_pauli(self): # Go again with additional None and zero operators aux_ops = [*aux_ops, None, 0] + with self.assertWarns(DeprecationWarning): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertEqual(len(result.aux_operator_eigenvalues), 4) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) @@ -680,9 +729,11 @@ def test_aux_operator_std_dev_pauli(self): self.assertAlmostEqual(result.aux_operator_eigenvalues[2][1], 0.0) self.assertAlmostEqual(result.aux_operator_eigenvalues[3][1], 0.0) - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") + @unittest.skipUnless(optionals.HAS_AER, "Qiskit aer is required to run these tests") def test_aux_operator_std_dev_aer_pauli(self): """Test non-zero standard deviations of aux operators with AerPauliExpectation.""" + from qiskit_aer import AerSimulator + wavefunction = self.ry_wavefunction with self.assertWarns(DeprecationWarning): vqe = VQE( @@ -690,19 +741,21 @@ def test_aux_operator_std_dev_aer_pauli(self): expectation=AerPauliExpectation(), optimizer=COBYLA(maxiter=0), quantum_instance=QuantumInstance( - backend=Aer.get_backend("qasm_simulator"), + backend=AerSimulator(), shots=1, seed_simulator=algorithm_globals.random_seed, seed_transpiler=algorithm_globals.random_seed, ), ) + with self.assertWarns(DeprecationWarning): + # Go again with two auxiliary operators + aux_op1 = PauliSumOp.from_list([("II", 2.0)]) + aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) + aux_ops = [aux_op1, aux_op2] - # Go again with two auxiliary operators - aux_op1 = PauliSumOp.from_list([("II", 2.0)]) - aux_op2 = PauliSumOp.from_list([("II", 0.5), ("ZZ", 0.5), ("YY", 0.5), ("XX", -0.5)]) - aux_ops = [aux_op1, aux_op2] with self.assertWarns(DeprecationWarning): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertEqual(len(result.aux_operator_eigenvalues), 2) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) @@ -715,6 +768,7 @@ def test_aux_operator_std_dev_aer_pauli(self): aux_ops = [*aux_ops, None, 0] with self.assertWarns(DeprecationWarning): result = vqe.compute_minimum_eigenvalue(self.h2_op, aux_operators=aux_ops) + self.assertEqual(len(result.aux_operator_eigenvalues), 4) # expectation values self.assertAlmostEqual(result.aux_operator_eigenvalues[0][0], 2.0, places=6) @@ -739,21 +793,20 @@ def test_2step_transpile(self): bound_counter = LogPass("bound_pass_manager") bound_pass = PassManager(bound_counter) - quantum_instance = QuantumInstance( - backend=BasicAer.get_backend("statevector_simulator"), - basis_gates=["u3", "cx"], - pass_manager=pre_pass, - bound_pass_manager=bound_pass, - ) - optimizer = SPSA(maxiter=5, learning_rate=0.01, perturbation=0.01) with self.assertWarns(DeprecationWarning): - vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) - _ = vqe.compute_minimum_eigenvalue(Z) + quantum_instance = QuantumInstance( + backend=BasicAer.get_backend("statevector_simulator"), + basis_gates=["u3", "cx"], + pass_manager=pre_pass, + bound_pass_manager=bound_pass, + ) - with self.assertLogs(logger, level="INFO") as cm, self.assertWarns(DeprecationWarning): - _ = vqe.compute_minimum_eigenvalue(Z) + with self.assertWarns(DeprecationWarning): + vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) + with self.assertLogs(logger, level="INFO") as cm: + _ = vqe.compute_minimum_eigenvalue(Z) expected = [ "pre_passmanager", @@ -777,11 +830,16 @@ def test_construct_eigenstate_from_optpoint(self): """Test constructing the eigenstate from the optimal point, if the default ansatz is used.""" # use Hamiltonian yielding more than 11 parameters in the default ansatz - hamiltonian = Z ^ Z ^ Z + with self.assertWarns(DeprecationWarning): + hamiltonian = Z ^ Z ^ Z + optimizer = SPSA(maxiter=1, learning_rate=0.01, perturbation=0.01) - quantum_instance = QuantumInstance( - backend=BasicAer.get_backend("statevector_simulator"), basis_gates=["u3", "cx"] - ) + + with self.assertWarns(DeprecationWarning): + quantum_instance = QuantumInstance( + backend=BasicAer.get_backend("statevector_simulator"), basis_gates=["u3", "cx"] + ) + with self.assertWarns(DeprecationWarning): vqe = VQE(optimizer=optimizer, quantum_instance=quantum_instance) result = vqe.compute_minimum_eigenvalue(hamiltonian) diff --git a/test/python/algorithms/time_evolvers/classical_methods/test_scipy_imaginary_evolver.py b/test/python/algorithms/time_evolvers/classical_methods/test_scipy_imaginary_evolver.py index 52e26d559623..5671bf221284 100644 --- a/test/python/algorithms/time_evolvers/classical_methods/test_scipy_imaginary_evolver.py +++ b/test/python/algorithms/time_evolvers/classical_methods/test_scipy_imaginary_evolver.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -123,13 +123,14 @@ def test_quantum_circuit_initial_state(self): def test_paulisumop_hamiltonian(self): """Tests if the hamiltonian can be a PauliSumOp""" - hamiltonian = PauliSumOp.from_list( - [ - ("XI", 1), - ("IX", 1), - ] - ) - observable = PauliSumOp.from_list([("ZZ", 1)]) + with self.assertWarns(DeprecationWarning): + hamiltonian = PauliSumOp.from_list( + [ + ("XI", 1), + ("IX", 1), + ] + ) + observable = PauliSumOp.from_list([("ZZ", 1)]) evolution_problem = TimeEvolutionProblem( hamiltonian=hamiltonian, time=1.0, diff --git a/test/python/algorithms/time_evolvers/test_pvqd.py b/test/python/algorithms/time_evolvers/test_pvqd.py index 55c7c0f45ba7..f0e3b3ae4471 100644 --- a/test/python/algorithms/time_evolvers/test_pvqd.py +++ b/test/python/algorithms/time_evolvers/test_pvqd.py @@ -12,6 +12,7 @@ """Tests for PVQD.""" import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase from functools import partial import numpy as np @@ -53,7 +54,7 @@ def inverse(self): @ddt -class TestPVQD(QiskitTestCase): +class TestPVQD(QiskitAlgorithmsTestCase): """Tests for the pVQD algorithm.""" def setUp(self): @@ -73,7 +74,8 @@ def test_pvqd(self, hamiltonian_type, gradient, num_timesteps): if hamiltonian_type == "ising": hamiltonian = self.hamiltonian elif hamiltonian_type == "pauli_sum_op": - hamiltonian = PauliSumOp(self.hamiltonian) + with self.assertWarns(DeprecationWarning): + hamiltonian = PauliSumOp(self.hamiltonian) else: # hamiltonian_type == "pauli": hamiltonian = Pauli("XX") diff --git a/test/python/algorithms/time_evolvers/test_time_evolution_problem.py b/test/python/algorithms/time_evolvers/test_time_evolution_problem.py index 8cc0134c2cdb..1982fb203749 100644 --- a/test/python/algorithms/time_evolvers/test_time_evolution_problem.py +++ b/test/python/algorithms/time_evolvers/test_time_evolution_problem.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2022. +# (C) Copyright IBM 2022, 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 @@ -52,7 +52,8 @@ def test_init_default(self): def test_init_all(self, initial_state): """Tests that all fields are initialized correctly.""" t_parameter = Parameter("t") - hamiltonian = t_parameter * Z + Y + with self.assertWarns(DeprecationWarning): + hamiltonian = t_parameter * Z + Y time = 2 aux_operators = [X, Y] param_value_dict = {t_parameter: 3.2} @@ -66,14 +67,16 @@ def test_init_all(self, initial_state): param_value_map=param_value_dict, ) - expected_hamiltonian = Y + t_parameter * Z + with self.assertWarns(DeprecationWarning): + expected_hamiltonian = Y + t_parameter * Z expected_time = 2 expected_type = QuantumCircuit expected_aux_operators = [X, Y] expected_t_param = t_parameter expected_param_value_dict = {t_parameter: 3.2} - self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) + with self.assertWarns(DeprecationWarning): + self.assertEqual(evo_problem.hamiltonian, expected_hamiltonian) self.assertEqual(evo_problem.time, expected_time) self.assertEqual(type(evo_problem.initial_state), expected_type) self.assertEqual(evo_problem.aux_operators, expected_aux_operators) @@ -84,7 +87,8 @@ def test_validate_params(self): """Tests expected errors are thrown on parameters mismatch.""" param_x = Parameter("x") with self.subTest(msg="Parameter missing in dict."): - hamiltonian = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Y")]), param_x) + with self.assertWarns(DeprecationWarning): + hamiltonian = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Y")]), param_x) evolution_problem = TimeEvolutionProblem(hamiltonian, 2, Zero) with assert_raises(ValueError): evolution_problem.validate_params() diff --git a/test/python/algorithms/time_evolvers/test_trotter_qrte.py b/test/python/algorithms/time_evolvers/test_trotter_qrte.py index e5d07a972c75..c784d2e31363 100644 --- a/test/python/algorithms/time_evolvers/test_trotter_qrte.py +++ b/test/python/algorithms/time_evolvers/test_trotter_qrte.py @@ -52,7 +52,8 @@ def setUp(self): @unpack def test_trotter_qrte_trotter_single_qubit(self, product_formula, expected_state): """Test for default TrotterQRTE on a single qubit.""" - operator = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Z")])) + with self.assertWarns(DeprecationWarning): + operator = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Z")])) initial_state = QuantumCircuit(1) time = 1 evolution_problem = TimeEvolutionProblem(operator, time, initial_state) @@ -168,7 +169,8 @@ def test_trotter_qrte_trotter_two_qubits(self, operator, expected_state): @unpack def test_trotter_qrte_qdrift(self, initial_state, expected_state): """Test for TrotterQRTE with QDrift.""" - operator = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Z")])) + with self.assertWarns(DeprecationWarning): + operator = PauliSumOp(SparsePauliOp([Pauli("X"), Pauli("Z")])) time = 1 evolution_problem = TimeEvolutionProblem(operator, time, initial_state) @@ -177,16 +179,18 @@ def test_trotter_qrte_qdrift(self, initial_state, expected_state): evolution_result = trotter_qrte.evolve(evolution_problem) np.testing.assert_array_almost_equal( - Statevector.from_instruction(evolution_result.evolved_state).data, expected_state.data + Statevector.from_instruction(evolution_result.evolved_state).data, + expected_state.data, ) @data((Parameter("t"), {}), (None, {Parameter("x"): 2}), (None, None)) @unpack def test_trotter_qrte_trotter_param_errors(self, t_param, param_value_dict): """Test TrotterQRTE with raising errors for parameters.""" - operator = Parameter("t") * PauliSumOp(SparsePauliOp([Pauli("X")])) + PauliSumOp( - SparsePauliOp([Pauli("Z")]) - ) + with self.assertWarns(DeprecationWarning): + operator = Parameter("t") * PauliSumOp(SparsePauliOp([Pauli("X")])) + PauliSumOp( + SparsePauliOp([Pauli("Z")]) + ) initial_state = QuantumCircuit(1) self._run_error_test(initial_state, operator, None, None, t_param, param_value_dict) @@ -194,7 +198,10 @@ def test_trotter_qrte_trotter_param_errors(self, t_param, param_value_dict): @unpack def test_trotter_qrte_trotter_aux_ops_errors(self, aux_ops, estimator): """Test TrotterQRTE with raising errors.""" - operator = PauliSumOp(SparsePauliOp([Pauli("X")])) + PauliSumOp(SparsePauliOp([Pauli("Z")])) + with self.assertWarns(DeprecationWarning): + operator = PauliSumOp(SparsePauliOp([Pauli("X")])) + PauliSumOp( + SparsePauliOp([Pauli("Z")]) + ) initial_state = QuantumCircuit(1) self._run_error_test(initial_state, operator, aux_ops, estimator, None, None) diff --git a/test/python/circuit/library/test_evolution_gate.py b/test/python/circuit/library/test_evolution_gate.py index 92926a077986..f6e625f2c0ce 100644 --- a/test/python/circuit/library/test_evolution_gate.py +++ b/test/python/circuit/library/test_evolution_gate.py @@ -38,7 +38,8 @@ def setUp(self): def test_matrix_decomposition(self): """Test the default decomposition.""" - op = (X ^ 3) + (Y ^ 3) + (Z ^ 3) + with self.assertWarns(DeprecationWarning): + op = (X ^ 3) + (Y ^ 3) + (Z ^ 3) time = 0.123 matrix = op.to_matrix() @@ -50,7 +51,8 @@ def test_matrix_decomposition(self): def test_lie_trotter(self): """Test constructing the circuit with Lie Trotter decomposition.""" - op = (X ^ 3) + (Y ^ 3) + (Z ^ 3) + with self.assertWarns(DeprecationWarning): + op = (X ^ 3) + (Y ^ 3) + (Z ^ 3) time = 0.123 reps = 4 evo_gate = PauliEvolutionGate(op, time, synthesis=LieTrotter(reps=reps)) @@ -59,29 +61,32 @@ def test_lie_trotter(self): def test_rzx_order(self): """Test ZX and XZ is mapped onto the correct qubits.""" - for op, indices in zip([X ^ Z, Z ^ X], [(0, 1), (1, 0)]): - with self.subTest(op=op, indices=indices): - evo_gate = PauliEvolutionGate(op) - decomposed = evo_gate.definition.decompose() - - # ┌───┐┌───────┐┌───┐ - # q_0: ─────┤ X ├┤ Rz(2) ├┤ X ├───── - # ┌───┐└─┬─┘└───────┘└─┬─┘┌───┐ - # q_1: ┤ H ├──■─────────────■──┤ H ├ - # └───┘ └───┘ - ref = QuantumCircuit(2) - ref.h(indices[1]) - ref.cx(indices[1], indices[0]) - ref.rz(2.0, indices[0]) - ref.cx(indices[1], indices[0]) - ref.h(indices[1]) - - # don't use circuit equality since RZX here decomposes with RZ on the bottom - self.assertTrue(Operator(decomposed).equiv(ref)) + with self.assertWarns(DeprecationWarning): + op = (X ^ 3) + (Y ^ 3) + (Z ^ 3) + for op, indices in zip([X ^ Z, Z ^ X], [(0, 1), (1, 0)]): + with self.subTest(op=op, indices=indices): + evo_gate = PauliEvolutionGate(op) + decomposed = evo_gate.definition.decompose() + + # ┌───┐┌───────┐┌───┐ + # q_0: ─────┤ X ├┤ Rz(2) ├┤ X ├───── + # ┌───┐└─┬─┘└───────┘└─┬─┘┌───┐ + # q_1: ┤ H ├──■─────────────■──┤ H ├ + # └───┘ └───┘ + ref = QuantumCircuit(2) + ref.h(indices[1]) + ref.cx(indices[1], indices[0]) + ref.rz(2.0, indices[0]) + ref.cx(indices[1], indices[0]) + ref.h(indices[1]) + + # don't use circuit equality since RZX here decomposes with RZ on the bottom + self.assertTrue(Operator(decomposed).equiv(ref)) def test_suzuki_trotter(self): """Test constructing the circuit with Lie Trotter decomposition.""" - op = (X ^ 3) + (Y ^ 3) + (Z ^ 3) + with self.assertWarns(DeprecationWarning): + op = (X ^ 3) + (Y ^ 3) + (Z ^ 3) time = 0.123 reps = 4 for order in [2, 4, 6]: @@ -103,7 +108,8 @@ def test_suzuki_trotter(self): def test_suzuki_trotter_manual(self): """Test the evolution circuit of Suzuki Trotter against a manually constructed circuit.""" - op = X + Y + with self.assertWarns(DeprecationWarning): + op = X + Y time = 0.1 reps = 1 evo_gate = PauliEvolutionGate(op, time, synthesis=SuzukiTrotter(order=4, reps=reps)) @@ -151,7 +157,8 @@ def test_qdrift_manual(self, op, time, reps, sampled_ops): def test_qdrift_evolution(self): """Test QDrift on an example.""" - op = 0.1 * (Z ^ Z) + (X ^ I) + (I ^ X) + 0.2 * (X ^ X) + with self.assertWarns(DeprecationWarning): + op = 0.1 * (Z ^ Z) + (X ^ I) + (I ^ X) + 0.2 * (X ^ X) reps = 20 qdrift = PauliEvolutionGate(op, time=0.5 / reps, synthesis=QDrift(reps=reps)).definition exact = scipy.linalg.expm(-0.5j * op.to_matrix()).dot(np.eye(4)[0, :]) @@ -163,7 +170,8 @@ def energy(evo): def test_passing_grouped_paulis(self): """Test passing a list of already grouped Paulis.""" - grouped_ops = [(X ^ Y) + (Y ^ X), (Z ^ I) + (Z ^ Z) + (I ^ Z), (X ^ X)] + with self.assertWarns(DeprecationWarning): + grouped_ops = [(X ^ Y) + (Y ^ X), (Z ^ I) + (Z ^ Z) + (I ^ Z), (X ^ X)] evo_gate = PauliEvolutionGate(grouped_ops, time=0.12, synthesis=LieTrotter()) decomposed = evo_gate.definition.decompose() self.assertEqual(decomposed.count_ops()["rz"], 4) @@ -172,7 +180,8 @@ def test_passing_grouped_paulis(self): def test_list_from_grouped_paulis(self): """Test getting a string representation from grouped Paulis.""" - grouped_ops = [(X ^ Y) + (Y ^ X), (Z ^ I) + (Z ^ Z) + (I ^ Z), (X ^ X)] + with self.assertWarns(DeprecationWarning): + grouped_ops = [(X ^ Y) + (Y ^ X), (Z ^ I) + (Z ^ Z) + (I ^ Z), (X ^ X)] evo_gate = PauliEvolutionGate(grouped_ops, time=0.12, synthesis=LieTrotter()) pauli_strings = [] @@ -192,7 +201,8 @@ def test_list_from_grouped_paulis(self): def test_dag_conversion(self): """Test constructing a circuit with evolutions yields a DAG with evolution blocks.""" time = Parameter("t") - evo = PauliEvolutionGate((Z ^ 2) + (X ^ 2), time=time) + with self.assertWarns(DeprecationWarning): + evo = PauliEvolutionGate((Z ^ 2) + (X ^ 2), time=time) circuit = QuantumCircuit(2) circuit.h(circuit.qubits) @@ -210,7 +220,8 @@ def test_dag_conversion(self): def test_cnot_chain_options(self, option): """Test selecting different kinds of CNOT chains.""" - op = Z ^ Z ^ Z + with self.assertWarns(DeprecationWarning): + op = Z ^ Z ^ Z synthesis = LieTrotter(reps=1, cx_structure=option) evo = PauliEvolutionGate(op, synthesis=synthesis) @@ -254,14 +265,16 @@ def test_different_input_types(self, op): def test_pauliop_coefficients_respected(self): """Test that global ``PauliOp`` coefficients are being taken care of.""" - evo = PauliEvolutionGate(5 * (Z ^ I), time=1, synthesis=LieTrotter()) + with self.assertWarns(DeprecationWarning): + evo = PauliEvolutionGate(5 * (Z ^ I), time=1, synthesis=LieTrotter()) circuit = evo.definition.decompose() rz_angle = circuit.data[0].operation.params[0] self.assertEqual(rz_angle, 10) def test_paulisumop_coefficients_respected(self): """Test that global ``PauliSumOp`` coefficients are being taken care of.""" - evo = PauliEvolutionGate(5 * (2 * X + 3 * Y - Z), time=1, synthesis=LieTrotter()) + with self.assertWarns(DeprecationWarning): + evo = PauliEvolutionGate(5 * (2 * X + 3 * Y - Z), time=1, synthesis=LieTrotter()) circuit = evo.definition.decompose() rz_angles = [ circuit.data[0].operation.params[0], # X @@ -275,7 +288,8 @@ def test_lie_trotter_two_qubit_correct_order(self): Regression test of Qiskit/qiskit-terra#7544. """ - operator = I ^ Z ^ Z + with self.assertWarns(DeprecationWarning): + operator = I ^ Z ^ Z time = 0.5 exact = scipy.linalg.expm(-1j * time * operator.to_matrix()) lie_trotter = PauliEvolutionGate(operator, time, synthesis=LieTrotter()) @@ -295,7 +309,8 @@ def test_paramtrized_op_raises(self): @data(LieTrotter, MatrixExponential) def test_inverse(self, synth_cls): """Test calculating the inverse is correct.""" - evo = PauliEvolutionGate(X + Y, time=0.12, synthesis=synth_cls()) + with self.assertWarns(DeprecationWarning): + evo = PauliEvolutionGate(X + Y, time=0.12, synthesis=synth_cls()) circuit = QuantumCircuit(1) circuit.append(evo, circuit.qubits) @@ -305,7 +320,8 @@ def test_inverse(self, synth_cls): def test_labels_and_name(self): """Test the name and labels are correct.""" - operators = [X, (X + Y), ((I ^ Z) + (Z ^ I) - 0.2 * (X ^ X))] + with self.assertWarns(DeprecationWarning): + operators = [X, (X + Y), ((I ^ Z) + (Z ^ I) - 0.2 * (X ^ X))] # note: the labels do not show coefficients! expected_labels = ["X", "(X + Y)", "(IZ + ZI + XX)"] diff --git a/test/python/circuit/library/test_evolved_op_ansatz.py b/test/python/circuit/library/test_evolved_op_ansatz.py index 3fc0161eac30..c809fe438820 100644 --- a/test/python/circuit/library/test_evolved_op_ansatz.py +++ b/test/python/circuit/library/test_evolved_op_ansatz.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -32,18 +32,19 @@ class TestEvolvedOperatorAnsatz(QiskitTestCase): def test_evolved_op_ansatz(self, use_opflow): """Test the default evolution.""" num_qubits = 3 - if use_opflow: - ops = [Z ^ num_qubits, Y ^ num_qubits, X ^ num_qubits] + with self.assertWarns(DeprecationWarning): + ops = [Z ^ num_qubits, Y ^ num_qubits, X ^ num_qubits] + evo = EvolvedOperatorAnsatz(ops, 2) + parameters = evo.parameters + else: ops = [Pauli("Z" * num_qubits), Pauli("Y" * num_qubits), Pauli("X" * num_qubits)] - - strings = ["z" * num_qubits, "y" * num_qubits, "x" * num_qubits] * 2 - - evo = EvolvedOperatorAnsatz(ops, 2) + evo = EvolvedOperatorAnsatz(ops, 2) + parameters = evo.parameters reference = QuantumCircuit(num_qubits) - parameters = evo.parameters + strings = ["z" * num_qubits, "y" * num_qubits, "x" * num_qubits] * 2 for string, time in zip(strings, parameters): reference.compose(evolve(string, time), inplace=True) @@ -53,17 +54,20 @@ def test_evolved_op_ansatz(self, use_opflow): def test_custom_evolution(self, use_opflow): """Test using another evolution than the default (e.g. matrix evolution).""" if use_opflow: - op = X ^ I ^ Z - matrix = op.to_matrix() - evolution = MatrixEvolution() + with self.assertWarns(DeprecationWarning): + op = X ^ I ^ Z + matrix = op.to_matrix() + evolution = MatrixEvolution() + evo = EvolvedOperatorAnsatz(op, evolution=evolution) + parameters = evo.parameters + else: op = SparsePauliOp(["ZIX"]) matrix = np.array(op) evolution = MatrixExponential() + evo = EvolvedOperatorAnsatz(op, evolution=evolution) + parameters = evo.parameters - evo = EvolvedOperatorAnsatz(op, evolution=evolution) - - parameters = evo.parameters reference = QuantumCircuit(3) reference.hamiltonian(matrix, parameters[0], [0, 1, 2]) @@ -77,10 +81,11 @@ def test_changing_operators(self): """Test rebuilding after the operators changed.""" ops = [X, Y, Z] - evo = EvolvedOperatorAnsatz(ops) - evo.operators = [X, Y] + with self.assertWarns(DeprecationWarning): + evo = EvolvedOperatorAnsatz(ops) + evo.operators = [X, Y] + parameters = evo.parameters - parameters = evo.parameters reference = QuantumCircuit(1) reference.rx(2 * parameters[0], 0) reference.ry(2 * parameters[1], 0) @@ -94,13 +99,13 @@ def test_invalid_reps(self): def test_insert_barriers(self): """Test using insert_barriers.""" - evo = EvolvedOperatorAnsatz(Z, reps=4, insert_barriers=True) - ref = QuantumCircuit(1) - for parameter in evo.parameters: - ref.rz(2.0 * parameter, 0) - ref.barrier() - - self.assertEqual(evo.decompose(), ref) + with self.assertWarns(DeprecationWarning): + evo = EvolvedOperatorAnsatz(Z, reps=4, insert_barriers=True) + ref = QuantumCircuit(1) + for parameter in evo.parameters: + ref.rz(2.0 * parameter, 0) + ref.barrier() + self.assertEqual(evo.decompose(), ref) def test_empty_build_fails(self): """Test setting no operators to evolve raises the appropriate error.""" diff --git a/test/python/circuit/library/test_qaoa_ansatz.py b/test/python/circuit/library/test_qaoa_ansatz.py index 9a05b2c420c2..dfbf25da69d4 100644 --- a/test/python/circuit/library/test_qaoa_ansatz.py +++ b/test/python/circuit/library/test_qaoa_ansatz.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -18,7 +18,7 @@ from qiskit.circuit import QuantumCircuit, Parameter from qiskit.circuit.library import HGate, RXGate, YGate, RYGate, RZGate from qiskit.circuit.library.n_local.qaoa_ansatz import QAOAAnsatz -from qiskit.opflow import I, Y, Z +from qiskit.opflow import I from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.test import QiskitTestCase @@ -29,8 +29,8 @@ class TestQAOAAnsatz(QiskitTestCase): def test_default_qaoa(self): """Test construction of the default circuit.""" + # To be changed once QAOAAnsatz drops support for opflow circuit = QAOAAnsatz(I, 1) - parameters = circuit.parameters circuit = circuit.decompose() @@ -42,8 +42,7 @@ def test_custom_initial_state(self): """Test circuit with a custom initial state.""" initial_state = QuantumCircuit(1) initial_state.y(0) - circuit = QAOAAnsatz(initial_state=initial_state, cost_operator=I, reps=1) - + circuit = QAOAAnsatz(initial_state=initial_state, cost_operator=Pauli("I"), reps=1) parameters = circuit.parameters circuit = circuit.decompose() self.assertEqual(1, len(parameters)) @@ -53,11 +52,11 @@ def test_custom_initial_state(self): def test_invalid_reps(self): """Test negative reps.""" with self.assertRaises(ValueError): - _ = QAOAAnsatz(I, reps=-1) + _ = QAOAAnsatz(Pauli("I"), reps=-1) def test_zero_reps(self): """Test zero reps.""" - circuit = QAOAAnsatz(I ^ 4, reps=0) + circuit = QAOAAnsatz(Pauli("IIII"), reps=0) reference = QuantumCircuit(4) reference.h(range(4)) @@ -67,7 +66,7 @@ def test_custom_circuit_mixer(self): """Test circuit with a custom mixer as a circuit""" mixer = QuantumCircuit(1) mixer.ry(1, 0) - circuit = QAOAAnsatz(cost_operator=I, reps=1, mixer_operator=mixer) + circuit = QAOAAnsatz(cost_operator=Pauli("I"), reps=1, mixer_operator=mixer) parameters = circuit.parameters circuit = circuit.decompose() @@ -77,18 +76,19 @@ def test_custom_circuit_mixer(self): def test_custom_operator_mixer(self): """Test circuit with a custom mixer as an operator.""" - mixer = Y - circuit = QAOAAnsatz(cost_operator=I, reps=1, mixer_operator=mixer) + mixer = Pauli("Y") + circuit = QAOAAnsatz(cost_operator=Pauli("I"), reps=1, mixer_operator=mixer) parameters = circuit.parameters circuit = circuit.decompose() self.assertEqual(1, len(parameters)) self.assertIsInstance(circuit.data[0].operation, HGate) - self.assertIsInstance(circuit.data[1].operation, RYGate) + self.assertIsInstance(circuit.decompose().data[1].operation, RYGate) def test_parameter_bounds(self): """Test the parameter bounds.""" - circuit = QAOAAnsatz(Z, reps=2) + + circuit = QAOAAnsatz(Pauli("Z"), reps=2) bounds = circuit.parameter_bounds for lower, upper in bounds[:2]: @@ -103,33 +103,34 @@ def test_all_custom_parameters(self): """Test circuit with all custom parameters.""" initial_state = QuantumCircuit(1) initial_state.y(0) - mixer = Z + mixer = Pauli("Z") circuit = QAOAAnsatz( - cost_operator=I, reps=2, initial_state=initial_state, mixer_operator=mixer + cost_operator=Pauli("I"), reps=2, initial_state=initial_state, mixer_operator=mixer ) - parameters = circuit.parameters circuit = circuit.decompose() + self.assertEqual(2, len(parameters)) self.assertIsInstance(circuit.data[0].operation, YGate) - self.assertIsInstance(circuit.data[1].operation, RZGate) - self.assertIsInstance(circuit.data[2].operation, RZGate) + self.assertIsInstance(circuit.decompose().data[1].operation, RZGate) + self.assertIsInstance(circuit.decompose().data[2].operation, RZGate) def test_configuration(self): """Test configuration checks.""" mixer = QuantumCircuit(2) - circuit = QAOAAnsatz(cost_operator=I, reps=1, mixer_operator=mixer) + circuit = QAOAAnsatz(cost_operator=Pauli("I"), reps=1, mixer_operator=mixer) self.assertRaises(ValueError, lambda: circuit.parameters) def test_rebuild(self): """Test how a circuit can be rebuilt.""" - circuit = QAOAAnsatz(cost_operator=Z ^ I) # circuit with 2 qubits + + circuit = QAOAAnsatz(cost_operator=Pauli("IZ")) # circuit with 2 qubits # force circuit to be built _ = circuit.parameters - circuit.cost_operator = Z # now it only has 1 qubit + circuit.cost_operator = Pauli("Z") # now it only has 1 qubit circuit.reps = 5 # and now 5 repetitions # rebuild the circuit self.assertEqual(1, circuit.num_qubits) @@ -143,7 +144,7 @@ def test_circuit_mixer(self): mixer.ry(x2, 1) reps = 4 - circuit = QAOAAnsatz(cost_operator=Z ^ Z, mixer_operator=mixer, reps=reps) + circuit = QAOAAnsatz(cost_operator=Pauli("ZZ"), mixer_operator=mixer, reps=reps) self.assertEqual(circuit.num_parameters, 3 * reps) def test_empty_op(self): diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 7788dab97abb..3bedc5cb9da0 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2020. +# (C) Copyright IBM 2020, 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 @@ -42,9 +42,9 @@ from qiskit.circuit.parametervector import ParameterVector from qiskit.synthesis import LieTrotter, SuzukiTrotter from qiskit.extensions import UnitaryGate -from qiskit.opflow import I, X, Y, Z from qiskit.test import QiskitTestCase from qiskit.qpy import dump, load +from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.quantum_info.random import random_unitary from qiskit.circuit.controlledgate import ControlledGate @@ -303,7 +303,12 @@ def test_parameter_expression(self): def test_string_parameter(self): """Test a PauliGate instruction that has string parameters.""" - circ = (X ^ Y ^ Z).to_circuit_op().to_circuit() + + circ = QuantumCircuit(3) + circ.z(0) + circ.y(1) + circ.x(2) + qpy_file = io.BytesIO() dump(circ, qpy_file) qpy_file.seek(0) @@ -677,8 +682,9 @@ def test_single_bit_teleportation(self): def test_qaoa(self): """Test loading a QAOA circuit works.""" - cost_operator = Z ^ I ^ I ^ Z + cost_operator = Pauli("ZIIZ") qaoa = QAOAAnsatz(cost_operator, reps=2) + qpy_file = io.BytesIO() dump(qaoa, qpy_file) qpy_file.seek(0) @@ -692,7 +698,10 @@ def test_qaoa(self): def test_evolutiongate(self): """Test loading a circuit with evolution gate works.""" synthesis = LieTrotter(reps=2) - evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=2, synthesis=synthesis) + evo = PauliEvolutionGate( + SparsePauliOp.from_list([("ZI", 1), ("IZ", 1)]), time=2, synthesis=synthesis + ) + qc = QuantumCircuit(2) qc.append(evo, range(2)) qpy_file = io.BytesIO() @@ -713,7 +722,10 @@ def test_evolutiongate_param_time(self): """Test loading a circuit with an evolution gate that has a parameter for time.""" synthesis = LieTrotter(reps=2) time = Parameter("t") - evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=time, synthesis=synthesis) + evo = PauliEvolutionGate( + SparsePauliOp.from_list([("ZI", 1), ("IZ", 1)]), time=time, synthesis=synthesis + ) + qc = QuantumCircuit(2) qc.append(evo, range(2)) qpy_file = io.BytesIO() @@ -734,7 +746,10 @@ def test_evolutiongate_param_expr_time(self): """Test loading a circuit with an evolution gate that has a parameter for time.""" synthesis = LieTrotter(reps=2) time = Parameter("t") - evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=time * time, synthesis=synthesis) + evo = PauliEvolutionGate( + SparsePauliOp.from_list([("ZI", 1), ("IZ", 1)]), time=time * time, synthesis=synthesis + ) + qc = QuantumCircuit(2) qc.append(evo, range(2)) qpy_file = io.BytesIO() @@ -755,7 +770,10 @@ def test_evolutiongate_param_vec_time(self): """Test loading a an evolution gate that has a param vector element for time.""" synthesis = LieTrotter(reps=2) time = ParameterVector("TimeVec", 1) - evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=time[0], synthesis=synthesis) + evo = PauliEvolutionGate( + SparsePauliOp.from_list([("ZI", 1), ("IZ", 1)]), time=time[0], synthesis=synthesis + ) + qc = QuantumCircuit(2) qc.append(evo, range(2)) qpy_file = io.BytesIO() @@ -774,7 +792,10 @@ def test_evolutiongate_param_vec_time(self): def test_op_list_evolutiongate(self): """Test loading a circuit with evolution gate works.""" - evo = PauliEvolutionGate([(Z ^ I) + (I ^ Z)] * 5, time=0.2, synthesis=None) + + evo = PauliEvolutionGate( + [SparsePauliOp.from_list([("ZI", 1), ("IZ", 1)])] * 5, time=0.2, synthesis=None + ) qc = QuantumCircuit(2) qc.append(evo, range(2)) qpy_file = io.BytesIO() @@ -794,7 +815,10 @@ def test_op_list_evolutiongate(self): def test_op_evolution_gate_suzuki_trotter(self): """Test qpy path with a suzuki trotter synthesis method on an evolution gate.""" synthesis = SuzukiTrotter() - evo = PauliEvolutionGate((Z ^ I) + (I ^ Z), time=0.2, synthesis=synthesis) + evo = PauliEvolutionGate( + SparsePauliOp.from_list([("ZI", 1), ("IZ", 1)]), time=0.2, synthesis=synthesis + ) + qc = QuantumCircuit(2) qc.append(evo, range(2)) qpy_file = io.BytesIO() diff --git a/test/python/opflow/opflow_test_case.py b/test/python/opflow/opflow_test_case.py index ef8d8c6824c7..142fb1db76b5 100644 --- a/test/python/opflow/opflow_test_case.py +++ b/test/python/opflow/opflow_test_case.py @@ -12,10 +12,19 @@ """Opflow Test Case""" +import warnings from qiskit.test import QiskitTestCase class QiskitOpflowTestCase(QiskitTestCase): """Opflow test Case""" - pass + def setUp(self): + super().setUp() + # ignore opflow msgs + warnings.filterwarnings("ignore", category=DeprecationWarning, message=r".*opflow.*") + + def tearDown(self): + super().tearDown() + # restore opflow msgs + warnings.filterwarnings("error", category=DeprecationWarning, message=r".*opflow.*") diff --git a/test/python/opflow/test_aer_pauli_expectation.py b/test/python/opflow/test_aer_pauli_expectation.py index c0676544a91e..808f7bab8716 100644 --- a/test/python/opflow/test_aer_pauli_expectation.py +++ b/test/python/opflow/test_aer_pauli_expectation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 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 @@ -15,7 +15,6 @@ import itertools import unittest from test.python.opflow import QiskitOpflowTestCase - import numpy as np from qiskit.circuit.library import RealAmplitudes @@ -40,36 +39,34 @@ Zero, MatrixOp, ) -from qiskit.utils import QuantumInstance +from qiskit.utils import QuantumInstance, optionals class TestAerPauliExpectation(QiskitOpflowTestCase): """Pauli Change of Basis Expectation tests.""" + @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") def setUp(self) -> None: super().setUp() - try: - from qiskit_aer import Aer + from qiskit_aer import AerSimulator - self.seed = 97 - self.backend = Aer.get_backend("aer_simulator") + self.seed = 97 + self.backend = AerSimulator() + with self.assertWarns(DeprecationWarning): q_instance = QuantumInstance( self.backend, seed_simulator=self.seed, seed_transpiler=self.seed ) self.sampler = CircuitSampler(q_instance, attach_results=True) self.expect = AerPauliExpectation() - except Exception as ex: # pylint: disable=broad-except - self.skipTest(f"Aer doesn't appear to be installed. Error: '{str(ex)}'") - return def test_pauli_expect_pair(self): """pauli expect pair test""" op = Z ^ Z # wvf = (Pl^Pl) + (Ze^Ze) wvf = CX @ (H ^ I) @ Zero - converted_meas = self.expect.convert(~StateFn(op) @ wvf) - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), 0, delta=0.1) def test_pauli_expect_single(self): @@ -79,7 +76,8 @@ def test_pauli_expect_single(self): for pauli, state in itertools.product(paulis, states): converted_meas = self.expect.convert(~StateFn(pauli) @ state) matmulmean = state.adjoint().to_matrix() @ pauli.to_matrix() @ state.to_matrix() - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), matmulmean, delta=0.1) def test_pauli_expect_op_vector(self): @@ -87,33 +85,36 @@ def test_pauli_expect_op_vector(self): paulis_op = ListOp([X, Y, Z, I]) converted_meas = self.expect.convert(~StateFn(paulis_op)) - plus_mean = converted_meas @ Plus - sampled_plus = self.sampler.convert(plus_mean) - np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0, 0, 1], decimal=1) + with self.assertWarns(DeprecationWarning): + plus_mean = converted_meas @ Plus + sampled_plus = self.sampler.convert(plus_mean) + np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0, 0, 1], decimal=1) + + minus_mean = converted_meas @ Minus + sampled_minus = self.sampler.convert(minus_mean) + np.testing.assert_array_almost_equal(sampled_minus.eval(), [-1, 0, 0, 1], decimal=1) - minus_mean = converted_meas @ Minus - sampled_minus = self.sampler.convert(minus_mean) - np.testing.assert_array_almost_equal(sampled_minus.eval(), [-1, 0, 0, 1], decimal=1) + zero_mean = converted_meas @ Zero + sampled_zero = self.sampler.convert(zero_mean) + np.testing.assert_array_almost_equal(sampled_zero.eval(), [0, 0, 1, 1], decimal=1) - zero_mean = converted_meas @ Zero - sampled_zero = self.sampler.convert(zero_mean) - np.testing.assert_array_almost_equal(sampled_zero.eval(), [0, 0, 1, 1], decimal=1) + sum_zero = (Plus + Minus) * (0.5**0.5) + sum_zero_mean = converted_meas @ sum_zero + sampled_zero_mean = self.sampler.convert(sum_zero_mean) - sum_zero = (Plus + Minus) * (0.5**0.5) - sum_zero_mean = converted_meas @ sum_zero - sampled_zero_mean = self.sampler.convert(sum_zero_mean) - # !!NOTE!!: Depolarizing channel (Sampling) means interference - # does not happen between circuits in sum, so expectation does - # not equal expectation for Zero!! - np.testing.assert_array_almost_equal(sampled_zero_mean.eval(), [0, 0, 0, 1]) + # !!NOTE!!: Depolarizing channel (Sampling) means interference + # does not happen between circuits in sum, so expectation does + # not equal expectation for Zero!! + np.testing.assert_array_almost_equal(sampled_zero_mean.eval(), [0, 0, 0, 1]) def test_pauli_expect_state_vector(self): """pauli expect state vector test""" states_op = ListOp([One, Zero, Plus, Minus]) - paulis_op = X converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) - sampled = self.sampler.convert(converted_meas) + + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) # Small test to see if execution results are accessible for composed_op in sampled: @@ -124,24 +125,21 @@ def test_pauli_expect_state_vector(self): def test_pauli_expect_non_hermitian_matrixop(self): """pauli expect state vector with non hermitian operator test""" states_op = ListOp([One, Zero, Plus, Minus]) - op_mat = np.array([[0, 1], [2, 3]]) op = MatrixOp(op_mat) - converted_meas = self.expect.convert(StateFn(op, is_measurement=True) @ states_op) - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) np.testing.assert_array_almost_equal(sampled.eval(), [3, 0, 3, 0], decimal=1) def test_pauli_expect_non_hermitian_pauliop(self): """pauli expect state vector with non hermitian operator test""" states_op = ListOp([One, Zero, Plus, Minus]) - op = 1j * X - converted_meas = self.expect.convert(StateFn(op, is_measurement=True) @ states_op) - sampled = self.sampler.convert(converted_meas) - + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) np.testing.assert_array_almost_equal(sampled.eval(), [0, 0, 1j, -1j], decimal=1) def test_pauli_expect_op_vector_state_vector(self): @@ -156,22 +154,27 @@ def test_pauli_expect_op_vector_state_vector(self): [+1, 1, 1, 1], ] converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) np.testing.assert_array_almost_equal(sampled.eval(), valids, decimal=1) def test_multi_representation_ops(self): """Test observables with mixed representations""" + mixed_ops = ListOp([X.to_matrix_op(), H, H + I, X]) converted_meas = self.expect.convert(~StateFn(mixed_ops)) plus_mean = converted_meas @ Plus - sampled_plus = self.sampler.convert(plus_mean) + with self.assertWarns(DeprecationWarning): + sampled_plus = self.sampler.convert(plus_mean) + np.testing.assert_array_almost_equal( sampled_plus.eval(), [1, 0.5**0.5, (1 + 0.5**0.5), 1], decimal=1 ) def test_parameterized_qobj(self): """grouped pauli expectation test""" + two_qubit_h2 = ( (-1.052373245772859 * I ^ I) + (0.39793742484318045 * I ^ Z) @@ -179,11 +182,10 @@ def test_parameterized_qobj(self): + (-0.01128010425623538 * Z ^ Z) + (0.18093119978423156 * X ^ X) ) - - aer_sampler = CircuitSampler( - self.sampler.quantum_instance, param_qobj=True, attach_results=True - ) - + with self.assertWarns(DeprecationWarning): + aer_sampler = CircuitSampler( + self.sampler.quantum_instance, param_qobj=True, attach_results=True + ) ansatz = RealAmplitudes() ansatz.num_qubits = 2 @@ -201,12 +203,13 @@ def generate_parameters(num): return param_bindings def validate_sampler(ideal, sut, param_bindings): - expect_sampled = ideal.convert(expect_op, params=param_bindings).eval() - actual_sampled = sut.convert(expect_op, params=param_bindings).eval() - self.assertTrue( - np.allclose(actual_sampled, expect_sampled), - f"{actual_sampled} != {expect_sampled}", - ) + with self.assertWarns(DeprecationWarning): + expect_sampled = ideal.convert(expect_op, params=param_bindings).eval() + actual_sampled = sut.convert(expect_op, params=param_bindings).eval() + self.assertTrue( + np.allclose(actual_sampled, expect_sampled), + f"{actual_sampled} != {expect_sampled}", + ) def get_circuit_templates(sampler): return sampler._transpiled_circ_templates @@ -236,9 +239,10 @@ def validate_aer_templates_reused(prev_templates, cur_templates): def test_pauli_expectation_param_qobj(self): """Test PauliExpectation with param_qobj""" - q_instance = QuantumInstance( - self.backend, seed_simulator=self.seed, seed_transpiler=self.seed, shots=10000 - ) + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + self.backend, seed_simulator=self.seed, seed_transpiler=self.seed, shots=10000 + ) qubit_op = (0.1 * I ^ I) + (0.2 * I ^ Z) + (0.3 * Z ^ I) + (0.4 * Z ^ Z) + (0.5 * X ^ X) ansatz = RealAmplitudes(qubit_op.num_qubits) ansatz_circuit_op = CircuitStateFn(ansatz) @@ -250,16 +254,17 @@ def test_pauli_expectation_param_qobj(self): params1[param] = [0] params2[param] = [0, 0] - sampler1 = CircuitSampler(backend=q_instance, param_qobj=False) - samples1 = sampler1.convert(expect_op, params=params1) - val1 = np.real(samples1.eval())[0] - samples2 = sampler1.convert(expect_op, params=params2) - val2 = np.real(samples2.eval()) - sampler2 = CircuitSampler(backend=q_instance, param_qobj=True) - samples3 = sampler2.convert(expect_op, params=params1) - val3 = np.real(samples3.eval()) - samples4 = sampler2.convert(expect_op, params=params2) - val4 = np.real(samples4.eval()) + with self.assertWarns(DeprecationWarning): + sampler1 = CircuitSampler(backend=q_instance, param_qobj=False) + samples1 = sampler1.convert(expect_op, params=params1) + val1 = np.real(samples1.eval())[0] + samples2 = sampler1.convert(expect_op, params=params2) + val2 = np.real(samples2.eval()) + sampler2 = CircuitSampler(backend=q_instance, param_qobj=True) + samples3 = sampler2.convert(expect_op, params=params1) + val3 = np.real(samples3.eval()) + samples4 = sampler2.convert(expect_op, params=params2) + val4 = np.real(samples4.eval()) np.testing.assert_array_almost_equal([val1] * 2, val2, decimal=2) np.testing.assert_array_almost_equal(val1, val3, decimal=2) @@ -277,12 +282,14 @@ def test_expectation_with_coeff(self): """Test AerPauliExpectation with coefficients.""" with self.subTest("integer coefficients"): exp = 3 * ~StateFn(X) @ (2 * Minus) - target = self.sampler.convert(self.expect.convert(exp)).eval() + with self.assertWarns(DeprecationWarning): + target = self.sampler.convert(self.expect.convert(exp)).eval() self.assertAlmostEqual(target, -12) with self.subTest("complex coefficients"): exp = 3j * ~StateFn(X) @ (2j * Minus) - target = self.sampler.convert(self.expect.convert(exp)).eval() + with self.assertWarns(DeprecationWarning): + target = self.sampler.convert(self.expect.convert(exp)).eval() self.assertAlmostEqual(target, -12j) diff --git a/test/python/opflow/test_expectation_factory.py b/test/python/opflow/test_expectation_factory.py index 47c9302e8c54..f03a8517733d 100644 --- a/test/python/opflow/test_expectation_factory.py +++ b/test/python/opflow/test_expectation_factory.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 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 @@ -16,26 +16,24 @@ from test.python.opflow import QiskitOpflowTestCase from qiskit.opflow import PauliExpectation, AerPauliExpectation, ExpectationFactory, Z, I, X -from qiskit.utils import has_aer - - -if has_aer(): - from qiskit import Aer +from qiskit.utils import optionals class TestExpectationFactory(QiskitOpflowTestCase): """Tests for the expectation factory.""" - @unittest.skipUnless(has_aer(), "qiskit-aer doesn't appear to be installed.") + @unittest.skipUnless(optionals.HAS_AER, "qiskit-aer is required to run this test") def test_aer_simulator_pauli_sum(self): """Test expectation selection with Aer's qasm_simulator.""" - backend = Aer.get_backend("aer_simulator") - op = 0.2 * (X ^ X) + 0.1 * (Z ^ I) + from qiskit_aer import AerSimulator - with self.subTest("Defaults"): - expectation = ExpectationFactory.build(op, backend, include_custom=False) - self.assertIsInstance(expectation, PauliExpectation) - - with self.subTest("Include custom"): - expectation = ExpectationFactory.build(op, backend, include_custom=True) - self.assertIsInstance(expectation, AerPauliExpectation) + backend = AerSimulator() + op = 0.2 * (X ^ X) + 0.1 * (Z ^ I) + with self.assertWarns(DeprecationWarning): + with self.subTest("Defaults"): + expectation = ExpectationFactory.build(op, backend, include_custom=False) + self.assertIsInstance(expectation, PauliExpectation) + + with self.subTest("Include custom"): + expectation = ExpectationFactory.build(op, backend, include_custom=True) + self.assertIsInstance(expectation, AerPauliExpectation) diff --git a/test/python/opflow/test_gradients.py b/test/python/opflow/test_gradients.py index 490d2d839ee7..07ada8e56ef9 100644 --- a/test/python/opflow/test_gradients.py +++ b/test/python/opflow/test_gradients.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2019, 2021. +# (C) Copyright IBM 2019, 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 @@ -840,6 +840,7 @@ def grad_combo_fn(x): qc = RealAmplitudes(2, reps=1) grad_op = ListOp([StateFn(qc.decompose())], combo_fn=combo_fn, grad_combo_fn=grad_combo_fn) grad = Gradient(grad_method=method).convert(grad_op) + value_dict = dict(zip(qc.ordered_parameters, np.random.rand(len(qc.ordered_parameters)))) correct_values = [ [(-0.16666259133549044 + 0j)], @@ -998,13 +999,17 @@ def test_circuit_sampler(self, method): ] backend = BasicAer.get_backend("qasm_simulator") - q_instance = QuantumInstance(backend=backend, shots=shots) + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance(backend=backend, shots=shots) - for i, value_dict in enumerate(values_dict): - sampler = CircuitSampler(backend=q_instance).convert( - state_grad, params={k: [v] for k, v in value_dict.items()} - ) - np.testing.assert_array_almost_equal(sampler.eval()[0], correct_values[i], decimal=1) + with self.assertWarns(DeprecationWarning): + for i, value_dict in enumerate(values_dict): + sampler = CircuitSampler(backend=q_instance).convert( + state_grad, params={k: [v] for k, v in value_dict.items()} + ) + np.testing.assert_array_almost_equal( + sampler.eval()[0], correct_values[i], decimal=1 + ) @data("lin_comb", "param_shift", "fin_diff") def test_circuit_sampler2(self, method): @@ -1048,13 +1053,15 @@ def test_circuit_sampler2(self, method): ] backend = BasicAer.get_backend("qasm_simulator") - q_instance = QuantumInstance(backend=backend, shots=shots) + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance(backend=backend, shots=shots) - for i, value_dict in enumerate(values_dict): - sampler = CircuitSampler(backend=q_instance).convert(prob_grad, params=value_dict) - result = sampler.eval()[0] - self.assertTrue(np.allclose(result[0].toarray(), correct_values[i][0], atol=0.1)) - self.assertTrue(np.allclose(result[1].toarray(), correct_values[i][1], atol=0.1)) + with self.assertWarns(DeprecationWarning): + for i, value_dict in enumerate(values_dict): + sampler = CircuitSampler(backend=q_instance).convert(prob_grad, params=value_dict) + result = sampler.eval()[0] + self.assertTrue(np.allclose(result[0].toarray(), correct_values[i][0], atol=0.1)) + self.assertTrue(np.allclose(result[1].toarray(), correct_values[i][1], atol=0.1)) @idata(["statevector_simulator", "qasm_simulator"]) def test_gradient_wrapper(self, backend_type): @@ -1079,31 +1086,38 @@ def test_gradient_wrapper(self, backend_type): shots = 8000 backend = BasicAer.get_backend(backend_type) - q_instance = QuantumInstance( - backend=backend, shots=shots, seed_simulator=2, seed_transpiler=2 - ) + + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + backend=backend, shots=shots, seed_simulator=2, seed_transpiler=2 + ) + if method == "fin_diff": np.random.seed(8) prob_grad = Gradient(grad_method=method, epsilon=shots ** (-1 / 6.0)).gradient_wrapper( operator=op, bind_params=params, backend=q_instance ) else: - prob_grad = Gradient(grad_method=method).gradient_wrapper( - operator=op, bind_params=params, backend=q_instance - ) + + with self.assertWarns(DeprecationWarning): + prob_grad = Gradient(grad_method=method).gradient_wrapper( + operator=op, bind_params=params, backend=q_instance + ) + values = [[np.pi / 4, 0], [np.pi / 4, np.pi / 4], [np.pi / 2, np.pi]] correct_values = [ [[0, 0], [1 / (2 * np.sqrt(2)), -1 / (2 * np.sqrt(2))]], [[1 / 4, -1 / 4], [1 / 4, -1 / 4]], [[0, 0], [-1 / 2, 1 / 2]], ] - for i, value in enumerate(values): - result = prob_grad(value) - if backend_type == "qasm_simulator": # sparse result - result = [result[0].toarray(), result[1].toarray()] + with self.assertWarns(DeprecationWarning): + for i, value in enumerate(values): + result = prob_grad(value) + if backend_type == "qasm_simulator": # sparse result + result = [result[0].toarray(), result[1].toarray()] - self.assertTrue(np.allclose(result[0], correct_values[i][0], atol=0.1)) - self.assertTrue(np.allclose(result[1], correct_values[i][1], atol=0.1)) + self.assertTrue(np.allclose(result[0], correct_values[i][0], atol=0.1)) + self.assertTrue(np.allclose(result[1], correct_values[i][1], atol=0.1)) @data(("statevector_simulator", 1e-7), ("qasm_simulator", 2e-1)) @unpack @@ -1139,13 +1153,14 @@ def test_gradient_wrapper2(self, backend_type, atol): correct_values = [[-4.0, 0], [-2.0, -4.82842712], [-0.68404029, -7.01396121]] for i, value in enumerate(values): backend = BasicAer.get_backend(backend_type) - q_instance = QuantumInstance( - backend=backend, shots=shots, seed_simulator=2, seed_transpiler=2 - ) - grad = NaturalGradient(grad_method=method).gradient_wrapper( - operator=op, bind_params=params, backend=q_instance - ) - result = grad(value) + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + backend=backend, shots=shots, seed_simulator=2, seed_transpiler=2 + ) + grad = NaturalGradient(grad_method=method).gradient_wrapper( + operator=op, bind_params=params, backend=q_instance + ) + result = grad(value) self.assertTrue(np.allclose(result, correct_values[i], atol=atol)) @slow_test @@ -1154,9 +1169,12 @@ def test_vqe(self): method = "lin_comb" backend = "qasm_simulator" - q_instance = QuantumInstance( - BasicAer.get_backend(backend), seed_simulator=79, seed_transpiler=2 - ) + + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + BasicAer.get_backend(backend), seed_simulator=79, seed_transpiler=2 + ) + # Define the Hamiltonian h2_hamiltonian = ( -1.05 * (I ^ I) + 0.39 * (I ^ Z) - 0.39 * (Z ^ I) - 0.01 * (Z ^ Z) + 0.18 * (X ^ X) @@ -1183,11 +1201,11 @@ def test_vqe(self): grad = Gradient(grad_method=method) # Gradient callable - vqe = VQE( - ansatz=wavefunction, optimizer=optimizer, gradient=grad, quantum_instance=q_instance - ) - - result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) + with self.assertWarns(DeprecationWarning): + vqe = VQE( + ansatz=wavefunction, optimizer=optimizer, gradient=grad, quantum_instance=q_instance + ) + result = vqe.compute_minimum_eigenvalue(operator=h2_hamiltonian) np.testing.assert_almost_equal(result.optimal_value, h2_energy, decimal=0) def test_qfi_overlap_works_with_bound_parameters(self): @@ -1500,15 +1518,15 @@ def test_aux_meas_op(self, aux_meas_op): for backend_type in ["qasm_simulator", "statevector_simulator"]: for j, value_dict in enumerate(value_dicts): - - q_instance = QuantumInstance( - backend=BasicAer.get_backend(backend_type), shots=shots - ) - result = ( - CircuitSampler(backend=q_instance) - .convert(prob_grad, params=value_dict) - .eval()[0] - ) + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + backend=BasicAer.get_backend(backend_type), shots=shots + ) + result = ( + CircuitSampler(backend=q_instance) + .convert(prob_grad, params=value_dict) + .eval()[0] + ) if backend_type == "qasm_simulator": # sparse result result = [result[0].toarray()[0], result[1].toarray()[0]] for i, item in enumerate(result): @@ -1544,7 +1562,8 @@ def test_unsupported_aux_meas_op(self): value_dict = {a: [np.pi / 4], b: [0]} backend = BasicAer.get_backend("qasm_simulator") - q_instance = QuantumInstance(backend=backend, shots=shots) + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance(backend=backend, shots=shots) CircuitSampler(backend=q_instance).convert(prob_grad, params=value_dict).eval() def test_nat_grad_error(self): @@ -1577,14 +1596,19 @@ def test_nat_grad_error(self): value = [0, np.pi / 2] backend = BasicAer.get_backend(backend_type) - q_instance = QuantumInstance( - backend=backend, shots=shots, seed_simulator=2, seed_transpiler=2 - ) - grad = NaturalGradient(grad_method=method).gradient_wrapper( - operator=op, bind_params=params, backend=q_instance - ) - with self.assertRaises(ValueError): - grad(value) + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + backend=backend, shots=shots, seed_simulator=2, seed_transpiler=2 + ) + + with self.assertWarns(DeprecationWarning): + grad = NaturalGradient(grad_method=method).gradient_wrapper( + operator=op, bind_params=params, backend=q_instance + ) + + with self.assertWarns(DeprecationWarning): + with self.assertRaises(ValueError): + grad(value) if __name__ == "__main__": diff --git a/test/python/opflow/test_matrix_expectation.py b/test/python/opflow/test_matrix_expectation.py index 5b873a5f6983..10448c3a64e1 100644 --- a/test/python/opflow/test_matrix_expectation.py +++ b/test/python/opflow/test_matrix_expectation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 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 @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"Test MatrixExpectation" +"""Test MatrixExpectation""" import unittest from test.python.opflow import QiskitOpflowTestCase @@ -45,60 +45,71 @@ def setUp(self) -> None: super().setUp() self.seed = 97 backend = BasicAer.get_backend("statevector_simulator") - q_instance = QuantumInstance(backend, seed_simulator=self.seed, seed_transpiler=self.seed) - self.sampler = CircuitSampler(q_instance, attach_results=True) + + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + backend, seed_simulator=self.seed, seed_transpiler=self.seed + ) + self.sampler = CircuitSampler(q_instance, attach_results=True) + self.expect = MatrixExpectation() def test_pauli_expect_pair(self): """pauli expect pair test""" + op = Z ^ Z # wf = (Pl^Pl) + (Ze^Ze) wf = CX @ (H ^ I) @ Zero - converted_meas = self.expect.convert(~StateFn(op) @ wf) self.assertAlmostEqual(converted_meas.eval(), 0, delta=0.1) - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), 0, delta=0.1) def test_pauli_expect_single(self): """pauli expect single test""" + paulis = [Z, X, Y, I] states = [Zero, One, Plus, Minus, S @ Plus, S @ Minus] for pauli, state in itertools.product(paulis, states): converted_meas = self.expect.convert(~StateFn(pauli) @ state) matmulmean = state.adjoint().to_matrix() @ pauli.to_matrix() @ state.to_matrix() self.assertAlmostEqual(converted_meas.eval(), matmulmean, delta=0.1) - - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), matmulmean, delta=0.1) def test_pauli_expect_op_vector(self): """pauli expect op vector test""" + paulis_op = ListOp([X, Y, Z, I]) converted_meas = self.expect.convert(~StateFn(paulis_op)) - plus_mean = converted_meas @ Plus - np.testing.assert_array_almost_equal(plus_mean.eval(), [1, 0, 0, 1], decimal=1) - sampled_plus = self.sampler.convert(plus_mean) - np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0, 0, 1], decimal=1) - - minus_mean = converted_meas @ Minus - np.testing.assert_array_almost_equal(minus_mean.eval(), [-1, 0, 0, 1], decimal=1) - sampled_minus = self.sampler.convert(minus_mean) - np.testing.assert_array_almost_equal(sampled_minus.eval(), [-1, 0, 0, 1], decimal=1) - - zero_mean = converted_meas @ Zero - np.testing.assert_array_almost_equal(zero_mean.eval(), [0, 0, 1, 1], decimal=1) - sampled_zero = self.sampler.convert(zero_mean) - np.testing.assert_array_almost_equal(sampled_zero.eval(), [0, 0, 1, 1], decimal=1) - - sum_zero = (Plus + Minus) * (0.5**0.5) - sum_zero_mean = converted_meas @ sum_zero - np.testing.assert_array_almost_equal(sum_zero_mean.eval(), [0, 0, 1, 1], decimal=1) - sampled_zero = self.sampler.convert(sum_zero) - np.testing.assert_array_almost_equal( - (converted_meas @ sampled_zero).eval(), [0, 0, 1, 1], decimal=1 - ) + with self.assertWarns(DeprecationWarning): + + plus_mean = converted_meas @ Plus + np.testing.assert_array_almost_equal(plus_mean.eval(), [1, 0, 0, 1], decimal=1) + sampled_plus = self.sampler.convert(plus_mean) + np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0, 0, 1], decimal=1) + + minus_mean = converted_meas @ Minus + np.testing.assert_array_almost_equal(minus_mean.eval(), [-1, 0, 0, 1], decimal=1) + sampled_minus = self.sampler.convert(minus_mean) + np.testing.assert_array_almost_equal(sampled_minus.eval(), [-1, 0, 0, 1], decimal=1) + + zero_mean = converted_meas @ Zero + np.testing.assert_array_almost_equal(zero_mean.eval(), [0, 0, 1, 1], decimal=1) + sampled_zero = self.sampler.convert(zero_mean) + np.testing.assert_array_almost_equal(sampled_zero.eval(), [0, 0, 1, 1], decimal=1) + + sum_zero = (Plus + Minus) * (0.5**0.5) + sum_zero_mean = converted_meas @ sum_zero + np.testing.assert_array_almost_equal(sum_zero_mean.eval(), [0, 0, 1, 1], decimal=1) + sampled_zero = self.sampler.convert(sum_zero) + + np.testing.assert_array_almost_equal( + (converted_meas @ sampled_zero).eval(), [0, 0, 1, 1], decimal=1 + ) for i, op in enumerate(paulis_op.oplist): mat_op = op.to_matrix() @@ -120,13 +131,13 @@ def test_pauli_expect_op_vector(self): def test_pauli_expect_state_vector(self): """pauli expect state vector test""" - states_op = ListOp([One, Zero, Plus, Minus]) + states_op = ListOp([One, Zero, Plus, Minus]) paulis_op = X converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) np.testing.assert_array_almost_equal(converted_meas.eval(), [0, 0, 1, -1], decimal=1) - - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) np.testing.assert_array_almost_equal(sampled.eval(), [0, 0, 1, -1], decimal=1) # Small test to see if execution results are accessible @@ -135,29 +146,36 @@ def test_pauli_expect_state_vector(self): def test_pauli_expect_op_vector_state_vector(self): """pauli expect op vector state vector test""" + paulis_op = ListOp([X, Y, Z, I]) states_op = ListOp([One, Zero, Plus, Minus]) valids = [[+0, 0, 1, -1], [+0, 0, 0, 0], [-1, 1, 0, -0], [+1, 1, 1, 1]] converted_meas = self.expect.convert(~StateFn(paulis_op)) np.testing.assert_array_almost_equal((converted_meas @ states_op).eval(), valids, decimal=1) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(states_op) - sampled = self.sampler.convert(states_op) np.testing.assert_array_almost_equal((converted_meas @ sampled).eval(), valids, decimal=1) def test_multi_representation_ops(self): """Test observables with mixed representations""" + mixed_ops = ListOp([X.to_matrix_op(), H, H + I, X]) converted_meas = self.expect.convert(~StateFn(mixed_ops)) plus_mean = converted_meas @ Plus - sampled_plus = self.sampler.convert(plus_mean) + + with self.assertWarns(DeprecationWarning): + sampled_plus = self.sampler.convert(plus_mean) + np.testing.assert_array_almost_equal( sampled_plus.eval(), [1, 0.5**0.5, (1 + 0.5**0.5), 1], decimal=1 ) def test_matrix_expectation_non_hermite_op(self): """Test MatrixExpectation for non hermitian operator""" + exp = ~StateFn(1j * Z) @ One self.assertEqual(self.expect.convert(exp).eval(), 1j) diff --git a/test/python/opflow/test_pauli_expectation.py b/test/python/opflow/test_pauli_expectation.py index f346fa25e80f..f50ddc347b62 100644 --- a/test/python/opflow/test_pauli_expectation.py +++ b/test/python/opflow/test_pauli_expectation.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 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 @@ -50,23 +50,29 @@ def setUp(self) -> None: super().setUp() self.seed = 97 backend = BasicAer.get_backend("qasm_simulator") - q_instance = QuantumInstance(backend, seed_simulator=self.seed, seed_transpiler=self.seed) - self.sampler = CircuitSampler(q_instance, attach_results=True) + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + backend, seed_simulator=self.seed, seed_transpiler=self.seed + ) + self.sampler = CircuitSampler(q_instance, attach_results=True) self.expect = PauliExpectation() def test_pauli_expect_pair(self): """pauli expect pair test""" + op = Z ^ Z # wf = (Pl^Pl) + (Ze^Ze) wf = CX @ (H ^ I) @ Zero converted_meas = self.expect.convert(~StateFn(op) @ wf) self.assertAlmostEqual(converted_meas.eval(), 0, delta=0.1) - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) self.assertAlmostEqual(sampled.eval(), 0, delta=0.1) def test_pauli_expect_single(self): """pauli expect single test""" + paulis = [Z, X, Y, I] states = [Zero, One, Plus, Minus, S @ Plus, S @ Minus] for pauli, state in itertools.product(paulis, states): @@ -74,33 +80,42 @@ def test_pauli_expect_single(self): matmulmean = state.adjoint().to_matrix() @ pauli.to_matrix() @ state.to_matrix() self.assertAlmostEqual(converted_meas.eval(), matmulmean, delta=0.1) - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) + self.assertAlmostEqual(sampled.eval(), matmulmean, delta=0.1) def test_pauli_expect_op_vector(self): """pauli expect op vector test""" + paulis_op = ListOp([X, Y, Z, I]) converted_meas = self.expect.convert(~StateFn(paulis_op)) plus_mean = converted_meas @ Plus np.testing.assert_array_almost_equal(plus_mean.eval(), [1, 0, 0, 1], decimal=1) - sampled_plus = self.sampler.convert(plus_mean) - np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0, 0, 1], decimal=1) - - minus_mean = converted_meas @ Minus - np.testing.assert_array_almost_equal(minus_mean.eval(), [-1, 0, 0, 1], decimal=1) - sampled_minus = self.sampler.convert(minus_mean) - np.testing.assert_array_almost_equal(sampled_minus.eval(), [-1, 0, 0, 1], decimal=1) - - zero_mean = converted_meas @ Zero - np.testing.assert_array_almost_equal(zero_mean.eval(), [0, 0, 1, 1], decimal=1) - sampled_zero = self.sampler.convert(zero_mean) - np.testing.assert_array_almost_equal(sampled_zero.eval(), [0, 0, 1, 1], decimal=1) - - sum_zero = (Plus + Minus) * (0.5**0.5) - sum_zero_mean = converted_meas @ sum_zero - np.testing.assert_array_almost_equal(sum_zero_mean.eval(), [0, 0, 1, 1], decimal=1) - sampled_zero_mean = self.sampler.convert(sum_zero_mean) + + with self.assertWarns(DeprecationWarning): + sampled_plus = self.sampler.convert(plus_mean) + np.testing.assert_array_almost_equal(sampled_plus.eval(), [1, 0, 0, 1], decimal=1) + + minus_mean = converted_meas @ Minus + np.testing.assert_array_almost_equal(minus_mean.eval(), [-1, 0, 0, 1], decimal=1) + + sampled_minus = self.sampler.convert(minus_mean) + np.testing.assert_array_almost_equal(sampled_minus.eval(), [-1, 0, 0, 1], decimal=1) + + zero_mean = converted_meas @ Zero + np.testing.assert_array_almost_equal(zero_mean.eval(), [0, 0, 1, 1], decimal=1) + + sampled_zero = self.sampler.convert(zero_mean) + np.testing.assert_array_almost_equal(sampled_zero.eval(), [0, 0, 1, 1], decimal=1) + + sum_zero = (Plus + Minus) * (0.5**0.5) + sum_zero_mean = converted_meas @ sum_zero + np.testing.assert_array_almost_equal(sum_zero_mean.eval(), [0, 0, 1, 1], decimal=1) + + sampled_zero_mean = self.sampler.convert(sum_zero_mean) + # !!NOTE!!: Depolarizing channel (Sampling) means interference # does not happen between circuits in sum, so expectation does # not equal expectation for Zero!! @@ -126,13 +141,16 @@ def test_pauli_expect_op_vector(self): def test_pauli_expect_state_vector(self): """pauli expect state vector test""" + states_op = ListOp([One, Zero, Plus, Minus]) paulis_op = X converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) np.testing.assert_array_almost_equal(converted_meas.eval(), [0, 0, 1, -1], decimal=1) - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) + np.testing.assert_array_almost_equal(sampled.eval(), [0, 0, 1, -1], decimal=1) # Small test to see if execution results are accessible @@ -141,6 +159,7 @@ def test_pauli_expect_state_vector(self): def test_pauli_expect_op_vector_state_vector(self): """pauli expect op vector state vector test""" + paulis_op = ListOp([X, Y, Z, I]) states_op = ListOp([One, Zero, Plus, Minus]) @@ -148,12 +167,15 @@ def test_pauli_expect_op_vector_state_vector(self): converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) np.testing.assert_array_almost_equal(converted_meas.eval(), valids, decimal=1) - sampled = self.sampler.convert(converted_meas) + with self.assertWarns(DeprecationWarning): + sampled = self.sampler.convert(converted_meas) + np.testing.assert_array_almost_equal(sampled.eval(), valids, decimal=1) def test_to_matrix_called(self): """test to matrix called in different situations""" qs = 45 + states_op = ListOp([Zero ^ qs, One ^ qs, (Zero ^ qs) + (One ^ qs)]) paulis_op = ListOp([Z ^ qs, (I ^ Z ^ I) ^ int(qs / 3)]) @@ -189,6 +211,7 @@ def test_not_to_matrix_called(self): def test_grouped_pauli_expectation(self): """grouped pauli expectation test""" + two_qubit_H2 = ( (-1.052373245772859 * I ^ I) + (0.39793742484318045 * I ^ Z) @@ -203,14 +226,18 @@ def test_grouped_pauli_expectation(self): self.assertEqual(num_circuits_ungrouped, 5) expect_op_grouped = PauliExpectation(group_paulis=True).convert(~StateFn(two_qubit_H2) @ wf) - q_instance = QuantumInstance( - BasicAer.get_backend("statevector_simulator"), - seed_simulator=self.seed, - seed_transpiler=self.seed, - ) - sampler = CircuitSampler(q_instance) - sampler._extract_circuitstatefns(expect_op_grouped) - num_circuits_grouped = len(sampler._circuit_ops_cache) + + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + BasicAer.get_backend("statevector_simulator"), + seed_simulator=self.seed, + seed_transpiler=self.seed, + ) + + sampler = CircuitSampler(q_instance) + sampler._extract_circuitstatefns(expect_op_grouped) + num_circuits_grouped = len(sampler._circuit_ops_cache) + self.assertEqual(num_circuits_grouped, 2) @unittest.skip(reason="IBMQ testing not available in general.") @@ -220,34 +247,43 @@ def test_ibmq_grouped_pauli_expectation(self): p = IBMQ.load_account() backend = p.get_backend("ibmq_qasm_simulator") - q_instance = QuantumInstance(backend, seed_simulator=self.seed, seed_transpiler=self.seed) + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance( + backend, seed_simulator=self.seed, seed_transpiler=self.seed + ) paulis_op = ListOp([X, Y, Z, I]) states_op = ListOp([One, Zero, Plus, Minus]) valids = [[+0, 0, 1, -1], [+0, 0, 0, 0], [-1, 1, 0, -0], [+1, 1, 1, 1]] converted_meas = self.expect.convert(~StateFn(paulis_op) @ states_op) - sampled = CircuitSampler(q_instance).convert(converted_meas) - np.testing.assert_array_almost_equal(sampled.eval(), valids, decimal=1) + + with self.assertWarns(DeprecationWarning): + sampled = CircuitSampler(q_instance).convert(converted_meas) + np.testing.assert_array_almost_equal(sampled.eval(), valids, decimal=1) def test_multi_representation_ops(self): """Test observables with mixed representations""" + mixed_ops = ListOp([X.to_matrix_op(), H, H + I, X]) converted_meas = self.expect.convert(~StateFn(mixed_ops)) - plus_mean = converted_meas @ Plus - sampled_plus = self.sampler.convert(plus_mean) + + with self.assertWarns(DeprecationWarning): + sampled_plus = self.sampler.convert(plus_mean) np.testing.assert_array_almost_equal( sampled_plus.eval(), [1, 0.5**0.5, (1 + 0.5**0.5), 1], decimal=1 ) def test_pauli_expectation_non_hermite_op(self): """Test PauliExpectation for non hermitian operator""" + exp = ~StateFn(1j * Z) @ One self.assertEqual(self.expect.convert(exp).eval(), 1j) def test_list_pauli_sum_op(self): """Test PauliExpectation for List[PauliSumOp]""" + test_op = ListOp([~StateFn(PauliSumOp.from_list([("XX", 1), ("ZI", 3), ("ZZ", 5)]))]) observable = self.expect.convert(test_op) self.assertIsInstance(observable, ListOp) @@ -256,14 +292,17 @@ def test_list_pauli_sum_op(self): def test_expectation_with_coeff(self): """Test PauliExpectation with coefficients.""" + with self.subTest("integer coefficients"): exp = 3 * ~StateFn(X) @ (2 * Minus) - target = self.sampler.convert(self.expect.convert(exp)).eval() + with self.assertWarns(DeprecationWarning): + target = self.sampler.convert(self.expect.convert(exp)).eval() self.assertEqual(target, -12) with self.subTest("complex coefficients"): exp = 3j * ~StateFn(X) @ (2j * Minus) - target = self.sampler.convert(self.expect.convert(exp)).eval() + with self.assertWarns(DeprecationWarning): + target = self.sampler.convert(self.expect.convert(exp)).eval() self.assertEqual(target, -12j) diff --git a/test/python/opflow/test_state_op_meas_evals.py b/test/python/opflow/test_state_op_meas_evals.py index 6a9f2fee5f02..e6d23390ea78 100644 --- a/test/python/opflow/test_state_op_meas_evals.py +++ b/test/python/opflow/test_state_op_meas_evals.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2021. +# (C) Copyright IBM 2018, 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 @@ -43,6 +43,7 @@ def test_statefn_overlaps(self): def test_wf_evals_x(self): """wf evals x test""" qbits = 4 + wf = ((Zero ^ qbits) + (One ^ qbits)) * (1 / 2**0.5) # Note: wf = Plus^qbits fails because TensoredOp can't handle it. wf_vec = StateFn(wf.to_matrix()) @@ -76,17 +77,20 @@ def test_coefficients_correctly_propagated(self): self.assertEqual((~StateFn(op) @ state).eval(), 0j) backend = Aer.get_backend("aer_simulator") - q_instance = QuantumInstance(backend, seed_simulator=97, seed_transpiler=97) + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance(backend, seed_simulator=97, seed_transpiler=97) op = I with self.subTest("zero coeff in summed StateFn and CircuitSampler"): - state = 0 * (Plus + Minus) - sampler = CircuitSampler(q_instance).convert(~StateFn(op) @ state) - self.assertEqual(sampler.eval(), 0j) + with self.assertWarns(DeprecationWarning): + state = 0 * (Plus + Minus) + sampler = CircuitSampler(q_instance).convert(~StateFn(op) @ state) + self.assertEqual(sampler.eval(), 0j) with self.subTest("coeff gets squared in CircuitSampler shot-based readout"): - state = (Plus + Minus) / numpy.sqrt(2) - sampler = CircuitSampler(q_instance).convert(~StateFn(op) @ state) - self.assertAlmostEqual(sampler.eval(), 1 + 0j) + with self.assertWarns(DeprecationWarning): + state = (Plus + Minus) / numpy.sqrt(2) + sampler = CircuitSampler(q_instance).convert(~StateFn(op) @ state) + self.assertAlmostEqual(sampler.eval(), 1 + 0j) def test_is_measurement_correctly_propagated(self): """Test if is_measurement property of StateFn is propagated to converted StateFn.""" @@ -96,10 +100,12 @@ def test_is_measurement_correctly_propagated(self): self.skipTest(f"Aer doesn't appear to be installed. Error: '{str(ex)}'") return backend = Aer.get_backend("aer_simulator") - q_instance = QuantumInstance(backend) # no seeds needed since no values are compared - state = Plus - sampler = CircuitSampler(q_instance).convert(~state @ state) - self.assertTrue(sampler.oplist[0].is_measurement) + + with self.assertWarns(DeprecationWarning): + q_instance = QuantumInstance(backend) # no seeds needed since no values are compared + state = Plus + sampler = CircuitSampler(q_instance).convert(~state @ state) + self.assertTrue(sampler.oplist[0].is_measurement) def test_parameter_binding_on_listop(self): """Test passing a ListOp with differing parameters works with the circuit sampler.""" @@ -117,16 +123,17 @@ def test_parameter_binding_on_listop(self): circuit3 = QuantumCircuit(1) circuit3.p(y, 0) - bindings = {x: -0.4, y: 0.4} - listop = ListOp([StateFn(circuit) for circuit in [circuit1, circuit2, circuit3]]) - - sampler = CircuitSampler(Aer.get_backend("aer_simulator")) - sampled = sampler.convert(listop, params=bindings) + with self.assertWarns(DeprecationWarning): + bindings = {x: -0.4, y: 0.4} + listop = ListOp([StateFn(circuit) for circuit in [circuit1, circuit2, circuit3]]) + sampler = CircuitSampler(Aer.get_backend("aer_simulator")) + sampled = sampler.convert(listop, params=bindings) self.assertTrue(all(len(op.parameters) == 0 for op in sampled.oplist)) def test_list_op_eval_coeff_with_nonlinear_combofn(self): """Test evaluating a ListOp with non-linear combo function works with coefficients.""" + state = One op = ListOp(5 * [I], coeff=2, combo_fn=numpy.prod) expr1 = ~StateFn(op) @ state @@ -147,11 +154,11 @@ def test_single_parameter_binds(self): x = Parameter("x") circuit = QuantumCircuit(1) circuit.ry(x, 0) - expr = ~StateFn(H) @ StateFn(circuit) - - sampler = CircuitSampler(Aer.get_backend("aer_simulator_statevector")) - res = sampler.convert(expr, params={x: 0}).eval() + with self.assertWarns(DeprecationWarning): + expr = ~StateFn(H) @ StateFn(circuit) + sampler = CircuitSampler(Aer.get_backend("aer_simulator_statevector")) + res = sampler.convert(expr, params={x: 0}).eval() self.assertIsInstance(res, complex) @@ -167,15 +174,17 @@ def test_circuit_sampler_caching(self, caching): x = Parameter("x") circuit = QuantumCircuit(1) circuit.ry(x, 0) - expr1 = ~StateFn(H) @ StateFn(circuit) - expr2 = ~StateFn(X) @ StateFn(circuit) - sampler = CircuitSampler(Aer.get_backend("aer_simulator_statevector"), caching=caching) + with self.assertWarns(DeprecationWarning): - res1 = sampler.convert(expr1, params={x: 0}).eval() - res2 = sampler.convert(expr2, params={x: 0}).eval() - res3 = sampler.convert(expr1, params={x: 0}).eval() - res4 = sampler.convert(expr2, params={x: 0}).eval() + expr1 = ~StateFn(H) @ StateFn(circuit) + expr2 = ~StateFn(X) @ StateFn(circuit) + sampler = CircuitSampler(Aer.get_backend("aer_simulator_statevector"), caching=caching) + + res1 = sampler.convert(expr1, params={x: 0}).eval() + res2 = sampler.convert(expr2, params={x: 0}).eval() + res3 = sampler.convert(expr1, params={x: 0}).eval() + res4 = sampler.convert(expr2, params={x: 0}).eval() self.assertEqual(res1, res3) self.assertEqual(res2, res4) @@ -201,9 +210,10 @@ def test_evaluating_nonunitary_circuit_state(self): """ circuit = QuantumCircuit(1) circuit.initialize([0, 1], [0]) - op = Z + op = Z res = (~StateFn(op) @ StateFn(circuit)).eval() + self.assertAlmostEqual(-1 + 0j, res) def test_quantum_instance_with_backend_shots(self): @@ -214,12 +224,15 @@ def test_quantum_instance_with_backend_shots(self): self.skipTest(f"Aer doesn't appear to be installed. Error: '{str(ex)}'") backend = AerSimulator(shots=10) - sampler = CircuitSampler(backend) - res = sampler.convert(~Plus @ Plus).eval() + + with self.assertWarns(DeprecationWarning): + sampler = CircuitSampler(backend) + res = sampler.convert(~Plus @ Plus).eval() self.assertAlmostEqual(res, 1 + 0j, places=2) def test_adjoint_vector_to_circuit_fn(self): """Test it is possible to adjoint a VectorStateFn that was converted to a CircuitStateFn.""" + left = StateFn([0, 1]) left_circuit = left.to_circuit_op().primitive diff --git a/test/python/result/test_sampled_expval.py b/test/python/result/test_sampled_expval.py index 65ebc3dd966f..f7cb06c5b86b 100644 --- a/test/python/result/test_sampled_expval.py +++ b/test/python/result/test_sampled_expval.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2017, 2018. +# (C) Copyright IBM 2017, 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 @@ -13,7 +13,6 @@ """Tests for qiskit.quantum_info.analysis""" import unittest - from qiskit.result import Counts, QuasiDistribution, ProbDistribution, sampled_expectation_value from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.opflow import PauliOp, PauliSumOp @@ -83,11 +82,13 @@ def test_same(self): exp2 = sampled_expectation_value(counts, Pauli(oper)) self.assertAlmostEqual(exp2, ans) - exp3 = sampled_expectation_value(counts, PauliOp(Pauli(oper))) + with self.assertWarns(DeprecationWarning): + exp3 = sampled_expectation_value(counts, PauliOp(Pauli(oper))) self.assertAlmostEqual(exp3, ans) spo = SparsePauliOp([oper], coeffs=[1]) - exp4 = sampled_expectation_value(counts, PauliSumOp(spo, coeff=2)) + with self.assertWarns(DeprecationWarning): + exp4 = sampled_expectation_value(counts, PauliSumOp(spo, coeff=2)) self.assertAlmostEqual(exp4, 2 * ans) exp5 = sampled_expectation_value(counts, SparsePauliOp.from_list([[oper, 1]])) diff --git a/test/python/transpiler/test_swap_strategy_router.py b/test/python/transpiler/test_swap_strategy_router.py index 7f5291fd0ee3..67f746d66724 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. +# (C) Copyright IBM 2022, 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 @@ -21,8 +21,7 @@ from qiskit.circuit.library.n_local import QAOAAnsatz from qiskit.converters import circuit_to_dag from qiskit.exceptions import QiskitError -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import Pauli +from qiskit.quantum_info import Pauli, SparsePauliOp from qiskit.transpiler.passes import FullAncillaAllocation from qiskit.transpiler.passes import EnlargeWithAncilla from qiskit.transpiler.passes import ApplyLayout @@ -77,7 +76,7 @@ def test_basic_zz(self): """ - op = PauliSumOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) + op = SparsePauliOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) circ = QuantumCircuit(4) circ.append(PauliEvolutionGate(op, 1), range(4)) @@ -113,7 +112,7 @@ def test_basic_xx(self): └─────────────────┘ └────────────────┘ """ - op = PauliSumOp.from_list([("XXII", -1), ("IIXX", 1), ("XIIX", -2), ("IXIX", 2)]) + op = SparsePauliOp.from_list([("XXII", -1), ("IIXX", 1), ("XIIX", -2), ("IXIX", 2)]) circ = QuantumCircuit(4) circ.append(PauliEvolutionGate(op, 3), range(4)) @@ -150,7 +149,8 @@ def test_idle_qubit(self): q_3: ───────────────────────────────────────── """ - op = PauliSumOp.from_list([("IIXX", 1), ("IXIX", 2)]) + + op = SparsePauliOp.from_list([("IIXX", 1), ("IXIX", 2)]) cmap = CouplingMap(couplinglist=[(0, 1), (1, 2), (2, 3)]) swap_strat = SwapStrategy(cmap, swap_layers=(((0, 1),),)) @@ -191,7 +191,7 @@ def test_basic_xx_with_measure(self): 0 1 2 3 """ - op = PauliSumOp.from_list([("XXII", -1), ("IIXX", 1), ("XIIX", -2), ("IXIX", 2)]) + op = SparsePauliOp.from_list([("XXII", -1), ("IIXX", 1), ("XIIX", -2), ("IXIX", 2)]) circ = QuantumCircuit(4, 4) circ.append(PauliEvolutionGate(op, 3), range(4)) @@ -258,10 +258,10 @@ def test_qaoa(self): for idx in range(4): mixer.ry(-idx, idx) - op = PauliSumOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) + op = SparsePauliOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) circ = QAOAAnsatz(op, reps=2, mixer_operator=mixer) - swapped = self.pm_.run(circ.decompose()) + param_dict = {p: idx + 1 for idx, p in enumerate(swapped.parameters)} swapped.assign_parameters(param_dict, inplace=True) @@ -303,7 +303,7 @@ def test_enlarge_with_ancilla(self): """This pass tests that idle qubits after an embedding are left idle.""" # Create a four qubit problem. - op = PauliSumOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) + op = SparsePauliOp.from_list([("IZZI", 1), ("ZIIZ", 2), ("ZIZI", 3)]) circ = QuantumCircuit(4) circ.append(PauliEvolutionGate(op, 1), range(4)) @@ -380,7 +380,7 @@ def test_ccx(self): Commuting2qGateRouter(swap_strat), ] ) - op = PauliSumOp.from_list([("IZZ", 1), ("ZIZ", 2)]) + op = SparsePauliOp.from_list([("IZZ", 1), ("ZIZ", 2)]) circ = QuantumCircuit(4) circ.append(PauliEvolutionGate(op, 1), range(3)) circ.ccx(0, 2, 1) @@ -425,7 +425,7 @@ def test_t_device(self): swap_strat = SwapStrategy(cmap, swaps) # A dense Pauli op. - op = PauliSumOp.from_list( + op = SparsePauliOp.from_list( [ ("IIIZZ", 1), ("IIZIZ", 2), @@ -495,7 +495,8 @@ def inst_info(op, qargs, qreg): def test_single_qubit_circuit(self): """Test that a circuit with only single qubit gates is left unchanged.""" - op = PauliSumOp.from_list([("IIIX", 1), ("IIXI", 2), ("IZII", 3), ("XIII", 4)]) + + op = SparsePauliOp.from_list([("IIIX", 1), ("IIXI", 2), ("IZII", 3), ("XIII", 4)]) circ = QuantumCircuit(4) circ.append(PauliEvolutionGate(op, 1), range(4)) @@ -508,7 +509,8 @@ def test_single_qubit_circuit(self): ) def test_edge_coloring(self, edge_coloring): """Test that the edge coloring works.""" - op = PauliSumOp.from_list([("IIZZ", 1), ("IZZI", 2), ("ZZII", 3), ("ZIZI", 4)]) + + op = SparsePauliOp.from_list([("IIZZ", 1), ("IZZI", 2), ("ZZII", 3), ("ZIZI", 4)]) swaps = (((1, 2),),) cmap = CouplingMap([[0, 1], [1, 2], [2, 3]]) @@ -572,7 +574,7 @@ def setUp(self): super().setUp() # A fully connected problem. - op = PauliSumOp.from_list( + op = SparsePauliOp.from_list( [("IIZZ", 1), ("IZIZ", 1), ("ZIIZ", 1), ("IZZI", 1), ("ZIZI", 1), ("ZZII", 1)] ) self.circ = QuantumCircuit(4) diff --git a/test/python/utils/mitigation/test_meas.py b/test/python/utils/mitigation/test_meas.py index 21c6322b09bd..a5105b5ba4d5 100644 --- a/test/python/utils/mitigation/test_meas.py +++ b/test/python/utils/mitigation/test_meas.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2021. +# (C) Copyright IBM 2021, 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 @@ -46,9 +46,9 @@ if optionals.HAS_AER: # pylint: disable=no-name-in-module - from qiskit.providers.aer import Aer - from qiskit.providers.aer.noise import NoiseModel - from qiskit.providers.aer.noise.errors.standard_errors import pauli_error + from qiskit_aer import AerSimulator + from qiskit_aer.noise import NoiseModel + from qiskit_aer.noise.errors.standard_errors import pauli_error # fixed seed for tests - for both simulator and transpiler SEED = 42 @@ -156,7 +156,7 @@ def meas_calibration_circ_execution(shots: int, seed: int): noise_model.add_all_qubit_quantum_error(error_meas, "measure") # run the circuits multiple times - backend = qiskit.Aer.get_backend("qasm_simulator") + backend = AerSimulator() cal_results = qiskit.execute( meas_calibs, backend=backend, shots=shots, noise_model=noise_model, seed_simulator=seed ).result() @@ -193,7 +193,7 @@ def tensored_calib_circ_execution(shots: int, seed: int): noise_model.add_all_qubit_quantum_error(error_meas, "measure") # run the circuits multiple times - backend = qiskit.Aer.get_backend("qasm_simulator") + backend = AerSimulator() cal_results = qiskit.execute( meas_calibs, backend=backend, shots=shots, noise_model=noise_model, seed_simulator=seed ).result() @@ -278,16 +278,20 @@ def test_ideal_meas_cal(self): # Generate the quantum register according to the pattern qubits, weight = self.choose_calibration(nq, pattern_type) - # Generate the calibration circuits - meas_calibs, state_labels = complete_meas_cal(qubit_list=qubits, circlabel="test") + with self.assertWarns(DeprecationWarning): + # Generate the calibration circuits + meas_calibs, state_labels = complete_meas_cal( + qubit_list=qubits, circlabel="test" + ) # Perform an ideal execution on the generated circuits - backend = Aer.get_backend("qasm_simulator") + backend = AerSimulator() job = qiskit.execute(meas_calibs, backend=backend, shots=self.shots) cal_results = job.result() - # Make a calibration matrix - meas_cal = CompleteMeasFitter(cal_results, state_labels, circlabel="test") + with self.assertWarns(DeprecationWarning): + # Make a calibration matrix + meas_cal = CompleteMeasFitter(cal_results, state_labels, circlabel="test") # Assert that the calibration matrix is equal to identity IdentityMatrix = np.identity(2**weight) @@ -307,8 +311,9 @@ def test_ideal_meas_cal(self): # Generate ideal (equally distributed) results results_dict, results_list = self.generate_ideal_results(state_labels, weight) - # Output the filter - meas_filter = meas_cal.filter + with self.assertWarns(DeprecationWarning): + # Output the filter + meas_filter = meas_cal.filter # Apply the calibration matrix to results # in list and dict forms using different methods @@ -329,10 +334,11 @@ def test_ideal_meas_cal(self): def test_meas_cal_on_circuit(self): """Test an execution on a circuit.""" # Generate the calibration circuits - meas_calibs, state_labels, ghz = meas_calib_circ_creation() + with self.assertWarns(DeprecationWarning): + meas_calibs, state_labels, ghz = meas_calib_circ_creation() # Run the calibration circuits - backend = Aer.get_backend("qasm_simulator") + backend = AerSimulator() job = qiskit.execute( meas_calibs, backend=backend, @@ -342,8 +348,9 @@ def test_meas_cal_on_circuit(self): ) cal_results = job.result() - # Make a calibration matrix - meas_cal = CompleteMeasFitter(cal_results, state_labels) + with self.assertWarns(DeprecationWarning): + # Make a calibration matrix + meas_cal = CompleteMeasFitter(cal_results, state_labels) # Calculate the fidelity fidelity = meas_cal.readout_fidelity() @@ -355,7 +362,8 @@ def test_meas_cal_on_circuit(self): # Predicted equally distributed results predicted_results = {"000": 0.5, "111": 0.5} - meas_filter = meas_cal.filter + with self.assertWarns(DeprecationWarning): + meas_filter = meas_cal.filter # Calculate the results after mitigation output_results_pseudo_inverse = meas_filter.apply( @@ -390,14 +398,16 @@ def test_ideal_tensored_meas_cal(self): meas_layout = [1, 2, 3, 4, 5, 6] # Generate the calibration circuits - meas_calibs, _ = tensored_meas_cal(mit_pattern=mit_pattern) + with self.assertWarns(DeprecationWarning): + meas_calibs, _ = tensored_meas_cal(mit_pattern=mit_pattern) # Perform an ideal execution on the generated circuits - backend = Aer.get_backend("qasm_simulator") + backend = AerSimulator() cal_results = qiskit.execute(meas_calibs, backend=backend, shots=self.shots).result() - # Make calibration matrices - meas_cal = TensoredMeasFitter(cal_results, mit_pattern=mit_pattern) + with self.assertWarns(DeprecationWarning): + # Make calibration matrices + meas_cal = TensoredMeasFitter(cal_results, mit_pattern=mit_pattern) # Assert that the calibration matrices are equal to identity cal_matrices = meas_cal.cal_matrices @@ -419,20 +429,19 @@ def test_ideal_tensored_meas_cal(self): "Error: the average fidelity is not equal to 1", ) - # Generate ideal (equally distributed) results - results_dict, _ = self.generate_ideal_results(count_keys(6), 6) - - # Output the filter - meas_filter = meas_cal.filter - - # Apply the calibration matrix to results - # in list and dict forms using different methods - results_dict_1 = meas_filter.apply( - results_dict, method="least_squares", meas_layout=meas_layout - ) - results_dict_0 = meas_filter.apply( - results_dict, method="pseudo_inverse", meas_layout=meas_layout - ) + with self.assertWarns(DeprecationWarning): + # Generate ideal (equally distributed) results + results_dict, _ = self.generate_ideal_results(count_keys(6), 6) + # Output the filter + meas_filter = meas_cal.filter + # Apply the calibration matrix to results + # in list and dict forms using different methods + results_dict_1 = meas_filter.apply( + results_dict, method="least_squares", meas_layout=meas_layout + ) + results_dict_0 = meas_filter.apply( + results_dict, method="pseudo_inverse", meas_layout=meas_layout + ) # Assert that the results are equally distributed self.assertDictEqual(results_dict, results_dict_0) @@ -444,11 +453,12 @@ def test_ideal_tensored_meas_cal(self): def test_tensored_meas_cal_on_circuit(self): """Test an execution on a circuit.""" - # Generate the calibration circuits - meas_calibs, mit_pattern, ghz, meas_layout = tensored_calib_circ_creation() + with self.assertWarns(DeprecationWarning): + # Generate the calibration circuits + meas_calibs, mit_pattern, ghz, meas_layout = tensored_calib_circ_creation() # Run the calibration circuits - backend = Aer.get_backend("qasm_simulator") + backend = AerSimulator() cal_results = qiskit.execute( meas_calibs, backend=backend, @@ -457,8 +467,9 @@ def test_tensored_meas_cal_on_circuit(self): seed_transpiler=SEED, ).result() - # Make a calibration matrix - meas_cal = TensoredMeasFitter(cal_results, mit_pattern=mit_pattern) + with self.assertWarns(DeprecationWarning): + # Make a calibration matrix + meas_cal = TensoredMeasFitter(cal_results, mit_pattern=mit_pattern) # Calculate the fidelity fidelity = meas_cal.readout_fidelity(0) * meas_cal.readout_fidelity(1) @@ -469,15 +480,15 @@ def test_tensored_meas_cal_on_circuit(self): # Predicted equally distributed results predicted_results = {"000": 0.5, "111": 0.5} - meas_filter = meas_cal.filter - - # Calculate the results after mitigation - output_results_pseudo_inverse = meas_filter.apply( - results, method="pseudo_inverse", meas_layout=meas_layout - ).get_counts(0) - output_results_least_square = meas_filter.apply( - results, method="least_squares", meas_layout=meas_layout - ).get_counts(0) + with self.assertWarns(DeprecationWarning): + meas_filter = meas_cal.filter + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + results, method="pseudo_inverse", meas_layout=meas_layout + ).get_counts(0) + output_results_least_square = meas_filter.apply( + results, method="least_squares", meas_layout=meas_layout + ).get_counts(0) # Compare with expected fidelity and expected results self.assertAlmostEqual(fidelity, 1.0) @@ -501,89 +512,89 @@ def test_meas_fitter_with_noise(self): """Test the MeasurementFitter with noise.""" tests = [] runs = 3 - for run in range(runs): - cal_results, state_labels, circuit_results = meas_calibration_circ_execution( - 1000, SEED + run - ) - - meas_cal = CompleteMeasFitter(cal_results, state_labels) - meas_filter = MeasurementFilter(meas_cal.cal_matrix, state_labels) - - # Calculate the results after mitigation - results_pseudo_inverse = meas_filter.apply(circuit_results, method="pseudo_inverse") - results_least_square = meas_filter.apply(circuit_results, method="least_squares") - tests.append( - { - "cal_matrix": convert_ndarray_to_list_in_data(meas_cal.cal_matrix), - "fidelity": meas_cal.readout_fidelity(), - "results": circuit_results, - "results_pseudo_inverse": results_pseudo_inverse, - "results_least_square": results_least_square, - } - ) + with self.assertWarns(DeprecationWarning): + for run in range(runs): + cal_results, state_labels, circuit_results = meas_calibration_circ_execution( + 1000, SEED + run + ) - # Set the state labels - state_labels = ["000", "001", "010", "011", "100", "101", "110", "111"] - meas_cal = CompleteMeasFitter(None, state_labels, circlabel="test") + meas_cal = CompleteMeasFitter(cal_results, state_labels) + meas_filter = MeasurementFilter(meas_cal.cal_matrix, state_labels) + + # Calculate the results after mitigation + results_pseudo_inverse = meas_filter.apply(circuit_results, method="pseudo_inverse") + results_least_square = meas_filter.apply(circuit_results, method="least_squares") + tests.append( + { + "cal_matrix": convert_ndarray_to_list_in_data(meas_cal.cal_matrix), + "fidelity": meas_cal.readout_fidelity(), + "results": circuit_results, + "results_pseudo_inverse": results_pseudo_inverse, + "results_least_square": results_least_square, + } + ) + # Set the state labels + state_labels = ["000", "001", "010", "011", "100", "101", "110", "111"] + meas_cal = CompleteMeasFitter(None, state_labels, circlabel="test") - for tst_index, _ in enumerate(tests): - # Set the calibration matrix - meas_cal.cal_matrix = tests[tst_index]["cal_matrix"] - # Calculate the fidelity - fidelity = meas_cal.readout_fidelity() + for tst_index, _ in enumerate(tests): + # Set the calibration matrix + meas_cal.cal_matrix = tests[tst_index]["cal_matrix"] + # Calculate the fidelity + fidelity = meas_cal.readout_fidelity() - meas_filter = MeasurementFilter(tests[tst_index]["cal_matrix"], state_labels) + meas_filter = MeasurementFilter(tests[tst_index]["cal_matrix"], state_labels) - # Calculate the results after mitigation - output_results_pseudo_inverse = meas_filter.apply( - tests[tst_index]["results"], method="pseudo_inverse" - ) - output_results_least_square = meas_filter.apply( - tests[tst_index]["results"], method="least_squares" - ) + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + tests[tst_index]["results"], method="pseudo_inverse" + ) + output_results_least_square = meas_filter.apply( + tests[tst_index]["results"], method="least_squares" + ) - # Compare with expected fidelity and expected results - self.assertAlmostEqual(fidelity, tests[tst_index]["fidelity"], places=0) - self.assertAlmostEqual( - output_results_pseudo_inverse["000"], - tests[tst_index]["results_pseudo_inverse"]["000"], - places=0, - ) + # Compare with expected fidelity and expected results + self.assertAlmostEqual(fidelity, tests[tst_index]["fidelity"], places=0) + self.assertAlmostEqual( + output_results_pseudo_inverse["000"], + tests[tst_index]["results_pseudo_inverse"]["000"], + places=0, + ) - self.assertAlmostEqual( - output_results_least_square["000"], - tests[tst_index]["results_least_square"]["000"], - places=0, - ) + self.assertAlmostEqual( + output_results_least_square["000"], + tests[tst_index]["results_least_square"]["000"], + places=0, + ) - self.assertAlmostEqual( - output_results_pseudo_inverse["111"], - tests[tst_index]["results_pseudo_inverse"]["111"], - places=0, - ) + self.assertAlmostEqual( + output_results_pseudo_inverse["111"], + tests[tst_index]["results_pseudo_inverse"]["111"], + places=0, + ) - self.assertAlmostEqual( - output_results_least_square["111"], - tests[tst_index]["results_least_square"]["111"], - places=0, - ) + self.assertAlmostEqual( + output_results_least_square["111"], + tests[tst_index]["results_least_square"]["111"], + places=0, + ) def test_tensored_meas_fitter_with_noise(self): """Test the TensoredFitter with noise.""" - cal_results, mit_pattern, circuit_results, meas_layout = tensored_calib_circ_execution( - 1000, SEED - ) - - meas_cal = TensoredMeasFitter(cal_results, mit_pattern=mit_pattern) - meas_filter = meas_cal.filter + with self.assertWarns(DeprecationWarning): + cal_results, mit_pattern, circuit_results, meas_layout = tensored_calib_circ_execution( + 1000, SEED + ) + meas_cal = TensoredMeasFitter(cal_results, mit_pattern=mit_pattern) + meas_filter = meas_cal.filter + # Calculate the results after mitigation + results_pseudo_inverse = meas_filter.apply( + circuit_results.get_counts(), method="pseudo_inverse", meas_layout=meas_layout + ) + results_least_square = meas_filter.apply( + circuit_results.get_counts(), method="least_squares", meas_layout=meas_layout + ) - # Calculate the results after mitigation - results_pseudo_inverse = meas_filter.apply( - circuit_results.get_counts(), method="pseudo_inverse", meas_layout=meas_layout - ) - results_least_square = meas_filter.apply( - circuit_results.get_counts(), method="least_squares", meas_layout=meas_layout - ) saved_info = { "cal_results": cal_results.to_dict(), "results": circuit_results.to_dict(), @@ -597,26 +608,27 @@ def test_tensored_meas_fitter_with_noise(self): saved_info["cal_results"] = Result.from_dict(saved_info["cal_results"]) saved_info["results"] = Result.from_dict(saved_info["results"]) - meas_cal = TensoredMeasFitter( - saved_info["cal_results"], mit_pattern=saved_info["mit_pattern"] - ) + with self.assertWarns(DeprecationWarning): + meas_cal = TensoredMeasFitter( + saved_info["cal_results"], mit_pattern=saved_info["mit_pattern"] + ) + # Calculate the fidelity + fidelity = meas_cal.readout_fidelity(0) * meas_cal.readout_fidelity(1) + # Compare with expected fidelity and expected results - # Calculate the fidelity - fidelity = meas_cal.readout_fidelity(0) * meas_cal.readout_fidelity(1) - # Compare with expected fidelity and expected results self.assertAlmostEqual(fidelity, saved_info["fidelity"], places=0) - meas_filter = meas_cal.filter - - # Calculate the results after mitigation - output_results_pseudo_inverse = meas_filter.apply( - saved_info["results"].get_counts(0), - method="pseudo_inverse", - meas_layout=saved_info["meas_layout"], - ) - output_results_least_square = meas_filter.apply( - saved_info["results"], method="least_squares", meas_layout=saved_info["meas_layout"] - ) + with self.assertWarns(DeprecationWarning): + meas_filter = meas_cal.filter + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + saved_info["results"].get_counts(0), + method="pseudo_inverse", + meas_layout=saved_info["meas_layout"], + ) + output_results_least_square = meas_filter.apply( + saved_info["results"], method="least_squares", meas_layout=saved_info["meas_layout"] + ) self.assertAlmostEqual( output_results_pseudo_inverse["000"], @@ -643,30 +655,30 @@ def test_tensored_meas_fitter_with_noise(self): ) substates_list = [] - for qubit_list in saved_info["mit_pattern"]: - substates_list.append(count_keys(len(qubit_list))[::-1]) - - fitter_other_order = TensoredMeasFitter( - saved_info["cal_results"], - substate_labels_list=substates_list, - mit_pattern=saved_info["mit_pattern"], - ) + with self.assertWarns(DeprecationWarning): + for qubit_list in saved_info["mit_pattern"]: + substates_list.append(count_keys(len(qubit_list))[::-1]) + fitter_other_order = TensoredMeasFitter( + saved_info["cal_results"], + substate_labels_list=substates_list, + mit_pattern=saved_info["mit_pattern"], + ) fidelity = fitter_other_order.readout_fidelity(0) * meas_cal.readout_fidelity(1) self.assertAlmostEqual(fidelity, saved_info["fidelity"], places=0) - meas_filter = fitter_other_order.filter - - # Calculate the results after mitigation - output_results_pseudo_inverse = meas_filter.apply( - saved_info["results"].get_counts(0), - method="pseudo_inverse", - meas_layout=saved_info["meas_layout"], - ) - output_results_least_square = meas_filter.apply( - saved_info["results"], method="least_squares", meas_layout=saved_info["meas_layout"] - ) + with self.assertWarns(DeprecationWarning): + meas_filter = fitter_other_order.filter + # Calculate the results after mitigation + output_results_pseudo_inverse = meas_filter.apply( + saved_info["results"].get_counts(0), + method="pseudo_inverse", + meas_layout=saved_info["meas_layout"], + ) + output_results_least_square = meas_filter.apply( + saved_info["results"], method="least_squares", meas_layout=saved_info["meas_layout"] + ) self.assertAlmostEqual( output_results_pseudo_inverse["000"], From 08f3f00ef92c8e192abe13722bfee9a7a6d4006e Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 19 Apr 2023 13:29:26 +0100 Subject: [PATCH 051/172] Add support to `switch` in `transpile` (#9928) * Add support for `switch` in transpiler For the most part, the heavy lifting for this was already done in the previous support for control flow. This commit refactors some of the handling into larger, more descriptive helper functions, and most of the rest of the work is in testing. I'm fairly confident there are some register-related problems to do with mappings in `DAGCircuit.substitute_node_*` and `QuantumCircuit.compose` right now, in both existing control flow and the new switch statement, which will need revisiting. * Update release note * Fix wording in release note Co-authored-by: Matthew Treinish --------- Co-authored-by: Matthew Treinish --- qiskit/dagcircuit/dagcircuit.py | 87 +++++++------ .../passes/routing/stochastic_swap.py | 36 ++++-- .../transpiler/preset_passmanagers/common.py | 2 +- .../notes/switch-case-9b6611d0603d36c0.yaml | 34 +++++ test/python/compiler/test_transpiler.py | 29 ++++- test/python/providers/test_fake_backends.py | 10 +- .../transpiler/test_gates_in_basis_pass.py | 3 +- .../python/transpiler/test_stochastic_swap.py | 117 +++++++++++++++++- test/python/transpiler/test_target.py | 52 +++++++- 9 files changed, 311 insertions(+), 59 deletions(-) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index 274502f5c0f4..a2ea26334c53 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -29,7 +29,7 @@ import numpy as np import rustworkx as rx -from qiskit.circuit import ControlFlowOp, ForLoopOp, IfElseOp, WhileLoopOp +from qiskit.circuit import ControlFlowOp, ForLoopOp, IfElseOp, WhileLoopOp, SwitchCaseOp from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.quantumregister import QuantumRegister, Qubit from qiskit.circuit.classicalregister import ClassicalRegister, Clbit @@ -700,38 +700,31 @@ def _map_condition(wire_map, condition, target_cregs): new_condition = (new_creg, new_cond_val) return new_condition - def _map_condition_with_import(self, op, wire_map, creg_map): - """Map the condition in ``op`` to its counterpart in ``self`` using ``wire_map`` and - ``creg_map`` as lookup caches. All single-bit conditions should have a cache hit in the - ``wire_map``, but registers may involve a full linear search the first time they are - encountered. ``creg_map`` is mutated by this function. ``wire_map`` is not; it is an error - if a wire is not in the map. - - This is different to ``_map_condition`` because it always succeeds; since the mapping for - all wires in the condition is assumed to exist, there can be no fragmented registers. If - there is no matching register (has the same bits in the same order) in ``self``, a new - register alias is added to represent the condition. This does not change the bits available - to ``self``, it just adds a new aliased grouping of them.""" - op_condition = getattr(op, "condition", None) - if op_condition is None: - return op - new_op = copy.copy(op) - target, value = op_condition - if isinstance(target, Clbit): - new_op.condition = (wire_map[target], value) - else: - if target.name not in creg_map: - mapped_bits = [wire_map[bit] for bit in target] - for our_creg in self.cregs.values(): - if mapped_bits == list(our_creg): - new_target = our_creg - break - else: - new_target = ClassicalRegister(bits=[wire_map[bit] for bit in target]) - self.add_creg(new_target) - creg_map[target.name] = new_target - new_op.condition = (creg_map[target.name], value) - return new_op + def _map_classical_resource_with_import(self, resource, wire_map, creg_map): + """Map the classical ``resource`` (a bit or register) in its counterpart in ``self`` using + ``wire_map`` and ``creg_map`` as lookup caches. All single-bit conditions should have a + cache hit in the ``wire_map``, but registers may involve a full linear search the first time + they are encountered. ``creg_map`` is mutated by this function. ``wire_map`` is not; it is + an error if a wire is not in the map. + + This is different to the logic in ``_map_condition`` because it always succeeds; since the + mapping for all wires in the condition is assumed to exist, there can be no fragmented + registers. If there is no matching register (has the same bits in the same order) in + ``self``, a new register alias is added to represent the condition. This does not change + the bits available to ``self``, it just adds a new aliased grouping of them.""" + if isinstance(resource, Clbit): + return wire_map[resource] + if resource.name not in creg_map: + mapped_bits = [wire_map[bit] for bit in resource] + for our_creg in self.cregs.values(): + if mapped_bits == list(our_creg): + new_resource = our_creg + break + else: + new_resource = ClassicalRegister(bits=[wire_map[bit] for bit in resource]) + self.add_creg(new_resource) + creg_map[resource.name] = new_resource + return creg_map[resource.name] def compose(self, other, qubits=None, clbits=None, front=False, inplace=True): """Compose the ``other`` circuit onto the output of this circuit. @@ -908,7 +901,9 @@ def size(self, *, recurse: bool = False): """ length = len(self._multi_graph) - 2 * len(self._wires) if not recurse: - if any(x in self._op_names for x in ("for_loop", "while_loop", "if_else")): + if any( + x in self._op_names for x in ("for_loop", "while_loop", "if_else", "switch_case") + ): raise DAGCircuitError( "Size with control flow is ambiguous." " You may use `recurse=True` to get a result," @@ -924,7 +919,7 @@ def size(self, *, recurse: bool = False): inner = len(indexset) * circuit_to_dag(node.op.blocks[0]).size(recurse=True) elif isinstance(node.op, WhileLoopOp): inner = circuit_to_dag(node.op.blocks[0]).size(recurse=True) - elif isinstance(node.op, IfElseOp): + elif isinstance(node.op, (IfElseOp, SwitchCaseOp)): inner = sum(circuit_to_dag(block).size(recurse=True) for block in node.op.blocks) else: raise DAGCircuitError(f"unknown control-flow type: '{node.op.name}'") @@ -970,7 +965,9 @@ def weight_fn(_source, target, _edge): return node_lookup.get(target, 1) else: - if any(x in self._op_names for x in ("for_loop", "while_loop", "if_else")): + if any( + x in self._op_names for x in ("for_loop", "while_loop", "if_else", "switch_case") + ): raise DAGCircuitError( "Depth with control flow is ambiguous." " You may use `recurse=True` to get a result," @@ -1327,7 +1324,23 @@ def edge_weight_map(wire): for old_node_index, new_node_index in node_map.items(): # update node attributes old_node = in_dag._multi_graph[old_node_index] - m_op = self._map_condition_with_import(old_node.op, wire_map, creg_map) + if isinstance(old_node.op, SwitchCaseOp): + m_op = SwitchCaseOp( + self._map_classical_resource_with_import( + old_node.op.target, wire_map, creg_map + ), + old_node.op.cases_specifier(), + label=old_node.op.label, + ) + elif getattr(old_node.op, "condition", None) is not None: + cond_target, cond_value = old_node.op.condition + m_op = copy.copy(old_node.op) + m_op.condition = ( + self._map_classical_resource_with_import(cond_target, wire_map, creg_map), + cond_value, + ) + else: + m_op = old_node.op m_qargs = [wire_map[x] for x in old_node.qargs] m_cargs = [wire_map[x] for x in old_node.cargs] new_node = DAGOpNode(m_op, qargs=m_qargs, cargs=m_cargs) diff --git a/qiskit/transpiler/passes/routing/stochastic_swap.py b/qiskit/transpiler/passes/routing/stochastic_swap.py index 147dfc830b07..f2fdbfec5f76 100644 --- a/qiskit/transpiler/passes/routing/stochastic_swap.py +++ b/qiskit/transpiler/passes/routing/stochastic_swap.py @@ -25,7 +25,16 @@ from qiskit.circuit.library.standard_gates import SwapGate from qiskit.transpiler.layout import Layout from qiskit.transpiler.target import Target -from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp, Instruction +from qiskit.circuit import ( + Clbit, + IfElseOp, + WhileLoopOp, + ForLoopOp, + SwitchCaseOp, + ControlFlowOp, + Instruction, + CASE_DEFAULT, +) from qiskit._accelerate import stochastic_swap as stochastic_swap_rs from qiskit._accelerate import nlayout from qiskit.transpiler.passes.layout import disjoint_utils @@ -386,7 +395,7 @@ def _controlflow_layer_update(self, dagcircuit_output, layer_dag, current_layout """ node = layer_dag.op_nodes()[0] - if not isinstance(node.op, (IfElseOp, ForLoopOp, WhileLoopOp)): + if not isinstance(node.op, (IfElseOp, ForLoopOp, WhileLoopOp, SwitchCaseOp)): raise TranspilerError(f"unsupported control flow operation: {node}") # For each block, expand it up be the full width of the containing DAG so we can be certain # that it is routable, then route it within that. When we recombine later, we'll reduce all @@ -399,18 +408,15 @@ def _controlflow_layer_update(self, dagcircuit_output, layer_dag, current_layout block_layouts.append(inner_pass.property_set["final_layout"].copy()) # Determine what layout we need to go towards. For some blocks (such as `for`), we must - # guarantee that the final layout is the same as the initial or the loop won't work. For an - # `if` with an `else`, we don't need that as long as the two branches are the same. We have - # to be careful with `if` _without_ an else, though - the `if` needs to restore the layout - # in case it isn't taken; we can't have two different virtual layouts. - if not (isinstance(node.op, IfElseOp) and len(node.op.blocks) == 2): - final_layout = current_layout - else: + # guarantee that the final layout is the same as the initial or the loop won't work. + if _controlflow_exhaustive_acyclic(node.op): # We heuristically just choose to use the layout of whatever the deepest block is, to # avoid extending the total depth by too much. final_layout = max( zip(block_layouts, block_dags), key=lambda x: x[1].depth(recurse=True) )[0] + else: + final_layout = current_layout if self.fake_run: return final_layout @@ -458,6 +464,18 @@ def _recursive_pass(self, initial_layout): ) +def _controlflow_exhaustive_acyclic(operation: ControlFlowOp): + """Return True if the entire control-flow operation represents a block that is guaranteed to be + entered, and does not cycle back to the initial layout.""" + if isinstance(operation, IfElseOp): + return len(operation.blocks) == 2 + if isinstance(operation, SwitchCaseOp): + cases = operation.cases() + max_matches = 2 if isinstance(operation.target, Clbit) else 1 << len(operation.target) + return CASE_DEFAULT in cases or len(cases) == max_matches + return False + + def _dag_from_block(block, node, root_dag): """Get a :class:`DAGCircuit` that represents the :class:`.QuantumCircuit` ``block`` embedded within the ``root_dag`` for full-width routing purposes. This means that all the qubits are in diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 22eae3829040..7cd3221c84ae 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -52,7 +52,7 @@ from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout -_CONTROL_FLOW_OP_NAMES = {"for_loop", "if_else", "while_loop"} +_CONTROL_FLOW_OP_NAMES = {"for_loop", "if_else", "while_loop", "switch_case"} _ControlFlowState = collections.namedtuple("_ControlFlowState", ("working", "not_working")) diff --git a/releasenotes/notes/switch-case-9b6611d0603d36c0.yaml b/releasenotes/notes/switch-case-9b6611d0603d36c0.yaml index 28f91c5d32ac..250963303202 100644 --- a/releasenotes/notes/switch-case-9b6611d0603d36c0.yaml +++ b/releasenotes/notes/switch-case-9b6611d0603d36c0.yaml @@ -6,3 +6,37 @@ features: input (such as a classical register or bit) and executing the circuit that corresponds to the matching value. Multiple values can point to the same circuit, and :data:`.CASE_DEFAULT` can be used as an always-matching label. + + You can also use a builder interface, similar to the other control-flow constructs to build up + these switch statements:: + + from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister + + qreg = QuantumRegister(2) + creg = ClassicalRegister(2) + qc = QuantumCircuit(qreg, creg) + + qc.h([0, 1]) + qc.measure([0, 1], [0, 1]) + with qc.switch(creg) as case: + with case(0): # if the register is '00' + qc.z(0) + with case(1, 2): # if the register is '01' or '10' + qc.cx(0, 1) + with case(case.DEFAULT): # the default case + qc.h(0) + + The ``switch`` statement has support throughout the Qiskit compiler stack; you can + :func:`.transpile` circuits containing it (if the backend advertises its support for the + construct), and it will serialize to QPY. + + The ``switch`` statement is not currently a feature of OpenQASM 3, but `it is under active + design and consideration `__, which is + expected to be adopted in the near future. Qiskit Terra has experimental support for + exporting this statement to the OpenQASM 3 syntax proposed in the linked pull request, using + an experimental feature flag. To export a ``switch`` statement circuit (such as the one + created above) to OpenQASM 3 using this speculative support, do:: + + from qiskit import qasm3 + + qasm3.dumps(qc, experimental=qasm3.ExperimentalFeatures.SWITCH_CASE_V1) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index db2df0db1eaf..2b8ef68903b7 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -47,7 +47,7 @@ SXGate, HGate, ) -from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp +from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, SwitchCaseOp, ControlFlowOp from qiskit.circuit.measure import Measure from qiskit.circuit.delay import Delay from qiskit.test import QiskitTestCase @@ -1523,6 +1523,7 @@ def test_transpile_with_custom_control_flow_target(self, opt_level): target.add_instruction(ForLoopOp, name="for_loop") target.add_instruction(WhileLoopOp, name="while_loop") target.add_instruction(IfElseOp, name="if_else") + target.add_instruction(SwitchCaseOp, name="switch_case") circuit = QuantumCircuit(6, 1) circuit.h(0) @@ -1546,6 +1547,15 @@ def test_transpile_with_custom_control_flow_target(self, opt_level): circuit.cx(3, 4) circuit.cz(3, 5) circuit.append(CustomCX(), [4, 5], []) + with circuit.switch(circuit.cregs[0]) as case_: + with case_(0): + circuit.cx(0, 1) + circuit.cz(0, 2) + circuit.append(CustomCX(), [1, 2], []) + with case_(1): + circuit.cx(1, 2) + circuit.cz(1, 3) + circuit.append(CustomCX(), [2, 3], []) transpiled = transpile( circuit, optimization_level=opt_level, target=target, seed_transpiler=12434 ) @@ -1655,6 +1665,13 @@ def _control_flow_circuit(self): base.append(CustomCX(), [2, 4]) base.ry(a, 4) base.measure(4, 2) + with base.switch(base.cregs[0]) as case_: + with case_(0, 1): + base.cz(3, 5) + with case_(case_.DEFAULT): + base.cz(1, 4) + base.append(CustomCX(), [2, 4]) + base.append(CustomCX(), [3, 4]) return base @data(0, 1, 2, 3) @@ -1702,7 +1719,8 @@ def test_qpy_roundtrip_control_flow(self, optimization_level): transpiled = transpile( self._control_flow_circuit(), backend=backend, - basis_gates=backend.configuration().basis_gates + ["if_else", "for_loop", "while_loop"], + basis_gates=backend.configuration().basis_gates + + ["if_else", "for_loop", "while_loop", "switch_case"], optimization_level=optimization_level, seed_transpiler=2022_10_17, ) @@ -1722,6 +1740,7 @@ def test_qpy_roundtrip_control_flow_backendv2(self, optimization_level): 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._control_flow_circuit(), backend=backend, @@ -1758,6 +1777,7 @@ def test_qasm3_output_control_flow(self, optimization_level): 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._control_flow_circuit(), backend=backend, @@ -1767,7 +1787,10 @@ def test_qasm3_output_control_flow(self, optimization_level): # TODO: There's not a huge amount we can sensibly test for the output here until we can # round-trip the OpenQASM 3 back into a Terra circuit. Mostly we're concerned that the dump # itself doesn't throw an error, though. - self.assertIsInstance(qasm3.dumps(transpiled).strip(), str) + self.assertIsInstance( + qasm3.dumps(transpiled, experimental=qasm3.ExperimentalFeatures.SWITCH_CASE_V1).strip(), + str, + ) @data(0, 1, 2, 3) def test_transpile_target_no_measurement_error(self, opt_level): diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index ee4d0b58eba6..843e4a0d9e05 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -62,7 +62,14 @@ from qiskit.quantum_info.operators.channel import SuperOp from qiskit.extensions import Initialize, UnitaryGate from qiskit.extensions.quantum_initializer import DiagonalGate, UCGate -from qiskit.circuit.controlflow import IfElseOp, WhileLoopOp, ForLoopOp, ContinueLoopOp, BreakLoopOp +from qiskit.circuit.controlflow import ( + IfElseOp, + WhileLoopOp, + ForLoopOp, + ContinueLoopOp, + BreakLoopOp, + SwitchCaseOp, +) FAKE_PROVIDER_FOR_BACKEND_V2 = FakeProviderForBackendV2() FAKE_PROVIDER = FakeProvider() @@ -429,6 +436,7 @@ def __init__(self, num_ctrl_qubits, ctrl_state=None): "if_else": IfElseOp, "while_loop": WhileLoopOp, "for_loop": ForLoopOp, + "switch_case": SwitchCaseOp, "break_loop": BreakLoopOp, "continue_loop": ContinueLoopOp, "save_statevector": SaveStatevector, diff --git a/test/python/transpiler/test_gates_in_basis_pass.py b/test/python/transpiler/test_gates_in_basis_pass.py index bae16c921582..e181423a00ae 100644 --- a/test/python/transpiler/test_gates_in_basis_pass.py +++ b/test/python/transpiler/test_gates_in_basis_pass.py @@ -12,7 +12,7 @@ """Test GatesInBasis pass.""" -from qiskit.circuit import QuantumCircuit, ForLoopOp, IfElseOp, Clbit +from qiskit.circuit import QuantumCircuit, ForLoopOp, IfElseOp, SwitchCaseOp, Clbit from qiskit.circuit.library import HGate, CXGate, UGate, XGate, ZGate from qiskit.circuit.measure import Measure from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary @@ -247,6 +247,7 @@ def test_basis_gates_target(self): ForLoopOp((), None, QuantumCircuit(4)), CXGate(), IfElseOp((Clbit(), True), QuantumCircuit(2), QuantumCircuit(2)), + SwitchCaseOp(Clbit(), [(False, QuantumCircuit(2)), (True, QuantumCircuit(2))]), XGate(), ZGate(), ] diff --git a/test/python/transpiler/test_stochastic_swap.py b/test/python/transpiler/test_stochastic_swap.py index 6816bec649af..152889f9512a 100644 --- a/test/python/transpiler/test_stochastic_swap.py +++ b/test/python/transpiler/test_stochastic_swap.py @@ -23,11 +23,12 @@ from qiskit.converters import circuit_to_dag, dag_to_circuit from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit from qiskit.test import QiskitTestCase +from qiskit.test._canonical import canonicalize_control_flow from qiskit.transpiler.passes.utils import CheckMap from qiskit.circuit.random import random_circuit from qiskit.providers.fake_provider import FakeMumbai, FakeMumbaiV2 from qiskit.compiler.transpiler import transpile -from qiskit.circuit import ControlFlowOp, Clbit +from qiskit.circuit import ControlFlowOp, Clbit, CASE_DEFAULT @ddt @@ -995,6 +996,120 @@ def test_while_loop(self): expected.measure(qreg, creg) self.assertEqual(dag_to_circuit(cdag), expected) + def test_switch_single_case(self): + """Test routing of 'switch' with just a single case.""" + qreg = QuantumRegister(5, "q") + creg = ClassicalRegister(3, "c") + qc = QuantumCircuit(qreg, creg) + + case0 = QuantumCircuit(qreg[[0, 1, 2]], creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.cx(2, 0) + qc.switch(creg, [(0, case0)], qreg[[0, 1, 2]], creg) + + coupling = CouplingMap.from_line(len(qreg)) + pass_ = StochasticSwap(coupling, seed=58) + test = pass_(qc) + + check = CheckMap(coupling) + check(test) + self.assertTrue(check.property_set["is_swap_mapped"]) + + expected = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg[[0, 1, 2]], creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.swap(0, 1) + case0.cx(2, 1) + case0.swap(0, 1) + expected.switch(creg, [(0, case0)], qreg[[0, 1, 2]], creg[:]) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + + def test_switch_nonexhaustive(self): + """Test routing of 'switch' with several but nonexhaustive cases.""" + qreg = QuantumRegister(5, "q") + creg = ClassicalRegister(3, "c") + + qc = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg, creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.cx(2, 0) + case1 = QuantumCircuit(qreg, creg[:]) + case1.cx(1, 2) + case1.cx(2, 3) + case1.cx(3, 1) + case2 = QuantumCircuit(qreg, creg[:]) + case2.cx(2, 3) + case2.cx(3, 4) + case2.cx(4, 2) + qc.switch(creg, [(0, case0), ((1, 2), case1), (3, case2)], qreg, creg) + + coupling = CouplingMap.from_line(len(qreg)) + pass_ = StochasticSwap(coupling, seed=58) + test = pass_(qc) + + check = CheckMap(coupling) + check(test) + self.assertTrue(check.property_set["is_swap_mapped"]) + + expected = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg, creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.swap(0, 1) + case0.cx(2, 1) + case0.swap(0, 1) + case1 = QuantumCircuit(qreg, creg[:]) + case1.cx(1, 2) + case1.cx(2, 3) + case1.swap(1, 2) + case1.cx(3, 2) + case1.swap(1, 2) + case2 = QuantumCircuit(qreg, creg[:]) + case2.cx(2, 3) + case2.cx(3, 4) + case2.swap(3, 4) + case2.cx(3, 2) + case2.swap(3, 4) + expected.switch(creg, [(0, case0), ((1, 2), case1), (3, case2)], qreg, creg) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + + @data((0, 1, 2, 3), (CASE_DEFAULT,)) + def test_switch_exhaustive(self, labels): + """Test routing of 'switch' with exhaustive cases; we should not require restoring the + layout afterwards.""" + qreg = QuantumRegister(5, "q") + creg = ClassicalRegister(2, "c") + + qc = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg[[0, 1, 2]], creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.cx(2, 0) + qc.switch(creg, [(labels, case0)], qreg[[0, 1, 2]], creg) + + coupling = CouplingMap.from_line(len(qreg)) + pass_ = StochasticSwap(coupling, seed=58) + test = pass_(qc) + + check = CheckMap(coupling) + check(test) + self.assertTrue(check.property_set["is_swap_mapped"]) + + expected = QuantumCircuit(qreg, creg) + case0 = QuantumCircuit(qreg[[0, 1, 2]], creg[:]) + case0.cx(0, 1) + case0.cx(1, 2) + case0.swap(0, 1) + case0.cx(2, 1) + expected.switch(creg, [(labels, case0)], qreg[[0, 1, 2]], creg) + + self.assertEqual(canonicalize_control_flow(test), canonicalize_control_flow(expected)) + def test_nested_inner_cnot(self): """test swap in nested if else controlflow construct; swap in inner""" seed = 1 diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 3316ed93b1f7..9bcaabb5fae2 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -30,7 +30,7 @@ RZXGate, CZGate, ) -from qiskit.circuit import IfElseOp, ForLoopOp, WhileLoopOp +from qiskit.circuit import IfElseOp, ForLoopOp, WhileLoopOp, SwitchCaseOp from qiskit.circuit.measure import Measure from qiskit.circuit.parameter import Parameter from qiskit import pulse @@ -1320,6 +1320,7 @@ def setUp(self): self.target_global_gates_only.add_instruction(IfElseOp, name="if_else") self.target_global_gates_only.add_instruction(ForLoopOp, name="for_loop") self.target_global_gates_only.add_instruction(WhileLoopOp, name="while_loop") + self.target_global_gates_only.add_instruction(SwitchCaseOp, name="switch_case") self.ibm_target = Target() i_props = { (0,): InstructionProperties(duration=35.5e-9, error=0.000413), @@ -1375,6 +1376,7 @@ def setUp(self): self.ibm_target.add_instruction(IfElseOp, name="if_else") self.ibm_target.add_instruction(ForLoopOp, name="for_loop") self.ibm_target.add_instruction(WhileLoopOp, name="while_loop") + self.ibm_target.add_instruction(SwitchCaseOp, name="switch_case") self.aqt_target = Target(description="AQT Target") rx_props = { (0,): None, @@ -1442,6 +1444,7 @@ def setUp(self): self.aqt_target.add_instruction(IfElseOp, name="if_else") self.aqt_target.add_instruction(ForLoopOp, name="for_loop") self.aqt_target.add_instruction(WhileLoopOp, name="while_loop") + self.aqt_target.add_instruction(SwitchCaseOp, name="switch_case") def test_qargs(self): expected_ibm = { @@ -1510,19 +1513,42 @@ def test_qargs_for_operation_name(self): self.assertIsNone(self.target_global_gates_only.qargs_for_operation_name("cx")) self.assertIsNone(self.ibm_target.qargs_for_operation_name("if_else")) self.assertIsNone(self.aqt_target.qargs_for_operation_name("while_loop")) + self.assertIsNone(self.aqt_target.qargs_for_operation_name("switch_case")) def test_instruction_names(self): self.assertEqual( self.ibm_target.operation_names, - {"rz", "id", "sx", "x", "cx", "measure", "if_else", "while_loop", "for_loop"}, + { + "rz", + "id", + "sx", + "x", + "cx", + "measure", + "if_else", + "while_loop", + "for_loop", + "switch_case", + }, ) self.assertEqual( self.aqt_target.operation_names, - {"rz", "ry", "rx", "rxx", "r", "measure", "if_else", "while_loop", "for_loop"}, + { + "rz", + "ry", + "rx", + "rxx", + "r", + "measure", + "if_else", + "while_loop", + "for_loop", + "switch_case", + }, ) self.assertEqual( self.target_global_gates_only.operation_names, - {"u", "cx", "measure", "if_else", "while_loop", "for_loop"}, + {"u", "cx", "measure", "if_else", "while_loop", "for_loop", "switch_case"}, ) def test_operations_for_qargs(self): @@ -1535,6 +1561,7 @@ def test_operations_for_qargs(self): IfElseOp, ForLoopOp, WhileLoopOp, + SwitchCaseOp, ] res = self.ibm_target.operations_for_qargs((0,)) self.assertEqual(len(expected), len(res)) @@ -1545,6 +1572,7 @@ def test_operations_for_qargs(self): IfElseOp, ForLoopOp, WhileLoopOp, + SwitchCaseOp, ] res = self.ibm_target.operations_for_qargs((0, 1)) self.assertEqual(len(expected), len(res)) @@ -1559,12 +1587,13 @@ def test_operations_for_qargs(self): IfElseOp, ForLoopOp, WhileLoopOp, + SwitchCaseOp, ] res = self.aqt_target.operations_for_qargs((0,)) self.assertEqual(len(expected), len(res)) for x in expected: self.assertIn(x, res) - expected = [RXXGate(self.theta), IfElseOp, ForLoopOp, WhileLoopOp] + expected = [RXXGate(self.theta), IfElseOp, ForLoopOp, WhileLoopOp, SwitchCaseOp] res = self.aqt_target.operations_for_qargs((0, 1)) self.assertEqual(len(expected), len(res)) for x in expected: @@ -1580,6 +1609,7 @@ def test_operation_names_for_qargs(self): "if_else", "for_loop", "while_loop", + "switch_case", } self.assertEqual(expected, self.ibm_target.operation_names_for_qargs((0,))) expected = { @@ -1587,6 +1617,7 @@ def test_operation_names_for_qargs(self): "if_else", "for_loop", "while_loop", + "switch_case", } self.assertEqual(expected, self.ibm_target.operation_names_for_qargs((0, 1))) expected = { @@ -1598,9 +1629,10 @@ def test_operation_names_for_qargs(self): "if_else", "for_loop", "while_loop", + "switch_case", } self.assertEqual(self.aqt_target.operation_names_for_qargs((0,)), expected) - expected = {"rxx", "if_else", "for_loop", "while_loop"} + expected = {"rxx", "if_else", "for_loop", "while_loop", "switch_case"} self.assertEqual(self.aqt_target.operation_names_for_qargs((0, 1)), expected) def test_operations(self): @@ -1614,6 +1646,7 @@ def test_operations(self): WhileLoopOp, IfElseOp, ForLoopOp, + SwitchCaseOp, ] for gate in ibm_expected: self.assertIn(gate, self.ibm_target.operations) @@ -1626,6 +1659,7 @@ def test_operations(self): ForLoopOp, IfElseOp, WhileLoopOp, + SwitchCaseOp, ] for gate in aqt_expected: self.assertIn(gate, self.aqt_target.operations) @@ -1636,6 +1670,7 @@ def test_operations(self): ForLoopOp, WhileLoopOp, IfElseOp, + SwitchCaseOp, ] for gate in fake_expected: self.assertIn(gate, self.target_global_gates_only.operations) @@ -1684,6 +1719,7 @@ def test_instructions(self): (IfElseOp, None), (ForLoopOp, None), (WhileLoopOp, None), + (SwitchCaseOp, None), ] self.assertEqual(ibm_expected, self.ibm_target.instructions) ideal_sim_expected = [ @@ -1693,6 +1729,7 @@ def test_instructions(self): (IfElseOp, None), (ForLoopOp, None), (WhileLoopOp, None), + (SwitchCaseOp, None), ] self.assertEqual(ideal_sim_expected, self.target_global_gates_only.instructions) @@ -1706,6 +1743,9 @@ def test_instruction_supported(self): self.assertTrue( self.aqt_target.instruction_supported(operation_class=WhileLoopOp, qargs=(0, 1, 2, 3)) ) + self.assertTrue( + self.aqt_target.instruction_supported(operation_class=SwitchCaseOp, qargs=(0, 1, 2, 3)) + ) self.assertFalse( self.ibm_target.instruction_supported( operation_class=IfElseOp, qargs=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10) From a461a913c2e5bd51b307f38b89d2ab37117b7bf4 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 19 Apr 2023 14:10:47 +0100 Subject: [PATCH 052/172] Add note on support window for experimental OQ3 (#9990) --- qiskit/qasm3/__init__.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/qiskit/qasm3/__init__.py b/qiskit/qasm3/__init__.py index 352464cdf037..744a40f5dad3 100644 --- a/qiskit/qasm3/__init__.py +++ b/qiskit/qasm3/__init__.py @@ -79,6 +79,21 @@ qasm_string = qasm3.dumps(qc, experimental=qasm3.ExperimentalFeatures.SWITCH_CASE_V1) +.. note:: + + All features enabled by the experimental flags are naturally transient. If it becomes necessary + to remove flags, they will be subject to `the standard Qiskit deprecation policy + `__. We will leave these experimental + flags in place for as long as is reasonable. + + However, we cannot guarantee any support windows for *consumers* of OpenQASM 3 code generated + using these experimental flags, if the OpenQASM 3 language specification changes the proposal + that the flag is based on. It is possible that any tool you are using to consume OpenQASM 3 + code created using these flags may update or remove their support while Qiskit continues to + offer the flag. You should not rely on the resultant experimental OpenQASM 3 code for long-term + storage of programs. + + Importing from OpenQASM 3 ========================= From 33704ea61f6a3fa60612cbe4463894d2859fe9ec Mon Sep 17 00:00:00 2001 From: Diego Emilio Serrano <65074936+diemilio@users.noreply.github.com> Date: Wed, 19 Apr 2023 20:48:44 -0400 Subject: [PATCH 053/172] Fix circuit drawer for instructions with circuit parameters (#9942) * fix: modify get_param_str to prevent circuit drawer from displaying params of type QuantumCircuit * fix lint errors * test: add circuit drawer test for instructions with parameters of type QuantumCircuit * test: add circuit drawer tests for no parameters and params of type ndarray * fix lint errors * remove variable to fix lint error * add reno * update releasenotes/notes/fix-circuit-drawer-for-qc-params --- qiskit/visualization/circuit/_utils.py | 12 ++--- ...drawer-for-qc-params-e78c67310ae43ccf.yaml | 5 ++ .../visualization/test_circuit_text_drawer.py | 50 ++++++++++++++++++- 3 files changed, 60 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/fix-circuit-drawer-for-qc-params-e78c67310ae43ccf.yaml diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index 5242e0c48d46..4117b0e76177 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -25,10 +25,9 @@ Gate, Instruction, Measure, - ControlFlowOp, ) from qiskit.circuit.library import PauliEvolutionGate -from qiskit.circuit import ClassicalRegister +from qiskit.circuit import ClassicalRegister, QuantumCircuit from qiskit.circuit.tools import pi_check from qiskit.converters import circuit_to_dag from qiskit.utils import optionals as _optionals @@ -119,10 +118,11 @@ def get_gate_ctrl_text(op, drawer, style=None, calibrations=None): def get_param_str(op, drawer, ndigits=3): """Get the params as a string to add to the gate text display""" - if not hasattr(op, "params") or any(isinstance(param, np.ndarray) for param in op.params): - return "" - - if isinstance(op, ControlFlowOp): + if ( + not hasattr(op, "params") + or any(isinstance(param, np.ndarray) for param in op.params) + or any(isinstance(param, QuantumCircuit) for param in op.params) + ): return "" if isinstance(op, Delay): diff --git a/releasenotes/notes/fix-circuit-drawer-for-qc-params-e78c67310ae43ccf.yaml b/releasenotes/notes/fix-circuit-drawer-for-qc-params-e78c67310ae43ccf.yaml new file mode 100644 index 000000000000..778c42041643 --- /dev/null +++ b/releasenotes/notes/fix-circuit-drawer-for-qc-params-e78c67310ae43ccf.yaml @@ -0,0 +1,5 @@ +fixes: + - | + Fixes the circuit drawer from displaying parameters when they are of type :class:`.QuantumCircuit` + since this resulted in an illegible drawing. + \ No newline at end of file diff --git a/test/python/visualization/test_circuit_text_drawer.py b/test/python/visualization/test_circuit_text_drawer.py index 9a44a7ed2cd6..e5d4f543734e 100644 --- a/test/python/visualization/test_circuit_text_drawer.py +++ b/test/python/visualization/test_circuit_text_drawer.py @@ -22,7 +22,7 @@ import numpy from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile -from qiskit.circuit import Gate, Parameter, Qubit, Clbit +from qiskit.circuit import Gate, Parameter, Qubit, Clbit, Instruction from qiskit.quantum_info.operators import SuperOp from qiskit.quantum_info.random import random_unitary from qiskit.test import QiskitTestCase @@ -1839,6 +1839,21 @@ def test_control_gate_label_with_cond_2_low_cregbundle(self): class TestTextDrawerParams(QiskitTestCase): """Test drawing parameters.""" + def test_text_no_parameters(self): + """Test drawing with no parameters""" + expected = "\n".join( + [ + " ┌───┐", + "q: |0>┤ X ├", + " └───┘", + ] + ) + + qr = QuantumRegister(1, "q") + circuit = QuantumCircuit(qr) + circuit.x(0) + self.assertEqual(str(_text_circuit_drawer(circuit)), expected) + def test_text_parameters_mix(self): """cu3 drawing with parameters""" expected = "\n".join( @@ -1904,6 +1919,39 @@ def test_text_utf8(self): circuit.u(0, phi, lam, 0) self.assertEqual(circuit.draw(output="text").single_string(), expected) + def test_text_ndarray_parameters(self): + """Test that if params are type ndarray, params are not displayed.""" + # fmt: off + expected = "\n".join([" ┌─────────┐", + "q: |0>┤ Unitary ├", + " └─────────┘"]) + # fmt: on + qr = QuantumRegister(1, "q") + circuit = QuantumCircuit(qr) + circuit.unitary(numpy.array([[0, 1], [1, 0]]), 0) + self.assertEqual(str(_text_circuit_drawer(circuit)), expected) + + def test_text_qc_parameters(self): + """Test that if params are type QuantumCircuit, params are not displayed.""" + expected = "\n".join( + [ + " ┌───────┐", + "q_0: |0>┤0 ├", + " │ name │", + "q_1: |0>┤1 ├", + " └───────┘", + ] + ) + + my_qc_param = QuantumCircuit(2) + my_qc_param.h(0) + my_qc_param.cx(0, 1) + inst = Instruction("name", 2, 0, [my_qc_param]) + qr = QuantumRegister(2, "q") + circuit = QuantumCircuit(qr) + circuit.append(inst, [0, 1]) + self.assertEqual(str(_text_circuit_drawer(circuit)), expected) + class TestTextDrawerVerticalCompressionLow(QiskitTestCase): """Test vertical_compression='low'""" From 983790d70e6a99d812859c2314c206d6a9377558 Mon Sep 17 00:00:00 2001 From: Ikko Hamamura Date: Thu, 20 Apr 2023 12:46:18 +0900 Subject: [PATCH 054/172] Remove remove_final_measurment hack in BackendEstimator (#9915) * Remove remove_final_measurment hack * fix skip_transpilation case * fix uncovered case * fix lint * add a test * add HAS_AER * Update qiskit/primitives/backend_estimator.py Co-authored-by: Matthew Treinish * black * refactor (swap if-clauses) --------- Co-authored-by: Matthew Treinish --- qiskit/primitives/backend_estimator.py | 39 ++++++++++++------- .../primitives/test_backend_estimator.py | 36 ++++++++++++++++- 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index 329805bc319e..0e48c5080b45 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -186,30 +186,39 @@ def _transpile(self): self._transpiled_circuits = [] for common_circuit, diff_circuits in self.preprocessed_circuits: # 1. transpile a common circuit - common_circuit = common_circuit.copy() - num_qubits = common_circuit.num_qubits - common_circuit.measure_all() - if not self._skip_transpilation: - common_circuit = transpile( + if self._skip_transpilation: + transpiled_circuit = common_circuit.copy() + perm_pattern = list(range(common_circuit.num_qubits)) + else: + transpiled_circuit = transpile( common_circuit, self.backend, **self.transpile_options.__dict__ ) - bit_map = {bit: index for index, bit in enumerate(common_circuit.qubits)} - layout = [bit_map[qr[0]] for _, qr, _ in common_circuit[-num_qubits:]] - common_circuit.remove_final_measurements() + if transpiled_circuit.layout is not None: + layout = transpiled_circuit.layout + virtual_bit_map = layout.initial_layout.get_virtual_bits() + perm_pattern = [virtual_bit_map[v] for v in common_circuit.qubits] + if layout.final_layout is not None: + final_mapping = dict( + enumerate(layout.final_layout.get_virtual_bits().values()) + ) + perm_pattern = [final_mapping[i] for i in perm_pattern] + else: + perm_pattern = list(range(transpiled_circuit.num_qubits)) + # 2. transpile diff circuits transpile_opts = copy.copy(self.transpile_options) - transpile_opts.update_options(initial_layout=layout) + transpile_opts.update_options(initial_layout=perm_pattern) diff_circuits = transpile(diff_circuits, self.backend, **transpile_opts.__dict__) # 3. combine transpiled_circuits = [] for diff_circuit in diff_circuits: - transpiled_circuit = common_circuit.copy() + transpiled_circuit_copy = transpiled_circuit.copy() for creg in diff_circuit.cregs: - if creg not in transpiled_circuit.cregs: - transpiled_circuit.add_register(creg) - transpiled_circuit.compose(diff_circuit, inplace=True) - transpiled_circuit.metadata = diff_circuit.metadata - transpiled_circuits.append(transpiled_circuit) + if creg not in transpiled_circuit_copy.cregs: + transpiled_circuit_copy.add_register(creg) + transpiled_circuit_copy.compose(diff_circuit, inplace=True) + transpiled_circuit_copy.metadata = diff_circuit.metadata + transpiled_circuits.append(transpiled_circuit_copy) self._transpiled_circuits += transpiled_circuits def _call( diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index 4a2c09b860c0..6759fa491d28 100644 --- a/test/python/primitives/test_backend_estimator.py +++ b/test/python/primitives/test_backend_estimator.py @@ -13,9 +13,9 @@ """Tests for Estimator.""" import unittest -from unittest.mock import patch from test import combine from test.python.transpiler._dummy_passes import DummyTP +from unittest.mock import patch import numpy as np from ddt import ddt @@ -28,6 +28,7 @@ from qiskit.quantum_info import SparsePauliOp from qiskit.test import QiskitTestCase from qiskit.transpiler import PassManager +from qiskit.utils import optionals BACKENDS = [FakeNairobi(), FakeNairobiV2()] @@ -347,6 +348,39 @@ def test_bound_pass_manager(self): _ = estimator.run([qc, qc], [op, op]).result() self.assertTrue(mock_pass.call_count == 2) + @combine(backend=BACKENDS) + def test_layout(self, backend): + """Test layout for split transpilation.""" + with self.subTest("initial layout test"): + qc = QuantumCircuit(3) + qc.x(0) + qc.cx(0, 1) + qc.cx(0, 2) + op = SparsePauliOp("IZI") + backend.set_options(seed_simulator=15) + estimator = BackendEstimator(backend) + estimator.set_transpile_options(seed_transpiler=15) + value = estimator.run(qc, op, shots=10000).result().values[0] + if optionals.HAS_AER: + self.assertEqual(value, -0.916) + else: + self.assertEqual(value, -1) + + with self.subTest("final layout test"): + qc = QuantumCircuit(3) + qc.x(0) + qc.cx(0, 1) + qc.cx(0, 2) + op = SparsePauliOp("IZI") + backend.set_options(seed_simulator=15) + estimator = BackendEstimator(backend) + estimator.set_transpile_options(initial_layout=[0, 1, 2], seed_transpiler=15) + value = estimator.run(qc, op, shots=10000).result().values[0] + if optionals.HAS_AER: + self.assertEqual(value, -0.8902) + else: + self.assertEqual(value, -1) + if __name__ == "__main__": unittest.main() From 1203a3b18551f06325354208274e61d02d8bd584 Mon Sep 17 00:00:00 2001 From: Kento Ueda <38037695+to24toro@users.noreply.github.com> Date: Thu, 20 Apr 2023 19:04:44 +0900 Subject: [PATCH 055/172] Bug fix macros.measure with backendv2 (#9987) * create measuregrouping class * add meas_map.setter in MeasureGrouping class * macros.measure * get_qubit_groups * generate_schedule * target.add_measuregrouping in backend_compat * target.add_measuregrouping in backend_converter * reformat and add docs * on the way of working on generate_schedule_in_measure * split measure into measure_v1 and measure_v2 macros.py target delete raise statement in measure_v2 modify instructions.Acquire to Acquire * macros.py * test_measuregrouping * bug fix schedule with backendV2 for 0.25.0 * modify comments * fix name of schedule in test_macros * delete meas_map as a Target attribute * minor changes in macros.py * add test to test_macros.py * make schedule_remapping_memory_slot private * delete since field from deprecate_arg * delete deprecate depcorator * black macros.py * revert about target * modify implementation of qubit_mem_slots * change the definition of meas_group_set * black macros.py * fix meas_group_set * fix qubit_mem_slots * reno * modify unassigned_qubit_indices * remove list() from unassigned_qubit_indices and unassigned_reg_indices --- qiskit/pulse/macros.py | 171 +++++++++++++++++- ...asure-with-backendV2-4354f00ab4f1cd3e.yaml | 5 + test/python/pulse/test_macros.py | 76 +++++++- 3 files changed, 242 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/fix-macros-measure-with-backendV2-4354f00ab4f1cd3e.yaml diff --git a/qiskit/pulse/macros.py b/qiskit/pulse/macros.py index 81e32e4a8b5b..d4ba4768ef57 100644 --- a/qiskit/pulse/macros.py +++ b/qiskit/pulse/macros.py @@ -11,14 +11,19 @@ # that they have been altered from the originals. """Module for common pulse programming macros.""" +from __future__ import annotations -from typing import Dict, List, Optional, Union +from typing import Dict, List, Optional, Union, TYPE_CHECKING from qiskit.pulse import channels, exceptions, instructions, utils from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.pulse.schedule import Schedule +if TYPE_CHECKING: + from qiskit.transpiler import Target + + def measure( qubits: List[int], backend=None, @@ -30,6 +35,13 @@ def measure( """Return a schedule which measures the requested qubits according to the given instruction mapping and measure map, or by using the defaults provided by the backend. + .. note:: + This function internally dispatches schedule generation logic depending on input backend model. + For the :class:`.BackendV1`, it considers conventional :class:`.InstructionScheduleMap` + and utilizes the backend calibration defined for a group of qubits in the `meas_map`. + For the :class:`.BackendV2`, it assembles calibrations of single qubit measurement + defined in the backend target to build a composite measurement schedule for `qubits`. + By default, the measurement results for each qubit are trivially mapped to the qubit index. This behavior is overridden by qubit_mem_slots. For instance, to measure qubit 0 into MemorySlot(1), qubit_mem_slots can be provided as {0: 1}. @@ -47,18 +59,67 @@ def measure( Returns: A measurement schedule corresponding to the inputs provided. + """ + + # backend is V2. + if hasattr(backend, "target"): + try: + meas_map = backend.configuration().meas_map + except AttributeError: + # TODO add meas_map to Target in 0.25 + meas_map = [list(range(backend.num_qubits))] + + return _measure_v2( + qubits=qubits, + target=backend.target, + meas_map=meas_map, + qubit_mem_slots=qubit_mem_slots or dict(zip(qubits, range(len(qubits)))), + measure_name=measure_name, + ) + # backend is V1 or backend is None. + else: + try: + return _measure_v1( + qubits=qubits, + inst_map=inst_map or backend.defaults().instruction_schedule_map, + meas_map=meas_map or backend.configuration().meas_map, + qubit_mem_slots=qubit_mem_slots, + measure_name=measure_name, + ) + except AttributeError as ex: + raise exceptions.PulseError( + "inst_map or meas_map, and backend cannot be None simultaneously" + ) from ex + +def _measure_v1( + qubits: List[int], + inst_map: InstructionScheduleMap, + meas_map: Union[List[List[int]], Dict[int, List[int]]], + qubit_mem_slots: Optional[Dict[int, int]] = None, + measure_name: str = "measure", +) -> Schedule: + """Return a schedule which measures the requested qubits according to the given + instruction mapping and measure map, or by using the defaults provided by the backendV1. + + Args: + qubits: List of qubits to be measured. + backend (Union[Backend, BaseBackend]): A backend instance, which contains + hardware-specific data required for scheduling. + inst_map: Mapping of circuit operations to pulse schedules. If None, defaults to the + ``instruction_schedule_map`` of ``backend``. + meas_map: List of sets of qubits that must be measured together. If None, defaults to + the ``meas_map`` of ``backend``. + qubit_mem_slots: Mapping of measured qubit index to classical bit index. + measure_name: Name of the measurement schedule. + Returns: + A measurement schedule corresponding to the inputs provided. Raises: PulseError: If both ``inst_map`` or ``meas_map``, and ``backend`` is None. """ + schedule = Schedule(name=f"Default measurement schedule for qubits {qubits}") - try: - inst_map = inst_map or backend.defaults().instruction_schedule_map - meas_map = meas_map or backend.configuration().meas_map - except AttributeError as ex: - raise exceptions.PulseError( - "inst_map or meas_map, and backend cannot be None simultaneously" - ) from ex + if isinstance(meas_map, list): meas_map = utils.format_meas_map(meas_map) @@ -92,6 +153,68 @@ def measure( return schedule +def _measure_v2( + qubits: List[int], + target: Target, + meas_map: Union[List[List[int]], Dict[int, List[int]]], + qubit_mem_slots: Dict[int, int], + measure_name: str = "measure", +) -> Schedule: + """Return a schedule which measures the requested qubits according to the given + target and measure map, or by using the defaults provided by the backendV2. + + Args: + qubits: List of qubits to be measured. + target: The :class:`~.Target` representing the target backend. + meas_map: List of sets of qubits that must be measured together. + qubit_mem_slots: Mapping of measured qubit index to classical bit index. + measure_name: Name of the measurement schedule. + + Returns: + A measurement schedule corresponding to the inputs provided. + """ + schedule = Schedule(name=f"Default measurement schedule for qubits {qubits}") + + if isinstance(meas_map, list): + meas_map = utils.format_meas_map(meas_map) + meas_group = set() + for qubit in qubits: + meas_group |= set(meas_map[qubit]) + meas_group = sorted(list(meas_group)) + + meas_group_set = set(range(max(meas_group) + 1)) + unassigned_qubit_indices = sorted(set(meas_group) - qubit_mem_slots.keys()) + unassigned_reg_indices = sorted(meas_group_set - set(qubit_mem_slots.values()), reverse=True) + if set(qubit_mem_slots.values()).issubset(meas_group_set): + for qubit in unassigned_qubit_indices: + qubit_mem_slots[qubit] = unassigned_reg_indices.pop() + + for measure_qubit in meas_group: + try: + if measure_qubit in qubits: + default_sched = target.get_calibration(measure_name, (measure_qubit,)).filter( + channels=[ + channels.MeasureChannel(measure_qubit), + channels.AcquireChannel(measure_qubit), + ] + ) + else: + default_sched = target.get_calibration(measure_name, (measure_qubit,)).filter( + channels=[ + channels.AcquireChannel(measure_qubit), + ] + ) + except KeyError as ex: + raise exceptions.PulseError( + "We could not find a default measurement schedule called '{}'. " + "Please provide another name using the 'measure_name' keyword " + "argument. For assistance, the instructions which are defined are: " + "{}".format(measure_name, target.instructions) + ) from ex + schedule += _schedule_remapping_memory_slot(default_sched, qubit_mem_slots) + return schedule + + def measure_all(backend) -> Schedule: """ Return a Schedule which measures all qubits of the given backend. @@ -104,3 +227,35 @@ def measure_all(backend) -> Schedule: A schedule corresponding to the inputs provided. """ return measure(qubits=list(range(backend.configuration().n_qubits)), backend=backend) + + +def _schedule_remapping_memory_slot( + schedule: Schedule, qubit_mem_slots: Dict[int, int] +) -> Schedule: + """ + A helper function to overwrite MemorySlot index of :class:`.Acquire` instruction. + + Args: + schedule: A measurement schedule. + qubit_mem_slots: Mapping of measured qubit index to classical bit index. + + Returns: + A measurement schedule with new memory slot index. + """ + new_schedule = Schedule() + for t0, inst in schedule.instructions: + if isinstance(inst, instructions.Acquire): + qubit_index = inst.channel.index + reg_index = qubit_mem_slots.get(qubit_index, qubit_index) + new_schedule.insert( + t0, + instructions.Acquire( + inst.duration, + channels.AcquireChannel(qubit_index), + mem_slot=channels.MemorySlot(reg_index), + ), + inplace=True, + ) + else: + new_schedule.insert(t0, inst, inplace=True) + return new_schedule diff --git a/releasenotes/notes/fix-macros-measure-with-backendV2-4354f00ab4f1cd3e.yaml b/releasenotes/notes/fix-macros-measure-with-backendV2-4354f00ab4f1cd3e.yaml new file mode 100644 index 000000000000..11d2bafe22a9 --- /dev/null +++ b/releasenotes/notes/fix-macros-measure-with-backendV2-4354f00ab4f1cd3e.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed failure in using :func:`~qiskit.pulse.macros.measure` + with :class:`.BackendV2` backends. diff --git a/test/python/pulse/test_macros.py b/test/python/pulse/test_macros.py index 3fa7553b3a74..a3869fd11b7d 100644 --- a/test/python/pulse/test_macros.py +++ b/test/python/pulse/test_macros.py @@ -24,7 +24,7 @@ ) from qiskit.pulse import macros from qiskit.pulse.exceptions import PulseError -from qiskit.providers.fake_provider import FakeOpenPulse2Q +from qiskit.providers.fake_provider import FakeOpenPulse2Q, FakeHanoiV2 from qiskit.test import QiskitTestCase @@ -34,6 +34,7 @@ class TestMeasure(QiskitTestCase): def setUp(self): super().setUp() self.backend = FakeOpenPulse2Q() + self.backend_v2 = FakeHanoiV2() self.inst_map = self.backend.defaults().instruction_schedule_map def test_measure(self): @@ -43,7 +44,6 @@ def test_measure(self): self.inst_map.get("measure", [0, 1]).filter(channels=[MeasureChannel(0)]), Acquire(10, AcquireChannel(0), MemorySlot(0)), ) - self.assertEqual(sched.instructions, expected.instructions) def test_measure_sched_with_qubit_mem_slots(self): @@ -91,6 +91,78 @@ def test_fail_measure(self): with self.assertRaises(PulseError): macros.measure(qubits=[0], inst_map=self.inst_map) + def test_measure_v2(self): + """Test macro - measure with backendV2.""" + sched = macros.measure(qubits=[0], backend=self.backend_v2) + expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( + channels=[ + MeasureChannel(0), + ] + ) + measure_duration = expected.filter(instruction_types=[Play]).duration + for qubit in range(self.backend_v2.num_qubits): + expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(qubit)) + self.assertEqual(sched.instructions, expected.instructions) + + def test_measure_v2_sched_with_qubit_mem_slots(self): + """Test measure with backendV2 and custom qubit_mem_slots.""" + sched = macros.measure(qubits=[0], backend=self.backend_v2, qubit_mem_slots={0: 2}) + expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( + channels=[ + MeasureChannel(0), + ] + ) + measure_duration = expected.filter(instruction_types=[Play]).duration + for qubit in range(self.backend_v2.num_qubits): + if qubit == 0: + expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(2)) + elif qubit == 1: + expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(0)) + elif qubit == 2: + expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(1)) + else: + expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(qubit)) + self.assertEqual(sched.instructions, expected.instructions) + + def test_measure_v2_sched_with_meas_map(self): + """Test measure with backendV2 custom meas_map as list and dict.""" + sched_with_meas_map_list = macros.measure( + qubits=[0], backend=self.backend_v2, meas_map=[[0, 1]] + ) + sched_with_meas_map_dict = macros.measure( + qubits=[0], backend=self.backend_v2, meas_map={0: [0, 1], 1: [0, 1]} + ) + expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( + channels=[ + MeasureChannel(0), + ] + ) + measure_duration = expected.filter(instruction_types=[Play]).duration + for qubit in range(self.backend_v2.num_qubits): + expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(qubit)) + self.assertEqual(sched_with_meas_map_list.instructions, expected.instructions) + self.assertEqual(sched_with_meas_map_dict.instructions, expected.instructions) + + def test_multiple_measure_v2(self): + """Test macro - multiple qubit measure with backendV2.""" + sched = macros.measure(qubits=[0, 1], backend=self.backend_v2) + expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( + channels=[ + MeasureChannel(0), + ] + ) + expected += self.backend_v2.target.get_calibration("measure", (1,)).filter( + channels=[ + MeasureChannel(1), + ] + ) + measure_duration = expected.filter(instruction_types=[Play]).duration + expected += Acquire(measure_duration, AcquireChannel(0), MemorySlot(0)) + expected += Acquire(measure_duration, AcquireChannel(1), MemorySlot(1)) + for qubit in range(2, self.backend_v2.num_qubits): + expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(qubit)) + self.assertEqual(sched.instructions, expected.instructions) + class TestMeasureAll(QiskitTestCase): """Pulse measure all macro.""" From 601bd9a132ee96edf2ac6b0dc164a0d2a2f61861 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Thu, 20 Apr 2023 17:15:11 +0300 Subject: [PATCH 056/172] Operator apply permutation (#9403) * adding apply_permutation method to Operator class * modify from_circuit to use apply_permutation * declaration fix * reimplementing based on review suggestions, but missing inversion * fixing inversion * fixing front vs back * adding test for reverse_qargs * Fixing operator dimensions and adding tests * Fixing operator dimensions and adding tests Co-authored-by: Gadi Aleksandrowicz * pylint * adding tests for one-sided permutations on heterogenous qudit spaces * improving tests following review * using ddt * applying suggestions from review * applying suggestions from review --------- Co-authored-by: Gadi Aleksandrowicz --- qiskit/quantum_info/operators/operator.py | 87 ++++++++++- ...or-apply-permutation-c113c05513cb7881.yaml | 19 +++ .../quantum_info/operators/test_operator.py | 136 ++++++++++++++++++ 3 files changed, 235 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/operator-apply-permutation-c113c05513cb7881.yaml diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index e7d3b962d326..784f46d8d818 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -198,6 +198,85 @@ def from_label(cls, label): op = op.compose(label_mats[char], qargs=[qubit]) return op + def apply_permutation(self, perm: list, front: bool = False): + """Modifies operator's data by composing it with a permutation. + + Args: + perm (list): permutation pattern, describing which qubits + occupy the positions 0, 1, 2, etc. after applying the permutation. + front (bool): When set to ``True`` the permutation is applied before the + operator, when set to ``False`` the permutation is applied after the + operator. + Returns: + Operator: The modified operator. + + Raises: + QiskitError: if the size of the permutation pattern does not match the + dimensions of the operator. + """ + + # See https://github.com/Qiskit/qiskit-terra/pull/9403 for the math + # behind the following code. + + inv_perm = np.argsort(perm) + raw_shape_l = self._op_shape.dims_l() + n_dims_l = len(raw_shape_l) + raw_shape_r = self._op_shape.dims_r() + n_dims_r = len(raw_shape_r) + + if front: + # The permutation is applied first, the operator is applied after; + # however, in terms of matrices, we compute [O][P]. + + if len(perm) != n_dims_r: + raise QiskitError( + "The size of the permutation pattern does not match dimensions of the operator." + ) + + # shape: original on left, permuted on right + shape_l = self._op_shape.dims_l() + shape_r = tuple(raw_shape_r[n_dims_r - n - 1] for n in reversed(perm)) + + # axes order: id on left, inv-permuted on right + axes_l = tuple(x for x in range(self._op_shape._num_qargs_l)) + axes_r = tuple(self._op_shape._num_qargs_l + x for x in (np.argsort(perm[::-1]))[::-1]) + + # updated shape: original on left, permuted on right + new_shape_l = self._op_shape.dims_l() + new_shape_r = tuple(raw_shape_r[n_dims_r - n - 1] for n in reversed(inv_perm)) + + else: + # The operator is applied first, the permutation is applied after; + # however, in terms of matrices, we compute [P][O]. + + if len(perm) != n_dims_l: + raise QiskitError( + "The size of the permutation pattern does not match dimensions of the operator." + ) + + # shape: inv-permuted on left, original on right + shape_l = tuple(raw_shape_l[n_dims_l - n - 1] for n in reversed(inv_perm)) + shape_r = self._op_shape.dims_r() + + # axes order: permuted on left, id on right + axes_l = tuple((np.argsort(inv_perm[::-1]))[::-1]) + axes_r = tuple( + self._op_shape._num_qargs_l + x for x in range(self._op_shape._num_qargs_r) + ) + + # updated shape: permuted on left, original on right + new_shape_l = tuple(raw_shape_l[n_dims_l - n - 1] for n in reversed(perm)) + new_shape_r = self._op_shape.dims_r() + + # Computing the new operator + split_shape = shape_l + shape_r + axes_order = axes_l + axes_r + new_mat = ( + self._data.reshape(split_shape).transpose(axes_order).reshape(self._op_shape.shape) + ) + new_op = Operator(new_mat, input_dims=new_shape_r, output_dims=new_shape_l) + return new_op + @classmethod def from_circuit(cls, circuit, ignore_set_layout=False, layout=None, final_layout=None): """Create a new Operator object from a :class:`.QuantumCircuit` @@ -261,14 +340,8 @@ def from_circuit(cls, circuit, ignore_set_layout=False, layout=None, final_layou op._append_instruction(instruction, qargs=qargs) # If final layout is set permute output indices based on layout if final_layout is not None: - # TODO: Do this without the intermediate Permutation object by just - # operating directly on the array directly - from qiskit.circuit.library import Permutation # pylint: disable=cyclic-import - - final_physical_to_virtual = final_layout.get_physical_bits() perm_pattern = [final_layout._v2p[v] for v in circuit.qubits] - perm_op = Operator(Permutation(len(final_physical_to_virtual), perm_pattern)) - op &= perm_op + op = op.apply_permutation(perm_pattern, front=False) return op def is_unitary(self, atol=None, rtol=None): diff --git a/releasenotes/notes/operator-apply-permutation-c113c05513cb7881.yaml b/releasenotes/notes/operator-apply-permutation-c113c05513cb7881.yaml new file mode 100644 index 000000000000..430e54ae5ce0 --- /dev/null +++ b/releasenotes/notes/operator-apply-permutation-c113c05513cb7881.yaml @@ -0,0 +1,19 @@ +--- +features: + - | + Added a method :meth:`qiskit.quantum_info.Operator.apply_permutation` that + pre-composes or post-composes an Operator with a Permutation. This method + works for general qudits. + + Here is an example to calculate :math:`P^\dagger.O.P` which reorders Operator's bits:: + + import numpy as np + from qiskit.quantum_info.operators import Operator + + op = Operator(np.array(range(576)).reshape((24, 24)), input_dims=(2, 3, 4), output_dims=(2, 3, 4)) + perm = [1, 2, 0] + inv_perm = [2, 0, 1] + conjugate_op = op.apply_permutation(inv_perm, front=True).apply_permutation(perm, front=False) + + The conjugate operator has dimensions `(4, 2, 3) x (4, 2, 3)`, which is consistent with permutation + moving qutrit to position 0, qubit to position 1, and the 4-qudit to position 2. diff --git a/test/python/quantum_info/operators/test_operator.py b/test/python/quantum_info/operators/test_operator.py index 9973f5320ac4..fbc3afee7764 100644 --- a/test/python/quantum_info/operators/test_operator.py +++ b/test/python/quantum_info/operators/test_operator.py @@ -17,7 +17,9 @@ import unittest import logging import copy +from test import combine import numpy as np +from ddt import ddt from numpy.testing import assert_allclose import scipy.linalg as la @@ -30,6 +32,7 @@ from qiskit.quantum_info.operators.predicates import matrix_equal from qiskit.compiler.transpiler import transpile from qiskit.circuit import Qubit +from qiskit.circuit.library import Permutation, PermutationGate logger = logging.getLogger(__name__) @@ -91,6 +94,7 @@ def simple_circuit_with_measure(self): return circ +@ddt class TestOperator(OperatorTestCase): """Tests for Operator linear operator class.""" @@ -1050,6 +1054,138 @@ def test_from_circuit_mixed_reg_loose_bits_transpiled(self): result = Operator.from_circuit(tqc) self.assertTrue(Operator(circuit).equiv(result)) + def test_apply_permutation_back(self): + """Test applying permutation to the operator, + where the operator is applied first and the permutation second.""" + op = Operator(self.rand_matrix(64, 64)) + pattern = [1, 2, 0, 3, 5, 4] + + # Consider several methods of computing this operator and show + # they all lead to the same result. + + # Compose the operator with the operator constructed from the + # permutation circuit. + op2 = op.copy() + perm_op = Operator(Permutation(6, pattern)) + op2 &= perm_op + + # Compose the operator with the operator constructed from the + # permutation gate. + op3 = op.copy() + perm_op = Operator(PermutationGate(pattern)) + op3 &= perm_op + + # Modify the operator using apply_permutation method. + op4 = op.copy() + op4 = op4.apply_permutation(pattern, front=False) + + self.assertEqual(op2, op3) + self.assertEqual(op2, op4) + + def test_apply_permutation_front(self): + """Test applying permutation to the operator, + where the permutation is applied first and the operator second""" + op = Operator(self.rand_matrix(64, 64)) + pattern = [1, 2, 0, 3, 5, 4] + + # Consider several methods of computing this operator and show + # they all lead to the same result. + + # Compose the operator with the operator constructed from the + # permutation circuit. + op2 = op.copy() + perm_op = Operator(Permutation(6, pattern)) + op2 = perm_op & op2 + + # Compose the operator with the operator constructed from the + # permutation gate. + op3 = op.copy() + perm_op = Operator(PermutationGate(pattern)) + op3 = perm_op & op3 + + # Modify the operator using apply_permutation method. + op4 = op.copy() + op4 = op4.apply_permutation(pattern, front=True) + + self.assertEqual(op2, op3) + self.assertEqual(op2, op4) + + def test_apply_permutation_qudits_back(self): + """Test applying permutation to the operator with heterogeneous qudit spaces, + where the operator O is applied first and the permutation P second. + The matrix of the resulting operator is the product [P][O] and + corresponds to suitably permuting the rows of O's matrix. + """ + mat = np.array(range(6 * 6)).reshape((6, 6)) + op = Operator(mat, input_dims=(2, 3), output_dims=(2, 3)) + perm = [1, 0] + actual = op.apply_permutation(perm, front=False) + + # Rows of mat are ordered to 00, 01, 02, 10, 11, 12; + # perm maps these to 00, 10, 20, 01, 11, 21, + # while the default ordering is 00, 01, 10, 11, 20, 21. + permuted_mat = mat.copy()[[0, 2, 4, 1, 3, 5]] + expected = Operator(permuted_mat, input_dims=(2, 3), output_dims=(3, 2)) + self.assertEqual(actual, expected) + + def test_apply_permutation_qudits_front(self): + """Test applying permutation to the operator with heterogeneous qudit spaces, + where the permutation P is applied first and the operator O is applied second. + The matrix of the resulting operator is the product [O][P] and + corresponds to suitably permuting the columns of O's matrix. + """ + mat = np.array(range(6 * 6)).reshape((6, 6)) + op = Operator(mat, input_dims=(2, 3), output_dims=(2, 3)) + perm = [1, 0] + actual = op.apply_permutation(perm, front=True) + + # Columns of mat are ordered to 00, 01, 02, 10, 11, 12; + # perm maps these to 00, 10, 20, 01, 11, 21, + # while the default ordering is 00, 01, 10, 11, 20, 21. + permuted_mat = mat.copy()[:, [0, 2, 4, 1, 3, 5]] + expected = Operator(permuted_mat, input_dims=(3, 2), output_dims=(2, 3)) + self.assertEqual(actual, expected) + + @combine( + dims=((2, 3, 4, 5), (5, 2, 4, 3), (3, 5, 2, 4), (5, 3, 4, 2), (4, 5, 2, 3), (4, 3, 2, 5)) + ) + def test_reverse_qargs_as_apply_permutation(self, dims): + """Test reversing qargs by pre- and post-composing with reversal + permutation. + """ + perm = [3, 2, 1, 0] + op = Operator( + np.array(range(120 * 120)).reshape((120, 120)), input_dims=dims, output_dims=dims + ) + op2 = op.reverse_qargs() + op3 = op.apply_permutation(perm, front=True).apply_permutation(perm, front=False) + self.assertEqual(op2, op3) + + def test_apply_permutation_exceptions(self): + """Checks that applying permutation raises an error when dimensions do not match.""" + op = Operator( + np.array(range(24 * 30)).reshape((24, 30)), input_dims=(6, 5), output_dims=(2, 3, 4) + ) + + with self.assertRaises(QiskitError): + op.apply_permutation([1, 0], front=False) + with self.assertRaises(QiskitError): + op.apply_permutation([2, 1, 0], front=True) + + def test_apply_permutation_dimensions(self): + """Checks the dimensions of the operator after applying permutation.""" + op = Operator( + np.array(range(24 * 30)).reshape((24, 30)), input_dims=(6, 5), output_dims=(2, 3, 4) + ) + op2 = op.apply_permutation([1, 2, 0], front=False) + self.assertEqual(op2.output_dims(), (4, 2, 3)) + + op = Operator( + np.array(range(24 * 30)).reshape((30, 24)), input_dims=(2, 3, 4), output_dims=(6, 5) + ) + op2 = op.apply_permutation([2, 0, 1], front=True) + self.assertEqual(op2.input_dims(), (4, 2, 3)) + if __name__ == "__main__": unittest.main() From b0db53c0931900aedb7563bc55a8108e405dce17 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 20 Apr 2023 13:48:02 -0400 Subject: [PATCH 057/172] Don't run routing in SabreLayout with split components (#10000) * Don't run routing in SabreLayout with split components This commit fixes an issue in the `SabreLayout` pass when run on backends with a disjoint connectivity graph. The `SabreLayout` pass internally runs routing as part of its operation and as a performance efficiency we use that routing output by default. However, in the case of a disjoint coupling map we are splitting the circuit up into different subcircuits to map that to the connected components on the backend. Doing final routing as part of that split context is no sound because depsite the quantum operations being isolated to each component, other operations could have a dependency between the components. This was previously discovered during the development of #9802 to be the case with classical data/bits, but since merging that it also has come up with barriers. To prevent any issues in the future this commit changes the SabreLayout pass to never use the routing output during the layout search when there is >1 connected component because we *need* to rerun routing on the full circuit after applying the layout. Fixes #9995 * Remove unused shared_clbits logic * Remove swap and routing assertions from disjoint sabre layout tests * Fix lint --- .../transpiler/passes/layout/sabre_layout.py | 18 ++---- test/python/compiler/test_transpiler.py | 57 ++++++++++++++++++- test/python/transpiler/test_sabre_layout.py | 44 ++------------ 3 files changed, 65 insertions(+), 54 deletions(-) diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index e4ad778e93ed..c1724d0e3ad5 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -232,8 +232,6 @@ def run(self, dag): ) initial_layout_dict = {} final_layout_dict = {} - shared_clbits = False - seen_clbits = set() for ( layout_dict, final_dict, @@ -244,20 +242,14 @@ def run(self, dag): ) in layout_components: initial_layout_dict.update({k: component_map[v] for k, v in layout_dict.items()}) final_layout_dict.update({component_map[k]: component_map[v] for k, v in final_dict}) - if not shared_clbits: - for clbit in local_dag.clbits: - if clbit in seen_clbits: - shared_clbits = True - break - seen_clbits.add(clbit) self.property_set["layout"] = Layout(initial_layout_dict) # If skip_routing is set then return the layout in the property set # and throwaway the extra work we did to compute the swap map. - # We also skip routing here if the input circuit is split over multiple - # connected components and there is a shared clbit between any - # components. We can only reliably route the full dag if there is any - # shared classical data. - if self.skip_routing or shared_clbits: + # We also skip routing here if there is more than one connected + # component we ran layout on. We can only reliably route the full dag + # in this case if there is any dependency between the components + # (typically shared classical data or barriers). + if self.skip_routing or len(layout_components) > 1: return dag # After this point the pass is no longer an analysis pass and the # output circuit returned is transformed with the layout applied diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 2b8ef68903b7..e29f79d2083f 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2274,7 +2274,7 @@ def _visit_block(circuit, qubit_mapping=None): ) @slow_test - @data(1, 2, 3) + @data(2, 3) def test_six_component_circuit(self, opt_level): """Test input circuit with more than 1 component per backend component.""" qc = QuantumCircuit(42) @@ -2329,6 +2329,61 @@ def test_six_component_circuit(self, opt_level): continue self.assertIn(qubits, self.backend.target[op_name]) + def test_six_component_circuit_level_1(self): + """Test input circuit with more than 1 component per backend component.""" + opt_level = 1 + qc = QuantumCircuit(42) + qc.h(0) + qc.h(10) + qc.h(20) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(0, 3) + qc.cx(0, 4) + qc.cx(0, 5) + qc.cx(0, 6) + qc.cx(0, 7) + qc.cx(0, 8) + qc.cx(0, 9) + qc.ecr(10, 11) + qc.ecr(10, 12) + qc.ecr(10, 13) + qc.ecr(10, 14) + qc.ecr(10, 15) + qc.ecr(10, 16) + qc.ecr(10, 17) + qc.ecr(10, 18) + qc.ecr(10, 19) + qc.cy(20, 21) + qc.cy(20, 22) + qc.cy(20, 23) + qc.cy(20, 24) + qc.cy(20, 25) + qc.cy(20, 26) + qc.cy(20, 27) + qc.cy(20, 28) + qc.cy(20, 29) + qc.h(30) + qc.cx(30, 31) + qc.cx(30, 32) + qc.cx(30, 33) + qc.h(34) + qc.cx(34, 35) + qc.cx(34, 36) + qc.cx(34, 37) + qc.h(38) + qc.cx(38, 39) + qc.cx(39, 40) + qc.cx(39, 41) + qc.measure_all() + tqc = transpile(qc, self.backend, optimization_level=opt_level, seed_transpiler=42) + for inst in tqc.data: + qubits = tuple(tqc.find_bit(x).index for x in inst.qubits) + op_name = inst.operation.name + if op_name == "barrier": + continue + self.assertIn(qubits, self.backend.target[op_name]) + @data(0, 1, 2, 3) def test_shared_classical_between_components_condition(self, opt_level): """Test a condition sharing classical bits between components.""" diff --git a/test/python/transpiler/test_sabre_layout.py b/test/python/transpiler/test_sabre_layout.py index 2dc639439308..84e054ea378f 100644 --- a/test/python/transpiler/test_sabre_layout.py +++ b/test/python/transpiler/test_sabre_layout.py @@ -241,18 +241,9 @@ def test_dual_ghz(self): layout_routing_pass = SabreLayout( self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 ) - out = layout_routing_pass(qc) + layout_routing_pass(qc) layout = layout_routing_pass.property_set["layout"] self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) - self.assertEqual(1, out.count_ops()["swap"]) - edge_set = set(self.dual_grid_cmap.graph.edge_list()) - for gate in out.data: - if len(gate.qubits) == 2: - qubits = tuple(out.find_bit(x).index for x in gate.qubits) - # Handle reverse edges which will be fixed by gate direction - # later - if qubits not in edge_set: - self.assertIn((qubits[1], qubits[0]), edge_set) def test_dual_ghz_with_wide_barrier(self): """Test a basic example with 2 circuit components and 2 cmap components.""" @@ -269,18 +260,9 @@ def test_dual_ghz_with_wide_barrier(self): layout_routing_pass = SabreLayout( self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 ) - out = layout_routing_pass(qc) + layout_routing_pass(qc) layout = layout_routing_pass.property_set["layout"] self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) - self.assertEqual(1, out.count_ops()["swap"]) - edge_set = set(self.dual_grid_cmap.graph.edge_list()) - for gate in out.data: - if len(gate.qubits) == 2: - qubits = tuple(out.find_bit(x).index for x in gate.qubits) - # Handle reverse edges which will be fixed by gate direction - # later - if qubits not in edge_set: - self.assertIn((qubits[1], qubits[0]), edge_set) def test_dual_ghz_with_intermediate_barriers(self): """Test dual ghz circuit with intermediate barriers local to each componennt.""" @@ -299,18 +281,9 @@ def test_dual_ghz_with_intermediate_barriers(self): layout_routing_pass = SabreLayout( self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 ) - out = layout_routing_pass(qc) + layout_routing_pass(qc) layout = layout_routing_pass.property_set["layout"] self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) - self.assertEqual(1, out.count_ops()["swap"]) - edge_set = set(self.dual_grid_cmap.graph.edge_list()) - for gate in out.data: - if len(gate.qubits) == 2: - qubits = tuple(out.find_bit(x).index for x in gate.qubits) - # Handle reverse edges which will be fixed by gate direction - # later - if qubits not in edge_set: - self.assertIn((qubits[1], qubits[0]), edge_set) def test_dual_ghz_with_intermediate_spanning_barriers(self): """Test dual ghz circuit with barrier in the middle across components.""" @@ -328,18 +301,9 @@ def test_dual_ghz_with_intermediate_spanning_barriers(self): layout_routing_pass = SabreLayout( self.dual_grid_cmap, seed=123456, swap_trials=1, layout_trials=1 ) - out = layout_routing_pass(qc) + layout_routing_pass(qc) layout = layout_routing_pass.property_set["layout"] self.assertEqual([layout[q] for q in qc.qubits], [3, 1, 2, 5, 4, 6, 7, 8]) - self.assertEqual(1, out.count_ops()["swap"]) - edge_set = set(self.dual_grid_cmap.graph.edge_list()) - for gate in out.data: - if len(gate.qubits) == 2: - qubits = tuple(out.find_bit(x).index for x in gate.qubits) - # Handle reverse edges which will be fixed by gate direction - # later - if qubits not in edge_set: - self.assertIn((qubits[1], qubits[0]), edge_set) def test_too_large_components(self): """Assert trying to run a circuit with too large a connected component raises.""" From d574f66a306b9b9c87be548f274cbf7f83d14328 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 20 Apr 2023 20:07:14 +0100 Subject: [PATCH 058/172] Give ownership of output from `Target.build_coupling_map` (#9999) * Give ownership of output from `Target.build_coupling_map` Previously, the return value from `Target.build_coupling_map` could contain a shared reference to the underlying `PyDiGraph`, which subsequent calls to `make_symmetric` could mutate, affecting the backing `Target` and consequently backend. This now always causes `build_coupling_map` to return a value that is fully owned by the caller, removes some now-redundant copies in other parts of the stack, and adds a manual cache into the `BackendV2.coupling_map` property to avoid performance surprises there. `BackendV2.coupling_map` was previously relying on `build_coupling_map` being cached for its performance, so this makes it more explicit. * Add integration test for `transpile` Optimisation level 3 fails for unrelated reasons; see gh-10004 and gh-9935 for details. * Normalise coupling-map-ownership tests --- qiskit/providers/backend.py | 5 +- .../transpiler/passes/layout/sabre_layout.py | 10 ++-- .../transpiler/passes/routing/sabre_swap.py | 10 ++-- qiskit/transpiler/target.py | 7 ++- test/python/compiler/test_transpiler.py | 17 +++++++ test/python/transpiler/test_target.py | 47 +++++++++++++++++++ 6 files changed, 83 insertions(+), 13 deletions(-) diff --git a/qiskit/providers/backend.py b/qiskit/providers/backend.py index 39ab6df36f24..d61810f765fd 100644 --- a/qiskit/providers/backend.py +++ b/qiskit/providers/backend.py @@ -354,6 +354,7 @@ def __init__( self.description = description self.online_date = online_date self.backend_version = backend_version + self._coupling_map = None @property def instructions(self) -> List[Tuple[Instruction, Tuple[int]]]: @@ -387,7 +388,9 @@ def num_qubits(self) -> int: @property def coupling_map(self): """Return the :class:`~qiskit.transpiler.CouplingMap` object""" - return self.target.build_coupling_map() + if self._coupling_map is None: + self._coupling_map = self.target.build_coupling_map() + return self._coupling_map @property def instruction_durations(self): diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index c1724d0e3ad5..2702786cde3a 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -36,6 +36,7 @@ ) from qiskit.transpiler.passes.routing.sabre_swap import process_swaps, apply_gate from qiskit.transpiler.target import Target +from qiskit.transpiler.coupling import CouplingMap from qiskit.tools.parallel import CPU_COUNT logger = logging.getLogger(__name__) @@ -150,10 +151,11 @@ def __init__( self.skip_routing = skip_routing if self.coupling_map is not None: if not self.coupling_map.is_symmetric: - # deepcopy is needed here to avoid modifications updating - # shared references in passes which require directional - # constraints - self.coupling_map = copy.deepcopy(self.coupling_map) + # deepcopy is needed here if we don't own the coupling map (i.e. we were passed it + # directly) to avoid modifications updating shared references in passes which + # require directional constraints + if isinstance(coupling_map, CouplingMap): + self.coupling_map = copy.deepcopy(self.coupling_map) self.coupling_map.make_symmetric() self._neighbor_table = NeighborTable(rx.adjacency_matrix(self.coupling_map.graph)) diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py index a1d2f907f608..c17b7f09f975 100644 --- a/qiskit/transpiler/passes/routing/sabre_swap.py +++ b/qiskit/transpiler/passes/routing/sabre_swap.py @@ -19,6 +19,7 @@ from qiskit.circuit.library.standard_gates import SwapGate from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.coupling import CouplingMap from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.layout import Layout from qiskit.transpiler.target import Target @@ -148,10 +149,11 @@ def __init__(self, coupling_map, heuristic="basic", seed=None, fake_run=False, t self.coupling_map = coupling_map self.target = None if self.coupling_map is not None and not self.coupling_map.is_symmetric: - # A deepcopy is needed here to avoid modifications updating - # shared references in passes which require directional - # constraints - self.coupling_map = deepcopy(self.coupling_map) + # A deepcopy is needed here if we don't own the coupling map (i.e. we were given it, + # rather than calculated it from the Target), to avoid modifications updating shared + # references in passes which require directional constraints. + if isinstance(coupling_map, CouplingMap): + self.coupling_map = deepcopy(self.coupling_map) self.coupling_map.make_symmetric() self._neighbor_table = None if self.coupling_map is not None: diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index b3306cde80e7..3e4c6aab6a21 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -1001,7 +1001,7 @@ def build_coupling_map(self, two_q_gate=None, filter_idle_qubits=False): If there is a mix of two qubit operations that have a connectivity constraint and those that are globally defined this will also return - ``None`` because the globally connectivity means there is no contstraint + ``None`` because the globally connectivity means there is no constraint on the target. If you wish to see the constraints of the two qubit operations that have constraints you should use the ``two_q_gate`` argument to limit the output to the gates which have a constraint. @@ -1059,17 +1059,16 @@ def build_coupling_map(self, two_q_gate=None, filter_idle_qubits=False): if filter_idle_qubits: cmap.graph = self._filter_coupling_graph() else: - cmap.graph = self._coupling_graph + cmap.graph = self._coupling_graph.copy() return cmap else: return None def _filter_coupling_graph(self): has_operations = set(itertools.chain.from_iterable(self.qargs)) - graph = self._coupling_graph + graph = self._coupling_graph.copy() to_remove = set(graph.node_indices()).difference(has_operations) if to_remove: - graph = graph.copy() graph.remove_nodes_from(list(to_remove)) return graph diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index e29f79d2083f..238a77ea699e 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -12,10 +12,12 @@ """Tests basic functionality of the transpile function""" +import copy import io import os import sys import math +import unittest from logging import StreamHandler, getLogger from unittest.mock import patch @@ -57,6 +59,7 @@ FakeBoeblingen, FakeMumbaiV2, FakeNairobiV2, + FakeSherbrooke, ) from qiskit.transpiler import Layout, CouplingMap from qiskit.transpiler import PassManager, TransformationPass @@ -2893,3 +2896,17 @@ def test_transpile_target_with_qubits_without_ops_circuit_too_large_disconnected qc.x(4) with self.assertRaises(TranspilerError): transpile(qc, target=target, optimization_level=opt_level) + + @data(0, 1, 2, 3) + def test_transpile_does_not_affect_backend_coupling(self, opt_level): + """Test that transpiliation of a circuit does not mutate the `CouplingMap` stored by a V2 + backend. Regression test of gh-9997.""" + if opt_level == 3: + raise unittest.SkipTest("unitary resynthesis fails due to gh-10004") + qc = QuantumCircuit(127) + for i in range(1, 127): + qc.ecr(0, i) + backend = FakeSherbrooke() + original_map = copy.deepcopy(backend.coupling_map) + transpile(qc, backend, optimization_level=opt_level) + self.assertEqual(original_map, backend.coupling_map) diff --git a/test/python/transpiler/test_target.py b/test/python/transpiler/test_target.py index 9bcaabb5fae2..d634741cc1bc 100644 --- a/test/python/transpiler/test_target.py +++ b/test/python/transpiler/test_target.py @@ -488,6 +488,53 @@ def test_coupling_map(self): ) self.assertEqual(None, self.ideal_sim_target.build_coupling_map()) + def test_coupling_map_mutations_do_not_propagate(self): + cm = CouplingMap.from_line(5, bidirectional=False) + cx_props = { + edge: InstructionProperties(duration=270.22e-9, error=0.00713) + for edge in cm.get_edges() + } + target = Target() + target.add_instruction(CXGate(), cx_props) + self.assertEqual(cm, target.build_coupling_map()) + symmetric = target.build_coupling_map() + symmetric.make_symmetric() + self.assertNotEqual(cm, symmetric) # sanity check for the test. + # Verify that mutating the output of `build_coupling_map` doesn't affect the target. + self.assertNotEqual(target.build_coupling_map(), symmetric) + + def test_coupling_map_filtered_mutations_do_not_propagate(self): + cm = CouplingMap.from_line(5, bidirectional=False) + cx_props = { + edge: InstructionProperties(duration=270.22e-9, error=0.00713) + for edge in cm.get_edges() + if 2 not in edge + } + target = Target() + target.add_instruction(CXGate(), cx_props) + symmetric = target.build_coupling_map(filter_idle_qubits=True) + symmetric.make_symmetric() + self.assertNotEqual(cm, symmetric) # sanity check for the test. + # Verify that mutating the output of `build_coupling_map` doesn't affect the target. + self.assertNotEqual(target.build_coupling_map(filter_idle_qubits=True), symmetric) + + def test_coupling_map_no_filter_mutations_do_not_propagate(self): + cm = CouplingMap.from_line(5, bidirectional=False) + cx_props = { + edge: InstructionProperties(duration=270.22e-9, error=0.00713) + for edge in cm.get_edges() + } + target = Target() + target.add_instruction(CXGate(), cx_props) + # The filter here does not actually do anything, because there's no idle qubits. This is + # just a test that this path is also not cached. + self.assertEqual(cm, target.build_coupling_map(filter_idle_qubits=True)) + symmetric = target.build_coupling_map(filter_idle_qubits=True) + symmetric.make_symmetric() + self.assertNotEqual(cm, symmetric) # sanity check for the test. + # Verify that mutating the output of `build_coupling_map` doesn't affect the target. + self.assertNotEqual(target.build_coupling_map(filter_idle_qubits=True), symmetric) + def test_coupling_map_2q_gate(self): cmap = self.fake_backend_target.build_coupling_map("ecr") self.assertEqual( From 4152009ee6d1bae8704f1e7b9ccc5727a53933bd Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 20 Apr 2023 15:24:00 -0400 Subject: [PATCH 059/172] Set version number to 0.24.0rc1 for first release candidate (#9977) For the 0.24.0 release we're going to push release candidates prior to the release 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 what gets tagged as 0.24.0rc1. --- qiskit/VERSION.txt | 2 +- releasenotes/notes/{ => 0.24}/6110-709f6fa891bdb26b.yaml | 0 .../notes/{ => 0.24}/adapt-vqe-thresholds-239ed9f250c63e71.yaml | 0 .../add-alternative-hls-construction-afec157f7cf15b0b.yaml | 0 .../{ => 0.24}/add-clifford-from-matrix-3184822cc559e0b7.yaml | 0 .../notes/{ => 0.24}/add-cmap-componets-7ed56cdf294150f1.yaml | 0 .../notes/{ => 0.24}/add-commutator-96ef07433e8ca4e7.yaml | 0 .../{ => 0.24}/add-ecr-sx-equivalences-5b6fe73ec599d1a4.yaml | 0 .../{ => 0.24}/add-equiv-stabilizerstate-6ef8790c765690c1.yaml | 0 .../add-gates-to-Clifford-class-7de8d3213c60836a.yaml | 0 .../{ => 0.24}/add-global-phase-gate-b52c5b25ab8a3cf6.yaml | 0 .../notes/{ => 0.24}/add-hls-plugins-038388970ad43c55.yaml | 0 .../notes/{ => 0.24}/add-inverse-ecr-e03720252a0c9c1e.yaml | 0 .../notes/{ => 0.24}/add-layout-attribute-c84e56c08ca93ada.yaml | 0 .../{ => 0.24}/add-minimum-point-pass-09cf9a9eec86fd48.yaml | 0 .../{ => 0.24}/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml | 0 .../add-support-for-qpy-reference-70478baa529fff8c.yaml | 0 .../add-swap_nodes-to-dagcircuit-methods-2964426f02251fc4.yaml | 0 .../{ => 0.24}/adding-partial-transpose-040a6ff00228841b.yaml | 0 .../{ => 0.24}/ae-warns-on-goodstate-7dbb689ba6a5e5e4.yaml | 0 .../{ => 0.24}/append-bad-argument-error-cc9eafe94cc39033.yaml | 0 releasenotes/notes/{ => 0.24}/bump-msrv-f6f2bd42b9636b5e.yaml | 0 .../circuit_metadata_always_dict-49015896dfa49d33.yaml | 0 .../circuit_visualization_args_removed-73399f362ab863b3.yaml | 0 .../{ => 0.24}/clifford_cz_lnn_synthesis-4b0328b581749df5.yaml | 0 .../clip-quantumstate-probabilities-5c9ce05ffa699a63.yaml | 0 .../computeuncompute-local-fidelity-501fe175762b7593.yaml | 0 .../notes/{ => 0.24}/coupling-map-eq-b0507b703d62a5f3.yaml | 0 .../notes/{ => 0.24}/crx-equivalences-cc9e5c98bb73fd49.yaml | 0 .../deepcopy-option-circuit_to_dag-1d494b7f9824ec93.yaml | 0 .../deepcopy-option-dag_to_circuit-2974aa9e66dc7643.yaml | 0 .../delete-args-and-methods-in-primitives-d89d444ec0217ae6.yaml | 0 .../notes/{ => 0.24}/deprecate-algorithms-c6e1e28b6091c507.yaml | 0 .../{ => 0.24}/deprecate-bip-mapping-f0025c4c724e1ec8.yaml | 0 .../notes/{ => 0.24}/deprecate-opflow-qi-32f7e27884deea3f.yaml | 0 .../{ => 0.24}/deprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml | 0 ...stic-barrier-before-final-measurements-04e817d995794067.yaml | 0 .../notes/{ => 0.24}/entry_point_obj-60625d9d797df1d9.yaml | 0 .../error-pass-callable-message-3f29f09b9faba736.yaml | 0 .../notes/{ => 0.24}/expression-var-order-d87e9b04fb5d545c.yaml | 0 ...r-faulty-qubits-and-gates-v2-converter-b56dac9c0ce8057f.yaml | 0 .../{ => 0.24}/filter-idle-qubits-cmap-74ac7711fc7476f3.yaml | 0 releasenotes/notes/{ => 0.24}/fix-9798-30c0eb0e5181b691.yaml | 0 .../notes/{ => 0.24}/fix-PauliOp-adjoint-a275876185df989f.yaml | 0 .../notes/{ => 0.24}/fix-ae-algorithms-1c0a43c596766cb3.yaml | 0 .../{ => 0.24}/fix-backendsampler-padding-ed959e6dc3deb3f3.yaml | 0 .../fix-backendv1-pm-config-from-backend-914869dd6e1c06be.yaml | 0 .../fix-backendv2-converter-simulator-e8f150d1fd6861fe.yaml | 0 .../{ => 0.24}/fix-backendv2converter-de342352cf882494.yaml | 0 .../fix-bound-pm-backend-primitives-98fd11c5e852501c.yaml | 0 .../fix-circuit-drawer-for-qc-params-e78c67310ae43ccf.yaml | 0 .../fix-circuit-drawing-low-compression-965c21de51b26ad2.yaml | 0 .../fix-control-with-string-parameter-4eb8a308170e08db.yaml | 0 .../fix-dag-parameterized-calibration-f5c0a740fcdeb2ec.yaml | 0 .../fix-deprecated-bit-qpy-roundtrip-9a23a795aa677c71.yaml | 0 .../{ => 0.24}/fix-empty-pauli-label-ce2580584db67a4d.yaml | 0 .../fix-gate-direction-calibration-c51202358d86e18f.yaml | 0 .../fix-instmap-add-with-arguments-250de2a7960565dc.yaml | 0 .../{ => 0.24}/fix-instmap-from-target-f38962c3fd03e5d3.yaml | 0 .../fix-macros-measure-with-backendV2-4354f00ab4f1cd3e.yaml | 0 .../fix-marginal-distribution-np-ints-ee78859bfda79b60.yaml | 0 .../fix-memory-commutation-checker-dbb441de68706b6f.yaml | 0 .../fix-missing-instproperty-calibration-e578052819592a0b.yaml | 0 .../fix-numpy-eigensolver-sparse-0e255d7b13b5e43b.yaml | 0 .../{ => 0.24}/fix-parameter-is_real-8b8f99811e58075e.yaml | 0 .../fix-partial-reverse-gradient-f35fb1f30ee15692.yaml | 0 .../notes/{ => 0.24}/fix-qasm-reset-ef7b07bf55875be7.yaml | 0 .../notes/{ => 0.24}/fix-qasm2-c3sxgate-47171c9d17876219.yaml | 0 .../{ => 0.24}/fix-qasm3-name-escape-43a8b0e5ec59a471.yaml | 0 .../fix-qpy-import-StatePreparation-e20f8ab07bfe39a3.yaml | 0 .../notes/{ => 0.24}/fix-qpy-mcxgray-421cf8f673f24238.yaml | 0 .../fix-random-circuit-conditional-6067272319986c63.yaml | 0 .../fix-register-name-format-deprecation-61ad5b06d618bb29.yaml | 0 ...x-routing-passes-for-none-coupling_map-c4dd53594a9ef645.yaml | 0 .../fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml | 0 releasenotes/notes/{ => 0.24}/fix-sk-sdg-81ec87abe7af4a89.yaml | 0 .../fix-target-aquire_alignment-typo-d32ff31742b448a1.yaml | 0 .../notes/{ => 0.24}/fix-template-opt-bd3c40382e9a993b.yaml | 0 .../{ => 0.24}/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml | 0 .../fix-type-angles-euler-decompose-233e5cee7205ed03.yaml | 0 ...ll-custom-definitions-empty-definition-4fd175c035445540.yaml | 0 .../fix-vqd-with-spsa-optimizers-9ed02b80f26e8abf.yaml | 0 releasenotes/notes/{ => 0.24}/fix_9559-ec05304e52ff841f.yaml | 0 .../notes/{ => 0.24}/gate-direction-swap-885b6f8ba9779853.yaml | 0 .../{ => 0.24}/improve-transpile-typing-de1197f4dd13ac0c.yaml | 0 .../include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml | 0 .../iterative-phase-estimation-bugfix-b676ffc23cea8251.yaml | 0 ...-constructor-target-from-configuration-91f7eb569d95b330.yaml | 0 .../{ => 0.24}/new-deprecation-utilities-066aff05e221d7b1.yaml | 0 .../{ => 0.24}/operator-apply-permutation-c113c05513cb7881.yaml | 0 .../options-implement-mutable-mapping-b11f4e2c6df4cf31.yaml | 0 ...paulilist-do-not-broadcast-from-paulis-96de3832fba21b94.yaml | 0 .../notes/{ => 0.24}/primitive-job-submit-a7633872e2ae3c7b.yaml | 0 .../{ => 0.24}/qasm2-exporter-rewrite-8993dd24f930b180.yaml | 0 .../notes/{ => 0.24}/qasm2-parser-rust-ecf6570e2d445a94.yaml | 0 .../notes/{ => 0.24}/qnspsa-float-bug-fix-4035f7e1eb61dec2.yaml | 0 .../rearrange-gradient-result-order-1596e14db01395f5.yaml | 0 ...-deprecated-factorizers-linear-solvers-4631870129749624.yaml | 0 .../remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml | 0 .../remove-faulty-qubits-support-00850f69185c365e.yaml | 0 .../notes/{ => 0.24}/rename-fake-backends-b08f8a66bc19088b.yaml | 0 .../notes/{ => 0.24}/rename-graysynth-3fa4fcb7d096ab35.yaml | 0 .../notes/{ => 0.24}/sabre-sort-rng-056f26f205e38bab.yaml | 0 ...arsepauliop-improved-parameter-support-413f7598bac72166.yaml | 0 .../speedup-one-qubit-optimize-pass-483429af948a415e.yaml | 0 .../{ => 0.24}/stabilizer_state_synthesis-c48c0389941715a6.yaml | 0 .../{ => 0.24}/staged-pass-manager-drawer-f1da0308895a30d9.yaml | 0 .../notes/{ => 0.24}/su2-synthesis-295ebf03d9033263.yaml | 0 .../support-disjoint-cmap-sabre-551ae4295131a449.yaml | 0 releasenotes/notes/{ => 0.24}/switch-case-9b6611d0603d36c0.yaml | 0 .../symbolic-pulse-complex-deprecation-89ecdf968b1a2d89.yaml | 0 .../target-aware-layout-routing-2b39bd87a9f928e7.yaml | 0 .../notes/{ => 0.24}/target-transpile-d029922a5dbc3a52.yaml | 0 .../{ => 0.24}/transpile-coupling-maps-3a137f4ca8e97745.yaml | 0 .../trotter-time-dep-hamiltonians-8c6c3d5f629e72fb.yaml | 0 .../unitary-synthesis-based-on-errors-8041fcc9584f5db2.yaml | 0 .../notes/{ => 0.24}/unroll-forloops-7bf8000620f738e7.yaml | 0 .../update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml | 0 .../{ => 0.24}/update-state-visualization-6836bd53e3a24891.yaml | 0 .../{ => 0.24}/vf2-post-layout-max-trials-98b1c736e2e33861.yaml | 0 .../notes/{ => 0.24}/vf2postlayout-fix-16bb54d9bdf3aaf6.yaml | 0 ...qd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml | 0 setup.py | 2 +- 123 files changed, 2 insertions(+), 2 deletions(-) rename releasenotes/notes/{ => 0.24}/6110-709f6fa891bdb26b.yaml (100%) rename releasenotes/notes/{ => 0.24}/adapt-vqe-thresholds-239ed9f250c63e71.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-alternative-hls-construction-afec157f7cf15b0b.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-clifford-from-matrix-3184822cc559e0b7.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-cmap-componets-7ed56cdf294150f1.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-commutator-96ef07433e8ca4e7.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-ecr-sx-equivalences-5b6fe73ec599d1a4.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-equiv-stabilizerstate-6ef8790c765690c1.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-gates-to-Clifford-class-7de8d3213c60836a.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-global-phase-gate-b52c5b25ab8a3cf6.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-hls-plugins-038388970ad43c55.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-inverse-ecr-e03720252a0c9c1e.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-layout-attribute-c84e56c08ca93ada.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-minimum-point-pass-09cf9a9eec86fd48.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-support-for-qpy-reference-70478baa529fff8c.yaml (100%) rename releasenotes/notes/{ => 0.24}/add-swap_nodes-to-dagcircuit-methods-2964426f02251fc4.yaml (100%) rename releasenotes/notes/{ => 0.24}/adding-partial-transpose-040a6ff00228841b.yaml (100%) rename releasenotes/notes/{ => 0.24}/ae-warns-on-goodstate-7dbb689ba6a5e5e4.yaml (100%) rename releasenotes/notes/{ => 0.24}/append-bad-argument-error-cc9eafe94cc39033.yaml (100%) rename releasenotes/notes/{ => 0.24}/bump-msrv-f6f2bd42b9636b5e.yaml (100%) rename releasenotes/notes/{ => 0.24}/circuit_metadata_always_dict-49015896dfa49d33.yaml (100%) rename releasenotes/notes/{ => 0.24}/circuit_visualization_args_removed-73399f362ab863b3.yaml (100%) rename releasenotes/notes/{ => 0.24}/clifford_cz_lnn_synthesis-4b0328b581749df5.yaml (100%) rename releasenotes/notes/{ => 0.24}/clip-quantumstate-probabilities-5c9ce05ffa699a63.yaml (100%) rename releasenotes/notes/{ => 0.24}/computeuncompute-local-fidelity-501fe175762b7593.yaml (100%) rename releasenotes/notes/{ => 0.24}/coupling-map-eq-b0507b703d62a5f3.yaml (100%) rename releasenotes/notes/{ => 0.24}/crx-equivalences-cc9e5c98bb73fd49.yaml (100%) rename releasenotes/notes/{ => 0.24}/deepcopy-option-circuit_to_dag-1d494b7f9824ec93.yaml (100%) rename releasenotes/notes/{ => 0.24}/deepcopy-option-dag_to_circuit-2974aa9e66dc7643.yaml (100%) rename releasenotes/notes/{ => 0.24}/delete-args-and-methods-in-primitives-d89d444ec0217ae6.yaml (100%) rename releasenotes/notes/{ => 0.24}/deprecate-algorithms-c6e1e28b6091c507.yaml (100%) rename releasenotes/notes/{ => 0.24}/deprecate-bip-mapping-f0025c4c724e1ec8.yaml (100%) rename releasenotes/notes/{ => 0.24}/deprecate-opflow-qi-32f7e27884deea3f.yaml (100%) rename releasenotes/notes/{ => 0.24}/deprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml (100%) rename releasenotes/notes/{ => 0.24}/deterministic-barrier-before-final-measurements-04e817d995794067.yaml (100%) rename releasenotes/notes/{ => 0.24}/entry_point_obj-60625d9d797df1d9.yaml (100%) rename releasenotes/notes/{ => 0.24}/error-pass-callable-message-3f29f09b9faba736.yaml (100%) rename releasenotes/notes/{ => 0.24}/expression-var-order-d87e9b04fb5d545c.yaml (100%) rename releasenotes/notes/{ => 0.24}/filter-faulty-qubits-and-gates-v2-converter-b56dac9c0ce8057f.yaml (100%) rename releasenotes/notes/{ => 0.24}/filter-idle-qubits-cmap-74ac7711fc7476f3.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-9798-30c0eb0e5181b691.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-PauliOp-adjoint-a275876185df989f.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-ae-algorithms-1c0a43c596766cb3.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-backendsampler-padding-ed959e6dc3deb3f3.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-backendv1-pm-config-from-backend-914869dd6e1c06be.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-backendv2-converter-simulator-e8f150d1fd6861fe.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-backendv2converter-de342352cf882494.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-bound-pm-backend-primitives-98fd11c5e852501c.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-circuit-drawer-for-qc-params-e78c67310ae43ccf.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-circuit-drawing-low-compression-965c21de51b26ad2.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-control-with-string-parameter-4eb8a308170e08db.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-dag-parameterized-calibration-f5c0a740fcdeb2ec.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-deprecated-bit-qpy-roundtrip-9a23a795aa677c71.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-empty-pauli-label-ce2580584db67a4d.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-gate-direction-calibration-c51202358d86e18f.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-instmap-add-with-arguments-250de2a7960565dc.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-instmap-from-target-f38962c3fd03e5d3.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-macros-measure-with-backendV2-4354f00ab4f1cd3e.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-marginal-distribution-np-ints-ee78859bfda79b60.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-memory-commutation-checker-dbb441de68706b6f.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-missing-instproperty-calibration-e578052819592a0b.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-numpy-eigensolver-sparse-0e255d7b13b5e43b.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-parameter-is_real-8b8f99811e58075e.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-partial-reverse-gradient-f35fb1f30ee15692.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-qasm-reset-ef7b07bf55875be7.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-qasm2-c3sxgate-47171c9d17876219.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-qasm3-name-escape-43a8b0e5ec59a471.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-qpy-import-StatePreparation-e20f8ab07bfe39a3.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-qpy-mcxgray-421cf8f673f24238.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-random-circuit-conditional-6067272319986c63.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-register-name-format-deprecation-61ad5b06d618bb29.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-routing-passes-for-none-coupling_map-c4dd53594a9ef645.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-sk-sdg-81ec87abe7af4a89.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-target-aquire_alignment-typo-d32ff31742b448a1.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-template-opt-bd3c40382e9a993b.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-type-angles-euler-decompose-233e5cee7205ed03.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-unroll-custom-definitions-empty-definition-4fd175c035445540.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix-vqd-with-spsa-optimizers-9ed02b80f26e8abf.yaml (100%) rename releasenotes/notes/{ => 0.24}/fix_9559-ec05304e52ff841f.yaml (100%) rename releasenotes/notes/{ => 0.24}/gate-direction-swap-885b6f8ba9779853.yaml (100%) rename releasenotes/notes/{ => 0.24}/improve-transpile-typing-de1197f4dd13ac0c.yaml (100%) rename releasenotes/notes/{ => 0.24}/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml (100%) rename releasenotes/notes/{ => 0.24}/iterative-phase-estimation-bugfix-b676ffc23cea8251.yaml (100%) rename releasenotes/notes/{ => 0.24}/new-constructor-target-from-configuration-91f7eb569d95b330.yaml (100%) rename releasenotes/notes/{ => 0.24}/new-deprecation-utilities-066aff05e221d7b1.yaml (100%) rename releasenotes/notes/{ => 0.24}/operator-apply-permutation-c113c05513cb7881.yaml (100%) rename releasenotes/notes/{ => 0.24}/options-implement-mutable-mapping-b11f4e2c6df4cf31.yaml (100%) rename releasenotes/notes/{ => 0.24}/paulilist-do-not-broadcast-from-paulis-96de3832fba21b94.yaml (100%) rename releasenotes/notes/{ => 0.24}/primitive-job-submit-a7633872e2ae3c7b.yaml (100%) rename releasenotes/notes/{ => 0.24}/qasm2-exporter-rewrite-8993dd24f930b180.yaml (100%) rename releasenotes/notes/{ => 0.24}/qasm2-parser-rust-ecf6570e2d445a94.yaml (100%) rename releasenotes/notes/{ => 0.24}/qnspsa-float-bug-fix-4035f7e1eb61dec2.yaml (100%) rename releasenotes/notes/{ => 0.24}/rearrange-gradient-result-order-1596e14db01395f5.yaml (100%) rename releasenotes/notes/{ => 0.24}/remove-deprecated-factorizers-linear-solvers-4631870129749624.yaml (100%) rename releasenotes/notes/{ => 0.24}/remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml (100%) rename releasenotes/notes/{ => 0.24}/remove-faulty-qubits-support-00850f69185c365e.yaml (100%) rename releasenotes/notes/{ => 0.24}/rename-fake-backends-b08f8a66bc19088b.yaml (100%) rename releasenotes/notes/{ => 0.24}/rename-graysynth-3fa4fcb7d096ab35.yaml (100%) rename releasenotes/notes/{ => 0.24}/sabre-sort-rng-056f26f205e38bab.yaml (100%) rename releasenotes/notes/{ => 0.24}/sparsepauliop-improved-parameter-support-413f7598bac72166.yaml (100%) rename releasenotes/notes/{ => 0.24}/speedup-one-qubit-optimize-pass-483429af948a415e.yaml (100%) rename releasenotes/notes/{ => 0.24}/stabilizer_state_synthesis-c48c0389941715a6.yaml (100%) rename releasenotes/notes/{ => 0.24}/staged-pass-manager-drawer-f1da0308895a30d9.yaml (100%) rename releasenotes/notes/{ => 0.24}/su2-synthesis-295ebf03d9033263.yaml (100%) rename releasenotes/notes/{ => 0.24}/support-disjoint-cmap-sabre-551ae4295131a449.yaml (100%) rename releasenotes/notes/{ => 0.24}/switch-case-9b6611d0603d36c0.yaml (100%) rename releasenotes/notes/{ => 0.24}/symbolic-pulse-complex-deprecation-89ecdf968b1a2d89.yaml (100%) rename releasenotes/notes/{ => 0.24}/target-aware-layout-routing-2b39bd87a9f928e7.yaml (100%) rename releasenotes/notes/{ => 0.24}/target-transpile-d029922a5dbc3a52.yaml (100%) rename releasenotes/notes/{ => 0.24}/transpile-coupling-maps-3a137f4ca8e97745.yaml (100%) rename releasenotes/notes/{ => 0.24}/trotter-time-dep-hamiltonians-8c6c3d5f629e72fb.yaml (100%) rename releasenotes/notes/{ => 0.24}/unitary-synthesis-based-on-errors-8041fcc9584f5db2.yaml (100%) rename releasenotes/notes/{ => 0.24}/unroll-forloops-7bf8000620f738e7.yaml (100%) rename releasenotes/notes/{ => 0.24}/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml (100%) rename releasenotes/notes/{ => 0.24}/update-state-visualization-6836bd53e3a24891.yaml (100%) rename releasenotes/notes/{ => 0.24}/vf2-post-layout-max-trials-98b1c736e2e33861.yaml (100%) rename releasenotes/notes/{ => 0.24}/vf2postlayout-fix-16bb54d9bdf3aaf6.yaml (100%) rename releasenotes/notes/{ => 0.24}/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml (100%) diff --git a/qiskit/VERSION.txt b/qiskit/VERSION.txt index 2094a100ca8b..24f24553095f 100644 --- a/qiskit/VERSION.txt +++ b/qiskit/VERSION.txt @@ -1 +1 @@ -0.24.0 +0.24.0rc1 diff --git a/releasenotes/notes/6110-709f6fa891bdb26b.yaml b/releasenotes/notes/0.24/6110-709f6fa891bdb26b.yaml similarity index 100% rename from releasenotes/notes/6110-709f6fa891bdb26b.yaml rename to releasenotes/notes/0.24/6110-709f6fa891bdb26b.yaml diff --git a/releasenotes/notes/adapt-vqe-thresholds-239ed9f250c63e71.yaml b/releasenotes/notes/0.24/adapt-vqe-thresholds-239ed9f250c63e71.yaml similarity index 100% rename from releasenotes/notes/adapt-vqe-thresholds-239ed9f250c63e71.yaml rename to releasenotes/notes/0.24/adapt-vqe-thresholds-239ed9f250c63e71.yaml diff --git a/releasenotes/notes/add-alternative-hls-construction-afec157f7cf15b0b.yaml b/releasenotes/notes/0.24/add-alternative-hls-construction-afec157f7cf15b0b.yaml similarity index 100% rename from releasenotes/notes/add-alternative-hls-construction-afec157f7cf15b0b.yaml rename to releasenotes/notes/0.24/add-alternative-hls-construction-afec157f7cf15b0b.yaml diff --git a/releasenotes/notes/add-clifford-from-matrix-3184822cc559e0b7.yaml b/releasenotes/notes/0.24/add-clifford-from-matrix-3184822cc559e0b7.yaml similarity index 100% rename from releasenotes/notes/add-clifford-from-matrix-3184822cc559e0b7.yaml rename to releasenotes/notes/0.24/add-clifford-from-matrix-3184822cc559e0b7.yaml diff --git a/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml b/releasenotes/notes/0.24/add-cmap-componets-7ed56cdf294150f1.yaml similarity index 100% rename from releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml rename to releasenotes/notes/0.24/add-cmap-componets-7ed56cdf294150f1.yaml diff --git a/releasenotes/notes/add-commutator-96ef07433e8ca4e7.yaml b/releasenotes/notes/0.24/add-commutator-96ef07433e8ca4e7.yaml similarity index 100% rename from releasenotes/notes/add-commutator-96ef07433e8ca4e7.yaml rename to releasenotes/notes/0.24/add-commutator-96ef07433e8ca4e7.yaml diff --git a/releasenotes/notes/add-ecr-sx-equivalences-5b6fe73ec599d1a4.yaml b/releasenotes/notes/0.24/add-ecr-sx-equivalences-5b6fe73ec599d1a4.yaml similarity index 100% rename from releasenotes/notes/add-ecr-sx-equivalences-5b6fe73ec599d1a4.yaml rename to releasenotes/notes/0.24/add-ecr-sx-equivalences-5b6fe73ec599d1a4.yaml diff --git a/releasenotes/notes/add-equiv-stabilizerstate-6ef8790c765690c1.yaml b/releasenotes/notes/0.24/add-equiv-stabilizerstate-6ef8790c765690c1.yaml similarity index 100% rename from releasenotes/notes/add-equiv-stabilizerstate-6ef8790c765690c1.yaml rename to releasenotes/notes/0.24/add-equiv-stabilizerstate-6ef8790c765690c1.yaml diff --git a/releasenotes/notes/add-gates-to-Clifford-class-7de8d3213c60836a.yaml b/releasenotes/notes/0.24/add-gates-to-Clifford-class-7de8d3213c60836a.yaml similarity index 100% rename from releasenotes/notes/add-gates-to-Clifford-class-7de8d3213c60836a.yaml rename to releasenotes/notes/0.24/add-gates-to-Clifford-class-7de8d3213c60836a.yaml diff --git a/releasenotes/notes/add-global-phase-gate-b52c5b25ab8a3cf6.yaml b/releasenotes/notes/0.24/add-global-phase-gate-b52c5b25ab8a3cf6.yaml similarity index 100% rename from releasenotes/notes/add-global-phase-gate-b52c5b25ab8a3cf6.yaml rename to releasenotes/notes/0.24/add-global-phase-gate-b52c5b25ab8a3cf6.yaml diff --git a/releasenotes/notes/add-hls-plugins-038388970ad43c55.yaml b/releasenotes/notes/0.24/add-hls-plugins-038388970ad43c55.yaml similarity index 100% rename from releasenotes/notes/add-hls-plugins-038388970ad43c55.yaml rename to releasenotes/notes/0.24/add-hls-plugins-038388970ad43c55.yaml diff --git a/releasenotes/notes/add-inverse-ecr-e03720252a0c9c1e.yaml b/releasenotes/notes/0.24/add-inverse-ecr-e03720252a0c9c1e.yaml similarity index 100% rename from releasenotes/notes/add-inverse-ecr-e03720252a0c9c1e.yaml rename to releasenotes/notes/0.24/add-inverse-ecr-e03720252a0c9c1e.yaml diff --git a/releasenotes/notes/add-layout-attribute-c84e56c08ca93ada.yaml b/releasenotes/notes/0.24/add-layout-attribute-c84e56c08ca93ada.yaml similarity index 100% rename from releasenotes/notes/add-layout-attribute-c84e56c08ca93ada.yaml rename to releasenotes/notes/0.24/add-layout-attribute-c84e56c08ca93ada.yaml diff --git a/releasenotes/notes/add-minimum-point-pass-09cf9a9eec86fd48.yaml b/releasenotes/notes/0.24/add-minimum-point-pass-09cf9a9eec86fd48.yaml similarity index 100% rename from releasenotes/notes/add-minimum-point-pass-09cf9a9eec86fd48.yaml rename to releasenotes/notes/0.24/add-minimum-point-pass-09cf9a9eec86fd48.yaml diff --git a/releasenotes/notes/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml b/releasenotes/notes/0.24/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml similarity index 100% rename from releasenotes/notes/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml rename to releasenotes/notes/0.24/add-new-symbolic-pulses-4dc46ecaaa1ba928.yaml diff --git a/releasenotes/notes/add-support-for-qpy-reference-70478baa529fff8c.yaml b/releasenotes/notes/0.24/add-support-for-qpy-reference-70478baa529fff8c.yaml similarity index 100% rename from releasenotes/notes/add-support-for-qpy-reference-70478baa529fff8c.yaml rename to releasenotes/notes/0.24/add-support-for-qpy-reference-70478baa529fff8c.yaml diff --git a/releasenotes/notes/add-swap_nodes-to-dagcircuit-methods-2964426f02251fc4.yaml b/releasenotes/notes/0.24/add-swap_nodes-to-dagcircuit-methods-2964426f02251fc4.yaml similarity index 100% rename from releasenotes/notes/add-swap_nodes-to-dagcircuit-methods-2964426f02251fc4.yaml rename to releasenotes/notes/0.24/add-swap_nodes-to-dagcircuit-methods-2964426f02251fc4.yaml diff --git a/releasenotes/notes/adding-partial-transpose-040a6ff00228841b.yaml b/releasenotes/notes/0.24/adding-partial-transpose-040a6ff00228841b.yaml similarity index 100% rename from releasenotes/notes/adding-partial-transpose-040a6ff00228841b.yaml rename to releasenotes/notes/0.24/adding-partial-transpose-040a6ff00228841b.yaml diff --git a/releasenotes/notes/ae-warns-on-goodstate-7dbb689ba6a5e5e4.yaml b/releasenotes/notes/0.24/ae-warns-on-goodstate-7dbb689ba6a5e5e4.yaml similarity index 100% rename from releasenotes/notes/ae-warns-on-goodstate-7dbb689ba6a5e5e4.yaml rename to releasenotes/notes/0.24/ae-warns-on-goodstate-7dbb689ba6a5e5e4.yaml diff --git a/releasenotes/notes/append-bad-argument-error-cc9eafe94cc39033.yaml b/releasenotes/notes/0.24/append-bad-argument-error-cc9eafe94cc39033.yaml similarity index 100% rename from releasenotes/notes/append-bad-argument-error-cc9eafe94cc39033.yaml rename to releasenotes/notes/0.24/append-bad-argument-error-cc9eafe94cc39033.yaml diff --git a/releasenotes/notes/bump-msrv-f6f2bd42b9636b5e.yaml b/releasenotes/notes/0.24/bump-msrv-f6f2bd42b9636b5e.yaml similarity index 100% rename from releasenotes/notes/bump-msrv-f6f2bd42b9636b5e.yaml rename to releasenotes/notes/0.24/bump-msrv-f6f2bd42b9636b5e.yaml diff --git a/releasenotes/notes/circuit_metadata_always_dict-49015896dfa49d33.yaml b/releasenotes/notes/0.24/circuit_metadata_always_dict-49015896dfa49d33.yaml similarity index 100% rename from releasenotes/notes/circuit_metadata_always_dict-49015896dfa49d33.yaml rename to releasenotes/notes/0.24/circuit_metadata_always_dict-49015896dfa49d33.yaml diff --git a/releasenotes/notes/circuit_visualization_args_removed-73399f362ab863b3.yaml b/releasenotes/notes/0.24/circuit_visualization_args_removed-73399f362ab863b3.yaml similarity index 100% rename from releasenotes/notes/circuit_visualization_args_removed-73399f362ab863b3.yaml rename to releasenotes/notes/0.24/circuit_visualization_args_removed-73399f362ab863b3.yaml diff --git a/releasenotes/notes/clifford_cz_lnn_synthesis-4b0328b581749df5.yaml b/releasenotes/notes/0.24/clifford_cz_lnn_synthesis-4b0328b581749df5.yaml similarity index 100% rename from releasenotes/notes/clifford_cz_lnn_synthesis-4b0328b581749df5.yaml rename to releasenotes/notes/0.24/clifford_cz_lnn_synthesis-4b0328b581749df5.yaml diff --git a/releasenotes/notes/clip-quantumstate-probabilities-5c9ce05ffa699a63.yaml b/releasenotes/notes/0.24/clip-quantumstate-probabilities-5c9ce05ffa699a63.yaml similarity index 100% rename from releasenotes/notes/clip-quantumstate-probabilities-5c9ce05ffa699a63.yaml rename to releasenotes/notes/0.24/clip-quantumstate-probabilities-5c9ce05ffa699a63.yaml diff --git a/releasenotes/notes/computeuncompute-local-fidelity-501fe175762b7593.yaml b/releasenotes/notes/0.24/computeuncompute-local-fidelity-501fe175762b7593.yaml similarity index 100% rename from releasenotes/notes/computeuncompute-local-fidelity-501fe175762b7593.yaml rename to releasenotes/notes/0.24/computeuncompute-local-fidelity-501fe175762b7593.yaml diff --git a/releasenotes/notes/coupling-map-eq-b0507b703d62a5f3.yaml b/releasenotes/notes/0.24/coupling-map-eq-b0507b703d62a5f3.yaml similarity index 100% rename from releasenotes/notes/coupling-map-eq-b0507b703d62a5f3.yaml rename to releasenotes/notes/0.24/coupling-map-eq-b0507b703d62a5f3.yaml diff --git a/releasenotes/notes/crx-equivalences-cc9e5c98bb73fd49.yaml b/releasenotes/notes/0.24/crx-equivalences-cc9e5c98bb73fd49.yaml similarity index 100% rename from releasenotes/notes/crx-equivalences-cc9e5c98bb73fd49.yaml rename to releasenotes/notes/0.24/crx-equivalences-cc9e5c98bb73fd49.yaml diff --git a/releasenotes/notes/deepcopy-option-circuit_to_dag-1d494b7f9824ec93.yaml b/releasenotes/notes/0.24/deepcopy-option-circuit_to_dag-1d494b7f9824ec93.yaml similarity index 100% rename from releasenotes/notes/deepcopy-option-circuit_to_dag-1d494b7f9824ec93.yaml rename to releasenotes/notes/0.24/deepcopy-option-circuit_to_dag-1d494b7f9824ec93.yaml diff --git a/releasenotes/notes/deepcopy-option-dag_to_circuit-2974aa9e66dc7643.yaml b/releasenotes/notes/0.24/deepcopy-option-dag_to_circuit-2974aa9e66dc7643.yaml similarity index 100% rename from releasenotes/notes/deepcopy-option-dag_to_circuit-2974aa9e66dc7643.yaml rename to releasenotes/notes/0.24/deepcopy-option-dag_to_circuit-2974aa9e66dc7643.yaml diff --git a/releasenotes/notes/delete-args-and-methods-in-primitives-d89d444ec0217ae6.yaml b/releasenotes/notes/0.24/delete-args-and-methods-in-primitives-d89d444ec0217ae6.yaml similarity index 100% rename from releasenotes/notes/delete-args-and-methods-in-primitives-d89d444ec0217ae6.yaml rename to releasenotes/notes/0.24/delete-args-and-methods-in-primitives-d89d444ec0217ae6.yaml diff --git a/releasenotes/notes/deprecate-algorithms-c6e1e28b6091c507.yaml b/releasenotes/notes/0.24/deprecate-algorithms-c6e1e28b6091c507.yaml similarity index 100% rename from releasenotes/notes/deprecate-algorithms-c6e1e28b6091c507.yaml rename to releasenotes/notes/0.24/deprecate-algorithms-c6e1e28b6091c507.yaml diff --git a/releasenotes/notes/deprecate-bip-mapping-f0025c4c724e1ec8.yaml b/releasenotes/notes/0.24/deprecate-bip-mapping-f0025c4c724e1ec8.yaml similarity index 100% rename from releasenotes/notes/deprecate-bip-mapping-f0025c4c724e1ec8.yaml rename to releasenotes/notes/0.24/deprecate-bip-mapping-f0025c4c724e1ec8.yaml diff --git a/releasenotes/notes/deprecate-opflow-qi-32f7e27884deea3f.yaml b/releasenotes/notes/0.24/deprecate-opflow-qi-32f7e27884deea3f.yaml similarity index 100% rename from releasenotes/notes/deprecate-opflow-qi-32f7e27884deea3f.yaml rename to releasenotes/notes/0.24/deprecate-opflow-qi-32f7e27884deea3f.yaml diff --git a/releasenotes/notes/deprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml b/releasenotes/notes/0.24/deprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml similarity index 100% rename from releasenotes/notes/deprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml rename to releasenotes/notes/0.24/deprecate-pauli-table-fc6dcdb5eeb6e0c4.yaml diff --git a/releasenotes/notes/deterministic-barrier-before-final-measurements-04e817d995794067.yaml b/releasenotes/notes/0.24/deterministic-barrier-before-final-measurements-04e817d995794067.yaml similarity index 100% rename from releasenotes/notes/deterministic-barrier-before-final-measurements-04e817d995794067.yaml rename to releasenotes/notes/0.24/deterministic-barrier-before-final-measurements-04e817d995794067.yaml diff --git a/releasenotes/notes/entry_point_obj-60625d9d797df1d9.yaml b/releasenotes/notes/0.24/entry_point_obj-60625d9d797df1d9.yaml similarity index 100% rename from releasenotes/notes/entry_point_obj-60625d9d797df1d9.yaml rename to releasenotes/notes/0.24/entry_point_obj-60625d9d797df1d9.yaml diff --git a/releasenotes/notes/error-pass-callable-message-3f29f09b9faba736.yaml b/releasenotes/notes/0.24/error-pass-callable-message-3f29f09b9faba736.yaml similarity index 100% rename from releasenotes/notes/error-pass-callable-message-3f29f09b9faba736.yaml rename to releasenotes/notes/0.24/error-pass-callable-message-3f29f09b9faba736.yaml diff --git a/releasenotes/notes/expression-var-order-d87e9b04fb5d545c.yaml b/releasenotes/notes/0.24/expression-var-order-d87e9b04fb5d545c.yaml similarity index 100% rename from releasenotes/notes/expression-var-order-d87e9b04fb5d545c.yaml rename to releasenotes/notes/0.24/expression-var-order-d87e9b04fb5d545c.yaml diff --git a/releasenotes/notes/filter-faulty-qubits-and-gates-v2-converter-b56dac9c0ce8057f.yaml b/releasenotes/notes/0.24/filter-faulty-qubits-and-gates-v2-converter-b56dac9c0ce8057f.yaml similarity index 100% rename from releasenotes/notes/filter-faulty-qubits-and-gates-v2-converter-b56dac9c0ce8057f.yaml rename to releasenotes/notes/0.24/filter-faulty-qubits-and-gates-v2-converter-b56dac9c0ce8057f.yaml diff --git a/releasenotes/notes/filter-idle-qubits-cmap-74ac7711fc7476f3.yaml b/releasenotes/notes/0.24/filter-idle-qubits-cmap-74ac7711fc7476f3.yaml similarity index 100% rename from releasenotes/notes/filter-idle-qubits-cmap-74ac7711fc7476f3.yaml rename to releasenotes/notes/0.24/filter-idle-qubits-cmap-74ac7711fc7476f3.yaml diff --git a/releasenotes/notes/fix-9798-30c0eb0e5181b691.yaml b/releasenotes/notes/0.24/fix-9798-30c0eb0e5181b691.yaml similarity index 100% rename from releasenotes/notes/fix-9798-30c0eb0e5181b691.yaml rename to releasenotes/notes/0.24/fix-9798-30c0eb0e5181b691.yaml diff --git a/releasenotes/notes/fix-PauliOp-adjoint-a275876185df989f.yaml b/releasenotes/notes/0.24/fix-PauliOp-adjoint-a275876185df989f.yaml similarity index 100% rename from releasenotes/notes/fix-PauliOp-adjoint-a275876185df989f.yaml rename to releasenotes/notes/0.24/fix-PauliOp-adjoint-a275876185df989f.yaml diff --git a/releasenotes/notes/fix-ae-algorithms-1c0a43c596766cb3.yaml b/releasenotes/notes/0.24/fix-ae-algorithms-1c0a43c596766cb3.yaml similarity index 100% rename from releasenotes/notes/fix-ae-algorithms-1c0a43c596766cb3.yaml rename to releasenotes/notes/0.24/fix-ae-algorithms-1c0a43c596766cb3.yaml diff --git a/releasenotes/notes/fix-backendsampler-padding-ed959e6dc3deb3f3.yaml b/releasenotes/notes/0.24/fix-backendsampler-padding-ed959e6dc3deb3f3.yaml similarity index 100% rename from releasenotes/notes/fix-backendsampler-padding-ed959e6dc3deb3f3.yaml rename to releasenotes/notes/0.24/fix-backendsampler-padding-ed959e6dc3deb3f3.yaml diff --git a/releasenotes/notes/fix-backendv1-pm-config-from-backend-914869dd6e1c06be.yaml b/releasenotes/notes/0.24/fix-backendv1-pm-config-from-backend-914869dd6e1c06be.yaml similarity index 100% rename from releasenotes/notes/fix-backendv1-pm-config-from-backend-914869dd6e1c06be.yaml rename to releasenotes/notes/0.24/fix-backendv1-pm-config-from-backend-914869dd6e1c06be.yaml diff --git a/releasenotes/notes/fix-backendv2-converter-simulator-e8f150d1fd6861fe.yaml b/releasenotes/notes/0.24/fix-backendv2-converter-simulator-e8f150d1fd6861fe.yaml similarity index 100% rename from releasenotes/notes/fix-backendv2-converter-simulator-e8f150d1fd6861fe.yaml rename to releasenotes/notes/0.24/fix-backendv2-converter-simulator-e8f150d1fd6861fe.yaml diff --git a/releasenotes/notes/fix-backendv2converter-de342352cf882494.yaml b/releasenotes/notes/0.24/fix-backendv2converter-de342352cf882494.yaml similarity index 100% rename from releasenotes/notes/fix-backendv2converter-de342352cf882494.yaml rename to releasenotes/notes/0.24/fix-backendv2converter-de342352cf882494.yaml diff --git a/releasenotes/notes/fix-bound-pm-backend-primitives-98fd11c5e852501c.yaml b/releasenotes/notes/0.24/fix-bound-pm-backend-primitives-98fd11c5e852501c.yaml similarity index 100% rename from releasenotes/notes/fix-bound-pm-backend-primitives-98fd11c5e852501c.yaml rename to releasenotes/notes/0.24/fix-bound-pm-backend-primitives-98fd11c5e852501c.yaml diff --git a/releasenotes/notes/fix-circuit-drawer-for-qc-params-e78c67310ae43ccf.yaml b/releasenotes/notes/0.24/fix-circuit-drawer-for-qc-params-e78c67310ae43ccf.yaml similarity index 100% rename from releasenotes/notes/fix-circuit-drawer-for-qc-params-e78c67310ae43ccf.yaml rename to releasenotes/notes/0.24/fix-circuit-drawer-for-qc-params-e78c67310ae43ccf.yaml diff --git a/releasenotes/notes/fix-circuit-drawing-low-compression-965c21de51b26ad2.yaml b/releasenotes/notes/0.24/fix-circuit-drawing-low-compression-965c21de51b26ad2.yaml similarity index 100% rename from releasenotes/notes/fix-circuit-drawing-low-compression-965c21de51b26ad2.yaml rename to releasenotes/notes/0.24/fix-circuit-drawing-low-compression-965c21de51b26ad2.yaml diff --git a/releasenotes/notes/fix-control-with-string-parameter-4eb8a308170e08db.yaml b/releasenotes/notes/0.24/fix-control-with-string-parameter-4eb8a308170e08db.yaml similarity index 100% rename from releasenotes/notes/fix-control-with-string-parameter-4eb8a308170e08db.yaml rename to releasenotes/notes/0.24/fix-control-with-string-parameter-4eb8a308170e08db.yaml diff --git a/releasenotes/notes/fix-dag-parameterized-calibration-f5c0a740fcdeb2ec.yaml b/releasenotes/notes/0.24/fix-dag-parameterized-calibration-f5c0a740fcdeb2ec.yaml similarity index 100% rename from releasenotes/notes/fix-dag-parameterized-calibration-f5c0a740fcdeb2ec.yaml rename to releasenotes/notes/0.24/fix-dag-parameterized-calibration-f5c0a740fcdeb2ec.yaml diff --git a/releasenotes/notes/fix-deprecated-bit-qpy-roundtrip-9a23a795aa677c71.yaml b/releasenotes/notes/0.24/fix-deprecated-bit-qpy-roundtrip-9a23a795aa677c71.yaml similarity index 100% rename from releasenotes/notes/fix-deprecated-bit-qpy-roundtrip-9a23a795aa677c71.yaml rename to releasenotes/notes/0.24/fix-deprecated-bit-qpy-roundtrip-9a23a795aa677c71.yaml diff --git a/releasenotes/notes/fix-empty-pauli-label-ce2580584db67a4d.yaml b/releasenotes/notes/0.24/fix-empty-pauli-label-ce2580584db67a4d.yaml similarity index 100% rename from releasenotes/notes/fix-empty-pauli-label-ce2580584db67a4d.yaml rename to releasenotes/notes/0.24/fix-empty-pauli-label-ce2580584db67a4d.yaml diff --git a/releasenotes/notes/fix-gate-direction-calibration-c51202358d86e18f.yaml b/releasenotes/notes/0.24/fix-gate-direction-calibration-c51202358d86e18f.yaml similarity index 100% rename from releasenotes/notes/fix-gate-direction-calibration-c51202358d86e18f.yaml rename to releasenotes/notes/0.24/fix-gate-direction-calibration-c51202358d86e18f.yaml diff --git a/releasenotes/notes/fix-instmap-add-with-arguments-250de2a7960565dc.yaml b/releasenotes/notes/0.24/fix-instmap-add-with-arguments-250de2a7960565dc.yaml similarity index 100% rename from releasenotes/notes/fix-instmap-add-with-arguments-250de2a7960565dc.yaml rename to releasenotes/notes/0.24/fix-instmap-add-with-arguments-250de2a7960565dc.yaml diff --git a/releasenotes/notes/fix-instmap-from-target-f38962c3fd03e5d3.yaml b/releasenotes/notes/0.24/fix-instmap-from-target-f38962c3fd03e5d3.yaml similarity index 100% rename from releasenotes/notes/fix-instmap-from-target-f38962c3fd03e5d3.yaml rename to releasenotes/notes/0.24/fix-instmap-from-target-f38962c3fd03e5d3.yaml diff --git a/releasenotes/notes/fix-macros-measure-with-backendV2-4354f00ab4f1cd3e.yaml b/releasenotes/notes/0.24/fix-macros-measure-with-backendV2-4354f00ab4f1cd3e.yaml similarity index 100% rename from releasenotes/notes/fix-macros-measure-with-backendV2-4354f00ab4f1cd3e.yaml rename to releasenotes/notes/0.24/fix-macros-measure-with-backendV2-4354f00ab4f1cd3e.yaml diff --git a/releasenotes/notes/fix-marginal-distribution-np-ints-ee78859bfda79b60.yaml b/releasenotes/notes/0.24/fix-marginal-distribution-np-ints-ee78859bfda79b60.yaml similarity index 100% rename from releasenotes/notes/fix-marginal-distribution-np-ints-ee78859bfda79b60.yaml rename to releasenotes/notes/0.24/fix-marginal-distribution-np-ints-ee78859bfda79b60.yaml diff --git a/releasenotes/notes/fix-memory-commutation-checker-dbb441de68706b6f.yaml b/releasenotes/notes/0.24/fix-memory-commutation-checker-dbb441de68706b6f.yaml similarity index 100% rename from releasenotes/notes/fix-memory-commutation-checker-dbb441de68706b6f.yaml rename to releasenotes/notes/0.24/fix-memory-commutation-checker-dbb441de68706b6f.yaml diff --git a/releasenotes/notes/fix-missing-instproperty-calibration-e578052819592a0b.yaml b/releasenotes/notes/0.24/fix-missing-instproperty-calibration-e578052819592a0b.yaml similarity index 100% rename from releasenotes/notes/fix-missing-instproperty-calibration-e578052819592a0b.yaml rename to releasenotes/notes/0.24/fix-missing-instproperty-calibration-e578052819592a0b.yaml diff --git a/releasenotes/notes/fix-numpy-eigensolver-sparse-0e255d7b13b5e43b.yaml b/releasenotes/notes/0.24/fix-numpy-eigensolver-sparse-0e255d7b13b5e43b.yaml similarity index 100% rename from releasenotes/notes/fix-numpy-eigensolver-sparse-0e255d7b13b5e43b.yaml rename to releasenotes/notes/0.24/fix-numpy-eigensolver-sparse-0e255d7b13b5e43b.yaml diff --git a/releasenotes/notes/fix-parameter-is_real-8b8f99811e58075e.yaml b/releasenotes/notes/0.24/fix-parameter-is_real-8b8f99811e58075e.yaml similarity index 100% rename from releasenotes/notes/fix-parameter-is_real-8b8f99811e58075e.yaml rename to releasenotes/notes/0.24/fix-parameter-is_real-8b8f99811e58075e.yaml diff --git a/releasenotes/notes/fix-partial-reverse-gradient-f35fb1f30ee15692.yaml b/releasenotes/notes/0.24/fix-partial-reverse-gradient-f35fb1f30ee15692.yaml similarity index 100% rename from releasenotes/notes/fix-partial-reverse-gradient-f35fb1f30ee15692.yaml rename to releasenotes/notes/0.24/fix-partial-reverse-gradient-f35fb1f30ee15692.yaml diff --git a/releasenotes/notes/fix-qasm-reset-ef7b07bf55875be7.yaml b/releasenotes/notes/0.24/fix-qasm-reset-ef7b07bf55875be7.yaml similarity index 100% rename from releasenotes/notes/fix-qasm-reset-ef7b07bf55875be7.yaml rename to releasenotes/notes/0.24/fix-qasm-reset-ef7b07bf55875be7.yaml diff --git a/releasenotes/notes/fix-qasm2-c3sxgate-47171c9d17876219.yaml b/releasenotes/notes/0.24/fix-qasm2-c3sxgate-47171c9d17876219.yaml similarity index 100% rename from releasenotes/notes/fix-qasm2-c3sxgate-47171c9d17876219.yaml rename to releasenotes/notes/0.24/fix-qasm2-c3sxgate-47171c9d17876219.yaml diff --git a/releasenotes/notes/fix-qasm3-name-escape-43a8b0e5ec59a471.yaml b/releasenotes/notes/0.24/fix-qasm3-name-escape-43a8b0e5ec59a471.yaml similarity index 100% rename from releasenotes/notes/fix-qasm3-name-escape-43a8b0e5ec59a471.yaml rename to releasenotes/notes/0.24/fix-qasm3-name-escape-43a8b0e5ec59a471.yaml diff --git a/releasenotes/notes/fix-qpy-import-StatePreparation-e20f8ab07bfe39a3.yaml b/releasenotes/notes/0.24/fix-qpy-import-StatePreparation-e20f8ab07bfe39a3.yaml similarity index 100% rename from releasenotes/notes/fix-qpy-import-StatePreparation-e20f8ab07bfe39a3.yaml rename to releasenotes/notes/0.24/fix-qpy-import-StatePreparation-e20f8ab07bfe39a3.yaml diff --git a/releasenotes/notes/fix-qpy-mcxgray-421cf8f673f24238.yaml b/releasenotes/notes/0.24/fix-qpy-mcxgray-421cf8f673f24238.yaml similarity index 100% rename from releasenotes/notes/fix-qpy-mcxgray-421cf8f673f24238.yaml rename to releasenotes/notes/0.24/fix-qpy-mcxgray-421cf8f673f24238.yaml diff --git a/releasenotes/notes/fix-random-circuit-conditional-6067272319986c63.yaml b/releasenotes/notes/0.24/fix-random-circuit-conditional-6067272319986c63.yaml similarity index 100% rename from releasenotes/notes/fix-random-circuit-conditional-6067272319986c63.yaml rename to releasenotes/notes/0.24/fix-random-circuit-conditional-6067272319986c63.yaml diff --git a/releasenotes/notes/fix-register-name-format-deprecation-61ad5b06d618bb29.yaml b/releasenotes/notes/0.24/fix-register-name-format-deprecation-61ad5b06d618bb29.yaml similarity index 100% rename from releasenotes/notes/fix-register-name-format-deprecation-61ad5b06d618bb29.yaml rename to releasenotes/notes/0.24/fix-register-name-format-deprecation-61ad5b06d618bb29.yaml diff --git a/releasenotes/notes/fix-routing-passes-for-none-coupling_map-c4dd53594a9ef645.yaml b/releasenotes/notes/0.24/fix-routing-passes-for-none-coupling_map-c4dd53594a9ef645.yaml similarity index 100% rename from releasenotes/notes/fix-routing-passes-for-none-coupling_map-c4dd53594a9ef645.yaml rename to releasenotes/notes/0.24/fix-routing-passes-for-none-coupling_map-c4dd53594a9ef645.yaml diff --git a/releasenotes/notes/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml b/releasenotes/notes/0.24/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml similarity index 100% rename from releasenotes/notes/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml rename to releasenotes/notes/0.24/fix-setting-circuit-data-operation-1b8326b1b089f10c.yaml diff --git a/releasenotes/notes/fix-sk-sdg-81ec87abe7af4a89.yaml b/releasenotes/notes/0.24/fix-sk-sdg-81ec87abe7af4a89.yaml similarity index 100% rename from releasenotes/notes/fix-sk-sdg-81ec87abe7af4a89.yaml rename to releasenotes/notes/0.24/fix-sk-sdg-81ec87abe7af4a89.yaml diff --git a/releasenotes/notes/fix-target-aquire_alignment-typo-d32ff31742b448a1.yaml b/releasenotes/notes/0.24/fix-target-aquire_alignment-typo-d32ff31742b448a1.yaml similarity index 100% rename from releasenotes/notes/fix-target-aquire_alignment-typo-d32ff31742b448a1.yaml rename to releasenotes/notes/0.24/fix-target-aquire_alignment-typo-d32ff31742b448a1.yaml diff --git a/releasenotes/notes/fix-template-opt-bd3c40382e9a993b.yaml b/releasenotes/notes/0.24/fix-template-opt-bd3c40382e9a993b.yaml similarity index 100% rename from releasenotes/notes/fix-template-opt-bd3c40382e9a993b.yaml rename to releasenotes/notes/0.24/fix-template-opt-bd3c40382e9a993b.yaml diff --git a/releasenotes/notes/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml b/releasenotes/notes/0.24/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml similarity index 100% rename from releasenotes/notes/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml rename to releasenotes/notes/0.24/fix-tensoredop-to-matrix-6f22644f1bdb8b41.yaml diff --git a/releasenotes/notes/fix-type-angles-euler-decompose-233e5cee7205ed03.yaml b/releasenotes/notes/0.24/fix-type-angles-euler-decompose-233e5cee7205ed03.yaml similarity index 100% rename from releasenotes/notes/fix-type-angles-euler-decompose-233e5cee7205ed03.yaml rename to releasenotes/notes/0.24/fix-type-angles-euler-decompose-233e5cee7205ed03.yaml diff --git a/releasenotes/notes/fix-unroll-custom-definitions-empty-definition-4fd175c035445540.yaml b/releasenotes/notes/0.24/fix-unroll-custom-definitions-empty-definition-4fd175c035445540.yaml similarity index 100% rename from releasenotes/notes/fix-unroll-custom-definitions-empty-definition-4fd175c035445540.yaml rename to releasenotes/notes/0.24/fix-unroll-custom-definitions-empty-definition-4fd175c035445540.yaml diff --git a/releasenotes/notes/fix-vqd-with-spsa-optimizers-9ed02b80f26e8abf.yaml b/releasenotes/notes/0.24/fix-vqd-with-spsa-optimizers-9ed02b80f26e8abf.yaml similarity index 100% rename from releasenotes/notes/fix-vqd-with-spsa-optimizers-9ed02b80f26e8abf.yaml rename to releasenotes/notes/0.24/fix-vqd-with-spsa-optimizers-9ed02b80f26e8abf.yaml diff --git a/releasenotes/notes/fix_9559-ec05304e52ff841f.yaml b/releasenotes/notes/0.24/fix_9559-ec05304e52ff841f.yaml similarity index 100% rename from releasenotes/notes/fix_9559-ec05304e52ff841f.yaml rename to releasenotes/notes/0.24/fix_9559-ec05304e52ff841f.yaml diff --git a/releasenotes/notes/gate-direction-swap-885b6f8ba9779853.yaml b/releasenotes/notes/0.24/gate-direction-swap-885b6f8ba9779853.yaml similarity index 100% rename from releasenotes/notes/gate-direction-swap-885b6f8ba9779853.yaml rename to releasenotes/notes/0.24/gate-direction-swap-885b6f8ba9779853.yaml diff --git a/releasenotes/notes/improve-transpile-typing-de1197f4dd13ac0c.yaml b/releasenotes/notes/0.24/improve-transpile-typing-de1197f4dd13ac0c.yaml similarity index 100% rename from releasenotes/notes/improve-transpile-typing-de1197f4dd13ac0c.yaml rename to releasenotes/notes/0.24/improve-transpile-typing-de1197f4dd13ac0c.yaml diff --git a/releasenotes/notes/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml b/releasenotes/notes/0.24/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml similarity index 100% rename from releasenotes/notes/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml rename to releasenotes/notes/0.24/include-ecr-gates-for-pulse-scaling-8369eb584c6d8fe1.yaml diff --git a/releasenotes/notes/iterative-phase-estimation-bugfix-b676ffc23cea8251.yaml b/releasenotes/notes/0.24/iterative-phase-estimation-bugfix-b676ffc23cea8251.yaml similarity index 100% rename from releasenotes/notes/iterative-phase-estimation-bugfix-b676ffc23cea8251.yaml rename to releasenotes/notes/0.24/iterative-phase-estimation-bugfix-b676ffc23cea8251.yaml diff --git a/releasenotes/notes/new-constructor-target-from-configuration-91f7eb569d95b330.yaml b/releasenotes/notes/0.24/new-constructor-target-from-configuration-91f7eb569d95b330.yaml similarity index 100% rename from releasenotes/notes/new-constructor-target-from-configuration-91f7eb569d95b330.yaml rename to releasenotes/notes/0.24/new-constructor-target-from-configuration-91f7eb569d95b330.yaml diff --git a/releasenotes/notes/new-deprecation-utilities-066aff05e221d7b1.yaml b/releasenotes/notes/0.24/new-deprecation-utilities-066aff05e221d7b1.yaml similarity index 100% rename from releasenotes/notes/new-deprecation-utilities-066aff05e221d7b1.yaml rename to releasenotes/notes/0.24/new-deprecation-utilities-066aff05e221d7b1.yaml diff --git a/releasenotes/notes/operator-apply-permutation-c113c05513cb7881.yaml b/releasenotes/notes/0.24/operator-apply-permutation-c113c05513cb7881.yaml similarity index 100% rename from releasenotes/notes/operator-apply-permutation-c113c05513cb7881.yaml rename to releasenotes/notes/0.24/operator-apply-permutation-c113c05513cb7881.yaml diff --git a/releasenotes/notes/options-implement-mutable-mapping-b11f4e2c6df4cf31.yaml b/releasenotes/notes/0.24/options-implement-mutable-mapping-b11f4e2c6df4cf31.yaml similarity index 100% rename from releasenotes/notes/options-implement-mutable-mapping-b11f4e2c6df4cf31.yaml rename to releasenotes/notes/0.24/options-implement-mutable-mapping-b11f4e2c6df4cf31.yaml diff --git a/releasenotes/notes/paulilist-do-not-broadcast-from-paulis-96de3832fba21b94.yaml b/releasenotes/notes/0.24/paulilist-do-not-broadcast-from-paulis-96de3832fba21b94.yaml similarity index 100% rename from releasenotes/notes/paulilist-do-not-broadcast-from-paulis-96de3832fba21b94.yaml rename to releasenotes/notes/0.24/paulilist-do-not-broadcast-from-paulis-96de3832fba21b94.yaml diff --git a/releasenotes/notes/primitive-job-submit-a7633872e2ae3c7b.yaml b/releasenotes/notes/0.24/primitive-job-submit-a7633872e2ae3c7b.yaml similarity index 100% rename from releasenotes/notes/primitive-job-submit-a7633872e2ae3c7b.yaml rename to releasenotes/notes/0.24/primitive-job-submit-a7633872e2ae3c7b.yaml diff --git a/releasenotes/notes/qasm2-exporter-rewrite-8993dd24f930b180.yaml b/releasenotes/notes/0.24/qasm2-exporter-rewrite-8993dd24f930b180.yaml similarity index 100% rename from releasenotes/notes/qasm2-exporter-rewrite-8993dd24f930b180.yaml rename to releasenotes/notes/0.24/qasm2-exporter-rewrite-8993dd24f930b180.yaml diff --git a/releasenotes/notes/qasm2-parser-rust-ecf6570e2d445a94.yaml b/releasenotes/notes/0.24/qasm2-parser-rust-ecf6570e2d445a94.yaml similarity index 100% rename from releasenotes/notes/qasm2-parser-rust-ecf6570e2d445a94.yaml rename to releasenotes/notes/0.24/qasm2-parser-rust-ecf6570e2d445a94.yaml diff --git a/releasenotes/notes/qnspsa-float-bug-fix-4035f7e1eb61dec2.yaml b/releasenotes/notes/0.24/qnspsa-float-bug-fix-4035f7e1eb61dec2.yaml similarity index 100% rename from releasenotes/notes/qnspsa-float-bug-fix-4035f7e1eb61dec2.yaml rename to releasenotes/notes/0.24/qnspsa-float-bug-fix-4035f7e1eb61dec2.yaml diff --git a/releasenotes/notes/rearrange-gradient-result-order-1596e14db01395f5.yaml b/releasenotes/notes/0.24/rearrange-gradient-result-order-1596e14db01395f5.yaml similarity index 100% rename from releasenotes/notes/rearrange-gradient-result-order-1596e14db01395f5.yaml rename to releasenotes/notes/0.24/rearrange-gradient-result-order-1596e14db01395f5.yaml diff --git a/releasenotes/notes/remove-deprecated-factorizers-linear-solvers-4631870129749624.yaml b/releasenotes/notes/0.24/remove-deprecated-factorizers-linear-solvers-4631870129749624.yaml similarity index 100% rename from releasenotes/notes/remove-deprecated-factorizers-linear-solvers-4631870129749624.yaml rename to releasenotes/notes/0.24/remove-deprecated-factorizers-linear-solvers-4631870129749624.yaml diff --git a/releasenotes/notes/remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml b/releasenotes/notes/0.24/remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml similarity index 100% rename from releasenotes/notes/remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml rename to releasenotes/notes/0.24/remove-execute_function-max_credits-8c822b8b4b3d84ba.yaml diff --git a/releasenotes/notes/remove-faulty-qubits-support-00850f69185c365e.yaml b/releasenotes/notes/0.24/remove-faulty-qubits-support-00850f69185c365e.yaml similarity index 100% rename from releasenotes/notes/remove-faulty-qubits-support-00850f69185c365e.yaml rename to releasenotes/notes/0.24/remove-faulty-qubits-support-00850f69185c365e.yaml diff --git a/releasenotes/notes/rename-fake-backends-b08f8a66bc19088b.yaml b/releasenotes/notes/0.24/rename-fake-backends-b08f8a66bc19088b.yaml similarity index 100% rename from releasenotes/notes/rename-fake-backends-b08f8a66bc19088b.yaml rename to releasenotes/notes/0.24/rename-fake-backends-b08f8a66bc19088b.yaml diff --git a/releasenotes/notes/rename-graysynth-3fa4fcb7d096ab35.yaml b/releasenotes/notes/0.24/rename-graysynth-3fa4fcb7d096ab35.yaml similarity index 100% rename from releasenotes/notes/rename-graysynth-3fa4fcb7d096ab35.yaml rename to releasenotes/notes/0.24/rename-graysynth-3fa4fcb7d096ab35.yaml diff --git a/releasenotes/notes/sabre-sort-rng-056f26f205e38bab.yaml b/releasenotes/notes/0.24/sabre-sort-rng-056f26f205e38bab.yaml similarity index 100% rename from releasenotes/notes/sabre-sort-rng-056f26f205e38bab.yaml rename to releasenotes/notes/0.24/sabre-sort-rng-056f26f205e38bab.yaml diff --git a/releasenotes/notes/sparsepauliop-improved-parameter-support-413f7598bac72166.yaml b/releasenotes/notes/0.24/sparsepauliop-improved-parameter-support-413f7598bac72166.yaml similarity index 100% rename from releasenotes/notes/sparsepauliop-improved-parameter-support-413f7598bac72166.yaml rename to releasenotes/notes/0.24/sparsepauliop-improved-parameter-support-413f7598bac72166.yaml diff --git a/releasenotes/notes/speedup-one-qubit-optimize-pass-483429af948a415e.yaml b/releasenotes/notes/0.24/speedup-one-qubit-optimize-pass-483429af948a415e.yaml similarity index 100% rename from releasenotes/notes/speedup-one-qubit-optimize-pass-483429af948a415e.yaml rename to releasenotes/notes/0.24/speedup-one-qubit-optimize-pass-483429af948a415e.yaml diff --git a/releasenotes/notes/stabilizer_state_synthesis-c48c0389941715a6.yaml b/releasenotes/notes/0.24/stabilizer_state_synthesis-c48c0389941715a6.yaml similarity index 100% rename from releasenotes/notes/stabilizer_state_synthesis-c48c0389941715a6.yaml rename to releasenotes/notes/0.24/stabilizer_state_synthesis-c48c0389941715a6.yaml diff --git a/releasenotes/notes/staged-pass-manager-drawer-f1da0308895a30d9.yaml b/releasenotes/notes/0.24/staged-pass-manager-drawer-f1da0308895a30d9.yaml similarity index 100% rename from releasenotes/notes/staged-pass-manager-drawer-f1da0308895a30d9.yaml rename to releasenotes/notes/0.24/staged-pass-manager-drawer-f1da0308895a30d9.yaml diff --git a/releasenotes/notes/su2-synthesis-295ebf03d9033263.yaml b/releasenotes/notes/0.24/su2-synthesis-295ebf03d9033263.yaml similarity index 100% rename from releasenotes/notes/su2-synthesis-295ebf03d9033263.yaml rename to releasenotes/notes/0.24/su2-synthesis-295ebf03d9033263.yaml diff --git a/releasenotes/notes/support-disjoint-cmap-sabre-551ae4295131a449.yaml b/releasenotes/notes/0.24/support-disjoint-cmap-sabre-551ae4295131a449.yaml similarity index 100% rename from releasenotes/notes/support-disjoint-cmap-sabre-551ae4295131a449.yaml rename to releasenotes/notes/0.24/support-disjoint-cmap-sabre-551ae4295131a449.yaml diff --git a/releasenotes/notes/switch-case-9b6611d0603d36c0.yaml b/releasenotes/notes/0.24/switch-case-9b6611d0603d36c0.yaml similarity index 100% rename from releasenotes/notes/switch-case-9b6611d0603d36c0.yaml rename to releasenotes/notes/0.24/switch-case-9b6611d0603d36c0.yaml diff --git a/releasenotes/notes/symbolic-pulse-complex-deprecation-89ecdf968b1a2d89.yaml b/releasenotes/notes/0.24/symbolic-pulse-complex-deprecation-89ecdf968b1a2d89.yaml similarity index 100% rename from releasenotes/notes/symbolic-pulse-complex-deprecation-89ecdf968b1a2d89.yaml rename to releasenotes/notes/0.24/symbolic-pulse-complex-deprecation-89ecdf968b1a2d89.yaml diff --git a/releasenotes/notes/target-aware-layout-routing-2b39bd87a9f928e7.yaml b/releasenotes/notes/0.24/target-aware-layout-routing-2b39bd87a9f928e7.yaml similarity index 100% rename from releasenotes/notes/target-aware-layout-routing-2b39bd87a9f928e7.yaml rename to releasenotes/notes/0.24/target-aware-layout-routing-2b39bd87a9f928e7.yaml diff --git a/releasenotes/notes/target-transpile-d029922a5dbc3a52.yaml b/releasenotes/notes/0.24/target-transpile-d029922a5dbc3a52.yaml similarity index 100% rename from releasenotes/notes/target-transpile-d029922a5dbc3a52.yaml rename to releasenotes/notes/0.24/target-transpile-d029922a5dbc3a52.yaml diff --git a/releasenotes/notes/transpile-coupling-maps-3a137f4ca8e97745.yaml b/releasenotes/notes/0.24/transpile-coupling-maps-3a137f4ca8e97745.yaml similarity index 100% rename from releasenotes/notes/transpile-coupling-maps-3a137f4ca8e97745.yaml rename to releasenotes/notes/0.24/transpile-coupling-maps-3a137f4ca8e97745.yaml diff --git a/releasenotes/notes/trotter-time-dep-hamiltonians-8c6c3d5f629e72fb.yaml b/releasenotes/notes/0.24/trotter-time-dep-hamiltonians-8c6c3d5f629e72fb.yaml similarity index 100% rename from releasenotes/notes/trotter-time-dep-hamiltonians-8c6c3d5f629e72fb.yaml rename to releasenotes/notes/0.24/trotter-time-dep-hamiltonians-8c6c3d5f629e72fb.yaml diff --git a/releasenotes/notes/unitary-synthesis-based-on-errors-8041fcc9584f5db2.yaml b/releasenotes/notes/0.24/unitary-synthesis-based-on-errors-8041fcc9584f5db2.yaml similarity index 100% rename from releasenotes/notes/unitary-synthesis-based-on-errors-8041fcc9584f5db2.yaml rename to releasenotes/notes/0.24/unitary-synthesis-based-on-errors-8041fcc9584f5db2.yaml diff --git a/releasenotes/notes/unroll-forloops-7bf8000620f738e7.yaml b/releasenotes/notes/0.24/unroll-forloops-7bf8000620f738e7.yaml similarity index 100% rename from releasenotes/notes/unroll-forloops-7bf8000620f738e7.yaml rename to releasenotes/notes/0.24/unroll-forloops-7bf8000620f738e7.yaml diff --git a/releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml b/releasenotes/notes/0.24/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml similarity index 100% rename from releasenotes/notes/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml rename to releasenotes/notes/0.24/update-pulse-gate-pass-for-target-ebfb0ec9571f058e.yaml diff --git a/releasenotes/notes/update-state-visualization-6836bd53e3a24891.yaml b/releasenotes/notes/0.24/update-state-visualization-6836bd53e3a24891.yaml similarity index 100% rename from releasenotes/notes/update-state-visualization-6836bd53e3a24891.yaml rename to releasenotes/notes/0.24/update-state-visualization-6836bd53e3a24891.yaml diff --git a/releasenotes/notes/vf2-post-layout-max-trials-98b1c736e2e33861.yaml b/releasenotes/notes/0.24/vf2-post-layout-max-trials-98b1c736e2e33861.yaml similarity index 100% rename from releasenotes/notes/vf2-post-layout-max-trials-98b1c736e2e33861.yaml rename to releasenotes/notes/0.24/vf2-post-layout-max-trials-98b1c736e2e33861.yaml diff --git a/releasenotes/notes/vf2postlayout-fix-16bb54d9bdf3aaf6.yaml b/releasenotes/notes/0.24/vf2postlayout-fix-16bb54d9bdf3aaf6.yaml similarity index 100% rename from releasenotes/notes/vf2postlayout-fix-16bb54d9bdf3aaf6.yaml rename to releasenotes/notes/0.24/vf2postlayout-fix-16bb54d9bdf3aaf6.yaml diff --git a/releasenotes/notes/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml b/releasenotes/notes/0.24/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml similarity index 100% rename from releasenotes/notes/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml rename to releasenotes/notes/0.24/vqd-list-initial-points-list-optimizers-033d7439f86bbb71.yaml diff --git a/setup.py b/setup.py index ec447e6d250c..16beca8f75c1 100755 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ setup( name="qiskit-terra", - version="0.24.0", + version="0.24.0rc1", description="Software for developing quantum computing programs", long_description=README, long_description_content_type="text/markdown", From 908adb34323682cf48a8bbc711d01f651dd26932 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 20 Apr 2023 17:58:50 -0400 Subject: [PATCH 060/172] Bump main branch version post 0.24.0rc1 tag (#10005) Now that the first release candidate for the 0.24.0 release has been tagged, the stable branch for the 0.24 series has been created and we can start developing the 0.25.0 release on main. This commit bumps all the version strings from 0.24.0 to 0.25.0 (and the backport branch for mergify to 0.24) accordingly to differentiate the main branch from 0.24.*. --- .mergify.yml | 2 +- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/accelerate/Cargo.toml | 2 +- crates/qasm2/Cargo.toml | 2 +- docs/conf.py | 4 ++-- qiskit/VERSION.txt | 2 +- setup.py | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.mergify.yml b/.mergify.yml index d08e96864f67..1b37ce208d58 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -6,4 +6,4 @@ pull_request_rules: actions: backport: branches: - - stable/0.23 + - stable/0.24 diff --git a/Cargo.lock b/Cargo.lock index 2035c1a9e5a5..f5528c202c0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -410,7 +410,7 @@ dependencies = [ [[package]] name = "qiskit-qasm2" -version = "0.24.0" +version = "0.25.0" dependencies = [ "hashbrown 0.13.2", "pyo3", @@ -418,7 +418,7 @@ dependencies = [ [[package]] name = "qiskit_accelerate" -version = "0.24.0" +version = "0.25.0" dependencies = [ "ahash 0.8.3", "hashbrown 0.13.2", diff --git a/Cargo.toml b/Cargo.toml index ece643e5c8fe..823ac3a6b80a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ members = ["crates/*"] # to inherit from this rather than needing to duplicate its content themselves. Until we get there, # keep this in sync with each subpackage `Cargo.toml`'s `package` key. [workspace.package] -version = "0.24.0" +version = "0.25.0" edition = "2021" rust-version = "1.61" # Keep in sync with README.md and rust-toolchain.toml. license = "Apache-2.0" diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 602574ab0b55..7f057196b6a0 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -2,7 +2,7 @@ name = "qiskit_accelerate" # The following options can be inherited with (e.g.) `version.workspace = true` once we hit Rust # 1.64. Until then, keep in sync with the root `Cargo.toml`. -version = "0.24.0" +version = "0.25.0" edition = "2021" rust-version = "1.61" license = "Apache-2.0" diff --git a/crates/qasm2/Cargo.toml b/crates/qasm2/Cargo.toml index cc27943133af..67b567ea1121 100644 --- a/crates/qasm2/Cargo.toml +++ b/crates/qasm2/Cargo.toml @@ -2,7 +2,7 @@ name = "qiskit-qasm2" # The following options can be inherited with (e.g.) `version.workspace = true` once we hit Rust # 1.64. Until then, keep in sync with the root `Cargo.toml`. -version = "0.24.0" +version = "0.25.0" edition = "2021" rust-version = "1.61" license = "Apache-2.0" diff --git a/docs/conf.py b/docs/conf.py index cbd58b90e021..b443e895e9d0 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -23,9 +23,9 @@ author = "Qiskit Development Team" # The short X.Y version -version = "0.24" +version = "0.25" # The full version, including alpha/beta/rc tags -release = "0.24.0" +release = "0.25.0" extensions = [ "sphinx.ext.napoleon", diff --git a/qiskit/VERSION.txt b/qiskit/VERSION.txt index 24f24553095f..d21d277be513 100644 --- a/qiskit/VERSION.txt +++ b/qiskit/VERSION.txt @@ -1 +1 @@ -0.24.0rc1 +0.25.0 diff --git a/setup.py b/setup.py index 16beca8f75c1..8e43ffeef3c9 100755 --- a/setup.py +++ b/setup.py @@ -58,7 +58,7 @@ setup( name="qiskit-terra", - version="0.24.0rc1", + version="0.25.0", description="Software for developing quantum computing programs", long_description=README, long_description_content_type="text/markdown", From 73c1e1c4d020ddc0ed68910a2d30bfd78e2f1647 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 21 Apr 2023 14:46:21 +0100 Subject: [PATCH 061/172] Upgrade straggler GitHub Actions to Node 16 (#10010) * Upgrade straggler GitHub Actions to Node 16 The Node 12 runners are due to be removed from GitHub Actions this summer. We updated most of them in 5d977fbf (gh-8856), but a couple remained un-updated. The `actions-rs/toolchain` action is unmaintained. The replacement used in this commit is actively updated, and has mostly the same interface, although the Rust version is typically specified using the action version. * Reinstate Rust toolchain override --- .github/workflows/coverage.yml | 9 +++++---- .github/workflows/wheels.yml | 12 +++--------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 40e4d9332773..57c18b744ecf 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,6 +12,10 @@ jobs: coverage: name: Coverage runs-on: ubuntu-latest + env: + # Override the `rust-toolchain.toml` file because `grcov` has greater requirements. + RUSTUP_TOOLCHAIN: stable + steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 @@ -20,11 +24,8 @@ jobs: python-version: '3.8' - name: Install Rust toolchain - uses: actions-rs/toolchain@v1 + uses: dtolnay/rust-toolchain@stable with: - toolchain: stable - override: true - profile: default components: llvm-tools-preview - name: Install dependencies diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index d0caabccda74..c94dd428d870 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -18,9 +18,7 @@ jobs: name: Install Python with: python-version: '3.7' - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable + - uses: dtolnay/rust-toolchain@stable - name: Set up QEMU uses: docker/setup-qemu-action@v1 with: @@ -53,9 +51,7 @@ jobs: name: Install Python with: python-version: '3.7' - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable + - uses: dtolnay/rust-toolchain@stable - name: Set up QEMU uses: docker/setup-qemu-action@v1 with: @@ -88,9 +84,7 @@ jobs: name: Install Python with: python-version: '3.7' - - uses: actions-rs/toolchain@v1 - with: - toolchain: stable + - uses: dtolnay/rust-toolchain@stable - name: Set up QEMU uses: docker/setup-qemu-action@v1 with: From da552b6591c73027f7fbdd0b27a016dbaf161e08 Mon Sep 17 00:00:00 2001 From: Amol Deshmukh <34318357+des137@users.noreply.github.com> Date: Mon, 24 Apr 2023 15:34:00 -0400 Subject: [PATCH 062/172] Update sx.py (#10023) Correct the latex rendering of 'pi' --- qiskit/circuit/library/standard_gates/sx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/library/standard_gates/sx.py b/qiskit/circuit/library/standard_gates/sx.py index 7bcc28b62861..170bde6d9e37 100644 --- a/qiskit/circuit/library/standard_gates/sx.py +++ b/qiskit/circuit/library/standard_gates/sx.py @@ -54,7 +54,7 @@ class SXGate(Gate): 1 & -i \\ -i & 1 \end{pmatrix} - = e^{-i pi/4} \sqrt{X} + = e^{-i \pi/4} \sqrt{X} """ From df88a657e2953e92f2fe19e35a927f05c7402684 Mon Sep 17 00:00:00 2001 From: Tommy Chu Date: Mon, 24 Apr 2023 22:02:09 +0200 Subject: [PATCH 063/172] Fix typo (#10019) --- qiskit/visualization/gate_map.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index dac97554fd07..da270e8b0718 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -749,7 +749,7 @@ def plot_circuit_layout(circuit, backend, view="virtual", qubit_coordinates=None view (str): Layout view: either 'virtual' or 'physical'. 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 much mast the number of qubits on the backend. The sequence + sequence must match the number of qubits on the backend. The sequence should be the planar coordinates in a 0-based square grid where each qubit is located. From 4561cb6fff69b86a887ad5b0b8c17b0bd2539970 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 25 Apr 2023 11:13:13 -0400 Subject: [PATCH 064/172] Don't run both ZSX and ZSXX basis in Optimize1qGatesDecomposition (#10021) Currently in the Optimize1qGatesDecomposition transpielr pass we build a list of valid bases for the decomposer to synthesize the 1q unitary for. However, in the case of the overlapping bases ZSX and ZSXX if ZSXX is valid we end up doing the same work twice for no reason. ZSX and ZSXX produce the same basic output except ZSXX will always be the same or more efficient if multiple sx gates can be simplified to an x gate. So there is no need for us to run 2 decomposers in that case. This commit updates the pass to only run ZSXX if both ZSX and ZSXX are valid bases to use. --- .../passes/optimization/optimize_1q_decomposition.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py index 28caccc19340..e400dc3321e2 100644 --- a/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py +++ b/qiskit/transpiler/passes/optimization/optimize_1q_decomposition.py @@ -243,6 +243,16 @@ def _possible_decomposers(basis_set): for euler_basis_name, gates in euler_basis_gates.items(): if set(gates).issubset(basis_set): decomposers.append(euler_basis_name) + # If both U3 and U321 are in decomposer list only run U321 because + # in worst case it will produce the same U3 output, but in the general + # case it will use U2 and U1 which will be more efficient. if "U3" in decomposers and "U321" in decomposers: decomposers.remove("U3") + # If both ZSX and ZSXX are in decomposer list only run ZSXX because + # in the worst case it will produce the same output, but in the general + # case it will simplify X rotations to use X gate instead of multiple + # SX gates and be more efficient. Running multiple decomposers in this + # case will just waste time. + if "ZSX" in decomposers and "ZSXX" in decomposers: + decomposers.remove("ZSX") return decomposers From 7bcfb0bf20b05496e022a001ef00ce205b75f966 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 25 Apr 2023 11:14:48 -0400 Subject: [PATCH 065/172] Use backend_compat module's convert to target (#10016) This commit fixes an issue with the fake backend classes built on the BackendV2 that used ECR or CZ (or any 2q gate besides CX) as gates. The fundamental issue was the fake backend class had a hardcoded dict mapping gate names to classes to use for generating a target. This prevents the correct gate class from being reported when a backend uses gates outside of that dict. To fix this issue this commit switches the fake backends to use the convert_to_target function which is part of the BackendConverterV2 class. That function provides the same functionality as the one used internally by the fake provider except it is more general as its designed to work with any backend. At the time of its introduction this unification wasn't done because the fake backend's version would be more efficient because it doesn't generate unused intermediate object forms. But, a small loading time performance regression against correct operation and deduplicating code paths using the more general function is a better choice. In a future commit we can deprecate and remove the fake backend version of convert_to_target. Fixes #10004 Fixes #9983 --- qiskit/providers/backend_compat.py | 12 ++++++++++-- qiskit/providers/fake_provider/fake_backend.py | 16 +++++++++++----- test/python/providers/test_fake_backends.py | 10 ++++++++++ 3 files changed, 31 insertions(+), 7 deletions(-) diff --git a/qiskit/providers/backend_compat.py b/qiskit/providers/backend_compat.py index 524df5498bd2..d67c99201648 100644 --- a/qiskit/providers/backend_compat.py +++ b/qiskit/providers/backend_compat.py @@ -104,9 +104,17 @@ def convert_to_target( if filter_faulty: if not properties.is_qubit_operational(qubit): continue + try: + duration = properties.readout_length(qubit) + except BackendPropertyError: + duration = None + try: + error = properties.readout_error(qubit) + except BackendPropertyError: + error = None measure_props[(qubit,)] = InstructionProperties( - duration=properties.readout_length(qubit), - error=properties.readout_error(qubit), + duration=duration, + error=error, ) target.add_instruction(Measure(), measure_props) # Parse from configuration because properties doesn't exist diff --git a/qiskit/providers/fake_provider/fake_backend.py b/qiskit/providers/fake_provider/fake_backend.py index cc538e5b581f..54aa272c8f4b 100644 --- a/qiskit/providers/fake_provider/fake_backend.py +++ b/qiskit/providers/fake_provider/fake_backend.py @@ -25,20 +25,20 @@ from typing import List, Iterable from qiskit import circuit -from qiskit.providers.models import BackendProperties +from qiskit.providers.models import BackendProperties, BackendConfiguration, PulseDefaults from qiskit.providers import BackendV2, BackendV1 from qiskit import pulse from qiskit.exceptions import QiskitError from qiskit.utils import optionals as _optionals from qiskit.providers import basicaer from qiskit.transpiler import Target +from qiskit.providers.backend_compat import convert_to_target from .utils.json_decoder import ( decode_backend_configuration, decode_backend_properties, decode_pulse_defaults, ) -from .utils.backend_converter import convert_to_target class _Credentials: @@ -168,10 +168,16 @@ def target(self) -> Target: self._set_props_dict_from_json() if self._defs_dict is None: self._set_defs_dict_from_json() + conf = BackendConfiguration.from_dict(self._conf_dict) + props = None + if self._props_dict is not None: + props = BackendProperties.from_dict(self._props_dict) + defaults = None + if self._defs_dict is not None: + defaults = PulseDefaults.from_dict(self._defs_dict) + self._target = convert_to_target( - conf_dict=self._conf_dict, - props_dict=self._props_dict, - defs_dict=self._defs_dict, + conf, props, defaults, add_delay=True, filter_faulty=True ) return self._target diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index 843e4a0d9e05..4915763da85a 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -34,6 +34,8 @@ FakeYorktown, FakeMumbai, FakeWashington, + FakeSherbrooke, + FakePrague, ) from qiskit.providers.backend_compat import BackendV2Converter from qiskit.providers.models.backendproperties import BackendProperties @@ -55,6 +57,8 @@ RGate, MCXGrayCode, RYGate, + CZGate, + ECRGate, ) from qiskit.circuit import ControlledGate, Parameter from qiskit.quantum_info.operators.channel.quantum_channel import QuantumChannel @@ -230,6 +234,12 @@ def test_converter_delay_circuit(self): res = transpile(qc, backend_v2) self.assertIn("delay", res.count_ops()) + def test_non_cx_tests(self): + backend = FakePrague() + self.assertIsInstance(backend.target.operation_from_name("cz"), CZGate) + backend = FakeSherbrooke() + self.assertIsInstance(backend.target.operation_from_name("ecr"), ECRGate) + @unittest.skipUnless(optionals.HAS_AER, "Aer required for this test") def test_converter_simulator(self): class MCSXGate(ControlledGate): From f4548daad20c8de82da4858eace084f7622d669d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 25 Apr 2023 11:16:01 -0400 Subject: [PATCH 066/172] Fix qubit filtering in BackendV2Converter with add_delay (#10015) This commit fixes an issue with the filter_faulty and add_delay flag were both set on the BackendV2Converter and convert_to_target flag. In these cases the function would incorrectly add delay operations to the faulty qubits even though they should have no operations preset on them. This commit fixes the oversight to ensure that no operations are present on a qubit if it's listed as non-operational in an input BackendProperties. --- qiskit/providers/backend_compat.py | 9 +++++---- test/python/providers/test_fake_backends.py | 21 +++++++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/qiskit/providers/backend_compat.py b/qiskit/providers/backend_compat.py index d67c99201648..b2f0cb3ce56d 100644 --- a/qiskit/providers/backend_compat.py +++ b/qiskit/providers/backend_compat.py @@ -64,8 +64,11 @@ def convert_to_target( target = None if custom_name_mapping is not None: name_mapping.update(custom_name_mapping) + faulty_qubits = set() # Parse from properties if it exsits if properties is not None: + if filter_faulty: + faulty_qubits = set(properties.faulty_qubits()) qubit_properties = qubit_props_list_from_props(properties=properties) target = Target(num_qubits=configuration.n_qubits, qubit_properties=qubit_properties) # Parse instructions @@ -145,9 +148,6 @@ def convert_to_target( target.acquire_alignment = configuration.timing_constraints.get("acquire_alignment") # If a pulse defaults exists use that as the source of truth if defaults is not None: - faulty_qubits = set() - if filter_faulty and properties is not None: - faulty_qubits = set(properties.faulty_qubits()) inst_map = defaults.instruction_schedule_map for inst in inst_map.instructions: for qarg in inst_map.qubits_with_instruction(inst): @@ -182,7 +182,8 @@ def convert_to_target( ) if add_delay and "delay" not in target: target.add_instruction( - name_mapping["delay"], {(bit,): None for bit in range(target.num_qubits)} + name_mapping["delay"], + {(bit,): None for bit in range(target.num_qubits) if bit not in faulty_qubits}, ) return target diff --git a/test/python/providers/test_fake_backends.py b/test/python/providers/test_fake_backends.py index 4915763da85a..e0e337e7cd96 100644 --- a/test/python/providers/test_fake_backends.py +++ b/test/python/providers/test_fake_backends.py @@ -504,6 +504,27 @@ def test_filter_faulty_qubits_backend_v2_converter(self): for qarg in v2_backend.target.qargs: self.assertNotIn(i, qarg) + def test_filter_faulty_qubits_backend_v2_converter_with_delay(self): + """Test faulty qubits in v2 conversion.""" + backend = FakeWashington() + # Get properties dict to make it easier to work with the properties API + # is difficult to edit because of the multiple layers of nesting and + # different object types + props_dict = backend.properties().to_dict() + for i in range(62, 67): + non_operational = { + "date": datetime.datetime.utcnow(), + "name": "operational", + "unit": "", + "value": 0, + } + props_dict["qubits"][i].append(non_operational) + backend._properties = BackendProperties.from_dict(props_dict) + v2_backend = BackendV2Converter(backend, filter_faulty=True, add_delay=True) + for i in range(62, 67): + for qarg in v2_backend.target.qargs: + self.assertNotIn(i, qarg) + def test_filter_faulty_qubits_and_gates_backend_v2_converter(self): """Test faulty gates and qubits.""" backend = FakeWashington() From d69e5f5507231702b35c10451a40b5f72e217b2e Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Tue, 25 Apr 2023 11:17:59 -0400 Subject: [PATCH 067/172] Upgrade Sphinx theme to 1.11 (#9978) --- requirements-dev.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 1403ea993ac3..6d83ac76abbd 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -17,7 +17,7 @@ ddt>=1.2.0,!=1.4.0,!=1.4.3 seaborn>=0.9.0 reno>=3.4.0 Sphinx>=5.0 -qiskit-sphinx-theme~=1.10.3 +qiskit-sphinx-theme~=1.11.0 sphinx-design>=0.2.0 pygments>=2.4 scikit-learn>=0.20.0 From 116575a8cb7a3f4a4a9b4098fa796b5c0bceb142 Mon Sep 17 00:00:00 2001 From: Adenilton Silva <7927558+adjs@users.noreply.github.com> Date: Tue, 25 Apr 2023 12:33:36 -0300 Subject: [PATCH 068/172] Update test_controlled_gate.py (#9830) --- test/python/circuit/test_controlled_gate.py | 23 +++++++-------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 702a500c1fe7..66f829cb9192 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -286,7 +286,7 @@ def test_multi_controlled_composite_gate(self): op_mat = Operator(cgate).data cop_mat = _compute_control_matrix(op_mat, num_ctrl) ref_mat = Operator(qc).data - self.assertTrue(matrix_equal(cop_mat, ref_mat, ignore_phase=True)) + self.assertTrue(matrix_equal(cop_mat, ref_mat)) def test_single_controlled_composite_gate(self): """Test a singly controlled composite gate.""" @@ -306,7 +306,7 @@ def test_single_controlled_composite_gate(self): op_mat = Operator(cgate).data cop_mat = _compute_control_matrix(op_mat, num_ctrl) ref_mat = Operator(qc).data - self.assertTrue(matrix_equal(cop_mat, ref_mat, ignore_phase=True)) + self.assertTrue(matrix_equal(cop_mat, ref_mat)) def test_control_open_controlled_gate(self): """Test control(2) vs control.control where inner gate has open controls.""" @@ -389,9 +389,7 @@ def test_multi_control_u3(self): for itest in tests: info, target, decomp = itest[0], itest[1], itest[2] with self.subTest(i=info): - self.assertTrue( - matrix_equal(target, decomp, ignore_phase=True, atol=1e-8, rtol=1e-5) - ) + self.assertTrue(matrix_equal(target, decomp, atol=1e-8, rtol=1e-5)) def test_multi_control_u1(self): """Test the matrix representation of the controlled and controlled-controlled U1 gate.""" @@ -458,7 +456,7 @@ def test_multi_control_u1(self): info, target, decomp = itest[0], itest[1], itest[2] with self.subTest(i=info): self.log.info(info) - self.assertTrue(matrix_equal(target, decomp, ignore_phase=True)) + self.assertTrue(matrix_equal(target, decomp)) @data(1, 2, 3, 4) def test_multi_controlled_u1_matrix(self, num_controls): @@ -838,7 +836,7 @@ def test_controlled_unitary(self, num_ctrl_qubits): test_op = Operator(cgate) cop_mat = _compute_control_matrix(base_mat, num_ctrl_qubits) self.assertTrue(is_unitary_matrix(base_mat)) - self.assertTrue(matrix_equal(cop_mat, test_op.data, ignore_phase=True)) + self.assertTrue(matrix_equal(cop_mat, test_op.data)) @data(1, 2, 3, 4, 5) def test_controlled_random_unitary(self, num_ctrl_qubits): @@ -849,7 +847,7 @@ def test_controlled_random_unitary(self, num_ctrl_qubits): cgate = base_gate.control(num_ctrl_qubits) test_op = Operator(cgate) cop_mat = _compute_control_matrix(base_mat, num_ctrl_qubits) - self.assertTrue(matrix_equal(cop_mat, test_op.data, ignore_phase=True)) + self.assertTrue(matrix_equal(cop_mat, test_op.data)) @combine(num_ctrl_qubits=[1, 2, 3], ctrl_state=[0, None]) def test_open_controlled_unitary_z(self, num_ctrl_qubits, ctrl_state): @@ -1368,7 +1366,7 @@ def test_single_controlled_rotation_gates(self, gate, cgate): op_mat = Operator(gate).data ref_mat = Operator(cgate).data cop_mat = _compute_control_matrix(op_mat, self.num_ctrl) - self.assertTrue(matrix_equal(cop_mat, ref_mat, ignore_phase=True)) + self.assertTrue(matrix_equal(cop_mat, ref_mat)) cqc = QuantumCircuit(self.num_ctrl + self.num_target) cqc.append(cgate, cqc.qregs[0]) dag = circuit_to_dag(cqc) @@ -1436,12 +1434,7 @@ def test_controlled_standard_gates(self, num_ctrl_qubits, gate_class): # 'object has no attribute "control"' # skipping Id and Barrier continue - if gate.name == "rz": - iden = Operator.from_label("I") - zgen = Operator.from_label("Z") - base_mat = (np.cos(0.5 * theta) * iden - 1j * np.sin(0.5 * theta) * zgen).data - else: - base_mat = Operator(gate).data + base_mat = Operator(gate).data target_mat = _compute_control_matrix( base_mat, num_ctrl_qubits, ctrl_state=ctrl_state From 117d188257a8ec030eb35a2636a0508e48a6640a Mon Sep 17 00:00:00 2001 From: Toshinari Itoko <15028342+itoko@users.noreply.github.com> Date: Wed, 26 Apr 2023 00:36:47 +0900 Subject: [PATCH 069/172] Fix delay padding to respect target's constraints (#10007) * Add and update tests * Fix padding passes to respect target's constraints * Fix transpile with scheduling to respect target's constraints * Add release note * fix reno * use target.instruction_supported * simplify * Add check if all DD gates are supported on each qubit * Add logging * Update DD tests * Make DD gates check target-aware * Fix legacy DD pass --- qiskit/transpiler/passes/scheduling/alap.py | 5 +- qiskit/transpiler/passes/scheduling/asap.py | 5 +- .../passes/scheduling/base_scheduler.py | 15 +++- .../passes/scheduling/dynamical_decoupling.py | 27 ++++-- .../passes/scheduling/padding/base_padding.py | 36 +++++++- .../padding/dynamical_decoupling.py | 43 +++++++-- .../passes/scheduling/padding/pad_delay.py | 8 +- .../transpiler/preset_passmanagers/common.py | 2 +- .../fix-delay-padding-75937bda37ebc3fd.yaml | 13 +++ test/python/compiler/test_transpiler.py | 48 ++++++++++ .../legacy_scheduling/test_scheduling_pass.py | 90 ++++++++++++++++++- .../transpiler/test_dynamical_decoupling.py | 61 +++++++++++++ .../test_scheduling_padding_pass.py | 17 ++++ 13 files changed, 343 insertions(+), 27 deletions(-) create mode 100644 releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index 935df96a063f..5059656058f3 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -137,7 +137,7 @@ def run(self, dag): for bit in node.qargs: delta = t0 - idle_before[bit] - if delta > 0: + if delta > 0 and self._delay_supported(bit_indices[bit]): new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) idle_before[bit] = t1 @@ -148,7 +148,8 @@ def run(self, dag): delta = circuit_duration - before if not (delta > 0 and isinstance(bit, Qubit)): continue - new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) + if self._delay_supported(bit_indices[bit]): + new_dag.apply_operation_front(Delay(delta, time_unit), [bit], []) new_dag.name = dag.name new_dag.metadata = dag.metadata diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index 331a0fcd42f7..b404e73d00a7 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -150,7 +150,7 @@ def run(self, dag): # Add delay to qubit wire for bit in node.qargs: delta = t0 - idle_after[bit] - if delta > 0 and isinstance(bit, Qubit): + if delta > 0 and isinstance(bit, Qubit) and self._delay_supported(bit_indices[bit]): new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) idle_after[bit] = t1 @@ -161,7 +161,8 @@ def run(self, dag): delta = circuit_duration - after if not (delta > 0 and isinstance(bit, Qubit)): continue - new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) + if self._delay_supported(bit_indices[bit]): + new_dag.apply_operation_back(Delay(delta, time_unit), [bit], []) new_dag.name = dag.name new_dag.metadata = dag.metadata diff --git a/qiskit/transpiler/passes/scheduling/base_scheduler.py b/qiskit/transpiler/passes/scheduling/base_scheduler.py index bc96aacc2d57..9f851d56b7e0 100644 --- a/qiskit/transpiler/passes/scheduling/base_scheduler.py +++ b/qiskit/transpiler/passes/scheduling/base_scheduler.py @@ -245,15 +245,16 @@ def __init__( """ super().__init__() self.durations = durations + # Ensure op node durations are attached and in consistent unit + if target is not None: + self.durations = target.durations() + self.requires.append(TimeUnitConversion(self.durations)) # Control flow constraints. self.clbit_write_latency = clbit_write_latency self.conditional_latency = conditional_latency - # Ensure op node durations are attached and in consistent unit - self.requires.append(TimeUnitConversion(durations)) - if target is not None: - self.durations = target.durations() + self.target = target @staticmethod def _get_node_duration( @@ -281,5 +282,11 @@ def _get_node_duration( return duration + def _delay_supported(self, qarg: int) -> bool: + """Delay operation is supported on the qubit (qarg) or not.""" + if self.target is None or self.target.instruction_supported("delay", qargs=(qarg,)): + return True + return False + def run(self, dag: DAGCircuit): raise NotImplementedError diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index 9dfeb8454161..fa0b2b29187e 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -16,8 +16,7 @@ import warnings import numpy as np -from qiskit.circuit.delay import Delay -from qiskit.circuit.reset import Reset +from qiskit.circuit import Gate, Delay, Reset from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate from qiskit.dagcircuit import DAGOpNode, DAGInNode from qiskit.quantum_info.operators.predicates import matrix_equal @@ -128,8 +127,14 @@ def __init__( self._qubits = qubits self._spacing = spacing self._skip_reset_qubits = skip_reset_qubits + self._target = target if target is not None: self._durations = target.durations() + for gate in dd_sequence: + if gate.name not in target.operation_names: + raise TranspilerError( + f"{gate.name} in dd_sequence is not supported in the target" + ) def run(self, dag): """Run the DynamicalDecoupling pass on dag. @@ -178,18 +183,22 @@ def run(self, dag): end = mid / 2 self._spacing = [end] + [mid] * (num_pulses - 1) + [end] - new_dag = dag.copy_empty_like() + for qarg in list(self._qubits): + for gate in self._dd_sequence: + if not self.__gate_supported(gate, qarg): + self._qubits.discard(qarg) + break - qubit_index_map = {qubit: index for index, qubit in enumerate(new_dag.qubits)} index_sequence_duration_map = {} - for qubit in new_dag.qubits: - physical_qubit = qubit_index_map[qubit] + for physical_qubit in self._qubits: dd_sequence_duration = 0 for gate in self._dd_sequence: gate.duration = self._durations.get(gate, physical_qubit) dd_sequence_duration += gate.duration index_sequence_duration_map[physical_qubit] = dd_sequence_duration + new_dag = dag.copy_empty_like() + qubit_index_map = {qubit: index for index, qubit in enumerate(new_dag.qubits)} for nd in dag.topological_op_nodes(): if not isinstance(nd.op, Delay): new_dag.apply_operation_back(nd.op, nd.qargs, nd.cargs) @@ -252,6 +261,12 @@ def run(self, dag): return new_dag + 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,)): + return True + return False + def _mod_2pi(angle: float, atol: float = 0): """Wrap angle into interval [-π,π). If within atol of the endpoint, clamp to -π""" diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index 0e3126d74f50..5fb25790cfbb 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -12,6 +12,7 @@ """Padding pass to fill empty timeslot.""" +import logging from typing import List, Optional, Union from qiskit.circuit import Qubit, Clbit, Instruction @@ -19,6 +20,9 @@ from qiskit.dagcircuit import DAGCircuit, DAGNode from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.target import Target + +logger = logging.getLogger(__name__) class BasePadding(TransformationPass): @@ -49,6 +53,20 @@ class BasePadding(TransformationPass): which may result in violation of hardware alignment constraints. """ + def __init__( + self, + target: Target = None, + ): + """BasePadding initializer. + + Args: + target: The :class:`~.Target` representing the target backend. + If it supplied and it does not support delay instruction on a qubit, + padding passes do not pad any idle time of the qubit. + """ + super().__init__() + self.target = target + def run(self, dag: DAGCircuit): """Run the padding pass on ``dag``. @@ -83,6 +101,7 @@ def run(self, dag: DAGCircuit): new_dag.calibrations = dag.calibrations new_dag.global_phase = dag.global_phase + bit_indices = {q: index for index, q in enumerate(dag.qubits)} idle_after = {bit: 0 for bit in dag.qubits} # Compute fresh circuit duration from the node start time dictionary and op duration. @@ -104,9 +123,8 @@ def run(self, dag: DAGCircuit): continue for bit in node.qargs: - # Fill idle time with some sequence - if t0 - idle_after[bit] > 0: + if t0 - idle_after[bit] > 0 and self.__delay_supported(bit_indices[bit]): # Find previous node on the wire, i.e. always the latest node on the wire prev_node = next(new_dag.predecessors(new_dag.output_map[bit])) self._pad( @@ -129,7 +147,7 @@ def run(self, dag: DAGCircuit): # Add delays until the end of circuit. for bit in new_dag.qubits: - if circuit_duration - idle_after[bit] > 0: + if circuit_duration - idle_after[bit] > 0 and self.__delay_supported(bit_indices[bit]): node = new_dag.output_map[bit] prev_node = next(new_dag.predecessors(node)) self._pad( @@ -145,6 +163,12 @@ def run(self, dag: DAGCircuit): return new_dag + def __delay_supported(self, qarg: int) -> bool: + """Delay operation is supported on the qubit (qarg) or not.""" + if self.target is None or self.target.instruction_supported("delay", qargs=(qarg,)): + return True + return False + def _pre_runhook(self, dag: DAGCircuit): """Extra routine inserted before running the padding pass. @@ -159,6 +183,12 @@ def _pre_runhook(self, dag: DAGCircuit): f"The input circuit {dag.name} is not scheduled. Call one of scheduling passes " f"before running the {self.__class__.__name__} pass." ) + for qarg, _ in enumerate(dag.qubits): + if not self.__delay_supported(qarg): + logger.debug( + "No padding on qubit %d as delay is not supported on it", + qarg, + ) def _apply_scheduled_op( self, diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index f0a24ea810a9..e3923ed8b26e 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -12,6 +12,7 @@ """Dynamical Decoupling insertion pass.""" +import logging from typing import List, Optional import numpy as np @@ -29,6 +30,8 @@ from .base_padding import BasePadding +logger = logging.getLogger(__name__) + class PadDynamicalDecoupling(BasePadding): """Dynamical decoupling insertion pass. @@ -152,7 +155,7 @@ def __init__( non-multiple of the alignment constraint value is found. TypeError: If ``dd_sequence`` is not specified """ - super().__init__() + super().__init__(target=target) self._durations = durations if dd_sequence is None: raise TypeError("required argument 'dd_sequence' is not specified") @@ -163,10 +166,16 @@ def __init__( self._spacing = spacing self._extra_slack_distribution = extra_slack_distribution + self._no_dd_qubits = set() self._dd_sequence_lengths = {} self._sequence_phase = 0 if target is not None: self._durations = target.durations() + for gate in dd_sequence: + if gate.name not in target.operation_names: + raise TranspilerError( + f"{gate.name} in dd_sequence is not supported in the target" + ) def _pre_runhook(self, dag: DAGCircuit): super()._pre_runhook(dag) @@ -200,10 +209,18 @@ def _pre_runhook(self, dag: DAGCircuit): raise TranspilerError("The DD sequence does not make an identity operation.") self._sequence_phase = np.angle(noop[0][0]) + # Compute no DD qubits on which any gate in dd_sequence is not supported in the target + for qarg, _ in enumerate(dag.qubits): + for gate in self._dd_sequence: + if not self.__gate_supported(gate, qarg): + self._no_dd_qubits.add(qarg) + logger.debug( + "No DD on qubit %d as gate %s is not supported on it", qarg, gate.name + ) + break # Precompute qubit-wise DD sequence length for performance - for qubit in dag.qubits: - physical_index = dag.qubits.index(qubit) - if self._qubits and physical_index not in self._qubits: + for physical_index, qubit in enumerate(dag.qubits): + if not self.__is_dd_qubit(physical_index): continue sequence_lengths = [] @@ -231,6 +248,20 @@ def _pre_runhook(self, dag: DAGCircuit): gate.duration = gate_length self._dd_sequence_lengths[qubit] = sequence_lengths + 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,)): + return True + return False + + def __is_dd_qubit(self, qubit_index: int) -> bool: + """DD can be inserted in the qubit or not.""" + if (qubit_index in self._no_dd_qubits) or ( + self._qubits and qubit_index not in self._qubits + ): + return False + return True + def _pad( self, dag: DAGCircuit, @@ -268,7 +299,7 @@ def _pad( # Y3: 288 dt + 160 dt + 80 dt = 528 dt (33 x 16 dt) # Y4: 368 dt + 160 dt + 80 dt = 768 dt (48 x 16 dt) # - # As you can see, constraints on t0 are all satified without explicit scheduling. + # As you can see, constraints on t0 are all satisfied without explicit scheduling. time_interval = t_end - t_start if time_interval % self._alignment != 0: raise TranspilerError( @@ -277,7 +308,7 @@ def _pad( f"on qargs {next_node.qargs}." ) - if self._qubits and dag.qubits.index(qubit) not in self._qubits: + if not self.__is_dd_qubit(dag.qubits.index(qubit)): # Target physical qubit is not the target of this DD sequence. self._apply_scheduled_op(dag, t_start, Delay(time_interval, dag.unit), qubit) return diff --git a/qiskit/transpiler/passes/scheduling/padding/pad_delay.py b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py index c0a12267211a..f1517900874f 100644 --- a/qiskit/transpiler/passes/scheduling/padding/pad_delay.py +++ b/qiskit/transpiler/passes/scheduling/padding/pad_delay.py @@ -15,6 +15,7 @@ from qiskit.circuit import Qubit from qiskit.circuit.delay import Delay from qiskit.dagcircuit import DAGCircuit, DAGNode, DAGOutNode +from qiskit.transpiler.target import Target from .base_padding import BasePadding @@ -50,13 +51,16 @@ class PadDelay(BasePadding): See :class:`BasePadding` pass for details. """ - def __init__(self, fill_very_end: bool = True): + def __init__(self, fill_very_end: bool = True, target: Target = None): """Create new padding delay pass. Args: fill_very_end: Set ``True`` to fill the end of circuit with delay. + target: The :class:`~.Target` representing the target backend. + If it supplied and it does not support delay instruction on a qubit, + padding passes do not pad any idle time of the qubit. """ - super().__init__() + super().__init__(target=target) self.fill_very_end = fill_very_end def _pad( diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index 7cd3221c84ae..d8d3bc400e50 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -534,7 +534,7 @@ def _require_alignment(property_set): ) if scheduling_method: # Call padding pass if circuit is scheduled - scheduling.append(PadDelay()) + scheduling.append(PadDelay(target=target)) return scheduling diff --git a/releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml b/releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml new file mode 100644 index 000000000000..34c42ab5baf8 --- /dev/null +++ b/releasenotes/notes/fix-delay-padding-75937bda37ebc3fd.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Fixed an issue in tranpiler passes for padding delays, which did not respect target's constraints + and inserted delays even for qubits not supporting :class:`~.circuit.Delay` instruction. + :class:`~.PadDelay` and :class:`~.PadDynamicalDecoupling` are fixed + so that they do not pad any idle time of qubits such that the target does not support + ``Delay`` instructions for the qubits. + Also legacy scheduling passes ``ASAPSchedule`` and ``ALAPSchedule``, + which pad delays internally, are fixed in the same way. + In addition, :func:`transpile` is fixed to call ``PadDelay`` with a ``target`` object + so that it works correctly when called with ``scheduling_method`` option. + Fixed `#9993 `__ diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 238a77ea699e..2f03fd5112b1 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -2910,3 +2910,51 @@ def test_transpile_does_not_affect_backend_coupling(self, opt_level): original_map = copy.deepcopy(backend.coupling_map) transpile(qc, backend, optimization_level=opt_level) self.assertEqual(original_map, backend.coupling_map) + + @combine( + optimization_level=[0, 1, 2, 3], + scheduling_method=["asap", "alap"], + ) + def test_transpile_target_with_qubits_without_delays_with_scheduling( + self, optimization_level, scheduling_method + ): + """Test qubits without operations aren't ever used.""" + no_delay_qubits = [1, 3, 4] + target = Target(num_qubits=5, dt=1) + target.add_instruction( + XGate(), {(i,): InstructionProperties(duration=160) for i in range(4)} + ) + target.add_instruction( + HGate(), {(i,): InstructionProperties(duration=160) for i in range(4)} + ) + target.add_instruction( + CXGate(), + { + edge: InstructionProperties(duration=800) + for edge in [(0, 1), (1, 2), (2, 0), (2, 3)] + }, + ) + target.add_instruction( + Delay(Parameter("t")), {(i,): None for i in range(4) if i not in no_delay_qubits} + ) + qc = QuantumCircuit(4) + qc.x(0) + qc.cx(0, 1) + qc.cx(0, 2) + qc.cx(1, 3) + qc.cx(0, 3) + tqc = transpile( + qc, + target=target, + optimization_level=optimization_level, + scheduling_method=scheduling_method, + ) + invalid_qubits = { + 4, + } + self.assertEqual(tqc.num_qubits, 5) + for inst in tqc.data: + for bit in inst.qubits: + self.assertNotIn(tqc.find_bit(bit).index, invalid_qubits) + if isinstance(inst.operation, Delay): + self.assertNotIn(tqc.find_bit(bit).index, no_delay_qubits) diff --git a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py index f195143d32d4..2f375c46f67b 100644 --- a/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py +++ b/test/python/transpiler/legacy_scheduling/test_scheduling_pass.py @@ -15,11 +15,16 @@ import unittest from ddt import ddt, data, unpack + from qiskit import QuantumCircuit +from qiskit.circuit import Delay, Parameter +from qiskit.circuit.library.standard_gates import XGate, YGate, CXGate from qiskit.test import QiskitTestCase +from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.instruction_durations import InstructionDurations -from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule +from qiskit.transpiler.passes import ASAPSchedule, ALAPSchedule, DynamicalDecoupling from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.target import Target, InstructionProperties @ddt @@ -718,6 +723,89 @@ def test_dag_introduces_extra_dependency_between_conditionals(self): self.assertEqual(expected, scheduled) + @data(ALAPSchedule, ASAPSchedule) + def test_respect_target_instruction_constraints(self, schedule_pass): + """Test if ALAP/ASAP does not pad delays for qubits that do not support delay instructions. + See: https://github.com/Qiskit/qiskit-terra/issues/9993 + """ + target = Target(dt=1) + target.add_instruction(XGate(), {(1,): InstructionProperties(duration=200)}) + # delays are not supported + + qc = QuantumCircuit(2) + qc.x(1) + + pm = PassManager(schedule_pass(target=target)) + scheduled = pm.run(qc) + + expected = QuantumCircuit(2) + expected.x(1) + # no delay on qubit 0 + + self.assertEqual(expected, scheduled) + + def test_dd_respect_target_instruction_constraints(self): + """Test if DD pass does not pad delays for qubits that do not support delay instructions + and does not insert DD gates for qubits that do not support necessary gates. + See: https://github.com/Qiskit/qiskit-terra/issues/9993 + """ + qc = QuantumCircuit(3) + qc.cx(0, 1) + qc.cx(1, 2) + + target = Target(dt=1) + # Y is partially supported (not supported on qubit 2) + target.add_instruction( + XGate(), {(q,): InstructionProperties(duration=100) for q in range(2)} + ) + target.add_instruction( + CXGate(), + { + (0, 1): InstructionProperties(duration=1000), + (1, 2): InstructionProperties(duration=1000), + }, + ) + # delays are not supported + + # No DD instructions nor delays are padded due to no delay support in the target + pm_xx = PassManager( + [ + ALAPSchedule(target=target), + DynamicalDecoupling(durations=None, dd_sequence=[XGate(), XGate()], target=target), + ] + ) + scheduled = pm_xx.run(qc) + self.assertEqual(qc, scheduled) + + # Fails since Y is not supported in the target + with self.assertRaises(TranspilerError): + PassManager( + [ + ALAPSchedule(target=target), + DynamicalDecoupling( + durations=None, + dd_sequence=[XGate(), YGate(), XGate(), YGate()], + target=target, + ), + ] + ) + + # Add delay support to the target + target.add_instruction(Delay(Parameter("t")), {(q,): None for q in range(3)}) + # No error but no DD on qubit 2 (just delay is padded) since X is not supported on it + scheduled = pm_xx.run(qc) + + expected = QuantumCircuit(3) + expected.delay(1000, [2]) + expected.cx(0, 1) + expected.cx(1, 2) + expected.delay(200, [0]) + expected.x([0]) + expected.delay(400, [0]) + expected.x([0]) + expected.delay(200, [0]) + self.assertEqual(expected, scheduled) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_dynamical_decoupling.py b/test/python/transpiler/test_dynamical_decoupling.py index ca57a82239ed..bd8c44ab0973 100644 --- a/test/python/transpiler/test_dynamical_decoupling.py +++ b/test/python/transpiler/test_dynamical_decoupling.py @@ -195,6 +195,7 @@ def test_insert_dd_ghz_with_target(self): target.add_instruction( Reset(), {(x,): InstructionProperties(duration=1500) for x in range(4)} ) + target.add_instruction(Delay(Parameter("t")), {(x,): None for x in range(4)}) dd_sequence = [XGate(), XGate()] pm = PassManager( [ @@ -822,6 +823,66 @@ def test_dd_can_sequentially_called(self): self.assertEqual(circ1, circ2) + def test_respect_target_instruction_constraints(self): + """Test if DD pass does not pad delays for qubits that do not support delay instructions + and does not insert DD gates for qubits that do not support necessary gates. + See: https://github.com/Qiskit/qiskit-terra/issues/9993 + """ + qc = QuantumCircuit(3) + qc.cx(0, 1) + qc.cx(1, 2) + + target = Target(dt=1) + # Y is partially supported (not supported on qubit 2) + target.add_instruction( + XGate(), {(q,): InstructionProperties(duration=100) for q in range(2)} + ) + target.add_instruction( + CXGate(), + { + (0, 1): InstructionProperties(duration=1000), + (1, 2): InstructionProperties(duration=1000), + }, + ) + # delays are not supported + + # No DD instructions nor delays are padded due to no delay support in the target + pm_xx = PassManager( + [ + ALAPScheduleAnalysis(target=target), + PadDynamicalDecoupling(dd_sequence=[XGate(), XGate()], target=target), + ] + ) + scheduled = pm_xx.run(qc) + self.assertEqual(qc, scheduled) + + # Fails since Y is not supported in the target + with self.assertRaises(TranspilerError): + PassManager( + [ + ALAPScheduleAnalysis(target=target), + PadDynamicalDecoupling( + dd_sequence=[XGate(), YGate(), XGate(), YGate()], target=target + ), + ] + ) + + # Add delay support to the target + target.add_instruction(Delay(Parameter("t")), {(q,): None for q in range(3)}) + # No error but no DD on qubit 2 (just delay is padded) since X is not supported on it + scheduled = pm_xx.run(qc) + + expected = QuantumCircuit(3) + expected.delay(1000, [2]) + expected.cx(0, 1) + expected.cx(1, 2) + expected.delay(200, [0]) + expected.x([0]) + expected.delay(400, [0]) + expected.x([0]) + expected.delay(200, [0]) + self.assertEqual(expected, scheduled) + if __name__ == "__main__": unittest.main() diff --git a/test/python/transpiler/test_scheduling_padding_pass.py b/test/python/transpiler/test_scheduling_padding_pass.py index 29d086a44163..c981f98649b6 100644 --- a/test/python/transpiler/test_scheduling_padding_pass.py +++ b/test/python/transpiler/test_scheduling_padding_pass.py @@ -851,6 +851,23 @@ def test_no_pad_very_end_of_circuit(self): self.assertEqual(scheduled, qc) + @data(ALAPScheduleAnalysis, ASAPScheduleAnalysis) + def test_respect_target_instruction_constraints(self, schedule_pass): + """Test if DD pass does not pad delays for qubits that do not support delay instructions. + See: https://github.com/Qiskit/qiskit-terra/issues/9993 + """ + qc = QuantumCircuit(3) + qc.cx(1, 2) + + target = Target(dt=1) + target.add_instruction(CXGate(), {(1, 2): InstructionProperties(duration=1000)}) + # delays are not supported + + pm = PassManager([schedule_pass(target=target), PadDelay(target=target)]) + scheduled = pm.run(qc) + + self.assertEqual(qc, scheduled) + if __name__ == "__main__": unittest.main() From 6aa16e497a116ba752464f2001cba65e0528e0e8 Mon Sep 17 00:00:00 2001 From: Edwin Navarro Date: Tue, 25 Apr 2023 09:24:23 -0700 Subject: [PATCH 070/172] Allow decomposing composite gates by name (#9170) * Fix decompose with a lable or name * Finish tests and reno * Lint * Remove gate arg and fix parens * Update QuantumCircuit.decompose doc string * Unused imports --- .../library/standard_gates/xx_minus_yy.py | 6 +++--- .../library/standard_gates/xx_plus_yy.py | 6 +++--- qiskit/circuit/quantumcircuit.py | 7 +++++-- qiskit/transpiler/passes/basis/decompose.py | 17 ++++++++--------- .../fix-decompose-name-f83f5e4e64918aa9.yaml | 8 ++++++++ test/python/transpiler/test_decompose.py | 14 +++++++++----- 6 files changed, 36 insertions(+), 22 deletions(-) create mode 100644 releasenotes/notes/fix-decompose-name-f83f5e4e64918aa9.yaml diff --git a/qiskit/circuit/library/standard_gates/xx_minus_yy.py b/qiskit/circuit/library/standard_gates/xx_minus_yy.py index 9e6c3f2c6943..3496868dc427 100644 --- a/qiskit/circuit/library/standard_gates/xx_minus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_minus_yy.py @@ -41,7 +41,7 @@ class XXMinusYYGate(Gate): ┌───────────────┐ q_0: ┤0 ├ - │ {XX-YY}(θ,β) │ + │ (XX-YY)(θ,β) │ q_1: ┤1 ├ └───────────────┘ @@ -73,7 +73,7 @@ class XXMinusYYGate(Gate): ┌───────────────┐ q_0: ┤1 ├ - │ {XX-YY}(θ,β) │ + │ (XX-YY)(θ,β) │ q_1: ┤0 ├ └───────────────┘ @@ -95,7 +95,7 @@ def __init__( self, theta: ParameterValueType, beta: ParameterValueType = 0, - label: Optional[str] = "{XX-YY}", + label: Optional[str] = "(XX-YY)", ): """Create new XX-YY gate. diff --git a/qiskit/circuit/library/standard_gates/xx_plus_yy.py b/qiskit/circuit/library/standard_gates/xx_plus_yy.py index 5e0a8f184778..c3425c199c8e 100644 --- a/qiskit/circuit/library/standard_gates/xx_plus_yy.py +++ b/qiskit/circuit/library/standard_gates/xx_plus_yy.py @@ -32,7 +32,7 @@ class XXPlusYYGate(Gate): ┌───────────────┐ q_0: ┤0 ├ - │ {XX+YY}(θ,β) │ + │ (XX+YY)(θ,β) │ q_1: ┤1 ├ └───────────────┘ @@ -64,7 +64,7 @@ class XXPlusYYGate(Gate): ┌───────────────┐ q_0: ┤1 ├ - │ {XX+YY}(θ,β) │ + │ (XX+YY)(θ,β) │ q_1: ┤0 ├ └───────────────┘ @@ -86,7 +86,7 @@ def __init__( self, theta: ParameterValueType, beta: ParameterValueType = 0, - label: Optional[str] = "{XX+YY}", + label: Optional[str] = "(XX+YY)", ): """Create new XX+YY gate. diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 7f2bc7d8beb8..f8323e66c707 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -1575,8 +1575,11 @@ def decompose( to decompose one level (shallow decompose). Args: - gates_to_decompose (str or list(str)): optional subset of gates to decompose. - Defaults to all gates in circuit. + gates_to_decompose (type or str or list(type, str)): Optional subset of gates + to decompose. Can be a gate type, such as ``HGate``, or a gate name, such + as 'h', or a gate label, such as 'My H Gate', or a list of any combination + of these. If a gate name is entered, it will decompose all gates with that + name, whether the gates have labels or not. Defaults to all gates in circuit. reps (int): Optional number of times the circuit should be decomposed. For instance, ``reps=2`` equals calling ``circuit.decompose().decompose()``. can decompose specific gates specific time diff --git a/qiskit/transpiler/passes/basis/decompose.py b/qiskit/transpiler/passes/basis/decompose.py index 0dde718fc873..73d3cd54c6e7 100644 --- a/qiskit/transpiler/passes/basis/decompose.py +++ b/qiskit/transpiler/passes/basis/decompose.py @@ -72,8 +72,6 @@ def _should_decompose(self, node) -> bool: if self.gates_to_decompose is None: # check if no gates given return True - has_label = False - if not isinstance(self.gates_to_decompose, list): gates = [self.gates_to_decompose] else: @@ -82,15 +80,16 @@ def _should_decompose(self, node) -> bool: strings_list = [s for s in gates if isinstance(s, str)] gate_type_list = [g for g in gates if isinstance(g, type)] - if hasattr(node.op, "label") and node.op.label is not None: - has_label = True - - if has_label and ( # check if label or label wildcard is given - node.op.label in gates or any(fnmatch(node.op.label, p) for p in strings_list) + if ( + getattr(node.op, "label", None) is not None + and node.op.label != "" + and ( # check if label or label wildcard is given + node.op.label in gates or any(fnmatch(node.op.label, p) for p in strings_list) + ) ): return True - elif not has_label and ( # check if name or name wildcard is given - node.name in gates or any(fnmatch(node.name, p) for p in strings_list) + elif node.name in gates or any( # check if name or name wildcard is given + fnmatch(node.name, p) for p in strings_list ): return True elif any(isinstance(node.op, op) for op in gate_type_list): # check if Gate type given diff --git a/releasenotes/notes/fix-decompose-name-f83f5e4e64918aa9.yaml b/releasenotes/notes/fix-decompose-name-f83f5e4e64918aa9.yaml new file mode 100644 index 000000000000..54810d35afdf --- /dev/null +++ b/releasenotes/notes/fix-decompose-name-f83f5e4e64918aa9.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed an issue in the :func:`.QuantumCircuit.decompose` method + where passing a circuit name to the function that matched a + composite gate name would not decompose the gate if it had a label + assigned to it as well. + Fixed `#9136 `__ diff --git a/test/python/transpiler/test_decompose.py b/test/python/transpiler/test_decompose.py index 87e74a2b3a49..9b145374f7cd 100644 --- a/test/python/transpiler/test_decompose.py +++ b/test/python/transpiler/test_decompose.py @@ -261,12 +261,16 @@ def test_decompose_name_wildcards(self): decom_circ = self.complex_circuit.decompose(["circuit-*"]) dag = circuit_to_dag(decom_circ) - self.assertEqual(len(dag.op_nodes()), 5) - self.assertEqual(dag.op_nodes()[0].op.label, "gate1") - self.assertEqual(dag.op_nodes()[1].op.label, "gate2") - self.assertEqual(dag.op_nodes()[2].name, "mcx") + self.assertEqual(len(dag.op_nodes()), 9) + self.assertEqual(dag.op_nodes()[0].name, "h") + self.assertEqual(dag.op_nodes()[1].name, "t") + self.assertEqual(dag.op_nodes()[2].name, "x") self.assertEqual(dag.op_nodes()[3].name, "h") - self.assertRegex(dag.op_nodes()[4].name, "x") + self.assertRegex(dag.op_nodes()[4].name, "cx") + self.assertEqual(dag.op_nodes()[5].name, "x") + self.assertEqual(dag.op_nodes()[6].name, "mcx") + self.assertEqual(dag.op_nodes()[7].name, "h") + self.assertEqual(dag.op_nodes()[8].name, "x") def test_decompose_label_wildcards(self): """Test decomposition parameters so that label wildcards is decomposed""" From a3aa3aa094cd03446d5d60611d228fbd740096e4 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 25 Apr 2023 12:47:35 -0400 Subject: [PATCH 071/172] FIx 0q operation handling in `Statevector` (#10031) The root of the problem was the `OpShape` producing a "vector-like" shape if it detected that "input size" was 1. If both the input and output dimension are 1, it is instead a 0q operator (scalar). --- qiskit/quantum_info/operators/op_shape.py | 3 +++ .../fix-0q-operator-statevector-79199c65c24637c4.yaml | 6 ++++++ test/python/quantum_info/states/test_statevector.py | 9 ++++++++- 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-0q-operator-statevector-79199c65c24637c4.yaml diff --git a/qiskit/quantum_info/operators/op_shape.py b/qiskit/quantum_info/operators/op_shape.py index 2bd840f8c2b9..41d45feba4ac 100644 --- a/qiskit/quantum_info/operators/op_shape.py +++ b/qiskit/quantum_info/operators/op_shape.py @@ -123,6 +123,9 @@ def num_qargs(self): @property def shape(self): """Return a tuple of the matrix shape""" + if self._num_qargs_l == self._num_qargs_r == 0: + # Scalar shape is op-like + return (1, 1) if not self._num_qargs_r: # Vector shape return (self._dim_l,) diff --git a/releasenotes/notes/fix-0q-operator-statevector-79199c65c24637c4.yaml b/releasenotes/notes/fix-0q-operator-statevector-79199c65c24637c4.yaml new file mode 100644 index 000000000000..ab8a9b43256f --- /dev/null +++ b/releasenotes/notes/fix-0q-operator-statevector-79199c65c24637c4.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Construction of a :class:`~.quantum_info.Statevector` from a :class:`.QuantumCircuit` containing + zero-qubit operations will no longer raise an error. These operations impart a global phase on + the resulting statevector. diff --git a/test/python/quantum_info/states/test_statevector.py b/test/python/quantum_info/states/test_statevector.py index d81cd33d75f8..e724651e4184 100644 --- a/test/python/quantum_info/states/test_statevector.py +++ b/test/python/quantum_info/states/test_statevector.py @@ -24,7 +24,7 @@ from qiskit import QiskitError from qiskit import QuantumRegister, QuantumCircuit from qiskit import transpile -from qiskit.circuit.library import HGate, QFT +from qiskit.circuit.library import HGate, QFT, GlobalPhaseGate from qiskit.providers.basicaer import QasmSimulatorPy from qiskit.quantum_info.random import random_unitary, random_statevector, random_pauli @@ -186,6 +186,13 @@ def test_from_circuit(self): psi = Statevector.from_instruction(circuit) self.assertEqual(psi, target) + # Test 0q instruction + target = Statevector([1j, 0]) + circuit = QuantumCircuit(1) + circuit.append(GlobalPhaseGate(np.pi / 2), [], []) + psi = Statevector.from_instruction(circuit) + self.assertEqual(psi, target) + def test_from_instruction(self): """Test initialization from an instruction.""" target = np.dot(HGate().to_matrix(), [1, 0]) From 69dc975fde3de3a955a6ef71e153df4b62eb85ca Mon Sep 17 00:00:00 2001 From: Etienne Wodey <44871469+airwoodix@users.noreply.github.com> Date: Tue, 25 Apr 2023 23:46:46 +0200 Subject: [PATCH 072/172] circuit: narrow the return type of QuantumCircuit.assign_parameters (#10037) * circuit: add type overload on QuantumCircuit.assign_parameters * circuit: fix Literal import * Fixup release note --------- Co-authored-by: Jake Lishman --- qiskit/circuit/quantumcircuit.py | 23 +++++++++++++++++++ ...gn-parameters-typing-70c9623405cbd420.yaml | 7 ++++++ 2 files changed, 30 insertions(+) create mode 100644 releasenotes/notes/improve-quantum-circuit-assign-parameters-typing-70c9623405cbd420.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index f8323e66c707..0618105e0192 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -15,6 +15,7 @@ """Quantum circuit object.""" from __future__ import annotations +import sys import collections.abc import copy import itertools @@ -37,6 +38,7 @@ Iterable, Any, DefaultDict, + overload, ) import numpy as np from qiskit.exceptions import QiskitError, MissingOptionalLibraryError @@ -71,6 +73,11 @@ except Exception: # pylint: disable=broad-except HAS_PYGMENTS = False +if sys.version_info >= (3, 8): + from typing import Literal +else: + from typing_extensions import Literal + if typing.TYPE_CHECKING: import qiskit # pylint: disable=cyclic-import from qiskit.transpiler.layout import TranspileLayout # pylint: disable=cyclic-import @@ -2611,6 +2618,22 @@ def _unsorted_parameters(self) -> set[Parameter]: return parameters + @overload + def assign_parameters( + self, + parameters: Union[Mapping[Parameter, ParameterValueType], Sequence[ParameterValueType]], + inplace: Literal[False] = ..., + ) -> "QuantumCircuit": + ... + + @overload + def assign_parameters( + self, + parameters: Union[Mapping[Parameter, ParameterValueType], Sequence[ParameterValueType]], + inplace: Literal[True] = ..., + ) -> None: + ... + def assign_parameters( self, parameters: Union[Mapping[Parameter, ParameterValueType], Sequence[ParameterValueType]], diff --git a/releasenotes/notes/improve-quantum-circuit-assign-parameters-typing-70c9623405cbd420.yaml b/releasenotes/notes/improve-quantum-circuit-assign-parameters-typing-70c9623405cbd420.yaml new file mode 100644 index 000000000000..7804a518c30d --- /dev/null +++ b/releasenotes/notes/improve-quantum-circuit-assign-parameters-typing-70c9623405cbd420.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Improve the type annotations on the + :meth:`.QuantumCircuit.assign_parameters` + method to reflect the change in return type depending on the ``inplace`` + argument. From 8cb73a499ed8a8709818e2897c6e26230420f709 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 26 Apr 2023 21:57:58 -0400 Subject: [PATCH 073/172] Improve basis translator error message (#9845) * Improve basis translator error message When running the transpiler and the BasisTranslator is unable to translate the gates in a circuit to the specified target a moderately opaque error message is returned. In the context of the debugging the BasisTranslator pass it provides a bit more context but for the typical user encountering the error message it is a bit confusing because there is a bunch of superflous information that isn't actionable, and it also doesn't adequently explain the context of the error. This commit updates this error message to simplify the details returned so that it is just a list of the circuit's gates at the point of the translation and the specified target basis printed and also explaining why this error might be occuring. It also includes a link to the documentation on how to add custom equivalence rules just in case the target is using non-standard gates. This should hopefully make it clearer when transpile() fails in the BasisTranslator why this is occuring. * Add documentation section on error details * Fix typo --- qiskit/providers/__init__.py | 2 + .../passes/basis/basis_translator.py | 39 ++++++++++++++++--- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index f026a08e187f..f1f6d993f8e7 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -289,6 +289,8 @@ def run(circuits, **kwargs): attribute which the :func:`~qiskit.compiler.transpile` function will use as its model of a backend target for compilation. +.. _custom_basis_gates: + Custom Basis Gates ^^^^^^^^^^^^^^^^^^ diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 75da0b5bd361..47796592bd11 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -80,6 +80,24 @@ class BasisTranslator(TransformationPass): translating a 2 qubit operation on qubit 0 and 1 that the output might have ``u`` on qubit 1 and ``x`` on qubit 0. Typically running this pass a second time will correct these issues. + + .. _translation_errors: + + Translation Errors + ------------------ + + This pass will error if there is no path to translate an input gate to + the specified basis. However, during a typical/default preset passmanager + this pass gets run multiple times at different stages of the compilation + pipeline. This means that potentially the input gates that are getting + translated were not in the input circuit to :func:`~.transpile` as they + were generated by an intermediate transform in the circuit. + + 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 + :ref:`custom_basis_gates` for details on adding custom equivalence rules. """ def __init__(self, equivalence_library, target_basis, target=None): @@ -172,10 +190,14 @@ def run(self, dag): if local_basis_transforms is None: raise TranspilerError( - "Unable to map source basis {} to target basis {} on qarg {} " - "over library {}.".format( - local_source_basis, expanded_target, qarg, self._equiv_lib - ) + "Unable to translate the operations in the circuit: " + f"{[x[0] for x in local_source_basis]} to the backend's (or manually " + f"specified) target basis: {list(expanded_target)}. 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://qiskit.org/documentation/stubs/qiskit.transpiler.passes." + "BasisTranslator.html#translation_errors" ) qarg_local_basis_transforms[qarg] = local_basis_transforms @@ -187,8 +209,13 @@ def run(self, dag): if basis_transforms is None: raise TranspilerError( - "Unable to map source basis {} to target basis {} " - "over library {}.".format(source_basis, target_basis, self._equiv_lib) + "Unable to translate the operations in the circuit: " + f"{[x[0] for x in source_basis]} to the backend's (or manually specified) target " + 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://qiskit.org/documentation/stubs/qiskit.transpiler.passes.BasisTranslator." + "html#translation_errors" ) # Compose found path into a set of instruction substitution rules. From 73d8b9b337e6addce63b0744365c9b4d8ed17621 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Thu, 27 Apr 2023 07:11:07 -0400 Subject: [PATCH 074/172] Upgrade Reno for subsection support (#10032) --- requirements-dev.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 6d83ac76abbd..1ec6cb80579f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -15,7 +15,8 @@ stestr>=2.0.0,!=4.0.0 pylatexenc>=1.4 ddt>=1.2.0,!=1.4.0,!=1.4.3 seaborn>=0.9.0 -reno>=3.4.0 +# TODO: switch to stable release when 4.1 is released +reno @ git+https://github.com/openstack/reno.git@81587f616f17904336cdc431e25c42b46cd75b8f Sphinx>=5.0 qiskit-sphinx-theme~=1.11.0 sphinx-design>=0.2.0 From ba7a43961157b7563cd2c0427562146fbdd78f7a Mon Sep 17 00:00:00 2001 From: TsafrirA <113579969+TsafrirA@users.noreply.github.com> Date: Thu, 27 Apr 2023 14:45:30 +0300 Subject: [PATCH 075/172] Remove outdated comment from docs (#10041) Co-authored-by: Naoki Kanazawa --- qiskit/pulse/library/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index 19446c10f1b0..8cd80a3b32c9 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -38,11 +38,6 @@ a :class:`~SymbolicPulse` which will set values for the parameters and sample the parametric expression to create the :class:`~Waveform`. -.. note:: - - QPY serialization support for :class:`.SymbolicPulse` is currently not available. - This feature will be implemented soon in Qiskit terra version 0.21. - .. _pulse_models: From b91f6d31cdb62ebd8d2ebda8c97c52d2dfe40110 Mon Sep 17 00:00:00 2001 From: Evgenii Zheltonozhskii Date: Thu, 27 Apr 2023 17:50:19 +0300 Subject: [PATCH 076/172] Replace deprecated function call (#10044) --- qiskit/pulse/library/waveform.py | 2 +- test/python/pulse/test_pulse_lib.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/qiskit/pulse/library/waveform.py b/qiskit/pulse/library/waveform.py index b7ddb27a9d44..d0778bccab37 100644 --- a/qiskit/pulse/library/waveform.py +++ b/qiskit/pulse/library/waveform.py @@ -121,7 +121,7 @@ def __eq__(self, other: Pulse) -> bool: ) def __hash__(self) -> int: - return hash(self.samples.tostring()) + return hash(self.samples.tobytes()) def __repr__(self) -> str: opt = np.get_printoptions() diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index a16aee744aa3..f5485762ddd9 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -64,6 +64,16 @@ def test_sample_pulse(self): self.assertEqual(sample_pulse.duration, n_samples) self.assertEqual(sample_pulse.name, name) + def test_waveform_hashing(self): + """Test waveform hashing.""" + n_samples = 100 + samples = np.linspace(0, 1.0, n_samples, dtype=np.complex128) + name = "test" + sample_pulse = Waveform(samples, name=name) + sample_pulse2 = Waveform(samples, name="test2") + + self.assertEqual({sample_pulse, sample_pulse2}, {sample_pulse}) + def test_type_casting(self): """Test casting of input samples to numpy array.""" n_samples = 100 From 4df6f3d088af35bb595b2b89cb9209a9c81acf4b Mon Sep 17 00:00:00 2001 From: Slope <60516263+Slope86@users.noreply.github.com> Date: Thu, 27 Apr 2023 23:22:53 +0800 Subject: [PATCH 077/172] Fix for unintended rounding in 'state_to_latex' latex output (#9300) * [fix] unintended rounding cause by param max_size * reformatted * reformatted * Deprecated num_to_latex_ket Replace "numbers_to_latex_terms" with "_numbers_to_latex_terms" * reformatted --- qiskit/visualization/state_visualization.py | 11 ++-- ...unding-with-max-size-1498af5f9a467990.yaml | 6 +++ .../quantum_info/states/test_statevector.py | 54 +++++++++++++++++++ 3 files changed, 67 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/unintended-rounding-with-max-size-1498af5f9a467990.yaml diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index 7dae6433ee9c..2897d841812c 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -1361,7 +1361,9 @@ def _numbers_to_latex_terms(numbers: List[complex], decimals: int = 10) -> List[ return terms -def _state_to_latex_ket(data: List[complex], max_size: int = 12, prefix: str = "") -> str: +def _state_to_latex_ket( + data: List[complex], max_size: int = 12, prefix: str = "", decimals: int = 10 +) -> str: """Convert state vector to latex representation Args: @@ -1369,6 +1371,7 @@ def _state_to_latex_ket(data: List[complex], max_size: int = 12, prefix: str = " max_size: Maximum number of non-zero terms in the expression. If the number of non-zero terms is larger than the max_size, then the representation is truncated. prefix: Latex string to be prepended to the latex, intended for labels. + decimals: Number of decimal places to round to (default: 10). Returns: String with LaTeX representation of the state vector @@ -1378,16 +1381,16 @@ def _state_to_latex_ket(data: List[complex], max_size: int = 12, prefix: str = " def ket_name(i): return bin(i)[2:].zfill(num) - data = np.around(data, max_size) + data = np.around(data, decimals) nonzero_indices = np.where(data != 0)[0].tolist() if len(nonzero_indices) > max_size: nonzero_indices = ( nonzero_indices[: max_size // 2] + [0] + nonzero_indices[-max_size // 2 + 1 :] ) - latex_terms = _numbers_to_latex_terms(data[nonzero_indices], max_size) + latex_terms = _numbers_to_latex_terms(data[nonzero_indices], decimals) nonzero_indices[max_size // 2] = None else: - latex_terms = _numbers_to_latex_terms(data[nonzero_indices], max_size) + latex_terms = _numbers_to_latex_terms(data[nonzero_indices], decimals) latex_str = "" for idx, ket_idx in enumerate(nonzero_indices): diff --git a/releasenotes/notes/unintended-rounding-with-max-size-1498af5f9a467990.yaml b/releasenotes/notes/unintended-rounding-with-max-size-1498af5f9a467990.yaml new file mode 100644 index 000000000000..8beadb8cb994 --- /dev/null +++ b/releasenotes/notes/unintended-rounding-with-max-size-1498af5f9a467990.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + The function :func:`~qiskit.visualization.state_visualization.state_to_latex` produced not valid LaTeX with unintended coefficient rounding, + resulting in errors when calling :func:`~qiskit.visualization.state_visualization.state_drawer` is called. + Fixed `#9297 `__. diff --git a/test/python/quantum_info/states/test_statevector.py b/test/python/quantum_info/states/test_statevector.py index e724651e4184..4db49ccdd122 100644 --- a/test/python/quantum_info/states/test_statevector.py +++ b/test/python/quantum_info/states/test_statevector.py @@ -1263,6 +1263,60 @@ def test_state_to_latex_for_large_sparse_statevector(self): latex_representation = state_to_latex(sv) self.assertEqual(latex_representation, " |000000000000000\\rangle") + def test_state_to_latex_with_max_size_limit(self): + """Test limit the maximum number of non-zero terms in the expression""" + sv = Statevector( + [ + 0.35355339 + 0.0j, + 0.35355339 + 0.0j, + 0.35355339 + 0.0j, + 0.35355339 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 - 0.35355339j, + 0.0 + 0.35355339j, + 0.0 + 0.35355339j, + 0.0 - 0.35355339j, + ], + dims=(2, 2, 2, 2), + ) + latex_representation = state_to_latex(sv, max_size=5) + self.assertEqual( + latex_representation, + "\\frac{\\sqrt{2}}{4} |0000\\rangle+" + "\\frac{\\sqrt{2}}{4} |0001\\rangle + " + "\\ldots +" + "\\frac{\\sqrt{2} i}{4} |1110\\rangle- " + "\\frac{\\sqrt{2} i}{4} |1111\\rangle", + ) + + def test_state_to_latex_with_decimals_round(self): + """Test rounding of decimal places in the expression""" + sv = Statevector( + [ + 0.35355339 + 0.0j, + 0.35355339 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 + 0.0j, + 0.0 - 0.35355339j, + 0.0 + 0.35355339j, + ], + dims=(2, 2, 2), + ) + latex_representation = state_to_latex(sv, decimals=3) + self.assertEqual( + latex_representation, + "0.354 |000\\rangle+0.354 |001\\rangle- 0.354 i |110\\rangle+0.354 i |111\\rangle", + ) + def test_number_to_latex_terms(self): """Test conversions of complex numbers to latex terms""" From 21caf38cdec6b196f308ffcff9f756c9c9da6737 Mon Sep 17 00:00:00 2001 From: Edwin Navarro Date: Thu, 27 Apr 2023 08:24:12 -0700 Subject: [PATCH 078/172] Remove deprecations from mpl circuit drawer (#10020) * Remove deprecated args from mpl drawer * Reno mod --- .../circuit/circuit_visualization.py | 7 +- qiskit/visualization/circuit/matplotlib.py | 70 +------------------ ...eprecated-mpl-drawer-9d6eaa40d5a86777.yaml | 10 +++ 3 files changed, 14 insertions(+), 73 deletions(-) create mode 100644 releasenotes/notes/remove-deprecated-mpl-drawer-9d6eaa40d5a86777.yaml diff --git a/qiskit/visualization/circuit/circuit_visualization.py b/qiskit/visualization/circuit/circuit_visualization.py index f8cc4bc839d0..a2b5e25674b0 100644 --- a/qiskit/visualization/circuit/circuit_visualization.py +++ b/qiskit/visualization/circuit/circuit_visualization.py @@ -649,20 +649,15 @@ def _matplotlib_circuit_drawer( qubits, clbits, nodes, + circuit, scale=scale, style=style, reverse_bits=reverse_bits, plot_barriers=plot_barriers, - layout=None, fold=fold, ax=ax, initial_state=initial_state, cregbundle=cregbundle, - global_phase=None, - calibrations=None, - qregs=None, - cregs=None, with_layout=with_layout, - circuit=circuit, ) return qcd.draw(filename) diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index 155afff43776..b1b04ca6602c 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -20,8 +20,7 @@ import numpy as np -from qiskit.circuit import ControlledGate, Qubit, Clbit, ClassicalRegister -from qiskit.circuit import Measure, QuantumCircuit, QuantumRegister +from qiskit.circuit import ControlledGate, Qubit, Clbit, ClassicalRegister, Measure from qiskit.circuit.library.standard_gates import ( SwapGate, RZZGate, @@ -69,21 +68,16 @@ def __init__( qubits, clbits, nodes, + circuit, scale=None, style=None, reverse_bits=False, plot_barriers=True, - layout=None, fold=25, ax=None, initial_state=False, cregbundle=None, - global_phase=None, - qregs=None, - cregs=None, - calibrations=None, with_layout=False, - circuit=None, ): from matplotlib import patches from matplotlib import pyplot as plt @@ -91,65 +85,7 @@ def __init__( self._patches_mod = patches self._plt_mod = plt - if qregs is not None: - warn( - "The 'qregs' kwarg to the MatplotlibDrawer class is deprecated " - "as of 0.20.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) - if cregs is not None: - warn( - "The 'cregs' kwarg to the MatplotlibDrawer class is deprecated " - "as of 0.20.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) - if global_phase is not None: - warn( - "The 'global_phase' kwarg to the MatplotlibDrawer class is deprecated " - "as of 0.20.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) - if layout is not None: - warn( - "The 'layout' kwarg to the MatplotlibDrawer class is deprecated " - "as of 0.20.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) - if calibrations is not None: - warn( - "The 'calibrations' kwarg to the MatplotlibDrawer class is deprecated " - "as of 0.20.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) - # This check should be removed when the 5 deprecations above are removed - if circuit is None: - warn( - "The 'circuit' kwarg to the MaptlotlibDrawer class must be a valid " - "QuantumCircuit and not None. A new circuit is being created using " - "the qubits and clbits for rendering the drawing.", - DeprecationWarning, - 2, - ) - circ = QuantumCircuit(qubits, clbits) - for reg in qregs: - bits = [qubits[circ._qubit_indices[q].index] for q in reg] - circ.add_register(QuantumRegister(None, reg.name, list(bits))) - for reg in cregs: - bits = [clbits[circ._clbit_indices[q].index] for q in reg] - circ.add_register(ClassicalRegister(None, reg.name, list(bits))) - self._circuit = circ - else: - self._circuit = circuit + self._circuit = circuit self._qubits = qubits self._clbits = clbits self._qubits_dict = {} diff --git a/releasenotes/notes/remove-deprecated-mpl-drawer-9d6eaa40d5a86777.yaml b/releasenotes/notes/remove-deprecated-mpl-drawer-9d6eaa40d5a86777.yaml new file mode 100644 index 000000000000..59d8cef6d5e9 --- /dev/null +++ b/releasenotes/notes/remove-deprecated-mpl-drawer-9d6eaa40d5a86777.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + In the internal ``qiskit.visualization.circuit.matplotlib.MatplotlibDrawer`` object, the arguments + ``layout``, ``global_phase``, ``qregs`` and ``cregs`` have been removed. They were originally + deprecated in Qiskit Terra 0.20. These objects are simply inferred from the given ``circuit`` + now. + + This is an internal worker class of the visualization routines. It is unlikely you will + need to change any of your code. From ff80b61fef132930e87d63732d730f5eb5f2ae74 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Fri, 28 Apr 2023 18:29:12 +0900 Subject: [PATCH 079/172] Simplify `QAOA` (#10030) * simplify * add QAOA tests w/o opflow --- .../algorithms/minimum_eigensolvers/qaoa.py | 10 +- .../minimum_eigensolvers/test_qaoa.py | 56 ++-- .../minimum_eigensolvers/test_qaoa_opflow.py | 304 ++++++++++++++++++ 3 files changed, 329 insertions(+), 41 deletions(-) create mode 100644 test/python/algorithms/minimum_eigensolvers/test_qaoa_opflow.py diff --git a/qiskit/algorithms/minimum_eigensolvers/qaoa.py b/qiskit/algorithms/minimum_eigensolvers/qaoa.py index bb1df403a8fb..5a263d1bab3b 100644 --- a/qiskit/algorithms/minimum_eigensolvers/qaoa.py +++ b/qiskit/algorithms/minimum_eigensolvers/qaoa.py @@ -21,11 +21,10 @@ from qiskit.circuit import QuantumCircuit from qiskit.circuit.library.n_local.qaoa_ansatz import QAOAAnsatz from qiskit.quantum_info.operators.base_operator import BaseOperator -from qiskit.opflow import PauliSumOp, PrimitiveOp +from qiskit.opflow import PauliSumOp from qiskit.primitives import BaseSampler from qiskit.utils.validation import validate_min -from ..exceptions import AlgorithmError from .sampling_vqe import SamplingVQE @@ -136,13 +135,6 @@ def __init__( ) def _check_operator_ansatz(self, operator: BaseOperator | PauliSumOp): - if isinstance(operator, BaseOperator): - try: - operator = PrimitiveOp(operator) - except TypeError as error: - raise AlgorithmError( - f"Unsupported operator type {type(operator)} passed to QAOA." - ) from error # Recreates a circuit based on operator parameter. self.ansatz = QAOAAnsatz( operator, self.reps, initial_state=self.initial_state, mixer_operator=self.mixer diff --git a/test/python/algorithms/minimum_eigensolvers/test_qaoa.py b/test/python/algorithms/minimum_eigensolvers/test_qaoa.py index ff3cce47e239..87f67128fd71 100644 --- a/test/python/algorithms/minimum_eigensolvers/test_qaoa.py +++ b/test/python/algorithms/minimum_eigensolvers/test_qaoa.py @@ -13,32 +13,34 @@ """Test the QAOA algorithm.""" import unittest +from functools import partial from test.python.algorithms import QiskitAlgorithmsTestCase -from functools import partial import numpy as np - -from scipy.optimize import minimize as scipy_minimize -from ddt import ddt, idata, unpack - import rustworkx as rx +from ddt import ddt, idata, unpack +from scipy.optimize import minimize as scipy_minimize from qiskit import QuantumCircuit from qiskit.algorithms.minimum_eigensolvers import QAOA from qiskit.algorithms.optimizers import COBYLA, NELDER_MEAD from qiskit.circuit import Parameter -from qiskit.opflow import PauliSumOp -from qiskit.quantum_info import Pauli -from qiskit.result import QuasiDistribution from qiskit.primitives import Sampler +from qiskit.quantum_info import Pauli, SparsePauliOp +from qiskit.result import QuasiDistribution from qiskit.utils import algorithm_globals -I = PauliSumOp.from_list([("I", 1)]) -X = PauliSumOp.from_list([("X", 1)]) W1 = np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) P1 = 1 -M1 = (I ^ I ^ I ^ X) + (I ^ I ^ X ^ I) + (I ^ X ^ I ^ I) + (X ^ I ^ I ^ I) +M1 = SparsePauliOp.from_list( + [ + ("IIIX", 1), + ("IIXI", 1), + ("IXII", 1), + ("XIII", 1), + ] +) S1 = {"0101", "1010"} @@ -81,8 +83,7 @@ def test_qaoa(self, w, reps, mixer, solutions): qubit_op, _ = self._get_operator(w) qaoa = QAOA(self.sampler, COBYLA(), reps=reps, mixer=mixer) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) self.assertIn(graph_solution, solutions) @@ -111,8 +112,7 @@ def test_qaoa_qc_mixer(self, w, prob, solutions): mixer.rx(theta, range(num_qubits)) qaoa = QAOA(self.sampler, optimizer, reps=prob, mixer=mixer) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) self.assertIn(graph_solution, solutions) @@ -129,8 +129,7 @@ def test_qaoa_qc_mixer_many_parameters(self): mixer.rx(theta, range(num_qubits)) qaoa = QAOA(self.sampler, optimizer, reps=2, mixer=mixer) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) self.log.debug(x) graph_solution = self._get_graph_solution(x) @@ -146,8 +145,7 @@ def test_qaoa_qc_mixer_no_parameters(self): mixer.rx(np.pi / 2, range(num_qubits)) qaoa = QAOA(self.sampler, COBYLA(), reps=1, mixer=mixer) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) # we just assert that we get a result, it is not meaningful. self.assertIsNotNone(result.eigenstate) @@ -157,8 +155,7 @@ def test_change_operator_size(self): np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) ) qaoa = QAOA(self.sampler, COBYLA(), reps=1) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) with self.subTest(msg="QAOA 4x4"): @@ -176,8 +173,7 @@ def test_change_operator_size(self): ] ) ) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) with self.subTest(msg="QAOA 6x6"): @@ -202,8 +198,7 @@ def cb_callback(eval_count, parameters, mean, metadata): initial_point=init_pt, callback=cb_callback, ) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) x = self._sample_most_likely(result.eigenstate) graph_solution = self._get_graph_solution(x) @@ -224,8 +219,7 @@ def test_qaoa_random_initial_point(self): ) qubit_op, _ = self._get_operator(w) qaoa = QAOA(self.sampler, NELDER_MEAD(disp=True), reps=2) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) self.assertLess(result.eigenvalue, -0.97) @@ -239,8 +233,7 @@ def test_optimizer_scipy_callable(self): self.sampler, partial(scipy_minimize, method="Nelder-Mead", options={"maxiter": 2}), ) - with self.assertWarns(DeprecationWarning): - result = qaoa.compute_minimum_eigenvalue(qubit_op) + result = qaoa.compute_minimum_eigenvalue(qubit_op) self.assertEqual(result.cost_function_evals, 5) def _get_operator(self, weight_matrix): @@ -266,9 +259,8 @@ def _get_operator(self, weight_matrix): z_p[j] = True pauli_list.append([0.5 * weight_matrix[i, j], Pauli((z_p, x_p))]) shift -= 0.5 * weight_matrix[i, j] - opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] - with self.assertWarns(DeprecationWarning): - return PauliSumOp.from_list(opflow_list), shift + lst = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] + return SparsePauliOp.from_list(lst), shift def _get_graph_solution(self, x: np.ndarray) -> str: """Get graph solution from binary string. diff --git a/test/python/algorithms/minimum_eigensolvers/test_qaoa_opflow.py b/test/python/algorithms/minimum_eigensolvers/test_qaoa_opflow.py new file mode 100644 index 000000000000..fece9e19856c --- /dev/null +++ b/test/python/algorithms/minimum_eigensolvers/test_qaoa_opflow.py @@ -0,0 +1,304 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022, 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 the QAOA algorithm with opflow.""" + +import unittest +from test.python.algorithms import QiskitAlgorithmsTestCase + +from functools import partial +import numpy as np + +from scipy.optimize import minimize as scipy_minimize +from ddt import ddt, idata, unpack + +import rustworkx as rx + +from qiskit import QuantumCircuit +from qiskit.algorithms.minimum_eigensolvers import QAOA +from qiskit.algorithms.optimizers import COBYLA, NELDER_MEAD +from qiskit.circuit import Parameter +from qiskit.opflow import PauliSumOp +from qiskit.quantum_info import Pauli +from qiskit.result import QuasiDistribution +from qiskit.primitives import Sampler +from qiskit.utils import algorithm_globals + +I = PauliSumOp.from_list([("I", 1)]) +X = PauliSumOp.from_list([("X", 1)]) + +W1 = np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) +P1 = 1 +M1 = (I ^ I ^ I ^ X) + (I ^ I ^ X ^ I) + (I ^ X ^ I ^ I) + (X ^ I ^ I ^ I) +S1 = {"0101", "1010"} + + +W2 = np.array( + [ + [0.0, 8.0, -9.0, 0.0], + [8.0, 0.0, 7.0, 9.0], + [-9.0, 7.0, 0.0, -8.0], + [0.0, 9.0, -8.0, 0.0], + ] +) +P2 = 1 +M2 = None +S2 = {"1011", "0100"} + +CUSTOM_SUPERPOSITION = [1 / np.sqrt(15)] * 15 + [0] + + +@ddt +class TestQAOA(QiskitAlgorithmsTestCase): + """Test QAOA with MaxCut.""" + + def setUp(self): + super().setUp() + self.seed = 10598 + algorithm_globals.random_seed = self.seed + self.sampler = Sampler() + + @idata( + [ + [W1, P1, M1, S1], + [W2, P2, M2, S2], + ] + ) + @unpack + def test_qaoa(self, w, reps, mixer, solutions): + """QAOA test""" + self.log.debug("Testing %s-step QAOA with MaxCut on graph\n%s", reps, w) + + qubit_op, _ = self._get_operator(w) + + qaoa = QAOA(self.sampler, COBYLA(), reps=reps, mixer=mixer) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) + graph_solution = self._get_graph_solution(x) + self.assertIn(graph_solution, solutions) + + @idata( + [ + [W1, P1, S1], + [W2, P2, S2], + ] + ) + @unpack + def test_qaoa_qc_mixer(self, w, prob, solutions): + """QAOA test with a mixer as a parameterized circuit""" + self.log.debug( + "Testing %s-step QAOA with MaxCut on graph with a mixer as a parameterized circuit\n%s", + prob, + w, + ) + + optimizer = COBYLA() + qubit_op, _ = self._get_operator(w) + + num_qubits = qubit_op.num_qubits + mixer = QuantumCircuit(num_qubits) + theta = Parameter("θ") + mixer.rx(theta, range(num_qubits)) + + qaoa = QAOA(self.sampler, optimizer, reps=prob, mixer=mixer) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) + graph_solution = self._get_graph_solution(x) + self.assertIn(graph_solution, solutions) + + def test_qaoa_qc_mixer_many_parameters(self): + """QAOA test with a mixer as a parameterized circuit with the num of parameters > 1.""" + optimizer = COBYLA() + qubit_op, _ = self._get_operator(W1) + + num_qubits = qubit_op.num_qubits + mixer = QuantumCircuit(num_qubits) + for i in range(num_qubits): + theta = Parameter("θ" + str(i)) + mixer.rx(theta, range(num_qubits)) + + qaoa = QAOA(self.sampler, optimizer, reps=2, mixer=mixer) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) + self.log.debug(x) + graph_solution = self._get_graph_solution(x) + self.assertIn(graph_solution, S1) + + def test_qaoa_qc_mixer_no_parameters(self): + """QAOA test with a mixer as a parameterized circuit with zero parameters.""" + qubit_op, _ = self._get_operator(W1) + + num_qubits = qubit_op.num_qubits + mixer = QuantumCircuit(num_qubits) + # just arbitrary circuit + mixer.rx(np.pi / 2, range(num_qubits)) + + qaoa = QAOA(self.sampler, COBYLA(), reps=1, mixer=mixer) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + # we just assert that we get a result, it is not meaningful. + self.assertIsNotNone(result.eigenstate) + + def test_change_operator_size(self): + """QAOA change operator size test""" + qubit_op, _ = self._get_operator( + np.array([[0, 1, 0, 1], [1, 0, 1, 0], [0, 1, 0, 1], [1, 0, 1, 0]]) + ) + qaoa = QAOA(self.sampler, COBYLA(), reps=1) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) + graph_solution = self._get_graph_solution(x) + with self.subTest(msg="QAOA 4x4"): + self.assertIn(graph_solution, {"0101", "1010"}) + + qubit_op, _ = self._get_operator( + np.array( + [ + [0, 1, 0, 1, 0, 1], + [1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1], + [1, 0, 1, 0, 1, 0], + [0, 1, 0, 1, 0, 1], + [1, 0, 1, 0, 1, 0], + ] + ) + ) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) + graph_solution = self._get_graph_solution(x) + with self.subTest(msg="QAOA 6x6"): + self.assertIn(graph_solution, {"010101", "101010"}) + + @idata([[W2, S2, None], [W2, S2, [0.0, 0.0]], [W2, S2, [1.0, 0.8]]]) + @unpack + def test_qaoa_initial_point(self, w, solutions, init_pt): + """Check first parameter value used is initial point as expected""" + qubit_op, _ = self._get_operator(w) + + first_pt = [] + + def cb_callback(eval_count, parameters, mean, metadata): + nonlocal first_pt + if eval_count == 1: + first_pt = list(parameters) + + qaoa = QAOA( + self.sampler, + COBYLA(), + initial_point=init_pt, + callback=cb_callback, + ) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + x = self._sample_most_likely(result.eigenstate) + graph_solution = self._get_graph_solution(x) + + with self.subTest("Initial Point"): + # If None the preferred random initial point of QAOA variational form + if init_pt is None: + self.assertLess(result.eigenvalue, -0.97) + else: + self.assertListEqual(init_pt, first_pt) + + with self.subTest("Solution"): + self.assertIn(graph_solution, solutions) + + def test_qaoa_random_initial_point(self): + """QAOA random initial point""" + w = rx.adjacency_matrix( + rx.undirected_gnp_random_graph(5, 0.5, seed=algorithm_globals.random_seed) + ) + qubit_op, _ = self._get_operator(w) + qaoa = QAOA(self.sampler, NELDER_MEAD(disp=True), reps=2) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(operator=qubit_op) + + self.assertLess(result.eigenvalue, -0.97) + + def test_optimizer_scipy_callable(self): + """Test passing a SciPy optimizer directly as callable.""" + w = rx.adjacency_matrix( + rx.undirected_gnp_random_graph(5, 0.5, seed=algorithm_globals.random_seed) + ) + qubit_op, _ = self._get_operator(w) + qaoa = QAOA( + self.sampler, + partial(scipy_minimize, method="Nelder-Mead", options={"maxiter": 2}), + ) + with self.assertWarns(DeprecationWarning): + result = qaoa.compute_minimum_eigenvalue(qubit_op) + self.assertEqual(result.cost_function_evals, 5) + + def _get_operator(self, weight_matrix): + """Generate Hamiltonian for the max-cut problem of a graph. + + Args: + weight_matrix (numpy.ndarray) : adjacency matrix. + + Returns: + PauliSumOp: operator for the Hamiltonian + float: a constant shift for the obj function. + + """ + num_nodes = weight_matrix.shape[0] + pauli_list = [] + shift = 0 + for i in range(num_nodes): + for j in range(i): + if weight_matrix[i, j] != 0: + x_p = np.zeros(num_nodes, dtype=bool) + z_p = np.zeros(num_nodes, dtype=bool) + z_p[i] = True + z_p[j] = True + pauli_list.append([0.5 * weight_matrix[i, j], Pauli((z_p, x_p))]) + shift -= 0.5 * weight_matrix[i, j] + opflow_list = [(pauli[1].to_label(), pauli[0]) for pauli in pauli_list] + with self.assertWarns(DeprecationWarning): + return PauliSumOp.from_list(opflow_list), shift + + def _get_graph_solution(self, x: np.ndarray) -> str: + """Get graph solution from binary string. + + Args: + x : binary string as numpy array. + + Returns: + a graph solution as string. + """ + + return "".join([str(int(i)) for i in 1 - x]) + + def _sample_most_likely(self, state_vector: QuasiDistribution) -> np.ndarray: + """Compute the most likely binary string from state vector. + Args: + state_vector: Quasi-distribution. + + Returns: + Binary string as numpy.ndarray of ints. + """ + values = list(state_vector.values()) + n = int(np.log2(len(values))) + k = np.argmax(np.abs(values)) + x = np.zeros(n) + for i in range(n): + x[i] = k % 2 + k >>= 1 + return x + + +if __name__ == "__main__": + unittest.main() From 112bd6ea29f8605aea79a191ea9e0c40fb1ba5ea Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 2 May 2023 10:29:05 -0400 Subject: [PATCH 080/172] Remove recursion from ConstrainedReschedule pass (#10051) * Remove recurssion from ConstrainedReschedule pass The ConstrainedReschedule pass previosuly was using a recursive depth first traversal to push back overlapping gates after aligning operations. This however would cause a failure for a sufficiently large circuit when the recursion depth could potentially exceed the maximum stack depth allowed in python. To address this, this commit rewrites the depth first traversal to be iterative instead of recursive. This removes the stack depth limitation and should let the pass run with any size circuit. However, the performance of this pass is poor for large circuits. One thing we can look at using to try and speed it up is rustworkx's dfs_search() function which will let us shift the traversal to rust and call back to python to do the timing offsets. If this is insufficient we'll have to investigate a different algorithm for adjusting the time that doesn't require multiple iterations like the current approach. Fixes #10049 * Use rustworkx's dfs_search instead of manual dfs implementation This commit rewrites the pass to leverage rustworkx's dfs_search function which provides a way to have rustworkx traverse the graph in a depth first manner and then provides hook points to execute code at different named portions of the DFS. By leveraging this function we're able to speed up the search by leveraging rust to perform the actual graph traversal. * Revert "Use rustworkx's dfs_search instead of manual dfs implementation" This made performance of the pass worse so reverting this for now. We can investigate this at a later date. This reverts commit bd3cbb271030b3d88c1ee36691b623a1714348c1. * Remove visited node check from DFS This commit removes the visited node check and skip logic from the DFS traversal. To ensure this code behaves identically to the recursive version before this PR this logic is removed because there wasn't a similar check in that version. --- .../scheduling/alignments/reschedule.py | 110 +++++++++--------- 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py index af28fed1adb6..350ee31882c8 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py +++ b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py @@ -88,13 +88,9 @@ def _get_next_gate(cls, dag: DAGCircuit, node: DAGOpNode) -> List[DAGOpNode]: Returns: A list of non-delay successors. """ - op_nodes = [] for next_node in dag.successors(node): - if isinstance(next_node, DAGOutNode): - continue - op_nodes.append(next_node) - - return op_nodes + if not isinstance(next_node, DAGOutNode): + yield next_node def _push_node_back(self, dag: DAGCircuit, node: DAGOpNode, shift: int): """Update start time of current node. Successors are also shifted to avoid overlap. @@ -114,57 +110,63 @@ def _push_node_back(self, dag: DAGCircuit, node: DAGOpNode, shift: int): conditional_latency = self.property_set.get("conditional_latency", 0) clbit_write_latency = self.property_set.get("clbit_write_latency", 0) - # Compute shifted t1 of this node separately for qreg and creg - this_t0 = node_start_time[node] - new_t1q = this_t0 + node.op.duration + shift - this_qubits = set(node.qargs) - if isinstance(node.op, Measure): - # creg access ends at the end of instruction - new_t1c = new_t1q - this_clbits = set(node.cargs) - else: - if node.op.condition_bits: - # conditional access ends at the beginning of node start time - new_t1c = this_t0 + shift - this_clbits = set(node.op.condition_bits) - else: - new_t1c = None - this_clbits = set() - - # Check successors for overlap - for next_node in self._get_next_gate(dag, node): - # Compute next node start time separately for qreg and creg - next_t0q = node_start_time[next_node] - next_qubits = set(next_node.qargs) - if isinstance(next_node.op, Measure): - # creg access starts after write latency - next_t0c = next_t0q + clbit_write_latency - next_clbits = set(next_node.cargs) + nodes_with_overlap = [(node, shift)] + shift_stack = [] + while nodes_with_overlap: + node, shift = nodes_with_overlap.pop() + shift_stack.append((node, shift)) + # Compute shifted t1 of this node separately for qreg and creg + this_t0 = node_start_time[node] + new_t1q = this_t0 + node.op.duration + shift + this_qubits = set(node.qargs) + if isinstance(node.op, Measure): + # creg access ends at the end of instruction + new_t1c = new_t1q + this_clbits = set(node.cargs) else: - if next_node.op.condition_bits: - # conditional access starts before node start time - next_t0c = next_t0q - conditional_latency - next_clbits = set(next_node.op.condition_bits) + if node.op.condition_bits: + # conditional access ends at the beginning of node start time + new_t1c = this_t0 + shift + this_clbits = set(node.op.condition_bits) else: - next_t0c = None - next_clbits = set() - # Compute overlap if there is qubits overlap - if any(this_qubits & next_qubits): - qreg_overlap = new_t1q - next_t0q - else: - qreg_overlap = 0 - # Compute overlap if there is clbits overlap - if any(this_clbits & next_clbits): - creg_overlap = new_t1c - next_t0c - else: - creg_overlap = 0 - # Shift next node if there is finite overlap in either in qubits or clbits - overlap = max(qreg_overlap, creg_overlap) - if overlap > 0: - self._push_node_back(dag, next_node, overlap) - + new_t1c = None + this_clbits = set() + + # Check successors for overlap + for next_node in self._get_next_gate(dag, node): + # Compute next node start time separately for qreg and creg + next_t0q = node_start_time[next_node] + next_qubits = set(next_node.qargs) + if isinstance(next_node.op, Measure): + # creg access starts after write latency + next_t0c = next_t0q + clbit_write_latency + next_clbits = set(next_node.cargs) + else: + if next_node.op.condition_bits: + # conditional access starts before node start time + next_t0c = next_t0q - conditional_latency + next_clbits = set(next_node.op.condition_bits) + else: + next_t0c = None + next_clbits = set() + # Compute overlap if there is qubits overlap + if any(this_qubits & next_qubits): + qreg_overlap = new_t1q - next_t0q + else: + qreg_overlap = 0 + # Compute overlap if there is clbits overlap + if any(this_clbits & next_clbits): + creg_overlap = new_t1c - next_t0c + else: + creg_overlap = 0 + # Shift next node if there is finite overlap in either in qubits or clbits + overlap = max(qreg_overlap, creg_overlap) + if overlap > 0: + nodes_with_overlap.append((next_node, overlap)) # Update start time of this node after all overlaps are resolved - node_start_time[node] += shift + while shift_stack: + node, shift = shift_stack.pop() + node_start_time[node] += shift def run(self, dag: DAGCircuit): """Run rescheduler. From f13b1edbfda73ce223944d3f28935bbfaf680ca0 Mon Sep 17 00:00:00 2001 From: Kevin Hartman Date: Tue, 2 May 2023 12:29:40 -0400 Subject: [PATCH 081/172] Set `max_trials` for `VF2Layout` in preset pass managers. (#10054) * Set max_trials for VF2Layout in preset pass managers. By setting max_trials, we limit the number of layouts enumerated and scored when iterating through vf2_mapping(). This is necessary for scoring to complete in a reasonable amount of time for circuits with many connected components on larger (e.g. 400 qubit) devices. These limits were chosen using a fake 400 qubit device, using 200 connected components, where each component is a single CX gate. Because layout scoring scales linearly with the number of qubits in the circuit, 250,000 (O3) takes abount a minute, 25,000 (O2) takes about 6 seconds, and 2,500 (O1) takes less than a second. * Address review comments. * Return tuple of None instead for finer control and a better interface. * Add deprecation notice to release note. * Remove deprecation until 0.25.0. * Remove unused import. --- .../preset_passmanagers/builtin_plugins.py | 19 ++++++-- .../transpiler/preset_passmanagers/common.py | 45 ++++++++++++++++++- .../transpiler/preset_passmanagers/level1.py | 1 + .../transpiler/preset_passmanagers/level2.py | 1 + .../transpiler/preset_passmanagers/level3.py | 1 + ...et-pm-vf2-max-trials-958bb8a36fff472f.yaml | 23 ++++++++++ 6 files changed, 84 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/preset-pm-vf2-max-trials-958bb8a36fff472f.yaml diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index ca4c54494d34..8a44950d4367 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -37,7 +37,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana else: routing_pass = BasicSwap(target) - vf2_call_limit = common.get_vf2_call_limit( + vf2_call_limit, vf2_max_trials = common.get_vf2_limits( optimization_level, pass_manager_config.layout_method, pass_manager_config.initial_layout, @@ -56,6 +56,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, check_trivial=True, @@ -67,6 +68,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, use_barrier_before_measurement=True, @@ -77,6 +79,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, use_barrier_before_measurement=True, @@ -96,7 +99,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana if coupling_map_routing is None: coupling_map_routing = coupling_map backend_properties = pass_manager_config.backend_properties - vf2_call_limit = common.get_vf2_call_limit( + vf2_call_limit, vf2_max_trials = common.get_vf2_limits( optimization_level, pass_manager_config.layout_method, pass_manager_config.initial_layout, @@ -120,6 +123,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, check_trivial=True, @@ -131,6 +135,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, use_barrier_before_measurement=True, @@ -150,7 +155,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana if coupling_map_routing is None: coupling_map_routing = coupling_map backend_properties = pass_manager_config.backend_properties - vf2_call_limit = common.get_vf2_call_limit( + vf2_call_limit, vf2_max_trials = common.get_vf2_limits( optimization_level, pass_manager_config.layout_method, pass_manager_config.initial_layout, @@ -171,6 +176,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, check_trivial=True, @@ -183,6 +189,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, use_barrier_before_measurement=True, @@ -194,6 +201,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, use_barrier_before_measurement=True, @@ -213,7 +221,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana if coupling_map_routing is None: coupling_map_routing = coupling_map backend_properties = pass_manager_config.backend_properties - vf2_call_limit = common.get_vf2_call_limit( + vf2_call_limit, vf2_max_trials = common.get_vf2_limits( optimization_level, pass_manager_config.layout_method, pass_manager_config.initial_layout, @@ -244,6 +252,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, check_trivial=True, @@ -261,6 +270,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, use_barrier_before_measurement=True, @@ -277,6 +287,7 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana target, coupling_map=coupling_map, vf2_call_limit=vf2_call_limit, + vf2_max_trials=vf2_max_trials, backend_properties=backend_properties, seed_transpiler=seed_transpiler, use_barrier_before_measurement=True, diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index d8d3bc400e50..e801d04500fc 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -263,6 +263,7 @@ def generate_routing_passmanager( seed_transpiler=None, check_trivial=False, use_barrier_before_measurement=True, + vf2_max_trials=None, ): """Generate a routing :class:`~qiskit.transpiler.PassManager` @@ -273,7 +274,8 @@ def generate_routing_passmanager( coupling_map (CouplingMap): The coupling map of the backend to route for vf2_call_limit (int): The internal call limit for the vf2 post layout - pass. If this is ``None`` the vf2 post layout will not be run. + pass. If this is ``None`` or ``0`` the vf2 post layout will not be + run. backend_properties (BackendProperties): Properties of a backend to synthesize for (e.g. gate fidelities). seed_transpiler (int): Sets random seed for the stochastic parts of @@ -287,6 +289,9 @@ def generate_routing_passmanager( use_barrier_before_measurement (bool): If true (the default) the :class:`~.BarrierBeforeFinalMeasurements` transpiler pass will be run prior to the specified pass in the ``routing_pass`` argument. + vf2_max_trials (int): The maximum number of trials to run VF2 when + evaluating the vf2 post layout + pass. If this is ``None`` or ``0`` the vf2 post layout will not be run. Returns: PassManager: The routing pass manager """ @@ -314,7 +319,8 @@ def _swap_condition(property_set): else: routing.append([routing_pass], condition=_swap_condition) - if (target is not None or backend_properties is not None) and vf2_call_limit is not None: + is_vf2_fully_bounded = vf2_call_limit and vf2_max_trials + if (target is not None or backend_properties is not None) and is_vf2_fully_bounded: routing.append( VF2PostLayout( target, @@ -322,6 +328,7 @@ def _swap_condition(property_set): backend_properties, seed_transpiler, call_limit=vf2_call_limit, + max_trials=vf2_max_trials, strict_direction=False, ), condition=_run_post_layout_condition, @@ -554,3 +561,37 @@ def get_vf2_call_limit( elif optimization_level == 3: vf2_call_limit = int(3e7) # Set call limit to ~60 sec with rustworkx 0.10.2 return vf2_call_limit + + +VF2Limits = collections.namedtuple("VF2Limits", ("call_limit", "max_trials")) + + +def get_vf2_limits( + optimization_level: int, + layout_method: Optional[str] = None, + initial_layout: Optional[Layout] = None, +) -> VF2Limits: + """Get the VF2 limits for VF2-based layout passes. + + Returns: + VF2Limits: An namedtuple with optional elements + ``call_limit`` and ``max_trials``. + """ + limits = VF2Limits(None, None) + if layout_method is None and initial_layout is None: + if optimization_level == 1: + limits = VF2Limits( + int(5e4), # Set call limit to ~100ms with rustworkx 0.10.2 + 2500, # Limits layout scoring to < 600ms on ~400 qubit devices + ) + elif optimization_level == 2: + limits = VF2Limits( + int(5e6), # Set call limit to ~10 sec with rustworkx 0.10.2 + 25000, # Limits layout scoring to < 6 sec on ~400 qubit devices + ) + elif optimization_level == 3: + limits = VF2Limits( + int(3e7), # Set call limit to ~60 sec with rustworkx 0.10.2 + 250000, # Limits layout scoring to < 60 sec on ~400 qubit devices + ) + return limits diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index bb4413b03219..46045b54ef54 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -133,6 +133,7 @@ def _vf2_match_not_found(property_set): call_limit=int(5e4), # Set call limit to ~100ms with rustworkx 0.10.2 properties=backend_properties, target=target, + max_trials=2500, # Limits layout scoring to < 600ms on ~400 qubit devices ) ) diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index c7313cf4475d..900cb2668e93 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -118,6 +118,7 @@ def _vf2_match_not_found(property_set): call_limit=int(5e6), # Set call limit to ~10 sec with rustworkx 0.10.2 properties=backend_properties, target=target, + max_trials=25000, # Limits layout scoring to < 6 sec on ~400 qubit devices ) ) diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index cc20771d9e54..57cfa5d06201 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -124,6 +124,7 @@ def _vf2_match_not_found(property_set): call_limit=int(3e7), # Set call limit to ~60 sec with rustworkx 0.10.2 properties=backend_properties, target=target, + max_trials=250000, # Limits layout scoring to < 60 sec on ~400 qubit devices ) ) diff --git a/releasenotes/notes/preset-pm-vf2-max-trials-958bb8a36fff472f.yaml b/releasenotes/notes/preset-pm-vf2-max-trials-958bb8a36fff472f.yaml new file mode 100644 index 000000000000..f28aaf325056 --- /dev/null +++ b/releasenotes/notes/preset-pm-vf2-max-trials-958bb8a36fff472f.yaml @@ -0,0 +1,23 @@ +--- +upgrade: + - | + The maximum number of trials evaluated when searching for the best + layout using :class:`.VF2Layout` and :class:`.VF2PostLayout` is now + limited in + :func:`qiskit.transpiler.preset_passmanagers.level_1_pass_manager`, + :func:`qiskit.transpiler.preset_passmanagers.level_2_pass_manager`, + and + :func:`qiskit.transpiler.preset_passmanagers.level_3_pass_manager` + to ``2,500``, ``25,000``, and ``250,000``, respectively. Previously, + all possible layouts were evaluated. This change was made to prevent + transpilation from hanging during layout scoring for circuits with many + connected components on larger devices, which scales combinatorially + since each connected component must be evaluated in all possible + positions on the device. To perform a full search as + before, manually run :class:`.VF2PostLayout` over the transpiled circuit + in strict mode, specifying ``0`` for ``max_trials``. +fixes: + - | + Fixed a potential performance scaling issue with layout scoring in preset + pass managers, which could occur when transpiling circuits with many + connected components on large devices. From 07a822011a7f56505dd0e13389ada98e762c00c7 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Tue, 2 May 2023 13:34:03 -0400 Subject: [PATCH 082/172] `@deprecate_arg` can handle positional arguments (#9884) * `@deprecate_arg` can handle positional arguments * Add a comment for tricky code * Fix variadic *args * Catch deprecation warning in test * Go back to using co_varnames for performance But we now error if *args is used * Ignore lint & explain test better Thanks Kevin! --- qiskit/algorithms/optimizers/qnspsa.py | 3 + qiskit/utils/deprecation.py | 106 ++++++++++++++++------ test/python/utils/test_deprecation.py | 120 +++++++++++++++++++++---- 3 files changed, 182 insertions(+), 47 deletions(-) diff --git a/qiskit/algorithms/optimizers/qnspsa.py b/qiskit/algorithms/optimizers/qnspsa.py index 21b902353e44..c9c195729a84 100644 --- a/qiskit/algorithms/optimizers/qnspsa.py +++ b/qiskit/algorithms/optimizers/qnspsa.py @@ -259,6 +259,9 @@ def settings(self) -> dict[str, Any]: "backend", since="0.24.0", additional_msg="See https://qisk.it/algo_migration for a migration guide.", + # We allow passing a sampler as the second argument because that will become a positional + # argument for `sampler` after removing `backend` and `expectation`. + predicate=lambda backend: not isinstance(backend, BaseSampler), ) @deprecate_arg( "expectation", diff --git a/qiskit/utils/deprecation.py b/qiskit/utils/deprecation.py index 77f5b43e5e6a..92cd4d44b863 100644 --- a/qiskit/utils/deprecation.py +++ b/qiskit/utils/deprecation.py @@ -12,7 +12,10 @@ """Deprecation utilities""" +from __future__ import annotations + import functools +import inspect import warnings from typing import Any, Callable, Dict, Optional, Type, Tuple, Union @@ -165,18 +168,35 @@ def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): - if kwargs: - _maybe_warn_and_rename_kwarg( - func_name, - kwargs, - old_arg=name, - new_alias=new_alias, - warning_msg=msg, - category=category, - predicate=predicate, - ) + _maybe_warn_and_rename_kwarg( + args, + kwargs, + func_name=func_name, + original_func_co_varnames=wrapper.__original_func_co_varnames, + old_arg_name=name, + new_alias=new_alias, + warning_msg=msg, + category=category, + predicate=predicate, + ) return func(*args, **kwargs) + # When decorators get called repeatedly, `func` refers to the result of the prior + # decorator, not the original underlying function. This trick allows us to record the + # original function's variable names regardless of how many decorators are used. + # + # If it's the very first decorator call, we also check that *args and **kwargs are not used. + if hasattr(func, "__original_func_co_varnames"): + wrapper.__original_func_co_varnames = func.__original_func_co_varnames + else: + wrapper.__original_func_co_varnames = func.__code__.co_varnames + param_kinds = {param.kind for param in inspect.signature(func).parameters.values()} + if inspect.Parameter.VAR_POSITIONAL in param_kinds: + raise ValueError( + "@deprecate_arg cannot be used with functions that take variable *args. Use " + "warnings.warn() directly instead." + ) + add_deprecation_to_docstring(wrapper, msg, since=since, pending=pending) return wrapper @@ -216,19 +236,36 @@ def decorator(func): @functools.wraps(func) def wrapper(*args, **kwargs): - if kwargs: - for old, new in kwarg_map.items(): - _maybe_warn_and_rename_kwarg( - func_name, - kwargs, - old_arg=old, - new_alias=new, - warning_msg=old_kwarg_to_msg[old], - category=category, - predicate=None, - ) + for old, new in kwarg_map.items(): + _maybe_warn_and_rename_kwarg( + args, + kwargs, + func_name=func_name, + original_func_co_varnames=wrapper.__original_func_co_varnames, + old_arg_name=old, + new_alias=new, + warning_msg=old_kwarg_to_msg[old], + category=category, + predicate=None, + ) return func(*args, **kwargs) + # When decorators get called repeatedly, `func` refers to the result of the prior + # decorator, not the original underlying function. This trick allows us to record the + # original function's variable names regardless of how many decorators are used. + # + # If it's the very first decorator call, we also check that *args and **kwargs are not used. + if hasattr(func, "__original_func_co_varnames"): + wrapper.__original_func_co_varnames = func.__original_func_co_varnames + else: + wrapper.__original_func_co_varnames = func.__code__.co_varnames + param_kinds = {param.kind for param in inspect.signature(func).parameters.values()} + if inspect.Parameter.VAR_POSITIONAL in param_kinds: + raise ValueError( + "@deprecate_arg cannot be used with functions that take variable *args. Use " + "warnings.warn() directly instead." + ) + for msg in old_kwarg_to_msg.values(): add_deprecation_to_docstring( wrapper, msg, since=since, pending=issubclass(category, PendingDeprecationWarning) @@ -275,24 +312,37 @@ def wrapper(*args, **kwargs): def _maybe_warn_and_rename_kwarg( - func_name: str, + args: tuple[Any, ...], kwargs: Dict[str, Any], *, - old_arg: str, + func_name: str, + original_func_co_varnames: tuple[str, ...], + old_arg_name: str, new_alias: Optional[str], warning_msg: str, category: Type[Warning], predicate: Optional[Callable[[Any], bool]], ) -> None: - if old_arg not in kwargs: + # In Python 3.10+, we should set `zip(strict=False)` (the default). That is, we want to + # stop iterating once `args` is done, since some args may have not been explicitly passed as + # positional args. + arg_names_to_values = {name: val for val, name in zip(args, original_func_co_varnames)} + arg_names_to_values.update(kwargs) + + if old_arg_name not in arg_names_to_values: return - if new_alias and new_alias in kwargs: - raise TypeError(f"{func_name} received both {new_alias} and {old_arg} (deprecated).") - if predicate and not predicate(kwargs[old_arg]): + if new_alias and new_alias in arg_names_to_values: + raise TypeError(f"{func_name} received both {new_alias} and {old_arg_name} (deprecated).") + + val = arg_names_to_values[old_arg_name] + if predicate and not predicate(val): return warnings.warn(warning_msg, category=category, stacklevel=3) + + # Finally, if there's a new_alias, add its value dynamically to kwargs so that the code author + # only has to deal with the new_alias in their logic. if new_alias is not None: - kwargs[new_alias] = kwargs.pop(old_arg) + kwargs[new_alias] = val def _write_deprecation_msg( diff --git a/test/python/utils/test_deprecation.py b/test/python/utils/test_deprecation.py index c9013f39f94b..ccfd226b56ca 100644 --- a/test/python/utils/test_deprecation.py +++ b/test/python/utils/test_deprecation.py @@ -12,6 +12,8 @@ """Tests for the functions in ``utils.deprecation``.""" +from __future__ import annotations + from textwrap import dedent from qiskit.test import QiskitTestCase @@ -229,25 +231,86 @@ def test_deprecate_arg_runtime_warning(self) -> None: * If `predicate` is set, only warn if the predicate is True. """ - @deprecate_arg("arg1", since="9.99") - @deprecate_arg("arg2", new_alias="new_arg2", since="9.99") + @deprecate_arg("arg1", new_alias="new_arg1", since="9.99") + @deprecate_arg("arg2", since="9.99") @deprecate_arg("arg3", predicate=lambda arg3: arg3 == "deprecated value", since="9.99") - def my_func(*, arg1: str = "a", new_arg2: str, arg3: str = "a") -> None: - del arg1 + def my_func( + arg1: str = "a", + arg2: str = "a", + arg3: str = "a", + new_arg1: str | None = None, + ) -> None: + del arg2 del arg3 - assert new_arg2 == "z" + # If the old arg was set, we should set its `new_alias` to that value. + if arg1 != "a": + self.assertEqual(new_arg1, "z") + if new_arg1 is not None: + self.assertEqual(new_arg1, "z") + + # No warnings if no deprecated args used. + my_func() + my_func(new_arg1="z") - my_func(new_arg2="z") # No warnings if no deprecated args used. + # Warn if argument is specified, regardless of positional vs kwarg. with self.assertWarnsRegex(DeprecationWarning, "arg1"): - my_func(arg1="a", new_arg2="z") + my_func("z") + with self.assertWarnsRegex(DeprecationWarning, "arg1"): + my_func(arg1="z") + with self.assertWarnsRegex(DeprecationWarning, "arg2"): + my_func("z", "z") with self.assertWarnsRegex(DeprecationWarning, "arg2"): - # `arg2` should be converted into `new_arg2`. - my_func(arg2="z") # pylint: disable=missing-kwoa + my_func(arg2="z") + + # Error if new_alias specified at the same time as old argument name. + with self.assertRaises(TypeError): + my_func("a", new_arg1="z") + with self.assertRaises(TypeError): + my_func(arg1="a", new_arg1="z") + with self.assertRaises(TypeError): + my_func("a", "a", "a", "z") # Test the `predicate` functionality. - my_func(new_arg2="z", arg3="okay value") + my_func(arg3="okay value") + with self.assertWarnsRegex(DeprecationWarning, "arg3"): + my_func(arg3="deprecated value") with self.assertWarnsRegex(DeprecationWarning, "arg3"): - my_func(new_arg2="z", arg3="deprecated value") + my_func("z", "z", "deprecated value") + + def test_deprecate_arg_runtime_warning_variadic_args(self) -> None: + """Test that `@deprecate_arg` errors when defined on a function using `*args` but can + handle `**kwargs`. + + We know how to support *args robustly via `inspect.signature().bind()`, but it has worse + performance (see the commit history of PR #9884). Given how infrequent we expect + deprecations to involve *args, we simply error. + """ + + with self.assertRaises(ValueError): + + @deprecate_arg("my_kwarg", since="9.99") + def my_func1(*args: int, my_kwarg: int | None = None) -> None: + del args + del my_kwarg + + @deprecate_arg("my_kwarg", since="9.99") + def my_func2(my_kwarg: int | None = None, **kwargs) -> None: + # We assign an arbitrary variable `c` because it will be included in + # `my_func.__code__.co_varnames` and we want to make sure that does not break the + # implementation. + c = 0 # pylint: disable=unused-variable + del kwargs + del my_kwarg + + # No warnings if no deprecated args used. + my_func2() + my_func2(another_arg=0, yet_another=0) + + # Warn if argument is specified. + with self.assertWarnsRegex(DeprecationWarning, "my_kwarg"): + my_func2(my_kwarg=5) + with self.assertWarnsRegex(DeprecationWarning, "my_kwarg"): + my_func2(my_kwarg=5, another_arg=0, yet_another=0) def test_deprecate_arguments_runtime_warning(self) -> None: """Test that `@deprecate_arguments` warns whenever the arguments are used. @@ -255,17 +318,36 @@ def test_deprecate_arguments_runtime_warning(self) -> None: Also check that old arguments are passed in as their new alias. """ - @deprecate_arguments({"arg1": None, "arg2": "new_arg2"}, since="9.99") - def my_func(*, arg1: str = "a", new_arg2: str) -> None: - del arg1 - self.assertEqual(new_arg2, "z") + @deprecate_arguments({"arg1": "new_arg1", "arg2": None}, since="9.99") + def my_func(arg1: str = "a", arg2: str = "a", new_arg1: str | None = None) -> None: + del arg2 + # If the old arg was set, we should set its `new_alias` to that value. + if arg1 != "a": + self.assertEqual(new_arg1, "z") + if new_arg1 is not None: + self.assertEqual(new_arg1, "z") - my_func(new_arg2="z") # No warnings if no deprecated args used. + # No warnings if no deprecated args used. + my_func() + my_func(new_arg1="z") + + # Warn if argument is specified, regardless of positional vs kwarg. + with self.assertWarnsRegex(DeprecationWarning, "arg1"): + my_func("z") with self.assertWarnsRegex(DeprecationWarning, "arg1"): - my_func(arg1="a", new_arg2="z") + my_func(arg1="z") + with self.assertWarnsRegex(DeprecationWarning, "arg2"): + my_func("z", "z") with self.assertWarnsRegex(DeprecationWarning, "arg2"): - # `arg2` should be converted into `new_arg2`. - my_func(arg2="z") # pylint: disable=missing-kwoa + my_func(arg2="z") + + # Error if new_alias specified at the same time as old argument name. + with self.assertRaises(TypeError): + my_func("a", new_arg1="z") + with self.assertRaises(TypeError): + my_func(arg1="a", new_arg1="z") + with self.assertRaises(TypeError): + my_func("a", "a", "z") def test_deprecate_function_runtime_warning(self) -> None: """Test that `@deprecate_function` warns whenever the function is used.""" From f1bda21c1d83c2203edca33818d48441663b0f1c Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Wed, 3 May 2023 04:51:54 +0900 Subject: [PATCH 083/172] Address slow test errors (#10052) * address slow test errors * fix --- test/python/algorithms/test_phase_estimator.py | 4 ++-- test/python/primitives/test_backend_estimator.py | 4 ++-- test/python/primitives/test_backend_sampler.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/python/algorithms/test_phase_estimator.py b/test/python/algorithms/test_phase_estimator.py index 15a5ef879f1b..9040bc1d6912 100644 --- a/test/python/algorithms/test_phase_estimator.py +++ b/test/python/algorithms/test_phase_estimator.py @@ -136,8 +136,8 @@ def test_H2_hamiltonian(self): ) state_preparation = StateFn((I ^ H).to_circuit()) evo = PauliTrotterEvolution(trotter_mode="suzuki", reps=4) - - result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=evo) + with self.assertWarns(DeprecationWarning): + result = self.hamiltonian_pe(hamiltonian, state_preparation, evolution=evo) with self.subTest("Most likely eigenvalues"): self.assertAlmostEqual(result.most_likely_eigenvalue, -1.855, delta=0.001) with self.subTest("Most likely phase"): diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index 6759fa491d28..465a8aa9d95f 100644 --- a/test/python/primitives/test_backend_estimator.py +++ b/test/python/primitives/test_backend_estimator.py @@ -339,14 +339,14 @@ def test_bound_pass_manager(self): bound_pass = PassManager(dummy_pass) estimator = BackendEstimator(backend=FakeNairobi(), bound_pass_manager=bound_pass) _ = estimator.run(qc, op).result() - self.assertTrue(mock_pass.call_count == 1) + self.assertEqual(mock_pass.call_count, 1) with self.subTest("Test circuit batch"): with patch.object(DummyTP, "run", wraps=dummy_pass.run) as mock_pass: bound_pass = PassManager(dummy_pass) estimator = BackendEstimator(backend=FakeNairobi(), bound_pass_manager=bound_pass) _ = estimator.run([qc, qc], [op, op]).result() - self.assertTrue(mock_pass.call_count == 2) + self.assertEqual(mock_pass.call_count, 2) @combine(backend=BACKENDS) def test_layout(self, backend): diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index 9627dfdbea7a..cf8cd90556c9 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -392,13 +392,13 @@ def test_bound_pass_manager(self): bound_pass = PassManager(dummy_pass) sampler = BackendSampler(backend=FakeNairobi(), bound_pass_manager=bound_pass) _ = sampler.run(self._circuit[0]).result() - self.assertTrue(mock_pass.call_count == 1) + self.assertEqual(mock_pass.call_count, 1) with patch.object(DummyTP, "run", wraps=dummy_pass.run) as mock_pass: bound_pass = PassManager(dummy_pass) sampler = BackendSampler(backend=FakeNairobi(), bound_pass_manager=bound_pass) _ = sampler.run([self._circuit[0], self._circuit[0]]).result() - self.assertTrue(mock_pass.call_count == 2) + self.assertEqual(mock_pass.call_count, 2) if __name__ == "__main__": From 0ecf46345b34c95f5c8ae97a67b331f9380eda9a Mon Sep 17 00:00:00 2001 From: Giacomo Ranieri Date: Wed, 3 May 2023 13:50:53 +0200 Subject: [PATCH 084/172] Fix of Polynomial definition and copied doc from LinearPauliRotation (#9922) (#9970) * fix: Polynomial Pauli Rotations docs (#9922) * docs: Fix cos and sin argument (#9922) * fix line length (#9970) * fix: whitespace * fix: added suggested brakets Co-authored-by: Julien Gacon * Docs formatting * fix: doc formatting --------- Co-authored-by: Julien Gacon --- .../arithmetic/polynomial_pauli_rotations.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py b/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py index 42011e0c0b59..dfd0191b2447 100644 --- a/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py @@ -140,7 +140,8 @@ class PolynomialPauliRotations(FunctionalPauliRotations): .. math:: - |i\rangle |0\rangle \mapsto \cos(p(i)) |i\rangle |0\rangle + \sin(p(i)) |i\rangle |1\rangle + |i\rangle |0\rangle \mapsto \cos\left(\frac{p(i)}{2}\right) |i\rangle |0\rangle + + \sin\left(\frac{p(i)}{2}\right) |i\rangle |1\rangle Let n be the number of qubits representing the state, d the degree of p(x) and q_i the qubits, where q_0 is the least significant qubit. Then for @@ -153,7 +154,7 @@ class PolynomialPauliRotations(FunctionalPauliRotations): .. math:: - p(x) = \sum_{j=0}^{j=d} c_j x_j + p(x) = \sum_{j=0}^{j=d} c_j x^j where :math:`c` are the input coefficients, ``coeffs``. """ @@ -182,22 +183,32 @@ def __init__( @property def coeffs(self) -> list[float]: - """The multiplicative factor in the rotation angle of the controlled rotations. + """The coefficients of the polynomial. - The rotation angles are ``slope * 2^0``, ``slope * 2^1``, ... , ``slope * 2^(n-1)`` where - ``n`` is the number of state qubits. + ``coeffs[i]`` is the coefficient of the i-th power of the function input :math:`x`, + that means that the rotation angles are based on the coefficients value, + following the formula + + .. math:: + + c_j x^j , j=0, ..., d + + where :math:`d` is the degree of the polynomial :math:`p(x)` and :math:`c` are the coefficients + ``coeffs``. Returns: - The rotation angle common in all controlled rotations. + The coefficients of the polynomial. """ return self._coeffs @coeffs.setter def coeffs(self, coeffs: list[float]) -> None: - """Set the multiplicative factor of the rotation angles. + """Set the coefficients of the polynomial. + + ``coeffs[i]`` is the coefficient of the i-th power of x. Args: - The slope of the rotation angles. + The coefficients of the polynomial. """ self._invalidate() self._coeffs = coeffs From 08a2b147928f0dba528f95961c76656770ea467a Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Wed, 3 May 2023 15:24:20 +0200 Subject: [PATCH 085/172] Mark mcsu2 function as private (#10069) --- .../library/standard_gates/multi_control_rotation_gates.py | 6 +++--- .../notes/0.24/su2-synthesis-295ebf03d9033263.yaml | 7 +++---- test/python/circuit/test_controlled_gate.py | 4 ++-- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py index 410cf7bedc15..547883a6f7a9 100644 --- a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py +++ b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py @@ -83,7 +83,7 @@ def _apply_mcu_graycode(circuit, theta, phi, lam, ctls, tgt, use_basis_gates): last_pattern = pattern -def mcsu2_real_diagonal( +def _mcsu2_real_diagonal( circuit, unitary: np.ndarray, controls: Union[QuantumRegister, List[Qubit]], @@ -232,7 +232,7 @@ def mcrx( use_basis_gates=use_basis_gates, ) else: - mcsu2_real_diagonal(self, RXGate(theta).to_matrix(), control_qubits, target_qubit) + _mcsu2_real_diagonal(self, RXGate(theta).to_matrix(), control_qubits, target_qubit) def mcry( @@ -302,7 +302,7 @@ def mcry( use_basis_gates=use_basis_gates, ) else: - mcsu2_real_diagonal(self, RYGate(theta).to_matrix(), control_qubits, target_qubit) + _mcsu2_real_diagonal(self, RYGate(theta).to_matrix(), control_qubits, target_qubit) else: raise QiskitError(f"Unrecognized mode for building MCRY circuit: {mode}.") diff --git a/releasenotes/notes/0.24/su2-synthesis-295ebf03d9033263.yaml b/releasenotes/notes/0.24/su2-synthesis-295ebf03d9033263.yaml index e8a2bb138d00..2eb4f9d82fcc 100644 --- a/releasenotes/notes/0.24/su2-synthesis-295ebf03d9033263.yaml +++ b/releasenotes/notes/0.24/su2-synthesis-295ebf03d9033263.yaml @@ -1,7 +1,6 @@ --- features: - | - The new function `mcsu2_real_diagonal(circuit, unitary, controls, target, ctrl_state)` - applies a multi-controlled SU(2) gate unitary with one real diagonal. With n-1 controls, - the decomposition requires approximately 16n cx gates. Synthesis of mcrx and mcry without - auxiliary qubits now uses the `mcsu2_real_diagonal` decomposition. + Improve the decomposition of multi-controlled Pauli-X and Pauli-Y rotations on :math:`n` + controls to :math:`16n - 40` CX gates, for :math:`n \geq 4`. This improvement is based + on `arXiv:2302.06377 `__. diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index 66f829cb9192..a40f7747e2e7 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -77,7 +77,7 @@ from qiskit.circuit._utils import _compute_control_matrix import qiskit.circuit.library.standard_gates as allGates from qiskit.extensions import UnitaryGate -from qiskit.circuit.library.standard_gates.multi_control_rotation_gates import mcsu2_real_diagonal +from qiskit.circuit.library.standard_gates.multi_control_rotation_gates import _mcsu2_real_diagonal from .gate_utils import _get_free_params @@ -612,7 +612,7 @@ def test_mcsu2_real_diagonal(self): theta = 0.3 qc = QuantumCircuit(num_ctrls + 1) ry_matrix = RYGate(theta).to_matrix() - mcsu2_real_diagonal(qc, ry_matrix, list(range(num_ctrls)), num_ctrls) + _mcsu2_real_diagonal(qc, ry_matrix, list(range(num_ctrls)), num_ctrls) mcry_matrix = _compute_control_matrix(ry_matrix, 6) self.assertTrue(np.allclose(mcry_matrix, Operator(qc).to_matrix())) From b4037950179c49f4b5349af0e8b644ea510880fe Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Wed, 3 May 2023 08:13:35 -0600 Subject: [PATCH 086/172] Apply new deprecation decorators to providers folder (#9872) * Apply new deprecation decorators to primitives and providers folders * Deprecate the right arg. Oops! Thanks tests! * Fix some issues * Work around tests needing to use kwargs --- qiskit/providers/basicaer/qasm_simulator.py | 13 ++++++++----- qiskit/providers/models/backendconfiguration.py | 16 +++++++++------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/qiskit/providers/basicaer/qasm_simulator.py b/qiskit/providers/basicaer/qasm_simulator.py index 9d476ed6dbd6..f6557866dec6 100644 --- a/qiskit/providers/basicaer/qasm_simulator.py +++ b/qiskit/providers/basicaer/qasm_simulator.py @@ -36,6 +36,7 @@ import numpy as np from qiskit.circuit.quantumcircuit import QuantumCircuit +from qiskit.utils.deprecation import deprecate_arg from qiskit.utils.multiprocessing import local_hardware_info from qiskit.providers.models import QasmBackendConfiguration from qiskit.result import Result @@ -371,6 +372,13 @@ def _validate_measure_sampling(self, experiment): # measure sampling is allowed self._sample_measure = True + @deprecate_arg( + "qobj", + deprecation_description="Using a qobj for the first argument to QasmSimulatorPy.run()", + since="0.22.0", + pending=True, + predicate=lambda qobj: not isinstance(qobj, (QuantumCircuit, list)), + ) def run(self, qobj, **backend_options): """Run qobj asynchronously. @@ -410,11 +418,6 @@ def run(self, qobj, **backend_options): qobj = assemble(qobj, self, **out_options) qobj_options = qobj.config else: - warnings.warn( - "Using a qobj for run() is deprecated and will be removed in a future release.", - PendingDeprecationWarning, - stacklevel=2, - ) qobj_options = qobj.config self._set_options(qobj_config=qobj_options, backend_options=backend_options) job_id = str(uuid.uuid4()) diff --git a/qiskit/providers/models/backendconfiguration.py b/qiskit/providers/models/backendconfiguration.py index 899fe6dc0f48..e4c849728af4 100644 --- a/qiskit/providers/models/backendconfiguration.py +++ b/qiskit/providers/models/backendconfiguration.py @@ -14,7 +14,6 @@ import re import copy import numbers -import warnings from typing import Dict, List, Any, Iterable, Tuple, Union from collections import defaultdict @@ -27,6 +26,7 @@ DriveChannel, MeasureChannel, ) +from qiskit.utils.deprecation import deprecate_arg class GateConfig: @@ -831,6 +831,14 @@ def acquire(self, qubit: int) -> AcquireChannel: raise BackendConfigurationError(f"Invalid index for {qubit}-qubit systems.") return AcquireChannel(qubit) + @deprecate_arg( + "channel", + since="0.19.0", + additional_msg=( + "Instead, use the ``qubits`` argument. This method will now return accurate " + "ControlChannels determined by qubit indices." + ), + ) def control(self, qubits: Iterable[int] = None, channel: int = None) -> List[ControlChannel]: """ Return the secondary drive channel for the given qubit -- typically utilized for @@ -848,12 +856,6 @@ def control(self, qubits: Iterable[int] = None, channel: int = None) -> List[Con List of control channels. """ if channel is not None: - warnings.warn( - "The channel argument has been deprecated in favor of qubits. " - "This method will now return accurate ControlChannels determined " - "by qubit indices.", - DeprecationWarning, - ) qubits = [channel] try: if isinstance(qubits, list): From 93b561cc431be8d61a576b9604d11faeebc90446 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Wed, 3 May 2023 14:43:40 -0400 Subject: [PATCH 087/172] Filter possible 2q decomposers more carefully in unitary synthesis (#10008) * Detect invalid input in XXDecomposer and raise errors * Allow passing weyl decomposition to 2q decomposers This is an optimization. We plan to compute this decomposition before calling the 2q decomposers. So we want to be able to reuse it. * Decompose unitary to synthesize to filter 2q decomposers * Format with black * Remove obsolete function * Reformat * Fix lint complaints * Add tests for new error raised in XXDecomposer * lighter-weight bug fix * update test case for product of unitaries --------- Co-authored-by: Ali Javadi --- .../synthesis/xx_decompose/decomposer.py | 7 +++ .../passes/synthesis/unitary_synthesis.py | 33 ++++++------ .../xx_decompose/test_decomposer.py | 23 ++++++++ .../transpiler/test_unitary_synthesis.py | 53 ++++++++++++++++++- 4 files changed, 99 insertions(+), 17 deletions(-) diff --git a/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py b/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py index 3d5714b5c2ab..2527816530c8 100644 --- a/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py +++ b/qiskit/quantum_info/synthesis/xx_decompose/decomposer.py @@ -151,6 +151,13 @@ def _best_decomposition(canonical_coordinate, available_strengths): canonical_coordinate = np.array(canonical_coordinate) while True: + if len(priority_queue) == 0: + if len(available_strengths) == 0: + raise QiskitError( + "Attempting to synthesize entangling gate with no controlled gates in basis set." + ) + raise QiskitError("Unable to synthesize a 2q unitary with the supplied basis set.") + sequence_cost, sequence = heapq.heappop(priority_queue) strength_polytope = XXPolytope.from_strengths(*[x / 2 for x in sequence]) diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 9b42567b8697..efd92085887b 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -701,24 +701,25 @@ def is_controlled(gate): # if we are using the approximation_degree knob, use it to scale already-given fidelities if approximation_degree is not None: basis_2q_fidelity = {k: v * approximation_degree for k, v in basis_2q_fidelity.items()} - for basis_1q in available_1q_basis: - if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX": - pi2_decomposer = TwoQubitBasisDecomposer( - pi2_basis, - euler_basis=basis_1q, + if basis_2q_fidelity: + for basis_1q in available_1q_basis: + if isinstance(pi2_basis, CXGate) and basis_1q == "ZSX": + pi2_decomposer = TwoQubitBasisDecomposer( + pi2_basis, + euler_basis=basis_1q, + basis_fidelity=basis_2q_fidelity, + pulse_optimize=True, + ) + embodiments.update({pi / 2: XXEmbodiments[type(pi2_decomposer.gate)]}) + else: + pi2_decomposer = None + decomposer = XXDecomposer( basis_fidelity=basis_2q_fidelity, - pulse_optimize=True, + euler_basis=basis_1q, + embodiments=embodiments, + backup_optimizer=pi2_decomposer, ) - embodiments.update({pi / 2: XXEmbodiments[type(pi2_decomposer.gate)]}) - else: - pi2_decomposer = None - decomposer = XXDecomposer( - basis_fidelity=basis_2q_fidelity, - euler_basis=basis_1q, - embodiments=embodiments, - backup_optimizer=pi2_decomposer, - ) - decomposers.append(decomposer) + decomposers.append(decomposer) self._decomposer_cache[qubits_tuple] = decomposers return decomposers diff --git a/test/python/quantum_info/xx_decompose/test_decomposer.py b/test/python/quantum_info/xx_decompose/test_decomposer.py index 5c21ac13f384..c4309b327d5f 100644 --- a/test/python/quantum_info/xx_decompose/test_decomposer.py +++ b/test/python/quantum_info/xx_decompose/test_decomposer.py @@ -22,6 +22,7 @@ from scipy.stats import unitary_group import qiskit +from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.quantum_info.operators import Operator from qiskit.quantum_info.synthesis.xx_decompose.decomposer import ( XXDecomposer, @@ -132,3 +133,25 @@ def test_compilation_improvement(self): # the following are taken from Fig 14 of the XX synthesis paper self.assertAlmostEqual(mean(clever_costs), 1.445e-2, delta=5e-3) self.assertAlmostEqual(mean(naive_costs), 2.058e-2, delta=5e-3) + + def test_error_on_empty_basis_fidelity(self): + """Test synthesizing entangling gate with no entangling basis fails.""" + decomposer = XXDecomposer(basis_fidelity={}) + qc = QuantumCircuit(2) + qc.cx(0, 1) + mat = Operator(qc).to_matrix() + with self.assertRaisesRegex( + qiskit.exceptions.QiskitError, + "Attempting to synthesize entangling gate with no controlled gates in basis set.", + ): + decomposer(mat) + + def test_no_error_on_empty_basis_fidelity_trivial_target(self): + """Test synthesizing non-entangling gate with no entangling basis succeeds.""" + decomposer = XXDecomposer(basis_fidelity={}) + qc = QuantumCircuit(2) + qc.x(0) + qc.y(1) + mat = Operator(qc).to_matrix() + dqc = decomposer(mat) + self.assertTrue(np.allclose(mat, Operator(dqc).to_matrix())) diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 9731f84214ee..39a2a5880172 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -47,7 +47,12 @@ TrivialLayout, ) from qiskit.circuit.library import ( + IGate, CXGate, + RZGate, + SXGate, + XGate, + iSwapGate, ECRGate, UGate, ZGate, @@ -55,6 +60,7 @@ RZZGate, RXXGate, ) +from qiskit.circuit import Measure from qiskit.circuit.controlflow import IfElseOp from qiskit.circuit import Parameter, Gate @@ -855,7 +861,7 @@ def __init__(self): unitary_synth_pass = UnitarySynthesis(target=target) result_dag = unitary_synth_pass.run(dag) result_qc = dag_to_circuit(result_dag) - self.assertEqual(result_qc, QuantumCircuit(2)) + self.assertEqual(result_qc, qc) def test_default_does_not_fail_on_no_syntheses(self): qc = QuantumCircuit(1) @@ -863,6 +869,51 @@ def test_default_does_not_fail_on_no_syntheses(self): pass_ = UnitarySynthesis(["unknown", "gates"]) self.assertEqual(qc, pass_(qc)) + def test_iswap_no_cx_synthesis_succeeds(self): + """Test basis set with iswap but no cx can synthesize a circuit""" + target = Target() + theta = Parameter("theta") + + i_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + } + target.add_instruction(IGate(), i_props) + rz_props = { + (0,): InstructionProperties(duration=0, error=0), + (1,): InstructionProperties(duration=0, error=0), + } + target.add_instruction(RZGate(theta), rz_props) + sx_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + } + target.add_instruction(SXGate(), sx_props) + x_props = { + (0,): InstructionProperties(duration=35.5e-9, error=0.000413), + (1,): InstructionProperties(duration=35.5e-9, error=0.000502), + } + target.add_instruction(XGate(), x_props) + iswap_props = { + (0, 1): InstructionProperties(duration=519.11e-9, error=0.01201), + (1, 0): InstructionProperties(duration=554.66e-9, error=0.01201), + } + target.add_instruction(iSwapGate(), iswap_props) + measure_props = { + (0,): InstructionProperties(duration=5.813e-6, error=0.0751), + (1,): InstructionProperties(duration=5.813e-6, error=0.0225), + } + target.add_instruction(Measure(), measure_props) + + qc = QuantumCircuit(2) + cxmat = Operator(CXGate()).to_matrix() + qc.unitary(cxmat, [0, 1]) + unitary_synth_pass = UnitarySynthesis(target=target) + dag = circuit_to_dag(qc) + result_dag = unitary_synth_pass.run(dag) + result_qc = dag_to_circuit(result_dag) + self.assertTrue(np.allclose(Operator(result_qc.to_gate()).to_matrix(), cxmat)) + if __name__ == "__main__": unittest.main() From 72e7481be881243b31cbfa7b537191ed27196dde Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 3 May 2023 16:52:41 -0400 Subject: [PATCH 088/172] Undo breaking API change in unitary synthesis plugin interface (#10066) * Undo breaking API change in unitary synthesis plugin interface This commit reverts an unecessary breaking API change that was introduced in #9175. In that PR the data type for two arguments, gate_lengths and gate_errors, that was potentially passed to plugins was changed. This change was well intentioned as it was used by the default plugin as part of the changes to the default plugin made in that PR. But this neglected the downstream effects for any existing plugin interface users relying on the input to those fields. Making this change to existing fields would break any plugins that were using the old data format and is a violation of the Qiskit stability and deprecation policy. This commit reverts that change so that gate_lengths and gate_errors contain the same data format as in previous releases. Instead to facilitate the new workflow a new fields, gate_lengths_by_qubit and gate_errors_by_qubit, were added that leverage the new format introduced by #9175. By adding this as new optional fields existing users can continue to work as before, but the default plugin which requires the different data format can use the new data type. * Update docstring * Fix copy paste error --- qiskit/transpiler/passes/synthesis/plugin.py | 72 +++++++++++++++-- .../passes/synthesis/unitary_synthesis.py | 79 ++++++++++++++++++- ...esis-based-on-errors-8041fcc9584f5db2.yaml | 18 +++-- .../test_unitary_synthesis_plugin.py | 8 +- 4 files changed, 157 insertions(+), 20 deletions(-) diff --git a/qiskit/transpiler/passes/synthesis/plugin.py b/qiskit/transpiler/passes/synthesis/plugin.py index 0eb439c67e75..8ecbc4e08c77 100644 --- a/qiskit/transpiler/passes/synthesis/plugin.py +++ b/qiskit/transpiler/passes/synthesis/plugin.py @@ -84,6 +84,14 @@ def supports_gate_lengths(self): def supports_gate_errors(self): return False + @property + def supports_gate_lengths_by_qubit(self): + return False + + @property + def supports_gate_errors_by_qubit(self): + return False + @property def min_qubits(self): return None @@ -340,17 +348,69 @@ def supports_pulse_optimize(self): """ pass + @property + def supports_gate_lengths_by_qubit(self): + """Return whether the plugin supports taking ``gate_lengths_by_qubit`` + + This differs from ``supports_gate_lengths``/``gate_lengths`` by using a different + view of the same data. Instead of being keyed by gate name this is keyed by qubit + and uses :class:`~.Gate` instances to represent gates (instead of gate names) + + ``gate_lengths_by_qubit`` will be a dictionary in the form of + ``{(qubits,): [Gate, length]}``. For example:: + + { + (0,): [SXGate(): 0.0006149355812506126, RZGate(): 0.0], + (0, 1): [CXGate(): 0.012012477900732316] + } + + where the ``length`` value is in units of seconds. + + Do note that this dictionary might not be complete or could be empty + as it depends on the target backend reporting gate lengths on every + gate for each qubit. + + This defaults to False + """ + return False + + @property + def supports_gate_errors_by_qubit(self): + """Return whether the plugin supports taking ``gate_errors_by_qubit`` + + This differs from ``supports_gate_errors``/``gate_errors`` by using a different + view of the same data. Instead of being keyed by gate name this is keyed by qubit + and uses :class:`~.Gate` instances to represent gates (instead of gate names). + + ``gate_errors_by_qubit`` will be a dictionary in the form of + ``{(qubits,): [Gate, error]}``. For example:: + + { + (0,): [SXGate(): 0.0006149355812506126, RZGate(): 0.0], + (0, 1): [CXGate(): 0.012012477900732316] + } + + Do note that this dictionary might not be complete or could be empty + as it depends on the target backend reporting gate errors on every + gate for each qubit. The gate error rates reported in ``gate_errors`` + are provided by the target device ``Backend`` object and the exact + meaning might be different depending on the backend. + + This defaults to False + """ + return False + @property @abc.abstractmethod def supports_gate_lengths(self): """Return whether the plugin supports taking ``gate_lengths`` ``gate_lengths`` will be a dictionary in the form of - ``{(qubits,): [Gate, length]}``. For example:: + ``{gate_name: {(qubit_1, qubit_2): length}}``. For example:: { - (0,): [SXGate(): 0.0006149355812506126, RZGate(): 0.0], - (0, 1): [CXGate(): 0.012012477900732316] + 'sx': {(0,): 0.0006149355812506126, (1,): 0.0006149355812506126}, + 'cx': {(0, 1): 0.012012477900732316, (1, 0): 5.191111111111111e-07} } where the ``length`` value is in units of seconds. @@ -367,11 +427,11 @@ def supports_gate_errors(self): """Return whether the plugin supports taking ``gate_errors`` ``gate_errors`` will be a dictionary in the form of - ``{(qubits,): [Gate, error]}``. For example:: + ``{gate_name: {(qubit_1, qubit_2): error}}``. For example:: { - (0,): [SXGate(): 0.0006149355812506126, RZGate(): 0.0], - (0, 1): [CXGate(): 0.012012477900732316] + 'sx': {(0,): 0.0006149355812506126, (1,): 0.0006149355812506126}, + 'cx': {(0, 1): 0.012012477900732316, (1, 0): 5.191111111111111e-07} } Do note that this dictionary might not be complete or could be empty diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index efd92085887b..881d3cc10553 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -386,6 +386,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: plugin_method = DefaultUnitarySynthesis() plugin_kwargs = {"config": self._plugin_config} _gate_lengths = _gate_errors = None + _gate_lengths_by_qubit = _gate_errors_by_qubit = None if self.method == "default": # If the method is the default, we only need to evaluate one set of keyword arguments. @@ -417,6 +418,16 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: if method.supports_gate_errors: _gate_errors = _gate_errors or _build_gate_errors(self._backend_props, self._target) kwargs["gate_errors"] = _gate_errors + if method.supports_gate_lengths_by_qubit: + _gate_lengths_by_qubit = _gate_lengths_by_qubit or _build_gate_lengths_by_qubit( + self._backend_props, self._target + ) + kwargs["gate_lengths_by_qubit"] = _gate_lengths_by_qubit + if method.supports_gate_errors_by_qubit: + _gate_errors_by_qubit = _gate_errors_by_qubit or _build_gate_errors_by_qubit( + self._backend_props, self._target + ) + kwargs["gate_errors_by_qubit"] = _gate_errors_by_qubit supported_bases = method.supported_bases if supported_bases is not None: kwargs["matched_basis"] = _choose_bases(self._basis_gates, supported_bases) @@ -479,6 +490,58 @@ def _recurse(dag): def _build_gate_lengths(props=None, target=None): + """Builds a ``gate_lengths`` dictionary from either ``props`` (BackendV1) + or ``target`` (BackendV2). + + The dictionary has the form: + {gate_name: {(qubits,): duration}} + """ + gate_lengths = {} + if target is not None: + for gate, prop_dict in target.items(): + gate_lengths[gate] = {} + for qubit, gate_props in prop_dict.items(): + if gate_props is not None and gate_props.duration is not None: + gate_lengths[gate][qubit] = gate_props.duration + elif props is not None: + for gate in props._gates: + gate_lengths[gate] = {} + for k, v in props._gates[gate].items(): + length = v.get("gate_length") + if length: + gate_lengths[gate][k] = length[0] + if not gate_lengths[gate]: + del gate_lengths[gate] + return gate_lengths + + +def _build_gate_errors(props=None, target=None): + """Builds a ``gate_error`` dictionary from either ``props`` (BackendV1) + or ``target`` (BackendV2). + + The dictionary has the form: + {gate_name: {(qubits,): error_rate}} + """ + gate_errors = {} + if target is not None: + for gate, prop_dict in target.items(): + gate_errors[gate] = {} + for qubit, gate_props in prop_dict.items(): + if gate_props is not None and gate_props.error is not None: + gate_errors[gate][qubit] = gate_props.error + if props is not None: + for gate in props._gates: + gate_errors[gate] = {} + for k, v in props._gates[gate].items(): + error = v.get("gate_error") + if error: + gate_errors[gate][k] = error[0] + if not gate_errors[gate]: + del gate_errors[gate] + return gate_errors + + +def _build_gate_lengths_by_qubit(props=None, target=None): """ Builds a `gate_lengths` dictionary from either `props` (BackendV1) or `target (BackendV2)`. @@ -511,7 +574,7 @@ def _build_gate_lengths(props=None, target=None): return gate_lengths -def _build_gate_errors(props=None, target=None): +def _build_gate_errors_by_qubit(props=None, target=None): """ Builds a `gate_error` dictionary from either `props` (BackendV1) or `target (BackendV2)`. @@ -565,10 +628,18 @@ def supports_pulse_optimize(self): @property def supports_gate_lengths(self): - return True + return False @property def supports_gate_errors(self): + return False + + @property + def supports_gate_lengths_by_qubit(self): + return True + + @property + def supports_gate_errors_by_qubit(self): return True @property @@ -734,8 +805,8 @@ def run(self, unitary, **options): coupling_map = options["coupling_map"][0] natural_direction = options["natural_direction"] pulse_optimize = options["pulse_optimize"] - gate_lengths = options["gate_lengths"] - gate_errors = options["gate_errors"] + gate_lengths = options["gate_lengths_by_qubit"] + gate_errors = options["gate_errors_by_qubit"] qubits = options["coupling_map"][1] target = options["target"] diff --git a/releasenotes/notes/0.24/unitary-synthesis-based-on-errors-8041fcc9584f5db2.yaml b/releasenotes/notes/0.24/unitary-synthesis-based-on-errors-8041fcc9584f5db2.yaml index b86115b75e40..0fab89012d16 100644 --- a/releasenotes/notes/0.24/unitary-synthesis-based-on-errors-8041fcc9584f5db2.yaml +++ b/releasenotes/notes/0.24/unitary-synthesis-based-on-errors-8041fcc9584f5db2.yaml @@ -9,11 +9,17 @@ features: (e.g. RZ-RX-RZ or RZ-RY-RZ), generic unitary basis (e.g. U), and a few others. For a two-qubit decomposition, it can target any supercontrolled basis - (e.g. CNOT, iSWAP, B) or multiple controlled basis + (e.g. CNOT, iSWAP, B) or multiple controlled basis (e.g. CZ, CH, ZZ^.5, ZX^.2, etc.). -upgrade: - | - The interface for :class:`~.UnitarySynthesisPlugin` has been changed so - now `gate_lengths` and `gate_errors` have the form - ``{(qubits,): [Gate, length]}``. This allows gates that have the same - name but different parameters to be represented separately. + The interface for :class:`~.UnitarySynthesisPlugin` has two new + optional properties ``supports_gate_lengths_by_qubit`` and + ``supports_gate_errors_by_qubit`` which when set will add the + fields ``gate_lengths_by_qubit`` and ``gate_errors_by_qubit`` + respectively to the input options to the plugin's ``run()`` method. + These new fields are an alternative view of the data provided by + ``gate_lengths`` and ``gate_errors`` but instead have the form: + ``{(qubits,): [Gate, length]}`` (where ``Gate`` is the instance + of :class:`~.Gate` for that definition). This allows plugins to + reason about working with gates of the same type but but that + have different parameters set. diff --git a/test/python/transpiler/test_unitary_synthesis_plugin.py b/test/python/transpiler/test_unitary_synthesis_plugin.py index 57ccf6437139..4aae26987319 100644 --- a/test/python/transpiler/test_unitary_synthesis_plugin.py +++ b/test/python/transpiler/test_unitary_synthesis_plugin.py @@ -226,8 +226,8 @@ def test_all_keywords_passed_to_default_on_fallback(self): expected_kwargs = [ "basis_gates", "coupling_map", - "gate_errors", - "gate_lengths", + "gate_errors_by_qubit", + "gate_lengths_by_qubit", "natural_direction", "pulse_optimize", ] @@ -294,8 +294,8 @@ def test_config_not_passed_to_default_on_fallback(self): expected_kwargs = [ "basis_gates", "coupling_map", - "gate_errors", - "gate_lengths", + "gate_errors_by_qubit", + "gate_lengths_by_qubit", "natural_direction", "pulse_optimize", ] From 4dfef130859c470b23e74dbb3e3a5e5b097f7a99 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 5 May 2023 17:28:34 +0200 Subject: [PATCH 089/172] Drop support for Python 3.7 (#10009) * Drop support for Python 3.7 This updates our documentation and build processes to set Python 3.8 as the oldest support version of Python, consistent with our policy on Python support. This commit also removes remaining Python-version gating, since we were principally using this to account for differences between 3.7 and 3.8. Several backport packages are no longer required for any suppported Python version, because of this change. * Remove test for multiprocessing on Mac With Python 3.7 support now dropped, there are no versions of Python on macOS that we expect to support with multiprocessing again. There's no guarantee that the hypothetical Python 10.11 (???) that this test was previously checking would be any more valid than existing versions. --------- Co-authored-by: Matthew Treinish --- .github/workflows/wheels.yml | 6 +++--- CONTRIBUTING.md | 14 +++++++------- azure-pipelines.yml | 4 ++-- pyproject.toml | 4 ++-- qiskit/__init__.py | 9 --------- .../gradients/finite_diff_estimator_gradient.py | 7 +------ .../gradients/finite_diff_sampler_gradient.py | 8 +------- qiskit/algorithms/optimizers/optimizer.py | 9 +-------- qiskit/algorithms/optimizers/p_bfgs.py | 12 +++++------- qiskit/circuit/controlflow/switch_case.py | 8 +------- qiskit/compiler/transpiler.py | 9 ++------- qiskit/pulse/builder.py | 7 +------ qiskit/qasm2/__init__.py | 8 +------- qiskit/qobj/converters/pulse_instruction.py | 7 +------ qiskit/tools/parallel.py | 5 +---- qiskit/transpiler/passes/basis/basis_translator.py | 7 +------ qiskit/utils/multiprocessing.py | 13 ++----------- qiskit/version.py | 6 +----- .../notes/drop-python3.7-8689e1fa349a49df.yaml | 6 ++++++ requirements-dev.txt | 2 +- requirements.txt | 3 --- setup.py | 3 +-- test/python/test_examples.py | 4 ++-- test/python/tools/jupyter/test_notebooks.py | 2 +- test/python/tools/test_parallel.py | 7 ------- test/python/visualization/test_gate_map.py | 7 ------- tox.ini | 2 +- 27 files changed, 45 insertions(+), 134 deletions(-) create mode 100644 releasenotes/notes/drop-python3.7-8689e1fa349a49df.yaml diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index c94dd428d870..7b7bebc33e0a 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.7' + python-version: '3.10' - uses: dtolnay/rust-toolchain@stable - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -50,7 +50,7 @@ jobs: - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.7' + python-version: '3.10' - uses: dtolnay/rust-toolchain@stable - name: Set up QEMU uses: docker/setup-qemu-action@v1 @@ -83,7 +83,7 @@ jobs: - uses: actions/setup-python@v4 name: Install Python with: - python-version: '3.7' + python-version: '3.10' - uses: dtolnay/rust-toolchain@stable - name: Set up QEMU uses: docker/setup-qemu-action@v1 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 638ec5483f0d..73eacde8cf57 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -276,12 +276,12 @@ environment that tox sets up matches the CI environment more closely and it runs the tests in parallel (resulting in much faster execution). To run tests on all installed supported python versions and lint/style checks you can simply run `tox`. Or if you just want to run the tests once run for a specific python -version: `tox -epy37` (or replace py37 with the python version you want to use, -py35 or py36). +version: `tox -epy310` (or replace py310 with the python version you want to use, +py39 or py311). If you just want to run a subset of tests you can pass a selection regex to the test runner. For example, if you want to run all tests that have "dag" in -the test id you can run: `tox -epy37 -- dag`. You can pass arguments directly to +the test id you can run: `tox -epy310 -- dag`. You can pass arguments directly to the test runner after the bare `--`. To see all the options on test selection you can refer to the stestr manual: https://stestr.readthedocs.io/en/stable/MANUAL.html#test-selection @@ -291,21 +291,21 @@ you can do this faster with the `-n`/`--no-discover` option. For example: to run a module: ``` -tox -epy37 -- -n test.python.test_examples +tox -epy310 -- -n test.python.test_examples ``` or to run the same module by path: ``` -tox -epy37 -- -n test/python/test_examples.py +tox -epy310 -- -n test/python/test_examples.py ``` to run a class: ``` -tox -epy37 -- -n test.python.test_examples.TestPythonExamples +tox -epy310 -- -n test.python.test_examples.TestPythonExamples ``` to run a method: ``` -tox -epy37 -- -n test.python.test_examples.TestPythonExamples.test_all_examples +tox -epy310 -- -n test.python.test_examples.TestPythonExamples.test_all_examples ``` Alternatively there is a makefile provided to run tests, however this diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 015bf7a777ef..91aee4bd562e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -34,12 +34,12 @@ parameters: - name: "supportedPythonVersions" displayName: "All supported versions of Python" type: object - default: ["3.7", "3.8", "3.9", "3.10", "3.11"] + default: ["3.8", "3.9", "3.10", "3.11"] - name: "minimumPythonVersion" displayName: "Minimum supported version of Python" type: string - default: "3.7" + default: "3.8" - name: "maximumPythonVersion" displayName: "Maximum supported version of Python" diff --git a/pyproject.toml b/pyproject.toml index d8eebc130298..57400392b35e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,12 +4,12 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 100 -target-version = ['py37', 'py38', 'py39', 'py310', 'py311'] +target-version = ['py38', 'py39', 'py310', 'py311'] [tool.cibuildwheel] manylinux-x86_64-image = "manylinux2014" manylinux-i686-image = "manylinux2014" -skip = "pp* cp36-* *musllinux*" +skip = "pp* cp36-* cp37-* *musllinux*" test-skip = "cp310-win32 cp310-manylinux_i686 cp311-win32 cp311-manylinux_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/qiskit/__init__.py b/qiskit/__init__.py index 76bb5dace4c5..a8d90d12220f 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -21,15 +21,6 @@ import qiskit._accelerate -if sys.version_info < (3, 8): - warnings.warn( - "Using Qiskit with Python 3.7 is deprecated as of the 0.23.0 release. " - "Support for running Qiskit with Python 3.7 will be removed in the " - "0.25.0 release", - DeprecationWarning, - ) - - # 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 diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py index a588b74dedcf..f154b99db605 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py @@ -14,8 +14,8 @@ from __future__ import annotations -import sys from collections.abc import Sequence +from typing import Literal import numpy as np @@ -30,11 +30,6 @@ from ..exceptions import AlgorithmError -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - class FiniteDiffEstimatorGradient(BaseEstimatorGradient): """ diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py index 8926e13892d4..11bb2ec38749 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py @@ -14,8 +14,8 @@ from __future__ import annotations -import sys from collections import defaultdict +from typing import Literal, Sequence import numpy as np @@ -28,12 +28,6 @@ from ..exceptions import AlgorithmError -if sys.version_info >= (3, 8): - # pylint: disable=ungrouped-imports - from typing import Literal, Sequence -else: - from typing_extensions import Literal - class FiniteDiffSamplerGradient(BaseSamplerGradient): """ diff --git a/qiskit/algorithms/optimizers/optimizer.py b/qiskit/algorithms/optimizers/optimizer.py index 3f64384222d9..e253167b5d9d 100644 --- a/qiskit/algorithms/optimizers/optimizer.py +++ b/qiskit/algorithms/optimizers/optimizer.py @@ -18,20 +18,13 @@ from collections.abc import Callable from enum import IntEnum import logging -import sys -from typing import Any, Union +from typing import Any, Union, Protocol import numpy as np import scipy from qiskit.algorithms.algorithm_result import AlgorithmResult -if sys.version_info >= (3, 8): - # pylint: disable=ungrouped-imports - from typing import Protocol -else: - from typing_extensions import Protocol - logger = logging.getLogger(__name__) POINT = Union[float, np.ndarray] diff --git a/qiskit/algorithms/optimizers/p_bfgs.py b/qiskit/algorithms/optimizers/p_bfgs.py index a59a15cbca64..b06e5f703866 100644 --- a/qiskit/algorithms/optimizers/p_bfgs.py +++ b/qiskit/algorithms/optimizers/p_bfgs.py @@ -16,7 +16,6 @@ import logging import multiprocessing import platform -import sys from collections.abc import Callable from typing import SupportsFloat @@ -109,12 +108,11 @@ def minimize( # default. The fork start method should be considered unsafe as it can # lead to crashes. # However P_BFGS doesn't support spawn, so we revert to single process. - if sys.version_info >= (3, 8): - num_procs = 0 - logger.warning( - "For MacOS, python >= 3.8, using only current process. " - "Multiple core use not supported." - ) + num_procs = 0 + logger.warning( + "For MacOS, python >= 3.8, using only current process. " + "Multiple core use not supported." + ) elif platform.system() == "Windows": num_procs = 0 logger.warning( diff --git a/qiskit/circuit/controlflow/switch_case.py b/qiskit/circuit/controlflow/switch_case.py index 30586f25d9bf..179254970f90 100644 --- a/qiskit/circuit/controlflow/switch_case.py +++ b/qiskit/circuit/controlflow/switch_case.py @@ -15,8 +15,7 @@ __all__ = ("SwitchCaseOp", "CASE_DEFAULT") import contextlib -import sys -from typing import Union, Iterable, Any, Tuple, Optional, List +from typing import Union, Iterable, Any, Tuple, Optional, List, Literal from qiskit.circuit import ClassicalRegister, Clbit, QuantumCircuit from qiskit.circuit.exceptions import CircuitError @@ -25,11 +24,6 @@ from .control_flow import ControlFlowOp from ._builder_utils import unify_circuit_resources, partition_registers -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - class _DefaultCaseType: """The type of the default-case singleton. This is used instead of just having diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index 40fd9ff52659..dc93c85a5fcf 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -19,9 +19,10 @@ import logging import os import pickle -import sys from time import time from typing import List, Union, Dict, Callable, Any, Optional, Tuple, Iterable, TypeVar +from multiprocessing.shared_memory import SharedMemory +from multiprocessing.managers import SharedMemoryManager import warnings from qiskit import user_config @@ -48,12 +49,6 @@ from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.target import Target, target_to_backend_properties -if sys.version_info >= (3, 8): - from multiprocessing.shared_memory import SharedMemory - from multiprocessing.managers import SharedMemoryManager -else: - from shared_memory import SharedMemory, SharedMemoryManager - logger = logging.getLogger(__name__) _CircuitT = TypeVar("_CircuitT", bound=Union[QuantumCircuit, List[QuantumCircuit]]) diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index eda0b9946219..3a34e31e817d 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -461,10 +461,10 @@ import contextvars import functools import itertools -import sys import uuid import warnings from contextlib import contextmanager +from functools import singledispatchmethod from typing import ( Any, Callable, @@ -499,11 +499,6 @@ from qiskit.pulse.schedule import Schedule, ScheduleBlock from qiskit.pulse.transforms.alignments import AlignmentKind -if sys.version_info >= (3, 8): - from functools import singledispatchmethod -else: - from singledispatchmethod import singledispatchmethod - #: contextvars.ContextVar[BuilderContext]: active builder BUILDER_CONTEXTVAR = contextvars.ContextVar("backend") diff --git a/qiskit/qasm2/__init__.py b/qiskit/qasm2/__init__.py index 349321539435..79765f3951bb 100644 --- a/qiskit/qasm2/__init__.py +++ b/qiskit/qasm2/__init__.py @@ -385,9 +385,8 @@ def from_qasm_str(qasm_str: str): ] import os -import sys from pathlib import Path -from typing import Iterable, Union, Optional +from typing import Iterable, Union, Optional, Literal # Pylint can't handle the C-extension introspection of `_qasm2` because there's a re-import through # to `qiskit.qasm2.exceptions`, and pylint ends up trying to import `_qasm2` twice, which PyO3 @@ -405,11 +404,6 @@ def from_qasm_str(qasm_str: str): LEGACY_CUSTOM_CLASSICAL, ) -if sys.version_info >= (3, 8): - from typing import Literal -else: - from typing_extensions import Literal - LEGACY_INCLUDE_PATH = ( Path(__file__).parents[1] / "qasm" / "libs", diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 4ea888cc8a67..9d4635ad7e7b 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -16,9 +16,9 @@ import hashlib import re -import sys import warnings from enum import Enum +from functools import singledispatchmethod from typing import Union, List, Iterator, Optional import numpy as np @@ -32,11 +32,6 @@ from qiskit.qobj.utils import MeasLevel from qiskit.utils.deprecation import deprecate_func -if sys.version_info >= (3, 8): - from functools import singledispatchmethod -else: - from singledispatchmethod import singledispatchmethod - class ParametricPulseShapes(Enum): """Map the assembled pulse names to the pulse module waveforms. diff --git a/qiskit/tools/parallel.py b/qiskit/tools/parallel.py index cfbcd6c7ceb5..1b48791e3477 100644 --- a/qiskit/tools/parallel.py +++ b/qiskit/tools/parallel.py @@ -72,10 +72,7 @@ def get_platform_parallel_default(): parallel_default = False # On macOS default false on Python >=3.8 elif sys.platform == "darwin": - if sys.version_info[0] == 3 and sys.version_info[1] >= 8: - parallel_default = False - else: - parallel_default = True + parallel_default = False # On linux (and other OSes) default to True else: parallel_default = True diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 47796592bd11..3d74b7246754 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -13,10 +13,10 @@ """Translates gates to a target basis using a given equivalence library.""" -import sys import time import logging +from functools import singledispatchmethod from itertools import zip_longest from collections import defaultdict @@ -29,11 +29,6 @@ from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError -if sys.version_info >= (3, 8): - from functools import singledispatchmethod -else: - from singledispatchmethod import singledispatchmethod - logger = logging.getLogger(__name__) diff --git a/qiskit/utils/multiprocessing.py b/qiskit/utils/multiprocessing.py index b6617bcfd4d9..95964b3d5565 100644 --- a/qiskit/utils/multiprocessing.py +++ b/qiskit/utils/multiprocessing.py @@ -14,7 +14,6 @@ import multiprocessing as mp import platform -import sys import psutil @@ -44,14 +43,6 @@ def is_main_process(): if platform.system() == "Windows": return not isinstance(mp.current_process(), mp.context.SpawnProcess) else: - return not ( - isinstance(mp.current_process(), (mp.context.ForkProcess, mp.context.SpawnProcess)) - # In python 3.5 and 3.6, processes created by "ProcessPoolExecutor" are not - # mp.context.ForkProcess or mp.context.SpawnProcess. As a workaround, - # "name" of the process is checked instead. - or ( - sys.version_info[0] == 3 - and (sys.version_info[1] == 5 or sys.version_info[1] == 6) - and mp.current_process().name != "MainProcess" - ) + return not isinstance( + mp.current_process(), (mp.context.ForkProcess, mp.context.SpawnProcess) ) diff --git a/qiskit/version.py b/qiskit/version.py index e5c5cdb50974..6fed7c92bc7e 100644 --- a/qiskit/version.py +++ b/qiskit/version.py @@ -16,7 +16,6 @@ import os import subprocess -import sys from collections.abc import Mapping ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -102,10 +101,7 @@ def __init__(self): self._loaded = False def _load_versions(self): - if sys.version_info >= (3, 8): - from importlib.metadata import version - else: - from importlib_metadata import version + from importlib.metadata import version try: # TODO: Update to use qiskit_aer instead when we remove the diff --git a/releasenotes/notes/drop-python3.7-8689e1fa349a49df.yaml b/releasenotes/notes/drop-python3.7-8689e1fa349a49df.yaml new file mode 100644 index 000000000000..0bc05b296f2b --- /dev/null +++ b/releasenotes/notes/drop-python3.7-8689e1fa349a49df.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + Qiskit Terra 0.25 has dropped support for Python 3.7 following deprecation warnings started in + Qiskit Terra 0.23. This is consistent with Python 3.7's end-of-life on the 27th of June, 2023. + To continue using Qiskit, you must upgrade to a more recent version of Python. diff --git a/requirements-dev.txt b/requirements-dev.txt index 1ec6cb80579f..cf0990e6a36a 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -26,4 +26,4 @@ scikit-quant<=0.7;platform_system != 'Windows' jax;platform_system != 'Windows' jaxlib;platform_system != 'Windows' docplex -qiskit-qasm3-import; python_version>='3.8' +qiskit-qasm3-import diff --git a/requirements.txt b/requirements.txt index 1c1b0da48a4c..bf946eda74f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,9 +11,6 @@ stevedore>=3.0.0 # multiplication in version 0.10 wich breaks parameter assignment test # (can be removed once issue is fix) symengine>=0.9, <0.10; platform_machine == 'x86_64' or platform_machine == 'aarch64' or platform_machine == 'ppc64le' or platform_machine == 'amd64' or platform_machine == 'arm64' -shared-memory38;python_version<'3.8' -typing-extensions; python_version < '3.8' -singledispatchmethod; python_version < '3.8' # Hack around stevedore being broken by importlib_metadata 5.0; we need to pin # the requirements rather than the constraints if we need to cut a release diff --git a/setup.py b/setup.py index 8e43ffeef3c9..352df8669fe8 100755 --- a/setup.py +++ b/setup.py @@ -75,7 +75,6 @@ "Operating System :: MacOS", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", @@ -86,7 +85,7 @@ packages=find_packages(exclude=["test*"]), install_requires=REQUIREMENTS, include_package_data=True, - python_requires=">=3.7", + python_requires=">=3.8", extras_require={ "qasm3-import": qasm3_import_extras, "visualization": visualization_extras, diff --git a/test/python/test_examples.py b/test/python/test_examples.py index 398a4df920e1..4da8dc7b771f 100644 --- a/test/python/test_examples.py +++ b/test/python/test_examples.py @@ -32,7 +32,7 @@ class TestPythonExamples(QiskitTestCase): """Test example scripts""" @unittest.skipIf( - sys.platform == "darwin" and sys.version_info[1] >= 8, + sys.platform == "darwin", "Multiprocess spawn fails on macOS python >=3.8 without __name__ == '__main__' guard", ) def test_all_examples(self): @@ -59,7 +59,7 @@ def test_all_examples(self): self.assertEqual(run_example.returncode, 0, error_string) @unittest.skipIf( - sys.platform == "darwin" and sys.version_info[1] >= 8, + sys.platform == "darwin", "Multiprocess spawn fails on macOS python >=3.8 without __name__ == '__main__' guard", ) @online_test diff --git a/test/python/tools/jupyter/test_notebooks.py b/test/python/tools/jupyter/test_notebooks.py index 82b30e77e7f5..14e892947a92 100644 --- a/test/python/tools/jupyter/test_notebooks.py +++ b/test/python/tools/jupyter/test_notebooks.py @@ -78,7 +78,7 @@ def _execute_notebook(self, filename): execute_preprocessor.preprocess(notebook, {"metadata": {"path": self.execution_path}}) @unittest.skipIf( - sys.version_info >= (3, 8) and sys.platform != "linux", + sys.platform != "linux", "Fails with Python >=3.8 on osx and windows", ) def test_jupyter_jobs_pbars(self): diff --git a/test/python/tools/test_parallel.py b/test/python/tools/test_parallel.py index 9f62738b1b49..27801c1f6175 100644 --- a/test/python/tools/test_parallel.py +++ b/test/python/tools/test_parallel.py @@ -48,13 +48,6 @@ def test_windows_parallel_default(self): parallel_default = get_platform_parallel_default() self.assertEqual(parallel_default, False) - def test_mac_os_supported_version_parallel_default(self): - """Verifies the parallel default for macOS.""" - with patch("sys.platform", "darwin"): - with patch("sys.version_info", (10, 11, 0, "final", 0)): - parallel_default = get_platform_parallel_default() - self.assertEqual(parallel_default, True) - def test_mac_os_unsupported_version_parallel_default(self): """Verifies the parallel default for macOS.""" with patch("sys.platform", "darwin"): diff --git a/test/python/visualization/test_gate_map.py b/test/python/visualization/test_gate_map.py index 8b07583b6238..ead5f96e4dc3 100644 --- a/test/python/visualization/test_gate_map.py +++ b/test/python/visualization/test_gate_map.py @@ -12,7 +12,6 @@ """A test for visualizing device coupling maps""" import unittest -import sys from io import BytesIO from PIL import Image @@ -39,12 +38,6 @@ import matplotlib.pyplot as plt -@unittest.skipIf( - sys.version_info < (3, 7), - "Skipping image comparison tests on python 3.6 as they " - "depend on the local matplotlib environment matching the " - "environment for the reference images which is only >=3.7", -) @ddt class TestGateMap(QiskitVisualizationTestCase): """visual tests for plot_gate_map""" diff --git a/tox.ini b/tox.ini index 3a30fa9e1901..be1b1337a9bc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] minversion = 3.3.0 -envlist = py37, py38, py39, py310, py311, lint-incr +envlist = py38, py39, py310, py311, lint-incr isolated_build = true [testenv] From 0e44c5e38066c26e06d19fe5fcbc947eb1d25472 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 8 May 2023 10:52:10 -0400 Subject: [PATCH 090/172] Fix interaction graph vf2 scoring to include 1q component (#10084) * Fix interaction graph vf2 scoring to include 1q component In #9148 a bug was introduced into the vf2 scoring code. In that PR the vf2 passes were changed to treat standalone qubits as a special case to improve the algorithmic efficiency of the pass. However, that PR broke the scoring algorithm so that it was no longer factoring in the 1q component of the interaction graph when scoring a layout. This meant that when scoring a potential layout only the 2q error rates were been factored into the score and the pass was potentially selecting worse performing qubits. This commit fixes this error and ensures the scoring is looking at all the error rates. * Update qiskit/transpiler/passes/layout/vf2_utils.py --- crates/accelerate/src/vf2_layout.rs | 28 ++++++------- qiskit/transpiler/passes/layout/vf2_utils.py | 9 +++- .../fix-vf2-scoring-1q-e2ac29075831d64d.yaml | 6 +++ test/python/transpiler/test_vf2_layout.py | 42 ++++++++++++++++++- 4 files changed, 67 insertions(+), 18 deletions(-) create mode 100644 releasenotes/notes/fix-vf2-scoring-1q-e2ac29075831d64d.yaml diff --git a/crates/accelerate/src/vf2_layout.rs b/crates/accelerate/src/vf2_layout.rs index 8ad65cfbcd1f..dbac7e4ab9b7 100644 --- a/crates/accelerate/src/vf2_layout.rs +++ b/crates/accelerate/src/vf2_layout.rs @@ -73,21 +73,19 @@ pub fn score_layout( } else { edge_list.par_iter().filter_map(edge_filter_map).product() }; - if strict_direction { - fidelity *= if bit_list.len() < PARALLEL_THRESHOLD || !run_in_parallel { - bit_counts - .iter() - .enumerate() - .filter_map(bit_filter_map) - .product::() - } else { - bit_counts - .par_iter() - .enumerate() - .filter_map(bit_filter_map) - .product() - }; - } + fidelity *= if bit_list.len() < PARALLEL_THRESHOLD || !run_in_parallel { + bit_counts + .iter() + .enumerate() + .filter_map(bit_filter_map) + .product::() + } else { + bit_counts + .par_iter() + .enumerate() + .filter_map(bit_filter_map) + .product() + }; Ok(1. - fidelity) } diff --git a/qiskit/transpiler/passes/layout/vf2_utils.py b/qiskit/transpiler/passes/layout/vf2_utils.py index ba8c250208c4..ed14df998caa 100644 --- a/qiskit/transpiler/passes/layout/vf2_utils.py +++ b/qiskit/transpiler/passes/layout/vf2_utils.py @@ -111,9 +111,14 @@ def score_layout( size = 0 nlayout = NLayout(layout_mapping, size + 1, size + 1) bit_list = np.zeros(len(im_graph), dtype=np.int32) - if strict_direction: - for node_index in bit_map.values(): + for node_index in bit_map.values(): + try: bit_list[node_index] = sum(im_graph[node_index].values()) + # If node_index not in im_graph that means there was a standalone + # node we will score/sort separately outside the vf2 mapping, so we + # can skip the hole + except IndexError: + pass edge_list = { (edge[0], edge[1]): sum(edge[2].values()) for edge in im_graph.edge_index_map().values() } diff --git a/releasenotes/notes/fix-vf2-scoring-1q-e2ac29075831d64d.yaml b/releasenotes/notes/fix-vf2-scoring-1q-e2ac29075831d64d.yaml new file mode 100644 index 000000000000..b2901ede4489 --- /dev/null +++ b/releasenotes/notes/fix-vf2-scoring-1q-e2ac29075831d64d.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fix a bug in the :class:`~.VF2Layout` and :class:`~.VF2PostLayout` passes + where the passes wer failing to account for the 1 qubit error component when + evaluating a potential layout. diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index b25e07edc29b..b1eb02a489d9 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -36,7 +36,8 @@ FakeYorktown, FakeGuadalupeV2, ) -from qiskit.circuit.library import GraphState, CXGate, XGate +from qiskit.circuit import Measure +from qiskit.circuit.library import GraphState, CXGate, XGate, HGate from qiskit.transpiler import PassManager, AnalysisPass from qiskit.transpiler.target import InstructionProperties from qiskit.transpiler.preset_passmanagers.common import generate_embed_passmanager @@ -84,6 +85,45 @@ def run(dag, wire_map): class TestVF2LayoutSimple(LayoutTestCase): """Tests the VF2Layout pass""" + def test_1q_component_influence(self): + """Assert that the 1q component of a connected interaction graph is scored correctly.""" + target = Target() + target.add_instruction( + CXGate(), + { + (0, 1): InstructionProperties(error=0.0), + (1, 2): InstructionProperties(error=0.0), + (2, 3): InstructionProperties(error=0.0), + }, + ) + target.add_instruction( + HGate(), + { + (0,): InstructionProperties(error=0.0), + (1,): InstructionProperties(error=0.0), + (2,): InstructionProperties(error=0.0), + }, + ) + target.add_instruction( + Measure(), + { + (0,): InstructionProperties(error=0.1), + (1,): InstructionProperties(error=0.1), + (2,): InstructionProperties(error=0.9), + }, + ) + + qc = QuantumCircuit(2, 2) + qc.h(0) + qc.cx(0, 1) + qc.cx(1, 0) + qc.measure(0, 0) + qc.measure(1, 1) + vf2_pass = VF2Layout(target=target, seed=self.seed) + vf2_pass(qc) + layout = vf2_pass.property_set["layout"] + self.assertEqual([1, 0], list(layout._p2v.keys())) + def test_2q_circuit_2q_coupling(self): """A simple example, without considering the direction 0 - 1 From 3499bfa0c83f2f46dfe11f560107f74dbd81e457 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Mon, 8 May 2023 12:45:33 -0600 Subject: [PATCH 091/172] Turn off CI for forks (#10078) --- .github/workflows/coverage.yml | 1 + .github/workflows/neko.yml | 1 + .github/workflows/randomized_tests.yml | 1 + .github/workflows/slow.yml | 1 + 4 files changed, 4 insertions(+) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 57c18b744ecf..f85266bc5ccd 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -10,6 +10,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: coverage: + if: github.repository_owner == 'Qiskit' name: Coverage runs-on: ubuntu-latest env: diff --git a/.github/workflows/neko.yml b/.github/workflows/neko.yml index 9af0ced078ba..ba410d1752b9 100644 --- a/.github/workflows/neko.yml +++ b/.github/workflows/neko.yml @@ -9,6 +9,7 @@ concurrency: cancel-in-progress: ${{ github.event_name == 'pull_request' }} jobs: neko: + if: github.repository_owner == 'Qiskit' name: Qiskit Neko Integration Tests runs-on: ubuntu-latest steps: diff --git a/.github/workflows/randomized_tests.yml b/.github/workflows/randomized_tests.yml index 03af53519ff6..17b5557db844 100644 --- a/.github/workflows/randomized_tests.yml +++ b/.github/workflows/randomized_tests.yml @@ -5,6 +5,7 @@ on: workflow_dispatch: jobs: randomized_tests: + if: github.repository_owner == 'Qiskit' name: Randomized tests runs-on: ubuntu-latest steps: diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml index 319318e62028..62fa9ec19c62 100644 --- a/.github/workflows/slow.yml +++ b/.github/workflows/slow.yml @@ -5,6 +5,7 @@ on: workflow_dispatch: jobs: slow-tests: + if: github.repository_owner == 'Qiskit' name: Full-test-run-with-slow runs-on: ubuntu-latest steps: From b6fdb19da42791c8bdd25cff499a87f32ab4872c Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 9 May 2023 17:05:53 +0200 Subject: [PATCH 092/172] Switch `QuantumCircuit.from_qasm_str` and `from_qasm_file` to new parser (#9955) This uses the extensibility and the legacy compatiblity data in the new Rust OpenQASM 2 parser to switch over the old `QuantumCircuit` constructor methods to the new parser. This gives users of these methods large speedups while maintaining the same feature set (and fixing a long-standing bug surrounding `U` and `CX`!). One test is removed for the circuit methods; the circuit methods are set to use `strict=False` mode of the new parser, which is more permissive, including making the empty string an allowable program. The alternative was to set `strict=True`, which would have made an unrelated test in `SabreLayout` fail instead due to its slightly improper use of floating-point values in OpenQASM 2 (the OQ2 spec technically requires decimal points even in constructs like `1e3`, unlike other languages). Given that the previously OQ2 importer allowed these floats, it seemed safer to use `strict=False` mode, which is also generally more convenient for users. The tests for the old paths are copied, to ensure they continue working during their potential deprecation and removal. --- qiskit/circuit/quantumcircuit.py | 42 +- ...ircuit-qasm2-methods-b1a06ee2859e2cce.yaml | 18 + .../test_circuit_methods.py} | 23 +- test/python/qasm2/test_legacy_importer.py | 508 ++++++++++++++++++ 4 files changed, 568 insertions(+), 23 deletions(-) create mode 100644 releasenotes/notes/new-circuit-qasm2-methods-b1a06ee2859e2cce.yaml rename test/python/{circuit/test_circuit_load_from_qasm.py => qasm2/test_circuit_methods.py} (97%) create mode 100644 test/python/qasm2/test_legacy_importer.py diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 0618105e0192..9299ae054d73 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -46,7 +46,6 @@ from qiskit.circuit.instruction import Instruction from qiskit.circuit.gate import Gate from qiskit.circuit.parameter import Parameter -from qiskit.qasm.qasm import Qasm from qiskit.qasm.exceptions import QasmError from qiskit.circuit.exceptions import CircuitError from .parameterexpression import ParameterExpression, ParameterValueType @@ -2498,11 +2497,23 @@ def from_qasm_file(path: str) -> "QuantumCircuit": Args: path (str): Path to the file for a QASM program + Return: QuantumCircuit: The QuantumCircuit object for the input QASM + + See also: + :func:`.qasm2.load`: the complete interface to the OpenQASM 2 importer. """ - qasm = Qasm(filename=path) - return _circuit_from_qasm(qasm) + # pylint: disable=cyclic-import + from qiskit import qasm2 + + return qasm2.load( + path, + include_path=qasm2.LEGACY_INCLUDE_PATH, + custom_instructions=qasm2.LEGACY_CUSTOM_INSTRUCTIONS, + custom_classical=qasm2.LEGACY_CUSTOM_CLASSICAL, + strict=False, + ) @staticmethod def from_qasm_str(qasm_str: str) -> "QuantumCircuit": @@ -2512,9 +2523,20 @@ def from_qasm_str(qasm_str: str) -> "QuantumCircuit": qasm_str (str): A QASM program string Return: QuantumCircuit: The QuantumCircuit object for the input QASM + + See also: + :func:`.qasm2.loads`: the complete interface to the OpenQASM 2 importer. """ - qasm = Qasm(data=qasm_str) - return _circuit_from_qasm(qasm) + # pylint: disable=cyclic-import + from qiskit import qasm2 + + return qasm2.loads( + qasm_str, + include_path=qasm2.LEGACY_INCLUDE_PATH, + custom_instructions=qasm2.LEGACY_CUSTOM_INSTRUCTIONS, + custom_classical=qasm2.LEGACY_CUSTOM_CLASSICAL, + strict=False, + ) @property def global_phase(self) -> ParameterValueType: @@ -4930,16 +4952,6 @@ def qubit_stop_time(self, *qubits: Union[Qubit, int]) -> float: return 0 # If there are no instructions over bits -def _circuit_from_qasm(qasm: Qasm) -> "QuantumCircuit": - # pylint: disable=cyclic-import - from qiskit.converters import ast_to_dag - from qiskit.converters import dag_to_circuit - - ast = qasm.parse() - dag = ast_to_dag(ast) - return dag_to_circuit(dag, copy_operations=False) - - def _standard_compare(value1, value2): if value1 < value2: return -1 diff --git a/releasenotes/notes/new-circuit-qasm2-methods-b1a06ee2859e2cce.yaml b/releasenotes/notes/new-circuit-qasm2-methods-b1a06ee2859e2cce.yaml new file mode 100644 index 000000000000..36b4cba14f16 --- /dev/null +++ b/releasenotes/notes/new-circuit-qasm2-methods-b1a06ee2859e2cce.yaml @@ -0,0 +1,18 @@ +--- +upgrade: + - | + The OpenQASM 2 constructor methods on :class:`.QuantumCircuit` + (:meth:`~.QuantumCircuit.from_qasm_str` and :meth:`~.QuantumCircuit.from_qasm_file`) have been + switched to use the Rust-based parser added in Qiskit Terra 0.24. This should result in + significantly faster parsing times (10 times or more is not uncommon) and massively reduced + intermediate memory usage. + + The :class:`.QuantumCircuit` methods are kept with the same interface for continuity; the + preferred way to access the OpenQASM 2 importer is to use :func:`.qasm2.load` and + :func:`.qasm2.loads`, which offer an expanded interface to control the parsing and construction. +fixes: + - | + The OpenQASM 2 circuit-constructor methods (:meth:`.QuantumCircuit.from_qasm_str` and + :meth:`~.QuantumCircuit.from_qasm_file`) will no longer error when encountering a ``gate`` + definition that contains ``U`` or ``CX`` instructions. See `#5536 + `__. diff --git a/test/python/circuit/test_circuit_load_from_qasm.py b/test/python/qasm2/test_circuit_methods.py similarity index 97% rename from test/python/circuit/test_circuit_load_from_qasm.py rename to test/python/qasm2/test_circuit_methods.py index 9c264b8782e4..fdd30211b73f 100644 --- a/test/python/circuit/test_circuit_load_from_qasm.py +++ b/test/python/qasm2/test_circuit_methods.py @@ -120,14 +120,6 @@ def test_fail_qasm_file(self): """ self.assertRaises(QiskitError, QuantumCircuit.from_qasm_file, "") - def test_fail_qasm_string(self): - """ - Test fail_qasm_string. - - If all is correct we should get a QiskitError - """ - self.assertRaises(QiskitError, QuantumCircuit.from_qasm_str, "") - def test_qasm_text(self): """ Test qasm_text and get_circuit. @@ -492,6 +484,21 @@ def test_from_qasm_str_delay(self): expected.delay(172, qr[0]) self.assertEqualUnroll("u", circuit, expected) + def test_definition_with_u_cx(self): + """Test that gate-definition bodies can use U and CX.""" + qasm_string = """ +OPENQASM 2.0; +gate bell q0, q1 { U(pi/2, 0, pi) q0; CX q0, q1; } +qreg q[2]; +bell q[0], q[1]; +""" + circuit = QuantumCircuit.from_qasm_str(qasm_string) + qr = QuantumRegister(2, "q") + expected = QuantumCircuit(qr) + expected.h(0) + expected.cx(0, 1) + self.assertEqualUnroll(["u", "cx"], circuit, expected) + def assertEqualUnroll(self, basis, circuit, expected): """Compares the dags after unrolling to basis""" circuit_dag = circuit_to_dag(circuit) diff --git a/test/python/qasm2/test_legacy_importer.py b/test/python/qasm2/test_legacy_importer.py new file mode 100644 index 000000000000..070646b1d9ea --- /dev/null +++ b/test/python/qasm2/test_legacy_importer.py @@ -0,0 +1,508 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017. +# +# 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 cases for the legacy OpenQASM 2 parser.""" + +# pylint: disable=missing-function-docstring + + +import os + +from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister +from qiskit.circuit import Gate, Parameter +from qiskit.converters import ast_to_dag, dag_to_circuit +from qiskit.exceptions import QiskitError +from qiskit.qasm import Qasm +from qiskit.test import QiskitTestCase +from qiskit.transpiler.passes import Unroller +from qiskit.converters.circuit_to_dag import circuit_to_dag + + +def from_qasm_str(qasm_str): + return dag_to_circuit(ast_to_dag(Qasm(data=qasm_str).parse())) + + +def from_qasm_file(path): + return dag_to_circuit(ast_to_dag(Qasm(filename=path).parse())) + + +class LoadFromQasmTest(QiskitTestCase): + """Test circuit.from_qasm_* set of methods.""" + + def setUp(self): + super().setUp() + self.qasm_file_name = "entangled_registers.qasm" + self.qasm_dir = os.path.join( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "qasm" + ) + self.qasm_file_path = os.path.join(self.qasm_dir, self.qasm_file_name) + + def test_qasm_file(self): + """ + Test qasm_file and get_circuit. + + If all is correct we should get the qasm file loaded in _qasm_file_path + """ + q_circuit = from_qasm_file(self.qasm_file_path) + qr_a = QuantumRegister(4, "a") + qr_b = QuantumRegister(4, "b") + cr_c = ClassicalRegister(4, "c") + cr_d = ClassicalRegister(4, "d") + q_circuit_2 = QuantumCircuit(qr_a, qr_b, cr_c, cr_d) + q_circuit_2.h(qr_a) + q_circuit_2.cx(qr_a, qr_b) + q_circuit_2.barrier(qr_a) + q_circuit_2.barrier(qr_b) + q_circuit_2.measure(qr_a, cr_c) + q_circuit_2.measure(qr_b, cr_d) + self.assertEqual(q_circuit, q_circuit_2) + + def test_loading_all_qelib1_gates(self): + """Test setting up a circuit with all gates defined in qiskit/qasm/libs/qelib1.inc.""" + from qiskit.circuit.library import U1Gate, U2Gate, U3Gate, CU1Gate, CU3Gate, UGate + + all_gates_qasm = os.path.join(self.qasm_dir, "all_gates.qasm") + qasm_circuit = from_qasm_file(all_gates_qasm) + + ref_circuit = QuantumCircuit(3, 3) + + # abstract gates (legacy) + ref_circuit.append(UGate(0.2, 0.1, 0.6), [0]) + ref_circuit.cx(0, 1) + # the hardware primitives + ref_circuit.append(U3Gate(0.2, 0.1, 0.6), [0]) + ref_circuit.append(U2Gate(0.1, 0.6), [0]) + ref_circuit.append(U1Gate(0.6), [0]) + ref_circuit.id(0) + ref_circuit.cx(0, 1) + # the standard single qubit gates + ref_circuit.u(0.2, 0.1, 0.6, 0) + ref_circuit.p(0.6, 0) + ref_circuit.x(0) + ref_circuit.y(0) + ref_circuit.z(0) + ref_circuit.h(0) + ref_circuit.s(0) + ref_circuit.t(0) + ref_circuit.sdg(0) + ref_circuit.tdg(0) + ref_circuit.sx(0) + ref_circuit.sxdg(0) + # the standard rotations + ref_circuit.rx(0.1, 0) + ref_circuit.ry(0.1, 0) + ref_circuit.rz(0.1, 0) + # the barrier + ref_circuit.barrier() + # the standard user-defined gates + ref_circuit.swap(0, 1) + ref_circuit.cswap(0, 1, 2) + ref_circuit.cy(0, 1) + ref_circuit.cz(0, 1) + ref_circuit.ch(0, 1) + ref_circuit.csx(0, 1) + ref_circuit.append(CU1Gate(0.6), [0, 1]) + ref_circuit.append(CU3Gate(0.2, 0.1, 0.6), [0, 1]) + ref_circuit.cp(0.6, 0, 1) + ref_circuit.cu(0.2, 0.1, 0.6, 0, 0, 1) + ref_circuit.ccx(0, 1, 2) + ref_circuit.crx(0.6, 0, 1) + ref_circuit.cry(0.6, 0, 1) + ref_circuit.crz(0.6, 0, 1) + ref_circuit.rxx(0.2, 0, 1) + ref_circuit.rzz(0.2, 0, 1) + ref_circuit.measure([0, 1, 2], [0, 1, 2]) + + self.assertEqual(qasm_circuit, ref_circuit) + + def test_fail_qasm_file(self): + """ + Test fail_qasm_file. + + If all is correct we should get a QiskitError + """ + self.assertRaises(QiskitError, from_qasm_file, "") + + def test_qasm_text(self): + """ + Test qasm_text and get_circuit. + + If all is correct we should get the qasm file loaded from the string + """ + qasm_string = "// A simple 8 qubit example\nOPENQASM 2.0;\n" + qasm_string += 'include "qelib1.inc";\nqreg a[4];\n' + qasm_string += "qreg b[4];\ncreg c[4];\ncreg d[4];\nh a;\ncx a, b;\n" + qasm_string += "barrier a;\nbarrier b;\nmeasure a[0]->c[0];\n" + qasm_string += "measure a[1]->c[1];\nmeasure a[2]->c[2];\n" + qasm_string += "measure a[3]->c[3];\nmeasure b[0]->d[0];\n" + qasm_string += "measure b[1]->d[1];\nmeasure b[2]->d[2];\n" + qasm_string += "measure b[3]->d[3];" + q_circuit = from_qasm_str(qasm_string) + + qr_a = QuantumRegister(4, "a") + qr_b = QuantumRegister(4, "b") + cr_c = ClassicalRegister(4, "c") + cr_d = ClassicalRegister(4, "d") + ref = QuantumCircuit(qr_a, qr_b, cr_c, cr_d) + ref.h(qr_a[3]) + ref.cx(qr_a[3], qr_b[3]) + ref.h(qr_a[2]) + ref.cx(qr_a[2], qr_b[2]) + ref.h(qr_a[1]) + ref.cx(qr_a[1], qr_b[1]) + ref.h(qr_a[0]) + ref.cx(qr_a[0], qr_b[0]) + ref.barrier(qr_b) + ref.measure(qr_b, cr_d) + ref.barrier(qr_a) + ref.measure(qr_a, cr_c) + + self.assertEqual(len(q_circuit.cregs), 2) + self.assertEqual(len(q_circuit.qregs), 2) + self.assertEqual(q_circuit, ref) + + def test_qasm_text_conditional(self): + """ + Test qasm_text and get_circuit when conditionals are present. + """ + qasm_string = ( + "\n".join( + [ + "OPENQASM 2.0;", + 'include "qelib1.inc";', + "qreg q[1];", + "creg c0[4];", + "creg c1[4];", + "x q[0];", + "if(c1==4) x q[0];", + ] + ) + + "\n" + ) + q_circuit = from_qasm_str(qasm_string) + + qr = QuantumRegister(1, "q") + cr0 = ClassicalRegister(4, "c0") + cr1 = ClassicalRegister(4, "c1") + ref = QuantumCircuit(qr, cr0, cr1) + ref.x(qr[0]) + ref.x(qr[0]).c_if(cr1, 4) + + self.assertEqual(len(q_circuit.cregs), 2) + self.assertEqual(len(q_circuit.qregs), 1) + self.assertEqual(q_circuit, ref) + + def test_opaque_gate(self): + """ + Test parse an opaque gate + + See https://github.com/Qiskit/qiskit-terra/issues/1566. + """ + + qasm_string = ( + "\n".join( + [ + "OPENQASM 2.0;", + 'include "qelib1.inc";', + "opaque my_gate(theta,phi,lambda) a,b;", + "qreg q[3];", + "my_gate(1,2,3) q[1],q[2];", + ] + ) + + "\n" + ) + circuit = from_qasm_str(qasm_string) + + qr = QuantumRegister(3, "q") + expected = QuantumCircuit(qr) + expected.append(Gate(name="my_gate", num_qubits=2, params=[1, 2, 3]), [qr[1], qr[2]]) + + self.assertEqual(circuit, expected) + + def test_qasm_example_file(self): + """Loads qasm/example.qasm.""" + qasm_filename = os.path.join(self.qasm_dir, "example.qasm") + expected_circuit = from_qasm_str( + "\n".join( + [ + "OPENQASM 2.0;", + 'include "qelib1.inc";', + "qreg q[3];", + "qreg r[3];", + "creg c[3];", + "creg d[3];", + "h q[2];", + "cx q[2],r[2];", + "measure r[2] -> d[2];", + "h q[1];", + "cx q[1],r[1];", + "measure r[1] -> d[1];", + "h q[0];", + "cx q[0],r[0];", + "measure r[0] -> d[0];", + "barrier q[0],q[1],q[2];", + "measure q[2] -> c[2];", + "measure q[1] -> c[1];", + "measure q[0] -> c[0];", + ] + ) + + "\n" + ) + + q_circuit = from_qasm_file(qasm_filename) + + self.assertEqual(q_circuit, expected_circuit) + self.assertEqual(len(q_circuit.cregs), 2) + self.assertEqual(len(q_circuit.qregs), 2) + + def test_qasm_qas_string_order(self): + """Test that gates are returned in qasm in ascending order.""" + expected_qasm = ( + "\n".join( + [ + "OPENQASM 2.0;", + 'include "qelib1.inc";', + "qreg q[3];", + "h q[0];", + "h q[1];", + "h q[2];", + ] + ) + + "\n" + ) + qasm_string = """OPENQASM 2.0; + include "qelib1.inc"; + qreg q[3]; + h q;""" + q_circuit = from_qasm_str(qasm_string) + + self.assertEqual(q_circuit.qasm(), expected_qasm) + + def test_from_qasm_str_custom_gate1(self): + """Test load custom gates (simple case)""" + qasm_string = """OPENQASM 2.0; + include "qelib1.inc"; + gate rinv q {sdg q; h q; sdg q; h q; } + qreg qr[1]; + rinv qr[0];""" + circuit = from_qasm_str(qasm_string) + + rinv_q = QuantumRegister(1, name="q") + rinv_gate = QuantumCircuit(rinv_q, name="rinv") + rinv_gate.sdg(rinv_q) + rinv_gate.h(rinv_q) + rinv_gate.sdg(rinv_q) + rinv_gate.h(rinv_q) + rinv = rinv_gate.to_instruction() + qr = QuantumRegister(1, name="qr") + expected = QuantumCircuit(qr, name="circuit") + expected.append(rinv, [qr[0]]) + + self.assertEqualUnroll(["sdg", "h"], circuit, expected) + + def test_from_qasm_str_custom_gate2(self): + """Test load custom gates (no so simple case, different bit order) + See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-551307250 + """ + qasm_string = """OPENQASM 2.0; + include "qelib1.inc"; + gate swap2 a,b { + cx a,b; + cx b,a; // different bit order + cx a,b; + } + qreg qr[3]; + swap2 qr[0], qr[1]; + swap2 qr[1], qr[2];""" + circuit = from_qasm_str(qasm_string) + + ab_args = QuantumRegister(2, name="ab") + swap_gate = QuantumCircuit(ab_args, name="swap2") + swap_gate.cx(ab_args[0], ab_args[1]) + swap_gate.cx(ab_args[1], ab_args[0]) + swap_gate.cx(ab_args[0], ab_args[1]) + swap = swap_gate.to_instruction() + + qr = QuantumRegister(3, name="qr") + expected = QuantumCircuit(qr, name="circuit") + expected.append(swap, [qr[0], qr[1]]) + expected.append(swap, [qr[1], qr[2]]) + + self.assertEqualUnroll(["cx"], expected, circuit) + + def test_from_qasm_str_custom_gate3(self): + """Test load custom gates (no so simple case, different bit count) + See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-551307250 + """ + qasm_string = """OPENQASM 2.0; + include "qelib1.inc"; + gate cswap2 a,b,c + { + cx c,b; // different bit count + ccx a,b,c; //previously defined gate + cx c,b; + } + qreg qr[3]; + cswap2 qr[1], qr[0], qr[2];""" + circuit = from_qasm_str(qasm_string) + + abc_args = QuantumRegister(3, name="abc") + cswap_gate = QuantumCircuit(abc_args, name="cswap2") + cswap_gate.cx(abc_args[2], abc_args[1]) + cswap_gate.ccx(abc_args[0], abc_args[1], abc_args[2]) + cswap_gate.cx(abc_args[2], abc_args[1]) + cswap = cswap_gate.to_instruction() + + qr = QuantumRegister(3, name="qr") + expected = QuantumCircuit(qr, name="circuit") + expected.append(cswap, [qr[1], qr[0], qr[2]]) + + self.assertEqualUnroll(["cx", "h", "tdg", "t"], circuit, expected) + + def test_from_qasm_str_custom_gate4(self): + """Test load custom gates (parameterized) + See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-551307250 + """ + qasm_string = """OPENQASM 2.0; + include "qelib1.inc"; + gate my_gate(phi,lambda) q {u(1.5707963267948966,phi,lambda) q;} + qreg qr[1]; + my_gate(pi, pi) qr[0];""" + circuit = from_qasm_str(qasm_string) + + my_gate_circuit = QuantumCircuit(1, name="my_gate") + phi = Parameter("phi") + lam = Parameter("lambda") + my_gate_circuit.u(1.5707963267948966, phi, lam, 0) + my_gate = my_gate_circuit.to_gate() + + qr = QuantumRegister(1, name="qr") + expected = QuantumCircuit(qr, name="circuit") + expected.append(my_gate, [qr[0]]) + expected = expected.bind_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) + + self.assertEqualUnroll("u", circuit, expected) + + def test_from_qasm_str_custom_gate5(self): + """Test load custom gates (parameterized, with biop and constant) + See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-551307250 + """ + qasm_string = """OPENQASM 2.0; + include "qelib1.inc"; + gate my_gate(phi,lambda) q {u(pi/2,phi,lambda) q;} // biop with pi + qreg qr[1]; + my_gate(pi, pi) qr[0];""" + circuit = from_qasm_str(qasm_string) + + my_gate_circuit = QuantumCircuit(1, name="my_gate") + phi = Parameter("phi") + lam = Parameter("lambda") + my_gate_circuit.u(1.5707963267948966, phi, lam, 0) + my_gate = my_gate_circuit.to_gate() + + qr = QuantumRegister(1, name="qr") + expected = QuantumCircuit(qr, name="circuit") + expected.append(my_gate, [qr[0]]) + expected = expected.bind_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) + + self.assertEqualUnroll("u", circuit, expected) + + def test_from_qasm_str_custom_gate6(self): + """Test load custom gates (parameters used in expressions) + See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-591668924 + """ + qasm_string = """OPENQASM 2.0; + include "qelib1.inc"; + gate my_gate(phi,lambda) q + {rx(phi+pi) q; ry(lambda/2) q;} // parameters used in expressions + qreg qr[1]; + my_gate(pi, pi) qr[0];""" + circuit = from_qasm_str(qasm_string) + + my_gate_circuit = QuantumCircuit(1, name="my_gate") + phi = Parameter("phi") + lam = Parameter("lambda") + my_gate_circuit.rx(phi + 3.141592653589793, 0) + my_gate_circuit.ry(lam / 2, 0) + my_gate = my_gate_circuit.to_gate() + + qr = QuantumRegister(1, name="qr") + expected = QuantumCircuit(qr, name="circuit") + expected.append(my_gate, [qr[0]]) + expected = expected.bind_parameters({phi: 3.141592653589793, lam: 3.141592653589793}) + + self.assertEqualUnroll(["rx", "ry"], circuit, expected) + + def test_from_qasm_str_custom_gate7(self): + """Test load custom gates (build in functions) + See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-592208951 + """ + qasm_string = """OPENQASM 2.0; + include "qelib1.inc"; + gate my_gate(phi,lambda) q + {u(asin(cos(phi)/2), phi+pi, lambda/2) q;} // build func + qreg qr[1]; + my_gate(pi, pi) qr[0];""" + circuit = from_qasm_str(qasm_string) + + qr = QuantumRegister(1, name="qr") + expected = QuantumCircuit(qr, name="circuit") + expected.u(-0.5235987755982988, 6.283185307179586, 1.5707963267948966, qr[0]) + self.assertEqualUnroll("u", circuit, expected) + + def test_from_qasm_str_nested_custom_gate(self): + """Test chain of custom gates + See: https://github.com/Qiskit/qiskit-terra/pull/3393#issuecomment-592261942 + """ + qasm_string = """OPENQASM 2.0; + include "qelib1.inc"; + gate my_other_gate(phi,lambda) q + {u(asin(cos(phi)/2), phi+pi, lambda/2) q;} + gate my_gate(phi) r + {my_other_gate(phi, phi+pi) r;} + qreg qr[1]; + my_gate(pi) qr[0];""" + circuit = from_qasm_str(qasm_string) + + qr = QuantumRegister(1, name="qr") + expected = QuantumCircuit(qr, name="circuit") + expected.u(-0.5235987755982988, 6.283185307179586, 3.141592653589793, qr[0]) + self.assertEqualUnroll("u", circuit, expected) + + def test_from_qasm_str_delay(self): + """Test delay instruction/opaque-gate + See: https://github.com/Qiskit/qiskit-terra/issues/6510 + """ + qasm_string = """OPENQASM 2.0; + include "qelib1.inc"; + + opaque delay(time) q; + + qreg q[1]; + delay(172) q[0];""" + circuit = from_qasm_str(qasm_string) + + qr = QuantumRegister(1, name="q") + expected = QuantumCircuit(qr, name="circuit") + expected.delay(172, qr[0]) + self.assertEqualUnroll("u", circuit, expected) + + def assertEqualUnroll(self, basis, circuit, expected): + """Compares the dags after unrolling to basis""" + circuit_dag = circuit_to_dag(circuit) + expected_dag = circuit_to_dag(expected) + + circuit_result = Unroller(basis).run(circuit_dag) + expected_result = Unroller(basis).run(expected_dag) + + self.assertEqual(circuit_result, expected_result) From 941e1d7e30b1ced5c9211dc8417c444faff936d8 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Tue, 9 May 2023 10:16:56 -0600 Subject: [PATCH 093/172] Turn off Qiskit Bot notifications for code owners (#9931) --- qiskit_bot.yaml | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/qiskit_bot.yaml b/qiskit_bot.yaml index 480617b31ee1..a93681a77fe1 100644 --- a/qiskit_bot.yaml +++ b/qiskit_bot.yaml @@ -1,43 +1,46 @@ --- +# We use quotes around users who don`t want a GitHub notification, +# but whom we still want their name in the Qiskit Bot message so +# people know they are relevant. notifications: ".*": - - "@Qiskit/terra-core" + - "`@Qiskit/terra-core`" "visualization/pulse_v2": - - "@nkanazawa1989" + - "`@nkanazawa1989`" "visualization/timeline": - - "@nkanazawa1989" + - "`@nkanazawa1989`" "pulse": - - "@nkanazawa1989" + - "`@nkanazawa1989`" "scheduler": - - "@nkanazawa1989" + - "`@nkanazawa1989`" "qpy": - - "@mtreinish" - - "@nkanazawa1989" + - "`@mtreinish`" + - "`@nkanazawa1989`" "two_qubit_decompose": - - "@levbishop" + - "`@levbishop`" "quantum_info": - - "@ikkoham" + - "`@ikkoham`" "utils/run_circuits": - - "@woodsp-ibm" + - "`@woodsp-ibm`" "utils/quantum_instance": - - "@woodsp-ibm" + - "`@woodsp-ibm`" "opflow": - - "@woodsp-ibm" - - "@Cryoris" + - "`@woodsp-ibm`" + - "`@Cryoris`" "algorithms": - - "@ElePT" - - "@woodsp-ibm" + - "`@ElePT`" + - "`@woodsp-ibm`" "circuit/library": - - "@Cryoris" - - "@ajavadia" + - "`@Cryoris`" + - "`@ajavadia`" "primitives": - - "@ikkoham" - - "@t-imamichi" - - "@ajavadia" - - "@levbishop" + - "`@ikkoham`" + - "`@t-imamichi`" + - "`@ajavadia`" + - "`@levbishop`" ".*\\.rs$|^Cargo": - - "@mtreinish" - - "@kevinhartman" + - "`@mtreinish`" + - "`@kevinhartman`" - "@Eric-Arellano" "(?!.*pulse.*)\\bvisualization\\b": - "@enavarro51" From 5ee7ff24ce62ea547b3ee72db2f42ee54aac018c Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Tue, 9 May 2023 16:01:47 -0400 Subject: [PATCH 094/172] Remove deprecated import location `qiskit/util.py` (#10088) This is a follow up to #7212 --- qiskit/util.py | 35 ------------------- .../notes/remove-util-3cd9eae2efc95176.yaml | 6 ++++ 2 files changed, 6 insertions(+), 35 deletions(-) delete mode 100644 qiskit/util.py create mode 100644 releasenotes/notes/remove-util-3cd9eae2efc95176.yaml diff --git a/qiskit/util.py b/qiskit/util.py deleted file mode 100644 index b41f0d9f4128..000000000000 --- a/qiskit/util.py +++ /dev/null @@ -1,35 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2017. -# -# 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. - -"""Common utilities for Qiskit.""" - -import warnings - -from qiskit.utils.deprecation import deprecate_arguments -from qiskit.utils.deprecation import deprecate_function -from qiskit.utils.multiprocessing import is_main_process -from qiskit.utils.multiprocessing import local_hardware_info -from qiskit.utils.units import apply_prefix - -__all__ = [ - "deprecate_arguments", - "deprecate_function", - "is_main_process", - "local_hardware_info", - "apply_prefix", -] - -warnings.warn( - "The 'qiskit.util' namespace is deprecated since qiskit-terra 0.17 and will be removed in 0.20." - " It has been renamed to 'qiskit.utils'.", - category=DeprecationWarning, -) diff --git a/releasenotes/notes/remove-util-3cd9eae2efc95176.yaml b/releasenotes/notes/remove-util-3cd9eae2efc95176.yaml new file mode 100644 index 000000000000..2999490b1c34 --- /dev/null +++ b/releasenotes/notes/remove-util-3cd9eae2efc95176.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The ``qiskit.util`` import location has been removed, as it had + been deprecated since Qiskit Terra 0.17. Users should use the new + import location, ``qiskit.utils``. From c3d7d5a1158c20ae16b2358c7a71779891567632 Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Tue, 9 May 2023 23:30:33 +0300 Subject: [PATCH 095/172] Improve performance of `ConstrainedReschedule` pass (#10077) * experiments * deleting unused file * cleanup * removing duplicate implementation * clarifying docstring * applying suggestions from code review --- .../scheduling/alignments/reschedule.py | 186 +++++++++--------- 1 file changed, 94 insertions(+), 92 deletions(-) diff --git a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py index 350ee31882c8..325cb4c96546 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py +++ b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py @@ -15,6 +15,7 @@ from typing import List from qiskit.circuit.gate import Gate +from qiskit.circuit.delay import Delay from qiskit.circuit.measure import Measure from qiskit.dagcircuit import DAGCircuit, DAGOpNode, DAGOutNode from qiskit.transpiler.basepasses import AnalysisPass @@ -92,8 +93,9 @@ def _get_next_gate(cls, dag: DAGCircuit, node: DAGOpNode) -> List[DAGOpNode]: if not isinstance(next_node, DAGOutNode): yield next_node - def _push_node_back(self, dag: DAGCircuit, node: DAGOpNode, shift: int): - """Update start time of current node. Successors are also shifted to avoid overlap. + def _push_node_back(self, dag: DAGCircuit, node: DAGOpNode): + """Update the start time of the current node to satisfy alignment constraints. + Immediate successors are pushed back to avoid overlap and will be processed later. .. note:: @@ -104,78 +106,88 @@ def _push_node_back(self, dag: DAGCircuit, node: DAGOpNode, shift: int): Args: dag: DAG circuit to be rescheduled with constraints. node: Current node. - shift: Amount of required time shift. """ node_start_time = self.property_set["node_start_time"] conditional_latency = self.property_set.get("conditional_latency", 0) clbit_write_latency = self.property_set.get("clbit_write_latency", 0) - nodes_with_overlap = [(node, shift)] - shift_stack = [] - while nodes_with_overlap: - node, shift = nodes_with_overlap.pop() - shift_stack.append((node, shift)) - # Compute shifted t1 of this node separately for qreg and creg - this_t0 = node_start_time[node] - new_t1q = this_t0 + node.op.duration + shift - this_qubits = set(node.qargs) - if isinstance(node.op, Measure): - # creg access ends at the end of instruction - new_t1c = new_t1q - this_clbits = set(node.cargs) + if isinstance(node.op, Gate): + alignment = self.pulse_align + elif isinstance(node.op, Measure): + alignment = self.acquire_align + elif isinstance(node.op, Delay) or getattr(node.op, "_directive", False): + # Directive or delay. These can start at arbitrary time. + alignment = None + else: + raise TranspilerError(f"Unknown operation type for {repr(node)}.") + + this_t0 = node_start_time[node] + + if alignment is not None: + misalignment = node_start_time[node] % alignment + if misalignment != 0: + shift = max(0, alignment - misalignment) else: - if node.op.condition_bits: - # conditional access ends at the beginning of node start time - new_t1c = this_t0 + shift - this_clbits = set(node.op.condition_bits) - else: - new_t1c = None - this_clbits = set() - - # Check successors for overlap - for next_node in self._get_next_gate(dag, node): - # Compute next node start time separately for qreg and creg - next_t0q = node_start_time[next_node] - next_qubits = set(next_node.qargs) - if isinstance(next_node.op, Measure): - # creg access starts after write latency - next_t0c = next_t0q + clbit_write_latency - next_clbits = set(next_node.cargs) - else: - if next_node.op.condition_bits: - # conditional access starts before node start time - next_t0c = next_t0q - conditional_latency - next_clbits = set(next_node.op.condition_bits) - else: - next_t0c = None - next_clbits = set() - # Compute overlap if there is qubits overlap - if any(this_qubits & next_qubits): - qreg_overlap = new_t1q - next_t0q - else: - qreg_overlap = 0 - # Compute overlap if there is clbits overlap - if any(this_clbits & next_clbits): - creg_overlap = new_t1c - next_t0c + shift = 0 + this_t0 += shift + node_start_time[node] = this_t0 + + # Compute shifted t1 of this node separately for qreg and creg + new_t1q = this_t0 + node.op.duration + this_qubits = set(node.qargs) + if isinstance(node.op, Measure): + # creg access ends at the end of instruction + new_t1c = new_t1q + this_clbits = set(node.cargs) + else: + if node.op.condition_bits: + # conditional access ends at the beginning of node start time + new_t1c = this_t0 + this_clbits = set(node.op.condition_bits) + else: + new_t1c = None + this_clbits = set() + + # Check immediate successors for overlap + for next_node in self._get_next_gate(dag, node): + # Compute next node start time separately for qreg and creg + next_t0q = node_start_time[next_node] + next_qubits = set(next_node.qargs) + if isinstance(next_node.op, Measure): + # creg access starts after write latency + next_t0c = next_t0q + clbit_write_latency + next_clbits = set(next_node.cargs) + else: + if next_node.op.condition_bits: + # conditional access starts before node start time + next_t0c = next_t0q - conditional_latency + next_clbits = set(next_node.op.condition_bits) else: - creg_overlap = 0 - # Shift next node if there is finite overlap in either in qubits or clbits - overlap = max(qreg_overlap, creg_overlap) - if overlap > 0: - nodes_with_overlap.append((next_node, overlap)) - # Update start time of this node after all overlaps are resolved - while shift_stack: - node, shift = shift_stack.pop() - node_start_time[node] += shift + next_t0c = None + next_clbits = set() + # Compute overlap if there is qubits overlap + if any(this_qubits & next_qubits): + qreg_overlap = new_t1q - next_t0q + else: + qreg_overlap = 0 + # Compute overlap if there is clbits overlap + if any(this_clbits & next_clbits): + creg_overlap = new_t1c - next_t0c + else: + creg_overlap = 0 + + # Shift next node if there is finite overlap in either in qubits or clbits + overlap = max(qreg_overlap, creg_overlap) + node_start_time[next_node] = node_start_time[next_node] + overlap def run(self, dag: DAGCircuit): """Run rescheduler. This pass should perform rescheduling to satisfy: - - All DAGOpNode are placed at start time satisfying hardware alignment constraints. - - The end time of current does not overlap with the start time of successor nodes. - - Compiler directives are not necessary satisfying the constraints. + - All DAGOpNode nodes (except for compiler directives) are placed at start time + satisfying hardware alignment constraints. + - The end time of a node does not overlap with the start time of successor nodes. Assumptions: @@ -183,19 +195,19 @@ def run(self, dag: DAGCircuit): - All bits in either qargs or cargs associated with node synchronously start. - Start time of qargs and cargs may different due to I/O latency. - Based on the configurations above, rescheduler pass takes following strategy. + Based on the configurations above, the rescheduler pass takes the following strategy: - 1. Scan node from the beginning, i.e. from left of the circuit. The rescheduler - calls ``node_start_time`` from the property set, - and retrieves the scheduled start time of current node. - 2. If the start time of the node violates the alignment constraints, - the scheduler increases the start time until it satisfies the constraint. - 3. Check overlap with successor nodes. If any overlap occurs, the rescheduler - recursively pushs the successor nodes backward towards the end of the wire. - Note that shifted location doesn't need to satisfy the constraints, - thus it will be a minimum delay to resolve the overlap with the ancestor node. - 4. Repeat 1-3 until the node at the end of the wire. This will resolve - all misalignment without creating overlap between the nodes. + 1. The nodes are processed in the topological order, from the beginning of + the circuit (i.e. from left to right). For every node (including compiler + directives), the function ``_push_node_back`` performs steps 2 and 3. + 2. If the start time of the node violates the alignment constraint, + the start time is increased to satisfy the constraint. + 3. Each immediate successor whose start_time overlaps the node's end_time is + pushed backwards (towards the end of the wire). Note that at this point + the shifted successor does not need to satisfy the constraints, but this + will be taken care of when that successor node itself is processed. + 4. After every node is processed, all misalignment constraints will be resolved, + and there will be no overlap between the nodes. Args: dag: DAG circuit to be rescheduled with constraints. @@ -213,27 +225,17 @@ def run(self, dag: DAGCircuit): node_start_time = self.property_set["node_start_time"] for node in dag.topological_op_nodes(): - if node_start_time[node] == 0: - # Every instruction can start at t=0 - continue - if isinstance(node.op, Gate): - alignment = self.pulse_align - elif isinstance(node.op, Measure): - alignment = self.acquire_align - else: - # Directive or delay. These can start at arbitrary time. - continue + start_time = node_start_time.get(node) - try: - misalignment = node_start_time[node] % alignment - if misalignment == 0: - continue - shift = max(0, alignment - misalignment) - except KeyError as ex: + if start_time is None: raise TranspilerError( f"Start time of {repr(node)} is not found. This node is likely added after " "this circuit is scheduled. Run scheduler again." - ) from ex - if shift > 0: - self._push_node_back(dag, node, shift) + ) + + if start_time == 0: + # Every instruction can start at t=0. + continue + + self._push_node_back(dag, node) From 7a215f97c4f6155795490a24691d22ac89449b96 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Tue, 9 May 2023 22:56:22 +0200 Subject: [PATCH 096/172] Add the `qubit_coordinates_map` for `ibm_seattle` (#10089) * add ibm_seattle qubit_coordinates_map * Update releasenotes/notes/433_qubit_coordinates_map-8abc318fefdb99ac.yaml Co-authored-by: Kevin Krsulich --------- Co-authored-by: Kevin Krsulich --- qiskit/visualization/gate_map.py | 436 ++++++++++++++++++ ...ubit_coordinates_map-8abc318fefdb99ac.yaml | 4 + 2 files changed, 440 insertions(+) create mode 100644 releasenotes/notes/433_qubit_coordinates_map-8abc318fefdb99ac.yaml diff --git a/qiskit/visualization/gate_map.py b/qiskit/visualization/gate_map.py index da270e8b0718..86a3fbe9f858 100644 --- a/qiskit/visualization/gate_map.py +++ b/qiskit/visualization/gate_map.py @@ -474,6 +474,442 @@ def plot_gate_map( [12, 14], ] + qubit_coordinates_map[433] = [ + [0, 0], + [0, 1], + [0, 2], + [0, 3], + [0, 4], + [0, 5], + [0, 6], + [0, 7], + [0, 8], + [0, 9], + [0, 10], + [0, 11], + [0, 12], + [0, 13], + [0, 14], + [0, 15], + [0, 16], + [0, 17], + [0, 18], + [0, 19], + [0, 20], + [0, 21], + [0, 22], + [0, 23], + [0, 24], + [0, 25], + [1, 0], + [1, 4], + [1, 8], + [1, 12], + [1, 16], + [1, 20], + [1, 24], + [2, 0], + [2, 1], + [2, 2], + [2, 3], + [2, 4], + [2, 5], + [2, 6], + [2, 7], + [2, 8], + [2, 9], + [2, 10], + [2, 11], + [2, 12], + [2, 13], + [2, 14], + [2, 15], + [2, 16], + [2, 17], + [2, 18], + [2, 19], + [2, 20], + [2, 21], + [2, 22], + [2, 23], + [2, 24], + [2, 25], + [2, 26], + [3, 2], + [3, 6], + [3, 10], + [3, 14], + [3, 18], + [3, 22], + [3, 26], + [4, 0], + [4, 1], + [4, 2], + [4, 3], + [4, 4], + [4, 5], + [4, 6], + [4, 7], + [4, 8], + [4, 9], + [4, 10], + [4, 11], + [4, 12], + [4, 13], + [4, 14], + [4, 15], + [4, 16], + [4, 17], + [4, 18], + [4, 19], + [4, 20], + [4, 21], + [4, 22], + [4, 23], + [4, 24], + [4, 25], + [4, 26], + [5, 0], + [5, 4], + [5, 8], + [5, 12], + [5, 16], + [5, 20], + [5, 24], + [6, 0], + [6, 1], + [6, 2], + [6, 3], + [6, 4], + [6, 5], + [6, 6], + [6, 7], + [6, 8], + [6, 9], + [6, 10], + [6, 11], + [6, 12], + [6, 13], + [6, 14], + [6, 15], + [6, 16], + [6, 17], + [6, 18], + [6, 19], + [6, 20], + [6, 21], + [6, 22], + [6, 23], + [6, 24], + [6, 25], + [6, 26], + [7, 2], + [7, 6], + [7, 10], + [7, 14], + [7, 18], + [7, 22], + [7, 26], + [8, 0], + [8, 1], + [8, 2], + [8, 3], + [8, 4], + [8, 5], + [8, 6], + [8, 7], + [8, 8], + [8, 9], + [8, 10], + [8, 11], + [8, 12], + [8, 13], + [8, 14], + [8, 15], + [8, 16], + [8, 17], + [8, 18], + [8, 19], + [8, 20], + [8, 21], + [8, 22], + [8, 23], + [8, 24], + [8, 25], + [8, 26], + [9, 0], + [9, 4], + [9, 8], + [9, 12], + [9, 16], + [9, 20], + [9, 24], + [10, 0], + [10, 1], + [10, 2], + [10, 3], + [10, 4], + [10, 5], + [10, 6], + [10, 7], + [10, 8], + [10, 9], + [10, 10], + [10, 11], + [10, 12], + [10, 13], + [10, 14], + [10, 15], + [10, 16], + [10, 17], + [10, 18], + [10, 19], + [10, 20], + [10, 21], + [10, 22], + [10, 23], + [10, 24], + [10, 25], + [10, 26], + [11, 2], + [11, 6], + [11, 10], + [11, 14], + [11, 18], + [11, 22], + [11, 26], + [12, 0], + [12, 1], + [12, 2], + [12, 3], + [12, 4], + [12, 5], + [12, 6], + [12, 7], + [12, 8], + [12, 9], + [12, 10], + [12, 11], + [12, 12], + [12, 13], + [12, 14], + [12, 15], + [12, 16], + [12, 17], + [12, 18], + [12, 19], + [12, 20], + [12, 21], + [12, 22], + [12, 23], + [12, 24], + [12, 25], + [12, 26], + [13, 0], + [13, 4], + [13, 8], + [13, 12], + [13, 16], + [13, 20], + [13, 24], + [14, 0], + [14, 1], + [14, 2], + [14, 3], + [14, 4], + [14, 5], + [14, 6], + [14, 7], + [14, 8], + [14, 9], + [14, 10], + [14, 11], + [14, 12], + [14, 13], + [14, 14], + [14, 15], + [14, 16], + [14, 17], + [14, 18], + [14, 19], + [14, 20], + [14, 21], + [14, 22], + [14, 23], + [14, 24], + [14, 25], + [14, 26], + [15, 2], + [15, 6], + [15, 10], + [15, 14], + [15, 18], + [15, 22], + [15, 26], + [16, 0], + [16, 1], + [16, 2], + [16, 3], + [16, 4], + [16, 5], + [16, 6], + [16, 7], + [16, 8], + [16, 9], + [16, 10], + [16, 11], + [16, 12], + [16, 13], + [16, 14], + [16, 15], + [16, 16], + [16, 17], + [16, 18], + [16, 19], + [16, 20], + [16, 21], + [16, 22], + [16, 23], + [16, 24], + [16, 25], + [16, 26], + [17, 0], + [17, 4], + [17, 8], + [17, 12], + [17, 16], + [17, 20], + [17, 24], + [18, 0], + [18, 1], + [18, 2], + [18, 3], + [18, 4], + [18, 5], + [18, 6], + [18, 7], + [18, 8], + [18, 9], + [18, 10], + [18, 11], + [18, 12], + [18, 13], + [18, 14], + [18, 15], + [18, 16], + [18, 17], + [18, 18], + [18, 19], + [18, 20], + [18, 21], + [18, 22], + [18, 23], + [18, 24], + [18, 25], + [18, 26], + [19, 2], + [19, 6], + [19, 10], + [19, 14], + [19, 18], + [19, 22], + [19, 26], + [20, 0], + [20, 1], + [20, 2], + [20, 3], + [20, 4], + [20, 5], + [20, 6], + [20, 7], + [20, 8], + [20, 9], + [20, 10], + [20, 11], + [20, 12], + [20, 13], + [20, 14], + [20, 15], + [20, 16], + [20, 17], + [20, 18], + [20, 19], + [20, 20], + [20, 21], + [20, 22], + [20, 23], + [20, 24], + [20, 25], + [20, 26], + [21, 0], + [21, 4], + [21, 8], + [21, 12], + [21, 16], + [21, 20], + [21, 24], + [22, 0], + [22, 1], + [22, 2], + [22, 3], + [22, 4], + [22, 5], + [22, 6], + [22, 7], + [22, 8], + [22, 9], + [22, 10], + [22, 11], + [22, 12], + [22, 13], + [22, 14], + [22, 15], + [22, 16], + [22, 17], + [22, 18], + [22, 19], + [22, 20], + [22, 21], + [22, 22], + [22, 23], + [22, 24], + [22, 25], + [22, 26], + [23, 2], + [23, 6], + [23, 10], + [23, 14], + [23, 18], + [23, 22], + [23, 26], + [24, 1], + [24, 2], + [24, 3], + [24, 4], + [24, 5], + [24, 6], + [24, 7], + [24, 8], + [24, 9], + [24, 10], + [24, 11], + [24, 12], + [24, 13], + [24, 14], + [24, 15], + [24, 16], + [24, 17], + [24, 18], + [24, 19], + [24, 20], + [24, 21], + [24, 22], + [24, 23], + [24, 24], + [24, 25], + [24, 26], + ] + backend_version = _get_backend_interface_version(backend) if backend_version <= 1: from qiskit.transpiler.coupling import CouplingMap diff --git a/releasenotes/notes/433_qubit_coordinates_map-8abc318fefdb99ac.yaml b/releasenotes/notes/433_qubit_coordinates_map-8abc318fefdb99ac.yaml new file mode 100644 index 000000000000..ede523105e6c --- /dev/null +++ b/releasenotes/notes/433_qubit_coordinates_map-8abc318fefdb99ac.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Updated :func:`~qiskit.visualization.plot_gate_map`, :func:`~qiskit.visualization.plot_error_map`, and :func:`~qiskit.visualization.plot_circuit_layout` to support 433 qubit heavy-hex coupling maps. This allows coupling map visualizations for IBM's ``ibm_seattle``. From 06a5b9e00eb480cb691c50e88aa7d6418a978b70 Mon Sep 17 00:00:00 2001 From: "Kevin J. Sung" Date: Wed, 10 May 2023 08:12:26 -0400 Subject: [PATCH 097/172] define __getstate__ and __setstate__ for OneQubitGateErrorMap (#10092) * define __getstate__ and __setstate__ for OneQubitGateErrorMap * add test * fix test docstrings * add assert in test * add release note --- .../src/euler_one_qubit_decomposer.rs | 11 ++- ...fix-transpile-pickle-4045805b67c0c11b.yaml | 5 ++ test/python/compiler/test_transpiler.py | 86 +++++++++++-------- .../test_optimize_1q_decomposition.py | 9 ++ 4 files changed, 76 insertions(+), 35 deletions(-) create mode 100644 releasenotes/notes/fix-transpile-pickle-4045805b67c0c11b.yaml diff --git a/crates/accelerate/src/euler_one_qubit_decomposer.rs b/crates/accelerate/src/euler_one_qubit_decomposer.rs index 995d0e4810cb..1a30da570f93 100644 --- a/crates/accelerate/src/euler_one_qubit_decomposer.rs +++ b/crates/accelerate/src/euler_one_qubit_decomposer.rs @@ -34,7 +34,7 @@ enum SliceOrInt<'a> { Int(isize), } -#[pyclass] +#[pyclass(module = "qiskit._accelerate.euler_one_qubit_decomposer")] pub struct OneQubitGateErrorMap { error_map: Vec>, } @@ -50,9 +50,18 @@ impl OneQubitGateErrorMap { }, } } + fn add_qubit(&mut self, error_map: HashMap) { self.error_map.push(error_map); } + + fn __getstate__(&self) -> Vec> { + self.error_map.clone() + } + + fn __setstate__(&mut self, state: Vec>) { + self.error_map = state; + } } #[pyclass(sequence)] diff --git a/releasenotes/notes/fix-transpile-pickle-4045805b67c0c11b.yaml b/releasenotes/notes/fix-transpile-pickle-4045805b67c0c11b.yaml new file mode 100644 index 000000000000..7160cb7f9376 --- /dev/null +++ b/releasenotes/notes/fix-transpile-pickle-4045805b67c0c11b.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - Fix a bug in which running :class:`~.Optimize1qGatesDecomposition` + in parallel would raise an error due to OneQubitGateErrorMap + not being picklable. \ No newline at end of file diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index 2f03fd5112b1..fe5670aba861 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -14,66 +14,71 @@ import copy import io +import math import os import sys -import math import unittest - from logging import StreamHandler, getLogger -from unittest.mock import patch - -from ddt import ddt, data, unpack from test import combine # pylint: disable=wrong-import-order +from unittest.mock import patch import numpy as np import rustworkx as rx - -from qiskit.exceptions import QiskitError -from qiskit import BasicAer -from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, pulse, qpy, qasm3 -from qiskit.circuit import Parameter, Gate, Qubit, Clbit, Reset -from qiskit.compiler import transpile -from qiskit.dagcircuit import DAGOutNode, DAGOpNode -from qiskit.converters import circuit_to_dag +from ddt import data, ddt, unpack + +from qiskit import BasicAer, ClassicalRegister, QuantumCircuit, QuantumRegister, pulse, qasm3, qpy +from qiskit.circuit import ( + Clbit, + ControlFlowOp, + ForLoopOp, + Gate, + IfElseOp, + Parameter, + Qubit, + Reset, + SwitchCaseOp, + WhileLoopOp, +) +from qiskit.circuit.delay import Delay from qiskit.circuit.library import ( CXGate, - U3Gate, - U2Gate, - U1Gate, + CZGate, + HGate, RXGate, RYGate, RZGate, + SXGate, + U1Gate, + U2Gate, + U3Gate, UGate, - CZGate, XGate, - SXGate, - HGate, ) -from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, SwitchCaseOp, ControlFlowOp from qiskit.circuit.measure import Measure -from qiskit.circuit.delay import Delay -from qiskit.test import QiskitTestCase +from qiskit.compiler import transpile +from qiskit.converters import circuit_to_dag +from qiskit.dagcircuit import DAGOpNode, DAGOutNode +from qiskit.exceptions import QiskitError +from qiskit.providers.backend import BackendV2 from qiskit.providers.fake_provider import ( - FakeMelbourne, - FakeRueschlikon, FakeBoeblingen, + FakeMelbourne, FakeMumbaiV2, FakeNairobiV2, + FakeRueschlikon, FakeSherbrooke, ) -from qiskit.transpiler import Layout, CouplingMap -from qiskit.transpiler import PassManager, TransformationPass -from qiskit.transpiler.target import Target, InstructionProperties +from qiskit.providers.options import Options +from qiskit.pulse import InstructionScheduleMap +from qiskit.quantum_info import Operator, random_unitary +from qiskit.test import QiskitTestCase, slow_test +from qiskit.tools import parallel +from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass from qiskit.transpiler.exceptions import TranspilerError from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection -from qiskit.quantum_info import Operator, random_unitary from qiskit.transpiler.passmanager_config import PassManagerConfig -from qiskit.transpiler.preset_passmanagers import level_0_pass_manager -from qiskit.tools import parallel -from qiskit.pulse import InstructionScheduleMap -from qiskit.providers.backend import BackendV2 -from qiskit.providers.options import Options -from qiskit.test import slow_test +from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager +from qiskit.transpiler.target import InstructionProperties, Target class CustomCX(Gate): @@ -1890,6 +1895,19 @@ def restore_default(): self.addCleanup(restore_default) parallel.PARALLEL_DEFAULT = True + @data(0, 1, 2, 3) + def test_parallel_multiprocessing(self, opt_level): + """Test parallel dispatch works with multiprocessing.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.measure_all() + backend = FakeMumbaiV2() + pm = generate_preset_pass_manager(opt_level, backend) + res = pm.run([qc, qc]) + for circ in res: + self.assertIsInstance(circ, QuantumCircuit) + @data(0, 1, 2, 3) def test_parallel_with_target(self, opt_level): """Test that parallel dispatch works with a manual target.""" diff --git a/test/python/transpiler/test_optimize_1q_decomposition.py b/test/python/transpiler/test_optimize_1q_decomposition.py index f9b9759934c8..9f3cd3669afa 100644 --- a/test/python/transpiler/test_optimize_1q_decomposition.py +++ b/test/python/transpiler/test_optimize_1q_decomposition.py @@ -99,6 +99,15 @@ class TestOptimize1qGatesDecomposition(QiskitTestCase): """Test for 1q gate optimizations.""" + def test_run_pass_in_parallel(self): + """Test running pass on multiple circuits in parallel.""" + qr = QuantumRegister(1, "qr") + circuit = QuantumCircuit(qr) + passmanager = PassManager([Optimize1qGatesDecomposition(target=target_u1_u2_u3)]) + results = passmanager.run([circuit, circuit]) + for result in results: + self.assertTrue(Operator(circuit).equiv(Operator(result))) + @ddt.data(target_u1_u2_u3, target_rz_rx, target_rz_sx, target_rz_ry_u, target_h_p) def test_optimize_h_gates_target(self, target): """Transpile: qr:--[H]-[H]-[H]--""" From 2b18980926cf46371ad5d26888bb84af388dca4f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 10 May 2023 12:25:17 -0400 Subject: [PATCH 098/172] Remove concurrency handling from Makefile stestr usage (#10097) In #1973 the Makefile was updated to add manual concurrency handling to the invocation of stestr. At the time (> 4 yrs ag) our CI setup looked very different, everything was run in TravisCI which had much smaller memory quotas in the worker VMs and we also were actively using the Makefile to run tests. Since that time though CI and our development workflow have changed. At the time running multiple tests in parallel was causing us to exhaust the available RAM in the CI workers for MacOS and Windows so we set concurrency limits to prevent this. However, we no longer use travis, and the use of the makefile has long since been replaced by using tox as it provides a unified interface that manges venvs along with running tests in a consistent way across systems (including locally and CI). This commit reverts that change and lets stestr run with it's default concurrency when people use the Makefile. While the Makefile isn't widely used and has been largely superseded by tox, there are still some people using it and of those using MacOS or Windows this should improve local test throughput. --- Makefile | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/Makefile b/Makefile index e592a759231b..e6b8f5ce23d6 100644 --- a/Makefile +++ b/Makefile @@ -12,26 +12,6 @@ OS := $(shell uname -s) -ifeq ($(OS), Linux) - NPROCS := $(shell grep -c ^processor /proc/cpuinfo) -else ifeq ($(OS), Darwin) - NPROCS := 2 -else - NPROCS := 0 -endif # $(OS) - -ifeq ($(NPROCS), 2) - CONCURRENCY := 2 -else ifeq ($(NPROCS), 1) - CONCURRENCY := 1 -else ifeq ($(NPROCS), 3) - CONCURRENCY := 3 -else ifeq ($(NPROCS), 0) - CONCURRENCY := 0 -else - CONCURRENCY := $(shell echo "$(NPROCS) 2" | awk '{printf "%.0f", $$1 / $$2}') -endif - .PHONY: default env lint lint-incr style black test test_randomized pytest pytest_randomized test_ci coverage coverage_erase clean default: style lint-incr test ; @@ -87,8 +67,7 @@ pytest_randomized: pytest test/randomized test_ci: - @echo Detected $(NPROCS) CPUs running with $(CONCURRENCY) workers - QISKIT_TEST_CAPTURE_STREAMS=1 stestr run --concurrency $(CONCURRENCY) + QISKIT_TEST_CAPTURE_STREAMS=1 stestr run test_randomized: python3 -m unittest discover -s test/randomized -t . -v From 94eea5c7b0c27a5cf602f75ef2c872a876bc13ca Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Wed, 10 May 2023 14:27:43 -0600 Subject: [PATCH 099/172] Apply new deprecation decorators to dagcircuit folder (#9874) * Apply new deprecation decorators to assembler, compiler, and dagcircuit folders * Fix quantum_instance.py triggering deprecation for max_credits * Review feedback * Fmt - oops --- qiskit/dagcircuit/dagcircuit.py | 8 +++----- qiskit/dagcircuit/dagnode.py | 1 - 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index a2ea26334c53..fbc60591d1f7 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -38,7 +38,7 @@ from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.dagcircuit.exceptions import DAGCircuitError from qiskit.dagcircuit.dagnode import DAGNode, DAGOpNode, DAGInNode, DAGOutNode -from qiskit.utils.deprecation import deprecate_function +from qiskit.utils.deprecation import deprecate_func class DAGCircuit: @@ -526,10 +526,8 @@ def _add_op_node(self, op, qargs, cargs): self._increment_op(op) return node_index - @deprecate_function( - "The DAGCircuit._copy_circuit_metadata method is deprecated as of 0.20.0. It will be " - "removed no earlier than 3 months after the release date. You should use the " - "DAGCircuit.copy_empty_like method instead, which acts identically.", + @deprecate_func( + additional_msg="Instead, use :meth:`~copy_empty_like()`, which acts identically.", since="0.20.0", ) def _copy_circuit_metadata(self): diff --git a/qiskit/dagcircuit/dagnode.py b/qiskit/dagcircuit/dagnode.py index 022c57fa4aff..52b0e963c776 100644 --- a/qiskit/dagcircuit/dagnode.py +++ b/qiskit/dagcircuit/dagnode.py @@ -73,7 +73,6 @@ def semantic_eq(node1, node2, bit_indices1=None, bit_indices2=None): "release will require the mappings to be provided as arguments.", DeprecationWarning, ) - bit_indices1 = {arg: arg for arg in node1.qargs + node1.cargs} bit_indices2 = {arg: arg for arg in node2.qargs + node2.cargs} From 5323d8f460ed5f1e7b4040feeedd48950de818d6 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Wed, 10 May 2023 14:46:54 -0600 Subject: [PATCH 100/172] Apply new deprecation decorators to circuit folder (#9869) * Apply new deprecation decorators to circuit folder * Fix what I can (blocked by production issues) * Work around tests using bad production code * Revert "Work around tests using bad production code" This reverts commit 54f6ee81e524ba42ce127da72adc41bf9c72aa48. * Better fix for the deprecations * Use QiskitTestCase ignore mechanism * Review feedback * Fix lint and test failure --- qiskit/circuit/bit.py | 48 ++++++++----------- qiskit/circuit/classicalregister.py | 13 ++--- qiskit/circuit/instructionset.py | 28 +++++------ .../arithmetic/polynomial_pauli_rotations.py | 14 +++--- qiskit/circuit/quantumregister.py | 12 +++-- qiskit/test/base.py | 2 +- test/python/circuit/test_instructions.py | 4 +- 7 files changed, 56 insertions(+), 65 deletions(-) diff --git a/qiskit/circuit/bit.py b/qiskit/circuit/bit.py index 9d8cdbd0af20..c6c8db35ea65 100644 --- a/qiskit/circuit/bit.py +++ b/qiskit/circuit/bit.py @@ -13,9 +13,9 @@ """ Quantum bit and Classical bit objects. """ -import warnings from qiskit.circuit.exceptions import CircuitError +from qiskit.utils.deprecation import deprecate_func class Bit: @@ -59,12 +59,18 @@ def __init__(self, register=None, index=None): self._repr = f"{self.__class__.__name__}({self._register}, {self._index})" @property - def register(self): + @deprecate_func( + is_property=True, + since="0.17", + additional_msg=( + "Instead, use :meth:`~qiskit.circuit.quantumcircuit.QuantumCircuit.find_bit` to find " + "all the containing registers within a circuit and the index of the bit within the " + "circuit." + ), + ) + def register(self): # pylint: disable=bad-docstring-quotes """Get the register of an old-style bit. - .. deprecated:: 0.17 - Use :meth:`.QuantumCircuit.find_bit` instead. - In modern Qiskit Terra (version 0.17+), bits are the fundamental object and registers are aliases to collections of bits. A bit can be in many registers depending on the circuit, so a single containing register is no longer a property of a bit. It is an error to access @@ -72,24 +78,21 @@ def register(self): if (self._register, self._index) == (None, None): raise CircuitError("Attempt to query register of a new-style Bit.") - warnings.warn( - "'Bit.register' is deprecated since Qiskit Terra 0.17 and will be removed " - "in a future release. Bits may be in more than one register. " - "Use 'QuantumCircuit.find_bit' to find all the containing registers within a circuit, " - "and the index of the bit within the circuit.", - DeprecationWarning, - stacklevel=2, - ) - return self._register @property - def index(self): + @deprecate_func( + is_property=True, + since="0.17", + additional_msg=( + "Instead, use :meth:`~qiskit.circuit.quantumcircuit.QuantumCircuit.find_bit` to find " + "all the containing registers within a circuit and the index of the bit within the " + "circuit." + ), + ) + def index(self): # pylint: disable=bad-docstring-quotes """Get the index of an old-style bit in the register that owns it. - .. deprecated:: 0.17 - Use :meth:`.QuantumCircuit.find_bit` instead. - In modern Qiskit Terra (version 0.17+), bits are the fundamental object and registers are aliases to collections of bits. A bit can be in many registers depending on the circuit, so a single containing register is no longer a property of a bit. It is an error to access @@ -97,15 +100,6 @@ def index(self): if (self._register, self._index) == (None, None): raise CircuitError("Attempt to query index of a new-style Bit.") - warnings.warn( - "'Bit.index' is deprecated since Qiskit Terra 0.17 and will be removed " - "in a future release. Bits may be in more than one register. " - "Use 'QuantumCircuit.find_bit' to find all the containing registers within a circuit, " - "and the index of the bit within the circuit.", - DeprecationWarning, - stacklevel=2, - ) - return self._index def __repr__(self): diff --git a/qiskit/circuit/classicalregister.py b/qiskit/circuit/classicalregister.py index 19aeda7aeac1..644705ffee34 100644 --- a/qiskit/circuit/classicalregister.py +++ b/qiskit/circuit/classicalregister.py @@ -19,8 +19,7 @@ from qiskit.circuit.exceptions import CircuitError -# Over-specific import to avoid cyclic imports. -from qiskit.utils.deprecation import deprecate_function +from qiskit.utils.deprecation import deprecate_func from .register import Register from .bit import Bit @@ -58,10 +57,12 @@ class ClassicalRegister(Register): prefix = "c" bit_type = Clbit - @deprecate_function( - "Register.qasm() is deprecated since Terra 0.23, as correct exporting to OpenQASM 2 is " - "the responsibility of a larger exporter; it cannot safely be done on an object-by-object " - "basis without context. No replacement will be provided, because the premise is wrong.", + @deprecate_func( + additional_msg=( + "Correct exporting to OpenQASM 2 is the responsibility of a larger exporter; it cannot " + "safely be done on an object-by-object basis without context. No replacement will be " + "provided, because the premise is wrong." + ), since="0.23.0", ) def qasm(self): diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index 24a5ef9d4279..b1a06c386078 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -16,10 +16,10 @@ from __future__ import annotations import functools -import warnings from typing import Callable from qiskit.circuit.exceptions import CircuitError +from qiskit.utils.deprecation import deprecate_arg from .classicalregister import Clbit, ClassicalRegister from .operation import Operation from .quantumcircuitdata import CircuitInstruction @@ -95,7 +95,17 @@ class InstructionSet: __slots__ = ("_instructions", "_requester") - def __init__( + @deprecate_arg( + "circuit_cregs", + since="0.19.0", + additional_msg=( + "Instead, pass a complete resource requester with the 'resource_requester' argument. " + "The classical registers are insufficient to access all classical resources in a " + "circuit, as there may be loose classical bits as well. It can also cause integer " + "indices to be resolved incorrectly if any registers overlap." + ), + ) + def __init__( # pylint: disable=bad-docstring-quotes self, circuit_cregs: list[ClassicalRegister] | None = None, *, @@ -109,13 +119,6 @@ def __init__( Args: circuit_cregs (list[ClassicalRegister]): Optional. List of ``cregs`` of the circuit to which the instruction is added. Default: `None`. - - .. deprecated:: 0.19 - The classical registers are insufficient to access all classical resources in a - circuit, as there may be loose classical bits as well. It can also cause - integer indices to be resolved incorrectly if any registers overlap. Instead, - pass a complete requester to the ``resource_requester`` argument. - resource_requester: A callable that takes in the classical resource used in the condition, verifies that it is present in the attached circuit, resolves any indices into concrete :obj:`.Clbit` instances, and returns the concrete resource. If this @@ -136,13 +139,6 @@ def __init__( if circuit_cregs is not None: if resource_requester is not None: raise CircuitError("Cannot pass both 'circuit_cregs' and 'resource_requester'.") - warnings.warn( - "The 'circuit_cregs' argument to 'InstructionSet' is deprecated as of" - " qiskit-terra 0.19, and will be removed no sooner than 3 months after its release." - " Pass a complete resource requester as the 'resource_requester' instead.", - DeprecationWarning, - stacklevel=2, - ) self._requester: Callable[..., ClassicalRegister | Clbit] = _requester_from_cregs( tuple(circuit_cregs) ) diff --git a/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py b/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py index dfd0191b2447..c1194a9555c5 100644 --- a/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py +++ b/qiskit/circuit/library/arithmetic/polynomial_pauli_rotations.py @@ -14,12 +14,12 @@ """Polynomially controlled Pauli-rotations.""" from __future__ import annotations -import warnings from itertools import product from qiskit.circuit import QuantumRegister, QuantumCircuit from qiskit.circuit.exceptions import CircuitError +from qiskit.utils.deprecation import deprecate_func from .functional_pauli_rotations import FunctionalPauliRotations @@ -225,15 +225,13 @@ def degree(self) -> int: return 0 @property + @deprecate_func( + is_property=True, + since="0.16.0", + additional_msg="Instead, use the property :attr:`~num_ancillas`.", + ) def num_ancilla_qubits(self): """Deprecated. Use num_ancillas instead.""" - warnings.warn( - "The PolynomialPauliRotations.num_ancilla_qubits property is deprecated " - "as of 0.16.0. It will be removed no earlier than 3 months after the release " - "date. You should use the num_ancillas property instead.", - DeprecationWarning, - stacklevel=2, - ) return self.num_ancillas def _reset_registers(self, num_state_qubits): diff --git a/qiskit/circuit/quantumregister.py b/qiskit/circuit/quantumregister.py index 77e681528fd0..de87f73a79a3 100644 --- a/qiskit/circuit/quantumregister.py +++ b/qiskit/circuit/quantumregister.py @@ -20,7 +20,7 @@ from qiskit.circuit.exceptions import CircuitError # Over-specific import to avoid cyclic imports. -from qiskit.utils.deprecation import deprecate_function +from qiskit.utils.deprecation import deprecate_func from .register import Register from .bit import Bit @@ -58,10 +58,12 @@ class QuantumRegister(Register): prefix = "q" bit_type = Qubit - @deprecate_function( - "Register.qasm() is deprecated since Terra 0.23, as correct exporting to OpenQASM 2 is " - "the responsibility of a larger exporter; it cannot safely be done on an object-by-object " - "basis without context. No replacement will be provided, because the premise is wrong.", + @deprecate_func( + additional_msg=( + "Correct exporting to OpenQASM 2 is the responsibility of a larger exporter; it cannot " + "safely be done on an object-by-object basis without context. No replacement will be " + "provided, because the premise is wrong." + ), since="0.23.0", ) def qasm(self): diff --git a/qiskit/test/base.py b/qiskit/test/base.py index ac9da7a02a2d..870d17cfb535 100644 --- a/qiskit/test/base.py +++ b/qiskit/test/base.py @@ -227,7 +227,7 @@ def setUpClass(cls): r"elementwise comparison failed.*", r"The jsonschema validation included in qiskit-terra.*", r"The DerivativeBase.parameter_expression_grad method.*", - r"'Bit\.(register|index)' is deprecated.*", + r"The property ``qiskit\.circuit\.bit\.Bit\.(register|index)`` is deprecated.*", r"The CXDirection pass has been deprecated", r"The pauli_basis function with PauliTable.*", # Caused by internal scikit-learn scipy usage diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index 95466917d9f1..bb4b957d66dc 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -568,14 +568,14 @@ def test_instructionset_c_if_deprecated_resolution(self): registers = [ClassicalRegister(2), ClassicalRegister(3), ClassicalRegister(1)] bits = [bit for register in registers for bit in register] - deprecated_regex = r"The 'circuit_cregs' argument to 'InstructionSet' is deprecated .*" + deprecated_regex = r".* argument ``circuit_cregs`` is deprecated .*" def dummy_requester(specifier): """A dummy requester that technically fulfills the spec.""" raise CircuitError with self.subTest("cannot pass both registers and requester"): - with self.assertRaisesRegex( + with self.assertWarns(DeprecationWarning), self.assertRaisesRegex( CircuitError, r"Cannot pass both 'circuit_cregs' and 'resource_requester'\." ): InstructionSet(registers, resource_requester=dummy_requester) From 4d1afc094aeda22ba2431daff5d0ad1638322709 Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Thu, 11 May 2023 04:17:50 -0600 Subject: [PATCH 101/172] Apply new deprecation decorators to transpiler folder (#9868) * Apply new deprecation decorators to transpiler folder * Fix test * Review feedback * Remove redudant note --- qiskit/transpiler/coupling.py | 15 ++++++----- qiskit/transpiler/instruction_durations.py | 25 ++++++++++++------- .../passes/calibration/rzx_builder.py | 10 +++----- qiskit/transpiler/passes/scheduling/alap.py | 24 +++++++----------- .../scheduling/alignments/align_measures.py | 16 ++++++------ qiskit/transpiler/passes/scheduling/asap.py | 18 ++++++------- .../passes/scheduling/dynamical_decoupling.py | 18 ++++++------- .../synthesis/linear_functions_synthesis.py | 15 ++++------- .../passes/utils/check_cx_direction.py | 12 ++++----- .../transpiler/passes/utils/cx_direction.py | 12 ++++----- qiskit/transpiler/target.py | 21 +++++++++------- 11 files changed, 89 insertions(+), 97 deletions(-) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index fd53e06ed193..ed9424906709 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -21,13 +21,13 @@ import math from typing import List -import warnings import numpy as np import rustworkx as rx from rustworkx.visualization import graphviz_draw from qiskit.transpiler.exceptions import CouplingError +from qiskit.utils.deprecation import deprecate_func class CouplingMap: @@ -125,18 +125,17 @@ def add_edge(self, src, dst): self._dist_matrix = None # invalidate self._is_symmetric = None # invalidate + @deprecate_func( + additional_msg=( + "Instead, use :meth:`~reduce`. It does the same thing, but preserves nodelist order." + ), + since="0.20.0", + ) def subgraph(self, nodelist): """Return a CouplingMap object for a subgraph of self. nodelist (list): list of integer node labels """ - warnings.warn( - "The .subgraph() method is deprecated and will be removed in a " - "future release. Instead the .reduce() method should be used " - "instead which does the same thing but preserves nodelist order.", - DeprecationWarning, - stacklevel=2, - ) subcoupling = CouplingMap() subcoupling.graph = self.graph.subgraph(nodelist) return subcoupling diff --git a/qiskit/transpiler/instruction_durations.py b/qiskit/transpiler/instruction_durations.py index efc462f091b2..a56a39b97ddf 100644 --- a/qiskit/transpiler/instruction_durations.py +++ b/qiskit/transpiler/instruction_durations.py @@ -11,7 +11,6 @@ # that they have been altered from the originals. """Durations of instructions, one of transpiler configurations.""" -import warnings from typing import Optional, List, Tuple, Union, Iterable, Set from qiskit.circuit import Barrier, Delay @@ -19,9 +18,16 @@ from qiskit.circuit.duration import duration_in_dt from qiskit.providers import Backend from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils.deprecation import deprecate_arg from qiskit.utils.units import apply_prefix +def _is_deprecated_qubits_argument(qubits: Union[int, List[int], Qubit, List[Qubit]]) -> bool: + if isinstance(qubits, (int, Qubit)): + qubits = [qubits] + return isinstance(qubits[0], Qubit) + + class InstructionDurations: """Helper class to provide durations of instructions for scheduling. @@ -160,6 +166,15 @@ def update(self, inst_durations: Optional["InstructionDurationsType"], dt: float return self + @deprecate_arg( + "qubits", + deprecation_description=( + "Using a Qubit or List[Qubit] for the ``qubits`` argument to InstructionDurations.get()" + ), + additional_msg="Instead, use an integer for the qubit index.", + since="0.19.0", + predicate=_is_deprecated_qubits_argument, + ) def get( self, inst: Union[str, Instruction], @@ -197,14 +212,6 @@ def get( qubits = [qubits] if isinstance(qubits[0], Qubit): - warnings.warn( - "Querying an InstructionDurations object with a Qubit " - "has been deprecated and will be removed in a future " - "release. Instead, query using the integer qubit " - "index.", - DeprecationWarning, - stacklevel=2, - ) qubits = [q.index for q in qubits] try: diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py index 5ac9d910b280..0b519a5a375a 100644 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -34,6 +34,7 @@ from qiskit.pulse.filters import filter_instructions from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.transpiler.target import Target +from qiskit.utils.deprecation import deprecate_arg from .base_builder import CalibrationBuilder from .exceptions import CalibrationNotAvailable @@ -63,6 +64,7 @@ class RZXCalibrationBuilder(CalibrationBuilder): angle. Additional details can be found in https://arxiv.org/abs/2012.11660. """ + @deprecate_arg("qubit_channel_mapping", since="0.22.0") def __init__( self, instruction_schedule_map: InstructionScheduleMap = None, @@ -86,14 +88,8 @@ def __init__( Raises: QiskitError: Instruction schedule map is not provided. """ + del qubit_channel_mapping super().__init__() - - if qubit_channel_mapping: - warnings.warn( - "'qubit_channel_mapping' is no longer used. This value is ignored.", - DeprecationWarning, - ) - self._inst_map = instruction_schedule_map self._verbose = verbose if target: diff --git a/qiskit/transpiler/passes/scheduling/alap.py b/qiskit/transpiler/passes/scheduling/alap.py index 5059656058f3..0265aae7a1ff 100644 --- a/qiskit/transpiler/passes/scheduling/alap.py +++ b/qiskit/transpiler/passes/scheduling/alap.py @@ -12,11 +12,10 @@ """ALAP Scheduling.""" -import warnings - from qiskit.circuit import Delay, Qubit, Measure from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils.deprecation import deprecate_func from .base_scheduler import BaseSchedulerTransform @@ -26,23 +25,18 @@ class ALAPSchedule(BaseSchedulerTransform): See :class:`~qiskit.transpiler.passes.scheduling.base_scheduler.BaseSchedulerTransform` for the detailed behavior of the control flow operation, i.e. ``c_if``. - - .. note:: - - This base class has been superseded by :class:`~.ALAPScheduleAnalysis` and - the new scheduling workflow. It will be deprecated and subsequently - removed in a future release. """ + @deprecate_func( + additional_msg=( + "Instead, use :class:`~.ALAPScheduleAnalysis`, which is an " + "analysis pass that requires a padding pass to later modify the circuit." + ), + since="0.21.0", + pending=True, + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - warnings.warn( - "The ALAPSchedule class has been supersceded by the ALAPScheduleAnalysis class " - "which performs the as analysis pass that requires a padding pass to later modify " - "the circuit. This class will be deprecated in a future release and subsequently " - "removed after that.", - PendingDeprecationWarning, - ) def run(self, dag): """Run the ALAPSchedule pass on `dag`. diff --git a/qiskit/transpiler/passes/scheduling/alignments/align_measures.py b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py index 73ecf3e19de7..9a1b3c94c5e5 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/align_measures.py +++ b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py @@ -23,6 +23,7 @@ from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils.deprecation import deprecate_func class AlignMeasures(TransformationPass): @@ -86,6 +87,14 @@ class AlignMeasures(TransformationPass): However, it may return meaningless measurement data mainly due to the phase error. """ + @deprecate_func( + additional_msg=( + "Instead, use :class:`~.ConstrainedReschedule`, which performs the same function " + "but also supports aligning to additional timing constraints." + ), + since="0.21.0", + pending=True, + ) def __init__(self, alignment: int = 1): """Create new pass. @@ -95,13 +104,6 @@ def __init__(self, alignment: int = 1): the control electronics of your quantum processor. """ super().__init__() - warnings.warn( - "The AlignMeasures class has been supersceded by the ConstrainedReschedule class " - "which performs the same function but also supports aligning to additional timing " - "constraints. This class will be deprecated in a future release and subsequently " - "removed after that.", - PendingDeprecationWarning, - ) self.alignment = alignment def run(self, dag: DAGCircuit): diff --git a/qiskit/transpiler/passes/scheduling/asap.py b/qiskit/transpiler/passes/scheduling/asap.py index b404e73d00a7..a3a729b34622 100644 --- a/qiskit/transpiler/passes/scheduling/asap.py +++ b/qiskit/transpiler/passes/scheduling/asap.py @@ -12,11 +12,10 @@ """ASAP Scheduling.""" -import warnings - from qiskit.circuit import Delay, Qubit, Measure from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils.deprecation import deprecate_func from .base_scheduler import BaseSchedulerTransform @@ -34,15 +33,16 @@ class ASAPSchedule(BaseSchedulerTransform): removed in a future release. """ + @deprecate_func( + additional_msg=( + "Instead, use :class:`~.ASAPScheduleAnalysis`, which is an " + "analysis pass that requires a padding pass to later modify the circuit." + ), + since="0.21.0", + pending=True, + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - warnings.warn( - "The ASAPSchedule class has been supersceded by the ASAPScheduleAnalysis class " - "which performs the as analysis pass that requires a padding pass to later modify " - "the circuit. This class will be deprecated in a future release and subsequently " - "removed after that.", - PendingDeprecationWarning, - ) def run(self, dag): """Run the ASAPSchedule pass on `dag`. diff --git a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py index fa0b2b29187e..6de3e49b5923 100644 --- a/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/dynamical_decoupling.py @@ -13,7 +13,6 @@ """Dynamical Decoupling insertion pass.""" import itertools -import warnings import numpy as np from qiskit.circuit import Gate, Delay, Reset @@ -24,6 +23,7 @@ from qiskit.transpiler.passes.optimization import Optimize1qGates from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit.utils.deprecation import deprecate_func class DynamicalDecoupling(TransformationPass): @@ -90,6 +90,14 @@ def uhrig_pulse_location(k): timeline_drawer(circ_dd) """ + @deprecate_func( + additional_msg=( + "Instead, use :class:`~.DynamicalDecouplingPadding`, which performs the same " + "function but requires scheduling and alignment analysis passes to run prior to it." + ), + since="0.21.0", + pending=True, + ) def __init__( self, durations, dd_sequence, qubits=None, spacing=None, skip_reset_qubits=True, target=None ): @@ -113,14 +121,6 @@ def __init__( ``durations`` and this are specified then this argument will take precedence and ``durations`` will be ignored. """ - warnings.warn( - "The DynamicalDecoupling class has been supersceded by the " - "DynamicalDecouplingPadding class which performs the same function but " - "requires scheduling and alignment analysis passes to run prior to it. " - "This class will be deprecated in a future release and subsequently " - "removed after that.", - PendingDeprecationWarning, - ) super().__init__() self._durations = durations self._dd_sequence = dd_sequence diff --git a/qiskit/transpiler/passes/synthesis/linear_functions_synthesis.py b/qiskit/transpiler/passes/synthesis/linear_functions_synthesis.py index 892902bbdb2a..4d31be6bff12 100644 --- a/qiskit/transpiler/passes/synthesis/linear_functions_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/linear_functions_synthesis.py @@ -13,13 +13,12 @@ """Synthesize LinearFunctions.""" -import warnings - from qiskit.transpiler.basepasses import TransformationPass from qiskit.dagcircuit.dagcircuit import DAGCircuit from qiskit.circuit.library import PermutationGate from qiskit.circuit.exceptions import CircuitError from qiskit.transpiler.passes.synthesis.high_level_synthesis import HighLevelSynthesis, HLSConfig +from qiskit.utils.deprecation import deprecate_func class LinearFunctionsSynthesis(HighLevelSynthesis): @@ -28,15 +27,11 @@ class LinearFunctionsSynthesis(HighLevelSynthesis): Under the hood, this runs the default high-level synthesis plugin for linear functions. """ + @deprecate_func( + additional_msg="Instead, use :class:`~.HighLevelSynthesis`.", + since="0.23.0", + ) def __init__(self): - warnings.warn( - "The LinearFunctionsSynthesis class is deprecated as of Qiskit Terra 0.23.0 " - "and will be removed no sooner than 3 months after the release date. " - "Instead use the HighLevelSynthesis class.", - DeprecationWarning, - stacklevel=2, - ) - # This config synthesizes only linear functions using the "default" method. default_linear_config = HLSConfig( linear_function=[("default", {})], diff --git a/qiskit/transpiler/passes/utils/check_cx_direction.py b/qiskit/transpiler/passes/utils/check_cx_direction.py index 298a27aae130..2726d2e26e74 100644 --- a/qiskit/transpiler/passes/utils/check_cx_direction.py +++ b/qiskit/transpiler/passes/utils/check_cx_direction.py @@ -12,18 +12,16 @@ """Check if the CNOTs follow the right direction with respect to the coupling map..""" -import warnings from qiskit.transpiler.passes.utils.check_gate_direction import CheckGateDirection +from qiskit.utils.deprecation import deprecate_func class CheckCXDirection(CheckGateDirection): """Deprecated: use :class:`qiskit.transpiler.passes.CheckGateDirection` pass instead.""" + @deprecate_func( + additional_msg="Instead, use the more generic :class:`~.CheckGateDirection` pass.", + since="0.21.0", + ) def __init__(self, coupling_map=None, target=None): super().__init__(coupling_map=coupling_map, target=target) - warnings.warn( - "The CheckCXDirection pass has been deprecated " - "and replaced by a more generic CheckGateDirection pass.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/qiskit/transpiler/passes/utils/cx_direction.py b/qiskit/transpiler/passes/utils/cx_direction.py index deec72b168c3..e93bf0d69e79 100644 --- a/qiskit/transpiler/passes/utils/cx_direction.py +++ b/qiskit/transpiler/passes/utils/cx_direction.py @@ -12,18 +12,16 @@ """Rearrange the direction of the cx nodes to match the directed coupling map.""" -import warnings from qiskit.transpiler.passes.utils.gate_direction import GateDirection +from qiskit.utils.deprecation import deprecate_func class CXDirection(GateDirection): """Deprecated: use :class:`qiskit.transpiler.passes.GateDirection` pass instead.""" + @deprecate_func( + additional_msg="Instead, use the more generic :class:`~.GateDirection` pass.", + since="0.21.0", + ) def __init__(self, coupling_map): super().__init__(coupling_map) - warnings.warn( - "The CXDirection pass has been deprecated " - "and replaced by a more generic GateDirection pass.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 3e4c6aab6a21..61999e6e7418 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -20,7 +20,6 @@ from __future__ import annotations import itertools -import warnings from typing import Tuple, Union, Optional, Dict, List, Any from collections.abc import Mapping @@ -45,7 +44,7 @@ from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.providers.exceptions import BackendPropertyError from qiskit.pulse.exceptions import PulseError -from qiskit.utils.deprecation import deprecate_arguments +from qiskit.utils.deprecation import deprecate_arg, deprecate_func from qiskit.exceptions import QiskitError # import QubitProperties here to provide convenience alias for building a @@ -242,7 +241,7 @@ class Target(Mapping): "_global_operations", ) - @deprecate_arguments({"aquire_alignment": "acquire_alignment"}, since="0.23.0") + @deprecate_arg("aquire_alignment", new_alias="acquire_alignment", since="0.23.0") def __init__( self, description=None, @@ -1131,19 +1130,23 @@ def get_non_global_operation_names(self, strict_direction=False): return incomplete_basis_gates @property + @deprecate_func( + additional_msg="Use the property ``acquire_alignment`` instead.", + since="0.24.0", + is_property=True, + ) def aquire_alignment(self): """Alias of deprecated name. This will be removed.""" - warnings.warn( - "aquire_alignment is deprecated. Use acquire_alignment instead.", DeprecationWarning - ) return self.acquire_alignment @aquire_alignment.setter + @deprecate_func( + additional_msg="Use the property ``acquire_alignment`` instead.", + since="0.24.0", + is_property=True, + ) def aquire_alignment(self, new_value: int): """Alias of deprecated name. This will be removed.""" - warnings.warn( - "aquire_alignment is deprecated. Use acquire_alignment instead.", DeprecationWarning - ) self.acquire_alignment = new_value def __iter__(self): From e924ea9f6bae15bc67b704ebcdacda194480603e Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Thu, 11 May 2023 04:49:15 -0600 Subject: [PATCH 102/172] Apply new deprecation decorators to visualization folder (#9867) * Apply new deprecation decorators to visualization folder * Remove stray backtick * Fix helper functions setting deprecated args to default of `None` * Ignore pylint issue * Don't use deprecator for `circuit is None` --- qiskit/visualization/circuit/_utils.py | 24 +++------- .../circuit/circuit_visualization.py | 4 -- qiskit/visualization/circuit/latex.py | 45 +++++-------------- qiskit/visualization/counts_visualization.py | 31 +++++++++---- qiskit/visualization/pulse/interpolation.py | 24 +++++----- qiskit/visualization/pulse/matplotlib.py | 33 ++++++++------ qiskit/visualization/pulse/qcstyle.py | 41 +++++++++-------- qiskit/visualization/state_visualization.py | 38 +++++++--------- 8 files changed, 105 insertions(+), 135 deletions(-) diff --git a/qiskit/visualization/circuit/_utils.py b/qiskit/visualization/circuit/_utils.py index 4117b0e76177..0cb97755efac 100644 --- a/qiskit/visualization/circuit/_utils.py +++ b/qiskit/visualization/circuit/_utils.py @@ -14,7 +14,6 @@ import re from collections import OrderedDict -from warnings import warn import numpy as np @@ -31,6 +30,7 @@ from qiskit.circuit.tools import pi_check from qiskit.converters import circuit_to_dag from qiskit.utils import optionals as _optionals +from qiskit.utils.deprecation import deprecate_arg from ..exceptions import VisualizationError @@ -196,6 +196,7 @@ def get_bit_register(circuit, bit): return bit_loc.registers[0][0] if bit_loc.registers else None +@deprecate_arg("reverse_bits", since="0.22.0") def get_bit_reg_index(circuit, bit, reverse_bits=None): """Get the register for a bit if there is one, and the index of the bit from the top of the circuit, or the index of the bit within a register. @@ -210,15 +211,7 @@ def get_bit_reg_index(circuit, bit, reverse_bits=None): int: index of the bit from the top of the circuit int: index of the bit within the register, if there is a register """ - if reverse_bits is not None: - warn( - "The 'reverse_bits' kwarg to the function " - "~qiskit.visualization.utils.get_bit_reg_index " - "is deprecated as of 0.22.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) + del reverse_bits bit_loc = circuit.find_bit(bit) bit_index = bit_loc.index register, reg_index = bit_loc.registers[0] if bit_loc.registers else (None, None) @@ -291,6 +284,7 @@ def get_wire_label(drawer, register, index, layout=None, cregbundle=True): return wire_label +@deprecate_arg("reverse_bits", since="0.22.0") def get_condition_label_val(condition, circuit, cregbundle, reverse_bits=None): """Get the label and value list to display a condition @@ -304,15 +298,7 @@ def get_condition_label_val(condition, circuit, cregbundle, reverse_bits=None): str: label to display for the condition list(str): list of 1's and 0's indicating values of condition """ - if reverse_bits is not None: - warn( - "The 'reverse_bits' kwarg to the function " - "~qiskit.visualization.utils.get_condition_label_val " - "is deprecated as of 0.22.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) + del reverse_bits cond_is_bit = bool(isinstance(condition[0], Clbit)) cond_val = int(condition[1]) diff --git a/qiskit/visualization/circuit/circuit_visualization.py b/qiskit/visualization/circuit/circuit_visualization.py index a2b5e25674b0..0627f2617063 100644 --- a/qiskit/visualization/circuit/circuit_visualization.py +++ b/qiskit/visualization/circuit/circuit_visualization.py @@ -561,12 +561,8 @@ def _generate_latex_source( style=style, reverse_bits=reverse_bits, plot_barriers=plot_barriers, - layout=None, initial_state=initial_state, cregbundle=cregbundle, - global_phase=None, - qregs=None, - cregs=None, with_layout=with_layout, circuit=circuit, ) diff --git a/qiskit/visualization/circuit/latex.py b/qiskit/visualization/circuit/latex.py index 4f97ea34fc44..f7743f59ad88 100644 --- a/qiskit/visualization/circuit/latex.py +++ b/qiskit/visualization/circuit/latex.py @@ -24,6 +24,7 @@ from qiskit.circuit.library.standard_gates import SwapGate, XGate, ZGate, RZZGate, U1Gate, PhaseGate from qiskit.circuit.measure import Measure from qiskit.circuit.tools.pi_check import pi_check +from qiskit.utils.deprecation import deprecate_arg from .qcstyle import load_style from ._utils import ( @@ -47,7 +48,11 @@ class QCircuitImage: Thanks to Eric Sabo for the initial implementation for Qiskit. """ - def __init__( + @deprecate_arg("gregs", since="0.20.0") + @deprecate_arg("cregs", since="0.20.0") + @deprecate_arg("layout", since="0.20.0") + @deprecate_arg("global_phase", since="0.20.0") + def __init__( # pylint: disable=bad-docstring-quotes self, qubits, clbits, @@ -85,38 +90,8 @@ def __init__( Raises: ImportError: If pylatexenc is not installed """ - if qregs is not None: - warn( - "The 'qregs' kwarg to the QCircuitImage class is deprecated " - "as of 0.20.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) - if cregs is not None: - warn( - "The 'cregs' kwarg to the QCircuitImage class is deprecated " - "as of 0.20.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) - if layout is not None: - warn( - "The 'layout' kwarg to the QCircuitImage class is deprecated " - "as of 0.20.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) - if global_phase is not None: - warn( - "The 'global_phase' kwarg to the QCircuitImage class is deprecated " - "as of 0.20.0 and will be removed no earlier than 3 months " - "after the release date.", - DeprecationWarning, - 2, - ) + del layout + del global_phase # This check should be removed when the 4 deprecations above are removed if circuit is None: warn( @@ -127,10 +102,10 @@ def __init__( 2, ) circ = QuantumCircuit(qubits, clbits) - for reg in qregs: + for reg in qregs or []: bits = [qubits[circ._qubit_indices[q].index] for q in reg] circ.add_register(QuantumRegister(None, reg.name, list(bits))) - for reg in cregs: + for reg in cregs or []: bits = [clbits[circ._clbit_indices[q].index] for q in reg] circ.add_register(ClassicalRegister(None, reg.name, list(bits))) self._circuit = circ diff --git a/qiskit/visualization/counts_visualization.py b/qiskit/visualization/counts_visualization.py index 725dc04817c9..ad9ec4536ccc 100644 --- a/qiskit/visualization/counts_visualization.py +++ b/qiskit/visualization/counts_visualization.py @@ -16,11 +16,11 @@ from collections import OrderedDict import functools -import warnings import numpy as np from qiskit.utils import optionals as _optionals +from qiskit.utils.deprecation import deprecate_arg from qiskit.result import QuasiDistribution, ProbDistribution from .exceptions import VisualizationError from .utils import matplotlib_close_if_inline @@ -46,6 +46,28 @@ def hamming_distance(str1, str2): DIST_MEAS = {"hamming": hamming_distance} +def _is_deprecated_data_format(data) -> bool: + if not isinstance(data, list): + data = [data] + for dat in data: + if isinstance(dat, (QuasiDistribution, ProbDistribution)) or isinstance( + next(iter(dat.values())), float + ): + return True + return False + + +@deprecate_arg( + "data", + deprecation_description=( + "Using plot_histogram() ``data`` argument with QuasiDistribution, ProbDistribution, or a " + "distribution dictionary" + ), + since="0.22.0", + additional_msg="Instead, use ``plot_distribution()``.", + predicate=_is_deprecated_data_format, + pending=True, +) def plot_histogram( data, figsize=(7, 5), @@ -134,13 +156,6 @@ def plot_histogram( if isinstance(dat, (QuasiDistribution, ProbDistribution)) or isinstance( next(iter(dat.values())), float ): - warnings.warn( - "Using plot histogram with QuasiDistribution, ProbDistribution, or a " - "distribution dictionary will be deprecated in 0.23.0 and subsequently " - "removed in a future release. You should use plot_distribution() instead.", - PendingDeprecationWarning, - stacklevel=2, - ) kind = "distribution" return _plotting_core( data, diff --git a/qiskit/visualization/pulse/interpolation.py b/qiskit/visualization/pulse/interpolation.py index 188907da340b..b5d12a19cbe9 100644 --- a/qiskit/visualization/pulse/interpolation.py +++ b/qiskit/visualization/pulse/interpolation.py @@ -22,15 +22,16 @@ import numpy as np -from qiskit.utils.deprecation import deprecate_function +from qiskit.utils.deprecation import deprecate_func -@deprecate_function( - "`qiskit.visualization.pulse` and all its contents are deprecated since Terra 0.23." - " The new interface for pulse visualization is `qiskit.visualization.pulse_drawer_v2`." - " In no less than 6 months, `pulse_drawer_v2` will become `pulse_drawer`, and these old" - " objects will be completely removed.", +@deprecate_func( + additional_msg=( + "Instead, use the new interface in ``qiskit.visualization.pulse_drawer`` for " + "pulse visualization." + ), since="0.23.0", + removal_timeline="no earlier than 6 months after the release date", ) def interp1d( time: np.ndarray, samples: np.ndarray, nop: int, kind: str = "linear" @@ -64,12 +65,13 @@ def interp1d( return time_, cs_ry(time_), cs_iy(time_) -@deprecate_function( - "`qiskit.visualization.pulse` and all its contents are deprecated since Terra 0.23." - " The new interface for pulse visualization is `qiskit.visualization.pulse_drawer_v2`." - " In no less than 6 months, `pulse_drawer_v2` will become `pulse_drawer`, and these old" - " objects will be completely removed.", +@deprecate_func( + additional_msg=( + "Instead, use the new interface in ``qiskit.visualization.pulse_drawer`` for " + "pulse visualization." + ), since="0.23.0", + removal_timeline="no earlier than 6 months after the release date", ) def step_wise( time: np.ndarray, samples: np.ndarray, nop: int diff --git a/qiskit/visualization/pulse/matplotlib.py b/qiskit/visualization/pulse/matplotlib.py index 4c1d63204dd2..619677161ab2 100644 --- a/qiskit/visualization/pulse/matplotlib.py +++ b/qiskit/visualization/pulse/matplotlib.py @@ -46,17 +46,19 @@ SetPhase, ) from qiskit.pulse.schedule import ScheduleComponent -from qiskit.utils.deprecation import deprecate_function +from qiskit.utils.deprecation import deprecate_func class EventsOutputChannels: """Pulse dataset for channel.""" - @deprecate_function( - "`qiskit.visualization.pulse` and all its contents are deprecated since Terra 0.23." - " The new interface for pulse visualization is `qiskit.visualization.pulse_drawer`." - " In no less than 6 months the old objects will be completely removed.", + @deprecate_func( + additional_msg=( + "Instead, use the new interface in ``qiskit.visualization.pulse_drawer`` for " + "pulse visualization." + ), since="0.23.0", + removal_timeline="no earlier than 6 months after the release date", ) def __init__(self, t0: int, tf: int): """Create new channel dataset. @@ -287,11 +289,13 @@ def _trim(self, events: dict[int, Any]) -> dict[int, Any]: class WaveformDrawer: """A class to create figure for sample pulse.""" - @deprecate_function( - "`qiskit.visualization.pulse` and all its contents are deprecated since Terra 0.23." - " The new interface for pulse visualization is `qiskit.visualization.pulse_drawer`." - " In no less than 6 months the old objects will be completely removed.", + @deprecate_func( + additional_msg=( + "Instead, use the new interface in ``qiskit.visualization.pulse_drawer`` for " + "pulse visualization." + ), since="0.23.0", + removal_timeline="no earlier than 6 months after the release date", ) def __init__(self, style: PulseStyle): """Create new figure. @@ -388,12 +392,13 @@ def draw( class ScheduleDrawer: """A class to create figure for schedule and channel.""" - @deprecate_function( - "`qiskit.visualization.pulse` and all its contents are deprecated since Terra 0.23." - " The new interface for pulse visualization is `qiskit.visualization.pulse_drawer`." - " In no less than 6 months the old objects will be completely removed.", - stacklevel=3, + @deprecate_func( + additional_msg=( + "Instead, use the new interface in ``qiskit.visualization.pulse_drawer`` for " + "pulse visualization." + ), since="0.23.0", + removal_timeline="no earlier than 6 months after the release date", ) def __init__(self, style: SchedStyle): """Create new figure. diff --git a/qiskit/visualization/pulse/qcstyle.py b/qiskit/visualization/pulse/qcstyle.py index c0d46f34223d..5ef6ba33510f 100644 --- a/qiskit/visualization/pulse/qcstyle.py +++ b/qiskit/visualization/pulse/qcstyle.py @@ -16,11 +16,10 @@ Style sheets for pulse visualization. """ from __future__ import annotations -import warnings - +import logging from collections import namedtuple -import logging +from qiskit.utils.deprecation import deprecate_func logger = logging.getLogger(__name__) ComplexColors = namedtuple("ComplexColors", ["real", "imaginary"]) @@ -30,6 +29,15 @@ class SchedStyle: """Style sheet for Qiskit-Pulse schedule drawer.""" + @deprecate_func( + additional_msg=( + "In addition to this stylesheet being deprecated, the legacy pulse drawer is too. " + "Instead, use the new drawer ``qiskit.visualization.pulse_drawer`` " + "with new stylesheet classes provided by ``qiskit.visualization.pulse_v2``. " + "You can choose one of ``IQXStandard``, ``IQXSimple``, ``IQXDebugging``." + ), + since="0.23.0", + ) def __init__( self, figsize: tuple[float, float] | None = (10.0, 12.0), @@ -127,15 +135,6 @@ def __init__( If you want to show more events, increase figure height or reduce size of line height and table font size. """ - warnings.warn( - "The legacy pulse drawer is deprecated along with this stylesheet. " - "Please use new drawer `qiskit.visualization.pulse_drawer` " - "with new stylesheet classes provided by `qiskit.visualization.pulse_v2`. " - "You can choose one of `IQXStandard`, `IQXSimple`, `IQXDebugging`.", - DeprecationWarning, - stacklevel=2, - ) - self.figsize = figsize self.fig_unit_h_table = fig_unit_h_table self.use_table = use_table @@ -175,6 +174,15 @@ class PulseStyle: Style sheet for Qiskit-Pulse sample pulse drawer.""" + @deprecate_func( + additional_msg=( + "In addition to this stylesheet being deprecated, the legacy pulse drawer is too. " + "Instead, use the new drawer ``qiskit.visualization.pulse_drawer`` " + "with new stylesheet classes provided by ``qiskit.visualization.pulse_v2``. " + "You can choose one of ``IQXStandard``, ``IQXSimple``, ``IQXDebugging``." + ), + since="0.23.0", + ) def __init__( self, figsize: tuple[float, float] | None = (7.0, 5.0), @@ -208,15 +216,6 @@ def __init__( If the output is ``matplotlib``, the default parameter is ``rcParams['figure.dpi']``. """ - warnings.warn( - "The legacy pulse drawer is deprecated along with this stylesheet. " - "Please use new drawer `qiskit.visualization.pulse_drawer` " - "with new stylesheet classes provided by `qiskit.visualization.pulse_v2`. " - "You can choose one of `IQXStandard`, `IQXSimple`, `IQXDebugging`.", - DeprecationWarning, - stacklevel=2, - ) - self.figsize = figsize self.title_font_size = title_font_size self.wave_color = wave_color diff --git a/qiskit/visualization/state_visualization.py b/qiskit/visualization/state_visualization.py index 2897d841812c..f9232328f200 100644 --- a/qiskit/visualization/state_visualization.py +++ b/qiskit/visualization/state_visualization.py @@ -20,13 +20,13 @@ from typing import Optional, List, Union from functools import reduce import colorsys -import warnings + import numpy as np from qiskit import user_config from qiskit.quantum_info.states.statevector import Statevector from qiskit.quantum_info.operators.symplectic import PauliList, SparsePauliOp from qiskit.quantum_info.states.densitymatrix import DensityMatrix -from qiskit.utils.deprecation import deprecate_arguments +from qiskit.utils.deprecation import deprecate_arg, deprecate_func from qiskit.utils import optionals as _optionals from qiskit.circuit.tools.pi_check import pi_check @@ -35,7 +35,7 @@ from .exceptions import VisualizationError -@deprecate_arguments({"rho": "state"}, since="0.15.1") +@deprecate_arg("rho", new_alias="state", since="0.15.1") @_optionals.HAS_MATPLOTLIB.require_in_call def plot_state_hinton( state, title="", figsize=None, ax_real=None, ax_imag=None, *, rho=None, filename=None @@ -253,7 +253,7 @@ def plot_bloch_vector( return None -@deprecate_arguments({"rho": "state"}, since="0.15.1") +@deprecate_arg("rho", new_alias="state", since="0.15.1") @_optionals.HAS_MATPLOTLIB.require_in_call def plot_bloch_multivector( state, @@ -361,7 +361,7 @@ def plot_bloch_multivector( return fig.savefig(filename) -@deprecate_arguments({"rho": "state"}, since="0.15.1") +@deprecate_arg("rho", new_alias="state", since="0.15.1") @_optionals.HAS_MATPLOTLIB.require_in_call def plot_state_city( state, @@ -618,7 +618,7 @@ def plot_state_city( return fig.savefig(filename) -@deprecate_arguments({"rho": "state"}, since="0.15.1") +@deprecate_arg("rho", new_alias="state", since="0.15.1") @_optionals.HAS_MATPLOTLIB.require_in_call def plot_state_paulivec( state, title="", figsize=None, color=None, ax=None, *, rho=None, filename=None @@ -789,7 +789,7 @@ def phase_to_rgb(complex_number): return rgb -@deprecate_arguments({"rho": "state"}, since="0.15.1") +@deprecate_arg("rho", new_alias="state", since="0.15.1") @_optionals.HAS_MATPLOTLIB.require_in_call @_optionals.HAS_SEABORN.require_in_call def plot_state_qsphere( @@ -1291,6 +1291,10 @@ def state_to_latex( return prefix + latex_str + suffix +@deprecate_func( + additional_msg="For similar functionality, see sympy's ``nsimplify`` and ``latex`` functions.", + since="0.23.0", +) def num_to_latex_ket(raw_value: complex, first_term: bool, decimals: int = 10) -> Optional[str]: """Convert a complex number to latex code suitable for a ket expression @@ -1301,19 +1305,15 @@ def num_to_latex_ket(raw_value: complex, first_term: bool, decimals: int = 10) - Returns: String with latex code or None if no term is required """ - warnings.warn( - "qiskit.visualization.state_visualization.num_to_latex_ket is " - "deprecated as of 0.23.0 and will be removed no earlier than 3 months " - "after the release. For similar functionality, see sympy's `nsimplify` " - "and `latex` functions.", - category=DeprecationWarning, - stacklevel=2, - ) if np.around(np.abs(raw_value), decimals=decimals) == 0: return None return _num_to_latex(raw_value, first_term=first_term, decimals=decimals, coefficient=True) +@deprecate_func( + additional_msg="For similar functionality, see sympy's ``nsimplify`` and ``latex`` functions.", + since="0.23.0", +) def numbers_to_latex_terms(numbers: List[complex], decimals: int = 10) -> List[str]: """Convert a list of numbers to latex formatted terms The first non-zero term is treated differently. For this term a leading + is suppressed. @@ -1323,14 +1323,6 @@ def numbers_to_latex_terms(numbers: List[complex], decimals: int = 10) -> List[s Returns: List of formatted terms """ - warnings.warn( - "qiskit.visualization.state_visualization.num_to_latex_terms is " - "deprecated as of 0.23.0 and will be removed no earlier than 3 months " - "after the release. For similar functionality, see sympy's `nsimplify` " - "and `latex` functions.", - category=DeprecationWarning, - stacklevel=2, - ) first_term = True terms = [] for number in numbers: From 5177db6e09917809895fe37878422ba8fcb6321a Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 12 May 2023 17:49:11 +0200 Subject: [PATCH 103/172] Deprecate circuit-library Jupyter magic (#9940) * Deprecate circuit-library Jupyter magic This has not been used since the switch away from using `jupyter-execute` in gh-9346, so is neither maintained nor necessary. Any improvements to styling should be made to the new paths, not to these. These objects were originally intended only for the documentation so we expect user impact to be minimal, and the objects are not maintained anyway. * Correct versions for deprecation * Add missing import --- qiskit/tools/jupyter/jupyter_magics.py | 5 +++++ qiskit/tools/jupyter/library.py | 21 +++++++++++++++++++ ...cuit-library-jupyter-629f927e8dd5cc22.yaml | 14 +++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 releasenotes/notes/deprecate-circuit-library-jupyter-629f927e8dd5cc22.yaml diff --git a/qiskit/tools/jupyter/jupyter_magics.py b/qiskit/tools/jupyter/jupyter_magics.py index 6f90a1a76863..5fc9d92b7bb6 100644 --- a/qiskit/tools/jupyter/jupyter_magics.py +++ b/qiskit/tools/jupyter/jupyter_magics.py @@ -20,6 +20,7 @@ from IPython.core.magic import cell_magic, line_magic, Magics, magics_class, register_line_magic from qiskit.utils import optionals as _optionals +from qiskit.utils.deprecation import deprecate_func import qiskit from qiskit.tools.events.progressbar import TextProgressBar from .progressbar import HTMLProgressBar @@ -174,6 +175,10 @@ def qiskit_progress_bar(self, line="", cell=None): # pylint: disable=unused-arg if _optionals.HAS_MATPLOTLIB and get_ipython(): @register_line_magic + @deprecate_func( + since="0.25.0", + additional_msg="This was originally only for internal documentation and is no longer used.", + ) def circuit_library_info(circuit: qiskit.QuantumCircuit) -> None: """Displays library information for a quantum circuit. diff --git a/qiskit/tools/jupyter/library.py b/qiskit/tools/jupyter/library.py index 42eb4ded7a51..21a341871dbc 100644 --- a/qiskit/tools/jupyter/library.py +++ b/qiskit/tools/jupyter/library.py @@ -19,6 +19,7 @@ from qiskit import QuantumCircuit from qiskit.exceptions import MissingOptionalLibraryError from qiskit.utils import optionals as _optionals +from qiskit.utils.deprecation import deprecate_func try: import pygments @@ -49,6 +50,10 @@ def _generate_circuit_library_visualization(circuit: QuantumCircuit): plt.show() +@deprecate_func( + since="0.25.0", + additional_msg="This is unused by Qiskit, and no replacement will be publicly provided.", +) def circuit_data_table(circuit: QuantumCircuit) -> wid.HTML: """Create a HTML table widget for a given quantum circuit. @@ -114,6 +119,10 @@ def circuit_data_table(circuit: QuantumCircuit) -> wid.HTML: ) +@deprecate_func( + since="0.25.0", + additional_msg="This is unused by Qiskit, and no replacement will be publicly provided.", +) def properties_widget(circuit: QuantumCircuit) -> wid.VBox: """Create a HTML table widget with header for a given quantum circuit. @@ -130,6 +139,10 @@ def properties_widget(circuit: QuantumCircuit) -> wid.VBox: return properties +@deprecate_func( + since="0.25.0", + additional_msg="This is unused by Qiskit, and no replacement will be publicly provided.", +) def qasm_widget(circuit: QuantumCircuit) -> wid.VBox: """Generate a QASM widget with header for a quantum circuit. @@ -190,6 +203,10 @@ def qasm_widget(circuit: QuantumCircuit) -> wid.VBox: return qasm +@deprecate_func( + since="0.25.0", + additional_msg="This is unused by Qiskit, and no replacement will be publicly provided.", +) def circuit_diagram_widget() -> wid.Box: """Create a circuit diagram widget. @@ -212,6 +229,10 @@ def circuit_diagram_widget() -> wid.Box: return top +@deprecate_func( + since="0.25.0", + additional_msg="This is unused by Qiskit, and no replacement will be publicly provided.", +) def circuit_library_widget(circuit: QuantumCircuit) -> None: """Create a circuit library widget. diff --git a/releasenotes/notes/deprecate-circuit-library-jupyter-629f927e8dd5cc22.yaml b/releasenotes/notes/deprecate-circuit-library-jupyter-629f927e8dd5cc22.yaml new file mode 100644 index 000000000000..00a5a2487d20 --- /dev/null +++ b/releasenotes/notes/deprecate-circuit-library-jupyter-629f927e8dd5cc22.yaml @@ -0,0 +1,14 @@ +--- +deprecations: + - | + The Jupyter magic ``%circuit_library_info`` and the objects in ``qiskit.tools.jupyter.library`` + it calls in turn: + + - ``circuit_data_table`` + - ``properties_widget`` + - ``qasm_widget`` + - ``circuit_digram_widget`` + - ``circuit_library_widget`` + + are deprecated and will be removed in Terra 0.27. These objects were only intended for use in + the documentation build. They are no longer used there, so are no longer supported or maintained. From 86e0f825f2ab15c3e5de73eafecaabb738e02703 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Mon, 15 May 2023 16:22:59 +0200 Subject: [PATCH 104/172] CITATION.md from metapackage (#10111) --- CITATION.bib | 6 ++++++ README.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 CITATION.bib diff --git a/CITATION.bib b/CITATION.bib new file mode 100644 index 000000000000..a00798d9baaa --- /dev/null +++ b/CITATION.bib @@ -0,0 +1,6 @@ +@misc{Qiskit, + author = {{Qiskit contributors}}, + title = {Qiskit: An Open-source Framework for Quantum Computing}, + year = {2023}, + doi = {10.5281/zenodo.2573505} +} diff --git a/README.md b/README.md index 9b6c5b183844..f88d53267771 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ Now you're set up and ready to check out some of the other examples from our ## Authors and Citation Qiskit Terra is the work of [many people](https://github.com/Qiskit/qiskit-terra/graphs/contributors) who contribute -to the project at different levels. If you use Qiskit, please cite as per the included [BibTeX file](https://github.com/Qiskit/qiskit/blob/master/Qiskit.bib). +to the project at different levels. If you use Qiskit, please cite as per the included [BibTeX file](CITATION.bib). ## Changelog and Release Notes From a45a787ac87f69d5a4f5b318e782715c411ec571 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Tue, 16 May 2023 15:03:36 +0200 Subject: [PATCH 105/172] Improve ``PauliFeatureMap`` docs (#10068) * Improve clarity of docs * Fix missing dash and code block * fix references * Apply suggestions from code review * Update qiskit/circuit/library/data_preparation/pauli_feature_map.py --------- Co-authored-by: Luciano Bello --- .../data_preparation/pauli_feature_map.py | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/qiskit/circuit/library/data_preparation/pauli_feature_map.py b/qiskit/circuit/library/data_preparation/pauli_feature_map.py index 55c44aa1d5f5..932351bc8691 100644 --- a/qiskit/circuit/library/data_preparation/pauli_feature_map.py +++ b/qiskit/circuit/library/data_preparation/pauli_feature_map.py @@ -27,29 +27,33 @@ class PauliFeatureMap(NLocal): r"""The Pauli Expansion circuit. The Pauli Expansion circuit is a data encoding circuit that transforms input data - :math:`\vec{x} \in \mathbb{R}^n` as + :math:`\vec{x} \in \mathbb{R}^n`, where `n` is the ``feature_dimension``, as .. math:: - U_{\Phi(\vec{x})}=\exp\left(i\sum_{S\subseteq [n]} - \phi_S(\vec{x})\prod_{i\in S} P_i\right) + U_{\Phi(\vec{x})}=\exp\left(i\sum_{S \in \mathcal{I}} + \phi_S(\vec{x})\prod_{i\in S} P_i\right). - The circuit contains ``reps`` repetitions of this transformation. - The variable :math:`P_i \in \{ I, X, Y, Z \}` denotes the Pauli matrices. - The index :math:`S` describes connectivities between different qubits or datapoints: - :math:`S \in \{\binom{n}{k}\ combinations,\ k = 1,... n \}`. Per default the data-mapping + Here, :math:`S` is a set of qubit indices that describes the connections in the feature map, + :math:`\mathcal{I}` is a set containing all these index sets, and + :math:`P_i \in \{I, X, Y, Z\}`. Per default the data-mapping :math:`\phi_S` is .. math:: \phi_S(\vec{x}) = \begin{cases} - x_0 \text{ if } k = 1 \\ - \prod_{j \in S} (\pi - x_j) \text{ otherwise } - \end{cases} + x_i \text{ if } S = \{i\} \\ + \prod_{j \in S} (\pi - x_j) \text{ if } |S| > 1 + \end{cases}. + + The possible connections can be set using the ``entanglement`` and ``paulis`` arguments. + For example, for single-qubit :math:`Z` rotations and two-qubit :math:`YY` interactions + between all qubit pairs, we can set:: + - For example, if the Pauli strings are chosen to be :math:`P_0 = Z` and :math:`P_{0,1} = YY` on - 2 qubits and with 1 repetition using the default data-mapping, the Pauli evolution feature map - is represented by: + feature_map = PauliFeatureMap(..., paulis=["Z", "YY"], entanglement="full") + + which will produce blocks of the form .. parsed-literal:: @@ -59,9 +63,10 @@ class PauliFeatureMap(NLocal): ┤ H ├┤ U1(2.0*x[1]) ├┤ RX(pi/2) ├┤ X ├┤ U1(2.0*(pi - x[0])*(pi - x[1])) ├┤ X ├┤ RX(-pi/2) ├ └───┘└──────────────┘└──────────┘└───┘└─────────────────────────────────┘└───┘└───────────┘ - Please refer to :class:`ZFeatureMap` for the case :math:`k = 1`, :math:`P_0 = Z` - and to :class:`ZZFeatureMap` for the case :math:`k = 2`, :math:`P_0 = Z` and - :math:`P_{0,1} = ZZ`. + The circuit contains ``reps`` repetitions of this transformation. + + Please refer to :class:`.ZFeatureMap` for the case of single-qubit Pauli-:math:`Z` rotations + and to :class:`.ZZFeatureMap` for the single- and two-qubit Pauli-:math:`Z` rotations. Examples: @@ -99,8 +104,12 @@ class PauliFeatureMap(NLocal): OrderedDict([('cx', 39), ('rx', 36), ('u1', 21), ('h', 15), ('ry', 12), ('rz', 12)]) References: - [1]: Havlicek et al. (2018), Supervised learning with quantum enhanced feature spaces. - `arXiv:1804.11326 `_ + + + + [1] Havlicek et al. Supervised learning with quantum enhanced feature spaces, + `Nature 567, 209-212 (2019) `__. + """ def __init__( From 1d844eccae743032c4416fa67069dbc3478144a9 Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Wed, 17 May 2023 18:41:48 +0900 Subject: [PATCH 106/172] add abs method to ParameterExpression (#9309) * added abs method to parameterexpression * fixed wrong call in abs function * changed definition of abs to __abs__ * additionally implemented __le__, __ge__, __lt__, __gt__ operators * fixed requested features and implemented more tests for __lt__, __gt__ * fixed lint errors in parameterexpression and its tests * fixed formatting error in docstrings * removed __lt__ and friends, fixed recommendations * added more tests for abs function * fixed lint * added a releasenote to document change * mod function & add more tests * add new line at the end of file * fix lint * add alias of __abs__ for doc * mod test_compile_with_ufunc * add test * fix test * fix test * fix test * fix test * fix test * Update test_parameters.py * Update releasenotes/notes/add-abs-to-parameterexpression-347ffef62946b38b.yaml Co-authored-by: Julien Gacon --------- Co-authored-by: Christopher Zachow Co-authored-by: Julien Gacon --- qiskit/circuit/__init__.py | 6 +- qiskit/circuit/parameterexpression.py | 15 +++++ ...-parameterexpression-347ffef62946b38b.yaml | 14 ++++ test/python/circuit/test_parameters.py | 65 +++++++++++++++++-- 4 files changed, 90 insertions(+), 10 deletions(-) create mode 100644 releasenotes/notes/add-abs-to-parameterexpression-347ffef62946b38b.yaml diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 52848e2e77b1..083325efad2f 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -306,9 +306,9 @@ .. autosummary:: :toctree: ../stubs/ - Parameter - ParameterVector - ParameterExpression + Parameter + ParameterVector + ParameterExpression Random Circuits --------------- diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 0d5015a29259..6858efa1301d 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -498,6 +498,21 @@ def __copy__(self): def __deepcopy__(self, memo=None): return self + def __abs__(self): + """Absolute of a ParameterExpression""" + if _optionals.HAS_SYMENGINE: + import symengine + + return self._call(symengine.Abs) + else: + from sympy import Abs as _abs + + return self._call(_abs) + + def abs(self): + """Absolute of a ParameterExpression""" + return self.__abs__() + def __eq__(self, other): """Check if this parameter expression is equal to another parameter expression or a fixed value (only if this is a bound expression). diff --git a/releasenotes/notes/add-abs-to-parameterexpression-347ffef62946b38b.yaml b/releasenotes/notes/add-abs-to-parameterexpression-347ffef62946b38b.yaml new file mode 100644 index 000000000000..1385724d8d1f --- /dev/null +++ b/releasenotes/notes/add-abs-to-parameterexpression-347ffef62946b38b.yaml @@ -0,0 +1,14 @@ + +issues: + - | + Added support for taking absolute values of :class:`.ParameterExpression`\s. For example, + the following is now possible:: + + from qiskit.circuit import QuantumCircuit, Parameter + + x = Parameter("x") + circuit = QuantumCircuit(1) + circuit.rx(abs(x), 0) + + bound = circuit.bind_parameters({x: -1}) + diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index 5f49f026a7a5..e19fbd205166 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1113,6 +1113,7 @@ def test_circuit_with_ufunc(self): theta = Parameter(name="theta") qc = QuantumCircuit(2) + qc.p(numpy.abs(-phi), 0) qc.p(numpy.cos(phi), 0) qc.p(numpy.sin(phi), 0) qc.p(numpy.tan(phi), 0) @@ -1123,6 +1124,7 @@ def test_circuit_with_ufunc(self): qc.assign_parameters({phi: pi, theta: 1}, inplace=True) qc_ref = QuantumCircuit(2) + qc_ref.p(pi, 0) qc_ref.p(-1, 0) qc_ref.p(0, 0) qc_ref.p(0, 0) @@ -1133,14 +1135,40 @@ def test_circuit_with_ufunc(self): self.assertEqual(qc, qc_ref) def test_compile_with_ufunc(self): - """Test compiling of circuit with unbounded parameters + """Test compiling of circuit with unbound parameters after we apply universal functions.""" - phi = Parameter("phi") - qc = QuantumCircuit(1) - qc.rx(numpy.cos(phi), 0) - backend = BasicAer.get_backend("qasm_simulator") - qc_aer = transpile(qc, backend) - self.assertIn(phi, qc_aer.parameters) + from math import pi + + theta = ParameterVector("theta", length=7) + + qc = QuantumCircuit(7) + qc.rx(numpy.abs(theta[0]), 0) + qc.rx(numpy.cos(theta[1]), 1) + qc.rx(numpy.sin(theta[2]), 2) + qc.rx(numpy.tan(theta[3]), 3) + qc.rx(numpy.arccos(theta[4]), 4) + qc.rx(numpy.arctan(theta[5]), 5) + qc.rx(numpy.arcsin(theta[6]), 6) + + # transpile to different basis + transpiled = transpile(qc, basis_gates=["rz", "sx", "x", "cx"], optimization_level=0) + + for x in theta: + self.assertIn(x, transpiled.parameters) + + bound = transpiled.bind_parameters({theta: [-1, pi, pi, pi, 1, 1, 1]}) + + expected = QuantumCircuit(7) + expected.rx(1.0, 0) + expected.rx(-1.0, 1) + expected.rx(0.0, 2) + expected.rx(0.0, 3) + expected.rx(0.0, 4) + expected.rx(pi / 4, 5) + expected.rx(pi / 2, 6) + expected = transpile(expected, basis_gates=["rz", "sx", "x", "cx"], optimization_level=0) + + self.assertEqual(expected, bound) def test_parametervector_resize(self): """Test the resize method of the parameter vector.""" @@ -1207,6 +1235,29 @@ def test_compare_to_value_when_bound(self): bound_expr = x.bind({x: 2.3}) self.assertEqual(bound_expr, 2.3) + def test_abs_function_when_bound(self): + """Verify expression can be used with + abs functions when bound.""" + + x = Parameter("x") + xb_1 = x.bind({x: 2.0}) + xb_2 = x.bind({x: 3.0 + 4.0j}) + + self.assertEqual(abs(xb_1), 2.0) + self.assertEqual(abs(-xb_1), 2.0) + self.assertEqual(abs(xb_2), 5.0) + + def test_abs_function_when_not_bound(self): + """Verify expression can be used with + abs functions when not bound.""" + + x = Parameter("x") + y = Parameter("y") + + self.assertEqual(abs(x), abs(-x)) + self.assertEqual(abs(x) * abs(y), abs(x * y)) + self.assertEqual(abs(x) / abs(y), abs(x / y)) + def test_cast_to_float_when_bound(self): """Verify expression can be cast to a float when fully bound.""" From 591116f584fb462cab7cd4c4ecf879b0b89e8d3f Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Fri, 19 May 2023 14:14:28 +0200 Subject: [PATCH 107/172] Add how-to guides about primitives (#9716) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added how-to section * added how to compose circuits guide * added how to visualize circuit guide * add how to create parameterized circuit guide * add guides * removed latex circuit visualization * fixed title underlines * Fix typo * explicitly set cregbundle to False when appending circuit with classical register * Fix typo * Added explanation about needed latex distribution * added link to qiskit.visualization module * Simplified references * Change 'we' to 'you' * Changed how_to.rst to how_to/index.rst * Removed jupyter-execute from create_a_quantum_circuit * Removed jupyter-execute from create_a_parameterized_circuit * Removed jupyter-execute from compose_quantum_circuits * Removed jupyter-execute from visualize_a_quantum_circuit * Add sphinx.ext.doctest to conf.py * Change header symbol for index and visualization guide * Added guides for sampler and estimator and simplified toctree * Fixed underline * Add links to how-to create circuit * Added section about parameterized circuits to primitives how-tos * Remove other how-tos * Remove links * Added reference to other implementations of primitives * Update docs/how_to/use_estimator.rst Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> * Improved titles * set global doctest flags and disable automatic doctests * Added part about changing options * Add new aer docs page to intersphinx * Added notes about circuit measurements * Improved notes about circuit measurements * Update docs/how_to/use_estimator.rst Co-authored-by: Junye Huang * Update docs/how_to/use_sampler.rst Co-authored-by: Junye Huang * Update docs/how_to/use_sampler.rst Co-authored-by: Junye Huang * Update docs/how_to/use_sampler.rst Co-authored-by: Junye Huang * Added numpy to intersphinx and note about doctest and plot directive * Added note about quasi-probabilities * rename how to to How-to Guides * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_estimator.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/how_to/use_sampler.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Remove generally * Changed sampler initial note to mention BackendSampler * Update docs/how_to/use_sampler.rst Co-authored-by: Luciano Bello * Added code example for backend primitives and link to providers page * Update intersphinx mapping to ecosystem for aer and runtime * Change rustworkx link * Add "primitive" to estimator how-to title Co-authored-by: Luciano Bello * Change hypothetical import to avoid confusion Co-authored-by: Luciano Bello * Update docs/how_to/use_estimator.rst Co-authored-by: Luciano Bello * Include "primitive" word in sampler how-to title Co-authored-by: Luciano Bello * Improve phrasing Co-authored-by: Luciano Bello * Include notice about doctest and plot directive incompatibility in sampler guide * change->modify sampler options * change qiskit_provider to * shots->amount of shots in estimator how-to * Add quick explanation about using multiple circuits and observables and adjust plurals * singular * singular --------- Co-authored-by: Steve Wood <40241007+woodsp-ibm@users.noreply.github.com> Co-authored-by: Junye Huang Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: Luciano Bello --- docs/conf.py | 16 +- docs/how_to/index.rst | 15 ++ docs/how_to/use_estimator.rst | 269 ++++++++++++++++++++++++++++++++++ docs/how_to/use_sampler.rst | 255 ++++++++++++++++++++++++++++++++ docs/index.rst | 1 + 5 files changed, 546 insertions(+), 10 deletions(-) create mode 100644 docs/how_to/index.rst create mode 100644 docs/how_to/use_estimator.rst create mode 100644 docs/how_to/use_sampler.rst diff --git a/docs/conf.py b/docs/conf.py index b443e895e9d0..b3ec73843a54 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -65,9 +65,9 @@ modindex_common_prefix = ["qiskit."] intersphinx_mapping = { - "retworkx": ("https://qiskit.org/documentation/retworkx/", None), - "qiskit-ibm-runtime": ("https://qiskit.org/documentation/partners/qiskit_ibm_runtime/", None), - "qiskit-aer": ("https://qiskit.org/documentation/aer/", None), + "rustworkx": ("https://qiskit.org/ecosystem/rustworkx/", None), + "qiskit-ibm-runtime": ("https://qiskit.org/ecosystem/ibm-runtime/", None), + "qiskit-aer": ("https://qiskit.org/ecosystem/aer/", None), "numpy": ("https://numpy.org/doc/stable/", None) } @@ -116,17 +116,13 @@ autoclass_content = "both" -# -- Options for Doctest -------------------------------------------------------- -import sphinx.ext.doctest +# -- Options for Doctest -------------------------------------------------------- -# This option will make doctest ignore whitespace when testing code. -# It's specially important for circuit representation as it gives an -# error otherwise -doctest_default_flags = sphinx.ext.doctest.doctest.NORMALIZE_WHITESPACE +doctest_default_flags = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.IGNORE_EXCEPTION_DETAIL | doctest.DONT_ACCEPT_TRUE_FOR_1 # Leaving this string empty disables testing of doctest blocks from docstrings. # Doctest blocks are structures like this one: # >> code # output -doctest_test_doctest_blocks = "" +doctest_test_doctest_blocks = "" \ No newline at end of file diff --git a/docs/how_to/index.rst b/docs/how_to/index.rst new file mode 100644 index 000000000000..a20d20870d99 --- /dev/null +++ b/docs/how_to/index.rst @@ -0,0 +1,15 @@ +.. _how_to: + +############# +How-to Guides +############# + + +Use the primitives +================== + +.. toctree:: + :maxdepth: 1 + + use_sampler + use_estimator \ No newline at end of file diff --git a/docs/how_to/use_estimator.rst b/docs/how_to/use_estimator.rst new file mode 100644 index 000000000000..61a69ab5ffbc --- /dev/null +++ b/docs/how_to/use_estimator.rst @@ -0,0 +1,269 @@ +######################################################### +Compute an expectation value with ``Estimator`` primitive +######################################################### + +This guide shows how to get the expected value of an observable for a given quantum circuit with the :class:`~qiskit.primitives.Estimator` primitive. + +.. note:: + + While this guide uses Qiskit’s reference implementation, the ``Estimator`` primitive can be run with any provider using :class:`~qiskit.primitives.BackendEstimator` . + + .. code-block:: + + from qiskit.primitives import BackendEstimator + from import QiskitProvider + + provider = QiskitProvider() + backend = provider.get_backend('backend_name') + estimator = BackendEstimator(backend) + + There are some providers that implement primitives natively (see `this page `_ for more details). + + +Initialize observables +====================== + +The first step is to define the observables whose expected value you want to compute. Each observable can be any ``BaseOperator``, like the operators from :mod:`qiskit.quantum_info`. +Among them it is preferable to use :class:`~qiskit.quantum_info.SparsePauliOp`. + +.. testcode:: + + from qiskit.quantum_info import SparsePauliOp + + observable = SparsePauliOp(["II", "XX", "YY", "ZZ"], coeffs=[1, 1, -1, 1]) + +Initialize quantum circuit +========================== + +Then you need to create the :class:`~qiskit.circuit.QuantumCircuit`\ s for which you want to obtain the expected value. + +.. plot:: + :include-source: + + from qiskit import QuantumCircuit + + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0,1) + qc.draw("mpl") + +.. testsetup:: + + # This code is repeated (but hidden) because we will need to use the variables with the extension sphinx.ext.doctest (testsetup/testcode/testoutput directives) + # and we can't reuse the variables from the plot directive above because they are incompatible. + # The plot directive is used to draw the circuit with matplotlib and the code is shown because of the include-source flag. + + from qiskit import QuantumCircuit + + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0,1) + +.. note:: + + The :class:`~qiskit.circuit.QuantumCircuit` you pass to :class:`~qiskit.primitives.Estimator` must not include any measurements. + +Initialize the ``Estimator`` +============================ + +Then, you need to instantiate an :class:`~qiskit.primitives.Estimator`. + +.. testcode:: + + from qiskit.primitives import Estimator + + estimator = Estimator() + +Run and get results +=================== + +Now that you have defined your ``estimator``, you can run your estimation by calling the :meth:`~qiskit.primitives.Estimator.run` method, +which returns an instance of :class:`~.PrimitiveJob` (subclass of :class:`~qiskit.providers.JobV1`). You can get the results from the job (as a :class:`~qiskit.primitives.EstimatorResult` object) +with the :meth:`~qiskit.providers.JobV1.result` method. + +.. testcode:: + + job = estimator.run(qc, observable) + result = job.result() + print(result) + +.. testoutput:: + + EstimatorResult(values=array([4.]), metadata=[{}]) + +While this example only uses one :class:`~qiskit.circuit.QuantumCircuit` and one observable, if you want to get expectation values for multiple circuits and observables you can +pass a ``list`` of :class:`~qiskit.circuit.QuantumCircuit`\ s and a list of ``BaseOperator``\ s to the :meth:`~qiskit.primitives.Estimator.run` method. Both ``list``\ s must have +the same length. + +Get the expected value +---------------------- + +From these results you can extract the expected values with the attribute :attr:`~qiskit.primitives.EstimatorResult.values`. + +:attr:`~qiskit.primitives.EstimatorResult.values` returns a :class:`numpy.ndarray` +whose ``i``-th element is the expectation value corresponding to the ``i``-th circuit and ``i``-th observable. + +.. testcode:: + + exp_value = result.values[0] + print(exp_value) + +.. testoutput:: + + 3.999999999999999 + +Parameterized circuit with ``Estimator`` +======================================== + +The :class:`~qiskit.primitives.Estimator` primitive can be run with unbound parameterized circuits like the one below. +You can also manually bind values to the parameters of the circuit and follow the steps +of the previous example. + +.. testcode:: + + from qiskit.circuit import Parameter + + theta = Parameter('θ') + param_qc = QuantumCircuit(2) + param_qc.ry(theta, 0) + param_qc.cx(0,1) + print(param_qc.draw()) + +.. testoutput:: + + ┌───────┐ + q_0: ┤ Ry(θ) ├──■── + └───────┘┌─┴─┐ + q_1: ─────────┤ X ├ + └───┘ + +The main difference with the previous case is that now you need to specify the sets of parameter values +for which you want to evaluate the expectation value as a ``list`` of ``list``\ s of ``float``\ s. +The ``i``-th element of the outer``list`` is the set of parameter values +that corresponds to the ``i``-th circuit and observable. + +.. testcode:: + + import numpy as np + + parameter_values = [[0], [np.pi/6], [np.pi/2]] + + job = estimator.run([param_qc]*3, [observable]*3, parameter_values=parameter_values) + values = job.result().values + + for i in range(3): + print(f"Parameter: {parameter_values[i][0]:.5f}\t Expectation value: {values[i]}") + +.. testoutput:: + + Parameter: 0.00000 Expectation value: 2.0 + Parameter: 0.52360 Expectation value: 3.0 + Parameter: 1.57080 Expectation value: 4.0 + +Change run options +================== + +Your workflow might require tuning primitive run options, such as the amount of shots. + +By default, the reference :class:`~qiskit.primitives.Estimator` class performs an exact statevector +calculation based on the :class:`~qiskit.quantum_info.Statevector` class. However, this can be +modified to include shot noise if the number of ``shots`` is set. +For reproducibility purposes, a ``seed`` will also be set in the following examples. + +There are two main ways of setting options in the :class:`~qiskit.primitives.Estimator`: + +* Set keyword arguments in the :meth:`~qiskit.primitives.Estimator.run` method. +* Modify :class:`~qiskit.primitives.Estimator` options. + +Set keyword arguments for :meth:`~qiskit.primitives.Estimator.run` +------------------------------------------------------------------ + +If you only want to change the settings for a specific run, it can be more convenient to +set the options inside the :meth:`~qiskit.primitives.Estimator.run` method. You can do this by +passing them as keyword arguments. + +.. testcode:: + + job = estimator.run(qc, observable, shots=2048, seed=123) + result = job.result() + print(result) + +.. testoutput:: + + EstimatorResult(values=array([4.]), metadata=[{'variance': 3.552713678800501e-15, 'shots': 2048}]) + +.. testcode:: + + print(result.values[0]) + +.. testoutput:: + + 3.999999998697238 + +Modify :class:`~qiskit.primitives.Estimator` options +----------------------------------------------------- + +If you want to keep some configuration values for several runs, it can be better to +change the :class:`~qiskit.primitives.Estimator` options. That way you can use the same +:class:`~qiskit.primitives.Estimator` object as many times as you wish without having to +rewrite the configuration values every time you use :meth:`~qiskit.primitives.Estimator.run`. + +Modify existing :class:`~qiskit.primitives.Estimator` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you prefer to change the options of an already-defined :class:`~qiskit.primitives.Estimator`, you can use +:meth:`~qiskit.primitives.Estimator.set_options` and introduce the new options as keyword arguments. + +.. testcode:: + + estimator.set_options(shots=2048, seed=123) + + job = estimator.run(qc, observable) + result = job.result() + print(result) + +.. testoutput:: + + EstimatorResult(values=array([4.]), metadata=[{'variance': 3.552713678800501e-15, 'shots': 2048}]) + +.. testcode:: + + print(result.values[0]) + +.. testoutput:: + + 3.999999998697238 + + +Define a new :class:`~qiskit.primitives.Estimator` with the options +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you prefer to define a new :class:`~qiskit.primitives.Estimator` with new options, you need to +define a ``dict`` like this one: + +.. testcode:: + + options = {"shots": 2048, "seed": 123} + +And then you can introduce it into your new :class:`~qiskit.primitives.Estimator` with the +``options`` argument. + +.. testcode:: + + estimator = Estimator(options=options) + + job = estimator.run(qc, observable) + result = job.result() + print(result) + +.. testoutput:: + + EstimatorResult(values=array([4.]), metadata=[{'variance': 3.552713678800501e-15, 'shots': 2048}]) + +.. testcode:: + + print(result.values[0]) + +.. testoutput:: + + 3.999999998697238 \ No newline at end of file diff --git a/docs/how_to/use_sampler.rst b/docs/how_to/use_sampler.rst new file mode 100644 index 000000000000..da7257c28fa1 --- /dev/null +++ b/docs/how_to/use_sampler.rst @@ -0,0 +1,255 @@ +############################################################### +Compute circuit output probabilities with ``Sampler`` primitive +############################################################### + +This guide shows how to get the probability distribution of a quantum circuit with the :class:`~qiskit.primitives.Sampler` primitive. + +.. note:: + + While this guide uses Qiskit’s reference implementation, the ``Sampler`` primitive can be run with any provider using :class:`~qiskit.primitives.BackendSampler`. + + .. code-block:: + + from qiskit.primitives import BackendSampler + from import QiskitProvider + + provider = QiskitProvider() + backend = provider.get_backend('backend_name') + sampler = BackendSampler(backend) + + There are some providers that implement primitives natively (see `this page `_ for more details). + +Initialize quantum circuits +=========================== + +The first step is to create the :class:`~qiskit.circuit.QuantumCircuit`\ s from which you want to obtain the probability distribution. + +.. plot:: + :include-source: + + from qiskit import QuantumCircuit + + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0,1) + qc.measure_all() + qc.draw("mpl") + +.. testsetup:: + + # This code is repeated (but hidden) because we will need to use the variables with the extension sphinx.ext.doctest (testsetup/testcode/testoutput directives) + # and we can't reuse the variables from the plot directive above because they are incompatible. + # The plot directive is used to draw the circuit with matplotlib and the code is shown because of the include-source flag. + + from qiskit import QuantumCircuit + + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0,1) + qc.measure_all() + +.. note:: + + The :class:`~qiskit.circuit.QuantumCircuit` you pass to :class:`~qiskit.primitives.Sampler` has to include measurements. + +Initialize the ``Sampler`` +========================== + +Then, you need to create a :class:`~qiskit.primitives.Sampler` instance. + +.. testcode:: + + from qiskit.primitives import Sampler + + sampler = Sampler() + +Run and get results +=================== + +Now that you have defined your ``sampler``, you can run it by calling the :meth:`~qiskit.primitives.Sampler.run` method, +which returns an instance of :class:`~.PrimitiveJob` (subclass of :class:`~qiskit.providers.JobV1`). You can get the results from the job (as a :class:`~qiskit.primitives.SamplerResult` object) +with the :meth:`~qiskit.providers.JobV1.result` method. + +.. testcode:: + + job = sampler.run(qc) + result = job.result() + print(result) + +.. testoutput:: + + SamplerResult(quasi_dists=[{0: 0.4999999999999999, 3: 0.4999999999999999}], metadata=[{}]) + +While this example only uses one :class:`~qiskit.circuit.QuantumCircuit`, if you want to sample multiple circuits you can +pass a ``list`` of :class:`~qiskit.circuit.QuantumCircuit` instances to the :meth:`~qiskit.primitives.Sampler.run` method. + +Get the probability distribution +-------------------------------- + +From these results you can extract the quasi-probability distributions with the attribute :attr:`~qiskit.primitives.SamplerResult.quasi_dists`. + +Even though there is only one circuit in this example, :attr:`~qiskit.primitives.SamplerResult.quasi_dists` returns a list of :class:`~qiskit.result.QuasiDistribution`\ s. +``result.quasi_dists[i]`` is the quasi-probability distribution of the ``i``-th circuit. + +.. note:: + + A quasi-probability distribution differs from a probability distribution in that negative values are also allowed. + However the quasi-probabilities must sum up to 1 like probabilities. + Negative quasi-probabilities may appear when using error mitigation techniques. + +.. testcode:: + + quasi_dist = result.quasi_dists[0] + print(quasi_dist) + +.. testoutput:: + + {0: 0.4999999999999999, 3: 0.4999999999999999} + +Probability distribution with binary outputs +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you prefer to see the output keys as binary strings instead of decimal numbers, you can use the +:meth:`~qiskit.result.QuasiDistribution.binary_probabilities` method. + +.. testcode:: + + print(quasi_dist.binary_probabilities()) + +.. testoutput:: + + {'00': 0.4999999999999999, '11': 0.4999999999999999} + +Parameterized circuit with ``Sampler`` +======================================== + +The :class:`~qiskit.primitives.Sampler` primitive can be run with unbound parameterized circuits like the one below. +You can also manually bind values to the parameters of the circuit and follow the steps +of the previous example. + +.. testcode:: + + from qiskit.circuit import Parameter + + theta = Parameter('θ') + param_qc = QuantumCircuit(2) + param_qc.ry(theta, 0) + param_qc.cx(0,1) + param_qc.measure_all() + print(param_qc.draw()) + +.. testoutput:: + + ┌───────┐ ░ ┌─┐ + q_0: ┤ Ry(θ) ├──■───░─┤M├─── + └───────┘┌─┴─┐ ░ └╥┘┌─┐ + q_1: ─────────┤ X ├─░──╫─┤M├ + └───┘ ░ ║ └╥┘ + meas: 2/══════════════════╩══╩═ + 0 1 + +The main difference from the previous case is that now you need to specify the sets of parameter values +for which you want to evaluate the expectation value as a ``list`` of ``list``\ s of ``float``\ s. +The ``i``-th element of the outer ``list`` is the set of parameter values +that corresponds to the ``i``-th circuit. + +.. testcode:: + + import numpy as np + + parameter_values = [[0], [np.pi/6], [np.pi/2]] + + job = sampler.run([param_qc]*3, parameter_values=parameter_values) + dists = job.result().quasi_dists + + for i in range(3): + print(f"Parameter: {parameter_values[i][0]:.5f}\t Probabilities: {dists[i]}") + +.. testoutput:: + + Parameter: 0.00000 Probabilities: {0: 1.0} + Parameter: 0.52360 Probabilities: {0: 0.9330127018922194, 3: 0.0669872981077807} + Parameter: 1.57080 Probabilities: {0: 0.5000000000000001, 3: 0.4999999999999999} + +Change run options +================== + +Your workflow might require tuning primitive run options, such as the amount of shots. + +By default, the reference :class:`~qiskit.primitives.Sampler` class performs an exact statevector +calculation based on the :class:`~qiskit.quantum_info.Statevector` class. However, this can be +modified to include shot noise if the number of ``shots`` is set. +For reproducibility purposes, a ``seed`` will also be set in the following examples. + +There are two main ways of setting options in the :class:`~qiskit.primitives.Sampler`: + +* Set keyword arguments in the :meth:`~qiskit.primitives.Sampler.run` method. +* Modify :class:`~qiskit.primitives.Sampler` options. + +Set keyword arguments for :meth:`~qiskit.primitives.Sampler.run` +---------------------------------------------------------------- + +If you only want to change the settings for a specific run, it can be more convenient to +set the options inside the :meth:`~qiskit.primitives.Sampler.run` method. You can do this by +passing them as keyword arguments. + +.. testcode:: + + job = sampler.run(qc, shots=2048, seed=123) + result = job.result() + print(result) + +.. testoutput:: + + SamplerResult(quasi_dists=[{0: 0.5205078125, 3: 0.4794921875}], metadata=[{'shots': 2048}]) + +Modify :class:`~qiskit.primitives.Sampler` options +--------------------------------------------------- + +If you want to keep some configuration values for several runs, it can be better to +change the :class:`~qiskit.primitives.Sampler` options. That way you can use the same +:class:`~qiskit.primitives.Sampler` object as many times as you wish without having to +rewrite the configuration values every time you use :meth:`~qiskit.primitives.Sampler.run`. + +Modify existing :class:`~qiskit.primitives.Sampler` +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you prefer to change the options of an already-defined :class:`~qiskit.primitives.Sampler`, you can use +:meth:`~qiskit.primitives.Sampler.set_options` and introduce the new options as keyword arguments. + +.. testcode:: + + sampler.set_options(shots=2048, seed=123) + + job = sampler.run(qc) + result = job.result() + print(result) + +.. testoutput:: + + SamplerResult(quasi_dists=[{0: 0.5205078125, 3: 0.4794921875}], metadata=[{'shots': 2048}]) + +Define a new :class:`~qiskit.primitives.Sampler` with the options +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +If you prefer to define a new :class:`~qiskit.primitives.Sampler` with new options, you need to +define a ``dict`` like this one: + +.. testcode:: + + options = {"shots": 2048, "seed": 123} + +And then you can introduce it into your new :class:`~qiskit.primitives.Sampler` with the +``options`` argument. + +.. testcode:: + + sampler = Sampler(options=options) + + job = sampler.run(qc) + result = job.result() + print(result) + +.. testoutput:: + + SamplerResult(quasi_dists=[{0: 0.5205078125, 3: 0.4794921875}], metadata=[{'shots': 2048}]) \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 5dd462fe2ca2..135d5364eee6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -6,6 +6,7 @@ Qiskit Terra documentation :maxdepth: 2 :hidden: + How-to Guides API References Migration Guides Release Notes From 81d38340d12f057f73c60d26723b05176c259289 Mon Sep 17 00:00:00 2001 From: "Vicente P. Soloviev" Date: Fri, 19 May 2023 17:23:28 +0200 Subject: [PATCH 108/172] Allow access to UDMA optimizer history information (#8695) * fix issue 8687 by moving history attribute tu public by a getter * callback function * tests * add test and reno * lint --------- Co-authored-by: Julien Gacon --- qiskit/algorithms/optimizers/umda.py | 20 ++++++++++++- .../notes/umda-callback-eb644a49c5a9ad37.yaml | 7 +++++ .../python/algorithms/optimizers/test_umda.py | 30 ++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/umda-callback-eb644a49c5a9ad37.yaml diff --git a/qiskit/algorithms/optimizers/umda.py b/qiskit/algorithms/optimizers/umda.py index 3e84d59b7ad8..dd8739c2fac1 100644 --- a/qiskit/algorithms/optimizers/umda.py +++ b/qiskit/algorithms/optimizers/umda.py @@ -11,6 +11,7 @@ # that they have been altered from the originals. """Univariate Marginal Distribution Algorithm (Estimation-of-Distribution-Algorithm).""" + from __future__ import annotations from collections.abc import Callable @@ -122,12 +123,21 @@ class UMDA(Optimizer): ELITE_FACTOR = 0.4 STD_BOUND = 0.3 - def __init__(self, maxiter: int = 100, size_gen: int = 20, alpha: float = 0.5) -> None: + def __init__( + self, + maxiter: int = 100, + size_gen: int = 20, + alpha: float = 0.5, + callback: Callable[[int, np.array, float], None] | None = None, + ) -> None: r""" Args: maxiter: Maximum number of iterations. size_gen: Population size of each generation. alpha: Percentage (0, 1] of the population to be selected as elite selection. + callback: A callback function passed information in each iteration step. The + information is, in this order: the number of function evaluations, the parameters, + the best function value in this iteration. """ self.size_gen = size_gen @@ -148,6 +158,8 @@ def __init__(self, maxiter: int = 100, size_gen: int = 20, alpha: float = 0.5) - self._n_variables: int | None = None + self.callback = callback + def _initialization(self) -> np.ndarray: vector = np.zeros((4, self._n_variables)) @@ -243,6 +255,11 @@ def minimize( if not_better_count >= self._dead_iter: break + if self.callback is not None: + self.callback( + len(history) * self._size_gen, self._best_ind_global, self._best_cost_global + ) + self._new_generation() result.x = self._best_ind_global @@ -321,6 +338,7 @@ def settings(self) -> dict[str, Any]: "maxiter": self.maxiter, "alpha": self.alpha, "size_gen": self.size_gen, + "callback": self.callback, } def get_support_level(self): diff --git a/releasenotes/notes/umda-callback-eb644a49c5a9ad37.yaml b/releasenotes/notes/umda-callback-eb644a49c5a9ad37.yaml new file mode 100644 index 000000000000..f79a13a164e4 --- /dev/null +++ b/releasenotes/notes/umda-callback-eb644a49c5a9ad37.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Added the option to pass a callback to the :class:`.UMDA` optimizer, which allows + to keep track of the number of function evaluations, the current parameters, and the + best achieved function value. + diff --git a/test/python/algorithms/optimizers/test_umda.py b/test/python/algorithms/optimizers/test_umda.py index b767f4781231..8d05c1cd2454 100644 --- a/test/python/algorithms/optimizers/test_umda.py +++ b/test/python/algorithms/optimizers/test_umda.py @@ -14,6 +14,8 @@ from test.python.algorithms import QiskitAlgorithmsTestCase +import numpy as np + from qiskit.algorithms.optimizers.umda import UMDA @@ -45,6 +47,7 @@ def test_settings(self): "maxiter": 100, "alpha": 0.6, "size_gen": 30, + "callback": None, } assert umda.settings == set_ @@ -52,7 +55,6 @@ def test_settings(self): def test_minimize(self): """optimize function test""" from scipy.optimize import rosen - import numpy as np from qiskit.utils import algorithm_globals # UMDA is volatile so we need to set the seeds for the execution @@ -66,3 +68,29 @@ def test_minimize(self): assert len(res.x) == len(x_0) np.testing.assert_array_almost_equal(res.x, [1.0] * len(x_0), decimal=2) + + def test_callback(self): + """Test the callback.""" + + def objective(x): + return np.linalg.norm(x) - 1 + + nfevs, parameters, fvals = [], [], [] + + def store_history(*args): + nfevs.append(args[0]) + parameters.append(args[1]) + fvals.append(args[2]) + + optimizer = UMDA(maxiter=1, callback=store_history) + _ = optimizer.minimize(objective, x0=np.arange(5)) + + self.assertEqual(len(nfevs), 1) + self.assertIsInstance(nfevs[0], int) + + self.assertEqual(len(parameters), 1) + self.assertIsInstance(parameters[0], np.ndarray) + self.assertEqual(parameters[0].size, 5) + + self.assertEqual(len(fvals), 1) + self.assertIsInstance(fvals[0], float) From c3b0afaab2a3e1331c6b99903a0fe02e8848ca15 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Fri, 19 May 2023 11:59:25 -0400 Subject: [PATCH 109/172] Deprecate get_vf2_call_limit in preset_passmanagers (#10065) * Deprecate get_vf2_call_limit in preset_passmanagers This has been replaced by get_vf2_limits * Black formatting * Add release note for #10065 --- qiskit/transpiler/preset_passmanagers/common.py | 5 +++++ .../deprecate-get_vf2_call_limit-826e0f9212fb27b9.yaml | 8 ++++++++ test/python/transpiler/test_preset_passmanagers.py | 6 ++++++ 3 files changed, 19 insertions(+) create mode 100644 releasenotes/notes/deprecate-get_vf2_call_limit-826e0f9212fb27b9.yaml diff --git a/qiskit/transpiler/preset_passmanagers/common.py b/qiskit/transpiler/preset_passmanagers/common.py index e801d04500fc..4645b4ca1385 100644 --- a/qiskit/transpiler/preset_passmanagers/common.py +++ b/qiskit/transpiler/preset_passmanagers/common.py @@ -17,6 +17,7 @@ from typing import Optional from qiskit.circuit.equivalence_library import SessionEquivalenceLibrary as sel +from qiskit.utils.deprecation import deprecate_func from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.passes import Error @@ -546,6 +547,10 @@ def _require_alignment(property_set): return scheduling +@deprecate_func( + additional_msg="Instead, use :func:`~qiskit.transpiler.preset_passmanagers.common.get_vf2_limits`.", + since="0.25.0", +) def get_vf2_call_limit( optimization_level: int, layout_method: Optional[str] = None, diff --git a/releasenotes/notes/deprecate-get_vf2_call_limit-826e0f9212fb27b9.yaml b/releasenotes/notes/deprecate-get_vf2_call_limit-826e0f9212fb27b9.yaml new file mode 100644 index 000000000000..686ffe004233 --- /dev/null +++ b/releasenotes/notes/deprecate-get_vf2_call_limit-826e0f9212fb27b9.yaml @@ -0,0 +1,8 @@ +--- +deprecations: + - | + The function ``get_vf2_call_limit`` available via the module + :mod:`qiskit.transpiler.preset_passmanagers.common` has been + deprecated. This will likely affect very few users since this function was + neither explicitly exported nor documented. Its functionality has been + replaced and extended by a function in the same module. diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index 55eba7eabb13..e1dc7091aa3d 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -19,6 +19,7 @@ import numpy as np +import qiskit from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister from qiskit.circuit import Qubit, Gate, ControlFlowOp, ForLoopOp from qiskit.compiler import transpile, assemble @@ -263,6 +264,11 @@ def counting_callback_func(pass_, dag, time, property_set, count): ) self.assertEqual(gates_in_basis_true_count + 1, collect_2q_blocks_count) + def test_get_vf2_call_limit_deprecated(self): + """Test that calling test_get_vf2_call_limit emits deprecation warning.""" + with self.assertWarns(DeprecationWarning): + qiskit.transpiler.preset_passmanagers.common.get_vf2_call_limit(optimization_level=3) + @ddt class TestTranspileLevels(QiskitTestCase): From bedecbdce563f4e83acc11c7ec5ca6878a36a4b7 Mon Sep 17 00:00:00 2001 From: Mirko Amico <31739486+miamico@users.noreply.github.com> Date: Fri, 19 May 2023 19:58:45 -0600 Subject: [PATCH 110/172] add gaussian square echo pulse (#9370) * add gaussian square echo pulse * address some PR comments * fixed parameters of symbolicpulse * adding math description and reference * change variable names * fix lint * remove width_echo parameter * fix lint * add release notes * fix conflicts * address some PR comments * fixed parameters of symbolicpulse * adding math description and reference * change variable names * fix lint * remove width_echo parameter * fix lint * add release notes * Update first paragraph Co-authored-by: Naoki Kanazawa * Update amp param to float Co-authored-by: Naoki Kanazawa * Update angle def param to float Co-authored-by: Naoki Kanazawa * Update amp constrain Co-authored-by: Naoki Kanazawa * drafting tests * update docstring * finish up test * fix missing pulses from init * addding Cos * missing sin * fixed tests * black formatting * break string * fixing strings * reformatting * fix lint * fixing last things * fix tests * fix black * fix another test * fix amp validation * fixing docstring * Update qiskit/pulse/library/symbolic_pulses.py Co-authored-by: Will Shanks * rename to gaussian_square_echo * fix lint * Update qiskit/qobj/converters/pulse_instruction.py Co-authored-by: Will Shanks * Update releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml Co-authored-by: Will Shanks --------- Co-authored-by: Naoki Kanazawa Co-authored-by: Will Shanks --- qiskit/pulse/__init__.py | 1 + qiskit/pulse/library/__init__.py | 2 + qiskit/pulse/library/symbolic_pulses.py | 201 ++++++++++++++++++ qiskit/qobj/converters/pulse_instruction.py | 1 + ...an-square-echo-pulse-84306f1a02e2bb28.yaml | 9 + test/python/pulse/test_pulse_lib.py | 106 +++++++++ 6 files changed, 320 insertions(+) create mode 100644 releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index d5432eaa7be7..5fe159b947fb 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -142,6 +142,7 @@ Gaussian, GaussianSquare, GaussianSquareDrag, + gaussian_square_echo, Sin, Cos, Sawtooth, diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index 8cd80a3b32c9..c7103b7a0d0a 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -88,6 +88,7 @@ Gaussian GaussianSquare GaussianSquareDrag + gaussian_square_echo Sin Cos Sawtooth @@ -117,6 +118,7 @@ Gaussian, GaussianSquare, GaussianSquareDrag, + gaussian_square_echo, Drag, Constant, Sin, diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 9102df2355e3..2718792ad153 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -1068,6 +1068,207 @@ def GaussianSquareDrag( return instance +def gaussian_square_echo( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + sigma: Union[float, ParameterExpression], + width: Optional[Union[float, ParameterExpression]] = None, + angle: Optional[Union[float, ParameterExpression]] = 0.0, + active_amp: Optional[Union[float, ParameterExpression]] = 0.0, + active_angle: Optional[Union[float, ParameterExpression]] = 0.0, + risefall_sigma_ratio: Optional[Union[float, ParameterExpression]] = None, + name: Optional[str] = None, + limit_amplitude: Optional[bool] = None, +) -> SymbolicPulse: + """An echoed Gaussian square pulse with an active tone overlaid on it. + + The Gaussian Square Echo pulse is composed of three pulses. First, a Gaussian Square pulse + :math:`f_{echo}(x)` with amplitude ``amp`` and phase ``angle`` playing for half duration, + followed by a second Gaussian Square pulse :math:`-f_{echo}(x)` with opposite amplitude + and same phase playing for the rest of the duration. Third a Gaussian Square pulse + :math:`f_{active}(x)` with amplitude ``active_amp`` and phase ``active_angle`` + playing for the entire duration. The Gaussian Square Echo pulse :math:`g_e()` + can be written as: + + .. math:: + + g_e(x) &= \\begin{cases}\ + f_{\\text{active}} + f_{\\text{echo}}(x)\ + & x < \\frac{\\text{duration}}{2}\\\\ + f_{\\text{active}} - f_{\\text{echo}}(x)\ + & \\frac{\\text{duration}}{2} < x\ + \\end{cases}\\\\ + + One case where this pulse can be used is when implementing a direct CNOT gate with + a cross-resonance superconducting qubit architecture. When applying this pulse to + the target qubit, the active portion can be used to cancel IX terms from the + cross-resonance drive while the echo portion can reduce the impact of a static ZZ coupling. + + Exactly one of the ``risefall_sigma_ratio`` and ``width`` parameters has to be specified. + + If ``risefall_sigma_ratio`` is not ``None`` and ``width`` is ``None``: + + .. math:: + + \\text{risefall} &= \\text{risefall_sigma_ratio} \\times \\text{sigma}\\\\ + \\text{width} &= \\text{duration} - 2 \\times \\text{risefall} + + If ``width`` is not None and ``risefall_sigma_ratio`` is None: + + .. math:: \\text{risefall} = \\frac{\\text{duration} - \\text{width}}{2} + + References: + 1. |citation1|_ + + .. _citation1: https://iopscience.iop.org/article/10.1088/2058-9565/abe519 + + .. |citation1| replace:: *Jurcevic, P., Javadi-Abhari, A., Bishop, L. S., + Lauer, I., Bogorin, D. F., Brink, M., Capelluto, L., G{\"u}nl{\"u}k, O., + Itoko, T., Kanazawa, N. & others + Demonstration of quantum volume 64 on a superconducting quantum + computing system. (Section V)* + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The amplitude of the rise and fall and of the echoed pulse. + sigma: A measure of how wide or narrow the risefall is; see the class + docstring for more details. + width: The duration of the embedded square pulse. + angle: The angle in radians of the complex phase factor uniformly + scaling the echoed pulse. Default value 0. + active_amp: The amplitude of the active pulse. + active_angle: The angle in radian of the complex phase factor uniformly + scaling the active pulse. Default value 0. + risefall_sigma_ratio: The ratio of each risefall duration to sigma. + name: Display name for this pulse envelope. + limit_amplitude: If ``True``, then limit the amplitude of the + waveform to 1. The default is ``True`` and the amplitude is constrained to 1. + Returns: + ScalableSymbolicPulse instance. + Raises: + PulseError: When width and risefall_sigma_ratio are both empty or both non-empty. + """ + # Convert risefall_sigma_ratio into width which is defined in OpenPulse spec + if width is None and risefall_sigma_ratio is None: + raise PulseError( + "Either the pulse width or the risefall_sigma_ratio parameter must be specified." + ) + if width is not None and risefall_sigma_ratio is not None: + raise PulseError( + "Either the pulse width or the risefall_sigma_ratio parameter can be specified" + " but not both." + ) + + if width is None and risefall_sigma_ratio is not None: + width = duration - 2.0 * risefall_sigma_ratio * sigma + + parameters = { + "amp": amp, + "angle": angle, + "sigma": sigma, + "width": width, + "active_amp": active_amp, + "active_angle": active_angle, + } + + # Prepare symbolic expressions + ( + _t, + _duration, + _amp, + _sigma, + _active_amp, + _width, + _angle, + _active_angle, + ) = sym.symbols("t, duration, amp, sigma, active_amp, width, angle, active_angle") + + # gaussian square echo for rotary tone + _center = _duration / 4 + + _width_echo = (_duration - 2 * (_duration - _width)) / 2 + + _sq_t0 = _center - _width_echo / 2 + _sq_t1 = _center + _width_echo / 2 + + _gaussian_ledge = _lifted_gaussian(_t, _sq_t0, -1, _sigma) + _gaussian_redge = _lifted_gaussian(_t, _sq_t1, _duration / 2 + 1, _sigma) + + envelope_expr_p = ( + _amp + * sym.exp(sym.I * _angle) + * sym.Piecewise( + (_gaussian_ledge, _t <= _sq_t0), + (_gaussian_redge, _t >= _sq_t1), + (1, True), + ) + ) + + _center_echo = _duration / 2 + _duration / 4 + + _sq_t0_echo = _center_echo - _width_echo / 2 + _sq_t1_echo = _center_echo + _width_echo / 2 + + _gaussian_ledge_echo = _lifted_gaussian(_t, _sq_t0_echo, _duration / 2 - 1, _sigma) + _gaussian_redge_echo = _lifted_gaussian(_t, _sq_t1_echo, _duration + 1, _sigma) + + envelope_expr_echo = ( + -1 + * _amp + * sym.exp(sym.I * _angle) + * sym.Piecewise( + (_gaussian_ledge_echo, _t <= _sq_t0_echo), + (_gaussian_redge_echo, _t >= _sq_t1_echo), + (1, True), + ) + ) + + envelope_expr = sym.Piecewise( + (envelope_expr_p, _t <= _duration / 2), (envelope_expr_echo, _t >= _duration / 2), (0, True) + ) + + # gaussian square for active cancellation tone + _center_active = _duration / 2 + + _sq_t0_active = _center_active - _width / 2 + _sq_t1_active = _center_active + _width / 2 + + _gaussian_ledge_active = _lifted_gaussian(_t, _sq_t0_active, -1, _sigma) + _gaussian_redge_active = _lifted_gaussian(_t, _sq_t1_active, _duration + 1, _sigma) + + envelope_expr_active = ( + _active_amp + * sym.exp(sym.I * _active_angle) + * sym.Piecewise( + (_gaussian_ledge_active, _t <= _sq_t0_active), + (_gaussian_redge_active, _t >= _sq_t1_active), + (1, True), + ) + ) + + envelop_expr_total = envelope_expr + envelope_expr_active + + consts_expr = sym.And( + _sigma > 0, _width >= 0, _duration >= _width, _duration / 2 >= _width_echo + ) + + # Check validity of amplitudes + valid_amp_conditions_expr = sym.And(sym.Abs(_amp) + sym.Abs(_active_amp) <= 1.0) + + instance = SymbolicPulse( + pulse_type="gaussian_square_echo", + duration=duration, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelop_expr_total, + constraints=consts_expr, + valid_amp_conditions=valid_amp_conditions_expr, + ) + instance.validate_parameters() + + return instance + + class Drag(metaclass=_PulseType): """The Derivative Removal by Adiabatic Gate (DRAG) pulse is a standard Gaussian pulse with an additional Gaussian derivative component and lifting applied. diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index 9d4635ad7e7b..a278d7cca3e1 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -43,6 +43,7 @@ class ParametricPulseShapes(Enum): gaussian = "Gaussian" gaussian_square = "GaussianSquare" gaussian_square_drag = "GaussianSquareDrag" + gaussian_square_echo = "gaussian_square_echo" drag = "Drag" constant = "Constant" diff --git a/releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml b/releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml new file mode 100644 index 000000000000..b733f6ba6228 --- /dev/null +++ b/releasenotes/notes/gaussian-square-echo-pulse-84306f1a02e2bb28.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Add new :meth:`~qiskit.pulse.gaussian_square_echo` pulse shape. This pulse + is composed by three :class:`~qiskit.pulse.GaussianSquare` pulses. The + first two are echo pulses with duration half of the total duration and + implement rotary tones. The third pulse is a cancellation tone that lasts + the full duration of the pulse and implements correcting single qubit + rotations. \ No newline at end of file diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index f5485762ddd9..a289ad018955 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -24,6 +24,7 @@ Gaussian, GaussianSquare, GaussianSquareDrag, + gaussian_square_echo, Drag, Sin, Cos, @@ -288,6 +289,65 @@ def test_gaussian_square_drag_validation(self): with self.assertRaises(PulseError): GaussianSquareDrag(duration=50, width=0, sigma=4, amp=0.8, beta=-20) + def test_gaussian_square_echo_pulse(self): + """Test that gaussian_square_echo sample pulse matches expectations. + + Test that the real part of the envelop matches GaussianSquare with + given amplitude and phase active for half duration with another + GaussianSquare active for the other half duration with opposite + amplitude and a GaussianSquare active on the entire duration with + its own amplitude and phase + """ + risefall = 32 + sigma = 4 + amp = 0.5 + width = 100 + duration = width + 2 * risefall + active_amp = 0.1 + width_echo = (duration - 2 * (duration - width)) / 2 + + gse = gaussian_square_echo( + duration=duration, sigma=sigma, amp=amp, width=width, active_amp=active_amp + ) + gse_samples = gse.get_waveform().samples + + gs_echo_pulse_pos = GaussianSquare( + duration=duration / 2, sigma=sigma, amp=amp, width=width_echo + ) + gs_echo_pulse_neg = GaussianSquare( + duration=duration / 2, sigma=sigma, amp=-amp, width=width_echo + ) + gs_active_pulse = GaussianSquare( + duration=duration, sigma=sigma, amp=active_amp, width=width + ) + gs_echo_pulse_pos_samples = np.array( + gs_echo_pulse_pos.get_waveform().samples.tolist() + [0] * int(duration / 2) + ) + gs_echo_pulse_neg_samples = np.array( + [0] * int(duration / 2) + gs_echo_pulse_neg.get_waveform().samples.tolist() + ) + gs_active_pulse_samples = gs_active_pulse.get_waveform().samples + + np.testing.assert_almost_equal( + gse_samples, + gs_echo_pulse_pos_samples + gs_echo_pulse_neg_samples + gs_active_pulse_samples, + ) + + def test_gaussian_square_echo_active_amp_validation(self): + """Test gaussian square echo active amp parameter validation.""" + + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=0.2) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=0.4) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.5, active_amp=0.3) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=-0.1, active_amp=0.2) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=-0.2) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=0.6) + gaussian_square_echo(duration=50, width=0, sigma=16, amp=-0.5, angle=1.5, active_amp=0.25) + with self.assertRaises(PulseError): + gaussian_square_echo(duration=50, width=0, sigma=16, amp=0.1, active_amp=1.1) + with self.assertRaises(PulseError): + gaussian_square_echo(duration=50, width=0, sigma=4, amp=-0.8, active_amp=-0.3) + def test_drag_pulse(self): """Test that the Drag sample pulse matches the pulse library.""" drag = Drag(duration=25, sigma=4, amp=0.5j, beta=1) @@ -451,6 +511,22 @@ def test_repr(self): repr(gsd), "GaussianSquareDrag(duration=20, sigma=30, width=14.0, beta=1, amp=1.0, angle=0.0)", ) + gse = gaussian_square_echo(duration=20, sigma=30, amp=1.0, width=3) + self.assertEqual( + repr(gse), + ( + "gaussian_square_echo(duration=20, amp=1.0, angle=0.0, sigma=30, width=3," + " active_amp=0.0, active_angle=0.0)" + ), + ) + gse = gaussian_square_echo(duration=20, sigma=30, amp=1.0, risefall_sigma_ratio=0.1) + self.assertEqual( + repr(gse), + ( + "gaussian_square_echo(duration=20, amp=1.0, angle=0.0, sigma=30, width=14.0," + " active_amp=0.0, active_angle=0.0)" + ), + ) drag = Drag(duration=5, amp=0.5, sigma=7, beta=1) self.assertEqual(repr(drag), "Drag(duration=5, sigma=7, beta=1, amp=0.5, angle=0)") const = Constant(duration=150, amp=0.1, angle=0.3) @@ -476,6 +552,17 @@ def test_param_validation(self): with self.assertRaises(PulseError): GaussianSquareDrag(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10, beta=1) + with self.assertRaises(PulseError): + gaussian_square_echo( + duration=150, + amp=0.2, + sigma=8, + ) + with self.assertRaises(PulseError): + gaussian_square_echo(duration=150, amp=0.2, sigma=8, width=160) + with self.assertRaises(PulseError): + gaussian_square_echo(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10) + with self.assertRaises(PulseError): Constant(duration=150, amp=0.9 + 0.8j) with self.assertRaises(PulseError): @@ -536,6 +623,25 @@ def test_gaussian_square_drag_limit_amplitude_per_instance(self): ) self.assertGreater(np.abs(waveform.amp), 1.0) + def test_gaussian_square_echo_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + gaussian_square_echo(duration=1000, sigma=4.0, amp=1.01, width=100) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = gaussian_square_echo(duration=100, sigma=1.0, amp=1.1, width=10) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_gaussian_square_echo_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + gaussian_square_echo(duration=1000, sigma=4.0, amp=1.01, width=100) + + waveform = gaussian_square_echo( + duration=1000, sigma=4.0, amp=1.01, width=100, limit_amplitude=False + ) + self.assertGreater(np.abs(waveform.amp), 1.0) + def test_drag_limit_amplitude(self): """Test that the check for amplitude less than or equal to 1 can be disabled.""" with self.assertRaises(PulseError): From 9f647c7e420e02f829cfee54303520e1abe11521 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 22 May 2023 03:01:06 -0400 Subject: [PATCH 111/172] Fix tweedledum runtime detection in BooleanExpression.from_dimacs_file (#10132) This commit fixes an oversight in the 0.24.0 release that caused an accidental change in the exception raised when attempting to use BooleanExpression.from_dimacs_file without having tweedledum installed. In #9754 the detection of tweedledum was updated to avoid import time detection so that the module can be imported even if tweedledum isn't installed. This was done through the use of the optionals decorators so that tweedledum is only attempted to be imported when the classicalfunction modules is used. However, the decorators don't wrap classmethod constructors by default and this caused the incorrect exception type to be raised. This commit fixes this by doing the runtime checking manually inside the from_dimacs_file constructor. --- qiskit/circuit/classicalfunction/boolean_expression.py | 1 + .../fix-exception-from_dimacs_file-b9338f3c913a9bff.yaml | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 releasenotes/notes/fix-exception-from_dimacs_file-b9338f3c913a9bff.yaml diff --git a/qiskit/circuit/classicalfunction/boolean_expression.py b/qiskit/circuit/classicalfunction/boolean_expression.py index 1ed820058b18..0f4a53494af4 100644 --- a/qiskit/circuit/classicalfunction/boolean_expression.py +++ b/qiskit/circuit/classicalfunction/boolean_expression.py @@ -110,6 +110,7 @@ def from_dimacs_file(cls, filename: str): Raises: FileNotFoundError: If filename is not found. """ + HAS_TWEEDLEDUM.require_now("BooleanExpression") from tweedledum import BoolFunction # pylint: disable=import-error diff --git a/releasenotes/notes/fix-exception-from_dimacs_file-b9338f3c913a9bff.yaml b/releasenotes/notes/fix-exception-from_dimacs_file-b9338f3c913a9bff.yaml new file mode 100644 index 000000000000..b7f2d34d5cda --- /dev/null +++ b/releasenotes/notes/fix-exception-from_dimacs_file-b9338f3c913a9bff.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an issue with the :meth:`.BooleanExpression.from_dimacs_file` + constructor method where the exception type raised when tweedledum wasn't + installed was not the expected :class:`~.MissingOptionalLibrary`. + Fixed `#10079 `__ From 17590b64b1927bd9842164eea59510c42f33e178 Mon Sep 17 00:00:00 2001 From: Julien Gacon Date: Mon, 22 May 2023 15:23:40 +0200 Subject: [PATCH 112/172] Replace `assert` in tests with unittest methods (#10136) * Replace `assert` with unittest methods * Update test/python/algorithms/optimizers/test_umda.py Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- .../python/algorithms/optimizers/test_umda.py | 20 +++++++++---------- test/python/circuit/library/test_nlocal.py | 3 ++- .../operators/symplectic/test_pauli_list.py | 2 +- test/python/result/test_quasi.py | 5 +++-- 4 files changed, 15 insertions(+), 15 deletions(-) diff --git a/test/python/algorithms/optimizers/test_umda.py b/test/python/algorithms/optimizers/test_umda.py index 8d05c1cd2454..e33e11bda60d 100644 --- a/test/python/algorithms/optimizers/test_umda.py +++ b/test/python/algorithms/optimizers/test_umda.py @@ -15,8 +15,10 @@ from test.python.algorithms import QiskitAlgorithmsTestCase import numpy as np +from scipy.optimize import rosen from qiskit.algorithms.optimizers.umda import UMDA +from qiskit.utils import algorithm_globals class TestUMDA(QiskitAlgorithmsTestCase): @@ -30,10 +32,10 @@ def test_get_set(self): umda.alpha = 0.6 umda.maxiter = 100 - assert umda.disp is True - assert umda.size_gen == 30 - assert umda.alpha == 0.6 - assert umda.maxiter == 100 + self.assertTrue(umda.disp) + self.assertEqual(umda.size_gen, 30) + self.assertEqual(umda.alpha, 0.6) + self.assertEqual(umda.maxiter, 100) def test_settings(self): """Test if the settings display works well""" @@ -50,13 +52,10 @@ def test_settings(self): "callback": None, } - assert umda.settings == set_ + self.assertEqual(umda.settings, set_) def test_minimize(self): """optimize function test""" - from scipy.optimize import rosen - from qiskit.utils import algorithm_globals - # UMDA is volatile so we need to set the seeds for the execution algorithm_globals.random_seed = 52 @@ -64,9 +63,8 @@ def test_minimize(self): x_0 = [1.3, 0.7, 1.5] res = optimizer.minimize(rosen, x_0) - assert res.fun is not None - assert len(res.x) == len(x_0) - + self.assertIsNotNone(res.fun) + self.assertEqual(len(res.x), len(x_0)) np.testing.assert_array_almost_equal(res.x, [1.0] * len(x_0), decimal=2) def test_callback(self): diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py index 5c77bac358aa..f20377368ed4 100644 --- a/test/python/circuit/library/test_nlocal.py +++ b/test/python/circuit/library/test_nlocal.py @@ -913,7 +913,8 @@ def test_full_vs_reverse_linear(self, num_qubits): reverse = RealAmplitudes(num_qubits=num_qubits, entanglement="reverse_linear", reps=reps) full.assign_parameters(params, inplace=True) reverse.assign_parameters(params, inplace=True) - assert Operator(full) == Operator(reverse) + + self.assertEqual(Operator(full), Operator(reverse)) if __name__ == "__main__": 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 28e4d30b215f..379111df5eaa 100644 --- a/test/python/quantum_info/operators/symplectic/test_pauli_list.py +++ b/test/python/quantum_info/operators/symplectic/test_pauli_list.py @@ -2106,8 +2106,8 @@ def qubitwise_commutes(left: Pauli, right: Pauli) -> bool: # checking that every input Pauli in pauli_list is in a group in the ouput output_labels = [pauli.to_label() for group in groups for pauli in group] - # assert sorted(output_labels) == sorted(input_labels) self.assertListEqual(sorted(output_labels), sorted(input_labels)) + # Within each group, every operator qubit-wise commutes with every other operator. for group in groups: self.assertTrue( diff --git a/test/python/result/test_quasi.py b/test/python/result/test_quasi.py index 3c9b7d4c59a8..e124c569a81f 100644 --- a/test/python/result/test_quasi.py +++ b/test/python/result/test_quasi.py @@ -191,9 +191,10 @@ def test_known_quasi_conversion(self): ans = {0: 9 / 20, 1: 7 / 20, 2: 1 / 5} # Check probs are correct for key, val in closest.items(): - assert abs(ans[key] - val) < 1e-14 + self.assertAlmostEqual(ans[key], val, places=14) + # Check if distance calculation is correct - assert abs(dist - sqrt(0.38)) < 1e-14 + self.assertAlmostEqual(dist, sqrt(0.38), places=14) def test_marginal_distribution(self): """Test marginal_distribution with float value.""" From 7b677edf153a78d2765c05ee87a8ded0201bc3b2 Mon Sep 17 00:00:00 2001 From: Adenilton Silva <7927558+adjs@users.noreply.github.com> Date: Mon, 22 May 2023 10:49:37 -0300 Subject: [PATCH 113/172] Fix wrong relative phase of MCRZ (#9836) * efficient multicontrolled su2 gate decomposition Co-authored-by: thiagom123 Co-authored-by: IsmaelCesar Co-authored-by: Israel F. Araujo Co-authored-by: Adenilton Silva <7927558+adjs@users.noreply.github.com> * removed optimization flag Co-authored-by: thiagom123 Co-authored-by: IsmaelCesar Co-authored-by: Israel F. Araujo Co-authored-by: Adenilton Silva <7927558+adjs@users.noreply.github.com> * tox -eblack * updated docstrings * Adds `MCSU2Gate` to `__init__` * fixed circular import * defined control and inverse methods * changed MCSU2Gate from Gate to ControlledGate * adjusted some tests for controlled gates * reformatting * Fix regarding the integer `ctrl_state` parameter * Tests to check the CX count upper bound * Gate's `label` in the `control` function * Upd. Qiskit tests to include cases for MCSU2Gate * Upd. Qiskit tests to include cases for MCSU2Gate * Revert "Upd. Qiskit tests to include cases for MCSU2Gate" This reverts commit c1ceaf6b28830caa5709cdb6e8d4db209acc8c84. * Revert "Upd. Qiskit tests to include cases for MCSU2Gate" This reverts commit 7c756111a31e22e6c7d7aba0a3df2584fd00817f. * Revert "Tests to check the CX count upper bound" This reverts commit 100a690c3a83556fd280cc4ede9e2c0f5d838455. * Update test_controlled_gate.py * Update test_circuit_operations.py * remove mcsu2gate class * remove mcsu2gate class * fix mcry * lint * fix mcrx * add reference * Create `s_gate` directly * Revert "Create `s_gate` directly" This reverts commit b762b393428f8c6889a055a3217779d84e8754bf. * review * release notes * review 2 * backwards compat * function signature and number of controls * fix mcrz * Update multi_control_rotation_gates.py * review * Update test_qpy.py * Revert "Update test_qpy.py" This reverts commit fab1c80ab44be86812fb2b95b538918400966f7e. * Update test_qpy.py * Update multi_control_rotation_gates.py * Fix `use_basis_gates=True` case * lint --------- Co-authored-by: rafaella-vale <26910380+rafaella-vale@users.noreply.github.com> Co-authored-by: thiagom123 Co-authored-by: IsmaelCesar Co-authored-by: Israel F. Araujo Co-authored-by: Rafaella Vale Co-authored-by: Julien Gacon --- qiskit/circuit/add_control.py | 13 +- .../multi_control_rotation_gates.py | 112 ++++++++++++------ ...-mcrz-relative-phase-6ea81a369f8bda38.yaml | 7 ++ test/python/circuit/test_controlled_gate.py | 10 +- test/qpy_compat/test_qpy.py | 7 +- 5 files changed, 97 insertions(+), 52 deletions(-) create mode 100644 releasenotes/notes/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml diff --git a/qiskit/circuit/add_control.py b/qiskit/circuit/add_control.py index 39a91eba36ef..9a9837673d1a 100644 --- a/qiskit/circuit/add_control.py +++ b/qiskit/circuit/add_control.py @@ -148,6 +148,7 @@ def control( q_target[bit_indices[qargs[0]]], use_basis_gates=True, ) + continue elif gate.name == "p": from qiskit.circuit.library import MCPhaseGate @@ -184,13 +185,9 @@ def control( use_basis_gates=True, ) elif theta == 0 and phi == 0: - controlled_circ.mcrz( - lamb, q_control, q_target[bit_indices[qargs[0]]], use_basis_gates=True - ) + controlled_circ.mcp(lamb, q_control, q_target[bit_indices[qargs[0]]]) else: - controlled_circ.mcrz( - lamb, q_control, q_target[bit_indices[qargs[0]]], use_basis_gates=True - ) + controlled_circ.mcp(lamb, q_control, q_target[bit_indices[qargs[0]]]) controlled_circ.mcry( theta, q_control, @@ -198,9 +195,7 @@ def control( q_ancillae, use_basis_gates=True, ) - controlled_circ.mcrz( - phi, q_control, q_target[bit_indices[qargs[0]]], use_basis_gates=True - ) + controlled_circ.mcp(phi, q_control, q_target[bit_indices[qargs[0]]]) elif gate.name == "z": controlled_circ.h(q_target[bit_indices[qargs[0]]]) controlled_circ.mcx(q_control, q_target[bit_indices[qargs[0]]], q_ancillae) diff --git a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py index 547883a6f7a9..ce7f7861440b 100644 --- a/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py +++ b/qiskit/circuit/library/standard_gates/multi_control_rotation_gates.py @@ -84,36 +84,44 @@ def _apply_mcu_graycode(circuit, theta, phi, lam, ctls, tgt, use_basis_gates): def _mcsu2_real_diagonal( - circuit, unitary: np.ndarray, - controls: Union[QuantumRegister, List[Qubit]], - target: Union[Qubit, int], - ctrl_state: str = None, -): + num_controls: int, + ctrl_state: Optional[str] = None, + use_basis_gates: bool = False, +) -> QuantumCircuit: """ - Apply multi-controlled SU(2) gate with a real main diagonal or secondary diagonal. - https://arxiv.org/abs/2302.06377 + Return a multi-controlled SU(2) gate [1]_ with a real main diagonal or secondary diagonal. Args: - circuit (QuantumCircuit): The QuantumCircuit object to apply the diagonal operator on. - unitary (ndarray): SU(2) unitary matrix with one real diagonal - controls (QuantumRegister or list(Qubit)): The list of control qubits - target (Qubit or int): The target qubit - ctrl_state (str): control state of the operator SU(2) operator + unitary: SU(2) unitary matrix with one real diagonal. + num_controls: The number of control qubits. + ctrl_state: The state on which the SU(2) operation is controlled. Defaults to all + control qubits being in state 1. + use_basis_gates: If ``True``, use ``[p, u, cx]`` gates to implement the decomposition. + + Returns: + A :class:`.QuantumCircuit` implementing the multi-controlled SU(2) gate. Raises: - QiskitError: parameter errors + QiskitError: If the input matrix is invalid. + + References: + + .. [1]: R. Vale et al. Decomposition of Multi-controlled Special Unitary Single-Qubit Gates + `arXiv:2302.06377 (2023) `__ + """ # pylint: disable=cyclic-import - from qiskit.circuit.library import MCXVChain + from .x import MCXVChain from qiskit.extensions import UnitaryGate from qiskit.quantum_info.operators.predicates import is_unitary_matrix - - if not is_unitary_matrix(unitary): - raise QiskitError("parameter unitary in mcsu2_real_diagonal must be an unitary matrix") + from qiskit.compiler import transpile if unitary.shape != (2, 2): - raise QiskitError("parameter unitary in mcsu2_real_diagonal must be a 2x2 matrix") + raise QiskitError(f"The unitary must be a 2x2 matrix, but has shape {unitary.shape}.") + + if not is_unitary_matrix(unitary): + raise QiskitError(f"The unitary in must be an unitary matrix, but is {unitary}.") is_main_diag_real = np.isclose(unitary[0, 0].imag, 0.0) and np.isclose(unitary[1, 1].imag, 0.0) is_secondary_diag_real = np.isclose(unitary[0, 1].imag, 0.0) and np.isclose( @@ -121,7 +129,7 @@ def _mcsu2_real_diagonal( ) if not is_main_diag_real and not is_secondary_diag_real: - raise QiskitError("parameter unitary in mcsu2_real_diagonal must have one real diagonal") + raise QiskitError("The unitary must have one real diagonal.") if is_secondary_diag_real: x = unitary[0, 1] @@ -130,27 +138,34 @@ def _mcsu2_real_diagonal( x = -unitary[0, 1].real z = unitary[1, 1] - unitary[0, 1].imag * 1.0j - alpha_r = np.sqrt((np.sqrt((z.real + 1.0) / 2.0) + 1.0) / 2.0) - alpha_i = z.imag / (2.0 * np.sqrt((z.real + 1.0) * (np.sqrt((z.real + 1.0) / 2.0) + 1.0))) - alpha = alpha_r + 1.0j * alpha_i - beta = x / (2.0 * np.sqrt((z.real + 1.0) * (np.sqrt((z.real + 1.0) / 2.0) + 1.0))) + if np.isclose(z, -1): + s_op = [[1.0, 0.0], [0.0, 1.0j]] + else: + alpha_r = np.sqrt((np.sqrt((z.real + 1.0) / 2.0) + 1.0) / 2.0) + alpha_i = z.imag / (2.0 * np.sqrt((z.real + 1.0) * (np.sqrt((z.real + 1.0) / 2.0) + 1.0))) + alpha = alpha_r + 1.0j * alpha_i + beta = x / (2.0 * np.sqrt((z.real + 1.0) * (np.sqrt((z.real + 1.0) / 2.0) + 1.0))) + + # S gate definition + s_op = np.array([[alpha, -np.conj(beta)], [beta, np.conj(alpha)]]) - # S gate definition - s_op = np.array([[alpha, -np.conj(beta)], [beta, np.conj(alpha)]]) s_gate = UnitaryGate(s_op) - num_ctrl = len(controls) - k_1 = int(np.ceil(num_ctrl / 2.0)) - k_2 = int(np.floor(num_ctrl / 2.0)) + k_1 = int(np.ceil(num_controls / 2.0)) + k_2 = int(np.floor(num_controls / 2.0)) ctrl_state_k_1 = None ctrl_state_k_2 = None if ctrl_state is not None: - str_ctrl_state = f"{ctrl_state:0{num_ctrl}b}" + str_ctrl_state = f"{ctrl_state:0{num_controls}b}" ctrl_state_k_1 = str_ctrl_state[::-1][:k_1][::-1] ctrl_state_k_2 = str_ctrl_state[::-1][k_1:][::-1] + circuit = QuantumCircuit(num_controls + 1, name="MCSU2") + controls = list(range(num_controls)) # control indices, defined for code legibility + target = num_controls # target index, defined for code legibility + if not is_secondary_diag_real: circuit.h(target) @@ -178,6 +193,11 @@ def _mcsu2_real_diagonal( if not is_secondary_diag_real: circuit.h(target) + if use_basis_gates: + circuit = transpile(circuit, basis_gates=["p", "u", "cx"]) + + return circuit + def mcrx( self, @@ -232,7 +252,12 @@ def mcrx( use_basis_gates=use_basis_gates, ) else: - _mcsu2_real_diagonal(self, RXGate(theta).to_matrix(), control_qubits, target_qubit) + cgate = _mcsu2_real_diagonal( + RXGate(theta).to_matrix(), + num_controls=len(control_qubits), + use_basis_gates=use_basis_gates, + ) + self.compose(cgate, control_qubits + [target_qubit], inplace=True) def mcry( @@ -302,7 +327,12 @@ def mcry( use_basis_gates=use_basis_gates, ) else: - _mcsu2_real_diagonal(self, RYGate(theta).to_matrix(), control_qubits, target_qubit) + cgate = _mcsu2_real_diagonal( + RYGate(theta).to_matrix(), + num_controls=len(control_qubits), + use_basis_gates=use_basis_gates, + ) + self.compose(cgate, control_qubits + [target_qubit], inplace=True) else: raise QiskitError(f"Unrecognized mode for building MCRY circuit: {mode}.") @@ -327,6 +357,8 @@ def mcrz( Raises: QiskitError: parameter errors """ + from .rz import CRZGate, RZGate + control_qubits = self.qbit_argument_conversion(q_controls) target_qubit = self.qbit_argument_conversion(q_target) if len(target_qubit) != 1: @@ -336,13 +368,21 @@ def mcrz( self._check_dups(all_qubits) n_c = len(control_qubits) - if n_c == 1: # cu - _apply_cu(self, 0, 0, lam, control_qubits[0], target_qubit, use_basis_gates=use_basis_gates) + if n_c == 1: + if use_basis_gates: + self.u(0, 0, lam / 2, target_qubit) + self.cx(control_qubits[0], target_qubit) + self.u(0, 0, -lam / 2, target_qubit) + self.cx(control_qubits[0], target_qubit) + else: + self.append(CRZGate(lam), control_qubits + [target_qubit]) else: - lam_step = lam * (1 / (2 ** (n_c - 1))) - _apply_mcu_graycode( - self, 0, 0, lam_step, control_qubits, target_qubit, use_basis_gates=use_basis_gates + cgate = _mcsu2_real_diagonal( + RZGate(lam).to_matrix(), + num_controls=len(control_qubits), + use_basis_gates=use_basis_gates, ) + self.compose(cgate, control_qubits + [target_qubit], inplace=True) QuantumCircuit.mcrx = mcrx diff --git a/releasenotes/notes/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml b/releasenotes/notes/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml new file mode 100644 index 000000000000..aa89abeb662d --- /dev/null +++ b/releasenotes/notes/fix-mcrz-relative-phase-6ea81a369f8bda38.yaml @@ -0,0 +1,7 @@ +--- +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`. diff --git a/test/python/circuit/test_controlled_gate.py b/test/python/circuit/test_controlled_gate.py index a40f7747e2e7..248aabab7839 100644 --- a/test/python/circuit/test_controlled_gate.py +++ b/test/python/circuit/test_controlled_gate.py @@ -610,9 +610,8 @@ def test_mcsu2_real_diagonal(self): """Test mcsu2_real_diagonal""" num_ctrls = 6 theta = 0.3 - qc = QuantumCircuit(num_ctrls + 1) ry_matrix = RYGate(theta).to_matrix() - _mcsu2_real_diagonal(qc, ry_matrix, list(range(num_ctrls)), num_ctrls) + qc = _mcsu2_real_diagonal(ry_matrix, num_ctrls) mcry_matrix = _compute_control_matrix(ry_matrix, 6) self.assertTrue(np.allclose(mcry_matrix, Operator(qc).to_matrix())) @@ -657,6 +656,11 @@ def test_multi_controlled_rotation_gate_matrices( if bit == "0": qc.x(q_controls[idx]) + if use_basis_gates: + with self.subTest(msg="check only basis gates used"): + gates_used = set(qc.count_ops().keys()) + self.assertTrue(gates_used.issubset({"x", "u", "p", "cx"})) + backend = BasicAer.get_backend("unitary_simulator") simulated = execute(qc, backend).result().get_unitary(qc) @@ -665,7 +669,7 @@ def test_multi_controlled_rotation_gate_matrices( elif base_gate_name == "y": rot_mat = RYGate(theta).to_matrix() else: # case 'z' - rot_mat = U1Gate(theta).to_matrix() + rot_mat = RZGate(theta).to_matrix() expected = _compute_control_matrix(rot_mat, num_controls, ctrl_state=ctrl_state) with self.subTest(msg=f"control state = {ctrl_state}"): diff --git a/test/qpy_compat/test_qpy.py b/test/qpy_compat/test_qpy.py index ef01d3873e39..2a4b112e1508 100755 --- a/test/qpy_compat/test_qpy.py +++ b/test/qpy_compat/test_qpy.py @@ -603,15 +603,14 @@ def generate_circuits(version_parts): if version_parts >= (0, 19, 2): output_circuits["control_flow.qpy"] = generate_control_flow_circuits() if version_parts >= (0, 21, 0): - output_circuits["controlled_gates.qpy"] = generate_controlled_gates() output_circuits["schedule_blocks.qpy"] = generate_schedule_blocks() output_circuits["pulse_gates.qpy"] = generate_calibrated_circuits() - if version_parts >= (0, 21, 2): - output_circuits["open_controlled_gates.qpy"] = generate_open_controlled_gates() if version_parts >= (0, 24, 0): output_circuits["referenced_schedule_blocks.qpy"] = generate_referenced_schedule() output_circuits["control_flow_switch.qpy"] = generate_control_flow_switch_circuits() - + if version_parts >= (0, 25, 0): + output_circuits["open_controlled_gates.qpy"] = generate_open_controlled_gates() + output_circuits["controlled_gates.qpy"] = generate_controlled_gates() return output_circuits From e1056ee62c895b33406f69823fb5073cfdcba9bd Mon Sep 17 00:00:00 2001 From: Evgenii Zheltonozhskii Date: Mon, 22 May 2023 22:21:27 +0300 Subject: [PATCH 114/172] Fix mypy errors (transpiler) (#8266) * Fix transpiler mypy errors * Fix review comments, add a bit more annotations * Fix annotation * Fix sphinx * Fix lint * Update dynamical_decoupling.py * Replace deprecated function call * Update qiskit/transpiler/passes/layout/disjoint_utils.py Co-authored-by: Luciano Bello * Fix type definition --------- Co-authored-by: Luciano Bello --- qiskit/transpiler/instruction_durations.py | 30 ++++++----- qiskit/transpiler/layout.py | 9 ++-- .../passes/calibration/rzx_builder.py | 15 +++--- .../passes/layout/disjoint_utils.py | 22 +++++--- .../routing/algorithms/token_swapper.py | 11 ++-- .../passes/routing/algorithms/util.py | 11 ++-- .../commuting_2q_block.py | 8 +-- .../commuting_2q_gate_router.py | 38 ++++++++------ .../swap_strategy.py | 32 ++++++------ .../passes/routing/layout_transformation.py | 10 ++-- .../scheduling/alignments/align_measures.py | 21 +++++--- .../scheduling/alignments/reschedule.py | 6 +-- .../passes/scheduling/padding/base_padding.py | 7 +-- .../padding/dynamical_decoupling.py | 14 ++--- .../passes/synthesis/unitary_synthesis.py | 16 +++--- qiskit/transpiler/passmanager.py | 52 +++++++++---------- .../transpiler/preset_passmanagers/level0.py | 3 +- .../transpiler/preset_passmanagers/level1.py | 13 ++--- .../transpiler/preset_passmanagers/level2.py | 13 ++--- .../transpiler/preset_passmanagers/level3.py | 16 +++--- qiskit/transpiler/runningpassmanager.py | 10 ++-- .../transpiler/synthesis/aqc/approximate.py | 8 +-- .../synthesis/aqc/cnot_unit_circuit.py | 4 +- .../synthesis/aqc/cnot_unit_objective.py | 18 ++++--- .../aqc/fast_gradient/fast_grad_utils.py | 4 +- .../synthesis/aqc/fast_gradient/layer.py | 10 ++-- qiskit/transpiler/target.py | 42 +++++++-------- 27 files changed, 239 insertions(+), 204 deletions(-) diff --git a/qiskit/transpiler/instruction_durations.py b/qiskit/transpiler/instruction_durations.py index a56a39b97ddf..d1611840a040 100644 --- a/qiskit/transpiler/instruction_durations.py +++ b/qiskit/transpiler/instruction_durations.py @@ -11,8 +11,10 @@ # that they have been altered from the originals. """Durations of instructions, one of transpiler configurations.""" -from typing import Optional, List, Tuple, Union, Iterable, Set +from __future__ import annotations +from typing import Optional, List, Tuple, Union, Iterable +import qiskit.circuit from qiskit.circuit import Barrier, Delay from qiskit.circuit import Instruction, Qubit, ParameterExpression from qiskit.circuit.duration import duration_in_dt @@ -22,7 +24,7 @@ from qiskit.utils.units import apply_prefix -def _is_deprecated_qubits_argument(qubits: Union[int, List[int], Qubit, List[Qubit]]) -> bool: +def _is_deprecated_qubits_argument(qubits: Union[int, list[int], Qubit, list[Qubit]]) -> bool: if isinstance(qubits, (int, Qubit)): qubits = [qubits] return isinstance(qubits[0], Qubit) @@ -41,11 +43,13 @@ class InstructionDurations: """ def __init__( - self, instruction_durations: Optional["InstructionDurationsType"] = None, dt: float = None + self, instruction_durations: "InstructionDurationsType" | None = None, dt: float = None ): - self.duration_by_name = {} - self.duration_by_name_qubits = {} - self.duration_by_name_qubits_params = {} + self.duration_by_name: dict[str, tuple[float, str]] = {} + self.duration_by_name_qubits: dict[tuple[str, tuple[int, ...]], tuple[float, str]] = {} + self.duration_by_name_qubits_params: dict[ + tuple[str, tuple[int, ...], tuple[float, ...]], tuple[float, str] + ] = {} self.dt = dt if instruction_durations: self.update(instruction_durations) @@ -99,7 +103,7 @@ def from_backend(cls, backend: Backend): return InstructionDurations(instruction_durations, dt=dt) - def update(self, inst_durations: Optional["InstructionDurationsType"], dt: float = None): + def update(self, inst_durations: "InstructionDurationsType" | None, dt: float = None): """Update self with inst_durations (inst_durations overwrite self). Args: @@ -177,10 +181,10 @@ def update(self, inst_durations: Optional["InstructionDurationsType"], dt: float ) def get( self, - inst: Union[str, Instruction], - qubits: Union[int, List[int], Qubit, List[Qubit]], + inst: str | qiskit.circuit.Instruction, + qubits: int | list[int] | Qubit | list[Qubit] | list[int | Qubit], unit: str = "dt", - parameters: Optional[List[float]] = None, + parameters: list[float] | None = None, ) -> float: """Get the duration of the instruction with the name, qubits, and parameters. @@ -224,9 +228,9 @@ def get( def _get( self, name: str, - qubits: List[int], + qubits: list[int], to_unit: str, - parameters: Optional[Iterable[float]] = None, + parameters: Iterable[float] | None = None, ) -> float: """Get the duration of the instruction with the name, qubits, and parameters.""" if name == "barrier": @@ -270,7 +274,7 @@ def _convert_unit(self, duration: float, from_unit: str, to_unit: str) -> float: else: raise TranspilerError(f"Conversion from '{from_unit}' to '{to_unit}' is not supported") - def units_used(self) -> Set[str]: + def units_used(self) -> set[str]: """Get the set of all units used in this instruction durations. Returns: diff --git a/qiskit/transpiler/layout.py b/qiskit/transpiler/layout.py index 56f8f7f50954..0e318e6a8db1 100644 --- a/qiskit/transpiler/layout.py +++ b/qiskit/transpiler/layout.py @@ -17,9 +17,8 @@ Virtual (qu)bits are tuples, e.g. `(QuantumRegister(3, 'qr'), 2)` or simply `qr[2]`. Physical (qu)bits are integers. """ - +from __future__ import annotations from dataclasses import dataclass -from typing import Dict, Optional from qiskit.circuit.quantumregister import Qubit, QuantumRegister from qiskit.transpiler.exceptions import LayoutError @@ -262,7 +261,7 @@ def combine_into_edge_map(self, another_layout): return edge_map - def reorder_bits(self, bits): + def reorder_bits(self, bits) -> list[int]: """Given an ordered list of bits, reorder them according to this layout. The list of bits must exactly match the virtual bits in this layout. @@ -401,5 +400,5 @@ class TranspileLayout: """ initial_layout: Layout - input_qubit_mapping: Dict[Qubit, int] - final_layout: Optional[Layout] = None + input_qubit_mapping: dict[Qubit, int] + final_layout: Layout | None = None diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py index 0b519a5a375a..97799994fd08 100644 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -11,11 +11,12 @@ # that they have been altered from the originals. """RZX calibration builders.""" +from __future__ import annotations import enum import warnings +from collections.abc import Sequence from math import pi, erf -from typing import List, Tuple, Union import numpy as np from qiskit.circuit import Instruction as CircuitInst @@ -68,7 +69,7 @@ class RZXCalibrationBuilder(CalibrationBuilder): def __init__( self, instruction_schedule_map: InstructionScheduleMap = None, - qubit_channel_mapping: List[List[str]] = None, + qubit_channel_mapping: list[list[str]] = None, verbose: bool = True, target: Target = None, ): @@ -97,7 +98,7 @@ def __init__( if self._inst_map is None: raise QiskitError("Calibrations can only be added to Pulse-enabled backends") - def supported(self, node_op: CircuitInst, qubits: List) -> bool: + def supported(self, node_op: CircuitInst, qubits: list) -> bool: """Determine if a given node supports the calibration. Args: @@ -166,7 +167,7 @@ def rescale_cr_inst(instruction: Play, theta: float, sample_mult: int = 16) -> i return round_duration - def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: + def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | ScheduleBlock: """Builds the calibration schedule for the RZXGate(theta) with echos. Args: @@ -260,7 +261,7 @@ class RZXCalibrationBuilderNoEcho(RZXCalibrationBuilder): of the CX gate. """ - def get_calibration(self, node_op: CircuitInst, qubits: List) -> Union[Schedule, ScheduleBlock]: + def get_calibration(self, node_op: CircuitInst, qubits: list) -> Schedule | ScheduleBlock: """Builds the calibration schedule for the RZXGate(theta) without echos. Args: @@ -336,8 +337,8 @@ def _filter_comp_tone(time_inst_tup): def _check_calibration_type( - inst_sched_map: InstructionScheduleMap, qubits: List[int] -) -> Tuple[CRCalType, List[Play], List[Play]]: + inst_sched_map: InstructionScheduleMap, qubits: Sequence[int] +) -> tuple[CRCalType, list[Play], list[Play]]: """A helper function to check type of CR calibration. Args: diff --git a/qiskit/transpiler/passes/layout/disjoint_utils.py b/qiskit/transpiler/passes/layout/disjoint_utils.py index 2afef62955a3..4a7975200fbd 100644 --- a/qiskit/transpiler/passes/layout/disjoint_utils.py +++ b/qiskit/transpiler/passes/layout/disjoint_utils.py @@ -11,12 +11,13 @@ # that they have been altered from the originals. """This module contains common utils for disjoint coupling maps.""" - +from __future__ import annotations from collections import defaultdict from typing import List, Callable, TypeVar, Dict, Union import uuid import rustworkx as rx +from qiskit.dagcircuit import DAGOpNode from qiskit.circuit import Qubit, Barrier, Clbit from qiskit.dagcircuit.dagcircuit import DAGCircuit @@ -65,7 +66,7 @@ def run_pass_over_connected_components( for qreg in dag.qregs: out_dag.add_qreg(qreg) for creg in dag.cregs: - out_dag.add_cregs(creg) + out_dag.add_creg(creg) out_dag.compose(dag, qubits=dag.qubits, clbits=dag.clbits) out_component_pairs.append((out_dag, cmap_components[cmap_index])) res = [run_func(out_dag, cmap) for out_dag, cmap in out_component_pairs] @@ -119,7 +120,7 @@ def split_barriers(dag: DAGCircuit): def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True): """Mutate input dag to combine barriers with UUID labels into a single barrier.""" qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} - uuid_map = {} + uuid_map: dict[uuid.UUID, DAGOpNode] = {} for node in dag.op_nodes(Barrier): if isinstance(node.op.label, uuid.UUID): barrier_uuid = node.op.label @@ -139,9 +140,18 @@ def combine_barriers(dag: DAGCircuit, retain_uuid: bool = True): def require_layout_isolated_to_component( dag: DAGCircuit, components_source: Union[Target, CouplingMap] -) -> bool: - """Check that the layout of the dag does not require connectivity across connected components - in the CouplingMap""" +): + """ + Check that the layout of the dag does not require connectivity across connected components + in the CouplingMap + + Args: + dag: DAGCircuit to check. + components_source: Target to check against. + + Raises: + TranspilerError: Chosen layout is not valid for the target disjoint connectivity. + """ if isinstance(components_source, Target): coupling_map = components_source.build_coupling_map(filter_idle_qubits=True) else: diff --git a/qiskit/transpiler/passes/routing/algorithms/token_swapper.py b/qiskit/transpiler/passes/routing/algorithms/token_swapper.py index 63dd1e1f0d49..ffb995018fa4 100644 --- a/qiskit/transpiler/passes/routing/algorithms/token_swapper.py +++ b/qiskit/transpiler/passes/routing/algorithms/token_swapper.py @@ -26,9 +26,10 @@ """Permutation algorithms for general graphs.""" +from __future__ import annotations import copy import logging -from typing import Iterator, Mapping, MutableMapping, MutableSet, List, Iterable, Union +from collections.abc import Mapping, MutableSet, MutableMapping, Iterator, Iterable import numpy as np import rustworkx as rx @@ -46,9 +47,7 @@ class ApproximateTokenSwapper: Internally caches the graph and associated datastructures for re-use. """ - def __init__( - self, graph: rx.PyGraph, seed: Union[int, np.random.Generator, None] = None - ) -> None: + def __init__(self, graph: rx.PyGraph, seed: int | np.random.Generator | None = None) -> None: """Construct an ApproximateTokenSwapping object. Args: @@ -80,7 +79,7 @@ def permutation_circuit(self, permutation: Permutation, trials: int = 4) -> Perm parallel_swaps = [[swap] for swap in sequential_swaps] return permutation_circuit(parallel_swaps) - def map(self, mapping: Mapping[int, int], trials: int = 4) -> List[Swap[int]]: + def map(self, mapping: Mapping[int, int], trials: int = 4) -> list[Swap[int]]: """Perform an approximately optimal Token Swapping algorithm to implement the permutation. Supports partial mappings (i.e. not-permutations) for graphs with missing tokens. @@ -113,7 +112,7 @@ def map(self, mapping: Mapping[int, int], trials: int = 4) -> List[Swap[int]]: ) # Once we find a zero solution we stop. - def take_until_zero(results: Iterable[List[int]]) -> Iterator[List[int]]: + def take_until_zero(results: Iterable[list[Swap[int]]]) -> Iterable[list[Swap[int]]]: """Take results until one is emitted of length zero (and also emit that).""" for result in results: yield result diff --git a/qiskit/transpiler/passes/routing/algorithms/util.py b/qiskit/transpiler/passes/routing/algorithms/util.py index 096b9fd11f03..930c835603dd 100644 --- a/qiskit/transpiler/passes/routing/algorithms/util.py +++ b/qiskit/transpiler/passes/routing/algorithms/util.py @@ -26,7 +26,10 @@ """Utility functions shared between permutation functionality.""" -from typing import List, TypeVar, Iterable, MutableMapping +from __future__ import annotations + +from collections.abc import Iterable +from typing import TypeVar, MutableMapping from qiskit.circuit import QuantumRegister from qiskit.dagcircuit import DAGCircuit @@ -53,8 +56,8 @@ def swap_permutation( for swap_step in swaps: for sw1, sw2 in swap_step: # Take into account non-existent keys. - val1 = None # type: Optional[_V] - val2 = None # type: Optional[_V] + val1: _V | None = None + val2: _V | None = None if allow_missing_keys: val1 = mapping.pop(sw1, None) val2 = mapping.pop(sw2, None) @@ -68,7 +71,7 @@ def swap_permutation( mapping[sw1] = val2 -def permutation_circuit(swaps: Iterable[List[Swap[_V]]]) -> PermutationCircuit: +def permutation_circuit(swaps: Iterable[list[Swap[_V]]]) -> PermutationCircuit: """Produce a circuit description of a list of swaps. With a given permutation and permuter you can compute the swaps using the permuter function then feed it into this circuit function to obtain a circuit description. diff --git a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_block.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_block.py index d121b70baa3f..bc3d7f794bf0 100644 --- a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_block.py +++ b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/commuting_2q_block.py @@ -11,11 +11,12 @@ # that they have been altered from the originals. """A gate made of commuting two-qubit gates.""" +from __future__ import annotations -from typing import Iterable +from collections.abc import Iterable from qiskit.exceptions import QiskitError -from qiskit.circuit import Gate +from qiskit.circuit import Gate, Qubit, Clbit from qiskit.dagcircuit import DAGOpNode @@ -34,7 +35,8 @@ def __init__(self, node_block: Iterable[DAGOpNode]) -> None: Raises: QiskitError: If the nodes in the node block do not apply to two-qubits. """ - qubits, cbits = set(), set() + qubits: set[Qubit] = set() + cbits: set[Clbit] = set() for node in node_block: if len(node.qargs) != 2: raise QiskitError(f"Node {node.name} does not apply to two-qubits.") 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 11919bf18b6c..1b0c734bd0b8 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 @@ -11,11 +11,10 @@ # that they have been altered from the originals. """A swap strategy pass for blocks of commuting gates.""" - +from __future__ import annotations from collections import defaultdict -from typing import Dict, List, Optional, Tuple -from qiskit.circuit import Gate, QuantumCircuit +from qiskit.circuit import Gate, QuantumCircuit, Qubit from qiskit.converters import circuit_to_dag from qiskit.dagcircuit import DAGCircuit, DAGOpNode from qiskit.transpiler import TransformationPass, Layout, TranspilerError @@ -105,8 +104,8 @@ class Commuting2qGateRouter(TransformationPass): def __init__( self, - swap_strategy: Optional[SwapStrategy] = None, - edge_coloring: Optional[Dict[Tuple[int, int], int]] = None, + swap_strategy: SwapStrategy | None = None, + edge_coloring: dict[tuple[int, int], int] | None = None, ) -> None: r""" Args: @@ -127,7 +126,7 @@ def __init__( """ super().__init__() self._swap_strategy = swap_strategy - self._bit_indices = None + self._bit_indices: dict[Qubit, int] | None = None self._edge_coloring = edge_coloring def run(self, dag: DAGCircuit) -> DAGCircuit: @@ -206,7 +205,7 @@ def _compose_non_swap_nodes( """ # Add all the non-swap strategy nodes that we have accumulated up to now. order = layout.reorder_bits(new_dag.qubits) - order_bits = [None] * len(layout) + order_bits: list[int | None] = [None] * len(layout) for idx, val in enumerate(order): order_bits[val] = idx @@ -215,7 +214,7 @@ def _compose_non_swap_nodes( # Re-initialize the node accumulator return new_dag.copy_empty_like() - def _position_in_cmap(self, j: int, k: int, layout: Layout) -> Tuple[int, ...]: + def _position_in_cmap(self, j: int, k: int, layout: Layout) -> tuple[int, ...]: """A helper function to track the movement of virtual qubits through the swaps. Args: @@ -231,7 +230,9 @@ def _position_in_cmap(self, j: int, k: int, layout: Layout) -> Tuple[int, ...]: return bit0, bit1 - def _build_sub_layers(self, current_layer: Dict[tuple, Gate]) -> List[Dict[tuple, Gate]]: + def _build_sub_layers( + self, current_layer: dict[tuple[int, int], Gate] + ) -> list[dict[tuple[int, int], Gate]]: """A helper method to build-up sets of gates to simultaneously apply. This is done with an edge coloring if the ``edge_coloring`` init argument was given or with @@ -256,10 +257,12 @@ def _build_sub_layers(self, current_layer: Dict[tuple, Gate]) -> List[Dict[tuple return self._greedy_build_sub_layers(current_layer) def _edge_coloring_build_sub_layers( - self, current_layer: Dict[tuple, Gate] - ) -> List[Dict[tuple, Gate]]: + self, current_layer: dict[tuple[int, int], Gate] + ) -> list[dict[tuple[int, int], Gate]]: """The edge coloring method of building sub-layers of commuting gates.""" - sub_layers = [{} for _ in set(self._edge_coloring.values())] + sub_layers: list[dict[tuple[int, int], Gate]] = [ + {} for _ in set(self._edge_coloring.values()) + ] for edge, gate in current_layer.items(): color = self._edge_coloring[edge] sub_layers[color][edge] = gate @@ -267,11 +270,14 @@ def _edge_coloring_build_sub_layers( return sub_layers @staticmethod - def _greedy_build_sub_layers(current_layer: Dict[tuple, Gate]) -> List[Dict[tuple, Gate]]: + def _greedy_build_sub_layers( + current_layer: dict[tuple[int, int], Gate] + ) -> list[dict[tuple[int, int], Gate]]: """The greedy method of building sub-layers of commuting gates.""" sub_layers = [] while len(current_layer) > 0: - current_sub_layer, remaining_gates, blocked_vertices = {}, {}, set() + current_sub_layer, remaining_gates = {}, {} + blocked_vertices: set[tuple] = set() for edge, evo_gate in current_layer.items(): if blocked_vertices.isdisjoint(edge): @@ -342,10 +348,10 @@ def swap_decompose( def _make_op_layers( self, dag: DAGCircuit, op: Commuting2qBlock, layout: Layout, swap_strategy: SwapStrategy - ) -> Dict[int, Dict[tuple, Gate]]: + ) -> dict[int, dict[tuple, Gate]]: """Creates layers of two-qubit gates based on the distance in the swap strategy.""" - gate_layers = defaultdict(dict) + gate_layers: dict[int, dict[tuple, Gate]] = defaultdict(dict) for node in op.node_block: edge = (self._bit_indices[node.qargs[0]], self._bit_indices[node.qargs[1]]) diff --git a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/swap_strategy.py b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/swap_strategy.py index a3ec6d4b5653..72b62dc49a81 100644 --- a/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/swap_strategy.py +++ b/qiskit/transpiler/passes/routing/commuting_2q_gate_routing/swap_strategy.py @@ -11,8 +11,8 @@ # that they have been altered from the originals. """Defines a swap strategy class.""" - -from typing import Any, List, Optional, Set, Tuple +from __future__ import annotations +from typing import Any import copy import numpy as np @@ -48,7 +48,7 @@ class SwapStrategy: """ def __init__( - self, coupling_map: CouplingMap, swap_layers: Tuple[Tuple[Tuple[int, int], ...], ...] + self, coupling_map: CouplingMap, swap_layers: tuple[tuple[tuple[int, int], ...], ...] ) -> None: """ Args: @@ -66,9 +66,9 @@ def __init__( self._coupling_map = coupling_map self._num_vertices = coupling_map.size() self._swap_layers = swap_layers - self._distance_matrix = None - self._possible_edges = None - self._missing_couplings = None + self._distance_matrix: np.ndarray | None = None + self._possible_edges: set[tuple[int, int]] | None = None + self._missing_couplings: set[tuple[int, int]] | None = None self._inverse_composed_permutation = {0: list(range(self._num_vertices))} edge_set = set(self._coupling_map.get_edges()) @@ -86,7 +86,7 @@ def __init__( raise QiskitError(f"The {i}th swap layer contains a qubit with multiple swaps.") @classmethod - def from_line(cls, line: List[int], num_swap_layers: Optional[int] = None) -> "SwapStrategy": + def from_line(cls, line: list[int], num_swap_layers: int | None = None) -> "SwapStrategy": """Creates a swap strategy for a line graph with the specified number of SWAP layers. This SWAP strategy will use the full line if instructed to do so (i.e. num_variables @@ -151,7 +151,7 @@ def __repr__(self) -> str: return description - def swap_layer(self, idx: int) -> List[Tuple[int, int]]: + def swap_layer(self, idx: int) -> list[tuple[int, int]]: """Return the layer of swaps at the given index. Args: @@ -164,7 +164,7 @@ def swap_layer(self, idx: int) -> List[Tuple[int, int]]: return list(self._swap_layers[idx]) @property - def distance_matrix(self) -> np.array: + def distance_matrix(self) -> np.ndarray: """A matrix describing when qubits become adjacent in the swap strategy. Returns: @@ -190,7 +190,7 @@ def distance_matrix(self) -> np.array: return self._distance_matrix - def new_connections(self, idx: int) -> List[Set[int]]: + def new_connections(self, idx: int) -> list[set[int]]: """ Returns the new connections obtained after applying the SWAP layer specified by idx, i.e. a list of qubit pairs that are adjacent to one another after idx steps of the SWAP strategy. @@ -209,7 +209,7 @@ def new_connections(self, idx: int) -> List[Set[int]]: connections.append({i, j}) return connections - def _build_edges(self) -> Set[Tuple[int, int]]: + def _build_edges(self) -> set[tuple[int, int]]: """Build the possible edges that the swap strategy accommodates.""" possible_edges = set() @@ -221,7 +221,7 @@ def _build_edges(self) -> Set[Tuple[int, int]]: return possible_edges @property - def possible_edges(self) -> Set[Tuple[int, int]]: + def possible_edges(self) -> set[tuple[int, int]]: """Return the qubit connections that can be generated. Returns: @@ -233,7 +233,7 @@ def possible_edges(self) -> Set[Tuple[int, int]]: return self._possible_edges @property - def missing_couplings(self) -> Set[Tuple[int, int]]: + def missing_couplings(self) -> set[tuple[int, int]]: """Return the set of couplings that cannot be reached. Returns: @@ -262,8 +262,8 @@ def swapped_coupling_map(self, idx: int) -> CouplingMap: return CouplingMap(couplinglist=edges) def apply_swap_layer( - self, list_to_swap: List[Any], idx: int, inplace: bool = False - ) -> List[Any]: + self, list_to_swap: list[Any], idx: int, inplace: bool = False + ) -> list[Any]: """Permute the elements of ``list_to_swap`` based on layer indexed by ``idx``. Args: @@ -285,7 +285,7 @@ def apply_swap_layer( return x - def inverse_composed_permutation(self, idx: int) -> List[int]: + def inverse_composed_permutation(self, idx: int) -> list[int]: """ Returns the inversed composed permutation of all swap layers applied up to layer ``idx``. Permutations are represented by list of integers where the ith element diff --git a/qiskit/transpiler/passes/routing/layout_transformation.py b/qiskit/transpiler/passes/routing/layout_transformation.py index 3d67e0ffb572..218e6ca958d3 100644 --- a/qiskit/transpiler/passes/routing/layout_transformation.py +++ b/qiskit/transpiler/passes/routing/layout_transformation.py @@ -11,7 +11,7 @@ # that they have been altered from the originals. """Map (with minimum effort) a DAGCircuit onto a `coupling_map` adding swap gates.""" -from typing import Union +from __future__ import annotations import numpy as np @@ -31,10 +31,10 @@ class LayoutTransformation(TransformationPass): def __init__( self, - coupling_map: Union[CouplingMap, Target, None], - from_layout: Union[Layout, str], - to_layout: Union[Layout, str], - seed: Union[int, np.random.default_rng] = None, + coupling_map: CouplingMap | Target | None, + from_layout: Layout | str, + to_layout: Layout | str, + seed: int | np.random.Generator | None = None, trials=4, ): """LayoutTransformation initializer. diff --git a/qiskit/transpiler/passes/scheduling/alignments/align_measures.py b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py index 9a1b3c94c5e5..164e9557fbb7 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/align_measures.py +++ b/qiskit/transpiler/passes/scheduling/alignments/align_measures.py @@ -11,13 +11,16 @@ # that they have been altered from the originals. """Align measurement instructions.""" +from __future__ import annotations import itertools import warnings from collections import defaultdict -from typing import List, Union +from collections.abc import Iterable +from typing import Type + +from qiskit.circuit.quantumcircuit import ClbitSpecifier, QubitSpecifier from qiskit.circuit.delay import Delay -from qiskit.circuit.instruction import Instruction from qiskit.circuit.measure import Measure from qiskit.circuit.parameterexpression import ParameterExpression from qiskit.dagcircuit import DAGCircuit @@ -141,12 +144,14 @@ def run(self, dag: DAGCircuit): # * pad_with_delay is called only with non-delay node to avoid consecutive delay new_dag = dag.copy_empty_like() - qubit_time_available = defaultdict(int) # to track op start time - qubit_stop_times = defaultdict(int) # to track delay start time for padding - clbit_readable = defaultdict(int) - clbit_writeable = defaultdict(int) + qubit_time_available: dict[QubitSpecifier, int] = defaultdict(int) # to track op start time + qubit_stop_times: dict[QubitSpecifier, int] = defaultdict( + int + ) # to track delay start time for padding + clbit_readable: dict[ClbitSpecifier, int] = defaultdict(int) + clbit_writeable: dict[ClbitSpecifier, int] = defaultdict(int) - def pad_with_delays(qubits: List[int], until, unit) -> None: + def pad_with_delays(qubits: Iterable[QubitSpecifier], until, unit) -> None: """Pad idle time-slots in ``qubits`` with delays in ``unit`` until ``until``.""" for q in qubits: if qubit_stop_times[q] < until: @@ -206,7 +211,7 @@ def pad_with_delays(qubits: List[int], until, unit) -> None: def _check_alignment_required( dag: DAGCircuit, alignment: int, - instructions: Union[Instruction, List[Instruction]], + instructions: Type | list[Type], ) -> bool: """Check DAG nodes and return a boolean representing if instruction scheduling is necessary. diff --git a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py index 325cb4c96546..b53d0f864cef 100644 --- a/qiskit/transpiler/passes/scheduling/alignments/reschedule.py +++ b/qiskit/transpiler/passes/scheduling/alignments/reschedule.py @@ -11,8 +11,8 @@ # that they have been altered from the originals. """Rescheduler pass to adjust node start times.""" - -from typing import List +from __future__ import annotations +from collections.abc import Generator from qiskit.circuit.gate import Gate from qiskit.circuit.delay import Delay @@ -79,7 +79,7 @@ def __init__( self.pulse_align = pulse_alignment @classmethod - def _get_next_gate(cls, dag: DAGCircuit, node: DAGOpNode) -> List[DAGOpNode]: + def _get_next_gate(cls, dag: DAGCircuit, node: DAGOpNode) -> Generator[DAGOpNode, None, None]: """Get next non-delay nodes. Args: diff --git a/qiskit/transpiler/passes/scheduling/padding/base_padding.py b/qiskit/transpiler/passes/scheduling/padding/base_padding.py index 5fb25790cfbb..16d58dbae25e 100644 --- a/qiskit/transpiler/passes/scheduling/padding/base_padding.py +++ b/qiskit/transpiler/passes/scheduling/padding/base_padding.py @@ -11,9 +11,10 @@ # that they have been altered from the originals. """Padding pass to fill empty timeslot.""" +from __future__ import annotations +from collections.abc import Iterable import logging -from typing import List, Optional, Union from qiskit.circuit import Qubit, Clbit, Instruction from qiskit.circuit.delay import Delay @@ -195,8 +196,8 @@ def _apply_scheduled_op( dag: DAGCircuit, t_start: int, oper: Instruction, - qubits: Union[Qubit, List[Qubit]], - clbits: Optional[Union[Clbit, List[Clbit]]] = None, + qubits: Qubit | Iterable[Qubit], + clbits: Clbit | Iterable[Clbit] | None = None, ): """Add new operation to DAG with scheduled information. diff --git a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py index e3923ed8b26e..bbb1b39bbd72 100644 --- a/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py +++ b/qiskit/transpiler/passes/scheduling/padding/dynamical_decoupling.py @@ -11,11 +11,11 @@ # that they have been altered from the originals. """Dynamical Decoupling insertion pass.""" +from __future__ import annotations import logging -from typing import List, Optional - import numpy as np + from qiskit.circuit import Qubit, Gate from qiskit.circuit.delay import Delay from qiskit.circuit.library.standard_gates import IGate, UGate, U3Gate @@ -106,9 +106,9 @@ def uhrig_pulse_location(k): def __init__( self, durations: InstructionDurations = None, - dd_sequence: List[Gate] = None, - qubits: Optional[List[int]] = None, - spacing: Optional[List[float]] = None, + dd_sequence: list[Gate] = None, + qubits: list[int] | None = None, + spacing: list[float] | None = None, skip_reset_qubits: bool = True, pulse_alignment: int = 1, extra_slack_distribution: str = "middle", @@ -166,8 +166,8 @@ def __init__( self._spacing = spacing self._extra_slack_distribution = extra_slack_distribution - self._no_dd_qubits = set() - self._dd_sequence_lengths = {} + self._no_dd_qubits: set[int] = set() + self._dd_sequence_lengths: dict[Qubit, list[int]] = {} self._sequence_phase = 0 if target is not None: self._durations = target.durations() diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 881d3cc10553..e4555c7fba04 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -11,9 +11,9 @@ # that they have been altered from the originals. """Synthesize UnitaryGates.""" - +from __future__ import annotations from math import pi, inf, isclose -from typing import List, Union, Optional +from typing import Any from copy import deepcopy from itertools import product from functools import partial @@ -265,13 +265,13 @@ class UnitarySynthesis(TransformationPass): def __init__( self, - basis_gates: List[str] = None, - approximation_degree: Optional[float] = 1.0, + basis_gates: list[str] = None, + approximation_degree: float | None = 1.0, coupling_map: CouplingMap = None, backend_props: BackendProperties = None, - pulse_optimize: Union[bool, None] = None, - natural_direction: Union[bool, None] = None, - synth_gates: Union[List[str], None] = None, + pulse_optimize: bool | None = None, + natural_direction: bool | None = None, + synth_gates: list[str] | None = None, method: str = "default", min_qubits: int = None, plugin_config: dict = None, @@ -384,7 +384,7 @@ def run(self, dag: DAGCircuit) -> DAGCircuit: plugin_method = self.plugins.ext_plugins[self.method].obj else: plugin_method = DefaultUnitarySynthesis() - plugin_kwargs = {"config": self._plugin_config} + plugin_kwargs: dict[str, Any] = {"config": self._plugin_config} _gate_lengths = _gate_errors = None _gate_lengths_by_qubit = _gate_errors_by_qubit = None diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index eb339aec9018..03019e872ff4 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -11,10 +11,11 @@ # that they have been altered from the originals. """Manager for a set of Passes and their scheduling during transpilation.""" - +from __future__ import annotations import io import re -from typing import Union, List, Tuple, Callable, Dict, Any, Optional, Iterator, Iterable, TypeVar +from collections.abc import Iterator, Iterable, Callable, Sequence +from typing import Union, List, Any import dill @@ -24,14 +25,13 @@ from .exceptions import TranspilerError from .runningpassmanager import RunningPassManager, FlowController - -_CircuitsT = TypeVar("_CircuitsT", bound=Union[List[QuantumCircuit], QuantumCircuit]) +_CircuitsT = Union[List[QuantumCircuit], QuantumCircuit] class PassManager: """Manager for a set of Passes and their scheduling during transpilation.""" - def __init__(self, passes: Union[BasePass, List[BasePass]] = None, max_iteration: int = 1000): + def __init__(self, passes: BasePass | list[BasePass] | None = None, max_iteration: int = 1000): """Initialize an empty `PassManager` object (with no passes scheduled). Args: @@ -43,7 +43,7 @@ def __init__(self, passes: Union[BasePass, List[BasePass]] = None, max_iteration # the pass manager's schedule of passes, including any control-flow. # Populated via PassManager.append(). - self._pass_sets = [] + self._pass_sets: list[dict[str, Any]] = [] if passes is not None: self.append(passes) self.max_iteration = max_iteration @@ -51,7 +51,7 @@ def __init__(self, passes: Union[BasePass, List[BasePass]] = None, max_iteration def append( self, - passes: Union[BasePass, List[BasePass]], + passes: BasePass | Sequence[BasePass | FlowController], max_iteration: int = None, **flow_controller_conditions: Any, ) -> None: @@ -84,7 +84,7 @@ def append( def replace( self, index: int, - passes: Union[BasePass, List[BasePass]], + passes: BasePass | list[BasePass], max_iteration: int = None, **flow_controller_conditions: Any, ) -> None: @@ -164,8 +164,8 @@ def __add__(self, other): @staticmethod def _normalize_passes( - passes: Union[BasePass, List[BasePass], FlowController] - ) -> List[BasePass]: + passes: BasePass | Sequence[BasePass | FlowController] | FlowController, + ) -> Sequence[BasePass | FlowController] | FlowController: if isinstance(passes, FlowController): return passes if isinstance(passes, BasePass): @@ -187,8 +187,8 @@ def _normalize_passes( def run( self, circuits: _CircuitsT, - output_name: Optional[str] = None, - callback: Optional[Callable] = None, + output_name: str | None = None, + callback: Callable | None = None, ) -> _CircuitsT: """Run all the passes on the specified ``circuits``. @@ -249,8 +249,8 @@ def _in_parallel(circuit, pm_dill=None) -> QuantumCircuit: def _run_several_circuits( self, circuits: List[QuantumCircuit], - output_name: Optional[str] = None, - callback: Optional[Callable] = None, + output_name: str | None = None, + callback: Callable | None = None, ) -> List[QuantumCircuit]: """Run all the passes on the specified ``circuits``. @@ -274,8 +274,8 @@ def _run_several_circuits( def _run_single_circuit( self, circuit: QuantumCircuit, - output_name: Optional[str] = None, - callback: Optional[Callable] = None, + output_name: str | None = None, + callback: Callable | None = None, ) -> QuantumCircuit: """Run all the passes on a ``circuit``. @@ -319,7 +319,7 @@ def draw(self, filename=None, style=None, raw=False): return pass_manager_drawer(self, filename=filename, style=style, raw=raw) - def passes(self) -> List[Dict[str, BasePass]]: + def passes(self) -> list[dict[str, BasePass]]: """Return a list structure of the appended passes and its options. Returns: @@ -400,7 +400,7 @@ class StagedPassManager(PassManager): r"\s|\+|\-|\*|\/|\\|\%|\<|\>|\@|\!|\~|\^|\&|\:|\[|\]|\{|\}|\(|\)" ) - def __init__(self, stages: Optional[Iterable[str]] = None, **kwargs) -> None: + def __init__(self, stages: Iterable[str] | None = None, **kwargs) -> None: """Initialize a new StagedPassManager object Args: @@ -448,19 +448,19 @@ def _validate_stages(self, stages: Iterable[str]) -> None: msg.write(f", {invalid_stage}") raise ValueError(msg.getvalue()) - def _validate_init_kwargs(self, kwargs: Dict[str, Any]) -> None: + def _validate_init_kwargs(self, kwargs: dict[str, Any]) -> None: expanded_stages = set(self.expanded_stages) for stage in kwargs.keys(): if stage not in expanded_stages: raise AttributeError(f"{stage} is not a valid stage.") @property - def stages(self) -> Tuple[str, ...]: + def stages(self) -> tuple[str, ...]: """Pass manager stages""" return self._stages # pylint: disable=no-member @property - def expanded_stages(self) -> Tuple[str, ...]: + def expanded_stages(self) -> tuple[str, ...]: """Expanded Pass manager stages including ``pre_`` and ``post_`` phases.""" return self._expanded_stages # pylint: disable=no-member @@ -486,7 +486,7 @@ def __setattr__(self, attr, value): def append( self, - passes: Union[BasePass, List[BasePass]], + passes: BasePass | Sequence[BasePass | FlowController], max_iteration: int = None, **flow_controller_conditions: Any, ) -> None: @@ -495,7 +495,7 @@ def append( def replace( self, index: int, - passes: Union[BasePass, List[BasePass]], + passes: BasePass | list[BasePass], max_iteration: int = None, **flow_controller_conditions: Any, ) -> None: @@ -523,15 +523,15 @@ def _create_running_passmanager(self) -> RunningPassManager: self._update_passmanager() return super()._create_running_passmanager() - def passes(self) -> List[Dict[str, BasePass]]: + def passes(self) -> list[dict[str, BasePass]]: self._update_passmanager() return super().passes() def run( self, circuits: _CircuitsT, - output_name: Optional[str] = None, - callback: Optional[Callable] = None, + output_name: str | None = None, + callback: Callable | None = None, ) -> _CircuitsT: self._update_passmanager() return super().run(circuits, output_name, callback) diff --git a/qiskit/transpiler/preset_passmanagers/level0.py b/qiskit/transpiler/preset_passmanagers/level0.py index 36b62d2c22fe..b0790f1026f2 100644 --- a/qiskit/transpiler/preset_passmanagers/level0.py +++ b/qiskit/transpiler/preset_passmanagers/level0.py @@ -14,6 +14,7 @@ Level 0 pass manager: no explicit optimization other than mapping to backend. """ +from qiskit.transpiler.basepasses import BasePass from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.timing_constraints import TimingConstraints @@ -83,7 +84,7 @@ def _choose_layout_condition(property_set): coupling_map_layout = target if layout_method == "trivial": - _choose_layout = TrivialLayout(coupling_map_layout) + _choose_layout: BasePass = TrivialLayout(coupling_map_layout) elif layout_method == "dense": _choose_layout = DenseLayout(coupling_map, backend_properties, target=target) elif layout_method == "noise_adaptive": diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 46045b54ef54..db8b09b716b0 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -14,12 +14,13 @@ Level 1 pass manager: light optimization by simple adjacent gate collapsing. """ - +from __future__ import annotations +from qiskit.transpiler.basepasses import BasePass from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.passmanager import StagedPassManager -from qiskit.transpiler import ConditionalController +from qiskit.transpiler import ConditionalController, FlowController from qiskit.transpiler.passes import CXCancellation from qiskit.transpiler.passes import SetLayout @@ -118,13 +119,13 @@ def _vf2_match_not_found(property_set): else: coupling_map_layout = target - _choose_layout_0 = ( + _choose_layout_0: list[BasePass] = ( [] if pass_manager_config.layout_method else [TrivialLayout(coupling_map_layout), CheckMap(coupling_map_layout)] ) - _choose_layout_1 = ( + _choose_layout_1: list[BasePass] | BasePass = ( [] if pass_manager_config.layout_method else VF2Layout( @@ -138,7 +139,7 @@ def _vf2_match_not_found(property_set): ) if layout_method == "trivial": - _improve_layout = TrivialLayout(coupling_map_layout) + _improve_layout: BasePass = TrivialLayout(coupling_map_layout) elif layout_method == "dense": _improve_layout = DenseLayout(coupling_map, backend_properties, target=target) elif layout_method == "noise_adaptive": @@ -275,7 +276,7 @@ def _unroll_condition(property_set): return not property_set["all_gates_in_basis"] # Check if any gate is not in the basis, and if so, run unroll passes - _unroll_if_out_of_basis = [ + _unroll_if_out_of_basis: list[BasePass | FlowController] = [ GatesInBasis(basis_gates, target=target), ConditionalController(unroll, condition=_unroll_condition), ] diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index 900cb2668e93..743018881da3 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -15,12 +15,13 @@ Level 2 pass manager: medium optimization by noise adaptive qubit mapping and gate cancellation using commutativity rules. """ - +from __future__ import annotations +from qiskit.transpiler.basepasses import BasePass from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.passmanager import StagedPassManager -from qiskit.transpiler import ConditionalController +from qiskit.transpiler import ConditionalController, FlowController from qiskit.transpiler.passes import SetLayout from qiskit.transpiler.passes import VF2Layout @@ -109,7 +110,7 @@ def _vf2_match_not_found(property_set): return False # Try using VF2 layout to find a perfect layout - _choose_layout_0 = ( + _choose_layout_0: list[BasePass] | BasePass = ( [] if pass_manager_config.layout_method else VF2Layout( @@ -128,7 +129,7 @@ def _vf2_match_not_found(property_set): coupling_map_layout = target if layout_method == "trivial": - _choose_layout_1 = TrivialLayout(coupling_map_layout) + _choose_layout_1: BasePass = TrivialLayout(coupling_map_layout) elif layout_method == "dense": _choose_layout_1 = DenseLayout(coupling_map, backend_properties, target=target) elif layout_method == "noise_adaptive": @@ -160,7 +161,7 @@ def _vf2_match_not_found(property_set): def _opt_control(property_set): return (not property_set["depth_fixed_point"]) or (not property_set["size_fixed_point"]) - _opt = [ + _opt: list[BasePass] = [ Optimize1qGatesDecomposition(basis=basis_gates, target=target), CommutativeCancellation(basis_gates=basis_gates, target=target), ] @@ -230,7 +231,7 @@ def _unroll_condition(property_set): return not property_set["all_gates_in_basis"] # Check if any gate is not in the basis, and if so, run unroll passes - _unroll_if_out_of_basis = [ + _unroll_if_out_of_basis: list[BasePass | FlowController] = [ GatesInBasis(basis_gates, target=target), ConditionalController(unroll, condition=_unroll_condition), ] diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 57cfa5d06201..7fe64eed521d 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -15,8 +15,8 @@ Level 3 pass manager: heavy optimization by noise adaptive qubit mapping and gate cancellation using commutativity rules and unitary synthesis. """ - - +from __future__ import annotations +from qiskit.transpiler.basepasses import BasePass from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.timing_constraints import TimingConstraints from qiskit.transpiler.passmanager import PassManager @@ -41,7 +41,7 @@ from qiskit.transpiler.passes import UnitarySynthesis from qiskit.transpiler.passes import GatesInBasis from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements -from qiskit.transpiler.runningpassmanager import ConditionalController +from qiskit.transpiler.runningpassmanager import ConditionalController, FlowController from qiskit.transpiler.preset_passmanagers import common from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason from qiskit.transpiler.preset_passmanagers.plugin import ( @@ -115,7 +115,7 @@ def _vf2_match_not_found(property_set): return False # 2a. If layout method is not set, first try VF2Layout - _choose_layout_0 = ( + _choose_layout_0: list[BasePass] | BasePass = ( [] if pass_manager_config.layout_method else VF2Layout( @@ -135,7 +135,7 @@ def _vf2_match_not_found(property_set): # 2b. if VF2 didn't converge on a solution use layout_method (dense). if layout_method == "trivial": - _choose_layout_1 = TrivialLayout(coupling_map_layout) + _choose_layout_1: BasePass = TrivialLayout(coupling_map_layout) elif layout_method == "dense": _choose_layout_1 = DenseLayout(coupling_map, backend_properties, target=target) elif layout_method == "noise_adaptive": @@ -161,7 +161,7 @@ def _vf2_match_not_found(property_set): # 8. Optimize iteratively until no more change in depth. Removes useless gates # after reset and before measure, commutes gates and optimizes contiguous blocks. - _minimum_point_check = [ + _minimum_point_check: list[BasePass | FlowController] = [ Depth(recurse=True), Size(recurse=True), MinimumPoint(["depth", "size"], "optimization_loop"), @@ -170,7 +170,7 @@ def _vf2_match_not_found(property_set): def _opt_control(property_set): return not property_set["optimization_loop_minimum_point"] - _opt = [ + _opt: list[BasePass | FlowController] = [ Collect2qBlocks(), ConsolidateBlocks( basis_gates=basis_gates, target=target, approximation_degree=approximation_degree @@ -257,7 +257,7 @@ def _unroll_condition(property_set): return not property_set["all_gates_in_basis"] # Check if any gate is not in the basis, and if so, run unroll passes - _unroll_if_out_of_basis = [ + _unroll_if_out_of_basis: list[BasePass | FlowController] = [ GatesInBasis(basis_gates, target=target), ConditionalController(unroll, condition=_unroll_condition), ] diff --git a/qiskit/transpiler/runningpassmanager.py b/qiskit/transpiler/runningpassmanager.py index a89427af4ba4..6290ed4d52bc 100644 --- a/qiskit/transpiler/runningpassmanager.py +++ b/qiskit/transpiler/runningpassmanager.py @@ -12,7 +12,7 @@ """RunningPassManager class for the transpiler. This object holds the state of a pass manager during running-time.""" - +from __future__ import annotations from functools import partial from collections import OrderedDict import logging @@ -58,11 +58,11 @@ def __init__(self, max_iteration): self.count = 0 - def append(self, passes, **flow_controller_conditions): + def append(self, passes: list[BasePass], **flow_controller_conditions): """Append a Pass to the schedule of passes. Args: - passes (list[BasePass]): passes to be added to schedule + passes (list[TBasePass]): passes to be added to schedule flow_controller_conditions (kwargs): See add_flow_controller(): Dictionary of control flow plugins. Default: @@ -308,11 +308,11 @@ def remove_flow_controller(cls, name): del cls.registered_controllers[name] @classmethod - def controller_factory(cls, passes, options, **partial_controller): + def controller_factory(cls, passes: list[BasePass], options, **partial_controller): """Constructs a flow controller based on the partially evaluated controller arguments. Args: - passes (list[BasePass]): passes to add to the flow controller. + passes (list[TBasePass]): passes to add to the flow controller. options (dict): PassManager options. **partial_controller (dict): Partially evaluated controller arguments in the form `{name:partial}` diff --git a/qiskit/transpiler/synthesis/aqc/approximate.py b/qiskit/transpiler/synthesis/aqc/approximate.py index 418d766dc356..6c3f11fb71fc 100644 --- a/qiskit/transpiler/synthesis/aqc/approximate.py +++ b/qiskit/transpiler/synthesis/aqc/approximate.py @@ -10,9 +10,9 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. """Base classes for an approximate circuit definition.""" - +from __future__ import annotations from abc import ABC, abstractmethod -from typing import Optional +from typing import Optional, SupportsFloat import numpy as np from qiskit import QuantumCircuit @@ -61,10 +61,10 @@ class ApproximatingObjective(ABC): def __init__(self) -> None: # must be set before optimization - self._target_matrix = None + self._target_matrix: np.ndarray | None = None @abstractmethod - def objective(self, param_values: np.ndarray) -> float: + def objective(self, param_values: np.ndarray) -> SupportsFloat: """ Computes a value of the objective function given a vector of parameter values. diff --git a/qiskit/transpiler/synthesis/aqc/cnot_unit_circuit.py b/qiskit/transpiler/synthesis/aqc/cnot_unit_circuit.py index 8ece18d47c48..6973ae55d72f 100644 --- a/qiskit/transpiler/synthesis/aqc/cnot_unit_circuit.py +++ b/qiskit/transpiler/synthesis/aqc/cnot_unit_circuit.py @@ -13,7 +13,7 @@ This is the Parametric Circuit class: anything that you need for a circuit to be parametrized and used for approximate compiling optimization. """ - +from __future__ import annotations from typing import Optional import numpy as np @@ -53,7 +53,7 @@ def __init__( self._tol = tol # Thetas to be optimized by the AQC algorithm - self._thetas = None + self._thetas: np.ndarray | None = None @property def thetas(self) -> np.ndarray: diff --git a/qiskit/transpiler/synthesis/aqc/cnot_unit_objective.py b/qiskit/transpiler/synthesis/aqc/cnot_unit_objective.py index 46ee7ad45a6a..b8bcd6ea9abd 100644 --- a/qiskit/transpiler/synthesis/aqc/cnot_unit_objective.py +++ b/qiskit/transpiler/synthesis/aqc/cnot_unit_objective.py @@ -13,6 +13,8 @@ A definition of the approximate circuit compilation optimization problem based on CNOT unit definition. """ +from __future__ import annotations +import typing from abc import ABC import numpy as np @@ -68,13 +70,13 @@ def __init__(self, num_qubits: int, cnots: np.ndarray) -> None: super().__init__(num_qubits, cnots) # last objective computations to be re-used by gradient - self._last_thetas = None - self._cnot_right_collection = None - self._cnot_left_collection = None - self._rotation_matrix = None - self._cnot_matrix = None + self._last_thetas: np.ndarray | None = None + self._cnot_right_collection: np.ndarray | None = None + self._cnot_left_collection: np.ndarray | None = None + self._rotation_matrix: int | np.ndarray | None = None + self._cnot_matrix: np.ndarray | None = None - def objective(self, param_values: np.ndarray) -> float: + def objective(self, param_values: np.ndarray) -> typing.SupportsFloat: # rename parameters just to make shorter and make use of our dictionary thetas = param_values n = self._num_qubits @@ -143,7 +145,7 @@ def objective(self, param_values: np.ndarray) -> float: # this is the matrix corresponding to the initial rotations # we start with 1 and kronecker product each qubit's rotations - rotation_matrix = 1 + rotation_matrix: int | np.ndarray = 1 for q in range(n): theta_index = 4 * num_cnots + 3 * q rz0 = rz_matrix(thetas[0 + theta_index]) @@ -268,7 +270,7 @@ def gradient(self, param_values: np.ndarray) -> np.ndarray: # now we compute the partial derivatives in the rotation part # we start with 1 and kronecker product each qubit's rotations for i in range(3 * n): - der_rotation_matrix = 1 + der_rotation_matrix: int | np.ndarray = 1 for q in range(n): theta_index = 4 * num_cnots + 3 * q rz0 = rz_matrix(thetas[0 + theta_index]) diff --git a/qiskit/transpiler/synthesis/aqc/fast_gradient/fast_grad_utils.py b/qiskit/transpiler/synthesis/aqc/fast_gradient/fast_grad_utils.py index a3b61042b432..b13c96ea3fe5 100644 --- a/qiskit/transpiler/synthesis/aqc/fast_gradient/fast_grad_utils.py +++ b/qiskit/transpiler/synthesis/aqc/fast_gradient/fast_grad_utils.py @@ -13,7 +13,7 @@ """ Utility functions in the fast gradient implementation. """ - +from __future__ import annotations from typing import Union import numpy as np @@ -58,7 +58,7 @@ def reverse_bits(x: Union[int, np.ndarray], nbits: int, enable: bool) -> Union[i return x if isinstance(x, int): - res = int(0) + res: int | np.ndarray = int(0) else: x = x.copy() res = np.full_like(x, fill_value=0) diff --git a/qiskit/transpiler/synthesis/aqc/fast_gradient/layer.py b/qiskit/transpiler/synthesis/aqc/fast_gradient/layer.py index eb4a2cf5aa9b..c567260fad51 100644 --- a/qiskit/transpiler/synthesis/aqc/fast_gradient/layer.py +++ b/qiskit/transpiler/synthesis/aqc/fast_gradient/layer.py @@ -13,9 +13,9 @@ """ Layer classes for the fast gradient implementation. """ - +from __future__ import annotations from abc import abstractmethod, ABC -from typing import Tuple, Optional +from typing import Optional import numpy as np from .fast_grad_utils import ( bit_permutation_1q, @@ -46,7 +46,7 @@ def set_from_matrix(self, mat: np.ndarray): raise NotImplementedError() @abstractmethod - def get_attr(self) -> Tuple[np.ndarray, np.ndarray, np.ndarray]: + def get_attr(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """ Returns gate matrix, direct and inverse permutations. @@ -91,7 +91,7 @@ def set_from_matrix(self, mat: np.ndarray): """See base class description.""" np.copyto(self._gmat, mat) - def get_attr(self) -> (np.ndarray, np.ndarray, np.ndarray): + def get_attr(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """See base class description.""" return self._gmat, self._perm, self._inv_perm @@ -132,7 +132,7 @@ def set_from_matrix(self, mat: np.ndarray): """See base class description.""" np.copyto(self._gmat, mat) - def get_attr(self) -> (np.ndarray, np.ndarray, np.ndarray): + def get_attr(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]: """See base class description.""" return self._gmat, self._perm, self._inv_perm diff --git a/qiskit/transpiler/target.py b/qiskit/transpiler/target.py index 61999e6e7418..08c5dfdfe38d 100644 --- a/qiskit/transpiler/target.py +++ b/qiskit/transpiler/target.py @@ -21,7 +21,7 @@ import itertools -from typing import Tuple, Union, Optional, Dict, List, Any +from typing import Any from collections.abc import Mapping from collections import defaultdict import datetime @@ -70,9 +70,9 @@ class InstructionProperties: def __init__( self, - duration: float = None, - error: float = None, - calibration: Union[Schedule, ScheduleBlock, CalibrationEntry] = None, + duration: float | None = None, + error: float | None = None, + calibration: Schedule | ScheduleBlock | CalibrationEntry | None = None, ): """Create a new ``InstructionProperties`` object @@ -83,7 +83,7 @@ def __init__( set of qubits. calibration: The pulse representation of the instruction. """ - self._calibration = None + self._calibration: CalibrationEntry | None = None self.duration = duration self.error = error @@ -122,7 +122,7 @@ def calibration(self): return self._calibration.get_schedule() @calibration.setter - def calibration(self, calibration: Union[Schedule, ScheduleBlock, CalibrationEntry]): + def calibration(self, calibration: Schedule | ScheduleBlock | CalibrationEntry): if isinstance(calibration, (Schedule, ScheduleBlock)): new_entry = ScheduleDef() new_entry.define(calibration, user_provided=True) @@ -860,7 +860,7 @@ def check_obj_params(parameters, obj): def has_calibration( self, operation_name: str, - qargs: Tuple[int, ...], + qargs: tuple[int, ...], ) -> bool: """Return whether the instruction (operation + qubits) defines a calibration. @@ -881,10 +881,10 @@ def has_calibration( def get_calibration( self, operation_name: str, - qargs: Tuple[int, ...], + qargs: tuple[int, ...], *args: ParameterValueType, **kwargs: ParameterValueType, - ) -> Union[Schedule, ScheduleBlock]: + ) -> Schedule | ScheduleBlock: """Get calibrated pulse schedule for the instruction. If calibration is templated with parameters, one can also provide those values @@ -1209,15 +1209,15 @@ def __str__(self): @classmethod def from_configuration( cls, - basis_gates: List[str], - num_qubits: Optional[int] = None, - coupling_map: Optional[CouplingMap] = None, - inst_map: Optional[InstructionScheduleMap] = None, - backend_properties: Optional[BackendProperties] = None, - instruction_durations: Optional[InstructionDurations] = None, - dt: Optional[float] = None, - timing_constraints: Optional[TimingConstraints] = None, - custom_name_mapping: Optional[Dict[str, Any]] = None, + basis_gates: list[str], + num_qubits: int | None = None, + coupling_map: CouplingMap | None = None, + inst_map: InstructionScheduleMap | None = None, + backend_properties: BackendProperties | None = None, + instruction_durations: InstructionDurations | None = None, + dt: float | None = None, + timing_constraints: TimingConstraints | None = None, + custom_name_mapping: dict[str, Any] | None = None, ) -> Target: """Create a target object from the individual global configuration @@ -1351,7 +1351,7 @@ def from_configuration( "with <= 2 qubits (because connectivity is defined on a CouplingMap)." ) for gate in one_qubit_gates: - gate_properties = {} + gate_properties: dict[tuple, InstructionProperties] = {} for qubit in range(num_qubits): error = None duration = None @@ -1441,7 +1441,7 @@ def from_configuration( def target_to_backend_properties(target: Target): """Convert a :class:`~.Target` object into a legacy :class:`~.BackendProperties`""" - properties_dict = { + properties_dict: dict[str, Any] = { "backend_name": "", "backend_version": "", "last_update_date": None, @@ -1481,7 +1481,7 @@ def target_to_backend_properties(target: Target): } ) else: - qubit_props = {x: None for x in range(target.num_qubits)} + qubit_props: dict[int, Any] = {x: None for x in range(target.num_qubits)} for qargs, props in qargs_list.items(): if qargs is None: continue From a7cad0e6d4a5c3e67c30c8a15e9459f4ea84ff3a Mon Sep 17 00:00:00 2001 From: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> Date: Tue, 23 May 2023 07:50:52 -0600 Subject: [PATCH 115/172] Avoid installing deps with `tox -e docs-clean` (#10121) --- tox.ini | 1 + 1 file changed, 1 insertion(+) diff --git a/tox.ini b/tox.ini index be1b1337a9bc..89c6ad734c03 100644 --- a/tox.ini +++ b/tox.ini @@ -85,6 +85,7 @@ commands = [testenv:docs-clean] skip_install = true +deps = allowlist_externals = rm commands = From 631b6f3acea7149b1dc0964b55fe74a9822ad77b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 23 May 2023 15:59:15 +0200 Subject: [PATCH 116/172] Attempt to fix backend primitives unittest failure on slow tests (#10142) * Add subtests * Remove nested subtests --- .../primitives/test_backend_estimator.py | 8 +++-- .../python/primitives/test_backend_sampler.py | 32 +++++++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/test/python/primitives/test_backend_estimator.py b/test/python/primitives/test_backend_estimator.py index 465a8aa9d95f..df9387ee070b 100644 --- a/test/python/primitives/test_backend_estimator.py +++ b/test/python/primitives/test_backend_estimator.py @@ -329,12 +329,13 @@ def test_no_max_circuits(self): def test_bound_pass_manager(self): """Test bound pass manager.""" - dummy_pass = DummyTP() - qc = QuantumCircuit(2) op = SparsePauliOp.from_list([("II", 1)]) with self.subTest("Test single circuit"): + + dummy_pass = DummyTP() + with patch.object(DummyTP, "run", wraps=dummy_pass.run) as mock_pass: bound_pass = PassManager(dummy_pass) estimator = BackendEstimator(backend=FakeNairobi(), bound_pass_manager=bound_pass) @@ -342,6 +343,9 @@ def test_bound_pass_manager(self): self.assertEqual(mock_pass.call_count, 1) with self.subTest("Test circuit batch"): + + dummy_pass = DummyTP() + with patch.object(DummyTP, "run", wraps=dummy_pass.run) as mock_pass: bound_pass = PassManager(dummy_pass) estimator = BackendEstimator(backend=FakeNairobi(), bound_pass_manager=bound_pass) diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py index cf8cd90556c9..86dc709f8983 100644 --- a/test/python/primitives/test_backend_sampler.py +++ b/test/python/primitives/test_backend_sampler.py @@ -386,19 +386,25 @@ def test_outcome_bitstring_size(self): def test_bound_pass_manager(self): """Test bound pass manager.""" - dummy_pass = DummyTP() - - with patch.object(DummyTP, "run", wraps=dummy_pass.run) as mock_pass: - bound_pass = PassManager(dummy_pass) - sampler = BackendSampler(backend=FakeNairobi(), bound_pass_manager=bound_pass) - _ = sampler.run(self._circuit[0]).result() - self.assertEqual(mock_pass.call_count, 1) - - with patch.object(DummyTP, "run", wraps=dummy_pass.run) as mock_pass: - bound_pass = PassManager(dummy_pass) - sampler = BackendSampler(backend=FakeNairobi(), bound_pass_manager=bound_pass) - _ = sampler.run([self._circuit[0], self._circuit[0]]).result() - self.assertEqual(mock_pass.call_count, 2) + with self.subTest("Test single circuit"): + + dummy_pass = DummyTP() + + with patch.object(DummyTP, "run", wraps=dummy_pass.run) as mock_pass: + bound_pass = PassManager(dummy_pass) + sampler = BackendSampler(backend=FakeNairobi(), bound_pass_manager=bound_pass) + _ = sampler.run(self._circuit[0]).result() + self.assertEqual(mock_pass.call_count, 1) + + with self.subTest("Test circuit batch"): + + dummy_pass = DummyTP() + + with patch.object(DummyTP, "run", wraps=dummy_pass.run) as mock_pass: + bound_pass = PassManager(dummy_pass) + sampler = BackendSampler(backend=FakeNairobi(), bound_pass_manager=bound_pass) + _ = sampler.run([self._circuit[0], self._circuit[0]]).result() + self.assertEqual(mock_pass.call_count, 2) if __name__ == "__main__": From 02502b5d98796dcc2c8b80ac5d0f2af85e978e5c Mon Sep 17 00:00:00 2001 From: Evgenii Zheltonozhskii Date: Wed, 24 May 2023 15:28:01 +0300 Subject: [PATCH 117/172] Remove undefined variable (#10117) * Remove undefined variable (fix https://github.com/Qiskit/qiskit-terra/issues/10113) * Add test and bugfix description --- .../passes/calibration/rzx_builder.py | 5 ++++- ...exception-decription-3ba0b5db82c576cf.yaml | 4 ++++ .../transpiler/test_calibrationbuilder.py | 22 ++++++++++++++++++- 3 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-exception-decription-3ba0b5db82c576cf.yaml diff --git a/qiskit/transpiler/passes/calibration/rzx_builder.py b/qiskit/transpiler/passes/calibration/rzx_builder.py index 97799994fd08..4ad964758429 100644 --- a/qiskit/transpiler/passes/calibration/rzx_builder.py +++ b/qiskit/transpiler/passes/calibration/rzx_builder.py @@ -361,7 +361,10 @@ def _check_calibration_type( cr_sched = inst_sched_map.get("ecr", tuple(reversed(qubits))) cal_type = CRCalType.ECR_REVERSE else: - raise QiskitError(f"{repr(cr_sched)} native direction cannot be determined.") + raise QiskitError( + f"Native direction cannot be determined: operation on qubits {qubits} " + f"for the following instruction schedule map:\n{inst_sched_map}" + ) cr_tones = [t[1] for t in filter_instructions(cr_sched, [_filter_cr_tone]).instructions] comp_tones = [t[1] for t in filter_instructions(cr_sched, [_filter_comp_tone]).instructions] diff --git a/releasenotes/notes/fix-exception-decription-3ba0b5db82c576cf.yaml b/releasenotes/notes/fix-exception-decription-3ba0b5db82c576cf.yaml new file mode 100644 index 000000000000..5733e1c85287 --- /dev/null +++ b/releasenotes/notes/fix-exception-decription-3ba0b5db82c576cf.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - Fix a bug in :class:`~.RZXCalibrationBuilder` where calling calibration with wrong parameters + would crash instead of raising exception. diff --git a/test/python/transpiler/test_calibrationbuilder.py b/test/python/transpiler/test_calibrationbuilder.py index aadbc25c0332..7baa39c8ad6a 100644 --- a/test/python/transpiler/test_calibrationbuilder.py +++ b/test/python/transpiler/test_calibrationbuilder.py @@ -16,10 +16,12 @@ import numpy as np from ddt import data, ddt +from qiskit.converters import circuit_to_dag -from qiskit import circuit, schedule +from qiskit import circuit, schedule, QiskitError from qiskit.circuit.library.standard_gates import SXGate, RZGate from qiskit.providers.fake_provider import FakeHanoi # TODO - include FakeHanoiV2, FakeSherbrooke +from qiskit.providers.fake_provider import FakeArmonk from qiskit.pulse import ( ControlChannel, DriveChannel, @@ -247,6 +249,24 @@ def test_rzx_calibration_rotary_pulse_stretch(self, theta: float): test_sched.duration, self.compute_stretch_duration(self.d1p_play(cr_schedule), theta) ) + def test_raise(self): + """Test that the correct error is raised.""" + theta = np.pi / 4 + + qc = circuit.QuantumCircuit(2) + qc.rzx(theta, 0, 1) + dag = circuit_to_dag(qc) + + backend = FakeArmonk() + inst_map = backend.defaults().instruction_schedule_map + _pass = RZXCalibrationBuilder(inst_map) + + qubit_map = {qubit: i for i, qubit in enumerate(dag.qubits)} + with self.assertRaises(QiskitError): + for node in dag.gate_nodes(): + qubits = [qubit_map[q] for q in node.qargs] + _pass.get_calibration(node.op, qubits) + def test_ecr_cx_forward(self): """Test that correct pulse sequence is generated for native CR pair.""" # Sufficiently large angle to avoid minimum duration, i.e. amplitude rescaling From 558fb0ad21658ce696f5229896416cb29400b16f Mon Sep 17 00:00:00 2001 From: Kazuki Tsuoka Date: Thu, 25 May 2023 02:08:28 +0900 Subject: [PATCH 118/172] Fix typo in documentation (#10140) * fix typos * fix lint * fix typo on excitation_preserving.py --- qiskit/circuit/library/generalized_gates/rv.py | 6 ++++-- .../library/n_local/excitation_preserving.py | 4 ++-- .../circuit/library/standard_gates/global_phase.py | 3 +-- qiskit/circuit/library/standard_gates/p.py | 4 ++-- qiskit/circuit/library/standard_gates/r.py | 4 ++-- qiskit/circuit/library/standard_gates/rx.py | 12 ++++++------ qiskit/circuit/library/standard_gates/ry.py | 14 +++++++------- qiskit/circuit/library/standard_gates/rz.py | 2 +- qiskit/circuit/library/standard_gates/u1.py | 6 +++--- qiskit/circuit/library/standard_gates/y.py | 2 +- 10 files changed, 29 insertions(+), 28 deletions(-) diff --git a/qiskit/circuit/library/generalized_gates/rv.py b/qiskit/circuit/library/generalized_gates/rv.py index 04eb70b23e36..a65c9c25ef8a 100644 --- a/qiskit/circuit/library/generalized_gates/rv.py +++ b/qiskit/circuit/library/generalized_gates/rv.py @@ -40,8 +40,10 @@ class RVGate(Gate): \newcommand{\sinc}{\text{sinc}} R(\vec{v}) = e^{-i \vec{v}\cdot\vec{\sigma}} = \begin{pmatrix} - \cos{\th} -i v_z \sinc(\th) & -(i v_x + v_y) \sinc(\th) \\ - -(i v_x - v_y) \sinc(\th) & \cos(\th) + i v_z \sinc(\th) + \cos\left(\th\right) -i v_z \sinc\left(\th\right) + & -(i v_x + v_y) \sinc\left(\th\right) \\ + -(i v_x - v_y) \sinc\left(\th\right) + & \cos\left(\th\right) + i v_z \sinc\left(\th\right) \end{pmatrix} """ diff --git a/qiskit/circuit/library/n_local/excitation_preserving.py b/qiskit/circuit/library/n_local/excitation_preserving.py index d6c3be3b356a..a474e8ceb2c4 100644 --- a/qiskit/circuit/library/n_local/excitation_preserving.py +++ b/qiskit/circuit/library/n_local/excitation_preserving.py @@ -33,8 +33,8 @@ class ExcitationPreserving(TwoLocal): \begin{pmatrix} 1 & 0 & 0 & 0 \\ - 0 & \cos(\th) & -i\sin(\th) & 0 \\ - 0 & -i\sin(\th) & \cos(\th) & 0 \\ + 0 & \cos\left(\th\right) & -i\sin\left(\th\right) & 0 \\ + 0 & -i\sin\left(\th\right) & \cos\left(\th\right) & 0 \\ 0 & 0 & 0 & e^{-i\phi} \end{pmatrix} diff --git a/qiskit/circuit/library/standard_gates/global_phase.py b/qiskit/circuit/library/standard_gates/global_phase.py index a92c8d4c7c9d..e0a0d0e805e1 100644 --- a/qiskit/circuit/library/standard_gates/global_phase.py +++ b/qiskit/circuit/library/standard_gates/global_phase.py @@ -43,7 +43,6 @@ def __init__(self, phase: ParameterValueType, label: Optional[str] = None): super().__init__("global_phase", 0, [phase], label=label) def _define(self): - q = QuantumRegister(0, "q") qc = QuantumCircuit(q, name=self.name, global_phase=self.params[0]) @@ -52,7 +51,7 @@ def _define(self): def inverse(self): r"""Return inverted GLobalPhaseGate gate. - :math:`\text{GlobalPhaseGate}(\lambda){\dagger} = \text{GlobalPhaseGate}(-\lambda)` + :math:`\text{GlobalPhaseGate}(\lambda)^{\dagger} = \text{GlobalPhaseGate}(-\lambda)` """ return GlobalPhaseGate(-self.params[0]) diff --git a/qiskit/circuit/library/standard_gates/p.py b/qiskit/circuit/library/standard_gates/p.py index 67a3b273b3ef..de19ee5d8732 100644 --- a/qiskit/circuit/library/standard_gates/p.py +++ b/qiskit/circuit/library/standard_gates/p.py @@ -238,7 +238,7 @@ def control( return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) def inverse(self): - r"""Return inverted CPhase gate (:math:`CPhase(\lambda){\dagger} = CPhase(-\lambda)`)""" + r"""Return inverted CPhase gate (:math:`CPhase(\lambda)^{\dagger} = CPhase(-\lambda)`)""" return CPhaseGate(-self.params[0], ctrl_state=self.ctrl_state) def __array__(self, dtype=None): @@ -341,5 +341,5 @@ def control( return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) def inverse(self): - r"""Return inverted MCU1 gate (:math:`MCU1(\lambda){\dagger} = MCU1(-\lambda)`)""" + r"""Return inverted MCU1 gate (:math:`MCU1(\lambda)^{\dagger} = MCU1(-\lambda)`)""" return MCPhaseGate(-self.params[0], self.num_ctrl_qubits) diff --git a/qiskit/circuit/library/standard_gates/r.py b/qiskit/circuit/library/standard_gates/r.py index 28ca03385efc..9f05413879c1 100644 --- a/qiskit/circuit/library/standard_gates/r.py +++ b/qiskit/circuit/library/standard_gates/r.py @@ -44,8 +44,8 @@ class RGate(Gate): R(\theta, \phi) = e^{-i \th \left(\cos{\phi} x + \sin{\phi} y\right)} = \begin{pmatrix} - \cos{\th} & -i e^{-i \phi} \sin{\th} \\ - -i e^{i \phi} \sin{\th} & \cos{\th} + \cos\left(\th\right) & -i e^{-i \phi} \sin\left(\th\right) \\ + -i e^{i \phi} \sin\left(\th\right) & \cos\left(\th\right) \end{pmatrix} """ diff --git a/qiskit/circuit/library/standard_gates/rx.py b/qiskit/circuit/library/standard_gates/rx.py index 7eddd05e9510..76721fa84040 100644 --- a/qiskit/circuit/library/standard_gates/rx.py +++ b/qiskit/circuit/library/standard_gates/rx.py @@ -45,8 +45,8 @@ class RXGate(Gate): RX(\theta) = \exp\left(-i \th X\right) = \begin{pmatrix} - \cos{\th} & -i\sin{\th} \\ - -i\sin{\th} & \cos{\th} + \cos\left(\th\right) & -i\sin\left(\th\right) \\ + -i\sin\left(\th\right) & \cos\left(\th\right) \end{pmatrix} """ @@ -137,9 +137,9 @@ class CRXGate(ControlledGate): I \otimes |0\rangle\langle 0| + RX(\theta) \otimes |1\rangle\langle 1| = \begin{pmatrix} 1 & 0 & 0 & 0 \\ - 0 & \cos{\th} & 0 & -i\sin{\th} \\ + 0 & \cos\left(\th\right) & 0 & -i\sin\left(\th\right) \\ 0 & 0 & 1 & 0 \\ - 0 & -i\sin{\th} & 0 & \cos{\th} + 0 & -i\sin\left(\th\right) & 0 & \cos\left(\th\right) \end{pmatrix} .. note:: @@ -165,8 +165,8 @@ class CRXGate(ControlledGate): \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ - 0 & 0 & \cos{\th} & -i\sin{\th} \\ - 0 & 0 & -i\sin{\th} & \cos{\th} + 0 & 0 & \cos\left(\th\right) & -i\sin\left(\th\right) \\ + 0 & 0 & -i\sin\left(\th\right) & \cos\left(\th\right) \end{pmatrix} """ diff --git a/qiskit/circuit/library/standard_gates/ry.py b/qiskit/circuit/library/standard_gates/ry.py index 8cba2ed43252..fd561784f056 100644 --- a/qiskit/circuit/library/standard_gates/ry.py +++ b/qiskit/circuit/library/standard_gates/ry.py @@ -44,8 +44,8 @@ class RYGate(Gate): RY(\theta) = \exp\left(-i \th Y\right) = \begin{pmatrix} - \cos{\th} & -\sin{\th} \\ - \sin{\th} & \cos{\th} + \cos\left(\th\right) & -\sin\left(\th\right) \\ + \sin\left(\th\right) & \cos\left(\th\right) \end{pmatrix} """ @@ -95,7 +95,7 @@ def control( def inverse(self): r"""Return inverted RY gate. - :math:`RY(\lambda){\dagger} = RY(-\lambda)` + :math:`RY(\lambda)^{\dagger} = RY(-\lambda)` """ return RYGate(-self.params[0]) @@ -136,9 +136,9 @@ class CRYGate(ControlledGate): I \otimes |0\rangle\langle 0| + RY(\theta) \otimes |1\rangle\langle 1| = \begin{pmatrix} 1 & 0 & 0 & 0 \\ - 0 & \cos{\th} & 0 & -\sin{\th} \\ + 0 & \cos\left(\th\right) & 0 & -\sin\left(\th\right) \\ 0 & 0 & 1 & 0 \\ - 0 & \sin{\th} & 0 & \cos{\th} + 0 & \sin\left(\th\right) & 0 & \cos\left(\th\right) \end{pmatrix} .. note:: @@ -164,8 +164,8 @@ class CRYGate(ControlledGate): \begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ - 0 & 0 & \cos{\th} & -\sin{\th} \\ - 0 & 0 & \sin{\th} & \cos{\th} + 0 & 0 & \cos\left(\th\right) & -\sin\left(\th\right) \\ + 0 & 0 & \sin\left(\th\right) & \cos\left(\th\right) \end{pmatrix} """ diff --git a/qiskit/circuit/library/standard_gates/rz.py b/qiskit/circuit/library/standard_gates/rz.py index f3da059f82ff..3ae7c62a6913 100644 --- a/qiskit/circuit/library/standard_gates/rz.py +++ b/qiskit/circuit/library/standard_gates/rz.py @@ -106,7 +106,7 @@ def control( def inverse(self): r"""Return inverted RZ gate - :math:`RZ(\lambda){\dagger} = RZ(-\lambda)` + :math:`RZ(\lambda)^{\dagger} = RZ(-\lambda)` """ return RZGate(-self.params[0]) diff --git a/qiskit/circuit/library/standard_gates/u1.py b/qiskit/circuit/library/standard_gates/u1.py index 4dde8058adc4..168eb2e9dac7 100644 --- a/qiskit/circuit/library/standard_gates/u1.py +++ b/qiskit/circuit/library/standard_gates/u1.py @@ -138,7 +138,7 @@ def control( return gate def inverse(self): - r"""Return inverted U1 gate (:math:`U1(\lambda){\dagger} = U1(-\lambda)`)""" + r"""Return inverted U1 gate (:math:`U1(\lambda)^{\dagger} = U1(-\lambda)`)""" return U1Gate(-self.params[0]) def __array__(self, dtype=None): @@ -256,7 +256,7 @@ def control( return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) def inverse(self): - r"""Return inverted CU1 gate (:math:`CU1(\lambda){\dagger} = CU1(-\lambda)`)""" + r"""Return inverted CU1 gate (:math:`CU1(\lambda)^{\dagger} = CU1(-\lambda)`)""" return CU1Gate(-self.params[0], ctrl_state=self.ctrl_state) def __array__(self, dtype=None): @@ -365,5 +365,5 @@ def control( return gate def inverse(self): - r"""Return inverted MCU1 gate (:math:`MCU1(\lambda){\dagger} = MCU1(-\lambda)`)""" + r"""Return inverted MCU1 gate (:math:`MCU1(\lambda)^{\dagger} = MCU1(-\lambda)`)""" return MCU1Gate(-self.params[0], self.num_ctrl_qubits) diff --git a/qiskit/circuit/library/standard_gates/y.py b/qiskit/circuit/library/standard_gates/y.py index 6240a70338ad..871aa04c2e77 100644 --- a/qiskit/circuit/library/standard_gates/y.py +++ b/qiskit/circuit/library/standard_gates/y.py @@ -111,7 +111,7 @@ def control( return super().control(num_ctrl_qubits=num_ctrl_qubits, label=label, ctrl_state=ctrl_state) def inverse(self): - r"""Return inverted Y gate (:math:`Y{\dagger} = Y`)""" + r"""Return inverted Y gate (:math:`Y^{\dagger} = Y`)""" return YGate() # self-inverse def __array__(self, dtype=complex): From 716b648cb993de6e98cab4750374373d6cba2711 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 24 May 2023 20:31:30 +0100 Subject: [PATCH 119/172] Fix `initial_layout` in `transpile` with loose qubits (#10153) The previous logic around building a `Layout` object from an `initial_layout` was still couched in the "registers own bits" model, so could not cope with loose bits. It's most convenient to just build the mapping ourselves during the parsing, since the logic is quite specific to the form of the `initial_layout` argument, rather than being something that's inherently tied to the `Layout` class. --- qiskit/compiler/transpiler.py | 30 ++++++++++++------ ..._layout-loose-qubits-0c59b2d6fb99d7e6.yaml | 6 ++++ test/python/compiler/test_transpiler.py | 31 +++++++++++++++---- 3 files changed, 51 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/fix-initial_layout-loose-qubits-0c59b2d6fb99d7e6.yaml diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py index dc93c85a5fcf..706a223f8c92 100644 --- a/qiskit/compiler/transpiler.py +++ b/qiskit/compiler/transpiler.py @@ -28,7 +28,6 @@ from qiskit import user_config from qiskit.circuit.quantumcircuit import QuantumCircuit from qiskit.circuit.quantumregister import Qubit -from qiskit.converters import isinstanceint, isinstancelist from qiskit.dagcircuit import DAGCircuit from qiskit.providers.backend import Backend from qiskit.providers.models import BackendProperties @@ -757,16 +756,27 @@ def _parse_initial_layout(initial_layout, circuits): def _layout_from_raw(initial_layout, circuit): if initial_layout is None or isinstance(initial_layout, Layout): return initial_layout - elif isinstancelist(initial_layout): - if all(isinstanceint(elem) for elem in initial_layout): - initial_layout = Layout.from_intlist(initial_layout, *circuit.qregs) - elif all(elem is None or isinstance(elem, Qubit) for elem in initial_layout): - initial_layout = Layout.from_qubit_list(initial_layout, *circuit.qregs) - elif isinstance(initial_layout, dict): - initial_layout = Layout(initial_layout) + if isinstance(initial_layout, dict): + return Layout(initial_layout) + # Should be an iterable either of ints or bits/None. + specifier = tuple(initial_layout) + if all(phys is None or isinstance(phys, Qubit) for phys in specifier): + mapping = {phys: virt for phys, virt in enumerate(specifier) if virt is not None} + if len(mapping) != circuit.num_qubits: + raise TranspilerError( + f"'initial_layout' ({len(mapping)}) and circuit ({circuit.num_qubits}) had" + " different numbers of qubits" + ) else: - raise TranspilerError("The initial_layout parameter could not be parsed") - return initial_layout + if len(specifier) != circuit.num_qubits: + raise TranspilerError( + f"'initial_layout' ({len(specifier)}) and circuit ({circuit.num_qubits}) had" + " different numbers of qubits" + ) + if len(specifier) != len(set(specifier)): + raise TranspilerError(f"'initial_layout' contained duplicate entries: {specifier}") + mapping = {int(phys): virt for phys, virt in zip(specifier, circuit.qubits)} + return Layout(mapping) # multiple layouts? if isinstance(initial_layout, list) and any( diff --git a/releasenotes/notes/fix-initial_layout-loose-qubits-0c59b2d6fb99d7e6.yaml b/releasenotes/notes/fix-initial_layout-loose-qubits-0c59b2d6fb99d7e6.yaml new file mode 100644 index 000000000000..4bac5214f5c6 --- /dev/null +++ b/releasenotes/notes/fix-initial_layout-loose-qubits-0c59b2d6fb99d7e6.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Using ``initial_layout`` in calls to :func:`.transpile` will no longer error if the + circuit contains qubits not in any registers, or qubits that exist in more than one + register. See `#10125 `__. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index fe5670aba861..c25425d051f1 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -627,14 +627,9 @@ def test_wrong_initial_layout(self): QuantumRegister(3, "q")[2], ] - with self.assertRaises(TranspilerError) as cm: + with self.assertRaisesRegex(TranspilerError, "different numbers of qubits"): transpile(qc, backend, initial_layout=bad_initial_layout) - self.assertEqual( - "FullAncillaAllocation: The layout refers to a qubit that does not exist in circuit.", - cm.exception.message, - ) - def test_parameterized_circuit_for_simulator(self): """Verify that a parameterized circuit can be transpiled for a simulator backend.""" qr = QuantumRegister(2, name="qr") @@ -1616,6 +1611,30 @@ def test_transpile_identity_circuit_no_target(self, opt_level): result = transpile(qc, optimization_level=opt_level) self.assertEqual(empty_qc, result) + @data(0, 1, 2, 3) + def test_initial_layout_with_loose_qubits(self, opt_level): + """Regression test of gh-10125.""" + qc = QuantumCircuit([Qubit(), Qubit()]) + qc.cx(0, 1) + transpiled = transpile(qc, initial_layout=[1, 0], optimization_level=opt_level) + self.assertIsNotNone(transpiled.layout) + self.assertEqual( + transpiled.layout.initial_layout, Layout({0: qc.qubits[1], 1: qc.qubits[0]}) + ) + + @data(0, 1, 2, 3) + def test_initial_layout_with_overlapping_qubits(self, opt_level): + """Regression test of gh-10125.""" + qr1 = QuantumRegister(2, "qr1") + qr2 = QuantumRegister(bits=qr1[:]) + qc = QuantumCircuit(qr1, qr2) + qc.cx(0, 1) + transpiled = transpile(qc, initial_layout=[1, 0], optimization_level=opt_level) + self.assertIsNotNone(transpiled.layout) + self.assertEqual( + transpiled.layout.initial_layout, Layout({0: qc.qubits[1], 1: qc.qubits[0]}) + ) + @ddt class TestPostTranspileIntegration(QiskitTestCase): From 5013fe2239290414f2cfaafae13c6a9c09ddbbda Mon Sep 17 00:00:00 2001 From: TsafrirA <113579969+TsafrirA@users.noreply.github.com> Date: Thu, 25 May 2023 15:55:34 +0300 Subject: [PATCH 120/172] Minor docs correction for SymbolicPulse library (#10161) * Correct doc string for Sawtooth,Triangle, Cos, Sin * Added a few missing \ --- qiskit/pulse/library/symbolic_pulses.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 2718792ad153..878f6a4741f5 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -1450,7 +1450,7 @@ def Sin( .. math:: - f(x) &= \\text{A}\\sin\\left(2\\pi\text{freq}x+\\text{phase}\\right) , 0 <= x < duration + f(x) = \\text{A}\\sin\\left(2\\pi\\text{freq}x+\\text{phase}\\right) , 0 <= x < duration where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`. @@ -1517,7 +1517,7 @@ def Cos( .. math:: - f(x) &= \\text{A}\\cos\\left(2\\pi\text{freq}x+\\text{phase}\\right) , 0 <= x < duration + f(x) = \\text{A}\\cos\\left(2\\pi\\text{freq}x+\\text{phase}\\right) , 0 <= x < duration where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`. @@ -1584,7 +1584,7 @@ def Sawtooth( .. math:: - f(x) &= 2\\text{A}\\left[g\\left(x\\right)- + f(x) = 2\\text{A}\\left[g\\left(x\\right)- \\lfloor g\\left(x\\right)+\\frac{1}{2}\\rfloor\\right] where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, @@ -1655,7 +1655,7 @@ def Triangle( .. math:: - f(x) &= \\text{A}\\left[\\text{sawtooth}\\left(x\\right)right] , 0 <= x < duration + f(x) = \\text{A}\\left[\\text{sawtooth}\\left(x\\right)\\right] , 0 <= x < duration where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, and :math:`\\text{sawtooth}\\left(x\\right)` is a sawtooth wave with the same frequency From 788b89d9855dc89eb7770435f8b500790fe3b5bc Mon Sep 17 00:00:00 2001 From: Alexander Ivrii Date: Fri, 26 May 2023 21:49:15 +0300 Subject: [PATCH 121/172] Fixing BlockCollapser with Clbits (#9823) * Bug fix: collapsing blocks with clbits Co-authored-by: chriseclectic * release notes * similar fix to DAGDependency * additional fixes to handle classical bits in conditions * fixing lint issue * Fixing handling of conditions over full registers, following review comments --------- Co-authored-by: chriseclectic --- qiskit/dagcircuit/collect_blocks.py | 41 ++- qiskit/dagcircuit/dagcircuit.py | 7 +- qiskit/dagcircuit/dagdependency.py | 14 +- ...collapse-with-clbits-e14766353303d442.yaml | 9 + test/python/dagcircuit/test_collect_blocks.py | 324 +++++++++++++++++- 5 files changed, 375 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/fix-collapse-with-clbits-e14766353303d442.yaml diff --git a/qiskit/dagcircuit/collect_blocks.py b/qiskit/dagcircuit/collect_blocks.py index ed4df4c38b30..3c09d5dcb82b 100644 --- a/qiskit/dagcircuit/collect_blocks.py +++ b/qiskit/dagcircuit/collect_blocks.py @@ -14,7 +14,8 @@ """Various ways to divide a DAG into blocks of nodes, to split blocks of nodes into smaller sub-blocks, and to consolidate blocks.""" -from qiskit.circuit import QuantumCircuit, CircuitInstruction +from qiskit.circuit import QuantumCircuit, CircuitInstruction, ClassicalRegister +from qiskit.circuit.controlflow.condition import condition_bits from . import DAGOpNode, DAGCircuit, DAGDependency from .exceptions import DAGCircuitError @@ -254,22 +255,46 @@ def collapse_to_operation(self, blocks, collapse_fn): then uses collapse_fn to collapse this circuit into a single operation. """ global_index_map = {wire: idx for idx, wire in enumerate(self.dag.qubits)} + global_index_map.update({wire: idx for idx, wire in enumerate(self.dag.clbits)}) + for block in blocks: - # Find the set of qubits used in this block (which might be much smaller than - # the set of all qubits). + # Find the sets of qubits/clbits used in this block (which might be much smaller + # than the set of all qubits/clbits). cur_qubits = set() + cur_clbits = set() + + # Additionally, find the set of classical registers used in conditions over full registers + # (in such a case, we need to add that register to the block circuit, not just its clbits). + cur_clregs = [] + for node in block: cur_qubits.update(node.qargs) - - # For reproducibility, order these qubits compatibly with the global order. + cur_clbits.update(node.cargs) + cond = getattr(node.op, "condition", None) + if cond is not None: + cur_clbits.update(condition_bits(cond)) + if isinstance(cond[0], ClassicalRegister): + cur_clregs.append(cond[0]) + + # For reproducibility, order these qubits/clbits compatibly with the global order. sorted_qubits = sorted(cur_qubits, key=lambda x: global_index_map[x]) + sorted_clbits = sorted(cur_clbits, key=lambda x: global_index_map[x]) + + qc = QuantumCircuit(sorted_qubits, sorted_clbits) + + # Add classical registers used in conditions over registers + for reg in cur_clregs: + qc.add_register(reg) # Construct a quantum circuit from the nodes in the block, remapping the qubits. wire_pos_map = {qb: ix for ix, qb in enumerate(sorted_qubits)} - qc = QuantumCircuit(len(cur_qubits)) + wire_pos_map.update({qb: ix for ix, qb in enumerate(sorted_clbits)}) + for node in block: - remapped_qubits = [wire_pos_map[qarg] for qarg in node.qargs] - qc.append(CircuitInstruction(node.op, remapped_qubits, node.cargs)) + instructions = qc.append(CircuitInstruction(node.op, node.qargs, node.cargs)) + cond = getattr(node.op, "condition", None) + if cond is not None: + instructions.c_if(*cond) # Collapse this quantum circuit into an operation. op = collapse_fn(qc) diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index fbc60591d1f7..f40015d7f078 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -30,6 +30,7 @@ import rustworkx as rx from qiskit.circuit import ControlFlowOp, ForLoopOp, IfElseOp, WhileLoopOp, SwitchCaseOp +from qiskit.circuit.controlflow.condition import condition_bits from qiskit.circuit.exceptions import CircuitError from qiskit.circuit.quantumregister import QuantumRegister, Qubit from qiskit.circuit.classicalregister import ClassicalRegister, Clbit @@ -1129,8 +1130,10 @@ def replace_block_with_op(self, node_block, op, wire_pos_map, cycle_check=True): for nd in node_block: block_qargs |= set(nd.qargs) - if isinstance(nd, DAGOpNode) and getattr(nd.op, "condition", None): - block_cargs |= set(nd.cargs) + block_cargs |= set(nd.cargs) + cond = getattr(nd.op, "condition", None) + if cond is not None: + block_cargs.update(condition_bits(cond)) # Create replacement node new_node = DAGOpNode( diff --git a/qiskit/dagcircuit/dagdependency.py b/qiskit/dagcircuit/dagdependency.py index 1ed5eb96e113..128129e76372 100644 --- a/qiskit/dagcircuit/dagdependency.py +++ b/qiskit/dagcircuit/dagdependency.py @@ -19,6 +19,7 @@ import rustworkx as rx +from qiskit.circuit.controlflow.condition import condition_bits from qiskit.circuit.quantumregister import QuantumRegister, Qubit from qiskit.circuit.classicalregister import ClassicalRegister, Clbit from qiskit.dagcircuit.exceptions import DAGDependencyError @@ -394,11 +395,8 @@ def _create_op_node(self, operation, qargs, cargs): # (1) cindices_list are specific to template optimization and should not be computed # in this place. # (2) Template optimization pass needs currently does not handle general conditions. - if isinstance(operation.condition[0], Clbit): - condition_bits = [operation.condition[0]] - else: - condition_bits = operation.condition[0] - cindices_list = [self.clbits.index(clbit) for clbit in condition_bits] + cond_bits = condition_bits(operation.condition) + cindices_list = [self.clbits.index(clbit) for clbit in cond_bits] else: cindices_list = [] else: @@ -591,8 +589,10 @@ def replace_block_with_op(self, node_block, op, wire_pos_map, cycle_check=True): for nd in node_block: block_qargs |= set(nd.qargs) - if nd.op.condition: - block_cargs |= set(nd.cargs) + block_cargs |= set(nd.cargs) + cond = getattr(nd.op, "condition", None) + if cond is not None: + block_cargs.update(condition_bits(cond)) # Create replacement node new_node = self._create_op_node( diff --git a/releasenotes/notes/fix-collapse-with-clbits-e14766353303d442.yaml b/releasenotes/notes/fix-collapse-with-clbits-e14766353303d442.yaml new file mode 100644 index 000000000000..ef3e073e28af --- /dev/null +++ b/releasenotes/notes/fix-collapse-with-clbits-e14766353303d442.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed a bug in :class:`~BlockCollapser` where classical bits were ignored when collapsing + a block of nodes. + - | + Fixed a bug in :meth:`~qiskit.dagcircuit.DAGCircuit.replace_block_with_op` and + :meth:`~qiskit.dagcircuit.DAGDependency.replace_block_with_op` + that led to ignoring classical bits. diff --git a/test/python/dagcircuit/test_collect_blocks.py b/test/python/dagcircuit/test_collect_blocks.py index 6bfc5756d9df..fcafafa753af 100644 --- a/test/python/dagcircuit/test_collect_blocks.py +++ b/test/python/dagcircuit/test_collect_blocks.py @@ -15,10 +15,17 @@ import unittest -from qiskit.converters import circuit_to_dag, circuit_to_dagdependency +from qiskit import QuantumRegister, ClassicalRegister +from qiskit.converters import ( + circuit_to_dag, + circuit_to_dagdependency, + circuit_to_instruction, + dag_to_circuit, + dagdependency_to_circuit, +) from qiskit.test import QiskitTestCase -from qiskit.circuit import QuantumCircuit -from qiskit.dagcircuit.collect_blocks import BlockCollector, BlockSplitter +from qiskit.circuit import QuantumCircuit, Measure, Clbit +from qiskit.dagcircuit.collect_blocks import BlockCollector, BlockSplitter, BlockCollapser class TestCollectBlocks(QiskitTestCase): @@ -449,6 +456,317 @@ def test_do_not_split_blocks(self): ) self.assertEqual(len(blocks), 1) + def test_collect_blocks_with_cargs(self): + """Test collecting and collapsing blocks with classical bits appearing as cargs.""" + + qc = QuantumCircuit(3) + qc.h(0) + qc.h(1) + qc.h(2) + qc.measure_all() + + dag = circuit_to_dag(qc) + + # Collect all measure instructions + blocks = BlockCollector(dag).collect_all_matching_blocks( + lambda node: isinstance(node.op, Measure), split_blocks=False, min_block_size=1 + ) + + # We should have a single block consisting of 3 measures + self.assertEqual(len(blocks), 1) + self.assertEqual(len(blocks[0]), 3) + self.assertEqual(blocks[0][0].op, Measure()) + self.assertEqual(blocks[0][1].op, Measure()) + self.assertEqual(blocks[0][2].op, Measure()) + + def _collapse_fn(circuit): + op = circuit_to_instruction(circuit) + op.name = "COLLAPSED" + return op + + # Collapse block with measures into a single "COLLAPSED" block + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + collapsed_qc = dag_to_circuit(dag) + + self.assertEqual(len(collapsed_qc.data), 5) + self.assertEqual(collapsed_qc.data[0].operation.name, "h") + self.assertEqual(collapsed_qc.data[1].operation.name, "h") + self.assertEqual(collapsed_qc.data[2].operation.name, "h") + self.assertEqual(collapsed_qc.data[3].operation.name, "barrier") + self.assertEqual(collapsed_qc.data[4].operation.name, "COLLAPSED") + self.assertEqual(collapsed_qc.data[4].operation.definition.num_qubits, 3) + self.assertEqual(collapsed_qc.data[4].operation.definition.num_clbits, 3) + + def test_collect_blocks_with_cargs_dagdependency(self): + """Test collecting and collapsing blocks with classical bits appearing as cargs, + using DAGDependency.""" + + qc = QuantumCircuit(3) + qc.h(0) + qc.h(1) + qc.h(2) + qc.measure_all() + + dag = circuit_to_dagdependency(qc) + + # Collect all measure instructions + blocks = BlockCollector(dag).collect_all_matching_blocks( + lambda node: isinstance(node.op, Measure), split_blocks=False, min_block_size=1 + ) + + # We should have a single block consisting of 3 measures + self.assertEqual(len(blocks), 1) + self.assertEqual(len(blocks[0]), 3) + self.assertEqual(blocks[0][0].op, Measure()) + self.assertEqual(blocks[0][1].op, Measure()) + self.assertEqual(blocks[0][2].op, Measure()) + + def _collapse_fn(circuit): + op = circuit_to_instruction(circuit) + op.name = "COLLAPSED" + return op + + # Collapse block with measures into a single "COLLAPSED" block + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + collapsed_qc = dagdependency_to_circuit(dag) + + self.assertEqual(len(collapsed_qc.data), 5) + self.assertEqual(collapsed_qc.data[0].operation.name, "h") + self.assertEqual(collapsed_qc.data[1].operation.name, "h") + self.assertEqual(collapsed_qc.data[2].operation.name, "h") + self.assertEqual(collapsed_qc.data[3].operation.name, "barrier") + self.assertEqual(collapsed_qc.data[4].operation.name, "COLLAPSED") + self.assertEqual(collapsed_qc.data[4].operation.definition.num_qubits, 3) + self.assertEqual(collapsed_qc.data[4].operation.definition.num_clbits, 3) + + def test_collect_blocks_with_clbits(self): + """Test collecting and collapsing blocks with classical bits appearing under + condition.""" + + qc = QuantumCircuit(4, 3) + qc.cx(0, 1).c_if(0, 1) + qc.cx(2, 3) + qc.cx(1, 2) + qc.cx(0, 1) + qc.cx(2, 3).c_if(1, 0) + + dag = circuit_to_dag(qc) + + # Collect all cx gates (including the conditional ones) + blocks = BlockCollector(dag).collect_all_matching_blocks( + lambda node: node.op.name == "cx", split_blocks=False, min_block_size=1 + ) + + # We should have a single block consisting of all CX nodes + self.assertEqual(len(blocks), 1) + self.assertEqual(len(blocks[0]), 5) + + def _collapse_fn(circuit): + op = circuit_to_instruction(circuit) + op.name = "COLLAPSED" + return op + + # Collapse block with measures into a single "COLLAPSED" block + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + collapsed_qc = dag_to_circuit(dag) + + self.assertEqual(len(collapsed_qc.data), 1) + self.assertEqual(collapsed_qc.data[0].operation.name, "COLLAPSED") + self.assertEqual(collapsed_qc.data[0].operation.definition.num_qubits, 4) + self.assertEqual(collapsed_qc.data[0].operation.definition.num_clbits, 2) + + def test_collect_blocks_with_clbits_dagdependency(self): + """Test collecting and collapsing blocks with classical bits appearing + under conditions, using DAGDependency.""" + + qc = QuantumCircuit(4, 3) + qc.cx(0, 1).c_if(0, 1) + qc.cx(2, 3) + qc.cx(1, 2) + qc.cx(0, 1) + qc.cx(2, 3).c_if(1, 0) + + dag = circuit_to_dagdependency(qc) + + # Collect all cx gates (including the conditional ones) + blocks = BlockCollector(dag).collect_all_matching_blocks( + lambda node: node.op.name == "cx", split_blocks=False, min_block_size=1 + ) + + # We should have a single block consisting of all CX nodes + self.assertEqual(len(blocks), 1) + self.assertEqual(len(blocks[0]), 5) + + def _collapse_fn(circuit): + op = circuit_to_instruction(circuit) + op.name = "COLLAPSED" + return op + + # Collapse block with measures into a single "COLLAPSED" block + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + collapsed_qc = dagdependency_to_circuit(dag) + + self.assertEqual(len(collapsed_qc.data), 1) + self.assertEqual(collapsed_qc.data[0].operation.name, "COLLAPSED") + self.assertEqual(collapsed_qc.data[0].operation.definition.num_qubits, 4) + self.assertEqual(collapsed_qc.data[0].operation.definition.num_clbits, 2) + + def test_collect_blocks_with_clbits2(self): + """Test collecting and collapsing blocks with classical bits appearing under + condition.""" + + qreg = QuantumRegister(4, "qr") + creg = ClassicalRegister(3, "cr") + cbit = Clbit() + + qc = QuantumCircuit(qreg, creg, [cbit]) + qc.cx(0, 1).c_if(creg[1], 1) + qc.cx(2, 3).c_if(cbit, 0) + qc.cx(1, 2) + qc.cx(0, 1).c_if(creg[2], 1) + + dag = circuit_to_dag(qc) + + # Collect all cx gates (including the conditional ones) + blocks = BlockCollector(dag).collect_all_matching_blocks( + lambda node: node.op.name == "cx", split_blocks=False, min_block_size=1 + ) + + # We should have a single block consisting of all CX nodes + self.assertEqual(len(blocks), 1) + self.assertEqual(len(blocks[0]), 4) + + def _collapse_fn(circuit): + op = circuit_to_instruction(circuit) + op.name = "COLLAPSED" + return op + + # Collapse block with measures into a single "COLLAPSED" block + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + collapsed_qc = dag_to_circuit(dag) + + self.assertEqual(len(collapsed_qc.data), 1) + self.assertEqual(collapsed_qc.data[0].operation.name, "COLLAPSED") + self.assertEqual(collapsed_qc.data[0].operation.definition.num_qubits, 4) + self.assertEqual(collapsed_qc.data[0].operation.definition.num_clbits, 3) + + def test_collect_blocks_with_clbits2_dagdependency(self): + """Test collecting and collapsing blocks with classical bits appearing under + condition, using DAGDependency.""" + + qreg = QuantumRegister(4, "qr") + creg = ClassicalRegister(3, "cr") + cbit = Clbit() + + qc = QuantumCircuit(qreg, creg, [cbit]) + qc.cx(0, 1).c_if(creg[1], 1) + qc.cx(2, 3).c_if(cbit, 0) + qc.cx(1, 2) + qc.cx(0, 1).c_if(creg[2], 1) + + dag = circuit_to_dag(qc) + + # Collect all cx gates (including the conditional ones) + blocks = BlockCollector(dag).collect_all_matching_blocks( + lambda node: node.op.name == "cx", split_blocks=False, min_block_size=1 + ) + + # We should have a single block consisting of all CX nodes + self.assertEqual(len(blocks), 1) + self.assertEqual(len(blocks[0]), 4) + + def _collapse_fn(circuit): + op = circuit_to_instruction(circuit) + op.name = "COLLAPSED" + return op + + # Collapse block with measures into a single "COLLAPSED" block + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + collapsed_qc = dag_to_circuit(dag) + + self.assertEqual(len(collapsed_qc.data), 1) + self.assertEqual(collapsed_qc.data[0].operation.name, "COLLAPSED") + self.assertEqual(collapsed_qc.data[0].operation.definition.num_qubits, 4) + self.assertEqual(collapsed_qc.data[0].operation.definition.num_clbits, 3) + + def test_collect_blocks_with_cregs(self): + """Test collecting and collapsing blocks with classical registers appearing under + condition.""" + + qreg = QuantumRegister(4, "qr") + creg = ClassicalRegister(3, "cr") + creg2 = ClassicalRegister(2, "cr2") + + qc = QuantumCircuit(qreg, creg, creg2) + qc.cx(0, 1).c_if(creg, 3) + qc.cx(1, 2) + qc.cx(0, 1).c_if(creg[2], 1) + + dag = circuit_to_dag(qc) + + # Collect all cx gates (including the conditional ones) + blocks = BlockCollector(dag).collect_all_matching_blocks( + lambda node: node.op.name == "cx", split_blocks=False, min_block_size=1 + ) + + # We should have a single block consisting of all CX nodes + self.assertEqual(len(blocks), 1) + self.assertEqual(len(blocks[0]), 3) + + def _collapse_fn(circuit): + op = circuit_to_instruction(circuit) + op.name = "COLLAPSED" + return op + + # Collapse block with measures into a single "COLLAPSED" block + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + collapsed_qc = dag_to_circuit(dag) + + self.assertEqual(len(collapsed_qc.data), 1) + self.assertEqual(collapsed_qc.data[0].operation.name, "COLLAPSED") + self.assertEqual(len(collapsed_qc.data[0].operation.definition.cregs), 1) + self.assertEqual(collapsed_qc.data[0].operation.definition.num_qubits, 3) + self.assertEqual(collapsed_qc.data[0].operation.definition.num_clbits, 3) + + def test_collect_blocks_with_cregs_dagdependency(self): + """Test collecting and collapsing blocks with classical registers appearing under + condition, using DAGDependency.""" + + qreg = QuantumRegister(4, "qr") + creg = ClassicalRegister(3, "cr") + creg2 = ClassicalRegister(2, "cr2") + + qc = QuantumCircuit(qreg, creg, creg2) + qc.cx(0, 1).c_if(creg, 3) + qc.cx(1, 2) + qc.cx(0, 1).c_if(creg[2], 1) + + dag = circuit_to_dagdependency(qc) + + # Collect all cx gates (including the conditional ones) + blocks = BlockCollector(dag).collect_all_matching_blocks( + lambda node: node.op.name == "cx", split_blocks=False, min_block_size=1 + ) + + # We should have a single block consisting of all CX nodes + self.assertEqual(len(blocks), 1) + self.assertEqual(len(blocks[0]), 3) + + def _collapse_fn(circuit): + op = circuit_to_instruction(circuit) + op.name = "COLLAPSED" + return op + + # Collapse block with measures into a single "COLLAPSED" block + dag = BlockCollapser(dag).collapse_to_operation(blocks, _collapse_fn) + collapsed_qc = dagdependency_to_circuit(dag) + + self.assertEqual(len(collapsed_qc.data), 1) + self.assertEqual(collapsed_qc.data[0].operation.name, "COLLAPSED") + self.assertEqual(len(collapsed_qc.data[0].operation.definition.cregs), 1) + self.assertEqual(collapsed_qc.data[0].operation.definition.num_qubits, 3) + self.assertEqual(collapsed_qc.data[0].operation.definition.num_clbits, 3) + if __name__ == "__main__": unittest.main() From 68a603d140a92ea6ce7173b1886aeaad2206e585 Mon Sep 17 00:00:00 2001 From: Blesso Abraham <73220998+blesso1quanta@users.noreply.github.com> Date: Tue, 30 May 2023 20:40:18 +0530 Subject: [PATCH 122/172] Update __init__.py (#10175) I cleared a small typo in this file as as to as a --- qiskit/circuit/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/__init__.py b/qiskit/circuit/__init__.py index 083325efad2f..d319cabc418c 100644 --- a/qiskit/circuit/__init__.py +++ b/qiskit/circuit/__init__.py @@ -26,7 +26,7 @@ gates, measurements and resets, which may be conditioned on real-time classical computation. A set of quantum gates is said to be universal if any unitary transformation of the quantum data can be efficiently approximated arbitrarily well -as as sequence of gates in the set. Any quantum program can be represented by a +as a sequence of gates in the set. Any quantum program can be represented by a sequence of quantum circuits and classical near-time computation. In Qiskit, this core element is represented by the :class:`QuantumCircuit` class. From 76850e1fdf9135f2072c8a9624d3c58b6f989ef9 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 30 May 2023 17:33:06 +0100 Subject: [PATCH 123/172] Fix top-level `switch` statements in `QuantumCircuit.compose` (#10164) * Fix top-level `switch` statements in `QuantumCircuit.compose` The register-mapping code was not being applied to `SwitchCaseOp.target` in the same way that it is for conditions. This commit does not change any behaviour about recursing into _nested_ control-flow blocks, which still likely have problems with composition. * Apply suggestions from review --- qiskit/circuit/quantumcircuit.py | 41 +++++++++++++------ .../fix-compose-switch-19ada3828d939353.yaml | 5 +++ test/python/circuit/test_compose.py | 41 +++++++++++++++++++ 3 files changed, 74 insertions(+), 13 deletions(-) create mode 100644 releasenotes/notes/fix-compose-switch-19ada3828d939353.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 9299ae054d73..98e056c2a2a8 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -877,6 +877,9 @@ def compose( lcr_1: 0 ═══════════ lcr_1: 0 ═══════════════════════ """ + # pylint: disable=cyclic-import + from qiskit.circuit.controlflow.switch_case import SwitchCaseOp + if inplace and front and self._control_flow_scopes: # If we're composing onto ourselves while in a stateful control-flow builder context, # there's no clear meaning to composition to the "front" of the circuit. @@ -955,30 +958,42 @@ def compose( ) edge_map.update(zip(other.clbits, dest.cbit_argument_conversion(clbits))) + # Cache for `map_register_to_dest`. + _map_register_cache = {} + + def map_register_to_dest(theirs): + """Map the target's registers to suitable equivalents in the destination, adding an + extra one if there's no exact match.""" + if theirs.name in _map_register_cache: + return _map_register_cache[theirs.name] + mapped_bits = [edge_map[bit] for bit in theirs] + for ours in dest.cregs: + if mapped_bits == list(ours): + mapped_theirs = ours + break + else: + mapped_theirs = ClassicalRegister(bits=mapped_bits) + dest.add_register(mapped_theirs) + _map_register_cache[theirs.name] = mapped_theirs + return mapped_theirs + mapped_instrs: list[CircuitInstruction] = [] - condition_register_map = {} for instr in other.data: n_qargs: list[Qubit] = [edge_map[qarg] for qarg in instr.qubits] n_cargs: list[Clbit] = [edge_map[carg] for carg in instr.clbits] n_op = instr.operation.copy() - # Map their registers over to ours, adding an extra one if there's no exact match. if getattr(n_op, "condition", None) is not None: target, value = n_op.condition if isinstance(target, Clbit): n_op.condition = (edge_map[target], value) else: - if target.name not in condition_register_map: - mapped_bits = [edge_map[bit] for bit in target] - for our_creg in dest.cregs: - if mapped_bits == list(our_creg): - new_target = our_creg - break - else: - new_target = ClassicalRegister(bits=[edge_map[bit] for bit in target]) - dest.add_register(new_target) - condition_register_map[target.name] = new_target - n_op.condition = (condition_register_map[target.name], value) + n_op.condition = (map_register_to_dest(target), value) + elif isinstance(n_op, SwitchCaseOp): + if isinstance(n_op.target, Clbit): + n_op.target = edge_map[n_op.target] + else: + n_op.target = map_register_to_dest(n_op.target) mapped_instrs.append(CircuitInstruction(n_op, n_qargs, n_cargs)) diff --git a/releasenotes/notes/fix-compose-switch-19ada3828d939353.yaml b/releasenotes/notes/fix-compose-switch-19ada3828d939353.yaml new file mode 100644 index 000000000000..da83aafe33c7 --- /dev/null +++ b/releasenotes/notes/fix-compose-switch-19ada3828d939353.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed a bug in :meth:`.QuantumCircuit.compose` where the :attr:`.SwitchCaseOp.target` attribute + in the subcircuit would not get mapped to a register in the base circuit correctly. diff --git a/test/python/circuit/test_compose.py b/test/python/circuit/test_compose.py index 3fc745a9a705..0f77493361fb 100644 --- a/test/python/circuit/test_compose.py +++ b/test/python/circuit/test_compose.py @@ -29,6 +29,8 @@ Parameter, Gate, Instruction, + CASE_DEFAULT, + SwitchCaseOp, ) from qiskit.circuit.library import HGate, RZGate, CXGate, CCXGate, TwoLocal from qiskit.test import QiskitTestCase @@ -477,6 +479,45 @@ def test_compose_conditional_no_match(self): self.assertEqual(z.condition[1], 1) self.assertIs(x.condition[0][0], test.clbits[1]) + def test_compose_switch_match(self): + """Test that composition containing a `switch` with a register that matches proceeds + correctly.""" + case_0 = QuantumCircuit(1, 2) + case_0.x(0) + case_1 = QuantumCircuit(1, 2) + case_1.z(0) + case_default = QuantumCircuit(1, 2) + cr = ClassicalRegister(2, "target") + right = QuantumCircuit(QuantumRegister(1), cr) + right.switch(cr, [(0, case_0), (1, case_1), (CASE_DEFAULT, case_default)], [0], [0, 1]) + + test = QuantumCircuit(QuantumRegister(3), cr, ClassicalRegister(2)).compose( + right, [1], [0, 1] + ) + + expected = test.copy_empty_like() + expected.switch(cr, [(0, case_0), (1, case_1), (CASE_DEFAULT, case_default)], [1], [0, 1]) + self.assertEqual(test, expected) + + def test_compose_switch_no_match(self): + """Test that composition containing a `switch` with a register that matches proceeds + correctly.""" + case_0 = QuantumCircuit(1, 2) + case_0.x(0) + case_1 = QuantumCircuit(1, 2) + case_1.z(0) + case_default = QuantumCircuit(1, 2) + cr = ClassicalRegister(2, "target") + right = QuantumCircuit(QuantumRegister(1), cr) + right.switch(cr, [(0, case_0), (1, case_1), (CASE_DEFAULT, case_default)], [0], [0, 1]) + test = QuantumCircuit(3, 3).compose(right, [1], [0, 1]) + + self.assertEqual(len(test.data), 1) + self.assertIsInstance(test.data[0].operation, SwitchCaseOp) + target = test.data[0].operation.target + self.assertIn(target, test.cregs) + self.assertEqual(list(target), test.clbits[0:2]) + def test_compose_gate(self): """Composing with a gate. From 4762e26708fbc11d3b051dade4cf90b1d16bfb25 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 30 May 2023 13:21:47 -0400 Subject: [PATCH 124/172] Fix PassManagerConfig.from_backend with BackendV1 and no CouplingMap (#10172) * Fix PassManagerConfig.from_backend with BackendV1 and no CouplingMap This commit fixes an issue in the PassManagerConfig.from_backend constructor method when using BackendV1 based simulator backends. The pass was incorrectly handling the case when the backend configuration didn't have a coupling map defined and incorrectly creating a coupling map with 0 qubits instead of using None to indicate the lack of connectivity. This has been fixed so the coupling map creation is skipped if there is no coupling map attribute in the backend's configuration. Fixes #10171 * Fix tests --- qiskit/transpiler/passmanager_config.py | 4 +++- .../fix-pm-config-from-backend-f3b71b11858b4f08.yaml | 9 +++++++++ test/python/transpiler/test_passmanager_config.py | 3 ++- 3 files changed, 14 insertions(+), 2 deletions(-) create mode 100644 releasenotes/notes/fix-pm-config-from-backend-f3b71b11858b4f08.yaml diff --git a/qiskit/transpiler/passmanager_config.py b/qiskit/transpiler/passmanager_config.py index 5c1720ccb1d6..ae1f43b3c243 100644 --- a/qiskit/transpiler/passmanager_config.py +++ b/qiskit/transpiler/passmanager_config.py @@ -144,7 +144,9 @@ def from_backend(cls, backend, **pass_manager_options): res.inst_map = backend.instruction_schedule_map if res.coupling_map is None: if backend_version < 2: - res.coupling_map = CouplingMap(getattr(config, "coupling_map", None)) + cmap_edge_list = getattr(config, "coupling_map", None) + if cmap_edge_list is not None: + res.coupling_map = CouplingMap(cmap_edge_list) else: res.coupling_map = backend.coupling_map if res.instruction_durations is None: diff --git a/releasenotes/notes/fix-pm-config-from-backend-f3b71b11858b4f08.yaml b/releasenotes/notes/fix-pm-config-from-backend-f3b71b11858b4f08.yaml new file mode 100644 index 000000000000..c85355478f06 --- /dev/null +++ b/releasenotes/notes/fix-pm-config-from-backend-f3b71b11858b4f08.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed an issue with the :meth:`.PassManagerConfig.from_backend` constructor + when building a :class:`~.PassManagerConfig` object from a :class:`~.BackendV1` + instance that didn't have a coupling map attribute defined. Previously, the + constructor would incorrectly create a :class:`~.CouplingMap` object with + 0 qubits instead of using ``None``. + Fixed `#10171 `__ diff --git a/test/python/transpiler/test_passmanager_config.py b/test/python/transpiler/test_passmanager_config.py index d78b52e86542..96721e1884a3 100644 --- a/test/python/transpiler/test_passmanager_config.py +++ b/test/python/transpiler/test_passmanager_config.py @@ -86,6 +86,7 @@ def test_simulator_backend_v1(self): config = PassManagerConfig.from_backend(backend) self.assertIsInstance(config, PassManagerConfig) self.assertIsNone(config.inst_map) + self.assertIsNone(config.coupling_map) def test_invalid_user_option(self): """Test from_backend() with an invalid user option.""" @@ -103,7 +104,7 @@ def test_str(self): initial_layout: None basis_gates: ['id', 'rz', 'sx', 'x'] inst_map: None - coupling_map: + coupling_map: None layout_method: None routing_method: None translation_method: None From 291824a392f131e3b7d7ffca4c6fafdecf72c007 Mon Sep 17 00:00:00 2001 From: Luciano Bello Date: Tue, 30 May 2023 21:44:48 +0200 Subject: [PATCH 125/172] some phrasing in the migration guides (#10083) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * some phrasing in the migration guides * more changes * Update docs/migration_guides/algorithms_migration.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/migration_guides/qi_migration.rst Co-authored-by: Abby Mitchell <23662430+javabster@users.noreply.github.com> * rephrase a description for BackendEstimator and BackendSampler * redo intro to qi_migration * readjust opflow * Update docs/migration_guides/algorithms_migration.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/migration_guides/qi_migration.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/migration_guides/qi_migration.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * algorithm pass * Update docs/migration_guides/qi_migration.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/migration_guides/qi_migration.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/migration_guides/qi_migration.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/migration_guides/qi_migration.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/migration_guides/algorithms_migration.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * Update docs/migration_guides/opflow_migration.rst Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> * services with native primitive implementations * typo --------- Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com> Co-authored-by: Abby Mitchell <23662430+javabster@users.noreply.github.com> --- .../migration_guides/algorithms_migration.rst | 20 +++++----- docs/migration_guides/opflow_migration.rst | 7 +--- docs/migration_guides/qi_migration.rst | 39 ++++++++++--------- 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/docs/migration_guides/algorithms_migration.rst b/docs/migration_guides/algorithms_migration.rst index 033c662cc547..b7b7ccb11380 100644 --- a/docs/migration_guides/algorithms_migration.rst +++ b/docs/migration_guides/algorithms_migration.rst @@ -74,15 +74,17 @@ How to choose a primitive configuration for your algorithm *Back to* `TL;DR`_ -The classes in :mod:`qiskit.algorithms` state the base class primitive type (``Sampler``/``Estimator``) -they require for their initialization. Once the primitive type is known, you can choose between -four different primitive implementations, depending on how you want to configure your execution: +The classes in +:mod:`qiskit.algorithms` are initialized with any implementation of :class:`qiskit.primitive.BaseSampler` or class:`qiskit.primitive.BaseEstimator`. - a. Using **local** statevector simulators for quick prototyping: **Reference Primitives** in :mod:`qiskit.primitives` - b. Using **local** Aer simulators for finer algorithm tuning: **Aer Primitives** in :mod:`qiskit_aer.primitives` - c. Accessing backends using the **Qiskit Runtime Service**: **Runtime Primitives** in :mod:`qiskit_ibm_runtime` - d. Accessing backends using a **non-Runtime-enabled provider**: **Backend Primitives** in :mod:`qiskit.primitives` +Once the kind of primitive is known, you can choose between the primitive implementations that better adjust to your case. For example: + a. For quick prototyping, you can use the **reference implementations of primitives** included in Qiskit: :class:`qiskit.primitives.Sampler` and :class:`qiskit.primitives.Estimator`. + b. For finer algorithm tuning, a local simulator such as the **primitive implementation in Aer**: :class:`qiskit_aer.primitives.Sampler` and :class:`qiskit_aer.primitives.Estimator`. + c. For executing in quantum hardware you can: + + * access services with native primitive implementations, such as **IBM's Qiskit Runtime service** via :class:`qiskit_ibm_runtime.Sampler` and :class:`qiskit_ibm_runtime.Estimator` + * Wrap any backend with **Backend Primitives** (:class:`~qiskit.primitives.BackendSampler` and :class:`~qiskit.primitives.BackendEstimator`). These wrappers implement a primitive interface on top of a backend that only supports ``Backend.run()``. For more detailed information and examples, particularly on the use of the **Backend Primitives**, please refer to the `Quantum Instance migration guide `_. @@ -133,7 +135,7 @@ In this guide, we will cover 3 different common configurations for algorithms th from qiskit_aer.primitives import Sampler, Estimator - - Runtime Primitives with default configuration (see `VQD`_ example): + - IBM's Qiskit Runtime Primitives with default configuration (see `VQD`_ example): .. code-block:: python @@ -249,7 +251,7 @@ The legacy :class:`qiskit.algorithms.minimum_eigen_solvers.VQE` class has now be .. testcode:: - from qiskit.algorithms.minimum_eigensolvers import VQE # new import!!! + from qiskit.algorithms.minimum_eigensolvers import VQE # new import!!! from qiskit.algorithms.optimizers import SPSA from qiskit.circuit.library import TwoLocal from qiskit.quantum_info import SparsePauliOp diff --git a/docs/migration_guides/opflow_migration.rst b/docs/migration_guides/opflow_migration.rst index c6b899dae5cd..34554934a9cd 100644 --- a/docs/migration_guides/opflow_migration.rst +++ b/docs/migration_guides/opflow_migration.rst @@ -21,11 +21,8 @@ the :mod:`~qiskit.opflow` module to the :mod:`~qiskit.primitives` and :mod:`~qis .. attention:: Most references to the :class:`qiskit.primitives.Sampler` or :class:`qiskit.primitives.Estimator` in this guide - can be replaced with instances of the: - - - Aer primitives (:class:`qiskit_aer.primitives.Sampler`, :class:`qiskit_aer.primitives.Estimator`) - - Runtime primitives (:class:`qiskit_ibm_runtime.Sampler`, :class:`qiskit_ibm_runtime.Estimator`) - - Terra backend primitives (:class:`qiskit.primitives.BackendSampler`, :class:`qiskit.primitives.BackendEstimator`) + can be replaced with instances of any primitive implementation. For example Aer primitives (:class:`qiskit_aer.primitives.Sampler`/:class:`qiskit_aer.primitives.Estimator`) or IBM's Qiskit Runtime primitives (:class:`qiskit_ibm_runtime.Sampler`/:class:`qiskit_ibm_runtime.Estimator`). + Specific backends can be wrapped with (:class:`qiskit.primitives.BackendSampler`, :class:`qiskit.primitives.BackendEstimator`) to also present primitive-compatible interfaces. Certain classes, such as the :class:`~qiskit.opflow.expectations.AerPauliExpectation`, can only be replaced by a specific primitive instance diff --git a/docs/migration_guides/qi_migration.rst b/docs/migration_guides/qi_migration.rst index 09708f6848c8..1f27a468518e 100644 --- a/docs/migration_guides/qi_migration.rst +++ b/docs/migration_guides/qi_migration.rst @@ -50,20 +50,23 @@ Contents The Qiskit Primitives are algorithmic abstractions that encapsulate the access to backends or simulators for an easy integration into algorithm workflows. - The current pool of primitives includes **two** different **classes** (:class:`~qiskit.primitives.Sampler` and - :class:`~qiskit.primitives.Estimator`) that can be imported from **three** different locations ( - :mod:`qiskit.primitives`, :mod:`qiskit_aer.primitives` and :mod:`qiskit_ibm_runtime` ). In addition to the - reference Sampler and Estimator, :mod:`qiskit.primitives` also contains a - :class:`~qiskit.primitives.BackendSampler` and a :class:`~qiskit.primitives.BackendEstimator` class. These are + The current pool of primitives includes **two** different types of primitives: Sampler and + Estimator. + + Qiskit provides reference implementations in :class:`qiskit.primitives.Sampler` and :class:`qiskit.primitives.Estimator`. Additionally, + :class:`qiskit.primitives.BackendSampler` and a :class:`qiskit.primitives.BackendEstimator` are wrappers for ``backend.run()`` that follow the primitives interface. - This guide uses the following naming standard to refer to the primitives: + Providers can implement these primitives as subclasses of :class:`~qiskit.primitives.BaseSampler` and :class:`~qiskit.primitives.BaseEstimator` respectively. + IBM's Qiskit Runtime (:mod:`qiskit_ibm_runtime`) and Aer (:mod:`qiskit_aer.primitives`) are examples of native implementations of primitives. + + This guide uses the following naming convention: - - *Primitives* - Any Sampler/Estimator implementation - - *Reference Primitives* - The Sampler and Estimator in :mod:`qiskit.primitives` --> ``from qiskit.primitives import Sampler/Estimator`` - - *Aer Primitives* - The Sampler and Estimator in :mod:`qiskit_aer.primitives` --> ``from qiskit_aer.primitives import Sampler/Estimator`` - - *Runtime Primitives* - The Sampler and Estimator in :mod:`qiskit_ibm_runtime` --> ``from qiskit_ibm_runtime import Sampler/Estimator`` - - *Backend Primitives* - The BackendSampler and BackendEstimator in :mod:`qiskit.primitives` --> ``from qiskit import BackendSampler/BackendEstimator`` + - *Primitives* - Any Sampler/Estimator implementation using base classes :class:`qiskit.primitives.BackendSampler` and a :class:`qiskit.primitives.BackendEstimator`. + - *Reference Primitives* - :class:`qiskit.primitives.Sampler` and :class:`qiskit.primitives.Estimator` are reference implementations that come with Qiskit. + - *Aer Primitives* - The `Aer `_ primitive implementations: class:`qiskit_aer.primitives.Sampler` and :class:`qiskit_aer.primitives.Estimator`. + - *Qiskit Runtime Primitives* - IBM's Qiskit Runtime primitive implementations: class:`qiskit_ibm_runtime.Sampler` and :class:`qiskit_ibm_runtime.Estimator`. + - *Backend Primitives* - Instances of :class:`qiskit.primitives.BackendSampler` and :class:`qiskit.primitives.BackendEstimator`. These allow any backend to implement primitive interfaces For guidelines on which primitives to choose for your task, please continue reading. @@ -103,7 +106,7 @@ yourself two questions: a. Using **local** statevector simulators for quick prototyping: **Reference Primitives** b. Using **local** noisy simulations for finer algorithm tuning: **Aer Primitives** - c. Accessing **runtime-enabled backends** (or cloud simulators): **Runtime Primitives** + c. Accessing **runtime-enabled backends** (or cloud simulators): **Qiskit Runtime Primitives** d. Accessing **non runtime-enabled backends** : **Backend Primitives** Arguably, the ``Sampler`` is the closest primitive to :class:`~qiskit.utils.QuantumInstance`, as they @@ -136,7 +139,7 @@ primitives **expose a similar setting through their interface**: * - QuantumInstance - Reference Primitives - Aer Primitives - - Runtime Primitives + - Qiskit Runtime Primitives - Backend Primitives * - Select ``backend`` - No @@ -186,7 +189,7 @@ primitives **expose a similar setting through their interface**: - No -(*) For more information on error mitigation and setting options on Runtime Primitives, visit +(*) For more information on error mitigation and setting options on Qiskit Runtime Primitives, visit `this link `_. (**) For more information on Runtime sessions, visit `this how-to `_. @@ -447,12 +450,12 @@ Code examples **Using Primitives** - The Runtime Primitives offer a suite of error mitigation methods that can be easily turned on with the + The Qiskit Runtime Primitives offer a suite of error mitigation methods that can be easily turned on with the ``resilience_level`` option. These are, however, not configurable. The sampler's ``resilience_level=1`` is the closest alternative to the Quantum Instance's measurement error mitigation implementation, but this is not a 1-1 replacement. - For more information on the error mitigation options in the Runtime Primitives, you can check out the following + For more information on the error mitigation options in the Qiskit Runtime Primitives, you can check out the following `link `_. .. code-block:: python @@ -503,7 +506,7 @@ Code examples * You cannot explicitly access their transpilation routine. * The mechanism to apply custom transpilation passes to the Aer, Runtime and Backend primitives is to pre-transpile locally and set ``skip_transpilation=True`` in the corresponding primitive. - * The only primitives that currently accept a custom **bound** transpiler pass manager are the **Backend Primitives**. + * The only primitives that currently accept a custom **bound** transpiler pass manager are instances of :class:`~qiskit.primitives.BackendSampler` or :class:`~qiskit.primitives.BackendEstimator`. If a ``bound_pass_manager`` is defined, the ``skip_transpilation=True`` option will **not** skip this bound pass. .. attention:: @@ -518,7 +521,7 @@ Code examples so if the circuit ended up on more qubits it did not matter. Note that the primitives **do** handle parameter bindings, meaning that even if a ``bound_pass_manager`` is defined in a - Backend Primitive, you do not have to manually assign parameters as expected in the Quantum Instance workflow. + :class:`~qiskit.primitives.BackendSampler` or :class:`~qiskit.primitives.BackendEstimator`, you do not have to manually assign parameters as expected in the Quantum Instance workflow. The use-case that motivated the addition of the two-stage transpilation to the ``QuantumInstance`` was to allow running pulse-efficient transpilation passes with the :class:`~qiskit.opflow.CircuitSampler` class. The following From 901e3b89731f9437ccbbe5d9eb5dff657854568f Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Tue, 30 May 2023 18:43:36 -0400 Subject: [PATCH 126/172] Add ruff to local tests and CI (#10116) * Add ruff to local tests and CI This adds linting using ruff to the relevant configuration files. Only a few rules are enabled and none of them trigger an error in the current state of the repo. * Add comments on running black separately from tox * Simplify and remove potentially bug causing instructions in CONTRIBUTING * Update pyproject.toml Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> --------- Co-authored-by: Eric Arellano <14852634+Eric-Arellano@users.noreply.github.com> --- .azure/lint-linux.yml | 2 ++ CONTRIBUTING.md | 39 ++++++++++++++++++++++++--------------- Makefile | 7 +++++-- pyproject.toml | 8 ++++++++ requirements-dev.txt | 1 + tox.ini | 2 ++ 6 files changed, 42 insertions(+), 17 deletions(-) diff --git a/.azure/lint-linux.yml b/.azure/lint-linux.yml index aac89bc005ce..5b79db09ae1b 100644 --- a/.azure/lint-linux.yml +++ b/.azure/lint-linux.yml @@ -43,6 +43,8 @@ jobs: - bash: | set -e source test-job/bin/activate + echo "Running ruff" + ruff qiskit test tools examples setup.py echo "Running pylint" pylint -rn qiskit test tools echo "Running Cargo Clippy" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 73eacde8cf57..49d0d0ee376a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -422,25 +422,34 @@ Note: If you have run `test/ipynb/mpl_tester.ipynb` locally it is possible some ## Style and lint -Qiskit Terra uses 2 tools for verify code formatting and lint checking. The +Qiskit Terra uses three tools for verify code formatting and lint checking. The first tool is [black](https://github.com/psf/black) which is a code formatting tool that will automatically update the code formatting to a consistent style. The second tool is [pylint](https://www.pylint.org/) which is a code linter which does a deeper analysis of the Python code to find both style issues and -potential bugs and other common issues in Python. - -You can check that your local modifications conform to the style rules -by running `tox -elint` which will run `black` and `pylint` to check the local -code formatting and lint. If black returns a code formatting error you can -run `tox -eblack` to automatically update the code formatting to conform to -the style. However, if `pylint` returns any error you will have to fix these -issues by manually updating your code. - -Because `pylint` analysis can be slow, there is also a `tox -elint-incr` target, which only applies -`pylint` to files which have changed from the source github. On rare occasions this will miss some -issues that would have been caught by checking the complete source tree, but makes up for this by -being much faster (and those rare oversights will still be caught by the CI after you open a pull -request). +potential bugs and other common issues in Python. The third tool is the linter +[ruff](https://github.com/charliermarsh/ruff), which has been recently +introduced into Qiskit Terra on an experimental basis. Only a very small number +of rules are enabled. + +You can check that your local modifications conform to the style rules by +running `tox -elint` which will run `black`, `ruff`, and `pylint` to check the +local code formatting and lint. If black returns a code formatting error you can +run `tox -eblack` to automatically update the code formatting to conform to the +style. However, if `ruff` or `pylint` return any error you will have to fix +these issues by manually updating your code. + +Because `pylint` analysis can be slow, there is also a `tox -elint-incr` target, +which runs `black` and `ruff` just as `tox -elint` does, but only applies +`pylint` to files which have changed from the source github. On rare occasions +this will miss some issues that would have been caught by checking the complete +source tree, but makes up for this by being much faster (and those rare +oversights will still be caught by the CI after you open a pull request). + +Because they are so fast, it is sometimes convenient to run the tools `black` and `ruff` separately +rather than via `tox`. If you have installed the development packages in your python environment via +`pip install -r requirements-dev.txt`, then `ruff` and `black` will be available and can be run from +the command line. See [`tox.ini`](tox.ini) for how `tox` invokes them. ## Development Cycle diff --git a/Makefile b/Makefile index e6b8f5ce23d6..bea8a880e274 100644 --- a/Makefile +++ b/Makefile @@ -12,9 +12,9 @@ OS := $(shell uname -s) -.PHONY: default env lint lint-incr style black test test_randomized pytest pytest_randomized test_ci coverage coverage_erase clean +.PHONY: default ruff env lint lint-incr style black test test_randomized pytest pytest_randomized test_ci coverage coverage_erase clean -default: style lint-incr test ; +default: ruff style lint-incr test ; # Dependencies need to be installed on the Anaconda virtual environment. env: @@ -41,6 +41,9 @@ lint-incr: tools/verify_headers.py qiskit test tools examples tools/find_optional_imports.py +ruff: + ruff qiskit test tools examples setup.py + style: black --check qiskit test tools examples setup.py diff --git a/pyproject.toml b/pyproject.toml index 57400392b35e..53b83021584a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,14 @@ environment = 'RUSTUP_TOOLCHAIN="stable"' before-all = "yum install -y wget && {package}/tools/install_rust.sh" environment = 'PATH="$PATH:$HOME/.cargo/bin" CARGO_NET_GIT_FETCH_WITH_CLI="true" RUSTUP_TOOLCHAIN="stable"' +[tool.ruff] +select = [ + "F631", + "F632", + "F634", + "F823", +] + [tool.pylint.main] extension-pkg-allow-list = [ "numpy", diff --git a/requirements-dev.txt b/requirements-dev.txt index cf0990e6a36a..fe67acb6ef98 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -11,6 +11,7 @@ black[jupyter]~=22.0 pydot astroid==2.14.2 pylint==2.16.2 +ruff==0.0.267 stestr>=2.0.0,!=4.0.0 pylatexenc>=1.4 ddt>=1.2.0,!=1.4.0,!=1.4.3 diff --git a/tox.ini b/tox.ini index 89c6ad734c03..764253f46d4b 100644 --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,7 @@ commands = [testenv:lint] basepython = python3 commands = + ruff check qiskit test tools examples setup.py black --check {posargs} qiskit test tools examples setup.py pylint -rn qiskit test tools # This line is commented out until #6649 merges. We can't run this currently @@ -38,6 +39,7 @@ commands = basepython = python3 allowlist_externals = git commands = + ruff check qiskit test tools examples setup.py black --check {posargs} qiskit test tools examples setup.py -git fetch -q https://github.com/Qiskit/qiskit-terra.git :lint_incr_latest python {toxinidir}/tools/pylint_incr.py -rn -j4 -sn --paths :/qiskit/*.py :/test/*.py :/tools/*.py From f409015bd7e6bbff61942cf338fcaef0f1f0eed8 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 31 May 2023 17:25:39 -0400 Subject: [PATCH 127/172] Bump pyo3 and rust numpy version to 0.19.0 (#10186) * Bump pyo3 and rust numpy version to 0.19.0 PyO3 0.19.0 and rust-numpy 0.19.0 were just released. This commit updates the version used in qiskit to these latest releases. At the same time this updates usage of text signature for classes that was deprecated in the PyO3 0.19.0 release. While not fatal for normal builds this would have failed clippy in CI because we treat warnings as errors. * Apply suggestions from code review --- Cargo.lock | 110 ++++++++++-------- crates/accelerate/Cargo.toml | 4 +- crates/accelerate/src/edge_collections.rs | 2 +- crates/accelerate/src/error_map.rs | 2 +- crates/accelerate/src/nlayout.rs | 2 +- .../src/sabre_swap/neighbor_table.rs | 2 +- crates/accelerate/src/sabre_swap/sabre_dag.rs | 2 +- crates/qasm2/Cargo.toml | 2 +- crates/qasm2/src/lib.rs | 3 +- 9 files changed, 70 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f5528c202c0d..a0573c59a892 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,9 +45,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "crossbeam-channel" -version = "0.5.7" +version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2b3e8478797446514c91ef04bafcb59faba183e621ad488df88983cc14128c" +checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200" dependencies = [ "cfg-if", "crossbeam-utils", @@ -73,7 +73,7 @@ dependencies = [ "autocfg", "cfg-if", "crossbeam-utils", - "memoffset", + "memoffset 0.8.0", "scopeguard", ] @@ -100,9 +100,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "getrandom" -version = "0.2.8" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4" dependencies = [ "cfg-if", "libc", @@ -163,15 +163,15 @@ checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" [[package]] name = "libc" -version = "0.2.140" +version = "0.2.144" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" +checksum = "2b00cc1c228a6782d0f076e7b232802e0c5689d41bb5df366f2a6b6621cfdfe1" [[package]] name = "libm" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" +checksum = "f7012b1bbb0719e1097c47611d3898568c546d597c2e74d66f6087edd5233ff4" [[package]] name = "lock_api" @@ -185,10 +185,11 @@ dependencies = [ [[package]] name = "matrixmultiply" -version = "0.3.2" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add85d4dd35074e6fedc608f8c8f513a3548619a9024b751949ef0e8e45a4d84" +checksum = "090126dc04f95dc0d1c1c91f61bdd474b3930ca064c1edc8a849da2c6cbe1e77" dependencies = [ + "autocfg", "rawpointer", ] @@ -201,6 +202,15 @@ dependencies = [ "autocfg", ] +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "ndarray" version = "0.15.6" @@ -267,9 +277,9 @@ dependencies = [ [[package]] name = "numpy" -version = "0.18.0" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0fee4571867d318651c24f4a570c3f18408cf95f16ccb576b3ce85496a46e" +checksum = "437213adf41bbccf4aeae535fbfcdad0f6fed241e1ae182ebe97fa1f3ce19389" dependencies = [ "libc", "ndarray", @@ -282,9 +292,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.17.1" +version = "1.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" +checksum = "9670a07f94779e00908f3e686eab508878ebb390ba6e604d3a284c00e8d0487b" [[package]] name = "parking_lot" @@ -337,25 +347,25 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.51" +version = "1.0.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d727cae5b39d21da60fa540906919ad737832fe0b1c165da3a34d6548c849d6" +checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b" dependencies = [ "unicode-ident", ] [[package]] name = "pyo3" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3b1ac5b3731ba34fdaa9785f8d74d17448cd18f30cf19e0c7e7b1fdb5272109" +checksum = "cffef52f74ec3b1a1baf295d9b8fcc3070327aefc39a6d00656b13c1d0b8885c" dependencies = [ "cfg-if", "hashbrown 0.13.2", "indexmap", "indoc", "libc", - "memoffset", + "memoffset 0.9.0", "num-bigint", "num-complex", "parking_lot", @@ -367,9 +377,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cb946f5ac61bb61a5014924910d936ebd2b23b705f7a4a3c40b05c720b079a3" +checksum = "713eccf888fb05f1a96eb78c0dbc51907fee42b3377272dc902eb38985f418d5" dependencies = [ "once_cell", "target-lexicon", @@ -377,9 +387,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd4d7c5337821916ea2a1d21d1092e8443cf34879e53a0ac653fbb98f44ff65c" +checksum = "5b2ecbdcfb01cbbf56e179ce969a048fd7305a66d4cdf3303e0da09d69afe4c3" dependencies = [ "libc", "pyo3-build-config", @@ -387,9 +397,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9d39c55dab3fc5a4b25bbd1ac10a2da452c4aca13bb450f22818a002e29648d" +checksum = "b78fdc0899f2ea781c463679b20cb08af9247febc8d052de941951024cd8aea0" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -399,9 +409,9 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.18.3" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97daff08a4c48320587b5224cc98d609e3c27b6d437315bd40b605c98eeb5918" +checksum = "60da7b84f1227c3e2fe7593505de274dcf4c8928b4e0a1c23d551a14e4e80a0f" dependencies = [ "proc-macro2", "quote", @@ -437,9 +447,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.23" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488" dependencies = [ "proc-macro2", ] @@ -577,15 +587,15 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.12.6" +version = "0.12.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ae9980cab1db3fceee2f6c6f643d5d8de2997c58ee8d25fb0cc8a9e9e7348e5" +checksum = "fd1ba337640d60c3e96bc6f0638a939b9c9a7f2c316a1598c279828b3d1dc8c5" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0" [[package]] name = "unindent" @@ -616,9 +626,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -631,42 +641,42 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" [[package]] name = "windows_aarch64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" [[package]] name = "windows_i686_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" [[package]] name = "windows_i686_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" [[package]] name = "windows_x86_64_gnu" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" [[package]] name = "windows_x86_64_msvc" -version = "0.42.1" +version = "0.42.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 7f057196b6a0..983729ddeb1b 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -13,7 +13,7 @@ crate-type = ["cdylib"] [dependencies] rayon = "1.7" -numpy = "0.18.0" +numpy = "0.19.0" rand = "0.8" rand_pcg = "0.3" rand_distr = "0.4.3" @@ -25,7 +25,7 @@ rustworkx-core = "0.12" # The base version of PyO3 and setting a minimum feature set (e.g. probably just 'extension-module') # can be done in the workspace and inherited once we hit Rust 1.64. [dependencies.pyo3] -version = "0.18.3" +version = "0.19.0" features = ["extension-module", "hashbrown", "indexmap", "num-complex", "num-bigint"] [dependencies.ndarray] diff --git a/crates/accelerate/src/edge_collections.rs b/crates/accelerate/src/edge_collections.rs index 103d0db5d4cf..2fd06e5116f2 100644 --- a/crates/accelerate/src/edge_collections.rs +++ b/crates/accelerate/src/edge_collections.rs @@ -17,7 +17,6 @@ use pyo3::Python; /// A simple container that contains a vector representing edges in the /// coupling map that are found to be optimal by the swap mapper. #[pyclass(module = "qiskit._accelerate.stochastic_swap")] -#[pyo3(text_signature = "(/)")] #[derive(Clone, Debug)] pub struct EdgeCollection { pub edges: Vec, @@ -32,6 +31,7 @@ impl Default for EdgeCollection { #[pymethods] impl EdgeCollection { #[new] + #[pyo3(text_signature = "(/)")] pub fn new() -> Self { EdgeCollection { edges: Vec::new() } } diff --git a/crates/accelerate/src/error_map.rs b/crates/accelerate/src/error_map.rs index fca477298758..d699d383a7d0 100644 --- a/crates/accelerate/src/error_map.rs +++ b/crates/accelerate/src/error_map.rs @@ -32,7 +32,6 @@ use hashbrown::HashMap; /// qubit index. If an edge or qubit is ideal and has no error rate, you can /// either set it to ``0.0`` explicitly or as ``NaN``. #[pyclass(mapping, module = "qiskit._accelerate.error_map")] -#[pyo3(text_signature = "(num_qubits, num_edges, /")] #[derive(Clone, Debug)] pub struct ErrorMap { pub error_map: HashMap<[usize; 2], f64>, @@ -41,6 +40,7 @@ pub struct ErrorMap { #[pymethods] impl ErrorMap { #[new] + #[pyo3(text_signature = "(/, size=None)")] fn new(size: Option) -> Self { match size { Some(size) => ErrorMap { diff --git a/crates/accelerate/src/nlayout.rs b/crates/accelerate/src/nlayout.rs index c0306dce4207..87ae47a7fb43 100644 --- a/crates/accelerate/src/nlayout.rs +++ b/crates/accelerate/src/nlayout.rs @@ -25,7 +25,6 @@ use hashbrown::HashMap; /// logical_qubits (int): The number of logical qubits in the layout /// physical_qubits (int): The number of physical qubits in the layout #[pyclass(module = "qiskit._accelerate.stochastic_swap")] -#[pyo3(text_signature = "(qubit_indices, logical_qubits, physical_qubits, /)")] #[derive(Clone, Debug)] pub struct NLayout { pub logic_to_phys: Vec, @@ -43,6 +42,7 @@ impl NLayout { #[pymethods] impl NLayout { #[new] + #[pyo3(text_signature = "(qubit_indices, logical_qubits, physical_qubits, /)")] fn new( qubit_indices: HashMap, logical_qubits: usize, diff --git a/crates/accelerate/src/sabre_swap/neighbor_table.rs b/crates/accelerate/src/sabre_swap/neighbor_table.rs index 7568707da8bf..528d90a4c8cf 100644 --- a/crates/accelerate/src/sabre_swap/neighbor_table.rs +++ b/crates/accelerate/src/sabre_swap/neighbor_table.rs @@ -27,7 +27,6 @@ use rayon::prelude::*; /// and used solely to represent neighbors of each node in qiskit-terra's rust /// module. #[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(/)")] #[derive(Clone, Debug)] pub struct NeighborTable { pub neighbors: Vec>, @@ -36,6 +35,7 @@ pub struct NeighborTable { #[pymethods] impl NeighborTable { #[new] + #[pyo3(text_signature = "(/, adjacency_matrix=None)")] pub fn new(adjacency_matrix: Option>) -> Self { let run_in_parallel = getenv_use_multiple_threads(); let neighbors = match adjacency_matrix { diff --git a/crates/accelerate/src/sabre_swap/sabre_dag.rs b/crates/accelerate/src/sabre_swap/sabre_dag.rs index c686520debb1..26a16f485c57 100644 --- a/crates/accelerate/src/sabre_swap/sabre_dag.rs +++ b/crates/accelerate/src/sabre_swap/sabre_dag.rs @@ -19,7 +19,6 @@ use rustworkx_core::petgraph::prelude::*; /// DAGCircuit, but the contents of the node are a tuple of DAGCircuit node ids, /// a list of qargs and a list of cargs #[pyclass(module = "qiskit._accelerate.sabre_swap")] -#[pyo3(text_signature = "(num_qubits, num_clbits, nodes, /)")] #[derive(Clone, Debug)] pub struct SabreDAG { pub dag: DiGraph<(usize, Vec), ()>, @@ -29,6 +28,7 @@ pub struct SabreDAG { #[pymethods] impl SabreDAG { #[new] + #[pyo3(text_signature = "(num_qubits, num_clbits, nodes, /)")] pub fn new( num_qubits: usize, num_clbits: usize, diff --git a/crates/qasm2/Cargo.toml b/crates/qasm2/Cargo.toml index 67b567ea1121..5a82991f3f02 100644 --- a/crates/qasm2/Cargo.toml +++ b/crates/qasm2/Cargo.toml @@ -13,4 +13,4 @@ crate-type = ["cdylib"] [dependencies] hashbrown = "0.13.2" -pyo3 = { version = "0.18.3", features = ["extension-module"] } +pyo3 = { version = "0.19.0", features = ["extension-module"] } diff --git a/crates/qasm2/src/lib.rs b/crates/qasm2/src/lib.rs index fab26c5b8e93..b71a30ef96a7 100644 --- a/crates/qasm2/src/lib.rs +++ b/crates/qasm2/src/lib.rs @@ -51,7 +51,7 @@ impl CustomInstruction { /// The given `callable` must be a Python function that takes `num_params` floats, and returns a /// float. The `name` is the identifier that refers to it in the OpenQASM 2 program. This cannot /// clash with any defined gates. -#[pyclass(text_signature = "(name, num_params, callable, /)")] +#[pyclass()] #[derive(Clone)] pub struct CustomClassical { pub name: String, @@ -62,6 +62,7 @@ pub struct CustomClassical { #[pymethods] impl CustomClassical { #[new] + #[pyo3(text_signature = "(name, num_params, callable, /)")] fn __new__(name: String, num_params: usize, callable: PyObject) -> Self { Self { name, From 1b92ae5cc926f4a6e96d7dd8d595e9e7f61b72aa Mon Sep 17 00:00:00 2001 From: Guillermo-Mijares-Vilarino <106545082+Guillermo-Mijares-Vilarino@users.noreply.github.com> Date: Thu, 1 Jun 2023 09:41:58 +0200 Subject: [PATCH 128/172] Added explanation section to Qiskit-terra (#8685) * Created explanation page * Added explanation about qubit ordering * Fix typos and minor rephrases * changed explanation.rst to explanation/index.rst * Removed jupyter-sphinx * Changed header symbol from index * Grammar suggestion Co-authored-by: Frank Harkins * Add reference to YouTube video about qubit ordering --------- Co-authored-by: Frank Harkins Co-authored-by: Junye Huang --- docs/explanation/endianness.rst | 47 +++++++++++++++++++++++++++++++++ docs/explanation/index.rst | 12 +++++++++ docs/index.rst | 1 + 3 files changed, 60 insertions(+) create mode 100644 docs/explanation/endianness.rst create mode 100644 docs/explanation/index.rst diff --git a/docs/explanation/endianness.rst b/docs/explanation/endianness.rst new file mode 100644 index 000000000000..ee78ce4d17e0 --- /dev/null +++ b/docs/explanation/endianness.rst @@ -0,0 +1,47 @@ +######################### +Order of qubits in Qiskit +######################### + +While most physics textbooks represent an :math:`n`-qubit system as the tensor product :math:`Q_0\otimes Q_1 \otimes ... \otimes Q_{n-1}`, where :math:`Q_j` is the :math:`j^{\mathrm{th}}` qubit, Qiskit uses the inverse order, that is, :math:`Q_{n-1}\otimes ... \otimes Q_1 \otimes Q_{0}`. As explained in `this video `_ from `Qiskit's YouTube channel `_, this is done to follow the convention in classical computing, in which the :math:`n^{\mathrm{th}}` bit or most significant bit (MSB) is placed on the left (with index 0) while the least significant bit (LSB) is placed on the right (index :math:`n-1`). This ordering convention is called little-endian while the one from the physics textbooks is called big-endian. + +This means that if we have, for example, a 3-qubit system with qubit 0 in state :math:`|1\rangle` and qubits 1 and 2 in state :math:`|0\rangle`, Qiskit would represent this state as :math:`|001\rangle` while most physics textbooks would represent this state as :math:`|100\rangle`. + +The matrix representation of any multi-qubit gate is also affected by this different qubit ordering. For example, if we consider the single-qubit gate + +.. math:: + + U = \begin{pmatrix} u_{00} & u_{01} \\ u_{10} & u_{11} \end{pmatrix} + +And we want a controlled version :math:`C_U` whose control qubit is qubit 0 and whose target is qubit 1, following Qiskit's ordering its matrix representation would be + +.. math:: + + C_U = \begin{pmatrix} 1 & 0 & 0 & 0 \\0 & u_{00} & 0 & u_{01} \\ 0 & 0 & 1 & 0 \\ 0 & u_{10} & 0& u_{11} \end{pmatrix} + +while in a physics textbook it would be written as + +.. math:: + + C_U = \begin{pmatrix} 1 & 0 & 0 & 0 \\0 & 1 & 0 & 0 \\ 0 & 0 & u_{00} & u_{01} \\ 0 & 0 & u_{00} & u_{01} \end{pmatrix} + + +For more details about how this ordering of MSB and LSB affects the matrix representation of any particular gate, check its entry in the circuit :mod:`~qiskit.circuit.library`. + +This different order can also make the circuit corresponding to an algorithm from a textbook a bit more complicated to visualize. Fortunately, Qiskit provides a way to represent a :class:`~.QuantumCircuit` with the most significant qubits on top, just like in the textbooks. This can be done by setting the ``reverse_bits`` argument of the :meth:`~.QuantumCircuit.draw` method to ``True``. + +Let's try this for a 3-qubit Quantum Fourier Transform (:class:`~.QFT`). + +.. plot:: + :include-source: + :context: + + from qiskit.circuit.library import QFT + + qft = QFT(3) + qft.decompose().draw('mpl') + +.. plot:: + :include-source: + :context: close-figs + + qft.decompose().draw('mpl', reverse_bits=True) \ No newline at end of file diff --git a/docs/explanation/index.rst b/docs/explanation/index.rst new file mode 100644 index 000000000000..4a2d55c82ccb --- /dev/null +++ b/docs/explanation/index.rst @@ -0,0 +1,12 @@ +.. _explanation: + +########### +Explanation +########### + + + +.. toctree:: + :maxdepth: 1 + + Order of qubits in Qiskit \ No newline at end of file diff --git a/docs/index.rst b/docs/index.rst index 135d5364eee6..903fc64030c0 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,6 +8,7 @@ Qiskit Terra documentation How-to Guides API References + Explanation Migration Guides Release Notes From 174b661d57f4b888796c0895ac6b1ba79db11d52 Mon Sep 17 00:00:00 2001 From: John Lapeyre Date: Thu, 1 Jun 2023 09:17:20 -0400 Subject: [PATCH 129/172] Fix unitary synthesis for parameterized basis gates in Target (#10090) * Fix unitary synthesis for parameterized basis gates in Target Replace parameterized RXX with RXX(pi / 2) and parameterized RZX with RZX(pi / 4). * Run black * Fix dict ordering problem between linux and windows * Remove unused import * Try using seed in call to transpile to fix CI fail Linux and MacOS give one result and Windows another * Add comments explaining no symbolic gates can be sent to 2q decomposers * Make test for synthesis in with parameterized basis gate less strict Windows on the one hand, and MacOS and Linux on other give different circuits in a simple test of synthesis. Both are correct. This commit makes the test less strict. * Add release note for fix-issue-10082 --- .../passes/synthesis/unitary_synthesis.py | 17 ++++++++++++++--- ...l-with-symbolic-angles-a070b9973a16b8c3.yaml | 6 ++++++ .../python/transpiler/test_unitary_synthesis.py | 16 ++++++++++++++++ 3 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 releasenotes/notes/fix-synth-fail-with-symbolic-angles-a070b9973a16b8c3.yaml diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index e4555c7fba04..4dbce78d282a 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -31,7 +31,7 @@ TwoQubitWeylDecomposition, ) from qiskit.quantum_info import Operator -from qiskit.circuit import ControlFlowOp, Gate +from qiskit.circuit import ControlFlowOp, Gate, Parameter from qiskit.circuit.library.standard_gates import ( iSwapGate, CXGate, @@ -673,13 +673,24 @@ def _decomposer_2q_from_target(self, target, qubits, approximation_degree): # available instructions on this qubit pair, and their associated property. available_2q_basis = {} available_2q_props = {} + + # 2q gates sent to 2q decomposers must not have any symbolic parameters. The + # gates must be convertable to a numeric matrix. If a basis gate supports an arbitrary + # angle, we have to choose one angle (or more.) + def _replace_parameterized_gate(op): + if isinstance(op, RXXGate) and isinstance(op.params[0], Parameter): + op = RXXGate(pi / 2) + elif isinstance(op, RZXGate) and isinstance(op.params[0], Parameter): + op = RZXGate(pi / 4) + return op + try: keys = target.operation_names_for_qargs(qubits_tuple) for key in keys: op = target.operation_from_name(key) if not isinstance(op, Gate): continue - available_2q_basis[key] = op + available_2q_basis[key] = _replace_parameterized_gate(op) available_2q_props[key] = target[key][qubits_tuple] except KeyError: pass @@ -690,7 +701,7 @@ def _decomposer_2q_from_target(self, target, qubits, approximation_degree): op = target.operation_from_name(key) if not isinstance(op, Gate): continue - available_2q_basis[key] = op + available_2q_basis[key] = _replace_parameterized_gate(op) available_2q_props[key] = target[key][reverse_tuple] except KeyError: pass diff --git a/releasenotes/notes/fix-synth-fail-with-symbolic-angles-a070b9973a16b8c3.yaml b/releasenotes/notes/fix-synth-fail-with-symbolic-angles-a070b9973a16b8c3.yaml new file mode 100644 index 000000000000..a58af12280b2 --- /dev/null +++ b/releasenotes/notes/fix-synth-fail-with-symbolic-angles-a070b9973a16b8c3.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fixes a bug introduced in Qiskit 0.24.0 where numeric rotation angles were no longer substituted + for symbolic ones before preparing for two-qubit synthesis. This caused an exception to be + raised because the synthesis routines require numberic matrices. diff --git a/test/python/transpiler/test_unitary_synthesis.py b/test/python/transpiler/test_unitary_synthesis.py index 39a2a5880172..4aaecbf2e1a3 100644 --- a/test/python/transpiler/test_unitary_synthesis.py +++ b/test/python/transpiler/test_unitary_synthesis.py @@ -50,6 +50,7 @@ IGate, CXGate, RZGate, + RXGate, SXGate, XGate, iSwapGate, @@ -914,6 +915,21 @@ def test_iswap_no_cx_synthesis_succeeds(self): result_qc = dag_to_circuit(result_dag) self.assertTrue(np.allclose(Operator(result_qc.to_gate()).to_matrix(), cxmat)) + def test_parameterized_basis_gate_in_target(self): + """Test synthesis with parameterized RXX gate.""" + theta = Parameter("θ") + lam = Parameter("λ") + target = Target(num_qubits=2) + target.add_instruction(RZGate(lam)) + target.add_instruction(RXGate(theta)) + target.add_instruction(RXXGate(theta)) + qc = QuantumCircuit(2) + qc.cp(np.pi / 2, 0, 1) + qc_transpiled = transpile(qc, target=target, optimization_level=3, seed_transpiler=42) + opcount = qc_transpiled.count_ops() + self.assertTrue(set(opcount).issubset({"rz", "rx", "rxx"})) + self.assertTrue(np.allclose(Operator(qc_transpiled), Operator(qc))) + if __name__ == "__main__": unittest.main() From 332bd9fe0bea82c0fdf7329cea3da115d86e3fc2 Mon Sep 17 00:00:00 2001 From: Will Shanks Date: Thu, 1 Jun 2023 15:00:25 -0400 Subject: [PATCH 130/172] Assign values directly to fully bound parameters in quantum circuits (#10183) * Assign circuit parameters as int/float to instructions * Do not test that fully bound parameters are still ParameterExpressions * Change int to float in qasm output pi_check casts integers to floats but not integers inside ParameterExpressions. * Workaround symengine ComplexDouble not supporting float * black * Update releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml Co-authored-by: Jake Lishman * Update releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml Co-authored-by: Jake Lishman * Restore assigned parameter value type check to tests * Add test to check type and value of simple circuit parameter assignment * Add consistency check between assigned instruction data and calibrations dict keys * Add regression test * Add upgrade note * Remove support for complex instruction parameter assignment * Restore complex assignment Complex assignment maybe not be supported but allowing it in the parameter assignment step lets validate_parameters get the value and raise an appropriate exception. * black * More specific assertion methods * Use exact floating-point check --------- Co-authored-by: Jake Lishman Co-authored-by: John Lapeyre Co-authored-by: Jake Lishman --- qiskit/circuit/quantumcircuit.py | 18 +++++- ...er-to-concrete-value-7cad75c97183257f.yaml | 29 ++++++++++ .../circuit/test_circuit_load_from_qpy.py | 35 +++++++++++- test/python/circuit/test_parameters.py | 55 +++++++++++++++---- test/python/qasm3/test_export.py | 2 +- 5 files changed, 123 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 98e056c2a2a8..37cb1c508575 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2854,7 +2854,17 @@ def _assign_parameter(self, parameter: Parameter, value: ParameterValueType) -> new_param = assignee.assign(parameter, value) # if fully bound, validate if len(new_param.parameters) == 0: - instr.params[param_index] = instr.validate_parameter(new_param) + if new_param._symbol_expr.is_integer and new_param.is_real(): + val = int(new_param) + elif new_param.is_real(): + # Workaround symengine not supporting float() + val = complex(new_param).real + else: + # complex values may no longer be supported but we + # defer raising an exception to validdate_parameter + # below for now. + val = complex(new_param) + instr.params[param_index] = instr.validate_parameter(val) else: instr.params[param_index] = new_param @@ -2911,7 +2921,11 @@ def _assign_calibration_parameters( if isinstance(p, ParameterExpression) and parameter in p.parameters: new_param = p.assign(parameter, value) if not new_param.parameters: - new_param = float(new_param) + if new_param._symbol_expr.is_integer: + new_param = int(new_param) + else: + # Workaround symengine not supporting float() + new_param = complex(new_param).real new_cal_params.append(new_param) else: new_cal_params.append(p) diff --git a/releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml b/releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml new file mode 100644 index 000000000000..036d672e480d --- /dev/null +++ b/releasenotes/notes/circuit-assign-parameter-to-concrete-value-7cad75c97183257f.yaml @@ -0,0 +1,29 @@ +--- +fixes: + - | + Changed the binding of numeric values with + :meth:`.QuantumCircuit.assign_parameters` to avoid a mismatch between the + values of circuit instruction parameters and corresponding parameter keys + in the circuit's calibration dictionary. Fixed `#9764 + `_ and `#10166 + `_. See also the + related upgrade note regarding :meth:`.QuantumCircuit.assign_parameters`. +upgrade: + - | + Changed :meth:`.QuantumCircuit.assign_parameters` to bind + assigned integer and float values directly into the parameters of + :class:`~qiskit.circuit.Instruction` instances in the circuit rather than + binding the values wrapped within a + :class:`~qiskit.circuit.ParameterExpression`. This change should have + little user impact as ``float(QuantumCircuit.data[i].operation.params[j])`` + still produces a ``float`` (and is the only way to access the value of a + :class:`~qiskit.circuit.ParameterExpression`). Also, + :meth:`~qiskit.circuit.Instruction` parameters could already be ``float`` + as well as a :class:`~qiskit.circuit.ParameterExpression`, so code dealing + with instruction parameters should already handle both cases. The most + likely chance for user impact is in code that uses ``isinstance`` to check + for :class:`~qiskit.circuit.ParameterExpression` and behaves differently + depending on the result. Additionally, qpy serializes the numeric value in + a bound :class:`~qiskit.circuit.ParameterExpression` at a different + precision than a ``float`` (see also the related bug fix note about + :meth:`.QuantumCircuit.assign_parameters`). diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py index 3bedc5cb9da0..331c30471032 100644 --- a/test/python/circuit/test_circuit_load_from_qpy.py +++ b/test/python/circuit/test_circuit_load_from_qpy.py @@ -19,7 +19,7 @@ import numpy as np -from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister +from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, pulse from qiskit.circuit import CASE_DEFAULT from qiskit.circuit.classicalregister import Clbit from qiskit.circuit.quantumregister import Qubit @@ -274,6 +274,39 @@ def test_bound_parameter(self): self.assertEqual(qc, new_circ) self.assertDeprecatedBitProperties(qc, new_circ) + def test_bound_calibration_parameter(self): + """Test a circuit with a bound calibration parameter is correctly serialized. + + In particular, this test ensures that parameters on a circuit + instruction are consistent with the circuit's calibrations dictionary + after serialization. + """ + amp = Parameter("amp") + + with pulse.builder.build() as sched: + pulse.builder.play(pulse.Constant(100, amp), pulse.DriveChannel(0)) + + gate = Gate("custom", 1, [amp]) + + qc = QuantumCircuit(1) + qc.append(gate, (0,)) + qc.add_calibration(gate, (0,), sched) + qc.assign_parameters({amp: 1 / 3}, inplace=True) + + qpy_file = io.BytesIO() + dump(qc, qpy_file) + qpy_file.seek(0) + new_circ = load(qpy_file)[0] + self.assertEqual(qc, new_circ) + instruction = new_circ.data[0] + cal_key = ( + tuple(new_circ.find_bit(q).index for q in instruction.qubits), + tuple(instruction.operation.params), + ) + # Make sure that looking for a calibration based on the instruction's + # parameters succeeds + self.assertIn(cal_key, new_circ.calibrations[gate.name]) + def test_parameter_expression(self): """Test a circuit with a parameter expression.""" theta = Parameter("theta") diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index e19fbd205166..d610a3353067 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -21,7 +21,7 @@ from test import combine import numpy -from ddt import data, ddt +from ddt import data, ddt, named_data import qiskit import qiskit.circuit.library as circlib @@ -346,6 +346,23 @@ def test_multiple_named_parameters(self): self.assertEqual(theta.name, "θ") self.assertEqual(qc.parameters, {theta, x}) + @named_data( + ["int", 2, int], + ["float", 2.5, float], + ["float16", numpy.float16(2.5), float], + ["float32", numpy.float32(2.5), float], + ["float64", numpy.float64(2.5), float], + ) + def test_circuit_assignment_to_numeric(self, value, type_): + """Test binding a numeric value to a circuit instruction""" + x = Parameter("x") + qc = QuantumCircuit(1) + qc.append(Instruction("inst", 1, 0, [x]), (0,)) + qc.assign_parameters({x: value}, inplace=True) + bound = qc.data[0].operation.params[0] + self.assertIsInstance(bound, type_) + self.assertEqual(bound, value) + def test_partial_binding(self): """Test that binding a subset of circuit parameters returns a new parameterized circuit.""" theta = Parameter("θ") @@ -401,10 +418,10 @@ def test_expression_partial_binding(self): self.assertTrue(isinstance(pqc.data[0].operation.params[0], ParameterExpression)) self.assertEqual(str(pqc.data[0].operation.params[0]), "phi + 2") - fbqc = getattr(pqc, assign_fun)({phi: 1}) + fbqc = getattr(pqc, assign_fun)({phi: 1.0}) self.assertEqual(fbqc.parameters, set()) - self.assertTrue(isinstance(fbqc.data[0].operation.params[0], ParameterExpression)) + self.assertIsInstance(fbqc.data[0].operation.params[0], float) self.assertEqual(float(fbqc.data[0].operation.params[0]), 3) def test_two_parameter_expression_binding(self): @@ -448,7 +465,7 @@ def test_expression_partial_binding_zero(self): fbqc = getattr(pqc, assign_fun)({phi: 1}) self.assertEqual(fbqc.parameters, set()) - self.assertTrue(isinstance(fbqc.data[0].operation.params[0], ParameterExpression)) + 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): @@ -505,8 +522,15 @@ def test_calibration_assignment(self): circ.add_calibration("rxt", [0], rxt_q0, [theta]) circ = circ.assign_parameters({theta: 3.14}) - self.assertTrue(((0,), (3.14,)) in circ.calibrations["rxt"]) - sched = circ.calibrations["rxt"][((0,), (3.14,))] + instruction = circ.data[0] + cal_key = ( + tuple(circ.find_bit(q).index for q in instruction.qubits), + tuple(instruction.operation.params), + ) + self.assertEqual(cal_key, ((0,), (3.14,))) + # Make sure that key from instruction data matches the calibrations dictionary + self.assertIn(cal_key, circ.calibrations["rxt"]) + sched = circ.calibrations["rxt"][cal_key] self.assertEqual(sched.instructions[0][1].pulse.amp, 0.2) def test_calibration_assignment_doesnt_mutate(self): @@ -531,11 +555,11 @@ def test_calibration_assignment_doesnt_mutate(self): self.assertNotEqual(assigned_circ.calibrations, circ.calibrations) def test_calibration_assignment_w_expressions(self): - """That calibrations with multiple parameters and more expressions.""" + """That calibrations with multiple parameters are assigned correctly""" theta = Parameter("theta") sigma = Parameter("sigma") circ = QuantumCircuit(3, 3) - circ.append(Gate("rxt", 1, [theta, sigma]), [0]) + circ.append(Gate("rxt", 1, [theta / 2, sigma]), [0]) circ.measure(0, 0) rxt_q0 = pulse.Schedule( @@ -548,8 +572,15 @@ def test_calibration_assignment_w_expressions(self): circ.add_calibration("rxt", [0], rxt_q0, [theta / 2, sigma]) circ = circ.assign_parameters({theta: 3.14, sigma: 4}) - self.assertTrue(((0,), (3.14 / 2, 4)) in circ.calibrations["rxt"]) - sched = circ.calibrations["rxt"][((0,), (3.14 / 2, 4))] + instruction = circ.data[0] + cal_key = ( + tuple(circ.find_bit(q).index for q in instruction.qubits), + tuple(instruction.operation.params), + ) + self.assertEqual(cal_key, ((0,), (3.14 / 2, 4))) + # Make sure that key from instruction data matches the calibrations dictionary + self.assertIn(cal_key, circ.calibrations["rxt"]) + sched = circ.calibrations["rxt"][cal_key] self.assertEqual(sched.instructions[0][1].pulse.amp, 0.2) self.assertEqual(sched.instructions[0][1].pulse.sigma, 16) @@ -789,7 +820,7 @@ def test_binding_parameterized_circuits_built_in_multiproc(self): for qc in results: circuit.compose(qc, inplace=True) - parameter_values = [{x: 1 for x in parameters}] + parameter_values = [{x: 1.0 for x in parameters}] qobj = assemble( circuit, @@ -802,7 +833,7 @@ def test_binding_parameterized_circuits_built_in_multiproc(self): self.assertTrue( all( len(inst.params) == 1 - and isinstance(inst.params[0], ParameterExpression) + and isinstance(inst.params[0], float) and float(inst.params[0]) == 1 for inst in qobj.experiments[0].instructions ) diff --git a/test/python/qasm3/test_export.py b/test/python/qasm3/test_export.py index 2f3030ca3ce6..9978426e6d9e 100644 --- a/test/python/qasm3/test_export.py +++ b/test/python/qasm3/test_export.py @@ -469,7 +469,7 @@ def test_reused_custom_parameter(self): " rx(0.5) _gate_q_0;", "}", f"gate {circuit_name_1} _gate_q_0 {{", - " rx(1) _gate_q_0;", + " rx(1.0) _gate_q_0;", "}", "qubit[1] _all_qubits;", "let q = _all_qubits[0:0];", From c61b8162e819a364466a9610a18ec183c2ad56cb Mon Sep 17 00:00:00 2001 From: anisha-bopardikar <111171754+anisha-bopardikar@users.noreply.github.com> Date: Mon, 5 Jun 2023 18:27:46 +0530 Subject: [PATCH 131/172] Fix typo issue #10198 (#10199) --- qiskit/circuit/library/standard_gates/global_phase.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/library/standard_gates/global_phase.py b/qiskit/circuit/library/standard_gates/global_phase.py index e0a0d0e805e1..3ba77da0d59d 100644 --- a/qiskit/circuit/library/standard_gates/global_phase.py +++ b/qiskit/circuit/library/standard_gates/global_phase.py @@ -25,7 +25,7 @@ class GlobalPhaseGate(Gate): Can be applied to a :class:`~qiskit.circuit.QuantumCircuit` - **Mathamatical Representation:** + **Mathematical Representation:** .. math:: \text{GlobalPhaseGate}\ = From 6f29d7be78ab07d043c6e90645624f88d523d0d4 Mon Sep 17 00:00:00 2001 From: Diego Emilio Serrano <65074936+diemilio@users.noreply.github.com> Date: Tue, 6 Jun 2023 09:49:28 -0400 Subject: [PATCH 132/172] Fix output of `DensityMatrix.partial_transpose` to match input dimensions (#10163) * (fix) change output of partial_transpose to return DensityMatrix matching input dimensions * (test) check output dims match input dims when using DensityMatrix.partial_transpose * (docs) add bugfix reno * lint changes * Fix up release note --------- Co-authored-by: Jake Lishman --- qiskit/quantum_info/states/densitymatrix.py | 2 +- ...x-partial-transpose-output-dims-3082fcf4147055dc.yaml | 5 +++++ test/python/quantum_info/states/test_densitymatrix.py | 9 +++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-partial-transpose-output-dims-3082fcf4147055dc.yaml diff --git a/qiskit/quantum_info/states/densitymatrix.py b/qiskit/quantum_info/states/densitymatrix.py index 124a382f01d1..3103ef23036a 100644 --- a/qiskit/quantum_info/states/densitymatrix.py +++ b/qiskit/quantum_info/states/densitymatrix.py @@ -832,4 +832,4 @@ def partial_transpose(self, qargs): lst[i], lst[i + n] = lst[i + n], lst[i] rho = np.transpose(arr, lst) rho = np.reshape(rho, self._op_shape.shape) - return DensityMatrix(rho) + return DensityMatrix(rho, dims=self.dims()) diff --git a/releasenotes/notes/fix-partial-transpose-output-dims-3082fcf4147055dc.yaml b/releasenotes/notes/fix-partial-transpose-output-dims-3082fcf4147055dc.yaml new file mode 100644 index 000000000000..edba41333ce8 --- /dev/null +++ b/releasenotes/notes/fix-partial-transpose-output-dims-3082fcf4147055dc.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed the dimensions of the output density matrix from :meth:`.DensityMatrix.partial_transpose` + so they match the dimensions of the corresponding input density matrix. diff --git a/test/python/quantum_info/states/test_densitymatrix.py b/test/python/quantum_info/states/test_densitymatrix.py index 8886e35cc288..16fbbbc1b9a4 100644 --- a/test/python/quantum_info/states/test_densitymatrix.py +++ b/test/python/quantum_info/states/test_densitymatrix.py @@ -1219,6 +1219,15 @@ def test_density_matrix_partial_transpose(self): self.assertEqual(rho.partial_transpose([0]), DensityMatrix(rho1)) self.assertEqual(rho.partial_transpose([1]), DensityMatrix(rho1)) + with self.subTest(msg="dims(3,3)"): + mat = np.zeros((9, 9)) + mat1 = np.zeros((9, 9)) + mat[8, 0] = 1 + mat1[0, 8] = 1 + rho = DensityMatrix(mat, dims=(3, 3)) + rho1 = DensityMatrix(mat1, dims=(3, 3)) + self.assertEqual(rho.partial_transpose([0, 1]), rho1) + def test_clip_probabilities(self): """Test probabilities are clipped to [0, 1].""" dm = DensityMatrix([[1.1, 0], [0, 0]]) From 84d13d779599a547840f7da42bdcb9969e04b69f Mon Sep 17 00:00:00 2001 From: Mu-Te Joshua Lau <71618875+JoshuaLau0220@users.noreply.github.com> Date: Tue, 6 Jun 2023 23:29:43 +0800 Subject: [PATCH 133/172] Fix QuantumCircuit.draw() not outputting pdf in latex mode (#10212) * Fix QuantumCircuit.draw() not outputting pdf in latex mode * Added bugfix release note; moved import to the top * Fixup release note --------- Co-authored-by: Jake Lishman --- qiskit/visualization/circuit/circuit_visualization.py | 3 ++- ...e-LaTeX-drawer-of-QuantumCircuit-7dd3e84e1dea1abd.yaml | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-regression-in-the-LaTeX-drawer-of-QuantumCircuit-7dd3e84e1dea1abd.yaml diff --git a/qiskit/visualization/circuit/circuit_visualization.py b/qiskit/visualization/circuit/circuit_visualization.py index 0627f2617063..ebdffca002a2 100644 --- a/qiskit/visualization/circuit/circuit_visualization.py +++ b/qiskit/visualization/circuit/circuit_visualization.py @@ -28,6 +28,7 @@ import os import subprocess import tempfile +import shutil from warnings import warn from qiskit import user_config @@ -495,7 +496,7 @@ def _latex_circuit_drawer( image = trim_image(image) if filename: if filename.endswith(".pdf"): - os.rename(base + ".pdf", filename) + shutil.move(base + ".pdf", filename) else: try: image.save(filename) diff --git a/releasenotes/notes/fix-regression-in-the-LaTeX-drawer-of-QuantumCircuit-7dd3e84e1dea1abd.yaml b/releasenotes/notes/fix-regression-in-the-LaTeX-drawer-of-QuantumCircuit-7dd3e84e1dea1abd.yaml new file mode 100644 index 000000000000..fdeec4027e2c --- /dev/null +++ b/releasenotes/notes/fix-regression-in-the-LaTeX-drawer-of-QuantumCircuit-7dd3e84e1dea1abd.yaml @@ -0,0 +1,8 @@ + +fixes: + - | + Fixed a regression in the LaTeX drawer of :meth:`.QuantumCircuit.draw` + when temporary files are placed on a separate filesystem to the working + directory. See + `#10211 `__. + From 218c7735d484a59f2ff6bc0066c9cb27609ac931 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 6 Jun 2023 18:34:48 +0100 Subject: [PATCH 134/172] Refactor disjoint routing tests to be more resilient (#10210) * Refactor disjoint routing tests to be more resilient The previous method of running the assertions in these tests could produce false negatives if routing caused an operation to take place on a virtual qubit that was initially an ancilla. It was tricky to make sensible assertions in these cases, since it is slightly fiddly to determine which ancillas have been assigned to which physical components of the chip. This lead to `optimization_level=1` being skipped in one test, because the routing would use these ancillas. This refactors the test to be more resistant to valid choices the routing algorithm might make. We instead find _all_ the physical qubits that are in each connected component, determine the mapping of virtual qubits to components, and then make assertions based on the physical qubits. This is resistant to ancillas being used in the comparisons. * Update test/python/compiler/test_transpiler.py Co-authored-by: Matthew Treinish --------- Co-authored-by: Matthew Treinish --- test/python/compiler/test_transpiler.py | 104 ++++++++++++++++-------- 1 file changed, 68 insertions(+), 36 deletions(-) diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index c25425d051f1..b2b105c14ea5 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -92,6 +92,14 @@ def _define(self): self._definition.cx(0, 1) +def connected_qubits(physical: int, coupling_map: CouplingMap) -> set: + """Get the physical qubits that have a connection to this one in the coupling map.""" + for component in coupling_map.connected_components(): + if physical in (qubits := set(component.graph.nodes())): + return qubits + raise ValueError(f"physical qubit {physical} is not in the coupling map") + + @ddt class TestTranspile(QiskitTestCase): """Test transpile function.""" @@ -2466,10 +2474,12 @@ def test_shared_classical_between_components_condition_large_to_small(self, opt_ creg = ClassicalRegister(2) qc = QuantumCircuit(25) qc.add_register(creg) + # Component 0 qc.h(24) qc.cx(24, 23) qc.measure(24, creg[0]) qc.measure(23, creg[1]) + # Component 1 qc.h(0).c_if(creg, 0) for i in range(18): qc.ecr(0, i + 1).c_if(creg, 0) @@ -2495,14 +2505,22 @@ def _visit_block(circuit, qubit_mapping=None): tqc, qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, ) - # Check clbits are in order - # Traverse the output dag over the sole clbit. Checking that the qubits of the ops + # Check that virtual qubits that interact with each other via quantum links are placed into + # the same component of the coupling map. + initial_layout = tqc.layout.initial_layout + coupling_map = self.backend.target.build_coupling_map() + components = [ + connected_qubits(initial_layout[qc.qubits[23]], coupling_map), + connected_qubits(initial_layout[qc.qubits[0]], coupling_map), + ] + self.assertLessEqual({initial_layout[qc.qubits[i]] for i in [23, 24]}, components[0]) + self.assertLessEqual({initial_layout[qc.qubits[i]] for i in range(19)}, components[1]) + + # Check clbits are in order. + # Traverse the output dag over the sole clbit, checking that the qubits of the ops # go in order between the components. This is a sanity check to ensure that routing # doesn't reorder a classical data dependency between components. Inside a component # we have the dag ordering so nothing should be out of order within a component. - initial_layout = tqc.layout.initial_layout - first_component = {qc.qubits[23], qc.qubits[24]} - second_component = {qc.qubits[i] for i in range(19)} tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] @@ -2511,13 +2529,13 @@ def _visit_block(circuit, qubit_mapping=None): )[0] # The first node should be a measurement self.assertIsInstance(first_meas_node.op, Measure) - # This shoulde be in the first ocmponent - self.assertIn(initial_layout._p2v[qubit_map[first_meas_node.qargs[0]]], first_component) + # This should be in the first component + self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) op_node = tqc_dag._multi_graph.find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while isinstance(op_node, DAGOpNode): - self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + self.assertIn(qubit_map[op_node.qargs[0]], components[1]) op_node = tqc_dag._multi_graph.find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] @@ -2530,10 +2548,12 @@ def test_shared_classical_between_components_condition_large_to_small_reverse_in creg = ClassicalRegister(2) qc = QuantumCircuit(25) qc.add_register(creg) + # Component 0 qc.h(0) qc.cx(0, 1) qc.measure(0, creg[0]) qc.measure(1, creg[1]) + # Component 1 qc.h(24).c_if(creg, 0) for i in range(23, 5, -1): qc.ecr(24, i).c_if(creg, 0) @@ -2559,14 +2579,22 @@ def _visit_block(circuit, qubit_mapping=None): tqc, qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, ) - # Check clbits are in order - # Traverse the output dag over the sole clbit. Checking that the qubits of the ops + # Check that virtual qubits that interact with each other via quantum links are placed into + # the same component of the coupling map. + initial_layout = tqc.layout.initial_layout + coupling_map = self.backend.target.build_coupling_map() + components = [ + connected_qubits(initial_layout[qc.qubits[0]], coupling_map), + connected_qubits(initial_layout[qc.qubits[6]], coupling_map), + ] + self.assertLessEqual({initial_layout[qc.qubits[i]] for i in range(2)}, components[0]) + self.assertLessEqual({initial_layout[qc.qubits[i]] for i in range(6, 25)}, components[1]) + + # Check clbits are in order. + # Traverse the output dag over the sole clbit, checking that the qubits of the ops # go in order between the components. This is a sanity check to ensure that routing # doesn't reorder a classical data dependency between components. Inside a component # we have the dag ordering so nothing should be out of order within a component. - initial_layout = tqc.layout.initial_layout - first_component = {qc.qubits[i] for i in range(2)} - second_component = {qc.qubits[i] for i in range(6, 25)} tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] @@ -2576,39 +2604,35 @@ def _visit_block(circuit, qubit_mapping=None): # The first node should be a measurement self.assertIsInstance(first_meas_node.op, Measure) # This shoulde be in the first ocmponent - self.assertIn(initial_layout._p2v[qubit_map[first_meas_node.qargs[0]]], first_component) + self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) op_node = tqc_dag._multi_graph.find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while isinstance(op_node, DAGOpNode): - self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + self.assertIn(qubit_map[op_node.qargs[0]], components[1]) op_node = tqc_dag._multi_graph.find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] - # Level 1 skipped in this test for now because routing inserts more swaps - # and tricking the intermediate layout permutation to validate ordering - # will be different compared to higher optimization levels. We have similar - # coverage provided by above tests for level 1. - @data(2, 3) + @data(1, 2, 3) def test_chained_data_dependency(self, opt_level): """Test 3 component circuit with shared clbits between each component.""" creg = ClassicalRegister(1) qc = QuantumCircuit(30) qc.add_register(creg) - # Component 1 + # Component 0 qc.h(0) for i in range(9): qc.cx(0, i + 1) measure_op = Measure() qc.append(measure_op, [9], [creg[0]]) - # Component 2 + # Component 1 qc.h(10).c_if(creg, 0) for i in range(11, 20): qc.ecr(10, i).c_if(creg, 0) measure_op = Measure() qc.append(measure_op, [19], [creg[0]]) - # Component 3 + # Component 2 qc.h(20).c_if(creg, 0) for i in range(21, 30): qc.cz(20, i).c_if(creg, 0) @@ -2636,16 +2660,24 @@ def _visit_block(circuit, qubit_mapping=None): tqc, qubit_mapping={qubit: index for index, qubit in enumerate(tqc.qubits)}, ) - # Check clbits are in order - # Traverse the output dag over the sole clbit. Checking that the qubits of the ops + # Check that virtual qubits that interact with each other via quantum links are placed into + # the same component of the coupling map. + initial_layout = tqc.layout.initial_layout + coupling_map = self.backend.target.build_coupling_map() + components = [ + connected_qubits(initial_layout[qc.qubits[0]], coupling_map), + connected_qubits(initial_layout[qc.qubits[10]], coupling_map), + connected_qubits(initial_layout[qc.qubits[20]], coupling_map), + ] + self.assertLessEqual({initial_layout[qc.qubits[i]] for i in range(10)}, components[0]) + self.assertLessEqual({initial_layout[qc.qubits[i]] for i in range(10, 20)}, components[1]) + self.assertLessEqual({initial_layout[qc.qubits[i]] for i in range(20, 30)}, components[2]) + + # Check clbits are in order. + # Traverse the output dag over the sole clbit, checking that the qubits of the ops # go in order between the components. This is a sanity check to ensure that routing # doesn't reorder a classical data dependency between components. Inside a component - # we have the dag ordering so nothing should be incompatible there. - - initial_layout = tqc.layout.initial_layout - first_component = {qc.qubits[i] for i in range(10)} - second_component = {qc.qubits[i] for i in range(10, 20)} - third_component = {qc.qubits[i] for i in range(20, 30)} + # we have the dag ordering so nothing should be out of order within a component. tqc_dag = circuit_to_dag(tqc) qubit_map = {qubit: index for index, qubit in enumerate(tqc_dag.qubits)} input_node = tqc_dag.input_map[tqc_dag.clbits[0]] @@ -2653,25 +2685,25 @@ def _visit_block(circuit, qubit_mapping=None): input_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] self.assertIsInstance(first_meas_node.op, Measure) - self.assertIn(initial_layout._p2v[qubit_map[first_meas_node.qargs[0]]], first_component) + self.assertIn(qubit_map[first_meas_node.qargs[0]], components[0]) op_node = tqc_dag._multi_graph.find_successors_by_edge( first_meas_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while not isinstance(op_node.op, Measure): - self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + self.assertIn(qubit_map[op_node.qargs[0]], components[1]) op_node = tqc_dag._multi_graph.find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] - self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], second_component) + self.assertIn(qubit_map[op_node.qargs[0]], components[1]) op_node = tqc_dag._multi_graph.find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] while not isinstance(op_node.op, Measure): - self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], third_component) + self.assertIn(qubit_map[op_node.qargs[0]], components[2]) op_node = tqc_dag._multi_graph.find_successors_by_edge( op_node._node_id, lambda edge_data: isinstance(edge_data, Clbit) )[0] - self.assertIn(initial_layout._p2v[qubit_map[op_node.qargs[0]]], third_component) + self.assertIn(qubit_map[op_node.qargs[0]], components[2]) @data("sabre", "stochastic", "basic", "lookahead") def test_basic_connected_circuit_dense_layout(self, routing_method): From 2d8300c83494a44b65c2cb10113563ff60a29823 Mon Sep 17 00:00:00 2001 From: Junya Nakamura <47435718+junnaka51@users.noreply.github.com> Date: Wed, 7 Jun 2023 20:03:04 +0900 Subject: [PATCH 135/172] Support instruction filtering method in ScheduleBlock class (#9971) * activate filter method in ScheduleBlock class * use functools.singledispatch * add tests for filter method in ScheduleBlock class * filter out empty schedule_blocks * update doc * rm logical_and * add catch-TypeError functions decorated with singledispatch * use pulse builder in test * make another test function for nested block * add a release note * Rm optional args Co-authored-by: Naoki Kanazawa * add warning Co-authored-by: Naoki Kanazawa * update the release note Co-authored-by: Naoki Kanazawa * activate exclude method in ScheduleBlock class --------- Co-authored-by: Naoki Kanazawa --- qiskit/pulse/filters.py | 119 +++++++++- qiskit/pulse/schedule.py | 80 +++---- ...ilter-schedule-block-29d392ca351f1fb1.yaml | 27 +++ test/python/pulse/test_block.py | 224 ++++++++++++++++++ 4 files changed, 401 insertions(+), 49 deletions(-) create mode 100644 releasenotes/notes/filter-schedule-block-29d392ca351f1fb1.yaml diff --git a/qiskit/pulse/filters.py b/qiskit/pulse/filters.py index b90e46881294..dc3a6a7f1cf5 100644 --- a/qiskit/pulse/filters.py +++ b/qiskit/pulse/filters.py @@ -13,16 +13,31 @@ """A collection of functions that filter instructions in a pulse program.""" import abc +from functools import singledispatch from typing import Callable, List, Union, Iterable, Optional, Tuple, Any import numpy as np -from qiskit.pulse import Schedule +from qiskit.pulse import Schedule, ScheduleBlock, Instruction from qiskit.pulse.channels import Channel from qiskit.pulse.schedule import Interval +from qiskit.pulse.exceptions import PulseError +@singledispatch def filter_instructions( + sched, filters: List[Callable], negate: bool = False, recurse_subroutines: bool = True +): + """A catch-TypeError function which will only get called if none of the other decorated + functions, namely handle_schedule() and handle_scheduleblock(), handle the type passed. + """ + raise TypeError( + f"Type '{type(sched)}' is not valid data format as an input to filter_instructions." + ) + + +@filter_instructions.register +def handle_schedule( sched: Schedule, filters: List[Callable], negate: bool = False, recurse_subroutines: bool = True ) -> Schedule: """A filtering function that takes a schedule and returns a schedule consisting of @@ -61,6 +76,58 @@ def filter_instructions( return filter_schedule +@filter_instructions.register +def handle_scheduleblock( + sched_blk: ScheduleBlock, + filters: List[Callable], + negate: bool = False, + recurse_subroutines: bool = True, +) -> ScheduleBlock: + """A filtering function that takes a schedule_block and returns a schedule_block consisting of + filtered instructions. + + Args: + sched_blk: A pulse schedule_block to be filtered. + filters: List of callback functions that take an instruction and return boolean. + negate: Set `True` to accept an instruction if a filter function returns `False`. + Otherwise the instruction is accepted when the filter function returns `False`. + recurse_subroutines: Set `True` to individually filter instructions inside of a subroutine + defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. + + Returns: + Filtered pulse schedule_block. + """ + from qiskit.pulse.transforms import inline_subroutines + + target_sched_blk = sched_blk + if recurse_subroutines: + target_sched_blk = inline_subroutines(target_sched_blk) + + def apply_filters_to_insts_in_scheblk(blk: ScheduleBlock) -> ScheduleBlock: + blk_new = ScheduleBlock.initialize_from(blk) + for element in blk.blocks: + if isinstance(element, ScheduleBlock): + inner_blk = apply_filters_to_insts_in_scheblk(element) + if len(inner_blk) > 0: + blk_new.append(inner_blk) + + elif isinstance(element, Instruction): + valid_inst = all(filt(element) for filt in filters) + if negate: + valid_inst ^= True + if valid_inst: + blk_new.append(element) + + else: + raise PulseError( + f"An unexpected element '{element}' is included in ScheduleBlock.blocks." + ) + return blk_new + + filter_sched_blk = apply_filters_to_insts_in_scheblk(target_sched_blk) + return filter_sched_blk + + def composite_filter( channels: Optional[Union[Iterable[Channel], Channel]] = None, instruction_types: Optional[Union[Iterable[abc.ABCMeta], abc.ABCMeta]] = None, @@ -107,17 +174,39 @@ def with_channels(channels: Union[Iterable[Channel], Channel]) -> Callable: """ channels = _if_scalar_cast_to_list(channels) - def channel_filter(time_inst) -> bool: + @singledispatch + def channel_filter(time_inst): + """A catch-TypeError function which will only get called if none of the other decorated + functions, namely handle_numpyndarray() and handle_instruction(), handle the type passed. + """ + raise TypeError( + f"Type '{type(time_inst)}' is not valid data format as an input to channel_filter." + ) + + @channel_filter.register + def handle_numpyndarray(time_inst: np.ndarray) -> bool: """Filter channel. Args: - time_inst (Tuple[int, Instruction]): Time + time_inst (numpy.ndarray([int, Instruction])): Time Returns: If instruction matches with condition. """ return any(chan in channels for chan in time_inst[1].channels) + @channel_filter.register + def handle_instruction(inst: Instruction) -> bool: + """Filter channel. + + Args: + inst: Instruction + + Returns: + If instruction matches with condition. + """ + return any(chan in channels for chan in inst.channels) + return channel_filter @@ -132,17 +221,39 @@ def with_instruction_types(types: Union[Iterable[abc.ABCMeta], abc.ABCMeta]) -> """ types = _if_scalar_cast_to_list(types) + @singledispatch def instruction_filter(time_inst) -> bool: + """A catch-TypeError function which will only get called if none of the other decorated + functions, namely handle_numpyndarray() and handle_instruction(), handle the type passed. + """ + raise TypeError( + f"Type '{type(time_inst)}' is not valid data format as an input to instruction_filter." + ) + + @instruction_filter.register + def handle_numpyndarray(time_inst: np.ndarray) -> bool: """Filter instruction. Args: - time_inst (Tuple[int, Instruction]): Time + time_inst (numpy.ndarray([int, Instruction])): Time Returns: If instruction matches with condition. """ return isinstance(time_inst[1], tuple(types)) + @instruction_filter.register + def handle_instruction(inst: Instruction) -> bool: + """Filter instruction. + + Args: + inst: Instruction + + Returns: + If instruction matches with condition. + """ + return isinstance(inst, tuple(types)) + return instruction_filter diff --git a/qiskit/pulse/schedule.py b/qiskit/pulse/schedule.py index ff806de9a353..6643a03d1a31 100644 --- a/qiskit/pulse/schedule.py +++ b/qiskit/pulse/schedule.py @@ -1326,44 +1326,38 @@ def filter( *filter_funcs: List[Callable], channels: Optional[Iterable[Channel]] = None, instruction_types: Union[Iterable[abc.ABCMeta], abc.ABCMeta] = None, - time_ranges: Optional[Iterable[Tuple[int, int]]] = None, - intervals: Optional[Iterable[Interval]] = None, check_subroutine: bool = True, ): - """Return a new ``Schedule`` with only the instructions from this ``ScheduleBlock`` - which pass though the provided filters; i.e. an instruction will be retained iff + """Return a new ``ScheduleBlock`` with only the instructions from this ``ScheduleBlock`` + which pass though the provided filters; i.e. an instruction will be retained if every function in ``filter_funcs`` returns ``True``, the instruction occurs on - a channel type contained in ``channels``, the instruction type is contained - in ``instruction_types``, and the period over which the instruction operates - is *fully* contained in one specified in ``time_ranges`` or ``intervals``. + a channel type contained in ``channels``, and the instruction type is contained + in ``instruction_types``. - If no arguments are provided, ``self`` is returned. + .. warning:: + Because ``ScheduleBlock`` is not aware of the execution time of + the context instructions, filtering out some instructions may + change the execution time of the remaining instructions. - .. note:: This method is currently not supported. Support will be soon added - please create an issue if you believe this must be prioritized. + If no arguments are provided, ``self`` is returned. Args: - filter_funcs: A list of Callables which take a (int, Union['Schedule', Instruction]) - tuple and return a bool. + filter_funcs: A list of Callables which take a ``Instruction`` and return a bool. channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. instruction_types: For example, ``[PulseInstruction, AcquireInstruction]``. - time_ranges: For example, ``[(0, 5), (6, 10)]``. - intervals: For example, ``[(0, 5), (6, 10)]``. check_subroutine: Set `True` to individually filter instructions inside a subroutine defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. Returns: - ``Schedule`` consisting of instructions that matches with filtering condition. - - Raises: - PulseError: When this method is called. This method will be supported soon. + ``ScheduleBlock`` consisting of instructions that matches with filtering condition. """ - raise PulseError( - "Method ``ScheduleBlock.filter`` is not supported as this program " - "representation does not have the notion of an explicit instruction " - "time. Apply ``qiskit.pulse.transforms.block_to_schedule`` function to " - "this program to obtain the ``Schedule`` representation supporting " - "this method." + from qiskit.pulse.filters import composite_filter, filter_instructions + + filters = composite_filter(channels, instruction_types) + filters.extend(filter_funcs) + + return filter_instructions( + self, filters=filters, negate=False, recurse_subroutines=check_subroutine ) def exclude( @@ -1371,41 +1365,37 @@ def exclude( *filter_funcs: List[Callable], channels: Optional[Iterable[Channel]] = None, instruction_types: Union[Iterable[abc.ABCMeta], abc.ABCMeta] = None, - time_ranges: Optional[Iterable[Tuple[int, int]]] = None, - intervals: Optional[Iterable[Interval]] = None, check_subroutine: bool = True, ): - """Return a ``Schedule`` with only the instructions from this Schedule *failing* - at least one of the provided filters. + """Return a new ``ScheduleBlock`` with only the instructions from this ``ScheduleBlock`` + *failing* at least one of the provided filters. This method is the complement of py:meth:`~self.filter`, so that:: - self.filter(args) | self.exclude(args) == self + self.filter(args) + self.exclude(args) == self in terms of instructions included. - .. note:: This method is currently not supported. Support will be soon added - please create an issue if you believe this must be prioritized. + .. warning:: + Because ``ScheduleBlock`` is not aware of the execution time of + the context instructions, excluding some instructions may + change the execution time of the remaining instructions. Args: - filter_funcs: A list of Callables which take a (int, Union['Schedule', Instruction]) - tuple and return a bool. + filter_funcs: A list of Callables which take a ``Instruction`` and return a bool. channels: For example, ``[DriveChannel(0), AcquireChannel(0)]``. instruction_types: For example, ``[PulseInstruction, AcquireInstruction]``. - time_ranges: For example, ``[(0, 5), (6, 10)]``. - intervals: For example, ``[(0, 5), (6, 10)]``. check_subroutine: Set `True` to individually filter instructions inside of a subroutine defined by the :py:class:`~qiskit.pulse.instructions.Call` instruction. Returns: - ``Schedule`` consisting of instructions that are not match with filtering condition. - - Raises: - PulseError: When this method is called. This method will be supported soon. + ``ScheduleBlock`` consisting of instructions that do not match with + at least one of filtering conditions. """ - raise PulseError( - "Method ``ScheduleBlock.exclude`` is not supported as this program " - "representation does not have the notion of instruction " - "time. Apply ``qiskit.pulse.transforms.block_to_schedule`` function to " - "this program to obtain the ``Schedule`` representation supporting " - "this method." + from qiskit.pulse.filters import composite_filter, filter_instructions + + filters = composite_filter(channels, instruction_types) + filters.extend(filter_funcs) + + return filter_instructions( + self, filters=filters, negate=True, recurse_subroutines=check_subroutine ) def replace( diff --git a/releasenotes/notes/filter-schedule-block-29d392ca351f1fb1.yaml b/releasenotes/notes/filter-schedule-block-29d392ca351f1fb1.yaml new file mode 100644 index 000000000000..b49dd1a2a8c4 --- /dev/null +++ b/releasenotes/notes/filter-schedule-block-29d392ca351f1fb1.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + The method :meth:`~qiskit.pulse.schedule.ScheduleBlock.filter` is activated + in the :class:`~qiskit.pulse.schedule.ScheduleBlock` class. + This method enables users to retain only :class:`~qiskit.pulse.Instruction` + objects which pass through all the provided filters. + As builtin filter conditions, pulse :class:`~qiskit.pulse.channels.Channel` + subclass instance and :class:`~qiskit.pulse.instructions.Instruction` + subclass type can be specified. + User-defined callbacks taking :class:`~qiskit.pulse.instructions.Instruction` instance + can be added to the filters, too. + - | + The method :meth:`~qiskit.pulse.schedule.ScheduleBlock.exclude` is activated + in the :class:`~qiskit.pulse.schedule.ScheduleBlock` class. + This method enables users to retain only :class:`~qiskit.pulse.Instruction` + objects which do not pass at least one of all the provided filters. + As builtin filter conditions, pulse :class:`~qiskit.pulse.channels.Channel` + subclass instance and :class:`~qiskit.pulse.instructions.Instruction` + subclass type can be specified. + User-defined callbacks taking :class:`~qiskit.pulse.instructions.Instruction` instance + can be added to the filters, too. + This method is the complement of :meth:`~qiskit.pulse.schedule.ScheduleBlock.filter`, so + the following condition is always satisfied: + ``block.filter(*filters) + block.exclude(*filters) == block`` in terms of + instructions included, where ``block`` is a :class:`~qiskit.pulse.schedule.ScheduleBlock` + instance. diff --git a/test/python/pulse/test_block.py b/test/python/pulse/test_block.py index d8dd0ab2f6da..90441ef917d5 100644 --- a/test/python/pulse/test_block.py +++ b/test/python/pulse/test_block.py @@ -13,7 +13,9 @@ # pylint: disable=invalid-name """Test cases for the pulse schedule block.""" +import re import unittest +from typing import List, Any from qiskit import pulse, circuit from qiskit.pulse import transforms from qiskit.pulse.exceptions import PulseError @@ -749,3 +751,225 @@ def test_parametrized_context(self): ref_sched = ref_sched.insert(90, pulse.Delay(10, self.d0)) self.assertScheduleEqual(block, ref_sched) + + +class TestBlockFilter(BaseTestBlock): + """Test ScheduleBlock filtering methods.""" + + def test_filter_channels(self): + """Test filtering over channels.""" + with pulse.build() as blk: + pulse.play(self.test_waveform0, self.d0) + pulse.delay(10, self.d0) + pulse.play(self.test_waveform1, self.d1) + + filtered_blk = self._filter_and_test_consistency(blk, channels=[self.d0]) + self.assertEqual(len(filtered_blk.channels), 1) + self.assertTrue(self.d0 in filtered_blk.channels) + with pulse.build() as ref_blk: + pulse.play(self.test_waveform0, self.d0) + pulse.delay(10, self.d0) + self.assertEqual(filtered_blk, ref_blk) + + filtered_blk = self._filter_and_test_consistency(blk, channels=[self.d1]) + self.assertEqual(len(filtered_blk.channels), 1) + self.assertTrue(self.d1 in filtered_blk.channels) + with pulse.build() as ref_blk: + pulse.play(self.test_waveform1, self.d1) + self.assertEqual(filtered_blk, ref_blk) + + filtered_blk = self._filter_and_test_consistency(blk, channels=[self.d0, self.d1]) + self.assertEqual(len(filtered_blk.channels), 2) + for ch in [self.d0, self.d1]: + self.assertTrue(ch in filtered_blk.channels) + self.assertEqual(filtered_blk, blk) + + def test_filter_channels_nested_block(self): + """Test filtering over channels in a nested block.""" + with pulse.build() as blk: + with pulse.align_sequential(): + pulse.play(self.test_waveform0, self.d0) + pulse.delay(5, self.d0) + + with pulse.build(self.backend) as cx_blk: + pulse.cx(0, 1) + + pulse.call(cx_blk) + + for ch in [self.d0, self.d1, pulse.ControlChannel(0)]: + filtered_blk = self._filter_and_test_consistency(blk, channels=[ch]) + self.assertEqual(len(filtered_blk.channels), 1) + self.assertTrue(ch in filtered_blk.channels) + + def test_filter_inst_types(self): + """Test filtering on instruction types.""" + with pulse.build() as blk: + pulse.acquire(5, pulse.AcquireChannel(0), pulse.MemorySlot(0)) + + with pulse.build() as blk_internal: + pulse.play(self.test_waveform1, self.d1) + + pulse.call(blk_internal) + pulse.reference(name="dummy_reference") + pulse.delay(10, self.d0) + pulse.play(self.test_waveform0, self.d0) + pulse.barrier(self.d0, self.d1, pulse.AcquireChannel(0), pulse.MemorySlot(0)) + pulse.set_frequency(10, self.d0) + pulse.shift_frequency(5, self.d1) + pulse.set_phase(3.14 / 4.0, self.d0) + pulse.shift_phase(-3.14 / 2.0, self.d1) + pulse.snapshot(label="dummy_snapshot") + + # test filtering Acquire + filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.Acquire]) + self.assertEqual(len(filtered_blk.blocks), 1) + self.assertIsInstance(filtered_blk.blocks[0], pulse.Acquire) + self.assertEqual(len(filtered_blk.channels), 2) + + # test filtering Reference + filtered_blk = self._filter_and_test_consistency( + blk, instruction_types=[pulse.instructions.Reference] + ) + self.assertEqual(len(filtered_blk.blocks), 1) + self.assertIsInstance(filtered_blk.blocks[0], pulse.instructions.Reference) + + # test filtering Delay + filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.Delay]) + self.assertEqual(len(filtered_blk.blocks), 1) + self.assertIsInstance(filtered_blk.blocks[0], pulse.Delay) + self.assertEqual(len(filtered_blk.channels), 1) + + # test filtering Play + filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.Play]) + self.assertEqual(len(filtered_blk.blocks), 2) + self.assertIsInstance(filtered_blk.blocks[0].blocks[0], pulse.Play) + self.assertIsInstance(filtered_blk.blocks[1], pulse.Play) + self.assertEqual(len(filtered_blk.channels), 2) + + # test filtering RelativeBarrier + filtered_blk = self._filter_and_test_consistency( + blk, instruction_types=[pulse.instructions.RelativeBarrier] + ) + self.assertEqual(len(filtered_blk.blocks), 1) + self.assertIsInstance(filtered_blk.blocks[0], pulse.instructions.RelativeBarrier) + self.assertEqual(len(filtered_blk.channels), 4) + + # test filtering SetFrequency + filtered_blk = self._filter_and_test_consistency( + blk, instruction_types=[pulse.SetFrequency] + ) + self.assertEqual(len(filtered_blk.blocks), 1) + self.assertIsInstance(filtered_blk.blocks[0], pulse.SetFrequency) + self.assertEqual(len(filtered_blk.channels), 1) + + # test filtering ShiftFrequency + filtered_blk = self._filter_and_test_consistency( + blk, instruction_types=[pulse.ShiftFrequency] + ) + self.assertEqual(len(filtered_blk.blocks), 1) + self.assertIsInstance(filtered_blk.blocks[0], pulse.ShiftFrequency) + self.assertEqual(len(filtered_blk.channels), 1) + + # test filtering SetPhase + filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.SetPhase]) + self.assertEqual(len(filtered_blk.blocks), 1) + self.assertIsInstance(filtered_blk.blocks[0], pulse.SetPhase) + self.assertEqual(len(filtered_blk.channels), 1) + + # test filtering ShiftPhase + filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.ShiftPhase]) + self.assertEqual(len(filtered_blk.blocks), 1) + self.assertIsInstance(filtered_blk.blocks[0], pulse.ShiftPhase) + self.assertEqual(len(filtered_blk.channels), 1) + + # test filtering SnapShot + filtered_blk = self._filter_and_test_consistency(blk, instruction_types=[pulse.Snapshot]) + self.assertEqual(len(filtered_blk.blocks), 1) + self.assertIsInstance(filtered_blk.blocks[0], pulse.Snapshot) + self.assertEqual(len(filtered_blk.channels), 1) + + def test_filter_functionals(self): + """Test functional filtering.""" + with pulse.build() as blk: + pulse.play(self.test_waveform0, self.d0, "play0") + pulse.delay(10, self.d0, "delay0") + + with pulse.build() as blk_internal: + pulse.play(self.test_waveform1, self.d1, "play1") + + pulse.call(blk_internal) + pulse.play(self.test_waveform1, self.d1) + + def filter_with_inst_name(inst: pulse.Instruction) -> bool: + try: + if isinstance(inst.name, str): + match_obj = re.search(pattern="play", string=inst.name) + if match_obj is not None: + return True + except AttributeError: + pass + return False + + filtered_blk = self._filter_and_test_consistency(blk, filter_with_inst_name) + self.assertEqual(len(filtered_blk.blocks), 2) + self.assertIsInstance(filtered_blk.blocks[0], pulse.Play) + self.assertIsInstance(filtered_blk.blocks[1].blocks[0], pulse.Play) + self.assertEqual(len(filtered_blk.channels), 2) + + def test_filter_multiple(self): + """Test filter composition.""" + with pulse.build() as blk: + pulse.play(pulse.Constant(100, 0.1, name="play0"), self.d0) + pulse.delay(10, self.d0, "delay0") + + with pulse.build(name="internal_blk") as blk_internal: + pulse.play(pulse.Constant(50, 0.1, name="play1"), self.d0) + + pulse.call(blk_internal) + pulse.barrier(self.d0, self.d1) + pulse.play(pulse.Constant(100, 0.1, name="play2"), self.d1) + + def filter_with_pulse_name(inst: pulse.Instruction) -> bool: + try: + if isinstance(inst.pulse.name, str): + match_obj = re.search(pattern="play", string=inst.pulse.name) + if match_obj is not None: + return True + except AttributeError: + pass + return False + + filtered_blk = self._filter_and_test_consistency( + blk, filter_with_pulse_name, channels=[self.d1], instruction_types=[pulse.Play] + ) + self.assertEqual(len(filtered_blk.blocks), 1) + self.assertIsInstance(filtered_blk.blocks[0], pulse.Play) + self.assertEqual(len(filtered_blk.channels), 1) + + def _filter_and_test_consistency( + self, sched_blk: pulse.ScheduleBlock, *args: Any, **kwargs: Any + ) -> pulse.ScheduleBlock: + """ + Returns sched_blk.filter(*args, **kwargs), + including a test that sched_blk.filter | sched_blk.exclude == sched_blk + in terms of instructions. + """ + filtered = sched_blk.filter(*args, **kwargs) + excluded = sched_blk.exclude(*args, **kwargs) + + def list_instructions(blk: pulse.ScheduleBlock) -> List[pulse.Instruction]: + insts = list() + for element in blk.blocks: + if isinstance(element, pulse.ScheduleBlock): + inner_insts = list_instructions(element) + if len(inner_insts) != 0: + insts.extend(inner_insts) + elif isinstance(element, pulse.Instruction): + insts.append(element) + return insts + + sum_insts = list_instructions(filtered) + list_instructions(excluded) + ref_insts = list_instructions(sched_blk) + self.assertEqual(len(sum_insts), len(ref_insts)) + self.assertTrue(all(inst in ref_insts for inst in sum_insts)) + return filtered From f91136654b8934e618c895173504c8063251ef8c Mon Sep 17 00:00:00 2001 From: Kento Ueda <38037695+to24toro@users.noreply.github.com> Date: Wed, 7 Jun 2023 20:29:01 +0900 Subject: [PATCH 136/172] Fix the output macros.measure with backendV2 (#10135) * fix measure_v2 * modify measure_all * fix meas_map input in measure_v2 * add reno * fix reno * Update reno. Co-authored-by: Naoki Kanazawa * fix reno again --------- Co-authored-by: Naoki Kanazawa --- qiskit/pulse/macros.py | 22 ++--- ...utputs-of-measure_v2-8959ebbbf5f87294.yaml | 7 ++ test/python/pulse/test_macros.py | 92 +++++++++++++++---- 3 files changed, 87 insertions(+), 34 deletions(-) create mode 100644 releasenotes/notes/fix-outputs-of-measure_v2-8959ebbbf5f87294.yaml diff --git a/qiskit/pulse/macros.py b/qiskit/pulse/macros.py index d4ba4768ef57..357f5979fbd4 100644 --- a/qiskit/pulse/macros.py +++ b/qiskit/pulse/macros.py @@ -63,16 +63,11 @@ def measure( # backend is V2. if hasattr(backend, "target"): - try: - meas_map = backend.configuration().meas_map - except AttributeError: - # TODO add meas_map to Target in 0.25 - meas_map = [list(range(backend.num_qubits))] return _measure_v2( qubits=qubits, target=backend.target, - meas_map=meas_map, + meas_map=meas_map or backend.meas_map, qubit_mem_slots=qubit_mem_slots or dict(zip(qubits, range(len(qubits)))), measure_name=measure_name, ) @@ -198,12 +193,7 @@ def _measure_v2( channels.AcquireChannel(measure_qubit), ] ) - else: - default_sched = target.get_calibration(measure_name, (measure_qubit,)).filter( - channels=[ - channels.AcquireChannel(measure_qubit), - ] - ) + 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 '{}'. " @@ -211,7 +201,6 @@ def _measure_v2( "argument. For assistance, the instructions which are defined are: " "{}".format(measure_name, target.instructions) ) from ex - schedule += _schedule_remapping_memory_slot(default_sched, qubit_mem_slots) return schedule @@ -226,7 +215,12 @@ def measure_all(backend) -> Schedule: Returns: A schedule corresponding to the inputs provided. """ - return measure(qubits=list(range(backend.configuration().n_qubits)), backend=backend) + # backend is V2. + if hasattr(backend, "target"): + qubits = list(range(backend.num_qubits)) + else: + qubits = list(range(backend.configuration().n_qubits)) + return measure(qubits=qubits, backend=backend) def _schedule_remapping_memory_slot( diff --git a/releasenotes/notes/fix-outputs-of-measure_v2-8959ebbbf5f87294.yaml b/releasenotes/notes/fix-outputs-of-measure_v2-8959ebbbf5f87294.yaml new file mode 100644 index 000000000000..0699d854a1de --- /dev/null +++ b/releasenotes/notes/fix-outputs-of-measure_v2-8959ebbbf5f87294.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed the output of pulse :func:`~qiskit.pulse.macros.measure` and + :func:`~qiskit.pulse.macros.measure_all` when functions are called + with the :class:`.BackendV2` backend. + diff --git a/test/python/pulse/test_macros.py b/test/python/pulse/test_macros.py index a3869fd11b7d..8c82e0c569be 100644 --- a/test/python/pulse/test_macros.py +++ b/test/python/pulse/test_macros.py @@ -24,7 +24,7 @@ ) from qiskit.pulse import macros from qiskit.pulse.exceptions import PulseError -from qiskit.providers.fake_provider import FakeOpenPulse2Q, FakeHanoiV2 +from qiskit.providers.fake_provider import FakeOpenPulse2Q, FakeHanoi, FakeHanoiV2 from qiskit.test import QiskitTestCase @@ -95,13 +95,8 @@ def test_measure_v2(self): """Test macro - measure with backendV2.""" sched = macros.measure(qubits=[0], backend=self.backend_v2) expected = self.backend_v2.target.get_calibration("measure", (0,)).filter( - channels=[ - MeasureChannel(0), - ] + channels=[MeasureChannel(0), AcquireChannel(0)] ) - measure_duration = expected.filter(instruction_types=[Play]).duration - for qubit in range(self.backend_v2.num_qubits): - expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(qubit)) self.assertEqual(sched.instructions, expected.instructions) def test_measure_v2_sched_with_qubit_mem_slots(self): @@ -113,15 +108,7 @@ def test_measure_v2_sched_with_qubit_mem_slots(self): ] ) measure_duration = expected.filter(instruction_types=[Play]).duration - for qubit in range(self.backend_v2.num_qubits): - if qubit == 0: - expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(2)) - elif qubit == 1: - expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(0)) - elif qubit == 2: - expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(1)) - else: - expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(qubit)) + expected += Acquire(measure_duration, AcquireChannel(0), MemorySlot(2)) self.assertEqual(sched.instructions, expected.instructions) def test_measure_v2_sched_with_meas_map(self): @@ -138,8 +125,7 @@ def test_measure_v2_sched_with_meas_map(self): ] ) measure_duration = expected.filter(instruction_types=[Play]).duration - for qubit in range(self.backend_v2.num_qubits): - expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(qubit)) + expected += Acquire(measure_duration, AcquireChannel(0), MemorySlot(0)) self.assertEqual(sched_with_meas_map_list.instructions, expected.instructions) self.assertEqual(sched_with_meas_map_dict.instructions, expected.instructions) @@ -159,10 +145,58 @@ def test_multiple_measure_v2(self): measure_duration = expected.filter(instruction_types=[Play]).duration expected += Acquire(measure_duration, AcquireChannel(0), MemorySlot(0)) expected += Acquire(measure_duration, AcquireChannel(1), MemorySlot(1)) - for qubit in range(2, self.backend_v2.num_qubits): - expected += Acquire(measure_duration, AcquireChannel(qubit), MemorySlot(qubit)) self.assertEqual(sched.instructions, expected.instructions) + def test_output_with_measure_v1_and_measure_v2(self): + """Test make outputs of measure_v1 and measure_v2 consistent.""" + sched_measure_v1 = macros.measure(qubits=[0, 1], backend=FakeHanoi()) + sched_measure_v2 = macros.measure(qubits=[0, 1], backend=self.backend_v2) + self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) + + def test_output_with_measure_v1_and_measure_v2_sched_with_qubit_mem_slots(self): + """Test make outputs of measure_v1 and measure_v2 with custom qubit_mem_slots consistent.""" + sched_measure_v1 = macros.measure(qubits=[0], backend=FakeHanoi(), qubit_mem_slots={0: 2}) + sched_measure_v2 = macros.measure( + qubits=[0], backend=self.backend_v2, qubit_mem_slots={0: 2} + ) + self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) + + def test_output_with_measure_v1_and_measure_v2_sched_with_meas_map(self): + """Test make outputs of measure_v1 and measure_v2 + with custom meas_map as list and dict consistent.""" + num_qubits_list_measure_v1 = list(range(FakeHanoi().configuration().num_qubits)) + num_qubits_list_measure_v2 = list(range(self.backend_v2.num_qubits)) + sched_with_meas_map_list_v1 = macros.measure( + qubits=[0], backend=FakeHanoi(), meas_map=[num_qubits_list_measure_v1] + ) + sched_with_meas_map_dict_v1 = macros.measure( + qubits=[0], + backend=FakeHanoi(), + meas_map={0: num_qubits_list_measure_v1, 1: num_qubits_list_measure_v1}, + ) + sched_with_meas_map_list_v2 = macros.measure( + qubits=[0], backend=self.backend_v2, meas_map=[num_qubits_list_measure_v2] + ) + sched_with_meas_map_dict_v2 = macros.measure( + qubits=[0], + backend=self.backend_v2, + meas_map={0: num_qubits_list_measure_v2, 1: num_qubits_list_measure_v2}, + ) + self.assertEqual( + sched_with_meas_map_list_v1.instructions, + sched_with_meas_map_list_v2.instructions, + ) + self.assertEqual( + sched_with_meas_map_dict_v1.instructions, + sched_with_meas_map_dict_v2.instructions, + ) + + def test_output_with_multiple_measure_v1_and_measure_v2(self): + """Test macro - consistent output of multiple qubit measure with backendV1 and backendV2.""" + sched_measure_v1 = macros.measure(qubits=[0, 1], backend=FakeHanoi()) + sched_measure_v2 = macros.measure(qubits=[0, 1], backend=self.backend_v2) + self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) + class TestMeasureAll(QiskitTestCase): """Pulse measure all macro.""" @@ -170,6 +204,7 @@ class TestMeasureAll(QiskitTestCase): def setUp(self): super().setUp() self.backend = FakeOpenPulse2Q() + self.backend_v2 = FakeHanoiV2() self.inst_map = self.backend.defaults().instruction_schedule_map def test_measure_all(self): @@ -177,3 +212,20 @@ def test_measure_all(self): sched = macros.measure_all(self.backend) expected = Schedule(self.inst_map.get("measure", [0, 1])) self.assertEqual(sched.instructions, expected.instructions) + + def test_measure_all_v2(self): + """Test measure_all function with backendV2.""" + backend_v1 = FakeHanoi() + sched = macros.measure_all(self.backend_v2) + expected = Schedule( + backend_v1.defaults().instruction_schedule_map.get( + "measure", list(range(backend_v1.configuration().num_qubits)) + ) + ) + self.assertEqual(sched.instructions, expected.instructions) + + def test_output_of_measure_all_with_backend_v1_and_v2(self): + """Test make outputs of measure_all with backendV1 and backendV2 consistent.""" + sched_measure_v1 = macros.measure_all(backend=FakeHanoi()) + sched_measure_v2 = macros.measure_all(backend=self.backend_v2) + self.assertEqual(sched_measure_v1.instructions, sched_measure_v2.instructions) From cff51cea9bd136534154fca4a5f29631022500ed Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Jun 2023 13:30:39 +0000 Subject: [PATCH 137/172] Bump rustworkx-core from 0.12.1 to 0.13.0 (#10237) Bumps [rustworkx-core](https://github.com/Qiskit/rustworkx) from 0.12.1 to 0.13.0. - [Release notes](https://github.com/Qiskit/rustworkx/releases) - [Commits](https://github.com/Qiskit/rustworkx/compare/0.12.1...0.13.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> --- Cargo.lock | 41 +++++++++++++++++++++++++----------- crates/accelerate/Cargo.toml | 2 +- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a0573c59a892..2b82a72f31ab 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -111,20 +111,14 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.11.2" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ "ahash 0.7.6", "rayon", ] -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.13.2" @@ -161,6 +155,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "libc" version = "0.2.144" @@ -519,6 +522,17 @@ dependencies = [ "rayon-core", ] +[[package]] +name = "rayon-cond" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ac2a28c5317e6d26ac87a8629c0eb362690ed1d739f4040e21cfaafdf04e6f8" +dependencies = [ + "either", + "itertools", + "rayon", +] + [[package]] name = "rayon-core" version = "1.11.0" @@ -548,18 +562,21 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustworkx-core" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3ba36e6bf3cc44ce42ad35e75db9e90c39c701d6590a296372e392c4872478" +checksum = "e3e9d46abff559cde170ba3bca60c58698356053a395419d29a26f512df68f88" dependencies = [ - "ahash 0.7.6", + "ahash 0.8.3", "fixedbitset", - "hashbrown 0.11.2", + "hashbrown 0.12.3", "indexmap", "num-traits", "petgraph", "priority-queue", + "rand", + "rand_pcg", "rayon", + "rayon-cond", ] [[package]] diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 983729ddeb1b..4f4a08cd6eca 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -20,7 +20,7 @@ rand_distr = "0.4.3" ahash = "0.8.3" num-complex = "0.4" num-bigint = "0.4" -rustworkx-core = "0.12" +rustworkx-core = "0.13" # The base version of PyO3 and setting a minimum feature set (e.g. probably just 'extension-module') # can be done in the workspace and inherited once we hit Rust 1.64. From b36609befddce056383aba85d7f326fcac9edd7b Mon Sep 17 00:00:00 2001 From: "Daniel J. Egger" <38065505+eggerdj@users.noreply.github.com> Date: Fri, 9 Jun 2023 19:59:25 +0100 Subject: [PATCH 138/172] FullAncillaAllocation for backends witout a coupling map (#10240) * * Added support for targets without a coupling map. * * Removed restrictive raise. * * Changed order of target and coupling map * Apply suggestions from code review Co-authored-by: Matthew Treinish --------- Co-authored-by: Matthew Treinish --- .../passes/layout/full_ancilla_allocation.py | 12 ++++++---- ...a_allocation_no_cmap-ac3ff65b3639988e.yaml | 7 ++++++ .../test_full_ancilla_allocation.py | 22 +++++++++++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/ancilla_allocation_no_cmap-ac3ff65b3639988e.yaml diff --git a/qiskit/transpiler/passes/layout/full_ancilla_allocation.py b/qiskit/transpiler/passes/layout/full_ancilla_allocation.py index e3259b4d0ad9..81ae7922806e 100644 --- a/qiskit/transpiler/passes/layout/full_ancilla_allocation.py +++ b/qiskit/transpiler/passes/layout/full_ancilla_allocation.py @@ -19,11 +19,11 @@ class FullAncillaAllocation(AnalysisPass): - """Allocate all idle nodes from the coupling map as ancilla on the layout. + """Allocate all idle nodes from the coupling map or target as ancilla on the layout. A pass for allocating all idle physical qubits (those that exist in coupling - map but not the dag circuit) as ancilla. It will also choose new virtual - qubits to correspond to those physical ancilla. + map or target but not the dag circuit) as ancilla. It will also choose new + virtual qubits to correspond to those physical ancilla. Note: This is an analysis pass, and only responsible for choosing physical @@ -81,7 +81,11 @@ def run(self, dag): idle_physical_qubits = [q for q in layout_physical_qubits if q not in physical_bits] - if self.coupling_map: + if self.target: + idle_physical_qubits = [ + q for q in range(self.target.num_qubits) if q not in physical_bits + ] + elif self.coupling_map: idle_physical_qubits = [ q for q in self.coupling_map.physical_qubits if q not in physical_bits ] diff --git a/releasenotes/notes/ancilla_allocation_no_cmap-ac3ff65b3639988e.yaml b/releasenotes/notes/ancilla_allocation_no_cmap-ac3ff65b3639988e.yaml new file mode 100644 index 000000000000..ac8d2c1d9eb1 --- /dev/null +++ b/releasenotes/notes/ancilla_allocation_no_cmap-ac3ff65b3639988e.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed an issue with :class:`~.FullAncillaAllocation` so it can now function with :class:`~.Target` objects that do not have a + coupling map (typically because there are no 2 qubit gates in the :class:`~.Target`). In this case :class:`~.FullAncillaAllocation` will add + ancilla qubits so that the number of qubits in the :class:`~.DAGCircuit` matches the number + :attr:`.Target.num_qubits`. diff --git a/test/python/transpiler/test_full_ancilla_allocation.py b/test/python/transpiler/test_full_ancilla_allocation.py index 741b3b98a2b4..680d01bcd05d 100644 --- a/test/python/transpiler/test_full_ancilla_allocation.py +++ b/test/python/transpiler/test_full_ancilla_allocation.py @@ -214,6 +214,28 @@ def test_bad_layout(self): cm.exception.message, ) + def test_target_without_cmap(self): + """Test that FullAncillaAllocation works when the target does not have a coupling map. + + This situation occurs at the early stages of backend bring-up. + """ + target_data = {"basis_gates": ["h"], "num_qubits": 3} + target = Target.from_configuration(**target_data) + + circ = QuantumCircuit(1) + circ.h(0) + + pass_ = FullAncillaAllocation(target) + pass_.property_set["layout"] = Layout.from_intlist([0], *circ.qregs) + + # Pre pass check + self.assertEqual(len(pass_.property_set["layout"]), 1) + + pass_.run(circuit_to_dag(circ)) + + # Post pass check + self.assertEqual(len(pass_.property_set["layout"]), target.num_qubits) + if __name__ == "__main__": unittest.main() From c463b3c4741c7532494a583633d2798352fc828e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Mon, 12 Jun 2023 09:45:27 -0400 Subject: [PATCH 139/172] Use stable Python C API for building Rust extension (#10120) * Use stable Python C API for building Rust extension This commit tweaks the rust extension code to start using the PyO3 abi3 flag to build binaries that are compatible with all python versions, not just a single release. Previously, we were building against the version specific C API and that resulted in needing abinary file for each supported python version on each supported platform/architecture. By using the abi3 feature flag and marking the wheels as being built with the limited api we can reduce our packaging overhead to just having one wheel file per supported platform/architecture. The only real code change needed here was to update the memory marginalization function. PyO3's abi3 feature is incompatible with returning a big int object from rust (the C API they use for that conversion isn't part of the stable C API). So this commit updates the function to convert to create a python int manually using the PyO3 api where that was being done before. Co-authored-by: Jake Lishman * Set minimum version on abi3 flag to Python 3.8 * Fix lint * Use py_limited_api="auto" on RustExtension According to the docs for the setuptools-rust RustExtension class: https://setuptools-rust.readthedocs.io/en/latest/reference.html#setuptools_rust.RustExtension The best setting to use for the py_limited_api argument is `"auto"` as this will use the setting in the PyO3 module to determine the correct value to set. This commit updates the setup.py to follow the recommendation in the docs. * Update handling of phase input to expval rust calls The pauli_expval module in Rust that Statevector and DensityMatrix leverage when computing defines the input type of the phase argument as Complex64. Previously, the quantum info code in the Statevector and DensityMatrix classes were passing in a 1 element ndarray for this parameter. When using the the version specific Python C API in pyo3 it would convert the single element array to a scalar value. However when using abi3 this handling was not done (or was not done correctly) and this caused the tests to fail. This commit updates the quantum info module to pass the phase as a complex value instead of a 1 element numpy array to bypass this behavior change in PyO3 when using abi3. Co-authored-by: Jake Lishman * Set py_limited_api explicitly to True * DNM: Test cibuildwheel works with abi3 * Add abi3audit to cibuildwheel repair step * Force setuptools to use abi3 tag * Add wheel to sdist build * Workaround abiaudit3 not moving wheels and windows not having a default repair command * Add source of setup.py hack * Add comment about pending pyo3 abi3 bigint support * Revert "DNM: Test cibuildwheel works with abi3" This reverts commit 8ca24cf1e4044e28dd96335d04b9b344d1019c60. * Add release note * Simplify setting abi3 tag in built wheels * Update releasenotes/notes/use-abi3-4a935e0557d3833b.yaml Co-authored-by: Jake Lishman * Update release note * Update releasenotes/notes/use-abi3-4a935e0557d3833b.yaml --------- Co-authored-by: Jake Lishman Co-authored-by: Jake Lishman --- .azure/wheels.yml | 2 +- .github/workflows/wheels.yml | 6 ++-- crates/accelerate/Cargo.toml | 2 +- .../accelerate/src/results/marginalization.rs | 31 +++++++++++-------- crates/qasm2/Cargo.toml | 2 +- pyproject.toml | 7 +++++ qiskit/quantum_info/states/densitymatrix.py | 1 + qiskit/quantum_info/states/statevector.py | 1 + .../notes/use-abi3-4a935e0557d3833b.yaml | 17 ++++++++++ setup.py | 6 +++- 10 files changed, 55 insertions(+), 20 deletions(-) create mode 100644 releasenotes/notes/use-abi3-4a935e0557d3833b.yaml diff --git a/.azure/wheels.yml b/.azure/wheels.yml index 5b106747fd2d..fa8ecd17a051 100644 --- a/.azure/wheels.yml +++ b/.azure/wheels.yml @@ -25,7 +25,7 @@ jobs: - bash: | set -e python -m pip install --upgrade pip - python -m pip install cibuildwheel==2.11.2 + python -m pip install cibuildwheel==2.13.0 python -m pip install -U twine cibuildwheel --output-dir wheelhouse . diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 7b7bebc33e0a..8a7e22bb9c82 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -24,7 +24,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.11.2 + uses: pypa/cibuildwheel@v2.13.0 env: CIBW_ARCHS_LINUX: s390x CIBW_TEST_SKIP: "cp*" @@ -57,7 +57,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.11.2 + uses: pypa/cibuildwheel@v2.13.0 env: CIBW_ARCHS_LINUX: ppc64le CIBW_TEST_SKIP: "cp*" @@ -90,7 +90,7 @@ jobs: with: platforms: all - name: Build wheels - uses: pypa/cibuildwheel@v2.11.2 + uses: pypa/cibuildwheel@v2.13.0 env: CIBW_ARCHS_LINUX: aarch64 - uses: actions/upload-artifact@v3 diff --git a/crates/accelerate/Cargo.toml b/crates/accelerate/Cargo.toml index 4f4a08cd6eca..dea4382ef544 100644 --- a/crates/accelerate/Cargo.toml +++ b/crates/accelerate/Cargo.toml @@ -26,7 +26,7 @@ rustworkx-core = "0.13" # can be done in the workspace and inherited once we hit Rust 1.64. [dependencies.pyo3] version = "0.19.0" -features = ["extension-module", "hashbrown", "indexmap", "num-complex", "num-bigint"] +features = ["extension-module", "hashbrown", "indexmap", "num-complex", "num-bigint", "abi3-py38"] [dependencies.ndarray] version = "^0.15.6" diff --git a/crates/accelerate/src/results/marginalization.rs b/crates/accelerate/src/results/marginalization.rs index f71511f0e1b6..08e1b42e287e 100644 --- a/crates/accelerate/src/results/marginalization.rs +++ b/crates/accelerate/src/results/marginalization.rs @@ -152,19 +152,24 @@ pub fn marginal_memory( .collect() }; if return_int { - if out_mem.len() < parallel_threshold || !run_in_parallel { - Ok(out_mem - .iter() - .map(|x| BigUint::parse_bytes(x.as_bytes(), 2).unwrap()) - .collect::>() - .to_object(py)) - } else { - Ok(out_mem - .par_iter() - .map(|x| BigUint::parse_bytes(x.as_bytes(), 2).unwrap()) - .collect::>() - .to_object(py)) - } + // Replace with: + // + // .iter() + // .map(|x| BigUint::parse_bytes(x.as_bytes(), 2).unwrap()) + // .collect::>() + // .to_object(py)) + // + // (also this can be done in parallel, see + // https://github.com/Qiskit/qiskit-terra/pull/10120 for more + // details) + // + // After PyO3/pyo3#3198 is included in a pyo3 release. + let int_pyobject = py.import("builtins")?.getattr("int")?; + Ok(out_mem + .iter() + .map(|x| int_pyobject.call1((x, 2u8)).unwrap()) + .collect::>() + .to_object(py)) } else { Ok(out_mem.to_object(py)) } diff --git a/crates/qasm2/Cargo.toml b/crates/qasm2/Cargo.toml index 5a82991f3f02..91fa6472d261 100644 --- a/crates/qasm2/Cargo.toml +++ b/crates/qasm2/Cargo.toml @@ -13,4 +13,4 @@ crate-type = ["cdylib"] [dependencies] hashbrown = "0.13.2" -pyo3 = { version = "0.19.0", features = ["extension-module"] } +pyo3 = { version = "0.19.0", features = ["extension-module", "abi3-py38"] } diff --git a/pyproject.toml b/pyproject.toml index 53b83021584a..9a9663779a56 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,13 @@ environment = 'RUSTUP_TOOLCHAIN="stable"' [tool.cibuildwheel.linux] before-all = "yum install -y wget && {package}/tools/install_rust.sh" environment = 'PATH="$PATH:$HOME/.cargo/bin" CARGO_NET_GIT_FETCH_WITH_CLI="true" RUSTUP_TOOLCHAIN="stable"' +repair-wheel-command = "auditwheel repair -w {dest_dir} {wheel} && pipx run abi3audit --strict --report {wheel}" + +[tool.cibuildwheel.macos] +repair-wheel-command = "delocate-wheel --require-archs {delocate_archs} -w {dest_dir} -v {wheel} && pipx run abi3audit --strict --report {wheel}" + +[tool.cibuildwheel.windows] +repair-wheel-command = "cp {wheel} {dest_dir}/. && pipx run abi3audit --strict --report {wheel}" [tool.ruff] select = [ diff --git a/qiskit/quantum_info/states/densitymatrix.py b/qiskit/quantum_info/states/densitymatrix.py index 3103ef23036a..2b9555eb1e67 100644 --- a/qiskit/quantum_info/states/densitymatrix.py +++ b/qiskit/quantum_info/states/densitymatrix.py @@ -382,6 +382,7 @@ def _expectation_value_pauli(self, pauli, qargs=None): x_max = qubits[pauli.x][-1] y_phase = (-1j) ** pauli._count_y() + y_phase = y_phase[0] return pauli_phase * density_expval_pauli_with_x( data, self.num_qubits, z_mask, x_mask, y_phase, x_max ) diff --git a/qiskit/quantum_info/states/statevector.py b/qiskit/quantum_info/states/statevector.py index 5c63a6628379..639f147b1e71 100644 --- a/qiskit/quantum_info/states/statevector.py +++ b/qiskit/quantum_info/states/statevector.py @@ -476,6 +476,7 @@ def _expectation_value_pauli(self, pauli, qargs=None): x_max = qubits[pauli.x][-1] y_phase = (-1j) ** pauli._count_y() + y_phase = y_phase[0] return pauli_phase * expval_pauli_with_x( self.data, self.num_qubits, z_mask, x_mask, y_phase, x_max diff --git a/releasenotes/notes/use-abi3-4a935e0557d3833b.yaml b/releasenotes/notes/use-abi3-4a935e0557d3833b.yaml new file mode 100644 index 000000000000..db946ded97ef --- /dev/null +++ b/releasenotes/notes/use-abi3-4a935e0557d3833b.yaml @@ -0,0 +1,17 @@ +--- +upgrade: + - | + By default Qiskit builds its compiled extensions using the + `Python Stable ABI `__ + with support back to the oldest version of Python supported by Qiskit + (currently 3.8). This means that moving forward there + will be a single precompiled wheels that are shipped on release that + works with all of Qiskit's supported Python versions. There isn't any + expected runtime performance difference using the limited API so it is + enabled by default for all builds now. + Previously, the compiled extensions were built using the version specific API and + would only work with a single Python version. This change was made + to reduce the number of package files we need to build and publish in each + release. When building Qiskit from source there should be no changes + necessary to the build process except that the default tags in the output + filenames will be different to reflect the use of the limited API. diff --git a/setup.py b/setup.py index 352df8669fe8..ec0420f70691 100755 --- a/setup.py +++ b/setup.py @@ -110,9 +110,13 @@ debug=rust_debug, ), RustExtension( - "qiskit._qasm2", "crates/qasm2/Cargo.toml", binding=Binding.PyO3, debug=rust_debug + "qiskit._qasm2", + "crates/qasm2/Cargo.toml", + binding=Binding.PyO3, + debug=rust_debug, ), ], + options={"bdist_wheel": {"py_limited_api": "cp38"}}, zip_safe=False, entry_points={ "qiskit.unitary_synthesis": [ From a1615a864f9faedc9b116d7972e2502a3445d078 Mon Sep 17 00:00:00 2001 From: Kento Ueda <38037695+to24toro@users.noreply.github.com> Date: Tue, 13 Jun 2023 01:17:23 +0900 Subject: [PATCH 140/172] Dispatch a builder with backendV1 and backendV2 (#10150) * fix measure_v2 * modify measure_all * dispatch backend * add test of the builder with backendV2 * reconfigure test codes and some func * refactoring * add reno * fix _measure_v2 * fix backend.meas_map in measure_v2 * fix reno * delete get_qubit_channels from utils * add get_qubits_channels in qubit_channels * recostruct test about the builder with backendV2 * fix descriptions of test_macros * fix descriptions of test_macros again * delete import of backendV2 in utils * revert no need to modify code * Update releasenotes/notes/fix-dispatching-backends-28aff96f726ca9c5.yaml Co-authored-by: Naoki Kanazawa * Update a commnet in qiskit/pulse/builder.py Co-authored-by: Naoki Kanazawa * remove import TYPE_CHECKING * removed test_builder.py utils.py and test_analyzation.py from pull request --------- Co-authored-by: Naoki Kanazawa --- qiskit/pulse/builder.py | 66 +++- qiskit/pulse/macros.py | 5 +- ...dispatching-backends-28aff96f726ca9c5.yaml | 4 + test/python/pulse/test_builder_v2.py | 353 ++++++++++++++++++ 4 files changed, 420 insertions(+), 8 deletions(-) create mode 100644 releasenotes/notes/fix-dispatching-backends-28aff96f726ca9c5.yaml create mode 100644 test/python/pulse/test_builder_v2.py diff --git a/qiskit/pulse/builder.py b/qiskit/pulse/builder.py index 3a34e31e817d..27615c26f296 100644 --- a/qiskit/pulse/builder.py +++ b/qiskit/pulse/builder.py @@ -495,6 +495,7 @@ library, transforms, ) +from qiskit.providers.backend import BackendV2 from qiskit.pulse.instructions import directives from qiskit.pulse.schedule import Schedule, ScheduleBlock from qiskit.pulse.transforms.alignments import AlignmentKind @@ -677,6 +678,9 @@ def get_context(self) -> ScheduleBlock: @_requires_backend def num_qubits(self): """Get the number of qubits in the backend.""" + # backendV2 + if isinstance(self.backend, BackendV2): + return self.backend.num_qubits return self.backend.configuration().n_qubits @property @@ -1105,6 +1109,8 @@ def num_qubits() -> int: .. note:: Requires the active builder context to have a backend set. """ + if isinstance(active_backend(), BackendV2): + return active_backend().num_qubits return active_backend().configuration().n_qubits @@ -1120,6 +1126,12 @@ def seconds_to_samples(seconds: Union[float, np.ndarray]) -> Union[int, np.ndarr Returns: The number of samples for the time to elapse """ + # backendV2 + if isinstance(active_backend(), BackendV2): + if isinstance(seconds, np.ndarray): + return (seconds / active_backend().dt).astype(int) + else: + return int(seconds / active_backend().dt) if isinstance(seconds, np.ndarray): return (seconds / active_backend().configuration().dt).astype(int) return int(seconds / active_backend().configuration().dt) @@ -1135,6 +1147,9 @@ def samples_to_seconds(samples: Union[int, np.ndarray]) -> Union[float, np.ndarr Returns: The time that elapses in ``samples``. """ + # backendV2 + if isinstance(active_backend(), BackendV2): + return samples * active_backend().dt return samples * active_backend().configuration().dt @@ -1163,6 +1178,31 @@ def qubit_channels(qubit: int) -> Set[chans.Channel]: such as in the case where significant crosstalk exists. """ + + # implement as the inner function to avoid API change for a patch release in 0.24.2. + def get_qubit_channels_v2(backend: BackendV2, qubit: int): + r"""Return a list of channels which operate on the given ``qubit``. + Returns: + List of ``Channel``\s operated on my the given ``qubit``. + """ + channels = [] + + # add multi-qubit channels + for node_qubits in backend.coupling_map: + if qubit in node_qubits: + control_channel = backend.control_channel(node_qubits) + if control_channel: + channels.extend(control_channel) + + # add single qubit channels + channels.append(backend.drive_channel(qubit)) + channels.append(backend.measure_channel(qubit)) + channels.append(backend.acquire_channel(qubit)) + return channels + + # backendV2 + if isinstance(active_backend(), BackendV2): + return set(get_qubit_channels_v2(active_backend(), qubit)) return set(active_backend().configuration().get_qubit_channels(qubit)) @@ -1648,7 +1688,11 @@ def frequency_offset( finally: if compensate_phase: duration = builder.get_context().duration - t0 - dt = active_backend().configuration().dt + # backendV2 + if isinstance(active_backend(), BackendV2): + dt = active_backend().dt + else: + dt = active_backend().configuration().dt accumulated_phase = 2 * np.pi * ((duration * dt * frequency) % 1) for channel in channels: shift_phase(-accumulated_phase, channel) @@ -1675,6 +1719,9 @@ def drive_channel(qubit: int) -> chans.DriveChannel: .. note:: Requires the active builder context to have a backend set. """ + # backendV2 + if isinstance(active_backend(), BackendV2): + return active_backend().drive_channel(qubit) return active_backend().configuration().drive(qubit) @@ -1695,6 +1742,9 @@ def measure_channel(qubit: int) -> chans.MeasureChannel: .. note:: Requires the active builder context to have a backend set. """ + # backendV2 + if isinstance(active_backend(), BackendV2): + return active_backend().measure_channel(qubit) return active_backend().configuration().measure(qubit) @@ -1715,6 +1765,9 @@ def acquire_channel(qubit: int) -> chans.AcquireChannel: .. note:: Requires the active builder context to have a backend set. """ + # backendV2 + if isinstance(active_backend(), BackendV2): + return active_backend().acquire_channel(qubit) return active_backend().configuration().acquire(qubit) @@ -1745,6 +1798,9 @@ def control_channels(*qubits: Iterable[int]) -> List[chans.ControlChannel]: List of control channels associated with the supplied ordered list of qubits. """ + # backendV2 + if isinstance(active_backend(), BackendV2): + return active_backend().control_channel(qubits) return active_backend().configuration().control(qubits=qubits) @@ -2428,11 +2484,9 @@ def measure( registers = list(registers) except TypeError: registers = [registers] - measure_sched = macros.measure( qubits=qubits, - inst_map=backend.defaults().instruction_schedule_map, - meas_map=backend.configuration().meas_map, + backend=backend, qubit_mem_slots={qubit: register.index for qubit, register in zip(qubits, registers)}, ) @@ -2478,10 +2532,10 @@ def measure_all() -> List[chans.MemorySlot]: backend = active_backend() qubits = range(num_qubits()) registers = [chans.MemorySlot(qubit) for qubit in qubits] + measure_sched = macros.measure( qubits=qubits, - inst_map=backend.defaults().instruction_schedule_map, - meas_map=backend.configuration().meas_map, + backend=backend, qubit_mem_slots={qubit: qubit for qubit in qubits}, ) diff --git a/qiskit/pulse/macros.py b/qiskit/pulse/macros.py index 357f5979fbd4..f597ce1b4aea 100644 --- a/qiskit/pulse/macros.py +++ b/qiskit/pulse/macros.py @@ -18,6 +18,7 @@ from qiskit.pulse import channels, exceptions, instructions, utils from qiskit.pulse.instruction_schedule_map import InstructionScheduleMap from qiskit.pulse.schedule import Schedule +from qiskit.providers.backend import BackendV2 if TYPE_CHECKING: @@ -62,7 +63,7 @@ def measure( """ # backend is V2. - if hasattr(backend, "target"): + if isinstance(backend, BackendV2): return _measure_v2( qubits=qubits, @@ -216,7 +217,7 @@ def measure_all(backend) -> Schedule: A schedule corresponding to the inputs provided. """ # backend is V2. - if hasattr(backend, "target"): + if isinstance(backend, BackendV2): qubits = list(range(backend.num_qubits)) else: qubits = list(range(backend.configuration().n_qubits)) diff --git a/releasenotes/notes/fix-dispatching-backends-28aff96f726ca9c5.yaml b/releasenotes/notes/fix-dispatching-backends-28aff96f726ca9c5.yaml new file mode 100644 index 000000000000..5ab5aee5bc51 --- /dev/null +++ b/releasenotes/notes/fix-dispatching-backends-28aff96f726ca9c5.yaml @@ -0,0 +1,4 @@ +--- +fixes: + - | + Fixed an failure of the pulse builder when the context is initialized with :class:`.BackendV2`. diff --git a/test/python/pulse/test_builder_v2.py b/test/python/pulse/test_builder_v2.py new file mode 100644 index 000000000000..1f194a9d3c29 --- /dev/null +++ b/test/python/pulse/test_builder_v2.py @@ -0,0 +1,353 @@ +# 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 pulse builder with backendV2 context utilities.""" + +import numpy as np + +from qiskit import circuit, pulse +from qiskit.pulse import builder, macros + +from qiskit.pulse.instructions import directives +from qiskit.pulse.transforms import target_qobj_transform +from qiskit.providers.fake_provider import FakeMumbaiV2 +from qiskit.pulse import instructions +from qiskit.test import QiskitTestCase + + +class TestBuilderV2(QiskitTestCase): + """Test the pulse builder context with backendV2.""" + + def setUp(self): + super().setUp() + self.backend = FakeMumbaiV2() + + def assertScheduleEqual(self, program, target): + """Assert an error when two pulse programs are not equal. + + .. note:: Two programs are converted into standard execution format then compared. + """ + self.assertEqual(target_qobj_transform(program), target_qobj_transform(target)) + + +class TestContextsV2(TestBuilderV2): + """Test builder contexts.""" + + def test_transpiler_settings(self): + """Test the transpiler settings context. + + Tests that two cx gates are optimized away with higher optimization level. + """ + twice_cx_qc = circuit.QuantumCircuit(2) + twice_cx_qc.cx(0, 1) + twice_cx_qc.cx(0, 1) + + with pulse.build(self.backend) as schedule: + with pulse.transpiler_settings(optimization_level=0): + builder.call(twice_cx_qc) + self.assertNotEqual(len(schedule.instructions), 0) + + with pulse.build(self.backend) as schedule: + with pulse.transpiler_settings(optimization_level=3): + builder.call(twice_cx_qc) + self.assertEqual(len(schedule.instructions), 0) + + def test_scheduler_settings(self): + """Test the circuit scheduler settings context.""" + inst_map = pulse.InstructionScheduleMap() + d0 = pulse.DriveChannel(0) + test_x_sched = pulse.Schedule() + test_x_sched += instructions.Delay(10, d0) + inst_map.add("x", (0,), test_x_sched) + + ref_sched = pulse.Schedule() + ref_sched += pulse.instructions.Call(test_x_sched) + + x_qc = circuit.QuantumCircuit(2) + x_qc.x(0) + + with pulse.build(backend=self.backend) as schedule: + with pulse.transpiler_settings(basis_gates=["x"]): + with pulse.circuit_scheduler_settings(inst_map=inst_map): + builder.call(x_qc) + + self.assertScheduleEqual(schedule, ref_sched) + + def test_phase_compensated_frequency_offset(self): + """Test that the phase offset context properly compensates for phase + accumulation with backendV2.""" + d0 = pulse.DriveChannel(0) + with pulse.build(self.backend) as schedule: + with pulse.frequency_offset(1e9, d0, compensate_phase=True): + pulse.delay(10, d0) + + reference = pulse.Schedule() + reference += instructions.ShiftFrequency(1e9, d0) + reference += instructions.Delay(10, d0) + reference += instructions.ShiftPhase( + -2 * np.pi * ((1e9 * 10 * self.backend.target.dt) % 1), d0 + ) + reference += instructions.ShiftFrequency(-1e9, d0) + self.assertScheduleEqual(schedule, reference) + + +class TestChannelsV2(TestBuilderV2): + """Test builder channels.""" + + def test_drive_channel(self): + """Text context builder drive channel.""" + with pulse.build(self.backend): + self.assertEqual(pulse.drive_channel(0), pulse.DriveChannel(0)) + + def test_measure_channel(self): + """Text context builder measure channel.""" + with pulse.build(self.backend): + self.assertEqual(pulse.measure_channel(0), pulse.MeasureChannel(0)) + + def test_acquire_channel(self): + """Text context builder acquire channel.""" + with pulse.build(self.backend): + self.assertEqual(pulse.acquire_channel(0), pulse.AcquireChannel(0)) + + def test_control_channel(self): + """Text context builder control channel.""" + with pulse.build(self.backend): + self.assertEqual(pulse.control_channels(0, 1)[0], pulse.ControlChannel(0)) + + +class TestDirectivesV2(TestBuilderV2): + """Test builder directives.""" + + def test_barrier_on_qubits(self): + """Test barrier directive on qubits with backendV2. + A part of qubits map of Mumbai + 0 -- 1 -- 4 -- + | + | + 2 + """ + with pulse.build(self.backend) as schedule: + pulse.barrier(0, 1) + reference = pulse.ScheduleBlock() + reference += directives.RelativeBarrier( + pulse.DriveChannel(0), + pulse.DriveChannel(1), + pulse.MeasureChannel(0), + pulse.MeasureChannel(1), + pulse.ControlChannel(0), + pulse.ControlChannel(1), + pulse.ControlChannel(2), + pulse.ControlChannel(3), + pulse.ControlChannel(4), + pulse.ControlChannel(8), + pulse.AcquireChannel(0), + pulse.AcquireChannel(1), + ) + self.assertEqual(schedule, reference) + + +class TestUtilitiesV2(TestBuilderV2): + """Test builder utilities.""" + + def test_active_backend(self): + """Test getting active builder backend.""" + with pulse.build(self.backend): + self.assertEqual(pulse.active_backend(), self.backend) + + def test_qubit_channels(self): + """Test getting the qubit channels of the active builder's backend.""" + with pulse.build(self.backend): + qubit_channels = pulse.qubit_channels(0) + + self.assertEqual( + qubit_channels, + { + pulse.DriveChannel(0), + pulse.MeasureChannel(0), + pulse.AcquireChannel(0), + pulse.ControlChannel(0), + pulse.ControlChannel(1), + }, + ) + + def test_active_transpiler_settings(self): + """Test setting settings of active builder's transpiler.""" + with pulse.build(self.backend): + self.assertFalse(pulse.active_transpiler_settings()) + with pulse.transpiler_settings(test_setting=1): + self.assertEqual(pulse.active_transpiler_settings()["test_setting"], 1) + + def test_active_circuit_scheduler_settings(self): + """Test setting settings of active builder's circuit scheduler.""" + with pulse.build(self.backend): + self.assertFalse(pulse.active_circuit_scheduler_settings()) + with pulse.circuit_scheduler_settings(test_setting=1): + self.assertEqual(pulse.active_circuit_scheduler_settings()["test_setting"], 1) + + def test_num_qubits(self): + """Test builder utility to get number of qubits with backendV2.""" + with pulse.build(self.backend): + self.assertEqual(pulse.num_qubits(), 27) + + def test_samples_to_seconds(self): + """Test samples to time with backendV2""" + target = self.backend.target + target.dt = 0.1 + with pulse.build(self.backend): + time = pulse.samples_to_seconds(100) + self.assertTrue(isinstance(time, float)) + self.assertEqual(pulse.samples_to_seconds(100), 10) + + def test_samples_to_seconds_array(self): + """Test samples to time (array format) with backendV2.""" + target = self.backend.target + target.dt = 0.1 + with pulse.build(self.backend): + samples = np.array([100, 200, 300]) + times = pulse.samples_to_seconds(samples) + self.assertTrue(np.issubdtype(times.dtype, np.floating)) + np.testing.assert_allclose(times, np.array([10, 20, 30])) + + def test_seconds_to_samples(self): + """Test time to samples with backendV2""" + target = self.backend.target + target.dt = 0.1 + with pulse.build(self.backend): + samples = pulse.seconds_to_samples(10) + self.assertTrue(isinstance(samples, int)) + self.assertEqual(pulse.seconds_to_samples(10), 100) + + def test_seconds_to_samples_array(self): + """Test time to samples (array format) with backendV2.""" + target = self.backend.target + target.dt = 0.1 + with pulse.build(self.backend): + times = np.array([10, 20, 30]) + samples = pulse.seconds_to_samples(times) + self.assertTrue(np.issubdtype(samples.dtype, np.integer)) + np.testing.assert_allclose(pulse.seconds_to_samples(times), np.array([100, 200, 300])) + + +class TestMacrosV2(TestBuilderV2): + """Test builder macros with backendV2.""" + + def test_macro(self): + """Test builder macro decorator.""" + + @pulse.macro + def nested(a): + pulse.play(pulse.Gaussian(100, a, 20), pulse.drive_channel(0)) + return a * 2 + + @pulse.macro + def test(): + pulse.play(pulse.Constant(100, 1.0), pulse.drive_channel(0)) + output = nested(0.5) + return output + + with pulse.build(self.backend) as schedule: + output = test() + self.assertEqual(output, 0.5 * 2) + + reference = pulse.Schedule() + reference += pulse.Play(pulse.Constant(100, 1.0), pulse.DriveChannel(0)) + reference += pulse.Play(pulse.Gaussian(100, 0.5, 20), pulse.DriveChannel(0)) + + self.assertScheduleEqual(schedule, reference) + + def test_measure(self): + """Test utility function - measure with backendV2.""" + with pulse.build(self.backend) as schedule: + reg = pulse.measure(0) + + self.assertEqual(reg, pulse.MemorySlot(0)) + + reference = macros.measure(qubits=[0], backend=self.backend, meas_map=self.backend.meas_map) + + self.assertScheduleEqual(schedule, reference) + + def test_measure_multi_qubits(self): + """Test utility function - measure with multi qubits with backendV2.""" + with pulse.build(self.backend) as schedule: + regs = pulse.measure([0, 1]) + + self.assertListEqual(regs, [pulse.MemorySlot(0), pulse.MemorySlot(1)]) + + reference = macros.measure( + qubits=[0, 1], backend=self.backend, meas_map=self.backend.meas_map + ) + + self.assertScheduleEqual(schedule, reference) + + def test_measure_all(self): + """Test utility function - measure with backendV2..""" + with pulse.build(self.backend) as schedule: + regs = pulse.measure_all() + + self.assertEqual(regs, [pulse.MemorySlot(i) for i in range(self.backend.num_qubits)]) + reference = macros.measure_all(self.backend) + + self.assertScheduleEqual(schedule, reference) + + def test_delay_qubit(self): + """Test delaying on a qubit macro.""" + with pulse.build(self.backend) as schedule: + pulse.delay_qubits(10, 0) + + d0 = pulse.DriveChannel(0) + m0 = pulse.MeasureChannel(0) + a0 = pulse.AcquireChannel(0) + u0 = pulse.ControlChannel(0) + u1 = pulse.ControlChannel(1) + + reference = pulse.Schedule() + reference += instructions.Delay(10, d0) + reference += instructions.Delay(10, m0) + reference += instructions.Delay(10, a0) + reference += instructions.Delay(10, u0) + reference += instructions.Delay(10, u1) + + self.assertScheduleEqual(schedule, reference) + + def test_delay_qubits(self): + """Test delaying on multiple qubits with backendV2 to make sure we don't insert delays twice.""" + with pulse.build(self.backend) as schedule: + pulse.delay_qubits(10, 0, 1) + + d0 = pulse.DriveChannel(0) + d1 = pulse.DriveChannel(1) + m0 = pulse.MeasureChannel(0) + m1 = pulse.MeasureChannel(1) + a0 = pulse.AcquireChannel(0) + a1 = pulse.AcquireChannel(1) + u0 = pulse.ControlChannel(0) + u1 = pulse.ControlChannel(1) + u2 = pulse.ControlChannel(2) + u3 = pulse.ControlChannel(3) + u4 = pulse.ControlChannel(4) + u8 = pulse.ControlChannel(8) + + reference = pulse.Schedule() + reference += instructions.Delay(10, d0) + reference += instructions.Delay(10, d1) + reference += instructions.Delay(10, m0) + reference += instructions.Delay(10, m1) + reference += instructions.Delay(10, a0) + reference += instructions.Delay(10, a1) + reference += instructions.Delay(10, u0) + reference += instructions.Delay(10, u1) + reference += instructions.Delay(10, u2) + reference += instructions.Delay(10, u3) + reference += instructions.Delay(10, u4) + reference += instructions.Delay(10, u8) + + self.assertScheduleEqual(schedule, reference) From 86df15d3b9d80a74db91590af44af72a050371d2 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 12 Jun 2023 19:08:20 +0100 Subject: [PATCH 141/172] Improve casting and error message for `ParameterExpression` (#10244) * Improve casting and error message for `ParameterExpression` Previously, we assumed that the only reason a cast of `ParameterExpression` to `complex`/`float`/`int` could fail was because of unbound parameters, and emitted an error accordingly. This is not the case; a fully bound complex value will still fail to convert to `float`. This PR improves the error messages in these cases, and works around a difference of Sympy and Symengine, where the latter will fail to convert real-valued expressions that were symbollically complex at some point in their binding history to `float`. Sympy more reliably reduces values down to real-only values when the imaginary part is exactly cancelled, which is a use-case our users tend to expect. * Fix typo in test * Update releasenotes/notes/parameter-float-cast-48f3731fec5e47cd.yaml Co-authored-by: Kevin Hartman --------- Co-authored-by: Kevin Hartman --- qiskit/circuit/parameterexpression.py | 41 +++++++++++++------ qiskit/circuit/quantumcircuit.py | 6 +-- ...parameter-float-cast-48f3731fec5e47cd.yaml | 13 ++++++ test/python/circuit/test_parameters.py | 32 +++++++++++++++ 4 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 releasenotes/notes/parameter-float-cast-48f3731fec5e47cd.yaml diff --git a/qiskit/circuit/parameterexpression.py b/qiskit/circuit/parameterexpression.py index 6858efa1301d..dc7614a30586 100644 --- a/qiskit/circuit/parameterexpression.py +++ b/qiskit/circuit/parameterexpression.py @@ -464,30 +464,47 @@ def __complex__(self): return complex(self._symbol_expr) # TypeError is for sympy, RuntimeError for symengine except (TypeError, RuntimeError) as exc: - raise TypeError( - "ParameterExpression with unbound parameters ({}) " - "cannot be cast to a complex.".format(self.parameters) - ) from exc + if self.parameters: + raise TypeError( + "ParameterExpression with unbound parameters ({}) " + "cannot be cast to a complex.".format(self.parameters) + ) from None + raise TypeError("could not cast expression to complex") from exc def __float__(self): try: return float(self._symbol_expr) # TypeError is for sympy, RuntimeError for symengine except (TypeError, RuntimeError) as exc: - raise TypeError( - "ParameterExpression with unbound parameters ({}) " - "cannot be cast to a float.".format(self.parameters) - ) from exc + if self.parameters: + raise TypeError( + "ParameterExpression with unbound parameters ({}) " + "cannot be cast to a float.".format(self.parameters) + ) from None + try: + # 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 + # more expected form for our users. + cval = complex(self) + if cval.imag == 0.0: + return cval.real + except TypeError: + pass + raise TypeError("could not cast expression to float") from exc def __int__(self): try: return int(self._symbol_expr) # TypeError is for sympy, RuntimeError for symengine except (TypeError, RuntimeError) as exc: - raise TypeError( - "ParameterExpression with unbound parameters ({}) " - "cannot be cast to an int.".format(self.parameters) - ) from exc + if self.parameters: + raise TypeError( + "ParameterExpression with unbound parameters ({}) " + "cannot be cast to an int.".format(self.parameters) + ) from None + raise TypeError("could not cast expression to int") from exc def __hash__(self): return hash((frozenset(self._parameter_symbols), self._symbol_expr)) diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index 37cb1c508575..a9f17321f4a7 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -2857,8 +2857,7 @@ def _assign_parameter(self, parameter: Parameter, value: ParameterValueType) -> if new_param._symbol_expr.is_integer and new_param.is_real(): val = int(new_param) elif new_param.is_real(): - # Workaround symengine not supporting float() - val = complex(new_param).real + val = float(new_param) else: # complex values may no longer be supported but we # defer raising an exception to validdate_parameter @@ -2924,8 +2923,7 @@ def _assign_calibration_parameters( if new_param._symbol_expr.is_integer: new_param = int(new_param) else: - # Workaround symengine not supporting float() - new_param = complex(new_param).real + new_param = float(new_param) new_cal_params.append(new_param) else: new_cal_params.append(p) diff --git a/releasenotes/notes/parameter-float-cast-48f3731fec5e47cd.yaml b/releasenotes/notes/parameter-float-cast-48f3731fec5e47cd.yaml new file mode 100644 index 000000000000..9f9aef5aac76 --- /dev/null +++ b/releasenotes/notes/parameter-float-cast-48f3731fec5e47cd.yaml @@ -0,0 +1,13 @@ +--- +fixes: + - | + Improved the error messages returned when an attempt to convert a fully bound + :class:`.ParameterExpression` into a concrete ``float`` or ``int`` failed, for example because + the expression was naturally a complex number. + - | + Fixed ``float`` conversions for :class:`.ParameterExpression` values which had, at some point in + their construction history, an imaginary component that had subsequently been cancelled. When + using Sympy as a backend, these conversions would usually already have worked. When using + Symengine as the backend, these conversions would often fail with type errors, despite the + result having been symbolically evaluated to be real, and :meth:`.ParameterExpression.is_real` + being true. diff --git a/test/python/circuit/test_parameters.py b/test/python/circuit/test_parameters.py index d610a3353067..304804741338 100644 --- a/test/python/circuit/test_parameters.py +++ b/test/python/circuit/test_parameters.py @@ -1289,6 +1289,22 @@ def test_abs_function_when_not_bound(self): self.assertEqual(abs(x) * abs(y), abs(x * y)) self.assertEqual(abs(x) / abs(y), abs(x / y)) + def test_cast_to_complex_when_bound(self): + """Verify that the cast to complex works for bound objects.""" + x = Parameter("x") + y = Parameter("y") + bound_expr = (x + y).bind({x: 1.0, y: 1j}) + self.assertEqual(complex(bound_expr), 1 + 1j) + + def test_raise_if_cast_to_complex_when_not_fully_bound(self): + """Verify raises if casting to complex and not fully bound.""" + + x = Parameter("x") + y = Parameter("y") + bound_expr = (x + y).bind({x: 1j}) + with self.assertRaisesRegex(TypeError, "unbound parameters"): + complex(bound_expr) + def test_cast_to_float_when_bound(self): """Verify expression can be cast to a float when fully bound.""" @@ -1303,6 +1319,22 @@ def test_cast_to_float_when_underlying_expression_bound(self): expr = x - x + 2.3 self.assertEqual(float(expr), 2.3) + 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.""" + x = Parameter("x") + bound_expr = (x + 1.0 + 1.0j).bind({x: -1.0j}) + self.assertEqual(float(bound_expr), 1.0) + + def test_cast_to_float_of_complex_fails(self): + """Test that an attempt to produce a float from a complex value fails if there is an + imaginary part, with a sensible error message.""" + x = Parameter("x") + bound_expr = (x + 1.0j).bind({x: 1.0}) + with self.assertRaisesRegex(TypeError, "could not cast expression to float"): + float(bound_expr) + def test_raise_if_cast_to_float_when_not_fully_bound(self): """Verify raises if casting to float and not fully bound.""" From 676d90cf1548a43c98bc500452491ccababbd719 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 13 Jun 2023 13:37:54 +0100 Subject: [PATCH 142/172] Pin Aer to known good version (#10270) * Pin Aer to known good version The release of Aer 0.12.1 included a bug in the results output that made the general `Result` object unpickleable. Several places in our tests assume that these objects should be able to be pickled to be sent across a process boundary, or such like. * Make GitHub Actions respect constraints --- .github/workflows/coverage.yml | 6 +++--- constraints.txt | 4 ++++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f85266bc5ccd..71681644b2d0 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -39,10 +39,10 @@ jobs: # Modern pip (23.1+) can error out if it doesn't have `wheel` and we ask for one # of these legacy packages. - name: Ensure basic build requirements - run: python -m pip install --upgrade pip setuptools wheel + run: python -m pip install -c constraints.txt --upgrade pip setuptools wheel - name: Build and install qiskit-terra - run: python -m pip install -e . + run: python -m pip install -c constraints.txt -e . env: CARGO_INCREMENTAL: 0 RUSTFLAGS: "-Cinstrument-coverage" @@ -52,7 +52,7 @@ jobs: - name: Generate unittest coverage report run: | set -e - python -m pip install -r requirements-dev.txt qiskit-aer + python -m pip install -c constraints.txt -r requirements-dev.txt qiskit-aer stestr run # We set the --source-dir to '.' because we want all paths to appear relative to the repo # root (we need to combine them with the Python ones), but we only care about `grcov` diff --git a/constraints.txt b/constraints.txt index 852c3bbc40ea..3678fc172779 100644 --- a/constraints.txt +++ b/constraints.txt @@ -1,3 +1,7 @@ # jsonschema pinning needed due nbformat==5.1.3 using deprecated behaviour in # 4.0+. The pin can be removed after nbformat is updated. jsonschema==3.2.0 + +# Aer 0.12.1 has a bad value in the results output that prevents pickling. +# Remove pin once https://github.com/Qiskit/qiskit-aer/pull/1845 is released. +qiskit-aer==0.12.0 From 7df290b8f3245510cae2393c999dd8ee8a4037dc Mon Sep 17 00:00:00 2001 From: Evan McKinney <47376937+evmckinney9@users.noreply.github.com> Date: Wed, 14 Jun 2023 06:24:33 -0400 Subject: [PATCH 143/172] Fix BasicSwap FakeRun Typo (#10274) * fix typo, issue #10147 * create basicswap fake_run test * release note for #10149 fix * ensure fake_run modifes layout prop, not the circuit logic * black formatting, test_basic_swap --- .../transpiler/passes/routing/basic_swap.py | 2 +- ...ix-basicswap-fakerun-7469835327f6c8a1.yaml | 4 ++ test/python/transpiler/test_basic_swap.py | 39 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-basicswap-fakerun-7469835327f6c8a1.yaml diff --git a/qiskit/transpiler/passes/routing/basic_swap.py b/qiskit/transpiler/passes/routing/basic_swap.py index 0cae1d8651d3..65b22ef9c178 100644 --- a/qiskit/transpiler/passes/routing/basic_swap.py +++ b/qiskit/transpiler/passes/routing/basic_swap.py @@ -60,7 +60,7 @@ def run(self, dag): compatible with the DAG, or if the coupling_map=None. """ if self.fake_run: - return self.fake_run(dag) + return self._fake_run(dag) new_dag = dag.copy_empty_like() diff --git a/releasenotes/notes/fix-basicswap-fakerun-7469835327f6c8a1.yaml b/releasenotes/notes/fix-basicswap-fakerun-7469835327f6c8a1.yaml new file mode 100644 index 000000000000..c740479e2889 --- /dev/null +++ b/releasenotes/notes/fix-basicswap-fakerun-7469835327f6c8a1.yaml @@ -0,0 +1,4 @@ +fixes: + - | + Fixes a typo where BasicSwap called ``fake_run()`` the attribute instead of ``_fake_run()`` the function. + Refer to `#10149 ` for more details. diff --git a/test/python/transpiler/test_basic_swap.py b/test/python/transpiler/test_basic_swap.py index f9b9f7824eed..53bd599651d2 100644 --- a/test/python/transpiler/test_basic_swap.py +++ b/test/python/transpiler/test_basic_swap.py @@ -14,6 +14,8 @@ import unittest from qiskit.transpiler.passes import BasicSwap +from qiskit.transpiler.passmanager import PassManager +from qiskit.transpiler.layout import Layout from qiskit.transpiler import CouplingMap, Target from qiskit.circuit.library import CXGate from qiskit.converters import circuit_to_dag @@ -369,6 +371,43 @@ def test_far_swap_with_gate_the_middle(self): self.assertEqual(circuit_to_dag(expected), after) + def test_fake_run(self): + """A fake run, doesn't change dag + q0:--(+)-------.-- + | | + q1:---|--------|-- + | + q2:---|--------|-- + | | + q3:---.--[H]--(+)- + + CouplingMap map: [0]--[1]--[2]--[3] + + q0:-------(+)-------.--- + | | + q1:-----X--.--[H]--(+)-- + | + q2:--X--X--------------- + | + q3:--X------------------ + + """ + coupling = CouplingMap([[0, 1], [1, 2], [2, 3]]) + + qr = QuantumRegister(4, "q") + circuit = QuantumCircuit(qr) + circuit.cx(qr[3], qr[0]) + circuit.h(qr[3]) + circuit.cx(qr[0], qr[3]) + + fake_pm = PassManager([BasicSwap(coupling, fake_run=True)]) + real_pm = PassManager([BasicSwap(coupling, fake_run=False)]) + + self.assertEqual(circuit, fake_pm.run(circuit)) + self.assertNotEqual(circuit, real_pm.run(circuit)) + self.assertIsInstance(fake_pm.property_set["final_layout"], Layout) + self.assertEqual(fake_pm.property_set["final_layout"], real_pm.property_set["final_layout"]) + if __name__ == "__main__": unittest.main() From 81964e649938ce7b201364013e4fe17098e4533c Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 15 Jun 2023 14:09:52 +0100 Subject: [PATCH 144/172] Fix deprecation warnings emitted by `import qiskit.primitives` (#10287) The primitives still need to support the legacy `qiskit.opflow.PauliSumOp` while that object is deprecated but not removed. However, we do not want the import of `qiskit.primitives` to need to import `opflow` to do that, since that will cause the deprecation warnings emitted by the import of `opflow` to be hidden (they'll be blamed on Qiskit library code, so hidden by default). All type-hint usage we can hide behind the `TYPE_CHECKING` static analysis gate. For cases where we were actively runtime type-checking an object, we can gate the `isinstance` check behind a check that `qiskit.opflow` is already imported; the object cannot be a `PauliSumOp` if the module isn't initialised. --- qiskit/primitives/backend_estimator.py | 5 ++++- qiskit/primitives/base/base_estimator.py | 5 ++++- qiskit/primitives/estimator.py | 5 ++++- qiskit/primitives/utils.py | 17 +++++++++++++++-- ...itives-import-warnings-439e3e237fdb9d7b.yaml | 7 +++++++ 5 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/fix-primitives-import-warnings-439e3e237fdb9d7b.yaml diff --git a/qiskit/primitives/backend_estimator.py b/qiskit/primitives/backend_estimator.py index 0e48c5080b45..40c77f9e9a6b 100644 --- a/qiskit/primitives/backend_estimator.py +++ b/qiskit/primitives/backend_estimator.py @@ -16,6 +16,7 @@ from __future__ import annotations import copy +import typing from collections.abc import Sequence from itertools import accumulate @@ -23,7 +24,6 @@ from qiskit.circuit import QuantumCircuit from qiskit.compiler import transpile -from qiskit.opflow import PauliSumOp from qiskit.providers import BackendV1, BackendV2, Options from qiskit.quantum_info import Pauli, PauliList from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -34,6 +34,9 @@ from .primitive_job import PrimitiveJob from .utils import _circuit_key, _observable_key, init_observable +if typing.TYPE_CHECKING: + from qiskit.opflow import PauliSumOp + def _run_circuits( circuits: QuantumCircuit | list[QuantumCircuit], diff --git a/qiskit/primitives/base/base_estimator.py b/qiskit/primitives/base/base_estimator.py index f091bc0d335c..1dec010d7b12 100644 --- a/qiskit/primitives/base/base_estimator.py +++ b/qiskit/primitives/base/base_estimator.py @@ -84,10 +84,10 @@ from collections.abc import Sequence from copy import copy from typing import Generic, TypeVar +import typing from qiskit.circuit import QuantumCircuit from qiskit.circuit.parametertable import ParameterView -from qiskit.opflow import PauliSumOp from qiskit.providers import JobV1 as Job from qiskit.quantum_info.operators import SparsePauliOp from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -95,6 +95,9 @@ from ..utils import init_observable from .base_primitive import BasePrimitive +if typing.TYPE_CHECKING: + from qiskit.opflow import PauliSumOp + T = TypeVar("T", bound=Job) diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index 7333e44332c9..8afa7783ee40 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -17,12 +17,12 @@ from collections.abc import Sequence from typing import Any +import typing import numpy as np from qiskit.circuit import QuantumCircuit from qiskit.exceptions import QiskitError -from qiskit.opflow import PauliSumOp from qiskit.quantum_info import Statevector from qiskit.quantum_info.operators.base_operator import BaseOperator @@ -35,6 +35,9 @@ init_observable, ) +if typing.TYPE_CHECKING: + from qiskit.opflow import PauliSumOp + class Estimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): """ diff --git a/qiskit/primitives/utils.py b/qiskit/primitives/utils.py index 40165077e291..20db6c0d7295 100644 --- a/qiskit/primitives/utils.py +++ b/qiskit/primitives/utils.py @@ -14,6 +14,8 @@ """ from __future__ import annotations +import sys +import typing from collections.abc import Iterable import numpy as np @@ -21,11 +23,13 @@ from qiskit.circuit import Instruction, ParameterExpression, QuantumCircuit from qiskit.circuit.bit import Bit from qiskit.extensions.quantum_initializer.initializer import Initialize -from qiskit.opflow import PauliSumOp from qiskit.quantum_info import SparsePauliOp, Statevector from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.quantum_info.operators.symplectic.base_pauli import BasePauli +if typing.TYPE_CHECKING: + from qiskit.opflow import PauliSumOp + def init_circuit(state: QuantumCircuit | Statevector) -> QuantumCircuit: """Initialize state by converting the input to a quantum circuit. @@ -58,9 +62,18 @@ def init_observable(observable: BaseOperator | PauliSumOp | str) -> SparsePauliO TypeError: If the observable is a :class:`~qiskit.opflow.PauliSumOp` and has a parameterized coefficient. """ + # This dance is to avoid importing the deprecated `qiskit.opflow` if the user hasn't already + # done so. They can't hold a `qiskit.opflow.PauliSumOp` if `qiskit.opflow` hasn't been + # imported, and we don't want unrelated Qiskit library code to be responsible for the first + # import, so the deprecation warnings will show. + if "qiskit.opflow" in sys.modules: + pauli_sum_check = sys.modules["qiskit.opflow"].PauliSumOp + else: + pauli_sum_check = () + if isinstance(observable, SparsePauliOp): return observable - elif isinstance(observable, PauliSumOp): + elif isinstance(observable, pauli_sum_check): if isinstance(observable.coeff, ParameterExpression): raise TypeError( f"Observable must have numerical coefficient, not {type(observable.coeff)}." diff --git a/releasenotes/notes/fix-primitives-import-warnings-439e3e237fdb9d7b.yaml b/releasenotes/notes/fix-primitives-import-warnings-439e3e237fdb9d7b.yaml new file mode 100644 index 000000000000..f3827cca160c --- /dev/null +++ b/releasenotes/notes/fix-primitives-import-warnings-439e3e237fdb9d7b.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Importing :mod:`qiskit.primitives` will no longer cause deprecation warnings stemming from the + deprecated :mod:`qiskit.opflow` module. These warnings would have been hidden to users by the + default Python filters, but triggered the eager import of :mod:`.opflow`, which meant that a + subsequent import by a user would not trigger the warnings. From ee08a051dcd54ba4c1a85f64c3d24f0041afaa2b Mon Sep 17 00:00:00 2001 From: Soolu Thomas Date: Thu, 15 Jun 2023 13:08:52 -0400 Subject: [PATCH 145/172] Update typo in `qi_migration.rst` (#10292) --- docs/migration_guides/qi_migration.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/migration_guides/qi_migration.rst b/docs/migration_guides/qi_migration.rst index 1f27a468518e..6710dfd68437 100644 --- a/docs/migration_guides/qi_migration.rst +++ b/docs/migration_guides/qi_migration.rst @@ -64,8 +64,8 @@ Contents - *Primitives* - Any Sampler/Estimator implementation using base classes :class:`qiskit.primitives.BackendSampler` and a :class:`qiskit.primitives.BackendEstimator`. - *Reference Primitives* - :class:`qiskit.primitives.Sampler` and :class:`qiskit.primitives.Estimator` are reference implementations that come with Qiskit. - - *Aer Primitives* - The `Aer `_ primitive implementations: class:`qiskit_aer.primitives.Sampler` and :class:`qiskit_aer.primitives.Estimator`. - - *Qiskit Runtime Primitives* - IBM's Qiskit Runtime primitive implementations: class:`qiskit_ibm_runtime.Sampler` and :class:`qiskit_ibm_runtime.Estimator`. + - *Aer Primitives* - The `Aer `_ primitive implementations :class:`qiskit_aer.primitives.Sampler` and :class:`qiskit_aer.primitives.Estimator`. + - *Qiskit Runtime Primitives* - IBM's Qiskit Runtime primitive implementations :class:`qiskit_ibm_runtime.Sampler` and :class:`qiskit_ibm_runtime.Estimator`. - *Backend Primitives* - Instances of :class:`qiskit.primitives.BackendSampler` and :class:`qiskit.primitives.BackendEstimator`. These allow any backend to implement primitive interfaces For guidelines on which primitives to choose for your task, please continue reading. From 5f271bd480aa911eaa142e47d6b9ea2794342b98 Mon Sep 17 00:00:00 2001 From: Alexandre <75098594+Dpbm@users.noreply.github.com> Date: Thu, 15 Jun 2023 20:25:23 -0300 Subject: [PATCH 146/172] Fix qasm export for gates with same name (#10286) * added test for inner sequencial custom gates * fixed quantum circuit qasm test * fixed qasm method * added a function to rename the operation * updated test docstring * updated recheck operation.name comment * eblack fix test * added docs * lint test * updated test string format * Fixup release note --------- Co-authored-by: Jake Lishman --- qiskit/circuit/quantumcircuit.py | 20 +++++++++--- ...-qasm-circuit-export-943394536bc0d292.yaml | 7 ++++ test/python/circuit/test_circuit_qasm.py | 32 +++++++++++++++++++ 3 files changed, 55 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/fix-qasm-circuit-export-943394536bc0d292.yaml diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py index a9f17321f4a7..5b394e504616 100644 --- a/qiskit/circuit/quantumcircuit.py +++ b/qiskit/circuit/quantumcircuit.py @@ -5060,10 +5060,9 @@ def _qasm2_define_custom_operation(operation, existing_gate_names, gates_to_defi # Otherwise, if there's a naming clash, we need a unique name. if operation.name in gates_to_define: - new_name = f"{operation.name}_{id(operation)}" - operation = operation.copy(name=new_name) - else: - new_name = operation.name + operation = _rename_operation(operation) + + new_name = operation.name if parameterized_operation.params: parameters_qasm = ( @@ -5088,11 +5087,24 @@ def _qasm2_define_custom_operation(operation, existing_gate_names, gates_to_defi bits_qasm = ",".join(qubit_labels[q] for q in instruction.qubits) statements.append(f"{new_operation.qasm()} {bits_qasm};") body_qasm = " ".join(statements) + + # if an inner operation has the same name as the actual operation, it needs to be renamed + if operation.name in gates_to_define: + operation = _rename_operation(operation) + new_name = operation.name + definition_qasm = f"gate {new_name}{parameters_qasm} {qubits_qasm} {{ {body_qasm} }}" gates_to_define[new_name] = (parameterized_operation, definition_qasm) return operation +def _rename_operation(operation): + """Returns the operation with a new name following this pattern: {operation name}_{operation id}""" + new_name = f"{operation.name}_{id(operation)}" + updated_operation = operation.copy(name=new_name) + return updated_operation + + def _qasm_escape_name(name: str, prefix: str) -> str: """Returns a valid OpenQASM identifier, using `prefix` as a prefix if necessary. `prefix` must itself be a valid identifier.""" diff --git a/releasenotes/notes/fix-qasm-circuit-export-943394536bc0d292.yaml b/releasenotes/notes/fix-qasm-circuit-export-943394536bc0d292.yaml new file mode 100644 index 000000000000..fdbf087deef3 --- /dev/null +++ b/releasenotes/notes/fix-qasm-circuit-export-943394536bc0d292.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed the OpenQASM 2 output of :meth:`.QuantumCircuit.qasm` when a custom gate object contained + a gate with the same name. Ideally this shouldn't happen for most gates, but complex algorithmic + operations like the :class:`.GroverOperator` class could produce such structures accidentally. + See `#10162 `__. diff --git a/test/python/circuit/test_circuit_qasm.py b/test/python/circuit/test_circuit_qasm.py index 1d83ab8a024b..66d4d6d8b698 100644 --- a/test/python/circuit/test_circuit_qasm.py +++ b/test/python/circuit/test_circuit_qasm.py @@ -768,6 +768,38 @@ def test_opaque_output(self): """ self.assertEqual(qasm, expected) + def test_sequencial_inner_gates_with_same_name(self): + """Test if inner gates sequentially added with the same name result in the correct qasm""" + qubits_range = range(3) + + gate_a = QuantumCircuit(3, name="a") + gate_a.h(qubits_range) + gate_a = gate_a.to_instruction() + + gate_b = QuantumCircuit(3, name="a") + gate_b.append(gate_a, qubits_range) + gate_b.x(qubits_range) + gate_b = gate_b.to_instruction() + + qc = QuantumCircuit(3) + qc.append(gate_b, qubits_range) + qc.z(qubits_range) + + gate_a_id = id(qc.data[0].operation) + + expected_output = f"""OPENQASM 2.0; +include "qelib1.inc"; +gate a q0,q1,q2 {{ h q0; h q1; h q2; }} +gate a_{gate_a_id} q0,q1,q2 {{ a q0,q1,q2; x q0; x q1; x q2; }} +qreg q[3]; +a_{gate_a_id} q[0],q[1],q[2]; +z q[0]; +z q[1]; +z q[2]; +""" + + self.assertEqual(qc.qasm(), expected_output) + if __name__ == "__main__": unittest.main() From 45a1d988027bc0ffb753b63bbdadd80aab287dcc Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Fri, 16 Jun 2023 13:50:44 +0100 Subject: [PATCH 147/172] Add standard 1q Pauli equivalences to standard library (#10300) * Add standard 1q Pauli equivalences to standard library This makes `transpile` a little more reliable in cases where people are trying to use it to convert to a constrained basis. We can't necessarily recognise _all_ possible transformations into an incomplete basis, but simple Pauli relations are things people may well expect. * Fix test setup --- .../standard_gates/equivalence_library.py | 30 ++++++++++++++++++- ...d-pauli-equivalences-74c635ec5c23ee33.yaml | 7 +++++ test/python/compiler/test_transpiler.py | 14 +++++++++ 3 files changed, 50 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/add-pauli-equivalences-74c635ec5c23ee33.yaml diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index 0269dc0b4910..85116bfb65d7 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -1188,6 +1188,15 @@ def_x.append(inst, qargs, cargs) _sel.add_equivalence(XGate(), def_x) +# XGate +# global phase: π/2 +# ┌───┐ ┌───┐┌───┐ +# q: ┤ X ├ ≡ q: ┤ Y ├┤ Z ├ +# └───┘ └───┘└───┘ +def_x = QuantumCircuit(1, global_phase=pi / 2) +def_x.y(0) +def_x.z(0) +_sel.add_equivalence(XGate(), def_x) # CXGate @@ -1407,6 +1416,16 @@ def_y.append(inst, qargs, cargs) _sel.add_equivalence(YGate(), def_y) +# YGate +# global phase: π/2 +# ┌───┐ ┌───┐┌───┐ +# q: ┤ Y ├ ≡ q: ┤ Z ├┤ X ├ +# └───┘ └───┘└───┘ +def_y = QuantumCircuit(1, global_phase=pi / 2) +def_y.z(0) +def_y.x(0) +_sel.add_equivalence(YGate(), def_y) + # CYGate # # q_0: ──■── q_0: ─────────■─────── @@ -1433,7 +1452,6 @@ def_z.append(U1Gate(pi), [q[0]], []) _sel.add_equivalence(ZGate(), def_z) -# """ # ZGate # # ┌───┐ ┌───┐┌───┐ @@ -1448,6 +1466,16 @@ def_z.append(inst, qargs, cargs) _sel.add_equivalence(ZGate(), def_z) +# ZGate +# global phase: π/2 +# ┌───┐ ┌───┐┌───┐ +# q: ┤ Z ├ ≡ q: ┤ X ├┤ Y ├ +# └───┘ └───┘└───┘ +def_z = QuantumCircuit(1, global_phase=pi / 2) +def_z.x(0) +def_z.y(0) +_sel.add_equivalence(ZGate(), def_z) + # CZGate # # q_0: ─■─ q_0: ───────■─────── diff --git a/releasenotes/notes/add-pauli-equivalences-74c635ec5c23ee33.yaml b/releasenotes/notes/add-pauli-equivalences-74c635ec5c23ee33.yaml new file mode 100644 index 000000000000..cba26f7b8f82 --- /dev/null +++ b/releasenotes/notes/add-pauli-equivalences-74c635ec5c23ee33.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The transpiler's built-in :class:`.EquivalenceLibrary` has been taught the circular Pauli + relations :math:`X = iYZ`, :math:`Y = iZX` and :math:`Z = iXY`. This should make transpiling + to constrained, and potentially incomplete, basis sets more reliable. + See `#10293 `__ for more detail. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index b2b105c14ea5..ebfc79af9b04 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -1643,6 +1643,20 @@ def test_initial_layout_with_overlapping_qubits(self, opt_level): transpiled.layout.initial_layout, Layout({0: qc.qubits[1], 1: qc.qubits[0]}) ) + @combine(opt_level=[0, 1, 2, 3], basis=[["rz", "x"], ["rx", "z"], ["rz", "y"], ["ry", "x"]]) + def test_paulis_to_constrained_1q_basis(self, opt_level, basis): + """Test that Pauli-gate circuits can be transpiled to constrained 1q bases that do not + contain any root-Pauli gates.""" + qc = QuantumCircuit(1) + qc.x(0) + qc.barrier() + qc.y(0) + qc.barrier() + qc.z(0) + transpiled = transpile(qc, basis_gates=basis, optimization_level=opt_level) + self.assertGreaterEqual(set(basis) | {"barrier"}, transpiled.count_ops().keys()) + self.assertEqual(Operator(qc), Operator(transpiled)) + @ddt class TestPostTranspileIntegration(QiskitTestCase): From c1866956b21ef4dd4ab532ffacea76897eb540c7 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 16 Jun 2023 16:52:01 -0400 Subject: [PATCH 148/172] Remove deprecated InstructionSet circuit_cregs argument (#10302) This commit removes the deprecated circuit_cregs argument from the InstructionSet class. This argument was deprecated in the 0.19.0 release and has been supersceded by the resource_requester. --- qiskit/circuit/instructionset.py | 92 +------------------ ...ionset-circuit-cregs-91617e4b0993db9a.yaml | 33 +++++++ test/python/circuit/test_instructions.py | 77 ---------------- 3 files changed, 34 insertions(+), 168 deletions(-) create mode 100644 releasenotes/notes/remove-deprecate-instructionset-circuit-cregs-91617e4b0993db9a.yaml diff --git a/qiskit/circuit/instructionset.py b/qiskit/circuit/instructionset.py index b1a06c386078..8ea32f445a7d 100644 --- a/qiskit/circuit/instructionset.py +++ b/qiskit/circuit/instructionset.py @@ -15,99 +15,21 @@ """ from __future__ import annotations -import functools from typing import Callable from qiskit.circuit.exceptions import CircuitError -from qiskit.utils.deprecation import deprecate_arg from .classicalregister import Clbit, ClassicalRegister from .operation import Operation from .quantumcircuitdata import CircuitInstruction -# ClassicalRegister is hashable, and generally the registers in a circuit are completely fixed after -# its creation, so caching this allows us to only pay the register-unrolling penalty once. The -# cache does not need to be large, because in general only one circuit is constructed at once. -@functools.lru_cache(4) -def _requester_from_cregs( - cregs: tuple[ClassicalRegister], -) -> Callable[[Clbit | ClassicalRegister | int], ClassicalRegister | Clbit]: - """Get a classical resource requester from an iterable of classical registers. - - This implements the deprecated functionality of constructing an :obj:`.InstructionSet` with a - sequence of :obj:`.ClassicalRegister` instances. This is the old method of resolving integer - indices to a :obj:`.Clbit`, which is now replaced by using a requester from the - :obj:`.QuantumCircuit` instance for consistency. - - .. note:: - - This function has "incorrect" behaviour if any classical bit is in more than one register. - This is to maintain compatibility with the legacy usage of :obj:`.InstructionSet`, and - should not be used for any internal Qiskit code. Instead, use the proper requester methods - within :obj:`.QuantumCircuit`. - - This function can be removed when the deprecation of the ``circuit_cregs`` argument in - :obj:`.InstructionSet` expires. - - Args: - cregs: A tuple (needs to be immutable for the caching) of the classical registers to produce - a requester over. - - Returns: - A requester function that checks that a passed condition variable is valid, resolves - integers into concrete :obj:`.Clbit` instances, and returns a valid :obj:`.Clbit` or - :obj:`.ClassicalRegister` condition resource. - """ - - clbit_flat = tuple(clbit for creg in cregs for clbit in creg) - clbit_set = frozenset(clbit_flat) - creg_set = frozenset(cregs) - - def requester(classical: Clbit | ClassicalRegister | int) -> ClassicalRegister | Clbit: - if isinstance(classical, Clbit): - if classical not in clbit_set: - raise CircuitError( - f"Condition bit {classical} is not in the registers known here: {creg_set}" - ) - return classical - if isinstance(classical, ClassicalRegister): - if classical not in creg_set: - raise CircuitError( - f"Condition register {classical} is not one of the registers known here:" - f" {creg_set}" - ) - return classical - if isinstance(classical, int): - try: - return clbit_flat[classical] - except IndexError: - raise CircuitError(f"Bit index {classical} is out-of-range.") from None - raise CircuitError( - "Invalid classical condition. Must be an int, Clbit or ClassicalRegister, but received" - f" '{classical}'." - ) - - return requester - - class InstructionSet: """Instruction collection, and their contexts.""" __slots__ = ("_instructions", "_requester") - @deprecate_arg( - "circuit_cregs", - since="0.19.0", - additional_msg=( - "Instead, pass a complete resource requester with the 'resource_requester' argument. " - "The classical registers are insufficient to access all classical resources in a " - "circuit, as there may be loose classical bits as well. It can also cause integer " - "indices to be resolved incorrectly if any registers overlap." - ), - ) def __init__( # pylint: disable=bad-docstring-quotes self, - circuit_cregs: list[ClassicalRegister] | None = None, *, resource_requester: Callable[..., ClassicalRegister | Clbit] | None = None, ): @@ -117,8 +39,6 @@ def __init__( # pylint: disable=bad-docstring-quotes separately for each instruction. Args: - circuit_cregs (list[ClassicalRegister]): Optional. List of ``cregs`` of the - circuit to which the instruction is added. Default: `None`. resource_requester: A callable that takes in the classical resource used in the condition, verifies that it is present in the attached circuit, resolves any indices into concrete :obj:`.Clbit` instances, and returns the concrete resource. If this @@ -131,19 +51,9 @@ def __init__( # pylint: disable=bad-docstring-quotes :meth:`.c_if`, and assumes that a call implies that the resource will now be used. It may throw an error if the resource is not valid for usage. - Raises: - CircuitError: if both ``resource_requester`` and ``circuit_cregs`` are passed. Only one - of these may be passed, and it should be ``resource_requester``. """ self._instructions: list[CircuitInstruction] = [] - if circuit_cregs is not None: - if resource_requester is not None: - raise CircuitError("Cannot pass both 'circuit_cregs' and 'resource_requester'.") - self._requester: Callable[..., ClassicalRegister | Clbit] = _requester_from_cregs( - tuple(circuit_cregs) - ) - else: - self._requester = resource_requester + self._requester = resource_requester def __len__(self): """Return number of instructions in set""" diff --git a/releasenotes/notes/remove-deprecate-instructionset-circuit-cregs-91617e4b0993db9a.yaml b/releasenotes/notes/remove-deprecate-instructionset-circuit-cregs-91617e4b0993db9a.yaml new file mode 100644 index 000000000000..f1d8ebdad29b --- /dev/null +++ b/releasenotes/notes/remove-deprecate-instructionset-circuit-cregs-91617e4b0993db9a.yaml @@ -0,0 +1,33 @@ +--- +upgrade: + - | + The deprecated ``circuit_cregs`` argument to the constructor for the + :class:`~.InstructionSet` class has been removed. It was deprecated in the + 0.19.0 release. If you were using this argument and manually constructing + an :class:`~.InstructionSet` object (which should be quite uncommon as it's + mostly used internally) you should pass a callable to the + ``resource_requester`` keyword argument instead. For example:: + + from qiskit.circuit import Clbit, ClassicalRegister, InstructionSet + from qiskit.circuit.exceptions import CircuitError + + def my_requester(bits, registers): + bits_set = set(bits) + bits_flat = tuple(bits) + registers_set = set(registers) + + def requester(specifier): + if isinstance(specifer, Clbit) and specifier in bits_set: + return specifier + if isinstance(specifer, ClassicalRegster) and specifier in register_set: + return specifier + if isinstance(specifier, int) and 0 <= specifier < len(bits_flat): + return bits_flat[specifier] + raise CircuitError(f"Unknown resource: {specifier}") + + return requester + + my_bits = [Clbit() for _ in [None]*5] + my_registers = [ClassicalRegister(n) for n in range(3)] + + InstructionSet(resource_requester=my_requester(my_bits, my_registers)) diff --git a/test/python/circuit/test_instructions.py b/test/python/circuit/test_instructions.py index bb4b957d66dc..385786aa9729 100644 --- a/test/python/circuit/test_instructions.py +++ b/test/python/circuit/test_instructions.py @@ -559,83 +559,6 @@ def test_instructionset_c_if_with_no_requester(self): with self.assertRaisesRegex(CircuitError, r"Cannot pass an index as a condition .*"): instructions.c_if(0, 0) - def test_instructionset_c_if_deprecated_resolution(self): - r"""Test that the deprecated path of passing an iterable of :obj:`.ClassicalRegister`\ s to - :obj:`.InstructionSet` works, issues a deprecation warning, and resolves indices in the - simple cases it was meant to handle.""" - # The deprecated path can only cope with non-overlapping classical registers, with no loose - # clbits in the mix. - registers = [ClassicalRegister(2), ClassicalRegister(3), ClassicalRegister(1)] - bits = [bit for register in registers for bit in register] - - deprecated_regex = r".* argument ``circuit_cregs`` is deprecated .*" - - def dummy_requester(specifier): - """A dummy requester that technically fulfills the spec.""" - raise CircuitError - - with self.subTest("cannot pass both registers and requester"): - with self.assertWarns(DeprecationWarning), self.assertRaisesRegex( - CircuitError, r"Cannot pass both 'circuit_cregs' and 'resource_requester'\." - ): - InstructionSet(registers, resource_requester=dummy_requester) - - with self.subTest("classical register"): - instruction = HGate() - with self.assertWarnsRegex(DeprecationWarning, deprecated_regex): - instructions = InstructionSet(registers) - instructions.add(instruction, [Qubit()], []) - instructions.c_if(registers[0], 0) - self.assertIs(instruction.condition[0], registers[0]) - with self.subTest("classical bit"): - instruction = HGate() - with self.assertWarnsRegex(DeprecationWarning, deprecated_regex): - instructions = InstructionSet(registers) - instructions.add(instruction, [Qubit()], []) - instructions.c_if(registers[0][1], 0) - self.assertIs(instruction.condition[0], registers[0][1]) - for i, bit in enumerate(bits): - with self.subTest("bit index", index=i): - instruction = HGate() - with self.assertWarnsRegex(DeprecationWarning, deprecated_regex): - instructions = InstructionSet(registers) - instructions.add(instruction, [Qubit()], []) - instructions.c_if(i, 0) - self.assertIs(instruction.condition[0], bit) - - with self.subTest("raises on bad register"): - instruction = HGate() - with self.assertWarnsRegex(DeprecationWarning, deprecated_regex): - instructions = InstructionSet(registers) - instructions.add(instruction, [Qubit()], []) - with self.assertRaisesRegex( - CircuitError, r"Condition register .* is not one of the registers known here: .*" - ): - instructions.c_if(ClassicalRegister(2), 0) - with self.subTest("raises on bad bit"): - instruction = HGate() - with self.assertWarnsRegex(DeprecationWarning, deprecated_regex): - instructions = InstructionSet(registers) - instructions.add(instruction, [Qubit()], []) - with self.assertRaisesRegex( - CircuitError, "Condition bit .* is not in the registers known here: .*" - ): - instructions.c_if(Clbit(), 0) - with self.subTest("raises on bad index"): - instruction = HGate() - with self.assertWarnsRegex(DeprecationWarning, deprecated_regex): - instructions = InstructionSet(registers) - instructions.add(instruction, [Qubit()], []) - with self.assertRaisesRegex(CircuitError, r"Bit index .* is out-of-range\."): - instructions.c_if(len(bits), 0) - with self.subTest("raises on bad type"): - instruction = HGate() - with self.assertWarnsRegex(DeprecationWarning, deprecated_regex): - instructions = InstructionSet(registers) - instructions.add(instruction, [Qubit()], []) - with self.assertRaisesRegex(CircuitError, r"Invalid classical condition\. .*"): - instructions.c_if([0], 0) - def test_instructionset_c_if_calls_custom_requester(self): """Test that :meth:`.InstructionSet.c_if` calls a custom requester, and uses its output.""" # This isn't expected to be useful to end users, it's more about the principle that you can From b8a4448c3e6c41cf6162bfc7560e198e75946ca4 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 19 Jun 2023 12:23:33 +0100 Subject: [PATCH 149/172] Pin Numpy to less than 1.25 in CI (#10306) We are not (should not be) fundamentally incompatible with Numpy 1.25, there are just new deprecation warnings and seemingly some behavioural changes that are causing flakiness in the isometry CI. This temporarily pins Numpy to allow people to continue working while we address the root cause. --- constraints.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/constraints.txt b/constraints.txt index 3678fc172779..67572fd19305 100644 --- a/constraints.txt +++ b/constraints.txt @@ -5,3 +5,8 @@ jsonschema==3.2.0 # Aer 0.12.1 has a bad value in the results output that prevents pickling. # Remove pin once https://github.com/Qiskit/qiskit-aer/pull/1845 is released. qiskit-aer==0.12.0 + +# Numpy 1.25 deprecated some behaviours that we used, and caused the isometry +# tests to flake. See https://github.com/Qiskit/qiskit-terra/issues/10305, +# remove pin when resolving that. +numpy<1.25 From 364d6fdea34c7ec54e6f5d3581e8a0ec6c8bfb5d Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 19 Jun 2023 14:53:27 +0100 Subject: [PATCH 150/172] Fix Numpy 1.25 deprecations (#10308) Most of the effects we see are because of the removal of various aliases (`np.product` being a common one), but the new warning on implicit conversion of size-1 arrays to scalars that was initially found in parts of c463b3c has reared its head again too. --- qiskit/opflow/primitive_ops/pauli_op.py | 4 ++-- qiskit/opflow/primitive_ops/pauli_sum_op.py | 4 ++-- qiskit/quantum_info/operators/channel/chi.py | 4 ++-- qiskit/quantum_info/operators/channel/choi.py | 4 ++-- qiskit/quantum_info/operators/channel/ptm.py | 4 ++-- qiskit/quantum_info/operators/channel/stinespring.py | 2 +- qiskit/quantum_info/operators/channel/superop.py | 4 ++-- .../operators/channel/transformations.py | 2 +- qiskit/quantum_info/operators/operator.py | 2 +- qiskit/quantum_info/operators/random.py | 8 ++++---- .../quantum_info/operators/symplectic/pauli_list.py | 4 ++-- qiskit/quantum_info/states/densitymatrix.py | 2 +- qiskit/quantum_info/states/random.py | 4 ++-- qiskit/quantum_info/states/statevector.py | 2 +- qiskit/result/mitigation/local_readout_mitigator.py | 2 +- .../synthesis/discrete_basis/commutator_decompose.py | 8 ++++---- .../transpiler/passes/synthesis/unitary_synthesis.py | 2 +- test/python/quantum_info/operators/test_random.py | 12 ++++++------ test/python/quantum_info/operators/test_scalar_op.py | 4 ++-- test/python/quantum_info/states/test_random.py | 4 ++-- 20 files changed, 41 insertions(+), 41 deletions(-) diff --git a/qiskit/opflow/primitive_ops/pauli_op.py b/qiskit/opflow/primitive_ops/pauli_op.py index 5720a13357df..623608754671 100644 --- a/qiskit/opflow/primitive_ops/pauli_op.py +++ b/qiskit/opflow/primitive_ops/pauli_op.py @@ -250,8 +250,8 @@ def eval( bitstr = np.fromiter(bstr, dtype=int).astype(bool) new_b_str = np.logical_xor(bitstr, corrected_x_bits) new_str = "".join(map(str, 1 * new_b_str)) - z_factor = np.product(1 - 2 * np.logical_and(bitstr, corrected_z_bits)) - y_factor = np.product( + z_factor = np.prod(1 - 2 * np.logical_and(bitstr, corrected_z_bits)) + y_factor = np.prod( np.sqrt(1 - 2 * np.logical_and(corrected_x_bits, corrected_z_bits) + 0j) ) new_dict[new_str] = (v * z_factor * y_factor) + new_dict.get(new_str, 0) diff --git a/qiskit/opflow/primitive_ops/pauli_sum_op.py b/qiskit/opflow/primitive_ops/pauli_sum_op.py index 97ecd8fe23ad..b1bb9b7242a9 100644 --- a/qiskit/opflow/primitive_ops/pauli_sum_op.py +++ b/qiskit/opflow/primitive_ops/pauli_sum_op.py @@ -334,8 +334,8 @@ def eval( bitstr = np.fromiter(bstr, dtype=int).astype(bool) new_b_str = np.logical_xor(bitstr, corrected_x_bits) new_str = ["".join([str(b) for b in bs]) for bs in new_b_str.astype(int)] - z_factor = np.product(1 - 2 * np.logical_and(bitstr, corrected_z_bits), axis=1) - y_factor = np.product( + z_factor = np.prod(1 - 2 * np.logical_and(bitstr, corrected_z_bits), axis=1) + y_factor = np.prod( np.sqrt(1 - 2 * np.logical_and(corrected_x_bits, corrected_z_bits) + 0j), axis=1, ) diff --git a/qiskit/quantum_info/operators/channel/chi.py b/qiskit/quantum_info/operators/channel/chi.py index 0a7314add31d..93cb96d2e1a2 100644 --- a/qiskit/quantum_info/operators/channel/chi.py +++ b/qiskit/quantum_info/operators/channel/chi.py @@ -83,9 +83,9 @@ def __init__(self, data, input_dims=None, output_dims=None): if dim_l != dim_r: raise QiskitError("Invalid Chi-matrix input.") if input_dims: - input_dim = np.product(input_dims) + input_dim = np.prod(input_dims) if output_dims: - output_dim = np.product(input_dims) + output_dim = np.prod(input_dims) if output_dims is None and input_dims is None: output_dim = int(np.sqrt(dim_l)) input_dim = dim_l // output_dim diff --git a/qiskit/quantum_info/operators/channel/choi.py b/qiskit/quantum_info/operators/channel/choi.py index dfbf12be5761..7860088a1c7c 100644 --- a/qiskit/quantum_info/operators/channel/choi.py +++ b/qiskit/quantum_info/operators/channel/choi.py @@ -92,9 +92,9 @@ def __init__(self, data, input_dims=None, output_dims=None): if dim_l != dim_r: raise QiskitError("Invalid Choi-matrix input.") if input_dims: - input_dim = np.product(input_dims) + input_dim = np.prod(input_dims) if output_dims: - output_dim = np.product(output_dims) + output_dim = np.prod(output_dims) if output_dims is None and input_dims is None: output_dim = int(np.sqrt(dim_l)) input_dim = dim_l // output_dim diff --git a/qiskit/quantum_info/operators/channel/ptm.py b/qiskit/quantum_info/operators/channel/ptm.py index 17933b13ede9..3301e7c9c3b1 100644 --- a/qiskit/quantum_info/operators/channel/ptm.py +++ b/qiskit/quantum_info/operators/channel/ptm.py @@ -91,11 +91,11 @@ def __init__(self, data, input_dims=None, output_dims=None): # Determine input and output dimensions dout, din = ptm.shape if input_dims: - input_dim = np.product(input_dims) + input_dim = np.prod(input_dims) else: input_dim = int(np.sqrt(din)) if output_dims: - output_dim = np.product(input_dims) + output_dim = np.prod(input_dims) else: output_dim = int(np.sqrt(dout)) if output_dim**2 != dout or input_dim**2 != din or input_dim != output_dim: diff --git a/qiskit/quantum_info/operators/channel/stinespring.py b/qiskit/quantum_info/operators/channel/stinespring.py index ee615fb902b1..e9d9bb26b6f1 100644 --- a/qiskit/quantum_info/operators/channel/stinespring.py +++ b/qiskit/quantum_info/operators/channel/stinespring.py @@ -102,7 +102,7 @@ def __init__(self, data, input_dims=None, output_dims=None): raise QiskitError("Invalid Stinespring input.") input_dim = dim_right if output_dims: - output_dim = np.product(output_dims) + output_dim = np.prod(output_dims) else: output_dim = input_dim if dim_left % output_dim != 0: diff --git a/qiskit/quantum_info/operators/channel/superop.py b/qiskit/quantum_info/operators/channel/superop.py index 936bb9e79feb..4675a38f8522 100644 --- a/qiskit/quantum_info/operators/channel/superop.py +++ b/qiskit/quantum_info/operators/channel/superop.py @@ -212,7 +212,7 @@ def compose(self, other, qargs=None, front=False): indices = [2 * num_indices - 1 - qubit for qubit in qargs] + [ num_indices - 1 - qubit for qubit in qargs ] - final_shape = [np.product(output_dims) ** 2, np.product(input_dims) ** 2] + final_shape = [np.prod(output_dims) ** 2, np.prod(input_dims) ** 2] data = np.reshape( Operator._einsum_matmul(tensor, mat, indices, shift, right_mul), final_shape ) @@ -279,7 +279,7 @@ def _evolve(self, state, qargs=None): output_dims = self.output_dims() for i, qubit in enumerate(qargs): new_dims[qubit] = output_dims[i] - new_dim = np.product(new_dims) + new_dim = np.prod(new_dims) # reshape tensor to density matrix tensor = np.reshape(tensor, (new_dim, new_dim)) return DensityMatrix(tensor, dims=new_dims) diff --git a/qiskit/quantum_info/operators/channel/transformations.py b/qiskit/quantum_info/operators/channel/transformations.py index 2d980942550b..b077db7fb946 100644 --- a/qiskit/quantum_info/operators/channel/transformations.py +++ b/qiskit/quantum_info/operators/channel/transformations.py @@ -396,7 +396,7 @@ def _reravel(mat1, mat2, shape1, shape2): left_dims = shape1[:2] + shape2[:2] right_dims = shape1[2:] + shape2[2:] tensor_shape = left_dims + right_dims - final_shape = (np.product(left_dims), np.product(right_dims)) + final_shape = (np.prod(left_dims), np.prod(right_dims)) # Tensor product matrices data = np.kron(mat1, mat2) data = np.reshape( diff --git a/qiskit/quantum_info/operators/operator.py b/qiskit/quantum_info/operators/operator.py index 784f46d8d818..6c95e260f3eb 100644 --- a/qiskit/quantum_info/operators/operator.py +++ b/qiskit/quantum_info/operators/operator.py @@ -418,7 +418,7 @@ def compose(self, other, qargs=None, front=False): tensor = np.reshape(self.data, self._op_shape.tensor_shape) mat = np.reshape(other.data, other._op_shape.tensor_shape) indices = [num_indices - 1 - qubit for qubit in qargs] - final_shape = [int(np.product(output_dims)), int(np.product(input_dims))] + final_shape = [int(np.prod(output_dims)), int(np.prod(input_dims))] data = np.reshape( Operator._einsum_matmul(tensor, mat, indices, shift, right_mul), final_shape ) diff --git a/qiskit/quantum_info/operators/random.py b/qiskit/quantum_info/operators/random.py index ffa71f8efd7c..22806e154e61 100644 --- a/qiskit/quantum_info/operators/random.py +++ b/qiskit/quantum_info/operators/random.py @@ -53,7 +53,7 @@ def random_unitary(dims, seed=None): else: random_state = default_rng(seed) - dim = np.product(dims) + dim = np.prod(dims) from scipy import stats mat = stats.unitary_group.rvs(dim, random_state=random_state) @@ -84,7 +84,7 @@ def random_hermitian(dims, traceless=False, seed=None): rng = default_rng(seed) # Total dimension - dim = np.product(dims) + dim = np.prod(dims) from scipy import stats if traceless: @@ -133,8 +133,8 @@ def random_quantum_channel(input_dims=None, output_dims=None, rank=None, seed=No elif output_dims is None: output_dims = input_dims - d_in = np.product(input_dims) - d_out = np.product(output_dims) + d_in = np.prod(input_dims) + d_out = np.prod(output_dims) # If rank is not specified set to the maximum rank for the # Choi matrix (input_dim * output_dim) diff --git a/qiskit/quantum_info/operators/symplectic/pauli_list.py b/qiskit/quantum_info/operators/symplectic/pauli_list.py index ad6ca19f7b26..80b95bfa4c0e 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli_list.py +++ b/qiskit/quantum_info/operators/symplectic/pauli_list.py @@ -197,7 +197,7 @@ def _from_paulis(data): ) base_z[i] = pauli._z base_x[i] = pauli._x - base_phase[i] = pauli._phase + base_phase[i] = pauli._phase.item() return base_z, base_x, base_phase def __repr__(self): @@ -348,7 +348,7 @@ def __setitem__(self, index, value): self._phase[index] = value._phase else: # Row and Qubit indexing - self._phase[index[0]] += value._phase + self._phase[index[0]] += value._phase.item() self._phase %= 4 def delete(self, ind, qubit=False): diff --git a/qiskit/quantum_info/states/densitymatrix.py b/qiskit/quantum_info/states/densitymatrix.py index 2b9555eb1e67..e9788f77a1ac 100644 --- a/qiskit/quantum_info/states/densitymatrix.py +++ b/qiskit/quantum_info/states/densitymatrix.py @@ -581,7 +581,7 @@ def from_int(i, dims): as an N-qubit state. If it is not a power of two the state will have a single d-dimensional subsystem. """ - size = np.product(dims) + size = np.prod(dims) state = np.zeros((size, size), dtype=complex) state[i, i] = 1.0 return DensityMatrix(state, dims=dims) diff --git a/qiskit/quantum_info/states/random.py b/qiskit/quantum_info/states/random.py index 24f873be5aa8..54792f29d5cc 100644 --- a/qiskit/quantum_info/states/random.py +++ b/qiskit/quantum_info/states/random.py @@ -43,7 +43,7 @@ def random_statevector(dims, seed=None): else: rng = default_rng(seed) - dim = np.product(dims) + dim = np.prod(dims) # Random array over interval (0, 1] x = rng.random(dim) @@ -74,7 +74,7 @@ def random_density_matrix(dims, rank=None, method="Hilbert-Schmidt", seed=None): QiskitError: if the method is not valid. """ # Flatten dimensions - dim = np.product(dims) + dim = np.prod(dims) if rank is None: rank = dim # Use full rank diff --git a/qiskit/quantum_info/states/statevector.py b/qiskit/quantum_info/states/statevector.py index 639f147b1e71..d8d5b9a7af70 100644 --- a/qiskit/quantum_info/states/statevector.py +++ b/qiskit/quantum_info/states/statevector.py @@ -723,7 +723,7 @@ def from_int(i, dims): as an N-qubit state. If it is not a power of two the state will have a single d-dimensional subsystem. """ - size = np.product(dims) + size = np.prod(dims) state = np.zeros(size, dtype=complex) state[i] = 1.0 return Statevector(state, dims=dims) diff --git a/qiskit/result/mitigation/local_readout_mitigator.py b/qiskit/result/mitigation/local_readout_mitigator.py index ed7126143d96..20226a57fc9e 100644 --- a/qiskit/result/mitigation/local_readout_mitigator.py +++ b/qiskit/result/mitigation/local_readout_mitigator.py @@ -275,7 +275,7 @@ def _compute_gamma(self, qubits=None): else: qubit_indices = [self._qubit_index[qubit] for qubit in qubits] gammas = self._gammas[qubit_indices] - return np.product(gammas) + return np.prod(gammas) def stddev_upper_bound(self, shots: int, qubits: List[int] = None): """Return an upper bound on standard deviation of expval estimator. diff --git a/qiskit/synthesis/discrete_basis/commutator_decompose.py b/qiskit/synthesis/discrete_basis/commutator_decompose.py index caa669d409d7..188c9ae5b61a 100644 --- a/qiskit/synthesis/discrete_basis/commutator_decompose.py +++ b/qiskit/synthesis/discrete_basis/commutator_decompose.py @@ -89,11 +89,11 @@ def _solve_decomposition_angle(matrix: np.ndarray) -> float: trace = _compute_trace_so3(matrix) angle = math.acos((1 / 2) * (trace - 1)) + lhs = math.sin(angle / 2) + def objective(phi): - rhs = 2 * math.sin(phi / 2) ** 2 - rhs *= math.sqrt(1 - math.sin(phi / 2) ** 4) - lhs = math.sin(angle / 2) - return rhs - lhs + sin_sq = np.sin(phi / 2) ** 2 + return 2 * sin_sq * np.sqrt(1 - sin_sq**2) - lhs decomposition_angle = fsolve(objective, angle)[0] return decomposition_angle diff --git a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py index 4dbce78d282a..82fcb829b0f2 100644 --- a/qiskit/transpiler/passes/synthesis/unitary_synthesis.py +++ b/qiskit/transpiler/passes/synthesis/unitary_synthesis.py @@ -179,7 +179,7 @@ def _error(circuit, target=None, qubits=None): f"Target has no {inst.operation} on qubits {qubits}." ) from error # TODO:return np.sum(gate_durations) - return 1 - np.product(gate_fidelities) + return 1 - np.prod(gate_fidelities) def _preferred_direction( diff --git a/test/python/quantum_info/operators/test_random.py b/test/python/quantum_info/operators/test_random.py index 2837711a020f..c1987be4746a 100644 --- a/test/python/quantum_info/operators/test_random.py +++ b/test/python/quantum_info/operators/test_random.py @@ -59,8 +59,8 @@ def test_int_dims(self, dim): value = random_unitary(dim) self.assertIsInstance(value, Operator) self.assertTrue(value.is_unitary()) - self.assertEqual(np.product(value.input_dims()), dim) - self.assertEqual(np.product(value.output_dims()), dim) + self.assertEqual(np.prod(value.input_dims()), dim) + self.assertEqual(np.prod(value.output_dims()), dim) def test_fixed_seed(self): """Test fixing seed fixes output""" @@ -99,8 +99,8 @@ def test_int_dims(self, dim): value = random_hermitian(dim) self.assertIsInstance(value, Operator) self.assertTrue(is_hermitian_matrix(value.data)) - self.assertEqual(np.product(value.input_dims()), dim) - self.assertEqual(np.product(value.output_dims()), dim) + self.assertEqual(np.prod(value.input_dims()), dim) + self.assertEqual(np.prod(value.output_dims()), dim) def test_fixed_seed(self): """Test fixing seed fixes output""" @@ -139,8 +139,8 @@ def test_int_dims(self, dim): value = random_quantum_channel(dim) self.assertIsInstance(value, Stinespring) self.assertTrue(value.is_cptp()) - self.assertEqual(np.product(value.input_dims()), dim) - self.assertEqual(np.product(value.output_dims()), dim) + self.assertEqual(np.prod(value.input_dims()), dim) + self.assertEqual(np.prod(value.output_dims()), dim) @combine(rank=[1, 2, 3, 4]) def test_rank(self, rank): diff --git a/test/python/quantum_info/operators/test_scalar_op.py b/test/python/quantum_info/operators/test_scalar_op.py index 3f5b11ef56f3..fa07b286e9bb 100644 --- a/test/python/quantum_info/operators/test_scalar_op.py +++ b/test/python/quantum_info/operators/test_scalar_op.py @@ -58,7 +58,7 @@ def test_init(self, j): def test_custom_dims(self): """Test custom dims.""" dims = (2, 3, 4, 5) - dim = np.product(dims) + dim = np.prod(dims) op = ScalarOp(dims) self.assertEqual(op.dim, (dim, dim)) self.assertEqual(op.input_dims(), dims) @@ -84,7 +84,7 @@ class TestScalarOpMethods(ScalarOpTestCase): @combine(dims=[2, 4, 5, (2, 3), (3, 2)], coeff=[0, 1, 2.1 - 3.1j]) def test_to_operator(self, dims, coeff): """Test to_matrix and to_operator methods (dims={dims}, coeff={coeff})""" - dim = np.product(dims) + dim = np.prod(dims) iden = ScalarOp(dims, coeff=coeff) target = Operator(coeff * np.eye(dim), input_dims=dims, output_dims=dims) with self.subTest(msg="to_operator"): diff --git a/test/python/quantum_info/states/test_random.py b/test/python/quantum_info/states/test_random.py index 0383e0026212..9a947678985a 100644 --- a/test/python/quantum_info/states/test_random.py +++ b/test/python/quantum_info/states/test_random.py @@ -41,7 +41,7 @@ def test_int_dims(self, dim): value = random_statevector(dim) self.assertIsInstance(value, Statevector) self.assertTrue(value.is_valid()) - self.assertEqual(np.product(value.dims()), dim) + self.assertEqual(np.prod(value.dims()), dim) def test_fixed_seed(self): """Test fixing seed fixes output""" @@ -79,7 +79,7 @@ def test_int_dims(self, dim, method): value = random_density_matrix(dim, method=method) self.assertIsInstance(value, DensityMatrix) self.assertTrue(value.is_valid()) - self.assertEqual(np.product(value.dims()), dim) + self.assertEqual(np.prod(value.dims()), dim) @combine(method=["Hilbert-Schmidt", "Bures"]) def test_fixed_seed(self, method): From 93a8172c3ca8fda510393087a861ef12d661906f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elena=20Pe=C3=B1a=20Tapia?= <57907331+ElePT@users.noreply.github.com> Date: Tue, 20 Jun 2023 10:25:59 +0200 Subject: [PATCH 151/172] Fix VQD's `optimal_values` (#10279) * Fix optimal values * Add unittest * Add reno * Update unittest * Revert "Update unittest" This reverts commit 5000d2fb02e8126b376d9cd30d2862c202af2755. * Update test * Make test subtest --- qiskit/algorithms/eigensolvers/vqd.py | 2 +- releasenotes/notes/fix-vqd-result-27b26f0a6d49e7c7.yaml | 5 +++++ test/python/algorithms/eigensolvers/test_vqd.py | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/fix-vqd-result-27b26f0a6d49e7c7.yaml diff --git a/qiskit/algorithms/eigensolvers/vqd.py b/qiskit/algorithms/eigensolvers/vqd.py index 6a0c8a0842b4..cbb4d56af313 100644 --- a/qiskit/algorithms/eigensolvers/vqd.py +++ b/qiskit/algorithms/eigensolvers/vqd.py @@ -448,7 +448,7 @@ def _update_vqd_result( else np.array([opt_result.x]) ) result.optimal_parameters.append(dict(zip(ansatz.parameters, opt_result.x))) - result.optimal_values = np.concatenate([result.optimal_points, [opt_result.x]]) + result.optimal_values = np.concatenate([result.optimal_values, [opt_result.fun]]) result.cost_function_evals = np.concatenate([result.cost_function_evals, [opt_result.nfev]]) result.optimizer_times = np.concatenate([result.optimizer_times, [eval_time]]) result.eigenvalues.append(opt_result.fun + 0j) diff --git a/releasenotes/notes/fix-vqd-result-27b26f0a6d49e7c7.yaml b/releasenotes/notes/fix-vqd-result-27b26f0a6d49e7c7.yaml new file mode 100644 index 000000000000..8b2854c36da0 --- /dev/null +++ b/releasenotes/notes/fix-vqd-result-27b26f0a6d49e7c7.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fixed bug in :class:`~qiskit.algorithms.eigensolvers.VQD` where ``result.optimal_values`` was a + copy of ``result.optimal_points``. It now returns the corresponding values. diff --git a/test/python/algorithms/eigensolvers/test_vqd.py b/test/python/algorithms/eigensolvers/test_vqd.py index 16652a259ebd..a2520fcec815 100644 --- a/test/python/algorithms/eigensolvers/test_vqd.py +++ b/test/python/algorithms/eigensolvers/test_vqd.py @@ -103,6 +103,11 @@ def test_basic_operator(self, op): ) np.testing.assert_array_almost_equal(job.result().values, result.eigenvalues, 6) + with self.subTest(msg="assert returned values are eigenvalues"): + np.testing.assert_array_almost_equal( + result.optimal_values, self.h2_energy_excited[:2], decimal=3 + ) + def test_full_spectrum(self): """Test obtaining all eigenvalues.""" vqd = VQD(self.estimator, self.fidelity, self.ryrz_wavefunction, optimizer=L_BFGS_B(), k=4) From 9ef34b7a46aae4015ba4fee6e1ac1feaf03a960a Mon Sep 17 00:00:00 2001 From: Artemiy Burov <95361834+tyrolize@users.noreply.github.com> Date: Tue, 20 Jun 2023 17:17:17 +0200 Subject: [PATCH 152/172] Add more 1q and 2q Pauli rotation equivalences (#7407) * added rx to equevalence library * fixed sign in ry_to_rx * added rzz to rxx * gate correction Co-authored-by: Julien Gacon * added ryyrzz, rxxrzz, rzzryy translations, fixed ryrx translation to a more logical variant * fixed errors * Add Ryy to Rxx equivalence * Add release note --------- Co-authored-by: artemiyburov Co-authored-by: Julien Gacon Co-authored-by: Jake Lishman --- .../standard_gates/equivalence_library.py | 29 ++++++++++++++++++- ...otation-equivalences-6b2449c93c042dc9.yaml | 8 +++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/pauli-rotation-equivalences-6b2449c93c042dc9.yaml diff --git a/qiskit/circuit/library/standard_gates/equivalence_library.py b/qiskit/circuit/library/standard_gates/equivalence_library.py index 85116bfb65d7..6bda41addc7c 100644 --- a/qiskit/circuit/library/standard_gates/equivalence_library.py +++ b/qiskit/circuit/library/standard_gates/equivalence_library.py @@ -354,6 +354,13 @@ def_ry.append(RGate(theta, pi / 2), [q[0]], []) _sel.add_equivalence(RYGate(theta), def_ry) +q = QuantumRegister(1, "q") +ry_to_rx = QuantumCircuit(q) +ry_to_rx.sdg(0) +ry_to_rx.rx(theta, 0) +ry_to_rx.s(0) +_sel.add_equivalence(RYGate(theta), ry_to_rx) + # CRYGate # # q_0: ────■──── q_0: ─────────────■────────────────■── @@ -451,6 +458,20 @@ ryy_to_rzz.append(inst, qargs, cargs) _sel.add_equivalence(RYYGate(theta), ryy_to_rzz) +# RYY to RXX +q = QuantumRegister(2, "q") +theta = Parameter("theta") +ryy_to_rxx = QuantumCircuit(q) +for inst, qargs, cargs in [ + (SdgGate(), [q[0]], []), + (SdgGate(), [q[1]], []), + (RXXGate(theta), [q[0], q[1]], []), + (SGate(), [q[0]], []), + (SGate(), [q[1]], []), +]: + ryy_to_rxx.append(inst, qargs, cargs) +_sel.add_equivalence(RYYGate(theta), ryy_to_rxx) + # RZGate # global phase: -ϴ/2 # ┌───────┐ ┌───────┐ @@ -474,6 +495,13 @@ rz_to_sxry.sxdg(0) _sel.add_equivalence(RZGate(theta), rz_to_sxry) +q = QuantumRegister(1, "q") +rz_to_rx = QuantumCircuit(q) +rz_to_rx.h(0) +rz_to_rx.rx(theta, 0) +rz_to_rx.h(0) +_sel.add_equivalence(RZGate(theta), rz_to_rx) + # CRZGate # # q_0: ────■──── q_0: ─────────────■────────────────■── @@ -588,7 +616,6 @@ rzz_to_ryy.append(inst, qargs, cargs) _sel.add_equivalence(RZZGate(theta), rzz_to_ryy) - # RZXGate # # ┌─────────┐ diff --git a/releasenotes/notes/pauli-rotation-equivalences-6b2449c93c042dc9.yaml b/releasenotes/notes/pauli-rotation-equivalences-6b2449c93c042dc9.yaml new file mode 100644 index 000000000000..5c435eb51a53 --- /dev/null +++ b/releasenotes/notes/pauli-rotation-equivalences-6b2449c93c042dc9.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + The transpiler's built-in :class:`.EquivalenceLibrary` has been taught more Pauli-rotation + equivalences between the one-qubit :math:`R_X`, :math:`R_Y` and :math:`R_Z` gates, and between + the two-qubit :math:`R_{XX}`, :math:`R_{YY}` and :math:`R_{ZZ}` gates. This should make + simple basis translations more reliable, especially circuits that use :math:`Y` rotations. + See `#7332 `__. From f9958143eb0ea761e039ad31a090e231f4be42b4 Mon Sep 17 00:00:00 2001 From: daniel-fry <82879369+daniel-fry@users.noreply.github.com> Date: Tue, 20 Jun 2023 18:38:39 +0300 Subject: [PATCH 153/172] Allow to normalize inputs on ``StatePreparation`` and ``Initialize`` (#7189) * added normalize func * remove typecheck * changed to handle numpy arrays and lists both with complex numbers * Corrected docstring for normalization function * Add files via upload * initializer.py normalize function * fix line endings * addressing edits in docstrings and normalize function * review comments * attempt 1 to fix sphinx * add reno and improve error message * fix passing normalize to stateprep * Fixup PR --------- Co-authored-by: Julien Gacon Co-authored-by: Jake Lishman --- .../data_preparation/state_preparation.py | 22 +++++++++++++------ .../quantum_initializer/initializer.py | 11 ++++++---- .../normalize-stateprep-e21972dce8695509.yaml | 8 +++++++ test/python/circuit/test_initializer.py | 11 ++++++++++ 4 files changed, 41 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/normalize-stateprep-e21972dce8695509.yaml diff --git a/qiskit/circuit/library/data_preparation/state_preparation.py b/qiskit/circuit/library/data_preparation/state_preparation.py index 65b1c9162990..3e26c0e355d8 100644 --- a/qiskit/circuit/library/data_preparation/state_preparation.py +++ b/qiskit/circuit/library/data_preparation/state_preparation.py @@ -43,6 +43,7 @@ def __init__( num_qubits: Optional[int] = None, inverse: bool = False, label: Optional[str] = None, + normalize: bool = False, ): r""" Args: @@ -63,6 +64,7 @@ def __init__( and the remaining 3 qubits to be initialized to :math:`|0\rangle`. inverse: if True, the inverse state is constructed. label: An optional label for the gate + normalize (bool): Whether to normalize an input array to a unit vector. Raises: QiskitError: ``num_qubits`` parameter used when ``params`` is not an integer @@ -96,8 +98,15 @@ def __init__( self._from_label = isinstance(params, str) self._from_int = isinstance(params, int) - num_qubits = self._get_num_qubits(num_qubits, params) + # if initialized from a vector, check that the parameters are normalized + if not self._from_label and not self._from_int: + norm = np.linalg.norm(params) + if normalize: + params = np.array(params, dtype=np.complex128) / norm + elif not math.isclose(norm, 1.0, abs_tol=_EPS): + raise QiskitError(f"Sum of amplitudes-squared is not 1, but {norm}.") + num_qubits = self._get_num_qubits(num_qubits, params) params = [params] if isinstance(params, int) else params super().__init__(self._name, num_qubits, params, label=self._label) @@ -197,10 +206,6 @@ def _get_num_qubits(self, num_qubits, params): if num_qubits == 0 or not num_qubits.is_integer(): raise QiskitError("Desired statevector length not a positive power of 2.") - # Check if probabilities (amplitudes squared) sum to 1 - if not math.isclose(sum(np.absolute(params) ** 2), 1.0, abs_tol=_EPS): - raise QiskitError("Sum of amplitudes-squared does not equal one.") - num_qubits = int(num_qubits) return num_qubits @@ -409,7 +414,7 @@ def _multiplex(self, target_gate, list_of_angles, last_cnot=True): return circuit -def prepare_state(self, state, qubits=None, label=None): +def prepare_state(self, state, qubits=None, label=None, normalize=False): r"""Prepare qubits in a specific state. This class implements a state preparing unitary. Unlike @@ -433,6 +438,7 @@ def prepare_state(self, state, qubits=None, label=None): * int: Index of qubit to be initialized [Default: None]. * list: Indexes of qubits to be initialized [Default: None]. label (str): An optional label for the gate + normalize (bool): Whether to normalize an input array to a unit vector. Returns: qiskit.circuit.Instruction: a handle to the instruction that was just initialized @@ -511,7 +517,9 @@ def prepare_state(self, state, qubits=None, label=None): num_qubits = len(qubits) if isinstance(state, int) else None - return self.append(StatePreparation(state, num_qubits, label=label), qubits) + return self.append( + StatePreparation(state, num_qubits, label=label, normalize=normalize), qubits + ) QuantumCircuit.prepare_state = prepare_state diff --git a/qiskit/extensions/quantum_initializer/initializer.py b/qiskit/extensions/quantum_initializer/initializer.py index ea0349cd51e8..33f6c2314dd7 100644 --- a/qiskit/extensions/quantum_initializer/initializer.py +++ b/qiskit/extensions/quantum_initializer/initializer.py @@ -33,7 +33,7 @@ class Initialize(Instruction): which is not unitary. """ - def __init__(self, params, num_qubits=None): + def __init__(self, params, num_qubits=None, normalize=False): r"""Create new initialize composite. Args: @@ -53,8 +53,9 @@ def __init__(self, params, num_qubits=None): number of qubits in the `initialize` call. Example: `initialize` covers 5 qubits and params is 3. This allows qubits 0 and 1 to be initialized to :math:`|1\rangle` and the remaining 3 qubits to be initialized to :math:`|0\rangle`. + normalize (bool): Whether to normalize an input array to a unit vector. """ - self._stateprep = StatePreparation(params, num_qubits) + self._stateprep = StatePreparation(params, num_qubits, normalize=normalize) super().__init__("initialize", self._stateprep.num_qubits, 0, self._stateprep.params) @@ -87,7 +88,7 @@ def broadcast_arguments(self, qargs, cargs): return self._stateprep.broadcast_arguments(qargs, cargs) -def initialize(self, params, qubits=None): +def initialize(self, params, qubits=None, normalize=False): r"""Initialize qubits in a specific state. Qubit initialization is done by first resetting the qubits to :math:`|0\rangle` @@ -113,6 +114,8 @@ class to prepare the qubits in a specified state. * int: Index of qubit to be initialized [Default: None]. * list: Indexes of qubits to be initialized [Default: None]. + normalize (bool): whether to normalize an input array to a unit vector. + Returns: qiskit.circuit.Instruction: a handle to the instruction that was just initialized @@ -188,7 +191,7 @@ class to prepare the qubits in a specified state. qubits = [qubits] num_qubits = len(qubits) if isinstance(params, int) else None - return self.append(Initialize(params, num_qubits), qubits) + return self.append(Initialize(params, num_qubits, normalize), qubits) QuantumCircuit.initialize = initialize diff --git a/releasenotes/notes/normalize-stateprep-e21972dce8695509.yaml b/releasenotes/notes/normalize-stateprep-e21972dce8695509.yaml new file mode 100644 index 000000000000..e460b6e55ee1 --- /dev/null +++ b/releasenotes/notes/normalize-stateprep-e21972dce8695509.yaml @@ -0,0 +1,8 @@ +--- +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 + raising an exception when given non-normalized input. diff --git a/test/python/circuit/test_initializer.py b/test/python/circuit/test_initializer.py index fd6656d3c53b..eb81c6766831 100644 --- a/test/python/circuit/test_initializer.py +++ b/test/python/circuit/test_initializer.py @@ -237,6 +237,17 @@ def test_non_unit_probability(self): qc = QuantumCircuit(qr) self.assertRaises(QiskitError, qc.initialize, desired_vector, [qr[0], qr[1]]) + def test_normalize(self): + """Test initializing with a non-normalized vector is normalized, if specified.""" + desired_vector = [1, 1] + normalized = np.asarray(desired_vector) / np.linalg.norm(desired_vector) + + qc = QuantumCircuit(1) + qc.initialize(desired_vector, [0], normalize=True) + op = qc.data[0].operation + self.assertAlmostEqual(np.linalg.norm(op.params), 1) + self.assertEqual(Statevector(qc), Statevector(normalized)) + def test_wrong_vector_size(self): """Initializing to a vector with a size different to the qubit parameter length. See https://github.com/Qiskit/qiskit-terra/issues/2372""" From 2ba298481a2c7d0b498d22d2e9fc7710154c23bb Mon Sep 17 00:00:00 2001 From: Abby Mitchell <23662430+javabster@users.noreply.github.com> Date: Tue, 20 Jun 2023 12:35:49 -0400 Subject: [PATCH 154/172] Added classical_predecessors method (#9980) * Added classical_predecessors method * add accesed method * Added classsical_successor method * Added classical predecessors and successors method * added tol command * successors method * add test classical_successor method * changed DagOutNode * Apply suggestions from code review from @atharva-satpute. Thanks! Co-authored-by: atharva-satpute <55058959+atharva-satpute@users.noreply.github.com> * add assertRaises on test methods * correct typo variable name * change the descrption * change the text on the example * change the format of yaml * new example * new suggestions * fix test * new test example and change the release note * Fixup release note * Fix typo --------- Co-authored-by: Maldoalberto Co-authored-by: Maldoalberto Co-authored-by: Luciano Bello Co-authored-by: atharva-satpute <55058959+atharva-satpute@users.noreply.github.com> Co-authored-by: Jake Lishman --- qiskit/dagcircuit/dagcircuit.py | 18 +++++ ...assical-predecessors-9ecef0561822e934.yaml | 27 ++++++++ test/python/dagcircuit/test_dagcircuit.py | 67 +++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 releasenotes/notes/add-classical-predecessors-9ecef0561822e934.yaml diff --git a/qiskit/dagcircuit/dagcircuit.py b/qiskit/dagcircuit/dagcircuit.py index f40015d7f078..a0eb6feec718 100644 --- a/qiskit/dagcircuit/dagcircuit.py +++ b/qiskit/dagcircuit/dagcircuit.py @@ -1565,6 +1565,15 @@ def quantum_predecessors(self, node): ) ) + def classical_predecessors(self, node): + """Returns iterator of the predecessors of a node that are + 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) + ) + ) + def ancestors(self, node): """Returns set of the ancestors of a node as DAGOpNodes and DAGInNodes.""" return {self._multi_graph[x] for x in rx.ancestors(self._multi_graph, node._node_id)} @@ -1589,6 +1598,15 @@ def quantum_successors(self, node): ) ) + def classical_successors(self, node): + """Returns iterator of the successors of a node that are + 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) + ) + ) + def remove_op_node(self, node): """Remove an operation node n. diff --git a/releasenotes/notes/add-classical-predecessors-9ecef0561822e934.yaml b/releasenotes/notes/add-classical-predecessors-9ecef0561822e934.yaml new file mode 100644 index 000000000000..9c554633d4ff --- /dev/null +++ b/releasenotes/notes/add-classical-predecessors-9ecef0561822e934.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + Added :meth:`.DAGCircuit.classical_predecessors` and + :meth:`.DAGCircuit.classical_successors`, an alternative to select the classical + wires without having to go to the inner graph object directly of a node in the DAG. + The following example illustrates the new functionality:: + + from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister + from qiskit.converters import circuit_to_dag + from qiskit.circuit.library import RZGate + + q = QuantumRegister(3, 'q') + c = ClassicalRegister(3, 'c') + circ = QuantumCircuit(q, c) + circ.h(q[0]) + circ.cx(q[0], q[1]) + circ.measure(q[0], c[0]) + circ.rz(0.5, q[1]).c_if(c, 2) + circ.measure(q[1], c[0]) + dag = circuit_to_dag(circ) + + rz_node = dag.op_nodes(RZGate)[0] + # Contains the "measure" on clbit 0, and the "wire start" nodes for clbits 1 and 2. + classical_predecessors = list(dag.classical_predecessors(rz_node)) + # Contains the "measure" on clbit 0, and the "wire end" nodes for clbits 1 and 2. + classical_successors = list(dag.classical_successors(rz_node)) diff --git a/test/python/dagcircuit/test_dagcircuit.py b/test/python/dagcircuit/test_dagcircuit.py index 48ec6b7af29f..0a61bb499ce7 100644 --- a/test/python/dagcircuit/test_dagcircuit.py +++ b/test/python/dagcircuit/test_dagcircuit.py @@ -740,6 +740,73 @@ def test_quantum_predecessors(self): or (isinstance(predecessor2, DAGInNode) and isinstance(predecessor1.op, Reset)) ) + def test_classical_predecessors(self): + """The method dag.classical_predecessors() returns predecessors connected by classical edges""" + + # ┌───┐ ┌───┐ + # q_0: ┤ H ├──■───────────────────■──┤ H ├ + # ├───┤┌─┴─┐ ┌─┴─┐├───┤ + # q_1: ┤ H ├┤ X ├──■─────────■──┤ X ├┤ H ├ + # └───┘└───┘┌─┴─┐┌───┐┌─┴─┐└───┘└───┘ + # q_2: ──────────┤ X ├┤ H ├┤ X ├────────── + # └───┘└───┘└───┘ + # c: 5/═══════════════════════════════════ + + self.dag.apply_operation_back(HGate(), [self.qubit0], []) + self.dag.apply_operation_back(HGate(), [self.qubit1], []) + self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) + self.dag.apply_operation_back(CXGate(), [self.qubit1, self.qubit2], []) + self.dag.apply_operation_back(HGate(), [self.qubit2], []) + self.dag.apply_operation_back(CXGate(), [self.qubit1, self.qubit2], []) + self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) + self.dag.apply_operation_back(HGate(), [self.qubit0], []) + self.dag.apply_operation_back(HGate(), [self.qubit1], []) + self.dag.apply_operation_back(Measure(), [self.qubit0, self.clbit0], []) + self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], []) + + predecessor_measure = self.dag.classical_predecessors(self.dag.named_nodes("measure").pop()) + + predecessor1 = next(predecessor_measure) + + with self.assertRaises(StopIteration): + next(predecessor_measure) + + self.assertIsInstance(predecessor1, DAGInNode) + self.assertIsInstance(predecessor1.wire, Clbit) + + def test_classical_successors(self): + """The method dag.classical_successors() returns successors connected by classical edges""" + + # ┌───┐ ┌───┐ + # q_0: ┤ H ├──■───────────────────■──┤ H ├ + # ├───┤┌─┴─┐ ┌─┴─┐├───┤ + # q_1: ┤ H ├┤ X ├──■─────────■──┤ X ├┤ H ├ + # └───┘└───┘┌─┴─┐┌───┐┌─┴─┐└───┘└───┘ + # q_2: ──────────┤ X ├┤ H ├┤ X ├────────── + # └───┘└───┘└───┘ + # c: 5/═══════════════════════════════════ + + self.dag.apply_operation_back(HGate(), [self.qubit0], []) + self.dag.apply_operation_back(HGate(), [self.qubit1], []) + self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) + self.dag.apply_operation_back(CXGate(), [self.qubit1, self.qubit2], []) + self.dag.apply_operation_back(HGate(), [self.qubit2], []) + self.dag.apply_operation_back(CXGate(), [self.qubit1, self.qubit2], []) + self.dag.apply_operation_back(CXGate(), [self.qubit0, self.qubit1], []) + self.dag.apply_operation_back(HGate(), [self.qubit0], []) + self.dag.apply_operation_back(HGate(), [self.qubit1], []) + self.dag.apply_operation_back(Measure(), [self.qubit0, self.clbit0], []) + self.dag.apply_operation_back(Measure(), [self.qubit1, self.clbit1], []) + + successors_measure = self.dag.classical_successors(self.dag.named_nodes("measure").pop()) + + successors1 = next(successors_measure) + with self.assertRaises(StopIteration): + next(successors_measure) + + self.assertIsInstance(successors1, DAGOutNode) + self.assertIsInstance(successors1.wire, Clbit) + def test_is_predecessor(self): """The method dag.is_predecessor(A, B) checks if node B is a predecessor of A""" From 57cd30fb85f921b13c233d85268932ae4ce53494 Mon Sep 17 00:00:00 2001 From: Edwin Navarro Date: Tue, 20 Jun 2023 09:40:16 -0700 Subject: [PATCH 155/172] Prequel to if/else changes to mpl circuit drawer (#10096) * Remove deprecated args from mpl drawer * Reno mod * Convert to wire_map, node_data, etc * Lint * Minor changes to match flow changes * Cleanup * Fix layer width per node and comments * Lint * Adjust layer num loading * Revert n_lines * Lint * Add glob_data * Lint and cleanup * Unused arg * Move figure, mpl, and style info to draw() * Change font vars and fix ax * Make patches_mod global --- qiskit/visualization/circuit/matplotlib.py | 663 +++++++++++---------- 1 file changed, 351 insertions(+), 312 deletions(-) diff --git a/qiskit/visualization/circuit/matplotlib.py b/qiskit/visualization/circuit/matplotlib.py index b1b04ca6602c..965dec412337 100644 --- a/qiskit/visualization/circuit/matplotlib.py +++ b/qiskit/visualization/circuit/matplotlib.py @@ -55,6 +55,8 @@ PORDER_GRAY = 3 PORDER_TEXT = 6 +INFINITE_FOLD = 10000000 + @_optionals.HAS_MATPLOTLIB.require_in_instance @_optionals.HAS_PYLATEX.require_in_instance @@ -79,29 +81,13 @@ def __init__( cregbundle=None, with_layout=False, ): - from matplotlib import patches - from matplotlib import pyplot as plt - - self._patches_mod = patches - self._plt_mod = plt - self._circuit = circuit self._qubits = qubits self._clbits = clbits - self._qubits_dict = {} - self._clbits_dict = {} - self._q_anchors = {} - self._c_anchors = {} - self._wire_map = {} - self._nodes = nodes self._scale = 1.0 if scale is None else scale - self._style, def_font_ratio = load_style(style) - - # If font/subfont ratio changes from default, have to scale width calculations for - # subfont. Font change is auto scaled in the self._figure.set_size_inches call in draw() - self._subfont_factor = self._style["sfs"] * def_font_ratio / self._style["fs"] + self._style = style self._plot_barriers = plot_barriers self._reverse_bits = reverse_bits @@ -117,18 +103,7 @@ def __init__( if self._fold < 2: self._fold = -1 - if ax is None: - self._user_ax = False - self._figure = plt.figure() - self._figure.patch.set_facecolor(color=self._style["bg"]) - self._ax = self._figure.add_subplot(111) - else: - self._user_ax = True - self._ax = ax - self._figure = ax.get_figure() - self._ax.axis("off") - self._ax.set_aspect("equal") - self._ax.tick_params(labelbottom=False, labeltop=False, labelleft=False, labelright=False) + self._ax = ax self._initial_state = initial_state self._global_phase = self._circuit.global_phase @@ -148,18 +123,9 @@ def __init__( else: self._cregbundle = True if cregbundle is None else cregbundle - self._fs = self._style["fs"] - self._sfs = self._style["sfs"] self._lwidth1 = 1.0 self._lwidth15 = 1.5 self._lwidth2 = 2.0 - self._x_offset = 0.0 - - # _data per node with 'width', 'gate_text', 'raw_gate_text', - # 'ctrl_text', 'param', q_xy', 'c_xy', and 'c_indxs' - # and colors 'fc', 'ec', 'lc', 'sc', 'gt', and 'tc' - self._data = {} - self._layer_widths = [] # _char_list for finding text_width of names, labels, and params self._char_list = { @@ -263,31 +229,76 @@ def draw(self, filename=None, verbose=False): """Main entry point to 'matplotlib' ('mpl') drawer. Called from ``visualization.circuit_drawer`` and from ``QuantumCircuit.draw`` through circuit_drawer. """ - # All information for the drawing is first loaded into self._data for the gates and into - # self._qubits_dict and self._clbits_dict for the qubits, clbits, and wires, + + # Import matplotlib and load all the figure, window, and style info + from matplotlib import patches + from matplotlib import pyplot as plt + + # glob_data contains global values used throughout, "n_lines", "x_offset", "next_x_index", + # "patches_mod", subfont_factor" + glob_data = {} + + glob_data["patches_mod"] = patches + plt_mod = plt + + self._style, def_font_ratio = load_style(self._style) + + # If font/subfont ratio changes from default, have to scale width calculations for + # subfont. Font change is auto scaled in the mpl_figure.set_size_inches call in draw() + glob_data["subfont_factor"] = self._style["sfs"] * def_font_ratio / self._style["fs"] + + # if no user ax, setup default figure. Else use the user figure. + if self._ax is None: + is_user_ax = False + mpl_figure = plt.figure() + mpl_figure.patch.set_facecolor(color=self._style["bg"]) + self._ax = mpl_figure.add_subplot(111) + else: + is_user_ax = True + mpl_figure = self._ax.get_figure() + self._ax.axis("off") + self._ax.set_aspect("equal") + self._ax.tick_params(labelbottom=False, labeltop=False, labelleft=False, labelright=False) + + # All information for the drawing is first loaded into node_data for the gates and into + # qubits_dict, clbits_dict, and wire_map for the qubits, clbits, and wires, # followed by the coordinates for each gate. - # get layer widths - self._get_layer_widths() + # load the wire map + wire_map = get_wire_map(self._circuit, self._qubits + self._clbits, self._cregbundle) + + # node_data per node with 'width', 'gate_text', 'raw_gate_text', + # 'ctrl_text', 'param_text', q_xy', and 'c_xy', + # and colors 'fc', 'ec', 'lc', 'sc', 'gt', and 'tc' + node_data = {} + + # dicts for the names and locations of register/bit labels + qubits_dict = {} + clbits_dict = {} # load the _qubit_dict and _clbit_dict with register info - n_lines = self._set_bit_reg_info() + self._set_bit_reg_info(wire_map, qubits_dict, clbits_dict, glob_data) + + # get layer widths + layer_widths = self._get_layer_widths(node_data, glob_data) # load the coordinates for each gate and compute number of folds - max_anc = self._get_coords(n_lines) - num_folds = max(0, max_anc - 1) // self._fold if self._fold > 0 else 0 + max_x_index = self._get_coords( + node_data, wire_map, layer_widths, qubits_dict, clbits_dict, glob_data + ) + num_folds = max(0, max_x_index - 1) // self._fold if self._fold > 0 else 0 # The window size limits are computed, followed by one of the four possible ways # of scaling the drawing. # compute the window size - if max_anc > self._fold > 0: - xmax = self._fold + self._x_offset + 0.1 - ymax = (num_folds + 1) * (n_lines + 1) - 1 + if max_x_index > self._fold > 0: + xmax = self._fold + glob_data["x_offset"] + 0.1 + ymax = (num_folds + 1) * (glob_data["n_lines"] + 1) - 1 else: x_incr = 0.4 if not self._nodes else 0.9 - xmax = max_anc + 1 + self._x_offset - x_incr - ymax = n_lines + xmax = max_x_index + 1 + glob_data["x_offset"] - x_incr + ymax = glob_data["n_lines"] xl = -self._style["margin"][0] xr = xmax + self._style["margin"][1] @@ -297,36 +308,36 @@ def draw(self, filename=None, verbose=False): self._ax.set_ylim(yb, yt) # update figure size and, for backward compatibility, - # need to scale by a default value equal to (self._fs * 3.01 / 72 / 0.65) + # need to scale by a default value equal to (self._style["fs"] * 3.01 / 72 / 0.65) base_fig_w = (xr - xl) * 0.8361111 base_fig_h = (yt - yb) * 0.8361111 scale = self._scale # if user passes in an ax, this size takes priority over any other settings - if self._user_ax: + if is_user_ax: # from stackoverflow #19306510, get the bbox size for the ax and then reset scale - bbox = self._ax.get_window_extent().transformed(self._figure.dpi_scale_trans.inverted()) + bbox = self._ax.get_window_extent().transformed(mpl_figure.dpi_scale_trans.inverted()) scale = bbox.width / base_fig_w / 0.8361111 # if scale not 1.0, use this scale factor elif self._scale != 1.0: - self._figure.set_size_inches(base_fig_w * self._scale, base_fig_h * self._scale) + mpl_figure.set_size_inches(base_fig_w * self._scale, base_fig_h * self._scale) # if "figwidth" style param set, use this to scale elif self._style["figwidth"] > 0.0: # in order to get actual inches, need to scale by factor adj_fig_w = self._style["figwidth"] * 1.282736 - self._figure.set_size_inches(adj_fig_w, adj_fig_w * base_fig_h / base_fig_w) + mpl_figure.set_size_inches(adj_fig_w, adj_fig_w * base_fig_h / base_fig_w) scale = adj_fig_w / base_fig_w # otherwise, display default size else: - self._figure.set_size_inches(base_fig_w, base_fig_h) + mpl_figure.set_size_inches(base_fig_w, base_fig_h) # drawing will scale with 'set_size_inches', but fonts and linewidths do not if scale != 1.0: - self._fs *= scale - self._sfs *= scale + self._style["fs"] *= scale + self._style["sfs"] *= scale self._lwidth1 = 1.0 * scale self._lwidth15 = 1.5 * scale self._lwidth2 = 2.0 * scale @@ -334,46 +345,54 @@ 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: - self._plt_mod.text( - xl, yt, "Global Phase: %s" % pi_check(self._global_phase, output="mpl") - ) - self._draw_regs_wires(num_folds, xmax, n_lines, max_anc) - self._draw_ops(verbose) + plt_mod.text(xl, yt, "Global Phase: %s" % 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, node_data, wire_map, layer_widths, clbits_dict, glob_data, verbose + ) if filename: - self._figure.savefig( + mpl_figure.savefig( filename, dpi=self._style["dpi"], bbox_inches="tight", - facecolor=self._figure.get_facecolor(), + facecolor=mpl_figure.get_facecolor(), ) - if not self._user_ax: - matplotlib_close_if_inline(self._figure) - return self._figure + if not is_user_ax: + matplotlib_close_if_inline(mpl_figure) + return mpl_figure - def _get_layer_widths(self): + def _get_layer_widths(self, node_data, glob_data): """Compute the layer_widths for the layers""" - for layer in self._nodes: + + layer_widths = {} + for layer_num, layer in enumerate(self._nodes): widest_box = WID - for node in layer: + for i, node in enumerate(layer): + # Put the layer_num in the first node in the layer and put -1 in the rest + # so that layer widths are not counted more than once + if i != 0: + layer_num = -1 + layer_widths[node] = [1, layer_num] + op = node.op - self._data[node] = {} - self._data[node]["width"] = WID + node_data[node] = {} + node_data[node]["width"] = WID num_ctrl_qubits = 0 if not hasattr(op, "num_ctrl_qubits") else op.num_ctrl_qubits if ( getattr(op, "_directive", False) and (not op.label or not self._plot_barriers) ) or isinstance(op, Measure): - self._data[node]["raw_gate_text"] = op.name + node_data[node]["raw_gate_text"] = op.name continue base_type = None if not hasattr(op, "base_gate") else op.base_gate gate_text, ctrl_text, raw_gate_text = get_gate_ctrl_text( op, "mpl", style=self._style, calibrations=self._calibrations ) - self._data[node]["gate_text"] = gate_text - self._data[node]["ctrl_text"] = ctrl_text - self._data[node]["raw_gate_text"] = raw_gate_text - self._data[node]["param"] = "" + node_data[node]["gate_text"] = gate_text + node_data[node]["ctrl_text"] = ctrl_text + node_data[node]["raw_gate_text"] = raw_gate_text + node_data[node]["param_text"] = "" # if single qubit, no params, and no labels, layer_width is 1 if ( @@ -389,7 +408,9 @@ def _get_layer_widths(self): # small increments at end of the 3 _get_text_width calls are for small # spacing adjustments between gates - ctrl_width = self._get_text_width(ctrl_text, fontsize=self._sfs) - 0.05 + ctrl_width = ( + self._get_text_width(ctrl_text, glob_data, fontsize=self._style["sfs"]) - 0.05 + ) # get param_width, but 0 for gates with array params if ( @@ -397,11 +418,13 @@ def _get_layer_widths(self): and len(op.params) > 0 and not any(isinstance(param, np.ndarray) for param in op.params) ): - param = get_param_str(op, "mpl", ndigits=3) + param_text = get_param_str(op, "mpl", ndigits=3) if isinstance(op, Initialize): - param = f"$[{param.replace('$', '')}]$" - self._data[node]["param"] = param - raw_param_width = self._get_text_width(param, fontsize=self._sfs, param=True) + param_text = f"$[{param_text.replace('$', '')}]$" + node_data[node]["param_text"] = param_text + raw_param_width = self._get_text_width( + param_text, glob_data, fontsize=self._style["sfs"], param=True + ) param_width = raw_param_width + 0.08 else: param_width = raw_param_width = 0.0 @@ -411,14 +434,18 @@ def _get_layer_widths(self): if isinstance(base_type, PhaseGate): gate_text = "P" raw_gate_width = ( - self._get_text_width(gate_text + " ()", fontsize=self._sfs) + self._get_text_width( + gate_text + " ()", glob_data, fontsize=self._style["sfs"] + ) + raw_param_width ) gate_width = (raw_gate_width + 0.08) * 1.58 - # otherwise, standard gate or multiqubit gate + # Otherwise, standard gate or multiqubit gate else: - raw_gate_width = self._get_text_width(gate_text, fontsize=self._fs) + raw_gate_width = self._get_text_width( + gate_text, glob_data, fontsize=self._style["fs"] + ) gate_width = raw_gate_width + 0.10 # add .21 for the qubit numbers on the left of the multibit gates if len(node.qargs) - num_ctrl_qubits > 1: @@ -427,26 +454,28 @@ def _get_layer_widths(self): box_width = max(gate_width, ctrl_width, param_width, WID) if box_width > widest_box: widest_box = box_width - self._data[node]["width"] = max(raw_gate_width, raw_param_width) + node_data[node]["width"] = max(raw_gate_width, raw_param_width) + for node in layer: + layer_widths[node][0] = int(widest_box) + 1 - self._layer_widths.append(int(widest_box) + 1) + return layer_widths - def _set_bit_reg_info(self): + def _set_bit_reg_info(self, wire_map, qubits_dict, clbits_dict, glob_data): """Get all the info for drawing bit/reg names and numbers""" - self._wire_map = get_wire_map(self._circuit, self._qubits + self._clbits, self._cregbundle) longest_wire_label_width = 0 - n_lines = 0 + glob_data["n_lines"] = 0 initial_qbit = " |0>" if self._initial_state else "" initial_cbit = " 0" if self._initial_state else "" idx = 0 pos = y_off = -len(self._qubits) + 1 - for ii, wire in enumerate(self._wire_map): + for ii, wire in enumerate(wire_map): + # if it's a creg, register is the key and just load the index if isinstance(wire, ClassicalRegister): register = wire - index = self._wire_map[wire] + index = wire_map[wire] # otherwise, get the register from find_bit and use bit_index if # it's a bit, or the index of the bit in the register if it's a reg @@ -470,83 +499,87 @@ def _set_bit_reg_info(self): ) reg_remove_under = 0 if reg_size < 2 else 1 text_width = ( - self._get_text_width(wire_label, self._fs, reg_remove_under=reg_remove_under) * 1.15 + self._get_text_width( + wire_label, glob_data, self._style["fs"], reg_remove_under=reg_remove_under + ) + * 1.15 ) if text_width > longest_wire_label_width: longest_wire_label_width = text_width if isinstance(wire, Qubit): pos = -ii - self._qubits_dict[ii] = { + qubits_dict[ii] = { "y": pos, "wire_label": wire_label, - "index": bit_index, - "register": register, } - n_lines += 1 + glob_data["n_lines"] += 1 else: if ( not self._cregbundle or register is None or (self._cregbundle and isinstance(wire, ClassicalRegister)) ): - n_lines += 1 + glob_data["n_lines"] += 1 idx += 1 pos = y_off - idx - self._clbits_dict[ii] = { + clbits_dict[ii] = { "y": pos, "wire_label": wire_label, - "index": bit_index, "register": register, } + glob_data["x_offset"] = -1.2 + longest_wire_label_width - self._x_offset = -1.2 + longest_wire_label_width - return n_lines - - def _get_coords(self, n_lines): - """Load all the coordinate info needed to place the gates on the drawing""" + def _get_coords(self, node_data, wire_map, layer_widths, qubits_dict, clbits_dict, glob_data): + """Load all the coordinate info needed to place the gates on the drawing.""" - # create the anchor arrays - for key, qubit in self._qubits_dict.items(): - self._q_anchors[key] = Anchor(num_wires=n_lines, y_index=qubit["y"], fold=self._fold) - for key, clbit in self._clbits_dict.items(): - self._c_anchors[key] = Anchor(num_wires=n_lines, y_index=clbit["y"], fold=self._fold) - - # get all the necessary coordinates for placing gates on the wires prev_x_index = -1 - for i, layer in enumerate(self._nodes): - layer_width = self._layer_widths[i] - anc_x_index = prev_x_index + 1 + for layer in self._nodes: + curr_x_index = prev_x_index + 1 + l_width = [] for node in layer: - # get qubit index + + # get qubit indexes q_indxs = [] for qarg in node.qargs: if qarg in self._qubits: - q_indxs.append(self._wire_map[qarg]) + q_indxs.append(wire_map[qarg]) + # get clbit indexes c_indxs = [] for carg in node.cargs: if carg in self._clbits: register = get_bit_register(self._circuit, carg) if register is not None and self._cregbundle: - c_indxs.append(self._wire_map[register]) + c_indxs.append(wire_map[register]) else: - c_indxs.append(self._wire_map[carg]) - - # qubit coordinate - self._data[node]["q_xy"] = [ - self._q_anchors[ii].plot_coord(anc_x_index, layer_width, self._x_offset) + c_indxs.append(wire_map[carg]) + + # qubit coordinates + node_data[node]["q_xy"] = [ + self._plot_coord( + curr_x_index, + qubits_dict[ii]["y"], + layer_widths[node][0], + glob_data, + ) for ii in q_indxs ] - # clbit coordinate - self._data[node]["c_xy"] = [ - self._c_anchors[ii].plot_coord(anc_x_index, layer_width, self._x_offset) + # clbit coordinates + node_data[node]["c_xy"] = [ + self._plot_coord( + curr_x_index, + clbits_dict[ii]["y"], + layer_widths[node][0], + glob_data, + ) for ii in c_indxs ] + # update index based on the value from plotting - anc_x_index = self._q_anchors[q_indxs[0]].get_x_index() - self._data[node]["c_indxs"] = c_indxs + curr_x_index = glob_data["next_x_index"] + l_width.append(layer_widths[node][0]) # adjust the column if there have been barriers encountered, but not plotted barrier_offset = 0 @@ -555,12 +588,13 @@ def _get_coords(self, n_lines): barrier_offset = ( -1 if all(getattr(nd.op, "_directive", False) for nd in layer) else 0 ) - prev_x_index = anc_x_index + layer_width + barrier_offset - 1 + prev_x_index = curr_x_index + max(l_width) + barrier_offset - 1 return prev_x_index + 1 - def _get_text_width(self, text, fontsize, param=False, reg_remove_under=None): + def _get_text_width(self, text, glob_data, fontsize, param=False, reg_remove_under=None): """Compute the width of a string in the default font""" + from pylatexenc.latex2text import LatexNodes2Text if not text: @@ -592,7 +626,7 @@ def _get_text_width(self, text, fontsize, param=False, reg_remove_under=None): if param: text = text.replace("-", "+") - f = 0 if fontsize == self._fs else 1 + f = 0 if fontsize == self._style["fs"] else 1 sum_text = 0.0 for c in text: try: @@ -601,42 +635,40 @@ def _get_text_width(self, text, fontsize, param=False, reg_remove_under=None): # if non-ASCII char, use width of 'c', an average size sum_text += self._char_list["c"][f] if f == 1: - sum_text *= self._subfont_factor + sum_text *= glob_data["subfont_factor"] return sum_text - def _draw_regs_wires(self, num_folds, xmax, n_lines, max_anc): + def _draw_regs_wires(self, num_folds, xmax, max_x_index, qubits_dict, clbits_dict, glob_data): """Draw the register names and numbers, wires, and vertical lines at the ends""" for fold_num in range(num_folds + 1): # quantum registers - for qubit in self._qubits_dict.values(): + for qubit in qubits_dict.values(): qubit_label = qubit["wire_label"] - y = qubit["y"] - fold_num * (n_lines + 1) + y = qubit["y"] - fold_num * (glob_data["n_lines"] + 1) self._ax.text( - self._x_offset - 0.2, + glob_data["x_offset"] - 0.2, y, qubit_label, ha="right", va="center", - fontsize=1.25 * self._fs, + fontsize=1.25 * self._style["fs"], color=self._style["tc"], clip_on=True, zorder=PORDER_TEXT, ) # draw the qubit wire - self._line([self._x_offset, y], [xmax, y], zorder=PORDER_REGLINE) + self._line([glob_data["x_offset"], y], [xmax, y], zorder=PORDER_REGLINE) # classical registers this_clbit_dict = {} - for clbit in self._clbits_dict.values(): - clbit_label = clbit["wire_label"] - clbit_reg = clbit["register"] - y = clbit["y"] - fold_num * (n_lines + 1) + for clbit in clbits_dict.values(): + y = clbit["y"] - fold_num * (glob_data["n_lines"] + 1) if y not in this_clbit_dict.keys(): this_clbit_dict[y] = { "val": 1, - "wire_label": clbit_label, - "register": clbit_reg, + "wire_label": clbit["wire_label"], + "register": clbit["register"], } else: this_clbit_dict[y]["val"] += 1 @@ -645,36 +677,36 @@ def _draw_regs_wires(self, num_folds, xmax, n_lines, max_anc): # cregbundle if self._cregbundle and this_clbit["register"] is not None: self._ax.plot( - [self._x_offset + 0.2, self._x_offset + 0.3], + [glob_data["x_offset"] + 0.2, glob_data["x_offset"] + 0.3], [y - 0.1, y + 0.1], color=self._style["cc"], zorder=PORDER_LINE, ) self._ax.text( - self._x_offset + 0.1, + glob_data["x_offset"] + 0.1, y + 0.1, str(this_clbit["register"].size), ha="left", va="bottom", - fontsize=0.8 * self._fs, + fontsize=0.8 * self._style["fs"], color=self._style["tc"], clip_on=True, zorder=PORDER_TEXT, ) self._ax.text( - self._x_offset - 0.2, + glob_data["x_offset"] - 0.2, y, this_clbit["wire_label"], ha="right", va="center", - fontsize=1.25 * self._fs, + fontsize=1.25 * self._style["fs"], color=self._style["tc"], clip_on=True, zorder=PORDER_TEXT, ) # draw the clbit wire self._line( - [self._x_offset, y], + [glob_data["x_offset"], y], [xmax, y], lc=self._style["cc"], ls=self._style["cline"], @@ -685,10 +717,10 @@ def _draw_regs_wires(self, num_folds, xmax, n_lines, max_anc): feedline_r = num_folds > 0 and num_folds > fold_num feedline_l = fold_num > 0 if feedline_l or feedline_r: - xpos_l = self._x_offset - 0.01 - xpos_r = self._fold + self._x_offset + 0.1 - ypos1 = -fold_num * (n_lines + 1) - ypos2 = -(fold_num + 1) * (n_lines) - fold_num + 1 + xpos_l = glob_data["x_offset"] - 0.01 + xpos_r = self._fold + glob_data["x_offset"] + 0.1 + ypos1 = -fold_num * (glob_data["n_lines"] + 1) + ypos2 = -(fold_num + 1) * (glob_data["n_lines"]) - fold_num + 1 if feedline_l: self._ax.plot( [xpos_l, xpos_l], @@ -706,14 +738,14 @@ def _draw_regs_wires(self, num_folds, xmax, n_lines, max_anc): zorder=PORDER_LINE, ) - # draw anchor index number + # draw index number if self._style["index"]: - for layer_num in range(max_anc): + for layer_num in range(max_x_index): if self._fold > 0: - x_coord = layer_num % self._fold + self._x_offset + 0.53 - y_coord = -(layer_num // self._fold) * (n_lines + 1) + 0.65 + x_coord = layer_num % self._fold + glob_data["x_offset"] + 0.53 + y_coord = -(layer_num // self._fold) * (glob_data["n_lines"] + 1) + 0.65 else: - x_coord = layer_num + self._x_offset + 0.53 + x_coord = layer_num + glob_data["x_offset"] + 0.53 y_coord = 0.65 self._ax.text( x_coord, @@ -721,23 +753,27 @@ def _draw_regs_wires(self, num_folds, xmax, n_lines, max_anc): str(layer_num + 1), ha="center", va="center", - fontsize=self._sfs, + fontsize=self._style["sfs"], color=self._style["tc"], clip_on=True, zorder=PORDER_TEXT, ) - def _draw_ops(self, verbose=False): + def _draw_ops( + self, nodes, node_data, wire_map, layer_widths, clbits_dict, glob_data, verbose=False + ): """Draw the gates in the circuit""" + prev_x_index = -1 - for i, layer in enumerate(self._nodes): - layer_width = self._layer_widths[i] - anc_x_index = prev_x_index + 1 + for layer in nodes: + l_width = [] + curr_x_index = prev_x_index + 1 # draw the gates in this layer for node in layer: op = node.op - self._get_colors(node) + + self._get_colors(node, node_data) if verbose: print(op) @@ -745,35 +781,40 @@ def _draw_ops(self, verbose=False): # add conditional if getattr(op, "condition", None): cond_xy = [ - self._c_anchors[ii].plot_coord(anc_x_index, layer_width, self._x_offset) - for ii in self._clbits_dict - ] - if self._clbits_dict: - anc_x_index = max( - anc_x_index, next(iter(self._c_anchors.items()))[1].get_x_index() + self._plot_coord( + curr_x_index, + clbits_dict[ii]["y"], + layer_widths[node][0], + glob_data, ) - self._condition(node, cond_xy) + for ii in clbits_dict + ] + if clbits_dict: + curr_x_index = max(curr_x_index, glob_data["next_x_index"]) + self._condition(node, node_data, wire_map, cond_xy, glob_data) # draw measure if isinstance(op, Measure): - self._measure(node) + self._measure(node, node_data, glob_data) # draw barriers, snapshots, etc. elif getattr(op, "_directive", False): if self._plot_barriers: - self._barrier(node) + self._barrier(node, node_data, glob_data) # draw single qubit gates - elif len(self._data[node]["q_xy"]) == 1 and not node.cargs: - self._gate(node) + elif len(node_data[node]["q_xy"]) == 1 and not node.cargs: + self._gate(node, node_data, glob_data) # draw controlled gates elif isinstance(op, ControlledGate): - self._control_gate(node) + self._control_gate(node, node_data, glob_data) # draw multi-qubit gate as final default else: - self._multiqubit_gate(node) + self._multiqubit_gate(node, node_data, glob_data) + + l_width.append(layer_widths[node][0]) # adjust the column if there have been barriers encountered, but not plotted barrier_offset = 0 @@ -783,15 +824,16 @@ def _draw_ops(self, verbose=False): -1 if all(getattr(nd.op, "_directive", False) for nd in layer) else 0 ) - prev_x_index = anc_x_index + layer_width + barrier_offset - 1 + prev_x_index = curr_x_index + max(l_width) + barrier_offset - 1 - def _get_colors(self, node): + def _get_colors(self, node, node_data): """Get all the colors needed for drawing the circuit""" + op = node.op base_name = None if not hasattr(op, "base_gate") else op.base_gate.name color = None - if self._data[node]["raw_gate_text"] in self._style["dispcol"]: - color = self._style["dispcol"][self._data[node]["raw_gate_text"]] + if node_data[node]["raw_gate_text"] in self._style["dispcol"]: + color = self._style["dispcol"][node_data[node]["raw_gate_text"]] elif op.name in self._style["dispcol"]: color = self._style["dispcol"][op.name] if color is not None: @@ -825,15 +867,16 @@ def _get_colors(self, node): lc = fc # Subtext needs to be same color as gate text sc = gt - self._data[node]["fc"] = fc - self._data[node]["ec"] = ec - self._data[node]["gt"] = gt - self._data[node]["tc"] = self._style["tc"] - self._data[node]["sc"] = sc - self._data[node]["lc"] = lc - - def _condition(self, node, cond_xy): + node_data[node]["fc"] = fc + node_data[node]["ec"] = ec + node_data[node]["gt"] = gt + node_data[node]["tc"] = self._style["tc"] + node_data[node]["sc"] = sc + node_data[node]["lc"] = lc + + def _condition(self, node, node_data, wire_map, cond_xy, glob_data): """Add a conditional to a gate""" + label, val_bits = get_condition_label_val( node.op.condition, self._circuit, self._cregbundle ) @@ -847,17 +890,17 @@ def _condition(self, node, cond_xy): # other cases, only one bit is shown. if not self._cregbundle and isinstance(cond_bit_reg, ClassicalRegister): for idx in range(cond_bit_reg.size): - cond_pos.append(cond_xy[self._wire_map[cond_bit_reg[idx]] - first_clbit]) + cond_pos.append(cond_xy[wire_map[cond_bit_reg[idx]] - first_clbit]) # If it's a register bit and cregbundle, need to use the register to find the location elif self._cregbundle and isinstance(cond_bit_reg, Clbit): register = get_bit_register(self._circuit, cond_bit_reg) if register is not None: - cond_pos.append(cond_xy[self._wire_map[register] - first_clbit]) + cond_pos.append(cond_xy[wire_map[register] - first_clbit]) else: - cond_pos.append(cond_xy[self._wire_map[cond_bit_reg] - first_clbit]) + cond_pos.append(cond_xy[wire_map[cond_bit_reg] - first_clbit]) else: - cond_pos.append(cond_xy[self._wire_map[cond_bit_reg] - first_clbit]) + cond_pos.append(cond_xy[wire_map[cond_bit_reg] - first_clbit]) xy_plot = [] for idx, xy in enumerate(cond_pos): @@ -869,7 +912,7 @@ def _condition(self, node, cond_xy): fc = self._style["lc"] else: fc = self._style["bg"] - box = self._patches_mod.Circle( + box = glob_data["patches_mod"].Circle( xy=xy, radius=WID * 0.15, fc=fc, @@ -879,7 +922,8 @@ def _condition(self, node, cond_xy): ) self._ax.add_patch(box) xy_plot.append(xy) - qubit_b = min(self._data[node]["q_xy"], key=lambda xy: xy[1]) + + qubit_b = min(node_data[node]["q_xy"], key=lambda xy: xy[1]) clbit_b = min(xy_plot, key=lambda xy: xy[1]) # display the label at the bottom of the lowest conditional and draw the double line @@ -892,31 +936,31 @@ def _condition(self, node, cond_xy): label, ha="center", va="top", - fontsize=self._sfs, + fontsize=self._style["sfs"], color=self._style["tc"], clip_on=True, zorder=PORDER_TEXT, ) self._line(qubit_b, clbit_b, lc=self._style["cc"], ls=self._style["cline"]) - def _measure(self, node): + def _measure(self, node, node_data, glob_data): """Draw the measure symbol and the line to the clbit""" - qx, qy = self._data[node]["q_xy"][0] - cx, cy = self._data[node]["c_xy"][0] + qx, qy = node_data[node]["q_xy"][0] + cx, cy = node_data[node]["c_xy"][0] register, _, reg_index = get_bit_reg_index(self._circuit, node.cargs[0]) # draw gate box - self._gate(node) + self._gate(node, node_data, glob_data) # add measure symbol - arc = self._patches_mod.Arc( + arc = glob_data["patches_mod"].Arc( xy=(qx, qy - 0.15 * HIG), width=WID * 0.7, height=HIG * 0.7, theta1=0, theta2=180, fill=False, - ec=self._data[node]["gt"], + ec=node_data[node]["gt"], linewidth=self._lwidth2, zorder=PORDER_GATE, ) @@ -924,18 +968,18 @@ def _measure(self, node): self._ax.plot( [qx, qx + 0.35 * WID], [qy - 0.15 * HIG, qy + 0.20 * HIG], - color=self._data[node]["gt"], + color=node_data[node]["gt"], linewidth=self._lwidth2, zorder=PORDER_GATE, ) # arrow self._line( - self._data[node]["q_xy"][0], + node_data[node]["q_xy"][0], [cx, cy + 0.35 * WID], lc=self._style["cc"], ls=self._style["cline"], ) - arrowhead = self._patches_mod.Polygon( + arrowhead = glob_data["patches_mod"].Polygon( ( (cx - 0.20 * WID, cy + 0.35 * WID), (cx + 0.20 * WID, cy + 0.35 * WID), @@ -953,15 +997,15 @@ def _measure(self, node): str(reg_index), ha="left", va="bottom", - fontsize=0.8 * self._fs, + fontsize=0.8 * self._style["fs"], color=self._style["tc"], clip_on=True, zorder=PORDER_TEXT, ) - def _barrier(self, node): + def _barrier(self, node, node_data, glob_data): """Draw a barrier""" - for i, xy in enumerate(self._data[node]["q_xy"]): + for i, xy in enumerate(node_data[node]["q_xy"]): xpos, ypos = xy # For the topmost barrier, reduce the rectangle if there's a label to allow for the text. if i == 0 and node.op.label is not None: @@ -976,7 +1020,7 @@ def _barrier(self, node): color=self._style["lc"], zorder=PORDER_TEXT, ) - box = self._patches_mod.Rectangle( + box = glob_data["patches_mod"].Rectangle( xy=(xpos - (0.3 * WID), ypos - 0.5), width=0.6 * WID, height=1.0 + ypos_adj, @@ -997,74 +1041,74 @@ def _barrier(self, node): node.op.label, ha="center", va="top", - fontsize=self._fs, - color=self._data[node]["tc"], + fontsize=self._style["fs"], + color=node_data[node]["tc"], clip_on=True, zorder=PORDER_TEXT, ) - def _gate(self, node, xy=None): + def _gate(self, node, node_data, glob_data, xy=None): """Draw a 1-qubit gate""" if xy is None: - xy = self._data[node]["q_xy"][0] + xy = node_data[node]["q_xy"][0] xpos, ypos = xy - wid = max(self._data[node]["width"], WID) + wid = max(node_data[node]["width"], WID) - box = self._patches_mod.Rectangle( + box = glob_data["patches_mod"].Rectangle( xy=(xpos - 0.5 * wid, ypos - 0.5 * HIG), width=wid, height=HIG, - fc=self._data[node]["fc"], - ec=self._data[node]["ec"], + fc=node_data[node]["fc"], + ec=node_data[node]["ec"], linewidth=self._lwidth15, zorder=PORDER_GATE, ) self._ax.add_patch(box) - if "gate_text" in self._data[node]: + if "gate_text" in node_data[node]: gate_ypos = ypos - if "param" in self._data[node] and self._data[node]["param"] != "": + if "param_text" in node_data[node] and node_data[node]["param_text"] != "": gate_ypos = ypos + 0.15 * HIG self._ax.text( xpos, ypos - 0.3 * HIG, - self._data[node]["param"], + node_data[node]["param_text"], ha="center", va="center", - fontsize=self._sfs, - color=self._data[node]["sc"], + fontsize=self._style["sfs"], + color=node_data[node]["sc"], clip_on=True, zorder=PORDER_TEXT, ) self._ax.text( xpos, gate_ypos, - self._data[node]["gate_text"], + node_data[node]["gate_text"], ha="center", va="center", - fontsize=self._fs, - color=self._data[node]["gt"], + fontsize=self._style["fs"], + color=node_data[node]["gt"], clip_on=True, zorder=PORDER_TEXT, ) - def _multiqubit_gate(self, node, xy=None): + def _multiqubit_gate(self, node, node_data, glob_data, xy=None): """Draw a gate covering more than one qubit""" op = node.op if xy is None: - xy = self._data[node]["q_xy"] + xy = node_data[node]["q_xy"] # Swap gate if isinstance(op, SwapGate): - self._swap(xy, node, self._data[node]["lc"]) + self._swap(xy, node, node_data, node_data[node]["lc"]) return # RZZ Gate elif isinstance(op, RZZGate): - self._symmetric_gate(node, RZZGate) + self._symmetric_gate(node, node_data, RZZGate, glob_data) return - c_xy = self._data[node]["c_xy"] + c_xy = node_data[node]["c_xy"] xpos = min(x[0] for x in xy) ypos = min(y[1] for y in xy) ypos_max = max(y[1] for y in xy) @@ -1073,16 +1117,17 @@ def _multiqubit_gate(self, node, xy=None): cypos = min(y[1] for y in c_xy) ypos = min(ypos, cypos) - wid = max(self._data[node]["width"] + 0.21, WID) + wid = max(node_data[node]["width"] + 0.21, WID) - qubit_span = abs(ypos) - abs(ypos_max) + 1 - height = HIG + (qubit_span - 1) - box = self._patches_mod.Rectangle( + qubit_span = abs(ypos) - abs(ypos_max) + height = HIG + qubit_span + + box = glob_data["patches_mod"].Rectangle( xy=(xpos - 0.5 * wid, ypos - 0.5 * HIG), width=wid, height=height, - fc=self._data[node]["fc"], - ec=self._data[node]["ec"], + fc=node_data[node]["fc"], + ec=node_data[node]["ec"], linewidth=self._lwidth15, zorder=PORDER_GATE, ) @@ -1096,8 +1141,8 @@ def _multiqubit_gate(self, node, xy=None): str(bit), ha="left", va="center", - fontsize=self._fs, - color=self._data[node]["gt"], + fontsize=self._style["fs"], + color=node_data[node]["gt"], clip_on=True, zorder=PORDER_TEXT, ) @@ -1110,43 +1155,43 @@ def _multiqubit_gate(self, node, xy=None): str(bit), ha="left", va="center", - fontsize=self._fs, - color=self._data[node]["gt"], + fontsize=self._style["fs"], + color=node_data[node]["gt"], clip_on=True, zorder=PORDER_TEXT, ) - if "gate_text" in self._data[node] and self._data[node]["gate_text"] != "": - gate_ypos = ypos + 0.5 * (qubit_span - 1) - if "param" in self._data[node] and self._data[node]["param"] != "": + if "gate_text" in node_data[node] and node_data[node]["gate_text"] != "": + gate_ypos = ypos + 0.5 * qubit_span + if "param_text" in node_data[node] and node_data[node]["param_text"] != "": gate_ypos = ypos + 0.4 * height self._ax.text( xpos + 0.11, ypos + 0.2 * height, - self._data[node]["param"], + node_data[node]["param_text"], ha="center", va="center", - fontsize=self._sfs, - color=self._data[node]["sc"], + fontsize=self._style["sfs"], + color=node_data[node]["sc"], clip_on=True, zorder=PORDER_TEXT, ) self._ax.text( xpos + 0.11, gate_ypos, - self._data[node]["gate_text"], + node_data[node]["gate_text"], ha="center", va="center", - fontsize=self._fs, - color=self._data[node]["gt"], + fontsize=self._style["fs"], + color=node_data[node]["gt"], clip_on=True, zorder=PORDER_TEXT, ) - def _control_gate(self, node): + def _control_gate(self, node, node_data, glob_data): """Draw a controlled gate""" op = node.op + xy = node_data[node]["q_xy"] base_type = None if not hasattr(op, "base_gate") else op.base_gate - xy = self._data[node]["q_xy"] qubit_b = min(xy, key=lambda xy: xy[1]) qubit_t = max(xy, key=lambda xy: xy[1]) num_ctrl_qubits = op.num_ctrl_qubits @@ -1155,32 +1200,33 @@ def _control_gate(self, node): op.ctrl_state, num_ctrl_qubits, xy, - ec=self._data[node]["ec"], - tc=self._data[node]["tc"], - text=self._data[node]["ctrl_text"], + glob_data, + ec=node_data[node]["ec"], + tc=node_data[node]["tc"], + text=node_data[node]["ctrl_text"], qargs=node.qargs, ) - self._line(qubit_b, qubit_t, lc=self._data[node]["lc"]) + self._line(qubit_b, qubit_t, lc=node_data[node]["lc"]) if isinstance(op, RZZGate) or isinstance(base_type, (U1Gate, PhaseGate, ZGate, RZZGate)): - self._symmetric_gate(node, base_type) + self._symmetric_gate(node, node_data, base_type, glob_data) elif num_qargs == 1 and isinstance(base_type, XGate): tgt_color = self._style["dispcol"]["target"] tgt = tgt_color if isinstance(tgt_color, str) else tgt_color[0] - self._x_tgt_qubit(xy[num_ctrl_qubits], ec=self._data[node]["ec"], ac=tgt) + self._x_tgt_qubit(xy[num_ctrl_qubits], glob_data, ec=node_data[node]["ec"], ac=tgt) elif num_qargs == 1: - self._gate(node, xy[num_ctrl_qubits:][0]) + self._gate(node, node_data, glob_data, xy[num_ctrl_qubits:][0]) elif isinstance(base_type, SwapGate): - self._swap(xy[num_ctrl_qubits:], node, self._data[node]["lc"]) + self._swap(xy[num_ctrl_qubits:], node, node_data, node_data[node]["lc"]) else: - self._multiqubit_gate(node, xy[num_ctrl_qubits:]) + self._multiqubit_gate(node, node_data, glob_data, xy[num_ctrl_qubits:]) def _set_ctrl_bits( - self, ctrl_state, num_ctrl_qubits, qbit, ec=None, tc=None, text="", qargs=None + self, ctrl_state, num_ctrl_qubits, qbit, glob_data, ec=None, tc=None, text="", qargs=None ): """Determine which qubits are controls and whether they are open or closed""" # place the control label at the top or bottom of controls @@ -1202,12 +1248,14 @@ def _set_ctrl_bits( text_top = True elif not top and qlist[i] == max_ctbit: text_top = False - self._ctrl_qubit(qbit[i], fc=fc_open_close, ec=ec, tc=tc, text=text, text_top=text_top) + self._ctrl_qubit( + qbit[i], glob_data, fc=fc_open_close, ec=ec, tc=tc, text=text, text_top=text_top + ) - def _ctrl_qubit(self, xy, fc=None, ec=None, tc=None, text="", text_top=None): + def _ctrl_qubit(self, xy, glob_data, fc=None, ec=None, tc=None, text="", text_top=None): """Draw a control circle and if top or bottom control, draw control label""" xpos, ypos = xy - box = self._patches_mod.Circle( + box = glob_data["patches_mod"].Circle( xy=(xpos, ypos), radius=WID * 0.15, fc=fc, @@ -1236,17 +1284,17 @@ def _ctrl_qubit(self, xy, fc=None, ec=None, tc=None, text="", text_top=None): text, ha="center", va="top", - fontsize=self._sfs, + fontsize=self._style["sfs"], color=tc, clip_on=True, zorder=PORDER_TEXT, ) - def _x_tgt_qubit(self, xy, ec=None, ac=None): + def _x_tgt_qubit(self, xy, glob_data, ec=None, ac=None): """Draw the cnot target symbol""" linewidth = self._lwidth2 xpos, ypos = xy - box = self._patches_mod.Circle( + box = glob_data["patches_mod"].Circle( xy=(xpos, ypos), radius=HIG * 0.35, fc=ec, @@ -1272,44 +1320,50 @@ def _x_tgt_qubit(self, xy, ec=None, ac=None): zorder=PORDER_GATE + 1, ) - def _symmetric_gate(self, node, base_type): + def _symmetric_gate(self, node, node_data, base_type, glob_data): """Draw symmetric gates for cz, cu1, cp, and rzz""" op = node.op - xy = self._data[node]["q_xy"] + xy = node_data[node]["q_xy"] qubit_b = min(xy, key=lambda xy: xy[1]) qubit_t = max(xy, key=lambda xy: xy[1]) base_type = None if not hasattr(op, "base_gate") else op.base_gate - ec = self._data[node]["ec"] - tc = self._data[node]["tc"] - lc = self._data[node]["lc"] + ec = node_data[node]["ec"] + tc = node_data[node]["tc"] + lc = node_data[node]["lc"] # cz and mcz gates if not isinstance(op, ZGate) and isinstance(base_type, ZGate): num_ctrl_qubits = op.num_ctrl_qubits - self._ctrl_qubit(xy[-1], fc=ec, ec=ec, tc=tc) + self._ctrl_qubit(xy[-1], glob_data, fc=ec, ec=ec, tc=tc) self._line(qubit_b, qubit_t, lc=lc, zorder=PORDER_LINE + 1) # cu1, cp, rzz, and controlled rzz gates (sidetext gates) elif isinstance(op, RZZGate) or isinstance(base_type, (U1Gate, PhaseGate, RZZGate)): num_ctrl_qubits = 0 if isinstance(op, RZZGate) else op.num_ctrl_qubits - gate_text = "P" if isinstance(base_type, PhaseGate) else self._data[node]["gate_text"] + gate_text = "P" if isinstance(base_type, PhaseGate) else node_data[node]["gate_text"] - self._ctrl_qubit(xy[num_ctrl_qubits], fc=ec, ec=ec, tc=tc) + self._ctrl_qubit(xy[num_ctrl_qubits], glob_data, fc=ec, ec=ec, tc=tc) if not isinstance(base_type, (U1Gate, PhaseGate)): - self._ctrl_qubit(xy[num_ctrl_qubits + 1], fc=ec, ec=ec, tc=tc) - - self._sidetext(node, qubit_b, tc=tc, text=f"{gate_text} ({self._data[node]['param']})") + self._ctrl_qubit(xy[num_ctrl_qubits + 1], glob_data, fc=ec, ec=ec, tc=tc) + + self._sidetext( + node, + node_data, + qubit_b, + tc=tc, + text=f"{gate_text} ({node_data[node]['param_text']})", + ) self._line(qubit_b, qubit_t, lc=lc) - def _swap(self, xy, node, color=None): + def _swap(self, xy, node, node_data, color=None): """Draw a Swap gate""" self._swap_cross(xy[0], color=color) self._swap_cross(xy[1], color=color) self._line(xy[0], xy[1], lc=color) # add calibration text - gate_text = self._data[node]["gate_text"].split("\n")[-1] - if self._data[node]["raw_gate_text"] in self._calibrations: + gate_text = node_data[node]["gate_text"].split("\n")[-1] + if node_data[node]["raw_gate_text"] in self._calibrations: xpos, ypos = xy[0] self._ax.text( xpos, @@ -1342,19 +1396,19 @@ def _swap_cross(self, xy, color=None): zorder=PORDER_LINE + 1, ) - def _sidetext(self, node, xy, tc=None, text=""): + def _sidetext(self, node, node_data, xy, tc=None, text=""): """Draw the sidetext for symmetric gates""" xpos, ypos = xy # 0.11 = the initial gap, add 1/2 text width to place on the right - xp = xpos + 0.11 + self._data[node]["width"] / 2 + xp = xpos + 0.11 + node_data[node]["width"] / 2 self._ax.text( xp, ypos + HIG, text, ha="center", va="top", - fontsize=self._sfs, + fontsize=self._style["sfs"], color=tc, clip_on=True, zorder=PORDER_TEXT, @@ -1397,33 +1451,18 @@ def _line(self, xy0, xy1, lc=None, ls=None, zorder=PORDER_LINE): zorder=zorder, ) - -class Anchor: - """Locate the anchors for the gates""" - - def __init__(self, num_wires, y_index, fold): - self._num_wires = num_wires - self._fold = fold - self._y_index = y_index - self._x_index = 0 - - def plot_coord(self, x_index, gate_width, x_offset): + def _plot_coord(self, x_index, y_index, gate_width, glob_data): """Get the coord positions for an index""" - h_pos = x_index % self._fold + 1 # check folding - if self._fold > 0: - if h_pos + (gate_width - 1) > self._fold: - x_index += self._fold - (h_pos - 1) - x_pos = x_index % self._fold + 0.5 * gate_width + 0.04 - y_pos = self._y_index - (x_index // self._fold) * (self._num_wires + 1) - else: - x_pos = x_index + 0.5 * gate_width + 0.04 - y_pos = self._y_index + fold = self._fold if self._fold > 0 else INFINITE_FOLD + h_pos = x_index % fold + 1 - # could have been updated, so need to store - self._x_index = x_index - return x_pos + x_offset, y_pos + if h_pos + (gate_width - 1) > fold: + x_index += fold - (h_pos - 1) + x_pos = x_index % fold + glob_data["x_offset"] + 0.04 + x_pos += 0.5 * gate_width + y_pos = y_index - (x_index // fold) * (glob_data["n_lines"] + 1) - def get_x_index(self): - """Getter for the x index""" - return self._x_index + # could have been updated, so need to store + glob_data["next_x_index"] = x_index + return x_pos, y_pos From 924702e9ddb3f6f0124bc8acb285ac3d1da12aff Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Wed, 21 Jun 2023 13:56:53 +0100 Subject: [PATCH 156/172] Update minimum Python version for `pylint` (#10318) This got missed during 4dfef130, and is needed to avoid Pylint complaining about standard-library features that were added after the minimum version it thinks we're on. --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9a9663779a56..f7f8ec4c9286 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,7 @@ extension-pkg-allow-list = [ "tweedledum", ] load-plugins = ["pylint.extensions.docparams", "pylint.extensions.docstyle"] -py-version = "3.7" # update it when bumping minimum supported python version +py-version = "3.8" # update it when bumping minimum supported python version [tool.pylint.basic] good-names = ["a", "b", "i", "j", "k", "d", "n", "m", "ex", "v", "w", "x", "y", "z", "Run", "_", "logger", "q", "c", "r", "qr", "cr", "qc", "nd", "pi", "op", "b", "ar", "br", "p", "cp", "ax", "dt", "__unittest", "iSwapGate", "mu"] From fbd64d95d9002315819c5c59d9cfd1d53de5bddb Mon Sep 17 00:00:00 2001 From: Naoki Kanazawa Date: Fri, 23 Jun 2023 19:51:12 +0900 Subject: [PATCH 157/172] Pass manager refactoring: add passmanager module (#10124) * Add passmanager module and reorganize transpiler module * Documentation update Co-authored-by: Matthew Treinish * Readd qiskit.transpiler.propertyset * Documentation update Co-authored-by: Matthew Treinish Co-authored-by: John Lapeyre * BasePass -> GenericPass to avoid name overlap --------- Co-authored-by: Matthew Treinish Co-authored-by: John Lapeyre --- docs/apidocs/passmanager.rst | 6 + docs/apidocs/terra.rst | 1 + qiskit/passmanager/__init__.py | 106 +++++ qiskit/passmanager/base_pass.py | 80 ++++ qiskit/passmanager/exceptions.py | 19 + qiskit/passmanager/flow_controllers.py | 158 ++++++++ qiskit/passmanager/passmanager.py | 268 +++++++++++++ qiskit/passmanager/passrunner.py | 250 ++++++++++++ qiskit/passmanager/propertyset.py | 24 ++ qiskit/transpiler/__init__.py | 17 +- qiskit/transpiler/basepasses.py | 53 +-- qiskit/transpiler/exceptions.py | 3 +- qiskit/transpiler/passmanager.py | 237 ++++------- qiskit/transpiler/propertyset.py | 9 +- qiskit/transpiler/runningpassmanager.py | 377 ++++++------------ ...d-passmanager-module-3ae30cff52cb83f1.yaml | 26 ++ 16 files changed, 1154 insertions(+), 480 deletions(-) create mode 100644 docs/apidocs/passmanager.rst create mode 100644 qiskit/passmanager/__init__.py create mode 100644 qiskit/passmanager/base_pass.py create mode 100644 qiskit/passmanager/exceptions.py create mode 100644 qiskit/passmanager/flow_controllers.py create mode 100644 qiskit/passmanager/passmanager.py create mode 100644 qiskit/passmanager/passrunner.py create mode 100644 qiskit/passmanager/propertyset.py create mode 100644 releasenotes/notes/add-passmanager-module-3ae30cff52cb83f1.yaml diff --git a/docs/apidocs/passmanager.rst b/docs/apidocs/passmanager.rst new file mode 100644 index 000000000000..f11794df3f3f --- /dev/null +++ b/docs/apidocs/passmanager.rst @@ -0,0 +1,6 @@ +.. _qiskit-passmanager: + +.. automodule:: qiskit.passmanager + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/apidocs/terra.rst b/docs/apidocs/terra.rst index 48e8fb3f27b0..c78a22f68678 100644 --- a/docs/apidocs/terra.rst +++ b/docs/apidocs/terra.rst @@ -17,6 +17,7 @@ Qiskit Terra API Reference assembler dagcircuit extensions + passmanager providers_basicaer providers providers_fake_provider diff --git a/qiskit/passmanager/__init__.py b/qiskit/passmanager/__init__.py new file mode 100644 index 000000000000..24f94292bea6 --- /dev/null +++ b/qiskit/passmanager/__init__.py @@ -0,0 +1,106 @@ +# 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. + +""" +======================================= +Passmanager (:mod:`qiskit.passmanager`) +======================================= + +.. currentmodule:: qiskit.passmanager + +Overview +======== + +Qiskit pass manager is somewhat inspired by the `LLVM compiler `_, +but it is designed to take Qiskit object as an input instead of plain source code. + +The pass manager converts the input object into an intermediate representation (IR), +and it can be optimized and get lowered with a variety of transformations over multiple passes. +This representation must be preserved throughout the transformation. +The passes may consume the hardware constraints that Qiskit backend may provide. +Finally, the IR is converted back to some Qiskit object. +Note that the input type and output type don't need to match. + +Execution of passes is managed by the :class:`.FlowController`, +which is initialized with a set of transform and analysis passes and provides an iterator of them. +This iterator can be conditioned on the :class:`.PropertySet`, which is a namespace +storing the intermediate data necessary for the transformation. +A pass has read and write access to the property set namespace, +and the stored data is shared among scheduled passes. + +The :class:`BasePassManager` provides a user interface to build and execute transform passes. +It internally spawns a :class:`BasePassRunner` instance to apply transforms to +the input object. In this sense, the pass manager itself is unaware of the +underlying IR, but it is indirectly tied to a particular IR through the pass runner class. + +The responsibilities of the pass runner are the following: + +* Defining the input type / pass manager IR / output type. +* Converting an input object to a particular pass manager IR. +* Preparing own property set namespace. +* Running scheduled flow controllers to apply a series of transformations to the IR. +* Converting the IR back to an output object. + +A single pass runner always takes a single input object and returns a single output object. +Parallelism for multiple input objects is supported by the :class:`BasePassManager` by +broadcasting the pass runner via the :mod:`qiskit.tools.parallel_map` function. + +The base class :class:`BasePassRunner` doesn't define any associated type by itself, +and a developer needs to implement a subclass for a particular object type to optimize. +This `veil of ignorance` allows us to choose the most efficient data representation +for a particular optimization task, while we can reuse the pass flow control machinery +for different input and output types. + + +Base classes +------------ + +.. autosummary:: + :toctree: ../stubs/ + + BasePassRunner + BasePassManager + +Flow controllers +---------------- + +.. autosummary:: + :toctree: ../stubs/ + + FlowController + ConditionalController + DoWhileController + +PropertySet +----------- + +.. autosummary:: + :toctree: ../stubs/ + + PropertySet + +Exceptions +---------- + +.. autosummary:: + :toctree: ../stubs/ + + PassManagerError + +""" + +from .passrunner import BasePassRunner +from .passmanager import BasePassManager +from .flow_controllers import FlowController, ConditionalController, DoWhileController +from .base_pass import GenericPass +from .propertyset import PropertySet +from .exceptions import PassManagerError diff --git a/qiskit/passmanager/base_pass.py b/qiskit/passmanager/base_pass.py new file mode 100644 index 000000000000..f6cc27fc9b30 --- /dev/null +++ b/qiskit/passmanager/base_pass.py @@ -0,0 +1,80 @@ +# 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. + +"""Metaclass of Qiskit pass manager pass.""" + +from abc import abstractmethod +from collections.abc import Hashable +from inspect import signature +from typing import Any + +from .propertyset import PropertySet + + +class MetaPass(type): + """Metaclass for transpiler passes. + + Enforces the creation of some fields in the pass while allowing passes to + override ``__init__``. + """ + + def __call__(cls, *args, **kwargs): + pass_instance = type.__call__(cls, *args, **kwargs) + pass_instance._hash = hash(MetaPass._freeze_init_parameters(cls, args, kwargs)) + return pass_instance + + @staticmethod + def _freeze_init_parameters(class_, args, kwargs): + self_guard = object() + init_signature = signature(class_.__init__) + bound_signature = init_signature.bind(self_guard, *args, **kwargs) + arguments = [("class_.__name__", class_.__name__)] + for name, value in bound_signature.arguments.items(): + if value == self_guard: + continue + if isinstance(value, Hashable): + arguments.append((name, type(value), value)) + else: + arguments.append((name, type(value), repr(value))) + return frozenset(arguments) + + +class GenericPass(metaclass=MetaPass): + """Generic pass class with IR-agnostic minimal functionality.""" + + def __init__(self): + self.requires = [] # List of passes that requires + self.preserves = [] # List of passes that preserves + self.property_set = PropertySet() # This pass's pointer to the pass manager's property set. + self._hash = hash(None) + + def __hash__(self): + return self._hash + + def __eq__(self, other): + return hash(self) == hash(other) + + def name(self): + """Return the name of the pass.""" + return self.__class__.__name__ + + @abstractmethod + def run(self, passmanager_ir: Any): + """Run a pass on the pass manager IR. This is implemented by the pass developer. + + Args: + passmanager_ir: the dag on which the pass is run. + + Raises: + NotImplementedError: when this is left unimplemented for a pass. + """ + raise NotImplementedError diff --git a/qiskit/passmanager/exceptions.py b/qiskit/passmanager/exceptions.py new file mode 100644 index 000000000000..c1bca0563d7a --- /dev/null +++ b/qiskit/passmanager/exceptions.py @@ -0,0 +1,19 @@ +# 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. + +"""Qiskit pass manager exceptions.""" + +from qiskit.exceptions import QiskitError + + +class PassManagerError(QiskitError): + """Pass manager error.""" diff --git a/qiskit/passmanager/flow_controllers.py b/qiskit/passmanager/flow_controllers.py new file mode 100644 index 000000000000..6fe795222ac8 --- /dev/null +++ b/qiskit/passmanager/flow_controllers.py @@ -0,0 +1,158 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2019, 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. + +"""Pass flow controllers to provide pass iterator conditioned on the property set.""" +from __future__ import annotations +from collections import OrderedDict +from collections.abc import Sequence +from typing import Union, List +import logging + +from .base_pass import GenericPass +from .exceptions import PassManagerError + +logger = logging.getLogger(__name__) + + +class FlowController: + """Base class for multiple types of working list. + + This class is a base class for multiple types of working list. When you iterate on it, it + returns the next pass to run. + """ + + registered_controllers = OrderedDict() + + def __init__(self, passes, options, **partial_controller): + self._passes = passes + self.passes = FlowController.controller_factory(passes, options, **partial_controller) + self.options = options + + def __iter__(self): + yield from self.passes + + def dump_passes(self): + """Fetches the passes added to this flow controller. + + Returns: + dict: {'options': self.options, 'passes': [passes], 'type': type(self)} + """ + # TODO remove + ret = {"options": self.options, "passes": [], "type": type(self)} + for pass_ in self._passes: + if isinstance(pass_, FlowController): + ret["passes"].append(pass_.dump_passes()) + else: + ret["passes"].append(pass_) + return ret + + @classmethod + def add_flow_controller(cls, name, controller): + """Adds a flow controller. + + Args: + name (string): Name of the controller to add. + controller (type(FlowController)): The class implementing a flow controller. + """ + cls.registered_controllers[name] = controller + + @classmethod + def remove_flow_controller(cls, name): + """Removes a flow controller. + + Args: + name (string): Name of the controller to remove. + Raises: + KeyError: If the controller to remove was not registered. + """ + if name not in cls.registered_controllers: + raise KeyError("Flow controller not found: %s" % name) + del cls.registered_controllers[name] + + @classmethod + def controller_factory( + cls, + passes: Sequence[GenericPass | "FlowController"], + options: dict, + **partial_controller, + ): + """Constructs a flow controller based on the partially evaluated controller arguments. + + Args: + passes: passes to add to the flow controller. + options: PassManager options. + **partial_controller: Partially evaluated controller arguments in the form `{name:partial}` + + Raises: + PassManagerError: When partial_controller is not well-formed. + + Returns: + FlowController: A FlowController instance. + """ + if None in partial_controller.values(): + raise PassManagerError("The controller needs a condition.") + + if partial_controller: + for registered_controller in cls.registered_controllers.keys(): + if registered_controller in partial_controller: + return cls.registered_controllers[registered_controller]( + passes, options, **partial_controller + ) + raise PassManagerError("The controllers for %s are not registered" % partial_controller) + + return FlowControllerLinear(passes, options) + + +class FlowControllerLinear(FlowController): + """The basic controller runs the passes one after the other.""" + + def __init__(self, passes, options): # pylint: disable=super-init-not-called + self.passes = self._passes = passes + self.options = options + + +class DoWhileController(FlowController): + """Implements a set of passes in a do-while loop.""" + + def __init__(self, passes, options=None, do_while=None, **partial_controller): + self.do_while = do_while + self.max_iteration = options["max_iteration"] if options else 1000 + super().__init__(passes, options, **partial_controller) + + def __iter__(self): + for _ in range(self.max_iteration): + yield from self.passes + + if not self.do_while(): + return + + raise PassManagerError("Maximum iteration reached. max_iteration=%i" % self.max_iteration) + + +class ConditionalController(FlowController): + """Implements a set of passes under a certain condition.""" + + def __init__(self, passes, options=None, condition=None, **partial_controller): + self.condition = condition + super().__init__(passes, options, **partial_controller) + + def __iter__(self): + if self.condition(): + yield from self.passes + + +# Alias to a sequence of all kind of pass elements +PassSequence = Union[Union[GenericPass, FlowController], List[Union[GenericPass, FlowController]]] + +# Default controllers +FlowController.add_flow_controller("condition", ConditionalController) +FlowController.add_flow_controller("do_while", DoWhileController) diff --git a/qiskit/passmanager/passmanager.py b/qiskit/passmanager/passmanager.py new file mode 100644 index 000000000000..935a3afb3918 --- /dev/null +++ b/qiskit/passmanager/passmanager.py @@ -0,0 +1,268 @@ +# 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. + +"""Manager for a set of Passes and their scheduling during transpilation.""" +from __future__ import annotations +from abc import ABC +from collections.abc import Callable, Sequence +from typing import Any +import logging +import dill +from qiskit.tools.parallel import parallel_map + +from .base_pass import GenericPass +from .passrunner import BasePassRunner +from .exceptions import PassManagerError +from .flow_controllers import FlowController, PassSequence + +logger = logging.getLogger(__name__) + + +class BasePassManager(ABC): + """Pass manager base class.""" + + PASS_RUNNER = BasePassRunner + + def __init__( + self, + passes: PassSequence | None = None, + max_iteration: int = 1000, + ): + """Initialize an empty pass manager object. + + Args: + passes: A pass set to be added to the pass manager schedule. + max_iteration: The maximum number of iterations the schedule will be looped if the + condition is not met. + """ + # the pass manager's schedule of passes, including any control-flow. + # Populated via PassManager.append(). + self._pass_sets: list[dict[str, Any]] = [] + self.max_iteration = max_iteration + + if passes is not None: + self.append(passes) + + def append( + self, + passes: PassSequence, + **flow_controller_conditions: Callable, + ) -> None: + """Append a Pass Set to the schedule of passes. + + Args: + passes: A set of passes (a pass set) to be added to schedule. A pass set is a list of + passes that are controlled by the same flow controller. If a single pass is + provided, the pass set will only have that pass a single element. + It is also possible to append a + :class:`~qiskit.transpiler.runningpassmanager.FlowController` instance and the + rest of the parameter will be ignored. + flow_controller_conditions: Dictionary of control flow plugins. Default: + + * do_while (callable property_set -> boolean): The passes repeat until the + callable returns False. + Default: `lambda x: False # i.e. passes run once` + + * condition (callable property_set -> boolean): The passes run only if the + callable returns True. + Default: `lambda x: True # i.e. passes run` + """ + passes = self._normalize_passes(passes) + self._pass_sets.append({"passes": passes, "flow_controllers": flow_controller_conditions}) + + def replace( + self, + index: int, + passes: PassSequence, + **flow_controller_conditions: Any, + ) -> None: + """Replace a particular pass in the scheduler. + + Args: + index: Pass index to replace, based on the position in passes(). + passes: A pass set (as defined in :py:func:`qiskit.transpiler.PassManager.append`) + to be added to the pass manager schedule. + flow_controller_conditions: control flow plugins. + + Raises: + PassManagerError: if a pass in passes is not a proper pass or index not found. + """ + passes = self._normalize_passes(passes) + + try: + self._pass_sets[index] = { + "passes": passes, + "flow_controllers": flow_controller_conditions, + } + except IndexError as ex: + raise PassManagerError(f"Index to replace {index} does not exists") from ex + + def remove(self, index: int) -> None: + """Removes a particular pass in the scheduler. + + Args: + index: Pass index to replace, based on the position in passes(). + + Raises: + PassManagerError: if the index is not found. + """ + try: + del self._pass_sets[index] + except IndexError as ex: + raise PassManagerError(f"Index to replace {index} does not exists") from ex + + def __setitem__(self, index, item): + self.replace(index, item) + + def __len__(self): + return len(self._pass_sets) + + def __getitem__(self, index): + new_passmanager = self.__class__(max_iteration=self.max_iteration) + _pass_sets = self._pass_sets[index] + if isinstance(_pass_sets, dict): + _pass_sets = [_pass_sets] + new_passmanager._pass_sets = _pass_sets + return new_passmanager + + def __add__(self, other): + if isinstance(other, self.__class__): + new_passmanager = self.__class__(max_iteration=self.max_iteration) + new_passmanager._pass_sets = self._pass_sets + other._pass_sets + return new_passmanager + else: + try: + new_passmanager = self.__class__(max_iteration=self.max_iteration) + new_passmanager._pass_sets += self._pass_sets + new_passmanager.append(other) + return new_passmanager + except PassManagerError as ex: + raise TypeError( + "unsupported operand type + for %s and %s" % (self.__class__, other.__class__) + ) from ex + + def _normalize_passes( + self, + passes: PassSequence, + ) -> Sequence[GenericPass | FlowController] | FlowController: + if isinstance(passes, FlowController): + return passes + if isinstance(passes, GenericPass): + passes = [passes] + for pass_ in passes: + if isinstance(pass_, FlowController): + # Normalize passes in nested FlowController. + # TODO: Internal renormalisation should be the responsibility of the + # `FlowController`, but the separation between `FlowController`, + # `RunningPassManager` and `PassManager` is so muddled right now, it would be better + # to do this as part of more top-down refactoring. ---Jake, 2022-10-03. + pass_.passes = self._normalize_passes(pass_.passes) + elif not isinstance(pass_, GenericPass): + raise PassManagerError( + "%s is not a pass or FlowController instance " % pass_.__class__ + ) + return passes + + def run( + self, + in_programs: Any, + callback: Callable | None = None, + **metadata, + ) -> Any: + """Run all the passes on the specified ``circuits``. + + Args: + in_programs: Input programs to transform via all the registered passes. + callback: A callback function that will be called after each pass execution. The + function will be called with 5 keyword arguments:: + + pass_ (Pass): the pass being run + passmanager_ir (Any): depending on pass manager subclass + time (float): the time to execute the pass + property_set (PropertySet): the property set + count (int): the index for the pass execution + + The exact arguments pass expose the internals of the pass + manager and are subject to change as the pass manager internals + change. If you intend to reuse a callback function over + multiple releases be sure to check that the arguments being + passed are the same. + + To use the callback feature you define a function that will + take in kwargs dict and access the variables. For example:: + + def callback_func(**kwargs): + pass_ = kwargs['pass_'] + dag = kwargs['dag'] + time = kwargs['time'] + property_set = kwargs['property_set'] + count = kwargs['count'] + ... + + metadata: Metadata which might be attached to output program. + + Returns: + The transformed program(s). + """ + if not self._pass_sets and not metadata and callback is None: + return in_programs + + is_list = True + if isinstance(in_programs, self.PASS_RUNNER.IN_PROGRAM_TYPE): + in_programs = [in_programs] + is_list = False + + if len(in_programs) == 1: + out_program = self._run_single_circuit(in_programs[0], callback, **metadata) + if is_list: + return [out_program] + return out_program + + # TODO support for List(output_name) and List(callback) + del metadata + del callback + + return self._run_several_circuits(in_programs) + + def _create_running_passmanager(self) -> BasePassRunner: + # Must be implemented by followup PR. + # BasePassRunner.append assumes normalized pass input, which is not pass_sets. + raise NotImplementedError + + def _run_single_circuit( + self, + input_program: Any, + callback: Callable | None = None, + **metadata, + ) -> Any: + pass_runner = self._create_running_passmanager() + return pass_runner.run(input_program, callback=callback, **metadata) + + def _run_several_circuits( + self, + input_programs: Sequence[Any], + ) -> Any: + # Pass runner 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 runner without state collision, without building it per thread. + return parallel_map( + self._in_parallel, input_programs, task_kwargs={"pm_dill": dill.dumps(self)} + ) + + @staticmethod + def _in_parallel( + in_program: Any, + pm_dill: bytes = None, + ) -> Any: + pass_runner = dill.loads(pm_dill)._create_running_passmanager() + return pass_runner.run(in_program) diff --git a/qiskit/passmanager/passrunner.py b/qiskit/passmanager/passrunner.py new file mode 100644 index 000000000000..9ce983ce6591 --- /dev/null +++ b/qiskit/passmanager/passrunner.py @@ -0,0 +1,250 @@ +# 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. + +"""Pass runner to apply transformation on passmanager IR.""" +from __future__ import annotations +import logging +import time +from abc import ABC, abstractmethod +from functools import partial +from collections.abc import Callable +from typing import Any + +from .base_pass import GenericPass +from .exceptions import PassManagerError +from .flow_controllers import FlowController, ConditionalController, DoWhileController +from .propertyset import PropertySet + +logger = logging.getLogger(__name__) + +# NoneType is removed from types module in < Python3.10. +NoneType = type(None) + + +class BasePassRunner(ABC): + """Pass runner base class.""" + + IN_PROGRAM_TYPE = NoneType + OUT_PROGRAM_TYPE = NoneType + IR_TYPE = NoneType + + def __init__(self, max_iteration: int): + """Initialize an empty pass runner object. + + Args: + max_iteration: The schedule looping iterates until the condition is met or until + max_iteration is reached. + """ + self.callback = None + self.count = None + self.metadata = None + + # the pass manager's schedule of passes, including any control-flow. + # Populated via PassManager.append(). + self.working_list = [] + + # global property set is the context of the circuit held by the pass manager + # as it runs through its scheduled passes. The flow controller + # have read-only access (via the fenced_property_set). + self.property_set = PropertySet() + + # passes already run that have not been invalidated + self.valid_passes = set() + + # pass manager's overriding options for the passes it runs (for debugging) + self.passmanager_options = {"max_iteration": max_iteration} + + def append( + self, + flow_controller: FlowController, + ): + """Append a flow controller to the schedule of controllers. + + Args: + flow_controller: A normalized flow controller instance. + """ + # We assume flow controller is already normalized. + self.working_list.append(flow_controller) + + @abstractmethod + def _to_passmanager_ir(self, in_program): + """Convert input program into pass manager IR. + + Args: + in_program: Input program. + + Returns: + Pass manager IR. + """ + pass + + @abstractmethod + def _to_target(self, passmanager_ir): + """Convert pass manager IR into output program. + + Args: + passmanager_ir: Pass manager IR after optimization. + + Returns: + Output program. + """ + pass + + @abstractmethod + def _run_base_pass( + self, + pass_: GenericPass, + passmanager_ir: Any, + ) -> Any: + """Do a single base pass. + + Args: + pass_: A base pass to run. + passmanager_ir: Pass manager IR. + + Returns: + Pass manager IR with optimization. + """ + pass + + def _run_pass_generic( + self, + pass_sequence: GenericPass | FlowController, + passmanager_ir: Any, + options: dict[str, Any] | None = None, + ) -> Any: + """Do either base pass or single flow controller. + + Args: + pass_sequence: Base pass or flow controller to run. + passmanager_ir: Pass manager IR. + options: PassManager options. + + Returns: + Pass manager IR with optimization. + + Raises: + PassManagerError: When pass_sequence is not a valid class. + TypeError: When IR type changed during transformation. + """ + if isinstance(pass_sequence, GenericPass): + # First, do the requirements of this pass + for required_pass in pass_sequence.requires: + passmanager_ir = self._run_pass_generic( + pass_sequence=required_pass, + passmanager_ir=passmanager_ir, + options=options, + ) + # Run the pass itself, if not already run + if pass_sequence not in self.valid_passes: + start_time = time.time() + try: + passmanager_ir = self._run_base_pass( + pass_=pass_sequence, + passmanager_ir=passmanager_ir, + ) + finally: + run_time = time.time() - start_time + log_msg = f"Pass: {pass_sequence.name()} - {run_time * 1000:.5f} (ms)" + logger.info(log_msg) + if self.callback: + self.callback( + pass_=pass_sequence, + passmanager_ir=passmanager_ir, + time=run_time, + property_set=self.property_set, + count=self.count, + ) + self.count += 1 + self._update_valid_passes(pass_sequence) + if not isinstance(passmanager_ir, self.IR_TYPE): + raise TypeError( + f"A transformed object {passmanager_ir} is not valid IR in this pass manager. " + "Object representation type must be preserved during transformation. " + f"The pass {pass_sequence.name()} returns invalid object." + ) + return passmanager_ir + + if isinstance(pass_sequence, FlowController): + # This will be removed in followup PR. Code is temporary. + fenced_property_set = getattr(self, "fenced_property_set") + + if isinstance(pass_sequence, ConditionalController) and not isinstance( + pass_sequence.condition, partial + ): + pass_sequence.condition = partial(pass_sequence.condition, fenced_property_set) + if isinstance(pass_sequence, DoWhileController) and not isinstance( + pass_sequence.do_while, partial + ): + pass_sequence.do_while = partial(pass_sequence.do_while, fenced_property_set) + for pass_ in pass_sequence: + passmanager_ir = self._run_pass_generic( + pass_sequence=pass_, + passmanager_ir=passmanager_ir, + options=pass_sequence.options, + ) + return passmanager_ir + + raise PassManagerError( + f"{pass_sequence.__class__} is not a valid base pass nor flow controller." + ) + + def run( + self, + in_program: Any, + callback: Callable | None = None, + **metadata, + ) -> Any: + """Run all the passes on an input program. + + Args: + in_program: Input program to compile via all the registered passes. + callback: A callback function that will be called after each pass execution. + **metadata: Metadata attached to the output program. + + Returns: + Compiled or optimized program. + + Raises: + TypeError: When input or output object is unexpected type. + """ + if not isinstance(in_program, self.IN_PROGRAM_TYPE): + raise TypeError( + f"Input object {in_program} is not valid type for this pass manager. " + f"This pass manager accepts {self.IN_PROGRAM_TYPE}." + ) + + if callback: + self.callback = callback + self.count = 0 + self.metadata = metadata + + passmanager_ir = self._to_passmanager_ir(in_program) + del in_program + + for controller in self.working_list: + passmanager_ir = self._run_pass_generic( + pass_sequence=controller, + passmanager_ir=passmanager_ir, + options=self.passmanager_options, + ) + out_program = self._to_target(passmanager_ir) + + if not isinstance(out_program, self.OUT_PROGRAM_TYPE): + raise TypeError( + f"Output object {out_program} is not valid type for this pass manager. " + f"This pass manager must return {self.OUT_PROGRAM_TYPE}." + ) + return out_program + + def _update_valid_passes(self, pass_): + self.valid_passes.add(pass_) diff --git a/qiskit/passmanager/propertyset.py b/qiskit/passmanager/propertyset.py new file mode 100644 index 000000000000..d13eeb7c0032 --- /dev/null +++ b/qiskit/passmanager/propertyset.py @@ -0,0 +1,24 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2017, 2018, 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. + +"""A property set is maintained by the pass runner. + + +This is sort of shared memory space among passes. +""" + + +class PropertySet(dict): + """A default dictionary-like object""" + + def __missing__(self, key): + return None diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py index 76cd6fc3d6f4..3d32c911566f 100644 --- a/qiskit/transpiler/__init__.py +++ b/qiskit/transpiler/__init__.py @@ -1197,10 +1197,6 @@ StagedPassManager PassManager PassManagerConfig - PropertySet - FlowController - ConditionalController - DoWhileController Layout and Topology ------------------- @@ -1248,11 +1244,16 @@ TranspilerAccessError """ -from .runningpassmanager import FlowController, ConditionalController, DoWhileController -from .passmanager import PassManager +# For backward compatibility +from qiskit.passmanager import ( + FlowController, + ConditionalController, + DoWhileController, +) + +from .passmanager import PassManager, StagedPassManager from .passmanager_config import PassManagerConfig -from .passmanager import StagedPassManager -from .propertyset import PropertySet +from .propertyset import PropertySet # pylint: disable=no-name-in-module from .exceptions import TranspilerError, TranspilerAccessError from .fencedobjs import FencedDAGCircuit, FencedPropertySet from .basepasses import AnalysisPass, TransformationPass diff --git a/qiskit/transpiler/basepasses.py b/qiskit/transpiler/basepasses.py index f54fb8f27ab9..46b10b6510da 100644 --- a/qiskit/transpiler/basepasses.py +++ b/qiskit/transpiler/basepasses.py @@ -13,61 +13,18 @@ """Base transpiler passes.""" from abc import abstractmethod -from collections.abc import Hashable -from inspect import signature -from .propertyset import PropertySet -from .layout import TranspileLayout - - -class MetaPass(type): - """Metaclass for transpiler passes. - Enforces the creation of some fields in the pass while allowing passes to - override ``__init__``. - """ +from qiskit.passmanager.base_pass import GenericPass +from qiskit.passmanager.propertyset import PropertySet - def __call__(cls, *args, **kwargs): - pass_instance = type.__call__(cls, *args, **kwargs) - pass_instance._hash = hash(MetaPass._freeze_init_parameters(cls, args, kwargs)) - return pass_instance - - @staticmethod - def _freeze_init_parameters(class_, args, kwargs): - self_guard = object() - init_signature = signature(class_.__init__) - bound_signature = init_signature.bind(self_guard, *args, **kwargs) - arguments = [("class_.__name__", class_.__name__)] - for name, value in bound_signature.arguments.items(): - if value == self_guard: - continue - if isinstance(value, Hashable): - arguments.append((name, type(value), value)) - else: - arguments.append((name, type(value), repr(value))) - return frozenset(arguments) +from .layout import TranspileLayout -class BasePass(metaclass=MetaPass): +class BasePass(GenericPass): """Base class for transpiler passes.""" - def __init__(self): - self.requires = [] # List of passes that requires - self.preserves = [] # List of passes that preserves - self.property_set = PropertySet() # This pass's pointer to the pass manager's property set. - self._hash = hash(None) - - def __hash__(self): - return self._hash - - def __eq__(self, other): - return hash(self) == hash(other) - - def name(self): - """Return the name of the pass.""" - return self.__class__.__name__ - @abstractmethod - def run(self, dag): + def run(self, dag): # pylint: disable=arguments-differ """Run a pass on the DAGCircuit. This is implemented by the pass developer. Args: diff --git a/qiskit/transpiler/exceptions.py b/qiskit/transpiler/exceptions.py index c30aadedf173..ef79603bfed2 100644 --- a/qiskit/transpiler/exceptions.py +++ b/qiskit/transpiler/exceptions.py @@ -14,9 +14,10 @@ Exception for errors raised by the transpiler. """ from qiskit.exceptions import QiskitError +from qiskit.passmanager.exceptions import PassManagerError -class TranspilerAccessError(QiskitError): +class TranspilerAccessError(PassManagerError): """DEPRECATED: Exception of access error in the transpiler passes.""" diff --git a/qiskit/transpiler/passmanager.py b/qiskit/transpiler/passmanager.py index 03019e872ff4..0221f429cad7 100644 --- a/qiskit/transpiler/passmanager.py +++ b/qiskit/transpiler/passmanager.py @@ -12,26 +12,34 @@ """Manager for a set of Passes and their scheduling during transpilation.""" from __future__ import annotations +import inspect import io import re +from functools import wraps from collections.abc import Iterator, Iterable, Callable, Sequence from typing import Union, List, Any -import dill - -from qiskit.tools.parallel import parallel_map from qiskit.circuit import QuantumCircuit +from qiskit.passmanager import BasePassManager +from qiskit.passmanager.flow_controllers import PassSequence, FlowController +from qiskit.passmanager.exceptions import PassManagerError from .basepasses import BasePass from .exceptions import TranspilerError -from .runningpassmanager import RunningPassManager, FlowController +from .runningpassmanager import RunningPassManager _CircuitsT = Union[List[QuantumCircuit], QuantumCircuit] -class PassManager: +class PassManager(BasePassManager): """Manager for a set of Passes and their scheduling during transpilation.""" - def __init__(self, passes: BasePass | list[BasePass] | None = None, max_iteration: int = 1000): + PASS_RUNNER = RunningPassManager + + def __init__( + self, + passes: PassSequence | None = None, + max_iteration: int = 1000, + ): """Initialize an empty `PassManager` object (with no passes scheduled). Args: @@ -40,18 +48,12 @@ def __init__(self, passes: BasePass | list[BasePass] | None = None, max_iteratio max_iteration: The maximum number of iterations the schedule will be looped if the condition is not met. """ - # the pass manager's schedule of passes, including any control-flow. - # Populated via PassManager.append(). - - self._pass_sets: list[dict[str, Any]] = [] - if passes is not None: - self.append(passes) - self.max_iteration = max_iteration + super().__init__(passes, max_iteration) self.property_set = None def append( self, - passes: BasePass | Sequence[BasePass | FlowController], + passes: PassSequence, max_iteration: int = None, **flow_controller_conditions: Any, ) -> None: @@ -65,26 +67,28 @@ def append( :class:`~qiskit.transpiler.runningpassmanager.FlowController` instance and the rest of the parameter will be ignored. max_iteration: max number of iterations of passes. - flow_controller_conditions: control flow plugins. + flow_controller_conditions: Dictionary of control flow plugins. Default: + + * do_while (callable property_set -> boolean): The passes repeat until the + callable returns False. + Default: `lambda x: False # i.e. passes run once` + + * condition (callable property_set -> boolean): The passes run only if the + callable returns True. + Default: `lambda x: True # i.e. passes run` Raises: TranspilerError: if a pass in passes is not a proper pass. - - See Also: - ``RunningPassManager.add_flow_controller()`` for more information about the control - flow plugins. """ if max_iteration: # TODO remove this argument from append self.max_iteration = max_iteration - - passes = PassManager._normalize_passes(passes) - self._pass_sets.append({"passes": passes, "flow_controllers": flow_controller_conditions}) + super().append(passes, **flow_controller_conditions) def replace( self, index: int, - passes: BasePass | list[BasePass], + passes: PassSequence, max_iteration: int = None, **flow_controller_conditions: Any, ) -> None: @@ -99,91 +103,13 @@ def replace( Raises: TranspilerError: if a pass in passes is not a proper pass or index not found. - - See Also: - ``RunningPassManager.add_flow_controller()`` for more information about the control - flow plugins. """ if max_iteration: # TODO remove this argument from append self.max_iteration = max_iteration + super().replace(index, passes, **flow_controller_conditions) - passes = PassManager._normalize_passes(passes) - - try: - self._pass_sets[index] = { - "passes": passes, - "flow_controllers": flow_controller_conditions, - } - except IndexError as ex: - raise TranspilerError(f"Index to replace {index} does not exists") from ex - - def remove(self, index: int) -> None: - """Removes a particular pass in the scheduler. - - Args: - index: Pass index to replace, based on the position in passes(). - - Raises: - TranspilerError: if the index is not found. - """ - try: - del self._pass_sets[index] - except IndexError as ex: - raise TranspilerError(f"Index to replace {index} does not exists") from ex - - def __setitem__(self, index, item): - self.replace(index, item) - - def __len__(self): - return len(self._pass_sets) - - def __getitem__(self, index): - new_passmanager = PassManager(max_iteration=self.max_iteration) - _pass_sets = self._pass_sets[index] - if isinstance(_pass_sets, dict): - _pass_sets = [_pass_sets] - new_passmanager._pass_sets = _pass_sets - return new_passmanager - - def __add__(self, other): - if isinstance(other, PassManager): - new_passmanager = PassManager(max_iteration=self.max_iteration) - new_passmanager._pass_sets = self._pass_sets + other._pass_sets - return new_passmanager - else: - try: - new_passmanager = PassManager(max_iteration=self.max_iteration) - new_passmanager._pass_sets += self._pass_sets - new_passmanager.append(other) - return new_passmanager - except TranspilerError as ex: - raise TypeError( - f"unsupported operand type + for {self.__class__} and {other.__class__}" - ) from ex - - @staticmethod - def _normalize_passes( - passes: BasePass | Sequence[BasePass | FlowController] | FlowController, - ) -> Sequence[BasePass | FlowController] | FlowController: - if isinstance(passes, FlowController): - return passes - if isinstance(passes, BasePass): - passes = [passes] - for pass_ in passes: - if isinstance(pass_, FlowController): - # Normalize passes in nested FlowController. - # TODO: Internal renormalisation should be the responsibility of the - # `FlowController`, but the separation between `FlowController`, - # `RunningPassManager` and `PassManager` is so muddled right now, it would be better - # to do this as part of more top-down refactoring. ---Jake, 2022-10-03. - pass_.passes = PassManager._normalize_passes(pass_.passes) - elif not isinstance(pass_, BasePass): - raise TranspilerError( - "%s is not a BasePass or FlowController instance " % pass_.__class__ - ) - return passes - + # pylint: disable=arguments-differ def run( self, circuits: _CircuitsT, @@ -225,73 +151,30 @@ def callback_func(**kwargs): Returns: The transformed circuit(s). """ - if not self._pass_sets and output_name is None and callback is None: - return circuits - if isinstance(circuits, QuantumCircuit): - return self._run_single_circuit(circuits, output_name, callback) - if len(circuits) == 1: - return [self._run_single_circuit(circuits[0], output_name, callback)] - return self._run_several_circuits(circuits, output_name, callback) + return super().run( + in_programs=circuits, + callback=callback, + output_name=output_name, + ) def _create_running_passmanager(self) -> RunningPassManager: - running_passmanager = RunningPassManager(self.max_iteration) + running_passmanager = self.PASS_RUNNER(self.max_iteration) for pass_set in self._pass_sets: running_passmanager.append(pass_set["passes"], **pass_set["flow_controllers"]) return running_passmanager - @staticmethod - def _in_parallel(circuit, pm_dill=None) -> QuantumCircuit: - """Task used by the parallel map tools from ``_run_several_circuits``.""" - running_passmanager = dill.loads(pm_dill)._create_running_passmanager() - result = running_passmanager.run(circuit) - return result - - def _run_several_circuits( - self, - circuits: List[QuantumCircuit], - output_name: str | None = None, - callback: Callable | None = None, - ) -> List[QuantumCircuit]: - """Run all the passes on the specified ``circuits``. - - Args: - circuits: Circuits to transform via all the registered passes. - output_name: The output circuit name. If ``None``, it will be set to the same as the - input circuit name. - callback: A callback function that will be called after each pass execution. - - Returns: - The transformed circuits. - """ - # TODO support for List(output_name) and List(callback) - del output_name - del callback - - return parallel_map( - PassManager._in_parallel, circuits, task_kwargs={"pm_dill": dill.dumps(self)} - ) - def _run_single_circuit( self, - circuit: QuantumCircuit, - output_name: str | None = None, + input_program: QuantumCircuit, callback: Callable | None = None, + **metadata, ) -> QuantumCircuit: - """Run all the passes on a ``circuit``. - - Args: - circuit: Circuit to transform via all the registered passes. - output_name: The output circuit name. If ``None``, it will be set to the same as the - input circuit name. - callback: A callback function that will be called after each pass execution. + pass_runner = self._create_running_passmanager() + out_program = pass_runner.run(input_program, callback=callback, **metadata) + # Store property set of pass runner for backward compatibility + self.property_set = pass_runner.property_set - Returns: - The transformed circuit. - """ - running_passmanager = self._create_running_passmanager() - result = running_passmanager.run(circuit, output_name=output_name, callback=callback) - self.property_set = running_passmanager.property_set - return result + return out_program def draw(self, filename=None, style=None, raw=False): """Draw the pass manager. @@ -507,7 +390,15 @@ def remove(self, index: int) -> None: def __getitem__(self, index): self._update_passmanager() - return super().__getitem__(index) + + # Do not inherit from the PassManager, i.e. super() + # It returns instance of self.__class__ which is StagedPassManager. + new_passmanager = PassManager(max_iteration=self.max_iteration) + _pass_sets = self._pass_sets[index] + if isinstance(_pass_sets, dict): + _pass_sets = [_pass_sets] + new_passmanager._pass_sets = _pass_sets + return new_passmanager def __len__(self): self._update_passmanager() @@ -541,3 +432,29 @@ def draw(self, filename=None, style=None, raw=False): from qiskit.visualization import staged_pass_manager_drawer return staged_pass_manager_drawer(self, filename=filename, style=style, raw=raw) + + +# A temporary error handling with slight overhead at class loading. +# This method wraps all class methods to replace PassManagerError with TranspilerError. +# The pass flow controller mechanics raises PassManagerError, as it has been moved to base class. +# PassManagerError is not caught by TranspilerError due to the hierarchy. + + +def _replace_error(meth): + @wraps(meth) + def wrapper(*meth_args, **meth_kwargs): + try: + return meth(*meth_args, **meth_kwargs) + except PassManagerError as ex: + raise TranspilerError(ex.message) from ex + + return wrapper + + +for _name, _method in inspect.getmembers(PassManager, predicate=inspect.isfunction): + if _name.startswith("_"): + # Ignore protected and private. + # User usually doesn't directly execute and catch error from these methods. + continue + _wrapped = _replace_error(_method) + setattr(PassManager, _name, _wrapped) diff --git a/qiskit/transpiler/propertyset.py b/qiskit/transpiler/propertyset.py index 9cba8fc886e3..6244a49d2c28 100644 --- a/qiskit/transpiler/propertyset.py +++ b/qiskit/transpiler/propertyset.py @@ -14,8 +14,9 @@ about the current state of the circuit """ -class PropertySet(dict): - """A default dictionary-like object""" +from qiskit.passmanager import propertyset as passmanager_propertyset - def __missing__(self, key): - return None + +def __getattr__(name): + # Just redirect to new module. This will be deprecated. + return getattr(passmanager_propertyset, name) diff --git a/qiskit/transpiler/runningpassmanager.py b/qiskit/transpiler/runningpassmanager.py index 6290ed4d52bc..aa574414667a 100644 --- a/qiskit/transpiler/runningpassmanager.py +++ b/qiskit/transpiler/runningpassmanager.py @@ -13,57 +13,57 @@ """RunningPassManager class for the transpiler. This object holds the state of a pass manager during running-time.""" from __future__ import annotations -from functools import partial -from collections import OrderedDict import logging -from time import time +import inspect +from functools import partial, wraps +from typing import Callable -from qiskit.dagcircuit import DAGCircuit +from qiskit.circuit import QuantumCircuit from qiskit.converters import circuit_to_dag, dag_to_circuit +from qiskit.dagcircuit import DAGCircuit +from qiskit.passmanager import BasePassRunner +from qiskit.passmanager.flow_controllers import ( + PassSequence, + FlowController, + DoWhileController, + ConditionalController, +) +from qiskit.passmanager.exceptions import PassManagerError from qiskit.transpiler.basepasses import BasePass -from .propertyset import PropertySet -from .fencedobjs import FencedPropertySet, FencedDAGCircuit from .exceptions import TranspilerError +from .fencedobjs import FencedPropertySet, FencedDAGCircuit from .layout import TranspileLayout logger = logging.getLogger(__name__) -class RunningPassManager: +class RunningPassManager(BasePassRunner): """A RunningPassManager is a running pass manager.""" - def __init__(self, max_iteration): + IN_PROGRAM_TYPE = QuantumCircuit + OUT_PROGRAM_TYPE = QuantumCircuit + IR_TYPE = DAGCircuit + + def __init__(self, max_iteration: int): """Initialize an empty PassManager object (with no passes scheduled). Args: - max_iteration (int): The schedule looping iterates until the condition is met or until + max_iteration: The schedule looping iterates until the condition is met or until max_iteration is reached. """ - self.callback = None - # the pass manager's schedule of passes, including any control-flow. - # Populated via PassManager.append(). - self.working_list = [] - - # global property set is the context of the circuit held by the pass manager - # as it runs through its scheduled passes. The flow controller - # have read-only access (via the fenced_property_set). - self.property_set = PropertySet() + super().__init__(max_iteration) self.fenced_property_set = FencedPropertySet(self.property_set) - # passes already run that have not been invalidated - self.valid_passes = set() - - # pass manager's overriding options for the passes it runs (for debugging) - self.passmanager_options = {"max_iteration": max_iteration} - - self.count = 0 - - def append(self, passes: list[BasePass], **flow_controller_conditions): - """Append a Pass to the schedule of passes. + def append( + self, + passes: PassSequence, + **flow_controller_conditions, + ): + """Append a passes to the schedule of passes. Args: - passes (list[TBasePass]): passes to be added to schedule - flow_controller_conditions (kwargs): See add_flow_controller(): Dictionary of + passes: passes to be added to schedule + flow_controller_conditions: See add_flow_controller(): Dictionary of control flow plugins. Default: * do_while (callable property_set -> boolean): The passes repeat until the @@ -73,26 +73,19 @@ def append(self, passes: list[BasePass], **flow_controller_conditions): * condition (callable property_set -> boolean): The passes run only if the callable returns True. Default: `lambda x: True # i.e. passes run` - - Raises: - TranspilerError: if a pass in passes is not a proper pass. """ # attaches the property set to the controller so it has access to it. if isinstance(passes, ConditionalController): passes.condition = partial(passes.condition, self.fenced_property_set) - self.working_list.append(passes) - if isinstance(passes, DoWhileController): + elif isinstance(passes, DoWhileController): if not isinstance(passes.do_while, partial): passes.do_while = partial(passes.do_while, self.fenced_property_set) - self.working_list.append(passes) else: flow_controller_conditions = self._normalize_flow_controller(flow_controller_conditions) - self.working_list.append( - FlowController.controller_factory( - passes, self.passmanager_options, **flow_controller_conditions - ) + passes = FlowController.controller_factory( + passes, self.passmanager_options, **flow_controller_conditions ) - pass + super().append(passes) def _normalize_flow_controller(self, flow_controller): for name, param in flow_controller.items(): @@ -102,33 +95,18 @@ def _normalize_flow_controller(self, flow_controller): raise TranspilerError("The flow controller parameter %s is not callable" % name) return flow_controller - def run(self, circuit, output_name=None, callback=None): - """Run all the passes on a QuantumCircuit + def _to_passmanager_ir(self, in_program: QuantumCircuit) -> DAGCircuit: + if not isinstance(in_program, QuantumCircuit): + raise TranspilerError(f"Input {in_program.__class__} is not QuantumCircuit.") + return circuit_to_dag(in_program) - Args: - circuit (QuantumCircuit): circuit to transform via all the registered passes - output_name (str): The output circuit name. If not given, the same as the - input circuit - callback (callable): A callback function that will be called after each pass execution. - Returns: - QuantumCircuit: Transformed circuit. - """ - name = circuit.name - dag = circuit_to_dag(circuit) - del circuit - - if callback: - self.callback = callback + def _to_target(self, passmanager_ir: DAGCircuit) -> QuantumCircuit: + if not isinstance(passmanager_ir, DAGCircuit): + raise TranspilerError(f"Input {passmanager_ir.__class__} is not DAGCircuit.") - for passset in self.working_list: - for pass_ in passset: - dag = self._do_pass(pass_, dag, passset.options) + circuit = dag_to_circuit(passmanager_ir, copy_operations=False) + circuit.name = self.metadata["output_name"] - circuit = dag_to_circuit(dag, copy_operations=False) - if output_name: - circuit.name = output_name - else: - circuit.name = name if self.property_set["layout"] is not None: circuit._layout = TranspileLayout( initial_layout=self.property_set["layout"], @@ -144,237 +122,118 @@ def run(self, circuit, output_name=None, callback=None): # also converted into list with the same ordering with circuit.data. topological_start_times = [] start_times = self.property_set["node_start_time"] - for dag_node in dag.topological_op_nodes(): + for dag_node in passmanager_ir.topological_op_nodes(): topological_start_times.append(start_times[dag_node]) circuit._op_start_times = topological_start_times return circuit - def _do_pass(self, pass_, dag, options): - """Do either a pass and its "requires" or FlowController. + # pylint: disable=arguments-differ + def run( + self, + circuit: QuantumCircuit, + output_name: str = None, + callback: Callable = None, + ) -> QuantumCircuit: + """Run all the passes on a QuantumCircuit Args: - pass_ (BasePass or FlowController): Pass to do. - dag (DAGCircuit): The dag on which the pass is ran. - options (dict): PassManager options. + circuit: Circuit to transform via all the registered passes. + output_name: The output circuit name. If not given, the same as the input circuit. + callback: A callback function that will be called after each pass execution. + Returns: - DAGCircuit: The transformed dag in case of a transformation pass. - The same input dag in case of an analysis pass. - Raises: - TranspilerError: If the pass is not a proper pass instance. + QuantumCircuit: Transformed circuit. """ - if isinstance(pass_, BasePass): - # First, do the requires of pass_ - for required_pass in pass_.requires: - dag = self._do_pass(required_pass, dag, options) - - # Run the pass itself, if not already run - if pass_ not in self.valid_passes: - dag = self._run_this_pass(pass_, dag) - - # update the valid_passes property - self._update_valid_passes(pass_) - - # if provided a nested flow controller - elif isinstance(pass_, FlowController): - - if isinstance(pass_, ConditionalController) and not isinstance( - pass_.condition, partial - ): - pass_.condition = partial(pass_.condition, self.fenced_property_set) + return super().run( + in_program=circuit, + callback=_rename_callback_args(callback), + output_name=output_name or circuit.name, + ) + + def _run_base_pass( + self, + pass_: BasePass, + passmanager_ir: DAGCircuit, + ) -> DAGCircuit: + """Do either a pass and its "requires" or FlowController. - elif isinstance(pass_, DoWhileController) and not isinstance(pass_.do_while, partial): - pass_.do_while = partial(pass_.do_while, self.fenced_property_set) + Args: + pass_: A base pass to run. + passmanager_ir: Pass manager IR, i.e. DAGCircuit for this class. - for _pass in pass_: - dag = self._do_pass(_pass, dag, pass_.options) - else: - raise TranspilerError( - "Expecting type BasePass or FlowController, got %s." % type(pass_) - ) - return dag + Returns: + The transformed dag in case of a transformation pass. + The same input dag in case of an analysis pass. - def _run_this_pass(self, pass_, dag): + Raises: + TranspilerError: When transform pass returns non DAGCircuit. + TranspilerError: When pass is neither transform pass nor analysis pass. + """ pass_.property_set = self.property_set + if pass_.is_transformation_pass: # Measure time if we have a callback or logging set - start_time = time() - new_dag = pass_.run(dag) - end_time = time() - run_time = end_time - start_time - # Execute the callback function if one is set - if self.callback: - self.callback( - pass_=pass_, - dag=new_dag, - time=run_time, - property_set=self.property_set, - count=self.count, - ) - self.count += 1 - self._log_pass(start_time, end_time, pass_.name()) + new_dag = pass_.run(passmanager_ir) if isinstance(new_dag, DAGCircuit): - new_dag.calibrations = dag.calibrations + new_dag.calibrations = passmanager_ir.calibrations else: raise TranspilerError( "Transformation passes should return a transformed dag." "The pass %s is returning a %s" % (type(pass_).__name__, type(new_dag)) ) - dag = new_dag + passmanager_ir = new_dag elif pass_.is_analysis_pass: # Measure time if we have a callback or logging set - start_time = time() - pass_.run(FencedDAGCircuit(dag)) - end_time = time() - run_time = end_time - start_time - # Execute the callback function if one is set - if self.callback: - self.callback( - pass_=pass_, - dag=dag, - time=run_time, - property_set=self.property_set, - count=self.count, - ) - self.count += 1 - self._log_pass(start_time, end_time, pass_.name()) + pass_.run(FencedDAGCircuit(passmanager_ir)) else: raise TranspilerError("I dont know how to handle this type of pass") - return dag - - def _log_pass(self, start_time, end_time, name): - log_msg = f"Pass: {name} - {(end_time - start_time) * 1000:.5f} (ms)" - logger.info(log_msg) + return passmanager_ir def _update_valid_passes(self, pass_): - self.valid_passes.add(pass_) + super()._update_valid_passes(pass_) if not pass_.is_analysis_pass: # Analysis passes preserve all self.valid_passes.intersection_update(set(pass_.preserves)) -class FlowController: - """Base class for multiple types of working list. - - This class is a base class for multiple types of working list. When you iterate on it, it - returns the next pass to run. - """ - - registered_controllers = OrderedDict() - - def __init__(self, passes, options, **partial_controller): - self._passes = passes - self.passes = FlowController.controller_factory(passes, options, **partial_controller) - self.options = options - - def __iter__(self): - yield from self.passes - - def dump_passes(self): - """Fetches the passes added to this flow controller. - - Returns: - dict: {'options': self.options, 'passes': [passes], 'type': type(self)} - """ - # TODO remove - ret = {"options": self.options, "passes": [], "type": type(self)} - for pass_ in self._passes: - if isinstance(pass_, FlowController): - ret["passes"].append(pass_.dump_passes()) - else: - ret["passes"].append(pass_) - return ret - - @classmethod - def add_flow_controller(cls, name, controller): - """Adds a flow controller. - - Args: - name (string): Name of the controller to add. - controller (type(FlowController)): The class implementing a flow controller. - """ - cls.registered_controllers[name] = controller - - @classmethod - def remove_flow_controller(cls, name): - """Removes a flow controller. - - Args: - name (string): Name of the controller to remove. - Raises: - KeyError: If the controller to remove was not registered. - """ - if name not in cls.registered_controllers: - raise KeyError("Flow controller not found: %s" % name) - del cls.registered_controllers[name] - - @classmethod - def controller_factory(cls, passes: list[BasePass], options, **partial_controller): - """Constructs a flow controller based on the partially evaluated controller arguments. - - Args: - passes (list[TBasePass]): passes to add to the flow controller. - options (dict): PassManager options. - **partial_controller (dict): Partially evaluated controller arguments in the form - `{name:partial}` - - Raises: - TranspilerError: When partial_controller is not well-formed. - - Returns: - FlowController: A FlowController instance. - """ - if None in partial_controller.values(): - raise TranspilerError("The controller needs a condition.") - - if partial_controller: - for registered_controller in cls.registered_controllers.keys(): - if registered_controller in partial_controller: - return cls.registered_controllers[registered_controller]( - passes, options, **partial_controller - ) - raise TranspilerError("The controllers for %s are not registered" % partial_controller) - - return FlowControllerLinear(passes, options) - - -class FlowControllerLinear(FlowController): - """The basic controller runs the passes one after the other.""" - - def __init__(self, passes, options): # pylint: disable=super-init-not-called - self.passes = self._passes = passes - self.options = options - - -class DoWhileController(FlowController): - """Implements a set of passes in a do-while loop.""" - - def __init__(self, passes, options=None, do_while=None, **partial_controller): - self.do_while = do_while - self.max_iteration = options["max_iteration"] if options else 1000 - super().__init__(passes, options, **partial_controller) +def _rename_callback_args(callback): + """A helper function to run callback with conventional argument names.""" + if callback is None: + return callback - def __iter__(self): - for _ in range(self.max_iteration): - yield from self.passes + def _call_with_dag(pass_, passmanager_ir, time, property_set, count): + callback( + pass_=pass_, + dag=passmanager_ir, + time=time, + property_set=property_set, + count=count, + ) - if not self.do_while(): - return + return _call_with_dag - raise TranspilerError("Maximum iteration reached. max_iteration=%i" % self.max_iteration) +# A temporary error handling with slight overhead at class loading. +# This method wraps all class methods to replace PassManagerError with TranspilerError. +# The pass flow controller mechanics raises PassManagerError, as it has been moved to base class. +# PassManagerError is not caught by TranspilerError due to the hierarchy. -class ConditionalController(FlowController): - """Implements a set of passes under a certain condition.""" - def __init__(self, passes, options=None, condition=None, **partial_controller): - self.condition = condition - super().__init__(passes, options, **partial_controller) +def _replace_error(meth): + @wraps(meth) + def wrapper(*meth_args, **meth_kwargs): + try: + return meth(*meth_args, **meth_kwargs) + except PassManagerError as ex: + raise TranspilerError(ex.message) from ex - def __iter__(self): - if self.condition(): - yield from self.passes + return wrapper -# Default controllers -FlowController.add_flow_controller("condition", ConditionalController) -FlowController.add_flow_controller("do_while", DoWhileController) +for _name, _method in inspect.getmembers(RunningPassManager, predicate=inspect.isfunction): + if _name.startswith("_"): + # Ignore protected and private. + # User usually doesn't directly execute and catch error from these methods. + continue + _wrapped = _replace_error(_method) + setattr(RunningPassManager, _name, _wrapped) diff --git a/releasenotes/notes/add-passmanager-module-3ae30cff52cb83f1.yaml b/releasenotes/notes/add-passmanager-module-3ae30cff52cb83f1.yaml new file mode 100644 index 000000000000..a3df013fcf40 --- /dev/null +++ b/releasenotes/notes/add-passmanager-module-3ae30cff52cb83f1.yaml @@ -0,0 +1,26 @@ +--- +features: + - | + A new module :mod:`qiskit.passmanager` is added. + This module implements a generic pass manager and flow controllers, + and provides an infrastructure to manage execution of transform passes. + The pass manager is a baseclass and not aware of the input and output object types, + and you need to create a subclass of the pass manager + for a particular program data to optimize. + The :mod:`qiskit.transpiler` module is also reorganized to rebuild the existing + quantum circuit pass manager based off of new generic pass manager. + See upgrade notes for more details. +upgrade: + - | + :class:`qiskit.transpiler.PassManager` is now a subclass of + :class:`qiskit.passmanager.BasePassManager`. There is no functional modification + due to this class hierarchy change. + - | + New error baseclass :class:`~qiskit.passmanager.PassManagerError` is introduced. + This will replace :class:`~qiskit.transpiler.TranspilerError` raised in the + pass handling machinery. The TranspilerError is now only used for the errors + related to the failure in handling the quantum circuit or DAG circuit object. + Note that the TranspilerError can be caught by the PassManagerError + because of their class hierarchy. For backward compatibility, + :class:`qiskit.transpiler.PassManager` catches PassManagerError and + re-raises the TranspilerError. This error replacement will be dropped in future. From b9b85646eca5b8b27b8818f3fd9bc058a6903308 Mon Sep 17 00:00:00 2001 From: Lev Bishop <18673315+levbishop@users.noreply.github.com> Date: Sun, 25 Jun 2023 15:42:08 -0400 Subject: [PATCH 158/172] pylint enable used-before-assignment (#10335) --- pyproject.toml | 1 - qiskit/circuit/library/standard_gates/x.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f7f8ec4c9286..c24e90cdc702 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -105,7 +105,6 @@ disable = [ "use-list-literal", "use-implicit-booleaness-not-comparison", "use-maxsplit-arg", - "used-before-assignment", ] enable = [ diff --git a/qiskit/circuit/library/standard_gates/x.py b/qiskit/circuit/library/standard_gates/x.py index 659b6ffb4942..bbc5d91815c8 100644 --- a/qiskit/circuit/library/standard_gates/x.py +++ b/qiskit/circuit/library/standard_gates/x.py @@ -575,9 +575,9 @@ def qasm(self): # exporter code (low priority), or we would need to modify 'qelib1.inc' which would be # needlessly disruptive this late in OQ2's lifecycle. The current OQ2 exporter _always_ # outputs the `include 'qelib1.inc' line. ---Jake, 2022-11-21. + old_name = self.name + self.name = "c3sqrtx" try: - old_name = self.name - self.name = "c3sqrtx" return super().qasm() finally: self.name = old_name From d7a7146379ed6b069ddda8d20b05f95e5647ff10 Mon Sep 17 00:00:00 2001 From: Lev Bishop <18673315+levbishop@users.noreply.github.com> Date: Sun, 25 Jun 2023 15:43:28 -0400 Subject: [PATCH 159/172] Pylint: enable pointless-exception-statement (#10336) --- pyproject.toml | 1 - qiskit/pulse/transforms/alignments.py | 4 +++- qiskit/qpy/binary_io/schedules.py | 2 +- qiskit/quantum_info/operators/scalar_op.py | 2 +- qiskit/visualization/pulse_v2/plotters/matplotlib.py | 2 +- qiskit/visualization/timeline/layouts.py | 4 ++-- qiskit/visualization/timeline/plotters/matplotlib.py | 2 +- 7 files changed, 9 insertions(+), 8 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c24e90cdc702..8b28ebb93c36 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -89,7 +89,6 @@ disable = [ "no-value-for-parameter", "non-ascii-name", "not-context-manager", - "pointless-exception-statement", "superfluous-parens", "unknown-option-value", "unexpected-keyword-arg", diff --git a/qiskit/pulse/transforms/alignments.py b/qiskit/pulse/transforms/alignments.py index c4cac6d4b782..d19def4f34b8 100644 --- a/qiskit/pulse/transforms/alignments.py +++ b/qiskit/pulse/transforms/alignments.py @@ -409,7 +409,9 @@ 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: - PulseError("Invalid schedule position t=%d is specified at index=%d" % (_t0, ind)) + raise PulseError( + "Invalid schedule position t=%d is specified at index=%d" % (_t0, ind) + ) aligned.insert(_t0, child, inplace=True) return aligned diff --git a/qiskit/qpy/binary_io/schedules.py b/qiskit/qpy/binary_io/schedules.py index 3ca41722694d..6577e7cec3f8 100644 --- a/qiskit/qpy/binary_io/schedules.py +++ b/qiskit/qpy/binary_io/schedules.py @@ -421,7 +421,7 @@ def read_schedule_block(file_obj, version, metadata_deserializer=None): QiskitError: QPY version is earlier than block support. """ if version < 5: - QiskitError(f"QPY version {version} does not support ScheduleBlock.") + raise QiskitError(f"QPY version {version} does not support ScheduleBlock.") data = formats.SCHEDULE_BLOCK_HEADER._make( struct.unpack( diff --git a/qiskit/quantum_info/operators/scalar_op.py b/qiskit/quantum_info/operators/scalar_op.py index 22bce0ab7026..141189e01653 100644 --- a/qiskit/quantum_info/operators/scalar_op.py +++ b/qiskit/quantum_info/operators/scalar_op.py @@ -47,7 +47,7 @@ def __init__(self, dims=None, coeff=1): QiskitError: If the optional coefficient is invalid. """ if not isinstance(coeff, Number): - QiskitError(f"coeff {coeff} must be a number.") + raise QiskitError(f"coeff {coeff} must be a number.") self._coeff = coeff super().__init__(input_dims=dims, output_dims=dims) diff --git a/qiskit/visualization/pulse_v2/plotters/matplotlib.py b/qiskit/visualization/pulse_v2/plotters/matplotlib.py index d86e169901a3..e92a34189994 100644 --- a/qiskit/visualization/pulse_v2/plotters/matplotlib.py +++ b/qiskit/visualization/pulse_v2/plotters/matplotlib.py @@ -118,7 +118,7 @@ def draw(self): ) self.ax.add_patch(box) else: - VisualizationError( + raise VisualizationError( "Data {name} is not supported " "by {plotter}".format(name=data, plotter=self.__class__.__name__) ) diff --git a/qiskit/visualization/timeline/layouts.py b/qiskit/visualization/timeline/layouts.py index 7bd599fb7430..5c5737c6e1f4 100644 --- a/qiskit/visualization/timeline/layouts.py +++ b/qiskit/visualization/timeline/layouts.py @@ -79,7 +79,7 @@ def qreg_creg_ascending(bits: List[types.Bits]) -> List[types.Bits]: elif isinstance(bit, circuit.Clbit): cregs.append(bit) else: - VisualizationError(f"Unknown bit {bit} is provided.") + raise VisualizationError(f"Unknown bit {bit} is provided.") with warnings.catch_warnings(): warnings.simplefilter("ignore") @@ -109,7 +109,7 @@ def qreg_creg_descending(bits: List[types.Bits]) -> List[types.Bits]: elif isinstance(bit, circuit.Clbit): cregs.append(bit) else: - VisualizationError(f"Unknown bit {bit} is provided.") + raise VisualizationError(f"Unknown bit {bit} is provided.") qregs = sorted(qregs, key=lambda x: x.index, reverse=True) cregs = sorted(cregs, key=lambda x: x.index, reverse=True) diff --git a/qiskit/visualization/timeline/plotters/matplotlib.py b/qiskit/visualization/timeline/plotters/matplotlib.py index f71d72c5ef13..126d0981feda 100644 --- a/qiskit/visualization/timeline/plotters/matplotlib.py +++ b/qiskit/visualization/timeline/plotters/matplotlib.py @@ -131,7 +131,7 @@ def draw(self): self.ax.plot(xvals.repeat(len(offsets)), offsets, **data.styles) else: - VisualizationError( + raise VisualizationError( "Data {name} is not supported by {plotter}" "".format(name=data, plotter=self.__class__.__name__) ) From d67fc2c07a1191f90e2a9313c8487468b7e60ddb Mon Sep 17 00:00:00 2001 From: Junya Nakamura <47435718+junnaka51@users.noreply.github.com> Date: Mon, 26 Jun 2023 22:12:04 +0900 Subject: [PATCH 160/172] Deprecate pulse Call instruction (#10247) * deprecate pulse Call instruction * Update releasenotes/notes/deprecate-pulse-Call-instruction-538802d8fad7e257.yaml Co-authored-by: Naoki Kanazawa * delete spaces in the releasenote --------- Co-authored-by: Naoki Kanazawa --- qiskit/pulse/instructions/call.py | 6 ++++ ...lse-Call-instruction-538802d8fad7e257.yaml | 9 +++++ test/python/pulse/test_block.py | 3 +- test/python/pulse/test_builder.py | 15 ++++++--- test/python/pulse/test_instructions.py | 9 +++-- test/python/pulse/test_parameter_manager.py | 33 ++++++++++++------- test/python/pulse/test_transforms.py | 12 ++++--- 7 files changed, 63 insertions(+), 24 deletions(-) create mode 100644 releasenotes/notes/deprecate-pulse-Call-instruction-538802d8fad7e257.yaml diff --git a/qiskit/pulse/instructions/call.py b/qiskit/pulse/instructions/call.py index 05097ad5b798..49c9a6fe42c0 100644 --- a/qiskit/pulse/instructions/call.py +++ b/qiskit/pulse/instructions/call.py @@ -18,6 +18,7 @@ from qiskit.pulse.channels import Channel from qiskit.pulse.exceptions import PulseError from qiskit.pulse.instructions import instruction +from qiskit.utils.deprecation import deprecate_func class Call(instruction.Instruction): @@ -30,6 +31,11 @@ class Call(instruction.Instruction): # Prefix to use for auto naming. prefix = "call" + @deprecate_func( + since="0.25.0", + additional_msg="Instead, use the pulse builder function " + "qiskit.pulse.builder.call(subroutine) within an active building context.", + ) def __init__( self, subroutine, diff --git a/releasenotes/notes/deprecate-pulse-Call-instruction-538802d8fad7e257.yaml b/releasenotes/notes/deprecate-pulse-Call-instruction-538802d8fad7e257.yaml new file mode 100644 index 000000000000..6ed28e8e6a9d --- /dev/null +++ b/releasenotes/notes/deprecate-pulse-Call-instruction-538802d8fad7e257.yaml @@ -0,0 +1,9 @@ +--- +deprecations: + - | + The :class:`~qiskit.pulse.instructions.Call` has been deprecated and will + be removed in a future release. + Instead, use the `pulse builder + `_ + function :func:`~qiskit.pulse.builder.call` + within an active building context. diff --git a/test/python/pulse/test_block.py b/test/python/pulse/test_block.py index 90441ef917d5..20ec95713a2c 100644 --- a/test/python/pulse/test_block.py +++ b/test/python/pulse/test_block.py @@ -678,7 +678,8 @@ def test_nested_parametrized_instructions(self): test_waveform = pulse.Constant(100, self.amp0) param_sched = pulse.Schedule(pulse.Play(test_waveform, self.d0)) - call_inst = pulse.instructions.Call(param_sched) + with self.assertWarns(DeprecationWarning): + call_inst = pulse.instructions.Call(param_sched) sub_block = pulse.ScheduleBlock() sub_block += call_inst diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index 1d7f09d5df78..55ff16e2f367 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -212,7 +212,8 @@ def test_scheduler_settings(self): inst_map.add("x", (0,), test_x_sched) ref_sched = pulse.Schedule() - ref_sched += pulse.instructions.Call(test_x_sched) + with self.assertWarns(DeprecationWarning): + ref_sched += pulse.instructions.Call(test_x_sched) x_qc = circuit.QuantumCircuit(2) x_qc.x(0) @@ -961,7 +962,8 @@ def test_call(self): reference += instructions.Delay(20, d1) ref_sched = pulse.Schedule() - ref_sched += pulse.instructions.Call(reference) + with self.assertWarns(DeprecationWarning): + ref_sched += pulse.instructions.Call(reference) with pulse.build() as schedule: with pulse.align_right(): @@ -981,7 +983,8 @@ def test_call_circuit(self): reference = inst_map.get("u1", (0,), 0.0) ref_sched = pulse.Schedule() - ref_sched += pulse.instructions.Call(reference) + with self.assertWarns(DeprecationWarning): + ref_sched += pulse.instructions.Call(reference) u1_qc = circuit.QuantumCircuit(2) u1_qc.append(circuit.library.U1Gate(0.0), [0]) @@ -1009,7 +1012,8 @@ def test_call_circuit_with_cregs(self): reference = compiler.schedule(reference_qc, self.backend) ref_sched = pulse.Schedule() - ref_sched += pulse.instructions.Call(reference) + with self.assertWarns(DeprecationWarning): + ref_sched += pulse.instructions.Call(reference) self.assertScheduleEqual(schedule, ref_sched) @@ -1041,7 +1045,8 @@ def test_call_gate_and_circuit(self): ) reference = pulse.Schedule() - reference += pulse.instructions.Call(h_reference) + with self.assertWarns(DeprecationWarning): + reference += pulse.instructions.Call(h_reference) reference += cx_reference reference += measure_reference << reference.duration diff --git a/test/python/pulse/test_instructions.py b/test/python/pulse/test_instructions.py index c41e0a12080c..892d7f49d033 100644 --- a/test/python/pulse/test_instructions.py +++ b/test/python/pulse/test_instructions.py @@ -361,14 +361,16 @@ def setUp(self): def test_call(self): """Test basic call instruction.""" - call = instructions.Call(subroutine=self.subroutine) + with self.assertWarns(DeprecationWarning): + call = instructions.Call(subroutine=self.subroutine) self.assertEqual(call.duration, 10) self.assertEqual(call.subroutine, self.subroutine) def test_parameterized_call(self): """Test call instruction with parameterized subroutine.""" - call = instructions.Call(subroutine=self.function) + with self.assertWarns(DeprecationWarning): + call = instructions.Call(subroutine=self.function) self.assertTrue(call.is_parameterized()) self.assertEqual(len(call.parameters), 2) @@ -393,7 +395,8 @@ def test_assign_parameters_to_call(self): def test_call_initialize_with_parameter(self): """Test call instruction with parameterized subroutine with initial dict.""" init_dict = {self.param1: 0.1, self.param2: 0.5} - call = instructions.Call(subroutine=self.function, value_dict=init_dict) + with self.assertWarns(DeprecationWarning): + call = instructions.Call(subroutine=self.function, value_dict=init_dict) with pulse.build() as ref_sched: pulse.play(pulse.Gaussian(160, 0.1, 40), pulse.DriveChannel(0)) diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py index 4ffa519c0a8a..d395461ac3b1 100644 --- a/test/python/pulse/test_parameter_manager.py +++ b/test/python/pulse/test_parameter_manager.py @@ -87,7 +87,8 @@ def setUp(self): long_schedule += subroutine long_schedule += pulse.ShiftPhase(self.phi2, self.d2) long_schedule += pulse.Play(self.parametric_waveform2, self.d2) - long_schedule += pulse.Call(sched) + with self.assertWarns(DeprecationWarning): + long_schedule += pulse.Call(sched) long_schedule += pulse.Play(self.parametric_waveform3, self.d3) long_schedule += pulse.Acquire( @@ -152,7 +153,8 @@ def test_get_parameter_from_call(self): sched = pulse.Schedule() sched += pulse.ShiftPhase(self.phi1, self.d1) - test_obj = pulse.Call(subroutine=sched) + with self.assertWarns(DeprecationWarning): + test_obj = pulse.Call(subroutine=sched) visitor = ParameterGetter() visitor.visit(test_obj) @@ -257,7 +259,8 @@ def test_set_parameter_to_call(self): sched = pulse.Schedule() sched += pulse.ShiftPhase(self.phi1, self.d1) - test_obj = pulse.Call(subroutine=sched) + with self.assertWarns(DeprecationWarning): + test_obj = pulse.Call(subroutine=sched) value_dict = {self.phi1: 1.57, self.ch1: 2} @@ -267,7 +270,8 @@ def test_set_parameter_to_call(self): ref_sched = pulse.Schedule() ref_sched += pulse.ShiftPhase(1.57, pulse.DriveChannel(2)) - ref_obj = pulse.Call(subroutine=ref_sched) + with self.assertWarns(DeprecationWarning): + ref_obj = pulse.Call(subroutine=ref_sched) self.assertEqual(assigned, ref_obj) @@ -309,7 +313,8 @@ def test_nested_assignment_partial_bind(self): subroutine += pulse.Play(self.parametric_waveform1, self.d1) nested_block = pulse.ScheduleBlock() - nested_block += pulse.Call(subroutine=subroutine) + with self.assertWarns(DeprecationWarning): + nested_block += pulse.Call(subroutine=subroutine) test_obj = pulse.ScheduleBlock() test_obj += nested_block @@ -451,7 +456,8 @@ def test_set_parameter_to_complex_schedule(self): ref_obj += subroutine ref_obj += pulse.ShiftPhase(2.0, pulse.DriveChannel(2)) ref_obj += pulse.Play(pulse.Gaussian(125, 0.3, 25), pulse.DriveChannel(2)) - ref_obj += pulse.Call(sched) + with self.assertWarns(DeprecationWarning): + ref_obj += pulse.Call(sched) ref_obj += pulse.Play(pulse.Gaussian(150, 0.4, 25), pulse.DriveChannel(4)) ref_obj += pulse.Acquire( @@ -503,12 +509,14 @@ def test_parameters_from_subroutine(self): # from call instruction program_layer1 = pulse.Schedule() - program_layer1 += pulse.instructions.Call(program_layer0) + with self.assertWarns(DeprecationWarning): + program_layer1 += pulse.instructions.Call(program_layer0) self.assertEqual(program_layer1.get_parameters("amp")[0], param1) # from nested call instruction program_layer2 = pulse.Schedule() - program_layer2 += pulse.instructions.Call(program_layer1) + with self.assertWarns(DeprecationWarning): + program_layer2 += pulse.instructions.Call(program_layer1) self.assertEqual(program_layer2.get_parameters("amp")[0], param1) def test_assign_parameter_to_subroutine(self): @@ -522,13 +530,15 @@ def test_assign_parameter_to_subroutine(self): # to call instruction program_layer1 = pulse.Schedule() - program_layer1 += pulse.instructions.Call(program_layer0) + with self.assertWarns(DeprecationWarning): + program_layer1 += pulse.instructions.Call(program_layer0) target = program_layer1.assign_parameters({param1: 0.1}, inplace=False) self.assertEqual(inline_subroutines(target), reference) # to nested call instruction program_layer2 = pulse.Schedule() - program_layer2 += pulse.instructions.Call(program_layer1) + with self.assertWarns(DeprecationWarning): + program_layer2 += pulse.instructions.Call(program_layer1) target = program_layer2.assign_parameters({param1: 0.1}, inplace=False) self.assertEqual(inline_subroutines(target), reference) @@ -546,7 +556,8 @@ def test_assign_parameter_to_subroutine_parameter(self): main_prog = pulse.Schedule() pdict = {param1: param_sub1 + param_sub2} - main_prog += pulse.instructions.Call(subroutine, value_dict=pdict) + with self.assertWarns(DeprecationWarning): + main_prog += pulse.instructions.Call(subroutine, value_dict=pdict) # parameter is overwritten by parameters self.assertEqual(len(main_prog.parameters), 2) diff --git a/test/python/pulse/test_transforms.py b/test/python/pulse/test_transforms.py index 50d6cbd94498..d2d4b3567860 100644 --- a/test/python/pulse/test_transforms.py +++ b/test/python/pulse/test_transforms.py @@ -907,12 +907,14 @@ def test_remove_subroutines(self): subroutine = pulse.Schedule() subroutine.insert(0, pulse.Delay(20, d0), inplace=True) - subroutine.insert(20, pulse.instructions.Call(nested_routine), inplace=True) + with self.assertWarns(DeprecationWarning): + subroutine.insert(20, pulse.instructions.Call(nested_routine), inplace=True) subroutine.insert(50, pulse.Delay(10, d0), inplace=True) main_program = pulse.Schedule() main_program.insert(0, pulse.Delay(10, d0), inplace=True) - main_program.insert(30, pulse.instructions.Call(subroutine), inplace=True) + with self.assertWarns(DeprecationWarning): + main_program.insert(30, pulse.instructions.Call(subroutine), inplace=True) target = transforms.inline_subroutines(main_program) @@ -932,7 +934,8 @@ def test_call_in_nested_schedule(self): subroutine.insert(10, pulse.Delay(10, d0), inplace=True) nested_sched = pulse.Schedule() - nested_sched.insert(0, pulse.instructions.Call(subroutine), inplace=True) + with self.assertWarns(DeprecationWarning): + nested_sched.insert(0, pulse.instructions.Call(subroutine), inplace=True) main_sched = pulse.Schedule() main_sched.insert(0, nested_sched, inplace=True) @@ -956,7 +959,8 @@ def test_call_in_nested_block(self): subroutine.append(pulse.Delay(10, d0), inplace=True) nested_block = pulse.ScheduleBlock() - nested_block.append(pulse.instructions.Call(subroutine), inplace=True) + with self.assertWarns(DeprecationWarning): + nested_block.append(pulse.instructions.Call(subroutine), inplace=True) main_block = pulse.ScheduleBlock() main_block.append(nested_block, inplace=True) From 7b8203f9c2c29afb9e63398dd8a6727754991947 Mon Sep 17 00:00:00 2001 From: thspreetham98 Date: Mon, 26 Jun 2023 19:28:14 +0530 Subject: [PATCH 161/172] 10129 pauli doctring (#10334) * 10129: update pauli docstrings * base pauli docstring update * linting errors fixed --- .../operators/symplectic/base_pauli.py | 16 ++++++++++------ .../quantum_info/operators/symplectic/pauli.py | 17 +++++++++++------ .../operators/symplectic/pauli_list.py | 16 ++++++++++------ 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/qiskit/quantum_info/operators/symplectic/base_pauli.py b/qiskit/quantum_info/operators/symplectic/base_pauli.py index 1a7e8ae1bd26..3ff449c3feb6 100644 --- a/qiskit/quantum_info/operators/symplectic/base_pauli.py +++ b/qiskit/quantum_info/operators/symplectic/base_pauli.py @@ -227,20 +227,24 @@ def commutes(self, other, qargs=None): return a_dot_b == b_dot_a def evolve(self, other, qargs=None, frame="h"): - r"""Heisenberg picture evolution of a Pauli by a Clifford. + r"""Performs either Heisenberg (default) or Schrödinger picture + evolution of the Pauli by a Clifford and returns the evolved Pauli. - This returns the Pauli :math:`P^\prime = C^\dagger.P.C`. + Schrödinger picture evolution can be chosen by passing parameter ``frame='s'``. + This option yields a faster calculation. - By choosing the parameter frame='s', this function returns the Schrödinger evolution of the Pauli - :math:`P^\prime = C.P.C^\dagger`. This option yields a faster calculation. + Heisenberg picture evolves the Pauli as :math:`P^\prime = C^\dagger.P.C`. + + Schrödinger picture evolves the Pauli as :math:`P^\prime = C.P.C^\dagger`. Args: other (BasePauli or QuantumCircuit): The Clifford circuit to evolve by. qargs (list): a list of qubits to apply the Clifford to. - frame (string): 'h' for Heisenberg or 's' for Schrödinger framework. + frame (string): ``'h'`` for Heisenberg or ``'s'`` for Schrödinger framework. Returns: - BasePauli: the Pauli :math:`C^\dagger.P.C`. + BasePauli: the Pauli :math:`C^\dagger.P.C` (Heisenberg picture) + or the Pauli :math:`C.P.C^\dagger` (Schrödinger picture). Raises: QiskitError: if the Clifford number of qubits and ``qargs`` don't match. diff --git a/qiskit/quantum_info/operators/symplectic/pauli.py b/qiskit/quantum_info/operators/symplectic/pauli.py index f8b9c1bffcef..bd0d78bc90af 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli.py +++ b/qiskit/quantum_info/operators/symplectic/pauli.py @@ -567,20 +567,25 @@ def anticommutes(self, other, qargs=None): return np.logical_not(self.commutes(other, qargs=qargs)) def evolve(self, other, qargs=None, frame="h"): - r"""Heisenberg picture evolution of a Pauli by a Clifford. + r"""Performs either Heisenberg (default) or Schrödinger picture + evolution of the Pauli by a Clifford and returns the evolved Pauli. - This returns the Pauli :math:`P^\prime = C^\dagger.P.C`. + Schrödinger picture evolution can be chosen by passing parameter ``frame='s'``. + This option yields a faster calculation. - By choosing the parameter frame='s', this function returns the Schrödinger evolution of the Pauli - :math:`P^\prime = C.P.C^\dagger`. This option yields a faster calculation. + Heisenberg picture evolves the Pauli as :math:`P^\prime = C^\dagger.P.C`. + + Schrödinger picture evolves the Pauli as :math:`P^\prime = C.P.C^\dagger`. Args: other (Pauli or Clifford or QuantumCircuit): The Clifford operator to evolve by. qargs (list): a list of qubits to apply the Clifford to. - frame (string): 'h' for Heisenberg or 's' for Schrödinger framework. + frame (string): ``'h'`` for Heisenberg (default) or ``'s'`` for + Schrödinger framework. Returns: - Pauli: the Pauli :math:`C^\dagger.P.C`. + Pauli: the Pauli :math:`C^\dagger.P.C` (Heisenberg picture) + or the Pauli :math:`C.P.C^\dagger` (Schrödinger picture). Raises: QiskitError: if the Clifford number of qubits and qargs don't match. diff --git a/qiskit/quantum_info/operators/symplectic/pauli_list.py b/qiskit/quantum_info/operators/symplectic/pauli_list.py index 80b95bfa4c0e..9e282b4a34cd 100644 --- a/qiskit/quantum_info/operators/symplectic/pauli_list.py +++ b/qiskit/quantum_info/operators/symplectic/pauli_list.py @@ -901,20 +901,24 @@ def _commutes_with_all(self, other, anti=False): return inds def evolve(self, other, qargs=None, frame="h"): - r"""Evolve the Pauli by a Clifford. + r"""Performs either Heisenberg (default) or Schrödinger picture + evolution of the Pauli by a Clifford and returns the evolved Pauli. - This returns the Pauli :math:`P^\prime = C.P.C^\dagger`. + Schrödinger picture evolution can be chosen by passing parameter ``frame='s'``. + This option yields a faster calculation. - By choosing the parameter frame='s', this function returns the Schrödinger evolution of the Pauli - :math:`P^\prime = C.P.C^\dagger`. This option yields a faster calculation. + Heisenberg picture evolves the Pauli as :math:`P^\prime = C^\dagger.P.C`. + + Schrödinger picture evolves the Pauli as :math:`P^\prime = C.P.C^\dagger`. Args: other (Pauli or Clifford or QuantumCircuit): The Clifford operator to evolve by. qargs (list): a list of qubits to apply the Clifford to. - frame (string): 'h' for Heisenberg or 's' for Schrödinger framework. + frame (string): ``'h'`` for Heisenberg (default) or ``'s'`` for Schrödinger framework. Returns: - Pauli: the Pauli :math:`C.P.C^\dagger`. + PauliList: the Pauli :math:`C^\dagger.P.C` (Heisenberg picture) + or the Pauli :math:`C.P.C^\dagger` (Schrödinger picture). Raises: QiskitError: if the Clifford number of qubits and qargs don't match. From 14ba1659834d9c1695464ad7482c4d3045e00e3e Mon Sep 17 00:00:00 2001 From: Cristian Emiliano Godinez Ramirez <57567043+EmilianoG-byte@users.noreply.github.com> Date: Mon, 26 Jun 2023 18:06:16 +0200 Subject: [PATCH 162/172] fix typo in real_amplitude.py (#10347) --- qiskit/circuit/library/n_local/real_amplitudes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/circuit/library/n_local/real_amplitudes.py b/qiskit/circuit/library/n_local/real_amplitudes.py index 2838c898f2da..5e3f1ba671f9 100644 --- a/qiskit/circuit/library/n_local/real_amplitudes.py +++ b/qiskit/circuit/library/n_local/real_amplitudes.py @@ -24,7 +24,7 @@ class RealAmplitudes(TwoLocal): The ``RealAmplitudes`` circuit is a heuristic trial wave function used as Ansatz in chemistry applications or classification circuits in machine learning. The circuit consists of - of alternating layers of :math:`Y` rotations and :math:`CX` entanglements. The entanglement + alternating layers of :math:`Y` rotations and :math:`CX` entanglements. The entanglement pattern can be user-defined or selected from a predefined set. It is called ``RealAmplitudes`` since the prepared quantum states will only have real amplitudes, the complex part is always 0. From 791a45433a21a5604086bfcd96d804433b8aecfe Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 26 Jun 2023 17:25:47 +0100 Subject: [PATCH 163/172] Temporarily pin `scipy<1.11` in CI (#10348) The newest Scipy on Windows seems to have caused a convergence error in some of the Weyl-chamber code. This pins Scipy temporarily while we resolve that issue. --- constraints.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/constraints.txt b/constraints.txt index 67572fd19305..026d0e551b6a 100644 --- a/constraints.txt +++ b/constraints.txt @@ -10,3 +10,8 @@ qiskit-aer==0.12.0 # tests to flake. See https://github.com/Qiskit/qiskit-terra/issues/10305, # remove pin when resolving that. numpy<1.25 + +# Scipy 1.11 seems to have caused an instability in the Weyl coordinates +# eigensystem code for one of the test cases. See +# https://github.com/Qiskit/qiskit-terra/issues/10345 for current details. +scipy<1.11 From c6938529b13b3fd1a4260745873f0bc28d038078 Mon Sep 17 00:00:00 2001 From: Simone Gasperini Date: Mon, 26 Jun 2023 20:18:24 +0200 Subject: [PATCH 164/172] Allow multiplication of SparsePauliOp and ParameterExpression (#10264) * Allow multiplication of SparsePauliOp and ParameterExpression * Add unit tests for SparsePauliOp and ParameterExpression multiplication * Add release note for the new feature --- .../operators/symplectic/sparse_pauli_op.py | 4 ++-- ...meter-multiplication-245173f0b232f59b.yaml | 15 +++++++++++++ .../symplectic/test_sparse_pauli_op.py | 22 +++++++++++++++---- 3 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 releasenotes/notes/support-SparsePauliOp-Parameter-multiplication-245173f0b232f59b.yaml diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py index 503c29f94689..8ed37c400c9c 100644 --- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py +++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py @@ -366,8 +366,8 @@ def _add(self, other, qargs=None): return SparsePauliOp(paulis, coeffs, ignore_pauli_phase=True, copy=False) def _multiply(self, other): - if not isinstance(other, Number): - raise QiskitError("other is not a number") + if not isinstance(other, (Number, ParameterExpression)): + raise QiskitError("other is neither a Number nor a Parameter/ParameterExpression") if other == 0: # Check edge case that we deleted all Paulis # In this case we return an identity Pauli with a zero coefficient diff --git a/releasenotes/notes/support-SparsePauliOp-Parameter-multiplication-245173f0b232f59b.yaml b/releasenotes/notes/support-SparsePauliOp-Parameter-multiplication-245173f0b232f59b.yaml new file mode 100644 index 000000000000..a140e4aed629 --- /dev/null +++ b/releasenotes/notes/support-SparsePauliOp-Parameter-multiplication-245173f0b232f59b.yaml @@ -0,0 +1,15 @@ +features: + - | + Adds support for multiplication of :class:`.SparsePauliOp` objects + with :class:`.Parameter` objects by using the * operator, for example:: + + from qiskit.circuit import Parameter + from qiskit.quantum_info import SparsePauliOp + + param = Parameter("a") + op = SparsePauliOp("X") + param * op + +fixes: + - | + Fixes issue `#10185 `. 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 e9c5d6741bd2..afceedada3e3 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 @@ -20,7 +20,7 @@ from ddt import ddt from qiskit import QiskitError -from qiskit.circuit import Parameter, ParameterVector +from qiskit.circuit import ParameterExpression, Parameter, ParameterVector from qiskit.circuit.parametertable import ParameterView from qiskit.quantum_info.operators import Operator, Pauli, PauliList, PauliTable, SparsePauliOp from qiskit.test import QiskitTestCase @@ -588,14 +588,28 @@ def test_sub_qargs(self, num_qubits): self.assertEqual(value, target) np.testing.assert_array_equal(op.paulis.phase, np.zeros(op.size)) - @combine(num_qubits=[1, 2, 3], value=[0, 1, 1j, -3 + 4.4j, np.int64(2)], param=[None, "a"]) + @combine( + num_qubits=[1, 2, 3], + value=[ + 0, + 1, + 1j, + -3 + 4.4j, + np.int64(2), + Parameter("x"), + 0 * Parameter("x"), + (-2 + 1.7j) * Parameter("x"), + ], + param=[None, "a"], + ) def test_mul(self, num_qubits, value, param): """Test * method for {num_qubits} qubits and value {value}.""" spp_op = self.random_spp_op(num_qubits, 2**num_qubits, param) target = value * spp_op.to_matrix() op = value * spp_op value_mat = op.to_matrix() - if value != 0 and param is not None: + has_parameters = isinstance(value, ParameterExpression) or param is not None + if value != 0 and has_parameters: value_mat = bind_parameters_to_one(value_mat) target = bind_parameters_to_one(target) if value == 0: @@ -606,7 +620,7 @@ def test_mul(self, num_qubits, value, param): target = spp_op.to_matrix() * value op = spp_op * value value_mat = op.to_matrix() - if value != 0 and param is not None: + if value != 0 and has_parameters: value_mat = bind_parameters_to_one(value_mat) target = bind_parameters_to_one(target) if value == 0: From d9763523d45a747fd882a7e79cc44c02b5058916 Mon Sep 17 00:00:00 2001 From: Lev Bishop <18673315+levbishop@users.noreply.github.com> Date: Tue, 27 Jun 2023 07:46:40 -0400 Subject: [PATCH 165/172] pylint: enable consider-using-from-import (#10337) --- pyproject.toml | 1 - test/python/quantum_info/test_synthesis.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8b28ebb93c36..fc603f57a652 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,6 @@ disable = [ "consider-using-dict-items", "consider-using-enumerate", "consider-using-f-string", - "consider-using-from-import", "modified-iterating-list", "nested-min-max", "no-member", diff --git a/test/python/quantum_info/test_synthesis.py b/test/python/quantum_info/test_synthesis.py index 099ffb87191e..58074995c7a1 100644 --- a/test/python/quantum_info/test_synthesis.py +++ b/test/python/quantum_info/test_synthesis.py @@ -77,7 +77,7 @@ ) from qiskit.quantum_info.synthesis.ion_decompose import cnot_rxx_decompose -import qiskit.quantum_info.synthesis.qsd as qsd +from qiskit.quantum_info.synthesis import qsd from qiskit.test import QiskitTestCase From 2ca46985bdd90f6e8e5e56807d241cb23adc385e Mon Sep 17 00:00:00 2001 From: Isaac Cilia Attard Date: Wed, 28 Jun 2023 11:53:28 +0200 Subject: [PATCH 166/172] Reorganised algorithms.gradients (#9969) * Reorganised algorithms.gradients Fixes issue #9695 by reorganising algorithms.gradients into seperate folders. * Moved 'lin_comb_qgt.py' to the 'qgt' folder Moved 'lin_comb_qgt.py' to the 'qgt' folder to ensure all qgt related files are grouped together. * Formatted files with linter. Ran linter and reformatted files accordingly. * Updated '__init__.py' Updated '__init__.py' to reflect new folder structure. * Removed 'qgt' folder Assimilated QGT classes into related folders within 'algorithms.gradients'. Also, 'qgt_result.py' was placed into the 'base' folder since it constitutes a base class. * Update qiskit/algorithms/gradients/__init__.py Co-authored-by: Julien Gacon * Update qiskit/algorithms/gradients/__init__.py Co-authored-by: Julien Gacon * Removed 'qfi' folder Removed 'qfi' folder and moved remaining contents into parent folder 'gradients'. * Update qiskit/algorithms/gradients/__init__.py Co-authored-by: Julien Gacon * Attempted to resolve merge conflicts. Altered files to match new versions causing the conflict. * Ran linter Ran 'tox -eblack' on the repository to resolve linter issues. * Fixed base module hierarchy issue Altered contents of 'qfi.py' to refer to 'BaseQGT' according to the new hierarchy given in the PR. * Fixed more hierarchy issues. Changed more references to reflect new hierarchy. --------- Co-authored-by: Julien Gacon --- qiskit/algorithms/gradients/__init__.py | 100 +++++++++++------- qiskit/algorithms/gradients/base/__init__.py | 11 ++ .../{ => base}/base_estimator_gradient.py | 4 +- .../gradients/{ => base}/base_qgt.py | 4 +- .../{ => base}/base_sampler_gradient.py | 4 +- .../{ => base}/estimator_gradient_result.py | 0 .../gradients/{ => base}/qgt_result.py | 2 +- .../{ => base}/sampler_gradient_result.py | 0 .../gradients/finite_diff/__init__.py | 11 ++ .../finite_diff_estimator_gradient.py | 6 +- .../finite_diff_sampler_gradient.py | 6 +- .../algorithms/gradients/lin_comb/__init__.py | 11 ++ .../lin_comb_estimator_gradient.py | 8 +- .../gradients/{ => lin_comb}/lin_comb_qgt.py | 8 +- .../lin_comb_sampler_gradient.py | 8 +- .../gradients/param_shift/__init__.py | 11 ++ .../param_shift_estimator_gradient.py | 8 +- .../param_shift_sampler_gradient.py | 8 +- qiskit/algorithms/gradients/qfi.py | 4 +- .../{reverse_gradient => reverse}/__init__.py | 0 .../{reverse_gradient => reverse}/bind.py | 0 .../derive_circuit.py | 0 .../reverse_gradient.py | 4 +- .../reverse_qgt.py | 4 +- .../split_circuits.py | 0 qiskit/algorithms/gradients/spsa/__init__.py | 11 ++ .../{ => spsa}/spsa_estimator_gradient.py | 6 +- .../{ => spsa}/spsa_sampler_gradient.py | 6 +- 28 files changed, 160 insertions(+), 85 deletions(-) create mode 100644 qiskit/algorithms/gradients/base/__init__.py rename qiskit/algorithms/gradients/{ => base}/base_estimator_gradient.py (99%) rename qiskit/algorithms/gradients/{ => base}/base_qgt.py (99%) rename qiskit/algorithms/gradients/{ => base}/base_sampler_gradient.py (99%) rename qiskit/algorithms/gradients/{ => base}/estimator_gradient_result.py (100%) rename qiskit/algorithms/gradients/{ => base}/qgt_result.py (96%) rename qiskit/algorithms/gradients/{ => base}/sampler_gradient_result.py (100%) create mode 100644 qiskit/algorithms/gradients/finite_diff/__init__.py rename qiskit/algorithms/gradients/{ => finite_diff}/finite_diff_estimator_gradient.py (97%) rename qiskit/algorithms/gradients/{ => finite_diff}/finite_diff_sampler_gradient.py (97%) create mode 100644 qiskit/algorithms/gradients/lin_comb/__init__.py rename qiskit/algorithms/gradients/{ => lin_comb}/lin_comb_estimator_gradient.py (96%) rename qiskit/algorithms/gradients/{ => lin_comb}/lin_comb_qgt.py (97%) rename qiskit/algorithms/gradients/{ => lin_comb}/lin_comb_sampler_gradient.py (96%) create mode 100644 qiskit/algorithms/gradients/param_shift/__init__.py rename qiskit/algorithms/gradients/{ => param_shift}/param_shift_estimator_gradient.py (94%) rename qiskit/algorithms/gradients/{ => param_shift}/param_shift_sampler_gradient.py (94%) rename qiskit/algorithms/gradients/{reverse_gradient => reverse}/__init__.py (100%) rename qiskit/algorithms/gradients/{reverse_gradient => reverse}/bind.py (100%) rename qiskit/algorithms/gradients/{reverse_gradient => reverse}/derive_circuit.py (100%) rename qiskit/algorithms/gradients/{reverse_gradient => reverse}/reverse_gradient.py (98%) rename qiskit/algorithms/gradients/{reverse_gradient => reverse}/reverse_qgt.py (99%) rename qiskit/algorithms/gradients/{reverse_gradient => reverse}/split_circuits.py (100%) create mode 100644 qiskit/algorithms/gradients/spsa/__init__.py rename qiskit/algorithms/gradients/{ => spsa}/spsa_estimator_gradient.py (96%) rename qiskit/algorithms/gradients/{ => spsa}/spsa_sampler_gradient.py (97%) diff --git a/qiskit/algorithms/gradients/__init__.py b/qiskit/algorithms/gradients/__init__.py index 72aad2402c2d..ff3a2fceca5e 100644 --- a/qiskit/algorithms/gradients/__init__.py +++ b/qiskit/algorithms/gradients/__init__.py @@ -17,74 +17,94 @@ .. currentmodule:: qiskit.algorithms.gradients - -Estimator Gradients -=================== +Base Classes +============ .. autosummary:: :toctree: ../stubs/ BaseEstimatorGradient - DerivativeType - FiniteDiffEstimatorGradient - LinCombEstimatorGradient - ParamShiftEstimatorGradient - SPSAEstimatorGradient - ReverseEstimatorGradient + BaseQGT + BaseSamplerGradient + EstimatorGradientResult + SamplerGradientResult + QGTResult -Sampler Gradients -================= +Finite Differences +================== .. autosummary:: :toctree: ../stubs/ - BaseSamplerGradient + FiniteDiffEstimatorGradient FiniteDiffSamplerGradient + +Linear Combination of Unitaries +=============================== + +.. autosummary:: + :toctree: ../stubs/ + + LinCombEstimatorGradient LinCombSamplerGradient + LinCombQGT + +Parameter Shift Rules +===================== + +.. autosummary:: + :toctree: ../stubs/ + + ParamShiftEstimatorGradient ParamShiftSamplerGradient - SPSASamplerGradient -Quantum Geometric Tensor -======================== +Quantum Fisher Information +========================== + .. autosummary:: :toctree: ../stubs/ - BaseQGT - LinCombQGT + QFIResult QFI + +Classical Methods +================= + +.. autosummary:: + :toctree: ../stubs/ + + ReverseEstimatorGradient ReverseQGT -Results -======= +Simultaneous Perturbation Stochastic Approximation +================================================== .. autosummary:: :toctree: ../stubs/ - EstimatorGradientResult - QFIResult - QGTResult - SamplerGradientResult + SPSAEstimatorGradient + SPSASamplerGradient """ -from .base_estimator_gradient import BaseEstimatorGradient -from .base_qgt import BaseQGT -from .base_sampler_gradient import BaseSamplerGradient -from .estimator_gradient_result import EstimatorGradientResult -from .finite_diff_estimator_gradient import FiniteDiffEstimatorGradient -from .finite_diff_sampler_gradient import FiniteDiffSamplerGradient -from .lin_comb_estimator_gradient import DerivativeType, LinCombEstimatorGradient -from .lin_comb_qgt import LinCombQGT -from .lin_comb_sampler_gradient import LinCombSamplerGradient -from .param_shift_estimator_gradient import ParamShiftEstimatorGradient -from .param_shift_sampler_gradient import ParamShiftSamplerGradient +from .base.base_estimator_gradient import BaseEstimatorGradient +from .base.base_qgt import BaseQGT +from .base.base_sampler_gradient import BaseSamplerGradient +from .base.estimator_gradient_result import EstimatorGradientResult +from .finite_diff.finite_diff_estimator_gradient import FiniteDiffEstimatorGradient +from .finite_diff.finite_diff_sampler_gradient import FiniteDiffSamplerGradient +from .lin_comb.lin_comb_estimator_gradient import DerivativeType, LinCombEstimatorGradient +from .lin_comb.lin_comb_qgt import LinCombQGT +from .lin_comb.lin_comb_sampler_gradient import LinCombSamplerGradient +from .param_shift.param_shift_estimator_gradient import ParamShiftEstimatorGradient +from .param_shift.param_shift_sampler_gradient import ParamShiftSamplerGradient from .qfi import QFI from .qfi_result import QFIResult -from .qgt_result import QGTResult -from .sampler_gradient_result import SamplerGradientResult -from .spsa_estimator_gradient import SPSAEstimatorGradient -from .spsa_sampler_gradient import SPSASamplerGradient -from .reverse_gradient.reverse_gradient import ReverseEstimatorGradient -from .reverse_gradient.reverse_qgt import ReverseQGT +from .base.qgt_result import QGTResult +from .base.sampler_gradient_result import SamplerGradientResult +from .spsa.spsa_estimator_gradient import SPSAEstimatorGradient +from .spsa.spsa_sampler_gradient import SPSASamplerGradient +from .reverse.reverse_gradient import ReverseEstimatorGradient +from .reverse.reverse_qgt import ReverseQGT __all__ = [ "BaseEstimatorGradient", diff --git a/qiskit/algorithms/gradients/base/__init__.py b/qiskit/algorithms/gradients/base/__init__.py new file mode 100644 index 000000000000..8f5fd2a37f84 --- /dev/null +++ b/qiskit/algorithms/gradients/base/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022, 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. diff --git a/qiskit/algorithms/gradients/base_estimator_gradient.py b/qiskit/algorithms/gradients/base/base_estimator_gradient.py similarity index 99% rename from qiskit/algorithms/gradients/base_estimator_gradient.py rename to qiskit/algorithms/gradients/base/base_estimator_gradient.py index 926797db0a2a..0cbf478fa2ec 100644 --- a/qiskit/algorithms/gradients/base_estimator_gradient.py +++ b/qiskit/algorithms/gradients/base/base_estimator_gradient.py @@ -31,7 +31,7 @@ from qiskit.transpiler.passes import TranslateParameterizedGates from .estimator_gradient_result import EstimatorGradientResult -from .utils import ( +from ..utils import ( DerivativeType, GradientCircuit, _assign_unique_parameters, @@ -39,7 +39,7 @@ _make_gradient_parameter_values, ) -from ..algorithm_job import AlgorithmJob +from ...algorithm_job import AlgorithmJob class BaseEstimatorGradient(ABC): diff --git a/qiskit/algorithms/gradients/base_qgt.py b/qiskit/algorithms/gradients/base/base_qgt.py similarity index 99% rename from qiskit/algorithms/gradients/base_qgt.py rename to qiskit/algorithms/gradients/base/base_qgt.py index ffda914b878a..f2999a8f2bf0 100644 --- a/qiskit/algorithms/gradients/base_qgt.py +++ b/qiskit/algorithms/gradients/base/base_qgt.py @@ -29,7 +29,7 @@ from qiskit.transpiler.passes import TranslateParameterizedGates from .qgt_result import QGTResult -from .utils import ( +from ..utils import ( DerivativeType, GradientCircuit, _assign_unique_parameters, @@ -37,7 +37,7 @@ _make_gradient_parameter_values, ) -from ..algorithm_job import AlgorithmJob +from ...algorithm_job import AlgorithmJob class BaseQGT(ABC): diff --git a/qiskit/algorithms/gradients/base_sampler_gradient.py b/qiskit/algorithms/gradients/base/base_sampler_gradient.py similarity index 99% rename from qiskit/algorithms/gradients/base_sampler_gradient.py rename to qiskit/algorithms/gradients/base/base_sampler_gradient.py index 436a7a0dbe12..b4947365fd5d 100644 --- a/qiskit/algorithms/gradients/base_sampler_gradient.py +++ b/qiskit/algorithms/gradients/base/base_sampler_gradient.py @@ -28,14 +28,14 @@ from qiskit.transpiler.passes import TranslateParameterizedGates from .sampler_gradient_result import SamplerGradientResult -from .utils import ( +from ..utils import ( GradientCircuit, _assign_unique_parameters, _make_gradient_parameters, _make_gradient_parameter_values, ) -from ..algorithm_job import AlgorithmJob +from ...algorithm_job import AlgorithmJob class BaseSamplerGradient(ABC): diff --git a/qiskit/algorithms/gradients/estimator_gradient_result.py b/qiskit/algorithms/gradients/base/estimator_gradient_result.py similarity index 100% rename from qiskit/algorithms/gradients/estimator_gradient_result.py rename to qiskit/algorithms/gradients/base/estimator_gradient_result.py diff --git a/qiskit/algorithms/gradients/qgt_result.py b/qiskit/algorithms/gradients/base/qgt_result.py similarity index 96% rename from qiskit/algorithms/gradients/qgt_result.py rename to qiskit/algorithms/gradients/base/qgt_result.py index 5cb0e7f97c51..f110e1c68381 100644 --- a/qiskit/algorithms/gradients/qgt_result.py +++ b/qiskit/algorithms/gradients/base/qgt_result.py @@ -22,7 +22,7 @@ from qiskit.providers import Options -from .utils import DerivativeType +from ..utils import DerivativeType @dataclass(frozen=True) diff --git a/qiskit/algorithms/gradients/sampler_gradient_result.py b/qiskit/algorithms/gradients/base/sampler_gradient_result.py similarity index 100% rename from qiskit/algorithms/gradients/sampler_gradient_result.py rename to qiskit/algorithms/gradients/base/sampler_gradient_result.py diff --git a/qiskit/algorithms/gradients/finite_diff/__init__.py b/qiskit/algorithms/gradients/finite_diff/__init__.py new file mode 100644 index 000000000000..8f5fd2a37f84 --- /dev/null +++ b/qiskit/algorithms/gradients/finite_diff/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022, 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. diff --git a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py b/qiskit/algorithms/gradients/finite_diff/finite_diff_estimator_gradient.py similarity index 97% rename from qiskit/algorithms/gradients/finite_diff_estimator_gradient.py rename to qiskit/algorithms/gradients/finite_diff/finite_diff_estimator_gradient.py index f154b99db605..ea1b987c2ff2 100644 --- a/qiskit/algorithms/gradients/finite_diff_estimator_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff/finite_diff_estimator_gradient.py @@ -25,10 +25,10 @@ from qiskit.providers import Options from qiskit.quantum_info.operators.base_operator import BaseOperator -from .base_estimator_gradient import BaseEstimatorGradient -from .estimator_gradient_result import EstimatorGradientResult +from ..base.base_estimator_gradient import BaseEstimatorGradient +from ..base.estimator_gradient_result import EstimatorGradientResult -from ..exceptions import AlgorithmError +from ...exceptions import AlgorithmError class FiniteDiffEstimatorGradient(BaseEstimatorGradient): diff --git a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py b/qiskit/algorithms/gradients/finite_diff/finite_diff_sampler_gradient.py similarity index 97% rename from qiskit/algorithms/gradients/finite_diff_sampler_gradient.py rename to qiskit/algorithms/gradients/finite_diff/finite_diff_sampler_gradient.py index 11bb2ec38749..bc250286c828 100644 --- a/qiskit/algorithms/gradients/finite_diff_sampler_gradient.py +++ b/qiskit/algorithms/gradients/finite_diff/finite_diff_sampler_gradient.py @@ -23,10 +23,10 @@ from qiskit.primitives import BaseSampler from qiskit.providers import Options -from .base_sampler_gradient import BaseSamplerGradient -from .sampler_gradient_result import SamplerGradientResult +from ..base.base_sampler_gradient import BaseSamplerGradient +from ..base.sampler_gradient_result import SamplerGradientResult -from ..exceptions import AlgorithmError +from ...exceptions import AlgorithmError class FiniteDiffSamplerGradient(BaseSamplerGradient): diff --git a/qiskit/algorithms/gradients/lin_comb/__init__.py b/qiskit/algorithms/gradients/lin_comb/__init__.py new file mode 100644 index 000000000000..8f5fd2a37f84 --- /dev/null +++ b/qiskit/algorithms/gradients/lin_comb/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022, 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. diff --git a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py b/qiskit/algorithms/gradients/lin_comb/lin_comb_estimator_gradient.py similarity index 96% rename from qiskit/algorithms/gradients/lin_comb_estimator_gradient.py rename to qiskit/algorithms/gradients/lin_comb/lin_comb_estimator_gradient.py index 9edb15df0572..93deb48b5820 100644 --- a/qiskit/algorithms/gradients/lin_comb_estimator_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb/lin_comb_estimator_gradient.py @@ -26,11 +26,11 @@ from qiskit.providers import Options from qiskit.quantum_info.operators.base_operator import BaseOperator -from .base_estimator_gradient import BaseEstimatorGradient -from .estimator_gradient_result import EstimatorGradientResult -from .utils import DerivativeType, _make_lin_comb_gradient_circuit, _make_lin_comb_observables +from ..base.base_estimator_gradient import BaseEstimatorGradient +from ..base.estimator_gradient_result import EstimatorGradientResult +from ..utils import DerivativeType, _make_lin_comb_gradient_circuit, _make_lin_comb_observables -from ..exceptions import AlgorithmError +from ...exceptions import AlgorithmError class LinCombEstimatorGradient(BaseEstimatorGradient): diff --git a/qiskit/algorithms/gradients/lin_comb_qgt.py b/qiskit/algorithms/gradients/lin_comb/lin_comb_qgt.py similarity index 97% rename from qiskit/algorithms/gradients/lin_comb_qgt.py rename to qiskit/algorithms/gradients/lin_comb/lin_comb_qgt.py index d84a4eb6f11f..0a9e05a9344a 100644 --- a/qiskit/algorithms/gradients/lin_comb_qgt.py +++ b/qiskit/algorithms/gradients/lin_comb/lin_comb_qgt.py @@ -25,12 +25,12 @@ from qiskit.providers import Options from qiskit.quantum_info import SparsePauliOp -from .base_qgt import BaseQGT +from ..base.base_qgt import BaseQGT from .lin_comb_estimator_gradient import LinCombEstimatorGradient -from .qgt_result import QGTResult -from .utils import DerivativeType, _make_lin_comb_qgt_circuit, _make_lin_comb_observables +from ..base.qgt_result import QGTResult +from ..utils import DerivativeType, _make_lin_comb_qgt_circuit, _make_lin_comb_observables -from ..exceptions import AlgorithmError +from ...exceptions import AlgorithmError class LinCombQGT(BaseQGT): diff --git a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py b/qiskit/algorithms/gradients/lin_comb/lin_comb_sampler_gradient.py similarity index 96% rename from qiskit/algorithms/gradients/lin_comb_sampler_gradient.py rename to qiskit/algorithms/gradients/lin_comb/lin_comb_sampler_gradient.py index 41bd9488d1b9..759e77d460bb 100644 --- a/qiskit/algorithms/gradients/lin_comb_sampler_gradient.py +++ b/qiskit/algorithms/gradients/lin_comb/lin_comb_sampler_gradient.py @@ -23,11 +23,11 @@ from qiskit.primitives.utils import _circuit_key from qiskit.providers import Options -from .base_sampler_gradient import BaseSamplerGradient -from .sampler_gradient_result import SamplerGradientResult -from .utils import _make_lin_comb_gradient_circuit +from ..base.base_sampler_gradient import BaseSamplerGradient +from ..base.sampler_gradient_result import SamplerGradientResult +from ..utils import _make_lin_comb_gradient_circuit -from ..exceptions import AlgorithmError +from ...exceptions import AlgorithmError class LinCombSamplerGradient(BaseSamplerGradient): diff --git a/qiskit/algorithms/gradients/param_shift/__init__.py b/qiskit/algorithms/gradients/param_shift/__init__.py new file mode 100644 index 000000000000..8f5fd2a37f84 --- /dev/null +++ b/qiskit/algorithms/gradients/param_shift/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022, 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. diff --git a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py b/qiskit/algorithms/gradients/param_shift/param_shift_estimator_gradient.py similarity index 94% rename from qiskit/algorithms/gradients/param_shift_estimator_gradient.py rename to qiskit/algorithms/gradients/param_shift/param_shift_estimator_gradient.py index 892441492669..ef334a291e6e 100644 --- a/qiskit/algorithms/gradients/param_shift_estimator_gradient.py +++ b/qiskit/algorithms/gradients/param_shift/param_shift_estimator_gradient.py @@ -21,11 +21,11 @@ from qiskit.opflow import PauliSumOp from qiskit.quantum_info.operators.base_operator import BaseOperator -from .base_estimator_gradient import BaseEstimatorGradient -from .estimator_gradient_result import EstimatorGradientResult -from .utils import _make_param_shift_parameter_values +from ..base.base_estimator_gradient import BaseEstimatorGradient +from ..base.estimator_gradient_result import EstimatorGradientResult +from ..utils import _make_param_shift_parameter_values -from ..exceptions import AlgorithmError +from ...exceptions import AlgorithmError class ParamShiftEstimatorGradient(BaseEstimatorGradient): diff --git a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py b/qiskit/algorithms/gradients/param_shift/param_shift_sampler_gradient.py similarity index 94% rename from qiskit/algorithms/gradients/param_shift_sampler_gradient.py rename to qiskit/algorithms/gradients/param_shift/param_shift_sampler_gradient.py index b3d474c24f8b..642f4b002cd9 100644 --- a/qiskit/algorithms/gradients/param_shift_sampler_gradient.py +++ b/qiskit/algorithms/gradients/param_shift/param_shift_sampler_gradient.py @@ -20,11 +20,11 @@ from qiskit.circuit import Parameter, QuantumCircuit -from .base_sampler_gradient import BaseSamplerGradient -from .sampler_gradient_result import SamplerGradientResult -from .utils import _make_param_shift_parameter_values +from ..base.base_sampler_gradient import BaseSamplerGradient +from ..base.sampler_gradient_result import SamplerGradientResult +from ..utils import _make_param_shift_parameter_values -from ..exceptions import AlgorithmError +from ...exceptions import AlgorithmError class ParamShiftSamplerGradient(BaseSamplerGradient): diff --git a/qiskit/algorithms/gradients/qfi.py b/qiskit/algorithms/gradients/qfi.py index 1dcaa70d31e9..94aa86fde56a 100644 --- a/qiskit/algorithms/gradients/qfi.py +++ b/qiskit/algorithms/gradients/qfi.py @@ -22,8 +22,8 @@ from qiskit.circuit import Parameter, QuantumCircuit from qiskit.providers import Options -from .base_qgt import BaseQGT -from .lin_comb_estimator_gradient import DerivativeType +from .base.base_qgt import BaseQGT +from .lin_comb.lin_comb_estimator_gradient import DerivativeType from .qfi_result import QFIResult from ..algorithm_job import AlgorithmJob diff --git a/qiskit/algorithms/gradients/reverse_gradient/__init__.py b/qiskit/algorithms/gradients/reverse/__init__.py similarity index 100% rename from qiskit/algorithms/gradients/reverse_gradient/__init__.py rename to qiskit/algorithms/gradients/reverse/__init__.py diff --git a/qiskit/algorithms/gradients/reverse_gradient/bind.py b/qiskit/algorithms/gradients/reverse/bind.py similarity index 100% rename from qiskit/algorithms/gradients/reverse_gradient/bind.py rename to qiskit/algorithms/gradients/reverse/bind.py diff --git a/qiskit/algorithms/gradients/reverse_gradient/derive_circuit.py b/qiskit/algorithms/gradients/reverse/derive_circuit.py similarity index 100% rename from qiskit/algorithms/gradients/reverse_gradient/derive_circuit.py rename to qiskit/algorithms/gradients/reverse/derive_circuit.py diff --git a/qiskit/algorithms/gradients/reverse_gradient/reverse_gradient.py b/qiskit/algorithms/gradients/reverse/reverse_gradient.py similarity index 98% rename from qiskit/algorithms/gradients/reverse_gradient/reverse_gradient.py rename to qiskit/algorithms/gradients/reverse/reverse_gradient.py index e8d5f1f39618..c3bf8005d377 100644 --- a/qiskit/algorithms/gradients/reverse_gradient/reverse_gradient.py +++ b/qiskit/algorithms/gradients/reverse/reverse_gradient.py @@ -28,8 +28,8 @@ from .derive_circuit import derive_circuit from .split_circuits import split -from ..base_estimator_gradient import BaseEstimatorGradient -from ..estimator_gradient_result import EstimatorGradientResult +from ..base.base_estimator_gradient import BaseEstimatorGradient +from ..base.estimator_gradient_result import EstimatorGradientResult from ..utils import DerivativeType logger = logging.getLogger(__name__) diff --git a/qiskit/algorithms/gradients/reverse_gradient/reverse_qgt.py b/qiskit/algorithms/gradients/reverse/reverse_qgt.py similarity index 99% rename from qiskit/algorithms/gradients/reverse_gradient/reverse_qgt.py rename to qiskit/algorithms/gradients/reverse/reverse_qgt.py index 60e8c3cde2d6..0b845ee96961 100644 --- a/qiskit/algorithms/gradients/reverse_gradient/reverse_qgt.py +++ b/qiskit/algorithms/gradients/reverse/reverse_qgt.py @@ -23,8 +23,8 @@ from qiskit.providers import Options from qiskit.primitives import Estimator -from ..base_qgt import BaseQGT -from ..qgt_result import QGTResult +from ..base.base_qgt import BaseQGT +from ..base.qgt_result import QGTResult from ..utils import DerivativeType from .split_circuits import split diff --git a/qiskit/algorithms/gradients/reverse_gradient/split_circuits.py b/qiskit/algorithms/gradients/reverse/split_circuits.py similarity index 100% rename from qiskit/algorithms/gradients/reverse_gradient/split_circuits.py rename to qiskit/algorithms/gradients/reverse/split_circuits.py diff --git a/qiskit/algorithms/gradients/spsa/__init__.py b/qiskit/algorithms/gradients/spsa/__init__.py new file mode 100644 index 000000000000..8f5fd2a37f84 --- /dev/null +++ b/qiskit/algorithms/gradients/spsa/__init__.py @@ -0,0 +1,11 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2022, 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. diff --git a/qiskit/algorithms/gradients/spsa_estimator_gradient.py b/qiskit/algorithms/gradients/spsa/spsa_estimator_gradient.py similarity index 96% rename from qiskit/algorithms/gradients/spsa_estimator_gradient.py rename to qiskit/algorithms/gradients/spsa/spsa_estimator_gradient.py index bb6868e12545..021bcb5803f0 100644 --- a/qiskit/algorithms/gradients/spsa_estimator_gradient.py +++ b/qiskit/algorithms/gradients/spsa/spsa_estimator_gradient.py @@ -24,10 +24,10 @@ from qiskit.providers import Options from qiskit.quantum_info.operators.base_operator import BaseOperator -from .base_estimator_gradient import BaseEstimatorGradient -from .estimator_gradient_result import EstimatorGradientResult +from ..base.base_estimator_gradient import BaseEstimatorGradient +from ..base.estimator_gradient_result import EstimatorGradientResult -from ..exceptions import AlgorithmError +from ...exceptions import AlgorithmError class SPSAEstimatorGradient(BaseEstimatorGradient): diff --git a/qiskit/algorithms/gradients/spsa_sampler_gradient.py b/qiskit/algorithms/gradients/spsa/spsa_sampler_gradient.py similarity index 97% rename from qiskit/algorithms/gradients/spsa_sampler_gradient.py rename to qiskit/algorithms/gradients/spsa/spsa_sampler_gradient.py index 5ffebcc1af60..be23274d7ebc 100644 --- a/qiskit/algorithms/gradients/spsa_sampler_gradient.py +++ b/qiskit/algorithms/gradients/spsa/spsa_sampler_gradient.py @@ -23,10 +23,10 @@ from qiskit.primitives import BaseSampler from qiskit.providers import Options -from .base_sampler_gradient import BaseSamplerGradient -from .sampler_gradient_result import SamplerGradientResult +from ..base.base_sampler_gradient import BaseSamplerGradient +from ..base.sampler_gradient_result import SamplerGradientResult -from ..exceptions import AlgorithmError +from ...exceptions import AlgorithmError class SPSASamplerGradient(BaseSamplerGradient): From b73115f80bc575b5dbc8b98ad003cd3b4ff9c00d Mon Sep 17 00:00:00 2001 From: Lev Bishop <18673315+levbishop@users.noreply.github.com> Date: Wed, 28 Jun 2023 08:14:24 -0400 Subject: [PATCH 167/172] Ruff enable flake8-comprehensions (C4) (#10339) * Ruff enable flake8-comprehensions (C4) * add symbolic ruff rule id --------- Co-authored-by: John Lapeyre --- examples/python/initialize.py | 2 +- pyproject.toml | 1 + qiskit/pulse/macros.py | 2 +- qiskit/qobj/converters/pulse_instruction.py | 2 +- qiskit/transpiler/coupling.py | 2 +- qiskit/transpiler/passes/layout/disjoint_utils.py | 2 +- test/python/pulse/test_block.py | 2 +- 7 files changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/python/initialize.py b/examples/python/initialize.py index 73ab69cd8d4b..945537c591be 100644 --- a/examples/python/initialize.py +++ b/examples/python/initialize.py @@ -55,7 +55,7 @@ # Desired vector print("Desired probabilities: ") -print(list(map(lambda x: format(abs(x * x), ".3f"), desired_vector))) +print([format(abs(x * x), ".3f") for x in desired_vector]) # Initialize on local simulator sim_backend = BasicAer.get_backend("qasm_simulator") diff --git a/pyproject.toml b/pyproject.toml index fc603f57a652..50e52a3ef119 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ repair-wheel-command = "cp {wheel} {dest_dir}/. && pipx run abi3audit --strict - [tool.ruff] select = [ + "C4", # category: flake8-comprehensions "F631", "F632", "F634", diff --git a/qiskit/pulse/macros.py b/qiskit/pulse/macros.py index f597ce1b4aea..f45d94bbeb98 100644 --- a/qiskit/pulse/macros.py +++ b/qiskit/pulse/macros.py @@ -176,7 +176,7 @@ def _measure_v2( meas_group = set() for qubit in qubits: meas_group |= set(meas_map[qubit]) - meas_group = sorted(list(meas_group)) + meas_group = sorted(meas_group) meas_group_set = set(range(max(meas_group) + 1)) unassigned_qubit_indices = sorted(set(meas_group) - qubit_mem_slots.keys()) diff --git a/qiskit/qobj/converters/pulse_instruction.py b/qiskit/qobj/converters/pulse_instruction.py index a278d7cca3e1..d88a79b2faba 100644 --- a/qiskit/qobj/converters/pulse_instruction.py +++ b/qiskit/qobj/converters/pulse_instruction.py @@ -898,7 +898,7 @@ def _convert_parametric_pulse( try: pulse_name = instruction.label except AttributeError: - sorted_params = sorted(tuple(instruction.parameters.items()), key=lambda x: x[0]) + sorted_params = sorted(instruction.parameters.items(), key=lambda x: x[0]) base_str = "{pulse}_{params}".format( pulse=instruction.pulse_shape, params=str(sorted_params) ) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index ed9424906709..903341d1fc93 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -475,7 +475,7 @@ def connected_components(self) -> List["CouplingMap"]: output_list = [] for component in components: new_cmap = CouplingMap() - new_cmap.graph = self.graph.subgraph(list(sorted(component))) + new_cmap.graph = self.graph.subgraph(sorted(component)) output_list.append(new_cmap) return output_list diff --git a/qiskit/transpiler/passes/layout/disjoint_utils.py b/qiskit/transpiler/passes/layout/disjoint_utils.py index 4a7975200fbd..4b7ca06a094c 100644 --- a/qiskit/transpiler/passes/layout/disjoint_utils.py +++ b/qiskit/transpiler/passes/layout/disjoint_utils.py @@ -176,7 +176,7 @@ def separate_dag(dag: DAGCircuit) -> List[DAGCircuit]: connected_components = rx.weakly_connected_components(im_graph) component_qubits = [] for component in connected_components: - component_qubits.append(set(qubit_map[x] for x in component)) + component_qubits.append({qubit_map[x] for x in component}) qubits = set(dag.qubits) diff --git a/test/python/pulse/test_block.py b/test/python/pulse/test_block.py index 20ec95713a2c..0dc56fbe6b27 100644 --- a/test/python/pulse/test_block.py +++ b/test/python/pulse/test_block.py @@ -959,7 +959,7 @@ def _filter_and_test_consistency( excluded = sched_blk.exclude(*args, **kwargs) def list_instructions(blk: pulse.ScheduleBlock) -> List[pulse.Instruction]: - insts = list() + insts = [] for element in blk.blocks: if isinstance(element, pulse.ScheduleBlock): inner_insts = list_instructions(element) From e9f8b7c50968501e019d0cb426676ac606eb5a10 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 28 Jun 2023 11:49:37 -0400 Subject: [PATCH 168/172] Deprecate namespace packaging hooks (#10322) * Deprecate namespace packaging hooks This commit official deprecates the pkgutil namespace hooks that are used for extending the `qiskit.*` and `qiskit.providers.*` namespaces from external packages. These were previously used to enable the elements packaging model where different parts of qiskit lived in separate python packages under a shared namespace. This is no longer being used and leaving the namespace extendable by external packages carries a risk of issues or other accidental cross-interactions when an external package gets loaded as part of Qiskit. Fixes ##8809 * Update releasenotes/notes/deprecate-namespace-a2ac600f140755e2.yaml Co-authored-by: Jake Lishman --------- Co-authored-by: Jake Lishman --- qiskit/__init__.py | 4 ++-- qiskit/providers/__init__.py | 5 ++--- .../deprecate-namespace-a2ac600f140755e2.yaml | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 releasenotes/notes/deprecate-namespace-a2ac600f140755e2.yaml diff --git a/qiskit/__init__.py b/qiskit/__init__.py index a8d90d12220f..7d37eba3d359 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -73,8 +73,8 @@ # to be placed *before* the wrapper imports or any non-import code AND *before* # importing the package you want to allow extensions for (in this case `backends`). -# TODO: Remove when we drop support for importing qiskit-aer < 0.11.0 and the -# qiskit-ibmq-provider package is retired/archived. +# Support for the deprecated extending this namespace. +# Remove this after 0.46.0 release __path__ = pkgutil.extend_path(__path__, __name__) # Please note these are global instances, not modules. diff --git a/qiskit/providers/__init__.py b/qiskit/providers/__init__.py index f1f6d993f8e7..e581c9721fe0 100644 --- a/qiskit/providers/__init__.py +++ b/qiskit/providers/__init__.py @@ -777,7 +777,6 @@ def status(self): from qiskit.providers.jobstatus import JobStatus -# Allow extending this namespace. -# TODO: Remove when we drop support for importing qiskit-aer < 0.11.0 and the -# qiskit-ibmq-provider package is retired/archived. +# Support for the deprecated extending this namespace. +# Remove this after 0.46.0 release __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/releasenotes/notes/deprecate-namespace-a2ac600f140755e2.yaml b/releasenotes/notes/deprecate-namespace-a2ac600f140755e2.yaml new file mode 100644 index 000000000000..0396a6750b3b --- /dev/null +++ b/releasenotes/notes/deprecate-namespace-a2ac600f140755e2.yaml @@ -0,0 +1,17 @@ +--- +deprecations: + - | + Extensions of the ``qiskit`` and ``qiskit.providers`` namespaces by external + packages are now deprecated and the hook points enabling this will be + removed in a future release. In the past, the Qiskit project was composed + of elements that extended a shared namespace and these hook points enabled + doing that. However, it was not intended for these interfaces to ever be + used by other packages. Now that the overall Qiskit package is no longer + using that packaging model, leaving the possibility for these extensions + carry more risk than benefits and is therefore being deprecated for + future removal. If you're maintaining a package that extends the Qiskit + namespace (i.e. your users import from ``qiskit.x`` or + ``qiskit.providers.y``) you should transition to using a standalone + Python namespace for your package. No warning will be raised as part of this + because there is no method to inject a warning at the packaging level that + would be required to warn external packages of this change. From df5d413d789595e9cd4bfd217148e3aa625bc1e1 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Mon, 3 Jul 2023 16:14:02 +0100 Subject: [PATCH 169/172] Honour CI constraints in Neko tests (#10373) Ideally we'd be running the Neko tests against the requirements that will actually form the complete package. In practice, however, there are persistent failures from Neko stemming from PySCF's use of SciPy, which we get via Nature, which is no longer under Qiskit control. While we work out what the future of the Neko test suite is, with the application modules now no longer being IBM-managed Qiskit packages, we can apply the CI constraints so we can return to getting reports from the parts of the integration tests that are actually within our control. --- .github/workflows/neko.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/neko.yml b/.github/workflows/neko.yml index ba410d1752b9..bda60fd39790 100644 --- a/.github/workflows/neko.yml +++ b/.github/workflows/neko.yml @@ -16,3 +16,4 @@ jobs: - uses: Qiskit/qiskit-neko@main with: test_selection: terra + repo_install_command: "pip install -c constraints.txt ." From 8a8609f93a600019bf208610a348adcbe61e3acd Mon Sep 17 00:00:00 2001 From: TsafrirA <113579969+TsafrirA@users.noreply.github.com> Date: Tue, 4 Jul 2023 08:32:12 +0300 Subject: [PATCH 170/172] Begin deprecation cycle of the discrete pulse library (#10222) * Add SymbolicPulse counterparts to the discrete library, and start deprecation process. * Typo fix * Release notes styling * Docs and style corrections * autosummary update to avoid name clashes * Remove deprecation section from release notes * Minor changes to Square() --- docs/conf.py | 13 +- qiskit/pulse/__init__.py | 4 + qiskit/pulse/library/__init__.py | 8 + qiskit/pulse/library/continuous.py | 2 +- qiskit/pulse/library/discrete.py | 125 ++++++- qiskit/pulse/library/symbolic_pulses.py | 304 +++++++++++++++++- ...-library-deprecation-3a95eba7e29d8d49.yaml | 14 + test/python/pulse/test_discrete_pulses.py | 29 ++ test/python/pulse/test_pulse_lib.py | 170 +++++++++- 9 files changed, 652 insertions(+), 17 deletions(-) create mode 100644 releasenotes/notes/discrete-pulse-library-deprecation-3a95eba7e29d8d49.yaml diff --git a/docs/conf.py b/docs/conf.py index b3ec73843a54..f481d65102b1 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -68,7 +68,7 @@ "rustworkx": ("https://qiskit.org/ecosystem/rustworkx/", None), "qiskit-ibm-runtime": ("https://qiskit.org/ecosystem/ibm-runtime/", None), "qiskit-aer": ("https://qiskit.org/ecosystem/aer/", None), - "numpy": ("https://numpy.org/doc/stable/", None) + "numpy": ("https://numpy.org/doc/stable/", None), } # -- Options for HTML output ------------------------------------------------- @@ -112,6 +112,8 @@ "qiskit.pulse.library.Sin": "qiskit.pulse.library.Sin_class.rst", "qiskit.pulse.library.Gaussian": "qiskit.pulse.library.Gaussian_class.rst", "qiskit.pulse.library.Drag": "qiskit.pulse.library.Drag_class.rst", + "qiskit.pulse.library.Square": "qiskit.pulse.library.Square_fun.rst", + "qiskit.pulse.library.Sech": "qiskit.pulse.library.Sech_fun.rst", } autoclass_content = "both" @@ -119,10 +121,15 @@ # -- Options for Doctest -------------------------------------------------------- -doctest_default_flags = doctest.ELLIPSIS | doctest.NORMALIZE_WHITESPACE | doctest.IGNORE_EXCEPTION_DETAIL | doctest.DONT_ACCEPT_TRUE_FOR_1 +doctest_default_flags = ( + doctest.ELLIPSIS + | doctest.NORMALIZE_WHITESPACE + | doctest.IGNORE_EXCEPTION_DETAIL + | doctest.DONT_ACCEPT_TRUE_FOR_1 +) # Leaving this string empty disables testing of doctest blocks from docstrings. # Doctest blocks are structures like this one: # >> code # output -doctest_test_doctest_blocks = "" \ No newline at end of file +doctest_test_doctest_blocks = "" diff --git a/qiskit/pulse/__init__.py b/qiskit/pulse/__init__.py index 5fe159b947fb..07ebcba99b7b 100644 --- a/qiskit/pulse/__init__.py +++ b/qiskit/pulse/__init__.py @@ -147,6 +147,10 @@ Cos, Sawtooth, Triangle, + Square, + GaussianDeriv, + Sech, + SechDeriv, ParametricPulse, SymbolicPulse, ScalableSymbolicPulse, diff --git a/qiskit/pulse/library/__init__.py b/qiskit/pulse/library/__init__.py index c7103b7a0d0a..2562f96f24ef 100644 --- a/qiskit/pulse/library/__init__.py +++ b/qiskit/pulse/library/__init__.py @@ -89,10 +89,14 @@ GaussianSquare GaussianSquareDrag gaussian_square_echo + GaussianDeriv Sin Cos Sawtooth Triangle + Square + Sech + SechDeriv """ @@ -119,12 +123,16 @@ GaussianSquare, GaussianSquareDrag, gaussian_square_echo, + GaussianDeriv, Drag, Constant, Sin, Cos, Sawtooth, Triangle, + Square, + Sech, + SechDeriv, ) from .pulse import Pulse from .waveform import Waveform diff --git a/qiskit/pulse/library/continuous.py b/qiskit/pulse/library/continuous.py index a7098ef25850..cffe086798a5 100644 --- a/qiskit/pulse/library/continuous.py +++ b/qiskit/pulse/library/continuous.py @@ -222,7 +222,7 @@ def gaussian_deriv( rescale_amp=rescale_amp, ret_x=True, ) - gauss_deriv = -x / sigma * gauss + gauss_deriv = -x / sigma * gauss # Note that x is shifted and normalized by sigma if ret_gaussian: return gauss_deriv, gauss return gauss_deriv diff --git a/qiskit/pulse/library/discrete.py b/qiskit/pulse/library/discrete.py index d5f6932a0be2..046944471270 100644 --- a/qiskit/pulse/library/discrete.py +++ b/qiskit/pulse/library/discrete.py @@ -17,6 +17,7 @@ """ from typing import Optional +from qiskit.utils.deprecation import deprecate_func from ..exceptions import PulseError from .waveform import Waveform from . import continuous @@ -26,6 +27,16 @@ _sampled_constant_pulse = samplers.midpoint(continuous.constant) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including constant() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Constant(...).get_waveform(). " + " Note that complex value support for the `amp` parameter is pending deprecation" + " in the SymbolicPulse library. It is therefore recommended to use two float values" + " for (`amp`, `angle`) instead of complex `amp`", + pending=True, +) def constant(duration: int, amp: complex, name: Optional[str] = None) -> Waveform: r"""Generates constant-sampled :class:`~qiskit.pulse.library.Waveform`. @@ -46,6 +57,13 @@ def constant(duration: int, amp: complex, name: Optional[str] = None) -> Wavefor _sampled_zero_pulse = samplers.midpoint(continuous.zero) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including zero() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Constant(amp=0,...).get_waveform().", + pending=True, +) def zero(duration: int, name: Optional[str] = None) -> Waveform: """Generates zero-sampled :class:`~qiskit.pulse.library.Waveform`. @@ -65,6 +83,15 @@ def zero(duration: int, name: Optional[str] = None) -> Waveform: _sampled_square_pulse = samplers.midpoint(continuous.square) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including square() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Square(...).get_waveform()." + " Note that pulse.Square() does not support complex values for `amp`," + " and that the phase is defined differently. See documentation.", + pending=True, +) def square( duration: int, amp: complex, freq: float = None, phase: float = 0, name: Optional[str] = None ) -> Waveform: @@ -97,6 +124,17 @@ def square( _sampled_sawtooth_pulse = samplers.midpoint(continuous.sawtooth) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including sawtooth() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Sawtooth(...).get_waveform()." + " Note that pulse.Sawtooth() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`)." + " Also note that the phase is defined differently, such that 2*pi phase" + " shifts by a full cycle.", + pending=True, +) def sawtooth( duration: int, amp: complex, freq: float = None, phase: float = 0, name: Optional[str] = None ) -> Waveform: @@ -143,6 +181,15 @@ def sawtooth( _sampled_triangle_pulse = samplers.midpoint(continuous.triangle) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including triangle() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Triangle(...).get_waveform()." + " Note that pulse.Triangle() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def triangle( duration: int, amp: complex, freq: float = None, phase: float = 0, name: Optional[str] = None ) -> Waveform: @@ -189,6 +236,15 @@ def triangle( _sampled_cos_pulse = samplers.midpoint(continuous.cos) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including cos() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Cos(...).get_waveform()." + " Note that pulse.Cos() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def cos( duration: int, amp: complex, freq: float = None, phase: float = 0, name: Optional[str] = None ) -> Waveform: @@ -218,6 +274,15 @@ def cos( _sampled_sin_pulse = samplers.midpoint(continuous.sin) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including sin() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Sin(...).get_waveform()." + " Note that pulse.Sin() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def sin( duration: int, amp: complex, freq: float = None, phase: float = 0, name: Optional[str] = None ) -> Waveform: @@ -247,6 +312,16 @@ def sin( _sampled_gaussian_pulse = samplers.midpoint(continuous.gaussian) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including gaussian() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Gaussian(...).get_waveform()." + " Note that complex value support for the `amp` parameter is pending deprecation" + " in the SymbolicPulse library. It is therefore recommended to use two float values" + " for (`amp`, `angle`) instead of complex `amp`", + pending=True, +) def gaussian( duration: int, amp: complex, sigma: float, name: Optional[str] = None, zero_ends: bool = True ) -> Waveform: @@ -291,6 +366,15 @@ def gaussian( _sampled_gaussian_deriv_pulse = samplers.midpoint(continuous.gaussian_deriv) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including gaussian_deriv() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.GaussianDeriv(...).get_waveform()." + " Note that pulse.GaussianDeriv() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def gaussian_deriv( duration: int, amp: complex, sigma: float, name: Optional[str] = None ) -> Waveform: @@ -301,7 +385,8 @@ def gaussian_deriv( .. math:: - f(x) = A\frac{(x - \mu)}{\sigma^2}\exp\left(\left(\frac{x - \mu}{2\sigma}\right)^2 \right) + f(x) = -A\frac{(x - \mu)}{\sigma^2}\exp + \left(-\frac{1}{2}\left(\frac{x - \mu}{\sigma}\right)^2 \right) i.e. the derivative of the Gaussian function, with center :math:`\mu=` ``duration/2``. @@ -318,6 +403,15 @@ def gaussian_deriv( _sampled_sech_pulse = samplers.midpoint(continuous.sech) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including sech() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Sech(...).get_waveform()." + " Note that pulse.Sech() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def sech( duration: int, amp: complex, sigma: float, name: str = None, zero_ends: bool = True ) -> Waveform: @@ -360,6 +454,15 @@ def sech( _sampled_sech_deriv_pulse = samplers.midpoint(continuous.sech_deriv) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including sech_deriv() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.SechDeriv(...).get_waveform()." + " Note that pulse.SechDeriv() does not support complex values for `amp`." + " Instead, use two float values for (`amp`, `angle`).", + pending=True, +) def sech_deriv(duration: int, amp: complex, sigma: float, name: str = None) -> Waveform: r"""Generates unnormalized sech derivative :class:`~qiskit.pulse.library.Waveform`. @@ -385,6 +488,16 @@ def sech_deriv(duration: int, amp: complex, sigma: float, name: str = None) -> W _sampled_gaussian_square_pulse = samplers.midpoint(continuous.gaussian_square) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including gaussian_square() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.GaussianSquare(...).get_waveform()." + " Note that complex value support for the `amp` parameter is pending deprecation" + " in the SymbolicPulse library. It is therefore recommended to use two float values" + " for (`amp`, `angle`) instead of complex `amp`", + pending=True, +) def gaussian_square( duration: int, amp: complex, @@ -451,6 +564,16 @@ def gaussian_square( _sampled_drag_pulse = samplers.midpoint(continuous.drag) +@deprecate_func( + since="0.25.0", + additional_msg="The discrete pulses library, including drag() is pending deprecation." + " Instead, use the SymbolicPulse library to create the waveform with" + " pulse.Drag(...).get_waveform()." + " Note that complex value support for the `amp` parameter is pending deprecation" + " in the SymbolicPulse library. It is therefore recommended to use two float values" + " for (`amp`, `angle`) instead of complex `amp`", + pending=True, +) def drag( duration: int, amp: complex, diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index 878f6a4741f5..b7cb04df9503 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -1085,9 +1085,9 @@ def gaussian_square_echo( The Gaussian Square Echo pulse is composed of three pulses. First, a Gaussian Square pulse :math:`f_{echo}(x)` with amplitude ``amp`` and phase ``angle`` playing for half duration, followed by a second Gaussian Square pulse :math:`-f_{echo}(x)` with opposite amplitude - and same phase playing for the rest of the duration. Third a Gaussian Square pulse + and same phase playing for the rest of the duration. Third a Gaussian Square pulse :math:`f_{active}(x)` with amplitude ``active_amp`` and phase ``active_angle`` - playing for the entire duration. The Gaussian Square Echo pulse :math:`g_e()` + playing for the entire duration. The Gaussian Square Echo pulse :math:`g_e()` can be written as: .. math:: @@ -1099,11 +1099,11 @@ def gaussian_square_echo( & \\frac{\\text{duration}}{2} < x\ \\end{cases}\\\\ - One case where this pulse can be used is when implementing a direct CNOT gate with - a cross-resonance superconducting qubit architecture. When applying this pulse to - the target qubit, the active portion can be used to cancel IX terms from the + One case where this pulse can be used is when implementing a direct CNOT gate with + a cross-resonance superconducting qubit architecture. When applying this pulse to + the target qubit, the active portion can be used to cancel IX terms from the cross-resonance drive while the echo portion can reduce the impact of a static ZZ coupling. - + Exactly one of the ``risefall_sigma_ratio`` and ``width`` parameters has to be specified. If ``risefall_sigma_ratio`` is not ``None`` and ``width`` is ``None``: @@ -1122,10 +1122,10 @@ def gaussian_square_echo( .. _citation1: https://iopscience.iop.org/article/10.1088/2058-9565/abe519 - .. |citation1| replace:: *Jurcevic, P., Javadi-Abhari, A., Bishop, L. S., - Lauer, I., Bogorin, D. F., Brink, M., Capelluto, L., G{\"u}nl{\"u}k, O., + .. |citation1| replace:: *Jurcevic, P., Javadi-Abhari, A., Bishop, L. S., + Lauer, I., Bogorin, D. F., Brink, M., Capelluto, L., G{\"u}nl{\"u}k, O., Itoko, T., Kanazawa, N. & others - Demonstration of quantum volume 64 on a superconducting quantum + Demonstration of quantum volume 64 on a superconducting quantum computing system. (Section V)* Args: duration: Pulse length in terms of the sampling period `dt`. @@ -1269,6 +1269,72 @@ def gaussian_square_echo( return instance +def GaussianDeriv( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + sigma: Union[float, ParameterExpression], + angle: Optional[Union[float, ParameterExpression]] = 0.0, + name: Optional[str] = None, + limit_amplitude: Optional[bool] = None, +) -> ScalableSymbolicPulse: + """An unnormalized Gaussian derivative pulse. + + The Gaussian function is centered around the halfway point of the pulse, + and the envelope of the pulse is given by: + + .. math:: + + f(x) = -\\text{A}\\frac{x-\\mu}{\\text{sigma}^{2}}\\exp + \\left[-\\left(\\frac{x-\\mu}{2\\text{sigma}}\\right)^{2}\\right] , 0 <= x < duration + + where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, + and :math:`\\mu=\\text{duration}/2`. + + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The magnitude of the amplitude of the pulse + (the value of the corresponding Gaussian at the midpoint `duration`/2). + sigma: A measure of how wide or narrow the corresponding Gaussian peak is in terms of `dt`; + described mathematically in the class docstring. + angle: The angle in radians of the complex phase factor uniformly + scaling the pulse. Default value 0. + name: Display name for this pulse envelope. + limit_amplitude: If ``True``, then limit the amplitude of the + waveform to 1. The default is ``True`` and the amplitude is constrained to 1. + + Returns: + ScalableSymbolicPulse instance. + """ + parameters = {"sigma": sigma} + + # Prepare symbolic expressions + _t, _duration, _amp, _angle, _sigma = sym.symbols("t, duration, amp, angle, sigma") + envelope_expr = ( + -_amp + * sym.exp(sym.I * _angle) + * ((_t - (_duration / 2)) / _sigma**2) + * sym.exp(-(1 / 2) * ((_t - (_duration / 2)) / _sigma) ** 2) + ) + consts_expr = _sigma > 0 + valid_amp_conditions_expr = sym.Abs(_amp / _sigma) <= sym.exp(1 / 2) + + instance = ScalableSymbolicPulse( + pulse_type="GaussianDeriv", + duration=duration, + amp=amp, + angle=angle, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelope_expr, + constraints=consts_expr, + valid_amp_conditions=valid_amp_conditions_expr, + ) + instance.validate_parameters() + + return instance + + class Drag(metaclass=_PulseType): """The Derivative Removal by Adiabatic Gate (DRAG) pulse is a standard Gaussian pulse with an additional Gaussian derivative component and lifting applied. @@ -1649,7 +1715,7 @@ def Triangle( name: Optional[str] = None, limit_amplitude: Optional[bool] = None, ) -> ScalableSymbolicPulse: - """A triangle pulse. + """A triangle wave pulse. The envelope of the pulse is given by: @@ -1709,3 +1775,221 @@ def Triangle( instance.validate_parameters() return instance + + +def Square( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + phase: Union[float, ParameterExpression], + freq: Optional[Union[float, ParameterExpression]] = None, + angle: Optional[Union[float, ParameterExpression]] = 0.0, + name: Optional[str] = None, + limit_amplitude: Optional[bool] = None, +) -> ScalableSymbolicPulse: + """A square wave pulse. + + The envelope of the pulse is given by: + + .. math:: + + f(x) = \\text{A}\\text{sign}\\left[\\sin + \\left(2\\pi x\\times\\text{freq}+\\text{phase}\\right)\\right] , 0 <= x < duration + + where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, + and :math:`\\text{sign}` + is the sign function with the convention :math:`\\text{sign}\\left(0\\right)=1`. + + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The magnitude of the amplitude of the square wave. Wave range is [-`amp`,`amp`]. + phase: The phase of the square wave (note that this is not equivalent to the angle of + the complex amplitude). + freq: The frequency of the square wave, in terms of 1 over sampling period. + If not provided defaults to a single cycle (i.e :math:'\\frac{1}{\\text{duration}}'). + The frequency is limited to the range :math:`\\left(0,0.5\\right]` (the Nyquist frequency). + angle: The angle in radians of the complex phase factor uniformly + scaling the pulse. Default value 0. + name: Display name for this pulse envelope. + limit_amplitude: If ``True``, then limit the amplitude of the + waveform to 1. The default is ``True`` and the amplitude is constrained to 1. + + Returns: + ScalableSymbolicPulse instance. + """ + if freq is None: + freq = 1 / duration + parameters = {"freq": freq, "phase": phase} + + # Prepare symbolic expressions + _t, _duration, _amp, _angle, _freq, _phase = sym.symbols("t, duration, amp, angle, freq, phase") + _x = _freq * _t + _phase / (2 * sym.pi) + + envelope_expr = ( + _amp * sym.exp(sym.I * _angle) * (2 * (2 * sym.floor(_x) - sym.floor(2 * _x)) + 1) + ) + + consts_expr = sym.And(_freq > 0, _freq < 0.5) + + # This might fail for waves shorter than a single cycle + valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 + + instance = ScalableSymbolicPulse( + pulse_type="Square", + duration=duration, + amp=amp, + angle=angle, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelope_expr, + constraints=consts_expr, + valid_amp_conditions=valid_amp_conditions_expr, + ) + instance.validate_parameters() + + return instance + + +def Sech( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + sigma: Union[float, ParameterExpression], + angle: Optional[Union[float, ParameterExpression]] = 0.0, + name: Optional[str] = None, + zero_ends: Optional[bool] = True, + limit_amplitude: Optional[bool] = None, +) -> ScalableSymbolicPulse: + """An unnormalized sech pulse. + + The sech function is centered around the halfway point of the pulse, + and the envelope of the pulse is given by: + + .. math:: + + f(x) = \\text{A}\\text{sech}\\left( + \\frac{x-\\mu}{\\text{sigma}}\\right) , 0 <= x < duration + + where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, + and :math:`\\mu=\\text{duration}/2`. + + If `zero_ends` is set to `True`, the output `y` is modified: + .. math:: + + y\\left(x\\right) \\mapsto \\text{A}\\frac{y-y^{*}}{\\text{A}-y^{*}}, + + where :math:`y^{*}` is the value of :math:`y` at the endpoints (at :math:`x=-1 + and :math:`x=\\text{duration}+1`). This shifts the endpoints value to zero, while also + rescaling to preserve the amplitude at `:math:`\\text{duration}/2``. + + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The magnitude of the amplitude of the pulse (the value at the midpoint `duration`/2). + sigma: A measure of how wide or narrow the sech peak is in terms of `dt`; + described mathematically in the class docstring. + angle: The angle in radians of the complex phase factor uniformly + scaling the pulse. Default value 0. + name: Display name for this pulse envelope. + zero_ends: If True, zeros the ends at x = -1, x = `duration` + 1, + but rescales to preserve `amp`. Default value True. + limit_amplitude: If ``True``, then limit the amplitude of the + waveform to 1. The default is ``True`` and the amplitude is constrained to 1. + + Returns: + ScalableSymbolicPulse instance. + """ + parameters = {"sigma": sigma} + + # Prepare symbolic expressions + _t, _duration, _amp, _angle, _sigma = sym.symbols("t, duration, amp, angle, sigma") + complex_amp = _amp * sym.exp(sym.I * _angle) + envelope_expr = complex_amp * sym.sech((_t - (_duration / 2)) / _sigma) + + if zero_ends: + shift_val = complex_amp * sym.sech((-1 - (_duration / 2)) / _sigma) + envelope_expr = complex_amp * (envelope_expr - shift_val) / (complex_amp - shift_val) + + consts_expr = _sigma > 0 + + valid_amp_conditions_expr = sym.Abs(_amp) <= 1.0 + + instance = ScalableSymbolicPulse( + pulse_type="Sech", + duration=duration, + amp=amp, + angle=angle, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelope_expr, + constraints=consts_expr, + valid_amp_conditions=valid_amp_conditions_expr, + ) + instance.validate_parameters() + + return instance + + +def SechDeriv( + duration: Union[int, ParameterExpression], + amp: Union[float, ParameterExpression], + sigma: Union[float, ParameterExpression], + angle: Optional[Union[float, ParameterExpression]] = 0.0, + name: Optional[str] = None, + limit_amplitude: Optional[bool] = None, +) -> ScalableSymbolicPulse: + """An unnormalized sech derivative pulse. + + The sech function is centered around the halfway point of the pulse, and the envelope of the + pulse is given by: + + .. math:: + + f(x) = \\text{A}\\frac{d}{dx}\\left[\\text{sech} + \\left(\\frac{x-\\mu}{\\text{sigma}}\\right)\\right] , 0 <= x < duration + + where :math:`\\text{A} = \\text{amp} \\times\\exp\\left(i\\times\\text{angle}\\right)`, + :math:`\\mu=\\text{duration}/2`, and :math:`d/dx` is a derivative with respect to `x`. + + Args: + duration: Pulse length in terms of the sampling period `dt`. + amp: The magnitude of the amplitude of the pulse (the value of the corresponding sech + function at the midpoint `duration`/2). + sigma: A measure of how wide or narrow the corresponding sech peak is, in terms of `dt`; + described mathematically in the class docstring. + angle: The angle in radians of the complex phase factor uniformly + scaling the pulse. Default value 0. + name: Display name for this pulse envelope. + limit_amplitude: If ``True``, then limit the amplitude of the + waveform to 1. The default is ``True`` and the amplitude is constrained to 1. + + Returns: + ScalableSymbolicPulse instance. + """ + parameters = {"sigma": sigma} + + # Prepare symbolic expressions + _t, _duration, _amp, _angle, _sigma = sym.symbols("t, duration, amp, angle, sigma") + time_argument = (_t - (_duration / 2)) / _sigma + sech_deriv = -sym.tanh(time_argument) * sym.sech(time_argument) / _sigma + + envelope_expr = _amp * sym.exp(sym.I * _angle) * sech_deriv + + consts_expr = _sigma > 0 + + valid_amp_conditions_expr = sym.Abs(_amp) / _sigma <= 2.0 + + instance = ScalableSymbolicPulse( + pulse_type="SechDeriv", + duration=duration, + amp=amp, + angle=angle, + parameters=parameters, + name=name, + limit_amplitude=limit_amplitude, + envelope=envelope_expr, + constraints=consts_expr, + valid_amp_conditions=valid_amp_conditions_expr, + ) + instance.validate_parameters() + + return instance diff --git a/releasenotes/notes/discrete-pulse-library-deprecation-3a95eba7e29d8d49.yaml b/releasenotes/notes/discrete-pulse-library-deprecation-3a95eba7e29d8d49.yaml new file mode 100644 index 000000000000..37c776060d89 --- /dev/null +++ b/releasenotes/notes/discrete-pulse-library-deprecation-3a95eba7e29d8d49.yaml @@ -0,0 +1,14 @@ +--- +features: + - | + The :class:`~qiskit.pulse.SymbolicPulse` library was extended. The new pulses in the library are: + + * :func:`~qiskit.pulse.library.GaussianDeriv` + * :func:`~qiskit.pulse.library.Sech` + * :func:`~qiskit.pulse.library.SechDeriv` + * :func:`~qiskit.pulse.library.Square` + + The new functions return a :class:`ScalableSymbolicPulse`, and match the functionality + of the corresponding functions in the discrete pulse library, with the exception of + `Square()` for which a phase of :math:`2\\pi` shifts by a full cycle (contrary to the + discrete `square()` where such a shift was induced by a :math:`\\pi` phase). diff --git a/test/python/pulse/test_discrete_pulses.py b/test/python/pulse/test_discrete_pulses.py index 02b87bdfc364..53157d54c5f8 100644 --- a/test/python/pulse/test_discrete_pulses.py +++ b/test/python/pulse/test_discrete_pulses.py @@ -224,3 +224,32 @@ def test_drag(self): drag_pulse = library.drag(duration, amp, sigma, beta=beta) self.assertIsInstance(drag_pulse, Waveform) np.testing.assert_array_almost_equal(drag_pulse.samples, drag_ref) + + def test_pending_deprecation_warnings(self): + """Test that pending deprecation warnings are raised when the discrete library is used.""" + with self.assertWarns(PendingDeprecationWarning): + library.drag(duration=10, amp=0.5, sigma=0.1, beta=0.1) + with self.assertWarns(PendingDeprecationWarning): + library.gaussian_square(duration=10, amp=0.5, sigma=0.1, risefall=2, width=6) + with self.assertWarns(PendingDeprecationWarning): + library.gaussian(duration=10, amp=0.5, sigma=0.1) + with self.assertWarns(PendingDeprecationWarning): + library.sin(duration=10, amp=0.5) + with self.assertWarns(PendingDeprecationWarning): + library.cos(duration=10, amp=0.5) + with self.assertWarns(PendingDeprecationWarning): + library.sawtooth(duration=10, amp=0.5) + with self.assertWarns(PendingDeprecationWarning): + library.zero(duration=10) + with self.assertWarns(PendingDeprecationWarning): + library.constant(duration=10, amp=0.5) + with self.assertWarns(PendingDeprecationWarning): + library.triangle(duration=10, amp=0.5) + with self.assertWarns(PendingDeprecationWarning): + library.gaussian_deriv(duration=10, amp=0.5, sigma=3) + with self.assertWarns(PendingDeprecationWarning): + library.sech_deriv(duration=10, amp=0.5, sigma=3) + with self.assertWarns(PendingDeprecationWarning): + library.sech(duration=10, amp=0.5, sigma=3) + with self.assertWarns(PendingDeprecationWarning): + library.square(duration=10, amp=0.5) diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index a289ad018955..675864c323af 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -25,18 +25,26 @@ GaussianSquare, GaussianSquareDrag, gaussian_square_echo, + GaussianDeriv, Drag, Sin, Cos, Sawtooth, Triangle, + Square, + Sech, + SechDeriv, gaussian, gaussian_square, + gaussian_deriv, drag as pl_drag, sin, cos, triangle, sawtooth, + square, + sech, + sech_deriv, ) from qiskit.pulse import functional_pulse, PulseError @@ -138,12 +146,17 @@ def test_construction(self): Gaussian(duration=25, sigma=4, amp=0.5j) GaussianSquare(duration=150, amp=0.2, sigma=8, width=140) GaussianSquare(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=2.5) + GaussianDeriv(duration=150, amp=0.2, sigma=8) Constant(duration=150, amp=0.1 + 0.4j) Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4) Sin(duration=25, amp=0.5, freq=0.1, phase=0.5, angle=0.5) Cos(duration=30, amp=0.5, freq=0.1, phase=-0.5) Sawtooth(duration=40, amp=0.5, freq=0.2, phase=3.14) Triangle(duration=50, amp=0.5, freq=0.01, phase=0.5) + Square(duration=50, amp=0.5, freq=0.01, phase=0.5) + Sech(duration=50, amp=0.5, sigma=10) + Sech(duration=50, amp=0.5, sigma=10, zero_ends=False) + SechDeriv(duration=50, amp=0.5, sigma=10) # This test should be removed once deprecation of complex amp is completed. def test_complex_amp_deprecation(self): @@ -412,7 +425,7 @@ def test_sin_pulse(self): Sin(duration=duration, amp=amp, freq=5, phase=phase) def test_cos_pulse(self): - """Test that Cin sample pulse matches expectations, and parameter validation""" + """Test that Cos sample pulse matches expectations, and parameter validation""" duration = 100 amp = 0.5 freq = 0.1 @@ -428,6 +441,20 @@ def test_cos_pulse(self): with self.assertRaises(PulseError): Cos(duration=duration, amp=amp, freq=5, phase=phase) + def test_square_pulse(self): + """Test that Square sample pulse matches expectations, and parameter validation""" + duration = 100 + amp = 0.5 + freq = 0.1 + phase = 0.3 + square_pulse = Square(duration=duration, amp=amp, freq=freq, phase=phase) + square_waveform = square(duration=duration, amp=amp, freq=freq, phase=phase / 2) + + np.testing.assert_almost_equal(square_pulse.get_waveform().samples, square_waveform.samples) + + with self.assertRaises(PulseError): + Square(duration=duration, amp=amp, freq=5, phase=phase) + def test_sawtooth_pulse(self): """Test that Sawtooth sample pulse matches expectations, and parameter validation""" duration = 100 @@ -449,7 +476,7 @@ def test_sawtooth_pulse(self): Sawtooth(duration=duration, amp=amp, freq=5, phase=phase) def test_triangle_pulse(self): - """Test that Sawtooth sample pulse matches expectations, and parameter validation""" + """Test that Triangle sample pulse matches expectations, and parameter validation""" duration = 100 amp = 0.5 freq = 0.1 @@ -467,6 +494,51 @@ def test_triangle_pulse(self): with self.assertRaises(PulseError): Triangle(duration=duration, amp=amp, freq=5, phase=phase) + def test_gaussian_deriv_pulse(self): + """Test that GaussianDeriv sample pulse matches expectations""" + duration = 300 + amp = 0.5 + sigma = 100 + gaussian_deriv_pulse = GaussianDeriv(duration=duration, amp=amp, sigma=sigma) + gaussian_deriv_waveform = gaussian_deriv(duration=duration, amp=amp, sigma=sigma) + np.testing.assert_almost_equal( + gaussian_deriv_pulse.get_waveform().samples, gaussian_deriv_waveform.samples + ) + with self.assertRaises(PulseError): + Sech(duration=duration, amp=amp, sigma=0) + + def test_sech_pulse(self): + """Test that Sech sample pulse matches expectations, and parameter validation""" + duration = 100 + amp = 0.5 + sigma = 10 + # Zero ends = True + sech_pulse = Sech(duration=duration, amp=amp, sigma=sigma) + sech_waveform = sech(duration=duration, amp=amp, sigma=sigma) + np.testing.assert_almost_equal(sech_pulse.get_waveform().samples, sech_waveform.samples) + + # Zero ends = False + sech_pulse = Sech(duration=duration, amp=amp, sigma=sigma, zero_ends=False) + sech_waveform = sech(duration=duration, amp=amp, sigma=sigma, zero_ends=False) + np.testing.assert_almost_equal(sech_pulse.get_waveform().samples, sech_waveform.samples) + + with self.assertRaises(PulseError): + Sech(duration=duration, amp=amp, sigma=-5) + + def test_sech_deriv_pulse(self): + """Test that SechDeriv sample pulse matches expectations, and parameter validation""" + duration = 100 + amp = 0.5 + sigma = 10 + sech_deriv_pulse = SechDeriv(duration=duration, amp=amp, sigma=sigma) + sech_deriv_waveform = sech_deriv(duration=duration, amp=amp, sigma=sigma) + np.testing.assert_almost_equal( + sech_deriv_pulse.get_waveform().samples, sech_deriv_waveform.samples + ) + + with self.assertRaises(PulseError): + SechDeriv(duration=duration, amp=amp, sigma=-5) + def test_constant_samples(self): """Test the constant pulse and its sampled construction.""" const = Constant(duration=150, amp=0.1 + 0.4j) @@ -531,6 +603,32 @@ def test_repr(self): self.assertEqual(repr(drag), "Drag(duration=5, sigma=7, beta=1, amp=0.5, angle=0)") const = Constant(duration=150, amp=0.1, angle=0.3) self.assertEqual(repr(const), "Constant(duration=150, amp=0.1, angle=0.3)") + sin_pulse = Sin(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) + self.assertEqual( + repr(sin_pulse), "Sin(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" + ) + cos_pulse = Cos(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) + self.assertEqual( + repr(cos_pulse), "Cos(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" + ) + triangle_pulse = Triangle(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) + self.assertEqual( + repr(triangle_pulse), "Triangle(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" + ) + sawtooth_pulse = Sawtooth(duration=150, amp=0.1, angle=0.3, freq=0.2, phase=0) + self.assertEqual( + repr(sawtooth_pulse), "Sawtooth(duration=150, freq=0.2, phase=0, amp=0.1, angle=0.3)" + ) + sech_pulse = Sech(duration=150, amp=0.1, angle=0.3, sigma=10) + self.assertEqual(repr(sech_pulse), "Sech(duration=150, sigma=10, amp=0.1, angle=0.3)") + sech_deriv_pulse = SechDeriv(duration=150, amp=0.1, angle=0.3, sigma=10) + self.assertEqual( + repr(sech_deriv_pulse), "SechDeriv(duration=150, sigma=10, amp=0.1, angle=0.3)" + ) + gaussian_deriv_pulse = GaussianDeriv(duration=150, amp=0.1, angle=0.3, sigma=10) + self.assertEqual( + repr(gaussian_deriv_pulse), "GaussianDeriv(duration=150, sigma=10, amp=0.1, angle=0.3)" + ) def test_param_validation(self): """Test that parametric pulse parameters are validated when initialized.""" @@ -727,6 +825,74 @@ def test_triangle_limit_amplitude_per_instance(self): waveform = Triangle(duration=100, amp=1.1, phase=0, limit_amplitude=False) self.assertGreater(np.abs(waveform.amp), 1.0) + def test_square_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + Square(duration=100, amp=1.1, phase=0) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = Square(duration=100, amp=1.1, phase=0) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_square_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + Square(duration=100, amp=1.1, phase=0) + + waveform = Square(duration=100, amp=1.1, phase=0, limit_amplitude=False) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_gaussian_deriv_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + GaussianDeriv(duration=100, amp=5, sigma=1) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = GaussianDeriv(duration=100, amp=5, sigma=1) + self.assertGreater(np.abs(waveform.amp / waveform.sigma), np.exp(0.5)) + + def test_gaussian_deriv_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + GaussianDeriv(duration=100, amp=5, sigma=1) + + waveform = GaussianDeriv(duration=100, amp=5, sigma=1, limit_amplitude=False) + self.assertGreater(np.abs(waveform.amp / waveform.sigma), np.exp(0.5)) + + def test_sech_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + Sech(duration=100, amp=5, sigma=1) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = Sech(duration=100, amp=5, sigma=1) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_sech_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + Sech(duration=100, amp=5, sigma=1) + + waveform = Sech(duration=100, amp=5, sigma=1, limit_amplitude=False) + self.assertGreater(np.abs(waveform.amp), 1.0) + + def test_sech_deriv_limit_amplitude(self): + """Test that the check for amplitude less than or equal to 1 can be disabled.""" + with self.assertRaises(PulseError): + SechDeriv(duration=100, amp=5, sigma=1) + + with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): + waveform = SechDeriv(duration=100, amp=5, sigma=1) + self.assertGreater(np.abs(waveform.amp) / waveform.sigma, 2.0) + + def test_sech_deriv_limit_amplitude_per_instance(self): + """Test that the check for amplitude per instance.""" + with self.assertRaises(PulseError): + SechDeriv(duration=100, amp=5, sigma=1) + + waveform = SechDeriv(duration=100, amp=5, sigma=1, limit_amplitude=False) + self.assertGreater(np.abs(waveform.amp) / waveform.sigma, 2.0) + def test_get_parameters(self): """Test getting pulse parameters as attribute.""" drag_pulse = Drag(duration=100, amp=0.1, sigma=40, beta=3) From cd770b59f81dbe07e2bd2cc314460c186067b6bd Mon Sep 17 00:00:00 2001 From: TsafrirA <113579969+TsafrirA@users.noreply.github.com> Date: Wed, 5 Jul 2023 16:28:24 +0300 Subject: [PATCH 171/172] Deprecate complex amp support for `ScalableSymbolicPulse` (#10357) * Deprecate complex amp support for symbolic pulses. * assembler test fix * ParameterValueType + release notes edit * ParameterValueType + release notes edit * Release notes edit * Type hints fix --- qiskit/pulse/library/symbolic_pulses.py | 46 +++---- ...eprecate-complex-amp-41381bd9722bc878.yaml | 16 +++ test/python/compiler/test_assembler.py | 39 ++++-- test/python/pulse/test_parameter_manager.py | 6 +- test/python/pulse/test_pulse_lib.py | 116 ++++++++++-------- test/python/pulse/test_schedule.py | 4 +- test/python/pulse/test_transforms.py | 24 ++-- test/python/qobj/test_pulse_converter.py | 26 +++- test/python/scheduler/test_basic_scheduler.py | 5 +- 9 files changed, 173 insertions(+), 109 deletions(-) create mode 100644 releasenotes/notes/deprecate-complex-amp-41381bd9722bc878.yaml diff --git a/qiskit/pulse/library/symbolic_pulses.py b/qiskit/pulse/library/symbolic_pulses.py index b7cb04df9503..8c90777eae01 100644 --- a/qiskit/pulse/library/symbolic_pulses.py +++ b/qiskit/pulse/library/symbolic_pulses.py @@ -24,7 +24,7 @@ import numpy as np -from qiskit.circuit.parameterexpression import ParameterExpression +from qiskit.circuit.parameterexpression import ParameterExpression, ParameterValueType from qiskit.pulse.exceptions import PulseError from qiskit.pulse.library.pulse import Pulse from qiskit.pulse.library.waveform import Waveform @@ -595,16 +595,16 @@ class ScalableSymbolicPulse(SymbolicPulse): additional_msg=( "Instead, use a float for ``amp`` (for the magnitude) and a float for ``angle``" ), - since="0.23.0", - pending=True, + since="0.25.0", + pending=False, predicate=lambda amp: isinstance(amp, complex), ) def __init__( self, pulse_type: str, duration: Union[ParameterExpression, int], - amp: Union[ParameterExpression, float, complex], - angle: Union[ParameterExpression, float], + amp: ParameterValueType, + angle: ParameterValueType, parameters: Optional[Dict[str, Union[ParameterExpression, complex]]] = None, name: Optional[str] = None, limit_amplitude: Optional[bool] = None, @@ -739,9 +739,9 @@ class Gaussian(metaclass=_PulseType): def __new__( cls, duration: Union[int, ParameterExpression], - amp: Union[complex, float, ParameterExpression], - sigma: Union[float, ParameterExpression], - angle: Optional[Union[float, ParameterExpression]] = None, + amp: ParameterValueType, + sigma: ParameterValueType, + angle: Optional[ParameterValueType] = None, name: Optional[str] = None, limit_amplitude: Optional[bool] = None, ) -> ScalableSymbolicPulse: @@ -750,7 +750,7 @@ def __new__( Args: duration: Pulse length in terms of the sampling period `dt`. amp: The magnitude of the amplitude of the Gaussian envelope. - Complex amp support will be deprecated. + Complex amp support is deprecated. sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically in the class docstring. angle: The angle of the complex amplitude of the Gaussian envelope. Default value 0. @@ -835,11 +835,11 @@ class GaussianSquare(metaclass=_PulseType): def __new__( cls, duration: Union[int, ParameterExpression], - amp: Union[complex, float, ParameterExpression], - sigma: Union[float, ParameterExpression], - width: Optional[Union[float, ParameterExpression]] = None, - angle: Optional[Union[float, ParameterExpression]] = None, - risefall_sigma_ratio: Optional[Union[float, ParameterExpression]] = None, + amp: ParameterValueType, + sigma: ParameterValueType, + width: Optional[ParameterValueType] = None, + angle: Optional[ParameterValueType] = None, + risefall_sigma_ratio: Optional[ParameterValueType] = None, name: Optional[str] = None, limit_amplitude: Optional[bool] = None, ) -> ScalableSymbolicPulse: @@ -848,7 +848,7 @@ def __new__( Args: duration: Pulse length in terms of the sampling period `dt`. amp: The magnitude of the amplitude of the Gaussian and square pulse. - Complex amp support will be deprecated. + Complex amp support is deprecated. sigma: A measure of how wide or narrow the Gaussian risefall is; see the class docstring for more details. width: The duration of the embedded square pulse. @@ -1378,10 +1378,10 @@ class Drag(metaclass=_PulseType): def __new__( cls, duration: Union[int, ParameterExpression], - amp: Union[complex, float, ParameterExpression], - sigma: Union[float, ParameterExpression], - beta: Union[float, ParameterExpression], - angle: Optional[Union[float, ParameterExpression]] = None, + amp: ParameterValueType, + sigma: ParameterValueType, + beta: ParameterValueType, + angle: Optional[ParameterValueType] = None, name: Optional[str] = None, limit_amplitude: Optional[bool] = None, ) -> ScalableSymbolicPulse: @@ -1390,7 +1390,7 @@ def __new__( Args: duration: Pulse length in terms of the sampling period `dt`. amp: The magnitude of the amplitude of the DRAG envelope. - Complex amp support will be deprecated. + Complex amp support is deprecated. sigma: A measure of how wide or narrow the Gaussian peak is; described mathematically in the class docstring. beta: The correction amplitude. @@ -1449,8 +1449,8 @@ class Constant(metaclass=_PulseType): def __new__( cls, duration: Union[int, ParameterExpression], - amp: Union[complex, float, ParameterExpression], - angle: Optional[Union[float, ParameterExpression]] = None, + amp: ParameterValueType, + angle: Optional[ParameterValueType] = None, name: Optional[str] = None, limit_amplitude: Optional[bool] = None, ) -> ScalableSymbolicPulse: @@ -1459,7 +1459,7 @@ def __new__( Args: duration: Pulse length in terms of the sampling period `dt`. amp: The magnitude of the amplitude of the square envelope. - Complex amp support will be deprecated. + Complex amp support is deprecated. angle: The angle of the complex amplitude of the square envelope. Default value 0. name: Display name for this pulse envelope. limit_amplitude: If ``True``, then limit the amplitude of the diff --git a/releasenotes/notes/deprecate-complex-amp-41381bd9722bc878.yaml b/releasenotes/notes/deprecate-complex-amp-41381bd9722bc878.yaml new file mode 100644 index 000000000000..23eadb7ff391 --- /dev/null +++ b/releasenotes/notes/deprecate-complex-amp-41381bd9722bc878.yaml @@ -0,0 +1,16 @@ +--- +deprecations: + - | + Initializing a :class:`~qiskit.pulse.library.ScalableSymbolicPulse` with complex `amp` value is now deprecated. + This change also affects the following library pulses: + + * :class:`~qiskit.pulse.library.Gaussian` + * :class:`~qiskit.pulse.library.GaussianSquare` + * :class:`~qiskit.pulse.library.Drag` + * :class:`~qiskit.pulse.library.Constant` + + Initializing them with complex `amp` is now deprecated as well. + + Instead, one should use two floats for the `amp` and `angle` parameters, where `amp` represents the + magnitude of the complex amplitude, and `angle` represents the angle of the complex amplitude. i.e. the + complex amplitude is given by `amp` * exp(1j * `angle`). diff --git a/test/python/compiler/test_assembler.py b/test/python/compiler/test_assembler.py index 04c8b3616a21..debd47e3a48b 100644 --- a/test/python/compiler/test_assembler.py +++ b/test/python/compiler/test_assembler.py @@ -1278,12 +1278,21 @@ def test_assemble_schedule_enum(self): def test_assemble_parametric(self): """Test that parametric pulses can be assembled properly into a PulseQobj.""" + amp = [0.5, 0.6, 1, 0.2] + angle = [np.pi / 2, 0.6, 0, 0] sched = pulse.Schedule(name="test_parametric") - sched += Play(pulse.Gaussian(duration=25, sigma=4, amp=0.5j), DriveChannel(0)) - sched += Play(pulse.Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4), DriveChannel(1)) - sched += Play(pulse.Constant(duration=25, amp=1), DriveChannel(2)) + sched += Play( + pulse.Gaussian(duration=25, sigma=4, amp=amp[0], angle=angle[0]), DriveChannel(0) + ) + sched += Play( + pulse.Drag(duration=25, amp=amp[1], angle=angle[1], sigma=7.8, beta=4), DriveChannel(1) + ) + sched += Play(pulse.Constant(duration=25, amp=amp[2], angle=angle[2]), DriveChannel(2)) sched += ( - Play(pulse.GaussianSquare(duration=150, amp=0.2, sigma=8, width=140), MeasureChannel(0)) + Play( + pulse.GaussianSquare(duration=150, amp=amp[3], angle=angle[3], sigma=8, width=140), + MeasureChannel(0), + ) << sched.duration ) backend = FakeOpenPulse3Q() @@ -1302,16 +1311,24 @@ def test_assemble_parametric(self): self.assertEqual(qobj_insts[1].pulse_shape, "drag") self.assertEqual(qobj_insts[2].pulse_shape, "constant") self.assertEqual(qobj_insts[3].pulse_shape, "gaussian_square") - self.assertDictEqual(qobj_insts[0].parameters, {"duration": 25, "sigma": 4, "amp": 0.5j}) self.assertDictEqual( - qobj_insts[1].parameters, {"duration": 25, "sigma": 7.8, "amp": 0.2 + 0.3j, "beta": 4} + qobj_insts[0].parameters, + {"duration": 25, "sigma": 4, "amp": amp[0] * np.exp(1j * angle[0])}, ) - self.assertDictEqual(qobj_insts[2].parameters, {"duration": 25, "amp": 1}) self.assertDictEqual( - qobj_insts[3].parameters, {"duration": 150, "sigma": 8, "amp": 0.2, "width": 140} + qobj_insts[1].parameters, + {"duration": 25, "sigma": 7.8, "amp": amp[1] * np.exp(1j * angle[1]), "beta": 4}, + ) + self.assertDictEqual( + qobj_insts[2].parameters, {"duration": 25, "amp": amp[2] * np.exp(1j * angle[2])} + ) + self.assertDictEqual( + qobj_insts[3].parameters, + {"duration": 150, "sigma": 8, "amp": amp[3] * np.exp(1j * angle[3]), "width": 140}, ) self.assertEqual( - qobj.to_dict()["experiments"][0]["instructions"][0]["parameters"]["amp"], 0.5j + qobj.to_dict()["experiments"][0]["instructions"][0]["parameters"]["amp"], + amp[0] * np.exp(1j * angle[0]), ) def test_assemble_parametric_unsupported(self): @@ -1319,7 +1336,9 @@ def test_assemble_parametric_unsupported(self): by the backend during assemble time. """ sched = pulse.Schedule(name="test_parametric_to_sample_pulse") - sched += Play(pulse.Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4), DriveChannel(1)) + sched += Play( + pulse.Drag(duration=25, amp=0.5, angle=-0.3, sigma=7.8, beta=4), DriveChannel(1) + ) sched += Play(pulse.Constant(duration=25, amp=1), DriveChannel(2)) backend = FakeOpenPulse3Q() diff --git a/test/python/pulse/test_parameter_manager.py b/test/python/pulse/test_parameter_manager.py index d395461ac3b1..e88e547e06eb 100644 --- a/test/python/pulse/test_parameter_manager.py +++ b/test/python/pulse/test_parameter_manager.py @@ -352,7 +352,8 @@ def test_complex_valued_parameter(self): with self.assertWarns(PendingDeprecationWarning): assigned = visitor.visit(test_obj) - ref_obj = pulse.Constant(duration=160, amp=1j * 0.1) + with self.assertWarns(DeprecationWarning): + ref_obj = pulse.Constant(duration=160, amp=1j * 0.1) self.assertEqual(assigned, ref_obj) @@ -368,7 +369,8 @@ def test_complex_value_to_parameter(self): with self.assertWarns(PendingDeprecationWarning): assigned = visitor.visit(test_obj) - ref_obj = pulse.Constant(duration=160, amp=1j * 0.1) + with self.assertWarns(DeprecationWarning): + ref_obj = pulse.Constant(duration=160, amp=1j * 0.1) self.assertEqual(assigned, ref_obj) diff --git a/test/python/pulse/test_pulse_lib.py b/test/python/pulse/test_pulse_lib.py index 675864c323af..575360f81758 100644 --- a/test/python/pulse/test_pulse_lib.py +++ b/test/python/pulse/test_pulse_lib.py @@ -143,12 +143,12 @@ class TestParametricPulses(QiskitTestCase): def test_construction(self): """Test that parametric pulses can be constructed without error.""" - Gaussian(duration=25, sigma=4, amp=0.5j) + Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2) GaussianSquare(duration=150, amp=0.2, sigma=8, width=140) GaussianSquare(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=2.5) + Constant(duration=150, amp=0.5, angle=np.pi * 0.23) + Drag(duration=25, amp=0.6, sigma=7.8, beta=4, angle=np.pi * 0.54) GaussianDeriv(duration=150, amp=0.2, sigma=8) - Constant(duration=150, amp=0.1 + 0.4j) - Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4) Sin(duration=25, amp=0.5, freq=0.1, phase=0.5, angle=0.5) Cos(duration=30, amp=0.5, freq=0.1, phase=-0.5) Sawtooth(duration=40, amp=0.5, freq=0.2, phase=3.14) @@ -164,17 +164,20 @@ def test_complex_amp_deprecation(self): and that pulses are equivalent.""" # Test deprecation warnings and errors: - with self.assertWarns(PendingDeprecationWarning): + with self.assertWarns(DeprecationWarning): Gaussian(duration=25, sigma=4, amp=0.5j) - with self.assertWarns(PendingDeprecationWarning): + with self.assertWarns(DeprecationWarning): GaussianSquare(duration=125, sigma=4, amp=0.5j, width=100) - with self.assertRaises(PulseError): - Gaussian(duration=25, sigma=4, amp=0.5j, angle=1) - with self.assertRaises(PulseError): - GaussianSquare(duration=125, sigma=4, amp=0.5j, width=100, angle=0.1) + with self.assertWarns(DeprecationWarning): + with self.assertRaises(PulseError): + Gaussian(duration=25, sigma=4, amp=0.5j, angle=1) + with self.assertWarns(DeprecationWarning): + with self.assertRaises(PulseError): + GaussianSquare(duration=125, sigma=4, amp=0.5j, width=100, angle=0.1) # Test that new and old API pulses are the same: - gauss_pulse_complex_amp = Gaussian(duration=25, sigma=4, amp=0.5j) + with self.assertWarns(DeprecationWarning): + gauss_pulse_complex_amp = Gaussian(duration=25, sigma=4, amp=0.5j) gauss_pulse_amp_angle = Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2) np.testing.assert_almost_equal( gauss_pulse_amp_angle.get_waveform().samples, @@ -183,7 +186,7 @@ def test_complex_amp_deprecation(self): def test_gaussian_pulse(self): """Test that Gaussian sample pulse matches the pulse library.""" - gauss = Gaussian(duration=25, sigma=4, amp=0.5j) + gauss = Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2) sample_pulse = gauss.get_waveform() self.assertIsInstance(sample_pulse, Waveform) pulse_lib_gauss = gaussian(duration=25, sigma=4, amp=0.5j, zero_ends=True).samples @@ -191,14 +194,16 @@ def test_gaussian_pulse(self): def test_gaussian_square_pulse(self): """Test that GaussianSquare sample pulse matches the pulse library.""" - gauss_sq = GaussianSquare(duration=125, sigma=4, amp=0.5j, width=100) + gauss_sq = GaussianSquare(duration=125, sigma=4, amp=0.5, width=100, angle=np.pi / 2) sample_pulse = gauss_sq.get_waveform() self.assertIsInstance(sample_pulse, Waveform) pulse_lib_gauss_sq = gaussian_square( duration=125, sigma=4, amp=0.5j, width=100, zero_ends=True ).samples np.testing.assert_almost_equal(sample_pulse.samples, pulse_lib_gauss_sq) - gauss_sq = GaussianSquare(duration=125, sigma=4, amp=0.5j, risefall_sigma_ratio=3.125) + gauss_sq = GaussianSquare( + duration=125, sigma=4, amp=0.5, risefall_sigma_ratio=3.125, angle=np.pi / 2 + ) sample_pulse = gauss_sq.get_waveform() self.assertIsInstance(sample_pulse, Waveform) pulse_lib_gauss_sq = gaussian_square( @@ -210,14 +215,17 @@ def test_gauss_square_extremes(self): """Test that the gaussian square pulse can build a gaussian.""" duration = 125 sigma = 4 - amp = 0.5j - gaus_square = GaussianSquare(duration=duration, sigma=sigma, amp=amp, width=0) - gaus = Gaussian(duration=duration, sigma=sigma, amp=amp) + amp = 0.5 + angle = np.pi / 2 + gaus_square = GaussianSquare(duration=duration, sigma=sigma, amp=amp, width=0, angle=angle) + gaus = Gaussian(duration=duration, sigma=sigma, amp=amp, angle=angle) np.testing.assert_almost_equal( gaus_square.get_waveform().samples, gaus.get_waveform().samples ) - gaus_square = GaussianSquare(duration=duration, sigma=sigma, amp=amp, width=121) - const = Constant(duration=duration, amp=amp) + gaus_square = GaussianSquare( + duration=duration, sigma=sigma, amp=amp, width=121, angle=angle + ) + const = Constant(duration=duration, amp=amp, angle=angle) np.testing.assert_almost_equal( gaus_square.get_waveform().samples[2:-2], const.get_waveform().samples[2:-2] ) @@ -226,7 +234,7 @@ def test_gauss_square_passes_validation_after_construction(self): """Test that parameter validation is consistent before and after construction. This previously used to raise an exception: see gh-7882.""" - pulse = GaussianSquare(duration=125, sigma=4, amp=0.5j, width=100) + pulse = GaussianSquare(duration=125, sigma=4, amp=0.5, width=100, angle=np.pi / 2) pulse.validate_parameters() def test_gaussian_square_drag_pulse(self): @@ -363,7 +371,7 @@ def test_gaussian_square_echo_active_amp_validation(self): def test_drag_pulse(self): """Test that the Drag sample pulse matches the pulse library.""" - drag = Drag(duration=25, sigma=4, amp=0.5j, beta=1) + drag = Drag(duration=25, sigma=4, amp=0.5, beta=1, angle=np.pi / 2) sample_pulse = drag.get_waveform() self.assertIsInstance(sample_pulse, Waveform) pulse_lib_drag = pl_drag(duration=25, sigma=4, amp=0.5j, beta=1, zero_ends=True).samples @@ -373,25 +381,26 @@ def test_drag_validation(self): """Test drag parameter validation, specifically the beta validation.""" duration = 25 sigma = 4 - amp = 0.5j + amp = 0.5 + angle = np.pi / 2 beta = 1 - wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta) + wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta, angle=angle) samples = wf.get_waveform().samples self.assertTrue(max(np.abs(samples)) <= 1) with self.assertRaises(PulseError): wf = Drag(duration=duration, sigma=sigma, amp=1.2, beta=beta) beta = sigma**2 with self.assertRaises(PulseError): - wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta) + wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta, angle=angle) # If sigma is high enough, side peaks fall out of range and norm restriction is met sigma = 100 - wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta) + wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta, angle=angle) def test_drag_beta_validation(self): """Test drag beta parameter validation.""" - def check_drag(duration, sigma, amp, beta): - wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta) + def check_drag(duration, sigma, amp, beta, angle=0): + wf = Drag(duration=duration, sigma=sigma, amp=amp, beta=beta, angle=angle) samples = wf.get_waveform().samples self.assertTrue(max(np.abs(samples)) <= 1) @@ -401,7 +410,7 @@ def check_drag(duration, sigma, amp, beta): check_drag(duration=50, sigma=16, amp=-1, beta=2) check_drag(duration=50, sigma=16, amp=1, beta=-2) check_drag(duration=50, sigma=16, amp=1, beta=6) - check_drag(duration=50, sigma=16, amp=-0.5j, beta=25) + check_drag(duration=50, sigma=16, amp=0.5, beta=25, angle=-np.pi / 2) with self.assertRaises(PulseError): check_drag(duration=50, sigma=16, amp=1, beta=20) with self.assertRaises(PulseError): @@ -541,8 +550,10 @@ def test_sech_deriv_pulse(self): def test_constant_samples(self): """Test the constant pulse and its sampled construction.""" - const = Constant(duration=150, amp=0.1 + 0.4j) - self.assertEqual(const.get_waveform().samples[0], 0.1 + 0.4j) + amp = 0.6 + angle = np.pi * 0.7 + const = Constant(duration=150, amp=amp, angle=angle) + self.assertEqual(const.get_waveform().samples[0], amp * np.exp(1j * angle)) self.assertEqual(len(const.get_waveform().samples), 150) def test_parameters(self): @@ -557,11 +568,6 @@ def test_repr(self): """Test the repr methods for parametric pulses.""" gaus = Gaussian(duration=25, amp=0.7, sigma=4, angle=0.3) self.assertEqual(repr(gaus), "Gaussian(duration=25, sigma=4, amp=0.7, angle=0.3)") - gaus = Gaussian( - duration=25, amp=0.1 + 0.7j, sigma=4 - ) # Should be removed once the deprecation of complex - # amp is completed. - self.assertEqual(repr(gaus), "Gaussian(duration=25, sigma=4, amp=(0.1+0.7j), angle=0)") gaus_square = GaussianSquare(duration=20, sigma=30, amp=1.0, width=3) self.assertEqual( repr(gaus_square), "GaussianSquare(duration=20, sigma=30, width=3, amp=1.0, angle=0)" @@ -633,7 +639,7 @@ def test_repr(self): def test_param_validation(self): """Test that parametric pulse parameters are validated when initialized.""" with self.assertRaises(PulseError): - Gaussian(duration=25, sigma=0, amp=0.5j) + Gaussian(duration=25, sigma=0, amp=0.5, angle=np.pi / 2) with self.assertRaises(PulseError): GaussianSquare(duration=150, amp=0.2, sigma=8) with self.assertRaises(PulseError): @@ -662,43 +668,45 @@ def test_param_validation(self): gaussian_square_echo(duration=150, amp=0.2, sigma=8, risefall_sigma_ratio=10) with self.assertRaises(PulseError): - Constant(duration=150, amp=0.9 + 0.8j) + Constant(duration=150, amp=1.5, angle=np.pi * 0.8) with self.assertRaises(PulseError): - Drag(duration=25, amp=0.2 + 0.3j, sigma=-7.8, beta=4) + Drag(duration=25, amp=0.5, sigma=-7.8, beta=4, angle=np.pi / 3) def test_gaussian_limit_amplitude(self): """Test that the check for amplitude less than or equal to 1 can be disabled.""" with self.assertRaises(PulseError): - Gaussian(duration=100, sigma=1.0, amp=1.1 + 0.8j) + Gaussian(duration=100, sigma=1.0, amp=1.7, angle=np.pi * 1.1) with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): - waveform = Gaussian(duration=100, sigma=1.0, amp=1.1 + 0.8j) + waveform = Gaussian(duration=100, sigma=1.0, amp=1.7, angle=np.pi * 1.1) self.assertGreater(np.abs(waveform.amp), 1.0) def test_gaussian_limit_amplitude_per_instance(self): """Test that the check for amplitude per instance.""" with self.assertRaises(PulseError): - Gaussian(duration=100, sigma=1.0, amp=1.1 + 0.8j) + Gaussian(duration=100, sigma=1.0, amp=1.6, angle=np.pi / 2.5) - waveform = Gaussian(duration=100, sigma=1.0, amp=1.1 + 0.8j, limit_amplitude=False) + waveform = Gaussian( + duration=100, sigma=1.0, amp=1.6, angle=np.pi / 2.5, limit_amplitude=False + ) self.assertGreater(np.abs(waveform.amp), 1.0) def test_gaussian_square_limit_amplitude(self): """Test that the check for amplitude less than or equal to 1 can be disabled.""" with self.assertRaises(PulseError): - GaussianSquare(duration=100, sigma=1.0, amp=1.1 + 0.8j, width=10) + GaussianSquare(duration=100, sigma=1.0, amp=1.5, width=10, angle=np.pi / 5) with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): - waveform = GaussianSquare(duration=100, sigma=1.0, amp=1.1 + 0.8j, width=10) + waveform = GaussianSquare(duration=100, sigma=1.0, amp=1.5, width=10, angle=np.pi / 5) self.assertGreater(np.abs(waveform.amp), 1.0) def test_gaussian_square_limit_amplitude_per_instance(self): """Test that the check for amplitude per instance.""" with self.assertRaises(PulseError): - GaussianSquare(duration=100, sigma=1.0, amp=1.1 + 0.8j, width=10) + GaussianSquare(duration=100, sigma=1.0, amp=1.5, width=10, angle=np.pi / 3) waveform = GaussianSquare( - duration=100, sigma=1.0, amp=1.1 + 0.8j, width=10, limit_amplitude=False + duration=100, sigma=1.0, amp=1.5, width=10, angle=np.pi / 3, limit_amplitude=False ) self.assertGreater(np.abs(waveform.amp), 1.0) @@ -743,35 +751,37 @@ def test_gaussian_square_echo_limit_amplitude_per_instance(self): def test_drag_limit_amplitude(self): """Test that the check for amplitude less than or equal to 1 can be disabled.""" with self.assertRaises(PulseError): - Drag(duration=100, sigma=1.0, beta=1.0, amp=1.1 + 0.8j) + Drag(duration=100, sigma=1.0, beta=1.0, amp=1.8, angle=np.pi * 0.3) with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): - waveform = Drag(duration=100, sigma=1.0, beta=1.0, amp=1.1 + 0.8j) + waveform = Drag(duration=100, sigma=1.0, beta=1.0, amp=1.8, angle=np.pi * 0.3) self.assertGreater(np.abs(waveform.amp), 1.0) def test_drag_limit_amplitude_per_instance(self): """Test that the check for amplitude per instance.""" with self.assertRaises(PulseError): - Drag(duration=100, sigma=1.0, beta=1.0, amp=1.1 + 0.8j) + Drag(duration=100, sigma=1.0, beta=1.0, amp=1.8, angle=np.pi * 0.3) - waveform = Drag(duration=100, sigma=1.0, beta=1.0, amp=1.1 + 0.8j, limit_amplitude=False) + waveform = Drag( + duration=100, sigma=1.0, beta=1.0, amp=1.8, angle=np.pi * 0.3, limit_amplitude=False + ) self.assertGreater(np.abs(waveform.amp), 1.0) def test_constant_limit_amplitude(self): """Test that the check for amplitude less than or equal to 1 can be disabled.""" with self.assertRaises(PulseError): - Constant(duration=100, amp=1.1 + 0.8j) + Constant(duration=100, amp=1.3, angle=0.1) with patch("qiskit.pulse.library.pulse.Pulse.limit_amplitude", new=False): - waveform = Constant(duration=100, amp=1.1 + 0.8j) + waveform = Constant(duration=100, amp=1.3, angle=0.1) self.assertGreater(np.abs(waveform.amp), 1.0) def test_constant_limit_amplitude_per_instance(self): """Test that the check for amplitude per instance.""" with self.assertRaises(PulseError): - Constant(duration=100, amp=1.1 + 0.8j) + Constant(duration=100, amp=1.6, angle=0.5) - waveform = Constant(duration=100, amp=1.1 + 0.8j, limit_amplitude=False) + waveform = Constant(duration=100, amp=1.6, angle=0.5, limit_amplitude=False) self.assertGreater(np.abs(waveform.amp), 1.0) def test_sin_limit_amplitude(self): diff --git a/test/python/pulse/test_schedule.py b/test/python/pulse/test_schedule.py index e8f6a2f5a84f..654b4ffe39f9 100644 --- a/test/python/pulse/test_schedule.py +++ b/test/python/pulse/test_schedule.py @@ -337,8 +337,8 @@ def test_schedule_with_acquire_on_single_qubit(self): def test_parametric_commands_in_sched(self): """Test that schedules can be built with parametric commands.""" sched = Schedule(name="test_parametric") - sched += Play(Gaussian(duration=25, sigma=4, amp=0.5j), DriveChannel(0)) - sched += Play(Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4), DriveChannel(1)) + sched += Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2), DriveChannel(0)) + sched += Play(Drag(duration=25, amp=0.4, angle=0.5, sigma=7.8, beta=4), DriveChannel(1)) sched += Play(Constant(duration=25, amp=1), DriveChannel(2)) sched_duration = sched.duration sched += ( diff --git a/test/python/pulse/test_transforms.py b/test/python/pulse/test_transforms.py index d2d4b3567860..8bb66631b3e4 100644 --- a/test/python/pulse/test_transforms.py +++ b/test/python/pulse/test_transforms.py @@ -437,14 +437,14 @@ def test_parametric_pulses_with_duplicates(self): """Test with parametric pulses.""" schedule = Schedule() drive_channel = DriveChannel(0) - schedule += Play(Gaussian(duration=25, sigma=4, amp=0.5j), drive_channel) - schedule += Play(Gaussian(duration=25, sigma=4, amp=0.5j), drive_channel) + schedule += Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2), drive_channel) + schedule += Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2), drive_channel) schedule += Play(GaussianSquare(duration=150, amp=0.2, sigma=8, width=140), drive_channel) schedule += Play(GaussianSquare(duration=150, amp=0.2, sigma=8, width=140), drive_channel) - schedule += Play(Constant(duration=150, amp=0.1 + 0.4j), drive_channel) - schedule += Play(Constant(duration=150, amp=0.1 + 0.4j), drive_channel) - schedule += Play(Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4), drive_channel) - schedule += Play(Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4), drive_channel) + schedule += Play(Constant(duration=150, amp=0.5, angle=0.7), drive_channel) + schedule += Play(Constant(duration=150, amp=0.5, angle=0.7), drive_channel) + schedule += Play(Drag(duration=25, amp=0.4, angle=-0.3, sigma=7.8, beta=4), drive_channel) + schedule += Play(Drag(duration=25, amp=0.4, angle=-0.3, sigma=7.8, beta=4), drive_channel) compressed_schedule = transforms.compress_pulses([schedule]) original_pulse_ids = get_pulse_ids([schedule]) @@ -456,14 +456,14 @@ def test_parametric_pulses_with_no_duplicates(self): """Test parametric pulses with no duplicates.""" schedule = Schedule() drive_channel = DriveChannel(0) - schedule += Play(Gaussian(duration=25, sigma=4, amp=0.5j), drive_channel) - schedule += Play(Gaussian(duration=25, sigma=4, amp=0.49j), drive_channel) + schedule += Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=np.pi / 2), drive_channel) + schedule += Play(Gaussian(duration=25, sigma=4, amp=0.49, angle=np.pi / 2), drive_channel) schedule += Play(GaussianSquare(duration=150, amp=0.2, sigma=8, width=140), drive_channel) schedule += Play(GaussianSquare(duration=150, amp=0.19, sigma=8, width=140), drive_channel) - schedule += Play(Constant(duration=150, amp=0.1 + 0.4j), drive_channel) - schedule += Play(Constant(duration=150, amp=0.1 + 0.41j), drive_channel) - schedule += Play(Drag(duration=25, amp=0.2 + 0.3j, sigma=7.8, beta=4), drive_channel) - schedule += Play(Drag(duration=25, amp=0.2 + 0.31j, sigma=7.8, beta=4), drive_channel) + schedule += Play(Constant(duration=150, amp=0.5, angle=0.3), drive_channel) + schedule += Play(Constant(duration=150, amp=0.51, angle=0.3), drive_channel) + schedule += Play(Drag(duration=25, amp=0.5, angle=0.5, sigma=7.8, beta=4), drive_channel) + schedule += Play(Drag(duration=25, amp=0.5, angle=0.51, sigma=7.8, beta=4), drive_channel) compressed_schedule = transforms.compress_pulses([schedule]) original_pulse_ids = get_pulse_ids([schedule]) diff --git a/test/python/qobj/test_pulse_converter.py b/test/python/qobj/test_pulse_converter.py index df0b683b12ec..2bdfeea8708d 100644 --- a/test/python/qobj/test_pulse_converter.py +++ b/test/python/qobj/test_pulse_converter.py @@ -63,22 +63,27 @@ def test_drive_instruction(self): def test_gaussian_pulse_instruction(self): """Test that parametric pulses are correctly converted to PulseQobjInstructions.""" + amp = 0.3 + angle = -0.7 converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) - instruction = Play(Gaussian(duration=25, sigma=15, amp=-0.5 + 0.2j), DriveChannel(0)) + instruction = Play(Gaussian(duration=25, sigma=15, amp=amp, angle=angle), DriveChannel(0)) valid_qobj = PulseQobjInstruction( name="parametric_pulse", pulse_shape="gaussian", ch="d0", t0=0, - parameters={"duration": 25, "sigma": 15, "amp": -0.5 + 0.2j}, + parameters={"duration": 25, "sigma": 15, "amp": amp * np.exp(1j * angle)}, ) self.assertEqual(converter(0, instruction), valid_qobj) def test_gaussian_square_pulse_instruction(self): """Test that parametric pulses are correctly converted to PulseQobjInstructions.""" converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) + amp = 0.7 + angle = -0.6 instruction = Play( - GaussianSquare(duration=1500, sigma=15, amp=-0.5 + 0.2j, width=1300), MeasureChannel(1) + GaussianSquare(duration=1500, sigma=15, amp=amp, width=1300, angle=angle), + MeasureChannel(1), ) valid_qobj = PulseQobjInstruction( @@ -86,7 +91,12 @@ def test_gaussian_square_pulse_instruction(self): pulse_shape="gaussian_square", ch="m1", t0=10, - parameters={"duration": 1500, "sigma": 15, "amp": -0.5 + 0.2j, "width": 1300}, + parameters={ + "duration": 1500, + "sigma": 15, + "amp": amp * np.exp(1j * angle), + "width": 1300, + }, ) self.assertEqual(converter(10, instruction), valid_qobj) @@ -106,15 +116,19 @@ def test_constant_pulse_instruction(self): def test_drag_pulse_instruction(self): """Test that parametric pulses are correctly converted to PulseQobjInstructions.""" + amp = 0.7 + angle = -0.6 converter = InstructionToQobjConverter(PulseQobjInstruction, meas_level=2) - instruction = Play(Drag(duration=25, sigma=15, amp=-0.5 + 0.2j, beta=0.5), DriveChannel(0)) + instruction = Play( + Drag(duration=25, sigma=15, amp=amp, angle=angle, beta=0.5), DriveChannel(0) + ) valid_qobj = PulseQobjInstruction( name="parametric_pulse", pulse_shape="drag", ch="d0", t0=30, - parameters={"duration": 25, "sigma": 15, "amp": -0.5 + 0.2j, "beta": 0.5}, + parameters={"duration": 25, "sigma": 15, "amp": amp * np.exp(1j * angle), "beta": 0.5}, ) self.assertEqual(converter(30, instruction), valid_qobj) diff --git a/test/python/scheduler/test_basic_scheduler.py b/test/python/scheduler/test_basic_scheduler.py index 8b7944eb7883..b9245da8d0c2 100644 --- a/test/python/scheduler/test_basic_scheduler.py +++ b/test/python/scheduler/test_basic_scheduler.py @@ -12,6 +12,7 @@ """Test cases for the pulse scheduler passes.""" +from numpy import pi from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit, schedule from qiskit.circuit import Gate, Parameter from qiskit.circuit.library import U1Gate, U2Gate, U3Gate @@ -352,7 +353,9 @@ def test_parametric_input(self): qr = QuantumRegister(1) qc = QuantumCircuit(qr) qc.append(Gate("gauss", 1, []), qargs=[qr[0]]) - custom_gauss = Schedule(Play(Gaussian(duration=25, sigma=4, amp=0.5j), DriveChannel(0))) + custom_gauss = Schedule( + Play(Gaussian(duration=25, sigma=4, amp=0.5, angle=pi / 2), DriveChannel(0)) + ) self.inst_map.add("gauss", [0], custom_gauss) sched = schedule(qc, self.backend, inst_map=self.inst_map) self.assertEqual(sched.instructions[0], custom_gauss.instructions[0]) From 163875e5d7729fe98cca16c46fc6b12408976964 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 6 Jul 2023 16:00:54 +0100 Subject: [PATCH 172/172] Fix `CheckMap` with control-flow builder nested conditionals (#10395) The recursion inside the `CheckMap` pass was based on a custom `DAGCircuit.compose` solution, rather than the more standard qubit-argument-binding setup we normally use. This did not pass in the clbit ordering to the composition, which could cause it to fail to map nested conditional statements. Instead, we can just enter each DAG naturally, and use the regular wire map to access the physical indices being referred to. --- qiskit/transpiler/passes/utils/check_map.py | 50 +++++++++---------- ...map-nested-condition-1776f952f6c6722a.yaml | 6 +++ test/python/transpiler/test_check_map.py | 22 ++++++++ 3 files changed, 51 insertions(+), 27 deletions(-) create mode 100644 releasenotes/notes/fix-checkmap-nested-condition-1776f952f6c6722a.yaml diff --git a/qiskit/transpiler/passes/utils/check_map.py b/qiskit/transpiler/passes/utils/check_map.py index f71e5640f163..d56a709253c9 100644 --- a/qiskit/transpiler/passes/utils/check_map.py +++ b/qiskit/transpiler/passes/utils/check_map.py @@ -15,6 +15,7 @@ from qiskit.transpiler.basepasses import AnalysisPass from qiskit.transpiler.target import Target from qiskit.circuit.controlflow import ControlFlowOp +from qiskit.converters import circuit_to_dag class CheckMap(AnalysisPass): @@ -64,35 +65,30 @@ def run(self, dag): Args: dag (DAGCircuit): DAG to map. """ - from qiskit.converters import circuit_to_dag - - self.property_set[self.property_set_field] = True - if not self.qargs: + self.property_set[self.property_set_field] = True return + wire_map = {bit: index for index, bit in enumerate(dag.qubits)} + self.property_set[self.property_set_field] = self._recurse(dag, wire_map) - qubit_indices = {bit: index for index, bit in enumerate(dag.qubits)} + def _recurse(self, dag, wire_map) -> bool: for node in dag.op_nodes(include_directives=False): - is_controlflow_op = isinstance(node.op, ControlFlowOp) - if len(node.qargs) == 2 and not is_controlflow_op: - if dag.has_calibration_for(node): - continue - physical_q0 = qubit_indices[node.qargs[0]] - physical_q1 = qubit_indices[node.qargs[1]] - if (physical_q0, physical_q1) not in self.qargs: - self.property_set["check_map_msg"] = "{}({}, {}) failed".format( - node.name, - physical_q0, - physical_q1, - ) - self.property_set[self.property_set_field] = False - return - elif is_controlflow_op: - order = [qubit_indices[bit] for bit in node.qargs] + if isinstance(node.op, ControlFlowOp): for block in node.op.blocks: - dag_block = circuit_to_dag(block) - mapped_dag = dag.copy_empty_like() - mapped_dag.compose(dag_block, qubits=order) - self.run(mapped_dag) - if not self.property_set[self.property_set_field]: - return + inner_wire_map = { + inner: wire_map[outer] for inner, outer in zip(block.qubits, node.qargs) + } + if not self._recurse(circuit_to_dag(block), inner_wire_map): + return False + elif ( + len(node.qargs) == 2 + 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]], + ) + return False + return True diff --git a/releasenotes/notes/fix-checkmap-nested-condition-1776f952f6c6722a.yaml b/releasenotes/notes/fix-checkmap-nested-condition-1776f952f6c6722a.yaml new file mode 100644 index 000000000000..0805524cd7b0 --- /dev/null +++ b/releasenotes/notes/fix-checkmap-nested-condition-1776f952f6c6722a.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + The :class:`.CheckMap` transpiler pass will no longer spuriously error when dealing with nested + conditional structures created by the control-flow builder interface. See `#10394 + `__. diff --git a/test/python/transpiler/test_check_map.py b/test/python/transpiler/test_check_map.py index 8368e3cc94eb..27802c708a96 100644 --- a/test/python/transpiler/test_check_map.py +++ b/test/python/transpiler/test_check_map.py @@ -255,6 +255,28 @@ def test_nested_controlflow_false(self): pass_.run(dag) self.assertFalse(pass_.property_set["is_swap_mapped"]) + def test_nested_conditional_unusual_bit_order(self): + """Test that `CheckMap` succeeds when inner conditional blocks have clbits that are involved + in their own (nested conditionals), and the binding order is not the same as the + bit-definition order. See gh-10394.""" + qr = QuantumRegister(2, "q") + cr1 = ClassicalRegister(2, "c1") + cr2 = ClassicalRegister(2, "c2") + + # Note that the bits here are not in the same order as in the outer circuit object, but they + # are the same as the binding order in the `if_test`, so everything maps `{x: x}` and it + # should all be fine. This kind of thing is a staple of the control-flow builders. + inner_order = [cr2[0], cr1[0], cr2[1], cr1[1]] + inner = QuantumCircuit(qr, inner_order, cr1, cr2) + inner.cx(0, 1).c_if(cr2, 3) + + outer = QuantumCircuit(qr, cr1, cr2) + outer.if_test((cr1, 3), inner, outer.qubits, inner_order) + + pass_ = CheckMap(CouplingMap.from_line(2)) + pass_(outer) + self.assertTrue(pass_.property_set["is_swap_mapped"]) + if __name__ == "__main__": unittest.main()