npl 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- npl-1.0.0/PKG-INFO +14 -0
- npl-1.0.0/README.md +89 -0
- npl-1.0.0/npl/__init__.py +9 -0
- npl-1.0.0/npl/calculators/__init__.py +10 -0
- npl-1.0.0/npl/calculators/base_calculator.py +0 -0
- npl-1.0.0/npl/calculators/energy_calculator.py +424 -0
- npl-1.0.0/npl/calculators/top_calculator.py +35 -0
- npl-1.0.0/npl/core/__init__.py +13 -0
- npl-1.0.0/npl/core/adsorption.py +293 -0
- npl-1.0.0/npl/core/atom_wrapper.py +124 -0
- npl-1.0.0/npl/core/base_nanoparticle.py +522 -0
- npl-1.0.0/npl/core/database_class.py +0 -0
- npl-1.0.0/npl/core/nanoparticle.py +53 -0
- npl-1.0.0/npl/core/neighbor_list.py +108 -0
- npl-1.0.0/npl/core/profiler.py +17 -0
- npl-1.0.0/npl/descriptors/__init__.py +20 -0
- npl-1.0.0/npl/descriptors/global_feature_classifier.py +512 -0
- npl-1.0.0/npl/descriptors/local_environment_calculator.py +132 -0
- npl-1.0.0/npl/descriptors/local_environment_feature_classifier.py +175 -0
- npl-1.0.0/npl/monte_carlo/__init__.py +3 -0
- npl-1.0.0/npl/monte_carlo/ensembles/__init__.py +2 -0
- npl-1.0.0/npl/monte_carlo/ensembles/base_ensemble.py +99 -0
- npl-1.0.0/npl/monte_carlo/ensembles/canonical_ensemble.py +197 -0
- npl-1.0.0/npl/monte_carlo/monte_carlo.py +323 -0
- npl-1.0.0/npl/monte_carlo/monte_carlo_etop.py +100 -0
- npl-1.0.0/npl/monte_carlo/monte_carlo_global_features.py +87 -0
- npl-1.0.0/npl/monte_carlo/random_exchange_operator.py +77 -0
- npl-1.0.0/npl/monte_carlo/random_exchange_operator_etop.py +50 -0
- npl-1.0.0/npl/optimization/__init__.py +16 -0
- npl-1.0.0/npl/optimization/basin_hopping/__init__.py +1 -0
- npl-1.0.0/npl/optimization/basin_hopping/basin_hopping.py +77 -0
- npl-1.0.0/npl/optimization/genetic_algoritm/ConvexHull.py +75 -0
- npl-1.0.0/npl/optimization/genetic_algoritm/CutAndSpliceOperator.py +35 -0
- npl-1.0.0/npl/optimization/genetic_algoritm/ExchangeOperator.py +29 -0
- npl-1.0.0/npl/optimization/genetic_algoritm/LocalOptimizationOperator.py +17 -0
- npl-1.0.0/npl/optimization/genetic_algoritm/MutationOperator.py +39 -0
- npl-1.0.0/npl/optimization/genetic_algoritm/NichedPopulation.py +88 -0
- npl-1.0.0/npl/optimization/genetic_algoritm/SingleParticleGA.py +84 -0
- npl-1.0.0/npl/optimization/genetic_algoritm/__init__.py +0 -0
- npl-1.0.0/npl/optimization/go_search.py +196 -0
- npl-1.0.0/npl/optimization/local_optimization/__init__.py +2 -0
- npl-1.0.0/npl/optimization/local_optimization/garbage_exchange_operator.py +204 -0
- npl-1.0.0/npl/optimization/local_optimization/guided_exchange_operator.py +112 -0
- npl-1.0.0/npl/optimization/local_optimization/local_optimization.py +79 -0
- npl-1.0.0/npl/utils/__init__.py +0 -0
- npl-1.0.0/npl/utils/cutting_plane_utilities.py +69 -0
- npl-1.0.0/npl/utils/math_modules.py +58 -0
- npl-1.0.0/npl/utils/utils.py +96 -0
- npl-1.0.0/npl.egg-info/PKG-INFO +14 -0
- npl-1.0.0/npl.egg-info/SOURCES.txt +54 -0
- npl-1.0.0/npl.egg-info/dependency_links.txt +1 -0
- npl-1.0.0/npl.egg-info/requires.txt +4 -0
- npl-1.0.0/npl.egg-info/top_level.txt +1 -0
- npl-1.0.0/setup.cfg +4 -0
- npl-1.0.0/setup.py +19 -0
- npl-1.0.0/test/test_core.py +21 -0
npl-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: npl
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Nanoparticle Library for computational analysis of nanoparticles
|
|
5
|
+
Home-page:
|
|
6
|
+
Author: Riccardo Farris
|
|
7
|
+
Author-email: rfarris@ub.edu
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: acat>=1.7.1
|
|
10
|
+
Requires-Dist: scikit_learn>=1.5.0
|
|
11
|
+
Requires-Dist: scipy>=1.10.0
|
|
12
|
+
Requires-Dist: sortedcontainers>=2.4.0
|
|
13
|
+
|
|
14
|
+
This is a long description for the Nanoparticle Library.
|
npl-1.0.0/README.md
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
# <span style="font-size:larger;">NanoParticleLibrary (NPL)</span>
|
|
2
|
+
|
|
3
|
+
<!-- [](https://GitHub.com/yourusername/npl/releases/) -->
|
|
4
|
+
[](https://opensource.org/licenses/mit)
|
|
5
|
+
[](https://GitHub.com/farrisric/NPlib/issues)
|
|
6
|
+
[](https://nplib.readthedocs.io/en/latest/modules.html)
|
|
7
|
+
|
|
8
|
+
## Table of contents
|
|
9
|
+
|
|
10
|
+
- [NanoParticleLibrary (NPL)](#nanoparticlelibrary-npl)
|
|
11
|
+
- [Table of contents](#table-of-contents)
|
|
12
|
+
- [About NPL](#about-npl)
|
|
13
|
+
- [Documentation](#documentation)
|
|
14
|
+
- [Installation](#installation)
|
|
15
|
+
- [Requirements](#requirements)
|
|
16
|
+
- [Installation from PyPI](#installation-from-pypi)
|
|
17
|
+
- [Installation from source](#installation-from-source)
|
|
18
|
+
- [Usage](#usage)
|
|
19
|
+
- [Local Optimization](#local-optimization)
|
|
20
|
+
- [Basin-Hopping](#basin-hopping)
|
|
21
|
+
- [Examples](#examples)
|
|
22
|
+
- [Development](#development)
|
|
23
|
+
- [References](#references)
|
|
24
|
+
- [Contact](#contact)
|
|
25
|
+
- [License](#license)
|
|
26
|
+
|
|
27
|
+
## About NPL
|
|
28
|
+
|
|
29
|
+
NPL (NanoParticleLibrary) is a comprehensive wrapper around the popular ASE (Atomic Simulation Environment) library, designed to facilitate the manipulation and optimization of nanoparticles. It is particularly tailored for optimizing the chemical ordering in bimetallic nanoparticles.
|
|
30
|
+
|
|
31
|
+
## Documentation
|
|
32
|
+
|
|
33
|
+
A partial documentation is available at: https://nplib.readthedocs.io/en/latest/
|
|
34
|
+
|
|
35
|
+
## Installation
|
|
36
|
+
|
|
37
|
+
### Requirements
|
|
38
|
+
|
|
39
|
+
- Python 3.7+
|
|
40
|
+
- Atomic Simulation Environment (ASE) >= 3.21
|
|
41
|
+
- scikit-learn
|
|
42
|
+
- sortedcontainers
|
|
43
|
+
|
|
44
|
+
### Installation from PyPI
|
|
45
|
+
|
|
46
|
+
This is the recommended way to install NPL.
|
|
47
|
+
|
|
48
|
+
```sh
|
|
49
|
+
git clone https://github.com/farrisric/NPlib
|
|
50
|
+
pip install ./NPlib
|
|
51
|
+
```
|
|
52
|
+
## References
|
|
53
|
+
|
|
54
|
+
If you use this code, please cite our papers:
|
|
55
|
+
|
|
56
|
+
```bibtex
|
|
57
|
+
@neuman{10.1063/5.0214377,
|
|
58
|
+
author = {Felix Neumann and Johannes T Margraf and Karsten Reuter and Albert Bruix},
|
|
59
|
+
title = "{Interplay between shape and composition in bimetallic nanoparticles
|
|
60
|
+
revealed by an efficient optimal-exchange optimization algorithm}",
|
|
61
|
+
archivePrefix = {ChemRxiv},
|
|
62
|
+
doi = {10.26434/chemrxiv-2021-26ztp},
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@article{10.1063/5.0193848,
|
|
66
|
+
author = {Farris, Riccardo and Merinov, Boris V. and Bruix, Albert and Neyman, Konstantin M.},
|
|
67
|
+
title = "{Effects of Zr dopants on properties of PtNi nanoparticles for ORR catalysis: A DFT modeling}",
|
|
68
|
+
journal = {The Journal of Chemical Physics},
|
|
69
|
+
volume = {160},
|
|
70
|
+
number = {12},
|
|
71
|
+
pages = {124706},
|
|
72
|
+
year = {2024},
|
|
73
|
+
issn = {0021-9606},
|
|
74
|
+
doi = {10.1063/5.0193848},
|
|
75
|
+
url = {https://doi.org/10.1063/5.0193848},
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@farris{10.1063/5.0214377,
|
|
79
|
+
author = {Farris, Riccardo and Neyman, Konstantin M. and Bruix, Albert},
|
|
80
|
+
title = "{Determining the chemical ordering in nanoalloys by considering atomic coordination types}",
|
|
81
|
+
journal = {The Journal of Chemical Physics},
|
|
82
|
+
volume = {161},
|
|
83
|
+
number = {13},
|
|
84
|
+
pages = {134114},
|
|
85
|
+
year = {2024},
|
|
86
|
+
issn = {0021-9606},
|
|
87
|
+
doi = {10.1063/5.0214377}
|
|
88
|
+
}
|
|
89
|
+
```
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .energy_calculator import (EMTCalculator,
|
|
2
|
+
BayesianRRCalculator,
|
|
3
|
+
EnergyCalculator,
|
|
4
|
+
compute_coefficients_for_linear_topological_model)
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"EMTCalculator",
|
|
8
|
+
"BayesianRRCalculator",
|
|
9
|
+
"compute_coefficients_for_linear_topological_model"
|
|
10
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
from ase.optimize import BFGS
|
|
2
|
+
from ase.calculators.emt import EMT
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import copy
|
|
6
|
+
import pickle
|
|
7
|
+
import sklearn.gaussian_process as gp
|
|
8
|
+
from sklearn.linear_model import BayesianRidge
|
|
9
|
+
from sklearn.metrics import mean_absolute_error, root_mean_squared_error
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EnergyCalculator:
|
|
13
|
+
"""Base class for an energy calculator.
|
|
14
|
+
|
|
15
|
+
Valid implementations have to implement the compute_energy(particle) function.
|
|
16
|
+
Energies are saved in the particle object with the key of the respective calculator.
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.energy_key = None
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def compute_energy(self, particle):
|
|
23
|
+
raise NotImplementedError
|
|
24
|
+
|
|
25
|
+
def get_energy_key(self):
|
|
26
|
+
return copy.deepcopy(self.energy_key)
|
|
27
|
+
|
|
28
|
+
def set_energy_key(self, energy_key):
|
|
29
|
+
self.energy_key = energy_key
|
|
30
|
+
|
|
31
|
+
def save(self, name_file : str):
|
|
32
|
+
with open(name_file, 'wb') as out:
|
|
33
|
+
pickle.dump(self, out)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def load(name_file):
|
|
37
|
+
with open(name_file, 'rb') as calc:
|
|
38
|
+
return pickle.load(calc)
|
|
39
|
+
|
|
40
|
+
class EMTCalculator(EnergyCalculator):
|
|
41
|
+
"""Wrapper class around the asap3 EMT calculator."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, fmax=0.01, steps=50, relax_atoms=False):
|
|
44
|
+
EnergyCalculator.__init__(self)
|
|
45
|
+
self.fmax = fmax
|
|
46
|
+
self.steps = steps
|
|
47
|
+
self.energy_key = 'EMT'
|
|
48
|
+
self.relax_atoms = relax_atoms
|
|
49
|
+
|
|
50
|
+
def compute_energy(self, particle):
|
|
51
|
+
"""Compute the energy using EMT.
|
|
52
|
+
|
|
53
|
+
BFGS is used for relaxation. By default, the atoms are NOT relaxed, i.e. the
|
|
54
|
+
geometry remains unchanged unless this is explicitly stated.
|
|
55
|
+
|
|
56
|
+
Parameters:
|
|
57
|
+
particle : Nanoparticle
|
|
58
|
+
relax_atoms : bool
|
|
59
|
+
"""
|
|
60
|
+
cell_width = 1e3
|
|
61
|
+
cell_height = 1e3
|
|
62
|
+
cell_length = 1e3
|
|
63
|
+
|
|
64
|
+
atoms = particle.get_ase_atoms(exclude_x=True)
|
|
65
|
+
if not self.relax_atoms:
|
|
66
|
+
atoms = atoms.copy()
|
|
67
|
+
|
|
68
|
+
#atoms.set_cell(np.array([[cell_width, 0, 0], [0, cell_length, 0], [0, 0, cell_height]]))
|
|
69
|
+
atoms.set_calculator(EMT())
|
|
70
|
+
dyn = BFGS(atoms, logfile=None)
|
|
71
|
+
dyn.run(fmax=self.fmax, steps=self.steps)
|
|
72
|
+
|
|
73
|
+
energy = atoms.get_potential_energy()
|
|
74
|
+
particle.set_energy(self.energy_key, energy)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class GPRCalculator(EnergyCalculator):
|
|
78
|
+
"""Energy calculator using global feature vectors and Gaussian Process Regression."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, feature_key, kernel=None, alpha=0.01, normalize_y=True):
|
|
81
|
+
EnergyCalculator.__init__(self)
|
|
82
|
+
if kernel is None:
|
|
83
|
+
self.kernel = gp.kernels.ConstantKernel(1., (1e-1, 1e3)) * gp.kernels.RBF(1., (1e-3, 1e3))
|
|
84
|
+
else:
|
|
85
|
+
self.kernel = kernel
|
|
86
|
+
|
|
87
|
+
self.alpha = alpha
|
|
88
|
+
self.normalize_y = normalize_y
|
|
89
|
+
self.GPR = None
|
|
90
|
+
self.energy_key = 'GPR'
|
|
91
|
+
self.feature_key = feature_key
|
|
92
|
+
|
|
93
|
+
def fit(self, training_set, energy_key):
|
|
94
|
+
"""Fit the GPR model.
|
|
95
|
+
|
|
96
|
+
The feature vectors with key = self.feature_key will be used for feature vectors. The
|
|
97
|
+
energy with the specified energy_key will be the target function.
|
|
98
|
+
|
|
99
|
+
Parameters:
|
|
100
|
+
training_set : list of Nanoparticles
|
|
101
|
+
energy_key : str
|
|
102
|
+
"""
|
|
103
|
+
feature_vectors = [p.get_feature_vector(self.feature_key) for p in training_set]
|
|
104
|
+
energies = [p.get_energy(energy_key) for p in training_set]
|
|
105
|
+
|
|
106
|
+
self.GPR = gp.GaussianProcessRegressor(kernel=self.kernel, n_restarts_optimizer=20, alpha=self.alpha,
|
|
107
|
+
normalize_y=self.normalize_y)
|
|
108
|
+
self.GPR.fit(feature_vectors, energies)
|
|
109
|
+
|
|
110
|
+
def compute_energy(self, particle):
|
|
111
|
+
"""Compute the energy using GPR.
|
|
112
|
+
|
|
113
|
+
Assumes that a feature vector with key=self.feature_key is present in the particle.
|
|
114
|
+
|
|
115
|
+
Parameters:
|
|
116
|
+
particle : Nanoparticle
|
|
117
|
+
"""
|
|
118
|
+
energy = self.GPR.predict([particle.get_feature_vector(self.feature_key)])[0]
|
|
119
|
+
particle.set_energy(self.energy_key, energy)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class MixingEnergyCalculator(EnergyCalculator):
|
|
123
|
+
"""Compute the mixing energy using an arbitrary energy model.
|
|
124
|
+
|
|
125
|
+
For the original energy model it is assumed that all previous steps in the energy pipeline, e.g. calculation
|
|
126
|
+
of local environment, feature vectors etc. has been carried out.
|
|
127
|
+
"""
|
|
128
|
+
def __init__(self, base_calculator=None, mixing_parameters=None, recompute_energies=False):
|
|
129
|
+
EnergyCalculator.__init__(self)
|
|
130
|
+
|
|
131
|
+
if mixing_parameters is None:
|
|
132
|
+
self.mixing_parameters = dict()
|
|
133
|
+
else:
|
|
134
|
+
self.mixing_parameters = mixing_parameters
|
|
135
|
+
|
|
136
|
+
self.base_calculator = base_calculator
|
|
137
|
+
self.recompute_energies = recompute_energies
|
|
138
|
+
self.energy_key = 'Mixing Energy'
|
|
139
|
+
|
|
140
|
+
def compute_mixing_parameters(self, particle, symbols):
|
|
141
|
+
"""Compute the energies for the pure particles of the given symbols as reference points.
|
|
142
|
+
|
|
143
|
+
Parameters:
|
|
144
|
+
particle : Nanoparticle
|
|
145
|
+
symbols : list of str
|
|
146
|
+
"""
|
|
147
|
+
for symbol in symbols:
|
|
148
|
+
particle.random_ordering({symbol: 1.0})
|
|
149
|
+
self.base_calculator.compute_energy(particle)
|
|
150
|
+
self.mixing_parameters[symbol] = particle.get_energy('EMT')
|
|
151
|
+
|
|
152
|
+
def compute_energy(self, particle):
|
|
153
|
+
"""Compute the mixing energy of the particle using the base energy model.
|
|
154
|
+
|
|
155
|
+
If energies have been computed using the same energy model as the base calculator they are reused if
|
|
156
|
+
self.recompute_energies == False
|
|
157
|
+
|
|
158
|
+
Parameters:
|
|
159
|
+
particle : Nanoparticle
|
|
160
|
+
"""
|
|
161
|
+
if self.recompute_energies:
|
|
162
|
+
self.base_calculator.compute_energy(particle)
|
|
163
|
+
|
|
164
|
+
energy_key = self.base_calculator.get_energy_key()
|
|
165
|
+
mixing_energy = particle.get_energy(energy_key)
|
|
166
|
+
|
|
167
|
+
n_atoms = particle.atoms.get_n_atoms()
|
|
168
|
+
|
|
169
|
+
for symbol in particle.get_stoichiometry():
|
|
170
|
+
mixing_energy -= self.mixing_parameters[symbol] * particle.get_stoichiometry()[symbol] / n_atoms
|
|
171
|
+
|
|
172
|
+
particle.set_energy(self.energy_key, mixing_energy)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class BayesianRRCalculator(EnergyCalculator):
|
|
176
|
+
"""Energy calculator using global feature vectors and Bayesian Ridge Regression."""
|
|
177
|
+
|
|
178
|
+
def __init__(self, feature_key):
|
|
179
|
+
EnergyCalculator.__init__(self)
|
|
180
|
+
|
|
181
|
+
self.ridge = BayesianRidge(fit_intercept=False)
|
|
182
|
+
|
|
183
|
+
self.energy_key = 'BRR'
|
|
184
|
+
self.feature_key = feature_key
|
|
185
|
+
|
|
186
|
+
def fit(self, training_set, energy_key, validation_set=None):
|
|
187
|
+
"""Fit the Bayesian Ridge Regression (BRR) model.
|
|
188
|
+
|
|
189
|
+
Parameters:
|
|
190
|
+
----------
|
|
191
|
+
training_set : list of Nanoparticles
|
|
192
|
+
The dataset used for training the model.
|
|
193
|
+
energy_key : str
|
|
194
|
+
The key used to extract energy values from the nanoparticles.
|
|
195
|
+
validation_set : float or list of Nanoparticles, optional
|
|
196
|
+
If a float is provided, it represents the fraction of the training set to be used as the validation set.
|
|
197
|
+
If a list is provided, it is used as the validation set directly. Default is None.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
-------
|
|
201
|
+
None
|
|
202
|
+
"""
|
|
203
|
+
if isinstance(validation_set, float):
|
|
204
|
+
split_index = int(len(training_set) * validation_set)
|
|
205
|
+
validation_set = training_set[split_index:]
|
|
206
|
+
training_set = training_set[:split_index]
|
|
207
|
+
|
|
208
|
+
feature_vectors = [p.get_feature_vector(self.feature_key) for p in training_set]
|
|
209
|
+
energies = [p.get_energy(energy_key) for p in training_set]
|
|
210
|
+
|
|
211
|
+
self.ridge.fit(feature_vectors, energies)
|
|
212
|
+
|
|
213
|
+
if validation_set:
|
|
214
|
+
self.validate(validation_set, energy_key)
|
|
215
|
+
|
|
216
|
+
def validate(self, validation_set, energy_key):
|
|
217
|
+
"""Validate the Bayesian Ridge Regression (BRR) model.
|
|
218
|
+
|
|
219
|
+
Parameters:
|
|
220
|
+
----------
|
|
221
|
+
validation_set : list of Nanoparticles
|
|
222
|
+
The dataset used for validating the model.
|
|
223
|
+
energy_key : str
|
|
224
|
+
The key used to extract energy values from the nanoparticles.
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
-------
|
|
228
|
+
None
|
|
229
|
+
"""
|
|
230
|
+
pred_validation = [self.compute_energy(p) for p in validation_set]
|
|
231
|
+
true_validation = [p.get_energy(energy_key) for p in validation_set]
|
|
232
|
+
print('Mean Absolute error {:.4f} eV'.format(mean_absolute_error(true_validation, pred_validation)))
|
|
233
|
+
print('Root Mean Square error {:.4f} eV'.format(root_mean_squared_error(true_validation, pred_validation)))
|
|
234
|
+
|
|
235
|
+
def get_coefficients(self):
|
|
236
|
+
return self.ridge.coef_
|
|
237
|
+
|
|
238
|
+
def set_coefficients(self, new_coefficients):
|
|
239
|
+
self.ridge.coef_ = new_coefficients
|
|
240
|
+
|
|
241
|
+
def set_feature_key(self, feature_key):
|
|
242
|
+
self.feature_key = feature_key
|
|
243
|
+
|
|
244
|
+
def compute_energy(self, particle):
|
|
245
|
+
"""Compute the energy using BRR.
|
|
246
|
+
|
|
247
|
+
Assumes that a feature vector with key=self.feature_key is present in the particle.
|
|
248
|
+
|
|
249
|
+
Parameters:
|
|
250
|
+
particle : Nanoparticle
|
|
251
|
+
"""
|
|
252
|
+
#brr_energy = self.ridge.predict([particle.get_feature_vector(self.feature_key)])
|
|
253
|
+
#brr_energy = np.dot(np.transpose(self.ridge.coef_), particle.get_feature_vector(self.feature_key))
|
|
254
|
+
brr_energy = np.dot(np.transpose(self.ridge.coef_), particle.get_feature_vector(self.feature_key))
|
|
255
|
+
particle.set_energy(self.energy_key, brr_energy)
|
|
256
|
+
return brr_energy
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class DipoleMomentCalculator:
|
|
260
|
+
|
|
261
|
+
def __init__(self):
|
|
262
|
+
self.total_dipole_moment = None
|
|
263
|
+
self.dipole_moments = None
|
|
264
|
+
self.environments = None
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def compute_dipole_moment(self, particle, charges = [1, -1]):
|
|
268
|
+
|
|
269
|
+
symbols = particle.get_all_symbols()
|
|
270
|
+
fake_charges = {symbols[0] : charges[0], symbols[1] : charges[1]}
|
|
271
|
+
partial_charges = [fake_charges[symbol] for symbol in particle.get_symbols()]
|
|
272
|
+
|
|
273
|
+
dipole_moments = []
|
|
274
|
+
environments = []
|
|
275
|
+
for central_atom_idx in particle.get_atom_indices_from_coordination_number([12]):
|
|
276
|
+
particle.translate_atoms_positions(particle.get_position(central_atom_idx))
|
|
277
|
+
dipole_moment = 0
|
|
278
|
+
for atom_idx in particle.get_coordination_atoms(central_atom_idx):
|
|
279
|
+
dipole_moment += partial_charges[atom_idx] * particle.get_position(atom_idx)
|
|
280
|
+
|
|
281
|
+
dipole_moments.append(np.linalg.norm(dipole_moment))
|
|
282
|
+
environments.append(particle.get_coordination_atoms(central_atom_idx))
|
|
283
|
+
|
|
284
|
+
self.total_dipole_moment = np.average(dipole_moments)/particle.get_n_atoms()
|
|
285
|
+
self.dipole_moments = dipole_moments
|
|
286
|
+
self.environments = environments
|
|
287
|
+
|
|
288
|
+
def get_total_dipole_moment(self):
|
|
289
|
+
return self.total_dipole_moment
|
|
290
|
+
|
|
291
|
+
def get_dipole_moments(self):
|
|
292
|
+
return self.dipole_moments
|
|
293
|
+
|
|
294
|
+
def get_environments(self):
|
|
295
|
+
return self.environments
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
class LateralInteractionCalculator:
|
|
299
|
+
|
|
300
|
+
def __init__(self):
|
|
301
|
+
EnergyCalculator.__init__(self)
|
|
302
|
+
self.interaction_matrix = None
|
|
303
|
+
self.energy_key = 'Lateral Interaction'
|
|
304
|
+
self.a = 6
|
|
305
|
+
|
|
306
|
+
def construct_interatomic_potential_matrix(self, particle):
|
|
307
|
+
def construct_adsorbate_grid(particle):
|
|
308
|
+
from npl.core.adsorption import PlaceAddAtoms
|
|
309
|
+
particle.construct_adsorption_list()
|
|
310
|
+
n_sites = particle.get_total_number_of_sites()
|
|
311
|
+
ads_site_list = particle.get_adsorption_list()
|
|
312
|
+
ads_placer = PlaceAddAtoms(particle.get_all_symbols())
|
|
313
|
+
ads_placer.bind_particle(particle)
|
|
314
|
+
adsorbate_positions = [list(ads_site_list[site]) for site in range(n_sites)]
|
|
315
|
+
particle = ads_placer.place_add_atom(particle, 'O', adsorbate_positions)
|
|
316
|
+
return particle
|
|
317
|
+
|
|
318
|
+
def get_adsorbate_distance_matrix(particle, n_atoms_np):
|
|
319
|
+
ase_atoms = particle.get_ase_atoms()
|
|
320
|
+
adsorbate_all_distances = ase_atoms.get_all_distances()[n_atoms_np:]
|
|
321
|
+
distance_matrix = np.array([row[n_atoms_np:] for row in adsorbate_all_distances])
|
|
322
|
+
return distance_matrix
|
|
323
|
+
|
|
324
|
+
n_atoms_np = particle.get_n_atoms()
|
|
325
|
+
particle = construct_adsorbate_grid(particle)
|
|
326
|
+
distance_matrix = get_adsorbate_distance_matrix(particle, n_atoms_np)
|
|
327
|
+
interaction_matrix = np.zeros(distance_matrix.shape)
|
|
328
|
+
interaction_matrix = self.a / distance_matrix**2
|
|
329
|
+
|
|
330
|
+
dimension = len(interaction_matrix)
|
|
331
|
+
for i in np.arange(dimension):
|
|
332
|
+
interaction_matrix[i][i] = 0
|
|
333
|
+
|
|
334
|
+
self.interaction_matrix = interaction_matrix
|
|
335
|
+
|
|
336
|
+
def bind_grid(self, particle):
|
|
337
|
+
particle_for_grid = copy.deepcopy(particle)
|
|
338
|
+
self.construct_interatomic_potential_matrix(particle_for_grid)
|
|
339
|
+
|
|
340
|
+
def compute_energy(self, particle):
|
|
341
|
+
lateral_interaction = 0
|
|
342
|
+
occupied_sites_indices = particle.get_occupation_status_by_indices(1)
|
|
343
|
+
for idx, site_index in enumerate(occupied_sites_indices):
|
|
344
|
+
for pair_index in occupied_sites_indices[idx:]:
|
|
345
|
+
lateral_interaction += self.interaction_matrix[site_index][pair_index]
|
|
346
|
+
|
|
347
|
+
particle.set_energy(self.energy_key, lateral_interaction)
|
|
348
|
+
|
|
349
|
+
# TODO move to relevant file -> Basin Hopping, Local optimization
|
|
350
|
+
# TODO remove scaling factors from topological descriptors
|
|
351
|
+
def compute_coefficients_for_linear_topological_model(global_topological_coefficients, symbols, n_atoms):
|
|
352
|
+
coordination_numbers = list(range(13))
|
|
353
|
+
symbols_copy = copy.deepcopy(symbols)
|
|
354
|
+
symbols_copy.sort()
|
|
355
|
+
symbol_a = symbols_copy[0]
|
|
356
|
+
print("Coef symbol_a: {}".format(symbol_a))
|
|
357
|
+
|
|
358
|
+
e_aa_bond = global_topological_coefficients[0]/n_atoms
|
|
359
|
+
e_bb_bond = global_topological_coefficients[1]/n_atoms
|
|
360
|
+
e_ab_bond = global_topological_coefficients[2]/n_atoms
|
|
361
|
+
|
|
362
|
+
coefficients = []
|
|
363
|
+
total_energies = []
|
|
364
|
+
for symbol in symbols_copy:
|
|
365
|
+
for cn_number in coordination_numbers:
|
|
366
|
+
for n_symbol_a_atoms in range(cn_number + 1):
|
|
367
|
+
energy = 0
|
|
368
|
+
|
|
369
|
+
if symbol == symbol_a:
|
|
370
|
+
energy += (global_topological_coefficients[3]*0.1) # careful...
|
|
371
|
+
energy += (n_symbol_a_atoms*e_aa_bond/2)
|
|
372
|
+
energy += ((cn_number - n_symbol_a_atoms)*e_ab_bond/2)
|
|
373
|
+
energy += (global_topological_coefficients[4 + cn_number])
|
|
374
|
+
|
|
375
|
+
total_energy = energy
|
|
376
|
+
total_energy += n_symbol_a_atoms*e_aa_bond/2
|
|
377
|
+
total_energy += (cn_number - n_symbol_a_atoms)*e_ab_bond/2
|
|
378
|
+
else:
|
|
379
|
+
energy += (n_symbol_a_atoms*e_ab_bond/2)
|
|
380
|
+
energy += ((cn_number - n_symbol_a_atoms)*e_bb_bond/2)
|
|
381
|
+
|
|
382
|
+
total_energy = energy
|
|
383
|
+
total_energy += n_symbol_a_atoms*e_ab_bond/2
|
|
384
|
+
total_energy += (cn_number - n_symbol_a_atoms)*e_bb_bond/2
|
|
385
|
+
|
|
386
|
+
coefficients.append(energy)
|
|
387
|
+
total_energies.append(total_energy)
|
|
388
|
+
|
|
389
|
+
coefficients = np.array(coefficients)
|
|
390
|
+
|
|
391
|
+
return coefficients, total_energies
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def compute_coefficients_for_shape_optimization(global_topological_coefficients, symbols):
|
|
395
|
+
coordination_numbers = list(range(13))
|
|
396
|
+
symbols_copy = copy.deepcopy(symbols)
|
|
397
|
+
symbols_copy.sort()
|
|
398
|
+
|
|
399
|
+
e_aa_bond = global_topological_coefficients[0]
|
|
400
|
+
|
|
401
|
+
coordination_energies_a = dict()
|
|
402
|
+
for index, cn in enumerate(coordination_numbers):
|
|
403
|
+
coordination_energies_a[cn] = global_topological_coefficients[index + 1]
|
|
404
|
+
|
|
405
|
+
coefficients = []
|
|
406
|
+
total_energies = []
|
|
407
|
+
for cn_number in coordination_numbers:
|
|
408
|
+
for n_symbol_a_atoms in range(cn_number + 1):
|
|
409
|
+
energy = 0
|
|
410
|
+
|
|
411
|
+
energy += (n_symbol_a_atoms*e_aa_bond/2)
|
|
412
|
+
energy += (coordination_energies_a[cn_number])
|
|
413
|
+
|
|
414
|
+
total_energy = energy + n_symbol_a_atoms*e_aa_bond/2
|
|
415
|
+
|
|
416
|
+
coefficients.append(energy)
|
|
417
|
+
total_energies.append(total_energy)
|
|
418
|
+
|
|
419
|
+
coefficients += [0]*len(coefficients)
|
|
420
|
+
total_energies += [0]*len(total_energies)
|
|
421
|
+
|
|
422
|
+
coefficients = np.array(coefficients)
|
|
423
|
+
|
|
424
|
+
return coefficients, total_energies
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
from ase import Atoms
|
|
5
|
+
from ase.optimize import BFGS
|
|
6
|
+
from ase.calculators.calculator import Calculator
|
|
7
|
+
import pickle
|
|
8
|
+
|
|
9
|
+
class TOPCalculator(Calculator):
|
|
10
|
+
"""
|
|
11
|
+
A class representing a calculator for performing relaxation calculations using the ASE library.
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
calculator (Calculator): The calculator object used for performing the calculations.
|
|
15
|
+
fmax (float): The maximum force tolerance for the relaxation.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
def __init__(self,
|
|
19
|
+
model_paths : Union[list, str],
|
|
20
|
+
feature_key : str,
|
|
21
|
+
**kwargs
|
|
22
|
+
):
|
|
23
|
+
Calculator.__init__(self, **kwargs)
|
|
24
|
+
|
|
25
|
+
self.feature_key = feature_key
|
|
26
|
+
self.model = self.load_model(model_paths)
|
|
27
|
+
|
|
28
|
+
def load_model(self, model_path):
|
|
29
|
+
with open(model_path, 'rb') as calc:
|
|
30
|
+
return pickle.load(calc)
|
|
31
|
+
|
|
32
|
+
def calculate(self, atoms):
|
|
33
|
+
self.results = {}
|
|
34
|
+
feature_vector = atoms.info[self.feature_key]
|
|
35
|
+
self.results['energy'] = self.model.predict(feature_vector)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# npl/core/__init__.py
|
|
2
|
+
|
|
3
|
+
from npl.core.base_nanoparticle import BaseNanoparticle
|
|
4
|
+
from npl.core.nanoparticle import Nanoparticle
|
|
5
|
+
from npl.core.neighbor_list import NeighborList
|
|
6
|
+
from npl.core.adsorption import FindAdsorptionSites
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"BaseNanoparticle",
|
|
10
|
+
"Nanoparticle",
|
|
11
|
+
"NeighborList",
|
|
12
|
+
"FindAdsorptionSites"
|
|
13
|
+
]
|