Skip to content

Commit

Permalink
Add a public generate_cutting_experiments function (#385)
Browse files Browse the repository at this point in the history
* Add a public generate_cutting_experiments function

* playing with diff

* diff

* diff

* ruff

* revert accidental change

* diff

* cleanup

* cleanup

* Update circuit_knitting/cutting/cutting_evaluation.py

Co-authored-by: Jim Garrison <[email protected]>

* Fix tests

* peer review

* coverage

* peer review

* Update cutting_evaluation.py

* Update cutting_evaluation.py

* fix broken tests

* Better error

* fix miswording

* update test

* dont change tut2

* Update cutting_evaluation.py

* fix strange error

* Unnecessary conditional

* Move generate_exp tests to test_evaluation until we decide on home

* ruff

* Create a new module for subexperiment generation

* Update circuit_knitting/cutting/cutting_evaluation.py

Co-authored-by: Jim Garrison <[email protected]>

* peer review

* pydocstyle

* Add release note

* cleanup

* weird doc rendering

* peer review

* Fix incorrect error message

* Update circuit_knitting/cutting/cutting_experiments.py

Co-authored-by: Jim Garrison <[email protected]>

* Update error msg

* Update circuit_knitting/cutting/cutting_experiments.py

Co-authored-by: Jim Garrison <[email protected]>

* peer review

* Update circuit_knitting/cutting/cutting_evaluation.py

Co-authored-by: Jim Garrison <[email protected]>

---------

Co-authored-by: Jim Garrison <[email protected]>
  • Loading branch information
caleb-johnson and garrison authored Aug 31, 2023
1 parent 72f4b36 commit 91be0d3
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 23 deletions.
3 changes: 3 additions & 0 deletions 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 @@ -87,6 +88,7 @@
PartitionedCuttingProblem,
)
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

Expand All @@ -95,6 +97,7 @@
"partition_problem",
"cut_gates",
"decompose_gates",
"generate_cutting_experiments",
"execute_experiments",
"reconstruct_expectation_values",
"PartitionedCuttingProblem",
Expand Down
89 changes: 69 additions & 20 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.")
if not num_samples >= 1:
raise ValueError("The number of requested samples must be at least 1.")

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]]],
]:
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]
] = 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, coefficients, sampled_frequencies
return subexperiments_out, weights, subexperiments_legacy


def _run_experiments_batch(
Expand Down Expand Up @@ -377,7 +416,17 @@ 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 cut '
"relative to the other cuts in the circuit. For example, all "
"SingleQubitQPDGates belonging to the same cut, N, should have labels "
' formatted as "<your_label>_N". 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}, sample_{1}observable_{0}, \ldots, sample_{M}observable_{N}]`
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 be at least one.
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:`.generate_cutting_experiments`,
which can be used to generate quantum experiments, given an input circuit containing
:class:`.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

0 comments on commit 91be0d3

Please sign in to comment.