Skip to content

WIP: MACE foundation-model integration (small / medium / mpa-0 / matpes parity) - #558

Draft
PythonFZ wants to merge 118 commits into
mainfrom
feat/mace-foundation-integration
Draft

WIP: MACE foundation-model integration (small / medium / mpa-0 / matpes parity)#558
PythonFZ wants to merge 118 commits into
mainfrom
feat/mace-foundation-integration

Conversation

@PythonFZ

@PythonFZ PythonFZ commented May 4, 2026

Copy link
Copy Markdown
Contributor

Warning

Claude Code against mace-torch and mace-jax. I have not written this code!

Summary

Integrates torch-mace foundation models into apax via a new apax convert-mace path. End-to-end parity (within rtol 1e-4 energy / rtol 1e-3 force) on:

  • MACE-MP-0 small (Residual, scalar-only)
  • MACE-MP-0 medium (Residual, multi-irrep)
  • MACE-MPA-0 medium (Density variants + ZBL + AgnesiTransform)
  • MACE-matpes-r2scan-omat-ft (same architecture as mpa-0)

55 commits ahead of main. Marked draft / WIP for review while the broader feature branch settles; the most recent commit (34188618) closes the mpa-0 / matpes parity gap.

What's in the latest commit

Two design specs landed in tandem:

  • docs/superpowers/specs/2026-05-04-mace-density-zbl-design.md
  • docs/superpowers/specs/2026-05-04-mace-distance-transform.md

Architecture

  • _interaction_scaffold helper + three interaction blocks (Residual / Density / DensityResidual) with per-layer dispatch in MaceRepresentation.
  • MaceZBLPairRepulsion empirical correction (faithful port of mace.modules.radial.ZBLBasis); new output_scale field reproduces torch's "ZBL inside scale_shift" semantics without mutating buffers.
  • AgnesiTransform Linen module + MaceRadialEmbedding refactor that threads Z/idx through and applies the transform between cutoff and bessel.
  • MaceModelConfig.interaction_cls widened to a Literal union (single str OR per-layer list); new distance_transform discriminated union; MaceZBLPairRepulsion correction config.

Converter

  • Per-layer interaction-class detection; emits a single str when uniform, list otherwise.
  • Density density_fn weight scatter; size-aware _scatter_o3_linear_blocks for non-uniform layer-1 linear shapes; per-path skip_tp scatter.
  • _map_pair_repulsion and _map_distance_transform helpers respect both params (trainable) and buffers (fixed) collections.
  • _map_scale_shift collapses 2-D atomic_energies tables (matpes ships (1, 89) even single-head).

Bug fixes worth calling out

  • check_for_ensemble crashed on 0-d scalar buffers — treats ndim==0 as size 1 (preserves ensemble semantics; jnp.stack lifts every leaf to >=1-D).
  • AgnesiTransform clips r and r_0 to 0.02 so masked / padding edges with dr=0 don't trigger inf gradients via x^(-3.66).
  • Periodic SiO₂ test fixture bumped to [14, 14, 14] to avoid an unrelated apax PBC bug (NaN forces from self-image edges) that exists for every foundation; documented in fixture docstring.

Test plan

  • uv run pytest tests/unit_tests/ --ignore=tests/unit_tests/md — 93 pass, 0 fail.
  • uv run pytest tests/integration_tests/mace -m mace_parity — 17 pass (small / medium / mpa-0 water; matpes periodic SiO₂; ZBL dimer parity at rtol 1e-12).
  • Reviewer: confirm new ZBL output_scale semantics make sense for fresh-trained apax models (default 1.0 = no scaling).
  • Reviewer: cross-check _map_scale_shift 2-D atomic_energies handling vs. multi-head matpes when an explicit head is passed.
  • Address the orthogonal apax PBC bug (NaN forces with self-image edges in tight cells) in a follow-up.

