Coverage for /builds/ase/ase/ase/gui/status.py: 72.45%

98 statements  

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

1# fmt: off 

2 

3import warnings 

4from math import acos, pi, sqrt 

5 

6import numpy as np 

7 

8from ase.data import atomic_names as names 

9from ase.data import chemical_symbols as symbols 

10from ase.gui.i18n import _ 

11from ase.gui.utils import get_magmoms 

12 

13 

14def formula(Z): 

15 hist = {} 

16 for z in Z: 

17 if z in hist: 

18 hist[z] += 1 

19 else: 

20 hist[z] = 1 

21 Z = sorted(hist.keys()) 

22 strings = [] 

23 for z in Z: 

24 n = hist[z] 

25 s = ('' if n == 1 else str(n)) + symbols[z] 

26 strings.append(s) 

27 return '+'.join(strings) 

28 

29 

30class Status: 

31 def __init__(self, gui): 

32 self.gui = gui 

33 

34 def status(self, atoms): 

35 gui = self.gui 

36 natoms = len(atoms) 

37 indices = np.arange(natoms)[gui.images.selected[:natoms]] 

38 ordered_indices = [i for i in gui.images.selected_ordered 

39 if i < len(atoms)] 

40 n = len(indices) 

41 

42 if n == 0: 

43 line = '' 

44 if atoms.calc: 

45 calc = atoms.calc 

46 

47 def getresult(name, get_quantity): 

48 # ase/io/trajectory.py line 170 does this by using 

49 # the get_property(prop, atoms, allow_calculation=False) 

50 # so that is an alternative option. 

51 try: 

52 if calc.calculation_required(atoms, [name]): 

53 quantity = None 

54 else: 

55 quantity = get_quantity() 

56 except Exception as err: 

57 quantity = None 

58 errmsg = ('An error occurred while retrieving {} ' 

59 'from the calculator: {}'.format(name, err)) 

60 warnings.warn(errmsg) 

61 return quantity 

62 

63 energy = getresult('energy', atoms.get_potential_energy) 

64 forces = getresult('forces', atoms.get_forces) 

65 

66 if energy is not None: 

67 line += f'Energy = {energy:.3f} eV' 

68 

69 if forces is not None: 

70 maxf = np.linalg.norm(forces, axis=1).max() 

71 line += f' Max force = {maxf:.3f} eV/Å' 

72 gui.window.update_status_line(line) 

73 return 

74 

75 Z = atoms.numbers[indices] 

76 R = atoms.positions[indices] 

77 

78 if n == 1: 

79 tag = atoms.get_tags()[indices[0]] 

80 text = (' #%d %s (%s): %.3f Å, %.3f Å, %.3f Å ' % 

81 ((indices[0], names[Z[0]], symbols[Z[0]]) + tuple(R[0]))) 

82 text += _(' tag=%(tag)s') % dict(tag=tag) 

83 magmoms = get_magmoms(gui.atoms) 

84 if magmoms.any(): 

85 # TRANSLATORS: mom refers to magnetic moment 

86 text += _(' mom={:1.2f}'.format( 

87 magmoms[indices][0])) 

88 charges = gui.atoms.get_initial_charges() 

89 if charges.any(): 

90 text += _(' q={:1.2f}'.format( 

91 charges[indices][0])) 

92 haveit = {'numbers', 'positions', 'forces', 'momenta', 

93 'initial_charges', 'initial_magmoms', 'tags'} 

94 for key in atoms.arrays: 

95 if key not in haveit: 

96 val = atoms.get_array(key)[indices[0]] 

97 if val is not None: 

98 if isinstance(val, int): 

99 text += f' {key}={val:g}' 

100 else: 

101 text += f' {key}={val}' 

102 elif n == 2: 

103 D = R[0] - R[1] 

104 d = sqrt(np.dot(D, D)) 

105 text = f' {symbols[Z[0]]}-{symbols[Z[1]]}: {d:.3f} Å' 

106 elif n == 3: 

107 d = [] 

108 for c in range(3): 

109 D = R[c] - R[(c + 1) % 3] 

110 d.append(np.dot(D, D)) 

111 a = [] 

112 for c in range(3): 

113 t1 = 0.5 * (d[c] + d[(c + 1) % 3] - d[(c + 2) % 3]) 

114 t2 = sqrt(d[c] * d[(c + 1) % 3]) 

115 try: 

116 t3 = acos(t1 / t2) 

117 except ValueError: 

118 if t1 > 0: 

119 t3 = 0 

120 else: 

121 t3 = pi 

122 a.append(t3 * 180 / pi) 

123 text = (' %s-%s-%s: %.1f°, %.1f°, %.1f°' % 

124 tuple([symbols[z] for z in Z] + a)) 

125 elif len(ordered_indices) == 4: 

126 angle = gui.atoms.get_dihedral(*ordered_indices, mic=True) 

127 text = ('%s %s → %s → %s → %s: %.1f°' % 

128 tuple([_('dihedral')] + [symbols[z] for z in Z] + [angle])) 

129 else: 

130 text = ' ' + formula(Z) 

131 

132 gui.window.update_status_line(text)