Pular para o conteúdo principal

Introdução ao Qiskit

Neste notebook exploraremos como podemos programar portas quânticas e circuitos quânticos com Qiskit e até mesmo como podemos executá-los em simuladores e computadores quânticos reais usando Qiskit patterns. Mais tarde introduziremos diferentes formas de codificar informação e finalizaremos com um exemplo bônus de Teleporte Quântico.

Antes de começar

Siga as instruções de Install and set up se ainda não o fez, incluindo os passos para Set up to use IBM Quantum™ Platform.

Recomenda-se usar o ambiente de desenvolvimento Jupyter para interagir com computadores quânticos. Certifique-se de instalar o suporte de visualização extra recomendado ('qiskit[visualization]'). Você também precisará do pacote matplotlib para a segunda parte deste exemplo.

Para aprender sobre computação quântica em geral, visite o Curso Basics of quantum information no IBM Quantum Learning

Imports

# Added by doQumentation — required packages for this notebook
!pip install -q matplotlib numpy qiskit qiskit-aer qiskit-ibm-runtime
# Import necessary modules for this notebook
import time
import qiskit

from qiskit import QuantumCircuit
from qiskit.quantum_info import Statevector
from qiskit.visualization import plot_bloch_multivector, plot_state_qsphere
from qiskit_aer import AerSimulator
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import EstimatorV2 as Estimator
from qiskit_ibm_runtime import SamplerV2 as Sampler
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.visualization import plot_histogram
print(qiskit.__version__)
2.3.1

Para executar seus circuitos quânticos em hardware você precisa primeiro configurar sua conta. Você pode fazer isso da seguinte forma:

  1. Acesse a IBM Quantum® Platform atualizada.
  2. Vá para o canto superior direito (como mostrado na figura acima), crie seu token de API e copie-o para um local seguro.
  3. Na próxima célula, substitua deleteThisAndPasteYourAPIKeyHere pela sua chave de API.
  4. Vá para o canto inferior esquerdo (como mostrado na figura acima) e crie sua instância. Certifique-se de escolher o plano open.
  5. Após a instância ser criada, copie o código CRN associado a ela. Você pode precisar atualizar a página para ver a instância.
  6. Na célula abaixo, substitua deleteThisAndPasteYourCRNHere pelo seu código CRN.

Veja este guia para mais detalhes sobre como configurar sua conta IBM Cloud®.

⚠️ Nota: Trate sua chave de API como você trataria uma senha segura. Veja o guia Cloud setup para mais informações sobre como usar sua chave de API em ambientes seguros e não confiáveis.

#your_api_key = "deleteThisAndPasteYourAPIKeyHere"
#your_crn = "deleteThisAndPasteYourCRNHere"

QiskitRuntimeService.save_account(
channel="ibm_quantum_platform",
token=your_api_key,
instance=your_crn,
overwrite=True
)

1. Portas quânticas e circuitos quânticos

Circuitos quânticos são modelos para computação quântica nos quais uma computação é uma sequência de portas quânticas. Vamos dar uma olhada em algumas das portas quânticas populares.

Porta X

Uma porta X equivale a uma rotação em torno do eixo X da esfera de Bloch por π\pi radianos. Ela mapeia 0|0\rangle para 1|1\rangle e 1|1\rangle para 0|0\rangle. É o equivalente quântico da porta NOT para computadores clássicos e às vezes é chamada de bit-flip.

X=(0110)X = \begin{pmatrix} 0 & 1 \\ 1 & 0 \\ \end{pmatrix}

# Let's apply an X-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.draw(output='mpl')

Quantum circuit diagram

# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

Code output

Porta H

Uma porta Hadamard representa uma rotação de π\pi em torno do eixo que está no meio do eixo XX e do eixo ZZ. Ela mapeia o estado base 0|0\rangle para 0+12\frac{|0\rangle + |1\rangle}{\sqrt{2}}, o que significa que uma medição terá probabilidades iguais de ser 1 ou 0, criando uma 'superposição' de estados. Este estado também é escrito como +|+\rangle.

H=12(1111)H = \frac{1}{\sqrt{2}}\begin{pmatrix} 1 & 1 \\ 1 & -1 \\ \end{pmatrix}

# Let's apply an H-gate on a |0> qubit
qc = QuantumCircuit(1)
qc.x(0)
qc.h(0)
qc.draw(output='mpl')

Quantum circuit diagram

# Let's see Bloch sphere visualization
sv = Statevector(qc)
plot_bloch_multivector(sv)

Code output

Porta CX (porta CNOT)

