Computação Quântica

Conceito, princípios e aplicações
Voltar

RESUMO EM CONSTRUÇÃO

Material complementar:


Conceito:

  • Computação clássica: baseada na física clássica (arquitetura de Von Neumann), onde processador (CPU) opera com bits 0 (desligado, sem eletricidade) ou 1 (ligado, com eletricidade);
    • Processamento sequencial: 1 item por vez (ex: explorará cada caminho do labirinto, por vez, até encontrar solução otimizada).
  • Computação quântica: baseada na física quântica (mecânica quântica - partícula quantum), onde processador (QPU) opera com partículas subatômicas qubits (bit quântico - qbit - qubit topológico) 0, 1 ou ambos simultaneamente resultante de superposição de estados, onde partículas podem existir em múltiplos estados simultaneamente (Gato de Schrödinger). Entrelaçamento quântico permite que qubits atuem de ambas formas simultaneamente (ser inverso ao outro qubit. Sendo 0, será 1 ao mesmo tempo, independentemente da distância entre ambos - ação fantasmagórica à distância, Einstein). NISQ (Noisy Intermediate-Scale Quantum), descreve era atual de computadores quânticos, com dezenas a centenas de qubits suscetíveis a ruído. Com 300 qubits, computador quântico poderia representar mais estados do que nº de átomos no universo observável.
    • Qubit: descrito por vetor de estado em sistema quântico de 2 níveis, o qual é matematicamente equivalente a vetor em espaço vetorial complexo de 2 dimensões, conhecido como espaço de Hilbert, via notação bra-ket de Paul Dirac;
    • Processamento paralelo: itens processados simultaneamente (ex: explorará todos caminhos do labirinto simultaneamente até encontrar solução otimizada instantaneamente);
    • Resfriamento dos elétrons (-273,5°C) os mantém aproximadamente emaranhados (emaranhamento quântico), diminuindo agitação térmica, formando pares (pares de Cooper) que saltam de um lado a outro no circuito, formando qubit supercondutor;
    • Supercondutividade: fios supercondutores transportam elétrons sem resistência, eliminando perdas de energia, possibilitando circuitos quânticos (circuitos com capacitores e junções Josephson). Teletransporte quântico é processo pelo qual estado quântico exato de qubit é transferido de local para outro, sem mover sua partícula física;
    • Portas lógicas quânticas: operam em qubits, permitindo manipulação de estados quânticos de qubits para processamento. Porta Hadamard (H) cria superposição, porta CNOT (controlled-NOT) gera entrelaçamento, porta Z (fase). Porta NOT Quântica (Pauli-X), no caso clássico, troca 1 por 0 e vice-versa. Generalização para caso quântico é dada por operador X que satisfaz X |0⟩=|1⟩ e X|1⟩=|0⟩;
    • Interferência quântica: ondas de qubits podem interferir entre si, gerando respostas em padrões de interferência de probabilidade aumentada a encontrar partícula (interferência construtiva) ou padrões de interferência de probabilidade diminuída/nula a encontrar partícula (interferência destrutiva);
    • Decoerência: processo onde sistema em estado quântico colapsa/perde para estado não quântico, ocasionado intencionalmente, ou por fatores ambientais;
    • Correção de erro quântico (QEC): técnica para manter qubits estáveis e confiáveis, codificando dados de qubit lógico (ideal) em muitos qubits físicos (reais e ruidosos);
    • Pontos quânticos ("átomos artificiais"): pequeno semicondutor capaz de capturar elétron único e usá-lo em Spin do elétron, ou carga confinada, como qubit. Qubits de pontos quânticos podem ser manipulados com uso de campos magnéticos. São altamente escaláveis e compatíveis com tecnologias de semicondutores atuais.

Necessidade da computação quântica:

Chips compõem-se bilhões de transistores (semicondutores microscópicos que controlam fluxo de elétrons binários - não passa energia é 0, passa energia é 1). Em 1975, Gordon Moore, cofundador da Intel, observou que quantidade de transistores em chip de circuito integrado dobra aproximadamente a cada 2 anos (Lei de Moore), resultando chips menores e mais rápidos. Atualmente, transistores de silício chegaram à escala de nanômetros, com dimensões próximas ao tamanho de átomos individuais (miniaturização extrema). Ocasionará problemas físicos em túnel quântico (tunelamento quântico - elétrons começam atravessar barreiras microscópicas, causando vazamentos de corrente), aquecimento (quanto mais transistores, mais calor, limitando desempenho), custo e energia. Computação clássica está chegando ao limite físico do silício (espaço físico para encolher e acelerar chips está fisicamente lotado - problema do caixeiro-viajante). Moore prevê tal limite em meados de 2025.


Hardware de suporte:

Chip de processamento quântico é minúsculo (muitas vezes menor que moeda), mas necessita de grande sistema de suporte para funcionamento íntegro, pois qubits são extremamente sensíveis (qualquer calor, luz, som ou vibração pode destruir o estado quântico). Planos que compõem hardware quântico:

  1. Plano de dados quânticos (área de hospedagem): essência do computador quântico, inclui qubits físicos e estruturas necessárias para mantê-los no lugar;
  2. Plano de controle e medição (método de transferência): converte sinais digitais em analógicos ou de controle de ondas. Esses sinais analógicos executam operações nos qubits do plano de dados quânticos;
  3. Plano de processador de controle e processador host (computação para controle): plano de processador de controle implementa algoritmo quântico ou sequência de operações. Processador host interage com software quântico e fornece sinal digital ou sequência de bits clássicos para plano de controle e medição. Geralmente computador clássico é usado para suportar hardware quântico.
  • Blindagem térmica: isola chip de variações de temperatura, garantindo estabilidade do estado quântico. Resfriamento criogênico mantém chip a temperaturas próximas de -273,15°C (zero absoluto), garantindo estado de supercondutividade;
  • Blindagem eletromagnética: protege chip de interferências externas, garantindo operação estável, evitando feixes eletromagnéticos indesejados. Ímãs supercondutores mantém estado da matéria estável, com elétrons emaranhados e controláveis;
  • Proteção contra luz: materiais especiais bloqueiam feixes de luz visível, evitando interferência de agitação no estado quântico dos qubits;
  • Isolamento vibracional e acústico: combina técnicas de isolamento para proteger chip de vibrações e ruídos indesejados;
  • Proteção contra radiação: barreiras especiais protegem chip de radiações ionizantes, evitando decoerência;
  • Vácuo e gaiolas ópticas: eliminam partículas do ar que poderiam causar decoerência (perda do estado quântico);
  • Tecnologia fotônica: utiliza fótons (partículas de luz) como qubits fotônicos, alinhados e controlados por lasers, permitindo transmissão de dados quânticos (comunicação quântica). Chave quântica (QKD - Quantum Key Distribution) é transmitida via impulsão de laser por fótons (transporte via fibra óptica ou espaço livre) e, se interceptada, a própria luz denuncia tentativa de espionagem e dados são desmaterializados, tornando-a impossível de ser copiada sem detecção;
  • Tecnologia de íons aprisionados (trapped ions - armadilhas de íons - IonQ): átomos neutros/frios (armadilhas de átomos neutros - neutral atom trap) carregados (íons) são presos em gaiola de luz para resfriamento e manipulados com lasers. Cada íon aquecido representa 1 qubit;
  • Centros de Vacância em Diamante (NV Centers): defeito específico na estrutura cristalina do diamante, onde átomo de carbono é substituído por nitrogênio ao lado de lacuna (vacância), pode funcionar como qubit. Spin eletrônico do centro NV pode ser manipulado com micro-ondas e lido opticamente, e pode operar à temperatura ambiente, embora escalabilidade seja desafio.

Circuitos quânticos:

Sequência de portas quânticas usadas em cálculo.

  1. Entrada: qubits de entrada são preparados em estado inicial, geralmente '|0⟩' para cada qubit. Estado conjunto de múltiplos qubits é descrito matematicamente pelo que é chamado de seu produto tensorial;
  2. Linhas horizontais: cada linha representa evolução temporal de único qubit. Elas não são necessariamente fios físicos, podendo ser apenas passagem do tempo para íon aprisionado ou deslocamento espacial de fóton;
  3. Sentido: circuito é lido da esquerda para direita, descrevendo evolução do sistema quântico no tempo;
  4. Portas quânticas: blocos nas linhas representam portas quânticas, que são operações unitárias aplicadas aos qubits;
  5. Linhas verticais: segmento vertical conectando múltiplas linhas de qubits, como em uma porta CNOT, informa que porta atua simultaneamente nesses qubits. Linha vertical representa sincronismo da operação, e não o envio de informação;
  6. Controle: em porta controlada (como CNOT), um ponto sólido (●) em linha indica que qubit representado nessa linha é qubit de controle. Caso esteja no estado '|1⟩', porta realiza operação no qubit alvo; caso esteja no estado '|0⟩', porta não realiza operação alguma. Caso qubit de controle seja estado superposto ou ambos qubits estejam emaranhados, não é possível compreender comportamento individual do qubit de controle e do qubit alvo. Deve-se considerar ação do operador unitário, que representa todo circuito, atuando simultaneamente no estado conjunto dos qubits;
  7. Saída: no final do circuito, qubits que compõem saída podem ser medidos. Medição colapsa superposição de cada qubit para resultado clássico (0 ou 1).

Cotidiano com computação quântica:

Computação quântica complementará coexistência a computação clássica (integração híbrida), pois dispositivos computacionais ainda utilizam demais componentes de interpretação binária clássica. Computação quântica é utilizada em longos processamentos de dados, como descriptografia, pesquisas científicas, simulações moleculares, IAs avançadas, etc. Criptografia pós-quântica (PQC), novos algoritmos clássicos resistentes a ataques quânticos e clássicos, são desenvolvidos como resposta à ameaça da computação quântica. Distribuição quântica de chaves (QKD) é baseada em princípios quânticos para comunicação inviolável. Protocolo BB84 permite criar chave secreta entre 2 partes. Tentativas de espionagem alteram estados quânticos e são detectadas. Problemas de otimização em finanças e engenharia, onde computação quântica, via algoritmos QAOA ou recozimento quântico, pode encontrar soluções melhores e mais rápidas para problema do caixeiro-viajante, roteamento de veículos, e otimização de cadeias de suprimentos.

  • Supremacia quântica (quantum supremacy): ponto em que computador quântico executa tarefa impossível para computador clássico em tempo viável;
  • Utilidade quântica (quantum utility): cotidiano atual, em que computadores quânticos já executam tarefas úteis, ainda que sem superar métodos clássicos em todos casos;
  • Vantagem quântica prática (practical quantum advantage): estágio futuro esperado, onde desempenho e custo-benefício superarão efetivamente métodos clássicos em aplicações reais.

Recozimento quântico:

Método alternativo ao modelo de portas, usado para resolver problemas de otimização e amostragem. Busca estado de energia mínima de sistema físico representando problema. Qubits evoluem de superposição inicial para configuração final, usando tunelamento quântico para atravessar barreiras de energia e evitar ótimos locais, aumentando chance de alcançar solução global. Aplicações incluem otimização combinatória, finanças, rotas e machine learning. Problema da não linearidade, na aprendizagem clássica, ocorre quando métodos lineares não captam relações complexas. No ML quântico, embora operações sejam lineares, feature maps introduzem não linearidades eficazes. Circuitos variacionais (VQC) com portas parametrizadas aprendem funções altamente não lineares, semelhantes a redes neurais profundas.


Processadores de átomos de Rydberg:

Átomo de Rydberg corresponde a átomo excitado que possui, em média, 1 ou mais elétrons distantes do núcleo. Átomos de Rydberg têm várias propriedades peculiares, incluindo resposta exagerada a campos elétricos e magnéticos, bem como vida longa. Quando usados como qubits, oferecem interações atômicas fortes e controláveis ajustáveis ao selecionar diferentes estados.


Processadores baseados em ressonância magnética nuclear (RMN):

Dado quântico armazenado nos spins nucleares dos átomos em moléculas, e portas lógicas manipulam essa informação via radiação eletromagnética. Pósitron ou elétron podem ter spin 'para cima', 'para baixo', ou ambos simultaneamente, representando estados do qubit. Momentos magnéticos nucleares fazem movimento natural de precessão na presença de campos magnéticos. Estados quânticos dos núcleos podem ser manipulados irradiando núcleos com pulsos de rádio frequência sintonizados na frequência de precessão dos mesmos.


Pilares dos algoritmos quânticos:

  • Transformada quântica de Fourier (QFT): versão quântica da Transformada Discreta de Fourier (DFT), base do algoritmo de Shor, que transforma amplitudes em espaço de frequências com ganho exponencial;
  • Amplificação de amplitude: aumenta probabilidade do estado solução. Usada no algoritmo de Grover, aplicando rotações iterativas no vetor de estado;
  • Interferência quântica: cancela estados incorretos (interferência destrutiva) e reforça corretos (construtiva), sendo base da eficiência quântica;
  • Simulação hamiltoniana: simula sistemas quânticos via portas lógicas, mapeando Hamiltoniana (operador matemático que descreve energia total de sistema quântico) do sistema físico. Origem em Feynman;
  • Princípio da incerteza: resultados são probabilísticos, requerindo múltiplas execuções e análise estatística para inferir solução.

Algoritmos quânticos:

  • Algoritmo de Shor (Peter Shor - 1994): resolve fatoração de nºs inteiros em tempo polinomial. Usa Transformada Quântica de Fourier para encontrar período de função. Permite deduzir fatores primos de N. Ameaça criptografia de chave pública baseada em fatoração, como RSA (Rivest - Shamir - Adleman);
  • Algoritmo de Grover (Lov Grover - 1996): aceleração quadrática em busca não estruturada. Reduz tempo de busca de O(N) para O(√N). Aplicável a problemas de otimização e busca geral;
  • Algoritmo Adiabático e Computação Quântica Adiabática (adiabatic quantum computing): baseado no teorema adiabático da física, onde sistema inicia no estado fundamental e evolui lentamente, mantendo-o. Usado em computadores quânticos adiabáticos como D-Wave, e voltado para problemas de otimização, como rotas logísticas. Processo envolve Hamiltoniana inicial simples evoluída para final complexa, em que estado final representa solução do problema. Computação adiabática é polinomialmente equivalente ao modelo de circuito, mas com aplicações distintas;
  • Algoritmo HHL (Harrow, Hassidim e Lloyd): resolve sistemas de equações lineares e oferece aceleração exponencial em situações com aplicações potenciais em diversas áreas da ciência e engenharia;
  • Algoritmos de simulação quântica: simulam sistemas quânticos complexos, como interações moleculares e materiais novos. Utilizam portas lógicas para modelar dinâmica quântica;
  • Algoritmos de otimização quântica: resolvem problemas de otimização combinatória, como problema do caixeiro-viajante. Exemplos incluem QAOA (Quantum Approximate Optimization Algorithm) e VQE (Variational Quantum Eigensolver);
  • Algoritmos de aprendizado de máquina quântica (QML): exploram paralelismo quântico para acelerar tarefas de aprendizado de máquina (Machine Learning), como classificação e clustering. Exemplos incluem QSVM (Quantum Support Vector Machine) e QNN (Quantum Neural Networks);
  • Algoritmos de criptografia quântica: utilizam princípios quânticos para comunicação segura. Exemplo é QKD (Quantum Key Distribution), que permite troca segura de chaves criptográficas, detectando qualquer tentativa de interceptação;
  • Algoritmos híbridos quântico-clássicos: combinam computação quântica e clássica para resolver problemas complexos. Exemplos incluem VQE e QAOA, onde parte do processamento é feito em computador clássico e parte em computador quântico;
  • Algoritmos de otimização quântica baseados em annealing quântico: utilizam processos de resfriamento para encontrar soluções aproximadas para problemas de otimização. Exemplos incluem Quantum Annealing e algoritmos implementados em computadores quânticos adiabáticos;
  • Algoritmos de simulação de materiais quânticos: modelam propriedades de materiais em nível quântico, permitindo estudo de novos materiais com propriedades desejadas. Exemplos incluem simulações de moléculas e materiais usando técnicas como VQE;
  • Algoritmos de busca quântica em grafos: exploram estruturas de grafos para resolver problemas de busca e otimização. Exemplos incluem algoritmos baseados em caminhada quântica;
  • Algoritmos de fatoração quântica além de Shor: exploram variações e melhorias no algoritmo de Shor para fatoração de nºs inteiros, visando maior eficiência e robustez;
  • Algoritmos de simulação quântica de sistemas físicos: modelam sistemas físicos complexos, como dinâmica de partículas e interações quânticas, utilizando técnicas avançadas de simulação;
  • Algoritmos de otimização quântica para logística e planejamento: aplicam computação quântica para resolver problemas práticos em logística, como roteirização e alocação de recursos;
  • Algoritmos de aprendizado profundo quântico: combinam redes neurais profundas (Deep Learning Neural Networks) com computação quântica para melhorar desempenho em tarefas complexas de aprendizado de máquina;
  • Algoritmos de criptografia pós-quântica: desenvolvem métodos criptográficos resistentes a ataques de computadores quânticos, garantindo segurança futura na comunicação;
  • Algoritmos de simulação quântica para química computacional: modelam reações químicas e propriedades moleculares, facilitando descobertas em química e farmacologia;
  • Algoritmos de otimização quântica para finanças: aplicam computação quântica para resolver problemas financeiros, como otimização de portfólios e precificação de derivativos;
  • Algoritmos de aprendizado de máquina quântica para análise de dados: utilizam computação quântica para acelerar análise de grandes volumes de dados (Big Data), melhorando eficiência em tarefas como mineração de dados (Data Mining) e análise preditiva (Predictive Analytics).

Medições quânticas:

Ato de transformar possibilidade (superposição) em realidade (0 ou 1). Qubits estão constantemente em superposição (probabilidades % de estar em 0 à 1), e tornam-se definidos (0 ou 1) após medição. Exemplo, em superposição, um qubit pode estar 70% em 0 e 30% em 1, mas após medição será 0 ou 1.

  • Computação clássica: bit possui estado definido (0 ou 1, contendo 2 combinações distintas). 2 bits combinados, formam-se 4 combinações distintas (00, 01, 10 e 11). n bits = 2ⁿ estados distintos;
    • 1 bit: 2 estados possíveis (1 por vez);
    • 2 bits: 4 estados possíveis (1 por vez);
    • 3 bits: 8 estados possíveis (1 por vez).
  • Computação quântica: qubit possui estado indefinido (superposição, contendo 2 combinações simultâneas, cada uma com respectiva probabilidade - combinação linear de estados básicos sob coeficientes de probabilidades para estados possíveis de 0 e 1). 2 qubits combinados, formam-se superposição de 4 combinações simultaneamente (00, 01, 10 e 11). n qubits = 2ⁿ estados simultaneamente.
    • 1 qubit: 2 estados possíveis (2 simultaneamente (percentuais de cada));
    • 2 qubits: 4 estados possíveis (4 simultaneamente (percentuais de cada));
    • 3 qubits: 8 estados possíveis (8 simultaneamente (percentuais de cada)).

Esparçador Pauli (Sparse Pauli) adventa matrizes de Pauli, onde são aplicadas 3 matrizes X, Y e Z em, além de I (identidade I), em um qubit, para medir seu estado.

  • X = inverte qubit (como inverter 0/1)
  • Z = muda fase (como mudar a "cor" invisível do estado)
  • Y = combinação de X e Z
  • I = não faz nada