Out of scope

  • RealAgnostic (non-residual non-density) interaction variant — not used by foundations in scope; additive when a model needs it.
  • SoftTransform (the tanh-based distance transform) — additive one-class extension.
  • Multi-head foundation training. Single-head loading (matpes / mpa-0) works.
  • Fresh end-to-end training of a Density-variant model — covered by P4 work, not parity-tested here.

🤖 Generated with Claude Code

PythonFZ and others added 30 commits April 20, 2026 18:03
Design spec for integrating MACE-MP and MACE-MPA foundation models as a
native linen descriptor in apax, with an optional apax[mace] extra
(e3nn-jax + cuequivariance-jax) and a convert-mace CLI. Fine-tuning
reuses existing shallow-ensemble and property-head infrastructure.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tasked plan covering P0–P6: plumbing, native MACE forward pass in linen,
cuequivariance dispatch, torch→apax weight converter + foundation loader,
fine-tuning with shallow ensembles, JAX-MD integration, and benchmarks.

Companion to spec at
docs/superpowers/specs/2026-04-20-mace-foundation-model-integration-design.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Converter CLI now accepts canonical model names (e.g. "medium-mpa-0")
  resolved via mace.calculators.foundations_models.mace_mp(), inheriting
  upstream's bundled-local / cache / download behavior.
- Parity tests compare against the upstream MACECalculator (ASE wrapper)
  rather than just the raw torch module — matches what downstream users see.
- Stress parity + finite-difference force-consistency tests added.
- Dev-only `mace-convert` uv dependency group isolates torch/mace-torch
  from the default apax install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implements the smooth polynomial envelope cutoff from Klicpera et al. 2020,
used by MACE, as an nn.Module alongside existing BesselBasis/GaussianBasis.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…sk stub

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds MaceModelConfig pydantic class with all MACE-specific fields
(r_max, num_bessel, num_polynomial_cutoff, max_ell, hidden_irreps,
num_interactions, correlation, interaction_cls, use_cueq) plus
foundation-model loading fields (pretrained, freeze_backbone,
unfreeze_backbone_epoch). Appends to ModelConfig union alongside
existing GMNNConfig, EquivMPConfig, So3kratesConfig.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Narrow pretrained field from Optional[Union[str, Path]] to Optional[str],
remove now-unused pathlib import, and update the class docstring to say
"model" not "descriptor", use PositiveFloat for r_max type, and use
numpy-style parameter formatting without inline-union syntax.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add integration smoke test that trains the MACE P0 skeleton for 1 epoch
on MD22 stachyose, verifying the full pipeline (config validation → builder
→ data pipeline → trainer → checkpoint writing). Also adds "default" to the
optimizer partition map so unknown parameter names (e.g. skeleton_w) fall
back to nn_lr rather than raising a ValueError.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All 6 P0 tasks landed on this branch (a5888ca..bc5a7f8).
Tick step-level checkboxes so the plan reflects reality and note
completion in the phase marker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds apax/layers/descriptor/mace_blocks.py with assemble_edge_features,
the first P1 building block for MaceRepresentation: computes Bessel radial
basis × polynomial cutoff and spherical harmonics of edge vectors.

Covers P1.1 of the MACE foundation-model integration plan.
…ojection, add tests)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires P1.1–P1.4 blocks into MaceRepresentation:
LinearNodeEmbedding -> N x (InteractionBlock -> ProductBlock) -> concat scalars.

Also adds force_irreps_out=True to InteractionBlock.skip_linear so the
residual sum stays shape-consistent on the first layer, where node_feats
is scalar-only and a pure Linear can't reach higher-l channels.

Mask helpers are inlined rather than imported from so3krates, which
top-level-imports an unavailable myrto dependency in this env.

Closes P1 exit criterion — 19/19 descriptor + block + builder tests green.
P1 (native MACE forward pass) complete on feat/mace-foundation-integration
through a0c22c3; 19/19 descriptor + block + builder unit tests green.

