The first function I use quite frequently is one which converts an atoms object into a string format. This allows me to use an atoms object as the input to my calculation from any computer. That way, I can stage them from my personal computer ans simply have Fireworks unpack and run them once they are on the cluster.
import numpy as np
import json
def atoms_to_encode(images):
""" Converts an list of atoms objects to an encoding
from a .traj file.
"""
if not isinstance(images, list):
images = [images]
# Convert all constraints into dictionary format
constraints = [_.todict() for _ in images[0].constraints]
for i, C in enumerate(constraints):
# Turn any arrays in the kwargs into lists
for k, v in list(C['kwargs'].items()):
if isinstance(v, np.ndarray):
constraints[i]['kwargs'][k] = v.tolist()
# Convert any arrays from the parameter settings into lists
keys = images[0].info
for k, v in list(keys.items()):
if isinstance(v, np.ndarray):
keys[k] = v.tolist()
data = {'trajectory': {}}
# Assemble the compressed dictionary of results
for i, atoms in enumerate(images):
if i == 0:
# For first images, collect cell and positions normally
pos = atoms.get_positions()
update_pos = pos
cell = atoms.get_cell()
update_cell = cell
# Add the parameters which do not change
data['numbers'] = images[0].get_atomic_numbers().tolist()
data['pbc'] = images[0].get_pbc().tolist()
data['constraints'] = constraints
data['calculator_parameters'] = keys
else:
# For consecutive images, check for duplication
# If duplicates are found, do not store it
if np.array_equal(atoms.get_positions(), pos):
update_pos = np.array([])
else:
pos = atoms.get_positions()
update_pos = pos
if np.array_equal(atoms.get_cell(), cell):
update_cell = np.array([])
else:
cell = atoms.get_cell()
update_cell = cell
if atoms._calc:
nrg = atoms.get_potential_energy()
force = atoms.get_forces()
stress = atoms.get_stress()
# Stage results and convert to lists in needed
results = {
'positions': update_pos,
'cell': update_cell,
'energy': nrg,
'forces': force,
'stress': stress}
else:
results = {
'positions': update_pos,
'cell': update_cell}
for k, v in list(results.items()):
if isinstance(v, np.ndarray):
results[k] = v.tolist()
# Store trajectory, throwing out None values
data['trajectory'][i] = {
k: v for k, v in list(
results.items()) if v is not None}
# Return the reduced results in JSON compression
return json.dumps(data)
Now that I have this function, I can store it into my path can call it to turn an atoms object into a JSON string which is safe to add into the database. Lets revisit the original quantum espresso example and see how this works.
#!/usr/bin/env python
from ase import Atoms
from qefw import atoms_to_encode
# Define the ase-espresso keys we will use.
keys = {
'mode': 'relax',
'opt_algorithm': 'bfgs',
'xc': 'RPBE',
'outdir': '.',
'output': {'removesave': True},
'pw': 200,
'dw': 2000,
'dipole': {'status': True},
'kpts': (1, 1, 1),
'calcstress': True,
'convergence': {
'energy': 1e-5,
'mixing': 0.35}}
# Create the atoms object
atoms = Atoms(
'H2',
[[0.0, 0.0, 0.0],
[0.0, 0.0, 0.8]])
atoms.center(vacuum=4)
atoms.info = keys
# Call the encoding function.
encoding = atoms_to_encode(atoms)
print(encoding)
{"trajectory": {"0": {"positions": [[4.0, 4.0, 4.0], [4.0, 4.0, 4.8]], "cell": [[8.0, 0.0, 0.0], [0.0, 8.0, 0.0], [0.0, 0.0, 8.8]]}}, "numbers": [1, 1], "pbc": [false, false, false], "constraints": [], "calculator_parameters": {"mode": "relax", "opt_algorithm": "bfgs", "xc": "RPBE", "outdir": ".", "output": {"removesave": true}, "pw": 200, "dw": 2000, "dipole": {"status": true}, "kpts": [1, 1, 1], "calcstress": true, "convergence": {"energy": 1e-05, "mixing": 0.35}}}
This provides a lovely bundled up atoms object which is complete with the ase-espresso keywords we expect to be run with this atoms object. Not only is this exactly what we need to run the calculation, it's also perfect documentation for calling on later to identify what this calculation is.