Em 3 qubits, operação X ⊗ Z ⊗ I (ou "XZI" - Pauli String, sequência de Paulis) aplica X no qubit 1, Z no qubit 2, e I (nada) no qubit 3. SparsePauliOp (Sparse Pauli Operator) é forma compacta (esparsa) de guardar combinações sem ocupar tanta memória. Estado quântico é "como sistema está", SparsePauli é "tipo de óculos com que você observa". Você escolhe "conjunto de lentes" (matrizes Pauli) para olhar seu sistema e obter informações (medições). Pauli noise (ruído de Pauli) é um modelo de ruído quântico que descreve erros aleatórios aplicados a qubits usando matrizes de Pauli (X, Y, Z). Usadas para simular imperfeições reais nas transformações de qubits, em computadores quânticos. X-error inverte estado (Bit-flip noise), Z-error muda fase (Phase-flip noise), e Y-error combina erros X e Z (inversão + mudança de fase, Bit-phase-flip noise). Depolarizing noise destaca forma simétrica de Pauli noise na aplicação de X, Y ou Z com mesma probabilidade.


Benchmarks quânticos:

Avaliam desempenho e utilidade dos computadores quânticos, sem implicar vantagem comprovada sobre métodos clássicos. Fidelidade de camada mede capacidade geral do processador em executar circuitos, revelando detalhes sobre qubits, portas e interferências. CLOPS (operações de camada de circuito por segundo) mede velocidade de execução de circuitos de volume quântico, combinando desempenho quântico e clássico. Ambas permitem comparar sistemas e acompanhar ganhos de performance. Profundidade do circuito indica quantas operações paralelas podem ser executadas antes da decoerência, determinando complexidade dos circuitos possíveis.


Linguagens de programação:

Linguagens utilizadas especificamente para processamento quântico. Ambientes disponíveis para desenvolvimento são IBM Quantum Experience, Azure Quantum, Google Quantum AI e AWS Braket. Plataformas IBM Quantum Platform, Azure Quantum, Google Quantum AI e AWS Braket oferecem ferramentas e recursos para desenvolvimento quântico.

  • Qiskit: principal stack de software da IBM para computação quântica, indo além do desenvolvimento de circuitos. Inclui SDK, middleware e serviços para escrever, otimizar e executar programas em sistemas IBM Quantum, com novas ferramentas de IA generativa para assistência de código;
  • Q#: linguagem quântica desenvolvida pela Microsoft, baseada no C#, voltada para desenvolvimento de algoritmos quânticos, controlando comportamento dos qubits e explorando superposição e entrelaçamento quântico. Exemplo abaixo cria par de Bell (estado de entrelaçamento quântico entre 2 qubits):
    
    open Microsoft.Quantum.Intrinsic; // Importa operações quânticas básicas
    open Microsoft.Quantum.Measurement; // Importa operações de medição
    open Microsoft.Quantum.Canon; // Importa operações canônicas
    
    operation CreateBellPair() : (Result, Result) {
        use qubits = Qubit[2]; // Aloca array de 2 qubits
        
        H(qubits[0]); // Aplica porta quântica Hadamard no 1º qubit (superposição)
        CNOT(qubits[0], qubits[1]); // Aplica porta quântica CNOT (Controlled-NOT, entrelaçamento)
    
        let result1 = M(qubits[0]); // Mede 1º qubit
        let result2 = M(qubits[1]); // Mede 2º qubit
    
        Reset(qubits[0]); // Reseta 1º qubit
        Reset(qubits[1]); // Reseta 2º qubit
    
        return (result1, result2); // Retorna resultados das medições (sempre iguais, exemplo, ambos 0 ou ambos 1)
    }
    

Qiskit

  1. Instalação:
    
    pip3 install qiskit qiskit-ibm-runtime matplotlib qiskit[visualization] jupyter
    
  2. Conexão:
    
    from qiskit_ibm_runtime import QiskitRuntimeService
    
    QiskitRuntimeService.save_account(
        channel="ibm_quantum_platform",
        token="SEU-TOKEN-IBM-QUANTUM-AQUI",
        overwrite=True
    ) # Usar apenas 1 vez para salvar conta
    
    service = QiskitRuntimeService() # Usar para conectar conta em cada script
    print("Conta conectada com sucesso!")
    


Exemplos práticos (Qiskit 2.2, em notebooks Python):

  1. OBS: Preparação do ambiente (antes de executar os exemplos):
    
    # Instalar runtime (via terminal): pip3 install qiskit qiskit-ibm-runtime matplotlib qiskit[visualization] jupyter
    
    # Conexão com IBM Quantum (única vez no ambiente):
    from qiskit_ibm_runtime import QiskitRuntimeService
    QiskitRuntimeService.save_account(token="SEU-TOKEN-IBM-QUANTUM-AQUI")
    
  2. Estado Bell (par de entrelaçamento quântico):
    
    from qiskit import QuantumCircuit
    from qiskit.quantum_info import SparsePauliOp
    from qiskit.transpiler import generate_preset_pass_manager
    from qiskit_ibm_runtime import EstimatorV2 as Estimator, QiskitRuntimeService
    service = QiskitRuntimeService() # Conecta conta IBM Quantum, utilizar em cada script
    
    qc = QuantumCircuit(2) # Cria circuito quântico com 2 qubits
    qc.h(0) # Aplica porta Hadamard no qubit 0 (superposição)
    qc.cx(0, 1) # Aplica porta CNOT (CX) com qubit 0 controlando qubit 1 (entrelaçamento)
    qc.measure_all() # Mede todos qubits
    qc.draw("mpl") # Desenha circuito
    print(qc)
    
  3. Estado GHZ (Greenberger–Horne–Zeilinger, estado de entrelaçamento quântico entre 3 ou mais qubits): circuito GHZ cria estado quântico onde 3 qubits ficam perfeitamente entrelaçados, onde todos estejam ao mesmo tempo em |000⟩ e |111⟩, compartilhando única correlação global que não pode ser separada entre eles
    
    from qiskit import QuantumCircuit
    from qiskit_ibm_runtime import QiskitRuntimeService
    service = QiskitRuntimeService() # Conectar conta IBM Quantum
    
    from qiskit import QuantumCircuit
    from qiskit.quantum_info import Statevector
    from qiskit.visualization import plot_state_qsphere, plot_bloch_multivector
    import matplotlib.pyplot as plt
    from qiskit_ibm_runtime import QiskitRuntimeService
    service = QiskitRuntimeService()
    
    ghz = QuantumCircuit(3) # Circuito com 3 qubits
    
    ghz.h(0) # Hadamard (colocar qubit 0 em superposição)
    ghz.cx(0, 1) # CNOT (entrelaçar qubit 0 com qubit 1)
    ghz.cx(0, 2) # CNOT (entrelaçar qubit 0 com qubit 2)
    
    print("Circuito GHZ (desenho em texto):")
    print(ghz.draw("text")) # Qubits entrelaçados em estado GHZ
    
    state_ghz = Statevector.from_instruction(ghz) # Vetor de estado do circuito GHZ
    print("Vetor de estado GHZ resultante:")
    print(state_ghz)
    
    # Visualização QSphere — mostra amplitudes e fases
    fig = plot_state_qsphere(state_ghz)
    fig.figure.savefig("ghz_qsphere.png")
    plt.imshow(fig.figure.canvas.renderer.buffer_rgba())
    plt.show() # Estados com diferentes pesos/amplitudes em lados opostos da esfera, mas com mesma fase em coerência quântica (superposição coerente) e fase relativa zero entre eles
    
    # Visualização nos vetores de Bloch dos qubits individuais
    fig = plot_bloch_multivector(state_ghz)
    fig.figure.savefig("ghz_bloch.png")
    plt.imshow(fig.figure.canvas.renderer.buffer_rgba())
    plt.show() # Cada qubit individualmente está em estado misto (completamente indefinido), mas sistema global está em estado puro GHZ entrelaçado
    
  4. Estado W (w state, estado de entrelaçamento quântico robusto entre 3 qubits): diferente do GHZ, estado W preserva entrelaçamento mesmo se qubit for perdido, sendo mais robusto
    
    from qiskit import QuantumCircuit
    from qiskit.quantum_info import Statevector
    from qiskit.visualization import plot_state_qsphere, plot_bloch_multivector
    import matplotlib.pyplot as plt
    import math
    import numpy as np
    from qiskit_ibm_runtime import QiskitRuntimeService
    service = QiskitRuntimeService()
    
    w_vec = np.array([0, 1, 1, 0, 1, 0, 0, 0], dtype=complex) / math.sqrt(3) # Vetor de estado W de 3 qubits, onde |001>, |010>, |100> têm amplitude 1/√3
    
    w = QuantumCircuit(3, name="W_state") # Circuito de 3 qubits em estado W
    w.initialize(w_vec, [0, 1, 2]) # Inicializa 3 qubits no vetor w_vec
    
    print("Circuito W (desenho em texto):")
    print(w.draw("text"))
    
    print("Circuito W (desenho em Matplotlib):")
    fig = w.draw("mpl")
    fig.savefig("w_circuit.png")
    plt.imshow(fig.canvas.renderer.buffer_rgba())
    plt.axis("off")
    plt.show()
    
    # QSphere: amplitudes e fases (3 bases computacionais têm amplitude diferente de zero, todas iguais a 1√3 ≈ 0.577)
    fig = plot_state_qsphere(Statevector.from_instruction(w))
    fig.figure.savefig("w_qsphere.png")
    plt.imshow(fig.figure.canvas.renderer.buffer_rgba())
    plt.axis("off")
    plt.show()
    
    # Bloch multivector: vetores de Bloch dos qubits individuais (3 qubits estão em estados individuais mistos, não puros)
    # Chance de ser 1 (seta para baixo): Qubit só é 1 em 1 dos 3 termos da superposição, probabilidade = 1/3 (≅ 33%)
    # Chance de ser 0 (seta para cima): Qubit é "0" nos outros 2 termos, probabilidade = 2/3 (≅ 67%)
    fig2 = plot_bloch_multivector(Statevector.from_instruction(w))
    fig2.figure.savefig("w_bloch.png")
    plt.imshow(fig2.figure.canvas.renderer.buffer_rgba())
    plt.axis("off")
    plt.show()
    
  5. Interferência quântica: portas quânticas manipulam amplitudes (valores complexos) - quando caminhos de amplitudes se somam com fases diferentes, elas podem se reforçar (construtiva) ou cancelar (destrutiva). Exemplo clássico: aplicar H, depois Z, depois H mostra interferência (par H Z H implementa rotação equivalente a X até diferença de fase, resultando em padrões de probabilidade diferentes dependendo das fases)
    
    # pip3 install qiskit qiskit-ibm-runtime qiskit-aer matplotlib qiskit[visualization]
    # pip3 install qiskit-aer
    from qiskit_ibm_runtime import QiskitRuntimeService
    # QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")
    
    import math
    import numpy as np
    import matplotlib.pyplot as plt
    from qiskit import QuantumCircuit, transpile
    from qiskit.quantum_info import Statevector
    from qiskit.visualization import plot_state_qsphere, plot_bloch_multivector
    from qiskit_aer import AerSimulator  
    from qiskit_ibm_runtime import QiskitRuntimeService
    service = QiskitRuntimeService()
    
    def show_fig(fig):
        fig.figure.savefig("tmp_fig.png")
        plt.imshow(fig.figure.canvas.renderer.buffer_rgba())
        plt.axis("off")
        plt.show()
    
    qc_super = QuantumCircuit(1, name="superposition")  # Superposição básica
    qc_super.h(0) # Hadamard cria superposição (|0> + |1>)/√2
    
    print("Circuito superposição (Hadamard):")
    print(qc_super.draw("text"))
    fig = qc_super.draw(output="mpl")
    show_fig(fig)
    
    # Vetor de estado resultante (sem medida)
    state = Statevector.from_instruction(qc_super)
    fig_qs = plot_state_qsphere(state)
    show_fig(fig_qs)
    fig_bloch = plot_bloch_multivector(state)
    show_fig(fig_bloch) # Bloch sphere, onde porta H leva |0> ao equador (positivo, superposição igual de |0> e |1>)
    
    # Interferência básica HZH vs HIH
    qc_id = QuantumCircuit(1, name="H-I-H") # circuito identidade (identidade não altera estado)
    qc_id.h(0)
    qc_id.h(0)
    qc_z = QuantumCircuit(1, name="H-Z-H") # circuito com interferência de fase Z no meio
    qc_z.h(0)
    qc_z.z(0)
    qc_z.h(0)
    print("Circuito H-I-H (texto):")
    print(qc_id.draw("text"))
    print("Circuito H-Z-H (texto):")
    print(qc_z.draw("text"))
    
    state_id = Statevector.from_instruction(qc_id) # vetor de estado H-I-H
    state_z  = Statevector.from_instruction(qc_z) # vetor de estado H-Z-H
    print("Vetor de estado (H-I-H):", state_id.data)
    print("Vetor de estado (H-Z-H):", state_z.data)
    
    fig_z = plot_state_qsphere(state_z) # QSphere para H-Z-H (mostra interferência causada pela porta Z, seta para baixo, informando +1 em |1>)
    show_fig(fig_z)
    
    fig_z_bloch = plot_bloch_multivector(state_z) # Bloch para H-Z-H (mostra que estado final é |1>, devido à interferência destrutiva em |0>)
    show_fig(fig_z_bloch)
    
    sim = AerSimulator() # Simulador local de medições (sem ruído) para validar probabilidades
    
    qc_meas = QuantumCircuit(1,1) # Medição do circuito original de superposição
    qc_meas.h(0)
    qc_meas.measure(0,0)
    
    qc_id_meas = qc_id.copy() # Medições para H-I-H e H-Z-H
    qc_id_meas.measure_all()
    qc_z_meas = qc_z.copy()
    qc_z_meas.measure_all()
    
    for circuit, name in [(qc_meas, "H"), (qc_id_meas, "H-I-H"), (qc_z_meas, "H-Z-H")]: # Compile e execute (shots)
        t_qc = transpile(circuit, sim)
        result = sim.run(t_qc, shots=1024).result()
        print(f"\nContagens ({name}): {result.get_counts()}")
    
    # H cria superposição (|0> + |1>)/√2, amplitudes iguais, medições ~50/50
    # H-I-H é identidade, retorna |0>
    # H-Z-H muda fase via aplicação de Z, interferindo e invertendo, cancelando |0> e reforçando |1>
    # Medições confirmam probabilidades calculadas pelo vetor de estado
    
    # O simulador rodou 1024 vezes, sendo 0 (522 vezes) e 1 (502 vezes). Portanto, ~50% para 0 e ~50% para 1
    # Em H-I-H, resultado foi 0 (1024 vezes) e 1 (0 vezes), confirmando identidade
    # Em H-Z-H, resultado foi 1 (1024 vezes) e 0 (0 vezes), confirmando interferência que inverteu estado de |0> para |1>
    