Optional P1.5 Step 3 (mace-jax random-weight parity test) is deferred to
P3's parity phase.
Registers the convert-mace CLI command with typer, stubs run_conversion
and load_mace_foundation in transfer_learning/mace_foundation.py, and
adds unit tests that verify graceful failure when torch is absent and
correct argument signature.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…fixes)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… partial)

Implements Steps 1, 2a, 3, 5, 6, 7 of P3.2. Adds mace-convert dep group,
run_conversion orchestration, all helpers (_load_torch_foundation_model,
_extract_config_from_torch, _validate_no_nan, _extract_norm_consts,
_torch_mace_version, _apax_version), gated integration tests (3 skip without
torch), and _map_state_to_pytree stub with deferred-mapping docstring.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Implement load_mace_foundation() to deserialize a converted .apax/
directory (params.msgpack + config.json) into a flax pytree compatible
with MaceRepresentation.init. Adds optional num_elements field to
MaceModelConfig and _resolve_short_name() for huggingface_hub fallback.
No torch imports in the loader path.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
P3.4: Add apax/nn/mace_foundation_model.py skeleton (MaceFoundationEnergyModel,
__call__ raises NotImplementedError pending P3.2 Step 4); add Notes section to
MaceRepresentation docstring flagging future return_per_layer_node_feats flag;
create gated integration tests in test_mace_parity.py (all skip without torch).

P3.5: Add _is_mace_foundation_dir helper and early NotImplementedError guard in
ASECalculator.__init__; add tests/unit_tests/md/__init__.py and gated unit test
that verifies the error is raised with the correct hint (1 test, passes).

P3.6: Enhance _extract_config_from_torch head validation to raise ValueError
with clear guidance when an unknown head name is requested on a multi-head model;
append test_convert_rejects_unknown_head to test_convert.py (skips without torch).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Flip P3.1/P3.3 and the scaffolded steps of P3.2/P3.4/P3.5/P3.6 to [x].
Annotate the four deferred boxes (P3.2 Step 4 map body; P3.4 Steps
2/3/6 per-layer feats, foundation-model __call__, parity iteration)
with a pointer to the `uv sync --group mace-convert --extra mace`
workflow that unblocks them. Update the Progress preamble with the
four P3 scaffolding commits and the current gap.
…ftMACE

Foundation models expose most hyperparameters only through submodules
rather than top-level attributes. Walk the submodule tree to read
max_ell, hidden_irreps, correlation, num_elements, num_bessel,
num_polynomial_cutoff, scale/shift, atomic_numbers, and the interaction
variant. Add ``test_extract_config_from_torch_small`` asserting these
match the known MACE-MP-0 small config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… parity)

Add ``LinearReadoutBlock``, ``NonLinearReadoutBlock``, and ``ScaleShift``
mirroring torch-mace's primitives. ``NonLinearReadoutBlock`` multiplies
SiLU output by torch-e3nn's ``normalize2mom`` constant for bit-for-bit
parity. Unit tests assert shape/finite and affine behaviour.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement the full forward pass mirroring torch-mace's ScaleShiftMACE for
single-head, scalar-only-hidden foundation models (MACE-MP-0 small):

- InteractionBlock gains a ``foundation_mode`` that uses a per-element
  ``PerElementSkipTP`` (the torch ``skip_tp``), divides the conv message by
  ``avg_num_neighbors``, and disables the radial-MLP output activation.
- ProductBlock gains ``input_irreps`` (separate from ``hidden_irreps``) and
  an optional ``post_linear`` to match torch's Equivariant product basis
  layout.
- New ``_scalar_irreps_only`` / ``_default_target_irreps`` helpers.
- ``MaceFoundationEnergyModel.__call__`` composes these plus the readout/
  scale-shift blocks to emit a per-atom energy. Maps input Z through the
  model's own atomic-numbers table (required by the upstream indexing
  convention) and flips dr_vec sign to match torch's edge convention.

