This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Dependency management uses uv. The [ci] extra installs everything needed for the full test suite (matgl, maml, matminer, mp-api, seekpath, jupyter/nbmake).
# Install for development (full test deps)
pip install -e '.[ci]'
# Run the test suite
pytest tests
pytest tests/test_elasticity.py # single file
pytest tests/test_elasticity.py::test_elasticity_calc # single test
pytest --cov=matcalc --cov-report=xml tests # with coverage (CI invocation)
# Lint / format / typecheck (these are the CI gates)
ruff check src
ruff format src --check
mypy -p matcalc
# CLI entry point (installed as console script `matcalc`)
matcalc calc -p ElasticityCalc -s structure.cif
matcalc calc -p RelaxCalc -m TensorNet-MatPES-PBE-2025.2 -s structure.cif -o results.json
matcalc clear # clears ~/.cache/matcalc benchmark data
# Release / docs automation (pyinvoke)
invoke make-docs # builds Sphinx + tutorials into docs/
invoke update-changelog # generates changes.md entries from PR titles
invoke release # creates GitHub release for current pyproject versionThe Test workflow runs inside docker.io/materialyzeai/lammps_gnnp because LAMMPS-backed tests need a LAMMPS build with ML-GNNP and ML-SNAP. Locally, LAMMPS-dependent tests will be skipped or fail without that environment.
Everything in src/matcalc/ is built around one base class in _base.py:
PropCalc.calc(structure) -> dict[str, Any]is the single entry point.structuremay be apymatgen.Structure, anase.Atoms, or a dict containingfinal_structure/structure. The dict form is what makes chaining work: each calculator's output dict is fed into the next calculator'scalc. A subclass's first line is typicallyresult = super().calc(structure)to normalize the input and pull outfinal_structure.PropCalc.calc_many(structures, n_jobs=...)parallelizes viajoblib.Paralleland returns a generator. Passallow_errors=Truefor large runs where you want failed structures to yieldNoneinstead of raising.PropCalc.calculatoris an ASECalculatorinstance. The setter accepts either an existingCalculatoror a model-name string — strings are routed throughPESCalculator.load_universal(). This is why every concrete calc acceptscalculator: str | Calculatorin its constructor.ChainedCalc(prop_calcs)runs calculators in sequence and accumulates their output dicts. The convention is to put aRelaxCalcfirst and then setrelax_structure=Falseon downstream calcs to avoid re-relaxing.
PESCalculator.load_universal(name) (re-exported as matcalc.load_fp and matcalc.load_up) is the single dispatcher for loading any supported model. Two data structures drive it:
MODEL_REGISTRY— canonical name →{provider, ...kwargs}. Naming convention is<Architecture>-<Dataset>-<Optional Version>, e.g.TensorNet-MatPES-PBE-2025.2.MODEL_ALIASES— short / legacy spellings (pbe,r2scan,mace,chgnet, …) that resolve to a canonical name. Lookups are case-insensitive.
To add a new model, add an entry to MODEL_REGISTRY and (if the provider is new) extend the loader branch in PESCalculator.load_universal. Provider stacks (matgl, mace-torch, sevenn, tensorpotential, orb-models, mattersim, fairchem-core, pet-mad, deepmd-kit) are imported lazily inside their branches so that missing optional deps don't break import of matcalc.
The test-fp-loaders matrix in .github/workflows/test.yml smoke-tests one canonical name per provider — keep it in sync when adding/removing canonical names for a covered provider.
Two simulation backends sit behind a thin dispatcher:
backend/_ase.py(run_ase) — default; uses ASE optimizers/MD drivers.backend/_lammps.py(run_lammps) — used byLAMMPSMDCalcand friends.backend/__init__.py::run_pes_calcchooses based onMATCALC_BACKENDenv var (read once intoconfig.SIMULATION_BACKEND, default"ASE").
SimulationResult (in backend/_base.py) is the common return shape — keep both backends returning structurally identical results so that downstream calcs don't branch on backend.
src/matcalc/elemental_refs/*.json.gzships per-functional elemental references (MatPES-PBE, MatPES-r2SCAN, MP-PBE) used byEnergeticsCalcfor formation/cohesive energies.EnergeticsCalcselects the reference set via thefunctionalarg.units.pyholds conversion factors. The most-used one iseVA3ToGPafor stress/elastic-tensor unit conversion — prefer it over hand-rolled constants.
benchmark.py defines Benchmark, BenchmarkSuite, and concrete ElasticityBenchmark, PhononBenchmark, etc. Benchmark datasets are pulled from the materialyze/matcalc-bench HuggingFace repo and cached in ~/.cache/matcalc (configured in config.py). Use n_samples during development to avoid full-dataset runs.
- Single-underscore module names (
_elasticity.py,_phonon.py, …) are implementation modules; the public API is whatsrc/matcalc/__init__.pyre-exports. When adding a new calculator, add the import there. - All modules begin with
from __future__ import annotations(enforced by ruffisort.required-imports). Ruff is run withselect = ["ALL"], so most lints are on; checkpyproject.tomlfor the ignore list before disabling locally. - Docstrings follow Google style. Type hints are required (mypy runs in CI;
no_implicit_optional = false). tests/conftest.pyprovides session-scoped fixtures for common structures (Si,Li2O,LiFePO4,SiO2) and amatpes_calculatorfixture. Reuse these instead of constructing structures inline. The autousesetup_teardownfixture cleans up files created during a test, so don't rely on artifacts persisting between tests.tests/pes/contains pre-trained classical-potential checkpoints (MTP, GAP, NNP, SNAP, ACE, NequIP, DPA3) used bytest_utils.pyto exercise the maml-backed loaders. These are real model files — don't move or rename them without updating the corresponding tests.