Coverage for ase / io / magres.py: 79.65%

226 statements  

« prev     ^ index     » next       coverage.py v7.14.0, created at 2026-05-21 15:52 +0000

1# fmt: off 

2 

3"""This module provides I/O functions for the MAGRES file format, introduced 

4by CASTEP as an output format to store structural data and ab-initio 

5calculated NMR parameters. 

6Authors: Simone Sturniolo (ase implementation), Tim Green (original magres 

7 parser code) 

8""" 

9 

10from collections import OrderedDict 

11 

12import numpy as np 

13 

14import ase.io._magres as _magres 

15import ase.units 

16from ase.atoms import Atoms 

17from ase.calculators.singlepoint import SinglePointDFTCalculator 

18from ase.spacegroup import Spacegroup 

19from ase.spacegroup.spacegroup import SpacegroupNotFoundError 

20 

21_mprops = { 

22 'ms': ('sigma', 1), 

23 'sus': ('S', 0), 

24 'efg': ('V', 1), 

25 'isc': ('K', 2)} 

26# (matrix name, number of atoms in interaction) for various magres quantities 

27 

28 

29def read_magres(fd, include_unrecognised=False): 

30 """Read magres file.""" 

31 

32 block_parsers = {'magres': _magres.parse_magres_block, 

33 'atoms': _magres.parse_atoms_block, 

34 'calculation': _magres.parse_generic_block, } 

35 

36 file_contents = fd.read() 

37 

38 # This works as a validity check 

39 version = _magres.get_version(file_contents) 

40 if version is None: 

41 # This isn't even a .magres file! 

42 raise RuntimeError('File is not in standard Magres format') 

43 blocks = _magres.parse_blocks(file_contents) 

44 

45 data_dict = {} 

46 

47 for block_data in blocks: 

48 block = _magres.parse_block(block_data) 

49 

50 if block[0] in block_parsers: 

51 block_dict = block_parsers[block[0]](block) 

52 data_dict[block[0]] = block_dict 

53 else: 

54 # Throw in the text content of blocks we don't recognise 

55 if include_unrecognised: 

56 data_dict[block[0]] = block_data[1] 

57 

58 # Now the loaded data must be turned into an ASE Atoms object 

59 

60 # First check if the file is even viable 

61 if 'atoms' not in data_dict: 

62 raise RuntimeError('Magres file does not contain structure data') 

63 

64 # Allowed units handling. This is redundant for now but 

65 # could turn out useful in the future 

66 

67 magres_units = {'Angstrom': ase.units.Ang} 

68 

69 # Lattice parameters? 

70 if 'lattice' in data_dict['atoms']: 

71 try: 

72 u = dict(data_dict['atoms']['units'])['lattice'] 

73 except KeyError: 

74 raise RuntimeError('No units detected in file for lattice') 

75 u = magres_units[u] 

76 cell = np.array(data_dict['atoms']['lattice'][0]) * u 

77 pbc = True 

78 else: 

79 cell = None 

80 pbc = False 

81 

82 # Now the atoms 

83 symbols = [] 

84 positions = [] 

85 indices = [] 

86 labels = [] 

87 

88 if 'atom' in data_dict['atoms']: 

89 try: 

90 u = dict(data_dict['atoms']['units'])['atom'] 

91 except KeyError: 

92 raise RuntimeError('No units detected in file for atom positions') 

93 u = magres_units[u] 

94 # Now we have to account for the possibility of there being CASTEP 

95 # 'custom' species amongst the symbols 

96 custom_species = None 

97 for a in data_dict['atoms']['atom']: 

98 spec_custom = a['species'].split(':', 1) 

99 if len(spec_custom) > 1 and custom_species is None: 

100 # Add it to the custom info! 

101 custom_species = list(symbols) 

102 symbols.append(spec_custom[0]) 

103 positions.append(a['position']) 

104 indices.append(a['index']) 

105 labels.append(a['label']) 

106 if custom_species is not None: 

107 custom_species.append(a['species']) 

108 

109 atoms = Atoms(cell=cell, 

110 pbc=pbc, 

111 symbols=symbols, 

112 positions=positions) 

113 

114 # Add custom species if present 

115 if custom_species is not None: 

116 atoms.new_array('castep_custom_species', np.array(custom_species)) 

117 

118 # Add the spacegroup, if present and recognizable 

119 if 'symmetry' in data_dict['atoms']: 

120 try: 

121 spg = Spacegroup(data_dict['atoms']['symmetry'][0]) 

122 except SpacegroupNotFoundError: 

123 # Not found 

124 spg = Spacegroup(1) # Most generic one 

125 atoms.info['spacegroup'] = spg 

126 

127 # Set up the rest of the properties as arrays 

128 atoms.new_array('indices', np.array(indices)) 

129 atoms.new_array('labels', np.array(labels)) 

130 

131 # Now for the magres specific stuff 

132 li_list = list(zip(labels, indices)) 

133 

