Source code for vibeqc.solvers._common
"""Common interfaces and data structures for non-mean-field solvers.
All solvers in this package share a common protocol:
result = solver.solve(hamiltonian, nelec, norb, options) -> SolverResult
A Hamiltonian carries one- and two-electron integrals in an orthonormal
(spatial-orbital) basis. The orbital provider is responsible for
delivering the transformation from AO → MO; the solvers themselves are
orbital-basis-agnostic.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Optional, Protocol
import numpy as np
# ── Hamiltonian ───────────────────────────────────────────────────────────
[docs]
@dataclass
class Hamiltonian:
"""One- and two-electron integrals in an orthonormal spatial-orbital basis.
Attributes
----------
h1e : (norb, norb) ndarray
One-electron (core) Hamiltonian h_{pq}.
h2e : (norb, norb, norb, norb) ndarray
Two-electron integrals in **physicist's** notation:
g_{pqrs} = (pr|qs) = ∫∫ φ_p*(r₁) φ_q*(r₂) r₁₂⁻¹ φ_r(r₁) φ_s(r₂).
Stored as a 4-index tensor with anti-symmetry NOT folded in —
the solvers apply Slater–Condon rules that need the full tensor.
nuclear_repulsion : float
Nuclear-nuclear repulsion energy E_nuc (Hartree).
norb : int
Number of spatial orbitals.
nelec : int
Number of electrons.
ms2 : int
2 × S_z = n_alpha − n_beta. 0 for closed-shell singlets.
description : str
Human-readable label for logging / diagnostics.
"""
h1e: np.ndarray
h2e: np.ndarray
nuclear_repulsion: float = 0.0
norb: int = 0
nelec: int = 0
ms2: int = 0
description: str = ""
def __post_init__(self) -> None:
if self.norb == 0 and self.h1e is not None:
self.norb = self.h1e.shape[0]
# ── Solver options ────────────────────────────────────────────────────────
@dataclass
class SolverOptions:
"""Base options shared by all non-mean-field solvers.
Sub-classes add method-specific controls.
"""
#: Target number of determinants / bond-dimension / constraint set size.
#: Interpretation is method-specific.
target_size: int = 100
#: Energy convergence threshold (Hartree).
conv_tol_energy: float = 1e-6
#: Maximum number of macro-iterations.
max_iter: int = 50
#: Verbosity: 0 = silent, 1 = per-iteration, 2 = debug.
verbose: int = 0
#: Random seed for reproducibility.
random_seed: int = 42
# ── Solver result ─────────────────────────────────────────────────────────
[docs]
@dataclass
class SolverResult:
"""Common result container for all non-mean-field solvers.
Fields marked ``method-specific`` are populated by individual backends
and may be ``None``.
"""
#: Total energy (Hartree). Always includes nuclear repulsion.
energy: float
#: Method name, e.g. ``"selected_ci"``, ``"dmrg"``, ``"v2rdm"``.
method: str
#: Whether the solver considers itself converged.
converged: bool = True
#: Number of macro-iterations / sweeps taken.
n_iter: int = 0
#: Energy trace per macro-iteration (if tracked).
energy_trace: list[float] = field(default_factory=list)
# ── method-specific ──
#: Wavefunction coefficients (determinant × configuration), CI only.
ci_coeffs: Optional[np.ndarray] = None
#: Determinant / configuration labels, CI only.
ci_labels: Optional[list[tuple[int, ...]]] = None
#: 1-RDM (norb, norb), if computed by the solver.
rdm1: Optional[np.ndarray] = None
#: 2-RDM (norb, norb, norb, norb), if computed by the solver.
rdm2: Optional[np.ndarray] = None
#: Truncation error / discarded weight, DMRG.
truncation_error: Optional[float] = None
#: Bond dimension, DMRG.
bond_dim: Optional[int] = None
#: N-representability residual norm, v2RDM.
constraint_residual: Optional[float] = None
#: Perturbative correction, Selected-CI.
pt2_correction: Optional[float] = None
#: Transcorrelated diagnostics dict.
tc_diagnostics: Optional[dict] = None
@property
def energy_total(self) -> float:
"""Alias for ``energy``."""
return self.energy
# ── Solver protocol ───────────────────────────────────────────────────────
class SolverProtocol(Protocol):
"""Structural protocol for a non-mean-field solver."""
def solve(
self,
hamiltonian: Hamiltonian,
options: SolverOptions | None = None,
) -> SolverResult: ...
# ── Utility ───────────────────────────────────────────────────────────────
def _physicist_to_chemist(h2e: np.ndarray) -> np.ndarray:
"""Convert physicist's ERI g_{pqrs} = (pr|qs) → chemist's (pq|rs).
(pq|rs) = g_{prqs}
"""
return h2e.transpose(0, 2, 1, 3)
def _chemist_to_physicist(eri_chem: np.ndarray) -> np.ndarray:
"""Convert chemist's (pq|rs) → physicist's g_{pqrs} = (pr|qs)."""
return eri_chem.transpose(0, 2, 1, 3)
def _antisymmetrize_h2e(h2e_phys: np.ndarray) -> np.ndarray:
"""Build the antisymmetrized two-electron tensor <pq||rs>.
<pq||rs> = g_{pqrs} - g_{pqsr}
"""
return h2e_phys - h2e_phys.transpose(0, 1, 3, 2)