Skip to content

Commit 43c8632

Browse files
Add units (#378)
* Save units for all calculations * Fix ASE unit definition * Test units * Use Base units for MD stats --------- Co-authored-by: Jacob Wilkins <[email protected]>
1 parent b727ff3 commit 43c8632

File tree

10 files changed

+137
-23
lines changed

10 files changed

+137
-23
lines changed

janus_core/calculations/base.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@
1818
from janus_core.helpers.struct_io import input_structs
1919
from janus_core.helpers.utils import FileNameMixin, none_to_dict, set_log_tracker
2020

21+
UNITS = {
22+
"energy": "eV",
23+
"forces": "ev/Ang",
24+
"stress": "ev/Ang^3",
25+
"hessian": "ev/Ang^2",
26+
"time": "fs",
27+
"real_time": "s",
28+
"temperature": "K",
29+
"pressure": "GPa",
30+
"momenta": "(eV*u)^0.5",
31+
"density": "g/cm^3",
32+
"volume": "Ang^3",
33+
}
34+
2135

2236
class BaseCalculation(FileNameMixin):
2337
"""
@@ -209,3 +223,21 @@ def __init__(
209223
self.tracker = config_tracker(
210224
self.logger, self.track_carbon, **self.tracker_kwargs
211225
)
226+
227+
def _set_info_units(
228+
self, keys: Sequence[str] = ("energy", "forces", "stress")
229+
) -> None:
230+
"""
231+
Save units to structure info.
232+
233+
Parameters
234+
----------
235+
keys : Sequence
236+
Keys for which to add units to structure info. Default is
237+
("energy", "forces", "stress").
238+
"""
239+
if isinstance(self.struct, Sequence):
240+
for image in self.struct:
241+
image.info["units"] = {key: UNITS[key] for key in keys}
242+
else:
243+
self.struct.info["units"] = {key: UNITS[key] for key in keys}

janus_core/calculations/eos.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ def run(self) -> EoSResults:
295295
Dictionary containing equation of state ASE object, and the fitted minimum
296296
bulk modulus, volume, and energy.
297297
"""
298+
self._set_info_units()
299+
298300
if self.minimize:
299301
if self.logger:
300302
self.logger.info("Minimising initial structure")

janus_core/calculations/geom_opt.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,8 @@ def run(self) -> None:
308308
if self.tracker:
309309
self.tracker.start_task("Geometry optimization")
310310

311+
self._set_info_units()
312+
311313
converged = self.dyn.run(fmax=self.fmax, steps=self.steps)
312314

313315
# Calculate current maximum force

janus_core/calculations/md.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from typing import Any
1313
from warnings import warn
1414

15-
from ase import Atoms, units
15+
from ase import Atoms
1616
from ase.geometry.analysis import Analysis
1717
from ase.io import read
1818
from ase.md.langevin import Langevin
@@ -23,9 +23,11 @@
2323
ZeroRotation,
2424
)
2525
from ase.md.verlet import VelocityVerlet
26+
from ase.units import create_units
2627
import numpy as np
2728
import yaml
2829

