Skip to content

Commit b1d4b5b

Browse files
committed
ASEClusterRunner: tolerate ASE calculators without stress support
The runner computed atoms.get_stress() unconditionally, so a calculator without a stress implementation crashed every job even when the ensemble was computed with get_stress=False. Guard the stress call (like ASEDirectCalculator already did) and omit the stress key from the output JSON; ASEFileCalculator.read_results now treats it as optional. Adds regression tests covering a stress-less calculator on both DirectCluster and the file-bridge runner path.
1 parent 4be3395 commit b1d4b5b

3 files changed

Lines changed: 81 additions & 4 deletions

File tree

Modules/ASEClusterRunner.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
python -m sscha.ASEClusterRunner --calc calculator.pkl \
88
--input PREFIX_input.json --output PREFIX.json
99
10-
The output JSON contains energy [eV], forces [eV/Angstrom], stress
11-
[ASE Voigt (xx, yy, zz, yz, xz, xy), eV/Angstrom^3] and the computed Atoms.
10+
The output JSON contains energy [eV], forces [eV/Angstrom], the computed
11+
Atoms and, when the calculator supports it, stress [ASE Voigt
12+
(xx, yy, zz, yz, xz, xy), eV/Angstrom^3].
1213
"""
1314
import argparse
1415
import pickle
@@ -36,9 +37,12 @@ def main():
3637
payload = {
3738
"energy": float(atoms.get_potential_energy()), # eV
3839
"forces": atoms.get_forces(), # eV / Angstrom
39-
"stress": atoms.get_stress(), # ASE Voigt, -eV/Angstrom^3
4040
"atoms": atoms, # structure actually computed
4141
}
42+
try:
43+
payload["stress"] = atoms.get_stress() # ASE Voigt, -eV/Angstrom^3
44+
except Exception:
45+
pass # calculator without stress: call compute_ensemble with get_stress=False
4246
with open(args.output, "w") as handle:
4347
handle.write(ase.io.jsonio.encode(payload))
4448

Modules/ClusterCalculators.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,11 @@ def read_results(self):
187187
self.results = {
188188
"energy": payload["energy"], # eV
189189
"forces": payload["forces"], # eV / Angstrom
190-
"stress": payload["stress"], # ASE Voigt (xx,yy,zz,yz,xz,xy), -eV/Angstrom^3
191190
}
191+
if "stress" in payload:
192+
# ASE Voigt (xx,yy,zz,yz,xz,xy), -eV/Angstrom^3. Absent when the
193+
# calculator does not support stress (get_stress=False).
194+
self.results["stress"] = payload["stress"]
192195
self.structure = CC.Structure.Structure()
193196
self.structure.generate_from_ase_atoms(payload["atoms"])
194197

tests/test_otf/test_cluster_interface.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,73 @@ def __getstate__(self):
150150
raise TypeError("no pickle")
151151
with pytest.raises(TypeError):
152152
cluster_calcs.ASEFileCalculator(Unpicklable())
153+
154+
155+
def test_directcluster_stress_less_calculator():
156+
"""A calculator without stress works via DirectCluster with get_stress=False."""
157+
class NoStressEMT(EMT):
158+
def get_stress(self, atoms=None, voigt=True):
159+
raise NotImplementedError("NoStressEMT does not support stress")
160+
161+
np.random.seed(0)
162+
ensemble = sscha.Ensemble.Ensemble(get_gold_dyn(), 0)
163+
ensemble.generate(4)
164+
cluster = sscha.BaseCluster.DirectCluster(batch_size=2, job_number=1)
165+
ensemble.compute_ensemble(cluster_calcs.ASEDirectCalculator(NoStressEMT()),
166+
compute_stress=False, cluster=cluster)
167+
168+
assert all(ensemble.force_computed)
169+
assert not any(ensemble.stress_computed)
170+
from ase.units import create_units
171+
Ry = create_units("2006")["Ry"]
172+
atoms = ensemble.structures[0].get_ase_atoms()
173+
atoms.calc = NoStressEMT()
174+
assert ensemble.energies[0] == pytest.approx(atoms.get_potential_energy() / Ry, rel=1e-10)
175+
assert np.allclose(ensemble.forces[0], atoms.get_forces() / Ry, atol=1e-10)
176+
177+
178+
_NO_STRESS_MODULE = """
179+
from ase.calculators.emt import EMT
180+
181+
182+
class NoStressEMT(EMT):
183+
\"\"\"EMT that refuses to compute the stress (like stress-less codes).\"\"\"
184+
def get_stress(self, atoms=None, voigt=True):
185+
raise NotImplementedError("NoStressEMT does not support stress")
186+
"""
187+
188+
189+
def test_ase_file_calculator_stress_less_roundtrip(tmp_path):
190+
"""The file bridge tolerates calculators without stress (get_stress=False).
191+
192+
The runner must not crash on a missing stress implementation: the output
193+
JSON simply omits the ``stress`` key and ``read_results`` keeps working.
194+
"""
195+
import sys as _sys
196+
197+
(tmp_path / "_nostress_calc.py").write_text(_NO_STRESS_MODULE)
198+
_sys.path.insert(0, str(tmp_path))
199+
try:
200+
from _nostress_calc import NoStressEMT
201+
202+
calc = cluster_calcs.ASEFileCalculator(NoStressEMT())
203+
calc.set_directory(str(tmp_path))
204+
calc.set_label("ESP_0")
205+
struct = get_gold_structure()
206+
calc.write_input(struct)
207+
208+
# Execute the runner exactly as Cluster.get_execution_command would
209+
cmd = calc.command.replace("PREFIX", os.path.join(str(tmp_path), "ESP_0"))
210+
subprocess.run(cmd, shell=True, check=True, cwd=str(tmp_path))
211+
212+
calc.read_results()
213+
assert "energy" in calc.results
214+
assert "forces" in calc.results
215+
assert "stress" not in calc.results
216+
217+
atoms = struct.get_ase_atoms()
218+
atoms.calc = NoStressEMT()
219+
assert calc.results["energy"] == pytest.approx(atoms.get_potential_energy(), rel=1e-10)
220+
assert np.allclose(calc.results["forces"], atoms.get_forces(), atol=1e-10)
221+
finally:
222+
_sys.path.remove(str(tmp_path))

0 commit comments

Comments
 (0)