ACO orientation scans and path-atlas plots¶
Rigid dimer orientation scans (S² × SO(3) × r) diagnose spurious wells on the
hybrid PES. This page covers running the scan, plotting the figure set,
and saving outputs under a training-run UUID.
Related: Dimer scans (COM distance / LR solvers).
Scripts¶
| Script | Role |
|---|---|
scripts/scan_dimer_orientations.py |
Batched hybrid_forward ray scan → rays.csv + summary.json |
scripts/plot_orient_hemisphere_annotated.py |
Hemisphere maps, ASE overlays, path atlas, 1D slices |
scripts/run_gfn2_nms_hybrid.sh |
Train → freeze → orientation gate (cluster workflow) |
Plotting needs only CPU + ASE + matplotlib (no CHARMM / GPU). The scan itself needs a JAX checkpoint (GPU recommended for ~240 rays).
UUID layout¶
Store every figure set for a checkpoint under that run’s UUID:
artifacts/orient_plots/<uuid>/
rays.csv # copy of the primary scan (optional)
scan_summary.json # from scan summary.json
shared_scales.json # locked colour / axis scales
path_atlas_<label>.png
equirectangular_maps_<label>.png
hemisphere_*.png
perspectives_gallery_<label>.png
slice_dir*_with_ase_<label>.png
FLIP_<label>.md # auto index of outputs
README.md # how this folder was made
Example (GFN2-NMS epoch-222 gate):
UUID=f448e34c-cca7-43f6-8b8e-f4986b9403eb
artifacts/orient_plots/$UUID/
Use the same UUID string in --label so filenames carry the run id:
--label "gfn2nms-${UUID}"
Large PNGs live under artifacts/ (gitignored). Keep the recipe in-repo via
this page; optionally mirror the folder to portable storage or the cluster.
1. Run the orientation scan¶
Match the gate in run_gfn2_nms_hybrid.sh (ACO example):
UUID=f448e34c-cca7-43f6-8b8e-f4986b9403eb
CKPT=/path/to/ckpts/gfn2_nms/gfn2nms-${UUID}/epoch-222 # or a frozen copy
DATA=/path/to/out_combined_dedup/..._test.npz
OUT=/path/to/orient_gfn2nms_${UUID:0:8}
uv run python scripts/scan_dimer_orientations.py \
--checkpoint "$CKPT" \
--data "$DATA" \
--resid ACO \
--n-directions 10 \
--n-orientations 24 \
--n-r 36 \
--mm-switch-on 6.0 \
--use-ema \
--out "$OUT"
Outputs: $OUT/rays.csv, $OUT/summary.json.
Companion surfaces for flip comparisons (same geometry grid when possible):
- ML hybrid: e.g.
orient_6A/rays.csv - GFN2 / xTB reference: e.g.
orient_xtb/rays.csv - Validate oris:
validate_ACO/rays_ACO.csv(spliced into atlas tags when present)
2. Plot the primary (full figure set)¶
UUID=f448e34c-cca7-43f6-8b8e-f4986b9403eb
DATA=/Volumes/PortableSSD/DATA/acodcm # or your data root
PLOT=artifacts/orient_plots/${UUID}
mkdir -p "$PLOT"
cp "$DATA/orient_gfn2nms_f448e34c/summary.json" "$PLOT/scan_summary.json"
uv run python scripts/plot_orient_hemisphere_annotated.py \
--rays "$DATA/orient_gfn2nms_f448e34c/rays.csv" \
--label "gfn2nms-${UUID}" \
--monomer "$DATA/pdb/aco.pdb" \
--ref-rays "$DATA/orient_xtb/rays.csv" \
--match-rays "$DATA/orient_xtb/rays.csv" \
--match-rays "$DATA/orient_6A/rays.csv" \
--validate "$DATA/validate_ACO/rays_ACO.csv" \
--out "$PLOT"
Important: shared scales¶
--match-rays locks colour bars and E(r) / r axes across flip partners.
Pass companions only (not the primary again): primary is always included
from --rays. Repeating the primary in --match-rays skews percentiles.
After regenerating companions with overlapping match sets, freeze scales once:
uv run python - <<'PY'
from pathlib import Path
import json
from dataclasses import asdict
from scripts.plot_orient_hemisphere_annotated import build_shared_scales
from scripts.plot_orient_hemisphere_surfaces import _load_rays
DATA = Path("/Volumes/PortableSSD/DATA/acodcm")
OUT = Path("artifacts/orient_plots/f448e34c-cca7-43f6-8b8e-f4986b9403eb")
shared = build_shared_scales(
_load_rays(DATA / "orient_gfn2nms_f448e34c/rays.csv"),
_load_rays(DATA / "orient_6A/rays.csv"),
_load_rays(DATA / "orient_xtb/rays.csv"),
how="min",
)
(OUT / "shared_scales.json").write_text(json.dumps(asdict(shared), indent=2) + "\n")
print(asdict(shared))
PY
3. Flip companions (path atlas only)¶
Same --ref-rays / --validate / shared companions so tags and colour maps line up:
uv run python scripts/plot_orient_hemisphere_annotated.py \
--rays "$DATA/orient_6A/rays.csv" --label ML --atlas-only \
--monomer "$DATA/pdb/aco.pdb" \
--ref-rays "$DATA/orient_xtb/rays.csv" \
--match-rays "$DATA/orient_xtb/rays.csv" \
--match-rays "$DATA/orient_gfn2nms_f448e34c/rays.csv" \
--validate "$DATA/validate_ACO/rays_ACO.csv" \
--out "$PLOT"
uv run python scripts/plot_orient_hemisphere_annotated.py \
--rays "$DATA/orient_xtb/rays.csv" --label GFN2 --atlas-only \
--monomer "$DATA/pdb/aco.pdb" \
--ref-rays "$DATA/orient_xtb/rays.csv" \
--match-rays "$DATA/orient_6A/rays.csv" \
--match-rays "$DATA/orient_gfn2nms_f448e34c/rays.csv" \
--validate "$DATA/validate_ACO/rays_ACO.csv" \
--out "$PLOT"
Open path_atlas_gfn2nms-<uuid>.png, path_atlas_ML.png, and
path_atlas_GFN2.png side by side.
What each figure is¶
| File | Content |
|---|---|
path_atlas_*.png |
Tags A–D from physical surface minima (prefer --ref-rays); complete graph among tags; 3-frame filmstrips A \| mid \| B; maps + E(r) panels |
exchange_symmetry_mean_centered_*.png |
Standalone û/−û xy scatter with each dataset (ML and ref) mean-centered by its own μ (preserves the y=x diagonal) |
equirectangular_maps_*.png |
Energy on approach / orientation projections |
hemisphere_annotated_dashboard_*.png |
Six hemispheres + legend + tags |
hemisphere_ase_ring_*.png |
ASE overlays around hemispheres |
perspectives_gallery_*.png |
Tagged dimers from several cameras |
slice_dir*_with_ase_*.png |
1D E(r) slices with matching letter tags |
projection_explainer_*.png |
How the projections are built |
FLIP_*.md |
Auto-generated file list + regenerate snippet |
CLI knobs worth knowing:
--ref-rays— star physical minima (typically GFN2)--ref-top-n— how many deepest ref wells to star (default 12)--atlas-only— skip non-atlas panels (companions)--how min|mean— aggregate used for shared scales (defaultmin)--label— badge + filename suffix
Example run (epoch-222 GFN2-NMS)¶
| Field | Value |
|---|---|
| UUID | f448e34c-cca7-43f6-8b8e-f4986b9403eb |
| Checkpoint | …/gfn2_nms/gfn2nms-<uuid>/epoch-222 (EMA) |
| Scan | 10 dirs × 24 oris × 36 r, mm_switch_on=6.0 |
| Rays | 240 evaluated; 17.5% spurious; deepest −4.54 kcal/mol |
| Local plots | artifacts/orient_plots/f448e34c-cca7-43f6-8b8e-f4986b9403eb/ |
Pass criteria for a healthy gate are project-specific; the scan summary’s
frac_rays_spurious and deepest_kcal are the usual first look before reading
the path atlas.
4. Evaluate the test set (same UUID)¶
Pin the frozen epoch (do not pass the live run root — that picks the latest
epoch) and evaluate the in-distribution NPZ. GFN2-NMS energies are already
relative — do not pass --subtract-atom-energies.
UUID=f448e34c-cca7-43f6-8b8e-f4986b9403eb
ACODCM=/mmhome/boittier/home/mmml_tutorial/acodcm
FROZEN=$ACODCM/ckpts/_gate_frozen_e222 # contains only epoch-222
OUT=$ACODCM/eval_gfn2nms_${UUID}
mmml physnet-evaluate \
--checkpoint "$FROZEN" \
--data "$ACODCM/gfn2_nms_test.npz" \
-o "$OUT/gfn2_nms_test_e222" \
--batch-size 16 --plots --use-ema
Sync locally:
rsync -avz studix:$OUT/ artifacts/orient_plots/${UUID}/eval/