A porta NOT controlada (ou CNOT ou CX) atua em dois qubits. Ela executa a operação NOT (equivalente a aplicar uma porta X) no segundo qubit somente quando o primeiro qubit está em 1|1\rangle e, caso contrário, deixa-o inalterado. Nota: o Qiskit numera os bits em uma string da direita para a esquerda.

CX=(1000010000010010)CX = \begin{pmatrix} 1 & 0 & 0 & 0\\ 0 & 1 & 0 & 0\\ 0 & 0 & 0 & 1\\ 0 & 0 & 1 & 0\\ \end{pmatrix}

# Let's apply a CX-gate on |11>
qc = QuantumCircuit(2)
qc.x(0)
qc.x(1)
qc.cx(0,1)
qc.draw(output='mpl')

Quantum circuit diagram

sv=Statevector(qc)
plot_state_qsphere(sv)

Code output

Crie o primeiro estado de Bell

ϕ+=12(00+11)|\phi^+ \rangle = \frac{1}{\sqrt 2}(|00 \rangle + |11 \rangle)

# Create a Bell state circuit

qc = QuantumCircuit(2)
qc.h(0)
qc.cx(0,1)

# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

# Plot the state using q-sphere visualization
sv = Statevector(qc)
plot_state_qsphere(sv)
# q-sphere is useful for visualizing states when Bloch sphere fails to

Code output

Crie o segundo estado de Bell

ϕ=12(0011)|\phi^- \rangle = \frac{1}{\sqrt 2}(|00 \rangle - |11 \rangle)

# Create a circuit with the second Bell state

qc = QuantumCircuit(2)
qc.x(0)
qc.h(0)
qc.cx(0,1)

qc.draw("mpl")

Quantum circuit diagram

A explicação é que:

H1=12(01)=H|1\rangle=\frac{1}{\sqrt{2} }(|0\rangle-|1\rangle) = |-\rangle
# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

Crie o estado GHZ de 3 qubits

GHZ=12(000+111)|GHZ \rangle = \frac{1}{\sqrt 2}(|000 \rangle + |111 \rangle)

# Create a circuit with 3-qubit GHZ state

qc= QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)

qc.draw("mpl")

Quantum circuit diagram

# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

Crie o estado do logo do Qiskit

Qiskit=12(0010+1101)|Qiskit \rangle = \frac{1}{\sqrt 2}(|0010 \rangle + |1101 \rangle)

Centered Image
# Create a circuit with the Qiskit logo state

qc = QuantumCircuit(4)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
qc.cx(0,3)
qc.x(1)

# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

# Get the statevector of the circuit
sv = Statevector(qc)

# Plot the state using qsphere visualization
plot_state_qsphere(sv)

Quantum circuit diagram

2. Crie e execute um programa quântico simples

Os quatro passos para escrever um programa quântico usando Qiskit patterns são:

  1. Mapear o problema para um formato quântico nativo.

  2. Otimizar os circuitos e operadores.

  3. Executar usando uma função primitiva quântica.

  4. Analisar os resultados.

2.1 Mapear o problema para um formato quântico nativo

Em um programa quântico, circuitos quânticos são o formato nativo no qual representar instruções quânticas, e operadores representam os observáveis a serem medidos. Ao criar um circuito, você normalmente criará um novo objeto QuantumCircuit e, em seguida, adicionará instruções a ele em sequência.

A célula de código a seguir cria um circuito que produz o estado GHZ, que é um estado em que três qubits estão totalmente emaranhados entre si.

O Qiskit SDK usa a numeração de bits LSb 0 onde o nthn^{th} dígito tem valor 1n1 \ll n ou 2n2^n. Para mais detalhes, veja o tópico Bit-ordering in the Qiskit SDK.

# Create a GHZ state circuit

qc = QuantumCircuit(3)
qc.h(0)
qc.cx(0,1)
qc.cx(0,2)
# Draw the circuit
qc.draw("mpl")

Quantum circuit diagram

Veja QuantumCircuit na documentação para todas as operações disponíveis.

Ao criar circuitos quânticos, você também deve considerar que tipo de dado deseja receber após a execução. O Qiskit fornece duas formas de retornar dados: você pode obter uma distribuição de probabilidade para um conjunto de qubits que escolher medir, ou pode obter o valor esperado de um observável. Prepare sua carga de trabalho para medir seu circuito de uma dessas duas maneiras com as primitivas Qiskit (explicadas em detalhes no Passo 3).

Este exemplo mede valores esperados usando o submódulo qiskit.quantum_info, que é especificado usando operadores (objetos matemáticos usados para representar uma ação ou processo que muda um estado quântico). A célula de código a seguir cria seis operadores de Pauli de três qubits: ZZZ, ZZX, ZII, XXI, ZZI e III.

# Set up six different observables.

observables_labels = ["ZZZ", "ZZX", "ZII", "XXI", "ZZI", "III"]

observables = [SparsePauliOp(label) for label in observables_labels]
print(observables)
[SparsePauliOp(['ZZZ'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZX'],
coeffs=[1.+0.j]), SparsePauliOp(['ZII'],
coeffs=[1.+0.j]), SparsePauliOp(['XXI'],
coeffs=[1.+0.j]), SparsePauliOp(['ZZI'],
coeffs=[1.+0.j]), SparsePauliOp(['III'],
coeffs=[1.+0.j])]

Aqui, algo como o operador ZZI é uma forma abreviada para o produto tensorial ZZIZ\otimes Z\otimes I, que significa medir Z no qubit 2 e Z no qubit 1 juntos, e obter informação sobre a correlação entre o qubit 2 e o qubit 1. Valores esperados como este também são tipicamente escritos como Z2Z1\langle Z_2 Z_1 \rangle.

Se o estado que observamos é o estado GHZ de três qubits, então a medição de Z2Z1\langle Z_2 Z_1 \rangle deve ser 1.

2.2 Otimizar os circuitos e operadores

Ao executar circuitos em um dispositivo, é importante otimizar o conjunto de instruções que o circuito contém e minimizar a profundidade geral (aproximadamente o número de instruções) do circuito. Isso garante que você obtenha os melhores resultados possíveis reduzindo os efeitos de erro e ruído. Além disso, as instruções do circuito devem estar em conformidade com a Instruction Set Architecture (ISA) do dispositivo backend e devem considerar as portas de base e a conectividade dos qubits do dispositivo.

O código a seguir instancia um dispositivo real para o qual enviar um job e transforma o circuito e os observáveis para corresponder à ISA daquele backend. Se você ainda não salvou suas credenciais, siga as instruções aqui para se autenticar com seu token de API.

# Choose a real backend
service = QiskitRuntimeService(channel='ibm_quantum_platform',)
backend = service.least_busy(min_num_qubits=156)
# print backend details
print(
f"Name: {backend.name}\n"
f"Version: {backend.backend_version}\n"
f"No. of qubits: {backend.num_qubits}\n"
f"Processor type: {backend.processor_type}\n"
)
Name: ibm_marrakesh
Version: 1.0.21
No. of qubits: 156
Processor type: {'family': 'Heron', 'revision': '2'}
# option to use the AerSimulator instead of a real quantum device
seed_sim=42
backend=AerSimulator.from_backend(backend,seed_simulator=seed_sim)

Faça a transpilação do circuito em um circuito ISA

# Convert to an ISA circuit and layout-mapped observables.

pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc)

isa_circuit.draw("mpl", idle_wires=False)

Quantum circuit diagram

mapped_observables = [
observable.apply_layout(isa_circuit.layout) for observable in observables
]
print(mapped_observables)
[SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIZIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIXIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIXIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIZIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j]), SparsePauliOp(['IIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIIII'],
coeffs=[1.+0.j])]

2.3 Executar usando as primitivas quânticas

Computadores quânticos podem produzir resultados aleatórios, portanto, você geralmente coleta uma amostra das saídas executando o circuito muitas vezes. Você pode estimar o valor do observável usando a classe Estimator. O Estimator é uma das duas primitivas; a outra é Sampler, que pode ser usada para obter dados de um computador quântico. Esses objetos possuem um método run() que executa a seleção de circuitos, observáveis e parâmetros (se aplicável), usando um primitive unified bloc (PUB). Ao executar este código em hardware quântico real, considere aplicar técnicas de mitigação e supressão de erros para reduzir o ruído intrínseco do computador quântico.

# Construct the Estimator instance.
estimator = Estimator(mode=backend)
estimator.options.resilience_level = 1
estimator.options.default_shots = 5000

Envie um job usando a primitiva Estimator.

# One pub, with one circuit to run against six different observables.
job = estimator.run([(isa_circuit, mapped_observables)])

# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job.job_id()}")
>>> Job ID: 97ecd036-1767-49b0-a1dc-c71638c3c3c4
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")

Após um job ser enviado, você pode esperar até que o job seja concluído dentro da sua instância python atual, ou usar o job_id para recuperar os dados em um momento posterior. (Veja a seção sobre como recuperar jobs para detalhes.)

Após o job ser concluído, examine sua saída através do atributo result() do job.

# This is the result of the entire submission.  You submitted one Pub,
# so this contains one inner result (and some metadata of its own).
job_result = job.result()

# This is the result from our single pub, which had six observables,
# so contains information on all six.
pub_result = job.result()[0]

Agora também podemos executar o circuito usando a primitiva Sampler

# We include the measurements in the circuit
qc.measure_all()
sampler = Sampler(mode=backend)
qc.draw(output="mpl")

Quantum circuit diagram

Envie um job usando a primitiva Sampler.

job_sampler = sampler.run(pm.run([qc]))

# Use the job ID to retrieve your job data later
print(f">>> Job ID: {job_sampler.job_id()}")
# Get the results
results_sampler = job_sampler.result()
>>> Job ID: a6ee4d2f-c80d-4a86-9a76-e4b1a74502e7

2.4 Analisar os resultados

A etapa de análise é tipicamente onde você pode pós-processar seus resultados usando, por exemplo, mitigação de erro de medição ou extrapolação de ruído zero (ZNE). Você pode alimentar esses resultados em outro fluxo de trabalho para análise adicional ou preparar um gráfico dos valores e dados principais. Em geral, este passo é específico para o seu problema. Para este exemplo, plote cada um dos valores esperados que foram medidos para o nosso circuito.

Os valores esperados e desvios padrão para os observáveis que você especificou ao Estimator são acessados através dos atributos PubResult.data.evs e PubResult.data.stds do resultado do job. Para obter os resultados do Sampler, use a função PubResult.data.meas.get_counts(), que retornará um dict de medições na forma de bitstrings como chaves e contagens como seus valores correspondentes. Para mais informações, veja Get started with Sampler.

# Plot the result
from matplotlib import pyplot as plt
values = pub_result.data.evs
errors = pub_result.data.stds
# plotting graph
# Plotting with error bars
plt.errorbar(observables_labels, values, yerr=errors, fmt='-o', capsize=5)
plt.xlabel("Observables")
plt.ylabel("Values")
plt.title("Plot of Observables vs Values with Error Bars")
plt.grid(True)
plt.tight_layout()
plt.show()

Plot output

Vemos que os observáveis ZZIZZI e IIIIII têm um valor esperado de 1, já que ZZIZZI introduz dois sinais de menos que se cancelam, e IIIIII atua como a identidade, deixando o estado GHZ inalterado. O resto dos observáveis tem um valor esperado de 0, já que seus operadores ZZ introduzem um número ímpar de sinais de menos, ou os operadores XX invertem um número de qubits que torna os estados sobrepostos ortogonais.

Agora plotamos os resultados para o Sampler

counts_list = results_sampler[0].data.meas.get_counts()
print(counts_list)
print(f"Outcomes : {counts_list}")
display(plot_histogram(counts_list, title="GHZ state"))
{'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}
Outcomes : {'111': 480, '000': 503, '101': 8, '100': 9, '001': 3, '011': 6, '010': 10, '110': 5}

Code output

2.5 Escalar para grandes números de qubits

Em computação quântica, o trabalho em escala de utilidade é crucial para fazer progresso na área. Tal trabalho requer que computações sejam feitas em uma escala muito maior; trabalhando com circuitos que podem usar mais de 100 qubits e mais de 1000 portas. Este exemplo dá um pequeno passo nessa direção, escalando o problema GHZ para n=10n=10 qubits. Ele usa o fluxo de trabalho dos Qiskit patterns e termina medindo o valor esperado Z0Zi\langle Z_0 Z_i \rangle .

Passo 1. Mapear o problema

Escreva uma função que retorna um QuantumCircuit que prepara um estado GHZ de nn qubits (essencialmente um estado de Bell estendido), em seguida, use essa função para preparar um estado GHZ de 10 qubits e colete os observáveis a serem medidos.

def get_qc_for_n_qubit_GHZ_state(n: int) -> QuantumCircuit:

qc = QuantumCircuit(n)
qc.h(0)
for i in range(n-1):
qc.cx(i, i+1)
return qc
n = 10
qc_n_GHZ = get_qc_for_n_qubit_GHZ_state(n)
qc_n_GHZ.draw("mpl")

Quantum circuit diagram

Em seguida, mapeie para os operadores de interesse. Este exemplo usa os operadores ZZ entre os qubits para examinar o comportamento à medida que se distanciam. Valores esperados cada vez mais imprecisos (corrompidos) entre qubits distantes revelariam o nível de ruído presente.

# ZZII...II, ZIZI...II, ... , ZIII...IZ
operator_strings = [
"Z" + i * "I" + "Z" + "I" * (n-i-2) for i in range(n-1)
]
print(operator_strings)
print(len(operator_strings))

operators = [SparsePauliOp(operator) for operator in operator_strings]
['ZZIIIIIIII', 'ZIZIIIIIII', 'ZIIZIIIIII', 'ZIIIZIIIII', 'ZIIIIZIIII', 'ZIIIIIZIII', 'ZIIIIIIZII', 'ZIIIIIIIZI', 'ZIIIIIIIIZ']
9

Passo 2. Otimizar o problema para execução no backend quântico

Transforme o circuito e os observáveis para corresponder à ISA do backend.

# Convert to an ISA circuit and layout-mapped observables.
pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
isa_circuit = pm.run(qc_n_GHZ)
isa_operators_list = [operator.apply_layout(isa_circuit.layout) for operator in operators]

Passo 3. Executar no backend

Envie o job e, se você o executar em hardware, habilite a supressão de erros usando uma técnica para reduzir erros chamada dynamical decoupling. O nível de resiliência especifica quanta resiliência construir contra erros. Níveis mais altos geram resultados mais precisos, em troca de tempos de processamento mais longos. Para uma explicação adicional das opções definidas no código a seguir, veja Configure error mitigation for Qiskit Runtime.

# Submit the circuit to Estimator
job = estimator.run([(isa_circuit, isa_operators_list)])
job_id = job.job_id()
/Users/jma/miniconda3/envs/3122/lib/python3.12/site-packages/qiskit_ibm_runtime/fake_provider/local_service.py:187: UserWarning: The resilience_level option has no effect in local testing mode.
warnings.warn("The resilience_level option has no effect in local testing mode.")

Passo 4. Pós-processar os resultados

Para entender melhor o comportamento de estados quânticos emaranhados em hardware real, analisamos as correlações pareadas entre qubits na base Z. Especificamente, olhamos para os valores esperados ⟨Z₀Zᵢ⟩, que medem quão fortemente o qubit 0 está correlacionado com cada outro qubit i. Em particular vamos plotar:

ZiZ0/Z1Z0\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle

Quais valores de ZiZ0/Z1Z0\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle você espera ver no gráfico?

Opções:

a) Decrescente à medida que aumentamos ii

