Skip to content

Fix circular-coordinate handling and histogram support in NMI - #113

Merged
NDoering99 merged 9 commits into
wolberlab:masterfrom
MarvinTaterra:master
Jul 27, 2026
Merged

Fix circular-coordinate handling and histogram support in NMI#113
NDoering99 merged 9 commits into
wolberlab:masterfrom
MarvinTaterra:master

Conversation

@MarvinTaterra

Copy link
Copy Markdown
Collaborator

Summary

Two independent defects in the mutual-information pipeline, and they amplify each other.
Both affect NMI and therefore everything downstream: graph weights, path weights,
per-replica confidence, the block reference added in #112, and bootstrap path-confidence
intervals.

Defect A — the periodic coordinate was differenced without wrapping

MDAnalysis reports phi on [-180, 180). A residue oscillating near the branch cut flips
between +179 and -179, so np.diff turned a real 2 degree step into -358.

Defect B — the histogram support was derived from each input's own extrema

Within a single analysis that is defensible adaptive binning. It becomes a defect the moment
NMI values from different inputs are compared — which is exactly what the multi-replica
confidence path, the block reference and the bootstrap all do. Two replicas of identical
dynamics, one containing a single larger excursion, get different bin widths, and that
difference flows straight into MI_std -> MI_cv -> confidence.

Changes

  • Wrap the dihedral difference onto [-180, 180) in structure.py — the only np.diff
    in the package. Unconditional: this corrects a defect, not a configurable policy.
  • hist_range on NMICalculator, default (-180.0, 180.0), threaded through
    EdgeConfidenceCalculator, BlockSplitNull and BootstrapAnalysis so a whole run shares
    one binning. Added as the last parameter in each signature, so no positional call site
    breaks.
  • -hist-range LOW HIGH (or -hist-range none) to set the support per run.
  • -diagnose-bins and mdpath.src.mutual_information.binning_diagnostic(): per-residue
    histogram occupancy under the chosen support, plus branch-cut exposure (frames with
    |dphi| > 180 before wrapping), written to binning_diagnostic.csv.
  • Per-residue exposure is recorded in DataFrame.attrs["branch_cut_crossings"] and preserved
    across the multi-trajectory column alignment (a bare df[common_cols] dropped it).
  • Docstring notes in confidence.py and null_model.py that per-replica and per-block values
    are only comparable under a fixed support.

Backwards compatibility

hist_range=None restores the previous behaviour and is an exact no-op — it reproduces
pre-fix NMI values bit-for-bit, asserted in test_hist_range_none_is_exact_no_op. Use it to
reproduce published numbers.

Note the asymmetry: None restores only the binning. The dihedral wrap is unconditional,
because an unwrapped periodic difference is simply wrong.

No CLI, output-file or column-name changes beyond the two new optional flags.

Two exact-value assertions in test_mutual_information.py were recomputed under the new
default (largest change -3.3 % in the plain case; the invert case moves more because
rebinning changed which pair is the maximum). No assertion was weakened, and the pre-fix
values are retained in the backwards-compatibility test.

Tests

New regression tests cover:

  1. wrapping across the branch cut (a 2 degree step stays 2 degrees, and |dphi| <= 180);
  2. identical bin edges for two inputs with deliberately different extrema;
  3. exact reproduction of pre-fix values via hist_range=None;
  4. a single contaminated frame no longer changing a clean residue's occupied-bin count.

@NDoering99
NDoering99 merged commit e82f4c3 into wolberlab:master Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants