Coverage for ase / atom.py: 94.44%

126 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-30 08:22 +0000

1# fmt: off 

2 

3"""This module defines the Atom object.""" 

4 

5import numpy as np 

6 

7from ase.data import atomic_masses, atomic_numbers, chemical_symbols 

8 

9# Singular, plural, default value: 

10names = {'position': ('positions', np.zeros(3)), 

11 'number': ('numbers', 0), 

12 'tag': ('tags', 0), 

13 'momentum': ('momenta', np.zeros(3)), 

14 'mass': ('masses', None), 

15 'magmom': ('initial_magmoms', 0.0), 

16 'charge': ('initial_charges', 0.0)} 

17 

18 

19def atomproperty(name, doc): 

20 """Helper function to easily create Atom attribute property.""" 

21 

22 def getter(self): 

23 return self.get(name) 

24 

25 def setter(self, value): 

26 self.set(name, value) 

27 

28 def deleter(self): 

29 self.delete(name) 

30 

31 return property(getter, setter, deleter, doc) 

32 

33 

34def abcproperty(index): 

35 """Helper function to easily create Atom ABC-property.""" 

36 

37 def getter(self): 

38 return self.scaled_position[index] 

39 

40 def setter(self, value): 

41 # We can't just do self.scaled_position[i] = value 

42 # because scaled_position is a new buffer, not a view into 

43 # something we can write back to. 

44 # This is a clear bug! 

45 spos = self.scaled_position 

46 spos[index] = value 

47 self.scaled_position = spos 

48 

49 return property(getter, setter, doc='ABC'[index] + '-coordinate') 

50 

51 

52def xyzproperty(index): 

53 """Helper function to easily create Atom XYZ-property.""" 

54 

55 def getter(self): 

56 return self.position[index] 

57 

58 def setter(self, value): 

59 self.position[index] = value 

60 

61 return property(getter, setter, doc='XYZ'[index] + '-coordinate') 

62 

63 

64class Atom: 

65 """Class for representing a single atom. 

66 

67 Parameters 

68 ---------- 

69 

70 symbol: str or int 

71 Can be a chemical symbol (str) or an atomic number (int). 

72 position: sequence of 3 floats 

73 Atomic position. 

74 tag: int 

75 Special purpose tag. 

76 momentum: sequence of 3 floats 

77 Momentum for atom. 

78 mass: float 

79 Atomic mass in atomic units. 

80 magmom: float or 3 floats 

81 Magnetic moment. 

82 charge: float 

83 Atomic charge. 

84 """ 

85 __slots__ = ['data', 'atoms', 'index'] 

86 

87 def __init__(self, symbol='X', position=(0, 0, 0), 

88 tag=None, momentum=None, mass=None, 

89 magmom=None, charge=None, 

90 atoms=None, index=None): 

91 

92 self.data = d = {} 

93 

94 if atoms is None: 

95 # This atom is not part of any Atoms object: 

96 if isinstance(symbol, str): 

97 d['number'] = atomic_numbers[symbol] 

98 else: 

99 d['number'] = symbol 

100 d['position'] = np.array(position, float) 

101 d['tag'] = tag 

102 if momentum is not None: 

103 momentum = np.array(momentum, float) 

104 d['momentum'] = momentum 

105 d['mass'] = mass 

106 if magmom is not None: 

107 magmom = np.array(magmom, float) 

108 d['magmom'] = magmom 

109 d['charge'] = charge 

110 

111 self.index = index 

112 self.atoms = atoms 

113 

114 @property 

115 def scaled_position(self): 

116 pos = self.position 

117 spos = self.atoms.cell.scaled_positions(pos[np.newaxis]) 

118 return spos[0] 

119 

120 @scaled_position.setter 

121 def scaled_position(self, value): 

122 pos = self.atoms.cell.cartesian_positions(value) 

123 self.position = pos 

124 

125 def __repr__(self): 

126 s = f"Atom('{self.symbol}', {list(self.position)}" 

127 for name in ['tag', 'momentum', 'mass', 'magmom', 'charge']: 

128 value = self.get_raw(name) 

129 if value is not None: 

130 if isinstance(value, np.ndarray): 

131 value = value.tolist() 

132 s += f', {name}={value}' 

133 if self.atoms is None: 

134 s += ')' 

135 else: 

136 s += ', index=%d)' % self.index 

137 return s 

138 

139 def cut_reference_to_atoms(self): 

140 """Cut reference to atoms object.""" 

141 for name in names: 

142 self.data[name] = self.get_raw(name) 

143 self.index = None 

144 self.atoms = None 

145 

146 def get_raw(self, name): 

147 """Get name attribute, return None if not explicitly set.""" 

148 if name == 'symbol': 

149 return chemical_symbols[self.get_raw('number')] 

150 

151 if self.atoms is None: 

152 return self.data[name] 

153 

154 plural = names[name][0] 

155 if plural in self.atoms.arrays: 

156 return self.atoms.arrays[plural][self.index] 

157 else: 

158 return None 

159 

160 def get(self, name): 

161 """Get name attribute, return default if not explicitly set.""" 

162 value = self.get_raw(name) 

163 if value is None: 

164 if name == 'mass': 

165 value = atomic_masses[self.number] 

166 else: 

167 value = names[name][1] 

168 return value 

169 

170 def set(self, name, value): 

171 """Set name attribute to value.""" 

172 if name == 'symbol': 

173 name = 'number' 

174 value = atomic_numbers[value] 

175 

176 if self.atoms is None: 

177 assert name in names 

178 self.data[name] = value 

179 else: 

180 plural, default = names[name] 

181 if plural in self.atoms.arrays: 

182 array = self.atoms.arrays[plural] 

183 if name == 'magmom' and array.ndim == 2: 

184 assert len(value) == 3 

185 array[self.index] = value 

186 else: 

187 if name == 'magmom' and np.asarray(value).ndim == 1: 

188 array = np.zeros((len(self.atoms), 3)) 

189 elif name == 'mass': 

190 array = self.atoms.get_masses() 

191 else: 

192 default = np.asarray(default) 

193 array = np.zeros((len(self.atoms),) + default.shape, 

194 default.dtype) 

195 array[self.index] = value 

196 self.atoms.new_array(plural, array) 

197 

198 def delete(self, name): 

199 """Delete name attribute.""" 

200 assert self.atoms is None 

201 assert name not in ['number', 'symbol', 'position'] 

202 self.data[name] = None 

203 

204 symbol = atomproperty('symbol', 'Chemical symbol') 

205 number = atomproperty('number', 'Atomic number') 

206 position = atomproperty('position', 'XYZ-coordinates') 

207 tag = atomproperty('tag', 'Integer tag') 

208 momentum = atomproperty('momentum', 'XYZ-momentum') 

209 mass = atomproperty('mass', 'Atomic mass') 

210 magmom = atomproperty('magmom', 'Initial magnetic moment') 

211 charge = atomproperty('charge', 'Initial atomic charge') 

212 x = xyzproperty(0) 

213 y = xyzproperty(1) 

214 z = xyzproperty(2) 

215 

216 a = abcproperty(0) 

217 b = abcproperty(1) 

218 c = abcproperty(2)