From 7772ec6cd64355379a678c618afa554b06943477 Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Wed, 4 Oct 2023 16:53:38 -0400 Subject: [PATCH 1/2] Swap register order, removing need to pass `num_qpd_bits` --- .../cutting/cutting_evaluation.py | 10 +-- .../cutting/cutting_experiments.py | 69 +++++++++++++++---- .../cutting/cutting_reconstruction.py | 36 ++-------- circuit_knitting/cutting/qpd/qpd.py | 12 ++-- .../how-tos/how_to_specify_cut_wires.ipynb | 3 - ...gate_cutting_to_reduce_circuit_width.ipynb | 8 +-- ...gate_cutting_to_reduce_circuit_depth.ipynb | 7 +- ...03_wire_cutting_via_move_instruction.ipynb | 8 +-- ...gister_order_swapped-5b07fb661c6250f9.yaml | 9 +++ test/cutting/test_cutting_evaluation.py | 10 +-- test/cutting/test_cutting_experiments.py | 46 +++++++++++-- test/cutting/test_cutting_reconstruction.py | 60 +++------------- test/cutting/test_cutting_roundtrip.py | 3 - test/cutting/test_cutting_workflows.py | 4 -- 14 files changed, 139 insertions(+), 146 deletions(-) create mode 100644 releasenotes/notes/register_order_swapped-5b07fb661c6250f9.yaml diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index 225cd716b..9993d17f0 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -132,15 +132,15 @@ def execute_experiments( assert isinstance(samplers, dict) samplers_dict = samplers - # Make sure the first two cregs in each circuit are for QPD and observable measurements + # Make sure the first two cregs in each circuit are for observable and QPD measurements, respectively. # Run a job for each partition and collect results results = {} for label in subexperiments_dict.keys(): for circ in subexperiments_dict[label]: if ( len(circ.cregs) != 2 - or circ.cregs[1].name != "observable_measurements" - or circ.cregs[0].name != "qpd_measurements" + or circ.cregs[0].name != "observable_measurements" + or circ.cregs[1].name != "qpd_measurements" or sum([reg.size for reg in circ.cregs]) != circ.num_clbits ): # If the classical bits/registers are in any other format than expected, the user must have @@ -150,10 +150,6 @@ def execute_experiments( ) results[label] = samplers_dict[label].run(subexperiments_dict[label]).result() - for label, result in results.items(): - for i, metadata in enumerate(result.metadata): - metadata["num_qpd_bits"] = len(subexperiments_dict[label][i].cregs[0]) - # If the input was a circuit, the output results should be a single SamplerResult instance results_out = results if isinstance(circuits, QuantumCircuit): diff --git a/circuit_knitting/cutting/cutting_experiments.py b/circuit_knitting/cutting/cutting_experiments.py index e8ebdc41c..2746b6fe8 100644 --- a/circuit_knitting/cutting/cutting_experiments.py +++ b/circuit_knitting/cutting/cutting_experiments.py @@ -148,12 +148,13 @@ def generate_cutting_experiments( subcircuit = subcircuit_dict[label] if is_separated: map_ids_tmp = tuple(map_ids[j] for j in subcirc_map_ids[label]) - decomp_qc = decompose_qpd_instructions( - subcircuit, subcirc_qpd_gate_ids[label], map_ids_tmp - ) for j, cog in enumerate(so.groups): - meas_qc = _append_measurement_circuit(decomp_qc, cog) - subexperiments_dict[label].append(meas_qc) + new_qc = _append_measurement_register(subcircuit, cog) + decompose_qpd_instructions( + new_qc, subcirc_qpd_gate_ids[label], map_ids_tmp, inplace=True + ) + _append_measurement_circuit(new_qc, cog, inplace=True) + subexperiments_dict[label].append(new_qc) # If the input was a single quantum circuit, return the subexperiments as a list subexperiments_out: list[QuantumCircuit] | dict[ @@ -231,6 +232,35 @@ def _get_bases(circuit: QuantumCircuit) -> tuple[list[QPDBasis], list[list[int]] return bases, qpd_gate_ids +def _append_measurement_register( + qc: QuantumCircuit, + cog: CommutingObservableGroup, + /, + *, + inplace: bool = False, +): + """Append a new classical register for the given ``CommutingObservableGroup``. + + The new register will be named ``"observable_measurements"`` and will be + the final register in the returned circuit, i.e. ``retval.cregs[-1]``. + + Args: + qc: The quantum circuit + cog: The commuting observable set for which to construct measurements + inplace: Whether to operate on the circuit in place (default: ``False``) + + Returns: + The modified circuit + """ + if not inplace: + qc = qc.copy() + + obs_creg = ClassicalRegister(len(cog.pauli_indices), name="observable_measurements") + qc.add_register(obs_creg) + + return qc + + def _append_measurement_circuit( qc: QuantumCircuit, cog: CommutingObservableGroup, @@ -239,15 +269,15 @@ def _append_measurement_circuit( qubit_locations: Sequence[int] | None = None, inplace: bool = False, ) -> QuantumCircuit: - """Append a new classical register and measurement instructions for the given ``CommutingObservableGroup``. + """Append measurement instructions for the given ``CommutingObservableGroup``. - The new register will be named ``"observable_measurements"`` and will be - the final register in the returned circuit, i.e. ``retval.cregs[-1]``. + The measurement results will be placed in a register with the name + ``"observable_measurements"``. Such a register can be created by calling + :func:`_append_measurement_register` before calling the current function. Args: qc: The quantum circuit - cog: The commuting observable set for - which to construct measurements + cog: The commuting observable set for which to construct measurements qubit_locations: A ``Sequence`` whose length is the number of qubits in the observables, where each element holds that qubit's corresponding index in the circuit. By default, the circuit and observables are assumed @@ -273,12 +303,27 @@ def _append_measurement_circuit( f"qubit_locations has {len(qubit_locations)} element(s) but the " f"observable(s) have {cog.general_observable.num_qubits} qubit(s)." ) + + # Find observable_measurements register + for reg in qc.cregs: + if reg.name == "observable_measurements": + obs_creg = reg + break + else: + raise ValueError('Cannot locate "observable_measurements" register') + + if obs_creg.size != len(cog.pauli_indices): + raise ValueError( + '"observable_measurements" register is the wrong size ' + "for the given commuting observable group " + f"({obs_creg.size} != {len(cog.pauli_indices)})" + ) + if not inplace: qc = qc.copy() # Append the appropriate measurements to qc - obs_creg = ClassicalRegister(len(cog.pauli_indices), name="observable_measurements") - qc.add_register(obs_creg) + # # Implement the necessary basis rotations and measurements, as # in BackendEstimator._measurement_circuit(). genobs_x = cog.general_observable.x diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index 51902e3d7..90c87257f 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -66,8 +66,6 @@ def reconstruct_expectation_values( Raises: ValueError: ``observables`` and ``results`` are of incompatible types. ValueError: An input observable has a phase not equal to 1. - ValueError: ``num_qpd_bits`` must be set for all result metadata dictionaries. - TypeError: ``num_qpd_bits`` must be an integer. """ if isinstance(observables, PauliList) and not isinstance(results, SamplerResult): raise ValueError( @@ -111,21 +109,7 @@ def reconstruct_expectation_values( for k, cog in enumerate(so.groups): quasi_probs = results_dict[label].quasi_dists[i * len(so.groups) + k] for outcome, quasi_prob in quasi_probs.items(): - try: - num_qpd_bits = results_dict[label].metadata[ - i * len(so.groups) + k - ]["num_qpd_bits"] - except KeyError as ex: - raise ValueError( - "The num_qpd_bits field must be set in each subexperiment " - "result metadata dictionary." - ) from ex - else: - subsystem_expvals[k] += quasi_prob * _process_outcome( - num_qpd_bits, - cog, - outcome, - ) + subsystem_expvals[k] += quasi_prob * _process_outcome(cog, outcome) for k, subobservable in enumerate(subobservables_by_subsystem[label]): current_expvals[k] *= np.mean( @@ -138,16 +122,12 @@ def reconstruct_expectation_values( def _process_outcome( - num_qpd_bits: int, cog: CommutingObservableGroup, outcome: int | str, / + cog: CommutingObservableGroup, outcome: int | str, / ) -> np.typing.NDArray[np.float64]: """ Process a single outcome of a QPD experiment with observables. Args: - num_qpd_bits: The number of QPD measurements in the circuit. It is - assumed that the second to last creg in the generating circuit - is the creg containing the QPD measurements, and the last - creg is associated with the observable measurements. cog: The observable set being measured by the current experiment outcome: The outcome of the classical bits @@ -156,15 +136,11 @@ def _process_outcome( this vector correspond to the elements of ``cog.commuting_observables``, and each result will be either +1 or -1. """ + num_meas_bits = len(cog.pauli_indices) + outcome = _outcome_to_int(outcome) - try: - qpd_outcomes = outcome & ((1 << num_qpd_bits) - 1) - except TypeError as ex: - raise TypeError( - f"num_qpd_bits must be an integer, but a {type(num_qpd_bits)} was passed." - ) from ex - - meas_outcomes = outcome >> num_qpd_bits + meas_outcomes = outcome & ((1 << num_meas_bits) - 1) + qpd_outcomes = outcome >> num_meas_bits # qpd_factor will be -1 or +1, depending on the overall parity of qpd # measurements. diff --git a/circuit_knitting/cutting/qpd/qpd.py b/circuit_knitting/cutting/qpd/qpd.py index fd4068b24..c40084d31 100644 --- a/circuit_knitting/cutting/qpd/qpd.py +++ b/circuit_knitting/cutting/qpd/qpd.py @@ -500,6 +500,8 @@ def decompose_qpd_instructions( circuit: QuantumCircuit, instruction_ids: Sequence[Sequence[int]], map_ids: Sequence[int] | None = None, + *, + inplace: bool = False, ) -> QuantumCircuit: r""" Replace all QPD instructions in the circuit with local Qiskit operations and measurements. @@ -529,7 +531,9 @@ def decompose_qpd_instructions( ValueError: Length of ``map_ids`` does not equal the number of decompositions in the circuit. """ _validate_qpd_instructions(circuit, instruction_ids) - new_qc = circuit.copy() + + if not inplace: + circuit = circuit.copy() # pragma: no cover if map_ids is not None: if len(instruction_ids) != len(map_ids): @@ -540,12 +544,12 @@ def decompose_qpd_instructions( # If mapping is specified, set each gate's mapping for i, decomp_gate_ids in enumerate(instruction_ids): for gate_id in decomp_gate_ids: - new_qc.data[gate_id].operation.basis_id = map_ids[i] + circuit.data[gate_id].operation.basis_id = map_ids[i] # Convert all instances of BaseQPDGate in the circuit to Qiskit instructions - _decompose_qpd_instructions(new_qc, instruction_ids) + _decompose_qpd_instructions(circuit, instruction_ids) - return new_qc + return circuit _qpdbasis_from_instruction_funcs: dict[str, Callable[[Instruction], QPDBasis]] = {} diff --git a/docs/circuit_cutting/how-tos/how_to_specify_cut_wires.ipynb b/docs/circuit_cutting/how-tos/how_to_specify_cut_wires.ipynb index 806d2a264..b4ea9eda8 100644 --- a/docs/circuit_cutting/how-tos/how_to_specify_cut_wires.ipynb +++ b/docs/circuit_cutting/how-tos/how_to_specify_cut_wires.ipynb @@ -348,9 +348,6 @@ "metadata": {}, "outputs": [], "source": [ - "for label in results:\n", - " for i, subexperiment in enumerate(subexperiments[label]):\n", - " results[label].metadata[i][\"num_qpd_bits\"] = len(subexperiment.cregs[0])\n", "reconstructed_expvals = reconstruct_expectation_values(\n", " results,\n", " coefficients,\n", diff --git a/docs/circuit_cutting/tutorials/01_gate_cutting_to_reduce_circuit_width.ipynb b/docs/circuit_cutting/tutorials/01_gate_cutting_to_reduce_circuit_width.ipynb index ff85dd277..778b32a6b 100644 --- a/docs/circuit_cutting/tutorials/01_gate_cutting_to_reduce_circuit_width.ipynb +++ b/docs/circuit_cutting/tutorials/01_gate_cutting_to_reduce_circuit_width.ipynb @@ -307,9 +307,7 @@ "source": [ "### Reconstruct the expectation values\n", "\n", - "Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit.\n", - "\n", - "Include the number of bits used for cutting measurements in the results metadata. This will be automated in a future release, but users must specify it manually for now." + "Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit." ] }, { @@ -321,10 +319,6 @@ "source": [ "from circuit_knitting.cutting import reconstruct_expectation_values\n", "\n", - "for label, circuits in subexperiments.items():\n", - " for i, circuit in enumerate(circuits):\n", - " results[label].metadata[i][\"num_qpd_bits\"] = len(circuit.cregs[0])\n", - "\n", "reconstructed_expvals = reconstruct_expectation_values(\n", " results,\n", " coefficients,\n", diff --git a/docs/circuit_cutting/tutorials/02_gate_cutting_to_reduce_circuit_depth.ipynb b/docs/circuit_cutting/tutorials/02_gate_cutting_to_reduce_circuit_depth.ipynb index 1b61a456d..7539f4605 100644 --- a/docs/circuit_cutting/tutorials/02_gate_cutting_to_reduce_circuit_depth.ipynb +++ b/docs/circuit_cutting/tutorials/02_gate_cutting_to_reduce_circuit_depth.ipynb @@ -346,9 +346,7 @@ "source": [ "### Reconstruct the expectation values\n", "\n", - "Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit.\n", - "\n", - "Include the number of bits used for cutting measurements in the results metadata. This will be automated in a future release, but users must specify it manually for now." + "Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit." ] }, { @@ -360,9 +358,6 @@ "source": [ "from circuit_knitting.cutting import reconstruct_expectation_values\n", "\n", - "for i in range(len(subexperiments)):\n", - " results.metadata[i][\"num_qpd_bits\"] = len(subexperiments[i].cregs[0])\n", - "\n", "reconstructed_expvals = reconstruct_expectation_values(\n", " results,\n", " coefficients,\n", diff --git a/docs/circuit_cutting/tutorials/03_wire_cutting_via_move_instruction.ipynb b/docs/circuit_cutting/tutorials/03_wire_cutting_via_move_instruction.ipynb index 85ef550c5..768d0865e 100644 --- a/docs/circuit_cutting/tutorials/03_wire_cutting_via_move_instruction.ipynb +++ b/docs/circuit_cutting/tutorials/03_wire_cutting_via_move_instruction.ipynb @@ -403,9 +403,7 @@ "source": [ "### Reconstruct the expectation values\n", "\n", - "Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit.\n", - "\n", - "Include the number of bits used for cutting measurements in the results metadata. This will be automated in a future release, but users must specify it manually for now." + "Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit." ] }, { @@ -417,10 +415,6 @@ "source": [ "from circuit_knitting.cutting import reconstruct_expectation_values\n", "\n", - "for label, circuits in subexperiments.items():\n", - " for i, circuit in enumerate(circuits):\n", - " results[label].metadata[i][\"num_qpd_bits\"] = len(circuit.cregs[0])\n", - "\n", "reconstructed_expvals = reconstruct_expectation_values(\n", " results,\n", " coefficients,\n", diff --git a/releasenotes/notes/register_order_swapped-5b07fb661c6250f9.yaml b/releasenotes/notes/register_order_swapped-5b07fb661c6250f9.yaml new file mode 100644 index 000000000..5b902b466 --- /dev/null +++ b/releasenotes/notes/register_order_swapped-5b07fb661c6250f9.yaml @@ -0,0 +1,9 @@ +--- +upgrade: + - | + The order of the classical registers in the generated experiments + has been swapped. The ``"observable_measurements"`` register now + comes first, and the ``"qpd_measurements"`` register now comes + second. As a result of this change, it is no longer necessary to + manually insert ``num_qpd_bits`` into the ``metadata`` for each + experiment's result. diff --git a/test/cutting/test_cutting_evaluation.py b/test/cutting/test_cutting_evaluation.py index 80dc1d7fc..312be1f11 100644 --- a/test/cutting/test_cutting_evaluation.py +++ b/test/cutting/test_cutting_evaluation.py @@ -66,7 +66,7 @@ def test_execute_experiments(self): ) self.assertEqual( quasi_dists, - SamplerResult(quasi_dists=[{3: 1.0}], metadata=[{"num_qpd_bits": 0}]), + SamplerResult(quasi_dists=[{3: 1.0}], metadata=[{}]), ) self.assertEqual([(1.0, WeightType.EXACT)], coefficients) with self.subTest("Basic test with dicts"): @@ -101,12 +101,8 @@ def test_execute_experiments(self): samplers={"A": self.sampler, "B": deepcopy(self.sampler)}, ) comp_result = { - "A": SamplerResult( - quasi_dists=[{1: 1.0}], metadata=[{"num_qpd_bits": 0}] - ), - "B": SamplerResult( - quasi_dists=[{1: 1.0}], metadata=[{"num_qpd_bits": 0}] - ), + "A": SamplerResult(quasi_dists=[{1: 1.0}], metadata=[{}]), + "B": SamplerResult(quasi_dists=[{1: 1.0}], metadata=[{}]), } self.assertEqual(quasi_dists, comp_result) self.assertEqual([(1.0, WeightType.EXACT)], coefficients) diff --git a/test/cutting/test_cutting_experiments.py b/test/cutting/test_cutting_experiments.py index 46efeda66..6d435f34a 100644 --- a/test/cutting/test_cutting_experiments.py +++ b/test/cutting/test_cutting_experiments.py @@ -15,7 +15,7 @@ import pytest import numpy as np from qiskit.quantum_info import PauliList, Pauli -from qiskit.circuit import QuantumCircuit, ClassicalRegister +from qiskit.circuit import QuantumCircuit from qiskit.circuit.library.standard_gates import CXGate from circuit_knitting.cutting.qpd import ( @@ -27,7 +27,10 @@ from circuit_knitting.cutting import generate_cutting_experiments from circuit_knitting.cutting.qpd import WeightType from circuit_knitting.cutting import partition_problem -from circuit_knitting.cutting.cutting_experiments import _append_measurement_circuit +from circuit_knitting.cutting.cutting_experiments import ( + _append_measurement_register, + _append_measurement_circuit, +) class TestCuttingExperiments(unittest.TestCase): @@ -154,23 +157,37 @@ def test_generate_cutting_experiments(self): == "SingleQubitQPDGates are not supported in unseparable circuits." ) + def test_append_measurement_register(self): + qc = QuantumCircuit(2) + qc.h(0) + qc.cx(0, 1) + cog = CommutingObservableGroup(Pauli("XZ"), list(PauliList(["IZ", "XI", "XZ"]))) + with self.subTest("In place"): + qcx = qc.copy() + assert _append_measurement_register(qcx, cog, inplace=True) is qcx + with self.subTest("Out of place"): + assert _append_measurement_register(qc, cog) is not qc + with self.subTest("Correct number of bits"): + assert _append_measurement_register(qc, cog).num_clbits == len( + cog.pauli_indices + ) + def test_append_measurement_circuit(self): qc = QuantumCircuit(2) qc.h(0) qc.cx(0, 1) - qc.add_register(ClassicalRegister(1, name="qpd_measurements")) cog = CommutingObservableGroup(Pauli("XZ"), list(PauliList(["IZ", "XI", "XZ"]))) - qc2 = qc.copy() - qc2.add_register(ClassicalRegister(2, name="observable_measurements")) + _append_measurement_register(qc, cog, inplace=True) with self.subTest("In place"): qcx = qc.copy() assert _append_measurement_circuit(qcx, cog, inplace=True) is qcx with self.subTest("Out of place"): assert _append_measurement_circuit(qc, cog) is not qc with self.subTest("Correct measurement circuit"): - qc2.measure(0, 1) + qc2 = qc.copy() + qc2.measure(0, 0) qc2.h(1) - qc2.measure(1, 2) + qc2.measure(1, 1) assert _append_measurement_circuit(qc, cog) == qc2 with self.subTest("Mismatch between qubit_locations and number of qubits"): with pytest.raises(ValueError) as e_info: @@ -179,6 +196,21 @@ def test_append_measurement_circuit(self): e_info.value.args[0] == "qubit_locations has 1 element(s) but the observable(s) have 2 qubit(s)." ) + with self.subTest("No observable_measurements register"): + with pytest.raises(ValueError) as e_info: + _append_measurement_circuit(QuantumCircuit(2), cog) + assert ( + e_info.value.args[0] + == 'Cannot locate "observable_measurements" register' + ) + with self.subTest("observable_measurements register has wrong size"): + cog2 = CommutingObservableGroup(Pauli("XI"), list(PauliList(["XI"]))) + with pytest.raises(ValueError) as e_info: + _append_measurement_circuit(qc, cog2) + assert ( + e_info.value.args[0] + == '"observable_measurements" register is the wrong size for the given commuting observable group (2 != 1)' + ) with self.subTest("Mismatched qubits, no qubit_locations provided"): cog = CommutingObservableGroup(Pauli("X"), [Pauli("X")]) with pytest.raises(ValueError) as e_info: diff --git a/test/cutting/test_cutting_reconstruction.py b/test/cutting/test_cutting_reconstruction.py index 4bb4eab7a..3a3c4ca04 100644 --- a/test/cutting/test_cutting_reconstruction.py +++ b/test/cutting/test_cutting_reconstruction.py @@ -30,15 +30,6 @@ @ddt class TestCuttingReconstruction(unittest.TestCase): def setUp(self): - qc = QuantumCircuit(2) - qc.h(0) - qc.cx(0, 1) - self.qc0 = qc.copy() - qc.add_register(ClassicalRegister(1, name="qpd_measurements")) - self.qc1 = qc.copy() - qc.add_register(ClassicalRegister(2, name="observable_measurements")) - self.qc2 = qc - self.cog = CommutingObservableGroup( Pauli("XZ"), list(PauliList(["IZ", "XI", "XZ"])) ) @@ -48,11 +39,10 @@ def test_cutting_reconstruction(self): results = SamplerResult( quasi_dists=[QuasiDistribution({"0": 1.0})], metadata=[{}] ) - results.metadata[0]["num_qpd_bits"] = 1 weights = [(1.0, WeightType.EXACT)] subexperiments = [QuantumCircuit(2)] - creg1 = ClassicalRegister(1, name="qpd_measurements") - creg2 = ClassicalRegister(2, name="observable_measurements") + creg1 = ClassicalRegister(2, name="observable_measurements") + creg2 = ClassicalRegister(1, name="qpd_measurements") subexperiments[0].add_register(creg1) subexperiments[0].add_register(creg2) observables = PauliList(["ZZ"]) @@ -62,7 +52,6 @@ def test_cutting_reconstruction(self): results = SamplerResult( quasi_dists=[QuasiDistribution({"0": 1.0})], metadata=[{}] ) - results.metadata[0]["num_qpd_bits"] = 1 weights = [(0.5, WeightType.EXACT), (0.5, WeightType.EXACT)] subexperiments = {"A": QuantumCircuit(2)} observables = {"A": PauliList(["Z"]), "B": PauliList(["Z"])} @@ -84,11 +73,10 @@ def test_cutting_reconstruction(self): results = SamplerResult( quasi_dists=[QuasiDistribution({"0": 1.0})], metadata=[{}] ) - results.metadata[0]["num_qpd_bits"] = 1 weights = [(0.5, WeightType.EXACT)] subexperiments = [QuantumCircuit(2)] - creg1 = ClassicalRegister(1, name="qpd_measurements") - creg2 = ClassicalRegister(2, name="observable_measurements") + creg1 = ClassicalRegister(2, name="observable_measurements") + creg2 = ClassicalRegister(1, name="qpd_measurements") subexperiments[0].add_register(creg1) subexperiments[0].add_register(creg2) observables = PauliList(["iZZ"]) @@ -107,49 +95,23 @@ def test_cutting_reconstruction(self): e_info.value.args[0] == "An input observable has a phase not equal to 1." ) - with self.subTest("Test num_qpd_bits"): - results = SamplerResult( - quasi_dists=[QuasiDistribution({"0": 1.0})], metadata=[{}] - ) - results.metadata[0]["num_qpd_bits"] = 1.0 - weights = [(0.5, WeightType.EXACT)] - subexperiments = [QuantumCircuit(2)] - creg1 = ClassicalRegister(1, name="qpd_measurements") - creg2 = ClassicalRegister(2, name="observable_measurements") - subexperiments[0].add_register(creg1) - subexperiments[0].add_register(creg2) - observables = PauliList(["ZZ"]) - with pytest.raises(TypeError) as e_info: - reconstruct_expectation_values(results, weights, observables) - assert ( - e_info.value.args[0] - == "num_qpd_bits must be an integer, but a was passed." - ) - results.metadata[0] = {} - with pytest.raises(ValueError) as e_info: - reconstruct_expectation_values(results, weights, observables) - assert ( - e_info.value.args[0] - == "The num_qpd_bits field must be set in each subexperiment result metadata dictionary." - ) @data( ("000", [1, 1, 1]), - ("001", [-1, -1, -1]), - ("010", [-1, 1, -1]), - ("011", [1, -1, 1]), - ("100", [1, -1, -1]), - ("101", [-1, 1, 1]), - ("110", [-1, -1, 1]), + ("001", [-1, 1, -1]), + ("010", [1, -1, -1]), + ("011", [-1, -1, 1]), + ("100", [-1, -1, -1]), + ("101", [1, -1, 1]), + ("110", [-1, 1, 1]), ("111", [1, 1, -1]), ) @unpack def test_process_outcome(self, outcome, expected): - num_qpd_bits = len(self.qc2.cregs[-2]) for o in ( outcome, f"0b{outcome}", int(f"0b{outcome}", 0), hex(int(f"0b{outcome}", 0)), ): - assert np.all(_process_outcome(num_qpd_bits, self.cog, o) == expected) + assert np.all(_process_outcome(self.cog, o) == expected) diff --git a/test/cutting/test_cutting_roundtrip.py b/test/cutting/test_cutting_roundtrip.py index 867ff6fdc..59ba44331 100644 --- a/test/cutting/test_cutting_roundtrip.py +++ b/test/cutting/test_cutting_roundtrip.py @@ -168,9 +168,6 @@ def test_cutting_exact_reconstruction(example_circuit): label: sampler.run(subexperiments[label]).result() for label, sampler in samplers.items() } - for label in results: - for i, subexperiment in enumerate(subexperiments[label]): - results[label].metadata[i]["num_qpd_bits"] = len(subexperiment.cregs[0]) simulated_expvals = reconstruct_expectation_values( results, coefficients, subobservables ) diff --git a/test/cutting/test_cutting_workflows.py b/test/cutting/test_cutting_workflows.py index e743e8f00..e144b3afc 100644 --- a/test/cutting/test_cutting_workflows.py +++ b/test/cutting/test_cutting_workflows.py @@ -90,10 +90,6 @@ def test_exotic_labels(label1, label2): for label, sampler in samplers.items() } - for label in results: - for i, subexperiment in enumerate(subexperiments[label]): - results[label].metadata[i]["num_qpd_bits"] = len(subexperiment.cregs[0]) - simulated_expvals = reconstruct_expectation_values( results, coefficients, From f4dec604669fbe3d1103256221cb6d10ea1b32c7 Mon Sep 17 00:00:00 2001 From: Jim Garrison Date: Mon, 23 Oct 2023 16:26:28 -0400 Subject: [PATCH 2/2] Fix reconstruction when subsystem contains no observable --- .../cutting/cutting_reconstruction.py | 3 +- test/cutting/test_cutting_roundtrip.py | 46 +++++++++++++++---- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index 90c87257f..40094a1aa 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -22,6 +22,7 @@ from ..utils.observable_grouping import CommutingObservableGroup, ObservableCollection from ..utils.bitwise import bit_count from .cutting_decomposition import decompose_observables +from .cutting_experiments import _get_pauli_indices from .qpd import WeightType @@ -136,7 +137,7 @@ def _process_outcome( this vector correspond to the elements of ``cog.commuting_observables``, and each result will be either +1 or -1. """ - num_meas_bits = len(cog.pauli_indices) + num_meas_bits = len(_get_pauli_indices(cog)) outcome = _outcome_to_int(outcome) meas_outcomes = outcome & ((1 << num_meas_bits) - 1) diff --git a/test/cutting/test_cutting_roundtrip.py b/test/cutting/test_cutting_roundtrip.py index 9faa796be..ea4944054 100644 --- a/test/cutting/test_cutting_roundtrip.py +++ b/test/cutting/test_cutting_roundtrip.py @@ -179,28 +179,56 @@ def test_cutting_exact_reconstruction(example_circuit): assert np.allclose(exact_expvals, simulated_expvals, atol=1e-8) -def test_sampler_with_identity_subobservable(example_circuit): - """This test ensures that the sampler does not throw an error if you pass it a subcircuit with no observable measurements. +@pytest.mark.parametrize( + "sampler,is_exact_sampler", [(Sampler(), False), (ExactSampler(), True)] +) +def test_sampler_with_identity_subobservable(sampler, is_exact_sampler): + """This test ensures that the sampler works for a subcircuit with no observable measurements. - Tests temporary workaround to Issue #422. + Specifically, that - This test passes if no exceptions are raised. + - ``Sampler`` does not blow up (Issue #422); and + - ``ExactSampler`` returns correct results + This is related to https://github.com/Qiskit-Extensions/circuit-knitting-toolbox/issues/422. """ + # Create a circuit to cut + qc = QuantumCircuit(3) + append_random_unitary(qc, [0, 1]) + append_random_unitary(qc, [2]) + qc.rxx(np.pi / 3, 1, 2) + append_random_unitary(qc, [0, 1]) + append_random_unitary(qc, [2]) - qc = example_circuit - observable_to_test = PauliList( + # Determine expectation value using cutting + observables = PauliList( ["IIZ"] ) # Without the workaround to Issue #422, this observable causes a Sampler error. subcircuits, bases, subobservables = partition_problem( - qc, "AAB", observables=observable_to_test + qc, "AAB", observables=observables ) subexperiments, coefficients = generate_cutting_experiments( subcircuits, subobservables, num_samples=np.inf ) - samplers = {label: Sampler() for label in subexperiments.keys()} + samplers = {label: sampler for label in subexperiments.keys()} results = { label: sampler.run(subexperiments[label]).result() for label, sampler in samplers.items() } - _ = results + reconstructed_expvals = reconstruct_expectation_values( + results, coefficients, subobservables + ) + + if is_exact_sampler: + # Determine exact expectation values + estimator = Estimator() + exact_expvals = ( + estimator.run([qc] * len(observables), list(observables)).result().values + ) + + logger.info( + "Max error: %f", np.max(np.abs(exact_expvals - reconstructed_expvals)) + ) + + # Ensure both methods yielded equivalent expectation values + assert np.allclose(exact_expvals, reconstructed_expvals, atol=1e-8)