Algoritmo de Deutsch (Deutsch's problem):

1º algoritmo quântico da história. Resolve problema de Deutsch em etapa única, via computação quântica. Dada função 'f:{0,1}->{0,1}', queremos saber se a mesma é constante (f(0) = f(1)) ou balanceada (f(0) ≠ f(1)).

  • Constante: f(x)=0 (sendo f(0)=0 e f(1)=0) ou f(x)=1 (sendo f(0)=1 e f(1)=1);
  • Balanceada: f(x)=x (sendo f(0)=0 e f(1)=1) ou f(x)=¬x (sendo f(0)=1 e f(1)=0).

A solução, na física clássica, avaliam-se ambas f(0) e f(1). Na física quântica, avaliam-se simultaneamente, via superposição e interferência, em única resposta (diferente de paralelismo clássico), extraindo propriedades da função, não valores individuais. Oráculo (porta Uf) é função escondida que algoritmo pode invocar, onde sabe-se apenas quais entradas e saídas, não seu código interno. Algoritmo quântico resolve Deutsch com apenas 1 consulta ao oráculo. Superposição avalia f(0) e f(1) simultaneamente, enquanto Interferência elimina resultados inúteis. Medição estratégica extrai propriedade global da função. Função constante resulta em estado |0⟩, enquanto função balanceada resulta em estado |1⟩.


# pip3 install qiskit qiskit-ibm-runtime qiskit-aer matplotlib qiskit[visualization]
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit import QuantumCircuit, transpile
from qiskit_aer import Aer, AerSimulator
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

# Função oráculo, cria circuito quântico de função clássica f: {0,1} -> {0,1}
def deutsch_function(case: int):
 
    # case 1: f(x) = 0 (constante)
    # case 2: f(x) = x (balanceada)
    # case 3: f(x) = ¬x (balanceada)
    # case 4: f(x) = 1 (constante)
    if case not in [1, 2, 3, 4]:
        raise ValueError("`case` must be 1, 2, 3, or 4.")
 
    f = QuantumCircuit(2) # circuito quântico de 2 qubits (1 de entrada, 1 de saída)
    if case in [2, 3]:
        f.cx(0, 1) # CNOT (f(x) = x ou f(x) = ¬x)
    if case in [3, 4]:
        f.x(1) # NOT (f(x) = ¬x ou f(x) = 1)
    return f

# Exemplo (case 3): f(x) = ¬x (balanceada)
display(deutsch_function(3).draw(output="mpl"))
# q0 (qubit de entrada): |0⟩ ou |1⟩
# q1 (qubit de saída): |1⟩ (inicializado em |1⟩ para permitir interferência)
# Porta CNOT aplicada de q0 para q1 (inverte q1 se q0 for |1⟩)
# Porta X (NOT) aplicada em q1 (inverte q1)
# Resultado final: f(0) = 1 e f(1) = 0

def compile_circuit(function: QuantumCircuit):
    n = function.num_qubits - 1
    qc = QuantumCircuit(n + 1, n) # n qubits de entrada + 1 qubit de saída
 
    qc.x(n) # Porta X em qubit de saída (inicializa em |1⟩)
    qc.h(range(n + 1)) # Portas Hadamard em todos qubits (superposição, exceto saída. q0 é superposição de |0⟩ e |1⟩ simultaneamente
 
    qc.barrier()
    qc.compose(function, inplace=True) # Oráculo quântico (CNOT + X), Porta CNOT (q0 é controle, q1 é alvo). Se q0 = 1 então q1 é invertido, se q0 = 0 então nada acontece. Porta X em q1 (inverte q1)
    qc.barrier()
 
    qc.h(range(n)) # Porta Hadamard em q0 (interferência - interferência construtiva em |0⟩, interferência destrutiva em |1⟩)
    qc.measure(range(n), range(n)) # Medição dos qubits de entrada q0 (resultado: 0 = constante, 1 = balanceada)
 
    return qc

# Exemplo de compilação do circuito Deutsch para f(x) = ¬x (case 3). c é bit clássico, resultado da medição
display(compile_circuit(deutsch_function(3)).draw(output="mpl"))

# Algoritmo de Deutsch, determina se função é constante (0) ou balanceada (1)
def deutsch_algorithm(function: QuantumCircuit):
    qc = compile_circuit(function)
 
    result = AerSimulator().run(qc, shots=1, memory=True).result()
    measurements = result.get_memory()
    if measurements[0] == "0":
        return "constant"
    return "balanced"

f = deutsch_function(3)
display(deutsch_algorithm(f))

Algoritmo de Deutsch-Jozsa (Deutsch-Jozsa's problem):

Expansão do algoritmo de Deutsch para funções com múltiplos qubits de entrada, determinando se função é constante ou balanceada via única consulta ao oráculo. Deutsch estipula categorizações de funções quando qubits de entrada n for 0 ou 1. Para valores n maiores que 1, Deutsch-Jozsa considera entradas "indiferentes", sendo promessas de constante ou balanceada.

  • Constante: quando saída dos qubits de entrada será sempre 0 ou sempre 1;
  • Balanceada (equilibrada): quando saída dos qubits de entrada será 0 e 1 em nº igual de vezes;
  • Algoritmo retorna cadeia de bits com todos 0 se função for constante, e cadeia de bits com pelo menos um 1 se função for balanceada (equilibrada).

Após aplicar portas Hadamard (superposição) nos qubits de entrada, aplicação de 2ª camada de portas Hadamard alterarão estado quântico dos qubits de entrada.


# pip3 install qiskit qiskit-ibm-runtime qiskit-aer matplotlib qiskit[visualization]
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit import QuantumCircuit, transpile
from qiskit_aer import Aer, AerSimulator
from qiskit.visualization import plot_histogram
from IPython.display import display
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

# Função oráculo para Deutsch-Jozsa
def deutsch_jozsa_oracle(n, oracle_type="balanced"):
    oracle = QuantumCircuit(n + 1) # n qubits de entrada + 1 qubit auxiliar de saída (fase)

    if oracle_type == "constant": # Se função é constante (f(x) = 0), então nada ocorre
        pass

    elif oracle_type == "balanced": # Se função é balanceada (f(x) = 1 para metade dos inputs), então aplica CX (portas CNOTs para cada qubit, invertendo estado do qubit auxiliar)
        for qubit in range(n):
            oracle.cx(qubit, n)
    return oracle

# Construir circuito Deutsch-Jozsa
def deutsch_jozsa_circuit(n, oracle):
    qc = QuantumCircuit(n + 1, n) # n qubits de entrada + 1 qubit auxiliar de saída + n bits clássicos para medição

    # Colocar qubit auxiliar em |1>
    qc.x(n) # Aplica porta X (NOT) no qubit auxiliar
    qc.h(n) # Aplica porta Hadamard no qubit auxiliar

    # Aplica porta Hadamard nos qubits de entrada
    for qubit in range(n):
        qc.h(qubit)
    qc.barrier()

    qc.compose(oracle, inplace=True) # Aplica oráculo ao circuito
    qc.barrier()

    # Aplica 2ª camada de portas Hadamard nos qubits de entrada
    for qubit in range(n):
        qc.h(qubit)

    # Mede qubits de entrada nos bits clássicos correspondentes
    for qubit in range(n):
        qc.measure(qubit, qubit)
    return qc

n = 3  # Quantidade de qubits de entrada
oracle_type = "balanced"  # Tipo de função ('constant' ou 'balanced')
oracle = deutsch_jozsa_oracle(n, oracle_type) # Cria oráculo
qc = deutsch_jozsa_circuit(n, oracle) # Cria circuito Deutsch-Jozsa
display(qc.draw(output="mpl"))

simulator = Aer.get_backend("aer_simulator") # Usa simulador AER
compiled = transpile(qc, simulator) # Transpila circuito para backend do simulador
result = simulator.run(compiled, shots=1024).result() # Executa circuito no simulador
counts = result.get_counts() # Obtém resultados da medição

display(plot_histogram(counts))
print("Resultados:", counts) # Exibe resultados da medição (000 indica função constante, qualquer outro resultado indica função balanceada)

Algoritmo de Bernstein-Vazirani (Bernstein-Vazirani's problem):

Também chamado de problema de amostragem de Fourier, promete identificar string oculta via consulta única ao oráculo, ao invés de somente rótulos constante e balanceada.

  1. Recebe-se função caixa preta 'f(x)=s*x', para string 's' a ser descoberta;
  2. Aplica-se porta Hadamard em todos qubits de entrada, 1 porta NOT mais 1 Hadamard é aplicada ao qubit de saída;
  3. Aplica-se oráculo (porta Uf) em todos qubits, recebendo qubits de entrada (em superposição iguais), e aplicando função 'f(x)=s*x' ao qubit de saída, mantendo-o em estado inalterado, graças ao retrocesso de fase, mas invertendo alguns termos no estado;
  4. Aplica-se 2ª camada de portas Hadamard em todos qubits de entrada, seguida de medição, revelando string oculta 's', advinda do estado gerado nos qubits.

# pip3 install qiskit qiskit-ibm-runtime qiskit-aer matplotlib qiskit[visualization]
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit import QuantumCircuit, transpile
from qiskit_aer import Aer, AerSimulator
from qiskit.visualization import plot_histogram
from IPython.display import display
import matplotlib.pyplot as plt
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

s = "1011" # String oculta 's'
n = len(s) # Quantidade de qubits de entrada

# Função oráculo Uf para Bernstein-Vazirani
def bernstein_vazirani_oracle(secret_string: str) -> QuantumCircuit:
    n = len(secret_string)
    oracle = QuantumCircuit(n + 1) # n qubits de entrada + 1 qubit auxiliar

    for i, bit in enumerate(secret_string):
        if bit == "1":
            oracle.cx(i, n) # Aplica CNOT do qubit i de entrada para qubit auxiliar
    oracle.name = "U_f"
    return oracle

qc = QuantumCircuit(n + 1, n) # Circuito com n qubits de entrada, 1 qubit auxiliar e n bits clássicos

qc.x(n) # Aplica X (porta NOT) no qubit auxiliar, colocando-o em |1>
qc.h(range(n + 1)) # Aplica Hadamard em todos qubits
oracle = bernstein_vazirani_oracle(s) # Aplica oráculo ao circuito
qc.append(oracle, range(n + 1)) # Aplica oráculo aos qubits do circuito
qc.h(range(n)) # Aplica Hadamard novamente nos qubits de entrada
qc.measure(range(n), range(n)) # Mede qubits de entrada nos bits clássicos
display(qc.draw(output="mpl"))

simulator = AerSimulator()
tqc = transpile(qc, simulator) # Transpila circuito para simulador
result = simulator.run(tqc, shots=1024).result() # Executa circuito no simulador

counts = result.get_counts() # Obtém resultados da medição
print("Resultados da medição:", counts) # Exibe resultados (se houver mais de um, o mais frequente é a string oculta 's')

measured = list(counts.keys())[0]
print("Resultado quântico:", measured[::-1]) # Inverte string para corresponder à ordem dos qubits (q0 é o bit menos significativo)

Algoritmo de Simon (Simon's problem):

Similar ao algoritmo de Bernstein-Vazirani, mas encontrar string oculta com propriedade de simetria específica, de forma indireta, via deduções a partir de restrições.

  • Cadeia de caracteres totalmente 0 (exemplo: 0000): um-para-um (para cada cadeia de caracteres de saída possível de f, há exatamente 1 cadeia de caracteres de entrada que fazem com que f produza essa cadeia);
  • Cadeia de caracteres não totalmente 0 (exemplo 0101): dois-para-um (para cada cadeia de caracteres de saída possível de f, há exatamente 2 cadeias de caracteres de entrada que fazem com que f produza essa cadeia);
  1. Aplica-se 1ª camada de portas Hadamard em todos qubits de entrada;
  2. Aplica-se oráculo (porta Uf) em todos qubits, onde saída da função f é XOR no estado totalmente zero dos qubits inferiores da m;
  3. Aplica-se 2ª camada de portas Hadamard em todos qubits de entrada, onde análise diverge dos demais algoritmos quânticos anteriores;
  4. Aplica-se medição, onde nos casos de cadeia de caracteres totalmente 0, função um-para-um é confirmada. Em cadeia de caracteres não totalmente 0, função dois-para-um é confirmada;
  5. Pós-processamento: algoritmo não retorna s diretamente. Ele retorna vetores z que são ortogonais a s (em aritmética binária). Após coletar n-1 vetores linearmente independentes, resolve-se sistema linear clássico para achar s;

# pip3 install qiskit qiskit-ibm-runtime qiskit-aer qiskit[visualization]
import numpy as np
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

s = "1000" # String oculta
n = len(s) # Quantidade de qubits de entrada
assert n >= 2 and any(c == "1" for c in s) # Verifica s válido

qr = QuantumRegister(2*n) # Circuito com 2n qubits
cr = ClassicalRegister(n) # Registrador clássico para n bits
qc = QuantumCircuit(qr, cr) # Inicialização do circuito

qc.h(range(n)) # Aplicar porta Hadamard nos qubits de entrada
qc.barrier()

# Oráculo de Simon:
for i in range(n):
    qc.cx(qr[i], qr[n+i]) # Aplicar porta CNOT (entrelaçamento) nos qubits de entrada e saída

j = s.find("1") # Encontrar posição do qubit de controle (primeiro "1" em s)

for i, bit in enumerate(s):
    if bit == "1":
        qc.cx(qr[j], qr[n+i]) # Aplicar porta CNOT para criar função oculta

perm = list(np.random.permutation(n)) # Permutação aleatória dos qubits de entrada
init = list(range(n))
i = 0

while i < n:
    if init[i] != perm[i]:
        k = perm.index(init[i]) # Encontrar posição do qubit a ser trocado
        qc.swap(qr[n+i], qr[n+k]) # Aplicar porta SWAP para permutar qubits de saída
        init[i], init[k] = init[k], init[i] # Atualizar lista de inicialização
    else:
        i += 1

for i in range(n):
    if np.random.random() > 0.5:
        qc.x(qr[n+i]) # Aplicar porta X (NOT) para criar ruído

qc.barrier()

qc.h(range(n)) # Aplicar porta Hadamard novamente nos qubits de entrada
qc.measure(range(n), range(n)) # Medir qubits de entrada e armazenar resultados no registrador clássico
display(qc.draw("mpl"))

sim = AerSimulator()
tqc = transpile(qc, sim) # Transpilar circuito para simulador local
result = sim.run(tqc, shots=2048).result() # Executar circuito e obter resultados
counts = result.get_counts()
print("Contagens:", counts)
plot_histogram(counts)

# Resolver Simon (GF2):
Y = []
for bitstr, c in counts.items():
    if bitstr != "0"*n: # Se resultado não for vetor nulo
        y = [int(b) for b in bitstr[::-1]] # Converter string de bits para lista de inteiros (revertendo ordem)
        if y not in Y:
            Y.append(y)
Y = np.array(Y)

def gf2_rref(A):
    A = A.copy() % 2 # Garantir que A seja binária
    rows, cols = A.shape # Obter número de linhas e colunas
    r = 0
    pivots = [] # Lista para armazenar índices das colunas pivô
    for c in range(cols):
        pivot = None
        for i in range(r, rows):
            if A[i,c] == 1: # Encontrar linha com 1 na coluna c
                pivot = i
                break
        if pivot is None:
            continue
        A[[r,pivot]] = A[[pivot,r]] # Trocar linha r com linha pivot
        pivots.append(c) # Adicionar índice da coluna pivô à lista
        for i in range(rows):
            if i != r and A[i,c] == 1:
                A[i] ^= A[r] # Subtrair linha r da linha i (XOR para GF(2))
        r += 1
        if r == rows:
            break
    return A, pivots

R, pivots = gf2_rref(Y) # Obter forma reduzida por linhas e índices dos pivôs

free_cols = [c for c in range(n) if c not in pivots] # Colunas livres correspondem variáveis que podem ser escolhidas livremente
s_found = np.zeros(n, dtype=int) # Inicializar vetor s encontrado com zeros

if len(free_cols) == 0:
    print("Nenhuma variável livre - oráculo degenerado")
else:
    s_found[free_cols[0]] = 1 # Escolher primeira variável livre como 1 (pode ser qualquer combinação linear das variáveis livres)
    for r, c in enumerate(pivots):
        if R[r, free_cols[0]] == 1: # Se linha r tem 1 na coluna da variável livre escolhida, então s[c] deve ser 1 para satisfazer a equação
            s_found[c] = 1 # Definir s[c] como 1 para satisfazer equação Y·s = 0 mod 2

s_found_str = "".join(map(str, s_found)) # Converter vetor s encontrado para string de bits (valor final encontrado)

def dot_mod2(a,b):
    return sum(x*y for x,y in zip(a,b)) % 2 # Função para calcular produto escalar mod 2 entre dois vetores

ok = all(dot_mod2(row, s_found) == 0 for row in Y) # Verificar se Y·s_found = 0 mod 2 para todas linhas de Y

print("Sistema Y:", Y)
print("s encontrado:", s_found_str)
print("s real:", s)
print("Verificação Y·s = 0 mod2:", ok)

Teleportação Quântica (Quantum Teleportation):

Protocolo quântico para transferir estado quântico de qubit a outro, via comunicação clássica de 2 bits, resultante de emaranhamento quântico pós-medição de Bell AB. Matéria física não é teletransportada, apenas informação (estado quântico). Alice (remetente) possui qubit A, e Bob (receptor) possui qubit B emaranhado com Alice (Bell, par emaranhado AB), possibilitando transmitir estado quântico entre eles. Alice mede AB e passa a ter 3º qubit Q, com estado desconhecido, que deseja teletransportar ao Bob. Alice mede AB, obtendo 2 bits clássicos, e envia a Bob. Bob aplica correções em B com base nos bits recebidos, recuperando estado original de Q em B. Etapas do teletransporte quântico:

  1. Alice aplica porta CNOT (CX) no Bell QA, onde Q é controle e A é alvo;
  2. Alice aplica porta Hadamard (H) em Q;
  3. Alice mede (interferência destrutiva) Bell QA e transmite resultados, em 2 bits clássicos, ao Bob. Resultado da medição de A é a, e Q é b;
  4. Bob recebe a e b de Alice e transforma:
    • Se a=1, então Bob aplica porta Pauli X (NOT) em B;
    • Se b=1, então Bob aplica porta Pauli Z (inversão de fase) em B;
    • Exemplo: ab=[00,01,10,11], então Bob executa [I,Z,X,XZ] em B, sendo Q teletransportado para B.

Após teletransporte, é impossível clonar estado quântico de Q, pois mudou-se do valor original. O estado de A também foi modificado, não estando mais emaranhado com B. Fluxo do teletransporte quântico:

  1. Qubit 0 (Q): estado desconhecido (a ser teleportado);
  2. Qubits 1 (A) e 2 (B): criam par de Bell AB (canal quântico);
  3. Alice opera em (0,1 ou seja QA) e mede ambos, obtendo bits clássicos a e b;
  4. Bob corrige qubit 2 (B) com base em a e b, recuperando estado original de Q em B.

# pip3 install qiskit qiskit-ibm-runtime qiskit-aer qiskit[visualization]
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit import QuantumCircuit, transpile, ClassicalRegister, QuantumRegister
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel
from qiskit.primitives import BackendSamplerV2
from qiskit.visualization import plot_histogram
from IPython.display import display
import numpy as np
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

secret = QuantumRegister(1, "Q") # Qubit Q, que contém estado quântico a ser teletransportado
Alice = QuantumRegister(1, "A") # Qubit A de Alice (remetente)
Bob = QuantumRegister(1, "B") # Qubit B de Bob (destinatário)

cr = ClassicalRegister(3, "c") # Bits clássicos para armazenar resultados das medições de Alice
qc = QuantumCircuit(secret, Alice, Bob, cr) # Circuito quântico que implementa protocolo de teletransporte

qc.h(Alice) # Alice aplica porta Hadamard (superposição) em A
qc.cx(Alice, Bob) # Alice aplica porta CNOT (entrelaçamento) entre A e B
qc.barrier()

np.random.seed(42) # Criar estado quântico aleatório para Q
theta = np.random.uniform(0.0, 1.0) * np.pi # de 0 a pi
varphi = np.random.uniform(0.0, 2.0) * np.pi  # de 0 a 2*pi
qc.u(theta, varphi, 0.0, secret) # Alice aplica porta U (rotação unitária - prepara estado quântico possível do qubit a partir dos ângulos dados) em Q, parametrizada por theta e varphi
qc.barrier()

qc.cx(secret, Alice) # Alice aplica porta CNOT em Bell QA
qc.h(secret) # Alice aplica porta Hadamard em Q
qc.barrier()

qc.measure(Alice, cr[1]) # Alice mede A e armazena resultado em bit clássico cr[1]
qc.measure(secret, cr[0]) # Alice mede Q e armazena resultado em bit clássico cr[0]

# Condição para Bob aplicar correção em B, conforme das medições de Alice (cr[1] e cr[0])
with qc.if_test((cr[1], 1)):
    qc.x(Bob) # Se cr[1] for 1, Bob aplica porta Pauli X (NOT) em B
with qc.if_test((cr[0], 1)):
    qc.z(Bob) # Se cr[0] for 1, Bob aplica porta Pauli Z (fase) em B

qc.draw(output="mpl")
qc.barrier()

qc.u(theta, varphi, 0.0, Bob).inverse() # Bob aplica inversa da porta U para verificar se recuperou estado original de Q
qc.measure(Bob, cr[2]) # Bob mede B e armazena resultado em bit clássico cr[2]
qc.draw(output="mpl")

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127) # Encontra backend físico disponível com mínimo 127 qubits
print(backend.name)

target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
qc_isa = pm.run(qc) # Transpilação do circuito quântico para backend escolhido, otimizando para nível 3 (máximo)

sampler = Sampler(mode=backend)
noise_model = NoiseModel.from_backend(backend) # Extrai modelo de ruído do backend físico para simulação realista

backend_sim = AerSimulator(noise_model=noise_model)
sampler_sim = BackendSamplerV2(backend=backend_sim) # Simulador com modelo de ruído do backend físico

job = sampler.run([qc_isa]) # Executa circuito transpilado no backend físico via sampler
res = job.result() # Obtém resultados da execução do circuito no backend físico
counts = res[0].data.c.get_counts() # Extrai contagem de resultados das medições (bits clássicos) para análise
plot_histogram(counts) # Medição de Bob (cr[2])=0, indicando que estado original de Q foi teletransportado com sucesso para B 

Superdense Coding:

Protocolo quântico para transmitir 2 bits clássicos enviando apenas 1 qubit, desde que Alice e Bob compartilhem previamente par emaranhado (par de Bell). Em vez de permitir transmissão de qubit via 2 bits clássicos de comunicação, permite transmitir 2 bits clássicos qubit de comunicação quântica. Remetente (Alice) possui qubit A, e destinatário (Bob) possui qubit B. Juntos, possuem par de Bell AB. Alice quer enviar 2 bits clássicos (c e d) para Bob, via qubit. Conforme teorema de Holevo, emaranhamento compartilhado permite dobrar capacidade de transmissão de informação clássica dos qubits enviados. Etapas:

  1. Alice codifica bits clássicos c e d em seu qubit A (c=1 aplica porta X, d=1 aplica porta Z):
    • cd 00 aplica I em A (identidade, nada ocorre);
    • cd 01 aplica Z em A (inversão de fase, altera sinal de |1⟩ para -|1⟩ e mantém |0⟩ inalterado);
    • cd 10 aplica X em A (NOT, troca |0⟩ por |1⟩ e vice-versa);
    • cd 11 aplica XZ em A (NOT seguido de Z, inverte estado e altera fase, resultando em -|0⟩ e -|1⟩).
  2. Então, Alice envia seu qubit A para Bob;
  3. Bob recebe A e aplica porta CNOT (CX) em AB, onde A é controle e B é alvo;
  4. Bob aplica porta Hadamard (H) em A;
  5. Bob mede mede qubits AB (B para obter c e A para obter d), obtendo 2 bits clássicos enviados por Alice.

Em síntese, Alice escolhe qual estado de Bell deseja compartilhar com Bob, envia seu qubit para Bob, e Bob mede para determinar qual estado de Bell Alice escolheu.


# pip3 install qiskit qiskit-ibm-runtime qiskit-aer qiskit[visualization]
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit_aer import AerSimulator
from qiskit.visualization import plot_histogram
from IPython.display import display
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

c = "1"
d = "0"
qc = QuantumCircuit(2)

# Preparar estado de Bell entre Alice e Bob
qc.h(0) # Aplica Hadamard (superposição) no qubit de Alice
qc.cx(0, 1) # Aplica CNOT (entrelaçamento) para criar estado de Bell entre Alice (qubit 0) e Bob (qubit 1)
qc.barrier()

# Operações de Alice, dependendo dos bits c e d:
if d == "1":
    qc.z(0) # Se d é 1, aplica Z (inversão de fase) no qubit de Alice
if c == "1":
    qc.x(0) # Se c é 1, aplica X (inversão de bit) no qubit de Alice
qc.barrier()

# Operações de Bob para decodificar bits de Alice:
qc.cx(0, 1) # Aplica CNOT para desfazer entrelaçamento entre Alice e Bob
qc.h(0) # Aplica Hadamard para transformar estado de Bob em estado mensurável
qc.measure_all() # Mede ambos qubits para obter bits de Alice
display(qc.draw(output="mpl"))

result = AerSimulator().run(qc).result()
statistics = result.get_counts()

for outcome, frequency in statistics.items():
    print(f"Measured {outcome} with frequency {frequency}") # Exibe resultados de medição (cd)

display(plot_histogram(statistics)) # Exibe resultados de medição (cd) para teste de superdense coding

rbg = QuantumRegister(1, "coin") # 'coin' qubit para gerar bits de Alice
ebit0 = QuantumRegister(1, "A") # Qubit de Alice
ebit1 = QuantumRegister(1, "B") # Qubit de Bob

Alice_c = ClassicalRegister(1, "Alice c") # Registrador clássico para bit c de Alice
Alice_d = ClassicalRegister(1, "Alice d") # Registrador clássico para bit d de Alice
test = QuantumCircuit(rbg, ebit0, ebit1, Alice_d, Alice_c) # Circuito para teste de superdense coding

# Preparar estado de Bell entre Alice e Bob
test.h(ebit0) # Aplica Hadamard (superposição) no qubit de Alice
test.cx(ebit0, ebit1) # Aplica CNOT (entrelaçamento) para criar estado de Bell entre Alice (ebit0) e Bob (ebit1)
test.barrier()

# Use the 'coin' qubit twice to generate Alice's bits c and d.
test.h(rbg) # Aplica Hadamard para criar superposição no 'coin' qubit
test.measure(rbg, Alice_c) # Mede 'coin' qubit para obter bit c de Alice
test.h(rbg) # Aplica Hadamard novamente para preparar 'coin' qubit para bit d
test.measure(rbg, Alice_d) # Mede 'coin' qubit para obter bit d de Alice
test.barrier()

# Operações de Alice, dependendo dos bits c e d:
with test.if_test((Alice_d, 1), label="Z"):
    test.z(ebit0) # Se d é 1, aplica Z (inversão de fase) no qubit de Alice
with test.if_test((Alice_c, 1), label="X"):
    test.x(ebit0) # Se c é 1, aplica X (inversão de bit) no qubit de Alice
test.barrier()

# Operações de Bob:
test.cx(ebit0, ebit1) # Aplica CNOT para desfazer entrelaçamento entre Alice e Bob
test.h(ebit0) # Aplica Hadamard para transformar estado de Bob em estado mensurável
test.barrier()

Bob_c = ClassicalRegister(1, "Bob c") # Registrador clássico para bit c de Bob
Bob_d = ClassicalRegister(1, "Bob d") # Registrador clássico para bit d de Bob
test.add_register(Bob_d) # Adiciona registrador clássico para bit d de Bob
test.add_register(Bob_c) # Adiciona registrador clássico para bit c de Bob
test.measure(ebit0, Bob_d) # Mede qubit de Alice para obter bit d de Bob
test.measure(ebit1, Bob_c) # Mede qubit de Bob para obter bit c de Bob
display(test.draw(output="mpl"))

result = AerSimulator().run(test).result()
statistics = result.get_counts()
display(plot_histogram(statistics)) # Exibe resultados de medição (cd) para teste de superdense coding

Quantum Key Distribution (QKD):

Protocolo quântico para distribuição segura de chaves criptográficas entre 2 partes (Alice e Bob). Protocolo BB84 (Bennett & Brassard), Alice envia qubits em estados de polarização específicos, e Bob mede esses qubits usando bases de medição aleatórias. Após transmissão, Alice e Bob comparam suas bases de medição publicamente, descartando casos onde bases não coincidem. Bits restantes formam chave compartilhada. Se terceiro (Eve) tentar interceptar qubits, natureza quântica da informação fará com que presença de Eve seja detectada (Teorema da Não-Clonagem, No-cloning theorem), garantindo segurança da chave distribuída. Etapas de QKD:

  1. Alice gera sequência aleatória de bits 0s e 1s, e os elencará em tabela base para preparo de estado quântico, conforme tabela abaixo:
    • Bit 0, Base Z (computacional): |0⟩ (polarização vertical);
    • Bit 1, Base Z (computacional): |1⟩ (polarização horizontal);
    • Bit 0, Base X (Hadamard): |+⟩ = (|0⟩ + |1⟩)/√2 (polarização diagonal);
    • Bit 1, Base X (Hadamard): |-⟩ = (|0⟩ - |1⟩)/√2 (polarização anti-diagonal).
      Exemplo: Alice gera aleatoriamente 0 em base X:
    • Bits de Alice: 0, 1, 0, 0, 1, 1, 0, 1, 0
    • Bases de Alice: X, X, Z, Z, Z, X, Z, Z, X
    • Estados de Alice: |+⟩, |-⟩, |0⟩, |0⟩, |1⟩, |-⟩, |0⟩, |1⟩, |+⟩
  2. Bob faz escolha aleatória de bases para medir qubits recebidos de Alice. Quando Bob escolhe mesma base que Alice, ele obtém bit correto. Caso contrário, há 50% de chance de obter bit correto. Conforme tabela completa abaixo:
    • Bits de Alice: 0, 0, 1, 0, 0
    • Bases de Alice: X, Z, X, Z, X
    • Estados de Alice: |+⟩, |0⟩, |-⟩, |0⟩, |+⟩
    • Bases do Bob: X, Z, X, Z, X, X
    • Estados do Bob (a priori): |+⟩, |0⟩, |-⟩, |0⟩, |+⟩
    • Estados do Bob (medidos): |+⟩, |0⟩, |-⟩, |0⟩, |+⟩
    • Bits do Bob: 0, 0, 1, 0, 0
  3. Alice e Bob comparam publicamente suas bases de medição, descartando casos onde bases não coincidem. Bits restantes formam chave compartilhada. Exemplo, Alice e Bob comparam bases:
    • Bases de Alice: X, Z, X, Z, X
    • Bases do Bob: X, Z, X, Z, X
    • Bits compartilhados (bases coincidentes): 0, 0, 0

Resistência a interceptação e detecção de eavesdropping garantem segurança da chave distribuída, contra métodos de interceptação para descriptografia, conhecida por eavesdropper (espião, bisbilhoteiro) 'Eve'. Eve deve adivinhar base usada na codificação de cada bit. Eve não consegue criar cópia perfeita dos qubits devido ao Teorema da Não-Clonagem. Exemplo de tentativa de eavesdropper:

  • Bits de Alice: 0, 0, 1, 0, 0
  • Bases de Alice: X, Z, X, Z, X
  • Estados de Alice: |+⟩, |0⟩, |-⟩, |0⟩, |+⟩
  • Bases de suposição de Eve: Z, Z, Z, Z, X
  • Estados de Eve (a priori): ?, |0⟩, ?, |0⟩, |+⟩
  • Estados de Eve (medidos): |1⟩, |0⟩, |0⟩, |0⟩, |+⟩
  • Estados da Eve (enviados a Bob): |1⟩, |0⟩, |+⟩, |0⟩, |0⟩
  • Bases do Bob: X, Z, X, Z, X
  • Estados de Bob (a priori): ?, |0⟩, |+⟩, |0⟩, |+⟩
  • Estados de Bob (medidos): |-⟩, |0⟩, |+⟩, |0⟩, |+⟩
  • Bits do Bob: 1, 0, 0, 0, 0

Exemplo de QKD, via protocolo BB84:


# pip3 install qiskit qiskit-ibm-runtime qiskit-aer matplotlib qiskit[visualization]
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit import QuantumCircuit, ClassicalRegister, QuantumRegister
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel
from qiskit.primitives import BackendSamplerV2
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
import numpy as np
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

# SEM EAVESDROPPER:

rng = np.random.default_rng()
bit_num = 20
qc = QuantumCircuit(bit_num, bit_num)
 
# QKD 1 - Alice prepara bits e bases
abits = np.round(rng.random(bit_num))
abase = np.round(rng.random(bit_num))

# Alice prepara estados conforme tabela 1:
for n in range(bit_num):
    if abits[n] == 0:
        if abase[n] == 1:
            qc.h(n)
    if abits[n] == 1:
        if abase[n] == 0:
            qc.x(n)
        if abase[n] == 1:
            qc.x(n)
            qc.h(n)
qc.barrier()

# QKD 2 - Bob escolhe bases aleatórias para medir
bbase = np.round(rng.random(bit_num))
 
for m in range(bit_num):
    if bbase[m] == 1:
        qc.h(m)
    qc.measure(m, m)

print("Alice's bits are ", abits)
print("Alice's bases are ", abase)
print("Bob's bases are ", bbase)
display(qc.draw("mpl"))

# Executar circuito em backend físico com ruído
service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False, min_num_qubits = 127)
print(backend.name)

# Transpilar circuito para backend escolhido, otimizando para nível 3 (máximo)
noise_model = NoiseModel.from_backend(backend)
backend_sim = AerSimulator(noise_model=noise_model)
sampler_sim = BackendSamplerV2(backend=backend_sim)

target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
qc_isa = pm.run(qc)

# Executar circuito transpilado no backend físico via sampler
sampler = Sampler(mode=backend)
job = sampler.run([qc_isa], shots=1)
counts = job.result()[0].data.c.get_counts()
countsint = job.result()[0].data.c.get_int_counts()

# Analisar resultados de medição de Bob, comparando com bits e bases de Alice para determinar quais bits formam chave compartilhada
keys = counts.keys()
key = list(keys)[0]
bmeas = list(key)
bmeas_ints = []
for n in range(bit_num):
    bmeas_ints.append(int(bmeas[n]))
 
bbits = bmeas_ints[::-1]
print(bbits)

# QKD 3 - Pós-processamento para gerar chave compartilhada
agoodbits = []
bgoodbits = []
match_count = 0
for n in range(bit_num):
    # Check whether bases matched.
    if abase[n] == bbase[n]:
        agoodbits.append(int(abits[n]))
        bgoodbits.append(bbits[n])
        # If bits match when bases matched, increase count of matching bits
        if int(abits[n]) == bbits[n]:
            match_count += 1

print(agoodbits)
print(bgoodbits)
print("Fidelity:", match_count / len(agoodbits))
print("Loss:", 1 - match_count / len(agoodbits))


# COM EAVESDROPPER:

bit_num = 20
qr = QuantumRegister(bit_num, "q")
cr = ClassicalRegister(bit_num, "c")
qc = QuantumCircuit(qr, cr)

abits = np.round(rng.random(bit_num))
abase = np.round(rng.random(bit_num))

for n in range(bit_num):
  if abits[n] == 0:
    if abase[n] == 1:
      qc.h(n)
  if abits[n] == 1:
    if abase[n] == 0:
      qc.x(n)
    if abase[n] == 1:
      qc.x(n)
      qc.h(n)

qc.barrier()

# Eavesdropper (Eve) intercepta e mede os qubits
ebase = np.round(rng.random(bit_num))

for m in range(bit_num):
  if ebase[m] == 1:
    qc.h(m)
  qc.measure(qr[m], cr[m])

target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
qc_isa = pm.run(qc)

job = sampler_sim.run([qc_isa], shots=1)
counts = job.result()[0].data.c.get_counts()
countsint = job.result()[0].data.c.get_int_counts()

# Analisar resultados de medição de Eve, comparando com bits e bases de Alice para determinar quais bits formam chave compartilhada
keys = counts.keys()
key = list(keys)[0]
emeas = list(key)
emeas_ints = []

for n in range(bit_num):
  emeas_ints.append(int(emeas[n]))

ebits = emeas_ints[::-1]
print(ebits)

qr = QuantumRegister(bit_num, "q")
cr = ClassicalRegister(bit_num, "c")
qc = QuantumCircuit(qr, cr)

for n in range(bit_num):
  if ebits[n] == 0:
    if ebase[n] == 1:
      qc.h(n)
  if ebits[n] == 1:
    if ebase[n] == 0:
      qc.x(n)
    if ebase[n] == 1:
      qc.x(n)
      qc.h(n)

qc.barrier()

# Bob escolhe bases aleatórias para medir
bbase = np.round(rng.random(bit_num))

for m in range(bit_num):
  if bbase[m] == 1:
    qc.h(m)
  qc.measure(qr[m], cr[m])

target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
qc_isa = pm.run(qc)

job = sampler_sim.run([qc_isa], shots=1)
counts = job.result()[0].data.c.get_counts()
countsint = job.result()[0].data.c.get_int_counts()

keys = counts.keys()
key = list(keys)[0]
bmeas = list(key)
bmeas_ints = []

for n in range(bit_num):
  bmeas_ints.append(int(bmeas[n]))

bbits = bmeas_ints[::-1]
print(bbits)

agoodbits = []
bgoodbits = []
match_count = 0

for n in range(bit_num):
  if abase[n] == bbase[n]:
    agoodbits.append(int(abits[n]))
    bgoodbits.append(bbits[n])
    if int(abits[n]) == bbits[n]:
      match_count += 1

print(agoodbits)
print(bgoodbits)
print("Fidelity:", match_count / len(agoodbits))
print("Loss:", 1 - match_count / len(agoodbits))
# Com Eve, fidelidade e perda são piores, indicando que presença de Eve foi detectada, comprometendo segurança da chave compartilhada, alterando valor final da chave, e permitindo que Alice e Bob descartem chave comprometida, garantindo segurança da comunicação.

Transformação de Fourier quântica (QFT):

A Transformação de Fourier quântica (Quantum Fourier Transform, QFT) é operação fundamental em circuitos quânticos que transforma estado quântico em seu domínio de Fourier (transformar base em outra). É a versão quântica da Transformada Discreta de Fourier (DFT). Enquanto DFT clássica transforma vetor de nºs, QFT transforma amplitudes de estado quântico. Principais pares de bases conectados pela transformada de Fourier são posição e momento, e tempo e frequência. Transformada de Fourier é usada para representar função como combinação linear de novo conjunto das "funções de base". Transformações de base também são feitas regularmente em estados de qubit. Estados de Qubit também podem ser expressos na base de Fourier, sendo estado é expresso em termos de combinação linear ordenados (00,|00...00⟩ até 1,|11...11⟩). Fases dos componentes variam de 0 a 2π. Cada estado tem uma fase que é 2π/4 radianos mais alta do que estado anterior. Etapas da QFT, atuando como "detector de periodicidade":

  1. Seleciona base computacional;
  2. Aplica interferência de fase;
  3. Redistribui probabilidades;
  4. Revela estrutura periódica escondida.

Componentes da QFT:

  • Entrada: estado quântico;
  • Saída: estado quântico;
  • Custo: O(n²) portas (n = qubits);
  • Processamento: probabilístico;
  • Base: amplitudes quânticas.

QFT é implementada via portas Hadamard (H), Portas de fase controladas (CP, CRZ), e SWAP (para reverter ordem dos qubits). Para cada qubit: H → rotações controladas → próximo qubit. Exemplo de circuito lógico contendo 3 qubits:

  • q0:
    • H(q0)
    • CP(π/2) q1→q0
    • CP(π/4) q2→q0
  • q1:
    • H(q1)
    • CP(π/2) q2→q1
  • q2:
    • H(q2)
  • Depois → SWAP q0 ↔ q2

Complexidade QFT para n qubits:

  • Portas Hadamard: n
  • Portas controladas: n(n-1)/2
  • Complexidade: O(n²)

# pip3 install qiskit qiskit-ibm-runtime qiskit-aer qiskit[visualization]
import numpy as np
from qiskit import QuantumCircuit
from qiskit.visualization import plot_histogram
from qiskit.circuit.library import QFTGate
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit.primitives import BackendSamplerV2
from qiskit_aer import AerSimulator
from qiskit_aer.noise import NoiseModel
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False, min_num_qubits = 127)
print(backend.name)

noise_model = NoiseModel.from_backend(backend)
backend_sim = AerSimulator(noise_model=noise_model)
sampler_sim = BackendSamplerV2(backend=backend_sim)

# ALGORITMO 1: Transformar único estado de base computacional:

# Passo 1: Mapear estado de base computacional aleatório
qubits = 4
N = 2**qubits
qc = QuantumCircuit(qubits)

# Inverter estado de qubits aleatórios para colocar em estado de base computacional único aleatório
for i in range(1, qubits):
    if np.random.randint(0, 2):
        qc.x(i)
 
# Criar uma cópia do circuito acima (para ser usado quando aplicarmos QFT na próxima parte)
qc_qft = qc.copy()
qc.measure_all()
qc.draw("mpl")

# Passo 2: Transpilar circuito com QFT
target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
qc_isa = pm.run(qc)

# Passo 3: Executar job em computador quântico
sampler = Sampler(mode=backend)
pubs = [qc_isa] 
job = sampler.run(pubs, shots=1000)
res = job.result()
counts = res[0].data.meas.get_counts()
plot_histogram(counts)
# Conclusão: Resultado deve ser estado de base computacional único aleatório mapeado no passo 1


# Transformação de Fourier de único estado via QFTGate:
# Paso 1: Mapear estado de base computacional aleatório
qc_qft.compose(QFTGate(qubits), inplace=True)
qc_qft.measure_all()
qc_qft.draw("mpl")

# Passo 2: Transpilar circuito com QFT 
target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
qc_isa = pm.run(qc_qft)

# Passo 3: Executar job em computador quântico
sampler = Sampler(mode=backend)
pubs = [qc_isa]
job = sampler.run(pubs, shots=1000)
res = job.result()
counts = res[0].data.meas.get_counts()
plot_histogram(counts)


# Transformar 2 estados de base computacional:
# Passo 1: Mapear estado de base computacional aleatório
qubits = 4
N = 2**qubits

qc = QuantumCircuit(qubits) 
qc.h(qubits - 1)
qc_qft = qc.copy()
qc.measure_all()
qc.draw("mpl")

# Passo 2: Transpilar circuito com QFT
target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
qc_isa = pm.run(qc)

# Passo 3: Executar job em computador quântico
sampler = Sampler(mode=backend)
pubs = [qc_isa]
job = sampler.run(pubs, shots=1000)
res = job.result()
counts = res[0].data.meas.get_counts()
plot_histogram(counts)


# Transformação de Fourier de 2 estados via QFTGate:
# Passo 1: Mapear estado de base computacional aleatório
qc_qft.compose(QFTGate(qubits), inplace=True)
qc_qft.measure_all()
qc_qft.draw("mpl")

# Passo 2: Transpilar circuito com QFT
target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
qc_isa = pm.run(qc_qft)

# Passo 3: Executar job em computador quântico
sampler = Sampler(mode=backend)
pubs = [qc_isa]
job = sampler.run(pubs, shots=1000)
res = job.result()
counts = res[0].data.meas.get_counts()
plot_histogram(counts)


# Analisando algoritmo QFT:
qc = QuantumCircuit(1)
qc.compose(QFTGate(1), inplace=True)
qc.decompose().draw("mpl")

qc = QuantumCircuit(2)
qc.compose(QFTGate(2), inplace=True)
qc.decompose().draw("mpl")

qc = QuantumCircuit(3)
qc.compose(QFTGate(3), inplace=True)
qc.decompose().draw("mpl")

qc = QuantumCircuit(4)
qc.compose(QFTGate(4), inplace=True)
qc.decompose().draw("mpl")

Estimativa de fase quântica (QPE):

Quantum Phase Estimation é algoritmo para estimar fase (autovalor) a vetor próprio de operador unário, sendo útil para encontrar valores próprios de matrizes unitárias (converte fase em bits). Geralmente, utiliza-se QPE juntamente com QFT, usada para extrair informação de fase do estado quântico. QPE usa Hadamard (superposição), Portas controladas U²k, Transformada de Fourier Quântica Inversa (QFT⁻¹) e medição. Estrutura de circuito QPE:

  • Registrador 1- Contagem (t qubits):
    • Inicializados em |0⟩
    • Recebem Hadamard (superposição)
    • Controlam potências de U
  • Registrador 2- Estado alvo:
    • Preparado no autovetor |ψ⟩

Fluxo de funcionamento do QPE:

  1. Aplicar Hadamard no registrador de contagem;
  2. Aplicar portas controladas: CU²⁰,CU²¹,CU²²,...
  3. Aplicar QFT inversa
  4. Medir: obtém bits da fase

# pip3 install qiskit qiskit-ibm-runtime qiskit-aer qiskit[visualization]
from qiskit.circuit.library import QFT
from qiskit_ibm_runtime import QiskitRuntimeService, Sampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit.visualization import plot_histogram
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator, Aer
from qiskit_aer.noise import NoiseModel
import numpy as np
from numpy import pi
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

# Algoritmo 1: Noções de QPE

qpe = QuantumCircuit(4, 3) # Criar circuito com 4 qubits (3 para contagem e 1 para o estado eigen) e 3 bits clássicos para leitura
qpe.x(3) # Aplicar porta X (NOT) no qubit para prepará-lo no estado |1>, que é autovetor do operador U que queremos estimar
display(qpe.draw("mpl"))

for qubit in range(3):
  qpe.h(qubit) # Aplicar porta Hadamard (superposição) nos 3 qubits de contagem

display(qpe.draw("mpl"))

repetitions = 1

for counting_qubit in range(3):
  for i in range(repetitions):
    qpe.cp(pi / 4, counting_qubit, 3) # Realizamos operações unitárias controladas (C-U)
  repetitions *= 2

display(qpe.draw("mpl"))

qpe.append(QFT(3, inverse=True), [0, 1, 2]) # Aplicar QFT inversa nos qubits de contagem
display(qpe.draw("mpl"))

for n in range(3):
  qpe.measure(n, n) # Medir qubits

display(qpe.draw("mpl"))

aer_sim = AerSimulator()
shots = 2048

pm = generate_preset_pass_manager(backend=aer_sim, optimization_level=1)
t_qpe = pm.run(qpe)

# Algoritmo 2: QPE para 3 qubits de contagem e 1 qubit para estado

qpe = QuantumCircuit(4, 3)

for qubit in range(3):
  qpe.h(qubit)

qpe.x(3)

angle = 2 * pi / 3
repetitions = 1

for counting_qubit in range(3):
  for i in range(repetitions):
    qpe.cp(angle, counting_qubit, 3)
  repetitions *= 2

qpe.append(QFT(3, inverse=True), [0, 1, 2])

for n in range(3):
  qpe.measure(n, n)

display(qpe.draw("mpl"))

aer_sim = AerSimulator()
shots = 4096

pm = generate_preset_pass_manager(backend=aer_sim, optimization_level=1)
t_qpe = pm.run(qpe)

sampler = Sampler(mode=aer_sim)
job = sampler.run([t_qpe], shots=shots)
result = job.result()
answer = result[0].data.c.get_counts()

plot_histogram(answer)

# Mapear problema para circuitos e operadores quânticos:
service = QiskitRuntimeService()
qpe = QuantumCircuit(4, 3)
qpe.x(3)

for qubit in range(3):
  qpe.h(qubit)

repetitions = 1

for counting_qubit in range(3):
  for i in range(repetitions):
    qpe.cp(pi / 4, counting_qubit, 3)
  repetitions *= 2

qpe.append(QFT(3, inverse=True), [0, 1, 2])

for n in range(3):
  qpe.measure(n, n)

display(qpe.draw("mpl"))

backend = service.least_busy(simulator=False, operational=True, min_num_qubits=4)
print(backend.name)

pm = generate_preset_pass_manager(backend=backend, optimization_level=2)
qc_compiled = pm.run(qpe)
qc_compiled.draw("mpl", idle_wires=False)

real_sampler = Sampler(mode=backend)
job = real_sampler.run([qc_compiled], shots=1024)
job_id = job.job_id()
print("Job id:", job_id)

job = service.job(job_id)
job.status()
result_real = job.result()

print(result_real)
plot_histogram(result_real[0].data.c.get_counts())

Probabilistic Error Amplification (PEA):

Amplificação de erro probabilística (PEA) é técnica para reconstrução de ruído e amplificação precisa de sinais quânticos, para mitigar erros de hardware. Em vez de reduzir ruído diretamente, PEA amplifica erro, de forma controlada, medindo circuito em múltiplos níveis de ruído, concluindo (extrapolação) com estimativa do resultado sem ruído, via Zero-Noise Extrapolation (ZNE). Inicia aprendendo modelo giratório de cada camada de portas emaranhadas no circuito quântico. Após cada fase de aprendizado, circuitos são executados em cada fator de ruído, amplificando cada camada de emaranhamento pela injeção probabilística de ruído de único qubit proporcional. Estágios do PEA:

  1. Executar circuito original;
  2. Amplificar ruído probabilisticamente (inserindo operações equivalentes extras);
  3. Rodar circuito várias vezes, em múltiplos fatores de ruído;
  4. Coletar valores esperados;
  5. Ajustar curva (regressão);
  6. Extrapolar para ruído em limite 0;
  7. Retornar valor mitigado.

Etapas de execução PEA:

  • Entradas:
    • Circuito quântico C;
    • Observável O;
    • Backend real;
    • Fatores de escala de ruído.
  • Processamento:
  • Para cada fator s:
    • Gerar circuito amplificado Cs (PEA);
    • Executar Cs;
    • Medir valor esperado Es.
  • Ajustar curva Es vs s;
  • Extrapolar s em limite 0
  • Retornar valor mitigado.

# pip3 install qiskit qiskit-ibm-runtime qiskit-aer qiskit[visualization]
from qiskit_ibm_runtime import EstimatorV2 as Estimator, QiskitRuntimeService
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False, min_num_qubits=127)
print(backend.name)

