Optimisation des circuits quantiques
Toshinari Itoko (21 juin 2024)
Télécharger le PDF du cours original. Note : certains extraits de code peuvent être obsolètes car ce sont des images statiques.
Le temps QPU approximatif pour exécuter cette expérience est de 15 s.
(Note : Certaines cellules de la partie 2 sont copiées du notebook "Qiskit Deep dive", rédigé par Matthew Treinish (mainteneur Qiskit))
# Added by doQumentation — required packages for this notebook
!pip install -q qiskit qiskit-aer qiskit-ibm-runtime
# !pip install 'qiskit[visualization]'
# !pip install qiskit_ibm_runtime qiskit_aer
# !pip install jupyter
# !pip install matplotlib pylatexenc pydot pillow
import qiskit
qiskit.__version__
'2.0.2'
import qiskit_ibm_runtime
qiskit_ibm_runtime.__version__
'0.40.1'
import qiskit_aer
qiskit_aer.__version__
'0.17.1'
1. Introduction
Cette leçon aborde plusieurs aspects de l'optimisation des circuits en informatique quantique. Plus précisément, nous allons voir l'intérêt de l'optimisation des circuits en utilisant les paramètres d'optimisation intégrés dans Qiskit. Nous approfondirons ensuite le sujet pour voir ce que tu peux faire en tant qu'expert dans ton domaine d'application particulier pour construire des circuits de manière intelligente. Enfin, nous examinerons en détail ce qui se passe lors de la transpilation pour nous aider à optimiser nos circuits.
2. L'optimisation des circuits, ça compte
Nous comparons d'abord les résultats de l'exécution de circuits de préparation d'état GHZ à 5 qubits () avec et sans optimisation.
from qiskit.circuit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.primitives import BackendSamplerV2 as Sampler
from qiskit_ibm_runtime.fake_provider import FakeBrisbane
backend = FakeBrisbane()
Nous utilisons d'abord un circuit GHZ synthétisé naïvement comme suit.
num_qubits = 5
ghz_circ = QuantumCircuit(num_qubits)
ghz_circ.h(0)
[ghz_circ.cx(0, i) for i in range(1, num_qubits)]
ghz_circ.measure_all()
ghz_circ.draw("mpl")
2.1 Niveau d'optimisation
Il existe 4 niveaux optimization_level disponibles, de 0 à 3. Plus le niveau d'optimisation est élevé, plus l'effort de calcul consacré à l'optimisation du circuit est important. Le niveau 0 n'effectue aucune optimisation et fait juste le minimum nécessaire pour rendre le circuit exécutable sur le backend sélectionné. Le niveau 3 consacre le plus d'effort (et généralement de temps d'exécution) pour tenter d'optimiser le circuit. Le niveau 1 est le niveau d'optimisation par défaut.
Nous transpilons le circuit sans optimisation (optimization_level=0) et avec optimisation (optimization_level=2).
On observe une grande différence dans la longueur des circuits transpilés.
pm0 = generate_preset_pass_manager(
optimization_level=0, backend=backend, seed_transpiler=777
)
pm2 = generate_preset_pass_manager(
optimization_level=2, backend=backend, seed_transpiler=777
)
circ0 = pm0.run(ghz_circ)
circ2 = pm2.run(ghz_circ)
print("optimization_level=0:")
display(circ0.draw("mpl", idle_wires=False, fold=-1))
print("optimization_level=2:")
display(circ2.draw("mpl", idle_wires=False, fold=-1))
optimization_level=0:

