Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor reconstruct_expectation_values #391

Merged
merged 32 commits into from
Sep 8, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
bf5ef1c
Refactor reconstruct_experiments
caleb-johnson Aug 31, 2023
05343d4
Merge branch 'main' of github.com:Qiskit-Extensions/circuit-knitting-…
caleb-johnson Aug 31, 2023
70d0782
weight-->coeff
caleb-johnson Aug 31, 2023
b9e1c9a
cleanups
caleb-johnson Aug 31, 2023
9a53534
cleanups
caleb-johnson Aug 31, 2023
319cb95
New workflow works with cutting_evaluation
caleb-johnson Sep 1, 2023
5870fb9
Tests passing
caleb-johnson Sep 5, 2023
88fcaa8
re-add CuttingExperimentResults
caleb-johnson Sep 5, 2023
e81cffb
cleanup
caleb-johnson Sep 5, 2023
2ac8725
mypy
caleb-johnson Sep 5, 2023
c88ba68
fix inits
caleb-johnson Sep 5, 2023
5583ed6
release notes
caleb-johnson Sep 5, 2023
f444fc7
Merge branch 'main' of github.com:Qiskit-Extensions/circuit-knitting-…
caleb-johnson Sep 5, 2023
cd95e28
Remove private generate_cutting_experiments
caleb-johnson Sep 5, 2023
2e6f36f
Revert "Remove private generate_cutting_experiments"
caleb-johnson Sep 5, 2023
a878f08
fix sphinx
caleb-johnson Sep 5, 2023
59015e1
Add num_qpd_bit checks and tests
caleb-johnson Sep 5, 2023
c287784
fix hanging jobs :(
caleb-johnson Sep 5, 2023
aa0ab18
Update circuit_knitting/cutting/cutting_evaluation.py
caleb-johnson Sep 6, 2023
b3d7e75
Update circuit_knitting/cutting/cutting_reconstruction.py
caleb-johnson Sep 6, 2023
1761c4b
Update circuit_knitting/cutting/cutting_reconstruction.py
caleb-johnson Sep 6, 2023
86c6be7
Update circuit_knitting/cutting/cutting_reconstruction.py
caleb-johnson Sep 6, 2023
b246226
Update circuit_knitting/cutting/cutting_reconstruction.py
caleb-johnson Sep 6, 2023
a536ef7
Update circuit_knitting/cutting/cutting_reconstruction.py
caleb-johnson Sep 6, 2023
682ee45
Update circuit_knitting/cutting/cutting_reconstruction.py
caleb-johnson Sep 6, 2023
3b594b4
Update circuit_knitting/cutting/cutting_reconstruction.py
caleb-johnson Sep 6, 2023
7a8cd61
peer review
caleb-johnson Sep 6, 2023
cbc5d14
Fix links in release notes
caleb-johnson Sep 6, 2023
b55cc2f
quasi-dist(s)
caleb-johnson Sep 6, 2023
8bcc910
Update circuit_knitting/cutting/cutting_reconstruction.py
caleb-johnson Sep 7, 2023
6650998
Merge branch 'main' into reconstruct
garrison Sep 7, 2023
926cf3f
Update releasenotes/notes/refactor-reconstruct-45e00c3df1bdd4ff.yaml
caleb-johnson Sep 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 38 additions & 25 deletions circuit_knitting/cutting/cutting_reconstruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -26,49 +26,57 @@


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]:
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
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
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
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
to the results from each partition's subexperiments.
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.
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.
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
ValueError: An input observable has a phase not equal to 1.
"""
if len(coefficients) != len(quasi_dists):
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
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.")
Expand All @@ -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
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved

# 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(coefficients)):
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
current_expvals = np.ones((len(expvals),))
for j, label in enumerate(sorted_subsystems):
for label in sorted_subsystems:
so = subsystem_observables[label]
coeff = coefficients[i]
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
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]):
Expand Down
57 changes: 43 additions & 14 deletions test/cutting/test_cutting_reconstruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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."
Expand Down