qc = QuantumCircuit(2)
qc.h(0) # Aplicar porta Hadamard (superposição) no qubit 0
qc.cx(0, 1) # Aplicar porta CNOT (entrelaçamento) entre qubit 0 e 1
qc.rx(0.4, 0) # Rotação em X (0.4) no qubit 0
qc.ry(0.7, 1) # Rotação em Y (0.7) no qubit 1
display(qc.draw("mpl"))

observable = SparsePauliOp("ZZ") # Define observável "ZZ" (correlação entre qubits) para medir correlação entre 2 qubits

estimator = Estimator(mode=backend)
estimator.options.resilience.zne_mitigation = True # Habilitar mitigação de erro Zero Noise Extrapolation (ZNE) para melhorar precisão dos resultados em dispositivos quânticos ruidosos
estimator.options.resilience.zne.amplifier = "pea" # Define amplificador de erro como "pea" para aumentar ruído de forma controlada
estimator.options.resilience.zne.noise_factors = [1, 3, 5] # Define fatores de escala de ruído para ZNE, permitindo extrapolação a partir de múltiplos níveis de ruído
estimator.options.resilience.zne.extrapolator = "linear" # Define método de extrapolação como "linear" para estimar valor esperado no limite de ruído zero a partir dos resultados obtidos com diferentes níveis de ruído
print(estimator.options.resilience.zne) # Exibe as configurações de mitigação ZNE para verificar se estão corretas

