{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "090b6884",
      "metadata": {},
      "source": [
        "---\n",
        "title: \"Implementação do SQD\"\n",
        "description: \"A diagonalização quântica baseada em amostras (SQD) é implementada no contexto da resolução do estado fundamental de uma molécula de nitrogênio.\"\n",
        "---\n",
        "\n",
        "{/* cspell:ignore fontdict fontsize milli hcore ccsd Motta */}\n",
        "\n",
        "<span id=\"sqd-for-energy-estimation-of-a-chemistry-hamiltonian\" />\n",
        "\n",
        "# SQD para estimativa de energia de um hamiltoniano químico\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ee7de1bc",
      "metadata": {},
      "source": [
        "Nesta aula, aplicaremos a SQD para estimar a energia do estado fundamental de uma molécula.\n",
        "\n",
        "Em particular, discutiremos os seguintes tópicos usando a abordagem do padrão Qiskit $4$ -step:\n",
        "\n",
        "1. Etapa 1: Mapear o problema para circuitos e operadores quânticos\n",
        "   * Configure o Hamiltoniano molecular para $N_2$.\n",
        "   * Explique o cluster unitário local Jastrow (LUCJ) inspirado na química e amigável ao hardware [\\[1\\]](#references)\n",
        "2. Etapa 2: otimizar para o hardware de destino\n",
        "   * Otimizar a contagem de portas e o layout do ansatz para execução em hardware\n",
        "3. Etapa 3: Executar no hardware de destino\n",
        "   * Execute o circuito otimizado em uma QPU real para gerar amostras do subespaço.\n",
        "4. Etapa 4: Resultados do pós-processamento\n",
        "   * Introduzir o loop de recuperação de configuração autoconsistente [\\[2\\]](#references)\n",
        "     * Pós-processar o conjunto completo de amostras de bitstring, usando o conhecimento prévio do número de partículas e a ocupação orbital média calculada na iteração mais recente.\n",
        "     * Criar probabilisticamente lotes de subamostras a partir de cadeias de bits recuperadas.\n",
        "     * Projete e diagonalize o Hamiltoniano molecular em cada subespaço amostrado.\n",
        "     * Salve a energia mínima do estado fundamental encontrada em todos os lotes e atualize a ocupação orbital média.\n",
        "\n",
        "Usaremos vários pacotes de software durante a aula.\n",
        "\n",
        "* `PySCF` para definir a molécula e configurar o Hamiltoniano.\n",
        "* `ffsim` para construir o ansatz LUCJ.\n",
        "* `Qiskit` para transpilar o ansatz para execução em hardware.\n",
        "* `Qiskit IBM Runtime` para executar o circuito em uma QPU e coletar amostras.\n",
        "* `Qiskit addon SQD` recuperação de configuração e estimativa de energia do estado fundamental usando projeção de subespaço e diagonalização de matriz.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "719a9c0e-8c00-4fab-ba23-f2c8a7ebb573",
      "metadata": {},
      "source": [
        "<span id=\"1-map-problem-to-quantum-circuits-and-operators\" />\n",
        "\n",
        "## 1. Mapear o problema para circuitos e operadores quânticos\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d02f97af",
      "metadata": {},
      "source": [
        "<span id=\"molecular-hamiltonian\" />\n",
        "\n",
        "### Hamiltoniano molecular\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5dac27ce-56b1-4f7f-ab83-ac153c004e82",
      "metadata": {},
      "source": [
        "Um Hamiltoniano molecular assume a forma genérica:\n",
        "\n",
        "$$\n",
        "\\hat{H} = \\sum_{ \\substack{pr\\\\\\sigma} } h_{pr} \\, \\hat{a}^\\dagger_{p\\sigma} \\hat{a}_{r\\sigma}\n",
        "+\n",
        "\\sum_{ \\substack{prqs\\\\\\sigma\\tau} }\n",
        "\\frac{(pr|qs)}{2} \\,\n",
        "\\hat{a}^\\dagger_{p\\sigma}\n",
        "\\hat{a}^\\dagger_{q\\tau}\n",
        "\\hat{a}_{s\\tau}\n",
        "\\hat{a}_{r\\sigma}\n",
        "$$\n",
        "\n",
        "$\\hat{a}^\\dagger_{p\\sigma}$ / $\\hat{a}_{p\\sigma}$ são os operadores de criação/aniquilação fermiônica associados ao $p$ -ésimo elemento do conjunto de base e ao spin $\\sigma$. $h_{pr}$ e $(pr|qs)$ são as integrais eletrônicas de um e dois corpos. Usando o site pySCF,, definiremos a molécula e calcularemos as integrais de um e dois corpos do Hamiltoniano para o conjunto de bases `6-31g`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "cc987c08-7261-4c4c-a06b-609d7003efe9",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "converged SCF energy = -108.835236570774\n",
            "CASCI E = -109.046671778080  E(CI) = -32.8155692383188  S^2 = 0.0000000\n"
          ]
        }
      ],
      "source": [
        "import warnings\n",
        "import pyscf\n",
        "import pyscf.cc\n",
        "import pyscf.mcscf\n",
        "\n",
        "warnings.filterwarnings(\"ignore\")\n",
        "\n",
        "# Specify molecule properties\n",
        "open_shell = False\n",
        "spin_sq = 0\n",
        "\n",
        "# Build N2 molecule\n",
        "mol = pyscf.gto.Mole()\n",
        "mol.build(\n",
        "    atom=[[\"N\", (0, 0, 0)], [\"N\", (1.0, 0, 0)]],  # Two N atoms 1 angstrom apart\n",
        "    basis=\"6-31g\",\n",
        "    symmetry=\"Dooh\",\n",
        ")\n",
        "\n",
        "# Define active space\n",
        "n_frozen = 2\n",
        "active_space = range(n_frozen, mol.nao_nr())\n",
        "\n",
        "# Get molecular integrals\n",
        "scf = pyscf.scf.RHF(mol).run()\n",
        "num_orbitals = len(active_space)\n",
        "n_electrons = int(sum(scf.mo_occ[active_space]))\n",
        "num_elec_a = (n_electrons + mol.spin) // 2\n",
        "num_elec_b = (n_electrons - mol.spin) // 2\n",
        "cas = pyscf.mcscf.CASCI(scf, num_orbitals, (num_elec_a, num_elec_b))\n",
        "mo = cas.sort_mo(active_space, base=0)\n",
        "hcore, nuclear_repulsion_energy = cas.get_h1cas(mo)  # hcore: one-body integrals\n",
        "eri = pyscf.ao2mo.restore(1, cas.get_h2cas(mo), num_orbitals)  # eri: two-body integrals\n",
        "\n",
        "# Compute exact energy for comparison\n",
        "exact_energy = cas.run().e_tot"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3aa4e0f0",
      "metadata": {},
      "source": [
        "Nesta lição, usaremos a transformação de Jordan-Wigner (JW) para mapear uma função de onda fermiônica para uma função de onda qubit, de modo que ela possa ser preparada usando um circuito quântico. A transformação JW mapeia o espaço de Fock dos férmions em M orbitais espaciais no espaço de Hilbert de 2M qubits, ou seja, um orbital espacial é dividido em dois *orbitais de spin*, um associado a um elétron com spin up ( $\\alpha$ ) e outro com spin down ( $\\beta$ ). Um orbital de spin pode estar ocupado ou desocupado. Normalmente, quando nos referimos ao número de orbitais, estamos usando o número de orbitais *espaciais*. O número de orbitais de spin será o dobro. Nos circuitos quânticos, representaremos cada orbital de spin com um qubit. Assim, um conjunto de qubits representará spin-up ou $\\alpha$ -orbitals, e outro conjunto representará spin-down ou $\\beta$ -orbitals. Por exemplo, a molécula $N_2$ para o conjunto de base `6-31g` tem orbitais espaciais $16$ (ou seja, $16$ $\\alpha$ + $16$ $\\beta$ = $32$ orbitais de spin). Portanto, precisaremos de um circuito quântico de $32$ -qubit (podemos precisar de qubits de ancilla extras, conforme discutido posteriormente). Os qubits são medidos em bases computacionais para gerar cadeias de bits, que representam configurações eletrônicas ou determinantes (Slater). Ao longo desta lição, usaremos os termos bitstrings, configurações e determinantes de forma intercambiável. As cadeias de bits nos informam a ocupação de elétrons em orbitais de spin: um $1$ em uma posição de bit significa que o orbital de spin correspondente está ocupado, enquanto um $0$ significa que o orbital de spin está vazio. Como os problemas de estrutura eletrônica preservam as partículas, apenas um número fixo de orbitais de spin deve ser ocupado. A molécula $N_2$ tem elétrons $5$ spin-up ( $\\alpha$ ) e $5$ spin-down ( $\\beta$ ). Portanto, qualquer bitstring que represente os orbitais $\\alpha$ e $\\beta$ deve ter cinco $1\\text{s}$ cada para a molécula $N_2$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f6a89c89-85e1-4c0d-abeb-8ab8b154a7ba",
      "metadata": {},
      "source": [
        "<span id=\"11-quantum-circuit-for-sample-generation-the-lucj-ansatz\" />\n",
        "\n",
        "### 1.1 Circuito quântico para geração de amostras: a abordagem LUCJ\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "a464c865-1528-45c2-8ede-325583b15976",
      "metadata": {},
      "source": [
        "Nesta lição, usaremos o ansatz de Jastrow (LUCJ) \\ \\[[1\\]](#references) de cluster acoplado unitário local para a preparação do estado quântico e a amostragem subsequente. Primeiro, explicaremos os diferentes blocos de construção do ansatz UCJ completo e as aproximações feitas na versão local dele. Em seguida, usando o pacote ffsim, construiremos o ansatz LUCJ e o otimizaremos usando o transpilador Qiskit para execução em hardware.\n",
        "\n",
        "O ansatz UCJ tem a seguinte forma (para um produto de $L$ camadas ou repetições do operador UCJ)\n",
        "\n",
        "$$\n",
        "|\\psi\\rangle = \\prod_{\\mu=1}^{L}{(e^{K^{\\mu}} \\times {e^{iJ^{\\mu}}} \\times {e^{-K^{\\mu}}})} |\\Phi_{0}\\rangle\n",
        "$$\n",
        "\n",
        "onde $\\vert \\Phi_{0} \\rangle$ é um estado de referência, geralmente considerado o estado Hartree-Fock (HF). Como o estado Hartree-Fock é definido como tendo os orbitais de menor número ocupados, a preparação do estado HF envolverá a aplicação de portas X para definir os qubits correspondentes aos orbitais ocupados como um. Por exemplo, o bloco de preparação do estado HF para 4 orbitais espaciais e 2 de spin para cima e 2 de spin para baixo pode ter a seguinte aparência:\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "0f0bb614",
      "metadata": {},
      "source": [
        "![Um diagrama de circuito que mostra 8 qubits, sendo 4 denominados orbitais alfa e 4 denominados orbitais beta. Os dois primeiros alfa e os dois primeiros beta possuem uma porta “não”.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig1.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4a32d88b",
      "metadata": {},
      "source": [
        "Uma única repetição do operador UCJ ${(e^{K^{(\\mu)}} \\times {e^{iJ^{(\\mu)}}} \\times {e^{-K^{(\\mu)}}})}$ consiste em uma evolução de Coulomb diagonal ( $e^{iJ^{(\\mu)}}$ ) intercalada por rotações orbitais ( $e^{K^{(\\mu)}}$ e $e^{-K^{(\\mu)}}$ ).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "963e8386-39b6-40b7-9740-fffcf1573fe6",
      "metadata": {},
      "source": [
        "![Um diagrama de circuito que mostra que o circuito UCJ pode ser dividido em camadas de rotação e uma camada de evolução de Coulomb diagonal.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig2.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "271b1825-bc78-4957-bbf8-21a714e45ed3",
      "metadata": {},
      "source": [
        "Os blocos de rotação orbital funcionam em uma única espécie de spin ( $\\alpha$ (up-spin)/ $\\beta$ (down-spin)). Para cada espécie de elétron, a rotação orbital consiste em uma camada de portas de um único qubit $R_{z}$ seguida por uma sequência de portas de rotação de dois qubits de Given (portas $XX + YY$ ).\n",
        "\n",
        "As portas de 2 qubits atuam em orbitais de spin adjacentes (qubits vizinhos mais próximos) e, portanto, são implementáveis em IBM® QPUs sem a necessidade de portas SWAP.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "2e8ac1d2-8f04-4591-921b-8ba0174e4ad0",
      "metadata": {},
      "source": [
        "![Um diagrama de circuito que mostra 4 qubits orbitais alfa e 4 qubits orbitais beta. Os circuitos começam com portas R-Z e, em seguida, apresentam uma série de portas de rotação de Given.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig3.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ebc9b48c-e77d-4af5-b8cf-460daeeadcdb",
      "metadata": {},
      "source": [
        "O $e^{iJ^{(\\mu)}}$, também conhecido como operador de Coulomb diagonal, consiste em três blocos. Dois deles funcionam nos mesmos setores de spin ( $e^{iJ_{\\alpha \\alpha}^{(\\mu)}}$ e $e^{iJ_{\\beta \\beta}^{(\\mu)}}$ ) e um funciona entre dois setores de spin ( $e^{iJ_{\\alpha \\beta}^{(\\mu)}}$ ).\n",
        "\n",
        "Todos os blocos em $e^{iJ^{(\\mu)}}$ consistem em portas de número-número $U_{nn}(\\phi)$ [\\[1\\]](#references). Uma porta $U_{nn}(\\phi)$ pode ser dividida em uma porta $R_{ZZ}(\\frac{\\phi}{2})$ seguida por duas portas $Rz(-\\frac{\\phi}{2})$ de um único qubit que atuam em dois qubits separados.\n",
        "\n",
        "Os componentes de mesmo spin ( $J_{\\alpha \\alpha}$ e $J_{\\beta \\beta}$ ) têm portas $U_{nn}$ entre todos os pares possíveis de qubits. No entanto, como as QPUs supercondutoras têm conectividade restritiva, os qubits devem ser trocados para realizar portas entre qubits não adjacentes.\n",
        "\n",
        "Por exemplo, considere o seguinte bloco $e^{iJ_{\\alpha \\alpha}^{(\\mu)}}$ (ou $e^{iJ_{\\beta \\beta}^{(\\mu)}}$ ) para $N = 4$ orbitais espaciais. Para uma conectividade de qubit linear, as três últimas portas não são diretamente implementáveis, pois funcionam entre qubits não adjacentes (por exemplo, Q0 e Q2 não estão diretamente conectados). Portanto, precisamos de portas SWAP para torná-las adjacentes (a figura a seguir mostra um exemplo com $3$ portas SWAP).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d3e24a20-1c86-4aea-8300-13bd2ead4e8f",
      "metadata": {},
      "source": [
        "![Um diagrama de circuito que mostra qubits acoplados linearmente e os circuitos alfa/beta correspondentes.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig4.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "ef3f1d36-e8ab-46f0-bddb-fa8fb884e531",
      "metadata": {},
      "source": [
        "Em seguida, o $J_{\\alpha \\beta}$ implementa portas entre os mesmos orbitais indexados de diferentes setores de spin (por exemplo, entre $0\\alpha$ e $0\\beta$ ). Da mesma forma, se os qubits não forem fisicamente adjacentes em uma QPU, essas portas também exigirão SWAPs.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9afe0036-318c-43b9-9b2c-39a808bda82c",
      "metadata": {},
      "source": [
        "![Um diagrama de circuito que mostra 4 qubits alfa conectados aos 4 qubits beta.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig5.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "6219aa8e-8a63-4bfc-b52b-64507292471d",
      "metadata": {},
      "source": [
        "Com base na discussão acima, o ansatz UCJ enfrenta alguns obstáculos para a execução de HW, pois precisa de portas SWAP devido a interações de qubit não adjacentes. A variante local do ansatz UCJ, LUCJ, aborda esse desafio removendo alguns $U_{nn}$ do operador de Coulomb diagonal.\n",
        "\n",
        "Nos mesmos blocos de espécies de elétrons, $J_{\\alpha \\alpha}$ e $J_{\\beta \\beta}$ ), mantemos apenas as portas $U_{nn}$ compatíveis com a conectividade do vizinho mais próximo e removemos as portas entre qubits não adjacentes na versão LUCJ. A figura a seguir mostra o bloco LUCJ após a remoção de portas não adjacentes.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "24f54a55-4a09-4508-997a-96306835c7e6",
      "metadata": {},
      "source": [
        "![Um diagrama de circuito que mostra 4 qubits alfa e 4 qubits beta, cada um com portas R-Z, seguidos por portas de dois qubits.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig6.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "165d0a19-0366-4af8-8ea0-20df351f49f5",
      "metadata": {},
      "source": [
        "Em seguida, a versão LUCJ do bloco $J_{\\alpha \\beta}$ que funciona entre diferentes espécies de elétrons pode assumir formas diferentes com base na topologia do dispositivo.\n",
        "\n",
        "Aqui, também, a versão LUCJ elimina as portas não compatíveis. A figura abaixo mostra variantes do bloco $J_{\\alpha \\beta}$ para diferentes topologias de qubit, incluindo grade, hexagonal, heavy-hex e linear.\n",
        "\n",
        "* **Quadrado** : podemos ter portas $U_{nn}$ entre todos os orbitais $\\alpha$ e $\\beta$ sem nenhum SWAP e, portanto, não precisamos remover nenhuma porta $U_{nn}$.\n",
        "* **Heavy-hex** : As interações $\\alpha$ - $\\beta$ são mantidas entre cada $4$ -ésimo orbital de spin indexado (como o 0º, 4º e 8º) e são mediadas por *ancilla*, ou seja, precisamos de qubits de ancilla entre as cadeias lineares que representam os orbitais $\\alpha$ e $\\beta$. Esse arranjo precisa de um número limitado de SWAPs.\n",
        "* **Hexagonal** : Todos os outros orbitais, como o 0º, 2º e 4º orbitais indexados, tornam-se vizinhos mais próximos quando $\\alpha$ e $\\beta$ são dispostos em duas cadeias lineares adjacentes.\n",
        "* **Linear** : Apenas um orbital $\\alpha$ e um $\\beta$ estão conectados, o que significa que o bloco $J_{\\alpha \\beta}$ terá apenas uma porta.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8ec1e433-bc00-42bc-bdbb-288c56c32f9d",
      "metadata": {},
      "source": [
        "Diagramas de ![conectividade para diferentes disposições de qubits. Elas mostram qubits dispostos em uma grade quadrada, uma rede hexagonal, uma rede hexagonal reforçada (rede hexagonal com um qubit adicional ao longo de cada lado do hexágono) e uma cadeia linear.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig7.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3e070615-2b4a-4866-8c6c-d2d038447f45",
      "metadata": {},
      "source": [
        "Embora a remoção de portas da ansatz UCJ para construir a versão LUCJ a torne mais compatível com HW, a ansatz perde um pouco de expressividade. Portanto, mais repetições ( $L$ ) do operador UCJ modificado podem ser necessárias ao usar o ansatz LUCJ.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "04367dac",
      "metadata": {},
      "source": [
        "<span id=\"12-lucj-ansatz-initialization\" />\n",
        "\n",
        "### 1.2 Inicialização do LUCJ ansatz\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "f5eae55f",
      "metadata": {},
      "source": [
        "O LUCJ é um ansatz parametrizado, e precisamos inicializar os parâmetros antes da execução do hardware. Uma maneira de inicializar o ansatz é usar as amplitudes `t1` e `t2` do método CCSD (cluster singles and doubles) clássico acoplado, em que as amplitudes `t1` são o coeficiente de operadores de excitação simples e as amplitudes `t2` são para operadores de excitação dupla.\n",
        "\n",
        "Observe que, embora a inicialização do ansatz LUCJ com `t1` e `t2` amplitudes gere resultados decentes, os parâmetros do ansatz podem precisar de mais otimização.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "1835e74e-3354-425f-8596-c574f03e7a6e",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "E(CCSD) = -109.0398256929733  E_corr = -0.20458912219883\n"
          ]
        }
      ],
      "source": [
        "# Get CCSD t2 amplitudes for initializing the ansatz\n",
        "ccsd = pyscf.cc.CCSD(\n",
        "    scf, frozen=[i for i in range(mol.nao_nr()) if i not in active_space]\n",
        ")\n",
        "ccsd.run()\n",
        "\n",
        "t1 = ccsd.t1\n",
        "t2 = ccsd.t2"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "9f0842fa",
      "metadata": {},
      "source": [
        "<span id=\"13-constructing-the-lucj-ansatz-using-ffsim\" />\n",
        "\n",
        "### 1.3 Construindo a abordagem LUCJ usando `ffsim`\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e5f7f7e8-30e3-40e8-9b85-bf057b35b766",
      "metadata": {},
      "source": [
        "Usaremos o pacote [ffsim](https://github.com/qiskit-community/ffsim/tree/main) para criar e inicializar o ansatz com `t1` e `t2` amplitudes calculadas acima. Como nossa molécula tem um estado Hartree-Fock de casca fechada, usaremos a variante de equilíbrio de spin do ansatz UCJ, [UCJOpSpinBalanced](https://qiskit-community.github.io/ffsim/api/ffsim.html#ffsim.UCJOpSpinBalanced).\n",
        "\n",
        "Como o hardware do IBM tem uma topologia heavy-hex, adotaremos o padrão *em zigue-zague* usado em [\\[1\\]](#references) e explicado acima para interações de qubit. Nesse padrão, os orbitais (qubits) com o mesmo spin são conectados com uma topologia de linha (círculos vermelhos e azuis). Devido à topologia heavy-hex, os orbitais de diferentes spins têm conexões entre cada quarto orbital, ou seja, o 0º, o 4º, o 8º e assim por diante (círculos roxos).\n",
        "\n",
        "![Um padrão em zigue-zague traçado ao longo de uma rede de hexágonos densos.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig8.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "6799421a-4425-404a-9994-47215957054d",
      "metadata": {},
      "outputs": [],
      "source": [
        "import ffsim\n",
        "from qiskit import QuantumCircuit, QuantumRegister\n",
        "\n",
        "n_reps = 2\n",
        "alpha_alpha_indices = [(p, p + 1) for p in range(num_orbitals - 1)]\n",
        "alpha_beta_indices = [(p, p) for p in range(0, num_orbitals, 4)]\n",
        "\n",
        "ucj_op = ffsim.UCJOpSpinBalanced.from_t_amplitudes(\n",
        "    t2=t2,\n",
        "    t1=t1,\n",
        "    n_reps=n_reps,\n",
        "    interaction_pairs=(alpha_alpha_indices, alpha_beta_indices),\n",
        ")\n",
        "\n",
        "nelec = (num_elec_a, num_elec_b)\n",
        "\n",
        "# create an empty quantum circuit\n",
        "qubits = QuantumRegister(2 * num_orbitals, name=\"q\")\n",
        "circuit = QuantumCircuit(qubits)\n",
        "\n",
        "# prepare Hartree-Fock state as the reference state and append it to the quantum circuit\n",
        "circuit.append(ffsim.qiskit.PrepareHartreeFockJW(num_orbitals, nelec), qubits)\n",
        "\n",
        "# apply the UCJ operator to the reference state\n",
        "circuit.append(ffsim.qiskit.UCJOpSpinBalancedJW(ucj_op), qubits)\n",
        "circuit.measure_all()\n",
        "# circuit.decompose().draw(\"mpl\", scale=0.5, fold=-1)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "3b2de316",
      "metadata": {},
      "source": [
        "O ansatz LUCJ com camadas repetidas pode ser otimizado com a fusão de alguns blocos adjacentes. Considere um caso para `n_reps=2`. Os dois blocos de rotação orbital no meio podem ser fundidos em um único bloco de rotação orbital. O pacote `ffsim` tem um gerenciador de passagem chamado `ffsim.qiskit.PRE_INIT` para otimizar o circuito mesclando esses blocos adjacentes.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "7cb99cd9",
      "metadata": {},
      "source": [
        "![Um diagrama que mostra as camadas do modelo LUCJ.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig9.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e9ad460d",
      "metadata": {},
      "source": [
        "<span id=\"2-optimize-for-target-hardware\" />\n",
        "\n",
        "## 2. Otimize para o hardware de destino\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "cecf2994",
      "metadata": {},
      "source": [
        "Primeiro, buscamos um backend de nossa escolha. Otimizaremos nosso circuito para o backend e, em seguida, executaremos o circuito otimizado no mesmo backend para gerar amostras para o subespaço.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "4c8dbbef-378e-4ae3-9290-bde58b72024d",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_ibm_runtime import QiskitRuntimeService\n",
        "\n",
        "service = QiskitRuntimeService()\n",
        "# Use the least-busy backend or specify a quantum computer using the syntax commented out below.\n",
        "backend = service.least_busy(operational=True, simulator=False)\n",
        "# backend = service.backend(\"ibm_brisbane\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1b8ba388-6904-4725-ad97-2a31c0adaa07",
      "metadata": {},
      "source": [
        "Em seguida, recomendamos as seguintes etapas para otimizar o ansatz e torná-lo compatível com o hardware.\n",
        "\n",
        "* Selecione os qubits físicos (`initial_layout`) do hardware de destino que adere ao padrão em ziguezague (duas cadeias lineares com um qubit de ancila entre elas) descrito acima. A disposição dos qubits nesse padrão resulta em um circuito compatível com hardware eficiente com menos portas.\n",
        "* Gere um gerenciador de passes em etapas usando a função [`generate_preset_pass_manager`](/docs/api/qiskit/qiskit.transpiler.generate_preset_pass_manager) do Qiskit com sua escolha de `backend` e `initial_layout`.\n",
        "* Defina o estágio `pre_init` de seu gerenciador de passes em etapas para `ffsim.qiskit.PRE_INIT`. `ffsim.qiskit.PRE_INIT` inclui passagens do transpilador Qiskit que decompõem as portas em rotações orbitais e, em seguida, mesclam as rotações orbitais, resultando em menos portas no circuito final.\n",
        "* Execute o gerenciador de passes em seu circuito.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "ecd2675c-a302-43fb-80f4-d658d56360d5",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Gate counts (w/o pre-init passes): OrderedDict({'rz': 7579, 'sx': 6106, 'ecr': 2316, 'x': 336, 'measure': 32, 'barrier': 1})\n",
            "Gate counts (w/ pre-init passes): OrderedDict({'rz': 4088, 'sx': 3125, 'ecr': 1262, 'x': 201, 'measure': 32, 'barrier': 1})\n"
          ]
        }
      ],
      "source": [
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "\n",
        "spin_a_layout = [0, 14, 18, 19, 20, 33, 39, 40, 41, 53, 60, 61, 62, 72, 81, 82]\n",
        "spin_b_layout = [2, 3, 4, 15, 22, 23, 24, 34, 43, 44, 45, 54, 64, 65, 66, 73]\n",
        "\n",
        "initial_layout = spin_a_layout + spin_b_layout\n",
        "\n",
        "pass_manager = generate_preset_pass_manager(\n",
        "    optimization_level=3, backend=backend, initial_layout=initial_layout\n",
        ")\n",
        "\n",
        "# without PRE_INIT passes\n",
        "isa_circuit = pass_manager.run(circuit)\n",
        "print(f\"Gate counts (w/o pre-init passes): {isa_circuit.count_ops()}\")\n",
        "\n",
        "# with PRE_INIT passes\n",
        "# We will use the circuit generated by this pass manager for hardware execution\n",
        "pass_manager.pre_init = ffsim.qiskit.PRE_INIT\n",
        "isa_circuit = pass_manager.run(circuit)\n",
        "print(f\"Gate counts (w/ pre-init passes): {isa_circuit.count_ops()}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "72a780f9",
      "metadata": {},
      "source": [
        "<span id=\"3-execute-on-target-hardware\" />\n",
        "\n",
        "## 3. Executar no hardware de destino\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "4cd9164c-697a-4aa6-b71f-86720e3d5b66",
      "metadata": {},
      "source": [
        "Após otimizar o circuito para execução em hardware, estamos prontos para executá-lo no hardware de destino e coletar amostras para a estimativa da energia do estado fundamental. Como temos apenas um circuito, vamos usar o [modo de execução de tarefas ](/docs/guides/execution-modes)`qiskit-ibm-runtime` e executar nosso circuito.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "9f542448-5767-4e12-85c8-dd8584545dbb",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_ibm_runtime import SamplerV2 as Sampler\n",
        "\n",
        "sampler = Sampler(mode=backend)\n",
        "sampler.options.dynamical_decoupling.enable = True\n",
        "\n",
        "job = sampler.run([isa_circuit], shots=10_000)  # Takes approximately 5sec of QPU time"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "8c5eb3e9-2a5a-423b-ac18-7bba7f69f18f",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Run cell after IQX job completion\n",
        "primitive_result = job.result()\n",
        "pub_result = primitive_result[0]\n",
        "counts = pub_result.data.meas.get_counts()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e3ebf77a-8efd-43d6-bd19-e26423921c6f",
      "metadata": {},
      "source": [
        "<span id=\"4-post-process-results\" />\n",
        "\n",
        "## 4. Resultados pós-processamento\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "1e7f0ecd",
      "metadata": {},
      "source": [
        "A parte de pós-processamento do fluxo de trabalho do SQD pode ser resumida no diagrama a seguir.\n",
        "\n",
        "![Um fluxograma que mostra como os estados amostrados são utilizados para determinar os autovalores e autovetores do estado fundamental.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig10.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "8c51a527",
      "metadata": {},
      "source": [
        "A amostragem do ansatz LUCJ na base computacional gera um conjunto de configurações com ruído $\\tilde{\\mathcal{\\chi}}$, que são usadas na rotina de pós-processamento. Isso envolve um método chamado (detalhes discutidos posteriormente) *de recuperação de configuração* para corrigir probabilisticamente as configurações com números incorretos de elétrons. As configurações apenas com números de elétrons corretos $\\tilde{\\mathcal{\\chi}}_{R}$ são então subamostradas e distribuídas em vários lotes com base na frequência de aparecimento de cada configuração exclusiva. Cada lote de amostras define um subespaço ( $\\mathcal{S^{(k)}}$ ). Em seguida, o Hamiltoniano molecular, $H$, é projetado em subespaços:\n",
        "\n",
        "$$\n",
        "H_{\\mathcal{S}^{(k)}} = P_{\\mathcal{S}^{(k)}} H _{\\mathcal{S}^{(k)}} \\text{ with } P_{\\mathcal{S}^{(k)}} = \\sum_{x \\in \\mathcal{S}^{(k)}} \\vert x \\rangle \\langle x \\vert\n",
        "$$\n",
        "\n",
        "Cada Hamiltoniano projetado $H_{\\mathcal{S}^{(k)}}$ é então alimentado em um Eigensolver, onde é diagonalizado para calcular valores e vetores próprios para reconstruir um estado próprio. Nesta lição, projetamos e diagonalizamos o Hamiltoniano usando o pacote `qiskit-addon-sqd` , que utiliza o método de Davidson do site PySCF para a diagonalização.\n",
        "\n",
        "$$\n",
        "H_{\\mathcal{S}^{(k)}} \\vert \\psi^{(k)} \\rangle = E^{(k)} \\vert \\psi^{(k)} \\rangle\n",
        "$$\n",
        "\n",
        "Em seguida, coletamos o menor valor próprio (energia) dos lotes e também calculamos a ocupação orbital média, $\\text{n}$. As informações de ocupação média são usadas na etapa de recuperação de configuração para corrigir probabilisticamente as configurações de ruído.\n",
        "\n",
        "Em seguida, explicamos detalhadamente o loop de recuperação de configuração autoconsistente e mostramos exemplos concretos de código para implementar as etapas mencionadas acima para estimar a energia do estado fundamental do hamiltoniano $N_2$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "39997b5d-8bd3-4bc8-9ade-11fa5cfb34f9",
      "metadata": {},
      "source": [
        "<span id=\"41-configuration-recovery-overview\" />\n",
        "\n",
        "### 4.1 Recuperação da configuração: visão geral\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "b05881ac-80ce-47a6-ac28-c1237f85bc0a",
      "metadata": {},
      "source": [
        "Cada bit em uma cadeia de bits (determinante de Slater) representa um orbital de spin. A metade direita de uma cadeia de bits representa orbitais de spin para cima e a metade esquerda representa orbitais de spin para baixo. Um `1` significa que o orbital está ocupado por um elétron, e um `0` significa que o orbital está vazio. Sabemos a priori o número correto de partículas (elétron de spin ascendente e elétron de spin descendente). Suponha que tenhamos um determinante $x$ com $N_x$ elétrons (ou seja, há $N_x$ números de $1$ s na cadeia de bits) nele. O número correto de partículas é $N$. Se $N_x \\neq N$, então sabemos que a cadeia de bits está corrompida por ruído. A rotina de configuração autoconsistente tenta corrigir a cadeia de bits invertendo probabilisticamente $|N_x - N|$ bits, aproveitando as informações de ocupação orbital média. A ocupação orbital média ( $n$ ) nos informa a probabilidade de um orbital ser ocupado por um elétron. Se $N_x < N$, temos menos elétrons e precisamos inverter alguns $0$ s para $1$ s e vice-versa.\n",
        "\n",
        "A probabilidade de inversão pode ser $|x[i] - avg\\_occupancy[i]|$ para `i`-th spin orbital. Em [\\[2\\]](#references), os autores usaram uma probabilidade ponderada de inversão usando a função ReLU modificada.\n",
        "\n",
        "$$\n",
        "\\begin{align}\n",
        "    w(y) = \\begin{cases}\n",
        "\n",
        "    \\delta \\frac{y}{h} & \\text{if }  y \\leq h\\\\ \\nonumber\n",
        "\n",
        "    \\delta + (1 - \\delta) \\frac{y - h}{1 - h} & \\text{if } y > h\n",
        "\n",
        "\\end{cases}\n",
        "\\end{align}\n",
        "$$\n",
        "\n",
        "Aqui, $h$ define o local do \"canto\" da função ReLU, e o parâmetro $\\delta$ define o valor da função ReLU no canto. Para $\\delta = 0$, $w$ se torna a função ReLU verdadeira, e para $\\delta >0$, se torna ReLU\\* modificada\\*. No artigo, os autores usaram $\\delta = 0.01$ e $h =$ número de partículas alfa (ou beta)/número de orbitais de spin alfa (ou beta) $= N/M$ (fator de preenchimento).\n",
        "\n",
        "A ocupação orbital média ( $n$ ) não é conhecida a priori. A primeira iteração da estimativa do estado fundamental começa com configurações com apenas números de partículas corretos em ambas as espécies de spin. Após a primeira iteração, temos uma estimativa do estado fundamental e, usando a estimativa, podemos construir o primeiro palpite de $n$. Esse palpite de $n$ é usado para recuperar configurações, executar a próxima iteração de estimativa do estado fundamental e refinar de forma autoconsistente o palpite de $n$. O processo se repete até que um critério de parada seja atendido.\n",
        "\n",
        "Considere o seguinte exemplo para $N = 2$ e $x = |1000\\rangle$ ( $N_x = 1$ ). Precisamos inverter um dos 0s para 1 a fim de corrigi-lo para números de partículas, e as opções são `1100`, `1010` e `1001`. Com base na probabilidade de inversão, uma das opções será selecionada como *configuração recuperada* (ou a cadeia de bits com o número correto de partículas).\n",
        "\n",
        "Suponha que, na primeira iteração, executemos dois lotes e que os estados de solo estimados a partir deles sejam:\n",
        "\n",
        "$$\n",
        "\\begin{align}\\nonumber\n",
        "    \\text{Batch0: } \\vert \\psi \\rangle &= 0.8 \\times \\vert 1001 \\rangle + 0.6 \\times \\vert 0110 \\rangle \\\\ \\nonumber\n",
        "    \\text{Batch1: } \\vert \\psi \\rangle &= \\frac{1}{\\sqrt{3}} \\left( \\vert 1001 \\rangle + \\vert 0101 \\rangle + \\vert 0110 \\rangle \\right) \\nonumber\n",
        "\\end{align}\n",
        "$$\n",
        "\n",
        "Usando os estados da base computacional e suas amplitudes, podemos calcular a probabilidade de ocupações de elétrons (em resumo, *ocupações* ) por spin-orbital (qubit) (observe que a probabilidade = |amplitude| $^2$ ). Abaixo, tabulamos as ocupações por qubit para cada bitstring que aparece no estado fundamental estimado e calculamos a ocupação orbital total para um lote. Observe que, de acordo com a convenção de ordenação do Qiskit, o bit mais à direita representa qubit-0 ( Q0 ), e o bit mais à esquerda representa Q3.\n",
        "\n",
        "Ocupação ( Batch0 ):\n",
        "\n",
        "|                  |    Q3    |    Q2    |    Q1    |    Q0    |\n",
        "| :--------------: | :------: | :------: | :------: | :------: |\n",
        "|       1001       |   0.64   |    0.0   |    0.0   |   0.64   |\n",
        "|       0110       |    0.0   |   0.36   |   0.36   |    0.0   |\n",
        "| **n** *(Batch0)* | **0.64** | **0.36** | **0.36** | **0.64** |\n",
        "\n",
        "Ocupação ( Batch1 )\n",
        "\n",
        "|                  |    Q3    |    Q2    |    Q1    |    Q0    |\n",
        "| :--------------: | :------: | :------: | :------: | :------: |\n",
        "|       1001       |   0.33   |   0.00   |   0.00   |   0.33   |\n",
        "|       0101       |    0.0   |   0.33   |   0.00   |   0.33   |\n",
        "|       0110       |    0.0   |   0.33   |   0.33   |   0.00   |\n",
        "| **n** *(Batch1)* | **0.33** | **0.66** | **0.33** | **0.66** |\n",
        "\n",
        "Ocupação (média dos lotes)\n",
        "\n",
        "|                  |    Q3    |    Q2    |    Q1    |    Q0    |\n",
        "| :--------------: | :------: | :------: | :------: | :------: |\n",
        "| **n** *(Batch0)* |   0.64   |   0.36   |   0.36   |   0.64   |\n",
        "| **n** *(Batch1)* |   0.33   |   0.66   |   0.33   |   0.66   |\n",
        "|  **n** *(média)* | **0.49** | **0.51** | **0.35** | **0.65** |\n",
        "\n",
        "Usando a ocupação orbital média calculada acima, podemos encontrar as probabilidades de inversão para diferentes orbitais na configuração $x = \\vert 1000 \\rangle$. Como o orbital representado por Q3 já está ocupado e não precisa ser invertido, definimos seu p(flip) como $0$. Para os orbitais restantes, que estão desocupados, a probabilidade de inversão é $\\vert x[i] - \\text{n}[i] \\vert$ cada. Junto com p(flip), também calculamos o peso da probabilidade associada ao flipping usando a função ReLU modificada descrita acima.\n",
        "\n",
        "Probabilidade de flip ( $x = \\vert 1000 \\rangle$, $\\delta = 0.01$, $h = N/M = 2/4 = 0.50$ )\n",
        "\n",
        "|                                              |  Q3 |  Q2  |   Q1  |  Q0  |\n",
        "| :------------------------------------------: | :-: | :--: | :---: | :--: |\n",
        "| p(flip) ( $\\vert x[i] - \\text{n}[i] \\vert$ ) |  0  | 0.51 |  0.35 | 0.65 |\n",
        "|                  w(p(flip))                  |  0  | 0.03 | 0.007 | 0.31 |\n",
        "\n",
        "Por fim, usando as probabilidades ponderadas acima, podemos inverter um dos orbitais desocupados Q2, Q1 e Q0. Com base nos valores acima, o Q0 provavelmente será invertido, e uma possível configuração recuperada pode ser $\\vert \\text{1001} \\rangle$.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c1940235",
      "metadata": {},
      "source": [
        "![Um diagrama da recuperação da configuração.](https://quantum.cloud.ibm.com/learning/images/courses/quantum-diagonalization-algorithms/sqd2/sqd2-fig11.avif)\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5c614290",
      "metadata": {},
      "source": [
        "O processo completo de recuperação de configuração autoconsistente pode ser resumido da seguinte forma:\n",
        "\n",
        "**Primeira iteração:** Suponha que as cadeias de bits (configurações ou determinantes de Slater) geradas pelo computador quântico formem um conjunto $\\widetilde{\\chi}$, que inclui configurações com o número correto ( $\\widetilde{\\chi}_{correct}$ ) e incorreto ( $\\widetilde{\\chi}_{incorrect}$ ) de partículas em cada setor de spin.\n",
        "\n",
        "1. As configurações de ( $\\widetilde{\\chi}_{correct}$ ) são amostradas aleatoriamente para criar lotes $(\\mathcal{S}^{(1)}, \\cdots, \\mathcal{S}^{(K)})$ de vetores para projeção de subespaço. O número de lotes e amostras em cada lote são parâmetros definidos pelo usuário. Quanto maior o número de amostras em cada lote, maior a dimensão do subespaço e mais exigente em termos computacionais se torna a diagonalização. Por outro lado, um número muito pequeno de amostras pode perder os vetores de suporte do estado básico e levar a uma estimativa incorreta.\n",
        "2. Execute o solucionador de estado próprio (ou seja, projeção no subespaço e diagonalização) nos lotes e obtenha estados próprios aproximados. $|\\psi^{(1)}\\rangle, \\cdots, |\\psi^{(K)}\\rangle$.\n",
        "3. A partir dos estados próprios aproximados, construa a primeira estimativa para $n$.\n",
        "\n",
        "**Iterações subsequentes:**\n",
        "\n",
        "1. Usando $n$, corrija as configurações com o número de partículas errado em $\\widetilde{\\chi}_{incorrect}$. Suponha que as chamemos de $\\widetilde{\\chi}_{correct\\_new}$. Então, $\\widetilde{\\chi}_{recovered} (\\widetilde{\\chi}_{R}) = \\widetilde{\\chi}_{correct} \\cup \\widetilde{\\chi}_{correct\\_new}$ forma o novo conjunto de configurações com números de partículas corretos.\n",
        "2. $\\widetilde{\\chi}_{R}$ é amostrado para criar lotes $\\mathcal{S}^{(1)}, \\cdots, \\mathcal{S}^{(K)}$.\n",
        "3. O solucionador de estado próprio é executado com novos lotes e gera novas estimativas de estados fundamentais $|\\psi^{(1)}\\rangle, \\cdots, |\\psi^{(K)}\\rangle$.\n",
        "4. A partir dos estados próprios aproximados, construa uma estimativa refinada para $n$.\n",
        "5. Se o critério de parada não for atendido, volte para a etapa `2.1`.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "5eea548a",
      "metadata": {},
      "source": [
        "<span id=\"42-ground-state-estimation\" />\n",
        "\n",
        "### 4.2 Estimativa do estado fundamental\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "31dfe00e",
      "metadata": {},
      "source": [
        "Primeiro, transformaremos as contagens em uma matriz de bitstring e em uma matriz de probabilidade para pós-processamento.\n",
        "\n",
        "Cada linha da matriz representa uma bitstring exclusiva. Como os qubits são indexados a partir da direita de uma cadeia de bits no Qiskit, a coluna `0` representa o qubit `N-1` e a coluna `N-1` representa o qubit `0`, em que `N` é o número de qubits.\n",
        "\n",
        "Os orbitais alfa são representados no intervalo de índice de coluna `(N, N/2]` (metade direita), e os orbitais beta são representados no intervalo de coluna `(N/2, 0]` (metade esquerda).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "71550274",
      "metadata": {},
      "outputs": [],
      "source": [
        "from qiskit_addon_sqd.counts import counts_to_arrays\n",
        "\n",
        "# Convert counts into bitstring and probability arrays\n",
        "bitstring_matrix_full, probs_arr_full = counts_to_arrays(counts)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "acbdbdc5",
      "metadata": {},
      "source": [
        "Há algumas opções controladas pelo usuário que são importantes para essa técnica:\n",
        "\n",
        "* `iterations`: Número de iterações de recuperação de configuração autoconsistente\n",
        "* `n_batches`: Número de lotes de configurações usados pelas diferentes chamadas ao solucionador de estado próprio\n",
        "* `samples_per_batch`: Número de configurações exclusivas a serem incluídas em cada lote\n",
        "* `max_davidson_cycles`: Número máximo de ciclos Davidson executados por cada eigensolver\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "a2d22e0b-8f51-42a0-858f-ad0297cb0bae",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Starting configuration recovery iteration 0\n",
            "  Batch 0 subspace dimension: 21609\n",
            "  Batch 1 subspace dimension: 21609\n",
            "  Batch 2 subspace dimension: 21609\n",
            "  Batch 3 subspace dimension: 21609\n",
            "  Batch 4 subspace dimension: 21609\n",
            "Starting configuration recovery iteration 1\n",
            "  Batch 0 subspace dimension: 609961\n",
            "  Batch 1 subspace dimension: 616225\n",
            "  Batch 2 subspace dimension: 627264\n",
            "  Batch 3 subspace dimension: 633616\n",
            "  Batch 4 subspace dimension: 624100\n",
            "Starting configuration recovery iteration 2\n",
            "  Batch 0 subspace dimension: 564001\n",
            "  Batch 1 subspace dimension: 605284\n",
            "  Batch 2 subspace dimension: 582169\n",
            "  Batch 3 subspace dimension: 559504\n",
            "  Batch 4 subspace dimension: 591361\n",
            "Starting configuration recovery iteration 3\n",
            "  Batch 0 subspace dimension: 550564\n",
            "  Batch 1 subspace dimension: 549081\n",
            "  Batch 2 subspace dimension: 531441\n",
            "  Batch 3 subspace dimension: 527076\n",
            "  Batch 4 subspace dimension: 531441\n",
            "Starting configuration recovery iteration 4\n",
            "  Batch 0 subspace dimension: 544644\n",
            "  Batch 1 subspace dimension: 580644\n",
            "  Batch 2 subspace dimension: 527076\n",
            "  Batch 3 subspace dimension: 531441\n",
            "  Batch 4 subspace dimension: 537289\n"
          ]
        }
      ],
      "source": [
        "import numpy as np\n",
        "from qiskit_addon_sqd.configuration_recovery import recover_configurations\n",
        "from qiskit_addon_sqd.fermion import (\n",
        "    bitstring_matrix_to_ci_strs,\n",
        "    solve_fermion,\n",
        ")\n",
        "from qiskit_addon_sqd.subsampling import postselect_and_subsample\n",
        "\n",
        "rng = np.random.default_rng(24)\n",
        "# SQD options\n",
        "iterations = 5\n",
        "\n",
        "# Eigenstate solver options\n",
        "n_batches = 5\n",
        "samples_per_batch = 500\n",
        "max_davidson_cycles = 300\n",
        "\n",
        "# Self-consistent configuration recovery loop\n",
        "e_hist = np.zeros((iterations, n_batches))  # energy history\n",
        "s_hist = np.zeros((iterations, n_batches))  # spin history\n",
        "occupancy_hist = []\n",
        "avg_occupancy = None\n",
        "for i in range(iterations):\n",
        "    print(f\"Starting configuration recovery iteration {i}\")\n",
        "    # On the first iteration, we have no orbital occupancy information from the\n",
        "    # solver, so we begin with the full set of noisy configurations.\n",
        "    if avg_occupancy is None:\n",
        "        bs_mat_tmp = bitstring_matrix_full\n",
        "        probs_arr_tmp = probs_arr_full\n",
        "\n",
        "    # If we have average orbital occupancy information, we use it to refine\n",
        "    # the full set of noisy configurations.\n",
        "    else:\n",
        "        bs_mat_tmp, probs_arr_tmp = recover_configurations(\n",
        "            bitstring_matrix_full,\n",
        "            probs_arr_full,\n",
        "            avg_occupancy,\n",
        "            num_elec_a,\n",
        "            num_elec_b,\n",
        "            rand_seed=rng,\n",
        "        )\n",
        "\n",
        "    # Create batches of subsamples. We postselect here to remove configurations\n",
        "    # with incorrect hamming weight during iteration 0, since no config recovery was performed.\n",
        "    batches = postselect_and_subsample(\n",
        "        bs_mat_tmp,\n",
        "        probs_arr_tmp,\n",
        "        hamming_right=num_elec_a,\n",
        "        hamming_left=num_elec_b,\n",
        "        samples_per_batch=samples_per_batch,\n",
        "        num_batches=n_batches,\n",
        "        rand_seed=rng,\n",
        "    )\n",
        "\n",
        "    # Run eigenstate solvers in a loop. This loop should be parallelized for larger problems.\n",
        "    e_tmp = np.zeros(n_batches)\n",
        "    s_tmp = np.zeros(n_batches)\n",
        "    occs_tmp = []\n",
        "    coeffs = []\n",
        "    for j in range(n_batches):\n",
        "        strs_a, strs_b = bitstring_matrix_to_ci_strs(batches[j])\n",
        "        print(f\"  Batch {j} subspace dimension: {len(strs_a) * len(strs_b)}\")\n",
        "        energy_sci, coeffs_sci, avg_occs, spin = solve_fermion(\n",
        "            batches[j],\n",
        "            hcore,\n",
        "            eri,\n",
        "            open_shell=open_shell,\n",
        "            spin_sq=spin_sq,\n",
        "            max_cycle=max_davidson_cycles,\n",
        "        )\n",
        "        energy_sci += nuclear_repulsion_energy\n",
        "        e_tmp[j] = energy_sci\n",
        "        s_tmp[j] = spin\n",
        "        occs_tmp.append(avg_occs)\n",
        "        coeffs.append(coeffs_sci)\n",
        "\n",
        "    # Combine batch results\n",
        "    avg_occupancy = tuple(np.mean(occs_tmp, axis=0))\n",
        "\n",
        "    # Track optimization history\n",
        "    e_hist[i, :] = e_tmp\n",
        "    s_hist[i, :] = s_tmp\n",
        "    occupancy_hist.append(avg_occupancy)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e4f6ec2c-032d-4e18-8ea4-cf867cba5054",
      "metadata": {},
      "source": [
        "<span id=\"43-discussion-of-results\" />\n",
        "\n",
        "### 4.3 Discussão dos resultados\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "592eabb7-18f3-4622-8710-bfc43ad6cdec",
      "metadata": {},
      "source": [
        "O primeiro gráfico mostra que, após algumas iterações, estimamos a energia do estado fundamental dentro de \\~24 mH (a precisão química é normalmente aceita como sendo de 1 kcal/mol $\\approx$ 1.6 mH ). O segundo gráfico mostra a ocupação média de cada orbital espacial após a iteração final. Podemos ver que tanto os elétrons de spin para cima quanto os de spin para baixo ocupam os primeiros cinco orbitais com alta probabilidade em nossas soluções.\n",
        "\n",
        "Embora a energia estimada do estado fundamental seja decente, ela não está dentro do limite de precisão química ( $\\pm \\approx 1.6$ mH ). Essa lacuna pode ser atribuída à pequena dimensão do subespaço que usamos acima para projeção e diagonalização. Como usamos `samples_per_batch=500`, o subespaço é abrangido por no máximo $500$ vetores, que são os vetores ausentes do suporte do estado básico. O aumento do parâmetro `samples_per_batch` deve melhorar a precisão à custa de mais recursos de computação clássicos e tempo de execução.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "61959e69-a182-4636-abcb-a32349fc9076",
      "metadata": {},
      "outputs": [],
      "source": [
        "# Data for energies plot\n",
        "x1 = range(iterations)\n",
        "min_e = [np.min(e) for e in e_hist]\n",
        "e_diff = [abs(e - exact_energy) for e in min_e]\n",
        "yt1 = [1.0, 1e-1, 1e-2, 1e-3, 1e-4, 1e-5]\n",
        "\n",
        "# Chemical accuracy (+/- 1 milli-Hartree)\n",
        "chem_accuracy = 0.001\n",
        "\n",
        "# Data for avg spatial orbital occupancy\n",
        "y2 = occupancy_hist[-1][0] + occupancy_hist[-1][1]\n",
        "x2 = range(len(y2))"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "8cd90034-6ef3-41bd-a847-c115cade82f7",
      "metadata": {},
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "Exact energy: -109.04667 Ha\n",
            "SQD energy: -109.02234 Ha\n",
            "Absolute error: 0.02434 Ha\n"
          ]
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/learning/images/courses/quantum-diagonalization-algorithms/qda-4-sqd-implementation/extracted-outputs/8cd90034-6ef3-41bd-a847-c115cade82f7-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "import matplotlib.pyplot as plt\n",
        "\n",
        "fig, axs = plt.subplots(1, 2, figsize=(12, 6))\n",
        "\n",
        "# Plot energies\n",
        "axs[0].plot(x1, e_diff, label=\"energy error\", marker=\"o\")\n",
        "axs[0].set_xticks(x1)\n",
        "axs[0].set_xticklabels(x1)\n",
        "axs[0].set_yticks(yt1)\n",
        "axs[0].set_yticklabels(yt1)\n",
        "axs[0].set_yscale(\"log\")\n",
        "axs[0].set_ylim(1e-6)\n",
        "axs[0].axhline(\n",
        "    y=chem_accuracy, color=\"#BF5700\", linestyle=\"--\", label=\"chemical accuracy\"\n",
        ")\n",
        "axs[0].set_title(\"Approximated Ground State Energy Error vs SQD Iterations\")\n",
        "axs[0].set_xlabel(\"Iteration Index\", fontdict={\"fontsize\": 12})\n",
        "axs[0].set_ylabel(\"Energy Error (Ha)\", fontdict={\"fontsize\": 12})\n",
        "axs[0].legend()\n",
        "\n",
        "# Plot orbital occupancy\n",
        "axs[1].bar(x2, y2, width=0.8)\n",
        "axs[1].set_xticks(x2)\n",
        "axs[1].set_xticklabels(x2)\n",
        "axs[1].set_title(\"Avg Occupancy per Spatial Orbital\")\n",
        "axs[1].set_xlabel(\"Orbital Index\", fontdict={\"fontsize\": 12})\n",
        "axs[1].set_ylabel(\"Avg Occupancy\", fontdict={\"fontsize\": 12})\n",
        "\n",
        "print(f\"Exact energy: {exact_energy:.5f} Ha\")\n",
        "print(f\"SQD energy: {min_e[-1]:.5f} Ha\")\n",
        "print(f\"Absolute error: {e_diff[-1]:.5f} Ha\")\n",
        "plt.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "e1530472",
      "metadata": {},
      "source": [
        "<span id=\"exercise-for-the-reader\" />\n",
        "\n",
        "#### Exercício para o leitor\n",
        "\n",
        "Aumente progressivamente o parâmetro `samples_per_batch` (por exemplo, de $1000$ para $10000$ em uma etapa de $1000$; permitido pela memória do seu computador) e compare as energias estimadas do estado fundamental.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "d2b2241f",
      "metadata": {},
      "source": [
        "<span id=\"references\" />\n",
        "\n",
        "## Referências\n",
        "\n",
        "\\[1] M. Motta et al, \"Unindo intuição física e eficiência de hardware para estados eletrônicos correlacionados: o cluster unitário local Jastrow ansatz para estrutura eletrônica\" (2023). [Química. Ciências, 2023, 14, 11213](https://pubs.rsc.org/en/content/articlehtml/2023/sc/d3sc02516k).\n",
        "\n",
        "\\[2] J. Robledo-Moreno et al, \"Química além das soluções exatas em um supercomputador centrado em quantum\" (2024). [arXiv:quant-ph/2405.05068](https://arxiv.org/abs/2405.05068).\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "kernelspec": {
      "display_name": "Python 3",
      "language": "python",
      "name": "python3"
    },
    "language_info": {
      "codemirror_mode": {
        "name": "ipython",
        "version": 3
      },
      "file_extension": ".py",
      "mimetype": "text/x-python",
      "name": "python",
      "nbconvert_exporter": "python",
      "pygments_lexer": "ipython3",
      "version": "3"
    }
  },
  "nbformat": 4,
  "nbformat_minor": 5
}