@@ -203,6 +203,12 @@ def __init__(
203203 self ._direct_uncertainty_key_BASE = (
204204 "mtt::aux::non_conservative_forces_uncertainty"
205205 )
206+ # cache of the last conservative forces/stress ensemble computation, as
207+ # (atoms, forces_ensemble, stress_ensemble); avoids recomputing the
208+ # expensive Jacobian pass when called again for the same atoms.
209+ self ._uq_cache : Optional [
210+ Tuple [Atoms , Optional [np .ndarray ], Optional [np .ndarray ]]
211+ ] = None
206212
207213 self .calculator = MetatomicCalculator (
208214 loaded_model ,
@@ -340,6 +346,17 @@ def _run_forces_stress_uq(
340346 else :
341347 atoms = self .atoms
342348
349+ cached = self ._uq_cache
350+ if cached is not None and cached [0 ] == atoms :
351+ cached_forces , cached_stress = cached [1 ], cached [2 ]
352+ if (not compute_forces or cached_forces is not None ) and (
353+ not compute_stress or cached_stress is not None
354+ ):
355+ return (
356+ cached_forces if compute_forces else None ,
357+ cached_stress if compute_stress else None ,
358+ )
359+
343360 calc = self .calculator
344361 # Unwrap SymmetrizedCalculator if present
345362 if isinstance (calc , SymmetrizedCalculator ):
@@ -445,7 +462,16 @@ def _compute_ensemble(*positions_and_strain):
445462 # [3, 3, n_ensemble]
446463 stress_ensemble = stress_jac .transpose (1 , 2 , 0 ) / volume
447464
448- return forces_ensemble , stress_ensemble
465+ # keep whichever of forces/stress was already cached for these atoms but
466+ # not recomputed here, instead of discarding it
467+ if cached is not None and cached [0 ] == atoms :
468+ forces_ensemble = forces_ensemble if compute_forces else cached [1 ]
469+ stress_ensemble = stress_ensemble if compute_stress else cached [2 ]
470+ self ._uq_cache = (atoms .copy (), forces_ensemble , stress_ensemble )
471+ return (
472+ forces_ensemble if compute_forces else None ,
473+ stress_ensemble if compute_stress else None ,
474+ )
449475
450476 @property
451477 def _direct_ensemble_key (self ) -> str :
0 commit comments