1use molrs::types::F;
4use std::io::BufWriter;
5use std::path::PathBuf;
6use std::time::Instant;
7
8use crate::context::PackContext;
9use crate::frame::compute_mol_ids;
10use crate::numerics::objective_small_floor;
11
12#[derive(Debug, Clone, Copy)]
16pub struct PhaseInfo {
17 pub phase: usize,
19 pub total_phases: usize,
21 pub molecule_type: Option<usize>,
24}
25
26#[derive(Debug, Clone, Copy, Default)]
32pub struct PhaseReport {
33 pub iterations: usize,
35 pub fdist: F,
37 pub frest: F,
39 pub converged: bool,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
45pub enum MolpackLogLevel {
46 #[default]
48 Quiet,
49 Summary,
51 Progress,
53 Verbose,
55}
56
57impl MolpackLogLevel {
58 #[inline]
59 pub const fn is_enabled(self) -> bool {
60 !matches!(self, Self::Quiet)
61 }
62}
63
64#[derive(Debug, Clone)]
66pub struct StepInfo {
67 pub loop_idx: usize,
69 pub max_loops: usize,
71 pub phase: PhaseInfo,
73 pub fdist: F,
75 pub frest: F,
77 pub improvement_pct: F,
79 pub radscale: F,
81 pub precision: F,
83 pub relaxer_acceptance: Vec<(usize, F)>,
85}
86
87pub trait Handler: Send {
91 fn on_start(&mut self, _ntotat: usize, _ntotmol: usize) {}
94
95 fn on_initialized(&mut self, _sys: &PackContext) {}
98
99 fn on_step(&mut self, info: &StepInfo, sys: &PackContext);
101
102 fn on_phase_start(&mut self, _info: &PhaseInfo) {}
105
106 fn on_finish(&mut self, _sys: &PackContext) {}
108
109 fn should_stop(&self) -> bool {
111 false
112 }
113
114 fn on_inner_iter(&mut self, _iter: u32, _f: F, _sys: &PackContext) {}
127
128 fn on_phase_end(&mut self, _info: &PhaseInfo, _report: &PhaseReport) {}
135}
136
137pub struct NullHandler;
141
142impl Handler for NullHandler {
143 fn on_step(&mut self, _info: &StepInfo, _sys: &PackContext) {}
144}
145
146pub struct XYZHandler {
153 path: PathBuf,
154 every: usize,
156 file: Option<BufWriter<std::fs::File>>,
157 mol_ids: Vec<usize>,
159}
160
161impl XYZHandler {
162 pub fn new(path: impl Into<PathBuf>, every: usize) -> Self {
168 assert!(every >= 1, "every must be >= 1");
169 Self {
170 path: path.into(),
171 every,
172 file: None,
173 mol_ids: Vec::new(),
174 }
175 }
176
177 fn open(&mut self) {
178 if self.file.is_some() {
179 return;
180 }
181 match std::fs::OpenOptions::new()
182 .write(true)
183 .create(true)
184 .truncate(true)
185 .open(&self.path)
186 {
187 Ok(f) => self.file = Some(BufWriter::new(f)),
188 Err(e) => log::warn!("XYZHandler: cannot open {}: {e}", self.path.display()),
189 }
190 }
191
192 fn write_frame(&mut self, comment: &str, sys: &PackContext) {
193 self.open();
194 let Some(ref mut w) = self.file else { return };
195 use std::io::Write;
196 let nat = sys.xcart.len();
197 let _ = writeln!(w, "{nat}");
198 let _ = writeln!(w, "Properties=species:S:1:pos:R:3:mol:I:1 {comment}");
199 for (icart, pos) in sys.xcart.iter().enumerate() {
200 let elem = sys
201 .elements
202 .get(icart)
203 .and_then(|e| *e)
204 .map(|e| e.symbol())
205 .unwrap_or("X");
206 let mol_id = self.mol_ids.get(icart).copied().unwrap_or(0);
207 let _ = writeln!(
208 w,
209 "{elem} {:.6} {:.6} {:.6} {mol_id}",
210 pos[0], pos[1], pos[2]
211 );
212 }
213 let _ = w.flush();
214 }
215}
216
217impl Handler for XYZHandler {
218 fn on_initialized(&mut self, sys: &PackContext) {
219 self.mol_ids = compute_mol_ids(sys);
220 }
221
222 fn on_step(&mut self, info: &StepInfo, sys: &PackContext) {
223 if info.loop_idx.is_multiple_of(self.every) {
224 self.write_frame(&format!("step {}", info.loop_idx), sys);
225 }
226 }
227}
228
229pub struct ProgressHandler {
235 start: Option<Instant>,
236}
237
238impl ProgressHandler {
239 pub fn new() -> Self {
240 Self { start: None }
241 }
242}
243
244impl Default for ProgressHandler {
245 fn default() -> Self {
246 Self::new()
247 }
248}
249
250impl Handler for ProgressHandler {
251 fn on_start(&mut self, ntotat: usize, ntotmol: usize) {
252 self.start = Some(Instant::now());
253 eprintln!("Packing {ntotmol} molecules ({ntotat} atoms)...");
254 }
255
256 fn on_initialized(&mut self, sys: &PackContext) {
257 let elapsed = self.start.map(|t| t.elapsed().as_secs_f64()).unwrap_or(0.0);
258 eprintln!(
259 " Initializing... done ({:.1}s) overlap: {:.4e} constraints: {:.4e}",
260 elapsed, sys.fdist, sys.frest
261 );
262 }
263
264 fn on_phase_start(&mut self, info: &PhaseInfo) {
265 let desc = match info.molecule_type {
266 Some(itype) => format!("Compacting type {itype}"),
267 None => "Optimizing all types together".to_string(),
268 };
269 eprintln!(" Phase [{}/{}] {desc}", info.phase + 1, info.total_phases,);
270 }
271
272 fn on_step(&mut self, info: &StepInfo, _sys: &PackContext) {
273 let elapsed = self.start.map(|t| t.elapsed().as_secs_f64()).unwrap_or(0.0);
274 eprintln!(
275 " Step [{}/{}] overlap: {:.2e} constraints: {:.2e} improved {:.1}% ({:.1}s)",
276 info.loop_idx + 1,
277 info.max_loops,
278 info.fdist,
279 info.frest,
280 info.improvement_pct,
281 elapsed,
282 );
283 }
284
285 fn on_finish(&mut self, sys: &PackContext) {
286 let elapsed = self.start.map(|t| t.elapsed().as_secs_f64()).unwrap_or(0.0);
287 if sys.fdist < 0.01 && sys.frest < 0.01 {
288 eprintln!(
289 " Converged in {:.1}s — overlap: {:.2e} constraints: {:.2e}",
290 elapsed, sys.fdist, sys.frest,
291 );
292 } else {
293 eprintln!(
294 " Did not converge ({:.1}s) — overlap: {:.2e} constraints: {:.2e}",
295 elapsed, sys.fdist, sys.frest,
296 );
297 }
298 }
299}
300
301pub struct LammpsLogHandler {
310 level: MolpackLogLevel,
311 every: usize,
312 tolerance: F,
313 precision: F,
314 seed: u64,
315 max_loops: usize,
316 ntypes: usize,
317 periodic_box: Option<([F; 3], [F; 3], [bool; 3])>,
318 start: Option<Instant>,
319 phase_start: Option<Instant>,
320}
321
322impl LammpsLogHandler {
323 #[allow(clippy::too_many_arguments)]
324 pub fn new(
325 level: MolpackLogLevel,
326 every: usize,
327 tolerance: F,
328 precision: F,
329 seed: u64,
330 max_loops: usize,
331 ntypes: usize,
332 periodic_box: Option<([F; 3], [F; 3], [bool; 3])>,
333 ) -> Self {
334 Self {
335 level,
336 every: every.max(1),
337 tolerance,
338 precision,
339 seed,
340 max_loops,
341 ntypes,
342 periodic_box,
343 start: None,
344 phase_start: None,
345 }
346 }
347
348 fn elapsed(&self) -> F {
349 self.start.map(|t| t.elapsed().as_secs_f64()).unwrap_or(0.0)
350 }
351
352 fn phase_elapsed(&self) -> F {
353 self.phase_start
354 .map(|t| t.elapsed().as_secs_f64())
355 .unwrap_or(0.0)
356 }
357}
358
359impl Handler for LammpsLogHandler {
360 fn on_start(&mut self, ntotat: usize, ntotmol: usize) {
361 if !self.level.is_enabled() {
362 return;
363 }
364
365 let now = Instant::now();
366 self.start = Some(now);
367 self.phase_start = Some(now);
368
369 eprintln!("Molpack screen log");
370 eprintln!("System information:");
371 eprintln!(" molecule types = {}", self.ntypes);
372 eprintln!(" molecules = {ntotmol}");
373 eprintln!(" atoms = {ntotat}");
374 eprintln!("Settings:");
375 eprintln!(" tolerance = {:.6} A", self.tolerance);
376 eprintln!(" precision = {:.6}", self.precision);
377 eprintln!(" seed = {}", self.seed);
378 eprintln!(" nloop = {}", self.max_loops);
379 match self.periodic_box {
380 Some((min, max, flags)) => eprintln!(
381 " pbc = [{:.6}, {:.6}, {:.6}] -> [{:.6}, {:.6}, {:.6}] flags={:?}",
382 min[0], min[1], min[2], max[0], max[1], max[2], flags
383 ),
384 None => eprintln!(" pbc = off"),
385 }
386 }
387
388 fn on_initialized(&mut self, sys: &PackContext) {
389 if !self.level.is_enabled() {
390 return;
391 }
392 eprintln!(
393 "Initialization: time={:.3}s overlap={:.4e} restraints={:.4e}",
394 self.elapsed(),
395 sys.fdist,
396 sys.frest
397 );
398 }
399
400 fn on_phase_start(&mut self, info: &PhaseInfo) {
401 if !self.level.is_enabled() {
402 return;
403 }
404 self.phase_start = Some(Instant::now());
405 let desc = match info.molecule_type {
406 Some(itype) => format!("type {itype} compaction"),
407 None => "all-type optimization".to_string(),
408 };
409 eprintln!("Phase {}/{}: {desc}", info.phase + 1, info.total_phases);
410 if self.level >= MolpackLogLevel::Progress {
411 if self.level >= MolpackLogLevel::Verbose {
412 eprintln!(
413 "{:>8} {:>14} {:>14} {:>10} {:>10} {:>10}",
414 "Step", "Overlap", "Restraint", "Improve%", "RadScale", "Time"
415 );
416 } else {
417 eprintln!(
418 "{:>8} {:>14} {:>14} {:>10} {:>10}",
419 "Step", "Overlap", "Restraint", "Improve%", "Time"
420 );
421 }
422 }
423 }
424
425 fn on_step(&mut self, info: &StepInfo, _sys: &PackContext) {
426 if self.level < MolpackLogLevel::Progress || !info.loop_idx.is_multiple_of(self.every) {
427 return;
428 }
429 if self.level >= MolpackLogLevel::Verbose {
430 eprintln!(
431 "{:>8} {:>14.6e} {:>14.6e} {:>10.3} {:>10.4} {:>10.3}",
432 info.loop_idx + 1,
433 info.fdist,
434 info.frest,
435 info.improvement_pct,
436 info.radscale,
437 self.elapsed(),
438 );
439 } else {
440 eprintln!(
441 "{:>8} {:>14.6e} {:>14.6e} {:>10.3} {:>10.3}",
442 info.loop_idx + 1,
443 info.fdist,
444 info.frest,
445 info.improvement_pct,
446 self.elapsed(),
447 );
448 }
449 }
450
451 fn on_phase_end(&mut self, info: &PhaseInfo, report: &PhaseReport) {
452 if !self.level.is_enabled() {
453 return;
454 }
455 eprintln!(
456 "Phase {}/{} summary: steps={} converged={} overlap={:.6e} restraints={:.6e} time={:.3}s",
457 info.phase + 1,
458 info.total_phases,
459 report.iterations,
460 report.converged,
461 report.fdist,
462 report.frest,
463 self.phase_elapsed(),
464 );
465 }
466
467 fn on_finish(&mut self, sys: &PackContext) {
468 if !self.level.is_enabled() {
469 return;
470 }
471 let converged = sys.fdist < self.precision && sys.frest < self.precision;
472 eprintln!(
473 "Final summary: converged={} overlap={:.6e} restraints={:.6e} elapsed={:.3}s",
474 converged,
475 sys.fdist,
476 sys.frest,
477 self.elapsed(),
478 );
479 }
480}
481
482pub struct EarlyStopHandler {
492 pub threshold: F,
494 pub warmup: usize,
496 pub patience: usize,
498 prev_violation: F,
499 stall_count: usize,
500 stop: bool,
501}
502
503impl EarlyStopHandler {
504 pub fn new(threshold: F) -> Self {
505 Self {
506 threshold,
507 warmup: 5,
508 patience: 3,
509 prev_violation: F::INFINITY,
510 stall_count: 0,
511 stop: false,
512 }
513 }
514
515 pub fn with_warmup(mut self, warmup: usize) -> Self {
516 self.warmup = warmup;
517 self
518 }
519
520 pub fn with_patience(mut self, patience: usize) -> Self {
521 self.patience = patience.max(1);
522 self
523 }
524}
525
526impl Default for EarlyStopHandler {
527 fn default() -> Self {
530 Self::new(F::NEG_INFINITY)
531 }
532}
533
534impl Handler for EarlyStopHandler {
535 fn on_initialized(&mut self, _sys: &PackContext) {
536 self.prev_violation = F::INFINITY;
537 self.stall_count = 0;
538 self.stop = false;
539 }
540
541 fn on_phase_start(&mut self, _info: &PhaseInfo) {
542 self.prev_violation = F::INFINITY;
543 self.stall_count = 0;
544 self.stop = false;
545 }
546
547 fn on_step(&mut self, info: &StepInfo, _sys: &PackContext) {
548 let v = info.fdist + info.frest;
549 if info.loop_idx <= self.warmup {
550 self.prev_violation = v;
551 return;
552 }
553 let rel_change = if self.prev_violation > 0.0 {
554 (self.prev_violation - v) / self.prev_violation
555 } else if v < objective_small_floor() {
556 1.0 } else {
558 F::INFINITY
559 };
560
561 if rel_change < self.threshold {
562 self.stall_count += 1;
563 if self.stall_count >= self.patience {
564 log::debug!(
565 "EarlyStop: stalled for {} iters (rel_change={:.2e})",
566 self.stall_count,
567 rel_change
568 );
569 self.stop = true;
570 }
571 } else {
572 self.stall_count = 0;
573 }
574 self.prev_violation = v;
575 }
576
577 fn should_stop(&self) -> bool {
578 self.stop
579 }
580}