Pular para o conteúdo principal

Otimização de circuitos quânticos

nota

Toshinari Itoko (21 de junho de 2024)

Baixe o PDF da aula original. Observe que alguns trechos de código podem ter sido descontinuados, pois são imagens estáticas.

O tempo estimado de QPU para executar este experimento é de 15 s.

(Observação: Algumas células da parte 2 foram copiadas do notebook "Qiskit Deep dive", escrito por Matthew Treinish, mantenedor do 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. Introdução

Esta aula aborda vários aspectos da otimização de circuitos em computação quântica. Especificamente, veremos o valor da otimização de circuitos usando as configurações de otimização integradas ao Qiskit. Em seguida, vamos nos aprofundar um pouco e ver o que você pode fazer como especialista na sua área de aplicação para construir circuitos de forma inteligente. Por fim, daremos uma olhada detalhada no que acontece durante a transpilação para nos ajudar a otimizar nossos circuitos.

2. A otimização de circuitos faz diferença

Primeiro, comparamos os resultados de executar circuitos de preparação do estado GHZ de 5 qubits (12(00000+11111)\frac{1}{\sqrt{2}} \left( |00000\rangle + |11111\rangle \right)) com e sem otimização.

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

Primeiro, usamos um circuito GHZ sintetizado de forma ingênua, como mostrado a seguir.

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

Saída da célula de código anterior

2.1 Nível de otimização

Há 4 optimization_levels disponíveis, de 0 a 3. Quanto maior o nível de otimização, mais esforço computacional é empregado para otimizar o circuito. O nível 0 não realiza nenhuma otimização e apenas faz o mínimo necessário para que o circuito possa ser executado no backend selecionado. O nível 3 emprega o maior esforço (e tipicamente o maior tempo de execução) para tentar otimizar o circuito. O nível 1 é o nível de otimização padrão.

Transpilamos o circuito sem otimização (optimization_level=0) e com otimização (optimization_level=2). Observamos uma grande diferença no comprimento dos circuitos transpilados.

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:

Saída da célula de código anterior

optimization_level=2:

Saída da célula de código anterior

2.2 Exercício

Experimente também o optimization_level=1 e compare o circuito resultante com os dois acima. Tente modificando o código acima.

Solução:

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:

Saída da célula de código anterior

Execute em um backend falso (simulação com ruído). Veja o Apêndice 1 para saber como executar em um backend real.

# 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",
],
)

Saída da célula de código anterior

3. A síntese de circuitos faz diferença

A seguir, comparamos os resultados de executar dois circuitos de preparação do estado GHZ de 5 qubits (12(00000+11111)\frac{1}{\sqrt{2}} \left( |00000\rangle + |11111\rangle \right)) sintetizados de formas diferentes.

# Original GHZ circuit (naive synthesis)
ghz_circ.draw("mpl")

Saída da célula de código anterior

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

Saída da célula de código anterior

# 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:

Saída da célula de código anterior

new synthesis:

Saída da célula de código anterior

A nova síntese produz um circuito mais raso. Por quê?

Isso acontece porque o novo circuito pode ser mapeado em qubits conectados linearmente — e, portanto, também no grafo de acoplamento heavy-hexagon do IBM® Brisbane —, enquanto o circuito original requer conectividade em formato de estrela (um nó de grau 4) e, por isso, não pode ser mapeado no grafo de acoplamento heavy-hex, cujos nós têm grau no máximo 3. Como resultado, o circuito original exige roteamento de qubits, o que adiciona portas SWAP e aumenta a contagem de portas.

O que fizemos no novo circuito pode ser visto como uma síntese de circuito "consciente das restrições de acoplamento" feita manualmente. Em outras palavras: resolver manualmente a síntese do circuito e o mapeamento do circuito ao mesmo tempo.

# 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",
],
)

Saída da célula de código anterior

Em geral, a síntese de circuitos depende da aplicação, e é muito difícil para um software cobrir todas as aplicações possíveis. O transpilador do Qiskit, por acaso, não possui funções para sintetizar circuitos de preparação do estado GHZ. Nesse caso, vale a pena considerar a síntese manual de circuitos, como mostrado acima.

Nesta seção, examinamos os detalhes de como o transpilador do Qiskit funciona usando o seguinte circuito de exemplo simplificado.

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

Saída da célula de código anterior

3.1 Visualizando o fluxo completo de transpilação do Qiskit

Examinamos os passes do transpilador (tarefas) para 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()

Saída da célula de código anterior

O fluxo é composto por seis estágios:

print(pm.stages)
('init', 'layout', 'routing', 'translation', 'optimization', 'scheduling')

3.2 Visualizando um estágio individual

Primeiro, vamos visualizar todas as tarefas (passes do transpilador) realizadas no estágio init.

pm.init.draw()

Saída da célula de código anterior

Podemos executar cada estágio individualmente. Vamos executar o estágio init para o nosso circuito. Ao habilitar o logger, podemos ver os detalhes da execução.

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)

Saída da célula de código anterior

3.3 Exercício

Visualize os passes do estágio layout e execute o estágio para o circuito de saída do estágio init (init_out), modificando as células usadas acima.

Solução:

display(pm.layout.draw())
layout_out = pm.layout.run(init_out)
layout_out.draw("mpl", idle_wires=False, fold=-1)

Saída da célula de código anterior

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)

Saída da célula de código anterior

Faça o mesmo para o estágio translation.

Solução:

display(pm.translation.draw())
basis_out = pm.translation.run(layout_out)
basis_out.draw("mpl", idle_wires=False, fold=-1)

