Skip to content

Commit 532cd84

Browse files
jkitchinclaude
andcommitted
Add atoms change detection for ASE optimizer compatibility
Track atoms state (positions, cell, numbers, pbc) and detect changes between calculate() calls. This enables ASE optimizers like ExpCellFilter to work correctly by triggering new VASP calculations when the geometry changes. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 46a451b commit 532cd84

File tree

1 file changed

+52
-2
lines changed

1 file changed

+52
-2
lines changed

vasp/calculator.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,49 @@ def set(self, **kwargs) -> dict:
283283

284284
return changed
285285

286+
def _atoms_changed(self, atoms: Atoms | None) -> bool:
287+
"""Check if atoms have changed since last calculation.
288+
289+
Compares positions, cell, and atomic numbers to detect changes
290+
that require a new calculation (e.g., for ASE optimizers).
291+
"""
292+
if atoms is None:
293+
atoms = self.atoms
294+
if atoms is None:
295+
return False
296+
297+
# Check if we have stored state from previous calculation
298+
if not hasattr(self, "_last_atoms_state"):
299+
return True
300+
301+
last = self._last_atoms_state
302+
try:
303+
# Check atomic numbers
304+
if not np.array_equal(atoms.numbers, last["numbers"]):
305+
return True
306+
# Check positions (within tolerance)
307+
if not np.allclose(atoms.positions, last["positions"], atol=1e-10):
308+
return True
309+
# Check cell (within tolerance)
310+
if not np.allclose(atoms.cell[:], last["cell"], atol=1e-10):
311+
return True
312+
# Check pbc
313+
if not np.array_equal(atoms.pbc, last["pbc"]):
314+
return True
315+
except (KeyError, ValueError):
316+
return True
317+
318+
return False
319+
320+
def _store_atoms_state(self, atoms: Atoms) -> None:
321+
"""Store current atoms state for change detection."""
322+
self._last_atoms_state = {
323+
"numbers": atoms.numbers.copy(),
324+
"positions": atoms.positions.copy(),
325+
"cell": atoms.cell[:].copy(),
326+
"pbc": atoms.pbc.copy(),
327+
}
328+
286329
def calculate(
287330
self,
288331
atoms: Atoms | None = None,
@@ -314,12 +357,17 @@ def calculate(
314357
self.atoms = atoms
315358
self._setup_sorting(atoms)
316359

360+
# Check if atoms have changed (for ASE optimizers)
361+
atoms_changed = self._atoms_changed(self.atoms)
362+
317363
# Check current status
318364
status = self.runner.status(self.directory)
319-
log.debug(f"Current status: {status.state}")
365+
log.debug(f"Current status: {status.state}, atoms_changed: {atoms_changed}")
320366

321-
if status.state == JobState.COMPLETE and not self.force:
367+
if status.state == JobState.COMPLETE and not self.force and not atoms_changed:
322368
self.read_results()
369+
if self.atoms is not None:
370+
self._store_atoms_state(self.atoms)
323371
return
324372

325373
if status.state == JobState.QUEUED:
@@ -342,6 +390,8 @@ def calculate(
342390

343391
if result.state == JobState.COMPLETE:
344392
self.read_results()
393+
if self.atoms is not None:
394+
self._store_atoms_state(self.atoms)
345395
elif result.state == JobState.FAILED:
346396
raise VaspNotConverged(result.message or "Calculation failed")
347397

0 commit comments

Comments
 (0)