b) Constante em 1

c) Pequenos desvios em torno de 1

d) Alternando 1 e 0 para valores ímpares e pares de ii

data = list(range(1, len(operators) + 1))  # Distance between the Z operators
result = job.result()[0]
values = result.data.evs # Expectation value at each Z operator.
values = [
v / values[0] for v in values
] # Normalize the expectation values to evaluate how they decay with distance.

plt.plot(data, values, marker="o", label=f"{n}-qubit GHZ state")
plt.xlabel("Distance between qubits $i$")
plt.ylabel(r"$\langle Z_i Z_0 \rangle / \langle Z_1 Z_0 \rangle $")
plt.legend()
plt.show()

Plot output

Neste gráfico notamos que Z0Zi\langle Z_0 Z_i \rangle flutua em torno do valor 1, embora em uma simulação ideal todos os Z0Zi\langle Z_0 Z_i \rangle deveriam ser 1.

Como você pode ver, os resultados de experimentos com 10 qubits são bons, mas ainda têm alguns erros. Uma forma de melhorar os resultados é implementar o estado GHZ de forma mais eficiente.

Geralmente, o estado GHZ é implementado com uma sequência de portas CNOT em forma de escada. No entanto, você pode implementar o estado GHZ de forma mais eficiente, reduzindo a profundidade de 2 qubits de n para n/2 ou menos.

Uma métrica importante para avaliar quão precisos serão os resultados, ou quanto pouco ruído um circuito terá, é a profundidade da porta de 2 qubits. Isso ocorre porque as taxas de erro para portas de 2 qubits (~10 vezes maiores que portas de um único qubit) dominam os erros de todo o circuito. Use o seguinte código para obter a profundidade da porta de 2 qubits de um circuito.

qc.depth(lambda x: x.operation.num_qubits == 2)
def better_ghz(n):
"fan out"
s = int(n / 2)
qc = QuantumCircuit(n)
qc.h(s)
for m in range(s, 0, -1):
qc.cx(m, m - 1)
if not (n % 2 == 0 and m == s):
qc.cx(n - m - 1, n - m)
return qc

better_ghz(n).draw("mpl")

Quantum circuit diagram

# Check 2-qubit gate depth before transpilation
qc_better_ghz = better_ghz(n)
qc_better_ghz.depth(lambda x: x.operation.num_qubits == 2)
5

Uma coisa interessante a notar aqui é que conseguimos reduzir a profundidade quântica do circuito que queremos executar apenas sendo inteligentes e pensando em uma maneira diferente de programá-lo. No entanto, haverá situações e algoritmos em que não podemos confiar nesses truques inteligentes. É aqui que o transpilador é útil, ele nos ajuda a otimizar todos esses aspectos eficientemente, então não precisamos nos preocupar muito com eles.

3. Codificando informação

3.1 Codificação de amplitude

Agora que vimos como construir circuitos quânticos, é interessante explorar como podemos codificar informação clássica em estados quânticos. Um método poderoso é a codificação de amplitude, onde as amplitudes de um estado quântico representam os componentes de um vetor clássico.

