Zum Hauptinhalt springen
Dieses Projekt befindet sich in der Beta-Phase. Einen Fehler gefunden? GitHub Issue erstellen. Frage oder Idee? In Discussions fragen — wir freuen uns über dein Feedback!
Wie ist die Übersetzungsqualität auf dieser Seite?

Wire Cutting als Zwei-Qubit-`Move`-Anweisung formuliert

In diesem Tutorial werden wir Erwartungswerte eines Sieben-Qubit-Circuits rekonstruieren, indem wir ihn mithilfe von Wire Cutting in zwei Vier-Qubit-Circuits aufteilen.

Dies sind die Schritte, die wir in diesem Qiskit Pattern durchführen werden:

  • Schritt 1: Problem auf Quantencircuits und Operatoren abbilden:

    • Den Hamiltonian auf einen Quantencircuit abbilden.
  • Schritt 2: Für die Zielhardware optimieren [Verwendet das Cutting-Addon]:

    • Den Circuit und das Observable schneiden.
    • Die Teilexperimente für die Hardware transpilieren.

  • Schritt 3: Auf der Zielhardware ausführen:

    • Die in Schritt 2 erhaltenen Teilexperimente mit einem Sampler-Primitiv ausführen.
  • Schritt 4: Ergebnisse nachverarbeiten [Verwendet das Cutting-Addon]:

    • Die Ergebnisse aus Schritt 3 kombinieren, um den Erwartungswert des betreffenden Observables zu rekonstruieren.

Schritt 1: Abbilden​

Einen Circuit zum Schneiden erstellen​

Zunächst beginnen wir mit einem Circuit, der von Abb. 1(a) aus arXiv:2302.03366v1 inspiriert ist.

# Added by doQumentation — installs the packages this notebook needs if they are missing
import importlib.util

_needed = {"numpy": "numpy", "qiskit": "qiskit", "qiskit_addon_cutting": "qiskit-addon-cutting", "qiskit_aer": "qiskit-aer", "qiskit_ibm_runtime": "qiskit-ibm-runtime"}
_missing = [pip for module, pip in _needed.items()
if importlib.util.find_spec(module) is None]
# One at a time, so a package that fails to install does not block the others
for _pip in _missing:
%pip install -q {_pip}
if not _missing:
print("\u2713 All required packages are installed")
import numpy as np
from qiskit import QuantumCircuit

qc_0 = QuantumCircuit(7)
for i in range(7):
qc_0.rx(np.pi / 4, i)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
qc_0.cx(3, 4)
qc_0.cx(3, 5)
qc_0.cx(3, 6)
qc_0.cx(0, 3)
qc_0.cx(1, 3)
qc_0.cx(2, 3)
<qiskit.circuit.instructionset.InstructionSet at 0x7f16ab191a80>
qc_0.draw("mpl")

Quantum circuit diagram

Ein Observable angeben​

from qiskit.quantum_info import SparsePauliOp

observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])

Schritt 2: Optimieren​

Einen neuen Circuit erstellen, bei dem Move-Anweisungen an den gewünschten Schnittpunkten platziert wurden​

Für den obigen Circuit möchten wir zwei Wire Cuts auf der mittleren Qubit-Leitung platzieren, sodass sich der Circuit in zwei Circuits mit je vier Qubits aufteilen lässt. Eine Möglichkeit dazu besteht darin, manuell Zwei-Qubit-Move-Anweisungen zu platzieren, die den Zustand von einer Qubit-Leitung auf eine andere übertragen. Eine Move-Anweisung ist konzeptionell gleichwertig mit einer Reset-Operation auf dem zweiten Qubit, gefolgt von einem SWAP-Gate. Die Wirkung dieser Anweisung besteht darin, den Zustand des ersten (Quell-)Qubits auf das zweite (Ziel-)Qubit zu übertragen und dabei den eingehenden Zustand des zweiten Qubits zu verwerfen. Damit dies wie beabsichtigt funktioniert, ist es wichtig, dass das zweite (Ziel-)Qubit keine Verschränkung mit dem Rest des Systems aufweist; andernfalls führt die Reset-Operation dazu, dass der Zustand des restlichen Systems teilweise kollabiert.

Hier erstellen wir einen neuen Circuit mit einem zusätzlichen Qubit und den Move-Operationen an den entsprechenden Stellen. In diesem Beispiel können wir ein Qubit wiederverwenden: Das Quell-Qubit des ersten Move wird zum Ziel-Qubit des zweiten Move-Vorgangs.

Hinweis: Als Alternative zum direkten Arbeiten mit Move-Anweisungen kann man Wire Cuts auch mit einer Ein-Qubit-CutWire-Anweisung markieren. Die Funktion cut_wires dient dazu, CutWires in Move-Anweisungen auf neu allokierten Qubits umzuwandeln. Im Gegensatz zur manuellen Methode erlaubt diese automatische Methode jedoch keine Wiederverwendung von Qubit-Leitungen. Weitere Details findest du im CutWire-Anleitungsartikel.

from qiskit_addon_cutting.instructions import Move

