Coverage for ase / md / logger.py: 76.36%
55 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-30 08:22 +0000
1# fmt: off
3"""Logging for molecular dynamics."""
4import weakref
5from typing import IO, Any
7from ase import Atoms, units
8from ase.parallel import world
9from ase.utils import IOContext
12class MDLogger(IOContext):
13 """Class for logging molecular dynamics simulations.
15 Parameters
16 ----------
17 dyn : :class:`~ase.md.md.MolecularDynamics`
18 ASE :class:`~ase.md.md.MolecularDynamics` object.
19 Only a weak reference is kept.
20 atoms : :class:`~ase.Atoms`
21 ASE :class:`~ase.Atoms` object.
22 logfile : IO | str
23 File name or open file, "-" meaning standard output.
24 stress : bool, default: :obj:`False`
25 If :obj:`True`, the logger writes stress.
26 peratom : bool, default: :obj:`False`
27 If :obj:`True`, the logger writes energies per atom.
28 mode : str, default: ``'a'``
29 How the file is opened if logfile is a filename.
31 """
33 def __init__(
34 self,
35 dyn: Any, # not fully annotated so far to avoid a circular import
36 atoms: Atoms,
37 logfile: IO | str,
38 header: bool = True,
39 stress: bool = False,
40 peratom: bool = False,
41 mode: str = "a",
42 comm=world,
43 ):
44 self.dyn = weakref.proxy(dyn) if hasattr(dyn, "get_time") else None
45 self.atoms = atoms
46 global_natoms = atoms.get_global_number_of_atoms()
47 self.logfile = self.openfile(file=logfile, mode=mode, comm=comm)
48 self.stress = stress
49 self.peratom = peratom
50 if self.dyn is not None:
51 self.hdr = "%-9s " % ("Time[ps]",)
52 self.fmt = "%-10.4f "
53 else:
54 self.hdr = ""
55 self.fmt = ""
56 if self.peratom:
57 self.hdr += "%12s %12s %12s %6s" % ("Etot/N[eV]", "Epot/N[eV]",
58 "Ekin/N[eV]", "T[K]")
59 self.fmt += "%12.4f %12.4f %12.4f %6.1f"
60 else:
61 self.hdr += "%12s %12s %12s %6s" % ("Etot[eV]", "Epot[eV]",
62 "Ekin[eV]", "T[K]")
63 # Choose a sensible number of decimals
64 if global_natoms <= 100:
65 digits = 4
66 elif global_natoms <= 1000:
67 digits = 3
68 elif global_natoms <= 10000:
69 digits = 2
70 else:
71 digits = 1
72 self.fmt += 3 * ("%%12.%df " % (digits,)) + " %6.1f"
73 if self.stress:
74 self.hdr += (' ---------------------- stress [GPa] '
75 '-----------------------')
76 self.fmt += 6 * " %10.3f"
77 self.fmt += "\n"
78 if header:
79 self.logfile.write(self.hdr + "\n")
81 def __del__(self):
82 self.close()
84 def __call__(self):
85 epot = self.atoms.get_potential_energy()
86 ekin = self.atoms.get_kinetic_energy()
87 temp = self.atoms.get_temperature()
88 global_natoms = self.atoms.get_global_number_of_atoms()
89 if self.peratom:
90 epot /= global_natoms
91 ekin /= global_natoms
92 if self.dyn is not None:
93 t = self.dyn.get_time() / (1000 * units.fs)
94 dat = (t,)
95 else:
96 dat = ()
97 dat += (epot + ekin, epot, ekin, temp)
98 if self.stress:
99 dat += tuple(self.atoms.get_stress(
100 include_ideal_gas=True) / units.GPa)
101 self.logfile.write(self.fmt % dat)
102 self.logfile.flush()