Initial commit. Stage one work done.

This commit is contained in:
2026-07-23 11:35:25 -05:00
parent a7be272917
commit dd3b0f9c45
37 changed files with 3429 additions and 171 deletions

67
tests/libxc_reference.py Normal file
View File

@@ -0,0 +1,67 @@
"""Loaders for libxc's vendored testsuite data (golden reference).
Input files (libxc/testsuite/input/<system>): first line npoints, then
9 columns: rhoa rhob sigmaaa sigmaab sigmabb lapla laplb taua taub.
Regression files (libxc/testsuite/regression/<family>/<name>.<system>.<spin>.<order>.bz2):
first line "func_id npoints order", then a column-header line, then values.
"""
import bz2
from pathlib import Path
import numpy as np
LIBXC_ROOT = Path(__file__).resolve().parent.parent / "libxc"
INPUT_DIR = LIBXC_ROOT / "testsuite" / "input"
REGRESSION_DIR = LIBXC_ROOT / "testsuite" / "regression"
SYSTEMS = ("BrOH", "BrOH+", "H", "Li")
def load_input(system: str) -> dict[str, np.ndarray]:
"""Return the 9-column grid data for one test system."""
raw = np.loadtxt(INPUT_DIR / system, skiprows=1)
return {
"rho": raw[:, 0:2], # (N, 2): rhoa, rhob
"sigma": raw[:, 2:5], # (N, 3): sigmaaa, sigmaab, sigmabb
"lapl": raw[:, 5:7],
"tau": raw[:, 7:9],
}
def unpolarize(inp: dict[str, np.ndarray]) -> dict[str, np.ndarray]:
"""Collapse spin channels the way xc-regression.c does for nspin=1."""
return {
"rho": inp["rho"].sum(axis=1),
"sigma": inp["sigma"][:, 0] + 2.0 * inp["sigma"][:, 1] + inp["sigma"][:, 2],
"lapl": inp["lapl"].sum(axis=1),
"tau": inp["tau"].sum(axis=1),
}
def load_regression(
family_dir: str, functional: str, system: str, spin: str, order: int
) -> dict[str, np.ndarray]:
"""Return reference outputs keyed by column name (zk, vrho(a), ...)."""
path = (
REGRESSION_DIR
/ family_dir
/ f"{functional}.{system}.{spin}.{order}.bz2"
)
with bz2.open(path, "rt") as fh:
header = fh.readline().split()
func_id, npoints, forder = int(header[0]), int(header[1]), int(header[2])
assert forder == order
names = fh.readline().split()
data = np.loadtxt(fh)
data = np.asarray(data).reshape(npoints, len(names))
out = {name: data[:, i] for i, name in enumerate(names)}
out["_func_id"] = func_id
return out
def has_regression(family_dir: str, functional: str) -> bool:
return any(
(REGRESSION_DIR / family_dir).glob(f"{functional}.*.bz2")
)

84
tests/test_api.py Normal file
View File