qc_1 = QuantumCircuit(8)
for i in [*range(4), *range(5, 8)]:
qc_1.rx(np.pi / 4, i)
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.append(Move(), [3, 4])
qc_1.cx(4, 5)
qc_1.cx(4, 6)
qc_1.cx(4, 7)
qc_1.append(Move(), [4, 3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)

qc_1.draw("mpl")

Quantum circuit diagram

Ein zum neuen Circuit passendes Observable erstellen​

Dieses Observable entspricht observable, aber wir müssen die zusätzliche Qubit-Leitung, die hinzugefügt wurde, korrekt berücksichtigen (d. h. wir fügen an Index 4 ein „I" ein). Beachte, dass in Qiskit das Qubit-0 in der Zeichenkettendarstellung dem am weitesten rechts stehenden Pauli-Zeichen entspricht.

observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])

Den Circuit und die Observables aufteilen​

Wie in den vorherigen Tutorials werden Qubits mit einem gemeinsamen Partitionslabel zusammengefasst, und nicht-lokale Gates, die mehr als eine Partition überspannen, werden geschnitten.

from qiskit_addon_cutting import partition_problem

partitioned_problem = partition_problem(
circuit=qc_1, partition_labels="AAAABBBB", observables=observable_expanded.paulis
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases

Das zerlegte Problem visualisieren​

subobservables
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']),
'B': PauliList(['ZIII', 'IIII', 'IIII'])}
subcircuits["A"].draw("mpl")

Quantum circuit diagram

subcircuits["B"].draw("mpl")

Quantum circuit diagram

Den Sampling-Overhead für die gewählten Schnitte berechnen​

Hier schneiden wir zwei Leitungen, was einen Sampling-Overhead von 444^4 ergibt.

Weitere Informationen zum Sampling-Overhead durch Circuit Cutting findest du im Erklärungsmaterial.

print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
Sampling overhead: 256.0

Die Teilexperimente für das Backend generieren​

generate_cutting_experiments akzeptiert circuits/observables-Argumente als Dictionaries, die Qubit-Partitionslabels den jeweiligen subcircuit/subobservables zuordnen.

Um den Erwartungswert des vollständigen Circuits zu simulieren, werden viele Teilexperimente aus der gemeinsamen Quasiwahrscheinlichkeitsverteilung der zerlegten Gates generiert und dann auf einem oder mehreren Backends ausgeführt. Die Anzahl der aus der Verteilung entnommenen Stichproben wird durch num_samples gesteuert, und für jede eindeutige Stichprobe wird ein kombinierter Koeffizient angegeben. Weitere Informationen zur Berechnung der Koeffizienten findest du im Erklärungsmaterial.

from qiskit_addon_cutting import generate_cutting_experiments

subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)

Ein Backend auswählen​

Hier verwenden wir ein Fake-Backend, wodurch Qiskit Runtime im lokalen Modus ausgeführt wird (d. h. auf einem lokalen Simulator).

from qiskit_ibm_runtime.fake_provider import FakeManilaV2

backend = FakeManilaV2()

Die Teilexperimente für das Backend vorbereiten​

Wir müssen die Circuits mit unserem Backend als Ziel transpilieren, bevor wir sie an Qiskit Runtime übermitteln.

from qiskit.transpiler import generate_preset_pass_manager

# Transpile the subexperiments to ISA circuits
pass_manager = generate_preset_pass_manager(optimization_level=1, backend=backend)
isa_subexperiments = {
label: pass_manager.run(partition_subexpts)
for label, partition_subexpts in subexperiments.items()
}

Schritt 3: Ausführen​

Die Teilexperimente mit dem Qiskit Runtime Sampler-Primitiv ausführen​

from qiskit_ibm_runtime import SamplerV2, Batch

# Submit each partition's subexperiments to the Qiskit Runtime Sampler
# primitive, in a single batch so that the jobs will run back-to-back.
with Batch(backend=backend) as batch:
sampler = SamplerV2(mode=batch)
jobs = {
label: sampler.run(subsystem_subexpts, shots=2**12)
for label, subsystem_subexpts in isa_subexperiments.items()
}
/home/garrison/Qiskit/qiskit-ibm-runtime/qiskit_ibm_runtime/session.py:157: UserWarning: Session is not supported in local testing mode or when using a simulator.
warnings.warn(
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}

Schritt 4: Nachverarbeitung​

Den Erwartungswert rekonstruieren​

Erwartungswerte für jeden Observable-Term rekonstruieren und kombinieren, um den Erwartungswert des ursprünglichen Observables zu rekonstruieren.

from qiskit_addon_cutting import reconstruct_expectation_values

reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)

Den rekonstruierten Erwartungswert mit dem exakten Erwartungswert aus dem ursprünglichen Circuit und Observable vergleichen​

from qiskit_aer.primitives import EstimatorV2

estimator = EstimatorV2()
exact_expval = estimator.run([(qc_0, observable)]).result()[0].data.evs
print(f"Reconstructed expectation value: {np.real(np.round(reconstructed_expval, 8))}")
print(f"Exact expectation value: {np.round(exact_expval, 8)}")
print(f"Error in estimation: {np.real(np.round(reconstructed_expval-exact_expval, 8))}")
print(
f"Relative error in estimation: {np.real(np.round((reconstructed_expval-exact_expval) / exact_expval, 8))}"
)
Reconstructed expectation value: 1.51319069
Exact expectation value: 1.59099026
Error in estimation: -0.07779957
Relative error in estimation: -0.04890009