Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""This module provides I/O functions for the MAGRES file format, introduced
2by CASTEP as an output format to store structural data and ab-initio
3calculated NMR parameters.
4Authors: Simone Sturniolo (ase implementation), Tim Green (original magres
5 parser code)
6"""
8import re
9import numpy as np
10from collections import OrderedDict
12import ase.units
13from ase.atoms import Atoms
14from ase.spacegroup import Spacegroup
15from ase.spacegroup.spacegroup import SpacegroupNotFoundError
16from ase.calculators.singlepoint import SinglePointDFTCalculator
18_mprops = {
19 'ms': ('sigma', 1),
20 'sus': ('S', 0),
21 'efg': ('V', 1),
22 'isc': ('K', 2)}
23# (matrix name, number of atoms in interaction) for various magres quantities
26def read_magres(fd, include_unrecognised=False):
27 """
28 Reader function for magres files.
29 """
31 blocks_re = re.compile(r'[\[<](?P<block_name>.*?)[>\]](.*?)[<\[]/' +
32 r'(?P=block_name)[\]>]', re.M | re.S)
34 """
35 Here are defined the various functions required to parse
36 different blocks.
37 """
39 def tensor33(x):
40 return np.squeeze(np.reshape(x, (3, 3))).tolist()
42 def tensor31(x):
43 return np.squeeze(np.reshape(x, (3, 1))).tolist()
45 def get_version(file_contents):
46 """
47 Look for and parse the magres file format version line
48 """
50 lines = file_contents.split('\n')
51 match = re.match(r'\#\$magres-abinitio-v([0-9]+).([0-9]+)', lines[0])
53 if match:
54 version = match.groups()
55 version = tuple(vnum for vnum in version)
56 else:
57 version = None
59 return version
61 def parse_blocks(file_contents):
62 """
63 Parse series of XML-like deliminated blocks into a list of
64 (block_name, contents) tuples
65 """
67 blocks = blocks_re.findall(file_contents)
69 return blocks
71 def parse_block(block):
72 """
73 Parse block contents into a series of (tag, data) records
74 """
76 def clean_line(line):
77 # Remove comments and whitespace at start and ends of line
78 line = re.sub('#(.*?)\n', '', line)
79 line = line.strip()
81 return line
83 name, data = block
85 lines = [clean_line(line) for line in data.split('\n')]
87 records = []
89 for line in lines:
90 xs = line.split()
92 if len(xs) > 0:
93 tag = xs[0]
94 data = xs[1:]
96 records.append((tag, data))
98 return (name, records)
100 def check_units(d):
101 """
102 Verify that given units for a particular tag are correct.
103 """
105 allowed_units = {'lattice': 'Angstrom',
106 'atom': 'Angstrom',
107 'ms': 'ppm',
108 'efg': 'au',
109 'efg_local': 'au',
110 'efg_nonlocal': 'au',
111 'isc': '10^19.T^2.J^-1',
112 'isc_fc': '10^19.T^2.J^-1',
113 'isc_orbital_p': '10^19.T^2.J^-1',
114 'isc_orbital_d': '10^19.T^2.J^-1',
115 'isc_spin': '10^19.T^2.J^-1',
116 'isc': '10^19.T^2.J^-1',
117 'sus': '10^-6.cm^3.mol^-1',
118 'calc_cutoffenergy': 'Hartree', }
120 if d[0] in d and d[1] == allowed_units[d[0]]:
121 pass
122 else:
123 raise RuntimeError('Unrecognized units: %s %s' % (d[0], d[1]))
125 return d
127 def parse_magres_block(block):
128 """
129 Parse magres block into data dictionary given list of record
130 tuples.
131 """
133 name, records = block
135 # 3x3 tensor
136 def ntensor33(name):
137 return lambda d: {name: tensor33([float(x) for x in data])}
139 # Atom label, atom index and 3x3 tensor
140 def sitensor33(name):
141 return lambda d: {'atom': {'label': data[0],
142 'index': int(data[1])},
143 name: tensor33([float(x) for x in data[2:]])}
145 # 2x(Atom label, atom index) and 3x3 tensor
146 def sisitensor33(name):
147 return lambda d: {'atom1': {'label': data[0],
148 'index': int(data[1])},
149 'atom2': {'label': data[2],
150 'index': int(data[3])},
151 name: tensor33([float(x) for x in data[4:]])}
153 tags = {'ms': sitensor33('sigma'),
154 'sus': ntensor33('S'),
155 'efg': sitensor33('V'),
156 'efg_local': sitensor33('V'),
157 'efg_nonlocal': sitensor33('V'),
158 'isc': sisitensor33('K'),
159 'isc_fc': sisitensor33('K'),
160 'isc_spin': sisitensor33('K'),
161 'isc_orbital_p': sisitensor33('K'),
162 'isc_orbital_d': sisitensor33('K'),
163 'units': check_units}
165 data_dict = {}
167 for record in records:
168 tag, data = record
170 if tag not in data_dict:
171 data_dict[tag] = []
173 data_dict[tag].append(tags[tag](data))
175 return data_dict
177 def parse_atoms_block(block):
178 """
179 Parse atoms block into data dictionary given list of record tuples.
180 """
182 name, records = block
184 # Lattice record: a1, a2 a3, b1, b2, b3, c1, c2 c3
185 def lattice(d):
186 return tensor33([float(x) for x in data])
188 # Atom record: label, index, x, y, z
189 def atom(d):
190 return {'species': data[0],
191 'label': data[1],
192 'index': int(data[2]),
193 'position': tensor31([float(x) for x in data[3:]])}
195 def symmetry(d):
196 return ' '.join(data)
198 tags = {'lattice': lattice,
199 'atom': atom,
200 'units': check_units,
201 'symmetry': symmetry}
203 data_dict = {}
205 for record in records:
206 tag, data = record
207 if tag not in data_dict:
208 data_dict[tag] = []
209 data_dict[tag].append(tags[tag](data))
211 return data_dict
213 def parse_generic_block(block):
214 """
215 Parse any other block into data dictionary given list of record
216 tuples.
217 """
219 name, records = block
221 data_dict = {}
223 for record in records:
224 tag, data = record
226 if tag not in data_dict:
227 data_dict[tag] = []
229 data_dict[tag].append(data)
231 return data_dict
233 """
234 Actual parser code.
235 """
237 block_parsers = {'magres': parse_magres_block,
238 'atoms': parse_atoms_block,
239 'calculation': parse_generic_block, }
241 file_contents = fd.read()
243 # This works as a validity check
244 version = get_version(file_contents)
245 if version is None:
246 # This isn't even a .magres file!
247 raise RuntimeError('File is not in standard Magres format')
248 blocks = parse_blocks(file_contents)
250 data_dict = {}
252 for block_data in blocks:
253 block = parse_block(block_data)
255 if block[0] in block_parsers:
256 block_dict = block_parsers[block[0]](block)
257 data_dict[block[0]] = block_dict
258 else:
259 # Throw in the text content of blocks we don't recognise
260 if include_unrecognised:
261 data_dict[block[0]] = block_data[1]
263 # Now the loaded data must be turned into an ASE Atoms object
265 # First check if the file is even viable
266 if 'atoms' not in data_dict:
267 raise RuntimeError('Magres file does not contain structure data')
269 # Allowed units handling. This is redundant for now but
270 # could turn out useful in the future
272 magres_units = {'Angstrom': ase.units.Ang}
274 # Lattice parameters?
275 if 'lattice' in data_dict['atoms']:
276 try:
277 u = dict(data_dict['atoms']['units'])['lattice']
278 except KeyError:
279 raise RuntimeError('No units detected in file for lattice')
280 u = magres_units[u]
281 cell = np.array(data_dict['atoms']['lattice'][0]) * u
282 pbc = True
283 else:
284 cell = None
285 pbc = False
287 # Now the atoms
288 symbols = []
289 positions = []
290 indices = []
291 labels = []
293 if 'atom' in data_dict['atoms']:
294 try:
295 u = dict(data_dict['atoms']['units'])['atom']
296 except KeyError:
297 raise RuntimeError('No units detected in file for atom positions')
298 u = magres_units[u]
299 # Now we have to account for the possibility of there being CASTEP
300 # 'custom' species amongst the symbols
301 custom_species = None
302 for a in data_dict['atoms']['atom']:
303 spec_custom = a['species'].split(':', 1)
304 if len(spec_custom) > 1 and custom_species is None:
305 # Add it to the custom info!
306 custom_species = list(symbols)
307 symbols.append(spec_custom[0])
308 positions.append(a['position'])
309 indices.append(a['index'])
310 labels.append(a['label'])
311 if custom_species is not None:
312 custom_species.append(a['species'])
314 atoms = Atoms(cell=cell,
315 pbc=pbc,
316 symbols=symbols,
317 positions=positions)
319 # Add custom species if present
320 if custom_species is not None:
321 atoms.new_array('castep_custom_species', np.array(custom_species))
323 # Add the spacegroup, if present and recognizable
324 if 'symmetry' in data_dict['atoms']:
325 try:
326 spg = Spacegroup(data_dict['atoms']['symmetry'][0])
327 except SpacegroupNotFoundError:
328 # Not found
329 spg = Spacegroup(1) # Most generic one
330 atoms.info['spacegroup'] = spg
332 # Set up the rest of the properties as arrays
333 atoms.new_array('indices', np.array(indices))
334 atoms.new_array('labels', np.array(labels))
336 # Now for the magres specific stuff
337 li_list = list(zip(labels, indices))
339 def create_magres_array(name, order, block):
341 if order == 1:
342 u_arr = [None] * len(li_list)
343 elif order == 2:
344 u_arr = [[None] * (i + 1) for i in range(len(li_list))]
345 else:
346 raise ValueError(
347 'Invalid order value passed to create_magres_array')
349 for s in block:
350 # Find the atom index/indices
351 if order == 1:
352 # First find out which atom this is
353 at = (s['atom']['label'], s['atom']['index'])
354 try:
355 ai = li_list.index(at)
356 except ValueError:
357 raise RuntimeError('Invalid data in magres block')
358 # Then add the relevant quantity
359 u_arr[ai] = s[mn]
360 else:
361 at1 = (s['atom1']['label'], s['atom1']['index'])
362 at2 = (s['atom2']['label'], s['atom2']['index'])
363 ai1 = li_list.index(at1)
364 ai2 = li_list.index(at2)
365 # Sort them
366 ai1, ai2 = sorted((ai1, ai2), reverse=True)
367 u_arr[ai1][ai2] = s[mn]
369 if order == 1:
370 return np.array(u_arr)
371 else:
372 return np.array(u_arr, dtype=object)
374 if 'magres' in data_dict:
375 if 'units' in data_dict['magres']:
376 atoms.info['magres_units'] = dict(data_dict['magres']['units'])
377 for u in atoms.info['magres_units']:
378 # This bit to keep track of tags
379 u0 = u.split('_')[0]
381 if u0 not in _mprops:
382 raise RuntimeError('Invalid data in magres block')
384 mn, order = _mprops[u0]
386 if order > 0:
387 u_arr = create_magres_array(mn, order,
388 data_dict['magres'][u])
389 atoms.new_array(u, u_arr)
390 else:
391 # atoms.info['magres_data'] = atoms.info.get('magres_data',
392 # {})
393 # # We only take element 0 because for this sort of data
394 # # there should be only that
395 # atoms.info['magres_data'][u] = \
396 # data_dict['magres'][u][0][mn]
397 if atoms.calc is None:
398 calc = SinglePointDFTCalculator(atoms)
399 atoms.calc = calc
400 atoms.calc.results[u] = data_dict['magres'][u][0][mn]
402 if 'calculation' in data_dict:
403 atoms.info['magresblock_calculation'] = data_dict['calculation']
405 if include_unrecognised:
406 for b in data_dict:
407 if b not in block_parsers:
408 atoms.info['magresblock_' + b] = data_dict[b]
410 return atoms
413def tensor_string(tensor):
414 return ' '.join(' '.join(str(x) for x in xs) for xs in tensor)
417def write_magres(fd, image):
418 """
419 A writing function for magres files. Two steps: first data are arranged
420 into structures, then dumped to the actual file
421 """
423 image_data = {}
424 image_data['atoms'] = {'units': []}
425 # Contains units, lattice and each individual atom
426 if np.all(image.get_pbc()):
427 # Has lattice!
428 image_data['atoms']['units'].append(['lattice', 'Angstrom'])
429 image_data['atoms']['lattice'] = [image.get_cell()]
431 # Now for the atoms
432 if image.has('labels'):
433 labels = image.get_array('labels')
434 else:
435 labels = image.get_chemical_symbols()
437 if image.has('indices'):
438 indices = image.get_array('indices')
439 else:
440 indices = [labels[:i + 1].count(labels[i]) for i in range(len(labels))]
442 # Iterate over atoms
443 symbols = (image.get_array('castep_custom_species')
444 if image.has('castep_custom_species')
445 else image.get_chemical_symbols())
447 atom_info = list(zip(symbols,
448 image.get_positions()))
449 if len(atom_info) > 0:
450 image_data['atoms']['units'].append(['atom', 'Angstrom'])
451 image_data['atoms']['atom'] = []
453 for i, a in enumerate(atom_info):
454 image_data['atoms']['atom'].append({
455 'index': indices[i],
456 'position': a[1],
457 'species': a[0],
458 'label': labels[i]})
460 # Spacegroup, if present
461 if 'spacegroup' in image.info:
462 image_data['atoms']['symmetry'] = [image.info['spacegroup']
463 .symbol.replace(' ', '')]
465 # Now go on to do the same for magres information
466 if 'magres_units' in image.info:
468 image_data['magres'] = {'units': []}
470 for u in image.info['magres_units']:
471 # Get the type
472 p = u.split('_')[0]
473 if p in _mprops:
474 image_data['magres']['units'].append(
475 [u, image.info['magres_units'][u]])
476 image_data['magres'][u] = []
477 mn, order = _mprops[p]
479 if order == 0:
480 # The case of susceptibility
481 tens = {
482 mn: image.calc.results[u]
483 }
484 image_data['magres'][u] = tens
485 else:
486 arr = image.get_array(u)
487 li_tab = zip(labels, indices)
488 for i, (lab, ind) in enumerate(li_tab):
489 if order == 2:
490 for j, (lab2, ind2) in enumerate(li_tab[:i + 1]):
491 if arr[i][j] is not None:
492 tens = {mn: arr[i][j],
493 'atom1': {'label': lab,
494 'index': ind},
495 'atom2': {'label': lab2,
496 'index': ind2}}
497 image_data['magres'][u].append(tens)
498 elif order == 1:
499 if arr[i] is not None:
500 tens = {mn: arr[i],
501 'atom': {'label': lab,
502 'index': ind}}
503 image_data['magres'][u].append(tens)
505 # Calculation block, if present
506 if 'magresblock_calculation' in image.info:
507 image_data['calculation'] = image.info['magresblock_calculation']
509 def write_units(data, out):
510 if 'units' in data:
511 for tag, units in data['units']:
512 out.append(' units %s %s' % (tag, units))
514 def write_magres_block(data):
515 """
516 Write out a <magres> block from its dictionary representation
517 """
519 out = []
521 def nout(tag, tensor_name):
522 if tag in data:
523 out.append(' '.join([' ', tag,
524 tensor_string(data[tag][tensor_name])]))
526 def siout(tag, tensor_name):
527 if tag in data:
528 for atom_si in data[tag]:
529 out.append((' %s %s %d '
530 '%s') % (tag,
531 atom_si['atom']['label'],
532 atom_si['atom']['index'],
533 tensor_string(atom_si[tensor_name])))
535 write_units(data, out)
537 nout('sus', 'S')
538 siout('ms', 'sigma')
540 siout('efg_local', 'V')
541 siout('efg_nonlocal', 'V')
542 siout('efg', 'V')
544 def sisiout(tag, tensor_name):
545 if tag in data:
546 for isc in data[tag]:
547 out.append((' %s %s %d %s %d '
548 '%s') % (tag,
549 isc['atom1']['label'],
550 isc['atom1']['index'],
551 isc['atom2']['label'],
552 isc['atom2']['index'],
553 tensor_string(isc[tensor_name])))
555 sisiout('isc_fc', 'K')
556 sisiout('isc_orbital_p', 'K')
557 sisiout('isc_orbital_d', 'K')
558 sisiout('isc_spin', 'K')
559 sisiout('isc', 'K')
561 return '\n'.join(out)
563 def write_atoms_block(data):
564 out = []
566 write_units(data, out)
568 if 'lattice' in data:
569 for lat in data['lattice']:
570 out.append(" lattice %s" % tensor_string(lat))
572 if 'symmetry' in data:
573 for sym in data['symmetry']:
574 out.append(' symmetry %s' % sym)
576 if 'atom' in data:
577 for a in data['atom']:
578 out.append((' atom %s %s %s '
579 '%s') % (a['species'],
580 a['label'],
581 a['index'],
582 ' '.join(str(x) for x in a['position'])))
584 return '\n'.join(out)
586 def write_generic_block(data):
587 out = []
589 for tag, data in data.items():
590 for value in data:
591 out.append('%s %s' % (tag, ' '.join(str(x) for x in value)))
593 return '\n'.join(out)
595 # Using this to preserve order
596 block_writers = OrderedDict([('calculation', write_generic_block),
597 ('atoms', write_atoms_block),
598 ('magres', write_magres_block)])
600 # First, write the header
601 fd.write('#$magres-abinitio-v1.0\n')
602 fd.write('# Generated by the Atomic Simulation Environment library\n')
604 for b in block_writers:
605 if b in image_data:
606 fd.write('[{0}]\n'.format(b))
607 fd.write(block_writers[b](image_data[b]))
608 fd.write('\n[/{0}]\n'.format(b))
610 # Now on to check for any non-standard blocks...
611 for i in image.info:
612 if '_' in i:
613 ismag, b = i.split('_', 1)
614 if ismag == 'magresblock' and b not in block_writers:
615 fd.write('[{0}]\n'.format(b))
616 fd.write(image.info[i])
617 fd.write('[/{0}]\n'.format(b))