Skip to content

Commit 14c8250

Browse files
committed
Phase 5: slim AiiDAEnsemble to use the shared OTF helpers
Delete _predict_with_model, _compute_properties, _write_model, _update_gp, _train_gp and _clean_runs from AiiDAEnsemble (now inherited unchanged from Ensemble). Replace the inline default setup + empty-model guard and the TRAIN SECTION in compute_ensemble with calls to _otf_setup_defaults, _otf_predict and _otf_maybe_train_and_write. Keep the 'AIIDA CALCULATIONS' summary header via _clean_runs(..., label='AIIDA CALCULATIONS'). Drop the now-unused flare imports (compute_mae, get_env_indices, is_std_in_bound, deepcopy); keep FLARE_Atoms for the workchain monitoring loop. Behavior-preserving: the existing tests/aiida_ensemble/ suite (which calls the inherited helpers on an AiiDAEnsemble) passes unchanged.
1 parent c6c29d7 commit 14c8250

1 file changed

Lines changed: 5 additions & 243 deletions

File tree

Modules/aiida_ensemble.py

Lines changed: 5 additions & 243 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from __future__ import annotations
44

55
from typing import Literal
6-
from copy import copy, deepcopy
6+
from copy import copy
77
import time
88
import sys
99

@@ -26,8 +26,6 @@
2626

2727
try:
2828
from flare.atoms import FLARE_Atoms
29-
from flare.io.output import compute_mae
30-
from flare.learners.utils import get_env_indices, is_std_in_bound
3129
except ImportError:
3230
pass
3331

