Hide keyboard shortcuts

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

1import os 

2import re 

3 

4import numpy as np 

5 

6from ase import Atoms 

7from ase.data import atomic_numbers 

8from ase.io import read 

9from ase.calculators.calculator import kpts2ndarray 

10from ase.units import Bohr, Hartree 

11from ase.utils import reader 

12 

13 

14special_ase_keywords = set(['kpts']) 

15 

16 

17def process_special_kwargs(atoms, kwargs): 

18 kwargs = kwargs.copy() 

19 kpts = kwargs.pop('kpts', None) 

20 if kpts is not None: 

21 for kw in ['kpoints', 'reducedkpoints', 'kpointsgrid']: 

22 if kw in kwargs: 

23 raise ValueError('k-points specified multiple times') 

24 

25 kptsarray = kpts2ndarray(kpts, atoms) 

26 nkpts = len(kptsarray) 

27 fullarray = np.empty((nkpts, 4)) 

28 fullarray[:, 0] = 1.0 / nkpts # weights 

29 fullarray[:, 1:4] = kptsarray 

30 kwargs['kpointsreduced'] = fullarray.tolist() 

31 

32 # TODO xc=LDA/PBE etc. 

33 

34 # The idea is to get rid of the special keywords, since the rest 

35 # will be passed to Octopus 

36 # XXX do a better check of this 

37 for kw in special_ase_keywords: 

38 assert kw not in kwargs, kw 

39 return kwargs 

40 

41 

42class OctopusParseError(Exception): 

43 pass # Cannot parse input file 

44 

45 

46# Parse value as written in input file *or* something that one would be 

47# passing to the ASE interface, i.e., this might already be a boolean 

48def octbool2bool(value): 

49 value = value.lower() 

50 if isinstance(value, int): 

51 return bool(value) 

52 if value in ['true', 't', 'yes', '1']: 

53 return True 

54 elif value in ['no', 'f', 'false', '0']: 

55 return False 

56 else: 

57 raise ValueError('Failed to interpret "%s" as a boolean.' % value) 

58 

59 

60def list2block(name, rows): 

61 """Construct 'block' of Octopus input. 

62 

63 convert a list of rows to a string with the format x | x | .... 

64 for the octopus input file""" 

65 lines = [] 

66 lines.append('%' + name) 

67 for row in rows: 

68 lines.append(' ' + ' | '.join(str(obj) for obj in row)) 

69 lines.append('%') 

70 return lines 

71 

72 

73def normalize_keywords(kwargs): 

74 """Reduce keywords to unambiguous form (lowercase).""" 

75 newkwargs = {} 

76 for arg, value in kwargs.items(): 

77 lkey = arg.lower() 

78 newkwargs[lkey] = value 

79 return newkwargs 

80 

81 

82def input_line_iter(lines): 

83 """Convenient iterator for parsing input files 'cleanly'. 

84 

85 Discards comments etc.""" 

86 for line in lines: 

87 line = line.split('#')[0].strip() 

88 if not line or line.isspace(): 

89 continue 

90 line = line.strip() 

91 yield line 

92 

93 

94def block2list(namespace, lines, header=None): 

95 """Parse lines of block and return list of lists of strings.""" 

96 lines = iter(lines) 

97 block = [] 

98 if header is None: 

99 header = next(lines) 

100 assert header.startswith('%'), header 

101 name = header[1:].strip().lower() 

102 for line in lines: 

103 if line.startswith('%'): # Could also say line == '%' most likely. 

104 break 

105 tokens = [namespace.evaluate(token) 

106 for token in line.strip().split('|')] 

107 # XXX will fail for string literals containing '|' 

108 block.append(tokens) 

109 return name, block 

110 

111 

112class OctNamespace: 

113 def __init__(self): 

114 self.names = {} 

115 self.consts = {'pi': np.pi, 

116 'angstrom': 1. / Bohr, 

117 'ev': 1. / Hartree, 

118 'yes': True, 

119 'no': False, 

120 't': True, 

121 'f': False, 

122 'i': 1j, # This will probably cause trouble 

123 'true': True, 

124 'false': False} 

125 

126 def evaluate(self, value): 

127 value = value.strip() 

128 

129 for char in '"', "'": # String literal 

130 if value.startswith(char): 

131 assert value.endswith(char) 

132 return value 