134 def create_magres_array(name, order, block): 

135 

136 if order == 1: 

137 u_arr = [None] * len(li_list) 

138 elif order == 2: 

139 u_arr = [[None] * (i + 1) for i in range(len(li_list))] 

140 else: 

141 raise ValueError( 

142 'Invalid order value passed to create_magres_array') 

143 

144 for s in block: 

145 # Find the atom index/indices 

146 if order == 1: 

147 # First find out which atom this is 

148 at = (s['atom']['label'], s['atom']['index']) 

149 try: 

150 ai = li_list.index(at) 

151 except ValueError: 

152 raise RuntimeError('Invalid data in magres block') 

153 # Then add the relevant quantity 

154 u_arr[ai] = s[mn] 

155 else: 

156 at1 = (s['atom1']['label'], s['atom1']['index']) 

157 at2 = (s['atom2']['label'], s['atom2']['index']) 

158 ai1 = li_list.index(at1) 

159 ai2 = li_list.index(at2) 

160 # Sort them 

161 ai1, ai2 = sorted((ai1, ai2), reverse=True) 

162 u_arr[ai1][ai2] = s[mn] 

163 

164 if order == 1: 

165 return np.array(u_arr) 

166 else: 

167 return np.array(u_arr, dtype=object) 

168 

169 if 'magres' in data_dict: 

170 if 'units' in data_dict['magres']: 

171 atoms.info['magres_units'] = dict(data_dict['magres']['units']) 

172 for u in atoms.info['magres_units']: 

173 # This bit to keep track of tags 

174 u0 = u.split('_')[0] 

175 

176 if u0 not in _mprops: 

177 raise RuntimeError('Invalid data in magres block') 

178 

179 mn, order = _mprops[u0] 

180 

181 if order > 0: 

182 u_arr = create_magres_array(mn, order, 

183 data_dict['magres'][u]) 

184 atoms.new_array(u, u_arr) 

185 else: 

186 # atoms.info['magres_data'] = atoms.info.get('magres_data', 

187 # {}) 

188 # # We only take element 0 because for this sort of data 

189 # # there should be only that 

190 # atoms.info['magres_data'][u] = \ 

191 # data_dict['magres'][u][0][mn] 

192 if atoms.calc is None: 

193 calc = SinglePointDFTCalculator(atoms) 

194 atoms.calc = calc 

195 atoms.calc.results[u] = data_dict['magres'][u][0][mn] 

196 

197 if 'calculation' in data_dict: 

198 atoms.info['magresblock_calculation'] = data_dict['calculation'] 

199 

200 if include_unrecognised: 

201 for b in data_dict: 

202 if b not in block_parsers: 

203 atoms.info['magresblock_' + b] = data_dict[b] 

204 

205 return atoms 

206 

207 

208def tensor_string(tensor): 

209 return ' '.join(' '.join(str(x) for x in xs) for xs in tensor) 

210 

211 

212def write_magres(fd, image): 

213 """ 

214 A writing function for magres files. Two steps: first data are arranged 

215 into structures, then dumped to the actual file 

216 """ 

217 

218 image_data = {} 

219 image_data['atoms'] = {'units': []} 

220 # Contains units, lattice and each individual atom 

221 if np.all(image.get_pbc()): 

222 # Has lattice! 

223 image_data['atoms']['units'].append(['lattice', 'Angstrom']) 

224 image_data['atoms']['lattice'] = [image.get_cell()] 

225 

226 # Now for the atoms 

227 if image.has('labels'): 

228 labels = image.get_array('labels') 

229 else: 

230 labels = image.get_chemical_symbols() 

231 

232 if image.has('indices'): 

233 indices = image.get_array('indices') 

234 else: 

235 indices = [labels[:i + 1].count(labels[i]) for i in range(len(labels))] 

236 

237 # Iterate over atoms 

238 symbols = (image.get_array('castep_custom_species') 

239 if image.has('castep_custom_species') 

240 else image.get_chemical_symbols()) 

241 

242 atom_info = list(zip(symbols, 

243 image.get_positions())) 

244 if len(atom_info) > 0: 

245 image_data['atoms']['units'].append(['atom', 'Angstrom']) 

246 image_data['atoms']['atom'] = [] 

247 

248 for i, a in enumerate(atom_info): 

249 image_data['atoms']['atom'].append({ 

250 'index': indices[i], 

251 'position': a[1], 

252 'species': a[0], 

253 'label': labels[i]}) 

254 

255 # Spacegroup, if present 

256 if 'spacegroup' in image.info: 

257 image_data['atoms']['symmetry'] = [image.info['spacegroup'] 

258 .symbol.replace(' ', '')] 

259 

260 # Now go on to do the same for magres information 

261 if 'magres_units' in image.info: 

262 

263 image_data['magres'] = {'units': []} 

264 

265 for u in image.info['magres_units']: 

266 # Get the type 

267 p = u.split('_')[0] 

268 if p in _mprops: 

