2222from ase .mep .neb import NEB
2323from ase .optimize import BFGS , FIRE , LBFGS , BFGSLineSearch , LBFGSLineSearch , MDMin
2424from ase .optimize .sciopt import SciPyFminBFGS , SciPyFminCG
25+ from ase .stress import voigt_6_to_full_3x3_stress
26+ from ase .units import GPa
2527from emmet .core .neb import NebMethod , NebResult
28+ from emmet .core .trajectory import AtomTrajectory
2629from monty .serialization import dumpfn
2730from pymatgen .core .structure import Molecule , Structure
2831from pymatgen .core .trajectory import Trajectory as PmgTrajectory
@@ -83,7 +86,7 @@ def __init__(self, atoms: Atoms, store_md_outputs: bool = False) -> None:
8386 self .forces : list [np .ndarray ] = []
8487
8588 self ._calc_kwargs = {
86- "stress " : (
89+ "stresses " : (
8790 "stress" in self .atoms .calc .implemented_properties and self ._is_periodic
8891 ),
8992 "magmoms" : True ,
@@ -113,7 +116,7 @@ def __call__(self) -> None:
113116 # When _store_md_outputs is True, ideal gas contribution to
114117 # stress is included.
115118 # Only store stress for periodic systems.
116- if self ._calc_kwargs ["stress " ]:
119+ if self ._calc_kwargs ["stresses " ]:
117120 self .stresses .append (
118121 self .atoms .get_stress (include_ideal_gas = self ._store_md_outputs )
119122 )
@@ -144,7 +147,7 @@ def compute_energy(self) -> float:
144147 def save (
145148 self ,
146149 filename : str | PathLike | None ,
147- fmt : Literal ["pmg" , "ase" , "xdatcar" ] = "ase" ,
150+ fmt : Literal ["pmg" , "ase" , "xdatcar" , "parquet" ] = "ase" ,
148151 ) -> None :
149152 """
150153 Save the trajectory file using monty.serialization.
@@ -162,6 +165,8 @@ def save(
162165 self .to_pymatgen_trajectory (filename = filename , file_format = fmt ) # type: ignore[arg-type]
163166 elif fmt == "ase" :
164167 self .to_ase_trajectory (filename = filename )
168+ elif fmt == "parquet" :
169+ self .to_emmet_trajectory (filename = filename )
165170 else :
166171 raise ValueError (f"Unknown trajectory format { fmt } ." )
167172
@@ -189,7 +194,7 @@ def to_ase_trajectory(
189194 "energy" : self .energies [idx ],
190195 "forces" : self .forces [idx ],
191196 }
192- if self ._calc_kwargs ["stress " ]:
197+ if self ._calc_kwargs ["stresses " ]:
193198 kwargs ["stress" ] = self .stresses [idx ]
194199 if self ._calc_kwargs ["magmoms" ]:
195200 kwargs ["magmom" ] = self .magmoms [idx ]
@@ -218,7 +223,7 @@ def to_pymatgen_trajectory(
218223 If "xdatcar", writes a VASP-format XDATCAR object to file
219224 """
220225 frame_property_keys = ["energy" , "forces" ]
221- for k in ("stress " , "magmoms" , "velocities" , "temperature" ):
226+ for k in ("stresses " , "magmoms" , "velocities" , "temperature" ):
222227 if self ._calc_kwargs [k ]:
223228 frame_property_keys += [k ]
224229
@@ -276,12 +281,47 @@ def to_pymatgen_trajectory(
276281
277282 return pmg_traj
278283
284+ def to_emmet_trajectory (
285+ self , filename : str | PathLike | None = None
286+ ) -> AtomTrajectory :
287+ """Create an emmet.core.AtomTrajectory."""
288+ frame_props = {
289+ "cells" : "lattice" ,
290+ "energies" : "energy" ,
291+ "forces" : "forces" ,
292+ "stresses" : "stress" ,
293+ "magmoms" : "magmoms" ,
294+ "velocities" : "velocities" ,
295+ "temperatures" : "temperature" ,
296+ }
297+ for k in ("stresses" , "magmoms" ):
298+ if not self ._calc_kwargs [k ]:
299+ frame_props .pop (k )
300+
301+ ionic_step_data = {v : getattr (self , k ) for k , v in frame_props .items ()}
302+ if self ._calc_kwargs ["stresses" ]:
303+ # NOTE: convert stress units from eV/A³ to kBar (* -1 from standard output)
304+ # and to 3x3 matrix to comply with MP convention
305+ ionic_step_data ["stress" ] = [
306+ voigt_6_to_full_3x3_stress (val * - 10 / GPa ) for val in self .stresses
307+ ]
308+
309+ traj = AtomTrajectory (
310+ elements = self .atoms .get_atomic_numbers (),
311+ cart_coords = self .atom_positions ,
312+ num_ionic_steps = len (self .atom_positions ),
313+ ** ionic_step_data ,
314+ )
315+ if filename :
316+ traj .to (file_name = filename )
317+ return traj
318+
279319 def as_dict (self ) -> dict :
280320 """Make JSONable dict representation of the Trajectory."""
281321 traj_dict = {
282322 "energy" : self .energies ,
283323 "forces" : self .forces ,
284- "stress " : self .stresses ,
324+ "stresses " : self .stresses ,
285325 "atom_positions" : self .atom_positions ,
286326 "cells" : self .cells ,
287327 "atoms" : self .atoms ,
@@ -413,9 +453,9 @@ def relax(
413453 struct = self .ase_adaptor .get_structure (
414454 atoms , cls = Molecule if is_mol else Structure
415455 )
416- traj = obs .to_pymatgen_trajectory ( None )
456+ traj = obs .to_emmet_trajectory ( filename = None )
417457 is_force_conv = all (
418- np .linalg .norm (traj .frame_properties [- 1 ][ "forces" ][idx ]) < abs (fmax )
458+ np .linalg .norm (traj .forces [- 1 ][idx ]) < abs (fmax )
419459 for idx in range (len (struct ))
420460 )
421461
@@ -434,9 +474,7 @@ def relax(
434474 trajectory = traj ,
435475 converged = converged ,
436476 is_force_converged = is_force_conv ,
437- energy_downhill = (
438- traj .frame_properties [- 1 ]["energy" ] < traj .frame_properties [0 ]["energy" ]
439- ),
477+ energy_downhill = traj .energy [- 1 ] < traj .energy [0 ],
440478 dir_name = os .getcwd (),
441479 elapsed_time = t_f - t_i ,
442480 )
0 commit comments