From bf5ef1c9aa5752c6394f4b57c750b870b28396da Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Thu, 31 Aug 2023 16:26:30 -0500 Subject: [PATCH 01/29] Refactor reconstruct_experiments --- .../cutting/cutting_reconstruction.py | 67 +++++++++++-------- test/cutting/test_cutting_reconstruction.py | 57 ++++++++++++---- 2 files changed, 83 insertions(+), 41 deletions(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index 260e2d1de..f4b47797d 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,49 +26,57 @@ def reconstruct_expectation_values( - quasi_dists: Sequence[Sequence[Sequence[tuple[QuasiDistribution, int]]]], - coefficients: Sequence[tuple[float, WeightType]], + results: SamplerResult | dict[str | int, SamplerResult], + weights: Sequence[tuple[float, WeightType]], observables: PauliList | dict[str | int, PauliList], ) -> list[float]: r""" 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, the input 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 run separately, the input should be a dictionary mapping partition labels + to the results from each partition's subexperiments. + weights: The weights associated with each unique subexperiment. Each weight is a tuple + containing the scalar value as well as the ``WeightType``, which denotes + how the value was generated. 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. """ - 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,21 +87,26 @@ def reconstruct_expectation_values( label: ObservableCollection(subobservables) for label, subobservables in subobservables_by_subsystem.items() } + sorted_subsystems = sorted(subsystem_observables.keys()) # type: ignore - # Assign each weight's sign and calculate the expectation values for each observable - for i, coeff in enumerate(coefficients): - sorted_subsystems = sorted(subsystem_observables.keys()) # type: ignore + # Reconstruct the expectation values + for i in range(len(weights)): current_expvals = np.ones((len(expvals),)) - for j, label in enumerate(sorted_subsystems): + for label in sorted_subsystems: so = subsystem_observables[label] + weight = weights[i] 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] - for outcome, quasi_prob in quasi_probs.items(): + quasi_probs = results_dict[label].quasi_dists[i * len(so.groups) + k] # type: ignore + for outcome, quasi_prob in quasi_probs.items(): # type: ignore subsystem_expvals[k] += quasi_prob * _process_outcome( - quasi_dists[i][j][k][1], cog, outcome + results_dict[label].metadata[i * len(so.groups) + k][ + "num_qpd_bits" + ], + cog, + outcome, ) for k, subobservable in enumerate(subobservables_by_subsystem[label]): @@ -101,7 +114,7 @@ def reconstruct_expectation_values( [subsystem_expvals[m][n] for m, n in so.lookup[subobservable]] ) - expvals += coeff[0] * current_expvals + expvals += weight[0] * current_expvals return list(expvals) diff --git a/test/cutting/test_cutting_reconstruction.py b/test/cutting/test_cutting_reconstruction.py index 6aaa85d60..30349383b 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,36 +45,64 @@ 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." From 70d07829427144c13ed27b28484955f300ff4ea4 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Thu, 31 Aug 2023 16:38:01 -0500 Subject: [PATCH 02/29] weight-->coeff --- circuit_knitting/cutting/cutting_reconstruction.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index f4b47797d..d8743866e 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -27,7 +27,7 @@ def reconstruct_expectation_values( results: SamplerResult | dict[str | int, SamplerResult], - weights: Sequence[tuple[float, WeightType]], + coefficients: Sequence[tuple[float, WeightType]], observables: PauliList | dict[str | int, PauliList], ) -> list[float]: r""" @@ -40,7 +40,7 @@ def reconstruct_expectation_values( a single partition to the results. If the circuit was partitioned and its pieces run separately, the input should be a dictionary mapping partition labels to the results from each partition's subexperiments. - weights: The weights associated with each unique subexperiment. Each weight is a tuple + coefficients: The weights associated with each unique subexperiment. Each weight is a tuple containing the scalar value as well as the ``WeightType``, which denotes how the value was generated. observables: The observable(s) for which the expectation values will be calculated. @@ -90,11 +90,11 @@ def reconstruct_expectation_values( sorted_subsystems = sorted(subsystem_observables.keys()) # type: ignore # Reconstruct the expectation values - for i in range(len(weights)): + for i in range(len(coefficients)): current_expvals = np.ones((len(expvals),)) for label in sorted_subsystems: so = subsystem_observables[label] - weight = weights[i] + coeff = coefficients[i] subsystem_expvals = [ np.zeros(len(cog.commuting_observables)) for cog in so.groups ] @@ -114,7 +114,7 @@ def reconstruct_expectation_values( [subsystem_expvals[m][n] for m, n in so.lookup[subobservable]] ) - expvals += weight[0] * current_expvals + expvals += coeff[0] * current_expvals return list(expvals) From b9e1c9a7f0726dc7d48c97fa839211f19e5364c4 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Thu, 31 Aug 2023 16:46:07 -0500 Subject: [PATCH 03/29] cleanups --- circuit_knitting/cutting/cutting_reconstruction.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index d8743866e..df88a5772 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -40,9 +40,19 @@ def reconstruct_expectation_values( a single partition to the results. If the circuit was partitioned and its pieces run separately, the input 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 it is important to format your + subexperiments correctly before submitting to the Sampler primitive. + + :math:`[sample_{0}observable_{0}, \ldots, sample_{0}observable_{N}, sample_{1}observable_{0}, \ldots, sample_{M}observable_{N}]` + coefficients: The weights associated with each unique subexperiment. Each weight is a tuple - containing the scalar value as well as the ``WeightType``, which denotes - how the value was generated. + containing the scalar value as well as the :class:`WeightType`, which denotes + how the value was generated. The contributions from each subexperiment result to the final + expectation value will be scaled by this value. observables: The observable(s) for which the expectation values will be calculated. This should be a :class:`~qiskit.quantum_info.PauliList` if ``results`` is a :class:`~qiskit.primitives.SamplerResult` instance. Otherwise, it should be a From 9a5353481891a71496a917f46d0ac8002b35a72a Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Thu, 31 Aug 2023 16:48:16 -0500 Subject: [PATCH 04/29] cleanups --- circuit_knitting/cutting/cutting_reconstruction.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index df88a5772..b66b30b89 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -44,8 +44,9 @@ def reconstruct_expectation_values( 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 it is important to format your - subexperiments correctly before submitting to the Sampler primitive. + 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 format their subexperiments correctly 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}]` From 319cb952d4c36db459bf3a10e6c5e3d68d46d89c Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Fri, 1 Sep 2023 10:06:22 -0500 Subject: [PATCH 05/29] New workflow works with cutting_evaluation --- circuit_knitting/cutting/__init__.py | 4 +- .../cutting/cutting_evaluation.py | 87 +++++++------------ ...gate_cutting_to_reduce_circuit_width.ipynb | 2 +- 3 files changed, 31 insertions(+), 62 deletions(-) diff --git a/circuit_knitting/cutting/__init__.py b/circuit_knitting/cutting/__init__.py index 268123d27..5cada8bde 100644 --- a/circuit_knitting/cutting/__init__.py +++ b/circuit_knitting/cutting/__init__.py @@ -37,7 +37,6 @@ :template: autosummary/class_no_inherited_members.rst PartitionedCuttingProblem - CuttingExperimentResults instructions.CutWire instructions.Move @@ -87,7 +86,7 @@ decompose_gates, PartitionedCuttingProblem, ) -from .cutting_evaluation import execute_experiments, CuttingExperimentResults +from .cutting_evaluation import execute_experiments from .cutting_experiments import generate_cutting_experiments from .cutting_reconstruction import reconstruct_expectation_values from .wire_cutting_transforms import cut_wires, expand_observables @@ -101,7 +100,6 @@ "execute_experiments", "reconstruct_expectation_values", "PartitionedCuttingProblem", - "CuttingExperimentResults", "cut_wires", "expand_observables", ] diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index 995aed8d9..9f4d02cb9 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -38,13 +38,6 @@ from .cutting_decomposition import decompose_observables -class CuttingExperimentResults(NamedTuple): - """Circuit cutting subexperiment results and sampling coefficients.""" - - quasi_dists: list[list[list[tuple[QuasiDistribution, int]]]] - coeffs: Sequence[tuple[float, WeightType]] - - def execute_experiments( circuits: QuantumCircuit | dict[str | int, QuantumCircuit], subobservables: PauliList | dict[str | int, PauliList], @@ -64,10 +57,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 + - A list of :class:`~qiskit.primitives.SamplerResult` instances -- one for each partition. + - Coefficients corresponding to each unique subexperiment's sampling frequency Raises: ValueError: The number of requested samples must be at least one. @@ -120,58 +111,39 @@ def execute_experiments( # Generate the sub-experiments to run on backend ( - _, - coefficients, 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 + 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)) - ] + assert isinstance(samplers, dict) + samplers_dict = samplers - # 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]) + # Run experiments and append QPD bit information to the metadata + results = {label: samplers_dict[label].run(subexperiments_dict[label]).result() for label in sorted(subexperiments.keys())} + for label, result in results.items(): + for i, metadata in enumerate(result.metadata): + metadata['num_qpd_bits'] = len(subexperiments_dict[label][i].cregs[0]) + + results_out = results + if isinstance(circuits, QuantumCircuit): + assert len(results_dict.keys()) == 1 + results_out = results_dict[list(results_dict.keys())[0]] - return CuttingExperimentResults(quasi_dists, coefficients) + return results_out, coefficients def _append_measurement_circuit( @@ -380,23 +352,22 @@ def _run_experiments_batch( 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 + results_flat = sampler.run(experiments_flat).result() + # 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] + results_reshaped: list[list[SamplerResult]] = [[] 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]]] = [ + results: list[list[SamplerResult]] = [ [] for _ in range(len(subexperiments)) ] - for i, sample in enumerate(quasi_dists_reshaped): + for i, sample in enumerate(results_reshaped): for j, prob_dict in enumerate(sample): quasi_dists[i].append((prob_dict, num_qpd_bits[i][j])) 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 52346a54a..e9cbe0ab1 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 @@ -250,7 +250,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "7430eef1", "metadata": {}, "outputs": [], From 5870fb9d5ddc7645dd764acfaf84499ca1dbedd0 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Mon, 4 Sep 2023 21:35:13 -0500 Subject: [PATCH 06/29] Tests passing --- .../cutting/cutting_evaluation.py | 84 ++++++------------- .../circuit_cutting/__init__.py | 2 - test/cutting/test_cutting_evaluation.py | 26 +++--- 3 files changed, 41 insertions(+), 71 deletions(-) diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index 9f4d02cb9..f290923be 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -13,17 +13,14 @@ from __future__ import annotations -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 @@ -43,7 +40,9 @@ def execute_experiments( subobservables: PauliList | dict[str | int, PauliList], num_samples: int, samplers: BaseSampler | dict[str | int, BaseSampler], -) -> CuttingExperimentResults: +) -> tuple[ + SamplerResult | dict[str | int, SamplerResult], list[tuple[float, WeightType]] +]: r""" Generate the sampled circuits, append the observables, and run the sub-experiments. @@ -121,6 +120,7 @@ def execute_experiments( ) # Set up subexperiments and samplers + subexperiments_dict: dict[str | int, list[QuantumCircuit]] = {} if isinstance(subexperiments, list): subexperiments_dict = {"A": subexperiments} else: @@ -132,16 +132,34 @@ def execute_experiments( assert isinstance(samplers, dict) samplers_dict = samplers + jobs = {} + 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." + ) + jobs[label] = samplers_dict[label].run(subexperiments_dict[label]) + # Run experiments and append QPD bit information to the metadata - results = {label: samplers_dict[label].run(subexperiments_dict[label]).result() for label in sorted(subexperiments.keys())} + results = { + label: jobs[label].result() for label in sorted(subexperiments_dict.keys()) + } for label, result in results.items(): for i, metadata in enumerate(result.metadata): - metadata['num_qpd_bits'] = len(subexperiments_dict[label][i].cregs[0]) + metadata["num_qpd_bits"] = len(subexperiments_dict[label][i].cregs[0]) results_out = results if isinstance(circuits, QuantumCircuit): - assert len(results_dict.keys()) == 1 - results_out = results_dict[list(results_dict.keys())[0]] + assert len(results_out.keys()) == 1 + results_out = results[list(results.keys())[0]] return results_out, coefficients @@ -326,54 +344,6 @@ def _generate_cutting_experiments( 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 - results_flat = sampler.run(experiments_flat).result() - - - # Reshape the output data to match the input - results_reshaped: list[list[SamplerResult]] = [[] 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]) - count += 1 - - # Create the counts tuples, which include the number of QPD measurement bits - results: list[list[SamplerResult]] = [ - [] for _ in range(len(subexperiments)) - ] - for i, sample in enumerate(results_reshaped): - for j, prob_dict in enumerate(sample): - quasi_dists[i].append((prob_dict, num_qpd_bits[i][j])) - - return quasi_dists - - def _get_mapping_ids_by_partition( circuits: Sequence[QuantumCircuit], ) -> tuple[list[list[list[int]]], list[list[int]]]: diff --git a/circuit_knitting_toolbox/circuit_cutting/__init__.py b/circuit_knitting_toolbox/circuit_cutting/__init__.py index 68581cf78..d505824ec 100644 --- a/circuit_knitting_toolbox/circuit_cutting/__init__.py +++ b/circuit_knitting_toolbox/circuit_cutting/__init__.py @@ -22,7 +22,6 @@ execute_experiments, reconstruct_expectation_values, PartitionedCuttingProblem, - CuttingExperimentResults, qpd, cutqc, cutting_decomposition, @@ -39,7 +38,6 @@ "execute_experiments", "reconstruct_expectation_values", "PartitionedCuttingProblem", - "CuttingExperimentResults", ] sys.modules["circuit_knitting_toolbox.circuit_cutting.qpd"] = qpd 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) From 88fcaa850da3b7e8ccce7c1e09d301fc467e962c Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Mon, 4 Sep 2023 21:50:56 -0500 Subject: [PATCH 07/29] re-add CuttingExperimentResults --- circuit_knitting/cutting/__init__.py | 4 +++- circuit_knitting/cutting/cutting_evaluation.py | 14 ++++++++++---- .../circuit_cutting/__init__.py | 2 ++ 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/circuit_knitting/cutting/__init__.py b/circuit_knitting/cutting/__init__.py index 5cada8bde..65791c0ab 100644 --- a/circuit_knitting/cutting/__init__.py +++ b/circuit_knitting/cutting/__init__.py @@ -29,6 +29,7 @@ decompose_gates generate_cutting_experiments execute_experiments + CuttingExperimentResults reconstruct_expectation_values .. autosummary:: @@ -86,7 +87,7 @@ decompose_gates, PartitionedCuttingProblem, ) -from .cutting_evaluation import execute_experiments +from .cutting_evaluation import execute_experiments, CuttingExperimentResults from .cutting_experiments import generate_cutting_experiments from .cutting_reconstruction import reconstruct_expectation_values from .wire_cutting_transforms import cut_wires, expand_observables @@ -98,6 +99,7 @@ "decompose_gates", "generate_cutting_experiments", "execute_experiments", + "CuttingExperimentResults", "reconstruct_expectation_values", "PartitionedCuttingProblem", "cut_wires", diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index f290923be..cffe28c31 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -13,6 +13,7 @@ from __future__ import annotations +from typing import NamedTuple from collections import defaultdict from collections.abc import Sequence @@ -35,14 +36,19 @@ from .cutting_decomposition import decompose_observables +class CuttingExperimentResults(NamedTuple): + """Circuit cutting subexperiment results and sampling coefficients.""" + + results: SamplerResult | dict[str | int, SamplerResult] + coeffs: Sequence[tuple[float, WeightType]] + + def execute_experiments( circuits: QuantumCircuit | dict[str | int, QuantumCircuit], subobservables: PauliList | dict[str | int, PauliList], num_samples: int, samplers: BaseSampler | dict[str | int, BaseSampler], -) -> tuple[ - SamplerResult | dict[str | int, SamplerResult], list[tuple[float, WeightType]] -]: + ) -> CuttingExperimentResults: r""" Generate the sampled circuits, append the observables, and run the sub-experiments. @@ -161,7 +167,7 @@ def execute_experiments( assert len(results_out.keys()) == 1 results_out = results[list(results.keys())[0]] - return results_out, coefficients + return CuttingExperimentResults(results=results_out, coeffs=coefficients) def _append_measurement_circuit( diff --git a/circuit_knitting_toolbox/circuit_cutting/__init__.py b/circuit_knitting_toolbox/circuit_cutting/__init__.py index d505824ec..f47c34aab 100644 --- a/circuit_knitting_toolbox/circuit_cutting/__init__.py +++ b/circuit_knitting_toolbox/circuit_cutting/__init__.py @@ -20,6 +20,7 @@ cut_gates, decompose_gates, execute_experiments, + CuttingExperimentResults, reconstruct_expectation_values, PartitionedCuttingProblem, qpd, @@ -36,6 +37,7 @@ "cut_gates", "decompose_gates", "execute_experiments", + "CuttingExperimentResults", "reconstruct_expectation_values", "PartitionedCuttingProblem", ] From e81cffb5dbfc9e5550fbe5cc368d400243c8fb25 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Mon, 4 Sep 2023 22:06:12 -0500 Subject: [PATCH 08/29] cleanup --- circuit_knitting/cutting/__init__.py | 6 +-- .../cutting/cutting_evaluation.py | 38 ++++--------------- ...gate_cutting_to_reduce_circuit_width.ipynb | 2 +- 3 files changed, 12 insertions(+), 34 deletions(-) diff --git a/circuit_knitting/cutting/__init__.py b/circuit_knitting/cutting/__init__.py index 65791c0ab..a22e68916 100644 --- a/circuit_knitting/cutting/__init__.py +++ b/circuit_knitting/cutting/__init__.py @@ -29,7 +29,6 @@ decompose_gates generate_cutting_experiments execute_experiments - CuttingExperimentResults reconstruct_expectation_values .. autosummary:: @@ -38,6 +37,7 @@ :template: autosummary/class_no_inherited_members.rst PartitionedCuttingProblem + CuttingExperimentResults instructions.CutWire instructions.Move @@ -87,7 +87,7 @@ decompose_gates, PartitionedCuttingProblem, ) -from .cutting_evaluation import execute_experiments, CuttingExperimentResults +from .cutting_evaluation import execute_experiments from .cutting_experiments import generate_cutting_experiments from .cutting_reconstruction import reconstruct_expectation_values from .wire_cutting_transforms import cut_wires, expand_observables @@ -99,9 +99,9 @@ "decompose_gates", "generate_cutting_experiments", "execute_experiments", - "CuttingExperimentResults", "reconstruct_expectation_values", "PartitionedCuttingProblem", + "CuttingExperimentResults", "cut_wires", "expand_observables", ] diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index cffe28c31..595ad6464 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -48,7 +48,7 @@ def execute_experiments( subobservables: PauliList | dict[str | int, PauliList], num_samples: int, samplers: BaseSampler | dict[str | int, BaseSampler], - ) -> CuttingExperimentResults: +) -> CuttingExperimentResults: r""" Generate the sampled circuits, append the observables, and run the sub-experiments. @@ -115,14 +115,8 @@ def execute_experiments( _validate_samplers(samplers) # Generate the sub-experiments to run on backend - ( - subexperiments, - coefficients, - _, - ) = _generate_cutting_experiments( - circuits, - subobservables, - num_samples, + subexperiments, coefficients = _generate_cutting_experiments( + circuits, subobservables, num_samples ) # Set up subexperiments and samplers @@ -138,6 +132,8 @@ def execute_experiments( assert isinstance(samplers, dict) samplers_dict = samplers + # Make sure all input circuits are clear of classical regs. + # Submit a job for each circuit partition. jobs = {} for label in sorted(subexperiments_dict.keys()): for circ in subexperiments_dict[label]: @@ -154,7 +150,7 @@ def execute_experiments( ) jobs[label] = samplers_dict[label].run(subexperiments_dict[label]) - # Run experiments and append QPD bit information to the metadata + # Collect the results from each job, and add the number of qpd bits for each circuit to the metadata. results = { label: jobs[label].result() for label in sorted(subexperiments_dict.keys()) } @@ -162,6 +158,7 @@ def execute_experiments( 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): assert len(results_out.keys()) == 1 @@ -319,25 +316,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] @@ -347,7 +325,7 @@ def _generate_cutting_experiments( assert len(subexperiments_out.keys()) == 1 subexperiments_out = list(subexperiments_dict.values())[0] - return subexperiments_out, weights, subexperiments_legacy + return subexperiments_out, weights def _get_mapping_ids_by_partition( 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 e9cbe0ab1..52346a54a 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 @@ -250,7 +250,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "id": "7430eef1", "metadata": {}, "outputs": [], From 2ac87253f86bd9d82441436ff8780cf444645178 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Mon, 4 Sep 2023 22:16:45 -0500 Subject: [PATCH 09/29] mypy --- circuit_knitting/cutting/cutting_evaluation.py | 3 +-- circuit_knitting/cutting/cutting_experiments.py | 2 +- circuit_knitting/cutting/cutting_reconstruction.py | 6 +++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index 595ad6464..5bcfb4e9c 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -239,7 +239,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( @@ -288,7 +287,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) 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 b66b30b89..5582a11b4 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -98,7 +98,7 @@ def reconstruct_expectation_values( label: ObservableCollection(subobservables) for label, subobservables in subobservables_by_subsystem.items() } - sorted_subsystems = sorted(subsystem_observables.keys()) # type: ignore + sorted_subsystems = sorted(subsystem_observables.keys()) # Reconstruct the expectation values for i in range(len(coefficients)): @@ -110,8 +110,8 @@ def reconstruct_expectation_values( np.zeros(len(cog.commuting_observables)) for cog in so.groups ] for k, cog in enumerate(so.groups): - quasi_probs = results_dict[label].quasi_dists[i * len(so.groups) + k] # type: ignore - for outcome, quasi_prob in quasi_probs.items(): # type: ignore + 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( results_dict[label].metadata[i * len(so.groups) + k][ "num_qpd_bits" From c88ba68e807eac53ce6d744f3a29d1b3e9b5de7a Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Mon, 4 Sep 2023 22:21:24 -0500 Subject: [PATCH 10/29] fix inits --- circuit_knitting/cutting/__init__.py | 2 +- circuit_knitting_toolbox/circuit_cutting/__init__.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/circuit_knitting/cutting/__init__.py b/circuit_knitting/cutting/__init__.py index a22e68916..268123d27 100644 --- a/circuit_knitting/cutting/__init__.py +++ b/circuit_knitting/cutting/__init__.py @@ -87,7 +87,7 @@ decompose_gates, PartitionedCuttingProblem, ) -from .cutting_evaluation import execute_experiments +from .cutting_evaluation import execute_experiments, CuttingExperimentResults from .cutting_experiments import generate_cutting_experiments from .cutting_reconstruction import reconstruct_expectation_values from .wire_cutting_transforms import cut_wires, expand_observables diff --git a/circuit_knitting_toolbox/circuit_cutting/__init__.py b/circuit_knitting_toolbox/circuit_cutting/__init__.py index f47c34aab..68581cf78 100644 --- a/circuit_knitting_toolbox/circuit_cutting/__init__.py +++ b/circuit_knitting_toolbox/circuit_cutting/__init__.py @@ -20,9 +20,9 @@ cut_gates, decompose_gates, execute_experiments, - CuttingExperimentResults, reconstruct_expectation_values, PartitionedCuttingProblem, + CuttingExperimentResults, qpd, cutqc, cutting_decomposition, @@ -37,9 +37,9 @@ "cut_gates", "decompose_gates", "execute_experiments", - "CuttingExperimentResults", "reconstruct_expectation_values", "PartitionedCuttingProblem", + "CuttingExperimentResults", ] sys.modules["circuit_knitting_toolbox.circuit_cutting.qpd"] = qpd From 5583ed6017496509c6735f9dbb5bf945b2be81ee Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Mon, 4 Sep 2023 22:43:49 -0500 Subject: [PATCH 11/29] release notes --- releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml | 4 ++++ releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml | 4 ++++ 2 files changed, 8 insertions(+) create mode 100644 releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml create mode 100644 releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml diff --git a/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml b/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml new file mode 100644 index 000000000..f9e005949 --- /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-distributions for each subexperiment can be accessed via the ``SamplerResult.quasi_dists[i]`` field. The number of QPD bits contained in each subexperiment will be included in the output within each ``SamplerResult.metadata[i]["num_qpd_bits"] field. 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..acdb3a0db --- /dev/null +++ b/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + :func:`reconstruct_expectation_values` now takes a :class:`~qiskit.primitives.SamplerResult` instance or a dictionary mapping partition labels to :class:`~qiskit.primitives.SamplerResult` instances as input to the first positional arg, ``results``. ``results`` replaces the old ``quasi_dists`` positional arg. 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 ``SamplerResult.metadata[i]["num_qpd_bits"]`` field. From cd95e28bc61bff28edb2c23c60c39dcb71df287b Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Mon, 4 Sep 2023 23:09:00 -0500 Subject: [PATCH 12/29] Remove private generate_cutting_experiments --- .../cutting/cutting_evaluation.py | 97 +------------------ 1 file changed, 1 insertion(+), 96 deletions(-) diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index 5bcfb4e9c..cdd8fe588 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -70,7 +70,7 @@ def execute_experiments( ValueError: The types of ``circuits`` and ``subobservables`` arguments are incompatible. ValueError: ``SingleQubitQPDGate``\ s are not supported in unseparable circuits. ValueError: The keys for the input dictionaries are not equivalent. - ValueError: The input circuits may not contain any classical registers or bits. + ValueError: One or more input circuit contains classical registers. ValueError: If multiple samplers are passed, each one must be unique. """ if not num_samples >= 1: @@ -232,101 +232,6 @@ def _append_measurement_circuit( return qc -def _generate_cutting_experiments( - circuits: QuantumCircuit | dict[str | int, QuantumCircuit], - observables: PauliList | dict[str | int, PauliList], - num_samples: int | float, -) -> tuple[ - list[QuantumCircuit] | dict[str | int, list[QuantumCircuit]], - list[tuple[float, WeightType]], -]: - if isinstance(circuits, QuantumCircuit) and not isinstance(observables, PauliList): - raise ValueError( - "If the input circuits is a QuantumCircuit, the observables must be a PauliList." - ) - if isinstance(circuits, dict) and not isinstance(observables, dict): - raise ValueError( - "If the input circuits are contained in a dictionary keyed by partition labels, the input observables must also be represented by such a dictionary." - ) - if not num_samples >= 1: - raise ValueError("num_samples must be at least 1.") - - # Retrieving the unique bases, QPD gates, and decomposed observables is slightly different - # depending on the format of the execute_experiments input args, but the 2nd half of this function - # can be shared between both cases. - if isinstance(circuits, QuantumCircuit): - is_separated = False - subcircuit_list = [circuits] - subobservables_by_subsystem = decompose_observables( - observables, "A" * len(observables[0]) - ) - subsystem_observables = { - label: ObservableCollection(subobservables) - for label, subobservables in subobservables_by_subsystem.items() - } - # Gather the unique bases from the circuit - bases, qpd_gate_ids = _get_bases(circuits) - subcirc_qpd_gate_ids = [qpd_gate_ids] - - else: - is_separated = True - subcircuit_list = [circuits[key] for key in sorted(circuits.keys())] - # Gather the unique bases across the subcircuits - subcirc_qpd_gate_ids, subcirc_map_ids = _get_mapping_ids_by_partition( - subcircuit_list - ) - bases = _get_bases_by_partition(subcircuit_list, subcirc_qpd_gate_ids) - - # Create the commuting observable groups - subsystem_observables = { - label: ObservableCollection(so) for label, so in observables.items() - } - - # Sample the joint quasiprobability decomposition - random_samples = generate_qpd_weights(bases, num_samples=num_samples) - - # 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()]) - - # Sort samples in descending order of frequency - sorted_samples = sorted(random_samples.items(), key=lambda x: x[1][0], reverse=True) - - # Generate the output experiments and weights - subexperiments_dict: dict[str | int, list[QuantumCircuit]] = defaultdict(list) - weights: list[tuple[float, WeightType]] = [] - for z, (map_ids, (redundancy, weight_type)) in enumerate(sorted_samples): - actual_coeff = np.prod( - [basis.coeffs[map_id] for basis, map_id in strict_zip(bases, map_ids)] - ) - sampled_coeff = (redundancy / num_samples) * (kappa * np.sign(actual_coeff)) - weights.append((sampled_coeff, weight_type)) - map_ids_tmp = map_ids - for i, (subcircuit, label) in enumerate( - strict_zip(subcircuit_list, sorted(subsystem_observables.keys())) - ): - 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 - ) - so = subsystem_observables[label] - for j, cog in enumerate(so.groups): - meas_qc = _append_measurement_circuit(decomp_qc, cog) - subexperiments_dict[label].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] - ] = dict(subexperiments_dict) - assert isinstance(subexperiments_out, dict) - if isinstance(circuits, QuantumCircuit): - assert len(subexperiments_out.keys()) == 1 - subexperiments_out = list(subexperiments_dict.values())[0] - - return subexperiments_out, weights - - def _get_mapping_ids_by_partition( circuits: Sequence[QuantumCircuit], ) -> tuple[list[list[list[int]]], list[list[int]]]: From 2e6f36fb0d6a27c6389189fc3502384b73886cef Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Mon, 4 Sep 2023 23:32:19 -0500 Subject: [PATCH 13/29] Revert "Remove private generate_cutting_experiments" This reverts commit cd95e28bc61bff28edb2c23c60c39dcb71df287b. --- .../cutting/cutting_evaluation.py | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index cdd8fe588..5bcfb4e9c 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -70,7 +70,7 @@ def execute_experiments( ValueError: The types of ``circuits`` and ``subobservables`` arguments are incompatible. ValueError: ``SingleQubitQPDGate``\ s are not supported in unseparable circuits. ValueError: The keys for the input dictionaries are not equivalent. - ValueError: One or more input circuit contains classical registers. + ValueError: The input circuits may not contain any classical registers or bits. ValueError: If multiple samplers are passed, each one must be unique. """ if not num_samples >= 1: @@ -232,6 +232,101 @@ def _append_measurement_circuit( return qc +def _generate_cutting_experiments( + circuits: QuantumCircuit | dict[str | int, QuantumCircuit], + observables: PauliList | dict[str | int, PauliList], + num_samples: int | float, +) -> tuple[ + list[QuantumCircuit] | dict[str | int, list[QuantumCircuit]], + list[tuple[float, WeightType]], +]: + if isinstance(circuits, QuantumCircuit) and not isinstance(observables, PauliList): + raise ValueError( + "If the input circuits is a QuantumCircuit, the observables must be a PauliList." + ) + if isinstance(circuits, dict) and not isinstance(observables, dict): + raise ValueError( + "If the input circuits are contained in a dictionary keyed by partition labels, the input observables must also be represented by such a dictionary." + ) + if not num_samples >= 1: + raise ValueError("num_samples must be at least 1.") + + # Retrieving the unique bases, QPD gates, and decomposed observables is slightly different + # depending on the format of the execute_experiments input args, but the 2nd half of this function + # can be shared between both cases. + if isinstance(circuits, QuantumCircuit): + is_separated = False + subcircuit_list = [circuits] + subobservables_by_subsystem = decompose_observables( + observables, "A" * len(observables[0]) + ) + subsystem_observables = { + label: ObservableCollection(subobservables) + for label, subobservables in subobservables_by_subsystem.items() + } + # Gather the unique bases from the circuit + bases, qpd_gate_ids = _get_bases(circuits) + subcirc_qpd_gate_ids = [qpd_gate_ids] + + else: + is_separated = True + subcircuit_list = [circuits[key] for key in sorted(circuits.keys())] + # Gather the unique bases across the subcircuits + subcirc_qpd_gate_ids, subcirc_map_ids = _get_mapping_ids_by_partition( + subcircuit_list + ) + bases = _get_bases_by_partition(subcircuit_list, subcirc_qpd_gate_ids) + + # Create the commuting observable groups + subsystem_observables = { + label: ObservableCollection(so) for label, so in observables.items() + } + + # Sample the joint quasiprobability decomposition + random_samples = generate_qpd_weights(bases, num_samples=num_samples) + + # 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()]) + + # Sort samples in descending order of frequency + sorted_samples = sorted(random_samples.items(), key=lambda x: x[1][0], reverse=True) + + # Generate the output experiments and weights + subexperiments_dict: dict[str | int, list[QuantumCircuit]] = defaultdict(list) + weights: list[tuple[float, WeightType]] = [] + for z, (map_ids, (redundancy, weight_type)) in enumerate(sorted_samples): + actual_coeff = np.prod( + [basis.coeffs[map_id] for basis, map_id in strict_zip(bases, map_ids)] + ) + sampled_coeff = (redundancy / num_samples) * (kappa * np.sign(actual_coeff)) + weights.append((sampled_coeff, weight_type)) + map_ids_tmp = map_ids + for i, (subcircuit, label) in enumerate( + strict_zip(subcircuit_list, sorted(subsystem_observables.keys())) + ): + 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 + ) + so = subsystem_observables[label] + for j, cog in enumerate(so.groups): + meas_qc = _append_measurement_circuit(decomp_qc, cog) + subexperiments_dict[label].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] + ] = dict(subexperiments_dict) + assert isinstance(subexperiments_out, dict) + if isinstance(circuits, QuantumCircuit): + assert len(subexperiments_out.keys()) == 1 + subexperiments_out = list(subexperiments_dict.values())[0] + + return subexperiments_out, weights + + def _get_mapping_ids_by_partition( circuits: Sequence[QuantumCircuit], ) -> tuple[list[list[list[int]]], list[list[int]]]: From a878f08c10fe403c5634d5b6db3fe2b1a9f4ea90 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Tue, 5 Sep 2023 00:49:05 -0500 Subject: [PATCH 14/29] fix sphinx --- releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml | 2 +- releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml b/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml index f9e005949..cb832f621 100644 --- a/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml +++ b/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml @@ -1,4 +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-distributions for each subexperiment can be accessed via the ``SamplerResult.quasi_dists[i]`` field. The number of QPD bits contained in each subexperiment will be included in the output within each ``SamplerResult.metadata[i]["num_qpd_bits"] field. The output of this function is still valid as input to :func:`reconstruct_expectation_values`. + 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-distributions 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 index acdb3a0db..22ec6825c 100644 --- a/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml +++ b/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml @@ -1,4 +1,4 @@ --- upgrade: - | - :func:`reconstruct_expectation_values` now takes a :class:`~qiskit.primitives.SamplerResult` instance or a dictionary mapping partition labels to :class:`~qiskit.primitives.SamplerResult` instances as input to the first positional arg, ``results``. ``results`` replaces the old ``quasi_dists`` positional arg. 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 ``SamplerResult.metadata[i]["num_qpd_bits"]`` field. + :func:`reconstruct_expectation_values` now takes a :class:`~qiskit.primitives.SamplerResult` instance or a dictionary mapping partition labels to :class:`~qiskit.primitives.SamplerResult` instances as input to the first positional arg, ``results``. ``results`` replaces the old ``quasi_dists`` positional arg. 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. From 59015e1e12f2ad74807f0ab4f8caf60201a9f52f Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Tue, 5 Sep 2023 12:53:17 -0500 Subject: [PATCH 15/29] Add num_qpd_bit checks and tests --- .../cutting/cutting_evaluation.py | 4 +-- .../cutting/cutting_reconstruction.py | 30 ++++++++++++++----- test/cutting/test_cutting_reconstruction.py | 25 ++++++++++++++++ 3 files changed, 49 insertions(+), 10 deletions(-) diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index 5bcfb4e9c..a5c4a00b9 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -62,7 +62,7 @@ def execute_experiments( samplers: Sampler(s) on which to run the sub-experiments. Returns: - - A list of :class:`~qiskit.primitives.SamplerResult` instances -- one for each partition. + - One :class:`~qiskit.primitives.SamplerResult` instance for each partition. - Coefficients corresponding to each unique subexperiment's sampling frequency Raises: @@ -132,7 +132,7 @@ def execute_experiments( assert isinstance(samplers, dict) samplers_dict = samplers - # Make sure all input circuits are clear of classical regs. + # Make sure the first two cregs in each circuit are for QPD and observable measurements # Submit a job for each circuit partition. jobs = {} for label in sorted(subexperiments_dict.keys()): diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index 5582a11b4..8c913c1d8 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -66,6 +66,8 @@ 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( @@ -112,13 +114,19 @@ 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(): - subsystem_expvals[k] += quasi_prob * _process_outcome( - results_dict[label].metadata[i * len(so.groups) + k][ - "num_qpd_bits" - ], - cog, - outcome, - ) + try: + subsystem_expvals[k] += quasi_prob * _process_outcome( + results_dict[label].metadata[i * len(so.groups) + k][ + "num_qpd_bits" + ], + cog, + outcome, + ) + except KeyError: + raise ValueError( + "The num_qpd_bits field must be set in each subexperiment " + "result metadata dictionary." + ) for k, subobservable in enumerate(subobservables_by_subsystem[label]): current_expvals[k] *= np.mean( @@ -150,7 +158,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/test/cutting/test_cutting_reconstruction.py b/test/cutting/test_cutting_reconstruction.py index 30349383b..4bb4eab7a 100644 --- a/test/cutting/test_cutting_reconstruction.py +++ b/test/cutting/test_cutting_reconstruction.py @@ -107,6 +107,31 @@ 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]), From c2877846c4eb0f394b5a2a44e9bafdaa64b46834 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Tue, 5 Sep 2023 13:41:05 -0500 Subject: [PATCH 16/29] fix hanging jobs :( --- circuit_knitting/cutting/cutting_evaluation.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index a5c4a00b9..af93d6adb 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -133,8 +133,8 @@ def execute_experiments( samplers_dict = samplers # Make sure the first two cregs in each circuit are for QPD and observable measurements - # Submit a job for each circuit partition. - jobs = {} + # Run a job for each partition and collect results + results = {} for label in sorted(subexperiments_dict.keys()): for circ in subexperiments_dict[label]: if ( @@ -148,12 +148,8 @@ def execute_experiments( raise ValueError( "Circuits input to execute_experiments should contain no classical registers or bits." ) - jobs[label] = samplers_dict[label].run(subexperiments_dict[label]) + results[label] = samplers_dict[label].run(subexperiments_dict[label]).result() - # Collect the results from each job, and add the number of qpd bits for each circuit to the metadata. - results = { - label: jobs[label].result() for label in sorted(subexperiments_dict.keys()) - } for label, result in results.items(): for i, metadata in enumerate(result.metadata): metadata["num_qpd_bits"] = len(subexperiments_dict[label][i].cregs[0]) From aa0ab1806d90060a47c34d97f584dbf2e6d3e05d Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 13:43:21 -0500 Subject: [PATCH 17/29] Update circuit_knitting/cutting/cutting_evaluation.py Co-authored-by: Jim Garrison --- circuit_knitting/cutting/cutting_evaluation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py index af93d6adb..7386d63c4 100644 --- a/circuit_knitting/cutting/cutting_evaluation.py +++ b/circuit_knitting/cutting/cutting_evaluation.py @@ -63,7 +63,7 @@ def execute_experiments( Returns: - One :class:`~qiskit.primitives.SamplerResult` instance for each partition. - - Coefficients corresponding to each unique subexperiment's sampling frequency + - Coefficients corresponding to each unique subexperiment's contribution to the reconstructed result Raises: ValueError: The number of requested samples must be at least one. From b3d7e7538bdb62e19142a4685651d7e32e855d03 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 16:39:10 -0500 Subject: [PATCH 18/29] Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison --- circuit_knitting/cutting/cutting_reconstruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index 8c913c1d8..7bf5ccced 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -35,7 +35,7 @@ def reconstruct_expectation_values( Args: results: The results from running the cutting subexperiments. If the cut circuit - was not partitioned between qubits and run separately, the input should be + 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 run separately, the input should be a dictionary mapping partition labels From 1761c4be8161d973153971894bcf36d98fe691cf Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 16:39:26 -0500 Subject: [PATCH 19/29] Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison --- circuit_knitting/cutting/cutting_reconstruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index 7bf5ccced..4d76885ee 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -42,7 +42,7 @@ def reconstruct_expectation_values( 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 + 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 format their subexperiments correctly before submitting them From 86c6be71b33351da28643fb10a55ef5deeaad042 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 16:39:48 -0500 Subject: [PATCH 20/29] Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison --- circuit_knitting/cutting/cutting_reconstruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index 4d76885ee..75f08ee91 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -44,7 +44,7 @@ def reconstruct_expectation_values( 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` + 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 format their subexperiments correctly before submitting them to the sampler primitive. From b246226eb1142190171d47f6d7e5e0ef81a9a81a Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 16:40:19 -0500 Subject: [PATCH 21/29] Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison --- circuit_knitting/cutting/cutting_reconstruction.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index 75f08ee91..45941b657 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -45,8 +45,8 @@ def reconstruct_expectation_values( 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 format their subexperiments correctly before submitting them - to the sampler primitive. + 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}]` From a536ef7c015498527323c062a2c09d424e2a2f75 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 16:42:13 -0500 Subject: [PATCH 22/29] Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison --- circuit_knitting/cutting/cutting_reconstruction.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index 45941b657..a4a246430 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -50,10 +50,10 @@ def reconstruct_expectation_values( :math:`[sample_{0}observable_{0}, \ldots, sample_{0}observable_{N}, sample_{1}observable_{0}, \ldots, sample_{M}observable_{N}]` - coefficients: The weights associated with each unique subexperiment. Each weight is a tuple - containing the scalar value as well as the :class:`WeightType`, which denotes - how the value was generated. The contributions from each subexperiment result to the final - expectation value will be scaled by this value. + 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 ``results`` is a :class:`~qiskit.primitives.SamplerResult` instance. Otherwise, it should be a From 682ee459d7136440fd0f695e1972db7450aaa211 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 16:42:43 -0500 Subject: [PATCH 23/29] Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison --- circuit_knitting/cutting/cutting_reconstruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index a4a246430..bd486d31e 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -103,7 +103,7 @@ def reconstruct_expectation_values( sorted_subsystems = sorted(subsystem_observables.keys()) # Reconstruct the expectation values - for i in range(len(coefficients)): + for i, coeff in enumerate(coefficients): current_expvals = np.ones((len(expvals),)) for label in sorted_subsystems: so = subsystem_observables[label] From 3b594b48d406029d309fb27f32fe25ae71a75c76 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 16:43:01 -0500 Subject: [PATCH 24/29] Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison --- circuit_knitting/cutting/cutting_reconstruction.py | 1 - 1 file changed, 1 deletion(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index bd486d31e..c6b703574 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -107,7 +107,6 @@ def reconstruct_expectation_values( current_expvals = np.ones((len(expvals),)) for label in sorted_subsystems: so = subsystem_observables[label] - coeff = coefficients[i] subsystem_expvals = [ np.zeros(len(cog.commuting_observables)) for cog in so.groups ] From 7a8cd6109a9fb74fbab405ee75f1398f6b7d5d9b Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 16:47:41 -0500 Subject: [PATCH 25/29] peer review --- .../cutting/cutting_reconstruction.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index c6b703574..a2bfc2497 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -114,18 +114,20 @@ def reconstruct_expectation_values( quasi_probs = results_dict[label].quasi_dists[i * len(so.groups) + k] for outcome, quasi_prob in quasi_probs.items(): try: - subsystem_expvals[k] += quasi_prob * _process_outcome( - results_dict[label].metadata[i * len(so.groups) + k][ - "num_qpd_bits" - ], - cog, - outcome, - ) + 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( From cbc5d1409acfc72d2f6bb894c1386a8e040e155d Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 16:49:42 -0500 Subject: [PATCH 26/29] Fix links in release notes --- releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml | 2 +- releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml b/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml index cb832f621..5c0d8d7b6 100644 --- a/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml +++ b/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml @@ -1,4 +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-distributions 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`. + 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-distributions 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 index 22ec6825c..37e1f44d5 100644 --- a/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml +++ b/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml @@ -1,4 +1,4 @@ --- upgrade: - | - :func:`reconstruct_expectation_values` now takes a :class:`~qiskit.primitives.SamplerResult` instance or a dictionary mapping partition labels to :class:`~qiskit.primitives.SamplerResult` instances as input to the first positional arg, ``results``. ``results`` replaces the old ``quasi_dists`` positional arg. 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. + :func:`.reconstruct_expectation_values` now takes a :class:`~qiskit.primitives.SamplerResult` instance or a dictionary mapping partition labels to :class:`~qiskit.primitives.SamplerResult` instances as input to the first positional arg, ``results``. ``results`` replaces the old ``quasi_dists`` positional arg. 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. From b55cc2f337725c73c99d76a6e44b70746eccb6aa Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Wed, 6 Sep 2023 16:51:04 -0500 Subject: [PATCH 27/29] quasi-dist(s) --- releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml b/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml index 5c0d8d7b6..d4386f99f 100644 --- a/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml +++ b/releasenotes/notes/refactor-evaluate-05fe26e94ff68166.yaml @@ -1,4 +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-distributions 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`. + 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`. From 8bcc91020756bc8a001e0898e1880e762d9200d4 Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Thu, 7 Sep 2023 10:16:28 -0500 Subject: [PATCH 28/29] Update circuit_knitting/cutting/cutting_reconstruction.py Co-authored-by: Jim Garrison --- circuit_knitting/cutting/cutting_reconstruction.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index a2bfc2497..9a01579b8 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -38,7 +38,7 @@ def reconstruct_expectation_values( 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 run separately, the input should be a dictionary mapping partition labels + 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 From 926cf3f8f5983b04791cf00d5054f7054eb02fcc Mon Sep 17 00:00:00 2001 From: Caleb Johnson Date: Fri, 8 Sep 2023 12:09:19 -0500 Subject: [PATCH 29/29] Update releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml Co-authored-by: Jim Garrison --- releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml b/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml index 37e1f44d5..ac797bc3c 100644 --- a/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml +++ b/releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml @@ -1,4 +1,4 @@ --- upgrade: - | - :func:`.reconstruct_expectation_values` now takes a :class:`~qiskit.primitives.SamplerResult` instance or a dictionary mapping partition labels to :class:`~qiskit.primitives.SamplerResult` instances as input to the first positional arg, ``results``. ``results`` replaces the old ``quasi_dists`` positional arg. 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. + :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.