133 

134 value = value.lower() 

135 

136 if value in self.consts: # boolean or other constant 

137 return self.consts[value] 

138 

139 if value in self.names: # existing variable 

140 return self.names[value] 

141 

142 try: # literal integer 

143 v = int(value) 

144 except ValueError: 

145 pass 

146 else: 

147 if v == float(v): 

148 return v 

149 

150 try: # literal float 

151 return float(value) 

152 except ValueError: 

153 pass 

154 

155 if ('*' in value or '/' in value 

156 and not any(char in value for char in '()+')): 

157 floatvalue = 1.0 

158 op = '*' 

159 for token in re.split(r'([\*/])', value): 

160 if token in '*/': 

161 op = token 

162 continue 

163 

164 v = self.evaluate(token) 

165 

166 try: 

167 v = float(v) 

168 except TypeError: 

169 try: 

170 v = complex(v) 

171 except ValueError: 

172 break 

173 except ValueError: 

174 break # Cannot evaluate expression 

175 else: 

176 if op == '*': 

177 floatvalue *= v 

178 else: 

179 assert op == '/', op 

180 floatvalue /= v 

181 else: # Loop completed successfully 

182 return floatvalue 

183 return value # unknown name, or complex arithmetic expression 

184 

185 def add(self, name, value): 

186 value = self.evaluate(value) 

187 self.names[name.lower().strip()] = value 

188 

189 

190def parse_input_file(fd): 

191 namespace = OctNamespace() 

192 lines = input_line_iter(fd) 

193 blocks = {} 

194 while True: 

195 try: 

196 line = next(lines) 

197 except StopIteration: 

198 break 

199 else: 

200 if line.startswith('%'): 

201 name, value = block2list(namespace, lines, header=line) 

202 blocks[name] = value 

203 else: 

204 tokens = line.split('=', 1) 

205 assert len(tokens) == 2, tokens 

206 name, value = tokens 

207 namespace.add(name, value) 

208 

209 namespace.names.update(blocks) 

210 return namespace.names 

211 

212 

213def kwargs2cell(kwargs): 

214 # kwargs -> cell + remaining kwargs 

215 # cell will be None if not ASE-compatible. 

216 # 

217 # Returns numbers verbatim; caller must convert units. 

218 kwargs = normalize_keywords(kwargs) 

219 

220 if boxshape_is_ase_compatible(kwargs): 

221 kwargs.pop('boxshape', None) 

222 if 'lsize' in kwargs: 

223 Lsize = kwargs.pop('lsize') 

224 if not isinstance(Lsize, list): 

225 Lsize = [[Lsize] * 3] 

226 assert len(Lsize) == 1 

227 cell = np.array([2 * float(l) for l in Lsize[0]]) 

228 elif 'latticeparameters' in kwargs: 

229 # Eval latparam and latvec 

230 latparam = np.array(kwargs.pop('latticeparameters'), float).T 

231 cell = np.array(kwargs.pop('latticevectors', np.eye(3)), float) 

232 for a, vec in zip(latparam, cell): 

233 vec *= a 

234 assert cell.shape == (3, 3) 

235 else: 

236 cell = None 

237 return cell, kwargs 

238 

239 

240def boxshape_is_ase_compatible(kwargs): 

241 pdims = int(kwargs.get('periodicdimensions', 0)) 

242 default_boxshape = 'parallelepiped' if pdims > 0 else 'minimum' 

243 boxshape = kwargs.get('boxshape', default_boxshape).lower() 

244 # XXX add support for experimental keyword 'latticevectors' 

245 return boxshape == 'parallelepiped' 

246 

247 

248def kwargs2atoms(kwargs, directory=None): 

249 """Extract atoms object from keywords and return remaining keywords. 

250 

251 Some keyword arguments may refer to files. The directory keyword 

252 may be necessary to resolve the paths correctly, and is used for 

253 example when running 'ase gui somedir/inp'.""" 

254 kwargs = normalize_keywords(kwargs) 

255 

256 # Only input units accepted nowadays are 'atomic'. 

257 # But if we are loading an old file, and it specifies something else, 

258 # we can be sure that the user wanted that back then. 

259 

260 coord_keywords = ['coordinates', 

261 'xyzcoordinates', 

262 'pdbcoordinates', 

263 'reducedcoordinates', 

264 'xsfcoordinates', 

265 'xsfcoordinatesanimstep'] 