269 image_data['magres']['units'].append( 

270 [u, image.info['magres_units'][u]]) 

271 image_data['magres'][u] = [] 

272 mn, order = _mprops[p] 

273 

274 if order == 0: 

275 # The case of susceptibility 

276 tens = { 

277 mn: image.calc.results[u] 

278 } 

279 image_data['magres'][u] = tens 

280 else: 

281 arr = image.get_array(u) 

282 li_tab = zip(labels, indices) 

283 for i, (lab, ind) in enumerate(li_tab): 

284 if order == 2: 

285 for j, (lab2, ind2) in enumerate(li_tab[:i + 1]): 

286 if arr[i][j] is not None: 

287 tens = {mn: arr[i][j], 

288 'atom1': {'label': lab, 

289 'index': ind}, 

290 'atom2': {'label': lab2, 

291 'index': ind2}} 

292 image_data['magres'][u].append(tens) 

293 elif order == 1: 

294 if arr[i] is not None: 

295 tens = {mn: arr[i], 

296 'atom': {'label': lab, 

297 'index': ind}} 

298 image_data['magres'][u].append(tens) 

299 

300 # Calculation block, if present 

301 if 'magresblock_calculation' in image.info: 

302 image_data['calculation'] = image.info['magresblock_calculation'] 

303 

304 def write_units(data, out): 

305 if 'units' in data: 

306 for tag, units in data['units']: 

307 out.append(f' units {tag} {units}') 

308 

309 def write_magres_block(data): 

310 """ 

311 Write out a <magres> block from its dictionary representation 

312 """ 

313 

314 out = [] 

315 

316 def nout(tag, tensor_name): 

317 if tag in data: 

318 out.append(' '.join([' ', tag, 

319 tensor_string(data[tag][tensor_name])])) 

320 

321 def siout(tag, tensor_name): 

322 if tag in data: 

323 for atom_si in data[tag]: 

324 out.append((' %s %s %d ' 

325 '%s') % (tag, 

326 atom_si['atom']['label'], 

327 atom_si['atom']['index'], 

328 tensor_string(atom_si[tensor_name]))) 

329 

330 write_units(data, out) 

331 

332 nout('sus', 'S') 

333 siout('ms', 'sigma') 

334 

335 siout('efg_local', 'V') 

336 siout('efg_nonlocal', 'V') 

337 siout('efg', 'V') 

338 

339 def sisiout(tag, tensor_name): 

340 if tag in data: 

341 for isc in data[tag]: 

342 out.append((' %s %s %d %s %d ' 

343 '%s') % (tag, 

344 isc['atom1']['label'], 

345 isc['atom1']['index'], 

346 isc['atom2']['label'], 

347 isc['atom2']['index'], 

348 tensor_string(isc[tensor_name]))) 

349 

350 sisiout('isc_fc', 'K') 

351 sisiout('isc_orbital_p', 'K') 

352 sisiout('isc_orbital_d', 'K') 

353 sisiout('isc_spin', 'K') 

354 sisiout('isc', 'K') 

355 

356 return '\n'.join(out) 

357 

358 def write_atoms_block(data): 

359 out = [] 

360 

361 write_units(data, out) 

362 

363 if 'lattice' in data: 

364 for lat in data['lattice']: 

365 out.append(f" lattice {tensor_string(lat)}") 

366 

367 if 'symmetry' in data: 

368 for sym in data['symmetry']: 

369 out.append(f' symmetry {sym}') 

370 

371 if 'atom' in data: 

372 for a in data['atom']: 

373 out.append((' atom %s %s %s ' 

374 '%s') % (a['species'], 

375 a['label'], 

376 a['index'], 

377 ' '.join(str(x) for x in a['position']))) 

378 

379 return '\n'.join(out) 

380 

381 def write_generic_block(data): 

382 out = [] 

383 

384 for tag, data in data.items(): 

385 for value in data: 

386 out.append('{} {}'.format(tag, ' '.join(str(x) for x in value))) 

387 

388 return '\n'.join(out) 

389 

390 # Using this to preserve order 

391 block_writers = OrderedDict([('calculation', write_generic_block), 

392 ('atoms', write_atoms_block), 

393 ('magres', write_magres_block)]) 

394 

395 # First, write the header 

396 fd.write('#$magres-abinitio-v1.0\n') 

397 fd.write('# Generated by the Atomic Simulation Environment library\n') 

398 

399 for b in block_writers: 

400 if b in image_data: 

401 fd.write(f'[{b}]\n') 

402 fd.write(block_writers[b](image_data[b])) 

403 fd.write(f'\n[/{b}]\n') 

404 

405 # Now on to check for any non-standard blocks... 

406 for i in image.info: 

407 if '_' in i: 

408 ismag, b = i.split('_', 1) 

409 if ismag == 'magresblock' and b not in block_writers: 

410 fd.write(f'[{b}]\n') 

411 fd.write(image.info[i]) 

412 fd.write(f'[/{b}]\n')