Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
448f45d
add core functionality
IsaacParker30 Feb 10, 2026
694da24
added bulk water and ice
xradvincula Feb 11, 2026
4feaafa
fixed some comms
xradvincula Feb 11, 2026
adac747
sorting
xradvincula Feb 12, 2026
6cd3169
fixing liqice
xradvincula Feb 12, 2026
167d2ab
updated gitingore
IsaacParker30 Jul 2, 2026
1854802
Vendor shared AML MD-analysis module in utils
IsaacParker30 Jul 2, 2026
44d3b0e
Add shared md_water_analysis orchestration module
IsaacParker30 Jul 2, 2026
d5688d8
Add cell_to_bar decorator to shared utils/decorators.py
IsaacParker30 Jul 2, 2026
7dec3f2
Rewrite aqueous/interface analyses as thin delegating fixtures
IsaacParker30 Jul 2, 2026
809fe53
Revert tutorial notebook changes and drop personal gitignore entry
IsaacParker30 Jul 2, 2026
9c7f2e5
Remove duplicated aml/decorators modules and stray artifact
IsaacParker30 Jul 2, 2026
e84f9f7
Switch copper_water_interface MD to janus-core NVT
IsaacParker30 Jul 2, 2026
859d4a4
Switch bulk_water and ice MD to janus-core NVT
IsaacParker30 Jul 2, 2026
b4a2e2f
Read janus trajectory in RDF/VDOS/VACF loaders
IsaacParker30 Jul 2, 2026
f6dcf53
Share water dipole extraction between copper and water_slab
IsaacParker30 Jul 2, 2026
3ee9625
Ignore personal local change log
IsaacParker30 Jul 2, 2026
2959bb6
Add Fraction Breakdown Candidates metric to copper_water_interface
IsaacParker30 Jul 2, 2026
3d527f3
Enable element filtering for bulk_water, ice and copper_water_interface
IsaacParker30 Jul 2, 2026
bf54094
Tested app functionality and bug fixes
IsaacParker30 Jul 15, 2026
5d8df50
Unstashed changes
IsaacParker30 Jul 15, 2026
df85043
Added dipole profile test and changed notebook back to 3.12.8
IsaacParker30 Jul 15, 2026
43a1b10
Added dipole profile test and changed notebook back to 3.12.8
IsaacParker30 Jul 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,10 @@ ml_peg/app/data/*
!ml_peg/app/data/onboarding/
!ml_peg/app/data/table_download/
certs/
ml_peg/calcs/surfaces/copper_water_interface/data/
ml_peg/calcs/surfaces/copper_water_interface/outputs/
ml_peg/calcs/aqueous_solutions/bulk_water/data/
ml_peg/calcs/aqueous_solutions/ice/data/

# Personal local change log (not for committing)
CHANGES_LOG.md
105 changes: 105 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## What this is

**ML-PEG** ("ML Performance and Extrapolation Guide") is a benchmarking suite and interactive [Dash](https://dash.plotly.com/) web app for evaluating machine-learned interatomic potentials (MLIPs / foundation models) — MACE, Orb, PET-MAD, UMA, MatterSim, GRACE, CHGNet, etc. — against reference data (usually DFT, sometimes CCSD(T)/DMC/experiment) across many physical/chemical benchmarks. The end product is a normalised, weighted leaderboard: per-benchmark scores roll up into category scores and one overall score, explored in the app (https://ml-peg.stfc.ac.uk).

`CODEBASE_UNDERSTANDING.md` (untracked, personal notes) contains a deeper orientation and is worth reading for detail beyond this file.

## Environment & common commands

Use `uv` for dependency management. There is a `.venv` in the repo; the user's global aliases also provide venvs (`scfenv` is usually fine). Always sync/activate the environment before running tests.

```shell
uv sync --extra mace --extra orb --extra d3 # add extras per the model(s) you need
source .venv/bin/activate
pre-commit install
```

- Run all tests: `pytest -v`
- Run a single test file: `pytest -v tests/test_mlip_testing.py`
- Run one test: `pytest -v path/to/test_file.py::test_name`
- Lint/format (also enforced via pre-commit + CI): `ruff check --fix` and `ruff format`; docstrings validated by `numpydoc` (numpy convention).

Model extras conflict — you cannot install `uma`+`mace`, `mattersim`+`mace`, `uma`+`grace`, `mattersim`+`grace` together (see `[tool.uv].conflicts` in `pyproject.toml`).

### The `ml_peg` CLI (Typer, entry point `ml_peg.cli.cli:app`)

```shell
ml_peg calc --test X23 --models mace-mp-0a,orb-v3-consv-inf-omat # run MLIP calculations
ml_peg analyse --test X23 # outputs -> metrics + plots (JSON)
ml_peg app --port 8050 # launch Dash app
ml_peg download --key app/data/data.tar.gz --filename data.tar.gz # S3 (STFC Echo, bucket ml-peg-data)
ml_peg upload ...
ml_peg list calcs | analysis | app | models
```

`calc`/`analyse`/`app` accept `--models`, `--category`, `--test`; `calc` also takes `--run-slow`/`--run-very-slow`. Add `--help` to any subcommand for options.

## Architecture: the calc → analyse → app pipeline

Everything follows a **calc → analyse → app** flow, mirrored across three top-level package folders under a shared `<category>/<benchmark>/` directory convention. Both `calc` and `analyse` steps are **pytest test functions**, so the CLI drives them by globbing files and invoking pytest.

```
ml_peg/
├── calcs/ <category>/<benchmark>/calc_<benchmark>.py # run MLIPs -> local outputs/<model>/
├── analysis/ <category>/<benchmark>/analyse_<benchmark>.py # outputs -> metrics + Plotly JSON
│ metrics.yml # good/bad thresholds, weights, units, tooltips
├── app/ <category>/<benchmark>/app_<benchmark>.py # assemble Dash tab from the JSON
│ <category>/<category>.yml # category title/description
├── models/ models.yml + models.py + get_models.py # MLIP registry & loaders
└── cli/ cli.py # Typer CLI
```

1. **Calc** (`calcs/`): `calc_<name>.py` runs each MLIP, typically via `@pytest.mark.parametrize("mlip", MODELS.items())` where `MODELS = load_models(current_models)`. Writes to a local `outputs/<model_name>/`. Input/output data is generally **not committed** — pulled from S3 via `download_s3_data(...)` or tracked with DVC (`dvc.yaml`, `.dvc/`). Some benchmarks run an `mlipx`/`zntrack` Node via `dvc repro`.

2. **Analysis** (`analysis/`): `analyse_<name>.py` reads `outputs/`, computes metrics, and emits JSON (tables + figures) into `ml_peg/app/data/<category>/<benchmark>/`. Heavy use of decorators in `analysis/utils/decorators.py` that wrap a dict-returning function and serialise a figure/table as a side effect — e.g. `@build_table` (scored DataTable with normalisation), `@plot_parity`, `@plot_scatter`, `@plot_periodic_table`. Functions are chained as `@pytest.fixture`s feeding a final `metrics` fixture and a `test_<name>` entry point. Per-benchmark `metrics.yml` supplies `good`/`bad`, `unit`, `weight`, `tooltip`, `level_of_theory` (loaded via `load_metrics_config`).

3. **App** (`app/`): `app_<name>.py` subclasses `BaseApp` (`base_app.py`), loads the analysis JSON to rebuild the table (`rebuild_table`) and layout, and registers Dash callbacks. `build_app.py` discovers every `app_*.py` by glob, groups tabs by category, builds per-category and overall summary tables, and wires benchmark→category→overall score callbacks. `run_app.py` launches it (`run_app.py` at repo root is the thin launcher).

### Benchmark reference data

Reference (DFT/AIMD) data is shipped per benchmark as a zip in the S3 `ml-peg-data`
bucket, pulled by `download_s3_data(...)` and cached under `~/.cache/ml_peg/` (if the
zip is already present there, S3 is never contacted). For **local testing**, drop the
benchmark zip in `~/.cache/ml_peg/` directly.

The raw simulation trajectories are usually far too large to ship, so each benchmark's
reference *observables* (RDF/VDOS/VACF curves, dipole arrays, density profiles, …) are
precomputed offline and only those small artifacts go in the zip. For
`copper_water_interface` this generation lives **outside the repo** at:

`/share/ijp30/projects/estatics-archer/07-04-2025-estatics/aimd/cu111/production-run/long-run-pbe-dzvp/`
— the raw PBE-D3 AIMD run (`pbe-d3-md-pos.xyz` positions, `pbed3-cu-h2o-vel-1-units-corr.xyz`
velocities), with an `ml-peg/` subfolder holding:
- `make_reference.py` — reads the raw trajectories, reuses the repo's analysis primitives
to compute each reference artifact (must be run in the repo venv so `ml_peg` imports).
- `stage_local.py` — zips the artifacts listed in `REFERENCE_FILES` into
`copper_water_interface.zip` and copies it into `~/.cache/ml_peg/`.

So to add/regenerate a reference observable: add its generation to `make_reference.py`,
add the filename to `stage_local.py`'s `REFERENCE_FILES`, then run `make_reference.py`
followed by `stage_local.py`. (Uploading the new zip to S3 for the live app is a
separate `ml_peg upload` step.)

### Scoring & normalisation (key concept)

5-level hierarchy: **raw metric → normalised metric score (0–1) → benchmark score → category score → overall score**, each a weighted average of the level below. Normalisation is linear between per-metric `good` and `bad` thresholds from `metrics.yml`, clamped to [0,1] (1 = as good as needed, 0 = avoid). Users can override thresholds/weights live in the app; advanced custom `normalizer` functions live in `analysis/utils/utils.py`. See `docs/source/developer_guide/scoring_and_normalisation.rst`.

### Models registry

`ml_peg/models/models.yml` is the registry (many entries commented out); each entry names `module`/`class_name`, `device`, `default_dtype`, `level_of_theory`, `kwargs`, and D3-dispersion info. `models.py` defines dataclass wrappers around `mlipx.GenericASECalculator` (`GenericASECalc`, `PetMadCalc`, `OrbCalc`, `FairChemCalc`), all inheriting `SumCalc`, which can add a TorchDFTD3 D3-dispersion correction (`add_d3_calculator`, skipped when `trained_on_d3=True`). `get_models.py` provides `load_models`, `get_model_names`, `get_subset`, `load_model_configs`. The module-global `models.current_models` restricts a run to a subset and is set from the CLI `--models` flag via `conftest.py`.

## Testing notes

- Custom pytest markers `slow` / `very_slow` are skipped unless `--run-slow` / `--run-very-slow` is passed (see root `conftest.py`).
- A **mock model** is available for fast testing: `--run-mock` (add mock alongside real models) or `--mock-only` (mock only), configured in `ml_peg/calcs/conftest.py`; see `models/mock.py`, `mock.yml`.
- `pytest.ini_options` sets `pythonpath = ["."]` and collects `test_*.py`.

## Conventions

- Ruff enforces PEP 8, isort (`force-sort-within-sections`, `required-imports = ["from __future__ import annotations"]`), and numpydoc docstrings. Every module starts with `from __future__ import annotations`.
- `.gitignore` is aggressive: most data/plot/structure formats (`*.json`, `*.xyz`, `*.png`, `*.csv`, etc.) and per-benchmark `data/` dirs are ignored. Some committed outputs (e.g. X23/S24 `.xyz`) are deliberate exceptions — don't assume a data file is meant to be committed.
- Adding a new benchmark: create matching `calcs/`, `analysis/`, `app/` files under `<category>/<benchmark>/` following the `calc_`/`analyse_`/`app_` naming; see the tutorial `docs/source/tutorials/python/adding_benchmark.ipynb` and the developer guide.
144 changes: 144 additions & 0 deletions CODEBASE_UNDERSTANDING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# ML-PEG — Codebase Understanding

_A personal orientation note. Not tracked by git (see `.gitignore`)._

Reference: https://github.com/ddmms/ml-peg · Live guide: https://ml-peg.stfc.ac.uk

---

## What it is

**ML-PEG** ("ML Performance and Extrapolation Guide") is a benchmarking suite and
interactive [Dash](https://dash.plotly.com/) web app for evaluating **machine-learned
interatomic potentials (MLIPs / foundation models)** — MACE, Orb, PET-MAD, UMA
(fairchem), MatterSim, GRACE, CHGNet, etc. — against reference (usually DFT, sometimes
CCSD(T)/DMC/experimental) data across many physical/chemical benchmarks.

The end product is a scored, normalised leaderboard: each model gets a unitless score
per benchmark, rolled up into category scores and one overall score, all explorable in
the app (https://ml-peg.stfc.ac.uk).

Maintained by DDMMS (STFC / Cambridge). GPLv3. Python ≥3.10, packaged as `ml-peg` on
PyPI. Uses `uv` for dependency management, `janus-core` + `mlipx` for MLIP plumbing,
`ase` for atomistic calculations, and `dvc` + an S3 bucket (`ml-peg-data` on STFC Echo)
for data storage.

## The three-stage pipeline

Everything is organised around a **calc → analyse → app** flow, and around a
`category / benchmark` directory convention that is mirrored across three top-level
package folders. Both `calc` and `analyse` steps are just **pytest test functions** so
they can be discovered and run uniformly.

```
ml_peg/
├── calcs/ <category>/<benchmark>/calc_<benchmark>.py # run the MLIPs → outputs/
├── analysis/ <category>/<benchmark>/analyse_<benchmark>.py # outputs → metrics + plots (JSON)
│ metrics.yml # good/bad thresholds, weights, tooltips
├── app/ <category>/<benchmark>/app_<benchmark>.py # assemble Dash tab from JSON
│ <category>/<category>.yml # category title/description
├── models/ models.yml + models.py + get_models.py # MLIP registry & loaders
└── cli/ cli.py # `ml_peg` typer CLI
```

**1. Calculations** (`ml_peg/calcs/...`): `calc_<name>.py` runs the actual MLIP
calculations for each model, typically via
`@pytest.mark.parametrize("mlip", MODELS.items())`. `MODELS = load_models(current_models)`
loads calculators from `models.yml`. Results are written to a local `outputs/<model_name>/`
dir. Input/output data is generally not committed — it is pulled from S3 via
`download_s3_data(...)` / `ml_peg download`, or tracked with DVC (note the many
`.dvc/`, `dvc.yaml`, `.dvcignore` files). Some benchmarks alternatively define an
`mlipx`/`zntrack` Node run through `dvc repro`.

**2. Analysis** (`ml_peg/analysis/...`): `analyse_<name>.py` reads the `outputs/`,
computes metrics, and emits JSON (tables + Plotly figures) into
`ml_peg/app/data/<category>/<benchmark>/`. Heavy use of **decorators** from
`analysis/utils/decorators.py` that wrap a function returning a dict and, as a side
effect, serialise a figure/table:
- `@build_table` — turns `{metric: {model: value}}` into a scored DataTable JSON
(applies normalisation via thresholds, weights, tooltips).
- `@plot_parity`, `@plot_scatter`, `@plot_density_scatter`, `@plot_periodic_table`,
`@cell_to_scatter` — Plotly figure generators.
- benchmark-local `decorators.py` add bespoke ones (e.g. `@plot_hist`, `@cell_to_bar`).
Functions are chained as `@pytest.fixture`s feeding a final `metrics` fixture and a
`test_<name>` entry point. `metrics.yml` per benchmark supplies `good`/`bad` thresholds,
`unit`, `weight`, `tooltip`, `level_of_theory`, loaded via `load_metrics_config`.

**3. App** (`ml_peg/app/...`): `app_<name>.py` subclasses `BaseApp` (`base_app.py`),
loading the JSON produced by analysis to rebuild the table (`rebuild_table`) and layout,
and registering Dash callbacks. `build_app.py` discovers every `app_*.py` via glob,
groups tabs by category, builds per-category summary tables and one overall summary
table, and wires benchmark→category→overall score callbacks. `run_app.py` launches it.

## Scoring & normalisation (key concept)

5-level hierarchy: **raw metric → normalised metric score (0–1) → benchmark score →
category score → overall score**, each level a weighted average of the one below.

Normalisation uses per-metric **`good`** and **`bad`** thresholds from `metrics.yml`:
linear between them, clamped to [0,1] outside (1 = as good as anyone needs, 0 = avoid).
Users can override thresholds and weights live in the app. Advanced users can add custom
`normalizer` functions in `ml_peg/analysis/utils/utils.py`. Details:
`docs/source/developer_guide/scoring_and_normalisation.rst`.

## Models registry

`ml_peg/models/models.yml` is the model registry (many entries commented out). Each entry
names a `module`/`class_name`, `device`, `default_dtype`, `level_of_theory`, `kwargs`,
and D3-dispersion info. `models.py` defines dataclass wrappers around
`mlipx.GenericASECalculator` (`GenericASECalc`, `PetMadCalc`, `OrbCalc`, `FairChemCalc`),
all inheriting `SumCalc` which can bolt on a **TorchDFTD3** D3-dispersion correction
(`add_d3_calculator`, skipped when `trained_on_d3=True`). `get_models.py` provides
`load_models` (→ calculator objects), `get_model_names` (→ names for analysis), and
`get_subset` / `load_model_configs`. `current_models` is a module-global set by the CLI
to restrict the run to a subset (`--models a,b,c`).

## CLI (`ml_peg` command, Typer)

- `ml_peg calc` — run calculations (globs `calc_*` files, invokes pytest; `--models`, `--category`, `--test`, `--run-slow/--run-very-slow`).
- `ml_peg analyse` — run analysis (globs `analyse_*`, invokes pytest).
- `ml_peg app` — launch the Dash app (`--models`, `--category`, `--port`, `--debug`).
- `ml_peg download` / `ml_peg upload` — S3 (`ml-peg-data` bucket, STFC Echo endpoint) data transfer.
- `--version`.

## Benchmark categories (each a folder under calcs/analysis/app)

- **aqueous_solutions** — bulk_water, ice
- **bulk_crystal** — elasticity, lattice_constants, phonons
- **conformers** — 37Conf8, DipCONFS, Glucose205, Maltose222, MPCONF196, solvMPCONF196, OpenFF_Tors, UpU46
- **molecular** — GMTKN55, Wiggle150
- **molecular_crystal** — X23, DMC_ICE13
- **molecular_reactions** — BH2O_36
- **nebs** — li_diffusion
- **physicality** — diatomics, extensivity, locality
- **supramolecular** — LNCI16, PLF547, S30L
- **surfaces** — OC157, S24, elemental_slab_oxygen_adsorption, **copper_water_interface**

## Current branch: `cu-h2o-rdf` (copper–water interface)

This branch develops the **copper_water_interface** surface benchmark (the open file is
`calcs/surfaces/copper_water_interface/calc_copper_water_interface.py`).

- **Calc** runs **Langevin MD** (deuterated H, 330 K, fixed bottom slab layers via
`fix_indices`) on a Cu/water interface, writing `md-pos.xyz` (coords), `md-velc.xyz`
(velocities) and `md.thermo` (incl. per-area z-dipole). Input pulled from S3
(`copper_water_interface.zip`).
- **Analysis** compares trajectory-derived observables against PBE-D3 reference:
- **RDF** (radial distribution functions) per element pair — via `aml.py` + `mdtraj`.
- **VDOS** (vibrational density of states) from velocities.
- **VACF** (velocity autocorrelation function).
- **Dipole-moment z distribution** — std-dev deviation + histogram.
Each yields a percentage-based score (`error_score_percentage`); thresholds in
`metrics.yml` (`good`/`bad` = 100/80 for the correlation scores). Curve data is
pickled into `app/data/.../{rdf,vdos,vacf}_curves/` for interactive plots.
- Recent commits: "add core functionality", "added bulk water and ice", "fixing liqice".

## Practical notes

- `.gitignore` is aggressive: most data/plot/structure formats (`*.json`, `*.xyz`,
`*.png`, `*.csv`, `*.pkl`-via-`*.dat`, etc.) are ignored, plus per-benchmark `data/`
dirs. Committed outputs (e.g. X23/S24 `.xyz`) are deliberate exceptions.
- Dev setup: `uv sync --extra mace --extra orb --extra d3 …`; `pre-commit install`
(ruff + numpydoc, numpy docstring convention enforced); `pytest -v`.
- Docker/compose available in `containers/`; app served on port 8050.
- Local venvs per the global aliases (`maceenv`, `scfenv`, etc.) — `scfenv` usually fine.
Loading
Loading