This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
NEML2 is a Python-native material modeling library that vectorizes constitutive model evaluation on CPU/GPU using PyTorch as the tensor backend. Models are plain torch.nn.Module subclasses composed from small reusable pieces, the framework auto-resolves dependencies between them, and most users interact via HIT input files (the same format used by MOOSE) plus either the Python API or the neml2-run / neml2-compile CLIs.
The legacy C++ tower from v2.x was retired in the v3 migration. The only C++ that remains is neml2/csrc/aoti/, a thin runtime that loads the AOT-Inductor .pt2 packages produced by neml2-compile; the runtime is built once into neml2/lib/libneml2_aoti.so as part of the wheel and is invisible to most contributors. See for the v2 → v3 rewrite summary.
pip install -e ".[dev]" -v # editable + dev extras (pytest, pre-commit, sphinx, ...)This drives a scikit-build-core build of the small AOTI C++ runtime under build/<wheel_tag>/, then installs the Python package in editable mode. Python source edits take effect immediately; touching anything under neml2/csrc/ or CMakeLists.txt requires a re-pip install to rebuild the runtime.
Package versions and pinned deps live in dependencies.yaml — use python scripts/dep_manager.py {check|list|bump DEP.FIELD VALUE} rather than editing version strings by hand. Files reference their dep with a # dependencies: NAME.FIELD annotation immediately above the version literal. The torch compatibility matrix (compatibility.yaml) is a separate registry checked against the same dependencies.yaml torch entry — keep them in sync via python scripts/compat_matrix.py {seed|check|render}.
cmake --preset dev # configure (Debug)
cmake --build --preset dev # build libneml2_aoti
cmake --preset cc # configure-only; exports compile_commands.json for clangdOnly two presets exist: dev (build) and cc (compile-commands-only, no cmake --build). The wheel build that pip install drives uses cmake.build-type = "Release" internally — there is no developer-facing release preset.
All tests are pytest. Test layout under tests/ is organised by subsystem:
pytest tests/ # everything
pytest tests/test_model.py # one file
pytest tests/test_model.py::test_forward # one function
pytest tests/regression/ # the parametrized regression sweep
pytest tests/verification/ # the parametrized verification sweep
pytest --run-aoti-compile tests/aoti/ # opt in to the slow AOTI compile teststests/regression/test_regression.py and tests/verification/test_verification.py discover scenarios by walking their respective directories for .i files and emit one parametrize id per scenario (the input file's relative path). For spot-checks after a code change, target a single scenario via -k or the full parametrize id:
pytest tests/regression/test_regression.py -k maxwell
pytest 'tests/regression/test_regression.py::test_regression[solid_mechanics/viscoelasticity/maxwell/model.i]'Reserve unfiltered tests/regression/ / tests/verification/ runs for final confirmation.
When adding a Model subclass, use the /add-model skill; for a regression or verification scenario use /add-regression or /add-verification. The skills encode the test conventions so you don't have to rediscover them.
Sphinx with the shibuya theme and MyST-NB. Build with:
sphinx-build -j auto -W --keep-going -b html doc doc/_build/htmlThe /build-docs skill walks through the full pipeline (including notebook execution caching and the auto-generated HIT-syntax catalog). The neml2 package must be importable for autodoc / neml2-syntax to introspect the registered objects — an editable pip install -e ".[dev]" is enough.
The installed wheel exposes four console scripts (defined in pyproject.toml under [project.scripts]):
neml2-run <input.i>— drive a model through a load history.neml2-inspect <input.i>— print the resolved input/output graph of a wired-up input file. Use this beforeneml2-runwhen composing models; wiring bugs surface as obvious mismatches instead of cryptic shape errors deep in Newton.neml2-syntax --section Models --summary— browse the registered-object catalog with one-line docstrings (--type <Name>to drill into one). Run this when planning any new Model or wondering whether a primitive already does what you want.neml2-compile <input.i> --model <name>— export a model to an AOT-Inductor.pt2package + drop-in HIT stub. See for the artifact format and pipeline reference.
neml2-diagnose and neml2-time from v2 are gone.
The Python package layout under neml2/:
model.py—Modelbase class (atorch.nn.Module). All user-authored constitutive leaves inherit from this.factory.py— HIT input parsing vianmhit,load_input/load_model/load_nonlinear_systementry points,[Tensors]namespace for inline Python expressions.schema.py— declarative HIT syntax:input,output,parameter,optionfield helpers +HitSchemacontainer that drives both parsing and the auto-generated docs.chain_rule.py— type aliases for the first / second-order chain-rule sensitivity dicts threaded throughModel.forward(..., v=, v2=, vh=).resolver.py—DependencyResolverbuilds theComposedModeldependency graph from individual leaves' declared inputs and outputs.models/— the composable forward operators.ComposedModelglues children together via the dependency graph;ImplicitUpdatewraps a residual model in a Newton solve with optionalPredictor. Domain libraries live inmodels/{solid_mechanics,chemical_reactions,phase_field_fracture,porous_flow,finite_volume,common,kwn}/; crystal plasticity is a subdirectory ofsolid_mechanics/.types/— typed tensor wrappers (Scalar,Vec,R2,SR2,Rot,Quaternion,MillerIndex, fourth-orderSSR4/WSR4/ ...). Each is a dataclass registered withtorch.utils._pytree.register_dataclassso it round-trips throughtorch.export..dataexposes the underlyingtorch.Tensor.solvers.py—NonlinearSolver(Newton, NewtonWithLineSearch, SchurComplement) andLinearSolver(DenseLU).equation_systems.py—EquationSystem,LinearSystem,NonlinearSystem, sparse/dense assembled vectors and matrices, axis layout. Dense/Block variants (DenseRHS,DenseNewtonStep,DenseIFT,BlockRHS, ...) back both the eager Newton loop and the AOTI implicit-segment lowering.drivers/—TransientDriver,ModelUnitTest,TransientRegression,Verification— the top-level "run a model over a load history" objects exposed in input files.data/—CubicCrystal,CrystalGeometryand related crystallography data classes.user_tensors/— registered[Tensors]block types other thanPython(currently theCSV<Type>family).export.py— adapter aroundtorch.export+torch._inductor.aoti_compile_and_package; the single entry point through which every AOTI lowering passes.cli/aoti_compile.py,cli/aoti_export.py— theneml2-compileorchestration and the per-segment export path; see .aoti/— Python-sideAOTIModelshim that loads a_meta.json+ per-segment.pt2files and exposesforward/jvp/jacobian. Backed by the pybind moduleaoti/_aoti.cxx(which linkslibneml2_aoti.so).pyzag/—NEML2PyzagModeladapter that exposes a NEML2Modelas atorch.nn.Moduleconsumable by the pyzag training library.cli/— backing modules for the four console scripts above.csrc/aoti/— C++ runtime:neml2::aoti::Modelwrapstorch::inductor::AOTIModelPackageLoader. Built intoneml2/lib/libneml2_aoti.soand surfaced through the pybind binding.
Every concrete native object (model, driver, tensor, solver, ...) self-registers via the @register_neml2_object("TypeName") decorator from neml2.factory. HIT input files then instantiate them by type name. Model subclasses declare their input/output/parameter surface via a class-level hit = HitSchema(...) and a from_hit constructor; the @register_neml2_object decorator + HitSchema together feed both the live factory and the auto-generated neml2-syntax catalog.
When adding a new submodule under neml2/models/<domain>/, append the import to the parent __init__.py so import neml2 triggers registration — there is no lazy-loading machinery and unimported modules' types are invisible to the factory.
- Python source: linted and formatted with
ruff(line length 100), CI-enforced via thelintjob in.github/workflows/python.yml. Runpre-commit run --all-filesbefore pushing. - Type-checked with
pyrightagainst the installed package (CI: thetypecheckjob). - Math in docstrings uses MyST dollarmath (
$x$inline,$$...$$display); MySTdollarmathandamsmathextensions are enabled indoc/conf.py. Code references stay in`backticks`; the difference matters for rendering in the syntax catalog. - HIT inputs use the
nmhitPython parser (also a pre-commitnmhit-formathook). The format itself is unchanged from v2. - Notebooks under
doc/content/tutorials/**.ipynbare paired with.mdmirrors viajupytext --sync(pre-commit hook);.jupytext.tomldeclares the format pairing. Edit the.ipynb, never the paired.md. - Copyright headers are checked by a pre-commit hook (
python scripts/check_copyright.py); the script auto-fixes missing headers. - Avoid editing files in
build/,installed/, ordoc/_build/,doc/generated/— those are generated.scripts/clobber.sh [dir]removes git-ignored files if a build gets wedged.