matterlens 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matterlens-0.0.1/.github/workflows/ci.yml +16 -0
- matterlens-0.0.1/.github/workflows/publish.yml +41 -0
- matterlens-0.0.1/.gitignore +13 -0
- matterlens-0.0.1/PKG-INFO +66 -0
- matterlens-0.0.1/README.md +33 -0
- matterlens-0.0.1/docs/architecture-survey.md +203 -0
- matterlens-0.0.1/docs/case-studies.md +99 -0
- matterlens-0.0.1/docs/prior-art.md +115 -0
- matterlens-0.0.1/docs/releasing.md +24 -0
- matterlens-0.0.1/docs/v0-design.md +232 -0
- matterlens-0.0.1/pyproject.toml +43 -0
- matterlens-0.0.1/scripts/introspect/common_tree.py +28 -0
- matterlens-0.0.1/scripts/introspect/mace_tree.py +35 -0
- matterlens-0.0.1/scripts/introspect/orb_tree.err +5 -0
- matterlens-0.0.1/scripts/introspect/orb_tree.out +124 -0
- matterlens-0.0.1/scripts/introspect/orb_tree.py +22 -0
- matterlens-0.0.1/scripts/introspect/sevenn_tree.py +17 -0
- matterlens-0.0.1/scripts/introspect/uma_tree.err +4 -0
- matterlens-0.0.1/scripts/introspect/uma_tree.out +143 -0
- matterlens-0.0.1/scripts/introspect/uma_tree.py +30 -0
- matterlens-0.0.1/scripts/spike_hooked_mace.py +99 -0
- matterlens-0.0.1/src/matterlens/__init__.py +7 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
check:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v4
|
|
13
|
+
- uses: astral-sh/setup-uv@v6
|
|
14
|
+
- run: uv build
|
|
15
|
+
- run: uvx ruff check src
|
|
16
|
+
- run: uv run --isolated --no-project --python 3.12 --with dist/*.whl python -c "import matterlens"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Builds with uv and publishes via PyPI trusted publishing (OIDC), no API token.
|
|
4
|
+
# One-time setup on pypi.org: Publishing -> add a pending publisher for project
|
|
5
|
+
# "matterlens" with owner Garden-AI, repository matterlens, workflow
|
|
6
|
+
# publish.yml, environment pypi. Then push a tag like v0.0.1.
|
|
7
|
+
|
|
8
|
+
on:
|
|
9
|
+
push:
|
|
10
|
+
tags: ["v*"]
|
|
11
|
+
workflow_dispatch:
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v6
|
|
19
|
+
- name: Build sdist and wheel
|
|
20
|
+
run: uv build
|
|
21
|
+
- name: Smoke-test the wheel in a clean environment
|
|
22
|
+
run: uv run --isolated --no-project --python 3.12 --with dist/*.whl python -c "import matterlens; print(matterlens.__version__)"
|
|
23
|
+
- uses: actions/upload-artifact@v4
|
|
24
|
+
with:
|
|
25
|
+
name: dist
|
|
26
|
+
path: dist/
|
|
27
|
+
|
|
28
|
+
publish:
|
|
29
|
+
needs: build
|
|
30
|
+
runs-on: ubuntu-latest
|
|
31
|
+
environment: pypi
|
|
32
|
+
permissions:
|
|
33
|
+
id-token: write
|
|
34
|
+
steps:
|
|
35
|
+
- uses: actions/download-artifact@v4
|
|
36
|
+
with:
|
|
37
|
+
name: dist
|
|
38
|
+
path: dist/
|
|
39
|
+
- uses: astral-sh/setup-uv@v6
|
|
40
|
+
- name: Publish
|
|
41
|
+
run: uv publish
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: matterlens
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Hooks, caches, and lenses for machine-learning interatomic potentials and other geometric GNNs (the TransformerLens idea, for MLIPs).
|
|
5
|
+
Project-URL: Homepage, https://github.com/Garden-AI/matterlens
|
|
6
|
+
Project-URL: Repository, https://github.com/Garden-AI/matterlens
|
|
7
|
+
Author-email: Owen Price Skelly <owenpriceskelly@uchicago.edu>
|
|
8
|
+
License: MIT
|
|
9
|
+
Keywords: equivariant,gnn,interatomic-potential,interpretability,mace,mlip,uma
|
|
10
|
+
Classifier: Development Status :: 1 - Planning
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
16
|
+
Requires-Python: >=3.11
|
|
17
|
+
Requires-Dist: ase>=3.23
|
|
18
|
+
Requires-Dist: numpy
|
|
19
|
+
Requires-Dist: torch>=2.4
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
22
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
23
|
+
Provides-Extra: fairchem
|
|
24
|
+
Requires-Dist: fairchem-core>=2.20; extra == 'fairchem'
|
|
25
|
+
Provides-Extra: mace
|
|
26
|
+
Requires-Dist: mace-torch>=0.3.15; extra == 'mace'
|
|
27
|
+
Requires-Dist: torch<2.10; extra == 'mace'
|
|
28
|
+
Provides-Extra: orb
|
|
29
|
+
Requires-Dist: orb-models<0.8,>=0.6.2; extra == 'orb'
|
|
30
|
+
Provides-Extra: sevenn
|
|
31
|
+
Requires-Dist: sevenn>=0.10.0; extra == 'sevenn'
|
|
32
|
+
Description-Content-Type: text/markdown
|
|
33
|
+
|
|
34
|
+
# matterlens
|
|
35
|
+
|
|
36
|
+
Hooks, caches, and lenses for machine-learning interatomic potentials and
|
|
37
|
+
other geometric GNNs. The TransformerLens idea, built for models whose
|
|
38
|
+
activations are per-atom irreps rather than a token residual stream.
|
|
39
|
+
|
|
40
|
+
Status (2026-09-10): design stage. Read `docs/v0-design.md` for what the first
|
|
41
|
+
release is and why. `docs/architecture-survey.md` records what MACE, SevenNet,
|
|
42
|
+
Orb, and UMA module trees actually look like; `docs/prior-art.md` records the
|
|
43
|
+
landscape. `scripts/spike_hooked_mace.py` is a runnable feasibility spike.
|
|
44
|
+
|
|
45
|
+
Intended v0 usage:
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from matterlens import load
|
|
49
|
+
hm = load("mace", "mace-mp-0-medium") # native mace-torch model underneath
|
|
50
|
+
batch = hm.from_atoms(atoms)
|
|
51
|
+
out, cache = hm.run_with_cache(batch)
|
|
52
|
+
cache["blocks.1.node_feats"].shape # [n_atoms, 256] as 128x0e+128x1o
|
|
53
|
+
cache.blocks("blocks.1.node_feats")[1] # [n_atoms, 128, 3] the vector channels
|
|
54
|
+
with hm.hooks({"blocks.0.node_feats": lambda a, hp: torch.zeros_like(a)}):
|
|
55
|
+
ablated = hm.model(batch)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Part of the Amatterlens program (interpretability for scientific discovery),
|
|
59
|
+
workstream 1. Companion to Rootstock, which supplies the isolated
|
|
60
|
+
per-family environments each adapter installs into.
|
|
61
|
+
|
|
62
|
+
Run the spike:
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv run scripts/spike_hooked_mace.py
|
|
66
|
+
```
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# matterlens
|
|
2
|
+
|
|
3
|
+
Hooks, caches, and lenses for machine-learning interatomic potentials and
|
|
4
|
+
other geometric GNNs. The TransformerLens idea, built for models whose
|
|
5
|
+
activations are per-atom irreps rather than a token residual stream.
|
|
6
|
+
|
|
7
|
+
Status (2026-09-10): design stage. Read `docs/v0-design.md` for what the first
|
|
8
|
+
release is and why. `docs/architecture-survey.md` records what MACE, SevenNet,
|
|
9
|
+
Orb, and UMA module trees actually look like; `docs/prior-art.md` records the
|
|
10
|
+
landscape. `scripts/spike_hooked_mace.py` is a runnable feasibility spike.
|
|
11
|
+
|
|
12
|
+
Intended v0 usage:
|
|
13
|
+
|
|
14
|
+
```python
|
|
15
|
+
from matterlens import load
|
|
16
|
+
hm = load("mace", "mace-mp-0-medium") # native mace-torch model underneath
|
|
17
|
+
batch = hm.from_atoms(atoms)
|
|
18
|
+
out, cache = hm.run_with_cache(batch)
|
|
19
|
+
cache["blocks.1.node_feats"].shape # [n_atoms, 256] as 128x0e+128x1o
|
|
20
|
+
cache.blocks("blocks.1.node_feats")[1] # [n_atoms, 128, 3] the vector channels
|
|
21
|
+
with hm.hooks({"blocks.0.node_feats": lambda a, hp: torch.zeros_like(a)}):
|
|
22
|
+
ablated = hm.model(batch)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Part of the Amatterlens program (interpretability for scientific discovery),
|
|
26
|
+
workstream 1. Companion to Rootstock, which supplies the isolated
|
|
27
|
+
per-family environments each adapter installs into.
|
|
28
|
+
|
|
29
|
+
Run the spike:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
uv run scripts/spike_hooked_mace.py
|
|
33
|
+
```
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Architecture survey: what the module trees actually look like
|
|
2
|
+
|
|
3
|
+
Observed 2026-09-10 by loading each pretrained model on CPU (macOS arm64) and
|
|
4
|
+
registering forward hooks on every submodule (depth 1 to 3 depending on family). Shapes are for
|
|
5
|
+
the test structure named in each section. UMA was loaded with a
|
|
6
|
+
HuggingFace token on the second pass (2026-09-10). This is the ground truth the adapter
|
|
7
|
+
tables in `docs/v0-design.md` rest on; re-run the introspection scripts when
|
|
8
|
+
bumping a package.
|
|
9
|
+
|
|
10
|
+
Package versions: mace-torch 0.3.x (latest as of run), sevenn 0.10.x, orb-models
|
|
11
|
+
0.7.x, fairchem-core 2.x (latest PyPI release as of run).
|
|
12
|
+
|
|
13
|
+
## MACE (`mace-torch`), model `mace-mp-0 small`, H2O in a box (3 atoms, 6 edges)
|
|
14
|
+
|
|
15
|
+
Class `mace.modules.models.ScaleShiftMACE`. Forward is one long method over a
|
|
16
|
+
dict batch; blocks are three parallel `ModuleList`s indexed together.
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
node_embedding LinearNodeEmbeddingBlock out (3,128) irreps 89x0e -> 128x0e
|
|
20
|
+
radial_embedding RadialEmbeddingBlock out ((6,10), None) tuple: (edge_feats, cutoff)
|
|
21
|
+
spherical_harmonics SphericalHarmonics out (6,16) 1x0e+1x1o+1x2e+1x3o
|
|
22
|
+
atomic_energies_fn AtomicEnergiesBlock out (3,1)
|
|
23
|
+
interactions.{0,1} RealAgnosticResidualInteractionBlock out ((3,128,16), (3,128)) tuple: (message, sc)
|
|
24
|
+
.linear_up .conv_tp .conv_tp_weights .linear .skip_tp .reshape
|
|
25
|
+
products.{0,1} EquivariantProductBasisBlock out (3,128) node_feats, irreps 128x0e (L=0 model)
|
|
26
|
+
.symmetric_contractions .linear
|
|
27
|
+
readouts.0 LinearReadoutBlock out (3,1) per-atom energy contribution, layer 0
|
|
28
|
+
readouts.1 NonLinearReadoutBlock out (3,1) layer 1 (hidden 16x0e)
|
|
29
|
+
scale_shift ScaleShiftBlock out (3,)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Facts that matter for hooking:
|
|
33
|
+
|
|
34
|
+
- `interactions.i` output is a tuple; the message tensor is index 0 with
|
|
35
|
+
shape `[N, channels, (lmax+1)^2]` = `[3,128,16]`, i.e. **not** e3nn-flat
|
|
36
|
+
yet; `products.i` returns the flat node features. Hook `products.i` for
|
|
37
|
+
the residual-stream analogue.
|
|
38
|
+
- `hidden_irreps` for the small model is `128x0e` (scalars only). Medium and
|
|
39
|
+
large are `128x0e+128x1o`; `hidden_irreps` is on each interaction block.
|
|
40
|
+
- Per-layer `readouts` exist natively: a layer-resolved per-atom energy
|
|
41
|
+
decomposition for free. Observed values for H2O: layer 0 `[-0.186, -0.136, -0.136]`,
|
|
42
|
+
layer 1 `[0.157, 0.199, 0.199]` (eV, before scale/shift).
|
|
43
|
+
- e3nn linears carry a `_compiled_main` TorchScript submodule: opaque to
|
|
44
|
+
hooks and to source inspection. Do not hook below block level.
|
|
45
|
+
- dtype: the model is float64; building `AtomicData` without
|
|
46
|
+
`torch.set_default_dtype(torch.float64)` produces a float32 batch and a
|
|
47
|
+
TorchScript "both inputs should have same dtype" error deep inside the
|
|
48
|
+
tensor product. The calculator handles this for you; a raw `HookedModel`
|
|
49
|
+
must too.
|
|
50
|
+
- Zero-ablating `products.0` output moved H2O energy from -14.0479 to -14.1266 eV
|
|
51
|
+
(float64), confirming interventions propagate.
|
|
52
|
+
- Batch construction: `mace.data.config_from_atoms` ->
|
|
53
|
+
`AtomicData.from_config(cfg, z_table, cutoff=model.r_max)` -> geometric
|
|
54
|
+
DataLoader -> `.to_dict()`. `model(batch, compute_force=False)` returns a
|
|
55
|
+
dict with `energy`, `node_energy`, `forces`, ...
|
|
56
|
+
|
|
57
|
+
## SevenNet (`sevenn`), model `7net-0`, Cu 2x2x2 conventional (32 atoms, 1344 edges)
|
|
58
|
+
|
|
59
|
+
Class `sevenn.nn.sequential.AtomGraphSequential`: an `nn.Sequential` whose
|
|
60
|
+
`forward` is literally `for module in self: data = module(data)` over an
|
|
61
|
+
`AtomGraphData` dict. **Every module's output is the whole dict**, mutated
|
|
62
|
+
in place; node features live under key `x`.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
edge_embedding EdgeEmbedding (.basis_function (1344,8), .spherical (1344,9) 1x0e+1x1e+1x2e)
|
|
66
|
+
onehot_idx_to_onehot OnehotEmbedding
|
|
67
|
+
onehot_to_feature_x IrrepsLinear 89x0e -> 128x0e (.linear out (32,128))
|
|
68
|
+
{t}_self_connection_intro SelfConnectionLinearIntro 128x0e -> 224x0e+64x1e+32x2e
|
|
69
|
+
{t}_self_interaction_1 IrrepsLinear
|
|
70
|
+
{t}_convolution IrrepsConvolution (.convolution out (1344, 1152) edge-level TP; .weight_nn radial MLP)
|
|
71
|
+
{t}_self_interaction_2 IrrepsLinear -> 224x0e+64x1e+32x2e
|
|
72
|
+
{t}_self_connection_outro SelfConnectionOutro
|
|
73
|
+
{t}_equivariant_gate EquivariantGate gate out (32,480) = 128x0e+64x1e+32x2e <- node_feats after block t
|
|
74
|
+
reduce_input_to_hidden (.linear (32,64))
|
|
75
|
+
reduce_hidden_to_energy (.linear (32,1))
|
|
76
|
+
rescale_atomic_energy, reduce_total_enegy, force_output
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
t = 0..4 (five interaction blocks). Block 4 collapses to `128x0e`.
|
|
80
|
+
|
|
81
|
+
Facts that matter:
|
|
82
|
+
|
|
83
|
+
- Hook points must select `data["x"]` from the dict output; write-back
|
|
84
|
+
interventions must assign `data["x"] = new` (in-place) rather than return
|
|
85
|
+
a value. Leaf `.linear` submodules return plain tensors if you prefer,
|
|
86
|
+
but the gate has no clean tensor-returning leaf for the block output.
|
|
87
|
+
- Feature irreps `128x0e+64x1e+32x2e`; vector channels are labelled **even**
|
|
88
|
+
parity (`1e`), unlike MACE's `1o`. SevenNet's spherical encoding is
|
|
89
|
+
`1x1e -> 1x0e+1x1e+1x2e`: effectively SO(3), parity not tracked. A dipole
|
|
90
|
+
probe cannot use parity to select channels here; degree l only.
|
|
91
|
+
- Layer names are `{t}_...` strings, not a `ModuleList`; the adapter should
|
|
92
|
+
discover `t` by regex over `named_children()`.
|
|
93
|
+
- Module names `reduce_total_enegy` (sic) exist upstream; do not "fix" them.
|
|
94
|
+
|
|
95
|
+
## Orb v3 (`orb-models`), `orb-v3-direct-20-omat`, Cu 2x2x2 (32 atoms, 640 edges)
|
|
96
|
+
|
|
97
|
+
Class `orb_models.forcefield.models.direct_regressor.DirectForcefieldRegressor`
|
|
98
|
+
wrapping `model: MoleculeGNS` plus a `heads` ModuleDict. Hooked to depth 3.
|
|
99
|
+
|
|
100
|
+
```
|
|
101
|
+
model.atom_emb AtomEmbedding out (32,256)
|
|
102
|
+
model.rbf_transform BesselBasis out (640,8)
|
|
103
|
+
model.angular_transform SphericalHarmonics out (640,16) lmax 3, edge features only
|
|
104
|
+
model._encoder Encoder out ((32,256), (640,256)) (node, edge); ._node_fn / ._edge_fn are MLP + LayerNorm
|
|
105
|
+
model.gnn_stacks.{0..4} AttentionInteractionNetwork out ((32,256), (640,256)) (node, edge) <- node_feats = idx 0
|
|
106
|
+
._send_attn -> (640,1) per-edge sigmoid gate on the sender side
|
|
107
|
+
._receive_attn -> (640,1) per-edge sigmoid gate on the receiver side
|
|
108
|
+
._edge_mlp -> (640,256) updated edge features (the message)
|
|
109
|
+
._node_mlp -> (32,256) node update from aggregated messages
|
|
110
|
+
model._decoder Decoder out (32,3)
|
|
111
|
+
model -> {"node_features": (32,256), "edge_features": (640,256), "pred": (32,3)}
|
|
112
|
+
heads.energy mlp (1,1) + reference.linear (1,) heads.forces (32,3) heads.confidence (32,50) heads.stress (1,6)
|
|
113
|
+
pair_repulsion_fn ZBLBasis -> {"energy","forces","stress"}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Facts that matter:
|
|
117
|
+
|
|
118
|
+
- Plain `[N, 256]` node latents and `[E, 256]` edge latents, no irreps;
|
|
119
|
+
rotation handled by augmentation. Equivariance tests are
|
|
120
|
+
approximate-invariance tests.
|
|
121
|
+
- Every block returns a `(node, edge)` tuple, same convention as the
|
|
122
|
+
encoder; `HookPoint.select = lambda o: o[0]` for node state, `o[1]` for
|
|
123
|
+
edge state. Edge state persists across blocks, so Orb has a genuine edge
|
|
124
|
+
residual stream that the e3nn models lack.
|
|
125
|
+
- The two per-edge attention gates (`_send_attn`, `_receive_attn`, shape
|
|
126
|
+
`[E, 1]`) are the closest thing to attention patterns in the zoo, and a
|
|
127
|
+
natural hook point for "which neighbours does atom i listen to".
|
|
128
|
+
- Heads read `node_features` directly: energy via a pooled MLP plus a
|
|
129
|
+
linear per-element reference, forces as a direct per-node MLP (direct
|
|
130
|
+
model), confidence as 50 bins per atom.
|
|
131
|
+
- Loader returns `(model, atoms_adapter)`; the calculator needs both.
|
|
132
|
+
orb-models sets `torch.set_default_dtype(float32)` globally at import.
|
|
133
|
+
- Environment: orb-models pins `dm-tree==0.1.8`, which has no cp313 wheel
|
|
134
|
+
and fails to build from source; run the introspection script with
|
|
135
|
+
`uv run --python 3.12` (the script's `requires-python` cap alone did not
|
|
136
|
+
stop uv from picking 3.13 on 2026-09-10).
|
|
137
|
+
|
|
138
|
+
## UMA (`fairchem-core`), `uma-s-1p1`, Cu 2x2x2 (32 atoms, 2496 edges)
|
|
139
|
+
|
|
140
|
+
Loaded with an HF token via `pretrained_mlip.get_predict_unit`. Wrapping:
|
|
141
|
+
`MLIPPredictUnit.model` is an `AveragedModel` around a `HydraModel` with
|
|
142
|
+
`backbone: eSCNMDMoeBackbone` and `output_heads: ModuleDict`. Backbone
|
|
143
|
+
config for uma-s-1p1: `num_layers=4, lmax=2, mmax=2, sphere_channels=128,
|
|
144
|
+
num_experts=32`.
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
backbone.sphere_embedding Embedding out (32,128) initial scalar node embedding
|
|
148
|
+
backbone.charge/spin/dataset_embedding, mix_csd out (1,128) per-system conditioning (csd)
|
|
149
|
+
backbone.composition_embedding Embedding out (32,128)
|
|
150
|
+
backbone.routing_mlp Sequential out (1,32) MoLE expert mixing coefficients, per system
|
|
151
|
+
backbone.distance_expansion GaussianSmearing out (2496,64)
|
|
152
|
+
backbone.envelope PolynomialEnvelope out (2496,)
|
|
153
|
+
backbone.source/target_embedding Embedding out (2496,128)
|
|
154
|
+
backbone.edge_degree_embedding EdgeDegreeEmbedding out (32,9,128) first node state in grid layout
|
|
155
|
+
backbone.blocks.{0..3} eSCNMD_Block out (32,9,128) <- node_feats after block i
|
|
156
|
+
.norm_1 .edge_wise (Edgewise) .norm_2 .atom_wise (SpectralAtomwise | GridAtomwise)
|
|
157
|
+
backbone.norm EquivariantRMSNormArraySphericalHarmonicsV2 out (32,9,128)
|
|
158
|
+
backbone -> {"node_embedding": (32,9,128), "batch": (32,)}
|
|
159
|
+
output_heads.energyandforcehead.head MLP_EFS_Head -> {"energy": (1,), "embeddings": (32,9,128), "forces": (32,3), "stress": (1,9)}
|
|
160
|
+
output_heads.energyandforcehead DatasetSpecificSingleHeadWrapper -> {"{task}_energy": ..., "{task}_embeddings": ..., ...} for every task
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Facts that matter:
|
|
164
|
+
|
|
165
|
+
- **Default inference settings silence hooks.** `get_predict_unit(name)`
|
|
166
|
+
uses the `"default"` preset: `compile=True, merge_mole=True`. With that,
|
|
167
|
+
only the pre-compile embeddings and the head fired; nothing inside the
|
|
168
|
+
backbone did. Passing
|
|
169
|
+
`InferenceSettings(compile=False, activation_checkpointing=False, merge_mole=False)`
|
|
170
|
+
(or the `"traineval"` preset) makes every block boundary hookable. Energy
|
|
171
|
+
agreed to 1e-6 eV between the two modes. The UMA adapter must construct
|
|
172
|
+
the predict unit itself with hooks-safe settings.
|
|
173
|
+
- Node features `[N, (lmax+1)^2, C]` = `[32, 9, 128]`; the middle axis holds
|
|
174
|
+
SH coefficients, degree-l block at `l^2:(l+1)^2`. `blocks.i` output is
|
|
175
|
+
pre-`balance_channels`; `balance_channels` runs in the backbone forward
|
|
176
|
+
loop after each block and is not a module, so the post-balance state is
|
|
177
|
+
only observable as the next block's input (`register_forward_pre_hook`).
|
|
178
|
+
- The final `norm` output equals the backbone's `node_embedding`; the head
|
|
179
|
+
also returns it as `embeddings`. This is the tensor Chorna et al. and
|
|
180
|
+
Platonic-representation work used.
|
|
181
|
+
- MoLE: `routing_mlp` produces one 32-vector of expert coefficients per
|
|
182
|
+
system from the charge/spin/dataset embedding; effective linear weights in
|
|
183
|
+
every block are a system-dependent mixture. Activation hooks are
|
|
184
|
+
unaffected. Weight-level analysis must fix a system (or a task) first;
|
|
185
|
+
`merge_mole=True` materialises the merged weights but breaks hooks.
|
|
186
|
+
- The head wrapper emits per-task outputs (`omat_energy`, `oc20_energy`,
|
|
187
|
+
`omol_energy`, ...) for all tasks in one forward; the calculator selects
|
|
188
|
+
by `task_name`. Useful for cross-task comparison from a single cache.
|
|
189
|
+
- Custom Wigner-D rotation (`Jd.pt`, `WignerDataModule`) and optional
|
|
190
|
+
Triton kernels; no e3nn. Weights gated on HuggingFace (`facebook/UMA`).
|
|
191
|
+
|
|
192
|
+
## Cross-cutting conclusions
|
|
193
|
+
|
|
194
|
+
1. Three feature layouts: e3nn flat (MACE, SevenNet, NequIP), SO(3) grid
|
|
195
|
+
(UMA/eSEN/eqV2), plain (Orb, MatterSim). One `blocks()` view over all.
|
|
196
|
+
2. Three output conventions from hooked modules: plain tensor (MACE
|
|
197
|
+
products), tuple (MACE interactions, Orb encoder), mutated dict
|
|
198
|
+
(SevenNet, fairchem). `HookPoint.select` plus an optional write-back
|
|
199
|
+
handles all three.
|
|
200
|
+
3. Never hook below block boundaries: TorchScript, cuEquivariance,
|
|
201
|
+
OpenEquivariance, and Triton all rewrite block internals.
|
|
202
|
+
4. Every family has its own graph construction and dtype expectations;
|
|
203
|
+
`from_atoms` must delegate to the family's own code.
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# Case studies: how matterlens's abstractions map onto prior work
|
|
2
|
+
|
|
3
|
+
Written 2026-09-10 as pitch context. Each section takes a published or in-house
|
|
4
|
+
result, describes what the authors actually built to get at model internals,
|
|
5
|
+
and shows which matterlens abstraction replaces each piece. The science in these
|
|
6
|
+
papers (anchor selection, optimal-transport metrics, distillation to algebra,
|
|
7
|
+
transfer experiments) is outside matterlens's scope; what matterlens removes is the
|
|
8
|
+
bespoke extraction layer under it and the silent inconsistencies that layer
|
|
9
|
+
introduces.
|
|
10
|
+
|
|
11
|
+
## Platonic representation of foundation MLIPs (Li and Walsh, NMI 2026)
|
|
12
|
+
|
|
13
|
+
Code: https://github.com/WMD-group/PlatonicRep, file `mlip_embedding_extractor.py`
|
|
14
|
+
(1166 lines). What they built, per family, to obtain one vector per atom:
|
|
15
|
+
|
|
16
|
+
| Family | Mechanism | Glue | Which tensor | Invariance |
|
|
17
|
+
|---|---|---|---|---|
|
|
18
|
+
| MACE | built-in `get_descriptors` | ~35 lines | all layers concatenated (256-d) by default; notebook uses layer 1 only (128-d) | selected by MACE's default `invariants_only=True` |
|
|
19
|
+
| Orb v3 | reach into private `.model`, call `node_features` | ~57 lines | after all 5 blocks, before heads | not equivariant, nothing to select |
|
|
20
|
+
| SevenNet | forward hook on the module before the readout, plus manual iteration over `_modules` bypassing `forward` to skip the force pass | ~95 lines | last gate output | inherited: SevenNet's last layer is built with lmax 0 |
|
|
21
|
+
| NequIP | rebuild the `SequentialGraphNetwork` with inserted `SaveForOutput` modules | ~120 lines | node features key | not stated |
|
|
22
|
+
|
|
23
|
+
Other properties: one forward pass per structure, no batching; three separate
|
|
24
|
+
conda environments; no pinned versions; the Orb import path used no longer
|
|
25
|
+
exists in current orb-models, so the extractor is already broken by drift.
|
|
26
|
+
|
|
27
|
+
What changes with matterlens:
|
|
28
|
+
|
|
29
|
+
- The four mechanisms collapse to one call per family:
|
|
30
|
+
`cache = load(family, name).run_with_cache(hm.from_atoms(atoms), names=["head.node_feats"])`
|
|
31
|
+
then `cache.scalars("head.node_feats")`. Roughly 15 lines total.
|
|
32
|
+
- "Which tensor" becomes an explicit, comparable choice. The paper compares
|
|
33
|
+
MACE's all-layers concatenation against Orb's last layer; both happen to be
|
|
34
|
+
256 wide, which hides that they are different objects. With canonical names
|
|
35
|
+
the comparison is `head.node_feats` on every model, or `blocks.i.node_feats`
|
|
36
|
+
for a layer-resolved version they could not easily run.
|
|
37
|
+
- "Scalars only" becomes a declared operation (`.scalars()`) rather than an
|
|
38
|
+
accident of each architecture's defaults.
|
|
39
|
+
- The adapter test suite catches the two failure modes this code is exposed
|
|
40
|
+
to: import drift (fidelity test fails to load) and a hook that silently
|
|
41
|
+
never fires (UMA's compile default did exactly this on 2026-09-10).
|
|
42
|
+
- The activation store emits the Zenodo-style artifact with per-atom
|
|
43
|
+
metadata for free.
|
|
44
|
+
- New questions become cheap: at which block does cross-model alignment
|
|
45
|
+
emerge; do the degree-1 (vector) channels of MACE and SevenNet align, not
|
|
46
|
+
just the scalars; does patching an anchor direction from one model into
|
|
47
|
+
another change its energy.
|
|
48
|
+
|
|
49
|
+
## Comparing latent features of uMLIPs (Chorna, Ceriotti et al., 2026)
|
|
50
|
+
|
|
51
|
+
Paper: https://arxiv.org/abs/2512.05717. Materials Cloud record has per-model
|
|
52
|
+
extraction scripts in separate directories with separate conda environments.
|
|
53
|
+
UMA's mixture-of-experts routing was fixed to the OMat expert by hand;
|
|
54
|
+
single-atom structures were excluded as incompatible with UMA. The MACE
|
|
55
|
+
feature width is reported as 144, which is not any single hook point width in
|
|
56
|
+
MACE-MP-0b3 (128 channels); which tensor it is cannot be recovered from the
|
|
57
|
+
paper.
|
|
58
|
+
|
|
59
|
+
With matterlens: the UMA adapter owns the task selection and the compile/MoLE
|
|
60
|
+
handling once; `head.node_feats` is a named, documented tensor with a
|
|
61
|
+
recorded irreps string, so "144-d MACE features" would instead read as a
|
|
62
|
+
hook name and a layout that anyone can reproduce.
|
|
63
|
+
|
|
64
|
+
## Skala exchange-correlation functional (Viswanath and Blaiszik, in-house draft)
|
|
65
|
+
|
|
66
|
+
Draft: `docs/draft.tex`. This is the in-house existence proof for the whole
|
|
67
|
+
program, and its methods section is the matterlens workflow executed by hand on a
|
|
68
|
+
model that is not an MLIP:
|
|
69
|
+
|
|
70
|
+
| Step in the draft | MatterLens abstraction |
|
|
71
|
+
|---|---|
|
|
72
|
+
| Load the traced graph and re-derive the model as equations, verifying manual re-execution to machine precision | the adapter fidelity test (wrapped output equals native output) and the introspection scripts that dump the module tree |
|
|
73
|
+
| Reduce the nonlocal branch to per-atom moments organised as 16 radial shells times spherical harmonics up to degree 3 | a `HookPoint` on the moment tensor with an irreps/layout declaration; `cache.blocks()` to view it by degree |
|
|
74
|
+
| Truncate the moments to their l=0 part, keeping everything else, and observe the sign flip on multiply-bonded N/O systems | `hooks({"nonlocal.moments": keep_degree(0)})`, an intervention returning a masked tensor |
|
|
75
|
+
| The same surgery shell by shell to find bond and lone-pair detectors | the same hook with a mask over the radial-shell axis |
|
|
76
|
+
| Sigma-bond count linearly decodable from internal moments at leave-one-molecule-out R^2 of 0.90 | `probe.linear` with held-out and random-init controls |
|
|
77
|
+
| Distil the mechanism to algebra and transplant it onto r2SCAN | outside matterlens; this is the science |
|
|
78
|
+
|
|
79
|
+
Two observations for the pitch. First, the draft itself notes the nonlocal
|
|
80
|
+
branch rewrites exactly as the Atomic Cluster Expansion, which is the same
|
|
81
|
+
mathematical object as a MACE interaction block. The block-boundary plus
|
|
82
|
+
irreps abstraction therefore covers "grid-based functionals" in the program
|
|
83
|
+
plan without a new design, which is why the plan's ambition to span model
|
|
84
|
+
families is realistic. Second, Skala adds a fourth activation scope beyond
|
|
85
|
+
node, edge, and graph: per grid point (the pointwise meta-GGA branch). That
|
|
86
|
+
is a v1 extension of the cache's scope field, not a redesign.
|
|
87
|
+
|
|
88
|
+
## The pitch in one paragraph
|
|
89
|
+
|
|
90
|
+
Every group that has looked inside an MLIP so far wrote its own extraction
|
|
91
|
+
layer: hundreds of lines per model family, private attributes, hand-selected
|
|
92
|
+
tensors whose identity is not recoverable from the paper, and environments
|
|
93
|
+
that break within months. None of that code is reused by the next group.
|
|
94
|
+
MatterLens replaces it with named, tested hook points and an activation cache
|
|
95
|
+
that knows what its tensors mean, so that probing, ablation, patching, and
|
|
96
|
+
SAE training run identically across MACE, UMA, SevenNet, and Orb. The Skala
|
|
97
|
+
result shows the workflow produces real physics when done by hand once;
|
|
98
|
+
matterlens is what makes it repeatable for the d-band, dipole, and
|
|
99
|
+
superconductivity targets that follow.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Prior art (surveyed 2026-09-10)
|
|
2
|
+
|
|
3
|
+
Condensed from two web surveys run on 2026-09-10. Claims marked "uncertain"
|
|
4
|
+
were not verified beyond abstracts.
|
|
5
|
+
|
|
6
|
+
## The gap is real
|
|
7
|
+
|
|
8
|
+
No hooking or lens library exists for MLIPs or geometric GNNs. Searches for
|
|
9
|
+
"TransformerLens for GNNs" and "MatterLens" return nothing. GNN
|
|
10
|
+
"explainability" tooling exists but is the attribution/XAI tradition, not
|
|
11
|
+
mechanistic interpretability:
|
|
12
|
+
|
|
13
|
+
- PyG `torch_geometric.explain` (GNNExplainer, PGExplainer, CaptumExplainer):
|
|
14
|
+
node/edge masks. https://pytorch-geometric.readthedocs.io/en/latest/tutorial/explain.html
|
|
15
|
+
- GraphXAI (Zitnik lab): benchmark datasets and metrics, Sci. Data 2023. https://github.com/mims-harvard/GraphXAI
|
|
16
|
+
|
|
17
|
+
No SAE-on-MLIP paper found for MACE, UMA, Equiformer, SevenNet, Orb,
|
|
18
|
+
MatterSim, or NequIP. No d-band or oxidation-state probe on MLIP latents
|
|
19
|
+
found. Creed et al., "Six Open Questions in MLIP Foundation Models"
|
|
20
|
+
(2026-06) names interpretability as an open pillar. https://arxiv.org/abs/2606.07327
|
|
21
|
+
|
|
22
|
+
## Closest existing results on MLIP internals
|
|
23
|
+
|
|
24
|
+
- **Platonic representation of foundation MLIPs**, Li and Walsh, Nat. Mach.
|
|
25
|
+
Intell. 2026-05: MACE-MP-0 S/M/L, MACE-omat, SevenNet-omat, Orb-v3;
|
|
26
|
+
embeddings from the invariant component before energy readout; element
|
|
27
|
+
embeddings cluster by periodic group; equivariant models give compact
|
|
28
|
+
clusters. Code: https://github.com/WMD-group/PlatonicRep .
|
|
29
|
+
https://www.nature.com/articles/s42256-026-01235-7
|
|
30
|
+
- **Comparing latent features of uMLIPs**, Chorna, Ceriotti, Chong et al.,
|
|
31
|
+
Adv. Intell. Syst. 2026: MACE-MP-0b3, PET-MAD, DPA-3.1, UMA-S-1p1; feature
|
|
32
|
+
reconstruction error shows models encode chemistry very differently
|
|
33
|
+
(cross-model GFRE ~0.66); fine-tuning preserves pre-training features.
|
|
34
|
+
https://arxiv.org/abs/2512.05717
|
|
35
|
+
- **Goodfire and Radical AI on MatterGen** (2026-04): linear probes on
|
|
36
|
+
activations predict band gap; probe-guided accept/reject in the denoising
|
|
37
|
+
loop raises the hit rate. Probes, not SAEs; no code or preprint found.
|
|
38
|
+
https://www.goodfire.com/research/self-correcting-search
|
|
39
|
+
- **Sparsity-promoting fine-tuning of MACE**, Cho et al., ICLR 2026: 0.5 to
|
|
40
|
+
3 percent of weights suffice; sparsity patterns show enhanced d-orbital
|
|
41
|
+
contributions for transition metals. https://arxiv.org/abs/2606.18691
|
|
42
|
+
- **PROBE**, Mehdi, Cho, Isayev, 2026-05: attention classifier on frozen
|
|
43
|
+
per-atom embeddings gives reliability scores and per-atom importance.
|
|
44
|
+
https://arxiv.org/abs/2605.00640
|
|
45
|
+
- **Equivariant SAEs**, Erdogan and Lucic, 2025 (rev. 2026-08): SAEs on
|
|
46
|
+
symmetric data are unidentifiable; equivariant SAEs probe better despite
|
|
47
|
+
worse reconstruction. Whether any target model is an MLIP is uncertain.
|
|
48
|
+
https://arxiv.org/abs/2511.09432 , code https://github.com/ege-erdogan/equivariant-sae
|
|
49
|
+
- **GNN-LRP on force fields**: JCTC 2025 (SchNet/PaiNN)
|
|
50
|
+
https://pubs.acs.org/doi/10.1021/acs.jctc.4c01424 ; Nat. Commun. 2025-11
|
|
51
|
+
https://www.nature.com/articles/s41467-025-65863-0
|
|
52
|
+
- Vault science briefing on what to look for (dipoles, Born charges, LES):
|
|
53
|
+
`rootstock-scratch/notes/Mechanistic Interpretability of MLIPs.md`.
|
|
54
|
+
|
|
55
|
+
## Lens libraries for LLMs: what to copy and what to avoid
|
|
56
|
+
|
|
57
|
+
**TransformerLens** https://github.com/TransformerLensOrg/TransformerLens
|
|
58
|
+
|
|
59
|
+
- `HookPoint` identity modules at every interesting tensor; names are module
|
|
60
|
+
paths (`blocks.0.attn.hook_z`); `run_with_cache` returns an
|
|
61
|
+
`ActivationCache` with residual-stream helpers assuming `[batch, pos, d_model]`.
|
|
62
|
+
- 1.x reimplemented every architecture and converted HF weights into it.
|
|
63
|
+
The v3 migration guide calls this "a latent source of bugs"; v3's
|
|
64
|
+
`TransformerBridge` keeps the native HF module and maps it onto canonical
|
|
65
|
+
component names with legacy aliases. `HookedTransformer` is deprecated.
|
|
66
|
+
https://transformerlensorg.github.io/TransformerLens/content/migrating_to_v3.html
|
|
67
|
+
- Weight processing (`fold_ln`, `center_writing_weights`) are
|
|
68
|
+
computation-preserving rewrites specific to LN + residual streams; no
|
|
69
|
+
obvious equivariant analogue.
|
|
70
|
+
|
|
71
|
+
**nnsight / NDIF** https://github.com/ndif-team/nnsight
|
|
72
|
+
|
|
73
|
+
- Wraps any `nn.Module`; `.output`/`.input` on any submodule inside
|
|
74
|
+
`with model.trace(...)`; `.source` AST-instruments a module's forward to
|
|
75
|
+
expose intra-module tensors; `remote=True` runs on NDIF. Inherits HF
|
|
76
|
+
naming fragmentation; **nnterp** (https://arxiv.org/abs/2511.14465) adds a
|
|
77
|
+
renaming layer plus init-time validation tests. NDIF hosts LLMs, diffusion,
|
|
78
|
+
VLMs; nothing scientific found.
|
|
79
|
+
|
|
80
|
+
**pyvene** https://github.com/stanfordnlp/pyvene
|
|
81
|
+
|
|
82
|
+
- Per-model-type tables mapping canonical component names to
|
|
83
|
+
`(module path template, hook type, optional post-split)`. The cheapest
|
|
84
|
+
pattern for canonical names over heterogeneous modules; adopted here.
|
|
85
|
+
|
|
86
|
+
**baukit** https://github.com/davidbau/baukit : `Trace`/`TraceDict` by dotted
|
|
87
|
+
module name, retain/edit output. Minimal; the mechanism under everything.
|
|
88
|
+
|
|
89
|
+
**torchlens** https://github.com/johnmarktaylor91/torchlens : logs every
|
|
90
|
+
tensor op in a forward pass, validated, tested on GNNs. Useful for
|
|
91
|
+
discovering where a needed tensor lives when it is not at a module boundary.
|
|
92
|
+
|
|
93
|
+
**torch.fx**: fails on input-dependent control flow and un-wrapped
|
|
94
|
+
functions; MLIPs hit both (neighbour lists, scatter). Not used.
|
|
95
|
+
|
|
96
|
+
## SAE tooling interface
|
|
97
|
+
|
|
98
|
+
- SAELens needs a hook name, `d_in`, and an activation source; inference works
|
|
99
|
+
on any torch model. https://github.com/jbloomAus/SAELens
|
|
100
|
+
- `dictionary_learning` uses an `ActivationBuffer` over nnsight submodules;
|
|
101
|
+
many trainer variants. https://github.com/saprmarks/dictionary_learning
|
|
102
|
+
- EleutherAI `sparsify` takes plain module names with globs and trains
|
|
103
|
+
many SAEs at once, no activation store. https://github.com/EleutherAI/sparsify
|
|
104
|
+
- SAEDashboard hard-requires `HookedTransformer`. https://github.com/jbloomAus/SAEDashboard
|
|
105
|
+
|
|
106
|
+
Implication: the activation store only has to yield `[n_units, d_in]` rows
|
|
107
|
+
with metadata; "token" becomes "atom in structure".
|
|
108
|
+
|
|
109
|
+
## Hosting and community
|
|
110
|
+
|
|
111
|
+
- HuggingFace hosts MACE (`mace-foundations`), UMA (`facebook/UMA`, gated),
|
|
112
|
+
OMAT24 models, OrbMol; the Hugging Science community (2026) curates
|
|
113
|
+
science models. No HF-authored MLIP interp work found.
|
|
114
|
+
- NeurIPS 2026 "Interpretability for Discovery" workshop (Dec, Atlanta).
|
|
115
|
+
https://interpretability4discovery.github.io/
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Releasing
|
|
2
|
+
|
|
3
|
+
Publishing is automated by `.github/workflows/publish.yml` using uv and PyPI
|
|
4
|
+
trusted publishing, so no API token is stored anywhere.
|
|
5
|
+
|
|
6
|
+
One-time setup (2026-09-10, not yet done):
|
|
7
|
+
|
|
8
|
+
1. On pypi.org, log in as the account that will own the project, go to
|
|
9
|
+
"Your projects" -> "Publishing" -> "Add a new pending publisher", and enter:
|
|
10
|
+
PyPI project name `matterlens`, owner `Garden-AI`, repository `matterlens`,
|
|
11
|
+
workflow name `publish.yml`, environment name `pypi`.
|
|
12
|
+
2. In the GitHub repo, Settings -> Environments -> create `pypi`. Optionally
|
|
13
|
+
restrict it to tag deployments and require a reviewer.
|
|
14
|
+
|
|
15
|
+
Every release afterwards:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv version --bump patch # or set the version in pyproject.toml and __init__.py
|
|
19
|
+
git commit -am "Release vX.Y.Z" && git tag vX.Y.Z && git push --tags
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The workflow builds the sdist and wheel, smoke-imports the wheel in a clean
|
|
23
|
+
environment, and publishes. Keep `__version__` in `src/matterlens/__init__.py`
|
|
24
|
+
in step with `pyproject.toml` until a dynamic-version setup is adopted.
|