optimization_level=2:
2.2 Exercice
Essaie également optimization_level=1 et compare le circuit obtenu avec les deux précédents. Fais-le en modifiant le code ci-dessus.
Solution :
pm1 = generate_preset_pass_manager(
optimization_level=1, backend=backend, seed_transpiler=777
)
circ1 = pm1.run(ghz_circ)
print("optimization_level=1:")
display(circ1.draw("mpl", idle_wires=False, fold=-1))
optimization_level=1:
Exécution sur un faux backend (simulation bruitée). Voir l'Annexe 1 pour savoir comment exécuter sur un vrai backend.
# run the circuits on the fake backend (noisy simulator)
sampler = Sampler(backend=backend)
job = sampler.run([circ0, circ2], shots=10000)
print(f"Job ID: {job.job_id()}")
Job ID: 93a4ac70-e3ea-44ad-aea9-5045840c9076
# get results
result = job.result()
unoptimized_result = result[0].data.meas.get_counts()
optimized_result = result[1].data.meas.get_counts()
from qiskit.visualization import plot_histogram
# plot
sim_result = {"0" * 5: 0.5, "1" * 5: 0.5}
plot_histogram(
[result for result in [sim_result, unoptimized_result, optimized_result]],
bar_labels=False,
legend=[
"ideal",
"no optimization",
"with optimization",
],
)
3. La synthèse des circuits, ça compte aussi
Nous comparons ensuite les résultats de l'exécution de deux circuits de préparation d'état GHZ à 5 qubits () synthétisés différemment.
# Original GHZ circuit (naive synthesis)
ghz_circ.draw("mpl")
# A cleverly-synthesized GHZ circuit
ghz_circ2 = QuantumCircuit(5)
ghz_circ2.h(2)
ghz_circ2.cx(2, 1)
ghz_circ2.cx(2, 3)
ghz_circ2.cx(1, 0)
ghz_circ2.cx(3, 4)
ghz_circ2.measure_all()
ghz_circ2.draw("mpl")
# transpile both with the same optimization level 2
circ_org = pm2.run(ghz_circ)
circ_new = pm2.run(ghz_circ2)
print("original synthesis:")
display(circ_org.draw("mpl", idle_wires=False, fold=-1))
print("new synthesis:")
display(circ_new.draw("mpl", idle_wires=False, fold=-1))
original synthesis:
new synthesis:
La nouvelle synthèse produit un circuit moins profond. Pourquoi ?
C'est parce que le nouveau circuit peut être disposé sur des qubits connectés linéairement, et donc aussi sur le graphe de couplage hexagonal lourd d'IBM® Brisbane, tandis que le circuit original nécessite une connectivité en étoile (un nœud de degré 4) et ne peut donc pas être disposé sur le graphe de couplage heavy-hex, dont les nœuds ont au maximum le degré 3. En conséquence, le circuit original nécessite un routage de qubits qui ajoute des portes SWAP, ce qui augmente le nombre de portes.
Ce que nous avons fait dans le nouveau circuit peut être vu comme une synthèse de circuit « consciente des contraintes de couplage » effectuée manuellement. En d'autres termes : résoudre manuellement la synthèse du circuit et le mapping du circuit en même temps.
# run the circuits
sampler = Sampler(backend=backend)
job = sampler.run([circ_org, circ_new], shots=10000)
print(f"Job ID: {job.job_id()}")
Job ID: 19d635b0-4d8b-44c2-a76e-49e4b9078b1b
# get results
result = job.result()
synthesis_org_result = result[0].data.meas.get_counts()
synthesis_new_result = result[1].data.meas.get_counts()
# plot
sim_result = {"0" * 5: 0.5, "1" * 5: 0.5}
plot_histogram(
[
result
for result in [
sim_result,
unoptimized_result,
synthesis_org_result,
synthesis_new_result,
]
],
bar_labels=False,
legend=[
"ideal",
"no optimization",
"synthesis_org",
"synthesis_new",
],
)
En général, la synthèse de circuit dépend de l'application et il est trop difficile pour un logiciel de couvrir toutes les applications possibles. Le transpiler de Qiskit ne dispose pas de fonctions pour synthétiser le circuit de préparation d'état GHZ. Dans ce cas, une synthèse de circuit manuelle comme celle montrée ci-dessus vaut la peine d'être envisagée.
Dans cette section, nous examinons en détail le fonctionnement du transpiler de Qiskit à l'aide du circuit exemple jouet suivant.
# Build a toy example circuit
from math import pi
import itertools
from qiskit.circuit import QuantumCircuit
from qiskit.circuit.library import excitation_preserving
circuit = QuantumCircuit(4, name="Example circuit")
circuit.append(excitation_preserving(4, reps=1, flatten=True), range(4))
circuit.measure_all()
value_cycle = itertools.cycle([0, pi / 4, pi / 2, 3 * pi / 4, pi, 2 * pi])
circuit.assign_parameters(
[x[1] for x in zip(range(len(circuit.parameters)), value_cycle)], inplace=True
)
circuit.draw("mpl")
3.1 Afficher le flux complet de transpilation de Qiskit
Nous examinons les passes du transpiler (tâches) pour optimization_level=1.
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
# There is no need to read this entire image, but this outputs all the steps in the transpile() call
# for optimization level 1
pm = generate_preset_pass_manager(1, backend, seed_transpiler=42)
pm.draw()