30+
from janus_core.calculations.base import UNITS as JANUS_UNITS
2931
from janus_core.calculations.base import BaseCalculation
3032
from janus_core.calculations.geom_opt import GeomOpt
3133
from janus_core.helpers.janus_types import (
@@ -43,6 +45,7 @@
4345
from janus_core.processing.correlator import Correlation
4446
from janus_core.processing.post_process import compute_rdf, compute_vaf
4547

48+
units = create_units("2014")
4649
DENS_FACT = (units.m / 1.0e2) ** 3 / units.mol
4750

4851

@@ -525,7 +528,7 @@ def _set_info(self) -> None:
525528
"""Set time in fs, current dynamics step, and density to info."""
526529
time = (self.offset * self.timestep + self.dyn.get_time()) / units.fs
527530
step = self.offset + self.dyn.nsteps
528-
self.dyn.atoms.info["time_fs"] = time
531+
self.dyn.atoms.info["time"] = time
529532
self.dyn.atoms.info["step"] = step
530533
try:
531534
density = (
@@ -772,7 +775,7 @@ def get_stats(self) -> dict[str, float]:
772775
return {
773776
"Step": self.dyn.atoms.info["step"],
774777
"Real_Time": real_time.total_seconds(),
775-
"Time": self.dyn.atoms.info["time_fs"],
778+
"Time": self.dyn.atoms.info["time"],
776779
"Epot/N": e_pot,
777780
"EKin/N": e_kin,
778781
"T": current_temp,
@@ -800,21 +803,21 @@ def unit_info(self) -> dict[str, str]:
800803
"""
801804
return {
802805
"Step": None,
803-
"Real_Time": "s",
804-
"Time": "fs",
805-
"Epot/N": "eV",
806-
"EKin/N": "eV",
807-
"T": "K",
808-
"ETot/N": "eV",
809-
"Density": "g/cm^3",
810-
"Volume": "A^3",
811-
"P": "GPa",
812-
"Pxx": "GPa",
813-
"Pyy": "GPa",
814-
"Pzz": "GPa",
815-
"Pyz": "GPa",
816-
"Pxz": "GPa",
817-
"Pxy": "GPa",
806+
"Real_Time": JANUS_UNITS["real_time"],
807+
"Time": JANUS_UNITS["time"],
808+
"Epot/N": JANUS_UNITS["energy"],
809+
"EKin/N": JANUS_UNITS["energy"],
810+
"T": JANUS_UNITS["temperature"],
811+
"ETot/N": JANUS_UNITS["energy"],
812+
"Density": JANUS_UNITS["density"],
813+
"Volume": JANUS_UNITS["volume"],
814+
"P": JANUS_UNITS["pressure"],
815+
"Pxx": JANUS_UNITS["pressure"],
816+
"Pyy": JANUS_UNITS["pressure"],
817+
"Pzz": JANUS_UNITS["pressure"],
818+
"Pyz": JANUS_UNITS["pressure"],
819+
"Pxz": JANUS_UNITS["pressure"],
820+
"Pxy": JANUS_UNITS["pressure"],
818821
}
819822

820823
@property
@@ -1024,6 +1027,19 @@ def _write_restart(self) -> None:
10241027

10251028
def run(self) -> None:
10261029
"""Run molecular dynamics simulation and/or temperature ramp."""
1030+
unit_keys = (
1031+
"energy",
1032+
"forces",
1033+
"stress",
1034+
"time",
1035+
"real_time",
1036+
"temperature",
1037+
"pressure",
1038+
"density",
1039+
"momenta",
1040+
)
1041+
self._set_info_units(unit_keys)
1042+
10271043
if not self.restart:
10281044
if self.minimize:
10291045
self._optimize_structure()
@@ -1265,7 +1281,10 @@ def unit_info(self) -> dict[str, str]:
12651281
dict[str, str]
12661282
Units attached to statistical properties.
12671283
"""
1268-
return super().unit_info | {"Target_P": "GPa", "Target_T": "K"}
1284+
return super().unit_info | {
1285+
"Target_P": JANUS_UNITS["pressure"],
1286+
"Target_T": JANUS_UNITS["temperature"],
1287+
}
12691288

12701289
@property
12711290
def default_formats(self) -> dict[str, str]:
@@ -1362,7 +1381,7 @@ def unit_info(self) -> dict[str, str]:
13621381
dict[str, str]
13631382
Units attached to statistical properties.
13641383
"""
1365-
return super().unit_info | {"Target_T": "K"}
1384+
return super().unit_info | {"Target_T": JANUS_UNITS["temperature"]}
13661385

13671386
@property
13681387
def default_formats(self) -> dict[str, str]:
@@ -1505,7 +1524,7 @@ def unit_info(self) -> dict[str, str]:
15051524
dict[str, str]
15061525
Units attached to statistical properties.
15071526
"""
1508-
return super().unit_info | {"Target_T": "K"}
1527+
return super().unit_info | {"Target_T": JANUS_UNITS["temperature"]}
15091528

15101529
@property
15111530
def default_formats(self) -> dict[str, str]:

janus_core/calculations/phonons.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@ def calc_force_constants(
440440
if self.tracker:
441441
self.tracker.start_task("Phonon calculation")
442442

443+
self._set_info_units()
444+
443445
cell = self._ASE_to_PhonopyAtoms(self.struct)
444446

445447
if len(self.supercell) == 3:

janus_core/calculations/single_point.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,8 @@ def run(self) -> CalcResults:
325325
if self.tracker:
326326
self.tracker.start_task("Single point")
327327

328+
self._set_info_units(self.properties)
329+
328330
if "energy" in self.properties:
329331
self.results["energy"] = self._get_potential_energy()
330332
if "forces" in self.properties:

janus_core/processing/observables.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,16 @@
55
from abc import ABC, abstractmethod
66
from typing import TYPE_CHECKING
77

8-
from ase import Atoms, units
8+
from ase import Atoms
9+
from ase.units import create_units
910

1011
if TYPE_CHECKING:
1112
from janus_core.helpers.janus_types import SliceLike
1213

1314
from janus_core.helpers.utils import slicelike_to_startstopstep
1415

16+
units = create_units("2014")
17+
1518

1619
# pylint: disable=too-few-public-methods
1720
class Observable(ABC):

tests/test_geomopt_cli.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,3 +740,32 @@ def test_no_carbon(tmp_path):
740740
with open(summary_path, encoding="utf8") as file:
741741
geomopt_summary = yaml.safe_load(file)
742742
assert "emissions" not in geomopt_summary
743+
744+
745+
def test_units(tmp_path):
746+
"""Test correct units are saved."""
747+
results_path = tmp_path / "NaCl-opt.extxyz"
748+
log_path = tmp_path / "test.log"
749+
summary_path = tmp_path / "summary.yml"
750+
751+
result = runner.invoke(
752+
app,
753+
[
754+
"geomopt",
755+
"--struct",
756+
DATA_PATH / "NaCl.cif",
757+
"--out",
758+
results_path,
759+
"--log",
760+
log_path,
761+
"--summary",
762+
summary_path,
763+
],
764+
)
765+
assert result.exit_code == 0
766+
767+
atoms = read(results_path)
768+
expected_units = {"energy": "eV", "forces": "ev/Ang", "stress": "ev/Ang^3"}
769+
assert "units" in atoms.info
770+
for prop, units in expected_units.items():
771+
assert atoms.info["units"][prop] == units

tests/test_md_cli.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,23 @@ def test_md(ensemble):
120120
assert "momenta" in atoms.arrays
121121
assert "masses" in atoms.arrays
122122

123+
expected_units = {
124+
"time": "fs",
125+
"real_time": "s",
126+
"energy": "eV",
127+
"forces": "ev/Ang",
128+
"stress": "ev/Ang^3",
129+
"temperature": "K",
130+
"density": "g/cm^3",
131+
"momenta": "(eV*u)^0.5",
132+
}
133+
if ensemble in ("nvt", "nvt-nh"):
134+
expected_units["pressure"] = "GPa"
135+
136+
assert "units" in atoms.info
137+
for prop, units in expected_units.items():
138+
assert atoms.info["units"][prop] == units
139+
123140
finally:
124141
final_path.unlink(missing_ok=True)
125142
restart_path.unlink(missing_ok=True)
@@ -165,7 +182,7 @@ def test_log(tmp_path):
165182
assert len(lines) == 22
166183

167184
# Test constant volume
168-
assert lines[0].split(" | ")[8] == "Volume [A^3]"
185+
assert lines[0].split(" | ")[8] == "Volume [Ang^3]"
169186
init_volume = float(lines[1].split()[8])
170187
final_volume = float(lines[-1].split()[8])
171188
assert init_volume == 179.406144

tests/test_singlepoint_cli.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,11 @@ def test_singlepoint():
7272
assert "system_name" in atoms.info
7373
assert atoms.info["system_name"] == "NaCl"
7474

75+
expected_units = {"energy": "eV", "forces": "ev/Ang", "stress": "ev/Ang^3"}
76+
assert "units" in atoms.info
77+
for prop, units in expected_units.items():
78+
assert atoms.info["units"][prop] == units
79+
7580
clear_log_handlers()
7681

7782

@@ -399,6 +404,7 @@ def test_hessian(tmp_path):
399404
assert "mace_mp_hessian" in atoms.info
400405
assert "mace_stress" not in atoms.info
401406
assert atoms.info["mace_mp_hessian"].shape == (24, 8, 3)
407+
assert atoms.info["units"]["hessian"] == "ev/Ang^2"
402408

403409

404410
def test_no_carbon(tmp_path):

0 commit comments

Comments
 (0)