Saída da célula de código anterior

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)

Saída da célula de código anterior

Observação: Nem todo estágio individual pode sempre ser executado de forma independente (pois alguns precisam carregar informações de um estágio anterior).

3.4 Estágio de otimização

O último estágio padrão no pipeline é o de otimização. Depois de embutirmos o circuito para o alvo, o circuito cresceu bastante. A maior parte disso se deve a ineficiências nas relações de equivalência provenientes da tradução de base e da inserção de SWAPs. O estágio de otimização é utilizado para tentar minimizar o tamanho e a profundidade do circuito. Ele executa uma série de passes em um laço do while até atingir uma saída estável.

# pm.pre_optimization.draw()
pm.optimization.draw()

Saída da célula de código anterior

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)

Saída da célula de código anterior

4. Exemplos detalhados

4.1 Otimização de blocos de dois qubits usando síntese unitária de dois qubits

Para os níveis 2 e 3, temos mais passes (Collect2qBlocks, ConsolidateBlocks, UnitarySynthesis) para uma otimização mais avançada, especificamente a otimização de blocos de dois qubits. (Compare o fluxo do estágio de otimização do nível 2 com o do nível 1 mostrado acima)

A otimização de blocos de dois qubits é composta por duas etapas: coleta e consolidação dos blocos de 2 qubits, e síntese das matrizes unitárias de 2 qubits.

pm2 = generate_preset_pass_manager(2, backend, seed_transpiler=42)
pm2.optimization.draw()

Saída da célula de código anterior

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)

Saída da célula de código anterior

Saída da célula de código anterior

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

Saída da célula de código anterior

logger.setLevel("WARNING")

Vimos na Parte 2 que o fluxo real do compilador quântico não é tão simples e é composto por muitos passes (tarefas). Isso se deve principalmente à engenharia de software necessária para garantir o desempenho em uma ampla variedade de circuitos de aplicação e a manutenibilidade do software. O transpilador do Qiskit funciona bem na maioria dos casos, mas se você perceber que seu circuito não está sendo bem otimizado por ele, seria uma boa oportunidade para pesquisar sua própria otimização de circuito específica para a aplicação, como mostrado na Parte 1. A tecnologia de transpilação está evoluindo — sua contribuição em P&D é bem-vinda.

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

Saída da célula de código anterior

sampler.run([circ])  # IBMInputValueError will be raised

4.2 A otimização de circuitos faz diferença

Primeiro, comparamos os resultados da execução de circuitos de preparação do estado GHZ de 5 qubits (12(00000+11111)\frac{1}{\sqrt{2}} \left( |00000\rangle + |11111\rangle \right)) com e sem otimização.

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

Primeiro, usamos um circuito GHZ sintetizado de forma ingênua, como mostrado a seguir.

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

Saída da célula de código anterior

Transpilamos o circuito sem otimização (optimization_level=0) e com otimização (optimization_level=2). Como você pode ver, há uma grande diferença no comprimento dos circuitos transpilados.

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:

Saída da célula de código anterior

optimization_level=2:

Saída da célula de código anterior

# 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",
],
)

Saída da célula de código anterior

4.3 A síntese de circuitos faz diferença

Em seguida, comparamos os resultados da execução de dois circuitos de preparação do estado GHZ de 5 qubits (12(00000+11111)\frac{1}{\sqrt{2}} \left( |00000\rangle + |11111\rangle \right)) sintetizados de formas diferentes.

# Original GHZ circuit (naive synthesis)
ghz_circ.draw("mpl")

Saída da célula de código anterior

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

Saída da célula de código anterior

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:

Saída da célula de código anterior

new synthesis:

Saída da célula de código anterior

# 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",
],
)

Saída da célula de código anterior

4.4 Decomposição geral de portas de 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")

Saída da célula de código anterior

transpile(qc, basis_gates=["rz", "sx"]).draw(output="mpl")

Saída da célula de código anterior

4.5 Otimização de blocos de 1 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")

Saída da célula de código anterior

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

Saída da célula de código anterior

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 Decomposição de Toffoli

qc = QuantumCircuit(3)
qc.ccx(0, 1, 2)
qc.draw(output="mpl")

Saída da célula de código anterior

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

Saída da célula de código anterior

4.7 Decomposição da porta 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")

Saída da célula de código anterior

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

Saída da célula de código anterior

4.8 CX, ECR e CZ são iguais a menos de Cliffords locais

Note que HH (Hadamard), SS (rotação Z de π/2\pi/2), SS^\dagger (rotação Z de π/2-\pi/2) e XX (Pauli X) são todas portas de Clifford.

qc = QuantumCircuit(2)
qc.cx(0, 1)
qc.draw(output="mpl", style="bw")

Saída da célula de código anterior

qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["x", "s", "h", "sdg", "ecr"]).draw(output="mpl", style="bw")

Saída da célula de código anterior

qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["h", "cz"]).draw(output="mpl", style="bw")

Saída da célula de código anterior

Usando as portas de base de 1 qubit do backend IBM: "rz", "sx" e "x".

qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["rz", "sx", "x", "ecr"]).draw(output="mpl", style="bw")

Saída da célula de código anterior

qc = QuantumCircuit(2)
qc.cx(0, 1)
transpile(qc, basis_gates=["rz", "sx", "x", "cz"]).draw(output="mpl", style="bw")

Saída da célula de código anterior

# Check Qiskit version
import qiskit

qiskit.__version__
'2.0.2'