AULA 1 — IMPLEMENTAÇÃO EM QISKIT¶
Complemento prático da Aula 1: como representar e manipular os conceitos de sistemas únicos (vetores de estado, medição, operações unitárias) usando o Qiskit, a biblioteca Python da IBM para computação quântica. Baseado na página escrita "Qiskit implementation" do curso.
Os exemplos abaixo foram testados com qiskit==2.5.2. Como o Qiskit muda de versão com alguma frequência (inclusive com mudanças que quebram compatibilidade), vale sempre checar a versão instalada:
Instalação
1. Vetores e matrizes "na mão", com NumPy¶
Antes de usar as classes específicas do Qiskit, vale ver que nada impede representar tudo isso com NumPy puro — afinal, um estado quântico é, matematicamente, só um vetor coluna.
import numpy as np
ket0 = np.array([[1], [0]])
ket1 = np.array([[0], [1]])
plus = (ket0 + ket1) / np.sqrt(2)
print(plus)
# [[0.70710678]
# [0.70710678]]
H = (1 / np.sqrt(2)) * np.array([[1, 1], [1, -1]])
print(H @ ket0) # H|0> = |+>
# [[0.70710678]
# [0.70710678]]
print(H @ plus) # H|+> = |0> (a segunda entrada dá ~0, só erro de arredondamento)
# [[1.00000000e+00]
# [2.23711432e-17]]
O operador @ é o produto matricial do NumPy (equivalente a np.matmul), tanto para multiplicação matriz-matriz quanto matriz-vetor — é o que usamos aqui para aplicar \(H\) a um vetor.
Essa abordagem funciona, mas fica trabalhosa rapidamente (ler um vetor coluna "cru" não é tão legível quanto a notação de Dirac, e não há verificação automática de que algo é de fato um estado quântico válido). É aí que entram as classes do Qiskit.
2. Estados, medições e operações com qiskit.quantum_info¶
2.1 Definir e exibir vetores de estado: Statevector¶
A classe Statevector representa um vetor de estado quântico e sabe, por exemplo, verificar se ele é válido (norma euclidiana igual a 1):
from qiskit.quantum_info import Statevector
import numpy as np
v = Statevector([1, 0])
print(v)
# Statevector([1.+0.j, 0.+0.j], dims=(2,))
plus = Statevector([1 / np.sqrt(2), 1 / np.sqrt(2)])
print(plus.is_valid())
# True
nao_normalizado = Statevector([1, 1])
print(nao_normalizado.is_valid())
# False
Dentro de um notebook Jupyter, display(plus.draw("latex")) mostra o estado formatado como \(\tfrac{\sqrt2}{2}|0\rangle+\tfrac{\sqrt2}{2}|1\rangle\); fora de um notebook, draw("latex_source") devolve a string LaTeX puro:
2.2 Simulando medições: measure() e sample_counts()¶
O método measure() simula uma única medição na base padrão: devolve o resultado obtido (aleatório, de acordo com as probabilidades do estado) e o novo estado do sistema após o colapso.
outcome, post_state = plus.measure()
print(outcome, post_state)
# (por exemplo) 0 Statevector([1.+0.j, 0.+0.j], dims=(2,))
Rodar de novo pode dar 1 em vez de 0 — é aleatório, com 50% de chance cada, como esperado para o estado \(|+\rangle\). Importante: o post_state devolvido pode vir multiplicado por uma fase global (ex.: \(-|0\rangle\) em vez de \(|0\rangle\)) — como visto na Aula 1, isso representa exatamente o mesmo estado físico.
Para ver a distribuição de resultados (sem alterar o estado original), usa-se sample_counts(n), que simula n medições independentes e devolve as frequências:
counts = plus.sample_counts(4000)
print(counts)
# {'0': 2015, '1': 1985} (valores exatos variam a cada execução)
Com n grande, as frequências relativas se aproximam das probabilidades teóricas (\(1/2\) e \(1/2\)) — lei dos grandes números. qiskit.visualization.plot_histogram(counts) desenha isso como um gráfico de barras (requer matplotlib e, tipicamente, um ambiente de notebook para exibir a figura).
2.3 Operações com Operator¶
A classe Operator representa uma matriz (tipicamente unitária, mas não exclusivamente) e sabe verificar se é unitária:
from qiskit.quantum_info import Operator
H = Operator([[1 / np.sqrt(2), 1 / np.sqrt(2)], [1 / np.sqrt(2), -1 / np.sqrt(2)]])
S = Operator([[1, 0], [0, 1j]])
T = Operator([[1, 0], [0, (1 + 1j) / np.sqrt(2)]])
print(H.is_unitary())
# True
Para aplicar uma operação a um estado, usa-se Statevector.evolve:
estado = plus.evolve(T).evolve(H) # H(T|+>)
print(estado)
# Statevector([0.85355339+0.35355339j, 0.14644661-0.35355339j], dims=(2,))
Esse resultado confere exatamente com o cálculo feito "na mão" na Aula 1 para \(HT|+\rangle\): \(\left(\tfrac12+\tfrac{1+i}{2\sqrt2}\right)|0\rangle + \left(\tfrac12-\tfrac{1+i}{2\sqrt2}\right)|1\rangle \approx 0{,}854+0{,}354i\) na primeira entrada.
2.4 Um adiantamento de circuitos quânticos: QuantumCircuit¶
O Qiskit também permite montar a mesma sequência de operações como um circuito quântico — a forma como, na prática, se programa um computador quântico de verdade (circuitos são o assunto de uma lição futura, mas o Qiskit já deixa fazer essa ponte aqui):
from qiskit import QuantumCircuit
from qiskit.quantum_info import Operator, Statevector
qc = QuantumCircuit(1)
qc.h(0)
qc.t(0)
qc.h(0)
print(qc.draw(output="text"))
# ┌───┐┌───┐┌───┐
# q: ┤ H ├┤ T ├┤ H ├
# └───┘└───┘└───┘
(qc.draw(output="mpl") desenha a mesma coisa com Matplotlib, com uma estética mais parecida com a dos circuitos impressos em artigos/livros — útil em notebooks.)
A partir do circuito, dá para extrair a matriz unitária que ele representa, e aplicá-la a um estado inicial — confirmando que é exatamente a mesma conta de antes, agora partindo de \(|0\rangle\) em vez de \(|+\rangle\) (lembrando que \(|+\rangle = H|0\rangle\), e o circuito já inclui esse primeiro \(H\)):
U = Operator.from_circuit(qc)
print(U)
# Operator([[0.85355339+0.35355339j, 0.14644661-0.35355339j],
# [0.14644661-0.35355339j, 0.85355339+0.35355339j]], ...)
resultado = Statevector([1, 0]).evolve(U)
print(resultado)
# Statevector([0.85355339+0.35355339j, 0.14644661-0.35355339j], dims=(2,))
contagens = resultado.sample_counts(4000)
print(contagens)
# {'0': ~3414, '1': ~586} (≈ 85.4% / 14.6%, consistente com |amplitude|² de cada entrada)
Passo a passo: de onde vem essa matriz U, e por que U|0⟩ dá esse vetor
Por que \(U = H\,T\,H\) (nessa ordem) e não outra coisa. O circuito aplica as portas na ordem \(H\), depois \(T\), depois \(H\) de novo. Aplicar operações em sequência a um vetor \(v\) — primeiro \(H\), depois \(T\), depois \(H\) — significa calcular
Ou seja, a matriz combinada é o produto das matrizes na ordem inversa da aplicação: a primeira porta aplicada fica mais à direita no produto, a última fica mais à esquerda (a mesma regra de composição de operações unitárias vista na Aula 1). Por isso Operator.from_circuit(qc) calcula \(U = H\,T\,H\), não \(H\,H\,T\) nem qualquer outra ordem.
Calculando \(U = HTH\) na mão. Com \(c=\dfrac{1+i}{\sqrt2}\) (a entrada não trivial de \(T\)):
Substituindo \(c\approx0{,}7071+0{,}7071i\):
que são exatamente os quatro números impressos em U.
Por que Statevector([1, 0]).evolve(U) dá a primeira coluna de U. \((1,0)\) é o vetor \(|0\rangle\), e multiplicar qualquer matriz por \(|0\rangle\) sempre devolve a sua primeira coluna (o mesmo truque de "jogar o vetor da base dentro da matriz" usado na Aula 1 para calcular a ação de operações sobre estados da base padrão). Por isso resultado sai idêntico à primeira coluna de U.
Por que esse número já apareceu antes. Isso não é coincidência: o circuito faz \(H|0\rangle=|+\rangle\), depois \(T\), depois \(H\) de novo — ou seja, calcula exatamente \(H\big(T|+\rangle\big) = HT|+\rangle\), que é o mesmo valor obtido em 2.3 com plus.evolve(T).evolve(H). O circuito só está automatizando (e confirmando numericamente) a mesma conta.
Por que isso importa
Essa seção já antecipa a virada de chave do curso: tudo que fizemos até aqui (vetores, medição, matrizes unitárias) tem uma representação direta como circuito — a forma universal de descrever algoritmos quânticos e de efetivamente rodá-los num computador quântico real (via qiskit-ibm-runtime) ou num simulador. As próximas lições do curso (sistemas múltiplos, e depois a unidade de circuitos) constroem em cima exatamente dessa ponte.