Development Guide#
This guide details the technical foundations of AimsPy, explaining its architecture, data formats, and how to extend it with new functionality. It is intended for developers who wish to integrate new matrix sources, add new callbacks, or contribute to the core package.
Architecture Overview#
AimsPy is built around a layered architecture. All user-facing code lives in the top layer; the lower layers are private and explicitly marked as such.
Layer |
Module |
Purpose |
|---|---|---|
Public API |
|
User-facing classes and exceptions |
Interface adapters |
|
|
Callback framework |
|
|
ctypes binding |
|
|
FHI-aims patch |
|
Versioned diffs + |
The data flows top-down at construction time (config → binding → callbacks) and bottom-up at runtime (Fortran callbacks → Python → AimspyMatrix → user).
Single sources of truth#
AimsPy is deliberately conservative about where state lives:
_runtime_aux(adictonCalculator) — callback scratch state, populated by_wire_callbacksand read by themodify_h0wrapper._modify(aSimpleNamespaceonCalculator) — H0 modification config (source,strategy,factor,custom_fn,option).CALLBACK_SPECS(inaimspy._callbacks.registry) — the authoritative catalogue of all callback types.
Data Formats & Specifications#
AimsPy’s canonical in-memory representation is the AimspyMatrix block-sparse format, whose conventions (sign, parity, units, Hermitian partners) are documented in Key Concepts. All developers should read that section before touching matrix.py or interface/deeph/.
The DeepH on-disk format is shared with DeepH-dock; its full field-level specification is in the DeepH-dock Key Concepts page.
Extending AimsPy#
After forking the source code by following the Fork and Pull Request Process, you can set up your development environment by referring to Install from Source (for Developers).
Adding a New Callback#
Adding a new callback type requires touching the following well-defined places (documented at the top of aimspy/_callbacks/base.py):
Fortran patch (
aimspy/_patches/aimspy-patch_vX.Y.Z.diff) — add the abstract callback interface incallback.f90, theaimspy_register_<name>_callbacksubroutine inregister.f90, and the trigger point ininitialize_scf.f90(orscf_solver.f90for post-SCF callbacks likeexport_grid_data). Bump the patch version and update theMakefilePATCH_VERSIONline.aimspy/_binding/callback_types.py— add aCFUNCTYPEdeclaration matching the Fortran abstract interface:MyNewCb = CFUNCTYPE(None, c_void_p, c_void_p) # (aux, extra_ptr) — adjust to taste
aimspy/_binding/prototypes.py— add an entry to_PROTOTYPESmapping the register symbol to its(argtypes, restype).setup_prototypeswill pick it up automatically andBindingLib.has(name)will probe availability at runtime.aimspy/_callbacks/registry.py— add aCallbackSpecentry toCALLBACK_SPECS:CallbackSpec( name="my_new_cb", ctypes_type=MyNewCb, register_symbol="aimspy_register_my_new_cb_callback", register_arg_count=2, # 3 if the Fortran register takes an extra c_ptr trigger_stage="pre_scf", # or "post_scf" if you add a new stage fortran_module="initialize_scf.f90:NNN", ),
aimspy/_callbacks/base.py— add a branch to_build_ctypes_wrapperthat unpacks theauxand converts the C pointer arguments to numpy views before calling the user function. Follow the existingexport_ovlp/export_h0branches (and markintent(in)viewswriteable=False).
That’s it — CallbackManager.register, Calculator.register_callback, and CallbackName all derive from CALLBACK_SPECS, so the new callback is automatically wired through.
Example: export_grid_data callback#
The export_grid_data callback (post-SCF trigger in scf_solver.f90) exports real-space grid data:
Fortran:
export_grid_data.f90module with 8 module-level buffers (coords, partition_tab, indices, vks, vks0, c_vdw_potential). The vdW buffer is filled byintegrate_hamiltonian_matrix_p2during SCF and added tovksat export time.Python:
GridData._from_ccopies arrays from Fortran buffers, normalisesrho0(removes 4π factor), and fills structure fields fromaux["structure"].Cleanup:
aimspy_export_grid_data_finalize()deallocates all buffers inaimspy_finalize, preventing ~500 MB retention.
Adding a New External Matrix Source#
To plug in a new on-disk format (e.g. a different DFT code’s output):
Create a subpackage under
aimspy/interface/<your_format>/(e.g.aimspy/interface/openmx/).Implement a class satisfying the
ExternalMatrixSourceprotocol:from aimspy.matrix import AimspyMatrix from aimspy.structure import AimspyStructure class OpenMXData: """Reads OpenMX output and converts to AimspyMatrix.""" @classmethod def from_directory(cls, path: str) -> "OpenMXData": ... def to_aimspy(self, structure: AimspyStructure) -> AimspyMatrix: # Build the block-sparse dict following the conventions in # key_concepts.md#aimspymatrix-block-sparse-format: # - R_aimspy = -R_aims = R_deeph # - aims native atom/orbital order # - phase_i * phase_j already applied # - Hartree units # - both (R,i,j) and (-R,j,i) stored ...
Use it —
Calculator.modify_init_ham(source=OpenMXData.from_directory("..."), strategy=Strategy.REPLACE)will work with no other changes.
The ExternalMatrixSource protocol is @runtime_checkable, so isinstance(obj, ExternalMatrixSource) works for validation. The deferred-source decorator path also works: any object with to_aimspy(structure) -> AimspyMatrix is accepted.
Adding a New Modification Strategy#
Extend the Strategy enum in aimspy/calculator.py and add a branch to _apply_strategy. The strategy receives (live, external, structure, aux) and must mutate live (an AimspyMatrix) in place.
Testing#
AimsPy separates unit tests (no MPI, no libaims) from integration tests (require AIMSPY_TEST_AIMS_LIBPATH + mpiexec):
make test # unit tests only (pytest -v)
make test-integration # 6 MPI integration tests, in dependency order
make test-all # both
Unit tests — tests/unit/#
No MPI, no libaims. Cover AimspyStructure derived properties, DeepHData I/O roundtrips, POSCAR parsing, Strategy / CallbackName enums, the ExternalMatrixSource protocol, and force_close / CalcState transitions. tests/conftest.py provides mock_structure fixtures (3-atom MoS₂-like).
Integration tests — tests/test_*.py#
Require AIMSPY_TEST_AIMS_LIBPATH pointing at a patched libaims.so, run under mpiexec (default 8 ranks). The suite covers baseline SCF, DeepH export, warmstart, overlap capture, regression (50+ checks), and all Strategy variants.
Important: FHI-aims is a global Fortran singleton — one
init/finalizeper process.test_strategies.pytherefore runs each strategy in a separate MPI invocation viasubprocess, with the controlling Python process dispatching and aggregating results. New per-strategy tests should follow this pattern.
Test data#
tests/data/MoS2/ contains a MoS₂ fixture (control.in, geometry.in) and reference outputs (rs_hamiltonian.out, rs_overlap.out, rs_indices.out, basis-indices.out). Integration tests cross-validate against these references.
Best Practices & Conventions#
Code style:
ruff check .andblack --check .must pass.make lintruns both.Caching: use
@functools.cached_propertyfor derived properties on immutable dataclasses (seeAimspyStructure.phase_factor,basis_subidx,atom_permutation).Logging: INFO/WARNING emitted on rank 0 only; ERROR on all ranks. AimsPy attaches a
NullHandlerto theaimspylogger — it never configures the root logger.GC safety: ctypes wrappers and Python objects passed to Fortran via
c_void_p.from_buffer(py_object(aux))must be kept alive explicitly. SeeCallbackManager._pyobjs/_wrapped/_auxs.State guards:
Calculator._state_guardraisesAimspyStateErroron illegal transitions. New methods that touch Fortran should declare their allowed states.No
osoutsideaimspy._system:_system.pyis the only module that importsos. All other modules usepathlib.Path. This keepschdir(2)usage auditable.Documentation: include docstrings for all public functions and classes. Update the main documentation under
docs/if the public API changes.
Next Steps#
After familiarizing yourself with these concepts, you are ready to contribute code. Please follow the collaborative process outlined in the Collaboration Guide to submit your changes.