Smoke test asserts finite output shape ``(n_atoms,)``. Parity mapper and
end-to-end test follow in subsequent commits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PythonFZ and others added 30 commits May 5, 2026 17:44
… leaf paths in reset_layers

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…MACE property heads

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…riminator

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Task 4's integration tests revealed that e3nn_jax's Linear layer encodes the
output irreps in the parameter dict key string, so n_shallow_ensemble width
changes produce sibling orphan leaves rather than same-path shape changes.
Task 1's same-path shape check therefore never fires for the canonical
foundation→ensemble workflow. Task 1.5 adds a structural-mismatch pass that
groups leaves by parent path, detects parents with orphans on BOTH sides,
and emits target paths in the suggested reset_layers snippet.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nn-named keys

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…smatch workflow

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…_finetune_minimal

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the parent ModelBuilder guard (GMNN/EquivMP/So3krates + kind='mace')
explained why it errored but did not tell the user how to recover. Match the
tone of the sibling MaceBuilder guard (which tells MACE+kind='standard' users
to set kind='mace' or switch model) by suggesting kind='standard' or a model
change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sweep of branch review feedback. Removes anything that was speculative,
shimmed, or duplicated; tightens the parts that remain.

Workarounds replaced with real fixes:
- delete apax/utils/parity_debug.py and scripts/mace_layer_parity.py;
  remove all sow("debug", ...) gates and DenyList("debug") workarounds
- _map_pair_repulsion / _map_distance_transform dispatch off
  config.<...>.trainable instead of try-buffers-then-params; wire
  _map_distance_transform into _map_state_to_pytree (was unreachable)
- _map_readouts iterates linear slot keys via _scatter_o3_linear_blocks
  instead of literal "w[0,0] 128x0e,1x0e" — works for non-MP-0 widths
- check_for_ensemble filters by "params" collection instead of treating
  0-d leaves as size-1
- AgnesiTransform / MaceZBLPairRepulsion rewritten as @nn.compact so all
  variables resolve through .value (drops hasattr-based _scalar helper)

Dead code / placeholders dropped (no forward-compat shims):
- model.descriptor.use_cueq (raised NotImplementedError; reserved for P2)
- model.readout.kind and PropertyHead.kind (architecture follows the
  parent model type; "standard" energy-head fallback was a footgun)
- InteractionBlock = InteractionBlockResidual back-compat alias
- assemble_edge_features shim and its tests
- M parameter on _scatter_o3_linear_blocks (noqa-marked back-compat)
- InteractionBlockDensity.hidden_irreps "for parity" field
- layer_idx fields on all three interaction blocks + ProductBlock
- **kwargs swallowers in simulate.py energy wrappers
- build_hessian_neighbor_fns one-line alias
- Optional[List]/Optional[dict] widening + normalize_null validator on
  TransferLearningConfig, OptimizerConfig.kwargs, LossConfig.parameters,
  DataConfig.{shift,scale}_options
- getattr(self.model, "disable_cell_list", False) — disable_cell_list
  and nl_skin lifted to ApaxBase

Refactors:
- _build_mace_readout helper unifies energy + property head construction
- ModelBuilder.build_readout no longer knows about MACE
- _path_to_str helper replaces 5+ duplicated path-stringifying calls in
  parameter_transfer.py
- BOHR_RADIUS_ANG / COULOMB_EV_ANG / DR_FLOOR hoisted to module level
- except Exception narrowed to (ImportError, AttributeError) /
  (AttributeError, RuntimeError) in version probes and norm-const probe
- loss functions: drop mutable {} defaults, parameters/inputs are now
  required positional args; mass_weighted_hessian_loss gets a real
  numpy-style docstring

