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.
@@ -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,13 @@
1
+ __pycache__/
2
+ *.pyc
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .ruff_cache/
9
+ *.npz
10
+ *.pt
11
+ scratch/
12
+ docs/draft.tex
13
+ .obsidian/
@@ -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.