Le flux se compose de six étapes :
print(pm.stages)
('init', 'layout', 'routing', 'translation', 'optimization', 'scheduling')
3.2 Afficher une étape individuelle
D'abord, affichons toutes les tâches (passes du transpiler) effectuées dans l'étape init.
pm.init.draw()

Nous pouvons exécuter chaque étape individuellement. Exécutons l'étape init pour notre circuit. En activant le logger, nous pouvons voir les détails de l'exécution.
import logging
logger = logging.getLogger()
logger.setLevel("INFO")
init_out = pm.init.run(circuit)
init_out.draw("mpl", fold=-1)
INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03576 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.16618 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.07176 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.27299 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00811 (ms)
3.3 Exercice
Affiche les passes de l'étape layout et exécute l'étape pour le circuit de sortie de l'étape init (init_out), en modifiant les cellules utilisées ci-dessus.
Solution :
display(pm.layout.draw())
layout_out = pm.layout.run(init_out)
layout_out.draw("mpl", idle_wires=False, fold=-1)

INFO:qiskit.passmanager.base_tasks:Pass: SetLayout - 0.01001 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: TrivialLayout - 0.07129 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: CheckMap - 0.08917 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: VF2Layout - 1.24431 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: BarrierBeforeFinalMeasurements - 0.02599 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: SabreLayout - 5.11169 (ms)
Fais la même chose pour l'étape translation.
Solution :
display(pm.translation.draw())
basis_out = pm.translation.run(layout_out)
basis_out.draw("mpl", idle_wires=False, fold=-1)

INFO:qiskit.passmanager.base_tasks:Pass: UnitarySynthesis - 0.03386 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: HighLevelSynthesis - 0.02718 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 2.64192 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: CheckGateDirection - 0.02217 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: GateDirection - 0.36502 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: BasisTranslator - 0.64778 (ms)

Note : Il n'est pas toujours possible d'exécuter chaque étape individuellement de manière indépendante (car certaines d'entre elles doivent transmettre des informations de l'étape précédente).
3.4 Étape d'optimisation
La dernière étape par défaut dans le pipeline est l'optimisation. Après avoir embarqué le circuit pour la cible, le circuit s'est considérablement étendu. C'est principalement dû aux inefficacités dans les relations d'équivalence issues de la traduction de base et de l'insertion de SWAP. L'étape d'optimisation vise à minimiser la taille et la profondeur du circuit. Elle exécute une série de passes dans une boucle do while jusqu'à atteindre une sortie stable.
# pm.pre_optimization.draw()
pm.optimization.draw()

logger = logging.getLogger()
logger.setLevel("INFO")
opt_out = pm.optimization.run(basis_out)
INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.30112 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.03195 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.01216 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.01001 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.63729 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.41723 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.01192 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.05484 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.08583 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.20599 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00787 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00715 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Optimize1qGatesDecomposition - 0.16809 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: InverseCancellation - 0.17190 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: ContractIdleWiresInControlFlow - 0.00691 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: GatesInBasis - 0.02408 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Depth - 0.04935 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00525 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: Size - 0.00620 (ms)
INFO:qiskit.passmanager.base_tasks:Pass: FixedPoint - 0.00286 (ms)
opt_out.draw("mpl", idle_wires=False, fold=-1)

4. Exemples approfondis
4.1 Optimisation de blocs à deux qubits par synthèse unitaire à deux qubits
Pour les niveaux 2 et 3, nous avons davantage de passes (Collect2qBlocks, ConsolidateBlocks, UnitarySynthesis) pour plus d'optimisation, à savoir l'optimisation de blocs à deux qubits. (Compare le flux de l'étape d'optimisation pour le niveau 2 avec celui du niveau 1 ci-dessus)
L'optimisation de blocs à deux qubits se compose de deux étapes : la collecte et la consolidation des blocs à 2 qubits, puis la synthèse des matrices unitaires à 2 qubits.
pm2 = generate_preset_pass_manager(2, backend, seed_transpiler=42)
pm2.optimization.draw()

from qiskit.transpiler import PassManager
from qiskit.transpiler.passes import (
Collect2qBlocks,
ConsolidateBlocks,
UnitarySynthesis,
)
# Collect 2q blocks and consolidate to unitary when we expect that we can reduce the 2q gate count for that unitary
consolidate_pm = PassManager(
[
Collect2qBlocks(),
ConsolidateBlocks(target=backend.target),
]
)
display(basis_out.draw("mpl", idle_wires=False, fold=-1))
consolidated = consolidate_pm.run(basis_out)
consolidated.draw("mpl", idle_wires=False, fold=-1)