pm = generate_preset_pass_manager(
    backend=backend,
    optimization_level=1
)

isa_qc = pm.run(qc)
isa_observable = observable.apply_layout(isa_qc.layout)

job = estimator.run([(isa_qc, isa_observable)])
result = job.result()

print(result[0].data.evs) # exibe os valores esperados com mitigação ZNE aplicada (correlações entre qubits. Perto -1: correlação fraca, perto de 1: correlação forte)

Iterative Phase Estimation Algorithm (IPEA):

Também chamado Iterative quantum phase estimation (IQPE). Versão iterativa da estimativa de fase PEA, usando 1 qubit de controle repetidas vezes (iterações), medindo 1 qubit da fase por ciclo/iteração (qubit a qubit), compondo várias iterações. Diferente do PEA, IPEA dispensa QFT completa. Ideal para circuito quântico menor e mais profundo. Enquanto PEA descobre todos qubits da fase em única iteração, IPEA descobre 1 qubit, e usa feedback para descobrir próximo, sucessivamente. Etapas do IPEA:

  1. Criar operador unitário U via porta H (Hadamard, superposição);
  2. Aplicar U controlada por qubit de controle;
  3. Medir qubit de controle;
  4. Usar resultado para ajustar fase de U;
  5. Repetir processo ao próximo qubit de fase.

# pip3 install qiskit qiskit-aer qiskit-ibm-runtime numpy
import numpy as np
from random import random
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

backend = AerSimulator()
q = QuantumRegister(1, "q")
a = QuantumRegister(1, "anc")
c = ClassicalRegister(1, "c")

E_1, E_2 = (2 * np.pi * random(), 2 * np.pi * random()) # Autovalores simulados
t = 1 # Tempo de evolução simulado
print("E_2 real =", E_2)

unitary = QuantumCircuit(q, name="U") # Operador unitário U = exp(iHt) simulado via portas quânticas
unitary.p(E_2 * t, q[0]) # Simula evolução sob H por tempo t, aplicando fase E_2 * t
unitary.x(q[0]) # Aplica X (NOT, inverte |0> e |1>), simulando autovetor |1> de H
unitary.p(E_1 * t, q[0]) # Simula evolução sob H por tempo t, aplicando fase E_1 * t, mas como |1> é autovetor de H, não altera fase, mantendo |1> como autovetor
unitary.x(q[0]) # Aplica X novamente para restaurar |1> (autovetor) e garantir que U|1> = exp(iE_2t)|1>, simulando autovetor com autovalor E_2
print(unitary.draw())

control_u = unitary.to_gate().control(1) # Cria versão controlada de U, onde controle é qubit ancilla a[0] e alvo é q[0]
num_bits_estimate = 8
phase = 0

for k_precision in reversed(range(num_bits_estimate)): # Loop para cada iteração, começando do bit mais significativo
    qc = QuantumCircuit(q, a, c)

    qc.x(q[0]) # Aplicar porta X (NOT, inverter estado |0> para |1>) no qubit de trabalho
    qc.h(a[0]) # Aplicar porta H (Hadamard, superposição) no qubit ancilla (a[0]) para criar superposição de estados |0> e |1>

    for _ in range(2 ** k_precision):
        qc.append(control_u, [a[0], q[0]]) # Aplicar porta controlada U^(2^k) via circuito control_u, onde a[0] é controle e q[0] é alvo

    phase_shift = 2 * np.pi * phase * (2 ** k_precision) # Calcula deslocamento de fase necessário para corrigir fase acumulada com base na estimativa atual
    qc.p(-phase_shift, a[0]) # Aplicar porta de fase (P) para corrigir fase acumulada, onde -phase_shift é ângulo de correção e a[0] é qubit alvo
    qc.h(a[0])
    qc.measure(a[0], c[0])

    tqc = transpile(qc, backend)
    job = backend.run(tqc, shots=2000) # Executar circuito no backend para obter estatísticas de medição
    counts = job.result().get_counts() # Obter contagem de resultados de medição, onde '0' e '1' representam estados medidos do qubit ancilla

    bit = int(max(counts, key=counts.get)) # Determinar bit mais provável (0 ou 1) com base nas contagens, onde max(counts, key=counts.get) retorna chave ('0' ou '1') com maior contagem
    phase += bit / (2 ** (k_precision + 1)) # Atualizar estimativa de fase acumulada com base no bit medido, ajustando fase para refletir contribuição do bit atual
    print(f"bit {k_precision} =", bit)

eigenvalue_est = 2 * np.pi * phase / t # Converter fase estimada de volta para autovalor usando E = phase * (2π / t)
print(qc.draw())
print("Autovalor real:", E_2)
print("Autovalor estimado:", eigenvalue_est)

Quantum Amplitude Amplification (QAA):

Amplificação de amplitude quântica (Grover generalizado + operador de preparação arbitrário) é generalização do algoritmo de Grover, para amplificar probabilidade (amplitude) dos resultados desejados. QAA aplica sequência de operadores em circuito quântico, com oráculo para marcação de estados corretos, produzindo rotação no espaço de amplitudes, aumentando quadraticamente probabilidade de medir solução. Componentes do QAA:

  • Operador A: prepara estado inicial (superposição) a partir de |0⟩;
  • Oráculo O (S_f): operador unitário que marca soluções (estados desejados) invertendo fase;
  • Operador de reflexão (S0): operador de difusão D, que inverte amplitudes em torno da média, amplificando soluções marcadas;
  • Operador Q: geralmente operador Grover's Iteration (G), sequência de operadores que aplica oráculo e reflexão, repetidas em iterações (k) para amplificar amplitude dos estados desejados.

# pip3 install qiskit qiskit-aer qiskit-algorithms qiskit-ibm-runtime
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit_algorithms import Grover, AmplificationProblem
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit.visualization import plot_histogram
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

oracle = QuantumCircuit(2) # Oráculo marcando |11>
oracle.cz(0,1) # Aplica porta CZ (control-Z, inversão de fase em |11> do qubit 0 em relação ao 1) para marcar estado |11>

A = QuantumCircuit(2)
A.h([0,1]) # Aplica porta Hadamard (superposição) aos 2 qubits
A.ry(0.3, 0) # Aplica porta de rotação em torno do eixo Y (0.3 radianos) no qubit 0 para criar superposição ponderada, aumentando probabilidade de medir |11> após amplificação

problem = AmplificationProblem(
    oracle=oracle,
    state_preparation=A
) # Problema de amplificação definido com oráculo e preparação de estado personalizada

grover = Grover(iterations=1) # Algoritmo de Grover configurado para 1 iteração, suficiente para amplificar probabilidade de medir o estado marcado |11>

qc = grover.construct_circuit(problem) # Construir circuito de Grover para problema definido
qc.measure_all()

display(qc.draw('mpl'))

backend = AerSimulator()
tqc = transpile(qc, backend)
sampler = Sampler(backend)
job = sampler.run([tqc], shots=2000)
result = job.result()

counts = result[0].data.meas.get_counts()

print(counts) # Imprime resultado das contagens de medição, mostrando a frequência de cada estado medido, onde estado marcado |11> possuirá maior contagem devido à amplificação de Grover
plot_histogram(counts)

Algoritmo de Shor:

Algoritmo para fatoração de inteiros (Peter Shor, 1994), baseado em transformação quântica de Fourier (QFT), permitindo fatorar nºs inteiros em tempo polinomial, consideravelmente mais rápido que algoritmos clássicos. Consiste em 3 etapas principais:

  1. Redução do problema de fatoração para problema de ordem: escolher nº aleatório a, coprimo a N (nº a ser fatorado), e encontrar ordem (order) r de a módulo N (menor inteiro positivo tal que a^r ≡ 1 mod N);
  2. Estimativa de fase quântica (QPE): usar QPE com QFT para estimar fase associada à função periódica f(x) = a^x mod N, onde período é r, permitindo determinar r;
  3. Fatoração: se r é par e a^(r/2) ≠ -1 mod N, então fatores de N podem ser encontrados usando gcd(a^(r/2) ± 1, N).

Etapas do algoritmo de Shor:

  1. Escolher nº aleatório a, coprimo a N (tal que 1 < a < N e gcd(a,N) = 1);
  2. Usar QPE para estimar fase associada à função periódica f(x) = a^x mod N, onde período é r;
  3. Se r é par e a^(r/2) ≠ -1 mod N, então fatores de N podem ser encontrados usando greatest common divisor (máximo divisor comum - mdc): gcd(a^(r/2) ± 1, N);
  4. Se p e q são fatores não-triviais, então algoritmo é bem-sucedido.

Conceitos que envolvem Shor:

  • Quantum Fourier Transform (QFT): transforma amplitudes de estado quântico, usada para extrair informação de fase;
  • Quantum Phase Estimation (QPE): estimar ângulo (fase) θ associado a autovalor de operador unitário U;
  • Modular Exponentiation (Expo. Modular): constrói-se operadores que calculam a^x mod N dentro de circuito quântico, e QFT converte isso em amostra do período r.

Exemplo Qiskit de implementação do algoritmo de Shor para fatorar N=15, com a=2:


# pip3 install qiskit qiskit-aer qiskit-ibm-runtime qiskit[visualization] numpy
import numpy as np
from math import floor, gcd
from fractions import Fraction
from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
from qiskit.circuit.library import QFT
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit.visualization import plot_histogram
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

# Função para calcular a^(2^k) mod N
def a2kmodN(a, k, N):
    for _ in range(k):
        a = int(np.mod(a**2, N))
    return a

# Funções para criar operadores de multiplicação modular (M2 e M4 em mod15)
def M2mod15():
    circ = QuantumCircuit(4)
    circ.swap(2,3) # Aplica porta SWAP (troca estados entre qubits, 1º passa ao 2º, e 2º ao 1º)
    circ.swap(1,2)
    circ.swap(0,1)
    gate = circ.to_gate()
    gate.name = "M2_mod15"
    return gate

def M4mod15():
    circ = QuantumCircuit(4)
    circ.swap(1,3)
    circ.swap(0,2)
    gate = circ.to_gate()
    gate.name = "M4_mod15"
    return gate

N = 15
a = 2
num_target = floor(np.log2(N-1)) + 1 # Quantidade de qubits para representar estados alvo (4 para N=15)
num_control = 2 * num_target

control = QuantumRegister(num_control, "ctrl") # Registrador de controle para superposição e medição (8 qubits para precisão suficiente na estimativa do período r)
target = QuantumRegister(num_target, "targ") # Registrador alvo para representar estados de multiplicação modular (4 qubits para N=15)
output = ClassicalRegister(num_control, "out") # Registrador clássico para armazenar resultados da medição dos qubits de controle (8 bits para precisão suficiente na estimativa do período r)

qc = QuantumCircuit(control, target, output)
qc.x(target[0]) # Aplica porta X (NOT) para preparar estado |1> no registrador alvo

for qubit in control:
    qc.h(qubit) # Aplica porta Hadamard (superposição) nos qubits de controle

for k, qubit in enumerate(control):
    b = a2kmodN(a, k, N) # Calcula a^(2^k) mod N para determinar qual operador aplicar

    if b == 2:
        qc.compose(M2mod15().control(), [qubit] + target[:], inplace=True) # Aplica operador controlado M2mod15 se b=2
    elif b == 4:
        qc.compose(M4mod15().control(), [qubit] + target[:], inplace=True) # Aplica operador controlado M4mod15 se b=4

qc.compose(QFT(num_control, inverse=True), control, inplace=True) # Aplica QFT inversa nos qubits de controle
qc.measure(control, output)
display(qc.draw("mpl")) # print(qc.draw())

simulator = AerSimulator()
compiled_circuit = transpile(qc, simulator)
job = simulator.run(compiled_circuit, shots=1024)
result = job.result()
counts = result.get_counts()
print(counts)
plot_histogram(counts)

