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

Add a public generate_cutting_experiments function #385

Merged
merged 42 commits into from
Aug 31, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
8cc03ff
Add a public generate_cutting_experiments function
caleb-johnson Aug 29, 2023
dbe0cae
playing with diff
caleb-johnson Aug 29, 2023
76f8998
diff
caleb-johnson Aug 29, 2023
77c1c98
diff
caleb-johnson Aug 30, 2023
d4b42a0
ruff
caleb-johnson Aug 30, 2023
7a822bf
revert accidental change
caleb-johnson Aug 30, 2023
f1850c6
diff
caleb-johnson Aug 30, 2023
42be376
cleanup
caleb-johnson Aug 30, 2023
74e427c
cleanup
caleb-johnson Aug 30, 2023
0ec215d
Merge branch 'main' of github.com:Qiskit-Extensions/circuit-knitting-…
caleb-johnson Aug 30, 2023
f06f79c
Update circuit_knitting/cutting/cutting_evaluation.py
caleb-johnson Aug 30, 2023
dd42cb4
Fix tests
caleb-johnson Aug 30, 2023
951c86b
peer review
caleb-johnson Aug 30, 2023
2f09338
coverage
caleb-johnson Aug 30, 2023
1498cc9
peer review
caleb-johnson Aug 30, 2023
75bad44
Update cutting_evaluation.py
caleb-johnson Aug 30, 2023
d5cbd67
Update cutting_evaluation.py
caleb-johnson Aug 30, 2023
b2b48c0
fix broken tests
caleb-johnson Aug 30, 2023
ab3753c
Better error
caleb-johnson Aug 30, 2023
76510ba
fix miswording
caleb-johnson Aug 30, 2023
b35e485
update test
caleb-johnson Aug 30, 2023
7788198
dont change tut2
caleb-johnson Aug 30, 2023
f30b435
Update cutting_evaluation.py
caleb-johnson Aug 30, 2023
e4274d1
fix strange error
caleb-johnson Aug 31, 2023
9b6ac2c
Unnecessary conditional
caleb-johnson Aug 31, 2023
72f891b
Move generate_exp tests to test_evaluation until we decide on home
caleb-johnson Aug 31, 2023
1d9893e
ruff
caleb-johnson Aug 31, 2023
bcb9209
Create a new module for subexperiment generation
caleb-johnson Aug 31, 2023
3aa0b89
Update circuit_knitting/cutting/cutting_evaluation.py
caleb-johnson Aug 31, 2023
30b82e9
peer review
caleb-johnson Aug 31, 2023
cbfb6f9
pydocstyle
caleb-johnson Aug 31, 2023
6a7d913
Add release note
caleb-johnson Aug 31, 2023
eb149a3
cleanup
caleb-johnson Aug 31, 2023
2591ecd
weird doc rendering
caleb-johnson Aug 31, 2023
0d9b663
peer review
caleb-johnson Aug 31, 2023
1699db4
Fix incorrect error message
caleb-johnson Aug 31, 2023
48588f1
Update circuit_knitting/cutting/cutting_experiments.py
caleb-johnson Aug 31, 2023
fabfaf1
Update error msg
caleb-johnson Aug 31, 2023
9947747
Merge branch 'generate-exp2' of github.com:Qiskit-Extensions/circuit-…
caleb-johnson Aug 31, 2023
bbdfb4d
Update circuit_knitting/cutting/cutting_experiments.py
caleb-johnson Aug 31, 2023
84b6f08
peer review
caleb-johnson Aug 31, 2023
a2ef916
Update circuit_knitting/cutting/cutting_evaluation.py
caleb-johnson Aug 31, 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
8 changes: 7 additions & 1 deletion circuit_knitting/cutting/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
partition_problem
cut_gates
decompose_gates
generate_cutting_experiments
execute_experiments
reconstruct_expectation_values

Expand Down Expand Up @@ -86,7 +87,11 @@
decompose_gates,
PartitionedCuttingProblem,
)
from .cutting_evaluation import execute_experiments, CuttingExperimentResults
from .cutting_evaluation import (
execute_experiments,
CuttingExperimentResults,
)
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
from .cutting_experiments import generate_cutting_experiments
from .cutting_reconstruction import reconstruct_expectation_values
from .wire_cutting_transforms import cut_wires, expand_observables