Vamos considerar um exemplo simples. Suponha que queremos codificar o vetor clássico

x=[x0x1x2x3]\vec{x} = \begin{bmatrix} x_0 \\ x_1 \\ x_2 \\ x_3 \end{bmatrix}

em um estado quântico de dois qubits. O objetivo é preparar o estado quântico:

ψ=x000+x101+x210+x311\ket{\psi} = x_0\ket{00} + x_1\ket{01} + x_2\ket{10} + x_3\ket{11}

onde x0,x1,x2,x3Rx_0, x_1, x_2, x_3 \in \mathbb{R} (ou C\mathbb{C}) e o vetor é normalizado de tal forma que:

x02+x12+x22+x32=1|x_0|^2 + |x_1|^2 + |x_2|^2 + |x_3|^2 = 1

Agora consideramos o exemplo particular: x=[0.8924,0.3696,0.2391,0.0990]\vec{x} = [0.8924, 0.3696, 0.2391, 0.0990]

Então o estado quântico correspondente é:

ψ=0.892400+0.369601+0.239110+0.099011\begin{aligned} \ket{\psi} &= 0.8924\,\ket{00} + 0.3696\,\ket{01} + 0.2391\,\ket{10} + 0.0990\,\ket{11} \end{aligned}

Este estado pode ser preparado usando uma combinação de portas de rotação RyR_y de ângulos π/6\pi/6 e π/4\pi/4 para os qubits 0 e 1, respectivamente

from qiskit import QuantumCircuit
from qiskit_aer import AerSimulator
import numpy as np

qc = QuantumCircuit(2)

qc.ry(np.pi / 6, 0)
qc.ry(np.pi / 4, 1)

simulator = AerSimulator()
qc.save_statevector()
result = simulator.run(qc).result()
statevector = result.get_statevector()

print("Statevector:", statevector)
qc.draw(output="mpl")
Statevector: Statevector([0.8923991 +0.j, 0.23911762+0.j, 0.36964381+0.j,
0.09904576+0.j],
dims=(2, 2))

Quantum circuit diagram

from qiskit.quantum_info import Statevector

# Define our vector
v = np.array([0.8924, 0.3696, 0.2391, 0.0990])
v = v/np.linalg.norm(v)
# Create a statevector from the vector
state = Statevector(v)

# Initialize a quantum circuit with 2 qubits
qc = QuantumCircuit(2)
qc.initialize(state.data, [0, 1])

# Optional: simulate the state
print("Statevector:", state)

# Visualize the circuit
qc.decompose().decompose().decompose().decompose().decompose().draw("mpl")
Statevector: Statevector([0.89242154+0.j, 0.36960892+0.j, 0.23910577+0.j,
0.09900239+0.j],
dims=(2, 2))

Quantum circuit diagram

Portanto, vimos como codificar informação usando portas rotacionais.

3.2 Codificação de ângulo e circuitos parametrizados

Uma maneira particularmente interessante de codificar informação em um computador quântico é projetar um circuito quântico que contém alguns ângulos rotacionais θ\vec{\theta} ou parâmetros que podem ser ajustados para representar uma família de funções f(θ)f(\vec{\theta}). Vamos, por exemplo, considerar o seguinte circuito quântico parametrizado:

from qiskit import QuantumCircuit
from qiskit.circuit import Parameter

# Define a symbolic parameter
theta = Parameter("θ")

qc = QuantumCircuit(2)
# We applied a parametrized RX gate
qc.rx(theta, 0)
qc.cx(0, 1)
qc.draw("mpl")

Quantum circuit diagram

Matematicamente, podemos analisar qual é a família de funções que podemos representar com este circuito:

CNOT01Rx{0}(θ)00=CNOT01(cos(θ/2)00isin(θ/2)10)=cos(θ/2)00isin(θ/2)11\text{CNOT}_{01} \, R_x^{\{0\}}(\theta) |00\rangle = \text{CNOT}_{01} \left( \cos(\theta/2)\ket{00} - i\sin(\theta/2)\ket{10} \right) = \cos(\theta/2)\ket{00} - i\sin(\theta/2)\ket{11}

É bastante claro que o número de estados que podemos representar com este circuito quântico é limitado, pois não podemos representar os estados 10\ket{10} ou 01\ket{01} por exemplo. No entanto, a família de estados que podemos representar começa a crescer quando introduzimos mais rotações nos lugares adequados:

from qiskit import QuantumCircuit
from qiskit.circuit import Parameter

# Define a symbolic parameter
theta1 = Parameter("θ1")
theta2 = Parameter("θ2")

