Coverage for /builds/ase/ase/ase/md/logger.py: 76.36%

55 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-08-02 00:12 +0000

1# fmt: off 

2 

3"""Logging for molecular dynamics.""" 

4import weakref 

5from typing import IO, Any, Union 

6 

7from ase import Atoms, units 

8from ase.parallel import world 

9from ase.utils import IOContext 

10 

11 

12class MDLogger(IOContext): 

13 """Class for logging molecular dynamics simulations. 

14 

15 Parameters: 

16 dyn: The dynamics. Only a weak reference is kept. 

17 

18 atoms: The atoms. 

19 

20 logfile: File name or open file, "-" meaning standard output. 

21 

22 stress=False: Include stress in log. 

23 

24 peratom=False: Write energies per atom. 

25 

26 mode="a": How the file is opened if logfile is a filename. 

27 """ 

28 

29 def __init__( 

30 self, 

31 dyn: Any, # not fully annotated so far to avoid a circular import 

32 atoms: Atoms, 

33 logfile: Union[IO, str], 

34 header: bool = True, 

35 stress: bool = False, 

36 peratom: bool = False, 

37 mode: str = "a", 

38 comm=world, 

39 ): 

40 self.dyn = weakref.proxy(dyn) if hasattr(dyn, "get_time") else None 

41 self.atoms = atoms 

42 global_natoms = atoms.get_global_number_of_atoms() 

43 self.logfile = self.openfile(file=logfile, mode=mode, comm=comm) 

44 self.stress = stress 

45 self.peratom = peratom 

46 if self.dyn is not None: 

47 self.hdr = "%-9s " % ("Time[ps]",) 

48 self.fmt = "%-10.4f " 

49 else: 

50 self.hdr = "" 

51 self.fmt = "" 

52 if self.peratom: 

53 self.hdr += "%12s %12s %12s %6s" % ("Etot/N[eV]", "Epot/N[eV]", 

54 "Ekin/N[eV]", "T[K]") 

55 self.fmt += "%12.4f %12.4f %12.4f %6.1f" 

56 else: 

57 self.hdr += "%12s %12s %12s %6s" % ("Etot[eV]", "Epot[eV]", 

58 "Ekin[eV]", "T[K]") 

59 # Choose a sensible number of decimals 

60 if global_natoms <= 100: 

61 digits = 4 

62 elif global_natoms <= 1000: 

63 digits = 3 

64 elif global_natoms <= 10000: 

65 digits = 2 

66 else: 

67 digits = 1 

68 self.fmt += 3 * ("%%12.%df " % (digits,)) + " %6.1f" 

69 if self.stress: 

70 self.hdr += (' ---------------------- stress [GPa] ' 

71 '-----------------------') 

72 self.fmt += 6 * " %10.3f" 

73 self.fmt += "\n" 

74 if header: 

75 self.logfile.write(self.hdr + "\n") 

76 

77 def __del__(self): 

78 self.close() 

79 

80 def __call__(self): 

81 epot = self.atoms.get_potential_energy() 

82 ekin = self.atoms.get_kinetic_energy() 

83 temp = self.atoms.get_temperature() 

84 global_natoms = self.atoms.get_global_number_of_atoms() 

85 if self.peratom: 

86 epot /= global_natoms 

87 ekin /= global_natoms 

88 if self.dyn is not None: 

89 t = self.dyn.get_time() / (1000 * units.fs) 

90 dat = (t,) 

91 else: 

92 dat = () 

93 dat += (epot + ekin, epot, ekin, temp) 

94 if self.stress: 

95 dat += tuple(self.atoms.get_stress( 

96 include_ideal_gas=True) / units.GPa) 

97 self.logfile.write(self.fmt % dat) 

98 self.logfile.flush()