266 

267 nkeywords = 0 

268 for keyword in coord_keywords: 

269 if keyword in kwargs: 

270 nkeywords += 1 

271 if nkeywords == 0: 

272 raise OctopusParseError('No coordinates') 

273 elif nkeywords > 1: 

274 raise OctopusParseError('Multiple coordinate specifications present. ' 

275 'This may be okay in Octopus, but we do not ' 

276 'implement it.') 

277 

278 def get_positions_from_block(keyword): 

279 # %Coordinates or %ReducedCoordinates -> atomic numbers, positions. 

280 block = kwargs.pop(keyword) 

281 positions = [] 

282 numbers = [] 

283 tags = [] 

284 types = {} 

285 for row in block: 

286 assert len(row) in [ndims + 1, ndims + 2] 

287 row = row[:ndims + 1] 

288 sym = row[0] 

289 assert sym.startswith('"') or sym.startswith("'") 

290 assert sym[0] == sym[-1] 

291 sym = sym[1:-1] 

292 pos0 = np.zeros(3) 

293 ndim = int(kwargs.get('dimensions', 3)) 

294 pos0[:ndim] = [float(element) for element in row[1:]] 

295 number = atomic_numbers.get(sym) # Use 0 ~ 'X' for unknown? 

296 tag = 0 

297 if number is None: 

298 if sym not in types: 

299 tag = len(types) + 1 

300 types[sym] = tag 

301 number = 0 

302 tag = types[sym] 

303 tags.append(tag) 

304 numbers.append(number) 

305 positions.append(pos0) 

306 positions = np.array(positions) 

307 tags = np.array(tags, int) 

308 if types: 

309 ase_types = {} 

310 for sym, tag in types.items(): 

311 ase_types[('X', tag)] = sym 

312 info = {'types': ase_types} # 'info' dict for Atoms object 

313 else: 

314 tags = None 

315 info = None 

316 return numbers, positions, tags, info 

317 

318 def read_atoms_from_file(fname, fmt): 

319 assert fname.startswith('"') or fname.startswith("'") 

320 assert fname[0] == fname[-1] 

321 fname = fname[1:-1] 

322 if directory is not None: 

323 fname = os.path.join(directory, fname) 

324 # XXX test xyz, pbd and xsf 

325 if fmt == 'xsf' and 'xsfcoordinatesanimstep' in kwargs: 

326 anim_step = kwargs.pop('xsfcoordinatesanimstep') 

327 theslice = slice(anim_step, anim_step + 1, 1) 

328 # XXX test animstep 

329 else: 

330 theslice = slice(None, None, 1) 

331 images = read(fname, theslice, fmt) 

332 if len(images) != 1: 

333 raise OctopusParseError('Expected only one image. Don\'t know ' 

334 'what to do with %d images.' % len(images)) 

335 return images[0] 

336 

337 # We will attempt to extract cell and pbc from kwargs if 'lacking'. 

338 # But they might have been left unspecified on purpose. 

339 # 

340 # We need to keep track of these two variables "externally" 

341 # because the Atoms object assigns values when they are not given. 

342 cell = None 

343 pbc = None 

344 adjust_positions_by_half_cell = False 

345 

346 atoms = None 

347 xsfcoords = kwargs.pop('xsfcoordinates', None) 

348 if xsfcoords is not None: 

349 atoms = read_atoms_from_file(xsfcoords, 'xsf') 

350 atoms.positions *= Bohr 

351 atoms.cell *= Bohr 

352 # As it turns out, non-periodic xsf is not supported by octopus. 

353 # Also, it only supports fully periodic or fully non-periodic.... 

354 # So the only thing that we can test here is 3D fully periodic. 

355 if sum(atoms.pbc) != 3: 

356 raise NotImplementedError('XSF not fully periodic with Octopus') 

357 cell = atoms.cell 

358 pbc = atoms.pbc 

359 # Position adjustment doesn't actually matter but this should work 

360 # most 'nicely': 

361 adjust_positions_by_half_cell = False 

362 xyzcoords = kwargs.pop('xyzcoordinates', None) 

363 if xyzcoords is not None: 

364 atoms = read_atoms_from_file(xyzcoords, 'xyz') 

365 atoms.positions *= Bohr 

