Skip to content

ES/02 runner - #175

Open
samarjeet wants to merge 23 commits into
NVIDIA:mainfrom
samarjeet:es/02-runner
Open

samarjeet wants to merge 23 commits into
NVIDIA:mainfrom
samarjeet:es/02-runner

Conversation

@samarjeet

Copy link
Copy Markdown

ALCHEMI Toolkit Pull Request

EnhancedSampling runner (no exchange): walker identity stamping, force-step ordering, update() exactly-once, evaluate-only force priming, warm_start(); AdaptivePotentialMixin; HarmonicUmbrellaBias; LowerWall / UpperWall / FlatBottomRestraint; periodic_difference; unit tests; gallery example; user guide

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Performance improvement
  • Documentation update
  • Refactoring (no functional changes)
  • CI/CD or infrastructure change

Related Issues

Changes Made

Testing

  • Unit tests pass locally (make pytest)
  • Linting passes (make lint)
  • New tests added for new functionality meets coverage expectations?

Checklist

  • I have read and understand the Contributing Guidelines
  • I have updated the CHANGELOG.md
  • I have performed a self-review of my code
  • I have added docstrings to new functions/classes
  • I have updated the documentation (if applicable)

Additional Notes

Tip

This repository uses Greptile, an AI code review service, to help conduct
pull request reviews. We encourage contributors to read and consider suggestions
made by Greptile, but note that human maintainers will provide the necessary
reviews for merging: Greptile's comments are not a qualitative judgement
of your code, nor is it an indication that the PR will be accepted/rejected.
We encourage the use of emoji reactions to Greptile comments, depending on
their usefulness and accuracy.

…riable

Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
…tputs

Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
…rough for AFTER_STEP captures

Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
…state_id

Signed-off-by: Samarjeet Prasad <p.samar.j@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Aug 21, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR introduces an enhanced-sampling subsystem with conservative and adaptive bias abstractions, a dynamics runner, umbrella and wall biases, periodic CV utilities, tests, documentation, and an example.

  • Adds force priming, walker/state stamping, adaptive update delivery, diagnostics, and warm-start support.
  • Adds autograd-derived bias forces and stress plus harmonic umbrella, wall, flat-bottom, pair-distance, and periodic-difference utilities.
  • Deprecates BiasedPotentialHook and expands the user guide, changelog, agent skill, tests, and gallery examples.

Important Files Changed

Filename Overview
nvalchemi/enhanced_sampling/_runner.py Adds the enhanced-sampling lifecycle and diagnostics, but priming bypasses AFTER_COMPUTE safety hooks and steps_per_epoch accepts zero.
nvalchemi/enhanced_sampling/_bias.py Adds structural bias results and autograd-derived conservative forces and stress with careful batch restoration and output validation.
nvalchemi/enhanced_sampling/biases/umbrella.py Adds validated shared and per-state harmonic umbrella parameters with periodic CV differences.
nvalchemi/enhanced_sampling/biases/walls.py Adds one-sided and flat-bottom differentiable wall potentials with parameter validation.
nvalchemi/enhanced_sampling/cv/pair_distance.py Adds a differentiable reduced-cell MIC pair-distance CV with explicit eager and compiled-mode limitations.
nvalchemi/models/_utils.py Extends shared autograd helpers to materialize zero gradients for intentionally unused inputs while retaining existing defaults.
nvalchemi/hooks/bias.py Deprecates the legacy bias hook while preserving its behavior.
.claude/skills/nvalchemi-dynamics-hooks/SKILL.md Adds legacy-bias deprecation guidance, but its runner-shipping statement is already outdated.
docs/userguide/enhanced_sampling.md Documents the new enhanced-sampling API, lifecycle guarantees, buffer requirements, and current limitations.
test/enhanced_sampling/test_runner.py Adds broad runner coverage but does not cover priming with sibling AFTER_COMPUTE safety hooks or a zero epoch interval.

Reviews (1): Last reviewed commit: "Added a compile regression for per-state..." | Re-trigger Greptile

Comment on lines +714 to +715
self.dynamics.compute(batch)
self._evaluate_and_apply(batch)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Priming bypasses safety hooks

When default force priming is used with an AFTER_COMPUTE hook such as MaxForceClampHook or NaNDetectorHook, prime_forces() applies the bias directly without dispatching those hooks, causing the first integrator half-step to consume total forces that were neither clamped nor checked for non-finite values.

