diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py index ca1d80df..042158f6 100644 --- a/circuit_knitting/cutting/cutting_reconstruction.py +++ b/circuit_knitting/cutting/cutting_reconstruction.py @@ -13,7 +13,7 @@ from __future__ import annotations -from collections.abc import Sequence, Hashable +from collections.abc import Sequence, Hashable, Mapping import numpy as np from qiskit.quantum_info import PauliList @@ -75,28 +75,32 @@ def reconstruct_expectation_values( ValueError: ``observables`` and ``results`` are of incompatible types. ValueError: An input observable has a phase not equal to 1. """ - if isinstance(observables, PauliList) and not isinstance( - results, (SamplerResult, PrimitiveResult) - ): - raise ValueError( - "If observables is a PauliList, results must be a SamplerResult or PrimitiveResult instance." - ) - 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 circuit was not separated, transform input data structures to + # dictionary format. Perform some input validation in either case. if isinstance(observables, PauliList): + if not isinstance(results, (SamplerResult, PrimitiveResult)): + raise ValueError( + "If observables is a PauliList, results must be a SamplerResult or PrimitiveResult instance." + ) 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]) + subobservables_by_subsystem: Mapping[Hashable, PauliList] = ( + decompose_observables(observables, "A" * len(observables[0])) ) - results_dict: dict[Hashable, SamplerResult | PrimitiveResult] = {"A": results} + results_dict: Mapping[Hashable, SamplerResult | PrimitiveResult] = { + "A": results + } expvals = np.zeros(len(observables)) - else: + elif isinstance(observables, Mapping): + if not isinstance(results, Mapping): + raise ValueError( + "If observables is a dictionary, results must also be a dictionary." + ) + if observables.keys() != results.keys(): + raise ValueError( + "The subsystem labels of the observables and results do not match." + ) results_dict = results for label, subobservable in observables.items(): if any(obs.phase != 0 for obs in subobservable): @@ -104,11 +108,29 @@ def reconstruct_expectation_values( subobservables_by_subsystem = observables expvals = np.zeros(len(list(observables.values())[0])) + else: + raise ValueError("observables must be either a PauliList or dict.") + subsystem_observables = { label: ObservableCollection(subobservables) for label, subobservables in subobservables_by_subsystem.items() } + # Validate that the number of subexperiments executed is consistent with + # the number of coefficients and observable groups. + for label, so in subsystem_observables.items(): + current_result = results_dict[label] + if isinstance(current_result, SamplerResult): + # SamplerV1 provides a SamplerResult + current_result = current_result.quasi_dists + if len(current_result) != len(coefficients) * len(so.groups): + raise ValueError( + f"The number of subexperiments performed in subsystem '{label}' " + f"({len(current_result)}) should equal the number of coefficients " + f"({len(coefficients)}) times the number of mutually commuting " + f"subobservable groups ({len(so.groups)}), but it does not." + ) + # Reconstruct the expectation values for i, coeff in enumerate(coefficients): current_expvals = np.ones((len(expvals),)) diff --git a/test/cutting/test_cutting_reconstruction.py b/test/cutting/test_cutting_reconstruction.py index 208f0069..7b2495b5 100644 --- a/test/cutting/test_cutting_reconstruction.py +++ b/test/cutting/test_cutting_reconstruction.py @@ -22,7 +22,7 @@ BitArray, ) from qiskit.primitives.containers import make_data_bin -from qiskit.quantum_info import Pauli, PauliList +from qiskit.quantum_info import Pauli, PauliList, SparsePauliOp from circuit_knitting.utils.observable_grouping import CommutingObservableGroup from circuit_knitting.cutting.qpd import WeightType @@ -48,7 +48,7 @@ def test_cutting_reconstruction(self): observables = PauliList(["ZZ"]) expvals = reconstruct_expectation_values(results, weights, observables) self.assertEqual([1.0], expvals) - with self.subTest("Test mismatching inputs"): + with self.subTest("Test mismatching input types"): results = SamplerResult( quasi_dists=[QuasiDistribution({"0": 1.0})], metadata=[{}] ) @@ -68,6 +68,32 @@ def test_cutting_reconstruction(self): e_info.value.args[0] == "If observables is a PauliList, results must be a SamplerResult or PrimitiveResult instance." ) + with self.subTest("Test invalid observables type"): + results = SamplerResult( + quasi_dists=[QuasiDistribution({"0": 1.0})], metadata=[{}] + ) + weights = [(1.0, WeightType.EXACT)] + observables = [SparsePauliOp(["ZZ"])] + with pytest.raises(ValueError) as e_info: + reconstruct_expectation_values(results, weights, observables) + assert ( + e_info.value.args[0] + == "observables must be either a PauliList or dict." + ) + with self.subTest("Test mismatching subsystem labels"): + results = { + "A": SamplerResult( + quasi_dists=[QuasiDistribution({"0": 1.0})], metadata=[{}] + ) + } + weights = [(1.0, WeightType.EXACT)] + observables = {"B": [PauliList("ZZ")]} + with pytest.raises(ValueError) as e_info: + reconstruct_expectation_values(results, weights, observables) + assert ( + e_info.value.args[0] + == "The subsystem labels of the observables and results do not match." + ) with self.subTest("Test unsupported phase"): results = SamplerResult( quasi_dists=[QuasiDistribution({"0": 1.0})], metadata=[{}] @@ -110,6 +136,18 @@ def test_cutting_reconstruction(self): observables = PauliList(["II", "IZ", "ZI", "ZZ"]) expvals = reconstruct_expectation_values(results, weights, observables) assert expvals == pytest.approx([0.0, -0.6, 0.0, -0.2]) + with self.subTest("Test inconsistent number of subexperiment results provided"): + results = SamplerResult( + quasi_dists=[QuasiDistribution({"0": 1.0})], metadata=[{}] + ) + weights = [(1.0, WeightType.EXACT)] + observables = PauliList(["ZZ", "XX"]) + with pytest.raises(ValueError) as e_info: + reconstruct_expectation_values(results, weights, observables) + assert ( + e_info.value.args[0] + == "The number of subexperiments performed in subsystem 'A' (1) should equal the number of coefficients (1) times the number of mutually commuting subobservable groups (2), but it does not." + ) @data( ("000", [1, 1, 1]),