qc = QuantumCircuit(2)
qc.rx(theta1, 0)
qc.rx(theta2, 1)
qc.cx(0, 1)
qc.draw("mpl")

Quantum circuit diagram

Neste caso, os estados quânticos que representaremos são:

\begin{align*} \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2) R_x^{\{0}}(\theta_1) \ket{00} &= \text{CNOT}_{01} \, R_x^{\{1}}(\theta_2)\left( \cos(\theta_1/2)\ket{00} - i\sin(\theta_1/2)\ket{10} \right) \\ &= \text{CNOT}_{01}\left( \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \right. \\ &\quad \left. - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{10} + \sin(\theta_1/2)\sin(\theta_2/2)\ket{11} \right) \\ &= \cos(\theta_1/2)\cos(\theta_2/2)\ket{00} - i\cos(\theta_1/2)\sin(\theta_2/2)\ket{01} \\ &\quad + \sin(\theta_1/2)\sin(\theta_2/2)\ket{10} - i\sin(\theta_1/2)\cos(\theta_2/2)\ket{11} \end{align*}

Podemos ver que este circuito gera uma família mais ampla de estados quânticos comparado ao anterior. Em particular, ele agora pode produzir estados com amplitudes diferentes de zero para 01\ket{01} ou 10\ket{10} que não eram possíveis com o circuito acima. No entanto, este circuito ainda não é um gerador universal de estados quânticos, embora possa ser suficientemente expressivo para projetar circuitos com alguma flexibilidade para representar determinadas funções. Em geral, quanto mais parâmetros independentes (ângulos) introduzimos, mais expressividade o circuito tem para aproximar estados quânticos arbitrários.

Ansatzes e biblioteca de circuitos

Esse tipo de circuito quântico parametrizado pode ser usado para construir Ansatzes, estados quânticos de tentativa que visam aproximar a solução de um problema. Esses Ansatzes são um componente central dos Algoritmos quânticos variacionais, uma classe de algoritmos híbridos quântico-clássicos que usam um computador quântico para avaliar uma função de custo e um otimizador clássico para minimizá-la. Entraremos em detalhes sobre esses tópicos em uma Unidade posterior, mas por enquanto, introduziremos como construir um ansatz simples usando a Circuit library no Qiskit.

from qiskit.circuit.library import efficient_su2

SU2_ansatz = efficient_su2(4, su2_gates=["rx", "y"], entanglement="linear", reps=1)
SU2_ansatz.decompose().draw(output="mpl")

Quantum circuit diagram

Vimos como construir um Ansatz simples usando a função efficient_su2 da qiskit.circuit.library que será capaz de gerar uma ampla gama de estados quânticos ajustando seus parâmetros θ\vec{\theta}.

Conclusão

Neste notebook, você aprendeu como construir circuitos quânticos, desde construir portas quânticas até definir e medir observáveis, e como executar esses circuitos eficientemente tanto em simuladores quanto em hardware quântico real. Você também viu a importância de um design cuidadoso de circuito para minimizar erros ao trabalhar com dispositivos quânticos reais, bem como estratégias para escalar circuitos para um número maior de qubits, particularmente através do exemplo do estado GHZ. Além disso, você explorou diferentes técnicas para codificar informação clássica em estados quânticos, incluindo codificação de amplitude e codificação de ângulo. Com tudo isso, você está totalmente equipado para passar para a próxima sessão e começar a trabalhar com algoritmos quânticos.

Instalando o Qiskit Code Assistant no VSCode

Clique no link e siga as instruções.

Bônus: Teleporte quântico

Quando você ouve o termo teleporte quântico, pode imaginar uma tecnologia futurista de ficção científica que desintegra um objeto em um lugar e o faz reaparecer em algum lugar distante. Mas o teleporte quântico não é nada disso. Na realidade, o que é teletransportado não é matéria, é informação.

O teleporte quântico permite a transferência do estado quântico de um qubit de um local para outro. Embora essa transferência pareça instantânea, ela não viola as leis da física. Como isso é possível? Vamos investigar!

O teleporte quântico é um protocolo que permite a um remetente (Alice) transmitir o estado ψ|\psi\rangle de um qubit q para um destinatário (Bob) usando dois recursos chave: um par de qubits emaranhados compartilhados a e b e dois bits de comunicação clássica c0 e c1.

Essencialmente, o que o protocolo precisa é:

  • q: O qubit de Alice, inicialmente no estado ψ|\psi\rangle que queremos teletransportar.
  • a: A metade de Alice de um par emaranhado compartilhado.
  • b: A metade de Bob do par emaranhado compartilhado.
  • c0, c1: Bits clássicos para armazenar os resultados de medição de Alice.

