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)

Elaborado por Mateus Schwede
ubsocial.github.io