Key Concepts#
This section describes the core architecture and data formats used by AimsPy. Understanding these concepts is essential for using the API effectively and for extending the package with new functionality.
Overview#
AimsPy drives FHI-aims DFT calculations directly from Python by loading a patched libaims.so via ctypes. There is no subprocess and no file-staged I/O on hot paths — Hamiltonian, overlap, energy, and forces are exchanged as in-memory arrays through a callback framework exposed by the bundled FHI-aims patch.
The package is organised in three layers:
Layer |
Purpose |
Public? |
|---|---|---|
|
User-facing API |
Yes |
|
Callback framework ( |
No |
|
ctypes binding to |
No |
The central user-facing class is Calculator, whose lifecycle is governed by the CalcState state machine.
In-Memory Architecture#
The ctypes binding layer#
aimspy._binding.libloader.load_aims_lib(lib_path) is the only place that calls ctypes.CDLL on libaims.so. Two details matter:
MPICH symbol visibility.
mpi4py’s own shared object must be loaded withRTLD_GLOBALand anchored at module level (_mpi_cdll_anchor). Without the module-level anchor, theCDLLwould be garbage-collected (anddlclose’d) when the function returns, removing the global symbols thatlibaims.soneeds for lazy MPI symbol resolution. AimsPy loadslibaims.soitself withRTLD_GLOBALas well.Forward-compatible symbol probing.
BindingLib(inaimspy._binding.prototypes) remembers which C symbols were detected and exposes ahas(name)predicate.setup_prototypessilently skips symbols missing in olderlibaimsbuilds, so a single AimsPy release can drive multiple patch versions.
The Calculator lifecycle#
CalcState is a six-state enum with a directed lifecycle:
UNINIT ──init()──> INITED ──calc()──> [RUNNING] ──> DONE
│ │
└──close()───────────────────────┘
│
close()/force_close() ──────────> FINALIZED
(any state, on error) ──> FAILED ──force_close()──> FINALIZED
State |
Meaning |
Allowed next |
|---|---|---|
|
Freshly constructed |
|
|
|
|
|
Transient inside |
|
|
SCF converged; |
|
|
Operation aborted; Fortran runtime in unknown state |
|
|
|
— |
State transitions are guarded — calling calc() from UNINIT, or close() from RUNNING, raises AimspyStateError. Use force_close() for safe recovery from any state.
Note:
energyis also accessible in theRUNNINGstate (e.g. from inside a callback during pre-SCF), but the value may be uninitialized before the first SCF iteration completes.
Thread safety:
Calculatoris not thread-safe. FHI-aims useschdir(2)(process-global) internally, so concurrentCalculatoroperations in the same process will race. The typical MPI usage is one rank = one process.
Callback Framework#
The bundled FHI-aims patch inserts trigger points across the FHI-aims driver files — mainly inside src/initialize_scf.f90 (after reshape_matrices, before the initial diagonalisation), but also in src/prepare_scf.f90 (pre-SCF basis export), src/scf_solver.f90 (post-SCF grid export), and src/DFPT_main/DFPT_module.f90 (pre/post-CPSCF first-order Hamiltonian export and injection). The available callbacks are:
Callback |
Purpose |
|---|---|
|
Capture the CSR sparse-storage layout |
|
Export the overlap matrix |
|
Export the free-atom initial Hamiltonian (H_init) |
|
Generic Python hook (deferred source generation) |
|
Inject the modified H_init back into FHI-aims |
|
Export DFPT first-order Hamiltonian (post-CPSCF) |
|
Inject modified first-order Hamiltonian (pre-CPSCF) |
|
Export real-space grid data (post-SCF convergence) |
|
Export NAO radial basis splines (inside |
When modify_h0 is registered, the patch short-circuits the standard initial diagonalisation: it calls advance_KS_solution directly on the injected Hamiltonian and sets restart_zero_iteration=.true., which is what enables warmstart in several iterations.
Note: Matrix extraction and injection (overlap, Hamiltonian, H_init) require a periodic system with
use_local_index = .false.. Forward SCF calculations (without matrix extraction/injection) work with any system type. For isolated molecules, use a sufficiently large periodic cell with vacuum.
How callbacks are wired#
CalculatorConfig flags control which default callbacks are auto-registered by Calculator._wire_callbacks:
Flag |
Enables |
Effect |
|---|---|---|
(always) |
|
|
|
|
|
|
|
|
|
|
warmstart / scaling / custom modification |
|
|
|
|
|
dH/de warmstart injection (pre-CPSCF) |
|
|
|
|
|
|
Users can also register custom callbacks via Calculator.register_callback(name, fn, aux, extra_ptr) for advanced use cases.
Error handling#
A Python exception raised inside a callback never crashes Fortran. The CallbackManager records (name, exception, traceback_str) tuples and lets calc() complete. After aimspy_run returns, Calculator._check_callback_errors raises a single AimspyCallbackError aggregating all failures, with the per-callback details preserved on exc.callback_errors. Notably, forces are captured before the callback error check, so they survive even when a callback raises.
Hamiltonian Modification Strategies#
Calculator.modify_init_ham(source=..., strategy=...) configures how the live H_init buffer is mutated before SCF starts. The built-in strategies, dispatched by the pure function _apply_strategy:
Strategy |
Behaviour |
Required argument |
Typical use |
|---|---|---|---|
|
Clear and copy external blocks into the live |
|
Warmstart with a DeepH prediction |
|
Add external blocks on top of the live |
|
Correction (Delta-prediction): add predicted H − H₀ to H₀ to recover H |
|
Multiply the live |
|
Scaling experiments |
|
Call |
|
Arbitrary transforms |
Direct vs. deferred source#
modify_init_ham supports two modes:
Direct mode (
source=is passed): the source object is stored immediately. Thepython_funccallback then converts it viasource.to_aimspy(structure)and stores the result in_runtime_aux["external_aimspy"], whichmodify_h0reads.Deferred mode (used as a decorator): the user function is called during the
python_funccallback, afterexport_h0andexport_ovlphave fired, so it has live access tocalculator.initial_hamiltonianandcalculator.overlap. This is essential when the external source needs the runtime structure to be built.
# Direct
calc.modify_init_ham(source=data, strategy=Strategy.REPLACE)
# Deferred
@calc.modify_init_ham(strategy=Strategy.REPLACE, option={"path": "deeph_out/"})
def gen_source(calculator, option):
return DeepHData.from_directory(option["path"])
ExternalMatrixSource Protocol#
The ExternalMatrixSource Protocol (aimspy.interface) is the contract for any external matrix provider:
from typing import Protocol, runtime_checkable
@runtime_checkable
class ExternalMatrixSource(Protocol):
def to_aimspy(self, structure: AimspyStructure) -> AimspyMatrix: ...
The reference implementation is DeepHData (importable from aimspy directly), which reads the DeepH on-disk format (POSCAR + info.json + .h5) and converts to AimspyMatrix. Adding a new external format is a single subpackage under aimspy/interface/<format>/ with a class satisfying this protocol — no other code changes are required.
AimspyMatrix Block-Sparse Format#
AimspyMatrix is AimsPy’s canonical in-memory representation of block-sparse
real-space matrices. It holds a dict mapping atom-pair keys to dense numpy
blocks:
from aimspy import AimspyMatrix
import numpy as np
# Direct construction
matrix = AimspyMatrix(
blocks={
(0, 0, 0, 0, 0): np.array([[1.0, 0.5], [0.5, 1.0]]), # R=0, atom 0→0
(1, 0, 0, 0, 1): np.array([[0.1]]), # R=(1,0,0), atom 0→1
},
n_spin=1,
)
# Access
block = matrix.blocks[(0, 0, 0, 0, 0)] # ndarray, shape (2, 2)
print(matrix.n_pairs) # 2
Each key is a 5-tuple (R1, R2, R3, i_atom, j_atom):
R1, R2, R3— lattice vector components (integers), followingR_aimspy = -R_aims = R_deephi_atom, j_atom— 0-based atom indices in aims native order
Each value is an np.ndarray of shape (n_orb_i, n_orb_j), dtype float64,
where n_orb_i / n_orb_j are the number of basis functions on atom i / j.
Conventions#
Property |
Convention |
|---|---|
|
|
Atom indices |
aims native order (no reordering) |
Orbital order |
aims native basis order (no reordering) |
Parity |
wiki/DeepH convention ( |
Units |
Hartree (Hamiltonian), dimensionless (overlap) |
Hermitian partners |
both |
Phase factor#
The parity convention is implemented in AimspyStructure.phase_factor:
phase_factor = np.where((basis_m > 0) & (basis_m % 2 == 1), -1, 1).astype(np.int32)
It is self-inverse (phase² = 1), so applying it once converts aims ↔ aimspy
and applying it again undoes the conversion.
Construction#
Three ways to obtain an AimspyMatrix:
# 1. Direct (offline, for testing or custom data)
matrix = AimspyMatrix(blocks={...}, n_spin=1)
# 2. From FHI-aims CSR (after calc.do(), rank 0 only)
H = calc.hamiltonian # AimspyMatrix
# 3. From DeepH on-disk format
from aimspy import DeepHData
data = DeepHData.from_directory("deeph_out/")
H = data.to_aimspy(calc.structure) # AimspyMatrix
Conversions require an AimspyStructure (provides atom/orbital info and derived
properties like phase_factor, orbit_per_atom, atom_permutation) and a
CsrMatrixDescriptor (FHI-aims’ CSR sparse layout — see
API Reference for field details).
Conversion#
To aims CSR:
matrix.to_aims_csr(csr_descr, structure)returns a(n_spin, n_ham_size)C-contiguous array, ready forctypes.memmoveinto the Fortran buffer. Hermitian fallback: if(R,i,j)is missing,(-R,j,i)is used with transposition.To DeepH format:
DeepHData.from_aimspy(structure, hamiltonian=matrix, ...)handles atom reordering (aims → POSCAR) and unit conversion (Hartree → eV).
Limitations#
Spinless only: converters read/write spin channel 0;
n_spin=2leaves channel 1 as zero. Spin-polarised support is on the roadmap.Periodic only: matrix extraction/injection requires
use_local_index = .false.(see Troubleshooting).
DeepH Data Format#
DeepHData reads and writes the standard DeepH on-disk format used throughout the DeepH ecosystem. The format is shared with DeepH-dock — for the full field-level specification, see the DeepH-dock Key Concepts page. A summary:
some_directory/
├── POSCAR # Atomic structure (VASP format, element-grouped order)
├── info.json # System metadata + basis set info
├── overlap.h5 # Overlap matrix S (sparse)
├── hamiltonian.h5 # Hamiltonian H (sparse, eV)
├── hamiltonian_init.h5 # Free-atom initial Hamiltonian (sparse, eV)
├── force.h5 # (optional) Forces + energy (MD-style: cell/energy/force/stress)
└── electric_response.h5 # (optional) DFPT first-order Hamiltonian dH/de (sparse, eV)
Each matrix .h5 file stores four datasets: atom_pairs (N,5), chunk_boundaries (N+1,), chunk_shapes (N,2), and entries (M,). The atom order in POSCAR is element-grouped (different from aims native order); DeepHData handles the reordering via AimspyStructure.atom_permutation.
force.h5 uses a different MD-style layout: cell (3,3), energy (scalar), force (n_atoms,3), stress (6,) (zeros placeholder), with formula and natoms root attributes. Forces are in eV/Å (matching calc.forces), energy is in eV (converted from calc.energy Hartree).
electric_response.h5 stores the DFPT first-order Hamiltonian (dH/de) — the response of the Hamiltonian to an electric field perturbation. It uses the same atom_pairs as hamiltonian.h5, but chunk_shapes rows are 3× larger (one block per Cartesian direction [y, z, x] = real spherical harmonics m = -1, 0, +1), and entries is 3× longer. Units are eV (converted from Hartree). Requires electric_field_response DFPT + electric_field_serial .false. in control.in.
Unit conventions#
Quantity |
AimsPy internal |
DeepH on-disk |
|---|---|---|
Hamiltonian |
Hartree |
eV |
Overlap |
dimensionless |
dimensionless |
Coordinates |
Å |
Å (in POSCAR) |
Force |
eV/Å ( |
eV/Å ( |
Total energy |
Hartree ( |
eV ( |
First-order H (dH/de) |
Hartree |
eV ( |
DeepHData.from_memory converts Hartree → eV on write; DeepHData.to_aimspy converts eV → Hartree on read. Force requires no unit conversion (eV/Å throughout); only atom reordering is applied. Energy in force.h5 is converted from Hartree via from_aimspy(force=, energy=) or set_force(force, structure, energy=). First-order Hamiltonian is converted from Hartree via from_aimspy(first_order_hamiltonian=) or set_first_order_hamiltonian(), and back to Hartree via to_first_order_aimspy().
FHI-aims Patch System#
Three patch versions are bundled (v0.1.0 ~1100 lines, v0.2.0 ~1400 lines, v0.2.1 ~2200 lines — the latest, adding the grid/basis exports and the DFPT dH/de hooks). The patch does three things:
Adds
src/aimspy_api/with Fortran modules:callback.f90—TAimspyCsrMxDescr(bind(C) struct),TAimspyCallbackhandle type, abstract callback interfaces.api_bank.f90— module-levelsavearrays (c_hamiltonian,c_overlap),aimspy_energy,aimspy_forcesaccessors.info.f90—TAimspyInfobind(C) struct +aimspy_get_infopopulating asavebuffer.register.f90— theaimspy_register_*_callbackbind(C) subroutines.main.f90—aimspy_init/aimspy_run/aimspy_finalize/aimspy_alllifecycle entry points.export_grid_data.f90/export_basis_data.f90— module-level buffer assemblies + triggers for the post-SCF grid export and the pre-SCF NAO basis export (both finalized inaimspy_finalizeto release their buffers).
Hooks into
src/initialize_scf.f90— trigger points afterreshape_matrices, and the warmstart short-circuit callingadvance_KS_solutionon the injected Hamiltonian withrestart_zero_iteration=.true..Exposes
pbc_lists.f90arrays — addstargetattributes toindex_hamiltonian/column_index_hamiltonianso they can be exposed viac_loc.
The patch is versioned (currently v0.2.1) and managed by the aimspy patch CLI, which can apply, uninstall, dry-run, and list bundled versions. Multiple patch versions can ship side-by-side; the CLI auto-detects the currently-applied version by reading a PATCH_VERSION line that the patch itself writes into the source tree’s Makefile.
Grid Data (Real-Space)#
GridData is AimsPy’s in-memory representation of the FHI-aims real-space integration grid and the scalar fields living on it. It is captured after SCF convergence via the export_grid_data callback.
Fields#
Field |
Shape |
Units |
Description |
|---|---|---|---|
|
|
bohr |
Grid point coordinates (mapped to center cell for periodic) |
|
|
bohr³ |
Grid point integration weights |
|
|
— |
0-based atom index for each point |
|
|
— |
Radial shell index |
|
|
— |
Angular grid index |
|
|
e/bohr³ |
Converged electron density |
|
|
Hartree |
Kohn-Sham potential (includes vdW if active) |
|
|
Hartree |
Free-atom reference potential (no vdW) |
|
|
Hartree |
Hartree potential (includes nuclear attraction) |
|
|
Hartree |
Free-atom Hartree potential |
|
|
e/bohr³ |
Free-atom superposition density ( |
|
|
Å |
Atomic coordinates (from in-memory structure) |
|
|
— |
Element symbols |
|
|
Å |
Lattice vectors |
Key semantics#
rho0isrho_free: FHI-aims exportsfree_rho_superposwhich carries a factor of4π; AimsPy normalises at import sorho0IS the free-atom density.vksincludes vdW: Whenuse_vdw_correction_hirshfeld_sc,use_mbd_std, oruse_libmbdis active,vks = V_H + V_nuc + v_xc + v_vdw.vks0does NOT include vdW (free-atom reference has no vdW correction).LDA scalar only: The GGA non-local (vector) term
4*xc_gradient_derivis NOT exported.vksis exact for LDA, scalar part for GGA. Hybrid functionals are not supported.
Derived quantities#
gd.delta_rho # rho - rho_free (density difference)
gd.delta_vks # vks - vks0 (potential difference)
gd.vxc # vks - vh (exchange-correlation potential)
gd.vxc0 # vks0 - vh0 (free-atom XC potential)
gd.coords_ang # coords in Å (converted from bohr)
gd.vks_ev # vks in eV
MPI gather#
GridData.gather(local, comm) collects per-rank subsets to root using mpi4py.MPI.Comm.Gatherv (zero-pickle, memory-efficient). Root peak memory is ~1x the total dataset, compared to ~3x for the default comm.gather on a Python dict.
npz serialization#
gd.save_npz("grid.npz") # save (includes structure fields if present)
gd2 = GridData.load_npz("grid.npz") # load
The npz format is self-describing: it stores n_full_points, n_spin, n_atoms, all grid arrays, and optionally atom_coords/atom_symbols/lattice.
NAO Radial Basis (BasisData)#
With capture_basis_data=True, the export_basis_data callback (registered before aimspy_init, because it fires inside prepare_scf during init) captures the complete cubic-spline representation of the NAO radial basis: spline coefficients for u(r), (e−v)·u(r), and du/dr, plus per-species logarithmic grid parameters (r_grid_min, r_grid_inc, n_grid) and per-function outer_radius.
Evaluation:
basis_data.evaluate_u(i_fn, r)/evaluate_phi/evaluate_du_dr— the per-function 0-based species map is attached automatically at init (basis_data.species_of_fn); u(r) evaluates to zero outside[r_grid_min, outer_radius].Identity metadata (
n,l,type,speciesper radial function) comes fromcalc.info(basisfn_n/basisfn_l/basisfn_type/basisfn_species).Units: lengths in bohr, energies in Hartree; u(r) normalized so ∫u²dr = 1 (bohr^−1/2).
basis.h5 format#
BasisData.save_h5(path, info) builds an incremental element-per-group library: the file is created if missing; existing element groups are skipped silently (not overwritten), so one file can accumulate basis sets across calculations.
/attrs: format_version, generator, date, units, species_list, n_species
/<El>/attrs: element, z, r_grid_min, r_grid_inc, n_grid,
n_basis_rad, n_orbitals, l_max
/<El>/r_grid (n_grid,) bohr, shared log grid
/<El>/n, l, zeta (n_basis_rad,) int32 quantum numbers; zeta = index
among same (n,l) duplicates
/<El>/type (n_basis_rad,) S8 atomic/hydro/...
/<El>/outer_radius (n_basis_rad,) bohr, per-function cutoff
/<El>/spline_wave (n_basis_rad, 4, n_grid) cubic coeffs for u(r)
/<El>/spline_kinetic (same) for (e−v)·u(r)
/<El>/spline_deriv (same) for du/dr
Visualize offline with aimspy viz-basis basis.h5 or aimspy.viz_basis.plot_radial_basis.
Data Flow in AimsPy#
A typical warmstart workflow:
Input: FHI-aims
control.in+geometry.ininwork_dir; an external Hamiltonian source (e.g. aDeepHDatadirectory from a DeepH-trained model).Processing:
Calculator.initloadslibaims.so, callsaimspy_init, buildsAimspyStructure._wire_callbacksregisters the default callbacks based onCalculatorConfigflags.Calculator.calccallsaimspy_run. Inside FHI-aims, afterreshape_matrices:get_descrpopulates the CSR layout.export_ovlp/export_h0capture overlap and free-atomH_init(if enabled).python_funcconverts the external source viato_aimspy(structure).modify_h0applies theStrategy, writes the result back viamemmove, and the patch short-circuits the diagonalisation.
Output:
calc.hamiltonian(AimspyMatrix, Hartree),calc.energy(Hartree),calc.forces(eV/Å), and optionallycalc.overlap/calc.initial_hamiltonian. These can be exported to DeepH format viaDeepHData.from_aimspy(...).save(...).
For more on the API surface, see Basic Usage. For extending AimsPy with new callbacks or matrix sources, see the Development Guide.