# Synthesize unitaries
UnitarySynthesis(target=backend.target)(consolidated).draw(
"mpl", idle_wires=False, fold=-1
)

logger.setLevel("WARNING")
Nous avons vu dans la Partie 2 que le flux réel du compilateur quantique n'est pas si simple et se compose de nombreuses passes (tâches). C'est principalement dû à l'ingénierie logicielle nécessaire pour garantir les performances sur une large gamme de circuits d'application et la maintenabilité du logiciel. Le transpiler de Qiskit fonctionne bien dans la plupart des cas, mais si tu constates que ton circuit n'est pas bien optimisé par le transpiler de Qiskit, ce serait une bonne opportunité de rechercher ta propre optimisation de circuit spécifique à l'application, comme montré dans la Partie 1. La technologie de transpilation évolue, ta contribution en R&D est la bienvenue.
from qiskit.circuit import QuantumCircuit
from qiskit_ibm_runtime import QiskitRuntimeService, Sampler
service = QiskitRuntimeService()
backend = service.backend("ibm_brisbane")
sampler = Sampler(backend)
circ = QuantumCircuit(3)
circ.ccx(0, 1, 2)
circ.measure_all()
circ.draw("mpl")
sampler.run([circ]) # IBMInputValueError will be raised
4.2 L'optimisation des circuits, ça compte
Nous comparons d'abord les résultats de l'exécution de circuits de préparation d'état GHZ à 5 qubits () avec et sans optimisation.
from qiskit.circuit import QuantumCircuit
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, Sampler
service = QiskitRuntimeService()
# backend = service.backend('ibm_brisbane')
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=127
) # Eagle
backend
Nous utilisons d'abord un circuit GHZ synthétisé naïvement comme suit.
num_qubits = 5
ghz_circ = QuantumCircuit(num_qubits)
ghz_circ.h(0)
[ghz_circ.cx(0, i) for i in range(1, num_qubits)]
ghz_circ.measure_all()
ghz_circ.draw("mpl")
Nous transpilons le circuit sans optimisation (optimization_level=0) et avec optimisation (optimization_level=2).
Comme tu peux le constater, il y a une grande différence dans la longueur des circuits transpilés.
pm0 = generate_preset_pass_manager(
optimization_level=0, backend=backend, seed_transpiler=777
)
pm2 = generate_preset_pass_manager(
optimization_level=2, backend=backend, seed_transpiler=777
)
circ0 = pm0.run(ghz_circ)
circ2 = pm2.run(ghz_circ)
print("optimization_level=0:")
display(circ0.draw("mpl", idle_wires=False, fold=-1))
print("optimization_level=2:")
display(circ2.draw("mpl", idle_wires=False, fold=-1))
optimization_level=0:

