Skip to content

Callbacks and Optimizers

Callbacks observe a packing run. In-loop optimizers modify a copy's reference geometry between GENCAN iterations.

Screen output

Enable LAMMPS-style progress output through the builder:

use molpack::{GencanPack, LogLevel, PackEngine};

let engine = GencanPack::new().with_log(LogLevel::Progress, 10);

nsteps is how many loops pass between progress lines (at least 1). The library default is quiet and every loop; the CLI turns progress on. Python is the same call: with_log(level, nsteps=1).

Built-in callbacks

XyzTrajectoryCallback needs the io feature: it writes each snapshot through molrs's extended XYZ writer.

use molpack::{EarlyStopCallback, GencanPack, PackEngine, XyzTrajectoryCallback};

let engine = GencanPack::new()
    .with_callback(Box::new(XyzTrajectoryCallback::new("traj.xyz", 10)))
    // Stricter than the default early stop: 20 % per 5 loops.
    .with_early_stop(EarlyStopCallback::new(20.0).with_patience(5));

Packmol runs every phase until it converges or reaches nloop. GencanPack additionally ends a phase once, at the user's radii (radscale == 1), Packmol's bestf has improved by less than 10 % (its movebad threshold) over 10 loops — measured with Packmol's fimprov formula. A pack that stalls there is almost always too dense, and a converged == false in minutes beats running to max_loops. with_early_stop(None) restores Packmol's behaviour.

with_callback is a PackEngine builder, so it works the same on CbmcGrow. Use callbacks for progress logs, trajectory snapshots, custom observation, and early stop. Callbacks receive an immutable PackSystem view; they do not mutate engine state.

Custom callbacks

Implement the Callback trait when you need structured events from a run:

use molpack::{Callback, PackSystem, StepReport};

#[derive(Debug)]
struct WatchFdist;

impl Callback for WatchFdist {
    fn on_step(&mut self, step: &StepReport, _sys: &PackSystem) {
        eprintln!("phase={} loop={} fdist={}", step.phase.phase, step.loop_idx, step.fdist);
    }
}

Every StepReport also says which packing algorithm emitted it. A stage is one algorithm behind molpack's packing seam (the Stage trait), and step.stage is a StageProgress carrying index (0-based position of the stage in the run), total (how many stages the run has), and name (the stage's own name, "gencan" for the rigid-body path). One engine drives one stage, so a plain GencanPack or CbmcGrow run reports index = 0, total = 1.

Two further callbacks bracket a whole stage, the way on_phase_start / on_phase_end bracket one GENCAN phase:

use molpack::StageProgress;
use molpack::{Callback, PackSystem, StageOutcome, StepReport};

struct WatchStages;

impl Callback for WatchStages {
    fn on_step(&mut self, _step: &StepReport, _sys: &PackSystem) {}

    fn on_stage_start(&mut self, stage: &StageProgress) {
        eprintln!("stage {}/{} ({}) starting", stage.index + 1, stage.total, stage.name);
    }

    fn on_stage_end(&mut self, stage: &StageProgress, outcome: &StageOutcome, sys: &PackSystem) {
        eprintln!(
            "stage {} converged={} degraded={} fdist={} frest={}",
            stage.name, outcome.converged, outcome.degraded, sys.fdist, sys.frest,
        );
    }
}

Both have default no-op bodies, and a run driven by a single engine calls neither; they are the seam a caller that chains stages itself brackets each stage with. Note where the numbers come from: StageOutcome reports only what the stage alone knows (converged, degraded), while the violation maxima fdist and frest are read off the post-stage PackSystem, so every algorithm is judged by the same objective.

See Extending for a full custom-callback walkthrough and for writing a stage of your own.

In-loop optimizers

An in-loop optimizer reshapes a copy's reference geometry while it is being placed. It is useful for flexible molecules that need to sample torsions to fit their restraints. Bind one to the engine, naming the targets it applies to:

use molpack::{GencanPack, OptimizeSelect, PackEngine, TorsionMcOptimizer};

let engine = GencanPack::new().with_optimizer(
    OptimizeSelect::per_copy(["chain"]).with_environment(8.0),
    TorsionMcOptimizer::new(&graph)
        .with_temperature(0.5)
        .with_steps(20),
);

Each copy is relaxed on its own, so copies of one target start identical and then diverge. After each call molpack re-evaluates the packing objective and reverts the conformer if it got worse. OptimizeSelect::joint relaxes all selected copies as one group. Any molrs Optimizer fits the same slot; binding a force-field one such as Lbfgs needs molrs's ff module (enable it on your own molcrafts-molrs dependency; molpack has no ff feature).