Skip to content

Commit

Permalink
Merge branch 'main' into remove-ckt
Browse files Browse the repository at this point in the history
  • Loading branch information
garrison committed Oct 31, 2023
2 parents d2c337a + e8920ad commit 62302ec
Show file tree
Hide file tree
Showing 34 changed files with 295 additions and 212 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
name: html_docs
path: ./docs/_build/html
- name: Deploy docs
if: ${{ github.ref == 'refs/heads/stable/0.4' }}
if: ${{ github.ref == 'refs/heads/stable/0.5' }}
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
Expand Down
2 changes: 1 addition & 1 deletion .mergify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ pull_request_rules:
actions:
backport:
branches:
- stable/0.4
- stable/0.5
9 changes: 3 additions & 6 deletions circuit_knitting/cutting/cutqc/wire_cutting.py
Original file line number Diff line number Diff line change
Expand Up @@ -483,7 +483,6 @@ def find_wire_cuts(
subcircuits: Sequence[Any] = cut_solution["subcircuits"]
num_cuts: int = cut_solution["num_cuts"]
_print_cutter_result(
num_subcircuit=len(subcircuits),
num_cuts=num_cuts,
subcircuits=subcircuits,
counter=counter,
Expand Down Expand Up @@ -558,7 +557,6 @@ def cut_circuit_wire(
if verbose:
print("-" * 20)
_print_cutter_result(
num_subcircuit=len(cut_solution["subcircuits"]),
num_cuts=cut_solution["num_cuts"],
subcircuits=cut_solution["subcircuits"],
counter=cut_solution["counter"],
Expand All @@ -569,7 +567,6 @@ def cut_circuit_wire(


def _print_cutter_result(
num_subcircuit: int,
num_cuts: int,
subcircuits: Sequence[QuantumCircuit],
counter: dict[int, dict[str, int]],
Expand All @@ -579,7 +576,6 @@ def _print_cutter_result(
Pretty print the results.
Args:
num_subciruit: The number of subcircuits
num_cuts: The number of cuts
subcircuits: The list of subcircuits
counter: The dictionary containing all meta information regarding
Expand All @@ -589,7 +585,8 @@ def _print_cutter_result(
Returns:
None
"""
for subcircuit_idx in range(num_subcircuit):
print(f"num_cuts = {num_cuts}")
for subcircuit_idx, subcircuit in enumerate(subcircuits):
print("subcircuit %d" % subcircuit_idx)
print(
"\u03C1 qubits = %d, O qubits = %d, width = %d, effective = %d, depth = %d, size = %d"
Expand All @@ -602,7 +599,7 @@ def _print_cutter_result(
counter[subcircuit_idx]["size"],
)
)
print(subcircuits[subcircuit_idx])
print(subcircuit)
print("Estimated cost = %.3e" % classical_cost, flush=True)


Expand Down
10 changes: 3 additions & 7 deletions circuit_knitting/cutting/cutting_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,15 +132,15 @@ def execute_experiments(
assert isinstance(samplers, dict)
samplers_dict = samplers

# Make sure the first two cregs in each circuit are for QPD and observable measurements
# Make sure the first two cregs in each circuit are for observable and QPD measurements, respectively.
# Run a job for each partition and collect results
results = {}
for label in 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 circ.cregs[0].name != "observable_measurements"
or circ.cregs[1].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
Expand All @@ -150,10 +150,6 @@ def execute_experiments(
)
results[label] = samplers_dict[label].run(subexperiments_dict[label]).result()

for label, result in results.items():
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):
Expand Down
96 changes: 80 additions & 16 deletions circuit_knitting/cutting/cutting_experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def generate_cutting_experiments(
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}]`
:math:`[sample_{0}observable_{0}, \ldots, sample_{0}observable_{N-1}, sample_{1}observable_{0}, \ldots, sample_{M-1}observable_{N-1}]`
The coefficients will always be returned as a 1D array -- one coefficient for each unique sample.
Expand Down Expand Up @@ -148,12 +148,13 @@ def generate_cutting_experiments(
subcircuit = subcircuit_dict[label]
if is_separated:
map_ids_tmp = tuple(map_ids[j] for j in subcirc_map_ids[label])
decomp_qc = decompose_qpd_instructions(
subcircuit, subcirc_qpd_gate_ids[label], map_ids_tmp
)
for j, cog in enumerate(so.groups):
meas_qc = _append_measurement_circuit(decomp_qc, cog)
subexperiments_dict[label].append(meas_qc)
new_qc = _append_measurement_register(subcircuit, cog)
decompose_qpd_instructions(
new_qc, subcirc_qpd_gate_ids[label], map_ids_tmp, inplace=True
)
_append_measurement_circuit(new_qc, cog, inplace=True)
subexperiments_dict[label].append(new_qc)

# If the input was a single quantum circuit, return the subexperiments as a list
subexperiments_out: list[QuantumCircuit] | dict[
Expand Down Expand Up @@ -231,6 +232,37 @@ def _get_bases(circuit: QuantumCircuit) -> tuple[list[QPDBasis], list[list[int]]
return bases, qpd_gate_ids


def _append_measurement_register(
qc: QuantumCircuit,
cog: CommutingObservableGroup,
/,
*,
inplace: bool = False,
):
"""Append a new classical register for the given ``CommutingObservableGroup``.
The new register will be named ``"observable_measurements"`` and will be
the final register in the returned circuit, i.e. ``retval.cregs[-1]``.
Args:
qc: The quantum circuit
cog: The commuting observable set for which to construct measurements
inplace: Whether to operate on the circuit in place (default: ``False``)
Returns:
The modified circuit
"""
if not inplace:
qc = qc.copy()

pauli_indices = _get_pauli_indices(cog)

obs_creg = ClassicalRegister(len(pauli_indices), name="observable_measurements")
qc.add_register(obs_creg)

return qc


def _append_measurement_circuit(
qc: QuantumCircuit,
cog: CommutingObservableGroup,
Expand All @@ -239,15 +271,15 @@ def _append_measurement_circuit(
qubit_locations: Sequence[int] | None = None,
inplace: bool = False,
) -> QuantumCircuit:
"""Append a new classical register and measurement instructions for the given ``CommutingObservableGroup``.
"""Append measurement instructions for the given ``CommutingObservableGroup``.
The new register will be named ``"observable_measurements"`` and will be
the final register in the returned circuit, i.e. ``retval.cregs[-1]``.
The measurement results will be placed in a register with the name
``"observable_measurements"``. Such a register can be created by calling
:func:`_append_measurement_register` before calling the current function.
Args:
qc: The quantum circuit
cog: The commuting observable set for
which to construct measurements
cog: The commuting observable set for which to construct measurements
qubit_locations: A ``Sequence`` whose length is the number of qubits
in the observables, where each element holds that qubit's corresponding
index in the circuit. By default, the circuit and observables are assumed
Expand All @@ -273,24 +305,56 @@ def _append_measurement_circuit(
f"qubit_locations has {len(qubit_locations)} element(s) but the "
f"observable(s) have {cog.general_observable.num_qubits} qubit(s)."
)

# Find observable_measurements register
for reg in qc.cregs:
if reg.name == "observable_measurements":
obs_creg = reg
break
else:
raise ValueError('Cannot locate "observable_measurements" register')

pauli_indices = _get_pauli_indices(cog)

if obs_creg.size != len(pauli_indices):
raise ValueError(
'"observable_measurements" register is the wrong size '
"for the given commuting observable group "
f"({obs_creg.size} != {len(pauli_indices)})"
)

if not inplace:
qc = qc.copy()

# Append the appropriate measurements to qc
obs_creg = ClassicalRegister(len(cog.pauli_indices), name="observable_measurements")
qc.add_register(obs_creg)
#
# Implement the necessary basis rotations and measurements, as
# in BackendEstimator._measurement_circuit().
genobs_x = cog.general_observable.x
genobs_z = cog.general_observable.z
for clbit, subqubit in enumerate(cog.pauli_indices):
for clbit, subqubit in enumerate(pauli_indices):
# subqubit is the index of the qubit in the subsystem.
# actual_qubit is its index in the system of interest (if different).
actual_qubit = qubit_locations[subqubit]
if genobs_x[subqubit]:
if genobs_z[subqubit]:
qc.sdg(actual_qubit)
qc.h(actual_qubit)
# Rotate Y basis to Z basis
qc.sx(actual_qubit)
else:
# Rotate X basis to Z basis
qc.h(actual_qubit)
# Measure in Z basis
qc.measure(actual_qubit, obs_creg[clbit])

return qc


def _get_pauli_indices(cog: CommutingObservableGroup) -> list[int]:
"""Return the indices to qubits to be measured."""
# If the circuit has no measurements, the Sampler will fail. So, we
# measure one qubit as a temporary workaround to
# https://github.com/Qiskit-Extensions/circuit-knitting-toolbox/issues/422
pauli_indices = cog.pauli_indices
if not pauli_indices:
pauli_indices = [0]
return pauli_indices
39 changes: 8 additions & 31 deletions circuit_knitting/cutting/cutting_reconstruction.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
from ..utils.observable_grouping import CommutingObservableGroup, ObservableCollection
from ..utils.bitwise import bit_count
from .cutting_decomposition import decompose_observables
from .cutting_experiments import _get_pauli_indices
from .qpd import WeightType


Expand All @@ -48,7 +49,7 @@ def reconstruct_expectation_values(
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}]`
:math:`[sample_{0}observable_{0}, \ldots, sample_{0}observable_{N-1}, sample_{1}observable_{0}, \ldots, sample_{M-1}observable_{N-1}]`
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
Expand All @@ -66,8 +67,6 @@ 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(
Expand Down Expand Up @@ -111,21 +110,7 @@ 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():
try:
num_qpd_bits = results_dict[label].metadata[
i * len(so.groups) + k
]["num_qpd_bits"]
except KeyError as ex:
raise ValueError(
"The num_qpd_bits field must be set in each subexperiment "
"result metadata dictionary."
) from ex
else:
subsystem_expvals[k] += quasi_prob * _process_outcome(
num_qpd_bits,
cog,
outcome,
)
subsystem_expvals[k] += quasi_prob * _process_outcome(cog, outcome)

for k, subobservable in enumerate(subobservables_by_subsystem[label]):
current_expvals[k] *= np.mean(
Expand All @@ -138,16 +123,12 @@ def reconstruct_expectation_values(


def _process_outcome(
num_qpd_bits: int, cog: CommutingObservableGroup, outcome: int | str, /
cog: CommutingObservableGroup, outcome: int | str, /
) -> np.typing.NDArray[np.float64]:
"""
Process a single outcome of a QPD experiment with observables.
Args:
num_qpd_bits: The number of QPD measurements in the circuit. It is
assumed that the second to last creg in the generating circuit
is the creg containing the QPD measurements, and the last
creg is associated with the observable measurements.
cog: The observable set being measured by the current experiment
outcome: The outcome of the classical bits
Expand All @@ -156,15 +137,11 @@ def _process_outcome(
this vector correspond to the elements of ``cog.commuting_observables``,
and each result will be either +1 or -1.
"""
num_meas_bits = len(_get_pauli_indices(cog))

outcome = _outcome_to_int(outcome)
try:
qpd_outcomes = outcome & ((1 << num_qpd_bits) - 1)
except TypeError as ex:
raise TypeError(
f"num_qpd_bits must be an integer, but a {type(num_qpd_bits)} was passed."
) from ex

meas_outcomes = outcome >> num_qpd_bits
meas_outcomes = outcome & ((1 << num_meas_bits) - 1)
qpd_outcomes = outcome >> num_meas_bits

# qpd_factor will be -1 or +1, depending on the overall parity of qpd
# measurements.
Expand Down
Loading

0 comments on commit 62302ec

Please sign in to comment.