optimization_level=2:
# run the circuits
sampler = Sampler(backend)
job = sampler.run([circ0, circ2], shots=10000)
job_id = job.job_id()
print(f"Job ID: {job_id}")
Job ID: d13rnnemya70008ek1zg
# REPLACE WITH YOUR OWN JOB IDS
job = service.job(job_id)
# get results
result = job.result()
unoptimized_result = result[0].data.meas.get_counts()
optimized_result = result[1].data.meas.get_counts()
from qiskit.visualization import plot_histogram
# plot
sim_result = {"0" * 5: 0.5, "1" * 5: 0.5}
plot_histogram(
[result for result in [sim_result, unoptimized_result, optimized_result]],
bar_labels=False,
legend=[
"ideal",
"no optimization",
"with optimization",
],
)
4.3 La synthèse des circuits, ça compte aussi
Nous comparons ensuite les résultats de l'exécution de deux circuits de préparation d'état GHZ à 5 qubits () synthétisés différemment.
# Original GHZ circuit (naive synthesis)
ghz_circ.draw("mpl")
# A better GHZ circuit (smarter synthesis), you learned in a previous lecture
ghz_circ2 = QuantumCircuit(5)
ghz_circ2.h(2)
ghz_circ2.cx(2, 1)
ghz_circ2.cx(2, 3)
ghz_circ2.cx(1, 0)
ghz_circ2.cx(3, 4)
ghz_circ2.measure_all()
ghz_circ2.draw("mpl")
circ_org = pm2.run(ghz_circ)
circ_new = pm2.run(ghz_circ2)
print("original synthesis:")
display(circ_org.draw("mpl", idle_wires=False, fold=-1))
print("new synthesis:")
display(circ_new.draw("mpl", idle_wires=False, fold=-1))
original synthesis:
new synthesis:
# run the circuits
sampler = Sampler(backend)
job = sampler.run([circ_org, circ_new], shots=10000)
job_id = job.job_id()
print(f"Job ID: {job_id}")
Job ID: d13rp283grvg008j12fg
# REPLACE WITH YOUR OWN JOB IDS
job = service.job(job_id)
# get results
result = job.result()
synthesis_org_result = result[0].data.meas.get_counts()
synthesis_new_result = result[1].data.meas.get_counts()
# plot
sim_result = {"0" * 5: 0.5, "1" * 5: 0.5}
plot_histogram(
[result for result in [sim_result, synthesis_org_result, synthesis_new_result]],
bar_labels=False,
legend=[
"ideal",
"synthesis_org",
"synthesis_new",
],
)
4.4 Décomposition générale de porte à 1 qubit
from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.circuit.library.standard_gates import UGate
phi, theta, lam = Parameter("φ"), Parameter("θ"), Parameter("λ")
qc = QuantumCircuit(1)
qc.append(UGate(theta, phi, lam), [0])
qc.draw(output="mpl")
transpile(qc, basis_gates=["rz", "sx"]).draw(output="mpl")
4.5 Optimisation de blocs à un qubit
from qiskit import QuantumCircuit
qc = QuantumCircuit(1)
qc.x(0)
qc.y(0)
qc.z(0)
qc.rx(1.23, 0)
qc.ry(1.23, 0)
qc.rz(1.23, 0)
qc.h(0)
qc.s(0)
qc.t(0)
qc.sx(0)
qc.sdg(0)
qc.tdg(0)
qc.draw(output="mpl")
from qiskit.quantum_info import Operator
Operator(qc)
Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],
[ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],
input_dims=(2,), output_dims=(2,))
from qiskit import transpile
qc_opt = transpile(qc, basis_gates=["rz", "sx"])
qc_opt.draw(output="mpl")
Operator(qc_opt)
Operator([[ 0.45292511-0.57266982j, -0.66852684-0.14135058j],
[ 0.14135058+0.66852684j, -0.57266982+0.45292511j]],
input_dims=(2,), output_dims=(2,))
Operator(qc).equiv(Operator(qc_opt))
True
4.6 Décomposition de la porte de Toffoli
qc = QuantumCircuit(3)
qc.ccx(0, 1, 2)
qc.draw(output="mpl")
from qiskit import QuantumCircuit, transpile
qc = QuantumCircuit(3)
qc.ccx(0, 1, 2)
qc = transpile(qc, basis_gates=["rz", "sx", "cx"])
qc.draw(output="mpl")
4.7 Décomposition de la porte CU
from qiskit.circuit.library.standard_gates import CUGate
phi, theta, lam, gamma = Parameter("φ"), Parameter("θ"), Parameter("λ"), Parameter("γ")
qc = QuantumCircuit(2)
# qc.cu(theta, phi, lam, gamma, 0, 1)
qc.append(CUGate(theta, phi, lam, gamma), [0, 1])
qc.draw(output="mpl")
from qiskit.circuit.library.standard_gates import CUGate
phi, theta, lam, gamma = Parameter("φ"), Parameter("θ"), Parameter("λ"), Parameter("γ")
qc = QuantumCircuit(2)
qc.append(CUGate(theta, phi, lam, gamma), [0, 1])
qc = transpile(qc, basis_gates=["rz", "sx", "cx"])
qc.draw(output="mpl")
4.8 CX, ECR, CZ sont équivalents à des Clifford locaux près
Note que (Hadamard), (rotation Z de ), (rotation Z de ), (Pauli X) sont toutes des portes de Clifford.
qc = QuantumCircuit(2)
qc.cx(0, 1)
qc.draw(output="mpl", style="bw")
qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["x", "s", "h", "sdg", "ecr"]).draw(output="mpl", style="bw")
qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["h", "cz"]).draw(output="mpl", style="bw")
Utilisation des portes de base 1 qubit du backend IBM : "rz", "sx" et "x".
qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["rz", "sx", "x", "ecr"]).draw(output="mpl", style="bw")
qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["rz", "sx", "x", "cz"]).draw(output="mpl", style="bw")
# Check Qiskit version
import qiskit
qiskit.__version__
'2.0.2'