feat: split molecular liquids into RDF + density benchmarks#149
feat: split molecular liquids into RDF + density benchmarks#149lwalew wants to merge 12 commits into
Conversation
Split the water and solvent Molecular Liquids benchmarks into four scored benchmarks: water/solvent x RDF/density. Each is its own Benchmark subclass, score, leaderboard column and UI page, grouped under the "Molecular Liquids" category. - The RDF and density benchmark of each system group share a single NPT simulation via `reusable_output_id` and identical `ModelOutput` field sets, so the simulation is only run once when both are run together. - Density is now scored on its own (relative deviation from the reference density via `compute_metric_score`), rather than only shown in the RDF UI. - Shared simulation config, data loaders and density helpers moved to `utils/molecular_liquids.py`. - New `Benchmark.data_name` lets the density benchmarks reuse the RDF input data without duplicating HuggingFace uploads. - New density UI pages plot the density time series against the reference. - Docs: added a density benchmark page and moved the RDF api-reference docs under `api_reference/molecular_liquids/`. Note: the density score threshold (DENSITY_RELATIVE_DEVIATION_THRESHOLD) is a placeholder pending sign-off from the science team. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`Benchmark.__init__` already coerces `run_mode` to a `RunMode`, so annotate the stored attribute as `RunMode` and let the molecular-liquids helpers take a plain `RunMode`. Removes the unnecessary `_as_run_mode` normalization. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Match the density score to Leon's NPT density analysis: relative-deviation threshold of 0.02 and a gentle decay (DENSITY_SCORE_ALPHA=0.1), rather than the placeholder 0.05 with the steep global scoring.ALPHA=3.0. With these values the density scores reproduce the expected ranking (v1 models best on water, v2 models poor). The ideal per-solvent threshold should ultimately be based on the isothermal compressibility; still pending final sign-off. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
| **NPT** simulation using the **MLIP** model for **500,000 steps**, leveraging the | ||
| `jax-md <https://github.com/google/jax-md>`_ engine from the | ||
| `mlip <https://github.com/instadeepai/mlip>`_ library. Water is run at **295.15 K** and **1 atm**, | ||
| while all other solvents are run at **293.15 K** and **1 atm**. Because the RDF and density |
There was a problem hiding this comment.
First use of "RDF" here. I guess above we should have
as the :ref:`radial_distribution` (RDF) benchmark
| Reference densities are the experimental values at the simulation conditions: water | ||
| :math:`0.9978\ \text{g/cm}^3`, CCl4 :math:`1.594\ \text{g/cm}^3`, methanol | ||
| :math:`0.791\ \text{g/cm}^3` and acetonitrile :math:`0.786\ \text{g/cm}^3`. |
| """Shared configuration and helpers for the molecular liquids benchmarks. | ||
|
|
||
| The water and solvent radial-distribution and density benchmarks all run the same | ||
| NPT simulation of a box of molecules. To avoid running these simulations twice, the | ||
| RDF and density benchmarks of a given system group share a `reusable_output_id` and | ||
| identical `ModelOutput` field signatures (see `benchmarks_cli._transfer_model_output`). | ||
| This module centralizes the simulation configuration, input-data loading and density | ||
| calculation so that both benchmarks stay in sync. | ||
| """ |
There was a problem hiding this comment.
Possibly remove or simplify?
| #: Per-solvent molecular weight (g/mol) and number of atoms per molecule. The dict | ||
| #: order also defines the canonical order of the solvent systems. |
There was a problem hiding this comment.
Dict order not important
| #: Per-solvent molecular weight (g/mol) and number of atoms per molecule. The dict | |
| #: order also defines the canonical order of the solvent systems. | |
| #: Per-solvent molecular weight (g/mol) and number of atoms per molecule. |
There was a problem hiding this comment.
Not quite true with the current implementation. In DEV Mode we only run a single system + the UI also hardcodes the order. I'll change that though.
| return WATER_SIMULATION_CONFIG | ||
|
|
||
|
|
||
| def load_water_box(data_dir: str | os.PathLike) -> Atoms: |
There was a problem hiding this comment.
| def load_water_box(data_dir: str | os.PathLike) -> Atoms: | |
| def load_water_system(data_dir: str | os.PathLike) -> Atoms: |
| return run_simulation( | ||
| atoms=load_water_box(data_dir), | ||
| force_field=force_field, | ||
| md_integrator=MDIntegrator.NPT_MC_LANGEVIN, | ||
| molecule_indices=load_water_molecule_indices(data_dir), | ||
| **get_water_md_kwargs(run_mode), |
There was a problem hiding this comment.
These methods are only used here - can we make them private to this file
| return run_simulation( | |
| atoms=load_water_box(data_dir), | |
| force_field=force_field, | |
| md_integrator=MDIntegrator.NPT_MC_LANGEVIN, | |
| molecule_indices=load_water_molecule_indices(data_dir), | |
| **get_water_md_kwargs(run_mode), | |
| return run_simulation( | |
| atoms=_load_water_box(data_dir), | |
| force_field=force_field, | |
| md_integrator=MDIntegrator.NPT_MC_LANGEVIN, | |
| molecule_indices=_load_water_molecule_indices(data_dir), | |
| **_get_water_md_kwargs(run_mode), |
| system_names = get_solvent_system_names(run_mode) | ||
| md_kwargs = get_solvent_md_kwargs(run_mode) | ||
|
|
||
| simulation_states: list[SimulationState | None] = [] | ||
| for system_name in system_names: | ||
| logger.info("Running NPT simulation for %s.", system_name) | ||
| simulation_state = run_simulation( | ||
| atoms=load_solvent_system(data_dir, system_name), | ||
| force_field=force_field, | ||
| md_integrator=MDIntegrator.NPT_MC_LANGEVIN, | ||
| molecule_indices=load_solvent_molecule_indices(data_dir, system_name), | ||
| **md_kwargs, | ||
| ) | ||
| simulation_states.append(simulation_state) |
There was a problem hiding this comment.
Same again RE making these methods private
| system_names = get_solvent_system_names(run_mode) | |
| md_kwargs = get_solvent_md_kwargs(run_mode) | |
| simulation_states: list[SimulationState | None] = [] | |
| for system_name in system_names: | |
| logger.info("Running NPT simulation for %s.", system_name) | |
| simulation_state = run_simulation( | |
| atoms=load_solvent_system(data_dir, system_name), | |
| force_field=force_field, | |
| md_integrator=MDIntegrator.NPT_MC_LANGEVIN, | |
| molecule_indices=load_solvent_molecule_indices(data_dir, system_name), | |
| **md_kwargs, | |
| ) | |
| simulation_states.append(simulation_state) | |
| system_names = _get_solvent_system_names(run_mode) | |
| md_kwargs = _get_solvent_md_kwargs(run_mode) | |
| simulation_states: list[SimulationState | None] = [] | |
| for system_name in system_names: | |
| logger.info("Running NPT simulation for %s.", system_name) | |
| simulation_state = run_simulation( | |
| atoms=_load_solvent_system(data_dir, system_name), | |
| force_field=force_field, | |
| md_integrator=MDIntegrator.NPT_MC_LANGEVIN, | |
| molecule_indices=_load_solvent_molecule_indices(data_dir, system_name), | |
| **md_kwargs, | |
| ) | |
| simulation_states.append(simulation_state) |
| # -------------------------------------------------------------------------------------- | ||
| # Water data loading + simulation | ||
| # -------------------------------------------------------------------------------------- |
There was a problem hiding this comment.
I think it might be possible to remove these grouping headers once we've made the small helpers private as suggested below - should be easy-ish to tell where each is used as you go down the page
| Uses `data_name` when set, otherwise `name`, so that benchmarks can share | ||
| input data (e.g. an RDF and a density benchmark running the same system). |
There was a problem hiding this comment.
nitpick: generalise
| Uses `data_name` when set, otherwise `name`, so that benchmarks can share | |
| input data (e.g. an RDF and a density benchmark running the same system). | |
| Uses `data_name` when set, so that benchmarks can share input data. Otherwise uses `name`. |
mcarobene-id
left a comment
There was a problem hiding this comment.
This is really great, thanks @lwalew
I'm overall very happy with this, I just have a couple of small comments RE docstrings + comments.
Also a couple of small Qs:
- It's mentioned in a docstring that if an RDF + Density benchmark are run together then the sim is only run once (great). Is this is also true if we e.g. run one in one run, and then later run the other? I know there's outputs+results caching by default so just double-checking this applies here
- Is it possible to generate a version of the results pages with these new results pages? Would love to see the layouts of these new pages and view the density plots etc. before approving if possible :)
|
@mcarobene-id For the 2nd remark, yes let me add those! For the first part as you say it's really an in-memory mechanism we have. We can reuse the output if running the two benchmarks as separate jobs but it needs a bit of extra logic to get it up and running. You need to persist the |
…mment Address review feedback that solvent dict order isn't meaningful. The solvent UI pages no longer hardcode ["CCl4", "methanol", "acetonitrile"] or index `structures` positionally; they derive the system list from the results and look structures up by name (new `ordered_structure_names` helper). Also drop the misleading "canonical order" comment/docstring, since scoring is entirely by-name and nothing depends on the config dict ordering. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Docs and make some functions private
|
Claude finished @lwalew's task in 3m 2s —— View job Code review: split molecular liquids into RDF + densityReviewed against
Findings1. 2. Dead defensive guard in score aggregation (nit) score=statistics.mean(
r.score if r.score is not None else 0.0 for r in structure_results
)
3. Reference-density source still missing (doc, open) 4. Answers to the two open review questions
No — reuse is in-memory only, scoped to a single
That's outside what I can produce from a static review (needs real NPT trajectories + a running Streamlit app). The page code itself looks correct: Correctness spot-checks (all pass)
Nice work overall — the only thing I'd treat as a real to-do before merge is the CHANGELOG entry. |
|
@mcarobene-id Just ran ViSNet locally on DEV: 3" src="https://github.com/user-attachments/assets/688eb405-7877-4bf3-b089-4868e0 |
…nt score mean - Add a Release 0.1.5 CHANGELOG entry describing the RDF/density split, the new water_density / solvent_density benchmarks, the shared-simulation reuse, and the new Benchmark.data_name mechanism. - Drop the unreachable `r.score if r.score is not None else 0.0` guard in the two solvent benchmarks' score aggregation; the per-structure `score` field defaults to 0.0 and is never None. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This comment has been minimized.
This comment has been minimized.
|
Claude finished @lwalew's task in 2m 10s —— View job Re-review: split molecular liquids into RDF + densityRe-checked the latest commits against
Prior findings — status
mcarobene-id's review comments — status
Remaining open item (not blocking, needs a domain owner)Reference-density source (doc) — Rendered pagesThe Solvent density page renders as intended: summary table (score + avg/per-solvent deviations) plus a per-frame density time series with the dashed reference line — the CCl₄ plot clearly shows the model sitting ~0.22 g/cm³ below reference, exactly the blow-up the scoring is designed to catch. The Solvent RDF page shows g(r) with the reference-peak vertical line. Both look correct. ✓ VerdictAll actionable findings from my previous review are addressed, and the new commits are clean. The only thing still outstanding is the density reference source, which is a doc citation pending a domain owner and not a code blocker. Nice work. 👍 |




Description
Stacked on #140. Splits the two Molecular Liquids benchmarks into four scored benchmarks — water/solvent × RDF/density — implementing the plan agreed in the Slack thread (each score counts independently towards the overall model average, grouped under the existing Molecular Liquids category, no separate landing page).
water_radial_distributionwater_density(new)solvent_radial_distributionsolvent_density(new)Key points
reusable_output_idand identicalModelOutputfield sets, so the NPT simulation runs only once when both are run together (verified: engine instantiated once across the pair).compute_metric_score(density_deviation / reference_density, threshold, alpha), rather than only shown in the RDF UI table.utils/molecular_liquids.py.Benchmark.data_namelets the density benchmarks reuse the RDF input data (no duplicate HuggingFace uploads).api_reference/molecular_liquids/.Density scoring
Follows Leon's NPT density analysis:
DENSITY_RELATIVE_DEVIATION_THRESHOLD = 0.02(2% relative deviation) with a gentleDENSITY_SCORE_ALPHA = 0.1(deliberately not the steep globalscoring.ALPHA = 3.0). On the existing NPT runs these reproduce the expected ranking (v1 models best on water; v2 models poor; solvent scores dragged down by the near-universal CCl₄ blow-up).Checklist for adding a new benchmark
data_name— no new upload needed)Notes
feat/npt-sims-solvent(feat: use NPT sims for water + solvent benchmarks #140); retarget todeveloponce feat: use NPT sims for water + solvent benchmarks #140 merges.🤖 Generated with Claude Code