22
22
from ase .mep .neb import NEB
23
23
from ase .optimize import BFGS , FIRE , LBFGS , BFGSLineSearch , LBFGSLineSearch , MDMin
24
24
from ase .optimize .sciopt import SciPyFminBFGS , SciPyFminCG
25
+ from ase .stress import voigt_6_to_full_3x3_stress
26
+ from ase .units import GPa
25
27
from emmet .core .neb import NebMethod , NebResult
28
+ from emmet .core .trajectory import AtomTrajectory
26
29
from monty .serialization import dumpfn
27
30
from pymatgen .core .structure import Molecule , Structure
28
31
from pymatgen .core .trajectory import Trajectory as PmgTrajectory
@@ -83,7 +86,7 @@ def __init__(self, atoms: Atoms, store_md_outputs: bool = False) -> None:
83
86
self .forces : list [np .ndarray ] = []
84
87
85
88
self ._calc_kwargs = {
86
- "stress " : (
89
+ "stresses " : (
87
90
"stress" in self .atoms .calc .implemented_properties and self ._is_periodic
88
91
),
89
92
"magmoms" : True ,
@@ -113,7 +116,7 @@ def __call__(self) -> None:
113
116
# When _store_md_outputs is True, ideal gas contribution to
114
117
# stress is included.
115
118
# Only store stress for periodic systems.
116
- if self ._calc_kwargs ["stress " ]:
119
+ if self ._calc_kwargs ["stresses " ]:
117
120
self .stresses .append (
118
121
self .atoms .get_stress (include_ideal_gas = self ._store_md_outputs )
119
122
)
@@ -144,7 +147,7 @@ def compute_energy(self) -> float:
144
147
def save (
145
148
self ,
146
149
filename : str | PathLike | None ,
147
- fmt : Literal ["pmg" , "ase" , "xdatcar" ] = "ase" ,
150
+ fmt : Literal ["pmg" , "ase" , "xdatcar" , "parquet" ] = "ase" ,
148
151
) -> None :
149
152
"""
150
153
Save the trajectory file using monty.serialization.
@@ -162,6 +165,8 @@ def save(
162
165
self .to_pymatgen_trajectory (filename = filename , file_format = fmt ) # type: ignore[arg-type]
163
166
elif fmt == "ase" :
164
167
self .to_ase_trajectory (filename = filename )
168
+ elif fmt == "parquet" :
169
+ self .to_emmet_trajectory (filename = filename )
165
170
else :
166
171
raise ValueError (f"Unknown trajectory format { fmt } ." )
167
172
@@ -189,7 +194,7 @@ def to_ase_trajectory(
189
194
"energy" : self .energies [idx ],
190
195
"forces" : self .forces [idx ],
191
196
}
192
- if self ._calc_kwargs ["stress " ]:
197
+ if self ._calc_kwargs ["stresses " ]:
193
198
kwargs ["stress" ] = self .stresses [idx ]
194
199
if self ._calc_kwargs ["magmoms" ]:
195
200
kwargs ["magmom" ] = self .magmoms [idx ]
@@ -218,7 +223,7 @@ def to_pymatgen_trajectory(
218
223
If "xdatcar", writes a VASP-format XDATCAR object to file
219
224
"""
220
225
frame_property_keys = ["energy" , "forces" ]
221
- for k in ("stress " , "magmoms" , "velocities" , "temperature" ):
226
+ for k in ("stresses " , "magmoms" , "velocities" , "temperature" ):
222
227
if self ._calc_kwargs [k ]:
223
228
frame_property_keys += [k ]
224
229
@@ -276,12 +281,47 @@ def to_pymatgen_trajectory(
276
281
277
282
return pmg_traj
278
283
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
+
279
319
def as_dict (self ) -> dict :
280
320
"""Make JSONable dict representation of the Trajectory."""
281
321
traj_dict = {
282
322
"energy" : self .energies ,
283
323
"forces" : self .forces ,
284
- "stress " : self .stresses ,
324
+ "stresses " : self .stresses ,
285
325
"atom_positions" : self .atom_positions ,
286
326
"cells" : self .cells ,
287
327
"atoms" : self .atoms ,
@@ -413,9 +453,9 @@ def relax(
413
453
struct = self .ase_adaptor .get_structure (
414
454
atoms , cls = Molecule if is_mol else Structure
415
455
)
416
- traj = obs .to_pymatgen_trajectory ( None )
456
+ traj = obs .to_emmet_trajectory ( filename = None )
417
457
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 )
419
459
for idx in range (len (struct ))
420
460
)
421
461
@@ -434,9 +474,7 @@ def relax(
434
474
trajectory = traj ,
435
475
converged = converged ,
436
476
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 ],
440
478
dir_name = os .getcwd (),
441
479
elapsed_time = t_f - t_i ,
442
480
)
0 commit comments