Building Hamiltonians¶
A little history first¶
In 1788, Joseph-Louis Lagrange rewrote Newton's mechanics around a single
function, the Lagrangian L = T - V (kinetic energy minus potential
energy), from which every equation of motion for a system falls out by a
single recipe. Fifty years later, William Rowan Hamilton reorganized the
same physics around a different function: the Hamiltonian H = T + V,
the system's total energy, expressed not in terms of position and
velocity but position and momentum. Hamilton's version turned out to be
the one that survived into the 20th century: when quantum mechanics was
built, it was Hamilton's H — not Lagrange's L — that became an
operator, and the Schrödinger equation H|psi> = E|psi> is exactly
Hamilton's old energy function, now acting on a quantum state instead of
a point in phase space.
So a Hamiltonian, in this library, is always the same idea: a Hermitian matrix whose eigenvalues are the possible energies of a system, and whose eigenvectors are the states that have those energies. Building one from scratch means writing down that matrix — usually not by typing out every entry, but by describing it as a sum of simple pieces.
Step 1. The simplest possible Hamiltonian¶
import numpy as np
import dense_evolution as de
terms = [(1.0, {0: 'Z'})]
H = de.pauli_hamiltonian_to_matrix(terms, 1)
print(H.real)
print(np.linalg.eigvalsh(H))
pauli_hamiltonian_to_matrix takes a list of (coefficient, pauli_dict)
terms — here just one term, coefficient 1.0, acting as Z on qubit 0 —
and builds the full matrix. A single Z is already a genuine Hamiltonian:
its eigenvalues (-1, +1) are the two energies of a qubit that prefers
to point up or down, exactly the textbook two-level system every
quantum-mechanics course starts with.
Step 2. A sum of terms¶
terms = [(1.0, {0: 'X', 1: 'X'}), (1.0, {0: 'Y', 1: 'Y'}), (1.0, {0: 'Z', 1: 'Z'})]
H = de.pauli_hamiltonian_to_matrix(terms, 2)
print(np.linalg.eigvalsh(H))
This is the two-qubit Heisenberg exchange Hamiltonian H = X0X1 + Y0Y1 +
Z0Z1 — three terms instead of one, added together the same way any real
Hamiltonian is: term by term. Its ground energy -3 corresponds to the
singlet state, three units below the triplet's +1 — the real reason two
coupled spins prefer to anti-align, derived here from four numbers, not
asserted.
Every Hamiltonian in this library, however large, is built the same way:
decide which Pauli terms belong, list their coefficients, and let
pauli_hamiltonian_to_matrix (or, for a system too large to hold as a
dense matrix, pauli_sum_matvec for the
matrix-free H @ vector version) do the assembly.
Step 3. Fermionic systems: Jordan-Wigner¶
Not every system is naturally described by qubits — the Heisenberg Hamiltonian above happens to already be a spin system. Electrons, and the Majorana fermions used in models like SYK (Step 5), are a different kind of object, and need one extra step before they become a Pauli sum: Jordan-Wigner mapping.
from dense_evolution.fermions import majorana_pauli_terms
coeff1, term1 = majorana_pauli_terms(1, 2)
coeff2, term2 = majorana_pauli_terms(2, 2)
print(coeff1, term1)
print(coeff2, term2)
majorana_pauli_terms(mode_index, n_qubits) gives the Pauli representation
of one Majorana fermion mode — chi_1 = X_0, chi_2 = Y_0 here, with a
Z-string prepended for higher-indexed modes so that different modes
anticommute correctly ({chi_a, chi_b} = 2*delta_ab*I, verified against
the real matrices in Fermions). Multiply several of
these together (see dashboard_core.wormhole._multiply_pauli_dicts for a
real, reusable Pauli-string-multiplication helper) and feed the result to
pauli_hamiltonian_to_matrix, and you can build any Majorana-operator
Hamiltonian the same way Step 2 built a spin Hamiltonian.
Step 4. Real molecules: native Hartree-Fock¶
For an actual molecule, the Hamiltonian's terms aren't something you'd
want to write by hand — they come from solving the electronic structure
problem first. dense_evolution.native_hf does this from scratch (real
Gaussian-orbital integrals via the Obara-Saika recursion, then a Roothaan-Hall
self-consistent-field loop), and hands the converged result to
build_qubit_hamiltonian:
from dense_evolution.native_hf.bridge import build_qubit_hamiltonian
hamiltonian, n_qubits, hf_result = build_qubit_hamiltonian(
atomic_numbers=[1, 1],
geometry_angstrom=np.array([[0.0, 0.0, 0.0], [0.74, 0.0, 0.0]]),
n_electrons=2,
basis_name="sto-3g",
)
print(n_qubits, round(float(hf_result.total_energy), 6), hf_result.converged)
Four qubits, a converged SCF energy of -1.116759 Hartree for H2 at its
equilibrium bond length (0.74 Angstrom) — the same real molecule the
VQE example later optimizes over. See
Native Hartree-Fock for the full parameter reference
(active-space selection, basis sets, the HFResult fields) and
Dashboard Core — Hamiltonians for
the molecule catalog this engine backs.
Step 5. Something genuinely custom: an SYK Hamiltonian¶
Steps 1-4 all used Hamiltonians with a name (Heisenberg, a molecule's
electronic Hamiltonian). Nothing stops you from inventing your own,
combining the exact same two primitives — majorana_pauli_terms and
pauli_hamiltonian_to_matrix. Here is a real four-body Sachdev-Ye-Kitaev
(SYK) Hamiltonian, H = sum_{i<j<k<l} J_ijkl * chi_i*chi_j*chi_k*chi_l,
built from every 4-Majorana combination on 8 modes with random Gaussian
couplings:
import itertools
import sys
sys.path.insert(0, "tools/dashboard")
from core.wormhole import _multiply_pauli_dicts
def build_syk4_terms(n_majorana, J, seed):
n_qubits = n_majorana // 2
rng = np.random.default_rng(seed)
sigma = np.sqrt(6.0 * J**2 / n_majorana**3)
terms = []
for quad in itertools.combinations(range(1, n_majorana + 1), 4):
dicts = [majorana_pauli_terms(m, n_qubits)[1] for m in quad]
phase, merged = _multiply_pauli_dicts(dicts)
terms.append((rng.normal(0.0, sigma) * phase, merged))
return n_qubits, terms
n_qubits, terms = build_syk4_terms(n_majorana=8, J=5.0, seed=61)
H = de.pauli_hamiltonian_to_matrix(terms, n_qubits)
print(n_qubits, len(terms), np.allclose(H, H.conj().T))
Four qubits (8 Majorana modes, two per qubit), 70 terms (every
4-combination of 8 modes, C(8,4)=70), and a genuinely random-looking
Hamiltonian that is nonetheless exactly Hermitian by construction — the
same pattern the Chunk and wormhole-teleportation
experiments build on internally, just assembled here from first
principles instead of hidden behind a ready-made function.
Details¶
Coupling two independent Hamiltonians: the Klein-factor trap¶
A natural next step past Step 5 is coupling two Hamiltonians together —
e.g. a "left" and "right" copy of the same SYK model, as in
traversable-wormhole teleportation protocols. This looks like it should
be as simple as building each side's Majoranas independently and shifting
one side's qubit indices, but it silently breaks: a fermionic number
operator like n = c^dagger c, built from one Majorana on each side
(c = (chi_L + i*chi_R)/2), needs chi_L and chi_R to anticommute.
Two independently Jordan-Wigner-mapped registers, tensored together,
automatically commute instead (they act on disjoint qubits) — and under
that convention n collapses to the trivial constant 0.5 * I, not a
real 0-or-1-valued operator (verified directly: the resulting coupling
unitary fails to be unitary, ||U^dagger U - I|| ~ 55, regardless of
which sign convention is tried for the cross term). This is the standard
"Klein factor" problem in the SYK/wormhole literature — coupling two
independently-built fermionic registers needs an explicit parity-string
correction, not a sign tweak. If you're building a two-copy coupled
Hamiltonian, look up how the source paper you're reproducing handles this
explicitly; don't assume independent registers can simply be tensored
together.
When the matrix is too large to build¶
Every example above builds a dense 2**n_qubits matrix. Past roughly
20-25 qubits this stops being practical — use
pauli_sum_matvec (matrix-free H @ vector,
the same technique behind ground_state_energy_sparse's
scipy.sparse.linalg.eigsh path for large molecules) or
circuit_to_energy_fn if you need the energy as a
differentiable function of circuit parameters rather than the bare
matrix.
See Also¶
- Observables —
pauli_hamiltonian_to_matrix,pauli_sum_matvec. - Fermions —
majorana_pauli_terms, the Jordan-Wigner mapping used in Steps 3 and 5. - Native Hartree-Fock — the full molecular-Hamiltonian pipeline Step 4 only introduces.
- Autodiff —
circuit_to_energy_fn, for using a Hamiltonian built here inside a differentiable VQE loop.