import os
import sys
import shutil
import tempfile
import numpy as np
from copy import deepcopy
from collections import Counter
from ase import Atoms
from ase.data import atomic_numbers
from ase.calculators.lammps.unitconvert import convert
from .utilities import Logger
[docs]
def save_to_n2p2(desc_pars, model_pars, images=None, log=None,
reference_method='PBE'):
"""Generate input files for n2p2 to use its fast force inference.
By default,three or four types of files will be generated:
- scaling.data: scaling information for symmetry functions.
- input.nn: a settings file, including NN topology and SF setup.
- weights.xxx.data: NN weights and biases, xxx being the atomic number.
- input.data (optional): a configuration file includes atomic structures.
Parameters
----------
desc_pars: dict
AMP descriptor parameters.
model_pars: dict
AMP neural network model parameters.
images: list
List of ASE atoms objects.
log: str or Logger instance
Logging file.
reference_method : str
Method used to generate the reference data.
"""
if log is None:
log = Logger(sys.stdout)
elif isinstance(log, str):
log = Logger(log)
# desc_pars = calc.descriptor.parameters
# model_pars = calc.model.parameters
reference_method = reference_method.upper()
if (desc_pars['mode'] != 'atom-centered' or
model_pars['mode'] != 'atom-centered'):
raise NotImplementedError(
'N2P2 requires atom-centered symmetry functions.')
els = desc_pars['elements']
n_els = len(els)
cutoff_name = desc_pars['cutoff']['name'].lower()
if cutoff_name == 'cosine':
cutoff_type = 1
else:
raise NotImplementedError('Only cosine cutoff can be matched between'
' AMP and N2P2')
cutoff = desc_pars['cutoff']['kwargs']['Rc']
hl_dict = deepcopy(model_pars['hiddenlayers'])
hls = list(hl_dict.values())
for hl in hls:
if hl == hls[0]:
continue
else:
raise NotImplementedError('N2P2 requires the same NN for all'
' elements.')
hl = list(hls[0])
hl.append(1)
activation = model_pars['activation']
activation = [activation[0]]*(len(hl)) + ['l']
# Write the input.nn file
filename = 'input.nn'
overwrite_file(filename, suffix='.nn', log=log)
f = open(filename, 'w')
## Write header.
f.write('#'*79 + '\n')
f.write('# Length unit : Angstrom\n')
f.write('# Energy unit : eV\n')
f.write(f'# Reference method: {reference_method}\n')
f.write('#'*79 + '\n')
f.write('\n')
## General NNP settings
f.write('#'*79 + '\n')
f.write('# GENERAL NNP SETTINGS\n')
f.write('#'*79 + '\n')
f.write('{0: <32}{1:<15d}{2}\n'.format(
'number_of_elements', n_els, '# Number of elements.'))
f.write('{0: <32}{1:<15}{2}\n'.format(
'elements', ' '.join(els), '# Specification of elements.'))
f.write('{0: <32}{1:<15d}{2}\n'.format(
'cutoff_type', cutoff_type, '# Cutoff type.'))
f.write('{0: <32}{1:<15}{2}\n'.format(
'scale_symmetry_functions', ' ',
'# Scale all symmetry functions with min/max values.'))
f.write('{0: <32}{1:<15}{2}\n'.format(
'# scale_symmetry_functions_sigma', ' ',
'# Scale all symmetry functions with sigma.'))
f.write('{0: <32}{1:<15}{2}\n'.format(
'# atom_energy', 'S 0.0',
'# Free atom reference energy (S).'))
f.write('{0: <32}{1:<15.1f}{2}\n'.format(
'scale_min_short', -1., '# Minimum value for scaling.'))
f.write('{0: <32}{1:<15.1f}{2}\n'.format(
'scale_max_short', 1., '# Maximum value for scaling.'))
f.write('{0: <32}{1:<15d}{2}\n'.format(
'global_hidden_layers_short', len(hl), '# Number of hiddenlayers'))
hl_str = [str(_) for _ in hl]
f.write('{0: <32}{1:<15}{2}\n'.format(
'global_nodes_short', ' '.join(hl_str),
'# Number of nodes in each hidden layer'))
f.write('{0: <32}{1:<15}{2}\n'.format(
'global_activation_short', ' '.join(activation),
'# Activation function for each hidden layer and output layer.\n'))
f.write('#'*79 + '\n')
f.write('# ADDITIONAL SETTINGS FOR DATASET TOOLS\n')
f.write('#'*79 + '\n')
f.write('# These keywords are used only by tools handling data sets:\n')
f.write('{0: <32}{1:<15}{2}\n'.format(
'use_short_forces', '', '# Use forces.'))
f.write('{0: <32}{1:<15d}{2}\n'.format(
'random_seed', 1234567, '# Random number generator seed.\n'))
f.write('#'*79 + '\n')
f.write('# SYMMETRY FUNCTIONS\n')
f.write('#'*79 + '\n')
f.write('# Radial symmetry function (type 2):\n')
f.write('#symfunction_short <element-central> 2'
' <element-neighbor> <eta> <rshift> <rcutoff> \n\n')
f.write('# Narrow Angular symmetry function (type 3):\n')
f.write('#symfunction_short <element-central> 3 '
' <element-neighbor1> <element-neighbor2> '
'<eta> <lambda> <zeta> <rcutoff> <<rshift> \n\n')
f.write('# Wide Angular symmetry function (type 9):\n')
f.write('#symfunction_short <element-central> 9 '
' <element-neighbor1> <element-neighbor2> '
'<eta> <lambda> <zeta> <rcutoff> <<rshift> \n\n')
## Write symmetry functions
leading = 'symfunction_short'
Gs_indices_dict = {}
for el in els:
f.write(f'### Element {el}\n' )
Gs = desc_pars['Gs'][el]
# Gs = sorted(Gs, key=lambda x: x['type'])
Gs_indices, Gs = get_Gs_indices(Gs)
Gs_indices_dict[el] = Gs_indices
for G in Gs:
if G['type'] == 'G2':
neighb0 = G['element']
eta = G['eta'] / cutoff ** 2
offset = G['offset']
f.write('{0} {1:<3}{2} {3:<3}{4:.15E} {5:.3E} {6:.3E}\n'.\
format(leading, el, 2, neighb0, eta, offset, cutoff))
elif G['type'] == 'G4':
neighbs = G['elements']
neighbs = ' '.join(neighbs)
eta = G['eta'] / cutoff ** 2
gamma = G['gamma']
zeta = G['zeta']
f.write('{0} {1:<3}{2} {3:<8}{4:.15E} {5:d} {6:.3E} '
'{7:.3E} {8:.3E}\n'.\
format(leading, el, 3, neighbs, eta, gamma, zeta,
cutoff, 0.))
elif G['type'] == 'G5':
neighbs = G['elements']
neighbs = ' '.join(neighbs)
eta = G['eta'] / cutoff ** 2
gamma = G['gamma']
zeta = G['zeta']
f.write('{0} {1:<3}{2} {3:<8}{4:.15E} {5:d} {6:.3E} '
'{7:.3E} {8:.3E}\n'.\
format(leading, el, 9, neighbs, eta, gamma, zeta,
cutoff, 0.))
else:
raise NotImplementedError('Symmetry {0}'
' not known.'.format(G['type']))
f.write('\n')
f.close()
# Write the scaling.data file
scaling_file = 'scaling.data'
overwrite_file(scaling_file, suffix='.data', log=log)
f = open(scaling_file, 'w')
fprange = model_pars.fprange
## write headerlines
f.write('#'*79 + '\n')
f.write('# Symmetry function scaling data.\n')
f.write('#'*79 + '\n')
f.write('# Col Name Description\n')
f.write('#'*79 + '\n')
f.write('# 1 e_index Element index.\n')
f.write('# 2 sf_index Symmetry function index.\n')
f.write('# 3 sf_min Symmetry function minimum.\n')
f.write('# 4 sf_max Symmetry function maximum.\n')
f.write('# 5 sf_mean Symmetry function mean.\n')
f.write('# 6 sf_sigma Symmetry function sigma.\n')
f.write('#'*121 + '\n')
f.write('{0}{1:>9d}{2:>11d}{3:>25d}{4:>25d}{5:>25d}{6:>25d}\n'.format(
'#', 1, 2, 3, 4, 5, 6))
f.write('{0}{1:>9}{2:>11}{3:>25}{4:>25}{5:>25}{6:>25}\n'.format(
'#', 'e_index', 'sf_index', 'sf_min',
'sf_max', 'sf_mean', 'sf_sigma'))
f.write('#'*121 + '\n')
for e_i, el in enumerate(els):
e_index = e_i + 1
fprange_el = fprange[el]
sym_indices = Gs_indices_dict[el]
fprange_el = list(np.array(fprange_el)[sym_indices])
for sf_i, each_row in enumerate(fprange_el):
sf_index = sf_i + 1
sf_min, sf_max = each_row
sf_mean, sf_sigma = np.mean(each_row), np.std(each_row)
f.write('{0}{1:>10d}{2:>11d}{3:>25.16E}{4:>25.16E}'
'{5:>25.16E}{6:>25.16E}\n'.format(
'', e_index, sf_index, sf_min, sf_max, sf_mean, sf_sigma))
f.close()
# write NN weights and biases
ann_weights = model_pars.weights
scalings = model_pars.scalings
for el in els:
atomic_number = atomic_numbers[el]
nn_file = f'weights.{atomic_number:03d}.data'
overwrite_file(nn_file, suffix='.data', log=log)
f = open(nn_file, 'w')
f.write('#'*79 + '\n')
f.write('# Neural network connection values'
' (weights and biases).\n')
f.write('#'*79 + '\n')
f.write('# Col Name Description\n')
f.write('#'*79 + '\n')
f.write('# 1 connection Neural network connection value.\n')
f.write('# 2 t Connection type'
' (a = weight, b = bias).\n')
f.write('# 3 index Index enumerating weights.\n')
f.write('# 4 l_s Starting point layer'
' (end point layer for biases).\n')
f.write('# 5 n_s Starting point neuron in starting'
' layer (end point neuron for biases).\n')
f.write('# 6 l_e End point layer.\n')
f.write('# 7 n_e End point neuron in end layer.\n')
f.write('#'*79 + '\n')
f.write('# 1 2 3 4 5 6'
' 7\n')
f.write('# connection t index l_s n_s l_e'
' n_e\n')
f.write('#'*79 + '\n')
ann_weights_el = ann_weights[el]
scalings_el = scalings[el]
intercept = scalings_el['intercept']
slope = scalings_el['slope']
index = 0
ll = max(ann_weights_el.keys()) # number of last layer
syms_indices = Gs_indices_dict[el]
for key2 in sorted(ann_weights_el.keys()):
l_s, l_e = key2 - 1, key2
connections = np.array(ann_weights_el[key2])
if l_s < 1:
connections[:-1, :] = connections[syms_indices, :]
rows, cols = connections.shape
for ind, val in np.ndenumerate(connections):
n_s, n_e = ind[0] + 1, ind[1] + 1
index += 1
if n_s < rows:
t = 'a' # weight
f.write(f'{val:>24.16E} {t}{index:>10d}{l_s:>6d}'
f'{n_s:>6d}{l_e:>6d}{n_e:>6d}\n')
else:
t = 'b' # bias
f.write(f'{val:>24.16E} {t}{index:>10d}{l_e:>6d}'
f'{n_e:>6d}\n')
# coordinate different ll structures in amp and n2p2
l_s, l_e = ll, ll + 1
n_s, n_e = 1, 1
index += 1
t = 'a'
f.write(f"{slope:>24.16E} {t}{index:>10d}{l_s:>6d}"
f"{n_s:>6d}{l_e:>6d}{n_e:>6d}\n")
index += 1
t = 'b'
f.write(f"{intercept:>24.16E} {t}{index:>10d}{l_e:>6d}"
f"{n_e:>6d}\n")
f.close()
if not images:
return
save_to_n2p2_images_only(images, log=log)
[docs]
def save_to_n2p2_images_only(images, log=None):
"""Write the input.data;i.e., the atomic configurations. This is usually
used together with the `save_to_n2p2` function at above."""
if log is None:
log = Logger(sys.stdout)
elif isinstance(log, str):
log = Logger(log)
if not isinstance(images, list):
if isinstance(images, Atoms):
images = [images]
else:
raise ValueError(f'Type {type(images)} not supported.')
data_file = 'input.data'
overwrite_file(data_file, log=log, suffix='.data')
f = open(data_file, 'w')
for atoms in images:
positions = atoms.positions
symbols = atoms.symbols
count_elements = Counter(symbols)
count_str = ' '.join([' '.join([key, str(val)]) for key, val
in count_elements.items()])
pbc = atoms.pbc
cell = atoms.cell.array
try:
e = atoms.get_potential_energy()
except:
e = 0.
try:
forces = atoms.get_forces(apply_constraint=False)
except:
forces = np.zeros(positions.shape)
try:
charges = atoms.charges
except:
charges = np.zeros(len(atoms))
total_charge = 0.
f.write('begin\n')
if not any(pbc):
f.write('comment This non-periodic structure contains'
f' {count_str}\n')
else:
f.write('comment This periodic structure contains'
f' {count_str}\n')
f.write(f'lattice {cell[0, 0]:.9f} {cell[0, 1]:.9f}'
f' {cell[0, 2]:.9f}\n')
f.write(f'lattice {cell[1, 0]:.9f} {cell[1, 1]:.9f}'
f' {cell[1, 2]:.9f}\n')
f.write(f'lattice {cell[2, 0]:.9f} {cell[2, 1]:.9f}'
f' {cell[2, 2]:.9f}\n')
for i in range(len(atoms)):
position = positions[i]
symbol = symbols[i]
charge = charges[i]
n_i = 0. # not used
force = forces[i]
f.write(f'atom {position[0]:.7f} {position[1]:.7f}'
f' {position[2]:.7f} {symbol} {charge:.1f}'
f' {n_i:.1f} {force[0]:.6f} {force[1]:.6f}'
f' {force[2]:.6f}\n')
f.write(f'energy {e:.6f}\n')
f.write(f'charge {total_charge:.1f}\n')
f.write('end\n')
f.close()
[docs]
def overwrite_file(filename, log, suffix='.data'):
"""Overwrite files if existent."""
if os.path.exists(filename):
oldfilename = tempfile.NamedTemporaryFile(mode='w',
delete=False,
suffix=suffix).name
log('Overwriting file: "%s". Moving original to "%s".'
% (filename, oldfilename))
shutil.move(filename, oldfilename)
[docs]
def get_Gs_indices(Gs):
"""Handy function to deal with symmetry function orders."""
from operator import itemgetter
if not isinstance(Gs, list):
raise ValueError('Input not known!')
list_to_sort = []
for i, G in enumerate(Gs):
t = int(G['type'][-1])
if t == 2:
e1 = atomic_numbers[G['element']]
e2 = -9999
elif t == 4 or t == 5:
e1 = atomic_numbers[G['elements'][0]]
e2 = atomic_numbers[G['elements'][1]]
eta = G['eta']
rs = G['offset'] if 'offset' in G else 0.
zeta = G['zeta'] if 'zeta' in G else -9999.
la = G['gamma'] if 'gamma' in G else -9999
list_to_sort.append((t, eta, rs, zeta, la, e1, e2, i))
list_to_sort = sorted(list_to_sort, key=itemgetter(0, 1, 2, 3, 4, 5, 6),
reverse=False)
indices = [_[-1] for _ in list_to_sort]
Gs_sorted = list(np.array(Gs)[indices])
return indices, Gs_sorted
[docs]
def save_to_prophet(calc, filename='potential_', overwrite=False,
units="metal"):
"""Saves the calculator in a way that it can be used with PROPhet.
Parameters
----------
calc : obj
A trained Amp calculator object.
filename : str
File object or path to the file to write to.
overwrite : bool
If an output file with the same name exists, overwrite it.
units : str
LAMMPS units style to be used with the outfile file.
"""
if os.path.exists(filename):
if overwrite is False:
oldfilename = filename
filename = tempfile.NamedTemporaryFile(mode='w', delete=False,
suffix='.amp').name
calc._log('File "%s" exists. Instead saving to "%s".' %
(oldfilename, filename))
else:
oldfilename = tempfile.NamedTemporaryFile(mode='w',
delete=False,
suffix='.amp').name
calc._log('Overwriting file: "%s". Moving original to "%s".'
% (filename, oldfilename))
shutil.move(filename, oldfilename)
desc_pars = calc.descriptor.parameters
model_pars = calc.model.parameters
if (desc_pars['mode'] != 'atom-centered' or
model_pars['mode'] != 'atom-centered'):
raise NotImplementedError(
'PROPhet requires atom-centered symmetry functions.')
if desc_pars['cutoff']['name'] != 'Cosine':
raise NotImplementedError(
'PROPhet requires cosine cutoff functions.')
if model_pars['activation'] != 'tanh':
raise NotImplementedError(
'PROPhet requires tanh activation functions.')
els = desc_pars['elements']
n_els = len(els)
length_G2 = int(n_els)
length_G4 = int(n_els*(n_els+1)/2)
cutoff = convert(desc_pars['cutoff']['kwargs']['Rc'], 'distance', 'ASE', units)
# Get correct order of elements listed in the Amp object
el = desc_pars['elements'][0]
n_G2 = sum(1 for k in desc_pars['Gs'][el] if k['type'] == 'G2')
n_G4 = sum(1 for k in desc_pars['Gs'][el] if k['type'] == 'G4')
els_ordered = []
if n_G2 > 0:
for Gs in range(n_els):
els_ordered.append(desc_pars['Gs'][el][Gs]['element'])
elif n_G4 > 0:
for Gs in range(n_els):
els_ordered.append(desc_pars['Gs'][el][Gs]['elements'][0])
else:
raise RuntimeError('There must be at least one G2 or G4 symmetry '
'function.')
# Write each element's PROPhet input file
for el in desc_pars['elements']:
f = open(filename + el, 'w')
# Write header.
f.write('nn\n')
f.write('structure\n')
# Write elements.
f.write(el + ': ')
for el_i in els_ordered:
f.write(el_i+' ')
f.write('\n')
n_G2_el = sum(1 for k in desc_pars['Gs'][el] if k['type'] == 'G2')
n_G4_el = sum(1 for k in desc_pars['Gs'][el] if k['type'] == 'G4')
if n_G2_el != n_G2 or n_G4_el != n_G4:
raise NotImplementedError(
'PROPhet requires each element to have the same number of '
'symmetry functions.')
f.write(str(int(n_G2/length_G2+n_G4/length_G4))+'\n')
# Write G2s.
for Gs in range(0, n_G2, length_G2):
eta = desc_pars['Gs'][el][Gs]['eta']
for i in range(length_G2):
eta_2 = desc_pars['Gs'][el][Gs+i]['eta']
if eta != eta_2:
raise NotImplementedError(
'PROPhet requires each G2 function to have the '
'same eta value for all element pairs.')
f.write('G2 ' + str(cutoff) + ' 0 ' + str(eta/cutoff**2) +
' {}\n'.format(0.0)) #August 10/2-2020: Center of radial Gaussian. This should be changed if one wants to allow for non-centered Gaussians.
# Write G4s (G3s in PROPhet).
for Gs in range(n_G2, n_G2+n_G4, length_G4):
eta = desc_pars['Gs'][el][Gs]['eta']
gamma = desc_pars['Gs'][el][Gs]['gamma']
zeta = desc_pars['Gs'][el][Gs]['zeta']
for i in range(length_G4):
eta_2 = desc_pars['Gs'][el][Gs+i]['eta']
gamma_2 = desc_pars['Gs'][el][Gs+i]['gamma']
zeta_2 = desc_pars['Gs'][el][Gs+i]['zeta']
if eta != eta_2 or gamma != gamma_2 or zeta != zeta_2:
raise NotImplementedError(
'PROPhet requires each G4 function to have the '
'same eta, gamma, and zeta values for all '
'element pairs.')
f.write('G3 ' + str(cutoff) + ' 0 ' + str(eta/cutoff**2) +
' ' + str(zeta) + ' ' + str(gamma) + '\n')
# Write input means for G2.
for i in range(n_els):
for Gs in range(0, n_G2, length_G2):
# For debugging, to see the order of the PROPhet file
# if el==desc_pars['elements'][0]:
# print(desc_pars['Gs'][el][Gs+i])
mean = (model_pars['fprange'][el][Gs+i][1] +
model_pars['fprange'][el][Gs+i][0]) / 2.
#mean = model_pars['fprange'][el][Gs+i][1]
f.write(str(mean) + ' ')
# Write input means for G4.
for i in range(n_els):
for j in range(n_els-i):
for Gs in range(n_G2, n_G2 + n_G4, length_G4):
# For debugging, to see the order of the PROPhet file
# if el==desc_pars['elements'][0]:
# print(desc_pars['Gs'][el][Gs+j+n_els*i+int((i-i**2)/2)])
mean = (model_pars['fprange'][el][Gs + j + n_els * i +
int((i - i**2) / 2)][1] +
model_pars['fprange'][el][Gs + j + n_els * i +
int((i - i**2) / 2)][0])/2 #August added divide by 2
# NB the G4 mean is doubled to correct for PROPhet
# counting each neighbor pair twice as much as Amp
f.write(str(mean) + ' ')
f.write('\n')
# Write input variances for G2.
for i in range(n_els):
for Gs in range(0, n_G2, length_G2):
variance = (model_pars['fprange'][el][Gs+i][1] -
model_pars['fprange'][el][Gs+i][0]) / 2.
#variance = model_pars['fprange'][el][Gs+i][0]
f.write(str(variance) + ' ')
# Write input variances for G4.
for i in range(n_els):
for j in range(n_els-i):
for Gs in range(n_G2, n_G2 + n_G4, length_G4):
variance = (model_pars['fprange'][el][Gs + j + n_els * i +
int((i - i**2) /
2)][1] -
model_pars['fprange'][el][Gs + j + n_els * i +
int((i - i**2) /
2)][0])/2 #August added divide by 2
# NB the G4 variance is doubled to correct for PROPhet
# counting each neighbor pair twice as much as Amp
f.write(str(variance) + ' ')
f.write('\n')
f.write('energy\n')
# Write output mean.
f.write('0\n')
# Write output variance.
f.write('1\n')
curr_node = 0
# Write NN layer architecture.
for nodes in model_pars['hiddenlayers'][el]:
f.write(str(nodes)+' ')
f.write('1\n')
# Write first hidden layer of the NN for the symmetry functions.
layer = 0
f.write('[[ layer ' + str(layer) + ' ]]\n')
for node in range(model_pars['hiddenlayers'][el][layer]):
# Write each node of the layer.
f.write(' [ node ' + str(curr_node) + ' ] tanh\n')
f.write(' ')
# G2
for i in range(n_els):
for Gs in range(0, n_G2, length_G2):
f.write(str(model_pars['weights'][el]
[layer + 1][Gs + i][node]))
f.write(' ')
# G4
for i in range(n_els):
for j in range(n_els-i):
for Gs in range(n_G2, n_G2 + n_G4, length_G4):
f.write(str(model_pars['weights'][el]
[layer + 1][Gs + j + n_els * i +
int((i - i**2) / 2)][node]))
f.write(' ')
f.write('\n')
f.write(' ')
f.write(str(model_pars['weights'][el][layer+1][-1][node]))
f.write('\n')
curr_node += 1
# Write remaining hidden layers of the NN.
for layer in range(1, len(model_pars['hiddenlayers'][el])):
f.write('[[ layer ' + str(layer) + ' ]]\n')
for node in range(model_pars['hiddenlayers'][el][layer]):
f.write(' [ node ' + str(curr_node) + ' ] tanh\n')
f.write(' ')
for i in range(len(model_pars['weights'][el][layer+1])-1):
f.write(str(model_pars['weights'][el][layer+1][i][node]))
f.write(' ')
f.write('\n')
f.write(' ')
f.write(str(model_pars['weights'][el][layer+1][-1][node]))
f.write('\n')
curr_node += 1
# Write output layer of the NN, consisting of an activated node.
f.write('[[ layer ' + str(layer+1) + ' ]]\n')
f.write(' [ node ' + str(curr_node) + ' ] tanh\n')
f.write(' ')
for i in range(len(model_pars['weights'][el][layer+2])-1):
f.write(str(model_pars['weights'][el][layer+2][i][0]))
f.write(' ')
f.write('\n')
f.write(' ')
f.write(str(model_pars['weights'][el][layer+2][-1][0]))
f.write('\n')
curr_node += 1
# Write output layer of the NN, consisting of a linear node,
# representing Amp's scaling.
f.write('[[ layer ' + str(layer+2) + ' ]]\n')
f.write(' [ node ' + str(curr_node) + ' ] linear\n')
f.write(' ')
f.write(str(convert(model_pars['scalings'][el]['slope'], 'energy', 'ASE', units)))
f.write('\n')
f.write(' ')
f.write(str(convert(model_pars['scalings'][el]['intercept'], 'energy', 'ASE', units)))
f.write('\n')
f.close()
[docs]
def save_to_openkim(calc, filename='amp.params', overwrite=False,
units="metal"):
"""Saves the calculator in a way that it can be used with OpenKIM.
Parameters
----------
calc : obj
A trained Amp calculator object.
filename : str
File object or path to the file to write to.
overwrite : bool
If an output file with the same name exists, overwrite it.
units : str
LAMMPS units style to be used with the outfile file.
"""
if os.path.exists(filename):
if overwrite is False:
oldfilename = filename
filename = tempfile.NamedTemporaryFile(mode='w', delete=False,
suffix='.params')
calc._log('File "%s" exists. Instead saving to "%s".' %
(oldfilename, filename))
else:
oldfilename = tempfile.NamedTemporaryFile(mode='w',
delete=False,
suffix='.params')
calc._log('Overwriting file: "%s". Moving original to "%s".'
% (filename, oldfilename))
shutil.move(filename, oldfilename)
desc_pars = calc.descriptor.parameters
model_pars = calc.model.parameters
if (desc_pars['mode'] != 'atom-centered' or
model_pars['mode'] != 'atom-centered'):
raise NotImplementedError(
'KIM model requires atom-centered symmetry functions.')
if desc_pars['cutoff']['name'] != 'Cosine':
raise NotImplementedError(
'KIM model requires cosine cutoff functions.')
elements = desc_pars['elements']
# path = os.path.dirname(__file__)
elements = sorted(elements)
# f = open(path + '/../tools/amp-kim/amp_parameterized_model/' +
# filename, 'w')
f = open(filename, 'w')
f.write(str(len(elements)) + ' # number of chemical species')
f.write('\n')
f.write(' '.join(elements) + ' # chemical species')
f.write('\n')
f.write(' '.join(str(len(desc_pars['Gs'][element])) for element in
elements) +
' # number of fingerprints of each chemical species')
f.write('\n')
for element in elements:
count = 0
# writing symmetry functions
for G in desc_pars['Gs'][element]:
if G['type'] == 'G2':
f.write(element + ' ' + 'g2' + ' # fingerprint of %s' %
element)
f.write('\n')
f.write(G['element'] + ' ' + str(G['eta']) + ' # eta')
elif G['type'] == 'G4':
f.write(element + ' ' + 'g4' +
' # fingerprint of %s' % element)
f.write('\n')
f.write(G['elements'][0] + ' ' + G['elements'][1] + ' ' +
str(G['eta']) + ' ' + str(G['gamma']) + ' ' +
str(G['zeta']) + ' # eta, gamma, zeta')
f.write('\n')
# writing fingerprint range
f.write(str(model_pars['fprange'][element][count][0]) + ' ' +
str(model_pars['fprange'][element][count][1]) +
' # range of fingerprint %i of %s' % (count, element))
f.write('\n')
count += 1
# writing the cutoff
cutoff = convert(desc_pars['cutoff']['kwargs']['Rc'], 'distance', 'ASE', units)
f.write(str(cutoff) + ' # cutoff radius')
f.write('\n')
f.write(model_pars['activation'] + ' # activation function')
f.write('\n')
# writing the neural network structures
for element in elements:
f.write(str(len(model_pars['hiddenlayers'][element])) +
' # number of hidden-layers of %s neural network' % element)
f.write('\n')
f.write(' '.join(str(_) for _ in model_pars['hiddenlayers'][element]) +
' # number of nodes of hidden-layers of %s neural network' %
element)
f.write('\n')
# writing parameters of the neural network
f.write(' '.join(str(_) for _ in \
# calc.model.ravel.to_vector(model_pars.weights,
# model_pars.scalings)
calc.model.vector) +
' # weights, biases, and scalings of neural networks')
f.write('\n')
f.close()