@@ -0,0 +1,84 @@
"""API surface tests: registry integrity and the pylibxc-compatible shim."""
import numpy as np
import pytest
import funxc
from funxc.libxc_compat import LibXCFunctional
def test_registry_specs_wellformed():
for fid, spec in funxc.REGISTRY.items():
assert spec.id == fid
assert spec.family in ("lda", "gga", "mgga")
assert spec.kind in ("x", "c", "xc", "k")
assert spec.libxc_id > 0
assert spec.kernel_fn is not None
assert spec.doi
def test_get_spec_is_case_insensitive():
assert funxc.get_spec("gga_x_pbe") is funxc.get_spec("GGA_X_PBE")
assert funxc.get_spec("XC_GGA_X_PBE") is funxc.get_spec("GGA_X_PBE")
with pytest.raises(KeyError):
funxc.get_spec("GGA_X_DOES_NOT_EXIST")
def test_shim_polarized_shapes():
f = LibXCFunctional("gga_x_pbe", "polarized")
n = 4
rng = np.random.default_rng(0)
inp = {
"rho": rng.uniform(0.1, 1.0, (n, 2)),
"sigma": rng.uniform(0.0, 0.5, (n, 3)),
}
out = f.compute(inp)
assert out["zk"].shape == (n, 1)
assert out["vrho"].shape == (n, 2)
assert out["vsigma"].shape == (n, 3)
def test_shim_unpolarized_shapes_and_flat_input():
f = LibXCFunctional("lda_x", 1)
rho = np.linspace(0.1, 2.0, 5)
out = f.compute({"rho": rho})
assert out["zk"].shape == (5, 1)
assert out["vrho"].shape == (5, 1)
out_exc_only = f.compute({"rho": rho}, do_vxc=False)
assert set(out_exc_only) == {"zk"}
def test_shim_mgga_shapes():
f = LibXCFunctional("mgga_x_lta", "polarized")
n = 4
rng = np.random.default_rng(1)
inp = {
"rho": rng.uniform(0.1, 1.0, (n, 2)),
"sigma": rng.uniform(0.0, 0.5, (n, 3)),
"lapl": rng.uniform(-1.0, 1.0, (n, 2)),
"tau": rng.uniform(0.1, 1.0, (n, 2)),
}
out = f.compute(inp)
assert out["zk"].shape == (n, 1)
assert out["vrho"].shape == (n, 2)
assert out["vsigma"].shape == (n, 3)
assert out["vlapl"].shape == (n, 2)
assert out["vtau"].shape == (n, 2)
# LTA has no lapl dependence: vlapl must be exactly zero
assert np.all(out["vlapl"] == 0.0)
def test_shim_metadata():
f = LibXCFunctional("gga_c_lyp", "polarized")
assert f.get_number() == 131
assert f.get_family() == "gga"
def test_composition_blyp():
"""X and C functionals compose additively (BLYP = B88 + LYP)."""
rho = np.array([[0.4, 0.3]])
sigma = np.array([[0.02, 0.01, 0.015]])
b88 = funxc.functional("GGA_X_B88").exc(rho, sigma)
lyp = funxc.functional("GGA_C_LYP").exc(rho, sigma)
total = np.asarray(b88) + np.asarray(lyp)
assert np.all(np.isfinite(total)) and total[0] < 0

100
tests/test_guards.py Normal file
View File