Comment / docstring trims throughout; removed history-narrating prose
that documented prior refactors. All 142 unit tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Layer-by-layer parity harness consumed via the parity_debug sow
mechanism that was removed; the script is no longer functional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The MACE foundation converter was pulling
``mace_jax.adapters.cuequivariance.symmetric_contraction._convert_native_weights``
through the ``mace-convert`` extra. mace-jax is not on PyPI, was pinned by
git URL, and the function we needed was private. The dense linear-algebra
work is straightforward to reproduce directly on the cuequivariance
descriptor that apax already builds, so vendor it locally.

- new apax/transfer_learning/torch_sc_adapter.py with a public
  ``convert_native_weights`` entry point. Reduced + full-CG paths both
  supported. Full-CG transform's design-matrix solve replaces mace-jax's
  nnx ``SymmetricContraction`` with a direct ``cuex.equivariant_polynomial``
  application of the same descriptor.
- mace_foundation._map_products now imports from the local adapter.
- pyproject: remove mace-jax + its [tool.uv.sources] git pin; promote
  mace-convert from [dependency-groups] to [project.optional-dependencies]
  so ``pip install apax[mace-convert]`` works.
- rename in-package private symbol _INTERACTION_BLOCK_CLS ->
  INTERACTION_BLOCK_CLS (consumed by MaceRepresentation and tests).

Verified: 142 unit tests pass; all 3 mace_parity converter tests pass
against MACE-MP-0 small.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… like MPA-0

The converter wired ``_map_distance_transform`` to look at
``representation/distance_transform/...``, but the AgnesiTransform is a
field of MaceRadialEmbedding and is called from inside its forward pass —
not from MaceRepresentation. Linen materialises the slot under the parent
that actually invokes it, so the real path is
``representation/radial_embedding/distance_transform/...``.

The dead ``distance_transform: Any`` field on MaceRepresentation never
created a slot (linen only registers submodules that are called); it was
stale state from an earlier design. Drop the field, fix the converter
path, update the tests that used to pass it.

Regression test added: ``test_convert_medium_mpa0_with_distance_transform``
runs the full converter against MACE-MPA-0 medium and asserts that the
AgnesiTransform's ``a`` / ``q`` / ``p`` / ``covalent_radii`` are present
in the converted pytree with values matching the torch source.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phased plan for hardening the MACE integration and restoring its feature
path via direct commits on feat/mace-foundation-integration. Bucket A
(general UQ) shipped separately in PR #561. Phase 1 = feature path +
converter/base-builder hardening + D-guards + doc-deck fixes (math path
untouched); Phase 2 = decouple MaceReadout from descriptor internals,
gated on reproducing 1.6e-7 eV parity; Phase 3 = doc the new contract.
Bucket D first-block math deferred to a loud guard (YAGNI).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
MaceBuilder.build_readout(is_feature_fn=True) routed to the base
AtomisticReadout, built from the inherited-but-unused `nn` field. Training
and the foundation converter only ever populate MaceReadout params, so the
feature path produced an untrained random MLP (or a param-tree mismatch on
restored params) — breaking ASE get_descriptors and BAL for MACE.

Return None for the MACE feature readout so FeatureModel emits the real
per-atom descriptor features (n_atoms, num_interactions * hidden_dim).
Partial-layer feature extraction (only_use_n_layers) now raises a clear
NotImplementedError, deferred to the upcoming per-layer feature contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…drift

The foundation converter indexed auto-generated Flax module slots directly
(rep_params[f"InteractionBlock_{k}"], ProductBlock_{k}, readout_{k}). Renaming
any submodule in apax/layers/descriptor would surface as a bare KeyError deep
inside a mapping loop, with no hint of the cause.

Add _require_slots(prefix, count): it resolves the numbered slots by position
and raises an actionable KeyError naming the missing slot and the keys that are
present, pointing at the converter mapping. Wire it into _map_interactions,
_map_products and _map_readouts. Behavior is unchanged for a matching tree
(same dicts, mutated in place); only the failure mode improves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…criminate transform union

Three convention-alignment fixes (behavior-preserving):

