Aller au contenu principal
Ce projet est en phase bêta. Tu as trouvé un bug ? Ouvrir un ticket GitHub. Une question ou une idée ? Pose ta question dans Discussions — tes retours sont les bienvenus !
Quelle est la qualité de la traduction sur cette page ?

La coupure de fil formulée comme une instruction `Move` à deux qubits

Dans ce tutoriel, nous allons reconstruire les valeurs d'espérance d'un circuit à sept qubits en le divisant en deux circuits à quatre qubits chacun à l'aide de la coupure de fil.

Voici les étapes que nous allons suivre dans ce modèle Qiskit :

  • Étape 1 : Mapper le problème sur des circuits et des opérateurs quantiques :

    • Mapper le hamiltonien sur un circuit quantique.
  • Étape 2 : Optimiser pour le matériel cible [Utilise l'addon de coupure] :

    • Couper le circuit et l'observable.
    • Transpiler les sous-expériences pour le matériel.

  • Étape 3 : Exécuter sur le matériel cible :

    • Lancer les sous-expériences obtenues à l'Étape 2 en utilisant la primitive Sampler.
  • Étape 4 : Post-traiter les résultats [Utilise l'addon de coupure] :

    • Combiner les résultats de l'Étape 3 pour reconstruire la valeur d'espérance de l'observable en question.

Étape 1 : Mapper​

Créer un circuit à couper​

Pour commencer, nous partons d'un circuit inspiré de la Fig. 1(a) de arXiv:2302.03366v1.

# 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

Spécifier un observable​

from qiskit.quantum_info import SparsePauliOp

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

Étape 2 : Optimiser​

Créer un nouveau circuit dans lequel des instructions Move ont été placées aux emplacements de coupure souhaités​

Étant donné le circuit ci-dessus, nous souhaitons placer deux coupures de fil sur la ligne du qubit central, afin que le circuit puisse se séparer en deux circuits de quatre qubits chacun. Pour cela, on peut placer manuellement des instructions Move à deux qubits qui transfèrent l'état d'un fil de qubit à un autre. Une instruction Move est conceptuellement équivalente à une opération de réinitialisation sur le second qubit, suivie d'une porte SWAP. L'effet de cette instruction est de transférer l'état du premier qubit (source) vers le second qubit (destination), tout en éliminant l'état entrant du second qubit. Pour que cela fonctionne comme prévu, il est important que le second qubit (destination) ne partage aucune intrication avec le reste du système ; sinon, l'opération de réinitialisation entraînera un effondrement partiel de l'état du reste du système.

Ici, nous construisons un nouveau circuit avec un qubit supplémentaire et les opérations Move en place. Dans cet exemple, nous pouvons réutiliser un qubit : le qubit source du premier Move devient le qubit destination du second Move.

Remarque : plutôt que de manipuler directement des instructions Move, on peut marquer les coupures de fil à l'aide d'une instruction CutWire à un seul qubit. La fonction cut_wires existe pour transformer les CutWires en instructions Move sur des qubits nouvellement alloués. Cependant, contrairement à la méthode manuelle, cette méthode automatique ne permet pas la réutilisation des fils de qubit. Consulte le guide pratique sur CutWire pour plus de détails.

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

Créer un observable correspondant au nouveau circuit​

Cet observable correspond à observable, mais nous devons correctement tenir compte du fil de qubit supplémentaire qui a été ajouté (c'est-à-dire que nous insérons un "I" à l'index 4). Remarque : dans Qiskit, la représentation en chaîne du qubit-0 correspond au caractère de Pauli le plus à droite.

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

Séparer le circuit et les observables​

Comme dans les tutoriels précédents, les qubits partageant une même étiquette de partition seront regroupés, et les portes non locales couvrant plus d'une partition seront coupées.

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

Visualiser le problème décomposé​

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

Calculer le surcoût d'échantillonnage pour les coupures choisies​

Ici, nous coupons deux fils, ce qui entraîne un surcoût d'échantillonnage de 444^4.

Pour plus d'informations sur le surcoût d'échantillonnage engendré par la coupure de circuit, consulte la documentation explicative.

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

Générer les sous-expériences à exécuter sur le Backend​

generate_cutting_experiments accepte les arguments circuits/observables sous forme de dictionnaires associant les étiquettes de partition des qubits aux subcircuit/subobservables respectifs.

Pour simuler la valeur d'espérance du circuit complet, de nombreuses sous-expériences sont générées à partir de la distribution de quasi-probabilité conjointe des portes décomposées, puis exécutées sur un ou plusieurs Backends. Le nombre d'échantillons prélevés dans la distribution est contrôlé par num_samples, et un coefficient combiné est donné pour chaque échantillon unique. Pour plus d'informations sur la façon dont les coefficients sont calculés, consulte le matériel explicatif.

from qiskit_addon_cutting import generate_cutting_experiments

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

Choisir un Backend​

Ici, nous utilisons un faux Backend, ce qui entraînera l'exécution de Qiskit Runtime en mode local (c'est-à-dire sur un simulateur local).

from qiskit_ibm_runtime.fake_provider import FakeManilaV2

backend = FakeManilaV2()

Préparer les sous-expériences pour le Backend​

Nous devons Transpiler les circuits avec notre Backend comme cible avant de les soumettre à Qiskit Runtime.

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()
}

Étape 3 : Exécuter​

Lancer les sous-expériences en utilisant la primitive Sampler de Qiskit Runtime​

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()}

Étape 4 : Post-traitement​

Reconstruire la valeur d'espérance​

Reconstruire les valeurs d'espérance pour chaque terme de l'observable et les combiner pour reconstruire la valeur d'espérance de l'observable d'origine.

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)

Comparer la valeur d'espérance reconstruite avec la valeur d'espérance exacte du circuit et de l'observable d'origine​

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