diff --git a/.github/workflows/citation.yml b/.github/workflows/citation.yml
index f29b5950e..f8cab13b2 100644
--- a/.github/workflows/citation.yml
+++ b/.github/workflows/citation.yml
@@ -63,7 +63,7 @@ jobs:
fi
- name: Upload PDF
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: preview.pdf
path: preview.pdf
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index 3be2b272a..20c3a42b3 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -23,7 +23,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install tox
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index c20bf302a..eca5133ca 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -19,7 +19,7 @@ jobs:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- - uses: actions/setup-python@v4
+ - uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Install dependencies
@@ -39,12 +39,12 @@ jobs:
tox -edocs
- name: Upload docs artifact
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
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 }}
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index acbb56fe1..8869143f8 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -19,7 +19,7 @@ jobs:
with:
fetch-depth: 0
- name: Set up Python 3.9
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: '3.9'
- name: Install tox
diff --git a/.github/workflows/test_development_versions.yml b/.github/workflows/test_development_versions.yml
index 6ed554fb8..95aa7543e 100644
--- a/.github/workflows/test_development_versions.yml
+++ b/.github/workflows/test_development_versions.yml
@@ -23,7 +23,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies (development versions)
@@ -32,11 +32,12 @@ jobs:
python -m pip install --upgrade pip tox
python -m pip install toml typer
python tools/extremal-python-dependencies.py add-dependency \
- "qiskit-terra @ git+https://github.com/Qiskit/qiskit.git" \
+ "qiskit-terra @ git+https://github.com/Qiskit/qiskit.git@stable/0.46" \
--inplace
python tools/extremal-python-dependencies.py pin-dependencies \
- "qiskit @ git+https://github.com/Qiskit/qiskit.git#subdirectory=qiskit_pkg" \
+ "qiskit @ git+https://github.com/Qiskit/qiskit.git@stable/0.46#subdirectory=qiskit_pkg" \
"qiskit-ibm-runtime @ git+https://github.com/Qiskit/qiskit-ibm-runtime.git" \
+ "qiskit-algorithms @ git+https://github.com/qiskit-community/qiskit-algorithms.git" \
--inplace
- name: Modify tox.ini for more thorough check
shell: bash
diff --git a/.github/workflows/test_latest_versions.yml b/.github/workflows/test_latest_versions.yml
index abacaa444..d93bc4142 100644
--- a/.github/workflows/test_latest_versions.yml
+++ b/.github/workflows/test_latest_versions.yml
@@ -29,7 +29,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
diff --git a/.github/workflows/test_minimum_versions.yml b/.github/workflows/test_minimum_versions.yml
index 7e493727c..f337a42f7 100644
--- a/.github/workflows/test_minimum_versions.yml
+++ b/.github/workflows/test_minimum_versions.yml
@@ -21,7 +21,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
- uses: actions/setup-python@v4
+ uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies (minimum versions)
diff --git a/.mergify.yml b/.mergify.yml
index 0c4f2e613..aebea99e7 100644
--- a/.mergify.yml
+++ b/.mergify.yml
@@ -5,4 +5,4 @@ pull_request_rules:
actions:
backport:
branches:
- - stable/0.4
+ - stable/0.5
diff --git a/README.md b/README.md
index 7b4a2c80b..844638929 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,8 @@
[![Release](https://img.shields.io/pypi/v/circuit-knitting-toolbox.svg?label=Release)](https://github.com/Qiskit-Extensions/circuit-knitting-toolbox/releases)
![Platform](https://img.shields.io/badge/%F0%9F%92%BB%20Platform-Linux%20%7C%20macOS%20%7C%20Windows-informational)
[![Python](https://img.shields.io/pypi/pyversions/circuit-knitting-toolbox?label=Python&logo=python)](https://www.python.org/)
- [![Qiskit](https://img.shields.io/badge/Qiskit-%E2%89%A5%200.43.0-6133BD?logo=qiskit)](https://github.com/Qiskit/qiskit)
- [![Qiskit Nature](https://img.shields.io/badge/Qiskit%20Nature-%E2%89%A5%200.6.0-6133BD?logo=qiskit)](https://github.com/Qiskit/qiskit-nature)
+ [![Qiskit](https://img.shields.io/badge/Qiskit-%E2%89%A5%200.44.1%2C%20%3C1.0-6133BD?logo=qiskit)](https://github.com/Qiskit/qiskit)
+ [![Qiskit Nature](https://img.shields.io/badge/Qiskit%20Nature-%E2%89%A5%200.6.0%2C%20%3C0.7-6133BD?logo=qiskit)](https://github.com/Qiskit/qiskit-nature)
[![Docs (stable)](https://img.shields.io/badge/%F0%9F%93%84%20Docs-stable-blue.svg)](https://qiskit-extensions.github.io/circuit-knitting-toolbox/)
[![DOI](https://zenodo.org/badge/543181258.svg)](https://zenodo.org/badge/latestdoi/543181258)
diff --git a/circuit_knitting/cutting/cutqc/wire_cutting.py b/circuit_knitting/cutting/cutqc/wire_cutting.py
index 21f8a67bf..b9688abd1 100644
--- a/circuit_knitting/cutting/cutqc/wire_cutting.py
+++ b/circuit_knitting/cutting/cutqc/wire_cutting.py
@@ -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,
@@ -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"],
@@ -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]],
@@ -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
@@ -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"
@@ -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)
@@ -621,7 +618,7 @@ def _cuts_parser(
that are affected by these cuts
"""
dag = circuit_to_dag(circ)
- positions = []
+ positions: list[tuple[Qubit, int]] = []
for position in cuts:
if len(position) != 2:
raise ValueError(
diff --git a/circuit_knitting/cutting/cutting_evaluation.py b/circuit_knitting/cutting/cutting_evaluation.py
index 225cd716b..9993d17f0 100644
--- a/circuit_knitting/cutting/cutting_evaluation.py
+++ b/circuit_knitting/cutting/cutting_evaluation.py
@@ -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
@@ -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):
diff --git a/circuit_knitting/cutting/cutting_experiments.py b/circuit_knitting/cutting/cutting_experiments.py
index 1412b6d5c..cfe62d438 100644
--- a/circuit_knitting/cutting/cutting_experiments.py
+++ b/circuit_knitting/cutting/cutting_experiments.py
@@ -19,8 +19,11 @@
import numpy as np
from qiskit.circuit import QuantumCircuit, ClassicalRegister
from qiskit.quantum_info import PauliList
+from qiskit.transpiler import PassManager
+from qiskit.transpiler.passes import RemoveResetInZeroState, DAGFixedPoint
from ..utils.iteration import strict_zip
+from ..utils.transpiler_passes import RemoveFinalReset, ConsolidateResets
from ..utils.observable_grouping import ObservableCollection, CommutingObservableGroup
from .qpd import (
WeightType,
@@ -54,10 +57,16 @@ 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.
+ Note that this function also runs some transpiler passes on each generated
+ circuit, namely :class:`~qiskit.transpiler.passes.RemoveResetInZeroState`,
+ :class:`.RemoveFinalReset`, and :class:`.ConsolidateResets`, in order to
+ remove unnecessary :class:`~qiskit.circuit.library.Reset`\ s from the
+ circuit that are added by the subexperiment decompositions for cut wires.
+
Args:
circuits: The circuit(s) to partition and separate
observables: The observable(s) to evaluate for each unique sample
@@ -148,12 +157,32 @@ 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)
+
+ # Remove initial and final resets from the subexperiments. This will
+ # enable the `Move` operation to work on backends that don't support
+ # `Reset`, as long as qubits are not re-used. See
+ # https://github.com/Qiskit-Extensions/circuit-knitting-toolbox/issues/452.
+ # While we are at it, we also consolidate each run of multiple resets
+ # (which can arise when re-using qubits) into a single reset.
+ pass_manager = PassManager()
+ pass_manager.append(
+ [
+ RemoveResetInZeroState(),
+ RemoveFinalReset(),
+ ConsolidateResets(),
+ DAGFixedPoint(),
+ ],
+ do_while=lambda property_set: not property_set["dag_fixed_point"],
+ )
+ for label, subexperiments in subexperiments_dict.items():
+ subexperiments_dict[label] = pass_manager.run(subexperiments)
# If the input was a single quantum circuit, return the subexperiments as a list
subexperiments_out: list[QuantumCircuit] | dict[
@@ -231,6 +260,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,
@@ -239,15 +299,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
@@ -273,24 +333,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
diff --git a/circuit_knitting/cutting/cutting_reconstruction.py b/circuit_knitting/cutting/cutting_reconstruction.py
index ec7c68ede..40094a1aa 100644
--- a/circuit_knitting/cutting/cutting_reconstruction.py
+++ b/circuit_knitting/cutting/cutting_reconstruction.py
@@ -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
@@ -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
@@ -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(
@@ -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(
@@ -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
@@ -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.
diff --git a/circuit_knitting/cutting/qpd/instructions/qpd_gate.py b/circuit_knitting/cutting/qpd/instructions/qpd_gate.py
index 76571d0b4..840cd0874 100644
--- a/circuit_knitting/cutting/qpd/instructions/qpd_gate.py
+++ b/circuit_knitting/cutting/qpd/instructions/qpd_gate.py
@@ -14,14 +14,12 @@
from __future__ import annotations
-from abc import ABC, abstractmethod
-
from qiskit.circuit import QuantumCircuit, Instruction, CircuitInstruction
from ..qpd_basis import QPDBasis
-class BaseQPDGate(Instruction, ABC):
+class BaseQPDGate(Instruction):
"""Base class for a gate to be decomposed using quasiprobability decomposition."""
def __init__(
@@ -101,11 +99,6 @@ def __eq__(self, other):
and self.label == other.label
)
- @abstractmethod
- def _define(self) -> None:
- """Generate a decomposed gate."""
- raise NotImplementedError # pragma: no cover
-
class TwoQubitQPDGate(BaseQPDGate):
"""Two qubit gate to be decomposed using quasiprobability decomposition."""
@@ -198,9 +191,10 @@ def _set_qubit_id(self, qubit_id: int) -> None:
def _define(self) -> None:
if self.basis_id is None:
- raise ValueError(
- "Missing 'basis_id': unable to realize SingleQubitQPDGate."
- )
+ # With basis_id is not set, it does not make sense to define this
+ # operation in terms of more fundamental instructions, so we have
+ # self.definition remain as None.
+ return
qc = QuantumCircuit(1)
base = self.basis.maps[self.basis_id]
for op in base[self.qubit_id]:
diff --git a/circuit_knitting/cutting/qpd/qpd.py b/circuit_knitting/cutting/qpd/qpd.py
index 861405a36..c40084d31 100644
--- a/circuit_knitting/cutting/qpd/qpd.py
+++ b/circuit_knitting/cutting/qpd/qpd.py
@@ -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.
@@ -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):
@@ -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]] = {}
@@ -731,7 +735,7 @@ def _nonlocal_qpd_basis_from_u(
#
# Projective measurements in each basis
A0x = [HGate(), QPDMeasure(), HGate()]
- A0y = [SdgGate(), HGate(), QPDMeasure(), HGate(), SGate()]
+ A0y = [SXGate(), QPDMeasure(), SXdgGate()]
A0z = [QPDMeasure()]
# Single qubit rotations that swap two axes. There are "plus" and "minus"
# versions of these rotations. The "minus" rotations also flip the sign
@@ -829,17 +833,17 @@ def _nonlocal_qpd_basis_from_u(
@_register_qpdbasis_from_instruction("swap")
-def _(gate: SwapGate):
+def _(unused_gate: SwapGate):
return _nonlocal_qpd_basis_from_u([(1 + 1j) / np.sqrt(8)] * 4)
@_register_qpdbasis_from_instruction("iswap")
-def _(gate: iSwapGate):
+def _(unused_gate: iSwapGate):
return _nonlocal_qpd_basis_from_u([0.5, 0.5j, 0.5j, 0.5])
@_register_qpdbasis_from_instruction("dcx")
-def _(gate: DCXGate):
+def _(unused_gate: DCXGate):
retval = qpdbasis_from_instruction(iSwapGate())
# Modify basis according to DCXGate definition in Qiskit circuit library
# https://github.com/Qiskit/qiskit-terra/blob/e9f8b7c50968501e019d0cb426676ac606eb5a10/qiskit/circuit/library/standard_gates/equivalence_library.py#L938-L944
@@ -865,7 +869,7 @@ def _(gate: RXXGate | RYYGate | RZZGate | CRXGate | CRYGate | CRZGate):
pauli = YGate()
r_plus = RYGate(0.5 * np.pi)
# y basis measurement (and back again)
- measurement_0 = [SdgGate(), HGate(), QPDMeasure(), HGate(), SGate()]
+ measurement_0 = [SXGate(), QPDMeasure(), SXdgGate()]
else:
assert gate.name in ("rzz", "crz")
pauli = ZGate()
@@ -948,7 +952,7 @@ def _(gate: CPhaseGate):
@_register_qpdbasis_from_instruction("csx")
-def _(gate: CSXGate):
+def _(unused_gate: CSXGate):
retval = qpdbasis_from_instruction(CRXGate(np.pi / 2))
for operations in unique_by_id(m[0] for m in retval.maps):
operations.insert(0, TGate())
@@ -956,7 +960,7 @@ def _(gate: CSXGate):
@_register_qpdbasis_from_instruction("csxdg")
-def _(gate: ControlledGate):
+def _(unused_gate: ControlledGate):
retval = qpdbasis_from_instruction(CRXGate(-np.pi / 2))
for operations in unique_by_id(m[0] for m in retval.maps):
operations.insert(0, TdgGate())
@@ -1002,7 +1006,7 @@ def _(gate: CXGate | CYGate | CZGate | CHGate):
@_register_qpdbasis_from_instruction("ecr")
-def _(gate: ECRGate):
+def _(unused_gate: ECRGate):
retval = qpdbasis_from_instruction(CXGate())
# Modify basis according to ECRGate definition in Qiskit circuit library
# https://github.com/Qiskit/qiskit-terra/blob/d9763523d45a747fd882a7e79cc44c02b5058916/qiskit/circuit/library/standard_gates/equivalence_library.py#L656-L663
@@ -1032,18 +1036,18 @@ def _theta_from_instruction(gate: Gate, /) -> float:
@_register_qpdbasis_from_instruction("move")
-def _(gate: Move):
+def _(unused_gate: Move):
i_measurement = [Reset()]
x_measurement = [HGate(), QPDMeasure(), Reset()]
- y_measurement = [SdgGate(), HGate(), QPDMeasure(), Reset()]
+ y_measurement = [SXGate(), QPDMeasure(), Reset()]
z_measurement = [QPDMeasure(), Reset()]
prep_0 = [Reset()]
prep_1 = [Reset(), XGate()]
prep_plus = [Reset(), HGate()]
prep_minus = [Reset(), XGate(), HGate()]
- prep_iplus = [Reset(), HGate(), SGate()]
- prep_iminus = [Reset(), XGate(), HGate(), SGate()]
+ prep_iplus = [Reset(), SXdgGate()]
+ prep_iminus = [Reset(), XGate(), SXdgGate()]
# https://arxiv.org/abs/1904.00102v2 Eqs. (12)-(19)
maps1, maps2, coeffs = zip(
diff --git a/circuit_knitting/cutting/wire_cutting.py b/circuit_knitting/cutting/wire_cutting.py
deleted file mode 100644
index 374c53019..000000000
--- a/circuit_knitting/cutting/wire_cutting.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# 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.
-
-"""Code to initialize the deprecated cutqc wire cutting imports."""
-
-from warnings import warn
-
-from .cutqc import * # noqa: F403
-
-warn(
- f"The package namespace {__name__} is deprecated and will be removed "
- "no sooner than Circuit Knitting Toolbox 0.3.0. Use namespace "
- "circuit_knitting.cutting.cutqc instead.",
- DeprecationWarning,
- stacklevel=2,
-)
diff --git a/circuit_knitting/forging/cholesky_decomposition.py b/circuit_knitting/forging/cholesky_decomposition.py
index 1e5a8b726..ed9e4eb72 100644
--- a/circuit_knitting/forging/cholesky_decomposition.py
+++ b/circuit_knitting/forging/cholesky_decomposition.py
@@ -133,7 +133,7 @@ def convert_cholesky_operator(
Args:
operator: A `List[SparsePauliOp]` containing the single-body Hamiltonian followed
by the Cholesky operators
- shape: [single-body hamiltonian, cholesky_0, ..., cholesky_N]
+ shape: [single-body hamiltonian, cholesky_0, ..., cholesky_{N-1}]
ansatz: The ansatz for which to compute expectation values of operator. The
`EntanglementForgingAnsatz` also contains the bitstrings for each subsystem
diff --git a/circuit_knitting/forging/entanglement_forging_ground_state_solver.py b/circuit_knitting/forging/entanglement_forging_ground_state_solver.py
index 9baffe5f3..62fc2695d 100644
--- a/circuit_knitting/forging/entanglement_forging_ground_state_solver.py
+++ b/circuit_knitting/forging/entanglement_forging_ground_state_solver.py
@@ -25,7 +25,7 @@
import scipy
import numpy as np
-from qiskit.algorithms.optimizers import SPSA, Optimizer, OptimizerResult
+from qiskit_algorithms.optimizers import SPSA, Optimizer, OptimizerResult
from qiskit_nature.second_q.problems import (
ElectronicStructureProblem,
EigenstateResult,
diff --git a/circuit_knitting/forging/entanglement_forging_knitter.py b/circuit_knitting/forging/entanglement_forging_knitter.py
index b1becd674..3fe4bdb5c 100644
--- a/circuit_knitting/forging/entanglement_forging_knitter.py
+++ b/circuit_knitting/forging/entanglement_forging_knitter.py
@@ -428,8 +428,9 @@ def close_sessions(self) -> None:
f"There was a problem closing session id ({session_id}). "
"No backend to associate with session."
)
- session = Session(service=self.service, backend=self._backend_names[i])
- session._session_id = session_id
+ session = Session.from_id(
+ session_id, service=self.service, backend=self._backend_names[i]
+ )
session.close()
return
@@ -658,8 +659,7 @@ def _estimate_expvals(
"If passing a QiskitRuntimeService, a list of backend names must be specified."
)
service = QiskitRuntimeService(**service_args)
- session = Session(service=service, backend=backend_name)
- session._session_id = session_id
+ session = Session.from_id(session_id, service=service, backend=backend_name)
estimator = Estimator(session=session, options=options)
job = estimator.run(
diff --git a/circuit_knitting/utils/__init__.py b/circuit_knitting/utils/__init__.py
index d8dd0b15e..debf2ac14 100644
--- a/circuit_knitting/utils/__init__.py
+++ b/circuit_knitting/utils/__init__.py
@@ -59,6 +59,12 @@
=============================================================
.. automodule:: circuit_knitting.utils.transforms
+
+===================================================================
+Transpiler passes (:mod:`circuit_knitting.utils.transpiler_passes`)
+===================================================================
+
+.. automodule:: circuit_knitting.utils.transpiler_passes
"""
from .orbital_reduction import reduce_bitstrings
diff --git a/circuit_knitting/utils/simulation.py b/circuit_knitting/utils/simulation.py
index 2616208da..34e022dfd 100644
--- a/circuit_knitting/utils/simulation.py
+++ b/circuit_knitting/utils/simulation.py
@@ -152,7 +152,7 @@ def _call(
self,
circuits: tuple[QuantumCircuit, ...],
parameter_values: Sequence[Sequence[float]],
- **run_options,
+ **ignored_run_options,
) -> SamplerResult:
metadata: list[dict[str, Any]] = [{} for _ in range(len(circuits))]
bound_circuits = [
diff --git a/circuit_knitting/utils/transpiler_passes.py b/circuit_knitting/utils/transpiler_passes.py
new file mode 100644
index 000000000..c4885461e
--- /dev/null
+++ b/circuit_knitting/utils/transpiler_passes.py
@@ -0,0 +1,66 @@
+# 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.
+
+"""
+Transpiler passes useful for circuit knitting.
+
+.. currentmodule:: circuit_knitting.utils.transpiler_passes
+
+.. autosummary::
+ :toctree: ../stubs/
+
+ RemoveFinalReset
+ ConsolidateResets
+"""
+
+from qiskit.circuit import Reset, Qubit
+from qiskit.dagcircuit import DAGOpNode
+from qiskit.transpiler.basepasses import TransformationPass
+
+
+class RemoveFinalReset(TransformationPass):
+ """Remove reset when it is the final instruction on a qubit wire."""
+
+ def run(self, dag):
+ """Run the RemoveFinalReset pass on ``dag``.
+
+ Args:
+ dag (DAGCircuit): the DAG to be optimized.
+
+ Returns:
+ DAGCircuit: the optimized DAG.
+ """
+ for output_node in dag.output_map.values():
+ if isinstance(output_node.wire, Qubit):
+ pred = next(dag.predecessors(output_node))
+ if isinstance(pred, DAGOpNode) and isinstance(pred.op, Reset):
+ dag.remove_op_node(pred)
+ return dag
+
+
+class ConsolidateResets(TransformationPass):
+ """Consolidate a run duplicate resets in to a single reset."""
+
+ def run(self, dag):
+ """Run the ConsolidateResets pass on ``dag``.
+
+ Args:
+ dag (DAGCircuit): the DAG to be optimized.
+
+ Returns:
+ DAGCircuit: the optimized DAG.
+ """
+ resets = dag.op_nodes(Reset)
+ for reset in resets:
+ successor = next(dag.successors(reset))
+ if isinstance(successor, DAGOpNode) and isinstance(successor.op, Reset):
+ dag.remove_op_node(reset)
+ return dag
diff --git a/circuit_knitting_toolbox/README.md b/circuit_knitting_toolbox/README.md
deleted file mode 100644
index e3a86fa6f..000000000
--- a/circuit_knitting_toolbox/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-This directory exists only for backwards compatibility with the old
-`circuit_knitting_toolbox` import location. The actual source code is
-in the [`circuit_knitting`](../circuit-knitting) directory, instead.
diff --git a/circuit_knitting_toolbox/__init__.py b/circuit_knitting_toolbox/__init__.py
deleted file mode 100644
index 61584838c..000000000
--- a/circuit_knitting_toolbox/__init__.py
+++ /dev/null
@@ -1,27 +0,0 @@
-# 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.
-
-"""Deprecated import location ``circuit_knitting_toolbox``."""
-
-import sys
-from warnings import warn
-
-from circuit_knitting import utils
-
-sys.modules["circuit_knitting_toolbox.utils"] = utils
-
-warn(
- f"The package namespace {__name__} is deprecated and will be removed "
- "no sooner than Circuit Knitting Toolbox 0.4.0. Use namespace "
- "circuit_knitting instead.",
- DeprecationWarning,
- stacklevel=2,
-)
diff --git a/circuit_knitting_toolbox/circuit_cutting/__init__.py b/circuit_knitting_toolbox/circuit_cutting/__init__.py
deleted file mode 100644
index 68581cf78..000000000
--- a/circuit_knitting_toolbox/circuit_cutting/__init__.py
+++ /dev/null
@@ -1,75 +0,0 @@
-# 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.
-
-"""Deprecated import location ``circuit_knitting_toolbox.circuit_cutting``."""
-
-import sys
-from warnings import warn
-
-from circuit_knitting.cutting import (
- partition_circuit_qubits,
- partition_problem,
- cut_gates,
- decompose_gates,
- execute_experiments,
- reconstruct_expectation_values,
- PartitionedCuttingProblem,
- CuttingExperimentResults,
- qpd,
- cutqc,
- cutting_decomposition,
- cutting_evaluation,
- cutting_reconstruction,
- wire_cutting,
-)
-
-__all__ = [
- "partition_circuit_qubits",
- "partition_problem",
- "cut_gates",
- "decompose_gates",
- "execute_experiments",
- "reconstruct_expectation_values",
- "PartitionedCuttingProblem",
- "CuttingExperimentResults",
-]
-
-sys.modules["circuit_knitting_toolbox.circuit_cutting.qpd"] = qpd
-sys.modules["circuit_knitting_toolbox.circuit_cutting.qpd.qpd"] = qpd.qpd
-sys.modules["circuit_knitting_toolbox.circuit_cutting.qpd.qpd_basis"] = qpd.qpd_basis
-sys.modules[
- "circuit_knitting_toolbox.circuit_cutting.qpd.instructions"
-] = qpd.instructions
-sys.modules[
- "circuit_knitting_toolbox.circuit_cutting.qpd.instructions.qpd_gate"
-] = qpd.instructions.qpd_gate
-sys.modules[
- "circuit_knitting_toolbox.circuit_cutting.qpd.instructions.qpd_measure"
-] = qpd.instructions.qpd_measure
-sys.modules["circuit_knitting_toolbox.circuit_cutting.cutqc"] = cutqc
-sys.modules["circuit_knitting_toolbox.circuit_cutting.wire_cutting"] = wire_cutting
-sys.modules[
- "circuit_knitting_toolbox.circuit_cutting.cutting_decomposition"
-] = cutting_decomposition
-sys.modules[
- "circuit_knitting_toolbox.circuit_cutting.cutting_evaluation"
-] = cutting_evaluation
-sys.modules[
- "circuit_knitting_toolbox.circuit_cutting.cutting_reconstruction"
-] = cutting_reconstruction
-
-warn(
- f"The package namespace {__name__} is deprecated and will be removed "
- "no sooner than Circuit Knitting Toolbox 0.4.0. Use namespace "
- "circuit_knitting.cutting instead.",
- DeprecationWarning,
- stacklevel=2,
-)
diff --git a/circuit_knitting_toolbox/entanglement_forging/__init__.py b/circuit_knitting_toolbox/entanglement_forging/__init__.py
deleted file mode 100644
index 17a8fe692..000000000
--- a/circuit_knitting_toolbox/entanglement_forging/__init__.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# 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.
-
-"""Deprecated import location ``circuit_knitting_toolbox.entanglement_forging``."""
-
-import sys
-from warnings import warn
-
-from circuit_knitting.forging import (
- EntanglementForgingAnsatz,
- EntanglementForgingKnitter,
- EntanglementForgingOperator,
- EntanglementForgingGroundStateSolver,
- cholesky_decomposition,
- convert_cholesky_operator,
- entanglement_forging_ansatz,
- entanglement_forging_ground_state_solver,
- entanglement_forging_knitter,
- entanglement_forging_operator,
-)
-import circuit_knitting.forging.cholesky_decomposition as cholesky_module
-
-__all__ = [
- "EntanglementForgingAnsatz",
- "EntanglementForgingKnitter",
- "EntanglementForgingOperator",
- "EntanglementForgingGroundStateSolver",
- "cholesky_decomposition",
- "convert_cholesky_operator",
-]
-
-sys.modules[
- "circuit_knitting_toolbox.entanglement_forging.cholesky_decomposition"
-] = cholesky_module
-sys.modules[
- "circuit_knitting_toolbox.entanglement_forging.entanglement_forging_ansatz"
-] = entanglement_forging_ansatz
-sys.modules[
- "circuit_knitting_toolbox.entanglement_forging.entanglement_forging_ground_state_solver"
-] = entanglement_forging_ground_state_solver
-sys.modules[
- "circuit_knitting_toolbox.entanglement_forging.entanglement_forging_knitter"
-] = entanglement_forging_knitter
-sys.modules[
- "circuit_knitting_toolbox.entanglement_forging.entanglement_forging_operator"
-] = entanglement_forging_operator
-
-warn(
- f"The package namespace {__name__} is deprecated and will be removed "
- "no sooner than Circuit Knitting Toolbox 0.4.0. Use namespace "
- "circuit_knitting.forging instead.",
- DeprecationWarning,
- stacklevel=2,
-)
diff --git a/compose.yaml b/compose.yaml
index 2b70ab8d1..d0df582b3 100644
--- a/compose.yaml
+++ b/compose.yaml
@@ -1,3 +1,9 @@
+# This file exists so users can easily get started with the Circuit
+# Knitting Toolbox in a container using Docker Compose.
+#
+# Instructions are available at:
+# https://qiskit-extensions.github.io/circuit-knitting-toolbox/install.html#option-3-use-within-docker
+
services:
notebook:
build: .
diff --git a/docs/circuit_cutting/cutqc/tutorials/tutorial_3_cutting_with_quantum_serverless.ipynb b/docs/circuit_cutting/cutqc/tutorials/tutorial_3_cutting_with_quantum_serverless.ipynb
index 374393f3c..1b2b9ad6b 100644
--- a/docs/circuit_cutting/cutqc/tutorials/tutorial_3_cutting_with_quantum_serverless.ipynb
+++ b/docs/circuit_cutting/cutqc/tutorials/tutorial_3_cutting_with_quantum_serverless.ipynb
@@ -442,7 +442,6 @@
" circuit: QuantumCircuit,\n",
" subcircuit_instance_probabilities: dict[int, dict[int, np.ndarray]],\n",
" cuts: dict[str, Any],\n",
- " num_threads: int = 1,\n",
") -> np.ndarray:\n",
" return reconstruct_full_distribution(\n",
" circuit, subcircuit_instance_probabilities, cuts\n",
diff --git a/docs/circuit_cutting/explanation/index.rst b/docs/circuit_cutting/explanation/index.rst
index 5a40579f8..c8b6cc60d 100644
--- a/docs/circuit_cutting/explanation/index.rst
+++ b/docs/circuit_cutting/explanation/index.rst
@@ -42,7 +42,7 @@ Much of the circuit cutting literature describes a process where we sample from
Current limitations
-------------------
-* ``PauliList`` is the only supported observable format until no sooner than CKT v0.5.0.
+* ``PauliList`` is the only supported observable format until no sooner than CKT v0.6.0.
References
----------
diff --git a/docs/circuit_cutting/how-tos/how_to_specify_cut_wires.ipynb b/docs/circuit_cutting/how-tos/how_to_specify_cut_wires.ipynb
index 806d2a264..b4ea9eda8 100644
--- a/docs/circuit_cutting/how-tos/how_to_specify_cut_wires.ipynb
+++ b/docs/circuit_cutting/how-tos/how_to_specify_cut_wires.ipynb
@@ -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",
diff --git a/docs/circuit_cutting/tutorials/02_gate_cutting_to_reduce_circuit_depth.ipynb b/docs/circuit_cutting/tutorials/02_gate_cutting_to_reduce_circuit_depth.ipynb
index 1b61a456d..e3d035331 100644
--- a/docs/circuit_cutting/tutorials/02_gate_cutting_to_reduce_circuit_depth.ipynb
+++ b/docs/circuit_cutting/tutorials/02_gate_cutting_to_reduce_circuit_depth.ipynb
@@ -57,7 +57,7 @@
"source": [
"### Specify some observables\n",
"\n",
- "Currently, only `Pauli` observables with phase equal to 1 are supported. Full support for `SparsePauliOp` is expected in CKT v0.5.0."
+ "Currently, only `Pauli` observables with phase equal to 1 are supported. Full support for `SparsePauliOp` is expected in CKT v0.6.0."
]
},
{
@@ -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."
]
},
{
@@ -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",
diff --git a/docs/circuit_cutting/tutorials/03_wire_cutting_via_move_instruction.ipynb b/docs/circuit_cutting/tutorials/03_wire_cutting_via_move_instruction.ipynb
index 85ef550c5..768d0865e 100644
--- a/docs/circuit_cutting/tutorials/03_wire_cutting_via_move_instruction.ipynb
+++ b/docs/circuit_cutting/tutorials/03_wire_cutting_via_move_instruction.ipynb
@@ -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."
]
},
{
@@ -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",
diff --git a/docs/circuit_cutting/tutorials/README.rst b/docs/circuit_cutting/tutorials/README.rst
index 20e78091e..b43a4d10b 100644
--- a/docs/circuit_cutting/tutorials/README.rst
+++ b/docs/circuit_cutting/tutorials/README.rst
@@ -2,8 +2,7 @@ Circuit Cutting Tutorials
-------------------------
- `Tutorial 1 <01_gate_cutting_to_reduce_circuit_width.ipynb>`__:
- Separate sets of qubits by cutting nonlocal gates and run the
- subexperiments for each qubit partition in parallel.
+ Separate sets of qubits by cutting nonlocal gates.
- `Tutorial 2 <02_gate_cutting_to_reduce_circuit_depth.ipynb>`__:
Cut gates requiring many SWAPs to decrease circuit depth.
- `Tutorial 3 <03_wire_cutting_via_move_instruction.ipynb>`__:
diff --git a/docs/conf.py b/docs/conf.py
index be73e4995..0613636e0 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -30,7 +30,7 @@
sys.path.insert(0, str(_rootdir))
# The full version, including alpha/beta/rc tags
-release = metadata_version("circuit_knitting_toolbox")
+release = metadata_version("circuit-knitting-toolbox")
# The short X.Y version
version = ".".join(release.split(".")[:2])
diff --git a/docs/entanglement_forging/how-tos/specify-problem.rst b/docs/entanglement_forging/how-tos/specify-problem.rst
index 94b7edff6..4763948c5 100644
--- a/docs/entanglement_forging/how-tos/specify-problem.rst
+++ b/docs/entanglement_forging/how-tos/specify-problem.rst
@@ -2,30 +2,31 @@
How to specify the problem
##########################
-To specify the problem, we follow the Qiskit Nature workflow. We set up the ``molecule`` object, specify the driver, and instantiate the ``ElectronicStructureProblem``.
+To specify the problem, we follow the Qiskit Nature workflow. We set up the ``molecule`` object, specify the driver, and instantiate the :class:`.ElectronicStructureProblem`.
We first require the following modules:
.. jupyter-execute::
- from qiskit_nature.drivers import Molecule
- from qiskit_nature.drivers.second_quantization import PySCFDriver
- from qiskit_nature.problems.second_quantization import ElectronicStructureProblem
+ from qiskit_nature.second_q.formats import MoleculeInfo
+ from qiskit_nature.second_q.drivers import PySCFDriver
+ from qiskit_nature.second_q.problems import ElectronicBasis
To set up the ``molecule`` object, we specify the individual atoms and their positions:
.. jupyter-execute::
- molecule = Molecule(
- geometry=[
- ("H", [0.0, 0.0, 0.0]),
- ("H", [0.0, 0.0, 0.735]),
+ molecule = MoleculeInfo(
+ ["H", "H"],
+ [
+ (0.0, 0.0, 0.0),
+ (0.0, 0.0, 0.735),
],
charge=0,
multiplicity=1, # Multiplicity (2S+1) of the molecule, where S is the total spin angular momentum
)
-We then specify the driver (see ``PySCFDriver``) and load the molecule into the driver:
+We then specify the driver (see :class:`.PySCFDriver`) and load the molecule into the driver:
.. jupyter-execute::
@@ -33,8 +34,10 @@ We then specify the driver (see ``PySCFDriver``) and load the molecule into the
Here, the driver is an algorithm class that knows how to calculate the second quantized operators.
-Finally, we instantiate the ``ElectronicStructureProblem``, a class which wraps a number of different types of drivers:
+Finally, we instantiate the :class:`.ElectronicStructureProblem`, a class which wraps a number of different types of drivers:
.. jupyter-execute::
+ :stderr:
- problem = ElectronicStructureProblem(driver)
+ driver.run()
+ problem = driver.to_problem(basis=ElectronicBasis.AO)
diff --git a/docs/entanglement_forging/tutorials/tutorial_1_getting_started.ipynb b/docs/entanglement_forging/tutorials/tutorial_1_getting_started.ipynb
index b4db1dff3..dff5cc1be 100644
--- a/docs/entanglement_forging/tutorials/tutorial_1_getting_started.ipynb
+++ b/docs/entanglement_forging/tutorials/tutorial_1_getting_started.ipynb
@@ -144,10 +144,10 @@
"metadata": {},
"source": [
"### Set up `EntanglementForgingGroundStateSolver`\n",
- "Next, we set up the `EntanglementForgingGroundStateSolver`. It is passed the ansatz, a classical optimizer ([COBYLA](https://qiskit.org/documentation/stubs/qiskit.algorithms.optimizers.COBYLA.html?highlight=cobyla#qiskit.algorithms.optimizers.COBYLA), in this case), and an initial point.\n",
+ "Next, we set up the `EntanglementForgingGroundStateSolver`. It is passed the ansatz, a classical optimizer ([COBYLA](https://qiskit.org/ecosystem/algorithms/stubs/qiskit_algorithms.optimizers.COBYLA.html), in this case), and an initial point.\n",
"\n",
" * The `ansatz` field is required.\n",
- " * If no optimizer is passed, [SPSA](https://qiskit.org/documentation/stubs/qiskit.algorithms.optimizers.SPSA.html) with default settings will be used.\n",
+ " * If no optimizer is passed, [SPSA](https://qiskit.org/ecosystem/algorithms/stubs/qiskit_algorithms.optimizers.SPSA.html) with default settings will be used.\n",
" * If a Qiskit Runtime Service is not passed, then a local simulator will be used with the [Qiskit Primitives](https://qiskit.org/documentation/apidoc/primitives.html), and the `backend_names` argument will be ignored.\n",
" * If multiple backend names are passed, the expectation value calculations at each iteration will be divided evenly among them and calculated in parallel.\n",
" * If a single options argument is passed, it will be used for all backends. If a list of options is passed, they will be synchronized with the backends 1:1\n",
@@ -163,7 +163,7 @@
"outputs": [],
"source": [
"import numpy as np\n",
- "from qiskit.algorithms.optimizers import COBYLA\n",
+ "from qiskit_algorithms.optimizers import COBYLA\n",
"from circuit_knitting.forging import (\n",
" EntanglementForgingGroundStateSolver,\n",
")\n",
diff --git a/docs/entanglement_forging/tutorials/tutorial_2_forging_with_quantum_serverless.ipynb b/docs/entanglement_forging/tutorials/tutorial_2_forging_with_quantum_serverless.ipynb
index cb6c9c344..839c1e8a4 100644
--- a/docs/entanglement_forging/tutorials/tutorial_2_forging_with_quantum_serverless.ipynb
+++ b/docs/entanglement_forging/tutorials/tutorial_2_forging_with_quantum_serverless.ipynb
@@ -230,7 +230,7 @@
"outputs": [],
"source": [
"from typing import Sequence\n",
- "from qiskit.algorithms.optimizers import Optimizer\n",
+ "from qiskit_algorithms.optimizers import Optimizer\n",
"from qiskit.result import Result\n",
"from quantum_serverless import distribute_task\n",
"from circuit_knitting.forging import (\n",
@@ -315,7 +315,7 @@
"%%capture\n",
"\n",
"from dataclasses import asdict\n",
- "from qiskit.algorithms.optimizers import COBYLA\n",
+ "from qiskit_algorithms.optimizers import COBYLA\n",
"from quantum_serverless import get\n",
"\n",
"optimizer = COBYLA()\n",
diff --git a/pyproject.toml b/pyproject.toml
index 81b4c7f51..056d3ab40 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "circuit-knitting-toolbox"
-version = "0.4.0"
+version = "0.5.0"
description = "A software prototype for a circuit knitting toolbox which connects user applications with runtime primitives"
readme = "README.md"
license = {file = "LICENSE.txt"}
@@ -27,14 +27,13 @@ requires-python = ">=3.8"
dependencies = [
"numpy>=1.23.0",
- # scipy is currently bounded from above because scipy 1.11 removed the
- # `sym_pos` kwarg from `scipy.linalg.solve`, but pyscf has not yet been updated.
- "scipy>=1.5.2,<1.11",
- "rustworkx>=0.12.0",
+ "scipy>=1.5.2",
+ "rustworkx>=0.13.0",
"qiskit-aer>=0.12.0",
- "qiskit>=0.43.0",
- "qiskit-nature>=0.6.0",
- "qiskit-ibm-runtime>=0.9.2",
+ "qiskit>=0.44.1, <1.0",
+ "qiskit-algorithms>=0.2.1",
+ "qiskit-nature>=0.6.0, <0.7",
+ "qiskit-ibm-runtime>=0.12.2",
]
[project.optional-dependencies]
@@ -71,9 +70,9 @@ style = [
lint = [
"circuit-knitting-toolbox[style]",
"pydocstyle==6.3.0",
- "mypy==1.5.1",
+ "mypy==1.8.0",
"reno>=3.4.0",
- "pylint==2.17.5",
+ "pylint==3.0.3",
# pydocstyle prefers to parse our pyproject.toml, hence the following line
"toml",
]
@@ -105,18 +104,17 @@ notebook-dependencies = [
[tool.hatch.build.targets.wheel]
only-include = [
"circuit_knitting",
- "circuit_knitting_toolbox",
]
[tool.autoflake]
remove-unused-variables = true
remove-all-unused-imports = true
-[tool.coverage.run]
-omit = [
- # deprecated import location(s)
- "circuit_knitting/cutting/wire_cutting.py",
-]
+#[tool.coverage.run]
+#omit = [
+# # deprecated import location(s)
+# "circuit_knitting/path/to/deprecated/module",
+#]
[tool.pytest.ini_options]
testpaths = ["./circuit_knitting/", "./test/"]
diff --git a/releasenotes/notes/deprecate-execute_experiments-b96e39653546dcc1.yaml b/releasenotes/notes/0.5/deprecate-execute_experiments-b96e39653546dcc1.yaml
similarity index 100%
rename from releasenotes/notes/deprecate-execute_experiments-b96e39653546dcc1.yaml
rename to releasenotes/notes/0.5/deprecate-execute_experiments-b96e39653546dcc1.yaml
diff --git a/releasenotes/notes/0.5/prepare-0.5-1057fa4d83bf26dc.yaml b/releasenotes/notes/0.5/prepare-0.5-1057fa4d83bf26dc.yaml
new file mode 100644
index 000000000..17f94a9f9
--- /dev/null
+++ b/releasenotes/notes/0.5/prepare-0.5-1057fa4d83bf26dc.yaml
@@ -0,0 +1,8 @@
+---
+prelude: |
+ The primary purpose of this release is to swap the order of the
+ classical registers in the circuits generated the
+ :mod:`circuit_knitting.cutting` module. With this change,
+ ``"observable_measurements"`` now comes before
+ ``"qpd_measurements"``, and there is no longer a need to insert
+ ``num_qpd_bits`` into the result metadata by hand.
diff --git a/releasenotes/notes/0.5/qiskit-algorithms-ec3fe782619bab45.yaml b/releasenotes/notes/0.5/qiskit-algorithms-ec3fe782619bab45.yaml
new file mode 100644
index 000000000..3959f4ce3
--- /dev/null
+++ b/releasenotes/notes/0.5/qiskit-algorithms-ec3fe782619bab45.yaml
@@ -0,0 +1,7 @@
+---
+upgrade:
+ - |
+ CKT now depends on the `qiskit-algorithms
+ `__
+ package. This new package replaces the ``qiskit.algorithms``
+ module, which was deprecated in Qiskit 0.44.
diff --git a/releasenotes/notes/0.5/qiskit-nature-pinned-c359e3933bfef993.yaml b/releasenotes/notes/0.5/qiskit-nature-pinned-c359e3933bfef993.yaml
new file mode 100644
index 000000000..dd5f10fa9
--- /dev/null
+++ b/releasenotes/notes/0.5/qiskit-nature-pinned-c359e3933bfef993.yaml
@@ -0,0 +1,8 @@
+---
+other:
+ - |
+ ``qiskit-nature`` is now pinned to version 0.6.X, as the
+ entanglement forging code has not yet been updated to work with
+ Qiskit Nature 0.7.0. Compatibility with Qiskit Nature 0.7.0 is
+ tracked by `issue #406
+ `__.
diff --git a/releasenotes/notes/0.5/register_order_swapped-5b07fb661c6250f9.yaml b/releasenotes/notes/0.5/register_order_swapped-5b07fb661c6250f9.yaml
new file mode 100644
index 000000000..5b902b466
--- /dev/null
+++ b/releasenotes/notes/0.5/register_order_swapped-5b07fb661c6250f9.yaml
@@ -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.
diff --git a/releasenotes/notes/0.5/remove-wire-cutting-ad0e7ab8af699561.yaml b/releasenotes/notes/0.5/remove-wire-cutting-ad0e7ab8af699561.yaml
new file mode 100644
index 000000000..c1bba79d5
--- /dev/null
+++ b/releasenotes/notes/0.5/remove-wire-cutting-ad0e7ab8af699561.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+ - |
+ Removed the :mod:`circuit_knitting.cutting.wire_cutting` module. Users are now expected to import from the :mod:`circuit_knitting.cutting.cutqc` module instead.
diff --git a/releasenotes/notes/qiskit-0.44-required-5eec7abe45b88ecc.yaml b/releasenotes/notes/qiskit-0.44-required-5eec7abe45b88ecc.yaml
new file mode 100644
index 000000000..3bb94dad8
--- /dev/null
+++ b/releasenotes/notes/qiskit-0.44-required-5eec7abe45b88ecc.yaml
@@ -0,0 +1,8 @@
+---
+upgrade:
+ - |
+ The minimum supported version of ``qiskit`` is now 0.44.1, and the
+ minimum supported version of ``qiskit-ibm-runtime`` is now 0.12.2.
+ CKT also now explicitly requires a version of ``qiskit`` less than
+ 1.0, as there is no guarantee that the current version of CKT will
+ work with Qiskit 1.0.
diff --git a/releasenotes/notes/qpdgate-serialization-710ce2fc6ab898e0.yaml b/releasenotes/notes/qpdgate-serialization-710ce2fc6ab898e0.yaml
new file mode 100644
index 000000000..6fb1c45ac
--- /dev/null
+++ b/releasenotes/notes/qpdgate-serialization-710ce2fc6ab898e0.yaml
@@ -0,0 +1,9 @@
+---
+fixes:
+ - |
+ It is now possible to serialize :class:`.SingleQubitQPDGate`\ s
+ using :mod:`~qiskit.qpy`,
+ but some other issues with serialization and deserialization still
+ remain. See issue `#455
+ `__
+ for details.
diff --git a/releasenotes/notes/remove-ckt-path-2386dc156b1ed9ca.yaml b/releasenotes/notes/remove-ckt-path-2386dc156b1ed9ca.yaml
new file mode 100644
index 000000000..3d80df091
--- /dev/null
+++ b/releasenotes/notes/remove-ckt-path-2386dc156b1ed9ca.yaml
@@ -0,0 +1,4 @@
+---
+upgrade:
+ - |
+ Removed the ``circuit_knitting_toolbox`` import path. Users should now import from ``circuit_knitting`` instead.
diff --git a/releasenotes/notes/remove-redundant-resets-1893a61a341e6ce8.yaml b/releasenotes/notes/remove-redundant-resets-1893a61a341e6ce8.yaml
new file mode 100644
index 000000000..e8dbf6def
--- /dev/null
+++ b/releasenotes/notes/remove-redundant-resets-1893a61a341e6ce8.yaml
@@ -0,0 +1,12 @@
+---
+upgrade:
+ - |
+ The :func:`.generate_cutting_experiments` function now performs
+ some optimizations on the generated circuits before returning them
+ to the user. In particular, it performs the
+ :class:`~qiskit.transpiler.passes.RemoveResetInZeroState`,
+ :class:`.RemoveFinalReset`, and :class:`.ConsolidateResets`
+ passes, so that circuits with cut wires and no re-used qubits are
+ transformed into subexperiments that contain no
+ :class:`~qiskit.circuit.library.Reset`\ s. This allows such circuits to
+ work on a greater variety of hardware backends.
diff --git a/test/cutting/qpd/instructions/test_qpd_gate.py b/test/cutting/qpd/instructions/test_qpd_gate.py
index 114d62477..65064ce03 100644
--- a/test/cutting/qpd/instructions/test_qpd_gate.py
+++ b/test/cutting/qpd/instructions/test_qpd_gate.py
@@ -13,9 +13,11 @@
import unittest
import copy
+import io
import pytest
-from qiskit.circuit.library.standard_gates import XGate, YGate, ZGate
+from qiskit import QuantumCircuit, qpy
+from qiskit.circuit.library.standard_gates import CXGate, XGate, YGate, ZGate
from circuit_knitting.cutting.qpd import (
QPDBasis,
@@ -92,12 +94,7 @@ def test_qubit_id_out_of_range(self):
def test_missing_basis_id(self):
maps = [([XGate()], [YGate()])]
basis = QPDBasis(maps, [1.0])
- with pytest.raises(ValueError) as e_info:
- SingleQubitQPDGate(basis=basis, qubit_id=0).definition
- self.assertEqual(
- "Missing 'basis_id': unable to realize SingleQubitQPDGate.",
- e_info.value.args[0],
- )
+ assert SingleQubitQPDGate(basis=basis, qubit_id=0).definition is None
def test_compare_1q_and_2q(self):
maps = [([XGate()], [YGate()])]
@@ -107,3 +104,10 @@ def test_compare_1q_and_2q(self):
# Call both eq methods, since single qubit implements a slightly different equivalence
self.assertFalse(inst_2q == inst_1q)
self.assertFalse(inst_1q == inst_2q)
+
+ def test_qpy_serialization(self):
+ qc = QuantumCircuit(2)
+ qc.append(TwoQubitQPDGate.from_instruction(CXGate()), [0, 1])
+
+ f = io.BytesIO()
+ qpy.dump(qc, f)
diff --git a/test/cutting/qpd/test_qpd_basis.py b/test/cutting/qpd/test_qpd_basis.py
index 2cce36cf9..57e0052d6 100644
--- a/test/cutting/qpd/test_qpd_basis.py
+++ b/test/cutting/qpd/test_qpd_basis.py
@@ -51,7 +51,7 @@ def setUp(self):
y_gate = YGate()
y_r_plus = RYGate(1 * np.pi / 2)
y_r_minus = RYGate(-1 * np.pi / 2)
- y_measure = [SdgGate(), HGate(), QPDMeasure(), HGate(), SGate()]
+ y_measure = [SXGate(), QPDMeasure(), SXdgGate()]
self.truth_ryy_maps = [
([], []),
([y_gate], [y_gate]),
diff --git a/test/cutting/test_backwards_compatibility.py b/test/cutting/test_backwards_compatibility.py
index 347ea52d3..504650679 100644
--- a/test/cutting/test_backwards_compatibility.py
+++ b/test/cutting/test_backwards_compatibility.py
@@ -29,108 +29,6 @@
import pytest
-@pytest.mark.filterwarnings("ignore::PendingDeprecationWarning")
-@pytest.mark.filterwarnings("ignore::DeprecationWarning")
-@pytest.mark.skipforcoverage
-def test_v0_2_cutting_width_workflow():
- """v0.2 workflow to reduce circuit width through cutting
-
- docs/circuit_cutting/tutorials/gate_cutting_to_reduce_circuit_width.ipynb
- """
- import numpy as np
- from qiskit.circuit.library import EfficientSU2
- from qiskit.quantum_info import PauliList
- from qiskit_aer.primitives import Sampler
-
- from circuit_knitting_toolbox.circuit_cutting import (
- partition_problem,
- execute_experiments,
- reconstruct_expectation_values,
- )
-
- circuit = EfficientSU2(4, entanglement="linear", reps=2).decompose()
- circuit.assign_parameters([0.8] * len(circuit.parameters), inplace=True)
- observables = PauliList(["ZZII", "IZZI", "IIZZ", "XIXI", "ZIZZ", "IXIX"])
- subcircuits, bases, subobservables = partition_problem(
- circuit=circuit, partition_labels="AABB", observables=observables
- )
- assert np.prod([basis.overhead for basis in bases]) == pytest.approx(81)
-
- samplers = {
- "A": Sampler(run_options={"shots": 1}),
- "B": Sampler(run_options={"shots": 1}),
- }
- quasi_dists, coefficients = execute_experiments(
- circuits=subcircuits,
- subobservables=subobservables,
- num_samples=1500,
- samplers=samplers,
- )
- simulated_expvals = reconstruct_expectation_values(
- quasi_dists,
- coefficients,
- subobservables,
- )
- assert len(simulated_expvals) == len(observables)
-
-
-@pytest.mark.filterwarnings("ignore::PendingDeprecationWarning")
-@pytest.mark.filterwarnings("ignore::DeprecationWarning")
-@pytest.mark.skipforcoverage
-def test_v0_2_cutting_depth_workflow():
- """v0.2 workflow to reduce circuit depth through cutting
-
- docs/circuit_cutting/tutorials/gate_cutting_to_reduce_circuit_depth.ipynb
- """
- import numpy as np
- from qiskit import transpile
- from qiskit.circuit.library import EfficientSU2
- from qiskit_aer.primitives import Sampler
- from qiskit.providers.fake_provider import FakeHanoiV2 as FakeHanoi
- from qiskit.quantum_info import PauliList
-
- from circuit_knitting_toolbox.circuit_cutting import (
- decompose_gates,
- execute_experiments,
- reconstruct_expectation_values,
- )
-
- circuit = EfficientSU2(num_qubits=4, entanglement="circular").decompose()
- circuit.assign_parameters([0.8] * len(circuit.parameters), inplace=True)
- observables = PauliList(["ZZII", "IZZI", "IIZZ", "XIXI", "ZIZZ", "IXIX"])
- backend = FakeHanoi()
- transpile(circuit, backend=backend, initial_layout=[0, 1, 2, 3])
- cut_indices = [
- i
- for i, instruction in enumerate(circuit.data)
- if {circuit.find_bit(q)[0] for q in instruction.qubits} == {0, 3}
- ]
- # Decompose distant CNOTs into TwoQubitQPDGate instances
- qpd_circuit, bases = decompose_gates(circuit, cut_indices)
- assert np.prod([basis.overhead for basis in bases]) == pytest.approx(729)
- from circuit_knitting_toolbox.circuit_cutting.qpd import decompose_qpd_instructions
-
- for idx in cut_indices:
- qpd_circuit[idx].operation.basis_id = 2
- qpd_circuit_dx = decompose_qpd_instructions(
- qpd_circuit, [[idx] for idx in cut_indices]
- )
- transpile(qpd_circuit_dx, backend=backend, initial_layout=[0, 1, 2, 3])
- sampler = Sampler(run_options={"shots": 1})
- quasi_dists, coefficients = execute_experiments(
- circuits=qpd_circuit,
- subobservables=observables,
- num_samples=1500,
- samplers=sampler,
- )
- simulated_expvals = reconstruct_expectation_values(
- quasi_dists,
- coefficients,
- observables,
- )
- assert len(simulated_expvals) == len(observables)
-
-
@pytest.mark.filterwarnings("ignore::PendingDeprecationWarning")
@pytest.mark.filterwarnings("ignore::DeprecationWarning")
@pytest.mark.skipforcoverage
diff --git a/test/cutting/test_cutting_decomposition.py b/test/cutting/test_cutting_decomposition.py
index 1ce0d13e9..ee68ed806 100644
--- a/test/cutting/test_cutting_decomposition.py
+++ b/test/cutting/test_cutting_decomposition.py
@@ -137,7 +137,7 @@ def test_partition_circuit_qubits(self):
)
with self.subTest("Unsupported gate"):
compare_circuit = QuantumCircuit(3)
- compare_circuit.toffoli(0, 1, 2)
+ compare_circuit.ccx(0, 1, 2)
partitions = [0, 1, 1]
with pytest.raises(ValueError) as e_info:
partition_circuit_qubits(compare_circuit, partitions)
@@ -147,7 +147,7 @@ def test_partition_circuit_qubits(self):
)
with self.subTest("Toffoli gate in a single partition"):
circuit = QuantumCircuit(4)
- circuit.toffoli(0, 1, 2)
+ circuit.ccx(0, 1, 2)
circuit.rzz(np.pi / 7, 2, 3)
partition_circuit_qubits(circuit, "AAAB")
diff --git a/test/cutting/test_cutting_evaluation.py b/test/cutting/test_cutting_evaluation.py
index 80dc1d7fc..f9d3027dd 100644
--- a/test/cutting/test_cutting_evaluation.py
+++ b/test/cutting/test_cutting_evaluation.py
@@ -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"):
@@ -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)
@@ -220,7 +216,7 @@ def test_execute_experiments(self):
with self.subTest("Dict of non-unique samplers"):
qc = QuantumCircuit(2)
qc.x(0)
- qc.cnot(0, 1)
+ qc.cx(0, 1)
subcircuits, _, subobservables = partition_problem(
circuit=qc, partition_labels="AB", observables=PauliList(["XX"])
)
diff --git a/test/cutting/test_cutting_experiments.py b/test/cutting/test_cutting_experiments.py
index 46efeda66..6d435f34a 100644
--- a/test/cutting/test_cutting_experiments.py
+++ b/test/cutting/test_cutting_experiments.py
@@ -15,7 +15,7 @@
import pytest
import numpy as np
from qiskit.quantum_info import PauliList, Pauli
-from qiskit.circuit import QuantumCircuit, ClassicalRegister
+from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library.standard_gates import CXGate
from circuit_knitting.cutting.qpd import (
@@ -27,7 +27,10 @@
from circuit_knitting.cutting import generate_cutting_experiments
from circuit_knitting.cutting.qpd import WeightType
from circuit_knitting.cutting import partition_problem
-from circuit_knitting.cutting.cutting_experiments import _append_measurement_circuit
+from circuit_knitting.cutting.cutting_experiments import (
+ _append_measurement_register,
+ _append_measurement_circuit,
+)
class TestCuttingExperiments(unittest.TestCase):
@@ -154,23 +157,37 @@ def test_generate_cutting_experiments(self):
== "SingleQubitQPDGates are not supported in unseparable circuits."
)
+ def test_append_measurement_register(self):
+ qc = QuantumCircuit(2)
+ qc.h(0)
+ qc.cx(0, 1)
+ cog = CommutingObservableGroup(Pauli("XZ"), list(PauliList(["IZ", "XI", "XZ"])))
+ with self.subTest("In place"):
+ qcx = qc.copy()
+ assert _append_measurement_register(qcx, cog, inplace=True) is qcx
+ with self.subTest("Out of place"):
+ assert _append_measurement_register(qc, cog) is not qc
+ with self.subTest("Correct number of bits"):
+ assert _append_measurement_register(qc, cog).num_clbits == len(
+ cog.pauli_indices
+ )
+
def test_append_measurement_circuit(self):
qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0, 1)
- qc.add_register(ClassicalRegister(1, name="qpd_measurements"))
cog = CommutingObservableGroup(Pauli("XZ"), list(PauliList(["IZ", "XI", "XZ"])))
- qc2 = qc.copy()
- qc2.add_register(ClassicalRegister(2, name="observable_measurements"))
+ _append_measurement_register(qc, cog, inplace=True)
with self.subTest("In place"):
qcx = qc.copy()
assert _append_measurement_circuit(qcx, cog, inplace=True) is qcx
with self.subTest("Out of place"):
assert _append_measurement_circuit(qc, cog) is not qc
with self.subTest("Correct measurement circuit"):
- qc2.measure(0, 1)
+ qc2 = qc.copy()
+ qc2.measure(0, 0)
qc2.h(1)
- qc2.measure(1, 2)
+ qc2.measure(1, 1)
assert _append_measurement_circuit(qc, cog) == qc2
with self.subTest("Mismatch between qubit_locations and number of qubits"):
with pytest.raises(ValueError) as e_info:
@@ -179,6 +196,21 @@ def test_append_measurement_circuit(self):
e_info.value.args[0]
== "qubit_locations has 1 element(s) but the observable(s) have 2 qubit(s)."
)
+ with self.subTest("No observable_measurements register"):
+ with pytest.raises(ValueError) as e_info:
+ _append_measurement_circuit(QuantumCircuit(2), cog)
+ assert (
+ e_info.value.args[0]
+ == 'Cannot locate "observable_measurements" register'
+ )
+ with self.subTest("observable_measurements register has wrong size"):
+ cog2 = CommutingObservableGroup(Pauli("XI"), list(PauliList(["XI"])))
+ with pytest.raises(ValueError) as e_info:
+ _append_measurement_circuit(qc, cog2)
+ assert (
+ e_info.value.args[0]
+ == '"observable_measurements" register is the wrong size for the given commuting observable group (2 != 1)'
+ )
with self.subTest("Mismatched qubits, no qubit_locations provided"):
cog = CommutingObservableGroup(Pauli("X"), [Pauli("X")])
with pytest.raises(ValueError) as e_info:
diff --git a/test/cutting/test_cutting_reconstruction.py b/test/cutting/test_cutting_reconstruction.py
index 4bb4eab7a..3a3c4ca04 100644
--- a/test/cutting/test_cutting_reconstruction.py
+++ b/test/cutting/test_cutting_reconstruction.py
@@ -30,15 +30,6 @@
@ddt
class TestCuttingReconstruction(unittest.TestCase):
def setUp(self):
- qc = QuantumCircuit(2)
- qc.h(0)
- qc.cx(0, 1)
- self.qc0 = qc.copy()
- qc.add_register(ClassicalRegister(1, name="qpd_measurements"))
- self.qc1 = qc.copy()
- qc.add_register(ClassicalRegister(2, name="observable_measurements"))
- self.qc2 = qc
-
self.cog = CommutingObservableGroup(
Pauli("XZ"), list(PauliList(["IZ", "XI", "XZ"]))
)
@@ -48,11 +39,10 @@ def test_cutting_reconstruction(self):
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")
+ creg1 = ClassicalRegister(2, name="observable_measurements")
+ creg2 = ClassicalRegister(1, name="qpd_measurements")
subexperiments[0].add_register(creg1)
subexperiments[0].add_register(creg2)
observables = PauliList(["ZZ"])
@@ -62,7 +52,6 @@ def test_cutting_reconstruction(self):
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"])}
@@ -84,11 +73,10 @@ def test_cutting_reconstruction(self):
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")
+ creg1 = ClassicalRegister(2, name="observable_measurements")
+ creg2 = ClassicalRegister(1, name="qpd_measurements")
subexperiments[0].add_register(creg1)
subexperiments[0].add_register(creg2)
observables = PauliList(["iZZ"])
@@ -107,49 +95,23 @@ def test_cutting_reconstruction(self):
e_info.value.args[0]
== "An input observable has a phase not equal to 1."
)
- with self.subTest("Test num_qpd_bits"):
- results = SamplerResult(
- quasi_dists=[QuasiDistribution({"0": 1.0})], metadata=[{}]
- )
- results.metadata[0]["num_qpd_bits"] = 1.0
- 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(["ZZ"])
- with pytest.raises(TypeError) as e_info:
- reconstruct_expectation_values(results, weights, observables)
- assert (
- e_info.value.args[0]
- == "num_qpd_bits must be an integer, but a was passed."
- )
- results.metadata[0] = {}
- with pytest.raises(ValueError) as e_info:
- reconstruct_expectation_values(results, weights, observables)
- assert (
- e_info.value.args[0]
- == "The num_qpd_bits field must be set in each subexperiment result metadata dictionary."
- )
@data(
("000", [1, 1, 1]),
- ("001", [-1, -1, -1]),
- ("010", [-1, 1, -1]),
- ("011", [1, -1, 1]),
- ("100", [1, -1, -1]),
- ("101", [-1, 1, 1]),
- ("110", [-1, -1, 1]),
+ ("001", [-1, 1, -1]),
+ ("010", [1, -1, -1]),
+ ("011", [-1, -1, 1]),
+ ("100", [-1, -1, -1]),
+ ("101", [1, -1, 1]),
+ ("110", [-1, 1, 1]),
("111", [1, 1, -1]),
)
@unpack
def test_process_outcome(self, outcome, expected):
- num_qpd_bits = len(self.qc2.cregs[-2])
for o in (
outcome,
f"0b{outcome}",
int(f"0b{outcome}", 0),
hex(int(f"0b{outcome}", 0)),
):
- assert np.all(_process_outcome(num_qpd_bits, self.cog, o) == expected)
+ assert np.all(_process_outcome(self.cog, o) == expected)
diff --git a/test/cutting/test_cutting_roundtrip.py b/test/cutting/test_cutting_roundtrip.py
index 867ff6fdc..9a5273470 100644
--- a/test/cutting/test_cutting_roundtrip.py
+++ b/test/cutting/test_cutting_roundtrip.py
@@ -42,6 +42,7 @@
from qiskit.extensions import UnitaryGate
from qiskit.quantum_info import PauliList, random_unitary
from qiskit.primitives import Estimator
+from qiskit_aer.primitives import Sampler
from circuit_knitting.utils.simulation import ExactSampler
from circuit_knitting.cutting import (
@@ -94,13 +95,9 @@ def append_random_unitary(circuit: QuantumCircuit, qubits):
[Move(), Move()],
]
)
-def example_circuit(
- request,
-) -> tuple[QuantumCircuit, QuantumCircuit, list[list[int]]]:
+def example_circuit(request) -> QuantumCircuit:
"""Fixture for an example circuit.
- Returns both the original and one with QPDGates as a tuple.
-
Except for the parametrized gates, the system can be separated according to
the partition labels "AAB".
@@ -131,17 +128,8 @@ def example_circuit(
def test_cutting_exact_reconstruction(example_circuit):
- """Test gate-cut circuit vs original circuit on statevector simulator
-
- This test uses a statevector simulator to consider the expectation value of
- each of the :math:`2^N` different possible projection operators in the z
- basis at the end of the circuit (or in other words, the precise probability
- of each full-circuit measurement outcome in the limit of infinite shots).
- This test ensures that each such expectation value remains the same under
- the given QPD decomposed gates.
- """
- qc0 = example_circuit
- qc = qc0.copy()
+ """Test gate-cut circuit vs original circuit on statevector simulator"""
+ qc = example_circuit
observables = PauliList(["III", "IIY", "XII", "XYZ", "iZZZ", "-XZI"])
phases = np.array([(-1j) ** obs.phase for obs in observables])
@@ -149,7 +137,7 @@ def test_cutting_exact_reconstruction(example_circuit):
estimator = Estimator()
exact_expvals = (
- estimator.run([qc0] * len(observables), list(observables)).result().values
+ estimator.run([qc] * len(observables), list(observables)).result().values
)
subcircuits, bases, subobservables = partition_problem(
qc, "AAB", observables=observables_nophase
@@ -158,7 +146,7 @@ def test_cutting_exact_reconstruction(example_circuit):
subcircuits, subobservables, num_samples=np.inf
)
if np.random.randint(2):
- # Re-use a single sample
+ # Re-use a single sampler
sampler = ExactSampler()
samplers = {label: sampler for label in subcircuits.keys()}
else:
@@ -168,14 +156,66 @@ def test_cutting_exact_reconstruction(example_circuit):
label: sampler.run(subexperiments[label]).result()
for label, sampler in samplers.items()
}
- for label in results:
- for i, subexperiment in enumerate(subexperiments[label]):
- results[label].metadata[i]["num_qpd_bits"] = len(subexperiment.cregs[0])
- simulated_expvals = reconstruct_expectation_values(
+ reconstructed_expvals = reconstruct_expectation_values(
results, coefficients, subobservables
)
- simulated_expvals *= phases
+ reconstructed_expvals *= phases
+
+ logger.info("Max error: %f", np.max(np.abs(exact_expvals - reconstructed_expvals)))
+
+ assert np.allclose(exact_expvals, reconstructed_expvals, atol=1e-8)
+
+
+@pytest.mark.parametrize(
+ "sampler,is_exact_sampler", [(Sampler(), False), (ExactSampler(), True)]
+)
+def test_sampler_with_identity_subobservable(sampler, is_exact_sampler):
+ """This test ensures that the sampler works for a subcircuit with no observable measurements.
+
+ Specifically, that
+
+ - ``Sampler`` does not blow up (Issue #422); and
+ - ``ExactSampler`` returns correct results
+
+ This is related to https://github.com/Qiskit-Extensions/circuit-knitting-toolbox/issues/422.
+ """
+ # Create a circuit to cut
+ qc = QuantumCircuit(3)
+ append_random_unitary(qc, [0, 1])
+ append_random_unitary(qc, [2])
+ qc.rxx(np.pi / 3, 1, 2)
+ append_random_unitary(qc, [0, 1])
+ append_random_unitary(qc, [2])
+
+ # Determine expectation value using cutting
+ observables = PauliList(
+ ["IIZ"]
+ ) # Without the workaround to Issue #422, this observable causes a Sampler error.
+ subcircuits, bases, subobservables = partition_problem(
+ qc, "AAB", observables=observables
+ )
+ subexperiments, coefficients = generate_cutting_experiments(
+ subcircuits, subobservables, num_samples=np.inf
+ )
+ samplers = {label: sampler for label in subexperiments.keys()}
+ results = {
+ label: sampler.run(subexperiments[label]).result()
+ for label, sampler in samplers.items()
+ }
+ reconstructed_expvals = reconstruct_expectation_values(
+ results, coefficients, subobservables
+ )
+
+ if is_exact_sampler:
+ # Determine exact expectation values
+ estimator = Estimator()
+ exact_expvals = (
+ estimator.run([qc] * len(observables), list(observables)).result().values
+ )
- logger.info("Max error: %f", np.max(np.abs(exact_expvals - simulated_expvals)))
+ logger.info(
+ "Max error: %f", np.max(np.abs(exact_expvals - reconstructed_expvals))
+ )
- assert np.allclose(exact_expvals, simulated_expvals, atol=1e-8)
+ # Ensure both methods yielded equivalent expectation values
+ assert np.allclose(exact_expvals, reconstructed_expvals, atol=1e-8)
diff --git a/test/cutting/test_cutting_workflows.py b/test/cutting/test_cutting_workflows.py
index e743e8f00..7d3b1ed02 100644
--- a/test/cutting/test_cutting_workflows.py
+++ b/test/cutting/test_cutting_workflows.py
@@ -14,6 +14,7 @@
import pytest
from copy import deepcopy
+import numpy as np
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import EfficientSU2, CXGate
from qiskit.quantum_info import PauliList
@@ -23,10 +24,13 @@
from circuit_knitting.cutting.qpd.instructions import SingleQubitQPDGate
from circuit_knitting.cutting.qpd import QPDBasis
+from circuit_knitting.cutting.instructions import CutWire, Move
from circuit_knitting.cutting import (
partition_problem,
generate_cutting_experiments,
reconstruct_expectation_values,
+ cut_wires,
+ expand_observables,
)
@@ -90,16 +94,12 @@ def test_exotic_labels(label1, label2):
for label, sampler in samplers.items()
}
- for label in results:
- for i, subexperiment in enumerate(subexperiments[label]):
- results[label].metadata[i]["num_qpd_bits"] = len(subexperiment.cregs[0])
-
- simulated_expvals = reconstruct_expectation_values(
+ reconstructed_expvals = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
- assert len(simulated_expvals) == len(observables)
+ assert len(reconstructed_expvals) == len(observables)
def test_workflow_with_unused_qubits():
@@ -113,3 +113,69 @@ def test_workflow_with_unused_qubits():
subobservables,
num_samples=10,
)
+
+
+def test_wire_cut_workflow_without_reused_qubits():
+ """Test no resets in subexperiments when wire cut workflow has no re-used qubits."""
+ qc = QuantumCircuit(2)
+ qc.h(range(2))
+ qc.cx(0, 1)
+ qc.append(CutWire(), [0])
+ qc.cx(1, 0)
+
+ observables = PauliList(["IZ", "ZI", "ZZ", "XX"])
+
+ qc_1 = cut_wires(qc)
+ assert qc_1.num_qubits == 3
+
+ observables_1 = expand_observables(observables, qc, qc_1)
+
+ partitioned_problem = partition_problem(circuit=qc_1, observables=observables_1)
+
+ subexperiments, coefficients = generate_cutting_experiments(
+ circuits=partitioned_problem.subcircuits,
+ observables=partitioned_problem.subobservables,
+ num_samples=np.inf,
+ )
+
+ for subsystem_subexpts in subexperiments.values():
+ for subexpt in subsystem_subexpts:
+ assert "reset" not in subexpt.count_ops()
+
+
+def test_wire_cut_workflow_with_reused_qubits():
+ """Test at most a single reset in subexperiments when wire cut workflow has a single re-used qubit."""
+ qc = QuantumCircuit(8)
+ for i in [*range(4), *range(5, 8)]:
+ qc.rx(np.pi / 4, i)
+ qc.cx(0, 3)
+ qc.cx(1, 3)
+ qc.cx(2, 3)
+ qc.append(Move(), [3, 4])
+ qc.cx(4, 5)
+ qc.cx(4, 6)
+ qc.cx(4, 7)
+ qc.append(Move(), [4, 3])
+ qc.cx(0, 3)
+ qc.cx(1, 3)
+ qc.cx(2, 3)
+
+ observables = PauliList(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
+
+ partitioned_problem = partition_problem(
+ circuit=qc, partition_labels="AAAABBBB", observables=observables
+ )
+
+ subexperiments, coefficients = generate_cutting_experiments(
+ circuits=partitioned_problem.subcircuits,
+ observables=partitioned_problem.subobservables,
+ num_samples=np.inf,
+ )
+
+ # The initial circuit had a single instance of qubit re-use with a Move
+ # instruction. Each A subexperiment should have a single reset, and each B
+ # subexperiment should be free of resets.
+ for subexpt in subexperiments["A"]:
+ assert subexpt.count_ops()["reset"] == 1
+ for subexpt in subexperiments["B"]:
+ assert "reset" not in subexpt.count_ops()
diff --git a/test/forging/test_entanglement_forging_ground_state_solver.py b/test/forging/test_entanglement_forging_ground_state_solver.py
index a9b6f3eed..d6d036d19 100644
--- a/test/forging/test_entanglement_forging_ground_state_solver.py
+++ b/test/forging/test_entanglement_forging_ground_state_solver.py
@@ -17,10 +17,9 @@
import pytest
import numpy as np
-from qiskit.algorithms.optimizers import SPSA
+from qiskit_algorithms.optimizers import SPSA, COBYLA
from qiskit.circuit import QuantumCircuit, Parameter
from qiskit.circuit.library import TwoLocal
-from qiskit.algorithms.optimizers import COBYLA
from qiskit_nature.units import DistanceUnit
from qiskit_nature.second_q.drivers import PySCFDriver
from qiskit_nature.second_q.problems import (
diff --git a/test/utils/test_transforms.py b/test/utils/test_transforms.py
index f6e65aac8..9783d880e 100644
--- a/test/utils/test_transforms.py
+++ b/test/utils/test_transforms.py
@@ -386,7 +386,7 @@ def test_separate_circuit(self):
with self.subTest("frozenset for each partition label"):
circuit = QuantumCircuit(4)
circuit.x(0)
- circuit.cnot(1, 2)
+ circuit.cx(1, 2)
circuit.h(3)
partition_labels = [
frozenset([0]),
@@ -401,7 +401,7 @@ def test_separate_circuit(self):
compare[frozenset([0])] = QuantumCircuit(1)
compare[frozenset([0])].x(0)
compare[frozenset([1])] = QuantumCircuit(2)
- compare[frozenset([1])].cnot(0, 1)
+ compare[frozenset([1])].cx(0, 1)
compare[frozenset([2])] = QuantumCircuit(1)
compare[frozenset([2])].h(0)
assert separated_circuits.subcircuits.keys() == compare.keys()
diff --git a/test/utils/test_transpiler_passes.py b/test/utils/test_transpiler_passes.py
new file mode 100644
index 000000000..58872c7cd
--- /dev/null
+++ b/test/utils/test_transpiler_passes.py
@@ -0,0 +1,132 @@
+# 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.
+
+"""Tests for CKT transpilation passes."""
+
+import unittest
+
+from qiskit import QuantumRegister, QuantumCircuit
+from qiskit.transpiler import PassManager
+from qiskit.transpiler.passes import DAGFixedPoint
+from qiskit.converters import circuit_to_dag
+
+from circuit_knitting.utils.transpiler_passes import RemoveFinalReset, ConsolidateResets
+
+
+class TestRemoveFinalReset(unittest.TestCase):
+ """Test remove-reset-in-zero-state optimizations."""
+
+ def test_optimize_single_reset(self):
+ """Remove a single final reset
+ qr0:--[H]--|0>-- ==> qr0:--[H]--
+ """
+ qr = QuantumRegister(1, "qr")
+ circuit = QuantumCircuit(qr)
+ circuit.h(0)
+ circuit.reset(qr)
+ dag = circuit_to_dag(circuit)
+
+ expected = QuantumCircuit(qr)
+ expected.h(0)
+
+ pass_ = RemoveFinalReset()
+ after = pass_.run(dag)
+
+ self.assertEqual(circuit_to_dag(expected), after)
+
+ def test_dont_optimize_non_final_reset(self):
+ """Do not remove reset if not final instruction
+ qr0:--|0>--[H]-- ==> qr0:--|0>--[H]--
+ """
+ qr = QuantumRegister(1, "qr")
+ circuit = QuantumCircuit(qr)
+ circuit.reset(qr)
+ circuit.h(qr)
+ dag = circuit_to_dag(circuit)
+
+ expected = QuantumCircuit(qr)
+ expected.reset(qr)
+ expected.h(qr)
+
+ pass_ = RemoveFinalReset()
+ after = pass_.run(dag)
+
+ self.assertEqual(circuit_to_dag(expected), after)
+
+ def test_optimize_single_reset_in_diff_qubits(self):
+ """Remove a single final reset in different qubits
+ qr0:--[H]--|0>-- qr0:--[H]--
+ ==>
+ qr1:--[X]--|0>-- qr1:--[X]----
+ """
+ qr = QuantumRegister(2, "qr")
+ circuit = QuantumCircuit(qr)
+ circuit.h(0)
+ circuit.x(1)
+ circuit.reset(qr)
+ dag = circuit_to_dag(circuit)
+
+ expected = QuantumCircuit(qr)
+ expected.h(0)
+ expected.x(1)
+
+ pass_ = RemoveFinalReset()
+ after = pass_.run(dag)
+
+ self.assertEqual(circuit_to_dag(expected), after)
+
+
+class TestRemoveFinalResetFixedPoint(unittest.TestCase):
+ """Test RemoveFinalReset in a transpiler, using fixed point."""
+
+ def test_two_resets(self):
+ """Remove two final resets
+ qr0:--[H]-|0>-|0>-- ==> qr0:--[H]--
+ """
+ qr = QuantumRegister(1, "qr")
+ circuit = QuantumCircuit(qr)
+ circuit.h(qr[0])
+ circuit.reset(qr[0])
+ circuit.reset(qr[0])
+
+ expected = QuantumCircuit(qr)
+ expected.h(qr[0])
+
+ pass_manager = PassManager()
+ pass_manager.append(
+ [RemoveFinalReset(), DAGFixedPoint()],
+ do_while=lambda property_set: not property_set["dag_fixed_point"],
+ )
+ after = pass_manager.run(circuit)
+
+ self.assertEqual(expected, after)
+
+
+class TestConsolidateResets(unittest.TestCase):
+ """Test consolidate-resets optimization."""
+
+ def test_consolidate_double_reset(self):
+ """Consolidate a pair of resets.
+ qr0:--|0>--|0>-- ==> qr0:--|0>--
+ """
+ qr = QuantumRegister(1, "qr")
+ circuit = QuantumCircuit(qr)
+ circuit.reset(qr)
+ circuit.reset(qr)
+ dag = circuit_to_dag(circuit)
+
+ expected = QuantumCircuit(qr)
+ expected.reset(qr)
+
+ pass_ = ConsolidateResets()
+ after = pass_.run(dag)
+
+ self.assertEqual(circuit_to_dag(expected), after)
diff --git a/tox.ini b/tox.ini
index 6a2b05d6b..60b69ac3f 100644
--- a/tox.ini
+++ b/tox.ini
@@ -14,25 +14,25 @@ commands =
extras =
style
commands =
- ruff check --fix circuit_knitting/ circuit_knitting_toolbox/ docs/ test/ tools/
+ ruff check --fix circuit_knitting/ docs/ test/ tools/
nbqa ruff --fix docs/
- autoflake --in-place --recursive circuit_knitting/ circuit_knitting_toolbox/ docs/ test/ tools/
- black circuit_knitting/ circuit_knitting_toolbox/ docs/ test/ tools/
+ autoflake --in-place --recursive circuit_knitting/ docs/ test/ tools/
+ black circuit_knitting/ docs/ test/ tools/
[testenv:lint]
basepython = python3.10
extras =
lint
commands =
- ruff check circuit_knitting/ circuit_knitting_toolbox/ docs/ test/ tools/
+ ruff check circuit_knitting/ docs/ test/ tools/
nbqa ruff docs/
- autoflake --check --quiet --recursive circuit_knitting/ circuit_knitting_toolbox/ docs/ test/ tools/
- black --check circuit_knitting/ circuit_knitting_toolbox/ docs/ test/ tools/
- pydocstyle circuit_knitting/ circuit_knitting_toolbox/
- mypy circuit_knitting/ circuit_knitting_toolbox/
+ autoflake --check --quiet --recursive circuit_knitting/ docs/ test/ tools/
+ black --check circuit_knitting/ docs/ test/ tools/
+ pydocstyle circuit_knitting/
+ mypy circuit_knitting/
reno lint
- pylint -rn --py-version=3.8 --disable=all --enable=reimported,no-self-use,no-else-raise,redefined-argument-from-local,redefined-builtin,raise-missing-from,cyclic-import circuit_knitting/ test/ tools/
- nbqa pylint -rn --py-version=3.8 --disable=all --enable=reimported,no-self-use,no-else-raise,redefined-argument-from-local,redefined-builtin,raise-missing-from,cyclic-import docs/
+ pylint -rn --py-version=3.8 --disable=all --enable=reimported,no-self-use,no-else-raise,redefined-argument-from-local,redefined-builtin,raise-missing-from,cyclic-import,unused-argument circuit_knitting/ test/ tools/
+ nbqa pylint -rn --py-version=3.8 --disable=all --enable=reimported,no-self-use,no-else-raise,redefined-argument-from-local,redefined-builtin,raise-missing-from,cyclic-import,unused-argument docs/
[testenv:{,py-,py3-,py38-,py39-,py310-,py311-}notebook]
extras =