Coverage for /builds/ase/ase/ase/gui/quickinfo.py: 72.15%

79 statements  

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

1# fmt: off 

2 

3"Module for displaying information about the system." 

4 

5 

6import warnings 

7 

8import numpy as np 

9 

10from ase.gui.i18n import _ 

11 

12ucellformat = """\ 

13 {:8.3f} {:8.3f} {:8.3f} 

14 {:8.3f} {:8.3f} {:8.3f} 

15 {:8.3f} {:8.3f} {:8.3f} 

16""" 

17 

18 

19def info(gui): 

20 images = gui.images 

21 nimg = len(images) 

22 atoms = gui.atoms 

23 

24 tokens = [] 

25 

26 def add(token=''): 

27 tokens.append(token) 

28 

29 if len(atoms) < 1: 

30 add(_('This frame has no atoms.')) 

31 else: 

32 img = gui.frame 

33 

34 if nimg == 1: 

35 add(_('Single image loaded.')) 

36 else: 

37 add(_('Image {} loaded (0–{}).').format(img, nimg - 1)) 

38 add() 

39 add(_('Number of atoms: {}').format(len(atoms))) 

40 

41 # We need to write ų further down, so we have no choice but to 

42 # use proper subscripts in the chemical formula: 

43 formula = atoms.get_chemical_formula() 

44 subscripts = dict(zip('0123456789', '₀₁₂₃₄₅₆₇₈₉')) 

45 pretty_formula = ''.join(subscripts.get(c, c) for c in formula) 

46 add(pretty_formula) 

47 

48 add() 

49 add(_('Unit cell [Å]:')) 

50 add(ucellformat.format(*atoms.cell.ravel())) 

51 periodic = [[_('no'), _('yes')][int(periodic)] 

52 for periodic in atoms.pbc] 

53 # TRANSLATORS: This has the form Periodic: no, no, yes 

54 add(_('Periodic: {}, {}, {}').format(*periodic)) 

55 add() 

56 

57 cellpar = atoms.cell.cellpar() 

58 add() 

59 add(_('Lengths [Å]: {:.3f}, {:.3f}, {:.3f}').format(*cellpar[:3])) 

60 add(_('Angles: {:.1f}°, {:.1f}°, {:.1f}°').format(*cellpar[3:])) 

61 

62 if atoms.cell.rank == 3: 

63 add(_('Volume: {:.3f} ų').format(atoms.cell.volume)) 

64 

65 add() 

66 

67 if nimg > 1: 

68 if all((atoms.cell == img.cell).all() for img in images): 

69 add(_('Unit cell is fixed.')) 

70 else: 

71 add(_('Unit cell varies.')) 

72 

73 if atoms.pbc[:2].all() and atoms.cell.rank >= 1: 

74 try: 

75 lat = atoms.cell.get_bravais_lattice() 

76 except RuntimeError: 

77 add(_('Could not recognize the lattice type')) 

78 except Exception: 

79 add(_('Unexpected error determining lattice type')) 

80 else: 

81 add(_('Reduced Bravais lattice:\n{}').format(lat)) 

82 

83 # Print electronic structure information if we have a calculator 

84 if atoms.calc: 

85 calc = atoms.calc 

86 

87 def getresult(name, get_quantity): 

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

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

90 # so that is an alternative option. 

91 try: 

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

93 quantity = None 

94 else: 

95 quantity = get_quantity() 

96 except Exception as err: 

97 quantity = None 

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

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

100 warnings.warn(errmsg) 

101 return quantity 

102 

103 # SinglePointCalculators are named after the code which 

104 # produced the result, so this will typically list the 

105 # name of a code even if they are just cached results. 

106 add() 

107 from ase.calculators.singlepoint import SinglePointCalculator 

108 if isinstance(calc, SinglePointCalculator): 

109 add(_('Calculator: {} (cached)').format(calc.name)) 

110 else: 

111 add(_('Calculator: {} (attached)').format(calc.name)) 

112 

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

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

115 magmom = getresult('magmom', atoms.get_magnetic_moment) 

116 

117 if energy is not None: 

118 energy_str = _('Energy: {:.3f} eV').format(energy) 

119 add(energy_str) 

120 

121 if forces is not None: 

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

123 forces_str = _('Max force: {:.3f} eV/Å').format(maxf) 

124 add(forces_str) 

125 

126 if magmom is not None: 

127 mag_str = _('Magmom: {:.3f} µ').format(magmom) 

128 add(mag_str) 

129 

130 return '\n'.join(tokens)