From 2d8ac11375378b6b71709031a536d4311ed1cfb7 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Fri, 8 Sep 2023 14:25:11 -0500 Subject: [PATCH] Refactor reconstruct_expectation_values (#391) * Refactor reconstruct_experiments * weight-->coeff * cleanups * cleanups * New workflow works with cutting_evaluation * Tests passing * re-add CuttingExperimentResults * cleanup * mypy * fix inits * release notes * Remove private generate_cutting_experiments * Revert "Remove private generate_cutting_experiments" This reverts commit cd95e28bc61bff28edb2c23c60c39dcb71df287b. * fix sphinx * Add num_qpd_bit checks and tests * fix hanging jobs :( * Update circuit_knitting/cutting/cutting_evaluation.py Co-authored-by: Jim Garrison * Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison * Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison * Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison * Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison * Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison * Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison * Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison * peer review * Fix links in release notes * quasi-dist(s) * Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison * Update releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml Co-authored-by: Jim Garrison --------- Co-authored-by: Jim Garrison --- .../cutting/cutting_evaluation.py | 174 +++++------------- .../cutting/cutting_experiments.py | 2 +- .../cutting/cutting_reconstruction.py | 91 ++++++--- .../refactor-evaluate-05fe26e94ff68166.yaml | 4 + ...refactor-reconstruct-45e00c3df1bdd4ff.yaml | 4 + test/cutting/test_cutting_evaluation.py | 26 +-- test/cutting/test_cutting_reconstruction.py | 82 +++++++-- 7 files changed, 203 insertions(+), 180 deletions(-) create mode 100644 releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml create mode 100644 releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index 995aed8d9..7386d63c4 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -16,14 +16,12 @@ from typing import NamedTuple from collections import defaultdict from collections.abc import Sequence -from itertools import chain import numpy as np from qiskit.circuit import QuantumCircuit, ClassicalRegister from qiskit.quantum_info import PauliList -from qiskit.primitives import BaseSampler, Sampler as TerraSampler +from qiskit.primitives import BaseSampler, Sampler as TerraSampler, SamplerResult from qiskit_aer.primitives import Sampler as AerSampler -from qiskit.result import QuasiDistribution from ..utils.observable_grouping import CommutingObservableGroup, ObservableCollection from ..utils.iteration import strict_zip @@ -41,7 +39,7 @@ class CuttingExperimentResults(NamedTuple): """Circuit cutting subexperiment results and sampling coefficients.""" - quasi_dists: list[list[list[tuple[QuasiDistribution, int]]]] + results: SamplerResult | dict[str | int, SamplerResult] coeffs: Sequence[tuple[float, WeightType]] @@ -64,10 +62,8 @@ def execute_experiments( samplers: Sampler(s) on which to run the sub-experiments. Returns: - - A 3D list of length-2 tuples holding the quasi-distributions and QPD bit information - for each sub-experiment. The shape of the list is: (``num_unique_samples``, ``num_partitions``, ``num_commuting_observ_groups``) - - Coefficients corresponding to each unique subexperiment's - sampling frequency + - One :class:`~qiskit.primitives.SamplerResult` instance for each partition. + - Coefficients corresponding to each unique subexperiment's contribution to the reconstructed result Raises: ValueError: The number of requested samples must be at least one. @@ -119,59 +115,52 @@ def execute_experiments( _validate_samplers(samplers) # Generate the sub-experiments to run on backend - ( - _, - coefficients, - subexperiments, - ) = _generate_cutting_experiments( - circuits, - subobservables, - num_samples, + subexperiments, coefficients = _generate_cutting_experiments( + circuits, subobservables, num_samples ) - # Create a list of samplers to use -- one for each batch + # Set up subexperiments and samplers + subexperiments_dict: dict[str | int, list[QuantumCircuit]] = {} + if isinstance(subexperiments, list): + subexperiments_dict = {"A": subexperiments} + else: + assert isinstance(subexperiments, dict) + subexperiments_dict = subexperiments if isinstance(samplers, BaseSampler): - samplers_by_batch = [samplers] - batches = [ - [ - sample[i] - for sample in subexperiments - for i in range(len(subexperiments[0])) - ] - ] + samplers_dict = {key: samplers for key in subexperiments_dict.keys()} else: - samplers_by_batch = [samplers[key] for key in sorted(samplers.keys())] - batches = [ - [sample[i] for sample in subexperiments] - for i in range(len(subexperiments[0])) - ] - - # There should be one batch per input sampler - assert len(samplers_by_batch) == len(batches) - - # Run each batch of sub-experiments - quasi_dists_by_batch = [ - _run_experiments_batch( - batches[i], - samplers_by_batch[i], - ) - for i in range(len(samplers_by_batch)) - ] - - # Build the output data structure to match the shape of input subexperiments - quasi_dists: list[list[list[tuple[dict[str, int], int]]]] = [ - [] for _ in range(len(subexperiments)) - ] - count = 0 - for i in range(len(subexperiments)): - for j in range(len(subexperiments[0])): - if len(samplers_by_batch) == 1: - quasi_dists[i].append(quasi_dists_by_batch[0][count]) - count += 1 - else: - quasi_dists[i].append(quasi_dists_by_batch[j][i]) + assert isinstance(samplers, dict) + samplers_dict = samplers + + # Make sure the first two cregs in each circuit are for QPD and observable measurements + # Run a job for each partition and collect results + results = {} + for label in sorted(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 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 + # input them, so we can just raise this generic error in any case. + raise ValueError( + "Circuits input to execute_experiments should contain no classical registers or bits." + ) + 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]) - return CuttingExperimentResults(quasi_dists, coefficients) + # If the input was a circuit, the output results should be a single SamplerResult instance + results_out = results + if isinstance(circuits, QuantumCircuit): + assert len(results_out.keys()) == 1 + results_out = results[list(results.keys())[0]] + + return CuttingExperimentResults(results=results_out, coeffs=coefficients) def _append_measurement_circuit( @@ -246,7 +235,6 @@ def _generate_cutting_experiments( ) -> tuple[ list[QuantumCircuit] | dict[str | int, list[QuantumCircuit]], list[tuple[float, WeightType]], - list[list[list[QuantumCircuit]]], ]: if isinstance(circuits, QuantumCircuit) and not isinstance(observables, PauliList): raise ValueError( @@ -295,7 +283,7 @@ def _generate_cutting_experiments( # Calculate terms in coefficient calculation kappa = np.prod([basis.kappa for basis in bases]) - num_samples = sum([value[0] for value in random_samples.values()]) # type: ignore + num_samples = sum([value[0] for value in random_samples.values()]) # Sort samples in descending order of frequency sorted_samples = sorted(random_samples.items(), key=lambda x: x[1][0], reverse=True) @@ -323,25 +311,6 @@ def _generate_cutting_experiments( meas_qc = _append_measurement_circuit(decomp_qc, cog) subexperiments_dict[label].append(meas_qc) - # Generate legacy subexperiments list - subexperiments_legacy: list[list[list[QuantumCircuit]]] = [] - for z, (map_ids, (redundancy, weight_type)) in enumerate(sorted_samples): - subexperiments_legacy.append([]) - for i, (subcircuit, label) in enumerate( - strict_zip(subcircuit_list, sorted(subsystem_observables.keys())) - ): - map_ids_tmp = map_ids - if is_separated: - map_ids_tmp = tuple(map_ids[j] for j in subcirc_map_ids[i]) - decomp_qc = decompose_qpd_instructions( - subcircuit, subcirc_qpd_gate_ids[i], map_ids_tmp - ) - subexperiments_legacy[-1].append([]) - so = subsystem_observables[label] - for j, cog in enumerate(so.groups): - meas_qc = _append_measurement_circuit(decomp_qc, cog) - subexperiments_legacy[-1][-1].append(meas_qc) - # If the input was a single quantum circuit, return the subexperiments as a list subexperiments_out: list[QuantumCircuit] | dict[ str | int, list[QuantumCircuit] @@ -351,56 +320,7 @@ def _generate_cutting_experiments( assert len(subexperiments_out.keys()) == 1 subexperiments_out = list(subexperiments_dict.values())[0] - return subexperiments_out, weights, subexperiments_legacy - - -def _run_experiments_batch( - subexperiments: Sequence[Sequence[QuantumCircuit]], - sampler: BaseSampler, -) -> list[list[tuple[QuasiDistribution, int]]]: - """Run subexperiments on the backend.""" - num_qpd_bits_flat = [] - - # Run all the experiments in one big batch - experiments_flat = list(chain.from_iterable(subexperiments)) - - for circ in experiments_flat: - if ( - len(circ.cregs) != 2 - or circ.cregs[1].name != "observable_measurements" - or circ.cregs[0].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 - # input them, so we can just raise this generic error in any case. - raise ValueError( - "Circuits input to execute_experiments should contain no classical registers or bits." - ) - - num_qpd_bits_flat.append(len(circ.cregs[0])) - - # Run all of the batched experiments - quasi_dists_flat = sampler.run(experiments_flat).result().quasi_dists - - # Reshape the output data to match the input - quasi_dists_reshaped: list[list[QuasiDistribution]] = [[] for _ in subexperiments] - num_qpd_bits: list[list[int]] = [[] for _ in subexperiments] - count = 0 - for i, subcirc in enumerate(subexperiments): - for j in range(len(subcirc)): - quasi_dists_reshaped[i].append(quasi_dists_flat[count]) - num_qpd_bits[i].append(num_qpd_bits_flat[count]) - count += 1 - - # Create the counts tuples, which include the number of QPD measurement bits - quasi_dists: list[list[tuple[dict[str, float], int]]] = [ - [] for _ in range(len(subexperiments)) - ] - for i, sample in enumerate(quasi_dists_reshaped): - for j, prob_dict in enumerate(sample): - quasi_dists[i].append((prob_dict, num_qpd_bits[i][j])) - - return quasi_dists + return subexperiments_out, weights def _get_mapping_ids_by_partition( diff --git a/circuit_knitting/cutting/cutting_experiments.py b/circuit_knitting/cutting/cutting_experiments.py index 586a2d220..ba5f0232b 100644 --- a/circuit_knitting/cutting/cutting_experiments.py +++ b/circuit_knitting/cutting/cutting_experiments.py @@ -69,7 +69,7 @@ def generate_cutting_experiments( to the same cut. ValueError: :class:`SingleQubitQPDGate` instances are not allowed in unseparated circuits. """ - subexperiments, weights, _ = _generate_cutting_experiments( + subexperiments, weights = _generate_cutting_experiments( circuits, observables, num_samples ) return subexperiments, weights diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index 260e2d1de..9a01579b8 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -17,7 +17,7 @@ import numpy as np from qiskit.quantum_info import PauliList -from qiskit.result import QuasiDistribution +from qiskit.primitives import SamplerResult from ..utils.observable_grouping import CommutingObservableGroup, ObservableCollection from ..utils.bitwise import bit_count @@ -26,7 +26,7 @@ def reconstruct_expectation_values( - quasi_dists: Sequence[Sequence[Sequence[tuple[QuasiDistribution, int]]]], + results: SamplerResult | dict[str | int, SamplerResult], coefficients: Sequence[tuple[float, WeightType]], observables: PauliList | dict[str | int, PauliList], ) -> list[float]: @@ -34,41 +34,62 @@ def reconstruct_expectation_values( Reconstruct an expectation value from the results of the sub-experiments. Args: - quasi_dists: A 3D sequence of length-2 tuples containing the quasi distributions and - QPD bit information from each sub-experiment. Its expected shape is - (num_unique_samples, num_partitions, num_commuting_observ_groups) - coefficients: A sequence of coefficients, such that each coefficient is associated - with one unique sample. The length of ``coefficients`` should equal - the length of ``quasi_dists``. Each coefficient is a tuple containing the numerical - value and the ``WeightType`` denoting how the value was generated. + results: The results from running the cutting subexperiments. If the cut circuit + was not partitioned between qubits and run separately, this argument should be + a :class:`~qiskit.primitives.SamplerResult` instance or a dictionary mapping + a single partition to the results. If the circuit was partitioned and its + pieces were run separately, this argument should be a dictionary mapping partition labels + to the results from each partition's subexperiments. + + The subexperiment results are expected to be ordered in the same way the subexperiments + are ordered in the output of :func:`.generate_cutting_experiments` -- one result for every + sample and observable, as shown below. The Qiskit Sampler primitive will return the results + in the same order the experiments are submitted, so users who do not use :func:`.generate_cutting_experiments` + to generate their experiments should take care to order their subexperiments as follows before submitting them + to the sampler primitive: + + :math:`[sample_{0}observable_{0}, \ldots, sample_{0}observable_{N}, sample_{1}observable_{0}, \ldots, sample_{M}observable_{N}]` + + coefficients: A sequence containing the coefficient associated with each unique subexperiment. Each element is a tuple + containing the coefficient (a ``float``) together with its :class:`.WeightType`, which denotes + how the value was generated. The contribution from each subexperiment will be multiplied by + its corresponding coefficient, and the resulting terms will be summed to obtain the reconstructed expectation value. observables: The observable(s) for which the expectation values will be calculated. - This should be a :class:`~qiskit.quantum_info.PauliList` if the decomposed circuit - was not separated into subcircuits. If the decomposed circuit was separated, this - should be a dictionary mapping from partition label to subobservables. + This should be a :class:`~qiskit.quantum_info.PauliList` if ``results`` is a + :class:`~qiskit.primitives.SamplerResult` instance. Otherwise, it should be a + dictionary mapping partition labels to the observables associated with that partition. Returns: - A ``list`` of ``float``\ s, such that each float is a simulated expectation + A ``list`` of ``float``\ s, such that each float is an expectation value corresponding to the input observable in the same position Raises: - ValueError: The number of unique samples in quasi_dists does not equal the number of coefficients. + 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 len(coefficients) != len(quasi_dists): + if isinstance(observables, PauliList) and not isinstance(results, SamplerResult): raise ValueError( - f"The number of unique samples in the quasi_dists list ({len(quasi_dists)}) does " - f"not equal the number of coefficients ({len(coefficients)})." + "If observables is a PauliList, results must be a SamplerResult instance." ) - # Create the commuting observable groups + if isinstance(observables, dict) and not isinstance(results, dict): + raise ValueError( + "If observables is a dictionary, results must also be a dictionary." + ) + + # If circuit was not separated, transform input data structures to dictionary format if isinstance(observables, PauliList): if any(obs.phase != 0 for obs in observables): raise ValueError("An input observable has a phase not equal to 1.") subobservables_by_subsystem = decompose_observables( observables, "A" * len(observables[0]) ) + results_dict: dict[str | int, SamplerResult] = {"A": results} expvals = np.zeros(len(observables)) else: + results_dict = results for label, subobservable in observables.items(): if any(obs.phase != 0 for obs in subobservable): raise ValueError("An input observable has a phase not equal to 1.") @@ -79,22 +100,34 @@ def reconstruct_expectation_values( label: ObservableCollection(subobservables) for label, subobservables in subobservables_by_subsystem.items() } + sorted_subsystems = sorted(subsystem_observables.keys()) - # Assign each weight's sign and calculate the expectation values for each observable + # Reconstruct the expectation values for i, coeff in enumerate(coefficients): - sorted_subsystems = sorted(subsystem_observables.keys()) # type: ignore current_expvals = np.ones((len(expvals),)) - for j, label in enumerate(sorted_subsystems): + for label in sorted_subsystems: so = subsystem_observables[label] subsystem_expvals = [ np.zeros(len(cog.commuting_observables)) for cog in so.groups ] for k, cog in enumerate(so.groups): - quasi_probs = quasi_dists[i][j][k][0] + quasi_probs = results_dict[label].quasi_dists[i * len(so.groups) + k] for outcome, quasi_prob in quasi_probs.items(): - subsystem_expvals[k] += quasi_prob * _process_outcome( - quasi_dists[i][j][k][1], cog, outcome - ) + try: + num_qpd_bits = results_dict[label].metadata[ + i * len(so.groups) + k + ]["num_qpd_bits"] + except KeyError: + raise ValueError( + "The num_qpd_bits field must be set in each subexperiment " + "result metadata dictionary." + ) + else: + subsystem_expvals[k] += quasi_prob * _process_outcome( + num_qpd_bits, + cog, + outcome, + ) for k, subobservable in enumerate(subobservables_by_subsystem[label]): current_expvals[k] *= np.mean( @@ -126,7 +159,13 @@ def _process_outcome( and each result will be either +1 or -1. """ outcome = _outcome_to_int(outcome) - qpd_outcomes = outcome & ((1 << num_qpd_bits) - 1) + try: + qpd_outcomes = outcome & ((1 << num_qpd_bits) - 1) + except TypeError: + raise TypeError( + f"num_qpd_bits must be an integer, but a {type(num_qpd_bits)} was passed." + ) + meas_outcomes = outcome >> num_qpd_bits # qpd_factor will be -1 or +1, depending on the overall parity of qpd diff --git a/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml b/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml new file mode 100644 index 000000000..d4386f99f --- /dev/null +++ b/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + The :func:`.execute_experiments` function now returns a :class:`~qiskit.primitives.SamplerResult` instance for each circuit partition, rather than the 3D list of quasi-distributions returned previously. The quasi-distribution for each subexperiment can be accessed via the ``quasi_dists`` field of :class:`~qiskit.primitives.SamplerResult`. The number of QPD bits contained in each subexperiment will be included in the ``num_qpd_bits`` field of the ``metadata`` dictionary for each experiment result. The output of this function is still valid as input to :func:`.reconstruct_expectation_values`. diff --git a/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml b/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml new file mode 100644 index 000000000..ac797bc3c --- /dev/null +++ b/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + :func:`.reconstruct_expectation_values` now takes, as its first argument, a :class:`~qiskit.primitives.SamplerResult` instance or a dictionary mapping partition labels to :class:`~qiskit.primitives.SamplerResult` instances. This new ``results`` argument replaces the old ``quasi_dists`` argument. The :class:`~qiskit.primitives.SamplerResult` instances are expected to contain the number of QPD bits used in each circuit input to the Sampler. This should be specified in the ``num_qpd_bits`` field of the experiment result metadata. diff --git a/test/cutting/test_cutting_evaluation.py b/test/cutting/test_cutting_evaluation.py index 9cafb52a2..ab975e883 100644 --- a/test/cutting/test_cutting_evaluation.py +++ b/test/cutting/test_cutting_evaluation.py @@ -15,8 +15,7 @@ import pytest from qiskit.quantum_info import Pauli, PauliList -from qiskit.result import QuasiDistribution -from qiskit.primitives import Sampler as TerraSampler +from qiskit.primitives import Sampler as TerraSampler, SamplerResult from qiskit_aer.primitives import Sampler as AerSampler from qiskit.circuit import QuantumCircuit, ClassicalRegister, CircuitInstruction, Clbit from qiskit.circuit.library.standard_gates import XGate @@ -67,7 +66,10 @@ def test_execute_experiments(self): quasi_dists, coefficients = execute_experiments( self.circuit, self.observable, num_samples=50, samplers=self.sampler ) - self.assertEqual([[[(QuasiDistribution({3: 1.0}), 0)]]], quasi_dists) + self.assertEqual( + quasi_dists, + SamplerResult(quasi_dists=[{3: 1.0}], metadata=[{"num_qpd_bits": 0}]), + ) self.assertEqual([(1.0, WeightType.EXACT)], coefficients) with self.subTest("Basic test with dicts"): circ1 = QuantumCircuit(1) @@ -100,15 +102,15 @@ def test_execute_experiments(self): num_samples=50, samplers={"A": self.sampler, "B": deepcopy(self.sampler)}, ) - self.assertEqual( - [ - [ - [(QuasiDistribution({1: 1.0}), 0)], - [(QuasiDistribution({1: 1.0}), 0)], - ] - ], - quasi_dists, - ) + 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}] + ), + } + self.assertEqual(quasi_dists, comp_result) self.assertEqual([(1.0, WeightType.EXACT)], coefficients) with self.subTest("Terra/Aer samplers with dicts"): circ1 = QuantumCircuit(1) diff --git a/test/cutting/test_cutting_reconstruction.py b/test/cutting/test_cutting_reconstruction.py index 6aaa85d60..4bb4eab7a 100644 --- a/test/cutting/test_cutting_reconstruction.py +++ b/test/cutting/test_cutting_reconstruction.py @@ -15,6 +15,7 @@ import pytest import numpy as np from qiskit.result import QuasiDistribution +from qiskit.primitives import SamplerResult from qiskit.quantum_info import Pauli, PauliList from qiskit.circuit import QuantumCircuit, ClassicalRegister @@ -44,40 +45,93 @@ def setUp(self): def test_cutting_reconstruction(self): with self.subTest("Test PauliList observable"): - quasi_dists = [[[(QuasiDistribution({"0": 1.0}), 0)]]] - coefficients = [(1.0, WeightType.EXACT)] - observables = PauliList(["ZZ"]) - expvals = reconstruct_expectation_values( - quasi_dists, coefficients, observables + 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") + subexperiments[0].add_register(creg1) + subexperiments[0].add_register(creg2) + observables = PauliList(["ZZ"]) + expvals = reconstruct_expectation_values(results, weights, observables) self.assertEqual([1.0], expvals) with self.subTest("Test mismatching inputs"): - quasi_dists = [[[(QuasiDistribution({"0": 1.0}), 0)]]] - coefficients = [(0.5, WeightType.EXACT), (0.5, WeightType.EXACT)] + 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"])} + with pytest.raises(ValueError) as e_info: + reconstruct_expectation_values(results, weights, observables) + assert ( + e_info.value.args[0] + == "If observables is a dictionary, results must also be a dictionary." + ) + results2 = {"A": results} observables = PauliList(["ZZ"]) with pytest.raises(ValueError) as e_info: - reconstruct_expectation_values(quasi_dists, coefficients, observables) + reconstruct_expectation_values(results2, weights, observables) assert ( e_info.value.args[0] - == "The number of unique samples in the quasi_dists list (1) does not equal the number of coefficients (2)." + == "If observables is a PauliList, results must be a SamplerResult instance." ) with self.subTest("Test unsupported phase"): - quasi_dists = [[[(QuasiDistribution({"0": 1.0}), 0)]]] - coefficients = [(0.5, WeightType.EXACT)] + 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") + subexperiments[0].add_register(creg1) + subexperiments[0].add_register(creg2) observables = PauliList(["iZZ"]) with pytest.raises(ValueError) as e_info: - reconstruct_expectation_values(quasi_dists, coefficients, observables) + reconstruct_expectation_values(results, weights, observables) assert ( e_info.value.args[0] == "An input observable has a phase not equal to 1." ) - observables = {"A": PauliList(["iZZ"])} + results = {"A": results} + subexperiments = {"A": subexperiments} + observables = {"A": observables} with pytest.raises(ValueError) as e_info: - reconstruct_expectation_values(quasi_dists, coefficients, observables) + reconstruct_expectation_values(results, weights, observables) assert ( 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]),