Expand All @@ -95,6 +100,7 @@
"partition_problem",
"cut_gates",
"decompose_gates",
"generate_cutting_experiments",
"execute_experiments",
"reconstruct_expectation_values",
"PartitionedCuttingProblem",
Expand Down
88 changes: 69 additions & 19 deletions circuit_knitting/cutting/cutting_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@

from __future__ import annotations

from typing import Any, NamedTuple
from typing import NamedTuple
from collections import defaultdict
from collections.abc import Sequence
from itertools import chain

Expand Down Expand Up @@ -69,15 +70,15 @@ def execute_experiments(
sampling frequency

Raises:
ValueError: The number of requested samples must be positive.
ValueError: The number of requested samples must be at least one.
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: If multiple samplers are passed, each one must be unique.
"""
if num_samples <= 0:
raise ValueError("The number of requested samples must be positive.")
raise ValueError("The number of requested samples must be at least 1.")
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved

if isinstance(circuits, dict) and not isinstance(subobservables, dict):
raise ValueError(
Expand Down Expand Up @@ -119,9 +120,9 @@ def execute_experiments(

# Generate the sub-experiments to run on backend
(
subexperiments,
_,
coefficients,
sampled_frequencies,
subexperiments,
) = _generate_cutting_experiments(
circuits,
subobservables,
Expand Down Expand Up @@ -241,9 +242,23 @@ def _append_measurement_circuit(
def _generate_cutting_experiments(
circuits: QuantumCircuit | dict[str | int, QuantumCircuit],
observables: PauliList | dict[str | int, PauliList],
num_samples: int,
) -> tuple[list[list[list[QuantumCircuit]]], list[tuple[Any, WeightType]], list[float]]:
"""Generate all the experiments to run on the backend and their associated coefficients."""
num_samples: int | float,
) -> tuple[
list[QuantumCircuit] | dict[str | int, list[QuantumCircuit]],
list[tuple[float, WeightType]],
list[list[list[QuantumCircuit]]],
]:
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
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.
Expand Down Expand Up @@ -285,18 +300,33 @@ def _generate_cutting_experiments(
# Sort samples in descending order of frequency
sorted_samples = sorted(random_samples.items(), key=lambda x: x[1][0], reverse=True)

# Generate the outputs -- sub-experiments, coefficients, and frequencies
subexperiments: list[list[list[QuantumCircuit]]] = []
coefficients = []
sampled_frequencies = []
# 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):
subexperiments.append([])
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))
coefficients.append((sampled_coeff, weight_type))
sampled_frequencies.append(redundancy)
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)

# 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()))
):
Expand All @@ -306,13 +336,22 @@ def _generate_cutting_experiments(
decomp_qc = decompose_qpd_instructions(
subcircuit, subcirc_qpd_gate_ids[i], map_ids_tmp
)
subexperiments[-1].append([])
subexperiments_legacy[-1].append([])
so = subsystem_observables[label]
for j, cog in enumerate(so.groups):
meas_qc = _append_measurement_circuit(decomp_qc, cog)
subexperiments[-1][-1].append(meas_qc)
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]
] = subexperiments_dict
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
assert isinstance(subexperiments_out, dict)
if isinstance(circuits, QuantumCircuit):
assert len(subexperiments_out.keys()) == 1
subexperiments_out = list(subexperiments_dict.values())[0]

return subexperiments, coefficients, sampled_frequencies
return subexperiments_out, weights, subexperiments_legacy


def _run_experiments_batch(
Expand Down Expand Up @@ -377,7 +416,18 @@ def _get_mapping_ids_by_partition(
subcirc_map_ids.append([])
for i, inst in enumerate(circ.data):
if isinstance(inst.operation, SingleQubitQPDGate):
decomp_id = int(inst.operation.label.split("_")[-1])
try:
decomp_id = int(inst.operation.label.split("_")[-1])
except (AttributeError, ValueError):
raise ValueError(
"SingleQubitQPDGate instances in input circuit(s) must have their "
'labels suffixed with "_<id>", where <id> is the index of the gate '
"relative to the other gates belonging to the same cut. For example, "
"a two-qubit gate cut can be represented by two SingleQubitQPDGates -- one "
'labeled "<your_label>_0" and one labeled "<your_label>_1".'
" This allows SingleQubitQPDGates belonging to the same cut to be "
"sampled jointly."
)
decomp_ids.add(decomp_id)
subcirc_qpd_gate_ids[-1].append([i])
subcirc_map_ids[-1].append(decomp_id)
Expand Down
75 changes: 75 additions & 0 deletions circuit_knitting/cutting/cutting_experiments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# This code is a Qiskit project.

# (C) Copyright IBM 2023.

# This code is licensed under the Apache License, Version 2.0. You may
# obtain a copy of this license in the LICENSE.txt file in the root directory
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
# Any modifications or derivative works of this code must retain this
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

"""Functions for evaluating circuit cutting experiments."""

from __future__ import annotations

from qiskit.circuit import QuantumCircuit
from qiskit.quantum_info import PauliList

from .qpd import WeightType
from .cutting_evaluation import _generate_cutting_experiments


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]],
]:
r"""
Generate cutting subexperiments and their associated weights.

If the input, ``circuits``, is a :class:`QuantumCircuit` instance, the
output subexperiments will be contained within a 1D array, and ``observables`` is
expected to be a :class:`PauliList` instance.

If the input circuit and observables are specified by dictionaries with partition labels
as keys, the output subexperiments will be returned as a dictionary which maps each
partition label to a 1D array containing the subexperiments associated with that partition.

In both cases, the subexperiment lists are ordered as follows:

:math:`[sample_{0}observable_{0}, \ldots, sample_{0}observable_{N}, \ldots, sample_{M}observable_{N}]`
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved

The weights will always be returned as a 1D array -- one weight for each unique sample.

Args:
circuits: The circuit(s) to partition and separate
observables: The observable(s) to evaluate for each unique sample
num_samples: The number of samples to draw from the quasi-probability distribution. If set
to infinity, the weights will be generated rigorously rather than by sampling from
the distribution.
Returns:
A tuple containing the cutting experiments and their associated weights.
If the input circuits is a :class:`QuantumCircuit` instance, the output subexperiments
will be a sequence of circuits -- one for every unique sample and observable. If the
input circuits are represented as a dictionary keyed by partition labels, the output
subexperiments will also be a dictionary keyed by partition labels and containing
the subexperiments for each partition.
The weights are always a sequence of length-2 tuples, where each tuple contains the
weight and the :class:`WeightType`. Each weight corresponds to one unique sample.

Raises:
ValueError: ``num_samples`` must either be at least one.
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
ValueError: ``circuits`` and ``observables`` are incompatible types
ValueError: :class:`SingleQubitQPDGate` instances must have their cut ID
appended to the gate label so they may be associated with other gates belonging
to the same cut.
ValueError: :class:`SingleQubitQPDGate` instances are not allowed in unseparated circuits.
"""
subexperiments, weights, _ = _generate_cutting_experiments(
circuits, observables, num_samples
)
return subexperiments, weights
9 changes: 9 additions & 0 deletions releasenotes/notes/generate-experiments-2ac773442132c78d.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
features:
- |
Added a module, :mod:`circuit_knitting.cutting.cutting_experiments`, which is intended to hold
functions used for generating the quantum experiments needed for circuit cutting. This module
will initially hold one function, :func:`circuit_knitting.cutting.cutting_experiments.generate_cutting_experiments`,
caleb-johnson marked this conversation as resolved.
Show resolved Hide resolved
which can be used to generate quantum experiments, given an input circuit containing
:class:`circuit_knitting.cutting.qpd.BaseQPDGate` instances, some observables, and a number
of times the joint quasi-probability distribution for the cuts should be sampled.
6 changes: 3 additions & 3 deletions test/cutting/test_cutting_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
# copyright notice, and modified files need to carry a notice indicating
# that they have been altered from the originals.

import pytest
import unittest

import unittest
from copy import deepcopy

import pytest
from qiskit.quantum_info import Pauli, PauliList
from qiskit.result import QuasiDistribution
from qiskit.primitives import Sampler as TerraSampler
Expand Down Expand Up @@ -215,7 +215,7 @@ def test_execute_experiments(self):
)
assert (
e_info.value.args[0]
== "The number of requested samples must be positive."
== "The number of requested samples must be at least 1."
)
with self.subTest("Dict of non-unique samplers"):
qc = QuantumCircuit(2)
Expand Down
Loading