366 adjust_positions_by_half_cell = True 

367 pdbcoords = kwargs.pop('pdbcoordinates', None) 

368 if pdbcoords is not None: 

369 atoms = read_atoms_from_file(pdbcoords, 'pdb') 

370 pbc = atoms.pbc 

371 adjust_positions_by_half_cell = True 

372 # Due to an error in ASE pdb, we can only test the nonperiodic case. 

373 # atoms.cell *= Bohr # XXX cell? Not in nonperiodic case... 

374 atoms.positions *= Bohr 

375 if sum(atoms.pbc) != 0: 

376 raise NotImplementedError('Periodic pdb not supported by ASE.') 

377 

378 if cell is None: 

379 # cell could not be established from the file, so we set it on the 

380 # Atoms now if possible: 

381 cell, kwargs = kwargs2cell(kwargs) 

382 if cell is not None: 

383 cell *= Bohr 

384 if cell is not None and atoms is not None: 

385 atoms.cell = cell 

386 # In case of boxshape = sphere and similar, we still do not have 

387 # a cell. 

388 

389 ndims = int(kwargs.get('dimensions', 3)) 

390 if ndims != 3: 

391 raise NotImplementedError('Only 3D calculations supported.') 

392 

393 coords = kwargs.get('coordinates') 

394 if coords is not None: 

395 numbers, pos, tags, info = get_positions_from_block('coordinates') 

396 pos *= Bohr 

397 adjust_positions_by_half_cell = True 

398 atoms = Atoms(cell=cell, numbers=numbers, positions=pos, 

399 tags=tags, info=info) 

400 rcoords = kwargs.get('reducedcoordinates') 

401 if rcoords is not None: 

402 numbers, spos, tags, info = get_positions_from_block( 

403 'reducedcoordinates') 

404 if cell is None: 

405 raise ValueError('Cannot figure out what the cell is, ' 

406 'and thus cannot interpret reduced coordinates.') 

407 atoms = Atoms(cell=cell, numbers=numbers, scaled_positions=spos, 

408 tags=tags, info=info) 

409 if atoms is None: 

410 raise OctopusParseError('Apparently there are no atoms.') 

411 

412 # Either we have non-periodic BCs or the atoms object already 

413 # got its BCs from reading the file. In the latter case 

414 # we shall override only if PeriodicDimensions was given specifically: 

415 

416 if pbc is None: 

417 pdims = int(kwargs.pop('periodicdimensions', 0)) 

418 pbc = np.zeros(3, dtype=bool) 

419 pbc[:pdims] = True 

420 atoms.pbc = pbc 

421 

422 if (cell is not None and cell.shape == (3,) 

423 and adjust_positions_by_half_cell): 

424 nonpbc = (atoms.pbc == 0) 

425 atoms.positions[:, nonpbc] += np.array(cell)[None, nonpbc] / 2.0 

426 

427 return atoms, kwargs 

428 

429 

430def generate_input(atoms, kwargs): 

431 """Convert atoms and keyword arguments to Octopus input file.""" 

432 _lines = [] 

433 

434 def append(line): 

435 _lines.append(line) 

436 

437 def extend(lines): 

438 _lines.extend(lines) 

439 append('') 

440 

441 def setvar(key, var): 

442 append('%s = %s' % (key, var)) 

443 

444 for kw in ['lsize', 'latticevectors', 'latticeparameters']: 

445 assert kw not in kwargs 

446 

447 defaultboxshape = 'parallelepiped' if atoms.pbc.any() else 'minimum' 

448 boxshape = kwargs.get('boxshape', defaultboxshape).lower() 

449 use_ase_cell = (boxshape == 'parallelepiped') 

450 atomskwargs = atoms2kwargs(atoms, use_ase_cell) 

451 

452 if use_ase_cell: 

453 if 'lsize' in atomskwargs: 

454 block = list2block('LSize', atomskwargs['lsize']) 

455 elif 'latticevectors' in atomskwargs: 

456 extend(list2block('LatticeParameters', [[1., 1., 1.]])) 

457 block = list2block('LatticeVectors', atomskwargs['latticevectors']) 

458 extend(block) 

459 

460 # Allow override or issue errors? 

461 pdim = 'periodicdimensions' 

462 if pdim in kwargs: 

463 if int(kwargs[pdim]) != int(atomskwargs[pdim]): 

