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-Muster 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.
- Die in Schritt 2 erhaltenen Teilexperimente mit einem
- 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 — required packages for this notebook
!pip install -q numpy qiskit qiskit-addon-cutting qiskit-aer qiskit-ibm-runtime
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")

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 Schnittstellen 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 einzel-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")

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

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

Den Sampling-Overhead für die gewählten Schnitte berechnen
Hier schneiden wir zwei Leitungen, was einen Sampling-Overhead von 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