From 70c2f7813c27c557a1681d4c6b461e2f07ebca61 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 7 Aug 2024 20:16:33 -0400 Subject: [PATCH 1/5] Remove init peephole optimization discrete basis check (#12898) * Fix target handling in discrete basis check In #12727 a check was added to the default init stage's construction to avoid running 2q gate consolidation in the presence of targets with only discrete gates. However the way the target was being used in this check was incorrect. The name for an instruction in the target should be used as its identifier and then if we need the object representation that should query the target for that object based on the name. However the check was doing this backwards getting a list of operation objects and then using the name contained in the object. This will cause issues for instructions that use custom names such as when there are tuned variants or a custom gate instance with a unique name. While there is some question over whether we need this check as we will run the consolidate 2q blocks pass as part of the optimization stage which will have the same effect, this opts to just fix the target usage for it to minimize the diff. Also while not the explicit goal of this check it is protecting against some bugs in the consolidate blocks pass that occur when custom gates are used. So for the short term this check is retained, but in the future when these bugs in consolidate blocks are fixed we can revisit whether we want to remove the conditional logic. * Remove check and fix ConsolidateBlocks bug This commit pivots this PR branch to just remove the additional logic around skipping the optimization passes for discrete basis sets. The value the check was actually providing was not around a discrete basis set target and instead was to workaround a bug in the consolidate blocks pass. If a discrete basis set target was used this would still fail because we will unconditionally call `ConsolidateBlocks` during the optimization stage. This commit opts to just remove the extra complexity of the conditional execution of the peephole optimization passes and instead just fix the underlying bug in `ConsolidateBlocks` and remove the check. --- .../passes/optimization/consolidate_blocks.py | 8 ++- .../preset_passmanagers/builtin_plugins.py | 64 +++---------------- ...ustom-gate-no-target-e2d1e0b0ee7ace11.yaml | 9 +++ test/python/compiler/test_transpiler.py | 37 ----------- .../transpiler/test_consolidate_blocks.py | 19 +++++- 5 files changed, 44 insertions(+), 93 deletions(-) create mode 100644 releasenotes/notes/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml diff --git a/qiskit/transpiler/passes/optimization/consolidate_blocks.py b/qiskit/transpiler/passes/optimization/consolidate_blocks.py index 72a08efe0f7d..12f7285af9dc 100644 --- a/qiskit/transpiler/passes/optimization/consolidate_blocks.py +++ b/qiskit/transpiler/passes/optimization/consolidate_blocks.py @@ -28,6 +28,7 @@ from qiskit.transpiler.passes.synthesis import unitary_synthesis from qiskit.circuit.controlflow import CONTROL_FLOW_OP_NAMES from qiskit._accelerate.convert_2q_block_matrix import blocks_to_matrix +from qiskit.exceptions import QiskitError from .collect_1q_runs import Collect1qRuns from .collect_2q_blocks import Collect2qBlocks @@ -125,7 +126,12 @@ def run(self, dag): qc.append(nd.op, [q[block_index_map[i]] for i in nd.qargs]) unitary = UnitaryGate(Operator(qc), check_input=False) else: - matrix = blocks_to_matrix(block, block_index_map) + try: + matrix = blocks_to_matrix(block, block_index_map) + except QiskitError: + # If building a matrix for the block fails we should not consolidate it + # because there is nothing we can do with it. + continue unitary = UnitaryGate(matrix, check_input=False) max_2q_depth = 20 # If depth > 20, there will be 1q gates to consolidate. diff --git a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py index d7e6a3b2c174..7e3e2611546f 100644 --- a/qiskit/transpiler/preset_passmanagers/builtin_plugins.py +++ b/qiskit/transpiler/preset_passmanagers/builtin_plugins.py @@ -14,7 +14,6 @@ import os -from qiskit.circuit import Instruction from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries from qiskit.transpiler.passmanager import PassManager from qiskit.transpiler.exceptions import TranspilerError @@ -66,7 +65,6 @@ CYGate, SXGate, SXdgGate, - get_standard_gate_name_mapping, ) from qiskit.utils.parallel import CPU_COUNT from qiskit import user_config @@ -173,58 +171,16 @@ def pass_manager(self, pass_manager_config, optimization_level=None) -> PassMana ) ) init.append(CommutativeCancellation()) - # skip peephole optimization before routing if target basis gate set is discrete, - # i.e. only consists of Cliffords that an user might want to keep - # use rz, sx, x, cx as basis, rely on physical optimziation to fix everything later one - stdgates = get_standard_gate_name_mapping() - - def _is_one_op_non_discrete(ops): - """Checks if one operation in `ops` is not discrete, i.e. is parameterizable - Args: - ops (List(Operation)): list of operations to check - Returns - True if at least one operation in `ops` is not discrete, False otherwise - """ - found_one_continuous_gate = False - for op in ops: - if isinstance(op, str): - if op in _discrete_skipped_ops: - continue - op = stdgates.get(op, None) - - if op is not None and op.name in _discrete_skipped_ops: - continue - - if op is None or not isinstance(op, Instruction): - return False - - if len(op.params) > 0: - found_one_continuous_gate = True - return found_one_continuous_gate - - target = pass_manager_config.target - basis = pass_manager_config.basis_gates - # consolidate gates before routing if the user did not specify a discrete basis gate, i.e. - # * no target or basis gate set has been specified - # * target has been specified, and we have one non-discrete gate in the target's spec - # * basis gates have been specified, and we have one non-discrete gate in that set - do_consolidate_blocks_init = target is None and basis is None - do_consolidate_blocks_init |= target is not None and _is_one_op_non_discrete( - target.operations - ) - do_consolidate_blocks_init |= basis is not None and _is_one_op_non_discrete(basis) - - if do_consolidate_blocks_init: - init.append(Collect2qBlocks()) - init.append(ConsolidateBlocks()) - # If approximation degree is None that indicates a request to approximate up to the - # error rates in the target. However, in the init stage we don't yet know the target - # qubits being used to figure out the fidelity so just use the default fidelity parameter - # in this case. - if pass_manager_config.approximation_degree is not None: - init.append(Split2QUnitaries(pass_manager_config.approximation_degree)) - else: - init.append(Split2QUnitaries()) + init.append(Collect2qBlocks()) + init.append(ConsolidateBlocks()) + # If approximation degree is None that indicates a request to approximate up to the + # error rates in the target. However, in the init stage we don't yet know the target + # qubits being used to figure out the fidelity so just use the default fidelity parameter + # in this case. + if pass_manager_config.approximation_degree is not None: + init.append(Split2QUnitaries(pass_manager_config.approximation_degree)) + else: + init.append(Split2QUnitaries()) else: raise TranspilerError(f"Invalid optimization level {optimization_level}") return init diff --git a/releasenotes/notes/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml b/releasenotes/notes/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml new file mode 100644 index 000000000000..e4cf03778e3a --- /dev/null +++ b/releasenotes/notes/fix-consolidate-blocks-custom-gate-no-target-e2d1e0b0ee7ace11.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fixed a bug in the :class:`.ConsolidateBlocks` transpiler pass, when the + input circuit contains a custom opaque gate and neither the + ``basis_gates`` or ``target`` options are set the pass would raise a + ``QiskitError`` and fail. This has been corrected so that the in these + situations the transpiler pass will not consolidate the block identified + containing a custom gate instead of failing. diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py index f465c9997039..a348ad8b749d 100644 --- a/test/python/compiler/test_transpiler.py +++ b/test/python/compiler/test_transpiler.py @@ -84,7 +84,6 @@ from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout -from qiskit.transpiler.passes.optimization.split_2q_unitaries import Split2QUnitaries from qiskit.transpiler.passmanager_config import PassManagerConfig from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager @@ -874,42 +873,6 @@ def test_do_not_run_gatedirection_with_symmetric_cm(self): transpile(circ, coupling_map=coupling_map, initial_layout=layout) self.assertFalse(mock_pass.called) - def tests_conditional_run_split_2q_unitaries(self): - """Tests running `Split2QUnitaries` when basis gate set is (non-) discrete""" - qc = QuantumCircuit(3) - qc.sx(0) - qc.t(0) - qc.cx(0, 1) - qc.cx(1, 2) - - orig_pass = Split2QUnitaries() - with patch.object(Split2QUnitaries, "run", wraps=orig_pass.run) as mock_pass: - basis = ["t", "sx", "cx"] - backend = GenericBackendV2(3, basis_gates=basis) - transpile(qc, backend=backend) - transpile(qc, basis_gates=basis) - transpile(qc, target=backend.target) - self.assertFalse(mock_pass.called) - - orig_pass = Split2QUnitaries() - with patch.object(Split2QUnitaries, "run", wraps=orig_pass.run) as mock_pass: - basis = ["rz", "sx", "cx"] - backend = GenericBackendV2(3, basis_gates=basis) - transpile(qc, backend=backend, optimization_level=2) - self.assertTrue(mock_pass.called) - mock_pass.called = False - transpile(qc, basis_gates=basis, optimization_level=2) - self.assertTrue(mock_pass.called) - mock_pass.called = False - transpile(qc, target=backend.target, optimization_level=2) - self.assertTrue(mock_pass.called) - mock_pass.called = False - transpile(qc, backend=backend, optimization_level=3) - self.assertTrue(mock_pass.called) - mock_pass.called = False - transpile(qc, basis_gates=basis, optimization_level=3) - self.assertTrue(mock_pass.called) - def test_optimize_to_nothing(self): """Optimize gates up to fixed point in the default pipeline See https://github.com/Qiskit/qiskit-terra/issues/2035 diff --git a/test/python/transpiler/test_consolidate_blocks.py b/test/python/transpiler/test_consolidate_blocks.py index 8a11af2bd688..1984ad1a3dc4 100644 --- a/test/python/transpiler/test_consolidate_blocks.py +++ b/test/python/transpiler/test_consolidate_blocks.py @@ -17,7 +17,7 @@ import unittest import numpy as np -from qiskit.circuit import QuantumCircuit, QuantumRegister, IfElseOp +from qiskit.circuit import QuantumCircuit, QuantumRegister, IfElseOp, Gate from qiskit.circuit.library import U2Gate, SwapGate, CXGate, CZGate, UnitaryGate from qiskit.converters import circuit_to_dag from qiskit.transpiler.passes import ConsolidateBlocks @@ -553,6 +553,23 @@ def test_inverted_order(self): ) self.assertEqual(expected, actual) + def test_custom_no_target(self): + """Test pass doesn't fail with custom gate.""" + + class MyCustomGate(Gate): + """Custom gate.""" + + def __init__(self): + super().__init__(name="my_custom", num_qubits=2, params=[]) + + qc = QuantumCircuit(2) + qc.append(MyCustomGate(), [0, 1]) + + pm = PassManager([Collect2qBlocks(), ConsolidateBlocks()]) + res = pm.run(qc) + + self.assertEqual(res, qc) + if __name__ == "__main__": unittest.main() From fa5510c3af382cb51256af83a43c2009ca775c03 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 7 Aug 2024 20:18:43 -0400 Subject: [PATCH 2/5] Fix short circuit detection for basis translator (#12899) The BasisTranslator transpiler pass has a check at the very start that is designed to return fast if there is nothing to translate; in other words if the instructions in the circuit are already a subset of instructions supported by the target. This avoid doing a lot of unecessary work to determine this later during the operation of the pass. However, this check was not correctly constructed because of a type mismatch and would only ever get triggered if the input circuit was empty. The source basis is collected as a `Set[Tuple[str, int]]` where each tuple is the name and num of qubits for each operation in the circuit. While the target basis is just a `Set[str]` for the names supported on the target. This mismatch caused the subset check to never return True unless it was empty thereby bypassing the intent of the short circuit path. This commit fixes the logic by constructing a temporary set of just the source names to evaluate whether we should return early or not. --- qiskit/transpiler/passes/basis/basis_translator.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/basis/basis_translator.py b/qiskit/transpiler/passes/basis/basis_translator.py index 0d597b89de6a..a5a3936b1d7e 100644 --- a/qiskit/transpiler/passes/basis/basis_translator.py +++ b/qiskit/transpiler/passes/basis/basis_translator.py @@ -162,7 +162,8 @@ def run(self, dag): # If the source basis is a subset of the target basis and we have no circuit # instructions on qargs that have non-global operations there is nothing to # translate and we can exit early. - if source_basis.issubset(target_basis) and not qargs_local_source_basis: + source_basis_names = {x[0] for x in source_basis} + if source_basis_names.issubset(target_basis) and not qargs_local_source_basis: return dag logger.info( From adbe88707cba6ee327e0663563b8dd1713c4dfc6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 7 Aug 2024 20:35:20 -0400 Subject: [PATCH 3/5] Fix dag visualization with Var wires (#12848) * Fix dag visualization with Var wires This commit fixes the dag visualization for DAGs with classical variables. The Var type was not handled in the attribute callback functions for nodes and edges. This was causing the visualizer to fail if the dag contained these types. This fixes this by adding explict handling for the Var types and using the name attribute of the Var object. * Add release note and test --- qiskit/visualization/dag_visualization.py | 13 ++++++++++--- .../notes/fix-var-wires-4ebc40e0b19df253.yaml | 8 ++++++++ test/python/visualization/test_dag_drawer.py | 15 ++++++++++++++- 3 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 releasenotes/notes/fix-var-wires-4ebc40e0b19df253.yaml diff --git a/qiskit/visualization/dag_visualization.py b/qiskit/visualization/dag_visualization.py index ad2fca6e9bcf..dae97b51b0a4 100644 --- a/qiskit/visualization/dag_visualization.py +++ b/qiskit/visualization/dag_visualization.py @@ -174,10 +174,13 @@ def node_attr_func(node): label = register_bit_labels.get( node.wire, f"q_{dag.find_bit(node.wire).index}" ) - else: + elif isinstance(node.wire, Clbit): label = register_bit_labels.get( node.wire, f"c_{dag.find_bit(node.wire).index}" ) + else: + label = str(node.wire.name) + n["label"] = label n["color"] = "black" n["style"] = "filled" @@ -187,10 +190,12 @@ def node_attr_func(node): label = register_bit_labels.get( node.wire, f"q[{dag.find_bit(node.wire).index}]" ) - else: + elif isinstance(node.wire, Clbit): label = register_bit_labels.get( node.wire, f"c[{dag.find_bit(node.wire).index}]" ) + else: + label = str(node.wire.name) n["label"] = label n["color"] = "black" n["style"] = "filled" @@ -203,8 +208,10 @@ def edge_attr_func(edge): e = {} if isinstance(edge, Qubit): label = register_bit_labels.get(edge, f"q_{dag.find_bit(edge).index}") - else: + elif isinstance(edge, Clbit): label = register_bit_labels.get(edge, f"c_{dag.find_bit(edge).index}") + else: + label = str(edge.name) e["label"] = label return e diff --git a/releasenotes/notes/fix-var-wires-4ebc40e0b19df253.yaml b/releasenotes/notes/fix-var-wires-4ebc40e0b19df253.yaml new file mode 100644 index 000000000000..7cd1e74806b0 --- /dev/null +++ b/releasenotes/notes/fix-var-wires-4ebc40e0b19df253.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Fixed an issue with :func:`.dag_drawer` and :meth:`.DAGCircuit.draw` + when attempting to visualize a :class:`.DAGCircuit` instance that contained + :class:`.Var` wires. The visualizer would raise an exception trying to + do this which has been fixed so the expected visualization will be + generated. diff --git a/test/python/visualization/test_dag_drawer.py b/test/python/visualization/test_dag_drawer.py index 4b920390e880..d789b1e70682 100644 --- a/test/python/visualization/test_dag_drawer.py +++ b/test/python/visualization/test_dag_drawer.py @@ -16,12 +16,14 @@ import tempfile import unittest -from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit, Qubit, Clbit +from qiskit.circuit import QuantumRegister, ClassicalRegister, QuantumCircuit, Qubit, Clbit, Store from qiskit.visualization import dag_drawer from qiskit.exceptions import InvalidFileError from qiskit.visualization import VisualizationError from qiskit.converters import circuit_to_dag, circuit_to_dagdependency from qiskit.utils import optionals as _optionals +from qiskit.dagcircuit import DAGCircuit +from qiskit.circuit.classical import expr, types from .visualization import path_to_diagram_reference, QiskitVisualizationTestCase @@ -108,6 +110,17 @@ def test_dag_drawer_with_dag_dep(self): image = Image.open(tmp_path) self.assertImagesAreEqual(image, image_ref, 0.1) + @unittest.skipUnless(_optionals.HAS_GRAPHVIZ, "Graphviz not installed") + @unittest.skipUnless(_optionals.HAS_PIL, "PIL not installed") + def test_dag_drawer_with_var_wires(self): + """Test visualization works with var nodes.""" + a = expr.Var.new("a", types.Bool()) + dag = DAGCircuit() + dag.add_input_var(a) + dag.apply_operation_back(Store(a, a), (), ()) + image = dag_drawer(dag) + self.assertIsNotNone(image) + if __name__ == "__main__": unittest.main(verbosity=2) From 154601ba77b1d21d1f456754e72888d4d684cf7c Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Thu, 8 Aug 2024 11:08:16 +0100 Subject: [PATCH 4/5] Fix warnings with latest clippy (#12919) These aren't failing CI, which uses `clippy` at MSRV, they're just triggered by more recent versions. --- crates/accelerate/src/target_transpiler/mod.rs | 4 ++-- .../src/target_transpiler/nullable_index_map.rs | 10 +++------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/crates/accelerate/src/target_transpiler/mod.rs b/crates/accelerate/src/target_transpiler/mod.rs index bb0ec166c07e..0954e8e347f0 100644 --- a/crates/accelerate/src/target_transpiler/mod.rs +++ b/crates/accelerate/src/target_transpiler/mod.rs @@ -305,7 +305,7 @@ impl Target { match instruction { TargetOperation::Variadic(_) => { qargs_val = PropsMap::with_capacity(1); - qargs_val.extend([(None, None)].into_iter()); + qargs_val.extend([(None, None)]); self.variable_class_operations.insert(name.to_string()); } TargetOperation::Normal(_) => { @@ -872,7 +872,7 @@ impl Target { .unwrap() .extract::()? .into_iter() - .map(|(name, prop_map)| (name, PropsMap::from_iter(prop_map.into_iter()))), + .map(|(name, prop_map)| (name, PropsMap::from_iter(prop_map))), ); self._gate_name_map = state .get_item("gate_name_map")? diff --git a/crates/accelerate/src/target_transpiler/nullable_index_map.rs b/crates/accelerate/src/target_transpiler/nullable_index_map.rs index d3056c9edd8a..e6e2a0fca3a3 100644 --- a/crates/accelerate/src/target_transpiler/nullable_index_map.rs +++ b/crates/accelerate/src/target_transpiler/nullable_index_map.rs @@ -164,7 +164,7 @@ where pub fn iter(&self) -> Iter { Iter { map: self.map.iter(), - null_value: &self.null_val, + null_value: self.null_val.as_ref(), } } @@ -209,7 +209,7 @@ where /// Iterator for the key-value pairs in `NullableIndexMap`. pub struct Iter<'a, K, V> { map: BaseIter<'a, K, V>, - null_value: &'a Option, + null_value: Option<&'a V>, } impl<'a, K, V> Iterator for Iter<'a, K, V> { @@ -218,12 +218,8 @@ impl<'a, K, V> Iterator for Iter<'a, K, V> { fn next(&mut self) -> Option { if let Some((key, val)) = self.map.next() { Some((Some(key), val)) - } else if let Some(value) = self.null_value { - let value = value; - self.null_value = &None; - Some((None, value)) } else { - None + self.null_value.take().map(|value| (None, value)) } } From e7ee189be864e1404d2ef13ea2274bc6c6931525 Mon Sep 17 00:00:00 2001 From: Takashi Imamichi <31178928+t-imamichi@users.noreply.github.com> Date: Thu, 8 Aug 2024 21:21:16 +0900 Subject: [PATCH 5/5] Expose seed in `Estimator` and `StatevectorEstimator` (#12862) * Fix Estimator and StatevectorEstimator with reset * reno * use rng instead of seed * update reno and tests * simplify tests * apply review comments * Apply suggestions from code review Co-authored-by: Julien Gacon --------- Co-authored-by: Julien Gacon Co-authored-by: Julien Gacon --- qiskit/primitives/estimator.py | 17 ++++++--- qiskit/primitives/statevector_estimator.py | 13 +++++-- qiskit/primitives/utils.py | 20 ++++++++++ .../fix-estimator-reset-9e7539776df4cac4.yaml | 19 ++++++++++ test/python/primitives/test_estimator.py | 37 ++++++++++++++++++- .../primitives/test_statevector_estimator.py | 34 ++++++++++++++++- 6 files changed, 129 insertions(+), 11 deletions(-) create mode 100644 releasenotes/notes/fix-estimator-reset-9e7539776df4cac4.yaml diff --git a/qiskit/primitives/estimator.py b/qiskit/primitives/estimator.py index 6ae50ee7f0f5..d8e0a00f45e1 100644 --- a/qiskit/primitives/estimator.py +++ b/qiskit/primitives/estimator.py @@ -22,7 +22,6 @@ from qiskit.circuit import QuantumCircuit from qiskit.exceptions import QiskitError -from qiskit.quantum_info import Statevector from qiskit.quantum_info.operators.base_operator import BaseOperator from qiskit.utils.deprecation import deprecate_func @@ -31,7 +30,7 @@ from .utils import ( _circuit_key, _observable_key, - bound_circuit_to_instruction, + _statevector_from_circuit, init_observable, ) @@ -43,13 +42,21 @@ class Estimator(BaseEstimator[PrimitiveJob[EstimatorResult]]): :Run Options: - **shots** (None or int) -- - The number of shots. If None, it calculates the exact expectation - values. Otherwise, it samples from normal distributions with standard errors as standard + The number of shots. If None, it calculates the expectation values + with full state vector simulation. + Otherwise, it samples from normal distributions with standard errors as standard deviations using normal distribution approximation. - **seed** (np.random.Generator or int) -- Set a fixed seed or generator for the normal distribution. If shots is None, this option is ignored. + + .. note:: + The result of this class is exact if the circuit contains only unitary operations. + On the other hand, the result could be stochastic if the circuit contains a non-unitary + operation such as a reset for a some subsystems. + The stochastic result can be made reproducible by setting ``seed``, e.g., + ``Estimator(options={"seed":123})``. """ @deprecate_func( @@ -112,7 +119,7 @@ def _call( f"The number of qubits of a circuit ({circ.num_qubits}) does not match " f"the number of qubits of a observable ({obs.num_qubits})." ) - final_state = Statevector(bound_circuit_to_instruction(circ)) + final_state = _statevector_from_circuit(circ, rng) expectation_value = final_state.expectation_value(obs) if shots is None: expectation_values.append(expectation_value) diff --git a/qiskit/primitives/statevector_estimator.py b/qiskit/primitives/statevector_estimator.py index c57f2c5b77d5..f96d409631a3 100644 --- a/qiskit/primitives/statevector_estimator.py +++ b/qiskit/primitives/statevector_estimator.py @@ -19,13 +19,13 @@ import numpy as np -from qiskit.quantum_info import SparsePauliOp, Statevector +from qiskit.quantum_info import SparsePauliOp from .base import BaseEstimatorV2 from .containers import DataBin, EstimatorPubLike, PrimitiveResult, PubResult from .containers.estimator_pub import EstimatorPub from .primitive_job import PrimitiveJob -from .utils import bound_circuit_to_instruction +from .utils import _statevector_from_circuit class StatevectorEstimator(BaseEstimatorV2): @@ -41,6 +41,13 @@ class StatevectorEstimator(BaseEstimatorV2): called an estimator primitive unified bloc (PUB), produces its own array-based result. The :meth:`~.EstimatorV2.run` method can be given a sequence of pubs to run in one call. + .. note:: + The result of this class is exact if the circuit contains only unitary operations. + On the other hand, the result could be stochastic if the circuit contains a non-unitary + operation such as a reset for a some subsystems. + The stochastic result can be made reproducible by setting ``seed``, e.g., + ``StatevectorEstimator(seed=123)``. + .. plot:: :include-source: @@ -151,7 +158,7 @@ def _run_pub(self, pub: EstimatorPub) -> PubResult: for index in np.ndindex(*bc_circuits.shape): bound_circuit = bc_circuits[index] observable = bc_obs[index] - final_state = Statevector(bound_circuit_to_instruction(bound_circuit)) + final_state = _statevector_from_circuit(bound_circuit, rng) paulis, coeffs = zip(*observable.items()) obs = SparsePauliOp(paulis, coeffs) # TODO: support non Pauli operators expectation_value = np.real_if_close(final_state.expectation_value(obs)) diff --git a/qiskit/primitives/utils.py b/qiskit/primitives/utils.py index db3fcbd132dc..0bc362aa0ef4 100644 --- a/qiskit/primitives/utils.py +++ b/qiskit/primitives/utils.py @@ -225,3 +225,23 @@ def bound_circuit_to_instruction(circuit: QuantumCircuit) -> Instruction: ) inst.definition = circuit return inst + + +def _statevector_from_circuit( + circuit: QuantumCircuit, rng: np.random.Generator | None +) -> Statevector: + """Generate a statevector from a circuit + + If the input circuit includes any resets for a some subsystem, + :meth:`.Statevector.reset` behaves in a stochastic way in :meth:`.Statevector.evolve`. + This function sets a random number generator to be reproducible. + + See :meth:`.Statevector.reset` for details. + + Args: + circuit: The quantum circuit. + seed: The random number generator or None. + """ + sv = Statevector.from_int(0, 2**circuit.num_qubits) + sv.seed(rng) + return sv.evolve(bound_circuit_to_instruction(circuit)) diff --git a/releasenotes/notes/fix-estimator-reset-9e7539776df4cac4.yaml b/releasenotes/notes/fix-estimator-reset-9e7539776df4cac4.yaml new file mode 100644 index 000000000000..dba0d8f1c22b --- /dev/null +++ b/releasenotes/notes/fix-estimator-reset-9e7539776df4cac4.yaml @@ -0,0 +1,19 @@ +--- +features_primitives: + - | + :class:`.Estimator` and :class:`.StatevectorEstimator` return + expectation values in a stochastic way if the input circuit includes + a reset for a some subsystems. + The result was not reproducible, but it is now reproducible + if a random seed is set. For example:: + + from qiskit.primitives import StatevectorEstimator + + estimator = StatevectorEstimator(seed=123) + + or:: + + from qiskit.primitives import Estimator + + estimator = Estimator(options={"seed":123}) + diff --git a/test/python/primitives/test_estimator.py b/test/python/primitives/test_estimator.py index 783461c7e4ad..2c251af65d1b 100644 --- a/test/python/primitives/test_estimator.py +++ b/test/python/primitives/test_estimator.py @@ -13,6 +13,7 @@ """Tests for Estimator.""" import unittest +from test import QiskitTestCase import numpy as np from ddt import data, ddt, unpack @@ -24,7 +25,6 @@ from qiskit.primitives.base import validation from qiskit.primitives.utils import _observable_key from qiskit.quantum_info import Pauli, SparsePauliOp -from test import QiskitTestCase # pylint: disable=wrong-import-order class TestEstimator(QiskitTestCase): @@ -355,6 +355,41 @@ def get_op(i): keys = [_observable_key(get_op(i)) for i in range(5)] self.assertEqual(len(keys), len(set(keys))) + def test_reset(self): + """Test for circuits with reset.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.reset(0) + op = SparsePauliOp("ZI") + + seed = 12 + n = 1000 + with self.subTest("shots=None"): + with self.assertWarns(DeprecationWarning): + estimator = Estimator(options={"seed": seed}) + result = estimator.run([qc for _ in range(n)], [op] * n).result() + # expectation values should be stochastic due to reset for subsystems + np.testing.assert_allclose(result.values.mean(), 0, atol=1e-1) + + with self.assertWarns(DeprecationWarning): + result2 = estimator.run([qc for _ in range(n)], [op] * n).result() + # expectation values should be reproducible due to seed + np.testing.assert_allclose(result.values, result2.values) + + with self.subTest("shots=10000"): + shots = 10000 + with self.assertWarns(DeprecationWarning): + estimator = Estimator(options={"seed": seed}) + result = estimator.run([qc for _ in range(n)], [op] * n, shots=shots).result() + # expectation values should be stochastic due to reset for subsystems + np.testing.assert_allclose(result.values.mean(), 0, atol=1e-1) + + with self.assertWarns(DeprecationWarning): + result2 = estimator.run([qc for _ in range(n)], [op] * n, shots=shots).result() + # expectation values should be reproducible due to seed + np.testing.assert_allclose(result.values, result2.values) + @ddt class TestObservableValidation(QiskitTestCase): diff --git a/test/python/primitives/test_statevector_estimator.py b/test/python/primitives/test_statevector_estimator.py index 4eaa70e07a07..0cd76550f844 100644 --- a/test/python/primitives/test_statevector_estimator.py +++ b/test/python/primitives/test_statevector_estimator.py @@ -13,17 +13,17 @@ """Tests for Estimator.""" import unittest +from test import QiskitTestCase import numpy as np from qiskit.circuit import Parameter, QuantumCircuit from qiskit.circuit.library import RealAmplitudes from qiskit.primitives import StatevectorEstimator +from qiskit.primitives.containers.bindings_array import BindingsArray from qiskit.primitives.containers.estimator_pub import EstimatorPub from qiskit.primitives.containers.observables_array import ObservablesArray -from qiskit.primitives.containers.bindings_array import BindingsArray from qiskit.quantum_info import SparsePauliOp -from test import QiskitTestCase # pylint: disable=wrong-import-order class TestStatevectorEstimator(QiskitTestCase): @@ -307,6 +307,36 @@ def test_metadata(self): result[1].metadata, {"target_precision": 0.1, "circuit_metadata": qc2.metadata} ) + def test_reset(self): + """Test for circuits with reset.""" + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + qc.reset(0) + op = SparsePauliOp("ZI") + + seed = 12 + n = 1000 + estimator = StatevectorEstimator(seed=seed) + with self.subTest("precision=0"): + result = estimator.run([(qc, [op] * n)]).result() + # expectation values should be stochastic due to reset for subsystems + np.testing.assert_allclose(result[0].data.evs.mean(), 0, atol=1e-1) + + result2 = estimator.run([(qc, [op] * n)]).result() + # expectation values should be reproducible due to seed + np.testing.assert_allclose(result[0].data.evs, result2[0].data.evs) + + with self.subTest("precision=0.01"): + precision = 0.01 + result = estimator.run([(qc, [op] * n)], precision=precision).result() + # expectation values should be stochastic due to reset for subsystems + np.testing.assert_allclose(result[0].data.evs.mean(), 0, atol=1e-1) + + result2 = estimator.run([(qc, [op] * n)], precision=precision).result() + # expectation values should be reproducible due to seed + np.testing.assert_allclose(result[0].data.evs, result2[0].data.evs) + if __name__ == "__main__": unittest.main()