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(x) for x 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 kwargs = {key.lower(): value for key, value in kwargs.items()} 

435 

436 def append(line): 

437 _lines.append(line) 

438 

439 def extend(lines): 

440 _lines.extend(lines) 

441 append('') 

442 

443 def setvar(key, var): 

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

445 

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

447 assert kw not in kwargs 

448 

449 defaultboxshape = 'parallelepiped' if atoms.cell.rank > 0 else 'minimum' 

450 boxshape = kwargs.pop('boxshape', defaultboxshape).lower() 

451 use_ase_cell = (boxshape == 'parallelepiped') 

452 atomskwargs = atoms2kwargs(atoms, use_ase_cell) 

453 

454 setvar('boxshape', boxshape) 

455 

456 if use_ase_cell: 

457 if 'reducedcoordinates' in atomskwargs: 

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

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

460 else: 

461 assert 'lsize' in atomskwargs 

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

463 

464 extend(block) 

465 

466 # Allow override or issue errors? 

467 pdim = 'periodicdimensions' 

468 if pdim in kwargs: 

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

470 raise ValueError('Cannot reconcile periodicity in input ' 

471 'with that of Atoms object') 

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

473 

474 # We should say that we want the forces if the user requests forces. 

475 # However the Output keyword has changed between Octopus 10.1 and 11.4. 

476 # We'll leave it to the user to manually set keywords correctly. 

477 # The most complicated part is that the user probably needs to tell 

478 # Octopus to save the forces in xcrysden format in order for us to 

479 # parse them. 

480 

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

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

483 if isinstance(val, list): 

484 append('') 

485 dict_data = list2block(key, val) 

486 extend(dict_data) 

487 else: 

488 setvar(key, str(val)) 

489 append('') 

490 

491 if 'reducedcoordinates' in atomskwargs: 

492 coord_block = list2block('ReducedCoordinates', 

493 atomskwargs['reducedcoordinates']) 

494 else: 

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

496 extend(coord_block) 

497 return '\n'.join(_lines) 

498 

499 

500def atoms2kwargs(atoms, use_ase_cell): 

501 kwargs = {} 

502 

503 if any(atoms.pbc): 

504 coordtype = 'reducedcoordinates' 

505 coords = atoms.get_scaled_positions(wrap=False) 

506 else: 

507 coordtype = 'coordinates' 

508 coords = atoms.positions / Bohr 

509 

510 if use_ase_cell: 

511 cell = atoms.cell / Bohr 

512 if coordtype == 'coordinates': 

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

514 coords -= cell_offset 

515 else: 

516 assert coordtype == 'reducedcoordinates' 

517 

518 if atoms.cell.orthorhombic: 

519 Lsize = 0.5 * np.diag(cell) 

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

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

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

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

524 # block) in non-periodic directions. 

525 else: 

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

527 

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

529 

530 coord_block = [] 

531 for sym, pos, tag in zip(atoms.symbols, coords, atoms.get_tags()): 

532 if sym == 'X': 

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

534 if sym is None: 

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

536 'species info in atoms.info') 

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

538 

539 kwargs[coordtype] = coord_block 

540 npbc = sum(atoms.pbc) 

541 for c in range(npbc): 

542 if not atoms.pbc[c]: 

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

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

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

546 raise ValueError(msg) 

547 kwargs['periodicdimensions'] = npbc 

548 

549 # TODO InitialSpins 

550 # 

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

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

553 # 

554 # Velocities etc.? 

555 return kwargs 

556 

557 

558@reader 

559def read_octopus_in(fd, get_kwargs=False): 

560 kwargs = parse_input_file(fd) 

561 

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

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

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

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

566 # pwd. 

567 # 

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

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

570 # failing 'ase gui somedir/inp' 

571 try: 

572 fname = fd.name 

573 except AttributeError: 

574 directory = None 

575 else: 

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

577 

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

579 if get_kwargs: 

580 return atoms, remaining_kwargs 

581 else: 

582 return atoms