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 =