Aller au contenu principal

Expérience à l'échelle de l'utilité III

remarque

Toshinari Itoko, Tamiya Onodera, Kifumi Numata (19 juillet 2024)

Télécharge le pdf de la conférence originale. Note que certains extraits de code peuvent être obsolètes, car ce sont des images statiques.

Le temps de QPU approximatif pour exécuter cette première expérience est de 12 min 30 s. Il y a une expérience supplémentaire ci-dessous qui nécessite environ 4 min.

(Remarque : il est possible que ce notebook ne s'exécute pas dans le temps imparti par l'Open Plan. Assure-toi d'utiliser les ressources de calcul quantique judicieusement.)

# Added by doQumentation — required packages for this notebook
!pip install -q numpy qiskit qiskit-ibm-runtime rustworkx
import qiskit

qiskit.__version__
'2.0.2'
import qiskit_ibm_runtime

qiskit_ibm_runtime.__version__
'0.40.1'
import numpy as np
import rustworkx as rx

from qiskit import QuantumCircuit
from qiskit.visualization import plot_histogram
from qiskit.visualization import plot_gate_map
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.providers import BackendV2
from qiskit.quantum_info import SparsePauliOp

from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_ibm_runtime import Sampler, Estimator, Batch, SamplerOptions

1. Introduction

Faisons un bref rappel des états GHZ et du type de distribution que tu pourrais attendre en leur appliquant Sampler. Ensuite, nous exposerons explicitement l'objectif de cette leçon.

1.1 État GHZ

L'état GHZ (état de Greenberger-Horne-Zeilinger) pour nn qubits est défini comme

12(0n+1n)\frac{1}{\sqrt 2}(|0\rangle ^ {\otimes n}+ |1\rangle^ {\otimes n})

Naturellement, il peut être créé pour 6 qubits avec le circuit quantique suivant.

N = 6
qc = QuantumCircuit(N, N)

qc.h(0)
for i in range(N - 1):
qc.cx(0, i + 1)

# qc.measure_all()
qc.barrier()
qc.measure(list(range(N)), list(range(N)))

qc.draw(output="mpl", idle_wires=False, scale=0.5)

Sortie de la cellule de code précédente

print("Depth:", qc.depth())
Depth: 7

La profondeur n'est pas trop grande, même si tu sais déjà, d'après les leçons précédentes, que tu peux faire mieux. Choisissons un backend et transpilons ce circuit.

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
backend.name
# or
# backend = service.least_busy(operational=True)
# backend.name
'ibm_kingston'
pm = generate_preset_pass_manager(3, backend=backend)
qc_transpiled = pm.run(qc)
qc_transpiled.draw(output="mpl", idle_wires=False, fold=-1)

Sortie de la cellule de code précédente

print("Depth:", qc_transpiled.depth())
print(
"Two-qubit Depth:",
qc_transpiled.depth(filter_function=lambda x: x.operation.num_qubits == 2),
)
Depth: 27
Two-qubit Depth: 11

Là encore, la profondeur à deux qubits après transpilation n'est pas trop grande. Mais pour travailler avec un état GHZ sur davantage de qubits, tu devras clairement réfléchir à l'optimisation du circuit. Exécutons ceci avec Sampler et voyons ce que renvoie un véritable ordinateur quantique.

sampler = Sampler(mode=backend)
shots = 40000
job = sampler.run([qc_transpiled], shots=shots)
job_id = job.job_id()
print(job_id)
d147y20n2txg008jvv70
job.status()
'DONE'
job = service.job(job_id)
result = job.result()
plot_histogram(result[0].data.c.get_counts(), figsize=(30, 5))

Sortie de la cellule de code précédente

Voici le résultat du circuit GHZ à 6 qubits. Comme tu peux le voir, les états de tous les 0|0\rangle et de tous les 1|1\rangle dominent bien, mais les erreurs sont importantes. Essayons de voir quelle est la taille maximale d'un circuit GHZ que tu peux réaliser sur un dispositif Eagle, tout en obtenant des résultats où les états corrects ont au moins plus de 50 % de chances d'apparaître.

1.2 Ton objectif

Construis un circuit GHZ pour 20 qubits ou plus de sorte qu'à la mesure, la fidélité de ton état GHZ soit > 0,5. Remarque :

  • Tu dois utiliser un dispositif Eagle (min_num_qubits=127) et fixer le nombre de shots à 40 000.
  • Tu dois exécuter le circuit GHZ à l'aide de la fonction execute_ghz_fidelity, et calculer la fidélité à l'aide de la fonction check_ghz_fidelity_from_jobs.

Ceci est conçu comme un exercice indépendant, dans lequel tu mets à profit ce que tu as appris jusqu'ici dans ce cours.

def execute_ghz_fidelity(
ghz_circuit: QuantumCircuit, # Quantum circuit to create GHZ state (Circuit after Routing or without Routing), Classical register name is "c"
physical_qubits: list[int], # Physical qubits to represent GHZ state
backend: BackendV2,
sampler_options: dict | SamplerOptions | None = None,
):
N_SHOTS = 40_000
N = len(physical_qubits)
base_circuit = ghz_circuit.remove_final_measurements(inplace=False)
# M_k measurement circuits
mk_circuits = []
for k in range(1, N + 1):
circuit = base_circuit.copy()
# change measurement basis
for q in physical_qubits:
circuit.rz(-k * np.pi / N, q)
circuit.h(q)
mk_circuits.append(circuit)

obs = SparsePauliOp.from_sparse_list(
[("Z" * N, physical_qubits, 1)], num_qubits=backend.num_qubits
)
job_ids = []
pm1 = generate_preset_pass_manager(1, backend=backend)
org_transpiled = pm1.run(ghz_circuit)
mk_transpiled = pm1.run(mk_circuits)
with Batch(backend=backend):
sampler = Sampler(options=sampler_options)
sampler.options.twirling.enable_measure = True
job = sampler.run([org_transpiled], shots=N_SHOTS)
job_ids.append(job.job_id())
# print(f"Sampler job id: {job.job_id()}, shots={N_SHOTS}")
estimator = Estimator() # TREX is applied as default
estimator.options.dynamical_decoupling.enable = True
estimator.options.execution.rep_delay = 0.0005
estimator.options.twirling.enable_measure = True
job2 = estimator.run([(circ, obs) for circ in mk_transpiled], precision=1 / 100)
job_ids.append(job2.job_id())
# print("Estimator job id:", job2.job_id())
return [job.job_id(), job2.job_id()]
def check_ghz_fidelity_from_jobs(
sampler_job,
estimator_job,
num_qubits,
shots=40_000,
):
N = num_qubits
sampler_result = sampler_job.result()
counts = sampler_result[0].data.c.get_counts()
all_zero = counts.get("0" * N, 0) / shots
all_one = counts.get("1" * N, 0) / shots
top3 = sorted(counts, key=counts.get, reverse=True)[:3]
print(
f"N={N}: |00..0>: {counts.get('0'*N, 0)}, |11..1>: {counts.get('1'*N, 0)}, |3rd>: {counts.get(top3[2], 0)} ({top3[2]})"
)
print(f"P(|00..0>)={all_zero}, P(|11..1>)={all_one}")

estimator_result = estimator_job.result()
non_diagonal = (1 / N) * sum(
(-1) ** k * estimator_result[k - 1].data.evs for k in range(1, N + 1)
)
print(f"REM: Coherence (non-diagonal): {non_diagonal:.6f}")
fidelity = 0.5 * (all_zero + all_one + non_diagonal)
sigma = 0.5 * np.sqrt(
(1 - all_zero - all_one) * (all_zero + all_one) / shots
+ sum(estimator_result[k].data.stds ** 2 for k in range(N)) / (N * N)
)
print(f"GHZ fidelity = {fidelity:.6f} ± {sigma:.6f}")
if fidelity - 2 * sigma > 0.5:
print("GME (genuinely multipartite entangled) test: Passed")
else:
print("GME (genuinely multipartite entangled) test: Failed")
return {
"fidelity": fidelity,
"sigma": sigma,
"shots": shots,
"job_ids": [sampler_job.job_id(), estimator_job.job_id()],
}

Dans ce notebook, nous allons appliquer trois stratégies pour créer de bons états GHZ en utilisant 16 qubits et 30 qubits. Ces approches s'appuient sur des stratégies que tu connais déjà grâce aux leçons précédentes.

2. Stratégie 1. Sélection de qubits sensible au bruit

Nous spécifions d'abord un backend. Comme nous allons travailler abondamment avec les propriétés d'un backend spécifique, il est préférable d'en indiquer un précis, plutôt que d'utiliser l'option least_busy.

backend = service.backend("ibm_strasbourg") # eagle
twoq_gate = "ecr"
print(f"Device {backend.name} Loaded with {backend.num_qubits} qubits")
print(f"Two Qubit Gate: {twoq_gate}")
Device ibm_strasbourg Loaded with 127 qubits
Two Qubit Gate: ecr

Nous allons construire un circuit impliquant de nombreuses portes à deux qubits. Il est logique d'utiliser les qubits qui présentent les erreurs les plus faibles lors de l'implémentation de ces portes à deux qubits. Trouver la meilleure « chaîne de qubits » à partir des erreurs de portes à 2 qubits rapportées est un problème non trivial. Mais nous pouvons définir quelques fonctions pour nous aider à déterminer les meilleurs qubits à utiliser.

coupling_map = backend.target.build_coupling_map(twoq_gate)
G = coupling_map.graph
def to_edges(path): # create edges list from node paths
edges = []
prev_node = None
for node in path:
if prev_node is not None:
if G.has_edge(prev_node, node):
edges.append((prev_node, node))
else:
edges.append((node, prev_node))
prev_node = node
return edges

def path_fidelity(path, correct_by_duration: bool = True, readout_scale: float = None):
"""Compute an estimate of the total fidelity of 2-qubit gates on a path.
If `correct_by_duration` is true, each gate fidelity is worsen by
scale = max_duration / duration, that is, gate_fidelity^scale.
If `readout_scale` > 0 is supplied, readout_fidelity^readout_scale
for each qubit on the path is multiplied to the total fielity.
The path is given in node indices form, for example, [0, 1, 2].
An external function `to_edges` is used to obtain edge list, for example, [(0, 1), (1, 2)]."""
path_edges = to_edges(path)
max_duration = max(backend.target[twoq_gate][qs].duration for qs in path_edges)

def gate_fidelity(qpair):
duration = backend.target[twoq_gate][qpair].duration
scale = max_duration / duration if correct_by_duration else 1.0
# 1.25 = (d+1)/d with d = 4
return max(0.25, 1 - (1.25 * backend.target[twoq_gate][qpair].error)) ** scale

def readout_fidelity(qubit):
return max(0.25, 1 - backend.target["measure"][(qubit,)].error)

total_fidelity = np.prod(
[gate_fidelity(qs) for qs in path_edges]
) # two qubits gate fidelity for each path
if readout_scale:
total_fidelity *= (
np.prod([readout_fidelity(q) for q in path]) ** readout_scale
) # multiply readout fidelity
return total_fidelity

def flatten(paths, cutoff=None): # cutoff is for not making run time too large
return [
path
for s, s_paths in paths.items()
for t, st_paths in s_paths.items()
for path in st_paths[:cutoff]
if s < t
]
N = 16 # Number of qubits to use in the GHZ circuit
num_qubits_in_chain = N

Nous allons utiliser les fonctions ci-dessus pour trouver tous les chemins simples de N qubits entre toutes les paires de nœuds du graphe (Référence : all_pairs_all_simple_paths).

Ensuite, à l'aide de la fonction path_fidelity créée ci-dessus, nous trouverons la meilleure chaîne de qubits, celle qui a la plus grande fidélité de chemin.

from functools import partial

%%time
paths = rx.all_pairs_all_simple_paths(
G.to_undirected(multigraph=False),
min_depth=num_qubits_in_chain,
cutoff=num_qubits_in_chain,
)
paths = flatten(paths, cutoff=25) # If you have time, you could set a larger cutoff.
if not paths:
raise Exception(
f"No qubit chain with length={num_qubits_in_chain} exists in {backend.name}. Try smaller num_qubits_in_chain."
)

print(f"Selecting the best from {len(paths)} candidate paths")

best_qubit_chain = max(
paths, key=partial(path_fidelity, correct_by_duration=True, readout_scale=1.0)
)
assert len(best_qubit_chain) == num_qubits_in_chain
print(f"Predicted (best possible) process fidelity: {path_fidelity(best_qubit_chain)}")
Selecting the best from 6046 candidate paths
Predicted (best possible) process fidelity: 0.8929026784775056
CPU times: user 284 ms, sys: 10.9 ms, total: 295 ms
Wall time: 295 ms
np.array(best_qubit_chain)
array([55, 49, 48, 47, 46, 45, 54, 64, 65, 66, 73, 85, 86, 87, 88, 89],
dtype=uint64)

Représentons la meilleure chaîne de qubits, en rose, sur le diagramme de la carte de couplage.

qubit_color = []
for i in range(133):
if i in best_qubit_chain:
qubit_color.append("#ff00dd") # pink
else:
qubit_color.append("#8c00ff") # purple
plot_gate_map(
backend, qubit_color=qubit_color, qubit_size=50, font_size=25, figsize=(6, 6)
)

Output of the previous code cell

2.1 Construire un circuit GHZ sur la meilleure chaîne de qubits

Nous choisissons un qubit au milieu de la chaîne pour lui appliquer d'abord la porte H. Cela devrait réduire la profondeur du circuit d'environ la moitié.

ghz1 = QuantumCircuit(max(best_qubit_chain) + 1, N)
ghz1.h(best_qubit_chain[N // 2])
for i in range(N // 2, 0, -1):
ghz1.cx(best_qubit_chain[i], best_qubit_chain[i - 1])
for i in range(N // 2, N - 1, +1):
ghz1.cx(best_qubit_chain[i], best_qubit_chain[i + 1])
ghz1.barrier() # for visualization
ghz1.measure(best_qubit_chain, list(range(N)))
ghz1.draw(output="mpl", idle_wires=False, scale=0.5, fold=-1)

Output of the previous code cell

ghz1.depth()
10
pm = generate_preset_pass_manager(1, backend=backend)
ghz1_transpiled = pm.run(ghz1)
ghz1_transpiled.draw(output="mpl", idle_wires=False, fold=-1)

Output of the previous code cell

print("Depth:", ghz1_transpiled.depth())
print(
"Two-qubit Depth:",
ghz1_transpiled.depth(filter_function=lambda x: x.operation.num_qubits == 2),
)
Depth: 27
Two-qubit Depth: 8
opts = SamplerOptions()
res = execute_ghz_fidelity(
ghz_circuit=ghz1,
physical_qubits=best_qubit_chain,
backend=backend,
sampler_options=opts,
)
job_s = service.job(res[0]) # Use your job id showed above.
job_e = service.job(res[1])
print(job_s.status(), job_e.status())
DONE DONE

Veille à exécuter la cellule suivante seulement après que les statuts des jobs ci-dessus soient devenus 'DONE', afin d'afficher le résultat à l'aide de la fonction check_ghz_fidelity_from_jobs.

N = 16
# Check fidelity from job IDs
res = check_ghz_fidelity_from_jobs(
sampler_job=job_s,
estimator_job=job_e,
num_qubits=N,
)
N=16: |00..0>: 153, |11..1>: 8681, |3rd>: 2262 (1111111111101111)
P(|00..0>)=0.003825, P(|11..1>)=0.217025
REM: Coherence (non-diagonal): 0.073809
GHZ fidelity = 0.147329 ± 0.002438
GME (genuinely multipartite entangled) test: Failed
result = job_s.result()
plot_histogram(result[0].data.c.get_counts(), figsize=(30, 5))

Output of the previous code cell

Ce résultat ne satisfait pas les critères. Passons à l'idée suivante.

3. Stratégie 2. Arbre équilibré de qubits

L'idée suivante consiste à trouver un arbre équilibré de qubits. En utilisant l'arbre plutôt que la chaîne, la profondeur du circuit devrait diminuer. Avant cela, nous retirons du graphe de couplage les nœuds ayant de « mauvaises » erreurs de lecture et les arêtes ayant de « mauvaises » erreurs de porte.

BAD_READOUT_ERROR_THRESHOLD = 0.1
BAD_ECRGATE_ERROR_THRESHOLD = 0.1
bad_readout_qubits = [
q
for q in range(backend.num_qubits)
if backend.target["measure"][(q,)].error > BAD_READOUT_ERROR_THRESHOLD
]
bad_ecrgate_edges = [
qpair
for qpair in backend.target["ecr"]
if backend.target["ecr"][qpair].error > BAD_ECRGATE_ERROR_THRESHOLD
]
print("Bad readout qubits:", bad_readout_qubits)
print("Bad ECR gates:", bad_ecrgate_edges)
Bad readout qubits: [19, 28, 41, 72, 91, 114, 120]
Bad ECR gates: []
g = backend.coupling_map.graph.copy().to_undirected()
g.remove_edges_from(
bad_ecrgate_edges
) # remove edge first (otherwise might fail with a NoEdgeBetweenNodes error)
g.remove_nodes_from(bad_readout_qubits)

Traçons le graphe de la carte de couplage sans les mauvaises arêtes ni les mauvais qubits.

qubit_color = []
for i in range(133):
if i in bad_readout_qubits:
qubit_color.append("#000000") # black
else:
qubit_color.append("#8c00ff") # purple
line_color = []
for e in backend.target.build_coupling_map().get_edges():
if e in bad_ecrgate_edges:
line_color.append("#ffffff") # white
else:
line_color.append("#888888") # gray
plot_gate_map(
backend,
qubit_color=qubit_color,
line_color=line_color,
qubit_size=50,
font_size=25,
figsize=(6, 6),
)

Output of the previous code cell

Nous essayons de créer un état GHZ à 16 qubits comme précédemment.

N = 16

Nous appelons la fonction betweenness_centrality pour trouver un qubit servant de nœud racine. Le nœud ayant la valeur la plus élevée de centralité d'intermédiarité se trouve au centre du graphe. Référence : https://www.rustworkx.org/tutorial/betweenness_centrality.html

Ou tu peux le sélectionner manuellement.

# central = 65 #Select the center node manually
c_degree = dict(rx.betweenness_centrality(g))
central = max(c_degree, key=c_degree.get)
central
66

En partant du nœud racine, nous générons un arbre par parcours en largeur (BFS). Référence : https://qiskit.org/ecosystem/rustworkx/apiref/rustworkx.bfs_search.html#rustworkx-bfs-search

class TreeEdgesRecorder(rx.visit.BFSVisitor):
def __init__(self, N):
self.edges = []
self.N = N

def tree_edge(self, edge):
self.edges.append(edge)
if len(self.edges) >= self.N - 1:
raise rx.visit.StopSearch()

vis = TreeEdgesRecorder(N)
rx.bfs_search(g, [central], vis)
best_qubits = sorted(list(set(q for e in vis.edges for q in (e[0], e[1]))))
# print('Tree edges:', vis.edges)
print("Qubits selected:", best_qubits)
Qubits selected: [54, 55, 63, 64, 65, 66, 67, 68, 69, 70, 73, 83, 84, 85, 86, 87]

Représentons les qubits sélectionnés, en rose, sur le diagramme de la carte de couplage.

qubit_color = []
for i in range(133):
if i in bad_readout_qubits:
qubit_color.append("#000000") # black
elif i in best_qubits:
qubit_color.append("#ff00dd") # pink
else:
qubit_color.append("#8c00ff") # purple
plot_gate_map(
backend,
qubit_color=qubit_color,
line_color=line_color,
qubit_size=50,
font_size=25,
figsize=(6, 6),
)

Output of the previous code cell

Montrons la structure en arbre des qubits.

from rustworkx.visualization import graphviz_draw

tree = rx.PyDiGraph()
tree.extend_from_weighted_edge_list(vis.edges)
tree.remove_nodes_from([n for n in range(max(best_qubits) + 1) if n not in best_qubits])

graphviz_draw(tree, method="dot")

Output of the previous code cell

ghz2 = QuantumCircuit(max(best_qubits) + 1, N)

ghz2.h(tree.edge_list()[0][0]) # apply H-gate to the root node
# Apply CNOT from the root node to the each edge.
for u, v in tree.edge_list():
ghz2.cx(u, v)
ghz2.barrier() # for visualization
ghz2.measure(best_qubits, list(range(N)))
ghz2.draw(output="mpl", idle_wires=False, scale=0.5)

Output of the previous code cell

ghz2.depth()
8
pm = generate_preset_pass_manager(1, backend=backend)
ghz2_transpiled = pm.run(ghz2)
ghz2_transpiled.draw(output="mpl", idle_wires=False, fold=-1)

Output of the previous code cell

print("Depth:", ghz2_transpiled.depth())
print(
"Two-qubit Depth:",
ghz2_transpiled.depth(filter_function=lambda x: x.operation.num_qubits == 2),
)
Depth: 22
Two-qubit Depth: 6

La profondeur du circuit est maintenant bien plus faible que celle de la structure en chaîne.

res = execute_ghz_fidelity(
ghz_circuit=ghz2,
physical_qubits=best_qubits,
backend=backend,
sampler_options=opts,
)
job_s = service.job(res[0]) # Use your job id showed above.
job_e = service.job(res[1])
print(job_s.status(), job_e.status())
DONE DONE
N = 16
# Check fidelity from job IDs
res = check_ghz_fidelity_from_jobs(
sampler_job=job_s,
estimator_job=job_e,
num_qubits=N,
)
N=16: |00..0>: 9509, |11..1>: 10978, |3rd>: 1795 (1111110111111111)
P(|00..0>)=0.237725, P(|11..1>)=0.27445
REM: Coherence (non-diagonal): 0.606515
GHZ fidelity = 0.559345 ± 0.003188
GME (genuinely multipartite entangled) test: Passed

Nous avons réussi le test avec la structure en arbre équilibré !

result = job_s.result()
plot_histogram(result[0].data.c.get_counts(), figsize=(30, 5))

Output of the previous code cell

Maintenant, essayons de créer un état GHZ plus grand : un état GHZ à 30 qubits.

3.1 N = 30

Nous allons suivre le cadre des Qiskit patterns.

  • Étape 1 : Traduire le problème en circuits quantiques et en opérateurs
  • Étape 2 : Optimiser pour le matériel cible
  • Étape 3 : Exécuter sur le matériel cible
  • Étape 4 : Post-traiter les résultats

Étape 1 : Traduire le problème en circuits quantiques et en opérateurs, et Étape 2 : Optimiser pour le matériel cible

Ici, nous sélectionnons manuellement le nœud racine.

central = 62 # Select the center node manually
# c_degree = dict(rx.betweenness_centrality(g))
# central = max(c_degree, key=c_degree.get)
# central
N = 30

vis = TreeEdgesRecorder(N)
rx.bfs_search(g, [central], vis)
best_qubits = sorted(list(set(q for e in vis.edges for q in (e[0], e[1]))))
print("Qubits selected:", best_qubits)
Qubits selected: [34, 35, 42, 43, 44, 45, 46, 47, 48, 53, 54, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 71, 73, 77, 84, 85, 86]
qubit_color = []
for i in range(133):
if i in bad_readout_qubits:
qubit_color.append("#000000")
elif i in best_qubits:
qubit_color.append("#ff00dd")
else:
qubit_color.append("#8c00ff")
line_color = []
for e in backend.target.build_coupling_map().get_edges():
if e in bad_ecrgate_edges:
line_color.append("#ffffff")
else:
line_color.append("#888888")
plot_gate_map(
backend,
qubit_color=qubit_color,
line_color=line_color,
qubit_size=50,
font_size=25,
figsize=(6, 6),
)

Output of the previous code cell

from rustworkx.visualization import graphviz_draw

tree = rx.PyDiGraph()
tree.extend_from_weighted_edge_list(vis.edges)
tree.remove_nodes_from([n for n in range(max(best_qubits) + 1) if n not in best_qubits])

graphviz_draw(tree, method="dot")

Output of the previous code cell

La profondeur de cet arbre est de 5.

ghz3 = QuantumCircuit(max(best_qubits) + 1, N)

ghz3.h(tree.edge_list()[0][0]) # apply H-gate to the root node
# Apply CNOT from the root node to the each edge.
for u, v in tree.edge_list():
ghz3.cx(u, v)
ghz3.barrier() # for visualization
ghz3.measure(best_qubits, list(range(N)))
ghz3.draw(output="mpl", idle_wires=False, fold=-1)

Output of the previous code cell

ghz3.depth()
11
pm = generate_preset_pass_manager(1, backend=backend)
ghz3_transpiled = pm.run(ghz3)
ghz3_transpiled.draw(output="mpl", idle_wires=False, fold=-1)

Output of the previous code cell

print("Depth:", ghz3_transpiled.depth())
print(
"Two-qubit Depth:",
ghz3_transpiled.depth(filter_function=lambda x: x.operation.num_qubits == 2),
)
Depth: 31
Two-qubit Depth: 9

3.2 Sélectionner manuellement un autre nœud racine

central = 54

vis = TreeEdgesRecorder(N)
rx.bfs_search(g, [central], vis)
best_qubits = sorted(list(set(q for e in vis.edges for q in (e[0], e[1]))))
print("Qubits selected:", best_qubits)
Qubits selected: [23, 24, 25, 34, 35, 42, 43, 44, 45, 46, 47, 48, 49, 50, 54, 55, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 73, 84, 85, 86]
from rustworkx.visualization import graphviz_draw

tree = rx.PyDiGraph()
tree.extend_from_weighted_edge_list(vis.edges)
tree.remove_nodes_from([n for n in range(max(best_qubits) + 1) if n not in best_qubits])

graphviz_draw(tree, method="dot")

Output of the previous code cell

La profondeur de cet arbre est de 6.

ghz3 = QuantumCircuit(max(best_qubits) + 1, N)

ghz3.h(tree.edge_list()[0][0]) # apply H-gate to the root node
# Apply CNOT from the root node to the each edge.
for u, v in tree.edge_list():
ghz3.cx(u, v)
ghz3.barrier() # for visualization
ghz3.measure(best_qubits, list(range(N)))
ghz3.draw(output="mpl", idle_wires=False, fold=-1)

Output of the previous code cell

ghz3.depth()
11
pm = generate_preset_pass_manager(1, backend=backend)
ghz3_transpiled = pm.run(ghz3)
ghz3_transpiled.draw(output="mpl", idle_wires=False, fold=-1)

Output of the previous code cell

print("Depth:", ghz3_transpiled.depth())
print(
"Two-qubit Depth:",
ghz3_transpiled.depth(filter_function=lambda x: x.operation.num_qubits == 2),
)
Depth: 30
Two-qubit Depth: 9

Fait surprenant : alors que la profondeur de l'arbre est passée de 5 à 6, la profondeur à deux qubits, elle, a diminué de 9 à 8 ! Utilisons donc ce dernier circuit.

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

res = execute_ghz_fidelity(
ghz_circuit=ghz3,
physical_qubits=best_qubits,
backend=backend,
sampler_options=opts,
)
job_s = service.job(res[0]) # Use your job id showed above.
job_e = service.job(res[1])
print(job_s.status(), job_e.status())
DONE DONE

Étape 4 : Post-traiter les résultats

N = 30
# Check fidelity from job IDs
res = check_ghz_fidelity_from_jobs(
sampler_job=job_s,
estimator_job=job_e,
num_qubits=N,
)
N=30: |00..0>: 4, |11..1>: 218, |3rd>: 265 (111111111111111011111111111111)
P(|00..0>)=0.0001, P(|11..1>)=0.00545
REM: Coherence (non-diagonal): 0.187073
GHZ fidelity = 0.096312 ± 0.003254
GME (genuinely multipartite entangled) test: Failed

Comme tu peux le voir, ce résultat ne satisfait pas les critères.

# It will take some time
result = job_s.result()
plot_histogram(result[0].data.c.get_counts(), figsize=(30, 5))

Output of the previous code cell

4. Stratégie 3. Exécuter avec les options de suppression d'erreurs

Tu peux configurer les options de suppression des erreurs dans Sampler V2. Consulte le guide Gestion du bruit du Sampler et la référence de l'API ExecutionOptionsV2 pour plus d'informations.

opts = SamplerOptions()
opts.dynamical_decoupling.enable = True
opts.execution.rep_delay = 0.0005
opts.twirling.enable_gates = True
res = execute_ghz_fidelity(
ghz_circuit=ghz3,
physical_qubits=best_qubits,
backend=backend,
sampler_options=opts,
)
job_s = service.job(res[0]) # Use your job id showed above.
job_e = service.job(res[1])
print(job_s.status(), job_e.status())
DONE DONE
N = 30
# Check fidelity from job IDs
res = check_ghz_fidelity_from_jobs(
sampler_job=job_s,
estimator_job=job_e,
num_qubits=N,
)
N=30: |00..0>: 1459, |11..1>: 1543, |3rd>: 359 (111111111111111111111111111110)
P(|00..0>)=0.036475, P(|11..1>)=0.038575
REM: Coherence (non-diagonal): 0.165532
GHZ fidelity = 0.120291 ± 0.003369
GME (genuinely multipartite entangled) test: Failed
# It will take some time
result = job_s.result()
plot_histogram(result[0].data.c.get_counts(), figsize=(30, 5))

Sortie de la cellule de code précédente

Le résultat s'est amélioré, mais ne satisfait toujours pas les critères.

Nous avons vu trois idées jusqu'ici. Tu peux combiner et enrichir ces idées, ou proposer les tiennes, pour créer un meilleur circuit GHZ. Revoyons maintenant l'objectif.

5. Ton objectif (récapitulatif)

Construis un circuit GHZ pour 20 qubits ou plus de sorte que le résultat de la mesure satisfasse le critère : la fidélité de ton état GHZ doit être > 0,5.

  • Tu dois utiliser un dispositif Eagle (par exemple ibm_brisbane) et fixer le nombre de shots à 40 000.
  • Tu dois exécuter le circuit GHZ à l'aide de la fonction execute_ghz_fidelity, et calculer la fidélité à l'aide de la fonction check_ghz_fidelity_from_jobs. Tu dois trouver le circuit GHZ comportant le plus grand nombre de qubits qui satisfasse le critère. Écris ton code ci-dessous, puis affiche le résultat avec la fonction check_ghz_fidelity_from_jobs.

Nous mettons maintenant en œuvre le même flux de travail GHZ que précédemment, mais sur un dispositif Heron. Cela te donne un peu d'expérience avec la disposition et les caractéristiques des processeurs Heron. Aucune nouvelle stratégie n'est introduite.

Le temps de QPU approximatif pour exécuter cette expérience suivante est de 4 min 40 s.

service = QiskitRuntimeService()
backend = service.backend("ibm_kingston")
# backend = service.backend("ibm_fez")

twoq_gate = "cz"
print(f"Device {backend.name} Loaded with {backend.num_qubits} qubits")
print(f"Two Qubit Gate: {twoq_gate}")
Device ibm_kingston Loaded with 156 qubits
Two Qubit Gate: cz
BAD_READOUT_ERROR_THRESHOLD = 0.1
BAD_CZGATE_ERROR_THRESHOLD = 0.1
bad_readout_qubits = [
q
for q in range(backend.num_qubits)
if backend.target["measure"][(q,)].error > BAD_READOUT_ERROR_THRESHOLD
]
bad_czgate_edges = [
qpair
for qpair in backend.target["cz"]
if backend.target["cz"][qpair].error > BAD_CZGATE_ERROR_THRESHOLD
]
print("Bad readout qubits:", bad_readout_qubits)
print("Bad CZ gates:", bad_czgate_edges)
Bad readout qubits: [112, 113, 120, 131, 146]
Bad CZ gates: [(111, 112), (112, 111), (112, 113), (113, 112), (120, 121), (121, 120), (130, 131), (131, 130), (145, 146), (146, 145), (146, 147), (147, 146)]
g = backend.coupling_map.graph.copy().to_undirected()
g.remove_edges_from(
bad_czgate_edges
) # remove edge first (otherwise might fail with a NoEdgeBetweenNodes error)
g.remove_nodes_from(bad_readout_qubits)
qubit_color = []
for i in range(backend.num_qubits):
if i in bad_readout_qubits:
qubit_color.append("#000000") # black
else:
qubit_color.append("#8c00ff") # purple
line_color = []
for e in backend.target.build_coupling_map().get_edges():
if e in bad_czgate_edges:
line_color.append("#ffffff") # white
else:
line_color.append("#888888") # gray
plot_gate_map(
backend,
qubit_color=qubit_color,
line_color=line_color,
qubit_size=60,
font_size=30,
figsize=(10, 10),
)

Sortie de la cellule de code précédente

N = 40
central = 100 # Select the center node manually
# c_degree = dict(rx.betweenness_centrality(g))
# central = max(c_degree, key=c_degree.get)
# central
class TreeEdgesRecorder(rx.visit.BFSVisitor):
def __init__(self, N):
self.edges = []
self.N = N

def tree_edge(self, edge):
self.edges.append(edge)
if len(self.edges) >= self.N - 1:
raise rx.visit.StopSearch()

vis = TreeEdgesRecorder(N)
rx.bfs_search(g, [central], vis)
best_qubits = sorted(list(set(q for e in vis.edges for q in (e[0], e[1]))))
print("Qubits selected:", best_qubits)
Qubits selected: [61, 65, 76, 77, 80, 81, 82, 83, 84, 85, 86, 87, 96, 97, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 116, 117, 121, 122, 123, 124, 125, 126, 127, 136, 140, 141, 142, 143, 144, 145]
qubit_color = []
for i in range(backend.num_qubits):
if i in bad_readout_qubits:
qubit_color.append("#000000")
elif i in best_qubits:
qubit_color.append("#ff00dd")
else:
qubit_color.append("#8c00ff")
line_color = []
for e in backend.target.build_coupling_map().get_edges():
if e in bad_czgate_edges:
line_color.append("#ffffff")
else:
line_color.append("#888888")
plot_gate_map(
backend,
qubit_color=qubit_color,
line_color=line_color,
qubit_size=60,
font_size=30,
figsize=(10, 10),
)

Sortie de la cellule de code précédente

from rustworkx.visualization import graphviz_draw

tree = rx.PyDiGraph()
tree.extend_from_weighted_edge_list(vis.edges)
tree.remove_nodes_from([n for n in range(max(best_qubits) + 1) if n not in best_qubits])

graphviz_draw(tree, method="dot")

Sortie de la cellule de code précédente

ghz_h = QuantumCircuit(max(best_qubits) + 1, N)

ghz_h.h(tree.edge_list()[0][0]) # apply H-gate to the root node
# Apply CNOT from the root node to the each edge.
for u, v in tree.edge_list():
ghz_h.cx(u, v)
ghz_h.barrier() # for visualization
ghz_h.measure(best_qubits, list(range(N)))
ghz_h.draw(output="mpl", idle_wires=False, fold=-1)

Sortie de la cellule de code précédente

ghz_h.depth()
15
pm = generate_preset_pass_manager(1, backend=backend)
ghz_h_transpiled = pm.run(ghz_h)
ghz_h_transpiled.draw(output="mpl", idle_wires=False, fold=-1)

Sortie de la cellule de code précédente

print("Depth:", ghz_h_transpiled.depth())
print(
"Two-qubit Depth:",
ghz_h_transpiled.depth(filter_function=lambda x: x.operation.num_qubits == 2),
)
Depth: 45
Two-qubit Depth: 13
opts = SamplerOptions()
opts.dynamical_decoupling.enable = True
opts.execution.rep_delay = 0.0005
opts.twirling.enable_gates = True
res = execute_ghz_fidelity(
ghz_circuit=ghz_h,
physical_qubits=best_qubits,
backend=backend,
sampler_options=opts,
)
job_s = service.job(res[0]) # Use your job id showed above.
job_e = service.job(res[1])
print(job_s.status(), job_e.status())
RUNNING RUNNING
# Check fidelity from job IDs
N = 40
res = check_ghz_fidelity_from_jobs(
sampler_job=job_s,
estimator_job=job_e,
num_qubits=N,
)
N=40: |00..0>: 3186, |11..1>: 3277, |3rd>: 620 (1111111011111111111111111111111111111111)
P(|00..0>)=0.07965, P(|11..1>)=0.081925
REM: Coherence (non-diagonal): 0.029987
GHZ fidelity = 0.095781 ± 0.002619
GME (genuinely multipartite entangled) test: Failed
# It will take some time
result = job_s.result()
plot_histogram(result[0].data.c.get_counts(), figsize=(30, 5))

Sortie de la cellule de code précédente

Félicitations ! Tu as terminé ton introduction à l'informatique quantique à l'échelle de l'utilité ! Tu es désormais en mesure d'apporter des contributions significatives dans la quête de l'avantage quantique ! Merci d'avoir fait d'IBM Quantum® une partie de ton parcours quantique personnel.

Enquête de fin de cours

Félicitations pour avoir terminé ce cours ! Prends un instant pour nous aider à améliorer notre contenu en remplissant cette enquête rapide. Tes retours serviront à améliorer notre offre de contenu et l'expérience utilisateur. Merci !

Note: This survey is provided by IBM Quantum and relates to the original English content. To give feedback on doQumentation's website, translations, or code execution, please open a GitHub issue.