464 raise ValueError('Cannot reconcile periodicity in input ' 

465 'with that of Atoms object') 

466 setvar('periodicdimensions', atomskwargs[pdim]) 

467 

468 # We like to output forces 

469 if 'output' in kwargs: 

470 output_string = kwargs.pop('output') 

471 output_tokens = [token.strip() 

472 for token in output_string.lower().split('+')] 

473 else: 

474 output_tokens = [] 

475 

476 if 'forces' not in output_tokens: 

477 output_tokens.append('forces') 

478 setvar('output', ' + '.join(output_tokens)) 

479 # It is illegal to have output forces without any OutputFormat. 

480 # Even though the forces are written in the same format no matter 

481 # OutputFormat. Thus we have to make one up: 

482 

483 if 'outputformat' not in kwargs: 

484 setvar('outputformat', 'xcrysden') 

485 

486 for key, val in kwargs.items(): 

487 # Most datatypes are straightforward but blocks require some attention. 

488 if isinstance(val, list): 

489 append('') 

490 dict_data = list2block(key, val) 

491 extend(dict_data) 

492 else: 

493 setvar(key, str(val)) 

494 append('') 

495 

496 coord_block = list2block('Coordinates', atomskwargs['coordinates']) 

497 extend(coord_block) 

498 return '\n'.join(_lines) 

499 

500 

501def atoms2kwargs(atoms, use_ase_cell): 

502 kwargs = {} 

503 

504 positions = atoms.positions / Bohr 

505 

506 if use_ase_cell: 

507 cell = atoms.cell / Bohr 

508 cell_offset = 0.5 * cell.sum(axis=0) 

509 positions -= cell_offset 

510 if atoms.cell.orthorhombic: 

511 Lsize = 0.5 * np.diag(cell) 

512 kwargs['lsize'] = [[repr(size) for size in Lsize]] 

513 # ASE uses (0...cell) while Octopus uses -L/2...L/2. 

514 # Lsize is really cell / 2, and we have to adjust our 

515 # positions by subtracting Lsize (see construction of the coords 

516 # block) in non-periodic directions. 

517 else: 

518 kwargs['latticevectors'] = cell.tolist() 

519 

520 types = atoms.info.get('types', {}) 

521 

522 coord_block = [] 

523 for sym, pos, tag in zip(atoms.get_chemical_symbols(), 

524 positions, atoms.get_tags()): 

525 if sym == 'X': 

526 sym = types.get((sym, tag)) 

527 if sym is None: 

528 raise ValueError('Cannot represent atom X without tags and ' 

529 'species info in atoms.info') 

530 coord_block.append([repr(sym)] + [repr(x) for x in pos]) 

531 

532 kwargs['coordinates'] = coord_block 

533 npbc = sum(atoms.pbc) 

534 for c in range(npbc): 

535 if not atoms.pbc[c]: 

536 msg = ('Boundary conditions of Atoms object inconsistent ' 

537 'with requirements of Octopus. pbc must be either ' 

538 '000, 100, 110, or 111.') 

539 raise ValueError(msg) 

540 kwargs['periodicdimensions'] = npbc 

541 

542 # TODO InitialSpins 

543 # 

544 # TODO can use maximumiterations + output/outputformat to extract 

545 # things from restart file into output files without trouble. 

546 # 

547 # Velocities etc.? 

548 return kwargs 

549 

550 

551@reader 

552def read_octopus_in(fd, get_kwargs=False): 

553 kwargs = parse_input_file(fd) 

554 

555 # input files may contain internal references to other files such 

556 # as xyz or xsf. We need to know the directory where the file 

557 # resides in order to locate those. If fd is a real file 

558 # object, it contains the path and we can use it. Else assume 

559 # pwd. 

560 # 

561 # Maybe this is ugly; maybe it can lead to strange bugs if someone 

562 # wants a non-standard file-like type. But it's probably better than 

563 # failing 'ase gui somedir/inp' 

564 try: 

565 fname = fd.name 

566 except AttributeError: 

567 directory = None 

568 else: 

569 directory = os.path.split(fname)[0] 

570 

571 atoms, remaining_kwargs = kwargs2atoms(kwargs, directory=directory) 

572 if get_kwargs: 

573 return atoms, remaining_kwargs 

574 else: 

575 return atoms