- Move the MACE-specific "standard" bessel variant (MaceBesselBasis) out of the
  shared ModelBuilder.build_basis_function and into MaceBuilder.build_basis_function.
  The base now handles only the apax-native kocer bessel and raises for unknown
  variants, so the shared helper no longer references a MACE class.
- Replace the converter's duplicated `_N_SPECIES = 119` with the canonical
  `DEFAULT_N_SPECIES` exported from apax.nn.builder (single source of truth, also
  the ModelBuilder default).
- Make DistanceTransformConfig a name-discriminated union (Annotated + Field
  discriminator), matching the sibling InteractionConfig and surfacing the
  discriminator in the JSON schema.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Sharpen the converter's interaction-block guard to call out that the
  non-residual first-layer block 'RealAgnosticInteractionBlock' is
  intentionally not ported (every foundation in scope is Residual-first;
  YAGNI), so an unsupported foundation fails loudly with guidance.
- Document the edge-vector sign convention at the spherical-harmonics site in
  mace.py: apax uses dr_vec = R_sender - R_receiver (opposite of torch-mace);
  the scalar readout is invariant under that inversion, so parity holds to
  ~1e-7 eV. Warns against "fixing" the sign without re-checking parity.
- Replace the discarded _scalar_irreps_only(...) call with an explicit
  num_features == 0 guard (same ValueError contract, clearer intent) and delete
  the now-unused helper.
- Delete the never-called _extract_norm_consts() dead code.
- Convert an adjacent dict() to a literal (C408) in the file being touched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Slide 4: the multi-head readout's first linear uses
  instructions_1 = [(0, i) for i in range(num_heads)] (one input chunk fans
  out to every head), not [(i, i) ...] (that is instructions_2). Drop the
  apax-only n_out factor from the MACE irreps_out (PR #800 heads emit a single
  0e).
- Slides 1/4/6/8/9: soften the "single backward pass" claim for apax forces.
  ShallowEnsembleModel uses jax.jacrev over the (n_ensemble,) energy vector =
  O(n_ensemble) VJPs, vectorized/JIT-fused over a shared descriptor forward and
  optionally chunked — its advantage over PR #800 is vectorization and the
  shared forward, not a literal single VJP.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…width

MaceReadout took hidden_dim as a config arg that the builder computed from the
hidden_irreps string, independently of what MaceRepresentation actually emits —
the B1 coupling: a change to the descriptor's per-layer packing would silently
corrupt the readout's reshape.

