Inizia con il circuit cutting tramite wire cut
Versioni dei pacchetti
Il codice in questa pagina è stato sviluppato con i seguenti requisiti. Si consiglia di usare queste versioni o versioni più recenti.
qiskit[all]~=2.3.0
qiskit-ibm-runtime~=0.43.1
qiskit-aer~=0.17
qiskit-addon-cutting~=0.10.0
Questa guida mostra un esempio pratico di wire cut con il pacchetto qiskit-addon-cutting. Illustra come ricostruire i valori di aspettativa di un circuito a sette qubit tramite wire cutting.
Un wire cut è rappresentato in questo pacchetto come un'istruzione a due qubit Move, definita come un reset del secondo qubit su cui agisce l'istruzione, seguito da uno swap di entrambi i qubit. Questa operazione equivale a trasferire lo stato del primo qubit nel secondo qubit, scartando simultaneamente lo stato in ingresso del secondo qubit.
Il pacchetto è progettato per essere coerente con il modo in cui devi trattare i wire cut quando operi su qubit fisici. Ad esempio, un wire cut può prendere lo stato del qubit fisico e continuarlo come qubit fisico dopo il taglio. Puoi pensare all'"instruction cutting" come a un framework unificato per considerare sia i wire cut sia i gate cut all'interno dello stesso formalismo (poiché un wire cut è semplicemente un'istruzione Move tagliata). Usare questo framework per il wire cutting consente inoltre il riutilizzo dei qubit, illustrato nella sezione su come tagliare i wire manualmente.
L'istruzione a singolo qubit CutWire offre un'interfaccia più astratta e semplice per lavorare con i wire cut. Ti permette di indicare ad alto livello dove nel circuito deve essere effettuato il taglio, lasciando che l'addon per il circuit cutting inserisca le opportune istruzioni Move al posto tuo.
L'esempio seguente mostra la ricostruzione del valore di aspettativa dopo un wire cutting. Creerai un circuito con diverse porte non locali e definirai le osservabili da stimare.
# 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
from qiskit.transpiler import generate_preset_pass_manager
from qiskit.quantum_info import SparsePauliOp
from qiskit_ibm_runtime.fake_provider import FakeManilaV2
from qiskit_ibm_runtime import SamplerV2, Batch
from qiskit_aer.primitives import EstimatorV2
from qiskit_addon_cutting.instructions import Move, CutWire
from qiskit_addon_cutting import (
partition_problem,
generate_cutting_experiments,
cut_wires,
expand_observables,
reconstruct_expectation_values,
)
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)
# Define observable
observable = SparsePauliOp(["ZIIIIII", "IIIZIII", "IIIIIIZ"])
# Draw circuit
qc_0.draw("mpl")
Taglia i wire con l'istruzione ad alto livello CutWire​
Successivamente, effettua i wire cut usando l'istruzione a singolo qubit CutWire sul qubit . Una volta che i sottoesperimenti sono pronti per l'esecuzione, usa la funzione cut_wires() per trasformare le istruzioni CutWire in istruzioni Move su qubit appena allocati.
qc_1 = QuantumCircuit(7)
for i in range(7):
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(CutWire(), [3])
qc_1.cx(3, 4)
qc_1.cx(3, 5)
qc_1.cx(3, 6)
qc_1.append(CutWire(), [3])
qc_1.cx(0, 3)
qc_1.cx(1, 3)
qc_1.cx(2, 3)
qc_1.draw("mpl")
Quando un circuito viene espanso tramite uno o più wire cut, l'osservabile deve essere aggiornata per tenere conto dei qubit aggiuntivi introdotti. Il pacchetto qiskit-addon-cutting mette a disposizione la funzione di utilità expand_observables(), che accetta oggetti PauliList e i circuiti originale ed espanso come argomenti, e restituisce una nuova PauliList.
La PauliList restituita non conterrà alcuna informazione sui coefficienti dell'osservabile originale, ma questi possono essere ignorati fino alla ricostruzione del valore di aspettativa finale.
# Transform CutWire instructions to Move instructions
qc_2 = cut_wires(qc_1)
# Expand the observable to match the new circuit size
expanded_observable = expand_observables(observable.paulis, qc_0, qc_2)
print(f"Expanded Observable: {expanded_observable}")
qc_2.draw("mpl")
Expanded Observable: ['ZIIIIIIII', 'IIIZIIIII', 'IIIIIIIIZ']
Partiziona il circuito e l'osservabile​
Ora il problema può essere suddiviso in partizioni. Ciò avviene tramite la funzione partition_problem(), con un insieme opzionale di etichette di partizione per specificare come separare il circuito. I qubit che condividono un'etichetta di partizione comune vengono raggruppati insieme, e qualsiasi porta non locale che attraversa più di una partizione viene tagliata.
Se non vengono fornite etichette di partizione, il partizionamento verrà determinato automaticamente in base alla connettività del circuito. Leggi la sezione successiva su come tagliare i wire manualmente per ulteriori informazioni sull'uso delle etichette di partizione.
partitioned_problem = partition_problem(
circuit=qc_2,
observables=expanded_observable,
)
subcircuits = partitioned_problem.subcircuits
subobservables = partitioned_problem.subobservables
bases = partitioned_problem.bases
print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits[0].draw("mpl")
Subobservables to measure:
{0: PauliList(['IIIII', 'ZIIII', 'IIIIZ']), 1: PauliList(['ZIII', 'IIII', 'IIII'])}
Sampling overhead: 256.0
subcircuits[1].draw("mpl")
Con questo schema di partizionamento, hai tagliato due wire, con un overhead di campionamento di .
Genera i sottoesperimenti da eseguire e post-elabora i risultati​
Per stimare il valore di aspettativa del circuito completo, vengono generati diversi sottoesperimenti dalla distribuzione di quasi-probabilità congiunta delle porte decomposte, che vengono poi eseguiti su uno o più QPU. Il metodo generate_cutting_experiments esegue questa operazione accettando come argomenti i dizionari subcircuits e subobservables creati in precedenza, nonché il numero di campioni da estrarre dalla distribuzione.
L'argomento num_samples specifica quanti campioni estrarre dalla distribuzione di quasi-probabilità e determina la precisione dei coefficienti usati per la ricostruzione. Passare infinito (np.inf) garantisce che tutti i coefficienti vengano calcolati in modo esatto. Leggi la documentazione API su come generare i pesi e su come generare gli esperimenti di cutting per ulteriori informazioni.
# Generate subexperiments
subexperiments, coefficients = generate_cutting_experiments(
circuits=subcircuits, observables=subobservables, num_samples=np.inf
)
# Set a backend to use and transpile the subexperiments
backend = FakeManilaV2()
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()
}
# 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()
}
# Retrieve results
results = {label: job.result() for label, job in jobs.items()}
Infine, il valore di aspettativa del circuito completo può essere ricostruito usando il metodo reconstruct_expectation_values().
Il blocco di codice seguente ricostruisce i risultati e li confronta con il valore di aspettativa esatto.
reconstructed_expval_terms = reconstruct_expectation_values(
results,
coefficients,
subobservables,
)
# Apply the coefficients of the original observable
reconstructed_expval = np.dot(reconstructed_expval_terms, observable.coeffs)
# Compute the exact expectation value using the `qiskit_aer` package.
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.45965266
Exact expectation value: 1.59099026
Error in estimation: -0.1313376
Relative error in estimation: -0.08255085
Per ricostruire con precisione il valore di aspettativa, i coefficienti dell'osservabile originale (che sono diversi dall'output di generate_cutting_experiments()) devono essere applicati all'output della ricostruzione, poiché questa informazione viene persa quando vengono generati gli esperimenti di cutting o quando l'osservabile viene espansa.
In genere questi coefficienti possono essere applicati tramite numpy.dot(), come mostrato in precedenza.
Taglia i wire con l'istruzione a basso livello Move​
Un limite dell'uso dell'istruzione di livello superiore CutWire è che non consente il riutilizzo dei qubit. Se lo desideri per un esperimento di cutting, puoi invece posizionare manualmente le istruzioni Move. Tuttavia, poiché l'istruzione Move scarta lo stato del qubit di destinazione, è importante che questo qubit non condivida alcun entanglement con il resto del sistema. In caso contrario, l'operazione di reset causerà un collasso parziale dello stato del circuito dopo il wire cut.
Il blocco di codice seguente esegue un wire cut sul qubit per lo stesso circuito di esempio mostrato in precedenza. La differenza qui è che puoi riutilizzare un qubit invertendo l'operazione Move nel punto in cui è stato effettuato il secondo wire cut (tuttavia, ciò non è sempre possibile e dipende dal circuito che si sta tagliando).
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)
# Expand observable
observable_expanded = SparsePauliOp(["ZIIIIIII", "IIIIZIII", "IIIIIIIZ"])
qc_1.draw("mpl")
Il circuito qui sopra può ora essere partizionato e gli esperimenti di cutting generati. Per specificare esplicitamente come il circuito deve essere partizionato, puoi aggiungere etichette di partizione alla funzione partition_problem(). I qubit che condividono un'etichetta di partizione comune vengono raggruppati insieme, e qualsiasi porta non locale che attraversa più di una partizione viene tagliata. Le chiavi del dizionario restituito da partition_problem() corrisponderanno a quelle specificate nella stringa di etichette.
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
print(f"Subobservables to measure: \n{subobservables}\n")
print(f"Sampling overhead: {np.prod([basis.overhead for basis in bases])}")
subcircuits["A"].draw("mpl")
Subobservables to measure:
{'A': PauliList(['IIII', 'ZIII', 'IIIZ']), 'B': PauliList(['ZIII', 'IIII', 'IIII'])}
Sampling overhead: 256.0
subcircuits["B"].draw("mpl")
Ora gli esperimenti di cutting possono essere generati e il valore di aspettativa ricostruito nello stesso modo illustrato nella sezione precedente.
Passi successivi​
- Leggi la guida Inizia con il circuit cutting tramite gate cut.
- Leggi il paper su arXiv sul wire cutting ottimale per comprendere meglio l'equivalenza tra wire cutting e gate cutting.