# Estimativa do período r a partir dos resultados da medição (pós-processamento clássico)
for bitstring in counts:
    decimal = int(bitstring, 2)
    phase = decimal / (2**num_control)

    frac = Fraction(phase).limit_denominator(N)
    r = frac.denominator

    if r % 2 == 0:
        p = gcd(pow(a, r//2) - 1, N) # Calcula p usando valor de r encontrado
        q = gcd(pow(a, r//2) + 1, N) # Calcula q usando valor de r encontrado

        if p * q == N and p != 1 and q != 1:
            print(f"Fatores encontrados: {p} e {q}")
            break

# Conclusão: Shor é capaz de fatorar N=15 em fatores p=3 e q=5, demonstrando capacidade de encontrar fatores não-triviais usando QFT e QPE, ilustrando potencial dos algoritmos quânticos para resolver problemas difíceis de fatoração que são intractáveis para algoritmos clássicos eficientes

Algoritmo de Grover:

Grover Search, algoritmo quântico de busca quadrática (Lov Grover, 1996) em conjunto de dados não estruturados N. Opera em tarefa de pesquisa f(x), onde x são itens de pesquisa. Se há resposta em x, então f(x)=1. Senão, f(x)=0. Etapas do algoritmo de Grover:

  1. Registro de N qubits inicializados em estado |0⟩;
  2. Aplicar porta Hadamard (H) para criar superposição uniforme de todos estados possíveis;
  3. Aplicar oráculo O, que inverte fase dos estados que satisfazem condição de busca (marcando soluções, via porta Z, em Zf);
  4. Aplicar operador de reflexão/difusão D, que inverte amplitudes em torno da média, amplificando estados marcados (amplificação de amplitude);
    • Camadas de difusão que consistem em H, Zor e H são coletivamente chamadas "operador Grover" (Grover operator).
  5. Repetir (iterações) passos 3 e 4 por O(√N) vezes, onde N é nº total de estados, para amplificar significativamente probabilidade de medir soluções;
  6. Medir estado final para obter resultado da busca.

# pip3 install qiskit qiskit-ibm-runtime matplotlib qiskit[visualization]
import math
from qiskit import QuantumCircuit
from qiskit.circuit.library import grover_operator, MCMTGate, ZGate
from qiskit.visualization import plot_distribution
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

mcmt_ex = QuantumCircuit(3)
mcmt_ex.compose(MCMTGate(ZGate(), 3 - 1, 1), inplace=True) # Aplicar porta MCMT (Multi-Controlled Toffoli, objetivo de inverter estado, sob condição) com ZGate como alvo, 2 controles e 1 alvo
mcmt_ex.draw(output="mpl", style="iqp")

# Função oráculo O para Grover, que marca estados-alvo invertendo fase:
def grover_oracle(marked_states):
    if not isinstance(marked_states, list):
        marked_states = [marked_states]
    num_qubits = len(marked_states[0]) # Determinar nº de qubits a partir do comprimento dos estados-alvo

    qc = QuantumCircuit(num_qubits)

    for target in marked_states: # Iterar sobre cada estado-alvo a ser marcado
        rev_target = target[::-1]
        zero_inds = [
            ind for ind in range(num_qubits) if rev_target.startswith("0", ind)
        ] # Identificar índices dos bits '0' no estado-alvo (revertido) para aplicar portas X posteriormente
        qc.x(zero_inds)
        qc.compose(MCMTGate(ZGate(), num_qubits - 1, 1), inplace=True) # Aplicar porta MCMT para inverter fase do estado-alvo, usando ZGate como alvo e todos outros qubits como controles
        qc.x(zero_inds)
    return qc

marked_states = ["1110"]
oracle = grover_oracle(marked_states) # Criar oráculo para marcar estado "1110" (inverter fase desse estado)
oracle.draw(output="mpl", style="iqp")

grover_op = grover_operator(oracle) # Criar operador de Grover a partir do oráculo, que inclui etapas de difusão e amplificação de amplitude
grover_op.decompose(reps=0).draw(output="mpl", style="iqp")

optimal_num_iterations = math.floor(
    math.pi / (4 * math.asin(math.sqrt(len(marked_states) / 2**grover_op.num_qubits)))
) # Nº otimizado de iterações para aplicar Grover operator
print(optimal_num_iterations)

qc = QuantumCircuit(grover_op.num_qubits)
qc.h(range(grover_op.num_qubits)) # Aplicar porta Hadamard (H) para criar superposição uniforme de todos estados possíveis
qc.compose(grover_op.power(optimal_num_iterations), inplace=True) # Aplicar operador de Grover para amplificar amplitude dos estados marcados
qc.measure_all()
qc.draw(output="mpl", style="iqp")

service = QiskitRuntimeService()
backend = service.least_busy(operational=True, simulator=False)
backend.name

target = backend.target
pm = generate_preset_pass_manager(target=target, optimization_level=3)
qc_iza = pm.run(qc)

print("The total depth is ", qc_iza.depth())
print(
    "The depth of two-qubit gates is ",
    qc_iza.depth(lambda instruction: instruction.operation.num_qubits == 2),
)

sampler = Sampler(mode=backend)
sampler.options.default_shots = 10_000
result = sampler.run([qc_iza]).result()
dist = result[0].data.meas.get_counts()

print(dist)
plot_distribution(dist)

2P-Grover:

Two-Phase Grover é variação do algoritmo de Grover que aplica Divide and Conquer (dividir e conquistar) em grande espaço de busca, dividindo-o em fases menores para otimizar quantidade de iterações necessárias, onde Oracle pode ser estruturado hierarquicamente. Etapas:

  1. Divide-se qubits em 2 blocos:
    • Bloco A: n1 qubits;
    • Bloco B: n2 qubits;
    • Com n = n1 + n2.
  2. Fase 1: aplica-se Grover apenas no bloco A, encontrando subespaço candidato;
  3. Fase 2: no subespaço candidato encontrado, aplica-se Grover apenas no bloco B para localizar estado-alvo;
  4. A junção de ambos blocos resulta no estado-alvo completo.

# pip3 install qiskit qiskit-ibm-runtime qiskit-aer qiskit[visualization]
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit.circuit.library import GroverOperator
import math
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

# Objetivo: encontrar |1011>, via 4 qubits em 2 blocos (n1=2, n2=2)
n_total = 4

# Fase 1: Procurar 10 no bloco A (qubits 0 e 1)
oracle_phase1 = QuantumCircuit(2)
oracle_phase1.x(0)
oracle_phase1.cz(0, 1)
oracle_phase1.x(0)

grover_phase1 = GroverOperator(oracle_phase1)
t1 = 1

qc1 = QuantumCircuit(2) 
qc1.h([0,1])
qc1.compose(grover_phase1.power(t1), inplace=True)
qc1.measure_all()
display(qc1.draw("mpl"))

sim = AerSimulator()
compiled1 = transpile(qc1, sim)
result1 = sim.run(compiled1, shots=1024).result()
counts1 = result1.get_counts()

print("Fase 1:", counts1)

# Fase 2: Procurar 11 no bloco B (qubits 2 e 3), condicionado ao resultado da fase 1
oracle_phase2 = QuantumCircuit(2)
oracle_phase2.cz(0, 1)

grover_phase2 = GroverOperator(oracle_phase2)
t2 = 1

qc2 = QuantumCircuit(2)
qc2.h([0,1])
qc2.compose(grover_phase2.power(t2), inplace=True)
qc2.measure_all()
display(qc2.draw("mpl"))

compiled2 = transpile(qc2, sim)
result2 = sim.run(compiled2, shots=1024).result()
counts2 = result2.get_counts()

print("Fase 2:", counts2)

print("Resultado final esperado: |1011>")
print("Resultado final obtido:", counts1, counts2)

Quantum Walks:

Passeios quânticos são análogos quânticos dos random walks (passeios aleatórios clássicos) para domínio da mecânica quântica. São utilizados para modelar e resolver problemas de busca e otimização em contextos quânticos, sendo "passo a passo" de trajetória para resolução de problemas, via técnicas quânticas como amplitudes de probabilidades. Tipos de quantum walks:

  • Discrete-Time Quantum Walks (DTQW): moeda quântica (coin), geralmente qubit, decide direção do passo. Operador deslocamento (shift) move e caminha nesse espaço. Exemplo:
    
    # pip3 install qiskit qiskit-ibm-runtime qiskit-aer qiskit[visualization]
    from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
    from qiskit_aer import AerSimulator
    from qiskit_ibm_runtime import QiskitRuntimeService
    QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")
    
    # 1 qubit para moeda, 2 qubits para posição
    coin_qr = QuantumRegister(1, name="coin")
    pos_qr = QuantumRegister(2, name="pos")
    cr = ClassicalRegister(2, name="c")
    qc = QuantumCircuit(coin_qr, pos_qr, cr)
    
    # Inicialização: posição inicial |00⟩ e moeda em superposição
    coin = coin_qr[0]
    p0 = pos_qr[0]
    p1 = pos_qr[1]
    
    # Se coin = |0⟩, move para direita (posição +1 mod 4, incrementa posição)
    def shift_right(qc):
        qc.cx(coin, p0)
        qc.ccx(coin, p0, p1)
    
    # Se coin = |1⟩, move para esquerda (posição -1 mod 4, decrementa posição)
    def shift_left(qc):
        qc.x(coin)
        qc.ccx(coin, p0, p1)
        qc.cx(coin, p0)
        qc.x(coin)
    
    def step(qc):
        qc.h(coin)
        shift_right(qc)
        shift_left(qc)
    
    n_steps = 2
    
    for _ in range(n_steps): # Realiza 2 passos do quantum walk
        step(qc)
    
    qc.measure(pos_qr, cr)
    display(qc.draw(output="mpl"))
    
    sim = AerSimulator()
    tqc = transpile(qc, sim)
    result = sim.run(tqc, shots=1000).result()
    counts = result.get_counts()
    print("Distribuição final de posições após Quantum Walk:", counts)
    
  • Continuous-Time Quantum Walks (CTQW): não há moeda, onde evolução é contínua sob Hamiltoniano associado ao graf. Evolução é governada por Schrödinger, sendo usada para modelar processos de transporte quântico e algoritmos de busca. Exemplo:
    
    # pip3 install qiskit qiskit-ibm-runtime qiskit-aer qiskit[visualization]
    import numpy as np
    from scipy.linalg import expm
    from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile
    from qiskit.circuit.library import UnitaryGate
    from qiskit_aer import AerSimulator
    from qiskit_ibm_runtime import QiskitRuntimeService
    QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")
    
    # Posições representadas por 2 qubits (4 posições: 00, 01, 10, 11)
    pos_qr = QuantumRegister(2, name="pos")
    cr = ClassicalRegister(2, name="c")
    qc = QuantumCircuit(pos_qr, cr)
    
    # Matriz de adjacência para um ciclo de 4 posições (0-1-2-3-0)
    H = np.array([
        [0,1,0,1],
        [1,0,1,0],
        [0,1,0,1],
        [1,0,1,0]
    ], dtype=complex)
    
    t = 1.0 # Tempo de evolução contínua
    U = expm(-1j * H * t) # Operador de evolução unitário U = e^{-iHt}
    U_gate = UnitaryGate(U, label="CTQW") # Converter em porta unitária
    
    qc.append(U_gate, pos_qr) # Aplicar evolução
    qc.measure(pos_qr, cr)
    display(qc.draw(output="mpl"))
    
    sim = AerSimulator()
    tqc = transpile(qc, sim)
    result = sim.run(tqc, shots=1000).result()
    counts = result.get_counts()
    print("Distribuição final de posições após CTQW:", counts) # Nota-se transferência de amplitude entre posições
    

Trotterization:

Trotterização, em simulação quântica, é técnica, baseada em fórmula Trotter-Suzuki, de aplicação sucessiva de 1 ou mais portas quânticas, escolhidas para aproximar evolução temporal do sistema em intervalo de tempo. Evolução de sistema é descrito por Hamiltoniano H, com operador unitário U, sendo H soma de termos Pauli, com Pj representando produto tensorial dos mesmos atuando em n qubits. Quando Hamiltonianos H combinam-se, em série Taylor. Quando não combinam-se, termos não podem ser reorganizados na série Taylor, onde deve-se diminuir intervalo de tempo t, para que termo de 1ª ordem na expansão de Taylor seja dominante, de forma evolutiva no estado, via pequenas atualizações. Então, 't/r' é etapa de evolução, onde resultado é porta de aplicação r vezes (passos de Trotter). Menor t gera aproximação mais precisa. Erro de Trotter é O(dt^2) para Lie-Trotter (Trotterização de ordem 1) e O(dt^3) para Suzuki-Trotter (Trotterização de ordem 2).

Via modelo de Ising (H = Σ Jᵢⱼ σᵢ σⱼ), evolução temporal em redes lineares N=2 e N=6, em matriz de spinn σᵢ que interagem apenas com vizinhos mais próximos, possuindo direção subida (magnetização +1) e descida (magnetização -1), onde J é energia da iteração, e h magnitude de campo externo. Exemplo de aplicação de Trotterização Lie-Trotter (ordem 1), em modelo Ising:


# pip3 install qiskit qiskit-ibm-runtime qiskit[visualization]
import numpy as np
import matplotlib.pylab as plt
import warnings
from qiskit import QuantumCircuit
from qiskit.circuit.library import PauliEvolutionGate
from qiskit.primitives import StatevectorEstimator
from qiskit.quantum_info import SparsePauliOp
from qiskit.synthesis import SuzukiTrotter, LieTrotter
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")
warnings.filterwarnings("ignore")

# Função Hamiltoniano do modelo de Ising, com acoplamento J, campo externo h, e ângulo alpha.
def get_hamiltonian(nqubits, J, h, alpha):
    ZZ_tuples = [("ZZ", [i, i + 1], -J) for i in range(0, nqubits - 1)]
    Z_tuples = [("Z", [i], -h * np.sin(alpha)) for i in range(0, nqubits)]
    X_tuples = [("X", [i], -h * np.cos(alpha)) for i in range(0, nqubits)]
    
    hamiltonian = SparsePauliOp.from_sparse_list(
        [*ZZ_tuples, *Z_tuples, *X_tuples], num_qubits=nqubits
    )
    return hamiltonian.simplify()

n_qubits = 6
hamiltonian = get_hamiltonian(nqubits=n_qubits, J=0.2, h=1.2, alpha=np.pi / 8.0)
hamiltonian

num_timesteps = 60
evolution_time = 30.0
dt = evolution_time / num_timesteps
product_formula_lt = LieTrotter() # Trotterização de ordem 1, com erro de ordem O(dt^2)
product_formula_st2 = SuzukiTrotter(order=2) # Trotterização de ordem 2, com erro de ordem O(dt^3)
product_formula_st4 = SuzukiTrotter(order=4) # Trotterização de ordem 4, com erro de ordem O(dt^5)

initial_circuit = QuantumCircuit(n_qubits)
initial_circuit.prepare_state("001100")
initial_circuit.decompose(reps=1).draw("mpl")

single_step_evolution_gates_lt = PauliEvolutionGate(
    hamiltonian, dt, synthesis=product_formula_lt
)
single_step_evolution_lt = QuantumCircuit(n_qubits)
single_step_evolution_lt.append(
    single_step_evolution_gates_lt, single_step_evolution_lt.qubits
)

print("Step de Trotter com Lie-Trotter:")
print(f"Depth: {single_step_evolution_lt.decompose(reps=3).depth()}")
print(f"Gate count: {len(single_step_evolution_lt.decompose(reps=3))}")
print(f"Nonlocal gate count: {single_step_evolution_lt.decompose(reps=3).num_nonlocal_gates()}")
print(f"Gate breakdown: {', '.join([f'{k.upper()}: {v}' for k, v in single_step_evolution_lt.decompose(reps=3).count_ops().items()])}")
single_step_evolution_lt.decompose(reps=3).draw("mpl", fold=-1)

# Construção dos operadores de magnetização e correlação para modelo de Ising
magnetization = (
    SparsePauliOp.from_sparse_list(
        [("Z", [i], 1.0) for i in range(0, n_qubits)], num_qubits=n_qubits
    )
    / n_qubits
)
correlation = SparsePauliOp.from_sparse_list(
    [("ZZ", [i, i + 1], 1.0) for i in range(0, n_qubits - 1)], num_qubits=n_qubits
) / (n_qubits - 1)

print("Magentização : ", magnetization)
print("Correlação : ", correlation)

# Simulação da evolução temporal do modelo de Ising
evolved_state = QuantumCircuit(initial_circuit.num_qubits)
evolved_state.append(initial_circuit, evolved_state.qubits)
estimator = StatevectorEstimator()
shots = 10000
precision = np.sqrt(1 / shots)
energy_list = []
mag_list = []
corr_list = []

job = estimator.run(
    [(evolved_state, [hamiltonian, magnetization, correlation])], precision=precision
)

evs = job.result()[0].data.evs
energy_list.append(evs[0])
mag_list.append(evs[1])
corr_list.append(evs[2])

# Loop de evolução temporal (expansão de cada step de evolução e estimação dos valores esperados)
for n in range(num_timesteps):
    evolved_state.append(single_step_evolution_gates_lt, evolved_state.qubits)

    job = estimator.run(
        [(evolved_state, [hamiltonian, magnetization, correlation])],
        precision=precision,
    )

    evs = job.result()[0].data.evs
    energy_list.append(evs[0])
    mag_list.append(evs[1])
    corr_list.append(evs[2])

energy_array = np.array(energy_list)
mag_array = np.array(mag_list)
corr_array = np.array(corr_list)

# Evolução temporal dos observáveis do modelo de Ising
fig, axes = plt.subplots(3, sharex=True)
times = np.linspace(0, evolution_time, num_timesteps + 1)

axes[0].plot(
    times,
    energy_array,
    label="First order",
    marker="x",
    c="darkmagenta",
    ls="-",
    lw=0.8,
)

axes[1].plot(
    times, mag_array, label="First order", marker="x", c="darkmagenta", ls="-", lw=0.8
)

axes[2].plot(
    times, corr_array, label="First order", marker="x", c="darkmagenta", ls="-", lw=0.8
)

axes[0].set_ylabel("Energy")
axes[1].set_ylabel("Magnetization")
axes[2].set_ylabel("Mean spin correlation")
axes[2].set_xlabel("Time")

fig.suptitle("Evolução dos observáveis")
# Evolução temporal dos observáveis (Ising) simulada via Trotterização de Lie-Trotter de ordem 1
# Para resultados mais precisos, comparar com Trotterizações de Suzuki-Trotter de ordem 2 e 4

Jordan-Wigner Transform (JWT):

Transformação de Jordan-Wigner é técnica para mapear operadores de fermiônicos (férmions - que obedecem estatística de Fermi-Dirac) em operadores de spin (qubits), permitindo simulação de sistemas quânticos fermiônicos em computadores quânticos. JWT é usada para simular moléculas e materiais, onde partículas são fermiônicas. JWT mapeia operadores de criação e aniquilação de fermiônicos em produtos tensoriais de operadores de Pauli, onde cada modo fermiônico é representado por qubit. JWT é eficiente para sistemas unidimensionais, mas pode introduzir não-localidade em sistemas multidimensionais, exigindo técnicas adicionais para otimização. Exemplo de aplicação do JWT para simular modelo de Hubbard (modelo de elétrons em rede), usando Trotterization (evolução temporal);:


# pip3 install qiskit qiskit-aer qiskit-nature qiskit-ibm-runtime
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit.library import PauliEvolutionGate
from qiskit_nature.second_q.operators import FermionicOp
from qiskit_nature.second_q.mappers import JordanWignerMapper
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

# Parâmetros (modelo de Hubbard)
t = 1.0 # hopping
U = 4.0 # interação local
num_sites = 2
num_spin_orbitals = 2 * num_sites # up/down para cada sítio
fermion_terms = {} # Hamiltoniano fermiônico

# Termos de hopping (0=up e 1=down)
for spin in [0, 1]:
    i = 0 * 2 + spin
    j = 1 * 2 + spin

    fermion_terms[f"+_{i} -_{j}"] = -t
    fermion_terms[f"+_{j} -_{i}"] = -t

# Termo de interação local
for site in range(num_sites):
    up = site * 2
    down = site * 2 + 1
    fermion_terms[f"+_{up} -_{up} +_{down} -_{down}"] = U

# Operador fermiônico
fermionic_hamiltonian = FermionicOp(
    fermion_terms,
    num_spin_orbitals=num_spin_orbitals
)

mapper = JordanWignerMapper()
qubit_hamiltonian: SparsePauliOp = mapper.map(fermionic_hamiltonian)
print("Hamiltoniano em Pauli:", qubit_hamiltonian)

# Evolução temporal (Trotterização)
evolution_time = 2.0
num_steps = 50
dt = evolution_time / num_steps
qc = QuantumCircuit(num_spin_orbitals)
qc.x(0) # Estado inicial: 1 elétron spin-up no sítio 0

# Evolução unitária e^{-iHt}
for _ in range(num_steps):
    evo_gate = PauliEvolutionGate(qubit_hamiltonian, time=dt)
    qc.append(evo_gate, qc.qubits)

qc.measure_all()

sim = AerSimulator()
tqc = transpile(qc, sim)
job = sim.run(tqc, shots=1000)
result = job.result()
counts = result.get_counts()

print("Distribuição final:", counts)

Fisicamente, elétron "tunelou" para outro sítio, onde 0100 é Q3(0), Q2(1), Q1(0), Q0(0), elétron spin-up foi para sítio 1, e sistema evoluiu via hopping. 0001 é estado em que elétron está no último qubit (site 1 ↓). Efeito quântico gerado devido Hamiltoniano e evolução Trotterizada.


Bravyi–Kitaev Transformation (BK):

Técnica de mapeamento de operadores fermiônicos em operadores de spin (qubits), alternativa ao Jordan-Wigner, que reduz não-localidade em sistemas multidimensionais. BK usa estrutura hierárquica para mapear modos fermiônicos, onde cada qubit representa paridade e ocupação de modos. Enquanto JW armazena ocupação completa à esquerda (string longa de Z), BK armazena informação de paridade e ocupação distribuída em árvore binária. Exemplo de aplicação do BK para simular modelo de Hubbard, usando Trotterization:


# pip3 install qiskit qiskit-aer qiskit-nature qiskit-ibm-runtime
from qiskit import QuantumCircuit, transpile
from qiskit_aer import AerSimulator
from qiskit.quantum_info import SparsePauliOp
from qiskit.circuit.library import PauliEvolutionGate
from qiskit_nature.second_q.operators import FermionicOp
from qiskit_nature.second_q.mappers import BravyiKitaevMapper
from qiskit_ibm_runtime import QiskitRuntimeService
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

# Parâmetros (modelo de Hubbard)
t = 1.0 # hopping
U = 4.0 # interação local
num_sites = 2
num_spin_orbitals = 2 * num_sites # up/down para cada sítio
fermion_terms = {} # Hamiltoniano fermiônico

# Termos de hopping (0=up e 1=down)
for spin in [0, 1]:
    i = 0 * 2 + spin
    j = 1 * 2 + spin

    fermion_terms[f"+_{i} -_{j}"] = -t
    fermion_terms[f"+_{j} -_{i}"] = -t

# Termo de interação local
for site in range(num_sites):
    up = site * 2
    down = site * 2 + 1
    fermion_terms[f"+_{up} -_{up} +_{down} -_{down}"] = U

# Operador fermiônico
fermionic_hamiltonian = FermionicOp(
    fermion_terms,
    num_spin_orbitals=num_spin_orbitals
)

mapper = BravyiKitaevMapper()
qubit_hamiltonian: SparsePauliOp = mapper.map(fermionic_hamiltonian)
print("Hamiltoniano em Pauli:", qubit_hamiltonian)

# Evolução temporal (Trotterização)
evolution_time = 2.0
num_steps = 50
dt = evolution_time / num_steps
qc = QuantumCircuit(num_spin_orbitals)
qc.x(0) # Estado inicial: 1 elétron spin-up no sítio 0

# Evolução unitária e^{-iHt}
for _ in range(num_steps):
    evo_gate = PauliEvolutionGate(qubit_hamiltonian, time=dt)
    qc.append(evo_gate, qc.qubits)

qc.measure_all()

sim = AerSimulator()
tqc = transpile(qc, sim)
job = sim.run(tqc, shots=1000)
result = job.result()
counts = result.get_counts()

print("Distribuição final:", counts)

Resultado similar ao JWT, mas com estrutura de mapeamento diferente, onde BK reduz não-localidade em sistemas multidimensionais, sendo mais eficiente para simulação de sistemas quânticos fermiônicos complexos.


Barren Plateaus:

Planaltos áridos (planícies áridas) são regiões do espaço de parâmetros em circuito quântico variacional, onde gradiente da função custo aproxima-se de 0 conforme aumento de qubits. Ocasionado por profundidade do circuito, entanglement aleatório, e falta de estrutura nos ansatzes. Causa ineficácia aos algoritmos variacionais quânticos. Pode ser do tipo custo global, local (subconjunto de qubits) ou induzido por ruído quântico. Exemplo de barren plateau, onde observa-se decaimento do gradiente conforme aumento de qubits:


# pip3 install qiskit qiskit-aer qiskit-ibm-runtime matplotlib
import numpy as np
import matplotlib.pyplot as plt
from qiskit import QuantumCircuit
from qiskit.circuit import Parameter
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import EstimatorV2, QiskitRuntimeService
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

# Função custo (Z0) para observar decaimento do gradiente com aumento de qubits
def create_cost_operator(n):
    pauli_string = "Z" + "I"*(n-1)
    return SparsePauliOp(pauli_string)

# Circuito variacional profundo
def create_random_ansatz(n, depth):
    qc = QuantumCircuit(n)
    theta = Parameter("θ")
    
    for _ in range(depth):
        for i in range(n):
            qc.ry(theta, i) # RY com parâmetro compartilhado entre camadas
        for i in range(n-1):
            qc.cx(i, i+1) # Entanglement linear entre qubits adjacentes

    return qc, theta

# Parameter-shift para estimar gradiente
def parameter_shift_gradient(estimator, circuit, operator, theta_value):
    shift = np.pi / 2

    forward = estimator.run([
        (circuit, operator, [theta_value + shift])
    ]).result()[0].data.evs

    backward = estimator.run([
        (circuit, operator, [theta_value - shift])
    ]).result()[0].data.evs

    return 0.5 * (forward - backward)

backend = AerSimulator(method="statevector")
estimator = EstimatorV2(mode=backend)

qubit_list = [2, 4, 6, 8]
depth = 20
mean_gradients = []

for n in qubit_list:
    grads = []
    operator = create_cost_operator(n)
    circuit, theta = create_random_ansatz(n, depth)
    
    for _ in range(20):
        val = np.random.uniform(0, 2*np.pi)
        grad = parameter_shift_gradient(estimator, circuit, operator, val)
        grads.append(abs(grad))
    
    mean_val = np.mean(grads)
    mean_gradients.append(mean_val)
    
    print(f"Gradiente médio com {n} qubits: média |grad| = {mean_val}") # Quanto mais qubits, menor gradiente médio, indicando barren plateau

plt.figure()
plt.plot(qubit_list, mean_gradients, marker='o')
plt.xlabel("Número de Qubits")
plt.ylabel("Média |Gradiente|")
plt.title("Barren Plateau - decaimento do gradiente")
plt.yscale("log")
plt.show()

Variational Quantum Eigensolver (VQE):

Algoritmo híbrido quântico-clássico, para estimar menor valor próprio/autovalor (ground state) energético de Hamiltoniano H (energia fundamental de sistema quântico). Componentes do VQE:

  • Operador Hamiltoniano H do sistema quântico que deseja otimizar;
  • Ansatz parametrizado (trial state): circuito quântico com parâmetros ajustáveis theta (θ), criando função de onda multiqubit para aproximar-se do estado do sistema estudado;
  • Otimização clássica para ajustar parâmetros do ansatz, minimizando/otimizando energia/resultado esperado;
  • Estimator (estimador): medidas quânticas para estimar energia esperada do estado gerado pelo ansatz.

Objetivo do VQE é encontrar melhores parâmetros θ que resultam na menor energia possível (próxima do valor verdadeiro). Passo a passo do VQE:

  1. Definir Hamiltoniano H do sistema quântico, via mapeamento;
  2. Escolher ansatz parametrizado (circuito quântico) para gerar função de onda;
  3. Inicializar parâmetros do ansatz (aleatórios ou heurísticos);
  4. Executar circuito quântico para obter estado gerado pelo ansatz;
  5. Medir energia esperada do estado gerado, via Hamiltoniano H;
  6. Usar otimizador clássico para ajustar parâmetros do ansatz, minimizando energia esperada;
  7. Repetir passos 4-6 até convergência ou atingir critério de parada.

# pip3 install qiskit qiskit-ibm-runtime qiskit-aer qiskit-algorithms
from qiskit import QuantumCircuit
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import EstimatorV2, QiskitRuntimeService
from qiskit_algorithms.minimum_eigensolvers import VQE
from qiskit_algorithms.optimizers import COBYLA
from qiskit.circuit import Parameter
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

hamiltonian = SparsePauliOp.from_list([("Z", 1.0)]) # Hamiltoniano simples (H=Z)

# Circuito Ansatz simples com 1 qubit e parametro θ para rotação Y
qc_ansatz = QuantumCircuit(1)
theta = Parameter("θ")
qc_ansatz.ry(theta, 0)

# Estimador via simulador Aer (Statevector)
backend = AerSimulator(method="statevector")
estimator = EstimatorV2(mode=backend)

optimizer = COBYLA(maxiter=100) # Otimização com COBYLA, com máximo de 100 iterações

# Criar objeto VQE parametrizado
vqe = VQE(
    estimator=estimator,
    ansatz=qc_ansatz,
    optimizer=optimizer,
)

result = vqe.compute_minimum_eigenvalue(hamiltonian) # Executar VQE para encontrar valor mínimo do Hamiltoniano (Ground State)

print("Energia encontrada (Ground State VQE):", result.eigenvalue.real)
print("Melhor vetor de parâmetros:", result.optimal_parameters)

Quantum Approximate Optimization Algorithm (QAOA):

Algoritmo de Otimização Aproximada Quântica, tipo iterativo híbrido quântico-clássico, para resolver problemas de otimização combinatória, onde objetivo é minimizar função custo, em espaço de soluções. Componentes:

  • Cost Hamiltonian (HC): problema formulado como Hamiltoniano de custo a ser minimizado;
  • Mixer Hamiltonian (HM): Hamiltoniano de mistura que promove transições entre estados, via portas X (NOT) e parâmetros beta (y) e gama (b) de controle de evolução;
  • Problema de otimização formulado como Hamiltoniano de custo (cost Hamiltonian) HC, onde estados computacionais representam soluções e energia representa custo;
  • Otimizador clássico para ajustar beta e gama.

Passo a passo:

  1. Formular problema de otimização como Hamiltoniano de custo HC;
  2. Escolher Hamiltoniano de mistura HM;
  3. Inicializar estado de superposição uniforme (via portas Hadamard);
  4. Aplicar evolução sob HC por tempo gamma (y), via UC = e^(-iyHC);
  5. Aplicar evolução sob HM por tempo beta (b), via UM = e^(-ibHM);
  6. Repetir passos 4-5 por p camadas (nível de QAOA);
  7. Medir estado final e otimizar parâmetros (y, b) para maximizar probabilidade encontrar custo minimizado.

Max-Cut (corte máximo) é problema NP-hard de otimização combinatória, para dividir/particionar vértices de grafo em 2 grupos, maximizando nº de arestas entre eles. QAOA em Max-Cut formula Hamiltoniano de custo, onde qubits i e j (par de nós/nodes) iguais (arestas não cortadas) são penalizadas, e qubits diferentes (arestas cortadas) são recompensados. Hamiltoniano de mistura promove transições entre estados, permitindo exploração do espaço de soluções. Exemplo de aplicação do QAOA para resolver Max-Cut em grafo simples, com 5 nós/nodes:


# pip3 install qiskit qiskit-aer qiskit-ibm-runtime matplotlib scipy networkx
import matplotlib.pyplot as plt
import networkx as nx
from scipy.optimize import minimize
from qiskit import QuantumCircuit, transpile
from qiskit.circuit import Parameter
from qiskit.quantum_info import SparsePauliOp
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import EstimatorV2, QiskitRuntimeService
QiskitRuntimeService.save_account(token="SEU_TOKEN_AQUI")

# Grafo cíclico de 4 vértices:
edges = [(0,1),(1,2),(2,3),(3,0)]
n_qubits = 4

G = nx.Graph()
G.add_edges_from(edges)
nx.draw(G, with_labels=True)
plt.title("Grafo para MaxCut:")
plt.show()

# Hamiltoniano HC:
paulis = []
coeffs = []

for i,j in edges:
    z_string = ["I"] * n_qubits
    z_string[i] = "Z"
    z_string[j] = "Z"

    paulis.append("".join(z_string))
    coeffs.append(0.5)
    paulis.append("I"*n_qubits)
    coeffs.append(-0.5)

cost_operator = SparsePauliOp.from_list(list(zip(paulis, coeffs)))

# Parâmetros variacionais:
gamma = Parameter("γ")
beta = Parameter("β")

# Circuito QAOA:
def create_qaoa():
    qc = QuantumCircuit(n_qubits)
    qc.h(range(n_qubits))

    # Custo unitário:
    for i,j in edges:
        qc.cx(i,j)
        qc.rz(2*gamma, j)
        qc.cx(i,j)

    # Custo de mistura:
    for q in range(n_qubits):
        qc.rx(2*beta, q)

    return qc

# Estimator:
backend = AerSimulator(method="statevector")
estimator = EstimatorV2(mode=backend)
energy_history = []

# Função custo do MaxCut:
def energy(params):
    g, b = params
    qc = create_qaoa()

    qc = qc.assign_parameters({
        gamma:g,
        beta:b
    })

    job = estimator.run(
        [(qc, cost_operator)]
    )

    result = job.result()[0]
    energy_val = result.data.evs
    energy_history.append(energy_val)
    return energy_val

# Otimização clássica:
initial = [0.5,0.5] # valores iniciais para gamma e beta

opt_result = minimize(
    energy,
    initial,
    method="COBYLA"
)

print("Gamma =",opt_result.x[0])
print("Beta =",opt_result.x[1])
print("Energia mínima:", opt_result.fun)

# Gráfico de convergência (energia ao longo das iterações):
plt.plot(energy_history)
plt.xlabel("Iteração")
plt.ylabel("Energia")
plt.title("Convergência do QAOA")
plt.show()

# Circuíto QAOA para MaxCut:
final_circuit = create_qaoa().assign_parameters({
    gamma:opt_result.x[0],
    beta:opt_result.x[1]
})

display(final_circuit.draw(output="mpl"))

# Medir circuito final:
measured = final_circuit.copy()
measured.measure_all()

backend = AerSimulator()
compiled = transpile(measured, backend)
job = backend.run(compiled, shots=2000)
counts = job.result().get_counts()
print("Distribuição de bitstrings:", counts)

print("Cortes encontrados:")
for bit in counts:
    cut = compute_cut(bit, edges)
    print(bit, ":", cut)

# Melhor bitstring encontrado:
best_bitstring = max(counts, key=counts.get)
print("Bitstring mais provável:", best_bitstring)

# Calcular Max-Cut conforme bitstring encontrado:
def compute_cut(bitstring, edges):
    bitstring = bitstring[::-1]
    cut = 0

    for i,j in edges:
        if bitstring[i] != bitstring[j]:
            cut += 1
    return cut

maxcut_value = compute_cut(best_bitstring, edges)
print("Valor do MaxCut encontrado:", maxcut_value)

Quantum Machine Learning (QML):

QML inclui algoritmos e dados quânticos para aprendizado de máquina, juntamente com processamento clássico, devido à falta de QRAM e leitura de dados quânticos. QML encontra mapeamento do conjunto de recursos de dimensão superior, separando pontos de dados para classificar novos pontos. A alta dimensionalidade dos estados quânticos emaranhados não é paralelismo exponencial e não é condição suficiente para aumentar potência do ML. Desquantização (dequantization) é substituição de algoritmo quântico por equivalente clássico, sem perda de eficiência, mostrando que nem todo algoritmo quântico é superior. Exemplo de desquantização é sobrecarga de carregamento de dados, onde leitura de dados quânticos é ineficiente, e algoritmos quânticos que dependem disso podem ser desquantizados para equivalentes clássicos eficientes. Exemplo QML Qiskit:


# Arquivo CSV de dataset: https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv
# pip3 install qiskit qiskit-aer qiskit-ibm-runtime pandas numpy
from qiskit import transpile
from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.circuit.library import unitary_overlap
from qiskit.primitives import StatevectorSampler
from qiskit_ibm_runtime import QiskitRuntimeService
from qiskit_aer import AerSimulator
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
import pandas as pd
import numpy as np

# Etapa 1: mapear entradas clássicas para problema quântico
def get_training_data():
    df = pd.read_csv("dataset_graph7.csv", sep=",", header=None) # Ler dados de treino do dataset CSV
    training_data = df.values[:20, :]
    ind = np.argsort(training_data[:, -1])
    X_train = training_data[ind][:, :-1]
    return X_train
 
X_train = get_training_data() # Obter dados de treino do dataset CSV
num_samples = np.shape(X_train)[0]

# Quantidade de qubits é metade do nº de recursos, pois cada recurso é codificado em par de rotações RX e RZ
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)
entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]

fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits)

for cz in entangler_map:
    fm.cz(cz[0], cz[1])

for i in range(num_qubits):
    fm.rz(-2 * feature_params[2 * i + 1], i)
    fm.rx(-2 * feature_params[2 * i], i)
 
# Escolher 2 pontos de dados para comparar via circuito de sobreposição unitária (overlap)
x1 = 14
x2 = 19
unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])
unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

