"""Determinant and configuration-state-function data structures.
Representation of Slater determinants as tuples of occupied orbital
indices, with helper functions for building the determinant space,
computing excitation ranks, and generating connected determinants.
"""
from __future__ import annotations
from typing import Iterator, Optional
import numpy as np
# Type alias: a determinant is a sorted tuple of integer orbital indices.
Det = tuple[int, ...]
# (alpha_occ, beta_occ) for unrestricted determinants.
SpinDet = tuple[Det, Det]
def determinant_string(occ: Det) -> str:
"""Render a determinant as a binary occupation string (0 = empty, 1 = occ)."""
norb = max(occ) + 1 if occ else 0
s = ["0"] * norb
for i in occ:
s[i] = "1"
return "".join(s)
def excitation_rank(occ_a: Det, occ_b: Det) -> int:
"""Number of orbital replacements to go from ``occ_a`` to ``occ_b``.
Both must have the same length.
"""
if len(occ_a) != len(occ_b):
raise ValueError("Determinants must have the same electron count")
return len(set(occ_a) - set(occ_b))
def is_connected(occ_a: Det, occ_b: Det, max_rank: int = 2) -> bool:
"""True iff ``occ_a`` and ``occ_b`` differ by ≤ ``max_rank`` excitations.
In standard ab initio Hamiltonians, only single and double excitations
have non-zero matrix elements.
"""
return excitation_rank(occ_a, occ_b) <= max_rank
[docs]
def generate_determinants(
norb: int,
nalpha: int,
nbeta: int,
) -> list[SpinDet]:
"""Generate all determinants for ``nalpha`` α + ``nbeta`` β electrons in ``norb`` orbitals.
Returns a list of ``(alpha_occ, beta_occ)`` tuples in lexicographic order.
This is the complete FCI determinant space — use with caution (``C(norb, nalpha) × C(norb, nbeta)``).
"""
from itertools import combinations
alpha_dets = [tuple(c) for c in combinations(range(norb), nalpha)]
beta_dets = [tuple(c) for c in combinations(range(norb), nbeta)]
return [(a, b) for a in alpha_dets for b in beta_dets]
def generate_closed_shell_determinants(
norb: int,
nocc: int,
) -> list[Det]:
"""Generate all closed-shell (spin-restricted) determinants.
Each determinant is represented as a tuple of spatial-orbital indices
that are doubly occupied.
"""
from itertools import combinations
return [tuple(c) for c in combinations(range(norb), nocc)]
def generate_singles(
ref: Det,
norb: int,
) -> list[Det]:
"""All single excitations from ``ref`` (one electron moved to a virtual)."""
dets: list[Det] = []
occ_set = set(ref)
vir_set = set(range(norb)) - occ_set
for i in occ_set:
new_occ = list(ref)
new_occ.remove(i)
for a in sorted(vir_set):
dets.append(tuple(sorted(new_occ + [a])))
return dets
def generate_doubles(
ref: Det,
norb: int,
) -> list[Det]:
"""All double excitations from ``ref`` (two electrons moved)."""
dets: list[Det] = []
occ_set = set(ref)
occ_list = sorted(occ_set)
vir_set = set(range(norb)) - occ_set
vir_list = sorted(vir_set)
for idx_i, i in enumerate(occ_list):
for idx_j in range(idx_i + 1, len(occ_list)):
j = occ_list[idx_j]
for idx_a, a in enumerate(vir_list):
for idx_b in range(idx_a + 1, len(vir_list)):
b = vir_list[idx_b]
new_occ = sorted(set(ref) - {i, j} | {a, b})
dets.append(tuple(new_occ))
return dets
def reference_determinant(
nalpha: int,
nbeta: int,
*,
spin_restricted: bool = True,
) -> SpinDet:
"""The aufbau reference determinant: lowest-energy orbitals doubly occupied.
For spin-restricted: ``(0,1,...,nalpha-1), (0,1,...,nbeta-1)``.
"""
if spin_restricted and nalpha == nbeta:
occ = tuple(range(nalpha))
return (occ, occ)
else:
return (tuple(range(nalpha)), tuple(range(nbeta)))