@@ -0,0 +1,100 @@
"""NaN-safety and differentiability tests for the guard layer.
The regression tests establish numeric parity; these establish that funxc
stays finite (values *and* gradients) at the singular corners libxc's grids
avoid: rho -> 0 tails, sigma = 0, full spin polarization, and combinations.
"""
import jax
import jax.numpy as jnp
import numpy as np
import pytest
import funxc
GGA_IDS = [fid for fid in funxc.REGISTRY if funxc.REGISTRY[fid].family == "gga"]
LDA_IDS = [fid for fid in funxc.REGISTRY if funxc.REGISTRY[fid].family == "lda"]
# Adversarial polarized points:
# (rho_up, rho_dn, sigma_uu, sigma_ud, sigma_dd, tau_up, tau_dn)
EDGE_POINTS = [
(0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), # vacuum
(1e-300, 0.0, 0.0, 0.0, 0.0, 1e-300, 0.0), # denormal tail
(0.3, 0.0, 7e-3, 0.0, 0.0, 2.9e-3, 0.0), # fully polarized, tau ~ tau_W
(0.3, 0.3, 0.0, 0.0, 0.0, 0.0, 0.0), # sigma = tau = 0 exactly
(0.3, 1e-16, 1e-2, 0.0, 1e-30, 5e-2, 1e-30), # one channel below thr
(1e-14, 1e-14, 1e-25, -1e-25, 1e-25, 1e-25, 1e-25), # negative sigma_ud
(1e3, 1e3, 1e8, 1e8, 1e8, 1e8, 1e8), # high density, huge gradient
(0.5, 0.5, 1e12, 0.0, 1e12, 1e-5, 1e-5), # s -> inf, tau << tau_W (FHC)
]
def _edge_args(spec, f):
rho = np.array([[p[0], p[1]] for p in EDGE_POINTS])
sigma = np.array([[p[2], p[3], p[4]] for p in EDGE_POINTS])
tau = np.array([[p[5], p[6]] for p in EDGE_POINTS])
lapl = np.zeros_like(tau)
if spec.family == "lda":
return (rho,)
if spec.family == "gga":
return (rho, sigma)
return (rho, sigma, lapl, tau)
@pytest.mark.parametrize("fid", sorted(funxc.REGISTRY))
def test_values_and_gradients_finite_at_edges(fid):
spec = funxc.REGISTRY[fid]
f = funxc.functional(fid, polarized=True)
out = f.exc_vxc(*_edge_args(spec, f))
for key, val in out.items():
assert np.all(np.isfinite(val)), f"{fid}: non-finite {key} at edge points"
@pytest.mark.parametrize(
"fid",
["LDA_X", "LDA_C_PW", "GGA_X_PBE", "GGA_C_PBE", "GGA_C_LYP",
"GGA_XC_B97_D", "MGGA_X_LTA"],
)
def test_second_derivatives_finite(fid):
"""fxc-level derivatives must also stay finite (autodiff twice)."""
spec = funxc.REGISTRY[fid]
f = funxc.functional(fid, polarized=True)
if spec.family == "lda":
hess = jax.hessian(f.energy_density, argnums=0)
args = lambda p: (jnp.array(p[:2]),) # noqa: E731
elif spec.family == "gga":
hess = jax.hessian(f.energy_density, argnums=(0, 1))
args = lambda p: (jnp.array(p[:2]), jnp.array(p[2:5])) # noqa: E731
else:
hess = jax.hessian(f.energy_density, argnums=(0, 1, 2, 3))
args = lambda p: ( # noqa: E731
jnp.array(p[:2]), jnp.array(p[2:5]), jnp.zeros(2), jnp.array(p[5:7])
)
for p in EDGE_POINTS:
h = hess(*args(p))
leaves = jax.tree.leaves(h)
assert all(jnp.all(jnp.isfinite(x)) for x in leaves), (
f"{fid}: non-finite second derivative at {p}"
)
def test_vacuum_outputs_are_zero():
f = funxc.functional("GGA_X_PBE", polarized=True)
out = f.exc_vxc(np.zeros((1, 2)), np.zeros((1, 3)))
for key, val in out.items():
assert np.all(np.asarray(val) == 0.0), f"nonzero {key} in vacuum"
def test_jit_and_vmap_compose():
"""The per-point eps must survive user-side jit/vmap/grad composition."""
f = funxc.functional("GGA_C_PBE", polarized=True)
g = jax.jit(jax.vmap(jax.grad(f.energy_density, argnums=(0, 1))))
rho = jnp.array([[0.3, 0.2], [1.0, 1.0]])
sigma = jnp.array([[0.01, 0.0, 0.02], [0.1, 0.05, 0.1]])
vrho, vsigma = g(rho, sigma)
assert vrho.shape == (2, 2) and vsigma.shape == (2, 3)
assert jnp.all(jnp.isfinite(vrho)) and jnp.all(jnp.isfinite(vsigma))
def test_float64_active():
assert jnp.asarray(1.0).dtype == jnp.float64

134
tests/test_regression.py Normal file
View File

