Skip to content

Commit 7a2111a

Browse files
committed
Active-learning: new module and enhanced installation options
1 parent 7dc7654 commit 7a2111a

5 files changed

Lines changed: 275 additions & 2 deletions

File tree

Modules/Ensemble.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4265,7 +4265,7 @@ def set_otf(
42654265
update_threshold: float | None = None,
42664266
# other args
42674267
build_mode="bayesian",
4268-
train_hyps: tuple = (100,120),
4268+
train_hyps: tuple = (1,np.inf),
42694269
):
42704270
"""Set on-the-fly training.
42714271

Modules/ml/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Module for machine learning functionalities."""

Modules/ml/flare.py

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
"""Utility functions for setting up FLARE models."""
2+
from __future__ import annotations
3+
4+
import sys, json, yaml
5+
import numpy as np
6+
from ase.symbols import symbols2numbers
7+
8+
9+
def get_flare_calc(flare_config):
10+
"""Set up ASE flare calculator."""
11+
gp_name = flare_config.get("gp")
12+
if gp_name == "GaussianProcess":
13+
return get_gp_calc(flare_config)
14+
elif gp_name == "SGP_Wrapper":
15+
return get_sgp_calc(flare_config)
16+
else:
17+
raise NotImplementedError(f"{gp_name} is not implemented")
18+
19+
20+
def get_gp_calc(flare_config):
21+
"""Return a FLARE_Calculator with gp from GaussianProcess."""
22+
from flare.bffs.gp import GaussianProcess
23+
from flare.bffs.mgp import MappedGaussianProcess
24+
from flare.bffs.gp.calculator import FLARE_Calculator
25+
from flare.utils.parameter_helper import ParameterHelper
26+
27+
gp_file = flare_config.get("file", None)
28+
29+
# Load GP from file
30+
if gp_file is not None:
31+
with open(gp_file, "r") as f:
32+
gp_dct = json.loads(f.readline())
33+
if gp_dct.get("class", None) == "FLARE_Calculator":
34+
flare_calc = FLARE_Calculator.from_file(gp_file)
35+
else:
36+
gp, _ = GaussianProcess.from_file(gp_file)
37+
flare_calc = FLARE_Calculator(gp)
38+
return flare_calc
39+
40+
# Create gaussian process model
41+
kernels = flare_config.get("kernels")
42+
hyps = flare_config.get("hyps", "random")
43+
opt_algorithm = flare_config.get("opt_algorithm", "BFGS")
44+
max_iterations = flare_config.get("max_iterations", 20)
45+
bounds = flare_config.get("bounds", None)
46+
47+
gp_parameters = flare_config.get("gp_parameters")
48+
n_cpus = flare_config.get("n_cpus", 1)
49+
use_mapping = flare_config.get("use_mapping", False)
50+
51+
# set up GP hyperparameters
52+
pm = ParameterHelper(
53+
kernels=kernels,
54+
random=True,
55+
parameters=gp_parameters,
56+
)
57+
hm = pm.as_dict()
58+
if hyps == "random":
59+
hyps = hm["hyps"]
60+
61+
gp_model = GaussianProcess(
62+
kernels=kernels,
63+
component="mc",
64+
hyps=hyps,
65+
cutoffs=hm["cutoffs"],
66+
hyps_mask=None,
67+
hyp_labels=hm["hyp_labels"],
68+
opt_algorithm=opt_algorithm,
69+
maxiter=max_iterations,
70+
parallel=n_cpus > 1,
71+
per_atom_par=flare_config.get("per_atom_par", True),
72+
n_cpus=n_cpus,
73+
n_sample=flare_config.get("n_sample", 100),
74+
output=None,
75+
name=flare_config.get("name", "default_gp"),
76+
energy_noise=flare_config.get("energy_noise", 0.01),
77+
)
78+
79+
# create mapped gaussian process
80+
if use_mapping:
81+
grid_params = flare_config.get("grid_params")
82+
var_map = flare_config.get("var_map", "pca")
83+
unique_species = flare_config.get("unique_species")
84+
coded_unique_species = symbols2numbers(unique_species)
85+
mgp_model = MappedGaussianProcess(
86+
grid_params=grid_params,
87+
unique_species=coded_unique_species,
88+
n_cpus=n_cpus,
89+
var_map=var_map,
90+
)
91+
else:
92+
mgp_model = None
93+
94+
flare_calc = FLARE_Calculator(
95+
gp_model=gp_model,
96+
mgp_model=mgp_model,
97+
par=n_cpus > 1,
98+
use_mapping=use_mapping,
99+
)
100+
return flare_calc, kernels
101+
102+
103+
def get_sgp_calc(flare_config):
104+
"""Return a SGP_Calculator with sgp from SparseGP."""
105+
from flare.bffs.sgp._C_flare import NormalizedDotProduct, SquaredExponential
106+
from flare.bffs.sgp._C_flare import B2, B3, TwoBody, ThreeBody, FourBody
107+
from flare.bffs.sgp import SGP_Wrapper
108+
from flare.bffs.sgp.calculator import SGP_Calculator
109+
110+
sgp_file = flare_config.get("file", None)
111+
112+
# Load sparse GP from file
113+
if sgp_file is not None:
114+
with open(sgp_file, "r") as f:
115+
gp_dct = json.loads(f.readline())
116+
if gp_dct.get("class", None) == "SGP_Calculator":
117+
flare_calc, kernels = SGP_Calculator.from_file(sgp_file)
118+
else:
119+
sgp, kernels = SGP_Wrapper.from_file(sgp_file)
120+
flare_calc = SGP_Calculator(sgp)
121+
return flare_calc, kernels
122+
123+
kernels = flare_config.get("kernels")
124+
opt_algorithm = flare_config.get("opt_algorithm", "BFGS")
125+
max_iterations = flare_config.get("max_iterations", 20)
126+
bounds = flare_config.get("bounds", None)
127+
use_mapping = flare_config.get("use_mapping", False)
128+
129+
# Define kernels.
130+
kernels = []
131+
for k in flare_config["kernels"]:
132+
if k["name"] == "NormalizedDotProduct":
133+
kernels.append(NormalizedDotProduct(k["sigma"], k["power"]))
134+
elif k["name"] == "SquaredExponential":
135+
kernels.append(SquaredExponential(k["sigma"], k["ls"]))
136+
else:
137+
raise NotImplementedError(f"{k['name']} kernel is not implemented")
138+
139+
# Define descriptor calculators.
140+
n_species = len(flare_config["species"])
141+
cutoff = flare_config["cutoff"]
142+
descriptors = []
143+
for d in flare_config["descriptors"]:
144+
if "cutoff_matrix" in d: # multiple cutoffs
145+
assert np.allclose(np.array(d["cutoff_matrix"]).shape, (n_species, n_species)),\
146+
"cutoff_matrix needs to be of shape (n_species, n_species)"
147+
148+
if d["name"] == "B2":
149+
radial_hyps = [0.0, cutoff]
150+
cutoff_hyps = []
151+
descriptor_settings = [n_species, d["nmax"], d["lmax"]]
152+
if "cutoff_matrix" in d: # multiple cutoffs
153+
desc_calc = B2(
154+
d["radial_basis"],
155+
d["cutoff_function"],
156+
radial_hyps,
157+
cutoff_hyps,
158+
descriptor_settings,
159+
d["cutoff_matrix"],
160+
)
161+
else:
162+
desc_calc = B2(
163+
d["radial_basis"],
164+
d["cutoff_function"],
165+
radial_hyps,
166+
cutoff_hyps,
167+
descriptor_settings,
168+
)
169+
170+
elif d["name"] == "B3":
171+
radial_hyps = [0.0, cutoff]
172+
cutoff_hyps = []
173+
descriptor_settings = [n_species, d["nmax"], d["lmax"]]
174+
desc_calc = B3(
175+
d["radial_basis"],
176+
d["cutoff_function"],
177+
radial_hyps,
178+
cutoff_hyps,
179+
descriptor_settings,
180+
)
181+
182+
elif d["name"] == "TwoBody":
183+
desc_calc = TwoBody(cutoff, n_species, d["cutoff_function"], cutoff_hyps)
184+
185+
elif d["name"] == "ThreeBody":
186+
desc_calc = ThreeBody(cutoff, n_species, d["cutoff_function"], cutoff_hyps)
187+
188+
elif d["name"] == "FourBody":
189+
desc_calc = FourBody(cutoff, n_species, d["cutoff_function"], cutoff_hyps)
190+
191+
else:
192+
raise NotImplementedError(f"{d['name']} descriptor is not supported")
193+
194+
descriptors.append(desc_calc)
195+
196+
# Define remaining parameters for the SGP wrapper.
197+
species_map = {flare_config.get("species")[i]: i for i in range(n_species)}
198+
sae_dct = flare_config.get("single_atom_energies", None)
199+
if sae_dct is not None:
200+
assert n_species == len(
201+
sae_dct
202+
), "'single_atom_energies' should be the same length as 'species'"
203+
single_atom_energies = {i: sae_dct[i] for i in range(n_species)}
204+
else:
205+
single_atom_energies = {i: 0 for i in range(n_species)}
206+
207+
sgp = SGP_Wrapper(
208+
kernels=kernels,
209+
descriptor_calculators=descriptors,
210+
cutoff=cutoff,
211+
sigma_e=flare_config.get("energy_noise"),
212+
sigma_f=flare_config.get("forces_noise"),
213+
sigma_s=flare_config.get("stress_noise"),
214+
species_map=species_map,
215+
variance_type=flare_config.get("variance_type", "local"),
216+
single_atom_energies=single_atom_energies,
217+
energy_training=flare_config.get("energy_training", True),
218+
force_training=flare_config.get("force_training", True),
219+
stress_training=flare_config.get("stress_training", True),
220+
max_iterations=max_iterations,
221+
opt_method=opt_algorithm,
222+
bounds=bounds,
223+
)
224+
225+
flare_calc = SGP_Calculator(sgp, use_mapping)
226+
return flare_calc, kernels
227+
228+
229+
def get_model(file: str):
230+
"""Main method of the script."""
231+
with open(file, "r") as f:
232+
config = yaml.safe_load(f)
233+
234+
return get_flare_calc(config)
235+
236+
237+
if __name__ == "__main__":
238+
"""Launch script from cmd."""
239+
model, kernels = get_model(sys.argv[1])
240+
print(model, kernels)