# Criar circuito de sobreposição unitária
overlap_circ = unitary_overlap(unitary1, unitary2)
overlap_circ.measure_all()
overlap_circ.draw(scale=0.6, style="iqp")

# Etapa 2: Otimizar problema para execução quântica
backend = AerSimulator()
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
job = pm.run(overlap_circ)

# Etapa 3: Executar usando Qiskit em ambiente de simulação
num_shots = 10_000
sampler = StatevectorSampler()
counts = (
    sampler.run([overlap_circ], shots=num_shots).result()[0].data.meas.get_int_counts()
)

# Etapa 4: Análise e pós-processamento 
counts.get(0, 0.0) / num_shots # Probabilidade de medir 0, sobreposição entre estados quânticos codificados pelos pontos x1 e x2

QML quantum data encoding:

Codificação/transformação de dados clássicos em formato quântico, para uso em QML. A normalização quântica consiste em escalar dados para que norma 2 (comprimento redimensionado) do vetor seja 1 (resultante da soma das probabilidades de medição), permitindo representação como estado quântico. Métodos de codificação de dados quânticos:

  • Codificação básica: codificar string clássica P em estado base de sistema P, representado como cadeia de 4 bits (como 0101) e seu estado quântico (como |0101>). Cada bit clássico será qubit. Codificação de vários recursos, onde cada recurso é convertido em sua forma binária e atribuído a grupo distinto de qubits. A junção de todos grupos de qubits representa vetor de recursos completo. Exemplo para converter vetor de recursos (5,7,0) em estado quântico:
    
    # pip3 install qiskit qiskit-aer qiskit-ibm-runtime matplotlib scipy networkx
    from qiskit import QuantumCircuit
    
    x = 5 # binário 0101
    y = 7 # binário 0111
    z = 0 # binário 0000
    
    # Converter cada recurso para vetor com 4 bits
    x_bits = [int(b) for b in format(x, "04b")]
    y_bits = [int(b) for b in format(y, "04b")]
    z_bits = [int(b) for b in format(z, "04b")]
    
    # Combinar todos bits em vetor único
    all_bits = x_bits + y_bits + z_bits  # [0,1,0,1,0,1,1,1,0,0,0,0]
    
    qc = QuantumCircuit(12)
    
    # Aplicar portas X (NOT) nos bits 1
    for idx, bit in enumerate(all_bits):
        if bit == 1:
            qc.x(idx)
    
    qc.draw("mpl")
    
  • Codificação de amplitude: converter vetor clássico N nas amplitudes de estado quântico de log2(N) qubits. Cada valor torna-se amplitude da base computacional, precisando ser normalizado (soma dos quadrados = 1). Se N não for potência de 2, completam-se amplitudes restantes com 0. Exemplo para vetor de recursos (4,8,5,0) convertido em estado quântico:
    
    import math
    
    desired_state = [
        1 / math.sqrt(105) * 4,
        1 / math.sqrt(105) * 8,
        1 / math.sqrt(105) * 5,
        1 / math.sqrt(105) * 0,
    ]
    
    qc = QuantumCircuit(2)
    qc.initialize(desired_state, [0, 1])
    
    qc.decompose(reps=5).draw(output="mpl")
    
  • Codificação angular: converter dados clássicos em circuitos quânticos via rotações em qubits, onde cada valor é usado como ângulo de rotação em porta pauli quântica de rotação, normalmente RY (rotação no eixo Y) ou RX, proporcional ao valor dos dados. Geralmente, circuito resultante possui profundidade constante 1. Exemplo Qiskit de rotação de qubit a partir do estado inicial |0>, com função para visualizar processo de codificação angular:
    
    from qiskit.quantum_info import Statevector
    from qiskit.visualization.bloch import Bloch
    from qiskit.visualization.state_visualization import _bloch_multivector_data
    from math import pi
    import numpy as np
    
    qc = QuantumCircuit(1)
    state1 = Statevector.from_instruction(qc)
    qc.ry(pi / 2, 0) # Aplicar porta pauli RY (rotação no eixo Y)
    
    state2 = Statevector.from_instruction(qc)
    states = state1, state2
    
    # Função para plotar N estados em 1 Bloch Sphere visual
    def plot_Nstates(states, axis, plot_trace_points=True):
        bloch_vecs = [_bloch_multivector_data(s)[0] for s in states]
    
        if axis is None:
            bloch_plot = Bloch()
        else:
            bloch_plot = Bloch(axes=axis)
    
        bloch_plot.add_vectors(bloch_vecs)
    
        if len(states) > 1:
    
            def rgba_map(x, num):
                g = (0.95 - 0.05) / (num - 1)
                i = 0.95 - g * num
                y = g * x + i
                return (0.0, y, 0.0, 0.7)
    
            num = len(states)
            bloch_plot.vector_color = [rgba_map(x, num) for x in range(1, num + 1)]
    
        bloch_plot.vector_width = 3
        bloch_plot.vector_style = "simple"
    
        if plot_trace_points:
    
            def trace_points(bloch_vec1, bloch_vec2):
                # bloch_vec = (x,y,z)
                n_points = 15
                thetas = np.arccos([bloch_vec1[2], bloch_vec2[2]])
                phis = np.arctan2(
                    [bloch_vec1[1], bloch_vec2[1]], [bloch_vec1[0], bloch_vec2[0]]
                )
                
                if phis[1] < 0:
                    phis[1] = phis[1] + 2 * pi
                angles0 = np.linspace(phis[0], phis[1], n_points)
                angles1 = np.linspace(thetas[0], thetas[1], n_points)
    
                xp = np.cos(angles0) * np.sin(angles1)
                yp = np.sin(angles0) * np.sin(angles1)
                zp = np.cos(angles1)
                pnts = [xp, yp, zp]
                bloch_plot.add_points(pnts)
                bloch_plot.point_color = "k"
                bloch_plot.point_size = [4] * len(bloch_plot.points)
                bloch_plot.point_marker = ["o"]
    
            for i in range(len(bloch_vecs) - 1):
                trace_points(bloch_vecs[i], bloch_vecs[i + 1])
    
        bloch_plot.sphere_alpha = 0.05
        bloch_plot.frame_alpha = 0.15
        bloch_plot.figsize = [4, 4]
        bloch_plot.render()
    
    plot_Nstates(states, axis=None, plot_trace_points=True)
    
  • Codificação de fase: codificar dados clássicos em fase de estado quântico, via portas quânticas pauli de rotação de fase Z (RZ), onde cada valor é convertido em ângulo de rotação, e aplicado a qubits para criar superposição com fases relativas, com profundidade 2 e camada Hadamard. Exemplo Qiskit de codificação de fase em qubit de estado inicial |0>:
    
    qc = QuantumCircuit(1)
    qc.h(0) # Aplicar porta Hadamard (superposição) para colocar estado no equador da Bloch Sphere
    state1 = Statevector.from_instruction(qc)
    
    qc.p(pi / 2, 0) # Porta pauli RZ (rotação no eixo Z) no ângulo de rotação pi/2
    state2 = Statevector.from_instruction(qc)
    states = state1, state2
    
    qc.draw("mpl", scale=1)
    plot_Nstates(states, axis=None, plot_trace_points=True)
    
  • Codificação de ângulo denso (Dense Angle Encoding, DAE): combina codificação angular e de fase, onde cada recurso é codificado em par de rotações RX e RZ, permitindo representação mais compacta de dados, usando metade de qubits em comparação com codificação básica. Exemplo Qiskit de codificação de ângulo denso em qubit de estado inicial |0>:
    
    qc = QuantumCircuit(1)
    state1 = Statevector.from_instruction(qc)
    
    qc.ry(3 * pi / 8, 0)
    state2 = Statevector.from_instruction(qc)
    
    qc.rz(7 * pi / 4, 0)
    state3 = Statevector.from_instruction(qc)
    states = state1, state2, state3
    
    plot_Nstates(states, axis=None, plot_trace_points=True)
    

QML encoding feature maps:

Mapas de recursos integrados (feature maps) são circuitos quânticos parametrizados, usados para mapear dados clássicos em espaço de Hilbert de alta dimensão, onde cada ponto de dado é representado como estado quântico. Objetivo é criar representação quântica que permita separação linear dos dados, facilitando tarefas de classificação. Métodos de encoding com mapas de recursos integrados:

  • Efficient SU2: mapa de recursos eficiente, onde cada recurso é codificado em par de rotações RX e RZ, seguido por camadas de portas CNOT para criar entrelaçamento (entanglement) entre qubits. Exemplo Qiskit de encoding feature map Efficient SU2:
    
    # pip3 install qiskit matplotlib pylatexenc
    from qiskit import QuantumCircuit
    from qiskit.circuit.library import efficient_su2
    from qiskit.circuit.library import z_feature_map
    from qiskit.circuit.library import zz_feature_map
    from qiskit.circuit.library import pauli_feature_map
    from qiskit.visualization import circuit_drawer
    import matplotlib.pyplot as plt
    from math import pi
    
    circuit = efficient_su2(num_qubits=2, reps=1, insert_barriers=True) # Circuito SU2 com 2 qubits e 1 repetição
    circuit.decompose().draw()
    
    x = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0, 1.1, 1.2] # Vetor de parâmetros, onde cada elemento corresponde rotação em porta RX ou RZ
    circuit = efficient_su2(num_qubits=3, reps=1, insert_barriers=True)
    encode = circuit.assign_parameters(x)
    encode.decompose().draw()
    
  • Z feature map (ZFM): mapa de recursos baseado em portas de rotação Z (RZ), onde cada recurso é codificado como ângulo de rotação em porta RZ, seguido por camadas de portas CNOT para entrelaçamento. Exemplo Qiskit de encoding feature map Z feature map:
    
    qc0 = QuantumCircuit(1)
    qc1 = QuantumCircuit(1)
    
    qc0.h(0)
    qc0.p(pi / 2, 0)
    
    qc1.h(0)
    qc1.p(pi / 3, 0)
    
    # Combinar circuitos qc0 e qc1 em 1 circuito
    qc = QuantumCircuit(2)
    qc.compose(qc0, [0], inplace=True)
    qc.compose(qc1, [1], inplace=True)
    
    qc.draw()
    
    zfeature_map = z_feature_map(feature_dimension=2, reps=3) # Criar zfeature_map com 2 dimensões e 3 repetições
    zfeature_map = zfeature_map.assign_parameters([(1 / 2) * pi / 2, (1 / 2) * pi / 3]) # Atribuir parâmetros do zfeature_map com valores dos circuitos qc0 e qc1
    zfeature_map.decompose().draw()
    
  • ZZ feature map (ZZFM): mapa de recursos baseado em portas de rotação ZZ, onde cada recurso é codificado como ângulo de rotação em porta de rotação ZZ, seguida por camadas de portas CNOT para criar entrelaçamento. Exemplo Qiskit de encoding feature map ZZ feature map:
    
    qc = QuantumCircuit(2)
    qc.rzz(pi, 0, 1)
    qc.draw()
    
    qc.decompose().draw()
    
    feature_dim = 2 # Quantidade de características
    zzfeature_map = zz_feature_map(feature_dimension=feature_dim, entanglement="linear", reps=1) # Criar circuito de mapeamento de características
    zzfeature_map.decompose(reps=1).draw()
    
    feature_dim = 4
    zzfeature_map = zz_feature_map(feature_dimension=feature_dim, entanglement="linear", reps=1)
    zzfeature_map.decompose().draw()
    
  • Pauli feature map (PFM): mapa de recursos baseado em portas de rotação Pauli, onde cada recurso é codificado como ângulo de rotação em porta de rotação Pauli (RX, RY, RZ), seguida por camadas de portas CNOT para criar entrelaçamento. Exemplo Qiskit de encoding feature map Pauli feature map:
    
    feature_dim = 3
    pfmap = pauli_feature_map(
        feature_dimension=feature_dim, entanglement="linear", reps=1, paulis=["Y", "XX"]
    ) # Criar pauli feature map com dimensão de recurso 3, entrelaçamento linear, 1 repetição, usando portas Y e XX
    
    pfmap.decompose().draw()
    
  • Gallery of Pauli feature maps: galeria de mapas de recursos baseados em portas de rotação Pauli, com diferentes configurações de entrelaçamento, repetições e tipos de portas. Exemplo para visualizar galeria de Pauli feature maps:
    
    feature_dim = 2
    fig, axs = plt.subplots(9, 2)
    i_plot = 0
    
    for paulis in [
        ["I"],
        ["X"],
        ["Y"],
        ["Z"],
        ["XX"],
        ["XY"],
        ["XZ"],
        ["YY"],
        ["YZ"],
        ["ZZ"],
        ["X", "ZZ"],
        ["Y", "ZZ"],
        ["Z", "ZZ"],
        ["X", "YZ"],
        ["Y", "YZ"],
        ["Z", "YZ"],
        ["YY", "ZZ"],
        ["XY", "ZZ"],
    ]:
        pfmap = pauli_feature_map(feature_dimension=feature_dim, paulis=paulis, reps=1)
        
        circuit_drawer(
            pfmap.decompose(),
            output="mpl",
            style={"backgroundcolor": "#EEEEEE"},
            ax=axs[int((i_plot - i_plot % 2) / 2), i_plot % 2],
        )
        
        axs[int((i_plot - i_plot % 2) / 2), i_plot % 2].title.set_text(paulis)
        i_plot += 1
    
    fig.set_figheight(16)
    fig.set_figwidth(16)
    

QML quantum kernel:

Núcleo quântico é kernel matrix e entradas na mesma, onde quantum kernel method é método que usa processador quântico para estimar kernel. Função de kernel quântico visa similaridade entre pontos de dados, onde cada ponto é codificado como estado quântico via feature maps, e kernel é calculado como sobreposição (overlap) entre estados quânticos. Os códigos Qiskit abaixo exibem profundidade dos circuitos quânticos de codificação pré-codificados que usam emaranhamento substancial. Passo a passo:

  1. Mapear entradas clássicas para problema quântico: codificar dados de entrada em circuito quântico (feature map), mapeando vetores de dados para estados no espaço de Hilbert e permitindo calcular produto interno entre 2 dados;
    • Entrada: conjunto de dados de treinamento;
    • Saída: circuito abstrato para calcular entrada da matriz do kernel.
  2. Otimizar problema para execução quântica: aplicar rotinas de otimização do Qiskit para adaptar circuito ao backend quântico escolhido, reduzindo profundidade e erros;
    • Entrada: circuito abstrato, não otimizado para backend específico;
    • Saída: circuito-alvo e observável, otimizado para QPU selecionada (real ou simulador).
  3. Executar usando primitivas de Qiskit Runtime: executar circuito e obter distribuição de probabilidades (ou quase-probabilidades) dos estados medidos;
    • Entrada: circuito alvo;
    • Saída: distribuição de probabilidade.
  4. Pós-processamento, retornar resultado no formato clássico: extrair probabilidade do estado resultante e usá-la para preencher elemento da matriz de kernel, que depois será utilizada em algoritmos clássicos ML.
    • Entrada: distribuição de probabilidade;
    • Saída: único elemento da matriz de kernel, ou matriz de kernel se estiver repetindo.

Código Qiskit para Single kernel matrix:


# pip3 install qiskit qiskit-aer qiskit-ibm-runtime scikit-learn pandas numpy matplotlib
from qiskit.circuit.library import z_feature_map, unitary_overlap
from qiskit.circuit import Parameter, ParameterVector, QuantumCircuit
from qiskit.primitives import StatevectorSampler
from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
from qiskit_aer import AerSimulator
from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2 as Sampler
from qiskit.visualization import plot_distribution
from sklearn.svm import SVC
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

small_data = [
    [-0.194, 0.114, -0.006, 0.301, -0.359, -0.088, -0.156, 0.342, -0.016, 0.143, 1],
    [-0.1, 0.002, 0.244, 0.127, -0.064, -0.086, 0.072, 0.043, -0.053, 0.02, -1],
] # Dados de treino, com rótulos de categoria

train_data = [small_data[0][:-1], small_data[1][:-1]] # Remover rótulos de categoria para calcular produto interno

fm = z_feature_map(feature_dimension=np.shape(train_data)[1]) # Criar feature map, via ZFM
unitary1 = fm.assign_parameters(train_data[0]) # Gerar circuito quântico para 1º dado de treino
unitary2 = fm.assign_parameters(train_data[1]) # Gerar circuito quântico para 2º dado de treino

overlap_circ = unitary_overlap(unitary1, unitary2) # Unir circuitos quânticos para calcular produto interno
overlap_circ.measure_all()

print("Profundidade do circuito:", overlap_circ.decompose().depth())
overlap_circ.decompose().draw()

backend = AerSimulator()
pm = generate_preset_pass_manager(optimization_level=3, backend=backend)
overlap_ibm = pm.run(overlap_circ)

print("Profundidade do circuito:", overlap_ibm.decompose().depth())
overlap_ibm.decompose().depth(lambda instr: len(instr.qubits) > 1)

num_shots = 10000
sampler = Sampler(mode=backend)
results = sampler.run([overlap_ibm], shots=num_shots).result()
counts_bit = results[0].data.meas.get_counts()
counts = results[0].data.meas.get_int_counts()

print("Contagem de bits:", counts_bit)
print("Contagem de inteiros:", counts)
print("Valor do produto interno:", counts.get(0, 0.0) / num_shots)
plot_distribution(counts_bit)

def visualize_counts(probs, num_qubits):
    zero_prob = probs.get(0, 0.0)
    top_10 = dict(sorted(probs.items(), key=lambda item: item[1], reverse=True)[:10])
    top_10.update({0: zero_prob})
    by_key = dict(sorted(top_10.items(), key=lambda item: item[0]))
    xvals, yvals = list(zip(*by_key.items()))
    xvals = [bin(xval)[2:].zfill(num_qubits) for xval in xvals]
    
    plt.bar(xvals, yvals)
    plt.xticks(rotation=75)
    plt.title("Resultados do produto interno")
    plt.xlabel("Bitstrings medidos")
    plt.ylabel("Contagens")
    plt.show()

visualize_counts(counts, overlap_circ.num_qubits)

Código Qiskit para full kernel matrix


# Dataset CSV: https://raw.githubusercontent.com/qiskit-community/prototype-quantum-kernel-training/main/data/dataset_graph7.csv
df = pd.read_csv("dataset_graph7.csv", sep=",", header=None)

# Preparar dados de treino e teste:
train_size = 90
X_train = df.values[0:train_size, :-1]
train_labels = df.values[0:train_size, -1]

test_size = 30
X_test = df.values[train_size : train_size + test_size, :-1]
test_labels = df.values[train_size : train_size + test_size, -1]

# Kernel matrix vazia:
num_samples = np.shape(X_train)[0]
kernel_matrix = np.full((num_samples, num_samples), np.nan)
test_matrix = np.full((test_size, num_samples), np.nan)

# Preparar feature map para processar overlap
num_features = np.shape(X_train)[1]
num_qubits = int(num_features / 2)
entangler_map = [[0, 2], [3, 4], [2, 5], [1, 4], [2, 3], [4, 6]]

fm = QuantumCircuit(num_qubits)
training_param = Parameter("θ")
feature_params = ParameterVector("x", num_qubits * 2)
fm.ry(training_param, fm.qubits) # Rotação Y nos qubits

for cz in entangler_map: # Aplicar CZ (inversão de fase) entre qubits no map
    fm.cz(cz[0], cz[1])

for i in range(num_qubits): # Aplicar rotações Z e X nos qubits
    fm.rz(-2 * feature_params[2 * i + 1], i)
    fm.rx(-2 * feature_params[2 * i], i)

num_shots = 10000
sampler = StatevectorSampler()

for x1 in range(0, train_size):
    for x2 in range(x1 + 1, train_size):
        unitary1 = fm.assign_parameters(list(X_train[x1]) + [np.pi / 2])
        unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

        overlap_circ = unitary_overlap(unitary1, unitary2)
        overlap_circ.measure_all()

        counts = (
            sampler.run([overlap_circ], shots=num_shots)
            .result()[0]
            .data.meas.get_int_counts()
        )

        kernel_matrix[x1, x2] = counts.get(0, 0.0) / num_shots
        kernel_matrix[x2, x1] = counts.get(0, 0.0) / num_shots
    kernel_matrix[x1, x1] = 1

print("Treinamento concluído")

for x1 in range(0, test_size):
    for x2 in range(0, train_size):
        unitary1 = fm.assign_parameters(list(X_test[x1]) + [np.pi / 2])
        unitary2 = fm.assign_parameters(list(X_train[x2]) + [np.pi / 2])

        overlap_circ = unitary_overlap(unitary1, unitary2)
        overlap_circ.measure_all()

        counts = (
            sampler.run([overlap_circ], shots=num_shots)
            .result()[0]
            .data.meas.get_int_counts()
        )

        test_matrix[x1, x2] = counts.get(0, 0.0) / num_shots

print("Matriz de teste concluída")

qml_svc = SVC(kernel="precomputed") # Aplicar algoritmo ML Support Vector Classifier em kernel matrix pré-computada
qml_svc.fit(kernel_matrix, train_labels) # Treinar modelo via kernel matrix, e rótulos de treino como entrada
qml_score_precomputed_kernel = qml_svc.score(test_matrix, test_labels)
print("Score de teste em kernel classificado:", qml_score_precomputed_kernel) # Se 1.0, então Score 100%

Elaborado por Mateus Schwede
ubsocial.github.io