Coverage for /builds/debichem-team/python-ase/ase/md/logger.py: 76.36%

55 statements  

« prev     ^ index     » next       coverage.py v7.5.3, created at 2025-03-06 04:00 +0000

1"""Logging for molecular dynamics.""" 

2import weakref 

3from typing import IO, Any, Union 

4 

5from ase import Atoms, units 

6from ase.parallel import world 

7from ase.utils import IOContext 

8 

9 

10class MDLogger(IOContext): 

11 """Class for logging molecular dynamics simulations. 

12 

13 Parameters: 

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

15 

16 atoms: The atoms. 

17 

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

19 

20 stress=False: Include stress in log. 

21 

22 peratom=False: Write energies per atom. 

23 

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

25 """ 

26 

27 def __init__( 

28 self, 

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

30 atoms: Atoms, 

31 logfile: Union[IO, str], 

32 header: bool = True, 

33 stress: bool = False, 

34 peratom: bool = False, 

35 mode: str = "a", 

36 comm=world, 

37 ): 

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

39 self.atoms = atoms 

40 global_natoms = atoms.get_global_number_of_atoms() 

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

42 self.stress = stress 

43 self.peratom = peratom 

44 if self.dyn is not None: 

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

46 self.fmt = "%-10.4f " 

47 else: 

48 self.hdr = "" 

49 self.fmt = "" 

50 if self.peratom: 

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

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

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

54 else: 

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

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

57 # Choose a sensible number of decimals 

58 if global_natoms <= 100: 

59 digits = 4 

60 elif global_natoms <= 1000: 

61 digits = 3 

62 elif global_natoms <= 10000: 

63 digits = 2 

64 else: 

65 digits = 1 

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

67 if self.stress: 

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

69 '-----------------------') 

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

71 self.fmt += "\n" 

72 if header: 

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

74 

75 def __del__(self): 

76 self.close() 

77 

78 def __call__(self): 

79 epot = self.atoms.get_potential_energy() 

80 ekin = self.atoms.get_kinetic_energy() 

81 temp = self.atoms.get_temperature() 

82 global_natoms = self.atoms.get_global_number_of_atoms() 

83 if self.peratom: 

84 epot /= global_natoms 

85 ekin /= global_natoms 

86 if self.dyn is not None: 

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

88 dat = (t,) 

89 else: 

90 dat = () 

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

92 if self.stress: 

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

94 include_ideal_gas=True) / units.GPa) 

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

96 self.logfile.flush()