Knowledge Base Used: Hook System


self.dynamics = dynamics
self.biases: dict[str, BiasPotential] = dict(biases or {})
self.steps_per_epoch = int(steps_per_epoch)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Zero epoch interval crashes

When steps_per_epoch=0, construction accepts the value and the first identity stamp divides by it, causing prime_forces() or run() to fail immediately with integer division by zero.

Suggested change
self.steps_per_epoch = int(steps_per_epoch)
self.steps_per_epoch = int(steps_per_epoch)
if self.steps_per_epoch <= 0:
raise ValueError("steps_per_epoch must be a positive integer")

Comment on lines +171 to +173
> stays functional until the enhanced-sampling runner ships, and constructing
> it emits a `DeprecationWarning`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Runner lifecycle guidance is stale

The updated skill says BiasedPotentialHook remains functional until the enhanced-sampling runner ships, but this PR ships and documents that runner; refresh the statement so agent guidance accurately describes the deprecated hook's support timeline.

Rule Used: Check `.claude/skills/nvalchemi-dynamics-hooks/SKI... (source)

Knowledge Base Used: Hook System

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +97 to +125
class EnhancedSampling:
"""Run biased dynamics on top of an existing ``BaseDynamics``.

The runner installs one composite hook and otherwise stays out of the
way: the model, the integrator, the thermostat, and every other hook
behave exactly as they would unbiased.

Parameters
----------
dynamics:
Any ``BaseDynamics``. Not subclassed, not wrapped — the runner
registers a hook on it and calls its ``run``.
biases:
Mapping of unique name to :class:`BiasPotential`. May be empty,
which reduces the runner to identity stamping (useful on its own for
replica exchange in PR 5).
steps_per_epoch:
Steps per consistency epoch, the boundary at which
:meth:`AdaptivePotentialMixin.commit_epoch` fires.
compile_biases:
When ``True``, ``torch.compile`` each conservative bias's
``energy()``. Not ``evaluate()`` — that path calls
``requires_grad_()``, which ``torch.compile`` cannot trace; see the
:class:`~nvalchemi.enhanced_sampling.ConservativeBias` docstring.
prime_after_update:
When ``True`` (default), re-evaluate biases and rewrite the batch's
total forces after an ``update()`` bumps a bias's state version, so
that anything reading ``batch.forces`` between steps sees the current
bias rather than the previous one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseDynamics already owns the stepping loop, so a second run() means every
future workflow has to decide which loop it belongs to, and the two will drift.

The runner's remaining jobs are smaller than they look. It stamps identity
fields onto the batch, fires things on a cadence, and writes a checkpoint. The
toolkit already has mechanisms for the first two — register_bookkeeping_key
(the same mechanism status and system_id use, so the fields survive refill
and graduation) and Hook.frequency. The third is row 8. Note that #176 has to
add state_dict to BaseDynamics anyway for the runner to work, which is
already most of what a strategy would need.

# nvalchemi/dynamics/strategy.py — mirrors TrainingStrategy
class DynamicsStrategy(BaseModel):
    """Declarative recipe that builds and runs a configured BaseDynamics."""

    engine: type[BaseDynamics] = NVTLangevin
    engine_kwargs: dict[str, Any] = {}
    n_steps: int | None = None
    extra_hooks: list[Hook] = []          # runtime, not serialized

    def build_hooks(self) -> list[Hook]: ...
    def build(self, model) -> BaseDynamics: ...
    def run(self, batch, model, n_steps=None) -> Batch: ...
    def to_spec_dict(self) -> dict[str, Any]: ...

# nvalchemi/enhanced_sampling/strategy.py  (~80 lines, was 1414)
class EnhancedSampling(DynamicsStrategy):
    biases: dict[str, BiasPotential] = {}
    steps_per_epoch: int = 10_000

    def build_hooks(self) -> list[Hook]:
        return [WalkerIdentityHook(),
                *self.biases.values(),
                EpochCommitHook(frequency=self.steps_per_epoch)]

BaseDynamics.register_bookkeeping_key(
    "walker_id", lambda n, dev: torch.arange(n, device=dev).reshape(n, 1)
)

TrainingStrategy is the precedent worth copying: it is a Pydantic model that
configures a loop it does not own, and it serializes to a spec. Under this shape
NEB, Relax and EOSScan become sibling subclasses rather than sibling
runners.

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