|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | 5 | from typing import Literal |
6 | | -from copy import copy, deepcopy |
| 6 | +from copy import copy |
7 | 7 | import time |
8 | 8 | import sys |
9 | 9 |
|
|
26 | 26 |
|
27 | 27 | try: |
28 | 28 | 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 |
31 | 29 | except ImportError: |
32 | 30 | pass |
33 | 31 |
|
@@ -103,15 +101,8 @@ def compute_ensemble( # pylint: disable=arguments-renamed |
103 | 101 | # Predict only the ones that are within uncertainty, the rest do via DFT/AiiDA. |
104 | 102 | if self.gp_model is not None: |
105 | 103 | 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) |
115 | 106 |
|
116 | 107 | dft_counts += len(dft_indices) |
117 | 108 |
|
@@ -172,246 +163,17 @@ def compute_ensemble( # pylint: disable=arguments-renamed |
172 | 163 |
|
173 | 164 | # ================ TRAIN SECTION ================ # |
174 | 165 | 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) |
179 | 167 |
|
180 | 168 | sys.stdout.flush() |
181 | 169 |
|
182 | 170 | # ================ FINALIZE ================ # |
183 | 171 | # if self.has_stress: |
184 | 172 | # self.stress_computed = copy(self.force_computed) |
185 | 173 |
|
186 | | - self._clean_runs(dft_counts) |
| 174 | + self._clean_runs(dft_counts, label="AIIDA CALCULATIONS") |
187 | 175 | self.init() |
188 | 176 |
|
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 | | - |
415 | 177 |
|
416 | 178 | def get_running_workchains(workchains: list[WorkChainNode], success: list[bool]) -> list: |
417 | 179 | """Get the running workchains popping the finished ones. |
|
0 commit comments