E como ele funciona? O fluxo de trabalho é o seguinte

  1. Prepare o estado ψ|\psi\rangle de Alice em q. Criaremos um estado específico como +|+\rangle para verificação.
  2. Crie emaranhamento: Gere um par de Bell entre a e b.
  3. Operações de Alice: Alice realiza uma "medição de Bell" em seus dois qubits (q e a) e armazena os resultados clássicos em c0 e c1.
  4. Comunicação clássica: Alice envia seus dois bits clássicos (c0, c1) para Bob.
  5. Correções de Bob: Bob aplica portas quânticas específicas (X e/ou Z) ao seu qubit (b), condicionado aos valores de c0 e c1 que ele recebeu.

Se tudo for feito corretamente, o qubit b de Bob terminará no estado ψ|\psi\rangle, o estado original do q de Alice!

Para uma explicação mais aprofundada e exploração do teleporte quântico, incluindo a explicação matemática de por que este protocolo funciona, você pode consultar os recursos do IBM Quantum Learning: Quantum Teleportation. Isso é parte do curso Basics of Quantum Information.


import matplotlib.pyplot as plt
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram, plot_bloch_multivector

# Define individual quantum registers for each qubit
q = QuantumRegister(1, name='q') # message qubit
a = QuantumRegister(1, name='a') # Alice's entangled qubit
b = QuantumRegister(1, name='b') # Bob's entangled qubit

# Classical register for Alice's measurements
cr_alice = ClassicalRegister(2, name='c_alice')

# Create quantum circuit
teleport_qc = QuantumCircuit(q, a, b, cr_alice, name='Teleportation')

# Step 1: Prepare message state |+⟩ on q
teleport_qc.h(q[0])
teleport_qc.barrier()

# Step 2: Create entanglement between a and b
teleport_qc.h(a[0])
teleport_qc.cx(a[0], b[0])
teleport_qc.barrier()

# Step 3: Alice's Bell measurement
teleport_qc.cx(q[0], a[0])
teleport_qc.h(q[0])
teleport_qc.barrier()

# Step 4: Alice measures q and a
teleport_qc.measure(q[0], cr_alice[0])
teleport_qc.measure(a[0], cr_alice[1])
teleport_qc.barrier()

# Step 5: Bob's conditional measurements
with teleport_qc.if_test((cr_alice[1], 1)):
teleport_qc.x(b[0])
with teleport_qc.if_test((cr_alice[0], 1)):
teleport_qc.z(b[0])

# Draw the circuit
teleport_qc.draw(output='mpl')

Quantum circuit diagram

Após executar o protocolo, surge uma pergunta-chave: como verificamos que o teleporte funcionou? Não podemos 'ver' diretamente o estado do qubit de Bob após o protocolo. No entanto, como nós preparamos o estado inicial ψ|\psi\rangle de Alice (escolhemos +|+\rangle), podemos usar um tipo especial de simulação para verificar se o qubit b de Bob terminou nesse mesmo estado.

Usaremos o AerSimulator com save_statevector para verificar se o qubit b de Bob termina no estado original de Alice (+|+\rangle). Este simulador calcula o vetor de estado quântico final. e, em seguida, o representa usando plot_bloch_multivector para visualizar o qubit de Bob (b) comparado com o estado inicial de Alice (q).

# Simulate the teleportation circuit
sv_simulator = AerSimulator(method='statevector')
teleport_qc_sv = teleport_qc.copy()
teleport_qc_sv.save_statevector()

# Execute the circuit on the statevector simulator
job_sv = sv_simulator.run(teleport_qc_sv)
result_sv = job_sv.result()

# Get the final statevector
final_statevector = result_sv.get_statevector()
print("Visualizing final qubit states:")
display(plot_bloch_multivector(final_statevector))
print("Note that Alice's qubits have collapsed to |00⟩, |01⟩, |10⟩, or |11⟩, while Bob's qubit is in the original state |+⟩.")
Visualizing final qubit states:

Quantum circuit diagram

Note that Alice's qubits have collapsed to |00⟩, |01⟩, |10⟩, or |11⟩, while Bob's qubit is in the original state |+⟩.

Como podemos ver na visualização, os dois primeiros qubits (pertencentes a Alice) entraram em colapso para 0 ou 1. Enquanto isso, o terceiro qubit (pertencente a Bob), representado na terceira esfera de Bloch, aponta ao longo do eixo x, indicando que ele está no estado +|+\rangle, então implementamos com sucesso o protocolo de teleporte quântico!

Resumo

Neste ponto, é conveniente fazer um rápido resumo do que realizamos:

  • Alice transmitiu um estado quântico desconhecido para Bob.
  • Nenhuma partícula física foi transferida.
  • O estado original no qubit de Alice é destruído, de acordo com o teorema da Não-Clonagem.