meson.build

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,11 +207,19 @@ py.install_sources([
207207
'Modules/Relax.py',
208208
'Modules/SchaMinimizer.py',
209209
'Modules/Tools.py',
210-
'Modules/Utilities.py'
210+
'Modules/Utilities.py',
211211
],
212212
subdir: 'sscha',
213213
)
214214

215+
py.install_sources(
216+
[
217+
'Modules/ml/__init__.py',
218+
'Modules/ml/flare.py',
219+
],
220+
subdir: 'sscha/ml',
221+
)
222+
215223
# --- Installing Scripts ---
216224
# Meson is great for installing scripts and making them executable.
217225
# Scripts will be installed in the `bin` directory of the Python environment (e.g., venv/bin/).

pyproject.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,30 @@ Repository = "https://github.com/SSCHAcode/python-sscha"
2929
# Documentation = "https://documentacion.readthedocs.io/"
3030
Issues = "https://github.com/SSCHAcode/python-sscha/issues"
3131

32+
[project.optional-dependencies]
33+
pre-commit = [
34+
'pre-commit~=2.17',
35+
'pylint==2.13.7',
36+
'toml',
37+
]
38+
tests = [
39+
'pgtest~=1.3',
40+
'pytest~=6.0',
41+
'pytest-regressions~=2.3',
42+
'pytest-timeout',
43+
]
44+
aiida = [
45+
'aiida-core~=2.3',
46+
'aiida-quantumespresso~=4.10',
47+
'aiida-pseudo',
48+
]
49+
active-learning = [
50+
'mir-flare~=1.4',
51+
'aiida-core~=2.3',
52+
'aiida-quantumespresso~=4.10',
53+
'aiida-pseudo',
54+
]
55+
3256
# --- Meson-python specific configuration (Optional but useful) ---
3357
[tool.meson-python]
3458
# Here you can pass options to Meson that control the build process.

0 commit comments

Comments
 (0)