######################################################################## EMLE-Engine: https://github.com/chemle/emle-engine## Copyright: 2023-2025## Authors: Lester Hedges <lester.hedges@gmail.com># Kirill Zinovjev <kzinovjev@gmail.com>## EMLE-Engine is free software: you can redistribute it and/or modify# it under the terms of the GNU General Public License as published by# the Free Software Foundation, either version 2 of the License, or# (at your option) any later version.## EMLE-Engine is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the# GNU General Public License for more details.## You should have received a copy of the GNU General Public License# along with EMLE-Engine. If not, see <http://www.gnu.org/licenses/>.#####################################################################"""DeePMD in-vacuo backend implementation."""__all__=["DeePMD"]importaseas_aseimportnumpyas_npimportosas_osfrom.._unitsimport_EV_TO_HARTREE,_BOHR_TO_ANGSTROMfrom._backendimportBackendas_Backend
[docs]classDeePMD(_Backend):""" DeepMD in-vacuo backend implementation. """def__init__(self,model,deviation=None,deviation_threshold=None):# We support a str, or list/tuple of strings.ifnotisinstance(model,(str,list,tuple)):raiseTypeError("'model' must be of type 'str', or a list of 'str' types")else:# Make sure all values are strings.ifisinstance(model,(list,tuple)):forminmodel:ifnotisinstance(m,str):raiseTypeError("'model' must be of type 'str', or a list of 'str' types")# Convert to a list.else:model=[model]# Make sure all of the model files exist.forminmodel:ifnot_os.path.isfile(m):raiseIOError(f"Unable to locate DeePMD model file: '{m}'")# Validate the deviation file.ifdeviationisnotNone:ifnotisinstance(deviation,str):raiseTypeError("'deviation' must be of type 'str'")self._deviation=deviationifdeviation_thresholdisnotNone:try:deviation_threshold=float(deviation_threshold)except:raiseTypeError("'deviation_threshold' must be of type 'float'")self._deviation_threshold=deviation_thresholdelse:self._deviation=Noneself._deviation_threshold=None# Store the list of model files, removing any duplicates.self._model=list(set(model))iflen(self._model)==1anddeviation:raiseIOError("More that one DeePMD model needed to calculate the deviation!")# Initialise DeePMD backend attributes.try:fromdeepmd.inferimportDeepPotas_DeepPotself._potential=[_DeepPot(m)forminself._model]self._z_map=[]fordpinself._potential:self._z_map.append({element:indexforindex,elementinenumerate(dp.get_type_map())})except:raiseRuntimeError("Unable to create the DeePMD potentials!")self._max_f_std=None
[docs]defcalculate(self,atomic_numbers,xyz,forces=True):""" Compute the energy and forces. Parameters ---------- atomic_numbers: numpy.ndarray, (N_BATCH, N_QM_ATOMS,) The atomic numbers of the atoms in the QM region. xyz: numpy.ndarray, (N_BATCH, N_QM_ATOMS, 3) The coordinates of the atoms in the QM region in Angstrom. forces: bool Whether to calculate and return forces. Returns ------- energy: float The in-vacuo energy in Eh. forces: numpy.ndarray The in-vacuo forces in Eh/Bohr. """ifnotisinstance(atomic_numbers,_np.ndarray):raiseTypeError("'atomic_numbers' must be of type 'numpy.ndarray'")ifnotisinstance(xyz,_np.ndarray):raiseTypeError("'xyz' must be of type 'numpy.ndarray'")iflen(atomic_numbers)!=len(xyz):raiseValueError(f"Length of 'atomic_numbers' ({len(atomic_numbers)}) does not "f"match length of 'xyz' ({len(xyz)})")# Convert to batched NumPy arrays.iflen(atomic_numbers.shape)==1:atomic_numbers=_np.expand_dims(atomic_numbers,axis=0)xyz=_np.expand_dims(xyz,axis=0)e_list=[]f_list=[]# Run a calculation for each model and take the average.fori,dpinenumerate(self._potential):# Assume all frames have the same number of atoms.atom_types=[self._z_map[i][_ase.Atom(z).symbol]forzinatomic_numbers[0]]e,f,_=dp.eval(xyz,cells=None,atom_types=atom_types)e_list.append(e)f_list.append(f)# Write the maximum DeePMD force deviation to file.ifself._deviation:fromdeepmd.infer.model_deviimportcalc_model_devi_fmax_f_std=calc_model_devi_f(_np.array(f_list))[0][0]ifself._deviation_thresholdandmax_f_std>self._deviation_threshold:msg="Force deviation threshold reached!"self._max_f_std=max_f_stdraiseValueError(msg)withopen(self._deviation,"a")asf:f.write(f"{max_f_std:12.5f}\n")# To be written to qm_xyz_file.self._max_f_std=max_f_std# Take averages over models and return.e_mean=_np.mean(_np.array(e_list),axis=0)f_mean=-_np.mean(_np.array(f_list),axis=0)# Covert units.e,f=(e_mean.flatten()*_EV_TO_HARTREE,f_mean*_EV_TO_HARTREE*_BOHR_TO_ANGSTROM,)returne,fifforceselsee