{
  "cells": [
    {
      "cell_type": "markdown",
      "id": "c000",
      "metadata": {},
      "source": [
        "---\n",
        "title: Pooled sample-based quantum diagonalization of a nuclear Hamiltonian\n",
        "description: Pool nuclear Slater determinants from an ensemble of QPU circuits, repair them against exact symmetries, and diagonalize the shell-model Hamiltonian.\n",
        "---\n",
        "\n",
        "# Pooled sample-based quantum diagonalization of a nuclear Hamiltonian\n",
        "\n",
        "{/* cspell:ignore Arvidsson Brookhaven Clebsch Condon Gordan Honma Malrieu Mizusaki Mkhize Nesbet Otsuka Rancurel Shukur USDB Yordanov antisymmetrized antisymmetry dets eigh facecolor fontsize frameon labelsize markeredgecolor markeredgewidth multiconfigurational multiplets recoupling spes substate substates tbme tbmes textcoords virtuals wavefunctions xytext zorder */}\n",
        "\n",
        "*Usage estimate: 2.5 minutes on a Heron processor (NOTE: This is an estimate only. Your runtime might vary.)*\n",
        "\n",
        "<Admonition type=\"note\" title=\"Looking for the Fortran version?\">\n",
        "  This notebook presents the Python implementation. The Fortran implementation is in the\n",
        "  [Fortran companion directory](https://github.com/Qiskit/documentation/tree/main/docs/tutorials/assets/nuclear_sqd_pooled/fortran)\n",
        "  of this documentation repository. The Python version adds a self-consistent configuration-recovery step,\n",
        "  which the Fortran driver does not perform.\n",
        "</Admonition>\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c001",
      "metadata": {},
      "source": [
        "## Learning outcomes\n",
        "\n",
        "* Learn how a nuclear shell-model Hamiltonian, tabulated in a $J$-coupled basis of orbitals, becomes\n",
        "  a qubit Hamiltonian in the $m$-scheme, where one qubit is one single-particle state.\n",
        "* Build a fixed, non-variational excitation ansatz whose angles come from second-order\n",
        "  perturbation theory, so there is no classical optimization loop.\n",
        "* Compare qubit and fermionic excitations and measure how the choice affects the ensemble’s\n",
        "  two-qubit depth.\n",
        "* Run self-consistent configuration recovery with `qiskit-addon-sqd` when the conserved\n",
        "  quantities are nucleon numbers, $M_J$, and parity rather than electron numbers and spin.\n",
        "* Apply one workflow from a 24-qubit problem you can check exactly to a 40-qubit problem\n",
        "  with nearly two million basis states, beyond this tutorial's exact-diagonalization capacity.\n",
        "\n",
        "## Prerequisites\n",
        "\n",
        "Before starting, review the following topics:\n",
        "\n",
        "* [Sample-based quantum diagonalization](/docs/addons/qiskit-addon-sqd)\n",
        "  and the [SQD addon API reference](/docs/api/qiskit-addon-sqd).\n",
        "* [Sample-based quantum diagonalization of a chemistry Hamiltonian](/docs/tutorials/sample-based-quantum-diagonalization),\n",
        "  the electronic-structure counterpart of this tutorial.\n",
        "* [Transpile against a backend target](/docs/guides/transpile)\n",
        "  and [Introduction to primitives](/docs/guides/primitives).\n",
        "* Second quantization and the Jordan-Wigner mapping.\n",
        "\n",
        "## Background\n",
        "\n",
        "The nuclear shell model treats a nucleus as a few *valence* nucleons moving in a small set of\n",
        "single-particle orbitals above an inert *core*, interacting through an empirical two-body force\n",
        "fitted to measured spectra. It is widely used in low-energy nuclear structure. Its computational cost is\n",
        "combinatorial: the basis is every way of distributing the valence protons and neutrons over the\n",
        "available states, and this growth limits the model spaces accessible to exact diagonalization.\n",
        "\n",
        "Pooled sample-based quantum diagonalization (pooled SQD) [\\[1\\]](#references) splits that problem in two. A\n",
        "quantum circuit is used only to *propose* which basis states matter. It is measured in the\n",
        "computational basis, and each measured bitstring names one Slater determinant. The Hamiltonian is\n",
        "then built and diagonalized classically in the span of those determinants. Because the classical\n",
        "step is an exact diagonalization inside a subspace, it returns a variational upper bound\n",
        "on the true ground-state energy, and the bound can only fall as determinants are added.\n",
        "\n",
        "This division of work makes the method noise-tolerant, with an important limitation. Noise changes *which* determinants the circuit\n",
        "proposes. It does not enter the classical Hamiltonian, so it cannot move the eigenvalue of a given\n",
        "subspace: a shot that violates a conserved quantity is discarded or repaired, and a shot that\n",
        "survives is a legitimate basis vector however it was produced. Noise therefore costs you subspace\n",
        "quality, not correctness, and the number you report is an upper bound either way.\n",
        "\n",
        "Nuclear structure provides several exact quantum numbers for filtering samples. A physical determinant must carry the right number of valence protons\n",
        "*and* the right number of valence neutrons, the right total angular-momentum projection $M_J$, and\n",
        "the right parity. Each can be checked with an integer test on a bitstring. The fraction of samples rejected\n",
        "depends on the constraint and model space.\n",
        "\n",
        "![The 24-qubit sd-shell register: three proton orbitals and three neutron orbitals, each split into 2j+1 magnetic substates, one qubit per substate, with the reference determinant of neon-20 filled on the maximal magnetic substates of the 0d5/2 orbital.](https://quantum.cloud.ibm.com/docs/images/tutorials/nuclear-sqd-pooled/sd-shell-register.svg)\n",
        "\n",
        "Every qubit is one $m$-scheme single-particle state $(n, \\ell, j, m_j, t_z)$, and $|1\\rangle$ means\n",
        "occupied. The register uses a fixed order: protons first, then neutrons; within a species,\n",
        "orbitals in file order; within an orbital, $m_j$ descending. The two halves of a bitstring are\n",
        "therefore the proton configuration and the neutron configuration. This is the bipartition\n",
        "expected by the pooled SQD post-processing tools.\n",
        "\n",
        "### The workflow\n",
        "\n",
        "![Workflow diagram: a reference determinant feeds an ensemble of shallow excitation circuits, which are sampled on a QPU to produce bitstrings; the bitstrings are repaired and post-selected on proton and neutron number, recombined into a product subspace where the magnetic projection and parity are imposed, and diagonalized to give a variational upper bound; average occupancies from the resulting eigenvector feed back into the next repair.](https://quantum.cloud.ibm.com/docs/images/tutorials/nuclear-sqd-pooled/sqd-workflow.svg)\n",
        "\n",
        "Two stages in the diagram handle the nuclear symmetries.\n",
        "\n",
        "**Repair and post-selection** handle samples affected by hardware noise. The two half-register nucleon numbers\n",
        "are Hamming weights, so `qiskit-addon-sqd` handles them directly: `recover_configurations` repairs a\n",
        "broken bitstring by flipping the bits least consistent with the current estimate of the average\n",
        "orbital occupancies, rather than throwing the shot away.\n",
        "\n",
        "**The product subspace** introduces $M_J$. Because $M_J = M_p + M_n$ couples the two halves, it\n",
        "is not a property of either one, so it must not be used to filter whole shots: a bitstring whose\n",
        "proton half and neutron half are each valid still contributes two good half-configurations even when\n",
        "its total $M_J$ is wrong. The subspace is therefore spanned by every *product* of a sampled proton\n",
        "configuration with a sampled neutron configuration, keeping the products that land in the target\n",
        "$M_J$ and parity sector. This is the pooled SQD subspace construction, and it means a few thousand\n",
        "bitstrings can span a subspace far larger than the sample count.\n",
        "\n",
        "### Two governing equations\n",
        "\n",
        "The shell-model Hamiltonian is a one-body term plus a two-body interaction,\n",
        "\n",
        "$$\n",
        "\\begin{equation}\n",
        "\\tag{1}\n",
        "H = \\sum_{p} \\varepsilon_p\\, a_p^\\dagger a_p\n",
        "  + \\tfrac{1}{4}\\sum_{pqrs} \\langle pq \\| rs \\rangle\\, a_p^\\dagger a_q^\\dagger a_s a_r ,\n",
        "\\end{equation}\n",
        "$$\n",
        "\n",
        "where $p,q,r,s$ label $m$-scheme states and $t_z = -1$ for a proton, $+1$ for a neutron. Empirical\n",
        "interactions such as USDA [\\[2\\]](#references) and GXPF1 [\\[3\\]](#references) are tabulated not in\n",
        "the $m$-scheme but in the $J$-coupled basis, as matrix elements $\\langle ab; J | V | cd; J \\rangle$\n",
        "between normalized antisymmetrized two-body states of *orbitals* $a,b,c,d$. Recovering the\n",
        "$m$-scheme element is a Clebsch-Gordan recoupling,\n",
        "\n",
        "$$\n",
        "\\begin{equation}\n",
        "\\tag{2}\n",
        "\\langle pq \\| rs \\rangle = \\sqrt{1 + \\delta_{ab}}\\sqrt{1 + \\delta_{cd}}\n",
        "  \\sum_{J} \\langle j_p m_p\\, j_q m_q | J M \\rangle\n",
        "           \\langle j_r m_r\\, j_s m_s | J M \\rangle\n",
        "           \\langle ab; J | V | cd; J \\rangle ,\n",
        "\\end{equation}\n",
        "$$\n",
        "\n",
        "with the $\\sqrt{1+\\delta}$ factors undoing the normalization convention of the tabulated states.\n",
        "Everything else in this tutorial is built on these two equations.\n",
        "\n",
        "### The three runs\n",
        "\n",
        "|             | Nucleus                      | Shell | Qubits | Symmetry-allowed basis | Exactly checkable? |\n",
        "| ----------- | ---------------------------- | ----- | ------ | ---------------------- | ------------------ |\n",
        "| Small-scale | $^{20}\\mathrm{Ne}$ (2p + 2n) | $sd$  | 24     | 640                    | Yes                |\n",
        "| Large-scale | $^{44}\\mathrm{Ti}$ (2p + 2n) | $pf$  | 40     | 4,000                  | Yes                |\n",
        "| Large-scale | $^{48}\\mathrm{Cr}$ (4p + 4n) | $pf$  | 40     | 1,963,461              | No                 |\n",
        "\n",
        "The small-scale run is the walkthrough. Both large-scale runs use a 40-qubit register: the first is\n",
        "still small enough to diagonalize exactly on a laptop, so you can compare the hardware result with an exact reference. The second exceeds the\n",
        "exact-diagonalization capacity of this tutorial.\n",
        "\n",
        "**Every run here executes on a QPU.** That is a choice made for this tutorial rather than a\n",
        "requirement of the method: all three runs share a backend and a gate budget so you can compare their performance\n",
        "at different problem sizes.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c002",
      "metadata": {},
      "source": [
        "## Requirements\n",
        "\n",
        "Install the following packages before starting:\n",
        "\n",
        "* Qiskit SDK v2.0 or later (`pip install qiskit`)\n",
        "* `qiskit-ibm-runtime` v0.40 or later (`pip install qiskit-ibm-runtime`)\n",
        "* SQD addon v0.12 or later (`pip install qiskit-addon-sqd`)\n",
        "* NumPy, SciPy, and Matplotlib (`pip install numpy scipy matplotlib`)\n",
        "\n",
        "You also need an IBM Quantum® account with credentials saved locally, and access to a QPU\n",
        "with at least 40 qubits.\n",
        "\n",
        "No simulator package is needed, and no data files have to be downloaded. The two interaction files\n",
        "this tutorial uses are embedded in the following setup cell and written to a temporary directory when you\n",
        "run it.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c003",
      "metadata": {},
      "source": [
        "## Setup\n",
        "\n",
        "This section imports the tools and defines the shell-model helpers the workflow needs, in the order\n",
        "the workflow uses them. The physics behind each one is derived in the [Appendix](#appendix); the\n",
        "comments describe each function’s role in the workflow.\n",
        "\n",
        "Two interaction files are unpacked first. Both are published parameter sets, embedded here so the\n",
        "notebook is self-contained: `usda.snt` is the USDA $sd$-shell Hamiltonian [\\[2\\]](#references) and\n",
        "`gxpf1.snt` is the GXPF1 $pf$-shell Hamiltonian [\\[3\\]](#references).\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 1,
      "id": "c004",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:17.680465Z",
          "iopub.status.busy": "2026-09-16T21:58:17.680360Z",
          "iopub.status.idle": "2026-09-16T21:58:18.693474Z",
          "shell.execute_reply": "2026-09-16T21:58:18.693193Z"
        }
      },
      "outputs": [],
      "source": [
        "from __future__ import annotations\n",
        "\n",
        "import base64\n",
        "import gzip\n",
        "import itertools\n",
        "import tempfile\n",
        "from dataclasses import dataclass\n",
        "from functools import lru_cache\n",
        "from math import factorial, sqrt\n",
        "from pathlib import Path\n",
        "\n",
        "import matplotlib.pyplot as plt\n",
        "import numpy as np\n",
        "\n",
        "from qiskit import QuantumCircuit\n",
        "from qiskit.circuit.library import PauliEvolutionGate\n",
        "from qiskit.quantum_info import Operator, SparsePauliOp\n",
        "from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager\n",
        "from qiskit_addon_sqd.configuration_recovery import recover_configurations\n",
        "from qiskit_addon_sqd.counts import bit_array_to_arrays\n",
        "from qiskit_addon_sqd.subsampling import postselect_by_hamming_right_and_left\n",
        "from qiskit_ibm_runtime import QiskitRuntimeService, SamplerV2\n",
        "\n",
        "from scipy.linalg import eigh\n",
        "\n",
        "_USDA_SNT_GZ = (\n",
        "    \"H4sIAI5wqWoC/4WY224bRwyG7/UU9F0CONsZco5AUyBuC/QmQNG06KWhWGql1LYMy+kJefiOTrucGXJrQAgsf5nl8ecOr+CX\"\n",
        "    \"D9+9g/0K9pv1/T389rx7gPLrsH+Cz/vVctg+vgAsrgB+HeDdAD9t7zYv6+dr+DDA+z8223/X17B8XMFN+ev9+m+4ed799XjA\"\n",
        "    \"f9z8sy/4+s8BvoWYrsEERwbhFRqTXi+uCvOwW63vYf+0vFvDAgDo/AFIh8/hK7DH30354Omvb8o3V8c/vIUnMKtboK/wiGKF\"\n",
        "    \"+gnFEfVn9PQUe8bthNIRtftbsGfUtQbAGXUFfawM8K0BF9SP6MWA0BpwQcMRvRhwBSX86+fl3ct2d4jq4+eHa3hYv2x2q7fX\"\n",
        "    \"AJuPy+fb3cP69+Uh4luAT8djv95++eGV/fj6y6dvFudnmcX4lGOoBmttNOVncTL2FLs3NGT0l++ndJTv0cR8/t6NUanP8WMI\"\n",
        "    \"6nPC6C8/5wp+vnn//QLOP9ank3k2wRszkDn/Zwv15xiv41l2SDmjhuEFM4PJ0QkYcgzM4A1Jp1GDkcvtaWMAR9tosAa9huHk\"\n",
        "    \"AtoUBYwaF8hGarAxOywgxnoeN+Se2smF4H3QMPZQ653XMJpOi6GyrcLcZJtzLgoYcdsOyXKibV1Aom0fikJO0VLUMDeW3kAl\"\n",
        "    \"qQLWpd66qGE02eZ9lXqSPD3UWyl5DeOeGnINJnmaUggCVrtgh2DJNm3fVy8WF3LSMFa9hYga5iYX0IQsYK2nmTpJkoq81Acy\"\n",
        "    \"jLTTnPEaxqo3JGcbvZMeGnJVbw6mf2cUqcIsO80nFdOFq8JoSlY+Zb7FfNNZlnw2PqOE8SL3PtlSJhJGXGqsDRjbh4bmocWy\"\n",
        "    \"oiIxSxjv09JYgaxlGLZxA9EFFOImuIBt3EB04TK3Z5S8wizrrFCJQ4Xpgl9hzDYbu0LCNm6HzkJHweUkYazITalyF7NjGAnh\"\n",
        "    \"FZJFQniFZFHnqWgbdZ6qtoX50VZhvEJ81TJe7AUcQozH8yQMJ8ymUS07jLin6LXT3FRIhmwSTmtbJpEjr2AsvM7E1ra+ZaiM\"\n",
        "    \"NoxewZinBlPQTmPJyikn7TQ2630SbWuz4CLlwzuGhCHPqbUmRZQwpkiBDn1vsoQ55oKjbEvNNVgbXls0WvA0NKnPpku91Fkx\"\n",
        "    \"pahhXPDJtxVCQkBcMtJD27I8VJGAhWYYlVuZzc5e+jSIZWnLa57nOQ1iWZY+jSE3GP5/QIKi5E1Agqi9BbMZiUjEWNwChqKq\"\n",
        "    \"KTZY11ku5ca2Pqc4YKk1o2DstBjJC1htW2nAgEZ4aDdPvQmmmLeY4tWFt9Y3VAdlpW+oDcpa3zpM1jdUB2XV9agOyqrrcWZQ\"\n",
        "    \"sq7vMMffBseux5l5yjoLZ+Yp6yzU5mmt5DgzT5mSozZPoVIknJmnTJFQHZRV12OrlvpFQFbycoGKOWiYfl9QlXySGtSUvL9W\"\n",
        "    \"VJi/YG4grC4CmuCX6CZrKUsY64UyJolCQoZJqWeKhDOCzxQJNcGXbZMEX7EtzF+gvCI1npxjmKaW5Q06ewVjOY0u2+Y0kt5D\"\n",
        "    \"JhlETS2P+5CqyIMYkCI1NK50SO3TarSR2qfVaKOZPmXjg2beVJmS01zqp/CSmvoqvNSOD/kaS+qUoSGaTM2yz83fdjtMvsZy\"\n",
        "    \"xOv7N44Fff/GT5q5tXWYfB3jWND3b9yumUvK5SQnvK6w/VuHyfu3DpP3bx0m7984FvT9W4fJ+zceWq/v3zpM3r9xLOj7tw6T\"\n",
        "    \"928X053QgGz/1mHy/u3yZ8lTtn/jWND3b74NiDx2vRbeep56Lbz1oPRaeOv9G0dmxofXPK33bx0m798C1B9RuP4DPJ0WcLAa\"\n",
        "    \"AAA=\"\n",
        ")\n",
        "\n",
        "_GXPF1_SNT_GZ = (\n",
        "    \"H4sIAI5wqWoC/4WcQY8dtw3H7/kU41sCeF8lUqKkQw9NgbaXoEHQQ26BkdiIEWdt2E6L9tNXb9+ORIn8PwdZILB/S3I4IsW/\"\n",
        "    \"ZiYvjr//+P3f4nG8ef/x+PDm4dOvr9+9++rF8d3l+Mf7x99fvTz+dTn++fnTH7/1//z28pfL8e3H9/95fHm8evzl+lffvf3f\"\n",
        "    \"H59e/fb26L9zHN//+t9Pl+OH1/++HH89JL88gkQO8esfvjm+phDomyvW//39/S+v3x2fPrz6+fVX/dfS889B4fpz/aN43P7p\"\n",
        "    \"f3Bw/ynH8fD0Zy+uf/fn48MR3vxU/kRXlp7Z+PzDiqUnNn74iW8sb3azYvm0m29s2uxGxabTbryx2cZ7nGzu7KOKV2y8g5Un\"\n",
        "    \"dsZbbLyDLafd53irjXew9bR7jffF8fbx8+uPr37+/Pb941dPv3P93ZH4W/If6kUohXCm+Jbmh3yR0jicybwl9CFeuFILy908\"\n",
        "    \"HtIlcrlZyCNJ2q6MdGi7ZVy4tlvHJS52Y71FnHp8D+HCIYTlQuYFhdsv0yVxzZB6vsxwaZwJUumkIjWB1POdDhcqlR2KVo85\"\n",
        "    \"SoDU8NgdNodiZevqMUqB1LCVWsO2ZMQlyYs+bXEFk4m5bMJJlVgSpGjaouhQvFKxUIbUjIvI85hWW4FNvubyDnMxMkFKRc8V\"\n",
        "    \"Uml65OhQW1x9fe3XOAtsZPXJ36TIX18SaoQUnysnZsHUXPfcKqTyaSu36FHbWg0tN0jxWIXEAVLzbqcqkMpj3T8lwlBp9RhD\"\n",
        "    \"qpAaHpnzni9nRXPh6lDbysmJBVLzDtU71KztnDxqW1+JhDaKnTtUa4DUzH00Hp0VXQMHSM3c16irlpf7GEcN1dogpfpEDpAa\"\n",
        "    \"WRUOBKl0UqkIQ2qs+5IYU6OvthbFodIaF6/9i51VeF0TLZSN2lfh9T729utQOqtXKi/dl/21mihhW+M+BpIKqblypDWHSntc\"\n",
        "    \"mSA1O3nMe+6dHp0DEaRmXCU0h9p7dOYCqVm1MehOnvy7nWphSKnuu+Q++X2Ca4uQGrYkxp3yMkFLL0ygavPovmR64dhFY5QM\"\n",
        "    \"KVK1XRxqX4UydlHCXU44hY1ydndKuUBK11BzqH1NNOKNMvt27PMXVUWBemSqCVIjrr4nYFtzTSQJkJp3O7se09p9YygCKVL7\"\n",
        "    \"UN4opx5DNnE5q5AlerZ2jyExpEYmggTtMYFrzC1Bak58jetGeSvHenSip9ROik0m5lqdsxzjrCYhTM1uImOHYZzVPvnWjXIm\"\n",
        "    \"0RoaKyqhjjlqiEE3uWoYHqopbT/KY0pDD+Xtx9d8lvI0n6U8zWcpT/NpQqDms5Sn+TRVoOazlKf5LOVpPk1VqPl05AI1n6U8\"\n",
        "    \"zaepAjWfpTzNp6kKNZ/OQoGaz1Ke5rOUp/k0VaHm03mvUPOd1+esL6X5LOVpPkt5ms9SnubTVIGaz1Ke5rOUp/ks5Wk+TVWo\"\n",
        "    \"+SzlaT69mgVqPk0VqPks5Wk+S3maT1MVaj5dPwVqPkt5mk9TFWo+S3ma77x/8z56ms9SnuazlKf5LOVpPkt5ms9SnubTVIWa\"\n",
        "    \"z1Ke5jupfRWumk9TBWo+S3maz1Ke5rOUp/k0VaHms5Sn+XTnLVDzWcrTfJqqUPNZytN85/1z7rbSfJbyNN/5t06fUJrPUp7m\"\n",
        "    \"OykvE7T0wgqqdmo+Mb3Q03yCO6bSfII7ptJ8gruc0nxi+pen+eROL5yaT3CXU5pPx1Sh5hNcj0rzCajHVfMJqMdV88mdesyu\"\n",
        "    \"xwo1n+B6VJpPcD0qzSe4HpXmE1yPSvMJrkelwERVR4Waz1Ke5hNTQ3THoxO90nzFZMLTfAVnVWm+grOqNF/BWVWaTxMVar6C\"\n",
        "    \"r1FpvgK6yar56vbja76onormu8/5ViqOrNblZH6l0NPAlRq9sKaIbaFnhis15tUuH/E1oieLK1XOTIgsz3ROap9XpQ/SJWqK\"\n",
        "    \"bCYcKm6K4tp9iUqqzbd1j4qO+g2tpTV6sll1qLipk6fnw32/isDWPSruPborwz7x9eakKbbry6Gi7RN9nrhuHr4tPXXsVLT9\"\n",
        "    \"vvTdKlUQ18i9Q0Wn+2ZOlYpva0yiDhXthNz69p4S+7ZG7h0q2jma+/BIIfm2RnU4VLQ9R6hG4iWryVurhorOlNbzEJr4tubk\"\n",
        "    \"bim6e25iqbFbJY4MKfREfaXGNXKK2aF2BSapD6Oy5162yd1S5Og0iq3k5tua1WEpsus+ZO4ZS76tqU4sRc7+mAtJyL6teR8t\"\n",
        "    \"RVbDUO9yXM2a2O6jQ5GjdCi3HKpva04dluK7J1uWGtFLjhFS6J2HlZpKhzOm0JsRK5XnmjD7Izv5av0eBd7zVb5IsVU6ffTN\"\n",
        "    \"oQXf1peoeveNjZWaNURFZ0L8fTsX1s9hwO4eeHkOA3Z3Zct6ZOccgPAM0O91hR4ZPEcWMAPkdBWQvsdZj72xZuTRs2U95qFY\"\n",
        "    \"a8FZHVQpy/Mhd55YbGmPew2VGB2PRvOVsNlib0orYbO1U8fi0cbF6uQhw7jUycMybYs/dSiKwGxyLB5tXEmdM5WM4lKnGLFm\"\n",
        "    \"FFeaPTruK8ehlEcbV55n2yIwrnlOXorAuPKsxyIwrjz7l3greptzSgve+tqo2lLbcu9MQ4oiPDMpjzaueR8rUUZxqaql/T46\"\n",
        "    \"k5WiCM9fyuNpy5uZpM9WkvfaxpS1Ndd96JKV2LflUdrW1idaKOT0HNMnTCdnT/OZvcOZv5RHG5fqAJFhXLO2WykwrtlzJkV4\"\n",
        "    \"llMebVxp7kOUMopr7h1dSmcUl9LuxesT2z6kPOq4ttkkk7sj1/1JWWWnHvfdfVKE50LlUV+jObPqs2hO+zViytpSJ+CFSyu+\"\n",
        "    \"LY+yttRM3gf3HH1bHmVtzR5NXZQXcI0epW1tdyh2d86a2O52F9vR6V8FUgTm1dWjjWuepZUgMK45k2cKMC6PIjD7rh6j+gpi\"\n",
        "    \"0Y904b675/n2h3u6QteTedkoc7qy2LIeaUxpkXNAHgeV53khozOYxZb1qDpTrvAa+TxBClECvEbPlvWY5o4cA7zGNDSfFIbX\"\n",
        "    \"6NmyHmd1SEnwGgfVJ6YKr9GzZT3KebdDZIYe5bzGQLFAj54t7XHXfDkmJ6v789r17e7iTEMrxXhmUh5tXHNmainCuObMVBOO\"\n",
        "    \"y6MYzEyrx9MW2VMf4cC9NWlbzpmVoqytucPkIO12SsZ4/lKUtTX7V6LGsfi2phadlLa1nX89naUHc437uYnQni/nlEzWN/S8\"\n",
        "    \"s7TVo41LvUsW9+pwqFBl79HsPYkVr/sK9GjjYnVWSzBfqjoowXx5FN+ZC6dHG5d6WybjfM15ouYE8+VRDObC1aOOa3v+WEJq\"\n",
        "    \"Tlzb2VCRELOtbYEU47NH5dHGpd5nagzj0s+HAozLoxjMq6tHnfutHmNjblz23G/1qChra3rMOT6f3jGefRVlbamZKfR1COKa\"\n",
        "    \"PWdS1tZcXy3mnMm3pd4IGpS1Nd9Uai0Wir6t+fxxUtrWPvu27M1MG8XJ1KMz+yqK8Vmt8mjjmn1CKmcUF88zhVIzimtOtZNi\"\n",
        "    \"fO6rPGpbW9XGLrg50G5rfwNhUueu7swmEtI55yQ0m/RpqI3JPd15ujVtWY9TSbcxpSU8dSShAj16tk4b3r6dmUlI2/L27Ulp\"\n",
        "    \"W7vKLFK3uByK4nI2VP19SFEJ7EOrRxvXyESr413YhPchLsssV/0dRlEJ70PKo7a1n/vWa1rLbsu8mTooa0up8tpyEd+Wep42\"\n",
        "    \"KG1rf6su5ebkvpqztLzl3qltRSVc28ojmTM+9F2TwNm3Qgp9/eSeKnaNXFtwqH3nk1KaJE05M6ZDOdNjv9NJIvm2Zr+3lFeP\"\n",
        "    \"EvqGnH1bM/eWMtNQvFCRHnzUlJlzXMqZYFKqHHLxbdFUFIbiu+8XWmo+50vL6R06l1u/PBO/asN8B5zAmdXTDtP7r8h+jeWL\"\n",
        "    \"lFOPocYumti39SWq3v0iTvxdtDw9hhmUq9N6PbaqvzJyqyNeMi0nIm51LLbojgIrOUKP+l2yBj16tghrqxib1gqg0pLknfIq\"\n",
        "    \"bdoirJqkMMNrnDNATPgaPVuE9VBv0J7HbU107V42W44eUhSjPrF4JKyHUokZxqXeTV+exAI9pCjGp/zKIwE9dO2+XSTX1vbc\"\n",
        "    \"b6pJUYT1UOCWWw2+rdmZJkVYD1Ef0Uoi39acySdFWA9RztLVjm9LPQ0cFAENc3unJjrVsWuY0mK297EYxbrbcs7vlUfCGib3\"\n",
        "    \"bQ/GNWffMijCGkZRDHr0w+KRsIbpnem6dnZbe9VOitTsW7e3I+e7Pgnv7tfHQxvl7O7KlvU4zu95fouUUNV21RSGFk2oHhdb\"\n",
        "    \"ZGbf+b4cXf8nRlnb8maASVlbc+UkSnSb0hKox4eFIjNHz7c/aHydkvC6T9Lydo3Je+u8ZSdf+xsu06O2ta2vPr70W1l2W9v6\"\n",
        "    \"UhSbPoG+kwYnSDVnTKGvqYvfv3JuDVLom2vvpOZpvh9fizHuX71jVp79vqDcG8rrJolLnecTxe8TgMLfBTDoJteOGUVntYJr\"\n",
        "    \"5PHOVsKnUaEuJw8oE9MWA5V5e2tA9wmQrw5xQB6RrernS33VkEB1PCmKW5f7P6zudeW7TwAA\"\n",
        ")\n",
        "\n",
        "DATA = Path(tempfile.mkdtemp(prefix=\"nuclear_sqd_\"))\n",
        "for name, blob in ((\"usda.snt\", _USDA_SNT_GZ), (\"gxpf1.snt\", _GXPF1_SNT_GZ)):\n",
        "    (DATA / name).write_bytes(gzip.decompress(base64.b64decode(blob)))\n",
        "\n",
        "    if not (DATA / name).is_file():\n",
        "        raise RuntimeError(f\"{name} did not unpack to {DATA}\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c005",
      "metadata": {},
      "source": [
        "### The model space and the qubit register\n",
        "\n",
        "An `.snt` file holds the model space, the single-particle energies, and the $J$-coupled two-body\n",
        "matrix elements. For the mass-dependent interactions used here, the third and fourth fields of the two-body\n",
        "header specify the reference mass\n",
        "$A_{\\mathrm{ref}}$ at which the interaction was fitted and the exponent of its mass dependence. Both\n",
        "files carry the exponent $-0.3$, with $A_{\\mathrm{ref}} = 18$ for USDA and $42$ for GXPF1, so the\n",
        "tabulated matrix elements must be rescaled by $(A/A_{\\mathrm{ref}})^{-0.3}$ for the nucleus being\n",
        "computed [\\[2\\]](#references), [\\[3\\]](#references). Single-particle energies are not rescaled. Skipping\n",
        "this step changes the correlation energy by a few percent.\n",
        "\n",
        "The energies that follow are *valence* energies, measured from the inert core; they are not experimental\n",
        "separation energies.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 2,
      "id": "c006",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:18.695272Z",
          "iopub.status.busy": "2026-09-16T21:58:18.695119Z",
          "iopub.status.idle": "2026-09-16T21:58:18.700950Z",
          "shell.execute_reply": "2026-09-16T21:58:18.700531Z"
        }
      },
      "outputs": [],
      "source": [
        "@dataclass(frozen=True)\n",
        "class Orbital:\n",
        "    idx: int\n",
        "    n: int\n",
        "    ell: int\n",
        "    j2: int\n",
        "    tz: int  # j2 = 2j; tz = -1 proton, +1 neutron\n",
        "\n",
        "\n",
        "@dataclass(frozen=True)\n",
        "class SPState:\n",
        "    \"\"\"One m-scheme single-particle state, i.e. one qubit.\"\"\"\n",
        "\n",
        "    orb: int\n",
        "    j2: int\n",
        "    mj2: int\n",
        "    tz: int\n",
        "    ell: int\n",
        "    spe: float  # mj2 = 2 * m_j\n",
        "\n",
        "\n",
        "@dataclass\n",
        "class ModelSpace:\n",
        "    orbitals: list\n",
        "    spes: dict\n",
        "    tbmes: dict\n",
        "    core_z: int\n",
        "    core_n: int\n",
        "    mass_number: int\n",
        "    a_ref: int\n",
        "    mass_exponent: float\n",
        "    mass_factor: float\n",
        "\n",
        "\n",
        "def read_snt(path, n_protons, n_neutrons):\n",
        "    \"\"\"Parse a .snt interaction file, applying its mass dependence for this nucleus.\n",
        "\n",
        "    The two-body header line is ``n_tbme  method  A_ref  exponent``.  When ``method`` is 1 the\n",
        "    tabulated matrix elements are rescaled by ``(A / A_ref) ** exponent``, where A is the mass\n",
        "    number of the whole nucleus -- the core plus the valence nucleons.  A is derived from the\n",
        "    file's own core numbers rather than passed in, so it cannot silently disagree with the\n",
        "    valence counts the rest of the workflow uses.  Single-particle energies are not rescaled.\n",
        "    \"\"\"\n",
        "    rows = [\n",
        "        ln.split(\"!\")[0].split() for ln in Path(path).read_text().splitlines()\n",
        "    ]\n",
        "    rows = iter([r for r in rows if r])\n",
        "\n",
        "    n_p_orb, n_n_orb, core_z, core_n = (int(x) for x in next(rows)[:4])\n",
        "    orbitals = [\n",
        "        Orbital(*(int(x) for x in next(rows)[:5]))\n",
        "        for _ in range(n_p_orb + n_n_orb)\n",
        "    ]\n",
        "\n",
        "    spes = {}\n",
        "    for _ in range(int(next(rows)[0])):  # \"i  i  <i|H(1b)|i>\"\n",
        "        field = next(rows)\n",
        "        spes[int(field[0])] = float(field[2])\n",
        "\n",
        "    n_tbme, method, a_ref, exponent = next(rows)[:4]\n",
        "    n_tbme, method, a_ref, exponent = (\n",
        "        int(n_tbme),\n",
        "        int(method),\n",
        "        int(a_ref),\n",
        "        float(exponent),\n",
        "    )\n",
        "    mass_number = core_z + core_n + n_protons + n_neutrons\n",
        "    factor = (mass_number / a_ref) ** exponent if method == 1 else 1.0\n",
        "\n",
        "    tbmes = {}\n",
        "    for _ in range(n_tbme):  # \"a  b  c  d  J  value\"\n",
        "        field = next(rows)\n",
        "        tbmes[tuple(int(x) for x in field[:5])] = float(field[5]) * factor\n",
        "\n",
        "    return ModelSpace(\n",
        "        orbitals,\n",
        "        spes,\n",
        "        tbmes,\n",
        "        core_z,\n",
        "        core_n,\n",
        "        mass_number,\n",
        "        a_ref,\n",
        "        exponent,\n",
        "        factor,\n",
        "    )\n",
        "\n",
        "\n",
        "def m_scheme_states(ms):\n",
        "    \"\"\"The qubit register: protons then neutrons, orbitals in file order, m_j descending.\"\"\"\n",
        "    return [\n",
        "        SPState(o.idx, o.j2, m2, o.tz, o.ell, ms.spes[o.idx])\n",
        "        for tz in (-1, +1)\n",
        "        for o in ms.orbitals\n",
        "        if o.tz == tz\n",
        "        for m2 in range(o.j2, -o.j2 - 1, -2)\n",
        "    ]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c007",
      "metadata": {},
      "source": [
        "### Clebsch-Gordan recoupling\n",
        "\n",
        "Equation (2) requires Clebsch-Gordan coefficients for half-integer angular momenta. Every argument is\n",
        "passed as *twice* its physical value, so $j = 5/2$ enters as `5` and the arithmetic stays exact.\n",
        "\n",
        "`Interaction.v_ms` handles lookups of the interaction matrix elements. An `.snt` file stores each\n",
        "matrix element once, so a lookup might need the antisymmetrized pair-exchange phase $-(-1)^{j_a + j_b - J}$ on either\n",
        "side, and the bra and ket might be stored in either order.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 3,
      "id": "c008",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:18.702446Z",
          "iopub.status.busy": "2026-09-16T21:58:18.702328Z",
          "iopub.status.idle": "2026-09-16T21:58:18.708774Z",
          "shell.execute_reply": "2026-09-16T21:58:18.708534Z"
        }
      },
      "outputs": [],
      "source": [
        "@lru_cache(maxsize=None)\n",
        "def clebsch_gordan(j1_2, j2_2, J_2, m1_2, m2_2, M_2):\n",
        "    \"\"\"<j1 m1 j2 m2 | J M>.  Every argument is twice its physical value.\"\"\"\n",
        "    if m1_2 + m2_2 != M_2 or not abs(j1_2 - j2_2) <= J_2 <= j1_2 + j2_2:\n",
        "        return 0.0\n",
        "    if abs(m1_2) > j1_2 or abs(m2_2) > j2_2 or abs(M_2) > J_2:\n",
        "        return 0.0\n",
        "    if (j1_2 + j2_2 - J_2) % 2 or (j1_2 - m1_2) % 2 or (j2_2 - m2_2) % 2:\n",
        "        return 0.0\n",
        "\n",
        "    f, half = factorial, lambda x: x // 2\n",
        "    prefactor = sqrt(\n",
        "        (J_2 + 1)\n",
        "        * f(half(j1_2 + j2_2 - J_2))\n",
        "        * f(half(j1_2 - j2_2 + J_2))\n",
        "        * f(half(-j1_2 + j2_2 + J_2))\n",
        "        / f(half(j1_2 + j2_2 + J_2) + 1)\n",
        "        * f(half(J_2 + M_2))\n",
        "        * f(half(J_2 - M_2))\n",
        "        * f(half(j1_2 - m1_2))\n",
        "        * f(half(j1_2 + m1_2))\n",
        "        * f(half(j2_2 - m2_2))\n",
        "        * f(half(j2_2 + m2_2))\n",
        "    )\n",
        "    total = 0.0\n",
        "    for k in range(half(j1_2 + j2_2 - J_2) + 1):\n",
        "        d = [\n",
        "            half(j1_2 + j2_2 - J_2) - k,\n",
        "            half(j1_2 - m1_2) - k,\n",
        "            half(j2_2 + m2_2) - k,\n",
        "            half(J_2 - j2_2 + m1_2) + k,\n",
        "            half(J_2 - j1_2 - m2_2) + k,\n",
        "        ]\n",
        "        if all(x >= 0 for x in d):\n",
        "            total += (-1) ** k / (\n",
        "                f(k) * f(d[0]) * f(d[1]) * f(d[2]) * f(d[3]) * f(d[4])\n",
        "            )\n",
        "    return prefactor * total\n",
        "\n",
        "\n",
        "class Interaction:\n",
        "    \"\"\"Antisymmetrized m-scheme two-body matrix elements <pq||rs>, per Eq. (2).\"\"\"\n",
        "\n",
        "    def __init__(self, model_space, sp):\n",
        "        self.ms, self.sp, self._cache = model_space, sp, {}\n",
        "\n",
        "    def _tbme(self, oa, ob, oc, od, J, j_ab_2, j_cd_2):\n",
        "        \"\"\"<oa ob; J|V|oc od; J>, allowing for how the file happens to order each pair.\"\"\"\n",
        "        table = self.ms.tbmes\n",
        "        # |ba; J> = -(-1)^(j_a + j_b - J) |ab; J> for a normalized antisymmetrized pair;\n",
        "        # dropping the leading minus makes v_ms symmetric instead of antisymmetric, and\n",
        "        # the Hamiltonian then fails the rotational-invariance check in Step 1.\n",
        "        phase_ab = -1.0 if (j_ab_2 // 2 - J) % 2 == 0 else 1.0\n",
        "        phase_cd = -1.0 if (j_cd_2 // 2 - J) % 2 == 0 else 1.0\n",
        "        for keys, phase in (\n",
        "            (((oa, ob, oc, od), (oc, od, oa, ob)), 1.0),\n",
        "            (((ob, oa, oc, od), (oc, od, ob, oa)), phase_ab),\n",
        "            (((oa, ob, od, oc), (od, oc, oa, ob)), phase_cd),\n",
        "            (((ob, oa, od, oc), (od, oc, ob, oa)), phase_ab * phase_cd),\n",
        "        ):\n",
        "            for key in keys:\n",
        "                value = table.get(key + (J,))\n",
        "                if value is not None:\n",
        "                    return value * phase\n",
        "        return 0.0\n",
        "\n",
        "    def v_ms(self, p, q, r, s):\n",
        "        \"\"\"<pq||rs>, zero unless M_J and charge are conserved.\"\"\"\n",
        "        cached = self._cache.get((p, q, r, s))\n",
        "        if cached is not None:\n",
        "            return cached\n",
        "\n",
        "        P, Q, R, S = (self.sp[i] for i in (p, q, r, s))\n",
        "        value = 0.0\n",
        "        if P.mj2 + Q.mj2 == R.mj2 + S.mj2 and P.tz + Q.tz == R.tz + S.tz:\n",
        "            M = P.mj2 + Q.mj2\n",
        "            # sqrt(1 + delta): undo the normalization of the tabulated pair states\n",
        "            c12 = sqrt(2.0) if (P.tz == Q.tz and P.orb == Q.orb) else 1.0\n",
        "            c34 = sqrt(2.0) if (R.tz == S.tz and R.orb == S.orb) else 1.0\n",
        "            for J2 in range(\n",
        "                max(abs(P.j2 - Q.j2), abs(R.j2 - S.j2)),\n",
        "                min(P.j2 + Q.j2, R.j2 + S.j2) + 1,\n",
        "                2,\n",
        "            ):\n",
        "                cg_bra = clebsch_gordan(P.j2, Q.j2, J2, P.mj2, Q.mj2, M)\n",
        "                cg_ket = clebsch_gordan(R.j2, S.j2, J2, R.mj2, S.mj2, M)\n",
        "                if abs(cg_bra) < 1e-12 or abs(cg_ket) < 1e-12:\n",
        "                    continue\n",
        "                value += (\n",
        "                    c12\n",
        "                    * c34\n",
        "                    * cg_bra\n",
        "                    * cg_ket\n",
        "                    * self._tbme(\n",
        "                        P.orb,\n",
        "                        Q.orb,\n",
        "                        R.orb,\n",
        "                        S.orb,\n",
        "                        J2 // 2,\n",
        "                        P.j2 + Q.j2,\n",
        "                        R.j2 + S.j2,\n",
        "                    )\n",
        "                )\n",
        "\n",
        "        self._cache[(p, q, r, s)] = value\n",
        "        return value"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c009",
      "metadata": {},
      "source": [
        "### Matrix elements and the symmetry test\n",
        "\n",
        "A determinant is a sorted tuple of occupied qubit indices. Two determinants differing in more than\n",
        "two occupied states have a vanishing matrix element; otherwise, the Slater-Condon rules give a short\n",
        "sum over the interaction, multiplied by a fermionic sign counting how many occupied states lie between the\n",
        "operators in the fixed register ordering.\n",
        "\n",
        "`symmetry_allowed` is the integer test that all four exact quantum numbers reduce to. It is used both\n",
        "to filter samples and to enumerate the exact basis for the runs small enough to check.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 4,
      "id": "c010",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:18.710135Z",
          "iopub.status.busy": "2026-09-16T21:58:18.710017Z",
          "iopub.status.idle": "2026-09-16T21:58:18.716327Z",
          "shell.execute_reply": "2026-09-16T21:58:18.716100Z"
        }
      },
      "outputs": [],
      "source": [
        "def matrix_element(inter, det_a, det_b):\n",
        "    \"\"\"<A|H|B> for two determinants, each a sorted tuple of occupied qubit indices.\"\"\"\n",
        "    set_a, set_b = set(det_a), set(det_b)\n",
        "    out_a, out_b = sorted(set_a - set_b), sorted(set_b - set_a)\n",
        "    if len(out_a) != len(out_b) or len(out_a) > 2:\n",
        "        return 0.0\n",
        "\n",
        "    if not out_a:  # diagonal: one-body plus two-body\n",
        "        return sum(inter.sp[i].spe for i in det_a) + sum(\n",
        "            inter.v_ms(i, j, i, j)\n",
        "            for i, j in itertools.combinations(det_a, 2)\n",
        "        )\n",
        "\n",
        "    if len(out_a) == 1:  # one state moves, p -> q\n",
        "        p, q = out_a[0], out_b[0]\n",
        "        crossings = sum(1 for k in set_a if min(p, q) < k < max(p, q))\n",
        "        return (-1.0) ** crossings * sum(\n",
        "            inter.v_ms(p, j, q, j) for j in det_a if j not in (p, q)\n",
        "        )\n",
        "\n",
        "    (p, r), (q, s) = out_a, out_b  # two states move\n",
        "    crossings = sum(1 for k in set_a if p < k < r) + sum(\n",
        "        1 for k in set_b if q < k < s\n",
        "    )\n",
        "    return (-1.0) ** crossings * inter.v_ms(p, r, q, s)\n",
        "\n",
        "\n",
        "def subspace_hamiltonian(inter, dets):\n",
        "    \"\"\"Dense real-symmetric H projected onto the span of `dets`.\"\"\"\n",
        "    H = np.zeros((len(dets), len(dets)))\n",
        "    for a, det_a in enumerate(dets):\n",
        "        H[a, a] = matrix_element(inter, det_a, det_a)\n",
        "        for b in range(a + 1, len(dets)):\n",
        "            H[a, b] = H[b, a] = matrix_element(inter, det_a, dets[b])\n",
        "    return H\n",
        "\n",
        "\n",
        "def ground_state(inter, dets):\n",
        "    \"\"\"Lowest eigenvalue and eigenvector of H over `dets`.\"\"\"\n",
        "    values, vectors = np.linalg.eigh(subspace_hamiltonian(inter, dets))\n",
        "    return values[0], vectors[:, 0]\n",
        "\n",
        "\n",
        "def symmetry_allowed(\n",
        "    sp, det, n_protons, n_neutrons, mj2_target=0, parity_target=0\n",
        "):\n",
        "    \"\"\"The four exact shell-model quantum numbers, as integer tests on one determinant.\"\"\"\n",
        "    n_p = sum(1 for i in det if sp[i].tz == -1)\n",
        "    return (\n",
        "        n_p == n_protons\n",
        "        and len(det) - n_p == n_neutrons\n",
        "        and sum(sp[i].mj2 for i in det) == mj2_target\n",
        "        and sum(sp[i].ell for i in det) % 2 == parity_target\n",
        "    )\n",
        "\n",
        "\n",
        "def full_basis(sp, n_protons, n_neutrons, **targets):\n",
        "    \"\"\"Every symmetry-allowed determinant.  Only tractable for small model spaces.\"\"\"\n",
        "    protons = [i for i, s in enumerate(sp) if s.tz == -1]\n",
        "    neutrons = [i for i, s in enumerate(sp) if s.tz == +1]\n",
        "    return [\n",
        "        p + n\n",
        "        for p in itertools.combinations(protons, n_protons)\n",
        "        for n in itertools.combinations(neutrons, n_neutrons)\n",
        "        if symmetry_allowed(sp, p + n, n_protons, n_neutrons, **targets)\n",
        "    ]\n",
        "\n",
        "\n",
        "def count_basis(sp, n_protons, n_neutrons, mj2_target=0, parity_target=0):\n",
        "    \"\"\"How many determinants `full_basis` would return, without enumerating them.\n",
        "\n",
        "    A dynamic program over (occupied count, sum of 2*m_j, parity) per species.  This stays\n",
        "    cheap when the basis itself is far too large to build, which is how the largest run below\n",
        "    can report the size of the space it is sampling from.\n",
        "    \"\"\"\n",
        "\n",
        "    def species(states, k):\n",
        "        table = {(0, 0, 0): 1}\n",
        "        for s in states:\n",
        "            for key, value in list(table.items()):\n",
        "                count, m_sum, parity = key\n",
        "                if count < k:\n",
        "                    nxt = (count + 1, m_sum + s.mj2, (parity + s.ell) % 2)\n",
        "                    table[nxt] = table.get(nxt, 0) + value\n",
        "        totals = {}\n",
        "        for (count, m_sum, parity), value in table.items():\n",
        "            if count == k:\n",
        "                totals[(m_sum, parity)] = (\n",
        "                    totals.get((m_sum, parity), 0) + value\n",
        "                )\n",
        "        return totals\n",
        "\n",
        "    left = species([s for s in sp if s.tz == -1], n_protons)\n",
        "    right = species([s for s in sp if s.tz == +1], n_neutrons)\n",
        "    return sum(\n",
        "        a * b\n",
        "        for (mp, pp), a in left.items()\n",
        "        for (mn, pn), b in right.items()\n",
        "        if mp + mn == mj2_target and (pp + pn) % 2 == parity_target\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c011",
      "metadata": {},
      "source": [
        "### The reference determinant\n",
        "\n",
        "The ansatz is built on top of a single determinant, so that determinant should be the best one\n",
        "available. Filling the lowest single-particle energies ignores the two-body interaction. In these model\n",
        "spaces, that choice gives an energy 1–2 MeV above the lowest-energy determinant.\n",
        "\n",
        "Restricting to fillings made of time-reversed $(+m_j, -m_j)$ pairs forces $M_J = 0$ exactly and\n",
        "leaves only $\\binom{n_{\\mathrm{pairs}}}{k}$ candidates per species (a few thousand at most), so the\n",
        "best one can be found by searching them all on the full diagonal $\\langle \\Phi | H | \\Phi \\rangle$.\n",
        "Ties go to the most strongly aligned pairs, where the $J = 0$ pairing force is strongest. In every\n",
        "case in this tutorial that can be checked against a full enumeration, the search returns the global\n",
        "lowest-diagonal determinant, which is also the largest single component of the exact ground state.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 5,
      "id": "c012",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:18.717528Z",
          "iopub.status.busy": "2026-09-16T21:58:18.717443Z",
          "iopub.status.idle": "2026-09-16T21:58:18.720155Z",
          "shell.execute_reply": "2026-09-16T21:58:18.719955Z"
        }
      },
      "outputs": [],
      "source": [
        "def reference_determinant(sp, inter, n_protons, n_neutrons):\n",
        "    \"\"\"Lowest-diagonal determinant built from time-reversed (+m_j, -m_j) orbital pairs.\"\"\"\n",
        "    if n_protons % 2 or n_neutrons % 2:\n",
        "        raise ValueError(\n",
        "            \"an odd valence count has no time-reversed paired reference at M_J = 0\"\n",
        "        )\n",
        "\n",
        "    def species_pairs(tz):\n",
        "        return [\n",
        "            (\n",
        "                q,\n",
        "                next(\n",
        "                    p\n",
        "                    for p, t in enumerate(sp)\n",
        "                    if t.tz == tz and t.orb == s.orb and t.mj2 == -s.mj2\n",
        "                ),\n",
        "            )\n",
        "            for q, s in enumerate(sp)\n",
        "            if s.tz == tz and s.mj2 > 0\n",
        "        ]\n",
        "\n",
        "    best = None\n",
        "    for chosen_p in itertools.combinations(species_pairs(-1), n_protons // 2):\n",
        "        protons = tuple(q for pair in chosen_p for q in pair)\n",
        "        for chosen_n in itertools.combinations(\n",
        "            species_pairs(+1), n_neutrons // 2\n",
        "        ):\n",
        "            det = tuple(\n",
        "                sorted(protons + tuple(q for pair in chosen_n for q in pair))\n",
        "            )\n",
        "            # break ties toward the most aligned pairs, where J = 0 pairing is strongest\n",
        "            score = (\n",
        "                matrix_element(inter, det, det),\n",
        "                -sum(abs(sp[q].mj2) for q in det),\n",
        "            )\n",
        "            if best is None or score < best[0]:\n",
        "                best = (score, det)\n",
        "    return best[1]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c013",
      "metadata": {},
      "source": [
        "### The excitation pool and its perturbative ranking\n",
        "\n",
        "Correlation is carried by two-particle–two-hole ($2p2h$) excitations from the reference. Two\n",
        "selection rules reduce the pool before any circuit is built: an excitation must conserve $M_J$, and the hole pair and particle\n",
        "pair must be able to couple to a common total $J$, which is a triangle inequality.\n",
        "\n",
        "The remaining excitations are ranked by the Epstein-Nesbet second-order score of selected configuration interaction\n",
        "[\\[4\\]](#references),\n",
        "\n",
        "$$\n",
        "\\begin{equation}\n",
        "\\tag{3}\n",
        "s_\\alpha = \\frac{|\\langle \\Phi_{\\mathrm{ref}} | H | \\alpha \\rangle|^2}{|\\Delta_\\alpha|},\n",
        "\\qquad\n",
        "\\Delta_\\alpha = H_{\\mathrm{ref},\\mathrm{ref}} - H_{\\alpha\\alpha},\n",
        "\\end{equation}\n",
        "$$\n",
        "\n",
        "which estimates how much correlation energy each excitation carries. The same two numbers fix the\n",
        "circuit angle: with $V = \\langle \\Phi_{\\mathrm{ref}} | H | \\alpha \\rangle$, the first-order amplitude\n",
        "is $t_\\alpha = V / \\Delta_\\alpha$. The [Appendix](#appendix) explains why the first-order amplitude is\n",
        "the choice used in this tutorial rather than the exact two-level angle.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 6,
      "id": "c014",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:18.721378Z",
          "iopub.status.busy": "2026-09-16T21:58:18.721288Z",
          "iopub.status.idle": "2026-09-16T21:58:18.725568Z",
          "shell.execute_reply": "2026-09-16T21:58:18.725336Z"
        }
      },
      "outputs": [],
      "source": [
        "def excitation_pool(sp, occ):\n",
        "    \"\"\"2p2h quadruples (h1, h2, v1, v2): same-species pairs, then proton-neutron pairs.\"\"\"\n",
        "    holes = {tz: [i for i in occ if sp[i].tz == tz] for tz in (-1, +1)}\n",
        "    virtuals = {\n",
        "        tz: [i for i, s in enumerate(sp) if s.tz == tz and i not in occ]\n",
        "        for tz in (-1, +1)\n",
        "    }\n",
        "    pool = [\n",
        "        (h1, h2, v1, v2)\n",
        "        for tz in (-1, +1)\n",
        "        for h1, h2 in itertools.combinations(holes[tz], 2)\n",
        "        for v1, v2 in itertools.combinations(virtuals[tz], 2)\n",
        "    ]\n",
        "    pool += [\n",
        "        (h1, h2, v1, v2)\n",
        "        for h1 in holes[-1]\n",
        "        for h2 in holes[+1]\n",
        "        for v1 in virtuals[-1]\n",
        "        for v2 in virtuals[+1]\n",
        "    ]\n",
        "    return pool\n",
        "\n",
        "\n",
        "def conserves_symmetry(sp, op):\n",
        "    \"\"\"Keeps M_J, and the hole and particle pairs share a reachable total J.\"\"\"\n",
        "    h1, h2, v1, v2 = op\n",
        "    if sp[v1].mj2 + sp[v2].mj2 != sp[h1].mj2 + sp[h2].mj2:\n",
        "        return False\n",
        "    return max(abs(sp[v1].j2 - sp[v2].j2), abs(sp[h1].j2 - sp[h2].j2)) <= min(\n",
        "        sp[v1].j2 + sp[v2].j2, sp[h1].j2 + sp[h2].j2\n",
        "    )\n",
        "\n",
        "\n",
        "def en_denominator(inter, occ, holes, virtuals, floor=0.1):\n",
        "    \"\"\"Epstein-Nesbet gap: bare gap, spectator rearrangement, and the pair's own term.\"\"\"\n",
        "    gap = sum(inter.sp[h].spe for h in holes) - sum(\n",
        "        inter.sp[v].spe for v in virtuals\n",
        "    )\n",
        "    for k in occ:\n",
        "        if k in holes:\n",
        "            continue\n",
        "        gap += sum(inter.v_ms(h, k, h, k) for h in holes)\n",
        "        gap -= sum(inter.v_ms(v, k, v, k) for v in virtuals)\n",
        "    gap += inter.v_ms(holes[0], holes[1], holes[0], holes[1])\n",
        "    gap -= inter.v_ms(virtuals[0], virtuals[1], virtuals[0], virtuals[1])\n",
        "    return gap if abs(gap) >= floor else (floor if gap >= 0 else -floor)\n",
        "\n",
        "\n",
        "def rank_pool(inter, occ, pool):\n",
        "    \"\"\"Sort by descending PT2 score; return (operator, coupling, first-order amplitude).\"\"\"\n",
        "    ranked = []\n",
        "    for op in pool:\n",
        "        h1, h2, v1, v2 = op\n",
        "        coupling = inter.v_ms(v1, v2, h1, h2)\n",
        "        gap = en_denominator(inter, occ, (h1, h2), (v1, v2))\n",
        "        ranked.append((coupling**2 / abs(gap), op, coupling, coupling / gap))\n",
        "    ranked.sort(key=lambda row: (-row[0], row[1]))  # deterministic on ties\n",
        "    return [\n",
        "        (op, coupling, amplitude) for _, op, coupling, amplitude in ranked\n",
        "    ]"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c015",
      "metadata": {},
      "source": [
        "### Qubit-excitation blocks\n",
        "\n",
        "Under the Jordan-Wigner mapping a particle-conserving $2p2h$ excitation operator becomes a sum of\n",
        "eight Pauli strings, each carrying a string of $Z$ operators between the outermost indices. The\n",
        "$Z$ strings enforce fermionic antisymmetry, and they are expensive: a proton-neutron\n",
        "excitation spans the boundary between the two halves of the register and includes a parity string\n",
        "across that boundary.\n",
        "\n",
        "Dropping the $Z$ strings gives the *qubit-excitation* operator of Yordanov et al.\n",
        "[\\[5\\]](#references). The state prepared by this operator has different amplitudes, but it connects exactly the same pairs of determinants, so the set of determinants the circuit can\n",
        "reach is unchanged. Pooled SQD uses these determinants for the classical diagonalization. Step 2 compares\n",
        "the support of the two constructions and measures their hardware costs.\n",
        "\n",
        "Building the Pauli form from $a_j^\\dagger = \\tfrac{1}{2}(X_j - i Y_j) \\otimes Z_{<j}$, with the $Z$\n",
        "string optional, keeps the two constructions a single flag apart. All eight terms of one generator\n",
        "commute, so a single `PauliEvolutionGate` step is the exact exponential rather than a Trotter\n",
        "approximation to it.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 7,
      "id": "c016",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:18.726885Z",
          "iopub.status.busy": "2026-09-16T21:58:18.726802Z",
          "iopub.status.idle": "2026-09-16T21:58:18.729969Z",
          "shell.execute_reply": "2026-09-16T21:58:18.729751Z"
        }
      },
      "outputs": [],
      "source": [
        "def _ladder(num_qubits, q, dagger, parity):\n",
        "    \"\"\"Pauli form of a_q or a_q^dagger.  `parity` toggles the Jordan-Wigner Z string.\"\"\"\n",
        "    prefix = (\n",
        "        [\"Z\"] * q + [\"I\"] * (num_qubits - q) if parity else [\"I\"] * num_qubits\n",
        "    )\n",
        "    x_part, y_part = list(prefix), list(prefix)\n",
        "    x_part[q], y_part[q] = \"X\", \"Y\"\n",
        "    return SparsePauliOp(\n",
        "        [\"\".join(reversed(x_part)), \"\".join(reversed(y_part))],\n",
        "        coeffs=[0.5, 0.5 * (-1j if dagger else 1j)],\n",
        "    )\n",
        "\n",
        "\n",
        "def excitation_generator(num_qubits, op, parity=False):\n",
        "    \"\"\"Hermitian H with exp(-i theta H) = exp(theta (T - T^dagger)) for T = a+ a+ a a.\"\"\"\n",
        "    h1, h2, v1, v2 = op\n",
        "    T = SparsePauliOp(\"I\" * num_qubits)\n",
        "    for q, dagger in ((v1, True), (v2, True), (h2, False), (h1, False)):\n",
        "        T = (T @ _ladder(num_qubits, q, dagger, parity)).simplify()\n",
        "    return (1j * (T - T.adjoint())).simplify()\n",
        "\n",
        "\n",
        "def excitation_block(op, theta, parity=False):\n",
        "    \"\"\"(window, circuit) for one excitation.\n",
        "\n",
        "    A qubit excitation touches only its four qubits.  A fermionic excitation also carries Z\n",
        "    operators on every qubit between the outermost indices, so its window is the whole span --\n",
        "    which is exactly where its extra cost comes from.\n",
        "    \"\"\"\n",
        "    window = list(range(min(op), max(op) + 1)) if parity else sorted(op)\n",
        "    local = tuple(window.index(i) for i in op)\n",
        "    generator = excitation_generator(len(window), local, parity=parity)\n",
        "    return window, PauliEvolutionGate(generator, time=theta).definition\n",
        "\n",
        "\n",
        "def excitation_ansatz(\n",
        "    num_qubits, occ, operators, amplitudes, measure=True, parity=False\n",
        "):\n",
        "    \"\"\"X gates for the reference determinant, then one evolution block per excitation.\"\"\"\n",
        "    qc = QuantumCircuit(num_qubits)\n",
        "    for q in occ:\n",
        "        qc.x(q)\n",
        "    for op, theta in zip(operators, amplitudes):\n",
        "        if abs(theta) < 1e-12:\n",
        "            continue\n",
        "        window, block = excitation_block(op, theta, parity=parity)\n",
        "        qc.compose(block, qubits=window, inplace=True)\n",
        "    if measure:\n",
        "        qc.measure_all()\n",
        "    return qc"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c017",
      "metadata": {},
      "source": [
        "### The depth budget and the circuit ensemble\n",
        "\n",
        "A single deep circuit containing every ranked excitation can exceed the hardware’s coherence time. Spreading the pool\n",
        "over an *ensemble* of shallow circuits and pooling their shots into one determinant set turns Step 2\n",
        "into a packing problem: each excitation has a measured cost, each circuit has a budget, and the\n",
        "question is how much of the ranked pool fits.\n",
        "\n",
        "The budget is measured in **two-qubit depth** (layers of two-qubit gates on the critical path)\n",
        "rather than in a raw gate count, because depth sets the circuit's duration and therefore how\n",
        "much of the device's coherence it spends. The total count is reported alongside it, since that is the\n",
        "better proxy for accumulated gate error; the two answer different questions and neither substitutes\n",
        "for the other.\n",
        "\n",
        "Both quantities are extracted by *arity*: an instruction acting on exactly two qubits, whatever the\n",
        "backend happens to call its entangling gate. Matching on gate names instead could return zero\n",
        "for an unfamiliar basis set, incorrectly placing the whole pool in one circuit without exceeding\n",
        "the computed budget.\n",
        "\n",
        "Filling whichever circuit is currently emptiest, in rank order, keeps every circuit near the budget.\n",
        "Costs are measured on the real backend target, one excitation at a time, because a cost read off an\n",
        "abstract circuit is not the cost the transpiler produces.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 8,
      "id": "c018",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:18.731195Z",
          "iopub.status.busy": "2026-09-16T21:58:18.731050Z",
          "iopub.status.idle": "2026-09-16T21:58:18.735085Z",
          "shell.execute_reply": "2026-09-16T21:58:18.734906Z"
        }
      },
      "outputs": [],
      "source": [
        "DIRECTIVES = (\"barrier\", \"delay\")\n",
        "\n",
        "\n",
        "def is_two_qubit(instruction):\n",
        "    \"\"\"True for an operation on exactly two qubits, excluding directives.\n",
        "\n",
        "    Selecting by arity rather than by gate name keeps this correct on any backend, whatever its\n",
        "    two-qubit basis gate happens to be called -- cz on today's Heron devices, ecr on Eagle, or\n",
        "    something newer tomorrow.  A gate-name allow-list silently returns zero on anything it has\n",
        "    not heard of, which would collapse the whole pool into one circuit and pass every budget\n",
        "    check.  Barriers are excluded because a barrier spanning two qubits is not a gate.\n",
        "    \"\"\"\n",
        "    return (\n",
        "        len(instruction.qubits) == 2\n",
        "        and instruction.operation.name not in DIRECTIVES\n",
        "    )\n",
        "\n",
        "\n",
        "def two_qubit_count(qc):\n",
        "    \"\"\"How many two-qubit gates the circuit contains: the accumulated-gate-error proxy.\"\"\"\n",
        "    return sum(1 for instruction in qc.data if is_two_qubit(instruction))\n",
        "\n",
        "\n",
        "def two_qubit_depth(qc):\n",
        "    \"\"\"Layers of two-qubit gates on the critical path: the duration and decoherence proxy.\n",
        "\n",
        "    This is what the budget is measured in.  Two gates on disjoint qubit pairs run in the same\n",
        "    layer, so depth tracks how long the circuit takes -- and therefore how much coherence it\n",
        "    spends -- while the count above tracks how much gate error it accumulates.  Both are\n",
        "    reported; only depth is budgeted.\n",
        "    \"\"\"\n",
        "    return qc.depth(filter_function=is_two_qubit)\n",
        "\n",
        "\n",
        "def excitation_costs(num_qubits, ranked, pm, parity=False):\n",
        "    \"\"\"Transpiled two-qubit depth of each excitation on its own.\"\"\"\n",
        "    return [\n",
        "        two_qubit_depth(\n",
        "            pm.run(\n",
        "                excitation_ansatz(\n",
        "                    num_qubits, (), [op], [amp], measure=False, parity=parity\n",
        "                )\n",
        "            )\n",
        "        )\n",
        "        for op, _, amp in ranked\n",
        "    ]\n",
        "\n",
        "\n",
        "def pack_ensemble(\n",
        "    num_qubits, occ, ranked, costs, budget, n_circuits, parity=False\n",
        "):\n",
        "    \"\"\"Fill n_circuits in rank order, always adding to whichever is currently emptiest.\"\"\"\n",
        "    bins, loads = [[] for _ in range(n_circuits)], [0] * n_circuits\n",
        "    for (op, _, amplitude), cost in zip(ranked, costs):\n",
        "        emptiest = min(range(n_circuits), key=lambda b: loads[b])\n",
        "        if loads[emptiest] + cost > budget:\n",
        "            break  # every circuit is full\n",
        "        bins[emptiest].append((op, amplitude))\n",
        "        loads[emptiest] += cost\n",
        "    circuits = [\n",
        "        excitation_ansatz(\n",
        "            num_qubits,\n",
        "            occ,\n",
        "            [o for o, _ in b],\n",
        "            [a for _, a in b],\n",
        "            parity=parity,\n",
        "        )\n",
        "        for b in bins\n",
        "    ]\n",
        "    return circuits, bins\n",
        "\n",
        "\n",
        "def pack_to_budget(\n",
        "    num_qubits, occ, ranked, costs, budget, n_circuits, pm, attempts=6\n",
        "):\n",
        "    \"\"\"Pack, transpile, and shrink the target until the assembled circuits really fit.\n",
        "\n",
        "    Costs are measured one excitation at a time, but excitations that share qubits neither add\n",
        "    nor parallelize cleanly once the transpiler routes them together, so the assembled depth is\n",
        "    not the sum of its measured parts.  This loop closes that gap against the real transpiler,\n",
        "    and it runs entirely before any job is submitted -- a budget failure must never cost shots.\n",
        "    \"\"\"\n",
        "    target = budget\n",
        "    for attempt in range(attempts):\n",
        "        circuits, bins = pack_ensemble(\n",
        "            num_qubits, occ, ranked, costs, target, n_circuits, parity=False\n",
        "        )\n",
        "        isa = pm.run(circuits)\n",
        "        worst = max(two_qubit_depth(c) for c in isa)\n",
        "        if worst <= budget:\n",
        "            return circuits, bins, isa\n",
        "        target = max(min(costs), int(target * budget / worst * 0.95))\n",
        "    raise RuntimeError(\n",
        "        f\"could not fit {n_circuits} circuits inside a two-qubit depth of {budget} in \"\n",
        "        f\"{attempts} attempts; raise N_CIRCUITS or DEPTH_BUDGET and re-run this cell. \"\n",
        "        \"No QPU time was spent.\"\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c019",
      "metadata": {},
      "source": [
        "### Post-processing: repair, recombine, diagonalize\n",
        "\n",
        "Three helpers do the work of Step 4.\n",
        "\n",
        "`half_configurations` splits each sampled row into a proton half and a neutron half, and keeps each\n",
        "half that has the correct nucleon number. A row with a valid proton half contributes that half even if its\n",
        "neutron half has the wrong nucleon number. Each half carries the total sampled weight of the rows it appeared in, which\n",
        "is what ranks it if the subspace needs to be truncated.\n",
        "\n",
        "`grow_subspace` recombines the halves into every product that lands in the target $M_J$ and parity\n",
        "sector, *adding* to the subspace it is given rather than rebuilding it. That keeps successive\n",
        "subspaces nested, which is what makes the energy sequence monotone non-increasing instead of merely\n",
        "fluctuating around a bound.\n",
        "\n",
        "`recovery_loop` is the self-consistent configuration recovery of the pooled SQD paper\n",
        "[\\[1\\]](#references): repair the two half-register nucleon numbers against the current occupancy\n",
        "estimate, recombine, diagonalize, and take the next occupancy estimate from the eigenvector.\n",
        "\n",
        "Check the bit-ordering conventions carefully to avoid incorrect results. `qiskit-addon-sqd` writes column 0 of its\n",
        "bitstring matrix as the *highest* qubit index, so reversing a row gives occupation indexed by qubit;\n",
        "its \"right\" half is the low qubit indices, which is the proton block. Correspondingly,\n",
        "`recover_configurations` takes `num_elec_a` as the proton number and average occupancies ordered\n",
        "`(protons, neutrons)` by qubit index. The addon assumes bit $i$ pairs with bit $i + N$; in this\n",
        "register, proton qubit $i$ and neutron qubit $i + N$ are the same $(n, \\ell, j, m_j)$ state, so the\n",
        "assumption is physically meaningful here rather than incidental.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 9,
      "id": "c020",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:18.736247Z",
          "iopub.status.busy": "2026-09-16T21:58:18.736171Z",
          "iopub.status.idle": "2026-09-16T21:58:18.740940Z",
          "shell.execute_reply": "2026-09-16T21:58:18.740702Z"
        }
      },
      "outputs": [],
      "source": [
        "def half_configurations(\n",
        "    bitstring_matrix, probabilities, sp, n_protons, n_neutrons\n",
        "):\n",
        "    \"\"\"Split each row into proton and neutron halves, keeping each half on its own weight.\n",
        "\n",
        "    Column 0 of the addon's matrix is the highest qubit index, so reversing a row gives\n",
        "    occupation indexed by qubit.\n",
        "    \"\"\"\n",
        "    protons, neutrons = {}, {}\n",
        "    for row, weight in zip(\n",
        "        bitstring_matrix, np.asarray(probabilities, dtype=float)\n",
        "    ):\n",
        "        occupied = np.flatnonzero(row[::-1])\n",
        "        p = tuple(int(i) for i in occupied if sp[i].tz == -1)\n",
        "        n = tuple(int(i) for i in occupied if sp[i].tz == +1)\n",
        "        if len(p) == n_protons:\n",
        "            protons[p] = protons.get(p, 0.0) + weight\n",
        "        if len(n) == n_neutrons:\n",
        "            neutrons[n] = neutrons.get(n, 0.0) + weight\n",
        "    return protons, neutrons\n",
        "\n",
        "\n",
        "def product_subspace(sp, protons, neutrons, n_protons, n_neutrons, **targets):\n",
        "    \"\"\"Every (proton half) x (neutron half) product that lands in the target sector.\"\"\"\n",
        "    return sorted(\n",
        "        d\n",
        "        for d in (\n",
        "            tuple(sorted(tuple(p) + tuple(n)))\n",
        "            for p in protons\n",
        "            for n in neutrons\n",
        "        )\n",
        "        if symmetry_allowed(sp, d, n_protons, n_neutrons, **targets)\n",
        "    )\n",
        "\n",
        "\n",
        "def grow_subspace(\n",
        "    sp,\n",
        "    kept_protons,\n",
        "    kept_neutrons,\n",
        "    offered_protons,\n",
        "    offered_neutrons,\n",
        "    n_protons,\n",
        "    n_neutrons,\n",
        "    max_dimension=None,\n",
        "    **targets,\n",
        "):\n",
        "    \"\"\"Add as many offered halves as the dimension cap allows, never dropping a kept one.\"\"\"\n",
        "    kept_p, kept_n = list(kept_protons), list(kept_neutrons)\n",
        "    new_p = [c for c in offered_protons if c not in set(kept_p)]\n",
        "    new_n = [c for c in offered_neutrons if c not in set(kept_n)]\n",
        "\n",
        "    if max_dimension is None:\n",
        "        kept_p, kept_n = kept_p + new_p, kept_n + new_n\n",
        "        return (\n",
        "            product_subspace(\n",
        "                sp, kept_p, kept_n, n_protons, n_neutrons, **targets\n",
        "            ),\n",
        "            kept_p,\n",
        "            kept_n,\n",
        "        )\n",
        "\n",
        "    basis = product_subspace(\n",
        "        sp, kept_p, kept_n, n_protons, n_neutrons, **targets\n",
        "    )\n",
        "    step = max(1, (len(new_p) + len(new_n)) // 24)\n",
        "    taken_p = taken_n = 0\n",
        "    while taken_p < len(new_p) or taken_n < len(new_n):\n",
        "        try_p, try_n = (\n",
        "            min(taken_p + step, len(new_p)),\n",
        "            min(taken_n + step, len(new_n)),\n",
        "        )\n",
        "        candidate = product_subspace(\n",
        "            sp,\n",
        "            kept_p + new_p[:try_p],\n",
        "            kept_n + new_n[:try_n],\n",
        "            n_protons,\n",
        "            n_neutrons,\n",
        "            **targets,\n",
        "        )\n",
        "        if len(candidate) > max_dimension:\n",
        "            if step == 1:\n",
        "                break\n",
        "            step = max(1, step // 2)\n",
        "            continue\n",
        "        basis, taken_p, taken_n = candidate, try_p, try_n\n",
        "    return basis, kept_p + new_p[:taken_p], kept_n + new_n[:taken_n]\n",
        "\n",
        "\n",
        "def occupancies(sp, dets, vector):\n",
        "    \"\"\"Average occupancy of each qubit in a subspace eigenvector, as (protons, neutrons).\"\"\"\n",
        "    half = len(sp) // 2\n",
        "    occ = np.zeros(len(sp))\n",
        "    for weight, det in zip(np.abs(vector) ** 2, dets):\n",
        "        for q in det:\n",
        "            occ[q] += weight\n",
        "    return occ[:half], occ[half:]\n",
        "\n",
        "\n",
        "def sample_occupancies(sp, bitstring_matrix, probabilities):\n",
        "    \"\"\"The same quantity estimated directly from sampled bitstrings.\"\"\"\n",
        "    half = len(sp) // 2\n",
        "    weights = np.asarray(probabilities, dtype=float)\n",
        "    occ = (weights[:, None] * bitstring_matrix[:, ::-1]).sum(\n",
        "        axis=0\n",
        "    ) / weights.sum()\n",
        "    return occ[:half], occ[half:]"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 10,
      "id": "c021",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:18.742192Z",
          "iopub.status.busy": "2026-09-16T21:58:18.742078Z",
          "iopub.status.idle": "2026-09-16T21:58:18.746826Z",
          "shell.execute_reply": "2026-09-16T21:58:18.746641Z"
        }
      },
      "outputs": [],
      "source": [
        "def recovery_loop(\n",
        "    inter,\n",
        "    sp,\n",
        "    bitstring_matrix,\n",
        "    probabilities,\n",
        "    reference,\n",
        "    n_protons,\n",
        "    n_neutrons,\n",
        "    max_iterations=4,\n",
        "    energy_tol=1e-4,\n",
        "    max_dimension=None,\n",
        "    seed=None,\n",
        "    **targets,\n",
        "):\n",
        "    \"\"\"Self-consistent configuration recovery, diagonalizing in the product subspace.\n",
        "\n",
        "    `num_elec_a` is the proton number and `num_elec_b` the neutron number, matching the\n",
        "    addon's right/left bipartition of the bitstring matrix.\n",
        "    \"\"\"\n",
        "    half = len(sp) // 2\n",
        "    p_ref = tuple(i for i in reference if sp[i].tz == -1)\n",
        "    n_ref = tuple(i for i in reference if sp[i].tz == +1)\n",
        "\n",
        "    survivors, survivor_probs = postselect_by_hamming_right_and_left(\n",
        "        bitstring_matrix,\n",
        "        np.asarray(probabilities, dtype=float).copy(),\n",
        "        hamming_right=n_protons,\n",
        "        hamming_left=n_neutrons,\n",
        "    )\n",
        "\n",
        "    if len(survivors):\n",
        "        guess = sample_occupancies(sp, survivors, survivor_probs)\n",
        "    else:  # nothing survived: start from the reference itself\n",
        "        guess = (\n",
        "            np.array([1.0 if q in p_ref else 0.0 for q in range(half)]),\n",
        "            np.array(\n",
        "                [1.0 if q + half in n_ref else 0.0 for q in range(half)]\n",
        "            ),\n",
        "        )\n",
        "\n",
        "    weights_p, weights_n = {p_ref: np.inf}, {n_ref: np.inf}\n",
        "    kept_p, kept_n = [p_ref], [n_ref]\n",
        "    history, best = [], None\n",
        "\n",
        "    for iteration in range(max_iterations):\n",
        "        # keep the occupancy estimate strictly inside (0, 1): the heuristic divides by it\n",
        "        clipped = tuple(np.clip(a, 1e-4, 1.0 - 1e-4) for a in guess)\n",
        "        recovered, recovered_probs = recover_configurations(\n",
        "            bitstring_matrix,\n",
        "            probabilities,\n",
        "            clipped,\n",
        "            n_protons,\n",
        "            n_neutrons,\n",
        "            rand_seed=None if seed is None else seed + iteration,\n",
        "        )\n",
        "\n",
        "        new_p, new_n = half_configurations(\n",
        "            recovered, recovered_probs, sp, n_protons, n_neutrons\n",
        "        )\n",
        "        for config, weight in new_p.items():\n",
        "            weights_p[config] = weights_p.get(config, 0.0) + weight\n",
        "        for config, weight in new_n.items():\n",
        "            weights_n[config] = weights_n.get(config, 0.0) + weight\n",
        "\n",
        "        def order(w):\n",
        "            return sorted(w, key=lambda c: (-w[c], c))\n",
        "\n",
        "        basis, kept_p, kept_n = grow_subspace(\n",
        "            sp,\n",
        "            kept_p,\n",
        "            kept_n,\n",
        "            order(weights_p),\n",
        "            order(weights_n),\n",
        "            n_protons,\n",
        "            n_neutrons,\n",
        "            max_dimension=max_dimension,\n",
        "            **targets,\n",
        "        )\n",
        "        energy, vector = ground_state(inter, basis)\n",
        "\n",
        "        history.append(\n",
        "            dict(\n",
        "                iteration=iteration + 1,\n",
        "                energy=energy,\n",
        "                dimension=len(basis),\n",
        "                protons=len(kept_p),\n",
        "                neutrons=len(kept_n),\n",
        "                recovered=len(recovered),\n",
        "                survivors=len(survivors),\n",
        "            )\n",
        "        )\n",
        "        print(\n",
        "            f\"  iteration {iteration + 1}: {len(kept_p)} proton x {len(kept_n)} neutron \"\n",
        "            f\"halves -> dimension {len(basis)}, E = {energy:.6f} MeV\"\n",
        "        )\n",
        "\n",
        "        if best is None or energy < best[0]:\n",
        "            best = (energy, basis, vector)\n",
        "            guess = occupancies(\n",
        "                sp, basis, vector\n",
        "            )  # the self-consistent update\n",
        "        if (\n",
        "            len(history) > 1\n",
        "            and abs(history[-2][\"energy\"] - energy) < energy_tol\n",
        "        ):\n",
        "            break\n",
        "\n",
        "    return dict(\n",
        "        energy=best[0], basis=best[1], vector=best[2], history=history\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c022",
      "metadata": {},
      "source": [
        "### Backend, budget, and run parameters\n",
        "\n",
        "Every run that follows uses the same backend, the same pass managers, and the same depth budget, so the three\n",
        "are directly comparable. The budget ties them together: every circuit in every\n",
        "ensemble has to fit inside it, and it decides how much of a pool can be sampled at all.\n",
        "\n",
        "The values here were chosen by measuring transpiled cost against a Heron target. At a two-qubit depth of 300 and 16 circuits, both 24-qubit and 40-qubit ensembles come out well under 100 microseconds per\n",
        "circuit, against coherence times of a few hundred microseconds. Increasing the budget includes more of the pool but increases circuit duration. Measure this\n",
        "tradeoff for your backend.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 11,
      "id": "c023",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:18.747910Z",
          "iopub.status.busy": "2026-09-16T21:58:18.747812Z",
          "iopub.status.idle": "2026-09-16T21:58:28.573345Z",
          "shell.execute_reply": "2026-09-16T21:58:28.573066Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "ibm_phoenix: 120 qubits, two-qubit basis gate cz\n",
            "two-qubit depth budget 300, 16 circuits x 10,000 shots per run\n",
            "three runs: 48 circuits, 480,000 shots in total\n"
          ]
        }
      ],
      "source": [
        "# This example assumes you have saved your IBM Quantum Platform account locally.\n",
        "service = QiskitRuntimeService()\n",
        "backend = service.least_busy(\n",
        "    operational=True, simulator=False, min_num_qubits=40\n",
        ")\n",
        "\n",
        "pass_manager = generate_preset_pass_manager(\n",
        "    optimization_level=3, backend=backend, seed_transpiler=42\n",
        ")\n",
        "costing_manager = generate_preset_pass_manager(\n",
        "    optimization_level=1, backend=backend, seed_transpiler=42\n",
        ")\n",
        "\n",
        "DEPTH_BUDGET = 300  # two-qubit depth per circuit\n",
        "N_CIRCUITS = 16  # circuits per ensemble\n",
        "SHOTS = 10_000  # shots per circuit\n",
        "MAX_DIMENSION = 4_000  # largest subspace the dense solver here will build\n",
        "JOB_TAGS = [\"TUT_SBQDNH\"]  # initials of the title's content words\n",
        "\n",
        "# derive the two-qubit basis gate from the target by arity, not from a hard-coded name\n",
        "two_qubit_basis = sorted(\n",
        "    name\n",
        "    for name in backend.target.operation_names\n",
        "    if backend.target.operation_from_name(name).num_qubits == 2\n",
        ")\n",
        "if not two_qubit_basis:\n",
        "    raise RuntimeError(\n",
        "        f\"{backend.name} exposes no two-qubit gate; pick another backend\"\n",
        "    )\n",
        "\n",
        "print(\n",
        "    f\"{backend.name}: {backend.num_qubits} qubits, two-qubit basis gate {two_qubit_basis[0]}\"\n",
        ")\n",
        "print(\n",
        "    f\"two-qubit depth budget {DEPTH_BUDGET}, {N_CIRCUITS} circuits x {SHOTS:,} shots per run\"\n",
        ")\n",
        "print(\n",
        "    f\"three runs: {3 * N_CIRCUITS} circuits, {3 * N_CIRCUITS * SHOTS:,} shots in total\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c024",
      "metadata": {},
      "source": [
        "## Small-scale hardware example\n",
        "\n",
        "This section follows the four-step workflow on a QPU, using the same backend and the same\n",
        "gate budget as the large-scale runs. The smaller problem provides an exact reference for checking the result.\n",
        "\n",
        "The small-scale problem is $^{20}\\mathrm{Ne}$: two valence protons and two valence neutrons in the\n",
        "$sd$ shell above an $^{16}\\mathrm{O}$ core, with the USDA interaction [\\[2\\]](#references). Three\n",
        "orbitals per species give 24 qubits, and the complete symmetry-allowed basis is 640 determinants,\n",
        "small enough to compare the energy estimates with the exact answer.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c025",
      "metadata": {},
      "source": [
        "### Step 1: Map classical inputs to a quantum problem\n",
        "\n",
        "Read the interaction, build the register, and construct the reference determinant. The following table shows the register information from the [Background](#background), read directly\n",
        "from the interaction file.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 12,
      "id": "c026",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:28.574881Z",
          "iopub.status.busy": "2026-09-16T21:58:28.574749Z",
          "iopub.status.idle": "2026-09-16T21:58:28.581474Z",
          "shell.execute_reply": "2026-09-16T21:58:28.581243Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "core Z=8 N=8 plus 2p + 2n valence ->  A=20 on 24 qubits\n",
            "interaction: 158 J-coupled matrix elements, fitted at A_ref=18, rescaled by (A/A_ref)^-0.3 = 0.968886\n",
            "\n",
            "  orbital   SPE (MeV)   proton qubits   neutron qubits\n",
            "    0d3/2      2.1117             0-3            12-15\n",
            "    0d5/2     -3.9257             4-9            16-21\n",
            "    1s1/2     -3.2079           10-11            22-23\n",
            "\n",
            "reference determinant occupies qubits (4, 9, 16, 21)\n",
            "  M_J = 0, parity = +1, energy = -29.765549 MeV\n"
          ]
        }
      ],
      "source": [
        "N_PROTONS, N_NEUTRONS = 2, 2\n",
        "\n",
        "ms_sd = read_snt(DATA / \"usda.snt\", N_PROTONS, N_NEUTRONS)\n",
        "sp_sd = m_scheme_states(ms_sd)\n",
        "inter_sd = Interaction(ms_sd, sp_sd)\n",
        "occ_sd = reference_determinant(sp_sd, inter_sd, N_PROTONS, N_NEUTRONS)\n",
        "\n",
        "# post-selection splits the register in half, so the two species must contribute equally\n",
        "n_proton_states = sum(1 for s in sp_sd if s.tz == -1)\n",
        "if n_proton_states != len(sp_sd) - n_proton_states:\n",
        "    raise ValueError(\n",
        "        \"this workflow needs equal proton and neutron state counts\"\n",
        "    )\n",
        "\n",
        "SHELL_LABEL = {0: \"s\", 1: \"p\", 2: \"d\", 3: \"f\", 4: \"g\"}\n",
        "print(\n",
        "    f\"core Z={ms_sd.core_z} N={ms_sd.core_n} plus {N_PROTONS}p + {N_NEUTRONS}n valence \"\n",
        "    f\"->  A={ms_sd.mass_number} on {len(sp_sd)} qubits\"\n",
        ")\n",
        "print(\n",
        "    f\"interaction: {len(ms_sd.tbmes)} J-coupled matrix elements, fitted at \"\n",
        "    f\"A_ref={ms_sd.a_ref}, rescaled by (A/A_ref)^{ms_sd.mass_exponent:g} = \"\n",
        "    f\"{ms_sd.mass_factor:.6f}\\n\"\n",
        ")\n",
        "\n",
        "print(\n",
        "    f\"{'orbital':>9}  {'SPE (MeV)':>10}  {'proton qubits':>14}  {'neutron qubits':>15}\"\n",
        ")\n",
        "for o in (o for o in ms_sd.orbitals if o.tz == -1):\n",
        "    twin = next(\n",
        "        t\n",
        "        for t in ms_sd.orbitals\n",
        "        if t.tz == +1 and (t.n, t.ell, t.j2) == (o.n, o.ell, o.j2)\n",
        "    )\n",
        "    qp = [q for q, s in enumerate(sp_sd) if s.orb == o.idx]\n",
        "    qn = [q for q, s in enumerate(sp_sd) if s.orb == twin.idx]\n",
        "    print(\n",
        "        f\"{f'{o.n}{SHELL_LABEL[o.ell]}{o.j2}/2':>9}  {ms_sd.spes[o.idx]:>10.4f}  \"\n",
        "        f\"{f'{qp[0]}-{qp[-1]}':>14}  {f'{qn[0]}-{qn[-1]}':>15}\"\n",
        "    )\n",
        "\n",
        "print(f\"\\nreference determinant occupies qubits {occ_sd}\")\n",
        "print(\n",
        "    f\"  M_J = {sum(sp_sd[i].mj2 for i in occ_sd) / 2:g}, \"\n",
        "    f\"parity = {(-1) ** (sum(sp_sd[i].ell for i in occ_sd) % 2):+d}, \"\n",
        "    f\"energy = {matrix_element(inter_sd, occ_sd, occ_sd):.6f} MeV\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c027",
      "metadata": {},
      "source": [
        "Run two checks on the Hamiltonian before continuing. Both are inexpensive and can reveal\n",
        "recoupling errors that a single energy calculation might not detect.\n",
        "\n",
        "A rotationally invariant Hamiltonian organizes its eigenstates into $J$ multiplets, so every\n",
        "eigenvalue of the $M_J = 2$ sector must also appear in the $M_J = 0$ spectrum at the same energy. The gap between the ground state and the lowest state carrying $M_J = 2$ is the $2^+$ excitation\n",
        "energy, which is measured: $1.634$ MeV for $^{20}\\mathrm{Ne}$ [\\[6\\]](#references). An empirical\n",
        "$sd$-shell interaction is expected to agree within a few hundred keV.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 13,
      "id": "c028",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:28.582862Z",
          "iopub.status.busy": "2026-09-16T21:58:28.582757Z",
          "iopub.status.idle": "2026-09-16T21:58:29.221054Z",
          "shell.execute_reply": "2026-09-16T21:58:29.220793Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "rotational invariance: all 497 M_J=2 eigenvalues found in the M_J=0 spectrum\n",
            "E(2+) - E(0+) = 1.747 MeV     (experiment: 1.634 MeV)\n",
            "\n",
            "reference determinant        -29.765549 MeV\n",
            "exact diagonalization        -40.472331 MeV   (dimension 640)\n",
            "correlation energy to find   -10.706782 MeV\n"
          ]
        }
      ],
      "source": [
        "basis_exact_sd = full_basis(sp_sd, N_PROTONS, N_NEUTRONS)\n",
        "if len(basis_exact_sd) != count_basis(sp_sd, N_PROTONS, N_NEUTRONS):\n",
        "    raise AssertionError(\"the basis counter disagrees with the enumeration\")\n",
        "E_REF_SD = matrix_element(inter_sd, occ_sd, occ_sd)\n",
        "E_EXACT_SD, _ = ground_state(inter_sd, basis_exact_sd)\n",
        "\n",
        "# the M_J = 2 sector: its spectrum must be contained in the M_J = 0 spectrum\n",
        "basis_mj2 = full_basis(sp_sd, N_PROTONS, N_NEUTRONS, mj2_target=4)\n",
        "spectrum_0 = np.linalg.eigvalsh(\n",
        "    subspace_hamiltonian(inter_sd, basis_exact_sd)\n",
        ")\n",
        "spectrum_2 = np.linalg.eigvalsh(subspace_hamiltonian(inter_sd, basis_mj2))\n",
        "contained = sum(\n",
        "    1 for e in spectrum_2 if np.min(np.abs(spectrum_0 - e)) < 1e-7\n",
        ")\n",
        "if contained != len(spectrum_2):\n",
        "    raise AssertionError(\n",
        "        f\"rotational invariance broken: only {contained}/{len(spectrum_2)} \"\n",
        "        \"M_J=2 eigenvalues appear in the M_J=0 spectrum\"\n",
        "    )\n",
        "\n",
        "print(\n",
        "    f\"rotational invariance: all {contained} M_J=2 eigenvalues found in the M_J=0 spectrum\"\n",
        ")\n",
        "print(\n",
        "    f\"E(2+) - E(0+) = {spectrum_2[0] - E_EXACT_SD:.3f} MeV     (experiment: 1.634 MeV)\\n\"\n",
        ")\n",
        "print(f\"reference determinant       {E_REF_SD:11.6f} MeV\")\n",
        "print(\n",
        "    f\"exact diagonalization       {E_EXACT_SD:11.6f} MeV   (dimension {len(basis_exact_sd)})\"\n",
        ")\n",
        "print(f\"correlation energy to find  {E_EXACT_SD - E_REF_SD:11.6f} MeV\")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c029",
      "metadata": {},
      "source": [
        "Next, construct the operator pool. Applying the two selection rules gives an important result: for this reference, in this model space, **there are no allowed single excitations at all**.\n",
        "\n",
        "The reason is specific and checkable. A $1p1h$ excitation conserves $M_J$ only if the particle state\n",
        "has the same $m_j$ as the hole. The reference occupies the two states of largest $|m_j|$ in the\n",
        "lowest orbital ($m_j = \\pm 5/2$ of $0d_{5/2}$), and no other orbital in the $sd$ shell reaches\n",
        "$|m_j| = 5/2$, since $0d_{3/2}$ stops at $3/2$ and $1s_{1/2}$ at $1/2$. Therefore, no single excitation\n",
        "survives, and correlation is carried entirely by $2p2h$ excitations. This is a property of the\n",
        "reference and the shell, not a general law; the following cell counts it rather than assuming it.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 14,
      "id": "c030",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:29.222525Z",
          "iopub.status.busy": "2026-09-16T21:58:29.222434Z",
          "iopub.status.idle": "2026-09-16T21:58:29.234651Z",
          "shell.execute_reply": "2026-09-16T21:58:29.234414Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "1p1h:   40 raw  ->    0 conserve M_J\n",
            "2p2h:  490 raw  ->   78 conserve M_J and couple to a common J\n",
            "\n",
            "rank      holes    particles   <ref|H|a> (MeV)   amplitude\n",
            "   1        4,9          0,3           -1.8714      0.1375\n",
            "   2      16,21        12,15           -1.8714      0.1375\n",
            "   3       4,21         3,12            1.6775     -0.1029\n",
            "   4       9,16         0,15            1.6775     -0.1029\n",
            "   5        4,9        10,11           -0.8728      0.1168\n",
            "   6      16,21        22,23           -0.8728      0.1168\n",
            "   7       4,21         3,17            1.0622     -0.0882\n",
            "   8       4,21         8,12           -1.0622      0.0882\n",
            "\n",
            "the pool reaches 412 determinants, whose product subspace spans 640 of 640\n"
          ]
        }
      ],
      "source": [
        "raw_pool_sd = excitation_pool(sp_sd, occ_sd)\n",
        "pool_sd = [op for op in raw_pool_sd if conserves_symmetry(sp_sd, op)]\n",
        "ranked_sd = rank_pool(inter_sd, occ_sd, pool_sd)\n",
        "\n",
        "singles_sd = [\n",
        "    (h, v)\n",
        "    for h in occ_sd\n",
        "    for v in range(len(sp_sd))\n",
        "    if v not in occ_sd and sp_sd[h].tz == sp_sd[v].tz\n",
        "]\n",
        "singles_mj_sd = [\n",
        "    (h, v) for h, v in singles_sd if sp_sd[h].mj2 == sp_sd[v].mj2\n",
        "]\n",
        "\n",
        "print(\n",
        "    f\"1p1h: {len(singles_sd):4d} raw  ->  {len(singles_mj_sd):3d} conserve M_J\"\n",
        ")\n",
        "print(\n",
        "    f\"2p2h: {len(raw_pool_sd):4d} raw  ->  {len(pool_sd):3d} conserve M_J and couple to a common J\\n\"\n",
        ")\n",
        "\n",
        "print(\n",
        "    f\"{'rank':>4}  {'holes':>9}  {'particles':>11}  {'<ref|H|a> (MeV)':>16}  {'amplitude':>10}\"\n",
        ")\n",
        "for r, (op, coupling, amplitude) in enumerate(ranked_sd[:8], start=1):\n",
        "    print(\n",
        "        f\"{r:>4}  {f'{op[0]},{op[1]}':>9}  {f'{op[2]},{op[3]}':>11}  \"\n",
        "        f\"{coupling:>16.4f}  {amplitude:>10.4f}\"\n",
        "    )\n",
        "\n",
        "# what is the best this ansatz could possibly do?  Apply every excitation once and recombine.\n",
        "reachable = {occ_sd}\n",
        "for op, _, _ in ranked_sd:\n",
        "    h1, h2, v1, v2 = op\n",
        "    reachable |= {\n",
        "        tuple(sorted(set(d) - {h1, h2} | {v1, v2}))\n",
        "        for d in reachable\n",
        "        if {h1, h2} <= set(d) and not {v1, v2} & set(d)\n",
        "    }\n",
        "ceiling = product_subspace(\n",
        "    sp_sd,\n",
        "    {tuple(i for i in d if sp_sd[i].tz == -1) for d in reachable},\n",
        "    {tuple(i for i in d if sp_sd[i].tz == +1) for d in reachable},\n",
        "    N_PROTONS,\n",
        "    N_NEUTRONS,\n",
        ")\n",
        "print(\n",
        "    f\"\\nthe pool reaches {len(reachable)} determinants, whose product subspace spans \"\n",
        "    f\"{len(ceiling)} of {len(basis_exact_sd)}\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c031",
      "metadata": {},
      "source": [
        "### Step 2: Optimize problem for quantum hardware execution\n",
        "\n",
        "Transpilation reveals the hardware cost of the Jordan-Wigner $Z$ strings and the savings from\n",
        "using qubit excitations. The first cell measures both constructions against the real backend\n",
        "target and checks the claim, introduced in the [Setup](#setup), that dropping the $Z$ strings changes the\n",
        "amplitudes but not the set of determinants the circuit can reach.\n",
        "\n",
        "Compare two consequences of this substitution. A qubit excitation costs the same regardless of the distance between its\n",
        "indices, so proton-neutron excitations, which span the boundary between the two halves of the\n",
        "register and make up most of the pool, no longer have this additional cost. The whole pool then\n",
        "fits inside the budget, which means the limit on the result is sampling rather than circuit depth.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 15,
      "id": "c032",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:29.235950Z",
          "iopub.status.busy": "2026-09-16T21:58:29.235852Z",
          "iopub.status.idle": "2026-09-16T21:58:29.889679Z",
          "shell.execute_reply": "2026-09-16T21:58:29.889393Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "      excitation   span  reachable determinants   same as fermionic?\n",
            "    (4, 9, 5, 8)      6                       2                  yes\n",
            "(16, 21, 17, 20)      6                       2                  yes\n",
            "    (4, 9, 6, 7)      6                       2                  yes\n",
            "\n",
            "-> identical support; the amplitudes differ, and pooled SQD only consumes the support\n",
            "\n",
            "excitation  count    QEB 2q depth   fermionic 2q depth\n",
            "      same     26           40-48               48-144\n",
            "        pn     52           48-48               48-256\n",
            "pool total     78            3728                 9112\n",
            "\n",
            "fermionic / qubit-excitation cost ratio: 2.44x\n",
            "\n",
            "ensemble capacity: 16 circuits at two-qubit depth 300\n"
          ]
        }
      ],
      "source": [
        "# 1. do the two constructions reach the same determinants?\n",
        "# Apply one block to the reference on the window it spans and read off which basis states\n",
        "# acquire amplitude.  Column 0 of the unitary is the image of |0...0>, and the X gates that\n",
        "# place the reference are part of the circuit, so that column is exactly what is wanted.\n",
        "# A fermionic block's window is its whole span, and building a unitary on it costs 4^n, so\n",
        "# probe the narrowest excitations in the pool rather than the highest-ranked ones.\n",
        "PROBE_SPAN = 12\n",
        "narrow = sorted(ranked_sd, key=lambda row: max(row[0]) - min(row[0]))\n",
        "probes = [op for op, _, _ in narrow if max(op) - min(op) + 1 <= PROBE_SPAN][\n",
        "    :3\n",
        "]\n",
        "if len(probes) < 2:\n",
        "    raise RuntimeError(\n",
        "        f\"no excitation spans {PROBE_SPAN} qubits or fewer; raise PROBE_SPAN\"\n",
        "    )\n",
        "\n",
        "print(\n",
        "    f\"{'excitation':>16}  {'span':>5}  {'reachable determinants':>22}  {'same as fermionic?':>19}\"\n",
        ")\n",
        "for probe_op in probes:\n",
        "    probe_window = list(range(min(probe_op), max(probe_op) + 1))\n",
        "    probe_local = tuple(probe_window.index(i) for i in probe_op)\n",
        "    probe_occ = tuple(\n",
        "        probe_window.index(i) for i in occ_sd if i in probe_window\n",
        "    )\n",
        "\n",
        "    supports = {}\n",
        "    for parity in (True, False):\n",
        "        unitary = Operator(\n",
        "            excitation_ansatz(\n",
        "                len(probe_window),\n",
        "                probe_occ,\n",
        "                [probe_local],\n",
        "                [0.7],\n",
        "                measure=False,\n",
        "                parity=parity,\n",
        "            )\n",
        "        ).data\n",
        "        supports[parity] = frozenset(\n",
        "            np.flatnonzero(np.abs(unitary[:, 0]) > 1e-10).tolist()\n",
        "        )\n",
        "\n",
        "    if len(supports[True]) < 2:\n",
        "        raise AssertionError(\n",
        "            f\"{probe_op}: the block did not move any amplitude, so this \"\n",
        "            \"comparison would be vacuous\"\n",
        "        )\n",
        "    if supports[True] != supports[False]:\n",
        "        raise AssertionError(\n",
        "            f\"{probe_op}: the two constructions reach different determinants\"\n",
        "        )\n",
        "    print(\n",
        "        f\"{str(probe_op):>16}  {len(probe_window):>5}  {len(supports[True]):>22}  {'yes':>19}\"\n",
        "    )\n",
        "\n",
        "print(\n",
        "    \"\\n-> identical support; the amplitudes differ, and pooled SQD only consumes the support\\n\"\n",
        ")\n",
        "\n",
        "# 2. what does each one cost on this backend?\n",
        "cost_qeb = excitation_costs(\n",
        "    len(sp_sd), ranked_sd, costing_manager, parity=False\n",
        ")\n",
        "cost_jw = excitation_costs(\n",
        "    len(sp_sd), ranked_sd, costing_manager, parity=True\n",
        ")\n",
        "\n",
        "\n",
        "def species(op):\n",
        "    return \"same\" if len({sp_sd[i].tz for i in op}) == 1 else \"pn\"\n",
        "\n",
        "\n",
        "print(\n",
        "    f\"{'excitation':>10}  {'count':>5}  {'QEB 2q depth':>14}  {'fermionic 2q depth':>19}\"\n",
        ")\n",
        "for group in (\"same\", \"pn\"):\n",
        "    q = [\n",
        "        c\n",
        "        for (op, _, _), c in zip(ranked_sd, cost_qeb)\n",
        "        if species(op) == group\n",
        "    ]\n",
        "    j = [\n",
        "        c for (op, _, _), c in zip(ranked_sd, cost_jw) if species(op) == group\n",
        "    ]\n",
        "    print(\n",
        "        f\"{group:>10}  {len(q):>5}  {f'{min(q)}-{max(q)}':>14}  {f'{min(j)}-{max(j)}':>19}\"\n",
        "    )\n",
        "print(\n",
        "    f\"{'pool total':>10}  {len(ranked_sd):>5}  {sum(cost_qeb):>14}  {sum(cost_jw):>19}\"\n",
        ")\n",
        "print(\n",
        "    f\"\\nfermionic / qubit-excitation cost ratio: {sum(cost_jw) / sum(cost_qeb):.2f}x\"\n",
        ")\n",
        "print(\n",
        "    f\"\\nensemble capacity: {N_CIRCUITS} circuits at two-qubit depth {DEPTH_BUDGET}\"\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 16,
      "id": "c033",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:29.891025Z",
          "iopub.status.busy": "2026-09-16T21:58:29.890939Z",
          "iopub.status.idle": "2026-09-16T21:58:30.215899Z",
          "shell.execute_reply": "2026-09-16T21:58:30.215568Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "packed 78 of 78 excitations into 16 circuits\n",
            "  excitations per circuit  [5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 4, 4]\n",
            "  two-qubit depth          [228, 182, 177, 220, 214, 220, 226, 224, 222, 222, 181, 179, 136, 179, 171, 181]\n",
            "  two-qubit gates          [234, 231, 227, 228, 225, 225, 233, 229, 233, 226, 230, 223, 232, 226, 176, 185]\n",
            "\n",
            "worst circuit: two-qubit depth 228 of a 300 budget, 234 two-qubit gates\n"
          ]
        }
      ],
      "source": [
        "circuits_sd, bins_sd, isa_sd = pack_to_budget(\n",
        "    len(sp_sd),\n",
        "    occ_sd,\n",
        "    ranked_sd,\n",
        "    cost_qeb,\n",
        "    DEPTH_BUDGET,\n",
        "    N_CIRCUITS,\n",
        "    pass_manager,\n",
        ")\n",
        "\n",
        "PACKED_SD = sum(len(b) for b in bins_sd)\n",
        "worst_sd = max(two_qubit_depth(c) for c in isa_sd)\n",
        "worst_count_sd = max(two_qubit_count(c) for c in isa_sd)\n",
        "\n",
        "print(\n",
        "    f\"packed {PACKED_SD} of {len(ranked_sd)} excitations into {N_CIRCUITS} circuits\"\n",
        ")\n",
        "print(f\"  excitations per circuit  {[len(b) for b in bins_sd]}\")\n",
        "print(f\"  two-qubit depth          {[two_qubit_depth(c) for c in isa_sd]}\")\n",
        "print(f\"  two-qubit gates          {[two_qubit_count(c) for c in isa_sd]}\")\n",
        "print(\n",
        "    f\"\\nworst circuit: two-qubit depth {worst_sd} of a {DEPTH_BUDGET} budget, \"\n",
        "    f\"{worst_count_sd} two-qubit gates\"\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c034",
      "metadata": {},
      "source": [
        "### Step 3: Execute using Qiskit primitives\n",
        "\n",
        "Submit one job per problem, with the whole ensemble as a single list of circuits. Gate and\n",
        "measurement twirling and dynamical decoupling are enabled to reduce the effects of hardware noise.\n",
        "Their benefit depends on the circuit and backend.\n",
        "\n",
        "Each job’s ID is printed. Use `service.job(\"JOB_ID\")` to retrieve the completed job and its\n",
        "results without using additional QPU time.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 17,
      "id": "c035",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:30.217402Z",
          "iopub.status.busy": "2026-09-16T21:58:30.217314Z",
          "iopub.status.idle": "2026-09-16T21:58:30.220288Z",
          "shell.execute_reply": "2026-09-16T21:58:30.220026Z"
        }
      },
      "outputs": [],
      "source": [
        "def sample(isa_circuits, shots, tags):\n",
        "    \"\"\"Submit one Sampler job; return the per-circuit bit arrays and the measured QPU seconds.\"\"\"\n",
        "    sampler = SamplerV2(mode=backend)\n",
        "    sampler.options.environment.job_tags = tags\n",
        "    sampler.options.twirling.enable_gates = True\n",
        "    sampler.options.twirling.enable_measure = True\n",
        "    sampler.options.dynamical_decoupling.enable = True\n",
        "    sampler.options.dynamical_decoupling.sequence_type = \"XY4\"\n",
        "\n",
        "    job = sampler.run(isa_circuits, shots=shots)\n",
        "    print(\n",
        "        f\"job {job.job_id()}: {len(isa_circuits)} circuits x {shots:,} shots \"\n",
        "        f\"on {backend.name}\"\n",
        "    )\n",
        "    return [pub.data.meas for pub in job.result()]\n",
        "\n",
        "\n",
        "def pool_samples(bit_arrays, sp):\n",
        "    \"\"\"Merge the ensemble's bit arrays into one bitstring matrix and probability vector.\"\"\"\n",
        "    matrices, weights, total = [], [], 0\n",
        "    for bit_array in bit_arrays:\n",
        "        matrix, probabilities = bit_array_to_arrays(bit_array)\n",
        "        matrices.append(matrix)\n",
        "        weights.append(probabilities * bit_array.num_shots)\n",
        "        total += bit_array.num_shots\n",
        "    counts = np.concatenate(weights)\n",
        "    matrix = np.vstack(matrices)\n",
        "    # the same bitstring can appear in more than one circuit; merge duplicate rows\n",
        "    unique, inverse = np.unique(matrix, axis=0, return_inverse=True)\n",
        "    merged = np.zeros(len(unique))\n",
        "    np.add.at(merged, inverse.ravel(), counts)\n",
        "    return unique, merged / merged.sum(), total"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 18,
      "id": "c036",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:30.221750Z",
          "iopub.status.busy": "2026-09-16T21:58:30.221632Z",
          "iopub.status.idle": "2026-09-16T21:58:34.037122Z",
          "shell.execute_reply": "2026-09-16T21:58:34.036800Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "job dap30qtr85ps73fg21p0: 16 circuits x 10,000 shots on ibm_phoenix\n",
            "\n",
            "160,000 shots  ->  17,221 distinct bitstrings\n",
            "   31.5% of shots carry the right proton and neutron numbers\n",
            "  973 distinct bitstrings do\n",
            "\n",
            "    neutrons | protons        share\n",
            "001000010000 | 001000010000  20.08%   <- reference determinant\n",
            "000000010000 | 001000010000   2.25%\n",
            "001000010000 | 001000000000   2.19%\n",
            "001000010000 | 000000010000   1.93%\n"
          ]
        }
      ],
      "source": [
        "bit_arrays_sd = sample(isa_sd, SHOTS, JOB_TAGS + [\"20Ne\"])\n",
        "matrix_sd, probs_sd, shots_sd = pool_samples(bit_arrays_sd, sp_sd)\n",
        "\n",
        "survivors_sd, _ = postselect_by_hamming_right_and_left(\n",
        "    matrix_sd,\n",
        "    probs_sd.copy(),\n",
        "    hamming_right=N_PROTONS,\n",
        "    hamming_left=N_NEUTRONS,\n",
        ")\n",
        "shot_survival_sd = float(\n",
        "    probs_sd[\n",
        "        (matrix_sd[:, len(sp_sd) // 2 :].sum(axis=1) == N_PROTONS)\n",
        "        & (matrix_sd[:, : len(sp_sd) // 2].sum(axis=1) == N_NEUTRONS)\n",
        "    ].sum()\n",
        ")\n",
        "\n",
        "reference_bits = \"\".join(\n",
        "    \"1\" if q in occ_sd else \"0\" for q in range(len(sp_sd))\n",
        ")[::-1]\n",
        "print(f\"\\n{shots_sd:,} shots  ->  {len(matrix_sd):,} distinct bitstrings\")\n",
        "print(\n",
        "    f\"  {shot_survival_sd:6.1%} of shots carry the right proton and neutron numbers\"\n",
        ")\n",
        "print(f\"  {len(survivors_sd):,} distinct bitstrings do\")\n",
        "\n",
        "order = np.argsort(-probs_sd)\n",
        "half = len(sp_sd) // 2\n",
        "print(f\"\\n{'neutrons':>{half}} | {'protons':<{half}}   share\")\n",
        "for i in order[:4]:\n",
        "    bits = \"\".join(\"1\" if b else \"0\" for b in matrix_sd[i])\n",
        "    tag = \"   <- reference determinant\" if bits == reference_bits else \"\"\n",
        "    print(f\"{bits[:half]} | {bits[half:]}  {probs_sd[i]:6.2%}{tag}\")\n",
        "\n",
        "if len(survivors_sd) == 0:\n",
        "    raise RuntimeError(\n",
        "        \"no shot carried the right nucleon numbers; check the backend and \"\n",
        "        \"the transpiled circuits before spending more QPU time\"\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c037",
      "metadata": {},
      "source": [
        "### Step 4: Post-process and return result in desired classical format\n",
        "\n",
        "Convert the quantum samples into an energy estimate using the nuclear-symmetry constraints\n",
        "described in the [Background](#background) section.\n",
        "\n",
        "**Configuration recovery repairs the two nucleon numbers.** `recover_configurations` takes each shot that\n",
        "has the wrong number of protons or neutrons and flips the bits least consistent with the current\n",
        "estimate of the average orbital occupancies, instead of discarding it. On the first pass the\n",
        "occupancy estimate comes from the shots that already survived; afterward it comes from the\n",
        "eigenvector of the previous subspace, which makes the procedure self-consistent.\n",
        "\n",
        "**$M_J$ and parity are imposed on the recombined products, not on whole shots.** Every repaired shot\n",
        "contributes a proton half and a neutron half, and the subspace is spanned by every product of a\n",
        "sampled proton configuration with a sampled neutron configuration that lands at $M_J = 0$ with the\n",
        "right parity. Filtering whole shots on total $M_J$ instead would throw away two good halves for the\n",
        "sake of a quantum number that belongs to their combination.\n",
        "\n",
        "The four quantum-number checks reject different fractions of samples. The two nucleon numbers\n",
        "account for most of the filtering. Parity is *automatically* satisfied inside a single major shell: every $sd$ orbital\n",
        "has even $\\ell$ and every $pf$ orbital odd $\\ell$, so once the nucleon numbers are right the parity\n",
        "cannot be wrong. The parity check is retained because a cross-shell model space would make it an\n",
        "independent constraint. The $M_J$ check keeps products in the target angular-momentum sector. The value of\n",
        "having four exact quantum numbers is that they are *cheap and exact*, not that each one is a large\n",
        "filter.\n",
        "\n",
        "**Diagonalizing gives a variational upper bound.** Because each iteration's subspace contains the\n",
        "last, the sequence of energies falls monotonically, and every entry in it is a rigorous upper bound on\n",
        "the true ground-state energy, regardless of the noise in the samples that produced it.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 19,
      "id": "c038",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:34.038614Z",
          "iopub.status.busy": "2026-09-16T21:58:34.038496Z",
          "iopub.status.idle": "2026-09-16T21:58:36.299342Z",
          "shell.execute_reply": "2026-09-16T21:58:36.299077Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "  iteration 1: 66 proton x 66 neutron halves -> dimension 640, E = -40.472331 MeV\n",
            "  iteration 2: 66 proton x 66 neutron halves -> dimension 640, E = -40.472331 MeV\n",
            "\n",
            "reference determinant    -29.765549 MeV\n",
            "pooled SQD upper bound          -40.472331 MeV   (subspace dimension 640 of 640)\n",
            "exact diagonalization    -40.472331 MeV\n",
            "\n",
            "correlation energy recovered: 100.0%\n"
          ]
        }
      ],
      "source": [
        "result_sd = recovery_loop(\n",
        "    inter_sd,\n",
        "    sp_sd,\n",
        "    matrix_sd,\n",
        "    probs_sd,\n",
        "    occ_sd,\n",
        "    N_PROTONS,\n",
        "    N_NEUTRONS,\n",
        "    max_iterations=4,\n",
        "    max_dimension=MAX_DIMENSION,\n",
        "    seed=42,\n",
        ")\n",
        "\n",
        "E_SQD_SD = result_sd[\"energy\"]\n",
        "recovered_sd = 100 * (E_SQD_SD - E_REF_SD) / (E_EXACT_SD - E_REF_SD)\n",
        "\n",
        "print(f\"\\nreference determinant   {E_REF_SD:11.6f} MeV\")\n",
        "print(\n",
        "    f\"pooled SQD upper bound         {E_SQD_SD:11.6f} MeV   \"\n",
        "    f\"(subspace dimension {len(result_sd['basis'])} of {len(basis_exact_sd)})\"\n",
        ")\n",
        "print(f\"exact diagonalization   {E_EXACT_SD:11.6f} MeV\")\n",
        "print(f\"\\ncorrelation energy recovered: {recovered_sd:.1f}%\")\n",
        "\n",
        "energies_sd = [h[\"energy\"] for h in result_sd[\"history\"]]\n",
        "if any(b > a + 1e-9 for a, b in zip(energies_sd, energies_sd[1:])):\n",
        "    raise AssertionError(\n",
        "        \"the subspaces are not nested; the bound should never rise\"\n",
        "    )\n",
        "if E_SQD_SD < E_EXACT_SD - 1e-7:\n",
        "    raise AssertionError(\n",
        "        f\"pooled SQD returned {E_SQD_SD:.6f}, below the exact {E_EXACT_SD:.6f}; \"\n",
        "        \"a subspace bound cannot beat the full diagonalization\"\n",
        "    )"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c039",
      "metadata": {},
      "source": [
        "### Evaluate the results\n",
        "\n",
        "Use the following checks to evaluate your results on a Heron-class backend with these settings:\n",
        "\n",
        "* **Shot survival** on the two nucleon numbers measures the fraction of shots with the correct proton and neutron counts. It can fall as\n",
        "  the register grows. A survival rate near zero can indicate a problem with circuit execution. Check the ISA depth in Step 2 and the backend's calibration, not the post-processing.\n",
        "* **The recovery loop** should print a subspace dimension that stays constant or grows and an energy that stays constant or falls with\n",
        "  each iteration. If iteration 1 already reaches `MAX_DIMENSION`, the classical solver rather than the\n",
        "  sampling is the binding constraint.\n",
        "* **The recovered fraction** for $^{20}\\mathrm{Ne}$ should be high, because the ansatz ceiling\n",
        "  computed in Step 1 is the full 640-determinant space; this run is where sampling, not\n",
        "  expressiveness, is the only obstacle.\n",
        "* **The two assertions** in the preceding cell check the variational bounds. A bound that rises means the\n",
        "  subspaces stopped being nested, and a bound below the exact energy means something is wrong with the\n",
        "  Hamiltonian, not with the hardware.\n",
        "\n",
        "Counterintuitively, a *noisier* backend can give a slightly better bound than a clean one, because\n",
        "errors produce valid half-configurations the ideal circuit would never have sampled, and widening\n",
        "a variational subspace cannot raise its lowest eigenvalue. Noisy simulation can demonstrate the same effect; this tutorial shows it with\n",
        "hardware samples.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": null,
      "id": "c040",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:36.300858Z",
          "iopub.status.busy": "2026-09-16T21:58:36.300735Z",
          "iopub.status.idle": "2026-09-16T21:58:36.495758Z",
          "shell.execute_reply": "2026-09-16T21:58:36.495514Z"
        }
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/nuclear-sqd-pooled/extracted-outputs/c040-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "# IBM Carbon palette: Blue 60 and Blue 80 for data, Gray 100/70/30 for ink and rules\n",
        "SURFACE, INK, MUTED, RULE = \"#ffffff\", \"#161616\", \"#6f6f6f\", \"#c6c6c6\"\n",
        "SERIES, DEEP, PURPLE = \"#0f62fe\", \"#002d9c\", \"#6929c4\"\n",
        "\n",
        "\n",
        "def convergence_plot(\n",
        "    history, e_ref, e_exact, title, colour=SERIES, full_dim=None\n",
        "):\n",
        "    \"\"\"Energy against subspace dimension, scaled to the data rather than to the full window.\n",
        "\n",
        "    A good run lands within a fraction of a percent of the exact answer, so an axis spanning\n",
        "    reference-to-exact would squash every point onto one line.  The axis is therefore scaled to\n",
        "    the data (plus the exact line, when there is one), and the right-hand axis carries the\n",
        "    fraction of the correlation energy so the absolute and relative readings sit side by side.\n",
        "    \"\"\"\n",
        "    dimensions = [h[\"dimension\"] for h in history]\n",
        "    energies = [h[\"energy\"] for h in history]\n",
        "\n",
        "    fig, ax = plt.subplots(figsize=(7.4, 4.3), facecolor=SURFACE)\n",
        "    ax.set_facecolor(SURFACE)\n",
        "    ax.plot(\n",
        "        dimensions,\n",
        "        energies,\n",
        "        \"-o\",\n",
        "        color=colour,\n",
        "        linewidth=2,\n",
        "        markersize=8,\n",
        "        markeredgecolor=SURFACE,\n",
        "        markeredgewidth=1.5,\n",
        "        zorder=3,\n",
        "    )\n",
        "    stacked = {}\n",
        "    for h in history:\n",
        "        # a converged loop repeats the same point; stack the labels so they do not overprint\n",
        "        key = (round(h[\"dimension\"]), round(h[\"energy\"], 9))\n",
        "        offset = 12 + 11 * stacked.get(key, 0)\n",
        "        stacked[key] = stacked.get(key, 0) + 1\n",
        "        ax.annotate(\n",
        "            str(h[\"iteration\"]),\n",
        "            xy=(h[\"dimension\"], h[\"energy\"]),\n",
        "            xytext=(0, offset),\n",
        "            textcoords=\"offset points\",\n",
        "            ha=\"center\",\n",
        "            fontsize=8,\n",
        "            color=MUTED,\n",
        "        )\n",
        "\n",
        "    span = (max(dimensions) - min(dimensions)) or max(1, max(dimensions) // 4)\n",
        "    x_left, x_right = (\n",
        "        min(dimensions) - 0.14 * span,\n",
        "        max(dimensions) + 0.40 * span,\n",
        "    )\n",
        "    ax.set_xlim(x_left, x_right)\n",
        "\n",
        "    floor = min(energies) if e_exact is None else min(min(energies), e_exact)\n",
        "    height = max(max(energies) - floor, 1e-3)\n",
        "    ax.set_ylim(floor - 0.30 * height, max(energies) + 0.42 * height)\n",
        "\n",
        "    if e_exact is not None:\n",
        "        ax.axhline(\n",
        "            e_exact, color=MUTED, linestyle=\"--\", linewidth=1, zorder=1\n",
        "        )\n",
        "        label = \"exact\" + (f\", {full_dim:,} determinants\" if full_dim else \"\")\n",
        "        ax.annotate(\n",
        "            f\"{label}   {e_exact:.3f} MeV\".replace(\"-\", \"\\u2212\"),\n",
        "            xy=(x_left, e_exact),\n",
        "            xytext=(3, 5),\n",
        "            textcoords=\"offset points\",\n",
        "            ha=\"left\",\n",
        "            va=\"bottom\",\n",
        "            color=MUTED,\n",
        "            fontsize=9,\n",
        "        )\n",
        "\n",
        "    # the reference determinant is far off this scale; state it rather than plotting it\n",
        "    ax.annotate(\n",
        "        f\"reference determinant {e_ref:.3f} MeV\".replace(\"-\", \"\\u2212\")\n",
        "        + f\"   ({e_ref - max(energies):+.2f} MeV off the top of this axis)\".replace(\n",
        "            \"-\", \"\\u2212\"\n",
        "        ),\n",
        "        xy=(x_right, max(energies) + 0.42 * height),\n",
        "        xytext=(-3, -12),\n",
        "        textcoords=\"offset points\",\n",
        "        ha=\"right\",\n",
        "        va=\"top\",\n",
        "        color=MUTED,\n",
        "        fontsize=8.5,\n",
        "    )\n",
        "\n",
        "    if e_exact is not None and abs(e_exact - e_ref) > 1e-9:\n",
        "        right = ax.twinx()\n",
        "        low, high = ax.get_ylim()\n",
        "\n",
        "        def to_percent(e):\n",
        "            return 100 * (e - e_ref) / (e_exact - e_ref)\n",
        "\n",
        "        right.set_ylim(to_percent(low), to_percent(high))\n",
        "        right.set_ylabel(\"correlation energy recovered (%)\", color=MUTED)\n",
        "        right.tick_params(colors=MUTED)\n",
        "        for side in (\"top\", \"left\"):\n",
        "            right.spines[side].set_visible(False)\n",
        "        right.spines[\"right\"].set_color(MUTED)\n",
        "        right.spines[\"bottom\"].set_color(MUTED)\n",
        "\n",
        "    ax.set_xlabel(\"subspace dimension\", color=MUTED)\n",
        "    ax.set_ylabel(\"ground-state energy (MeV)\", color=MUTED)\n",
        "    ax.set_title(title, color=INK, fontsize=11.5, loc=\"left\", pad=12)\n",
        "    ax.grid(axis=\"y\", color=RULE, alpha=0.55)\n",
        "    ax.tick_params(colors=MUTED)\n",
        "    for side in (\"top\", \"right\"):\n",
        "        ax.spines[side].set_visible(False)\n",
        "    for side in (\"bottom\", \"left\"):\n",
        "        ax.spines[side].set_color(MUTED)\n",
        "    fig.tight_layout()\n",
        "    return fig\n",
        "\n",
        "\n",
        "convergence_plot(\n",
        "    result_sd[\"history\"],\n",
        "    E_REF_SD,\n",
        "    E_EXACT_SD,\n",
        "    f\"$^{{20}}$Ne: the bound falls as configuration recovery widens the subspace\\n\"\n",
        "    f\"({backend.name}, {N_CIRCUITS} circuits x {SHOTS:,} shots)\",\n",
        "    full_dim=len(basis_exact_sd),\n",
        ")\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c041",
      "metadata": {},
      "source": [
        "## Large-scale hardware example\n",
        "\n",
        "Scaling up changes only the inputs, so the next step is to combine the four stages into one function\n",
        "and run it twice, both times on a 40-qubit register in the $pf$ shell above a\n",
        "$^{40}\\mathrm{Ca}$ core with the GXPF1 interaction [\\[3\\]](#references).\n",
        "\n",
        "The two runs illustrate different aspects of scaling:\n",
        "\n",
        "* $^{44}\\mathrm{Ti}$, two valence protons and two valence neutrons, has a 4,000-determinant basis. The\n",
        "  register is 40 qubits, but the problem is still small enough to diagonalize exactly on a laptop, so\n",
        "  you can compare the hardware result with an exact reference after increasing the register size.\n",
        "* $^{48}\\mathrm{Cr}$, four valence protons and four valence neutrons, has 1,963,461 symmetry-allowed determinants in the same 40 qubits.\n",
        "  The tutorial's dense solver cannot diagonalize that full space, so the run returns\n",
        "  a rigorous upper bound and the reference determinant it improves on.\n",
        "\n",
        "Watch two quantities across the two runs. The fraction of the pool that fits inside the fixed gate\n",
        "budget shrinks as the pool grows, and `pack_ensemble` reports how much is included. The\n",
        "subspace stops being limited by sampling and starts being limited by `MAX_DIMENSION`, the largest\n",
        "matrix the dense classical solver here builds. At this scale, a production\n",
        "calculation would use a selected configuration interaction (selected-CI) solver.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c042",
      "metadata": {},
      "source": [
        "### Combine steps 1–4\n",
        "\n",
        "The following function calls the same stages as the walkthrough, in the same order.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 21,
      "id": "c043",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:36.497248Z",
          "iopub.status.busy": "2026-09-16T21:58:36.497123Z",
          "iopub.status.idle": "2026-09-16T21:58:36.503310Z",
          "shell.execute_reply": "2026-09-16T21:58:36.503037Z"
        }
      },
      "outputs": [],
      "source": [
        "def sqd_run(snt_file, n_protons, n_neutrons, name, exact=True):\n",
        "    \"\"\"The whole workflow for one nucleus.  Returns a record of every stage.\"\"\"\n",
        "    # -------------------------Step 1-------------------------\n",
        "    ms = read_snt(DATA / snt_file, n_protons, n_neutrons)\n",
        "    sp = m_scheme_states(ms)\n",
        "    inter = Interaction(ms, sp)\n",
        "    if sum(1 for s in sp if s.tz == -1) * 2 != len(sp):\n",
        "        raise ValueError(\n",
        "            f\"{name}: post-selection needs equal proton and neutron state counts\"\n",
        "        )\n",
        "    reference = reference_determinant(sp, inter, n_protons, n_neutrons)\n",
        "    e_ref = matrix_element(inter, reference, reference)\n",
        "\n",
        "    raw = excitation_pool(sp, reference)\n",
        "    ranked = rank_pool(\n",
        "        inter, reference, [op for op in raw if conserves_symmetry(sp, op)]\n",
        "    )\n",
        "    print(\n",
        "        f\"{name}: {len(sp)} qubits, {n_protons}p + {n_neutrons}n, A = {ms.mass_number}\"\n",
        "    )\n",
        "    print(\n",
        "        f\"  2p2h pool {len(raw)} raw -> {len(ranked)} symmetry-allowed; \"\n",
        "        f\"reference energy {e_ref:.6f} MeV\"\n",
        "    )\n",
        "\n",
        "    # -------------------------Step 2-------------------------\n",
        "    costs = excitation_costs(len(sp), ranked, costing_manager)\n",
        "    circuits, bins, isa = pack_to_budget(\n",
        "        len(sp),\n",
        "        reference,\n",
        "        ranked,\n",
        "        costs,\n",
        "        DEPTH_BUDGET,\n",
        "        N_CIRCUITS,\n",
        "        pass_manager,\n",
        "    )\n",
        "    packed = sum(len(b) for b in bins)\n",
        "    worst = max(two_qubit_depth(c) for c in isa)\n",
        "    worst_count = max(two_qubit_count(c) for c in isa)\n",
        "    print(\n",
        "        f\"  packed {packed} of {len(ranked)} excitations; worst circuit two-qubit depth \"\n",
        "        f\"{worst}, {worst_count} two-qubit gates\"\n",
        "    )\n",
        "\n",
        "    # -------------------------Step 3-------------------------\n",
        "    # a unique tag per run, so the jobs are findable later\n",
        "    bit_arrays = sample(isa, SHOTS, JOB_TAGS + [name])\n",
        "    matrix, probabilities, shots = pool_samples(bit_arrays, sp)\n",
        "    survival = float(\n",
        "        probabilities[\n",
        "            (matrix[:, len(sp) // 2 :].sum(axis=1) == n_protons)\n",
        "            & (matrix[:, : len(sp) // 2].sum(axis=1) == n_neutrons)\n",
        "        ].sum()\n",
        "    )\n",
        "    print(\n",
        "        f\"  {shots:,} shots -> {len(matrix):,} distinct bitstrings, \"\n",
        "        f\"{survival:.1%} of shots with the right nucleon numbers\"\n",
        "    )\n",
        "    if survival == 0.0:\n",
        "        raise RuntimeError(\n",
        "            f\"{name}: no shot carried the right nucleon numbers\"\n",
        "        )\n",
        "\n",
        "    # -------------------------Step 4-------------------------\n",
        "    result = recovery_loop(\n",
        "        inter,\n",
        "        sp,\n",
        "        matrix,\n",
        "        probabilities,\n",
        "        reference,\n",
        "        n_protons,\n",
        "        n_neutrons,\n",
        "        max_iterations=4,\n",
        "        max_dimension=MAX_DIMENSION,\n",
        "        seed=42,\n",
        "    )\n",
        "    energy = result[\"energy\"]\n",
        "\n",
        "    full_dim = count_basis(sp, n_protons, n_neutrons)  # cheap, even when huge\n",
        "    e_exact = None\n",
        "    if exact:\n",
        "        full = full_basis(sp, n_protons, n_neutrons)\n",
        "        if len(full) != full_dim:\n",
        "            raise AssertionError(\n",
        "                f\"{name}: counted {full_dim} determinants but enumerated \"\n",
        "                f\"{len(full)}\"\n",
        "            )\n",
        "        e_exact, _ = ground_state(inter, full)\n",
        "\n",
        "    print(f\"  reference {e_ref:11.6f} MeV     pooled SQD {energy:11.6f} MeV\")\n",
        "    if e_exact is not None:\n",
        "        print(\n",
        "            f\"  exact     {e_exact:11.6f} MeV  (dimension {full_dim})  ->  \"\n",
        "            f\"{100 * (energy - e_ref) / (e_exact - e_ref):.1f}% of the correlation energy\"\n",
        "        )\n",
        "        if energy < e_exact - 1e-7:\n",
        "            raise AssertionError(\n",
        "                f\"{name}: pooled SQD bound is below the exact energy\"\n",
        "            )\n",
        "    else:\n",
        "        print(\n",
        "            f\"  no exact reference: the symmetry-allowed basis is {full_dim:,} determinants\"\n",
        "        )\n",
        "        print(\n",
        "            f\"  the bound captures {energy - e_ref:.6f} MeV of correlation energy\"\n",
        "        )\n",
        "    print()\n",
        "\n",
        "    return dict(\n",
        "        name=name,\n",
        "        qubits=len(sp),\n",
        "        pool=len(ranked),\n",
        "        packed=packed,\n",
        "        two_qubit=worst,\n",
        "        two_qubit_gates=worst_count,\n",
        "        shots=shots,\n",
        "        distinct=len(matrix),\n",
        "        survival=survival,\n",
        "        dimension=len(result[\"basis\"]),\n",
        "        full_dim=full_dim,\n",
        "        e_ref=e_ref,\n",
        "        e_sqd=energy,\n",
        "        e_exact=e_exact,\n",
        "        history=result[\"history\"],\n",
        "        # the subspace and its eigenvector cannot be reconstructed from the summary --\n",
        "        # they depend on the sampled shots -- so keep them for the scaling analysis\n",
        "        interaction=inter,\n",
        "        states=sp,\n",
        "        reference=reference,\n",
        "        ranked=ranked,\n",
        "        basis=result[\"basis\"],\n",
        "        vector=result[\"vector\"],\n",
        "    )\n",
        "\n",
        "\n",
        "pretty = {\"20Ne\": \"$^{20}$Ne\", \"44Ti\": \"$^{44}$Ti\", \"48Cr\": \"$^{48}$Cr\"}\n",
        "\n",
        "small_scale = dict(\n",
        "    name=\"20Ne\",\n",
        "    qubits=len(sp_sd),\n",
        "    pool=len(ranked_sd),\n",
        "    packed=PACKED_SD,\n",
        "    two_qubit=worst_sd,\n",
        "    two_qubit_gates=worst_count_sd,\n",
        "    shots=shots_sd,\n",
        "    distinct=len(matrix_sd),\n",
        "    survival=shot_survival_sd,\n",
        "    dimension=len(result_sd[\"basis\"]),\n",
        "    full_dim=len(basis_exact_sd),\n",
        "    e_ref=E_REF_SD,\n",
        "    e_sqd=E_SQD_SD,\n",
        "    e_exact=E_EXACT_SD,\n",
        "    history=result_sd[\"history\"],\n",
        "    interaction=inter_sd,\n",
        "    states=sp_sd,\n",
        "    reference=occ_sd,\n",
        "    ranked=ranked_sd,\n",
        "    basis=result_sd[\"basis\"],\n",
        "    vector=result_sd[\"vector\"],\n",
        ")"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c044",
      "metadata": {},
      "source": [
        "### $^{44}\\mathrm{Ti}$: the same workflow on a 40-qubit register\n",
        "\n",
        "The $pf$ shell above $^{40}\\mathrm{Ca}$ has four orbitals per species and 20 magnetic substates each,\n",
        "so the register is 40 qubits. Two valence protons and two valence neutrons make $^{44}\\mathrm{Ti}$,\n",
        "with 4,000 symmetry-allowed determinants — about six times the $^{20}\\mathrm{Ne}$ basis, using\n",
        "40 qubits instead of 24.\n",
        "\n",
        "This is the larger of the two examples that the notebook can solve exactly, so you can compare\n",
        "the hardware result with an exact reference.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 22,
      "id": "c045",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:58:36.504562Z",
          "iopub.status.busy": "2026-09-16T21:58:36.504482Z",
          "iopub.status.idle": "2026-09-16T21:59:34.381745Z",
          "shell.execute_reply": "2026-09-16T21:59:34.381375Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "44Ti: 40 qubits, 2p + 2n, A = 44\n",
            "  2p2h pool 1602 raw -> 174 symmetry-allowed; reference energy -44.309387 MeV\n",
            "  packed 96 of 174 excitations; worst circuit two-qubit depth 272, 285 two-qubit gates\n",
            "job dap31a02fm4c73f67dp0: 16 circuits x 10,000 shots on ibm_phoenix\n",
            "  160,000 shots -> 48,170 distinct bitstrings, 18.6% of shots with the right nucleon numbers\n",
            "  iteration 1: 187 proton x 189 neutron halves -> dimension 3891, E = -47.849086 MeV\n",
            "  iteration 2: 190 proton x 190 neutron halves -> dimension 4000, E = -47.876666 MeV\n",
            "  iteration 3: 190 proton x 190 neutron halves -> dimension 4000, E = -47.876666 MeV\n",
            "  reference  -44.309387 MeV     pooled SQD  -47.876666 MeV\n",
            "  exact      -47.876666 MeV  (dimension 4000)  ->  100.0% of the correlation energy\n",
            "\n"
          ]
        }
      ],
      "source": [
        "large_scale_verified = sqd_run(\"gxpf1.snt\", 2, 2, \"44Ti\", exact=True)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c046",
      "metadata": {},
      "source": [
        "### $^{48}\\mathrm{Cr}$: beyond the tutorial's exact-diagonalization capacity\n",
        "\n",
        "Adding two protons and two neutrons uses the same 40-qubit register (`4, 4` for\n",
        "$^{48}\\mathrm{Cr}$) and increases the basis size by a factor of about 491, to 1,963,461 symmetry-allowed determinants. That\n",
        "matrix is far beyond anything this tutorial will build, so `exact=False`: there is no exact reference energy,\n",
        "only the variational bound and the reference determinant it improves on.\n",
        "\n",
        "Two things change at this scale, and both are visible in the printout. The pool grows to several\n",
        "hundred allowed excitations, so the fixed gate budget now covers a minority of it rather than all of\n",
        "it. Also, the product subspace the samples span is larger than `MAX_DIMENSION`, so the dense solver\n",
        "truncates it by sampled weight. The bound remains rigorous but can be less accurate than a bound\n",
        "computed from all the sampled configurations. A production calculation would retain the samples\n",
        "and use a solver that supports a larger subspace.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 23,
      "id": "c047",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T21:59:34.383195Z",
          "iopub.status.busy": "2026-09-16T21:59:34.383094Z",
          "iopub.status.idle": "2026-09-16T22:00:14.301187Z",
          "shell.execute_reply": "2026-09-16T22:00:14.300873Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "48Cr: 40 qubits, 4p + 4n, A = 48\n",
            "  2p2h pool 5536 raw -> 582 symmetry-allowed; reference energy -93.041237 MeV\n",
            "  packed 96 of 582 excitations; worst circuit two-qubit depth 224, 279 two-qubit gates\n",
            "job dap32a02fm4c73f67eog: 16 circuits x 10,000 shots on ibm_phoenix\n",
            "  160,000 shots -> 55,436 distinct bitstrings, 18.6% of shots with the right nucleon numbers\n",
            "  iteration 1: 223 proton x 223 neutron halves -> dimension 3977, E = -96.481598 MeV\n",
            "  iteration 2: 223 proton x 223 neutron halves -> dimension 3977, E = -96.481598 MeV\n",
            "  reference  -93.041237 MeV     pooled SQD  -96.481598 MeV\n",
            "  no exact reference: the symmetry-allowed basis is 1,963,461 determinants\n",
            "  the bound captures -3.440361 MeV of correlation energy\n",
            "\n"
          ]
        }
      ],
      "source": [
        "large_scale_unverified = sqd_run(\"gxpf1.snt\", 4, 4, \"48Cr\", exact=False)"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c048",
      "metadata": {},
      "source": [
        "### Evaluate a result without an exact reference\n",
        "\n",
        "The $^{48}\\mathrm{Cr}$ run has no exact reference within this tutorial. Use the existing samples\n",
        "to assess convergence and compare with the classical selection baseline, without additional QPU\n",
        "time or full-space diagonalization.\n",
        "\n",
        "**Is it converged?** Reorder the retained determinants by their weight in the converged eigenvector\n",
        "and the subspaces become *nested*, so diagonalizing the leading $d \\times d$ block for a ladder of $d$\n",
        "traces the bound's descent across two decades of subspace size. If it is still falling steeply at the\n",
        "largest $d$, the classical solver's dimension cap is the binding constraint and `MAX_DIMENSION` is the\n",
        "parameter to increase. If it has flattened, adding more of the retained determinants offers little improvement;\n",
        "further progress may require sampling additional configurations. The\n",
        "Hamiltonian is built once at full size and every rung is a principal block of it, so the whole\n",
        "sweep costs one matrix build rather than one per rung.\n",
        "\n",
        "**How does quantum sampling compare with classical selection?** Compare with a subspace of the\n",
        "same size chosen by the classical selection procedure: take the pool ranked by perturbation theory in score\n",
        "order, grow the product subspace to the same dimension, and diagonalize that instead. Both curves are\n",
        "rigorous upper bounds on the same Hamiltonian, so whichever sits lower at equal dimension picked the\n",
        "better determinants. This comparison decides whether hardware sampling improves the energy estimate\n",
        "relative to this classical baseline.\n",
        "\n",
        "This subspace is not selected for excited states. Configuration recovery steers the subspace\n",
        "using ground-state occupancies, so the higher eigenvalues are much further from converged than the\n",
        "lowest one, and the first excitation energy comes out well above the measured $2^+$. Reaching excited\n",
        "states properly needs a subspace selected for them.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 24,
      "id": "c049",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T22:00:14.302760Z",
          "iopub.status.busy": "2026-09-16T22:00:14.302676Z",
          "iopub.status.idle": "2026-09-16T22:00:32.065536Z",
          "shell.execute_reply": "2026-09-16T22:00:32.065231Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "48Cr: sweeping nested subspaces of the sampled basis (dimension 3977)\n",
            "48Cr: building the classically selected subspace at the same dimension\n",
            "  90% of the eigenvector norm sits on 107 determinants (5.4e-05 of the 1,963,461-determinant space)\n",
            "  99% of the eigenvector norm sits on 593 determinants (3.0e-04 of the 1,963,461-determinant space)\n",
            "  bound still falling -15.6 keV over the last doubling of dimension\n",
            "  sampled -96.481598 MeV vs classically selected -95.314510 MeV at a verified common dimension of 3,957\n",
            "  -> the sampled subspace is 1167 keV lower\n"
          ]
        }
      ],
      "source": [
        "def subspace_scaling(\n",
        "    inter, basis, vector, points=18, smallest=32, largest=None\n",
        "):\n",
        "    \"\"\"Nested Rayleigh-Ritz sweep: the lowest eigenvalue of the leading d x d block, for a ladder of d.\n",
        "\n",
        "    Reordering the basis by descending weight in the converged eigenvector makes every subspace in\n",
        "    the ladder a subset of the next, so the energies fall monotonically and each one is a valid\n",
        "    variational bound.  H is built once at full size; each rung is a principal block.\n",
        "    \"\"\"\n",
        "    order = np.argsort(-(np.abs(vector) ** 2))\n",
        "    ordered = [basis[i] for i in order]\n",
        "    weights = (np.abs(vector) ** 2)[order]\n",
        "    if (\n",
        "        largest is not None\n",
        "    ):  # cap the ladder so two subspaces end at a common dimension\n",
        "        ordered, weights = ordered[:largest], weights[:largest]\n",
        "    H = subspace_hamiltonian(inter, ordered)\n",
        "    dimensions = np.unique(\n",
        "        np.geomspace(smallest, len(ordered), points).astype(int)\n",
        "    )\n",
        "    rows = [\n",
        "        (\n",
        "            int(d),\n",
        "            float(\n",
        "                eigh(H[:d, :d], eigvals_only=True, subset_by_index=[0, 0])[0]\n",
        "            ),\n",
        "        )\n",
        "        for d in dimensions\n",
        "    ]\n",
        "    return rows, np.cumsum(weights)\n",
        "\n",
        "\n",
        "def classical_selection(\n",
        "    inter, sp, reference, ranked, target, n_protons, n_neutrons\n",
        "):\n",
        "    \"\"\"The subspace classical perturbative ranking would pick, grown to `target` dimension.\n",
        "\n",
        "    Same product construction as the sampled subspace, and the same truncation discipline -- half\n",
        "    configurations are offered to `grow_subspace` in order of importance and it takes as many as\n",
        "    fit.  The only difference from the sampled path is where the ordering comes from: PT2 score\n",
        "    here, measured sampling weight there.  So the comparison isolates *which determinants got\n",
        "    chosen* and nothing else.\n",
        "\n",
        "    Truncating by any other rule would not be a fair baseline.  Slicing an arbitrarily ordered\n",
        "    list, for instance, keeps determinants by accident rather than by importance and makes the\n",
        "    classical subspace look worse than classical selection really is.\n",
        "    \"\"\"\n",
        "    p_ref = tuple(i for i in reference if sp[i].tz == -1)\n",
        "    n_ref = tuple(i for i in reference if sp[i].tz == +1)\n",
        "    reached = {reference}\n",
        "    proton_order, neutron_order = [p_ref], [n_ref]\n",
        "    seen_p, seen_n = {p_ref}, {n_ref}\n",
        "    product_budget = 4 * target\n",
        "\n",
        "    for op, _, _ in ranked:  # ranked is already in descending PT2 score\n",
        "        h1, h2, v1, v2 = op\n",
        "        fresh = {\n",
        "            tuple(sorted(set(d) - {h1, h2} | {v1, v2}))\n",
        "            for d in reached\n",
        "            if {h1, h2} <= set(d) and not {v1, v2} & set(d)\n",
        "        }\n",
        "        reached |= fresh\n",
        "        for det in fresh:  # first appearance fixes a half's rank\n",
        "            half_p = tuple(i for i in det if sp[i].tz == -1)\n",
        "            half_n = tuple(i for i in det if sp[i].tz == +1)\n",
        "            if half_p not in seen_p:\n",
        "                seen_p.add(half_p)\n",
        "                proton_order.append(half_p)\n",
        "            if half_n not in seen_n:\n",
        "                seen_n.add(half_n)\n",
        "                neutron_order.append(half_n)\n",
        "        if len(proton_order) * len(neutron_order) > product_budget:\n",
        "            # Half-configuration products over-count the subspace, because only the\n",
        "            # symmetry-allowed ones survive `product_subspace`.  Stopping on the product\n",
        "            # count alone can therefore leave the basis far short of `target`, so check\n",
        "            # the dimension actually realized and widen the budget if it falls short.\n",
        "            trial, _, _ = grow_subspace(\n",
        "                sp,\n",
        "                [p_ref],\n",
        "                [n_ref],\n",
        "                proton_order,\n",
        "                neutron_order,\n",
        "                n_protons,\n",
        "                n_neutrons,\n",
        "                max_dimension=target,\n",
        "            )\n",
        "            if len(trial) >= target:\n",
        "                break\n",
        "            product_budget *= 2\n",
        "\n",
        "    basis, _, _ = grow_subspace(\n",
        "        sp,\n",
        "        [p_ref],\n",
        "        [n_ref],\n",
        "        proton_order,\n",
        "        neutron_order,\n",
        "        n_protons,\n",
        "        n_neutrons,\n",
        "        max_dimension=target,\n",
        "    )\n",
        "    return basis\n",
        "\n",
        "\n",
        "run = large_scale_unverified\n",
        "if \"basis\" not in run:\n",
        "    raise RuntimeError(\n",
        "        \"this cell needs the subspace and eigenvector that sqd_run now returns; \"\n",
        "        \"re-run the sqd_run definition and the 48Cr cell\"\n",
        "    )\n",
        "\n",
        "print(\n",
        "    f\"{run['name']}: sweeping nested subspaces of the sampled basis \"\n",
        "    f\"(dimension {run['dimension']})\"\n",
        ")\n",
        "sampled_rows, cumulative = subspace_scaling(\n",
        "    run[\"interaction\"], run[\"basis\"], run[\"vector\"]\n",
        ")\n",
        "\n",
        "print(\n",
        "    f\"{run['name']}: building the classically selected subspace at the same dimension\"\n",
        ")\n",
        "classical_basis = classical_selection(\n",
        "    run[\"interaction\"],\n",
        "    run[\"states\"],\n",
        "    run[\"reference\"],\n",
        "    run[\"ranked\"],\n",
        "    run[\"dimension\"],\n",
        "    4,\n",
        "    4,\n",
        ")\n",
        "# Both subspaces must be scored at the same dimension.  Symmetry filtering can still leave\n",
        "# the classical construction short of the target when the ranked pool runs out, so take the\n",
        "# dimension both actually reach, cap both ladders there, and verify they agree.\n",
        "common_dim = min(sampled_rows[-1][0], len(classical_basis))\n",
        "if common_dim < sampled_rows[-1][0]:\n",
        "    sampled_rows, _ = subspace_scaling(\n",
        "        run[\"interaction\"], run[\"basis\"], run[\"vector\"], largest=common_dim\n",
        "    )\n",
        "classical_rows, _ = subspace_scaling(\n",
        "    run[\"interaction\"],\n",
        "    classical_basis,\n",
        "    ground_state(run[\"interaction\"], classical_basis)[1],\n",
        "    largest=common_dim,\n",
        ")\n",
        "if sampled_rows[-1][0] != classical_rows[-1][0]:\n",
        "    raise RuntimeError(\n",
        "        f\"comparison dimensions differ: sampled {sampled_rows[-1][0]}, \"\n",
        "        f\"classical {classical_rows[-1][0]}\"\n",
        "    )\n",
        "\n",
        "advantage = sampled_rows[-1][1] - classical_rows[-1][1]\n",
        "direction = \"lower\" if advantage < 0 else \"higher\"\n",
        "verdict = \"beats\" if advantage < 0 else \"does not beat\"\n",
        "descent = next(\n",
        "    e for d, e in reversed(sampled_rows) if d <= sampled_rows[-1][0] / 2\n",
        ")\n",
        "for fraction in (0.90, 0.99):\n",
        "    count = int(np.searchsorted(cumulative, fraction) + 1)\n",
        "    print(\n",
        "        f\"  {fraction:.0%} of the eigenvector norm sits on {count} determinants \"\n",
        "        f\"({count / run['full_dim']:.1e} of the {run['full_dim']:,}-determinant space)\"\n",
        "    )\n",
        "print(\n",
        "    f\"  bound still falling {1000 * (sampled_rows[-1][1] - descent):+.1f} keV \"\n",
        "    f\"over the last doubling of dimension\"\n",
        ")\n",
        "print(\n",
        "    f\"  sampled {sampled_rows[-1][1]:.6f} MeV vs classically selected \"\n",
        "    f\"{classical_rows[-1][1]:.6f} MeV at a verified common dimension of \"\n",
        "    f\"{classical_rows[-1][0]:,}\"\n",
        ")\n",
        "print(\n",
        "    f\"  -> the sampled subspace is {abs(advantage) * 1000:.0f} keV {direction}\"\n",
        ")"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 25,
      "id": "c050",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T22:00:32.066904Z",
          "iopub.status.busy": "2026-09-16T22:00:32.066817Z",
          "iopub.status.idle": "2026-09-16T22:00:32.244569Z",
          "shell.execute_reply": "2026-09-16T22:00:32.244283Z"
        }
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/nuclear-sqd-pooled/extracted-outputs/c050-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "fig, axes = plt.subplots(1, 2, figsize=(11.2, 4.0), facecolor=SURFACE)\n",
        "\n",
        "# left: two nested convergence curves on the same axes\n",
        "ax = axes[0]\n",
        "ax.set_facecolor(SURFACE)\n",
        "ax.plot(\n",
        "    [d for d, _ in sampled_rows],\n",
        "    [e for _, e in sampled_rows],\n",
        "    \"-o\",\n",
        "    color=SERIES,\n",
        "    linewidth=2,\n",
        "    markersize=5,\n",
        "    markeredgecolor=SURFACE,\n",
        "    markeredgewidth=1,\n",
        "    zorder=4,\n",
        "    label=\"sampled on the QPU\",\n",
        ")\n",
        "ax.plot(\n",
        "    [d for d, _ in classical_rows],\n",
        "    [e for _, e in classical_rows],\n",
        "    \"--s\",\n",
        "    color=MUTED,\n",
        "    linewidth=1.6,\n",
        "    markersize=4,\n",
        "    markeredgecolor=SURFACE,\n",
        "    markeredgewidth=1,\n",
        "    zorder=3,\n",
        "    label=\"classically selected, same size\",\n",
        ")\n",
        "ax.axhline(run[\"e_ref\"], color=RULE, linestyle=\":\", linewidth=1.2, zorder=1)\n",
        "ax.annotate(\n",
        "    f\"reference determinant {run['e_ref']:.2f} MeV\".replace(\"-\", \"\\u2212\"),\n",
        "    xy=(sampled_rows[-1][0], run[\"e_ref\"]),\n",
        "    xytext=(-2, 4),\n",
        "    textcoords=\"offset points\",\n",
        "    ha=\"right\",\n",
        "    va=\"bottom\",\n",
        "    color=MUTED,\n",
        "    fontsize=8,\n",
        ")\n",
        "\n",
        "# mark the gap between the two curves at the largest dimension, not either curve alone\n",
        "edge = sampled_rows[-1][0]\n",
        "ax.plot(\n",
        "    [edge, edge],\n",
        "    [classical_rows[-1][1], sampled_rows[-1][1]],\n",
        "    \"-\",\n",
        "    color=SERIES,\n",
        "    linewidth=1.0,\n",
        "    alpha=0.7,\n",
        "    zorder=2,\n",
        ")\n",
        "ax.annotate(\n",
        "    f\"{abs(advantage) * 1000:.0f} keV {direction}\\nat equal dimension\",\n",
        "    xy=(edge, 0.5 * (classical_rows[-1][1] + sampled_rows[-1][1])),\n",
        "    xytext=(-8, 0),\n",
        "    textcoords=\"offset points\",\n",
        "    ha=\"right\",\n",
        "    va=\"center\",\n",
        "    color=SERIES,\n",
        "    fontsize=8.5,\n",
        ")\n",
        "\n",
        "ax.set_xscale(\"log\")\n",
        "ax.set_xlim(sampled_rows[0][0] * 0.75, edge * 1.5)\n",
        "ax.set_xlabel(\"subspace dimension\", color=MUTED)\n",
        "ax.set_ylabel(\"variational upper bound (MeV)\", color=MUTED)\n",
        "ax.set_title(\n",
        "    f\"{pretty[run['name']]}: the bound, and the subspace it {verdict}\",\n",
        "    color=INK,\n",
        "    fontsize=11,\n",
        "    loc=\"left\",\n",
        "    pad=10,\n",
        ")\n",
        "legend = ax.legend(frameon=False, fontsize=8.5, loc=\"lower left\")\n",
        "for text in legend.get_texts():\n",
        "    text.set_color(MUTED)\n",
        "\n",
        "# right: why a few thousand determinants can bound two million\n",
        "ax = axes[1]\n",
        "ax.set_facecolor(SURFACE)\n",
        "ranks = np.arange(1, len(cumulative) + 1)\n",
        "ax.plot(ranks, 100 * cumulative, \"-\", color=DEEP, linewidth=2, zorder=3)\n",
        "for fraction, style, label_y in ((0.90, \":\", 46), (0.99, \"--\", 24)):\n",
        "    count = int(np.searchsorted(cumulative, fraction) + 1)\n",
        "    ax.axvline(count, color=MUTED, linestyle=style, linewidth=1, zorder=1)\n",
        "    ax.annotate(\n",
        "        f\"{fraction:.0%} of the norm\\non {count} determinants\",\n",
        "        xy=(count, label_y),\n",
        "        xytext=(7, 0),\n",
        "        textcoords=\"offset points\",\n",
        "        ha=\"left\",\n",
        "        va=\"center\",\n",
        "        color=MUTED,\n",
        "        fontsize=8.5,\n",
        "    )\n",
        "ax.set_xscale(\"log\")\n",
        "ax.set_xlim(0.8, len(cumulative) * 2.6)\n",
        "ax.set_ylim(0, 104)\n",
        "ax.set_xlabel(\"determinants, ordered by weight\", color=MUTED)\n",
        "ax.set_ylabel(\"cumulative share of the eigenvector (%)\", color=MUTED)\n",
        "ax.set_title(\n",
        "    f\"Sparsity: {run['full_dim']:,} determinants in the sector\",\n",
        "    color=INK,\n",
        "    fontsize=11,\n",
        "    loc=\"left\",\n",
        "    pad=10,\n",
        ")\n",
        "\n",
        "for ax in axes:\n",
        "    ax.grid(axis=\"y\", color=RULE, alpha=0.55)\n",
        "    ax.tick_params(colors=MUTED, labelsize=8.5)\n",
        "    for side in (\"top\", \"right\"):\n",
        "        ax.spines[side].set_visible(False)\n",
        "    for side in (\"bottom\", \"left\"):\n",
        "        ax.spines[side].set_color(MUTED)\n",
        "fig.tight_layout()\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c051",
      "metadata": {},
      "source": [
        "### Compare the three runs\n",
        "\n",
        "Absolute energies are not comparable across different nuclei and different interactions, so focus on the fraction of the correlation energy recovered across runs, where an exact reference is available. Also compare circuit depth and the fraction of\n",
        "discarded shots.\n",
        "\n"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 26,
      "id": "c052",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T22:00:32.246109Z",
          "iopub.status.busy": "2026-09-16T22:00:32.246021Z",
          "iopub.status.idle": "2026-09-16T22:00:32.249319Z",
          "shell.execute_reply": "2026-09-16T22:00:32.249104Z"
        }
      },
      "outputs": [
        {
          "name": "stdout",
          "output_type": "stream",
          "text": [
            "   run  qubits       pool  2q depth  2q gates  shots kept     dim         of   % corr\n",
            "  20Ne      24      78/78       228       234      31.5%     640        640   100.0%\n",
            "  44Ti      40     96/174       272       285      18.6%    4000      4,000   100.0%\n",
            "  48Cr      40     96/582       224       279      18.6%    3977  1,963,461       --\n",
            "\n",
            "  20Ne   reference  -29.765549   pooled SQD  -40.472331   exact  -40.472331 MeV\n",
            "  44Ti   reference  -44.309387   pooled SQD  -47.876666   exact  -47.876666 MeV\n",
            "  48Cr   reference  -93.041237   pooled SQD  -96.481598   exact  unavailable MeV\n"
          ]
        }
      ],
      "source": [
        "runs = [small_scale, large_scale_verified, large_scale_unverified]\n",
        "\n",
        "print(\n",
        "    f\"{'run':>6}  {'qubits':>6}  {'pool':>9}  {'2q depth':>8}  {'2q gates':>8}  \"\n",
        "    f\"{'shots kept':>10}  {'dim':>6}  {'of':>9}  {'% corr':>7}\"\n",
        ")\n",
        "for r in runs:\n",
        "    fraction = (\n",
        "        \"--\"\n",
        "        if r[\"e_exact\"] is None\n",
        "        else f\"{100 * (r['e_sqd'] - r['e_ref']) / (r['e_exact'] - r['e_ref']):.1f}%\"\n",
        "    )\n",
        "    coverage = \"{}/{}\".format(r[\"packed\"], r[\"pool\"])\n",
        "    print(\n",
        "        f\"{r['name']:>6}  {r['qubits']:>6}  {coverage:>9}  \"\n",
        "        f\"{r['two_qubit']:>8}  {r['two_qubit_gates']:>8}  {r['survival']:>9.1%}  \"\n",
        "        f\"{r['dimension']:>6}  {(r['full_dim'] or 0):>9,}  {fraction:>7}\"\n",
        "    )\n",
        "\n",
        "print()\n",
        "for r in runs:\n",
        "    exact = (\n",
        "        f\"exact {r['e_exact']:11.6f}\"\n",
        "        if r[\"e_exact\"] is not None\n",
        "        else \"exact  unavailable\"\n",
        "    )\n",
        "    print(\n",
        "        f\"{r['name']:>6}   reference {r['e_ref']:11.6f}   pooled SQD {r['e_sqd']:11.6f}   {exact} MeV\"\n",
        "    )"
      ]
    },
    {
      "cell_type": "code",
      "execution_count": 27,
      "id": "c053",
      "metadata": {
        "execution": {
          "iopub.execute_input": "2026-09-16T22:00:32.250662Z",
          "iopub.status.busy": "2026-09-16T22:00:32.250570Z",
          "iopub.status.idle": "2026-09-16T22:00:32.345639Z",
          "shell.execute_reply": "2026-09-16T22:00:32.345350Z"
        }
      },
      "outputs": [
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/nuclear-sqd-pooled/extracted-outputs/c053-0.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        },
        {
          "data": {
            "text/plain": [
              "<Image src=\"/docs/images/tutorials/nuclear-sqd-pooled/extracted-outputs/c053-1.avif\" alt=\"Output of the previous code cell\" />"
            ]
          },
          "metadata": {},
          "output_type": "display_data"
        }
      ],
      "source": [
        "# Left: how much of the correlation energy was recovered, where the exact answer is known.\n",
        "# Right: the bound itself for the run that has nothing to score against.\n",
        "scored = [r for r in runs if r[\"e_exact\"] is not None]\n",
        "\n",
        "fig, ax = plt.subplots(figsize=(6.4, 3.9), facecolor=SURFACE)\n",
        "ax.set_facecolor(SURFACE)\n",
        "labels = [\n",
        "    f\"{pretty[r['name']]}\\n{r['qubits']} qubits\\n{r['full_dim']:,} determinants\"\n",
        "    for r in scored\n",
        "]\n",
        "fractions = [\n",
        "    100 * (r[\"e_sqd\"] - r[\"e_ref\"]) / (r[\"e_exact\"] - r[\"e_ref\"])\n",
        "    for r in scored\n",
        "]\n",
        "shades = [SERIES, DEEP, PURPLE]\n",
        "bars = ax.bar(\n",
        "    labels, fractions, width=0.46, color=shades[: len(scored)], zorder=3\n",
        ")\n",
        "for bar, fraction, r in zip(bars, fractions, scored):\n",
        "    ax.annotate(\n",
        "        f\"{fraction:.1f}%\",\n",
        "        xy=(bar.get_x() + bar.get_width() / 2, fraction),\n",
        "        xytext=(0, 5),\n",
        "        textcoords=\"offset points\",\n",
        "        ha=\"center\",\n",
        "        va=\"bottom\",\n",
        "        color=INK,\n",
        "        fontsize=10,\n",
        "    )\n",
        "    ax.annotate(\n",
        "        f\"dim {r['dimension']:,}\",\n",
        "        xy=(bar.get_x() + bar.get_width() / 2, 3),\n",
        "        ha=\"center\",\n",
        "        va=\"bottom\",\n",
        "        color=SURFACE,\n",
        "        fontsize=8.5,\n",
        "    )\n",
        "ax.axhline(100, color=MUTED, linestyle=\"--\", linewidth=1, zorder=1)\n",
        "ax.annotate(\n",
        "    \"exact diagonalization\",\n",
        "    xy=(-0.45, 100),\n",
        "    xytext=(0, 4),\n",
        "    textcoords=\"offset points\",\n",
        "    ha=\"left\",\n",
        "    va=\"bottom\",\n",
        "    color=MUTED,\n",
        "    fontsize=8.5,\n",
        ")\n",
        "ax.set_ylim(0, 118)\n",
        "ax.set_ylabel(\"correlation energy recovered (%)\", color=MUTED)\n",
        "ax.set_title(\n",
        "    f\"Where the exact answer is known ({backend.name})\",\n",
        "    color=INK,\n",
        "    fontsize=11,\n",
        "    loc=\"left\",\n",
        "    pad=10,\n",
        ")\n",
        "ax.grid(axis=\"y\", color=RULE, alpha=0.55)\n",
        "ax.tick_params(colors=MUTED, labelsize=8.5)\n",
        "for side in (\"top\", \"right\"):\n",
        "    ax.spines[side].set_visible(False)\n",
        "for side in (\"bottom\", \"left\"):\n",
        "    ax.spines[side].set_color(MUTED)\n",
        "fig.tight_layout()\n",
        "plt.show()\n",
        "\n",
        "# the same convergence view as the walkthrough, for the run with no exact reference\n",
        "convergence_plot(\n",
        "    large_scale_unverified[\"history\"],\n",
        "    large_scale_unverified[\"e_ref\"],\n",
        "    None,\n",
        "    f\"{pretty[large_scale_unverified['name']]}: \"\n",
        "    f\"{large_scale_unverified['full_dim']:,} determinants, no exact answer to score against\\n\"\n",
        "    f\"({backend.name}, {N_CIRCUITS} circuits x {SHOTS:,} shots)\",\n",
        "    colour=DEEP,\n",
        ")\n",
        "plt.show()"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c054",
      "metadata": {},
      "source": [
        "## Summary\n",
        "\n",
        "One workflow, unchanged apart from its inputs, ran on a QPU at three problem sizes: a 24-qubit\n",
        "problem you can check exactly, a 40-qubit problem you can still check exactly, and a 40-qubit problem\n",
        "with nearly two million basis states beyond this tutorial's exact-diagonalization capacity.\n",
        "\n",
        "The three runs illustrate the following points:\n",
        "\n",
        "* **The quantum step only has to propose determinants.** The circuit is fixed, seeded from\n",
        "  second-order perturbation theory, and never optimized. Nothing in the workflow needs its amplitudes\n",
        "  to be accurate, only its support to be useful. Classical diagonalization in the selected subspace gives a\n",
        "  variational upper bound, although the bound varies with the sampled configurations.\n",
        "* **Qubit excitations reduce circuit depth.** Because only the support matters, the fermionic excitation blocks\n",
        "  can be replaced by qubit excitations, whose cost does not grow with the distance between the\n",
        "  orbitals they connect. Step 2 measured the saving on the actual backend, which is the difference\n",
        "  between a circuit that fits comfortably inside coherence and one that does not.\n",
        "* **Configuration recovery reuses noisy samples.** Every shot with the wrong proton or neutron number is repaired\n",
        "  against the current occupancy estimate rather than discarded, and each repaired half-configuration\n",
        "  can add configurations to the subspace. Widening a variational subspace cannot raise its lowest\n",
        "  eigenvalue. This tutorial demonstrates configuration recovery using hardware samples.\n",
        "* **The binding constraint moves as you scale.** At 24 qubits the ansatz could reach the exact answer,\n",
        "  and only sampling stood in the way. At 40 qubits with four valence nucleons per species, the gate\n",
        "  budget covers a minority of the pool and the dense classical solver caps the subspace. Knowing which\n",
        "  of the three is limiting you is the practical skill this workflow teaches.\n",
        "\n",
        "## Next steps\n",
        "\n",
        "<Admonition type=\"note\" title=\"Recommendations\">\n",
        "  Explore these related resources:\n",
        "\n",
        "  * [Sample-based quantum diagonalization of a chemistry Hamiltonian](/docs/tutorials/sample-based-quantum-diagonalization): the same algorithm applied to electronic structure, using the SQD addon's selected-CI solver.\n",
        "  * [SQD addon documentation](/docs/api/qiskit-addon-sqd): post-selection, subsampling, and configuration-recovery utilities.\n",
        "  * [Quantum diagonalization algorithms](/learning/courses/quantum-diagonalization-algorithms): a full course on subspace diagonalization, including Krylov variants.\n",
        "  * [Introduction to transpilation](/docs/guides/transpile): the pass-manager options that matter when a circuit is dominated by two-qubit gates.\n",
        "  * [Execution modes](/docs/guides/execution-modes): explore batch mode for scheduling independent jobs.\n",
        "</Admonition>\n",
        "\n",
        "### Extensions to consider\n",
        "\n",
        "* **Replace the dense solver.** `MAX_DIMENSION` is the ceiling on everything at the $^{48}\\mathrm{Cr}$\n",
        "  scale, and `np.linalg.eigh` on a dense matrix is why. Building the same projected\n",
        "  Hamiltonian as a sparse matrix and using an iterative eigensolver such as\n",
        "  `scipy.sparse.linalg.eigsh`, or a Davidson or selected-CI solver designed for nuclear two-body\n",
        "  interactions, could support larger subspaces. The practical limit depends on matrix sparsity,\n",
        "  available memory, and solver convergence, and this tutorial does not benchmark that extension. The SQD addon's `qiskit_addon_sqd.fermion.solve_sci` is not a drop-in substitute: it wraps an\n",
        "  electronic-structure solver and expects one- and two-body integrals in that form, so the shared\n",
        "  proton $\\times$ neutron product structure is not sufficient on its own. Using it would mean mapping\n",
        "  the shell-model interaction of Equation (1) into those integrals and validating the result against the\n",
        "  exact energies this notebook already computes.\n",
        "* **Add batching and subsampling.** The published pooled SQD workflow diagonalizes several independent\n",
        "  subsamples per iteration and keeps the best. This tutorial uses one batch per iteration, which is\n",
        "  harmless for the variational bound but does not provide the variance information that indicates whether more shots\n",
        "  would help.\n",
        "* **Excited states and other sectors.** The higher eigenvalues of each subspace Hamiltonian are upper\n",
        "  bounds on excited states in the same symmetry sector, and running at $M_J \\neq 0$ reaches other\n",
        "  sectors. The $2^+$ check in Step 1 is already half of this calculation.\n",
        "* **A cross-shell model space.** Parity is automatically satisfied inside a single major shell, which\n",
        "  is why it does no work here. An $sd$-$pf$ space mixes $\\ell$ parities, making parity a genuine\n",
        "  fourth constraint, one that neither SQD's Hamming-weight repair nor the product\n",
        "  construction would catch on its own.\n",
        "* **Odd-mass nuclei.** `reference_determinant` requires an even valence count in each species,\n",
        "  because a time-reversed paired filling is what forces $M_J = 0$. An odd nucleus needs a\n",
        "  half-integer $M_J$ target and an unpaired reference.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c055",
      "metadata": {},
      "source": [
        "## Appendix\n",
        "\n",
        "This section explains the reasoning behind the helpers introduced in the [Setup](#setup) section.\n",
        "\n",
        "### Why the mass-dependence rescaling is not optional\n",
        "\n",
        "Empirical shell-model interactions are fitted at one mass and applied across a chain of isotopes, with\n",
        "the two-body matrix elements scaled as $(A/A_{\\mathrm{ref}})^{p}$. Both interaction files carry\n",
        "$p = -0.3$, with $A_{\\mathrm{ref}} = 18$ for the USD family and $42$ for GXPF1. On the two-body header\n",
        "line of a `.snt` file, those two numbers sit where an oscillator frequency and a core energy would\n",
        "plausibly go, which makes them easy to misread; reading the exponent as a constant core energy adds a\n",
        "spurious offset to every diagonal element *and* drops the rescaling, changing the correlation energy by\n",
        "a few percent. The symmetry check in Step 1 does not by itself verify the energy scale. Comparing\n",
        "the $2^+$ excitation energy, measured in MeV, with experiment provides an additional check on the\n",
        "mass-dependent rescaling. An excitation energy is a difference between levels, so it does not\n",
        "detect a constant offset applied to all energies.\n",
        "\n",
        "### Why the reference is found by search rather than by filling\n",
        "\n",
        "The obvious reference is the determinant that fills the lowest single-particle energies. It is not the\n",
        "lowest-energy determinant, because the diagonal of Equation (1) includes the two-body term\n",
        "$\\sum_{i<j} \\langle ij \\| ij \\rangle$, and the pairing interaction strongly prefers occupying\n",
        "time-reversed $(+m_j, -m_j)$ partners in the *largest* available $|m_j|$. In the $sd$ shell that is the\n",
        "difference between the $m_j = \\pm 1/2$ pair and the $m_j = \\pm 5/2$ pair of $0d_{5/2}$, and it is worth\n",
        "about 1 MeV; in the $pf$ shell, it is worth closer to 2. Because the reference energy defines the zero of the\n",
        "\"correlation energy recovered\" metric, a poor choice inflates that metric and gives a less accurate\n",
        "starting point.\n",
        "\n",
        "Restricting to paired fillings makes exhaustive search inexpensive, with $\\binom{n_{\\mathrm{pairs}}}{k}$ candidates per\n",
        "species (a few thousand at most), and ensures $M_J = 0$. In every case in this\n",
        "tutorial that can be checked against a full enumeration, the search returns the global lowest-diagonal\n",
        "determinant, which is also the single largest component of the exact ground state.\n",
        "\n",
        "### Why the first-order amplitude, not the exact two-level angle\n",
        "\n",
        "Diagonalizing the $2 \\times 2$ Hamiltonian in the space\n",
        "$\\{|\\Phi_{\\mathrm{ref}}\\rangle, |\\alpha\\rangle\\}$ gives the mixing angle\n",
        "$\\theta_{\\mathrm{exact}} = \\tfrac{1}{2}\\arctan(2V/\\Delta)$; it might be tempting to call that the correct\n",
        "choice for an *isolated* pair of levels. In this ansatz, several dozen excitation blocks act\n",
        "in sequence on the same reference, so optimizing each block separately does not necessarily\n",
        "optimize the composite circuit.\n",
        "\n",
        "The circuit’s role determines the choice of angle. Since\n",
        "$|\\tfrac{1}{2}\\arctan(2x)| \\le |x|$ for every real $x$, the exact angle is always *smaller* in\n",
        "magnitude than the first-order amplitude $t = V/\\Delta$, and therefore always leaves *more* amplitude\n",
        "on the reference determinant. A circuit that keeps more amplitude on the reference returns the\n",
        "reference more often and distinct excited determinants less often. For pooled SQD, the useful output of a shot\n",
        "is a determinant the classical step has not seen yet, which motivates using the larger angle in this tutorial. Neither angle needs to be accurate, because the classical diagonalization discards the circuit's\n",
        "amplitudes entirely and re-derives its own.\n",
        "\n",
        "### Why pooled SQD can use qubit excitations\n",
        "\n",
        "The fermionic excitation $T = a_{v_1}^\\dagger a_{v_2}^\\dagger a_{h_2} a_{h_1}$ maps under\n",
        "Jordan-Wigner to eight Pauli strings, each carrying $Z$ operators on every qubit between the outermost\n",
        "indices. Those strings encode the fermionic sign, and their cost grows with the span, which for a\n",
        "proton-neutron excitation is the whole register.\n",
        "\n",
        "Deleting them gives the qubit-excitation operator of Yordanov et al. [\\[5\\]](#references). It is a\n",
        "different operator: the state it prepares differs from the fermionic one in the *signs* of\n",
        "its amplitudes, and the two sampling distributions can differ substantially. What it does not change\n",
        "is which determinants have nonzero amplitude, because each block still rotates within the same\n",
        "two-dimensional space $\\{|d\\rangle, |d'\\rangle\\}$ for every determinant $d$ it acts on, and it still\n",
        "conserves both nucleon numbers, $M_J$, and parity exactly. The reachable set of determinants is\n",
        "therefore identical, and the reachable set is the only thing pooled SQD uses; the classical\n",
        "diagonalization assigns its own amplitudes regardless. Step 2 verifies the identical-support claim on\n",
        "a real operator from the pool and measures what the substitution saves.\n",
        "\n",
        "The limitation is that the sampling *weights* differ, so the two constructions will not discover\n",
        "determinants in the same order at finite shot count. Since the ranking that decides which excitations\n",
        "enter the circuits is classical and unchanged, and the classical step re-weights everything anyway,\n",
        "the difference in sampling weights is a tradeoff for reduced circuit depth.\n",
        "\n",
        "### Why $M_J$ belongs to the product stage\n",
        "\n",
        "Post-selection and configuration recovery both act on Hamming weights: the number of protons in one\n",
        "half of the register, and the number of neutrons in the other. $M_J = M_p + M_n$ is not of that form. It is a property of a proton configuration *paired with* a neutron configuration. A shot whose proton\n",
        "half and neutron half each carry the right nucleon number contains two usable half-configurations even\n",
        "when their $M_J$ values do not cancel, because the proton half at $M_p = +1$ is perfectly good once it\n",
        "is paired with a neutron half at $M_n = -1$. Filtering whole shots on total $M_J$ throws both halves\n",
        "away, and imposing $M_J$ on the recombined products keeps them. The same argument explains why\n",
        "`recover_configurations` needs no notion of $M_J$ to be useful in this case.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "id": "c056",
      "metadata": {},
      "source": [
        "## References\n",
        "\n",
        "1. J. Robledo-Moreno, M. Motta, H. Haas, et al., \"Chemistry beyond the scale of exact diagonalization\n",
        "   on a quantum-centric supercomputer\", *Science Advances* **11**, eadu9991 (2025).\n",
        "   [arXiv:2405.05068](https://arxiv.org/abs/2405.05068)\n",
        "2. B. A. Brown and W. A. Richter, \"New USD Hamiltonians for the sd shell\", *Physical Review C* **74**,\n",
        "   034315 (2006). The embedded `usda.snt` file carries the USDA parameters as tabulated by\n",
        "   W. A. Richter, S. Mkhize and B. A. Brown, \"sd-shell observables for the USDA and USDB Hamiltonians\",\n",
        "   *Physical Review C* **78**, 064302 (2008).\n",
        "3. M. Honma, T. Otsuka, B. A. Brown and T. Mizusaki, \"Effective interaction for pf-shell nuclei\",\n",
        "   *Physical Review C* **65**, 061301(R) (2002).\n",
        "4. B. Huron, J. P. Malrieu and P. Rancurel, \"Iterative perturbation calculations of ground and excited\n",
        "   state energies from multiconfigurational zeroth-order wavefunctions\", *The Journal of Chemical\n",
        "   Physics* **58**, 5745 (1973).\n",
        "5. Y. S. Yordanov, D. R. M. Arvidsson-Shukur and C. H. W. Barnes, \"Efficient quantum circuits for\n",
        "   quantum computational chemistry\", *Physical Review A* **102**, 062612 (2020).\n",
        "6. National Nuclear Data Center, [Evaluated Nuclear Structure Data File](https://www.nndc.bnl.gov/ensdf/),\n",
        "   Brookhaven National Laboratory. Source of the measured $2^+$ excitation energies quoted in Step 1.\n",
        "\n"
      ]
    },
    {
      "cell_type": "markdown",
      "metadata": {},
      "id": "a1b8767d",
      "source": "© IBM Corp., 2017-2026"
    }
  ],
  "metadata": {
    "description": "Sample nuclear Slater determinants on a QPU, repair them against exact symmetries, and diagonalize the shell-model Hamiltonian classically.",
    "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"
    },
    "title": "Sample-based quantum diagonalization of a nuclear Hamiltonian"
  },
  "nbformat": 4,
  "nbformat_minor": 5
}