Derive hidden_dim inside MaceReadout from the real per-atom feature width
(width // num_interactions) and raise a clear ValueError if the width is not an
exact multiple (a layout mismatch). Drop the hidden_dim arg from the module and
from _build_mace_readout. num_interactions stays: it is a shared architectural
constant (len(interactions)), not a descriptor-internal layout detail.

The reshape math is unchanged (derived hidden_dim equals the previous value), so
the foundation parity tests still match torch-mace to ~1e-7 eV (verified:
test_mace_parity / test_mace_s22_parity, 30 passed).

Note: the flat (n_atoms, n_features) descriptor contract and the 2D
get_descriptors/BAL feature path are intentionally preserved (emitting a 3D
per-layer tensor would break them for no added safety). Partial-layer feature
extraction therefore remains deferred behind the clear NotImplementedError.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…readout pattern

- Update the design spec's Phase 2/3 to the approach actually shipped
  (1bfd3be): MaceReadout derives hidden_dim from the per-atom feature width
  instead of the planned 3D descriptor contract, which would have broken the
  2D get_descriptors/BAL path for no safety gain. Records that partial-layer
  feature extraction stays deferred and that parity (30 passed) gated the change.
- Add a note to the onboarding doc (adding_new_models.html) describing how a
  per-layer-readout model (MACE) keeps the flat (n_atoms, n_features) contract
  and derives its per-layer split from num_interactions + the input width,
  rather than passing a redundant width that can desync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ation

Two convention cleanups surfaced by a self-review against apax practices:

- MaceBuilder.build_readout dispatched the energy head via object identity
  (`head_config is self.config`), which silently misroutes an equal-but-copied
  config dict to the property-head branch (KeyError on the missing top-level
  MLP_irreps). Dispatch structurally on the presence of the nested `readout`
  group instead. Behavior is unchanged on every real call path (energy /
  feature / property), so foundation parity is unaffected.
- Re-export MaceRepresentation from apax.layers.descriptor (the other three
  descriptors already are) and import it via the package in builder.py, for
  consistency with GMNN/EquivMP/So3krates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- build_radial_function now raises a clear NotImplementedError when the config
  lacks n_radial/emb_init (e.g. MACE), instead of a bare KeyError, since MACE
  builds its radial path in MaceBuilder directly. Closes the latent M1 trap.
- Add _require_key for fixed-name converter slots and route the two remaining
  hardcoded singletons through it / _require_slots: LinearNodeEmbedding_0 and
  the radial_embedding distance_transform buffer. Behavior is unchanged for a
  matching tree (same objects, mutated in place); only the drift failure mode
  improves, completing the converter-resilience work from cb6487c.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two test files weren't in the targeted suites run during their phases and so
broke silently on already-committed changes:

- test_basis_variant_dispatch.py asserted the base ModelBuilder builds
  MaceBesselBasis for the "standard" variant, but 3cd1069 moved that into
  MaceBuilder. Route "standard" through MaceBuilder (base owns "kocer" only).
- test_property_heads.py asserted readout.hidden_dim, removed from MaceReadout
  in 1bfd3be (now derived from the input width). Drop the stale assertion.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…g fields (S4)

MaceModelConfig inherited nn/w_init/b_init/use_ntk from BaseModelConfig, but
MACE uses MaceReadout (configured by readout.MLP_irreps) and never reads them —
so under extra="forbid" a user could set them in YAML and have them silently
ignored.

Introduce MLPReadoutModelConfig(BaseModelConfig) holding those four fields, and
make GMNN/EquivMP/So3krates extend it. MaceModelConfig extends BaseModelConfig
directly, so those fields are now rejected for MACE rather than ignored. Remove
the dead nn/b_init lines from the MACE integration config that this surfaced.

GMNN/EquivMP/So3krates keep the fields (builder + PropertyHead still read them);
the only behavior change is loud rejection of MLP-readout fields on MACE configs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pre-commit.ci pins ruff v0.15.12, newer than the repo's dev-env ruff, so it
flagged the branch's MACE files. Fix both failures:

- ruff-check: split the E702 multi-statement (semicolon) lines in
  test_mace_parity.py into separate statements (semantics unchanged).
- ruff-format: apply v0.15.12 formatting to the branch-touched MACE files.

Verified with `uvx prek run --all-files` (honors the pinned v0.15.12): all
hooks pass. Formatting only — no behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…mpat)

jax_md's quantity/simulate path calls the energy fn with extra kwargs (e.g.
kT), so the inner full_ensemble/shallow_ensemble/single_model must accept
**kwargs. A prior branch refactor (8cbfdd9) dropped them, regressing vs main
and breaking every jax-md run with:

    TypeError: create_energy_fn.<locals>.shallow_ensemble() got an unexpected
    keyword argument 'kT'

Restore **kwargs (matching origin/main). Fixes test_run_md,
test_constrained_jaxmd, test_jaxmd_schedule_and_thresold and the ApaxJaxMD
node test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
test_convert_medium_mpa0_with_distance_transform hardcoded
"/Users/fzills/tools/apax/tmp/mace-mpa-0-medium.model", which FileNotFounds on
any machine other than the author's (CI included). Load the same torch
foundation the converter uses via _load_torch_foundation_model("medium-mpa-0")
(resolved/cached by mace_mp). Drop the now-unused torch import.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant