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

Swap register order, removing need to pass num_qpd_bits #434

Merged
merged 4 commits into from
Oct 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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
91 changes: 72 additions & 19 deletions circuit_knitting/cutting/cutting_experiments.py
Original file line number Diff line number Diff line change
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,19 +305,29 @@ 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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Neat code here where the else sits outside the looped if ... I'll have to glance at how this works :)

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()

# If the circuit has no measurements, the Sampler will fail. So, we
garrison marked this conversation as resolved.
Show resolved Hide resolved
# 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]

# Append the appropriate measurements to qc
obs_creg = ClassicalRegister(len(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
Expand All @@ -305,3 +347,14 @@ def _append_measurement_circuit(
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
37 changes: 7 additions & 30 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 Down Expand Up @@ -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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

niiiiice

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
12 changes: 8 additions & 4 deletions circuit_knitting/cutting/qpd/qpd.py
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,8 @@ def decompose_qpd_instructions(
circuit: QuantumCircuit,
instruction_ids: Sequence[Sequence[int]],
map_ids: Sequence[int] | None = None,
*,
inplace: bool = False,
) -> QuantumCircuit:
r"""
Replace all QPD instructions in the circuit with local Qiskit operations and measurements.
Expand Down Expand Up @@ -529,7 +531,9 @@ def decompose_qpd_instructions(
ValueError: Length of ``map_ids`` does not equal the number of decompositions in the circuit.
"""
_validate_qpd_instructions(circuit, instruction_ids)
new_qc = circuit.copy()

if not inplace:
circuit = circuit.copy() # pragma: no cover

if map_ids is not None:
if len(instruction_ids) != len(map_ids):
Expand All @@ -540,12 +544,12 @@ def decompose_qpd_instructions(
# If mapping is specified, set each gate's mapping
for i, decomp_gate_ids in enumerate(instruction_ids):
for gate_id in decomp_gate_ids:
new_qc.data[gate_id].operation.basis_id = map_ids[i]
circuit.data[gate_id].operation.basis_id = map_ids[i]

# Convert all instances of BaseQPDGate in the circuit to Qiskit instructions
_decompose_qpd_instructions(new_qc, instruction_ids)
_decompose_qpd_instructions(circuit, instruction_ids)

return new_qc
return circuit


_qpdbasis_from_instruction_funcs: dict[str, Callable[[Instruction], QPDBasis]] = {}
Expand Down
3 changes: 0 additions & 3 deletions docs/circuit_cutting/how-tos/how_to_specify_cut_wires.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -348,9 +348,6 @@
"metadata": {},
"outputs": [],
"source": [
"for label in results:\n",
" for i, subexperiment in enumerate(subexperiments[label]):\n",
" results[label].metadata[i][\"num_qpd_bits\"] = len(subexperiment.cregs[0])\n",
"reconstructed_expvals = reconstruct_expectation_values(\n",
" results,\n",
" coefficients,\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,9 +307,7 @@
"source": [
"### Reconstruct the expectation values\n",
"\n",
"Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit.\n",
"\n",
"Include the number of bits used for cutting measurements in the results metadata. This will be automated in a future release, but users must specify it manually for now."
"Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit."
]
},
{
Expand All @@ -321,10 +319,6 @@
"source": [
"from circuit_knitting.cutting import reconstruct_expectation_values\n",
"\n",
"for label, circuits in subexperiments.items():\n",
" for i, circuit in enumerate(circuits):\n",
" results[label].metadata[i][\"num_qpd_bits\"] = len(circuit.cregs[0])\n",
"\n",
"reconstructed_expvals = reconstruct_expectation_values(\n",
" results,\n",
" coefficients,\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,9 +346,7 @@
"source": [
"### Reconstruct the expectation values\n",
"\n",
"Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit.\n",
"\n",
"Include the number of bits used for cutting measurements in the results metadata. This will be automated in a future release, but users must specify it manually for now."
"Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit."
]
},
{
Expand All @@ -360,9 +358,6 @@
"source": [
"from circuit_knitting.cutting import reconstruct_expectation_values\n",
"\n",
"for i in range(len(subexperiments)):\n",
" results.metadata[i][\"num_qpd_bits\"] = len(subexperiments[i].cregs[0])\n",
"\n",
"reconstructed_expvals = reconstruct_expectation_values(\n",
" results,\n",
" coefficients,\n",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -403,9 +403,7 @@
"source": [
"### Reconstruct the expectation values\n",
"\n",
"Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit.\n",
"\n",
"Include the number of bits used for cutting measurements in the results metadata. This will be automated in a future release, but users must specify it manually for now."
"Use the subexperiment results, subobservables, and sampling coefficients to reconstruct the expectation value of the original circuit."
]
},
{
Expand All @@ -417,10 +415,6 @@
"source": [
"from circuit_knitting.cutting import reconstruct_expectation_values\n",
"\n",
"for label, circuits in subexperiments.items():\n",
" for i, circuit in enumerate(circuits):\n",
" results[label].metadata[i][\"num_qpd_bits\"] = len(circuit.cregs[0])\n",
"\n",
"reconstructed_expvals = reconstruct_expectation_values(\n",
" results,\n",
" coefficients,\n",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
upgrade:
- |
The order of the classical registers in the generated experiments
has been swapped. The ``"observable_measurements"`` register now
comes first, and the ``"qpd_measurements"`` register now comes
second. As a result of this change, it is no longer necessary to
manually insert ``num_qpd_bits`` into the ``metadata`` for each
experiment's result.
10 changes: 3 additions & 7 deletions test/cutting/test_cutting_evaluation.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ def test_execute_experiments(self):
)
self.assertEqual(
quasi_dists,
SamplerResult(quasi_dists=[{3: 1.0}], metadata=[{"num_qpd_bits": 0}]),
SamplerResult(quasi_dists=[{3: 1.0}], metadata=[{}]),
)
self.assertEqual([(1.0, WeightType.EXACT)], coefficients)
with self.subTest("Basic test with dicts"):
Expand Down Expand Up @@ -101,12 +101,8 @@ def test_execute_experiments(self):
samplers={"A": self.sampler, "B": deepcopy(self.sampler)},
)
comp_result = {
"A": SamplerResult(
quasi_dists=[{1: 1.0}], metadata=[{"num_qpd_bits": 0}]
),
"B": SamplerResult(
quasi_dists=[{1: 1.0}], metadata=[{"num_qpd_bits": 0}]
),
"A": SamplerResult(quasi_dists=[{1: 1.0}], metadata=[{}]),
"B": SamplerResult(quasi_dists=[{1: 1.0}], metadata=[{}]),
}
self.assertEqual(quasi_dists, comp_result)
self.assertEqual([(1.0, WeightType.EXACT)], coefficients)
Expand Down
Loading