@@ -103,15 +101,8 @@ def compute_ensemble( # pylint: disable=arguments-renamed
103101
# Predict only the ones that are within uncertainty, the rest do via DFT/AiiDA.
104102
if self.gp_model is not None:
105103
number_of_atoms = structures[0].get_ase_atoms().get_global_number_of_atoms()
106-
107-
if self.max_atoms_added < 0:
108-
self.max_atoms_added = number_of_atoms
109-
110-
if self.init_atoms is None:
111-
self.init_atoms = list(range(number_of_atoms))
112-
113-
if len(self.gp_model.training_data) > 0:
114-
self._predict_with_model(structures, dft_indices)
104+
self._otf_setup_defaults(number_of_atoms)
105+
self._otf_predict(structures, dft_indices)
115106

116107
dft_counts += len(dft_indices)
117108

@@ -172,246 +163,17 @@ def compute_ensemble( # pylint: disable=arguments-renamed
172163

173164
# ================ TRAIN SECTION ================ #
174165
if self.gp_model is not None:
175-
if dft_counts > 0:
176-
if self.train_hyps[0] <= len(self.gp_model.training_data) <= self.train_hyps[1]:
177-
self._train_gp()
178-
self._write_model()
166+
self._otf_maybe_train_and_write(dft_counts)
179167

180168
sys.stdout.flush()
181169

182170
# ================ FINALIZE ================ #
183171
# if self.has_stress:
184172
# self.stress_computed = copy(self.force_computed)
185173

186-
self._clean_runs(dft_counts)
174+
self._clean_runs(dft_counts, label="AIIDA CALCULATIONS")
187175
self.init()
188176

189-
def _predict_with_model(
190-
self,
191-
structures: list[Structure],
192-
dft_indices: list[int],
193-
) -> None:
194-
"""Predict on all the structures and estimate errors.
195-
196-
This is used to remove the structures indecis to not compute via AiiDA/DFT.
197-
198-
Args:
199-
----
200-
structures: list of :class:`~cellconstructor.Structure.Structure` to simulate
201-
dft_indices: list of integers related to the structures
202-
203-
"""
204-
sub_indices = deepcopy(dft_indices)
205-
206-
for index in sub_indices:
207-
structure = structures[index]
208-
atoms = FLARE_Atoms.from_ase_atoms(structure.get_ase_atoms())
209-
self._compute_properties(atoms)
210-
211-
# get max uncertainty atoms
212-
if self.build_mode == 'bayesian':
213-
env_selection = is_std_in_bound
214-
elif self.build_mode == 'direct':
215-
env_selection = get_env_indices
216-
217-
tic = time.time()
218-
219-
std_in_bound, _ = env_selection(
220-
self.std_tolerance,
221-
self.gp_model.force_noise,
222-
atoms,
223-
max_atoms_added=self.max_atoms_added,
224-
update_style=self.update_style,
225-
update_threshold=self.update_threshold,
226-
)
227-
228-
self.output.write_wall_time(tic, task='Env Selection')
229-
230-
if not std_in_bound:
231-
print(f"[DFT CALLED] For structure with id={index}")
232-
else:
233-
print(f"[BFFS USED] For structure with id={index}")
234-
dft_indices.remove(index) # remove index computed via ML-FF
235-
236-
self.energies[index] = deepcopy(atoms.get_potential_energy()) / units.Ry # eV -> Ry
237-
self.forces[index] = deepcopy(atoms.get_forces()) / units.Ry # eV/Ang -> Ry/Ang
238-
if self.has_stress:
239-
self.stresses[index, :, :] = -1 * deepcopy(atoms.get_stress(voigt=False)) * (units.Bohr**3 / units.Ry) # -eV/(Ang^3) -> Ry/(Bohr^3)
240-
self.stress_computed[index] = True
241-
242-
self.force_computed[index] = True
243-
244-
sys.stdout.flush()
245-
246-
247-
def _compute_properties(self, atoms: FLARE_Atoms) -> None:
248-
"""Compute energies, forces, stresses, and their uncertainties.
249-
250-
The FLARE ASE calculator is used, and write the results.
251-
252-
Args:
253-
----
254-
atoms: a :class:`flare.atoms.FLARE_Atoms` instance for which to compute properties
255-
256-
"""
257-
tic = time.time()
258-
259-
atoms.calc = self.flare_calc
260-
atoms.calc.calculate(atoms)
261-
262-
self.output.write_wall_time(tic, task='Compute Properties')
263-
264-
def _write_model(self) -> None:
265-
"""Write the current model in a JSON file."""
266-
self.flare_calc.write_model(self.flare_name)
267-
268-
def _update_gp(
269-
self,
270-
atoms: FLARE_Atoms,
271-
dft_frcs: ndarray,
272-
dft_energy: float | None = None,
273-
dft_stress: ndarray | None = None,
274-
) -> None:
275-
"""Update the current GP model.
276-
277-
Args:
278-
----
279-
atoms (FLARE_Atoms): :class:`flare.atoms.FLARE_Atoms`` instance whose
280-
local environments will be added to the training set.
281-
dft_frcs (np.ndarray): DFT forces on all atoms in the structure, in eV/Angstrom.
282-
dft_energy (float): total energy of the entire structure, in eV.
283-
dft_stress (np.ndarray): DFT stress on structure.
284-
Sign as in ASE (-1 in respect with QE), units in eV/Angstrom^3,
285-
and in Voigt notation, i.e. (xx, yy, zz, yz, xz, xy).
286-
287-
"""
288-
from ase.calculators.singlepoint import SinglePointCalculator
289-
290-
tic = time.time()
291-
is_empty_model = len(self.gp_model.training_data) == 0
292-
293-
# Here we make the decision to skip adding environments, if the stds
294-
# are within the user-defined boundaries, even if the ab-initio calculation
295-
# was performed. This avoids slowing down the model, while the SSCHA
296-
# is feeded with the DFT results.
297-
if is_empty_model:
298-
std_in_bound = False
299-
train_atoms = self.init_atoms
300-
else:
301-
self._compute_properties(atoms)
302-
303-
# get max uncertainty atoms
304-
if self.build_mode == 'bayesian':
305-
env_selection = is_std_in_bound
306-
elif self.build_mode == 'direct':
307-
env_selection = get_env_indices
308-
309-
tic = time.time()
310-
311-
std_in_bound, train_atoms = env_selection(
312-
self.std_tolerance,
313-
self.gp_model.force_noise,
314-
atoms,
315-
max_atoms_added=self.max_atoms_added,
316-
update_style=self.update_style,
317-
update_threshold=self.update_threshold,
318-
)
319-
320-
self.output.write_wall_time(tic, task='Env Selection')
321-
322-
# compute mae and write to output
323-
e_mae, e_mav, f_mae, f_mav, s_mae, s_mav = compute_mae(
324-
atoms,
325-
self.output.basename,
326-
atoms.potential_energy,
327-
atoms.forces,
328-
atoms.stress,
329-
dft_energy,
330-
dft_frcs,
331-
dft_stress,
332-
False,
333-
)
334-
335-
if not std_in_bound:
336-
if not is_empty_model:
337-
stds = self.flare_calc.results.get('stds', np.zeros_like(dft_frcs))
338-
self.output.add_atom_info(train_atoms, stds)
339-
340-
# Convert ASE stress (xx, yy, zz, yz, xz, xy) to FLARE stress
341-
# (xx, xy, xz, yy, yz, zz).
342-
flare_stress = None
343-
if dft_stress is not None:
344-
flare_stress = -np.array([
345-
dft_stress[0],
346-
dft_stress[5],
347-
dft_stress[4],
348-
dft_stress[1],
349-
dft_stress[3],
350-
dft_stress[2],
351-
])
352-
353-
results = {
354-
'forces': dft_frcs,
355-
'energy': dft_energy,
356-
'free_energy': dft_energy,
357-
'stress': dft_stress,
358-
}
359-
360-
atoms.calc = SinglePointCalculator(atoms, **results)
361-
362-
# update gp model
363-
self.gp_model.update_db(
364-
atoms,
365-
dft_frcs,
366-
custom_range=train_atoms,
367-
energy=dft_energy,
368-
stress=flare_stress,
369-
)
370-
371-
self.gp_model.set_L_alpha()
372-
self.output.write_wall_time(tic, task='Update GP')
373-
374-
375-
def _train_gp(self) -> None:
376-
"""Optimize the hyperparameters of the current GP model."""
377-
tic = time.time()
378-
379-
self.gp_model.train(logger_name=self.output_name + 'hyps.dat')
380-
381-
self.output.write_wall_time(tic, task='Train Hyps')
382-
383-
hyps, labels = self.gp_model.hyps_and_labels
384-
if labels is None:
385-
labels = self.gp_model.hyp_labels
386-
387-
self.output.write_hyps(
388-
labels,
389-
hyps,
390-
tic, # actually here there should be the actual start time of the entire simulation
391-
self.gp_model.likelihood,
392-
self.gp_model.likelihood_gradient,
393-
hyps_mask=self.gp_model.hyps_mask,
394-
)
395-
396-
def _clean_runs(self, dft_counts: int) -> None:
397-
"""Clean the failed runs and print summary.
398-
399-
Args:
400-
----
401-
dft_counts (int): number of performed DFT calculations.
402-
403-
"""
404-
n_calcs = np.sum(self.force_computed.astype(int))
405-
print('=============== SUMMARY AIIDA CALCULATIONS =============== \n')
406-
print('Total structures included: ', n_calcs)
407-
print('Structures not included : ', self.N-n_calcs)
408-
if self.gp_model is not None:
409-
print('Steps using OTF-ML model : ', self.N-dft_counts)
410-
print()
411-
print('===================== END OF SUMMARY ===================== \n')
412-
if n_calcs != self.N:
413-
self.remove_noncomputed()
414-
415177

416178
def get_running_workchains(workchains: list[WorkChainNode], success: list[bool]) -> list:
417179
"""Get the running workchains popping the finished ones.

0 commit comments

Comments
 (0)