@@ -0,0 +1,134 @@
"""Golden-data tests: funxc vs libxc 7.0.0 vendored regression files.
Every registered functional is compared against libxc/testsuite/regression
for all four systems, both spin modes, orders 0 (zk) and 1 (vrho, vsigma).
Tolerance policy: |got - ref| <= ATOL + RTOL*|ref|.
Typical agreement is machine precision (zk ~1e-15 relative). The absolute
floor covers two known noise sources that libxc itself carries:
* roundoff residue of analytically-cancelling terms at zeta = +-1 (the
reference files store values like -1.1e-16 where the exact result is 0);
* expression-grouping noise ~(A*t^2)^2 * eps in the saturated large-t
limit of the PBE H(t) correlation term, visible only at density-tail
points (rho ~ 1e-11) where |vsigma| ~ 1e-6 and the noise is ~4e-10.
"""
import numpy as np
import pytest
import funxc
from libxc_reference import (
SYSTEMS,
has_regression,
load_input,
load_regression,
unpolarize,
)
ATOL = 2e-9
RTOL = 1e-7
# Points where the vendored regression *file* disagrees with the libxc 7.0.0
# *library* itself (module libxc/7.0.0): the testsuite files ship with the
# source tree and some predate behavior changes. Each entry was verified by
# comparing the installed .so against the file — funxc matches the library
# (e.g. LTA Li vrho(b) point 4: library -2.38273e-8, funxc -2.38273e-8,
# stale file -1.05e-14, a leftover of an older density screen).
STALE_GOLDEN = {
("MGGA_X_LTA", "Li", "pol", "vrho(b)", 4),
}
def family_dir(fid: str) -> str:
return "_".join(fid.lower().split("_")[:2])
FUNCTIONALS = [
fid for fid in sorted(funxc.REGISTRY) if has_regression(family_dir(fid), fid.lower())
]
def check(got, ref, label, stale=()):
got = np.asarray(got).reshape(-1)
err = np.abs(got - ref)
tol = ATOL + RTOL * np.abs(ref)
bad = err > tol
if len(stale):
bad[np.asarray(stale, dtype=int)] = False
assert not bad.any(), (
f"{label}: {bad.sum()}/{bad.size} points exceed tolerance; "
f"worst at i={err.argmax()}: got={got[err.argmax()]:.12e} "
f"ref={ref[err.argmax()]:.12e}"
)
@pytest.mark.parametrize("spin", ["pol", "unpol"])
@pytest.mark.parametrize("system", SYSTEMS)
@pytest.mark.parametrize("fid", FUNCTIONALS)
def test_against_libxc(fid, system, spin):
fdir = family_dir(fid)
f = funxc.functional(fid, polarized=(spin == "pol"))
inp = load_input(system)
if spin == "unpol":
inp = unpolarize(inp)
rho, sigma = inp["rho"], inp["sigma"]
if f.spec.family == "lda":
out = f.exc_vxc(rho)
elif f.spec.family == "gga":
out = f.exc_vxc(rho, sigma)
else:
out = f.exc_vxc(rho, sigma, inp["lapl"], inp["tau"])
try:
ref0 = load_regression(fdir, fid.lower(), system, spin, 0)
except FileNotFoundError:
# libxc keeps some combinations in regression/<family>/disabled
# (e.g. gga_c_lyp on the fully polarized H atom); follow suit.
pytest.skip(f"libxc testsuite has no {fid} {system} {spin} data")
assert ref0["_func_id"] == f.spec.libxc_id
check(out["zk"], ref0["zk"], f"{fid} {system} {spin} zk")
try:
ref1 = load_regression(fdir, fid.lower(), system, spin, 1)
except FileNotFoundError:
# Some combinations disable only the derivative data (e.g.
# gga_xc_b97_3c on polarized H); zk was still checked above.
pytest.skip(f"libxc testsuite has no {fid} {system} {spin} order-1 data")
if spin == "pol":
columns = {
"vrho": ("(a)", "(b)"),
"vsigma": ("(aa)", "(ab)", "(bb)"),
"vlapl": ("(a)", "(b)"),
"vtau": ("(a)", "(b)"),
}
for key, comps in columns.items():
if key not in out or f"{key}{comps[0]}" not in ref1:
continue
got = np.asarray(out[key])
for i, comp in enumerate(comps):
stale = [
idx
for (sf, ss, sm, sc, idx) in STALE_GOLDEN
if (sf, ss, sm, sc) == (fid, system, spin, f"{key}{comp}")
]
check(
got[:, i], ref1[f"{key}{comp}"],
f"{fid} {system} {key}{comp}", stale=stale,
)
else:
for key in ("vrho", "vsigma", "vlapl", "vtau"):
if key in out and key in ref1:
stale = [
idx
for (sf, ss, sm, sc, idx) in STALE_GOLDEN
if (sf, ss, sm, sc) == (fid, system, spin, key)
]
check(out[key], ref1[key], f"{fid} {system} {key}", stale=stale)
def test_all_registered_functionals_have_golden_data():
missing = [fid for fid in sorted(funxc.REGISTRY) if fid not in FUNCTIONALS]
assert not missing, f"no regression data for: {missing}"