firefly-activemd 0.3.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.
- firefly_activemd-0.3.0/PKG-INFO +54 -0
- firefly_activemd-0.3.0/README.md +43 -0
- firefly_activemd-0.3.0/firefly/__init__.py +2 -0
- firefly_activemd-0.3.0/firefly/cli.py +127 -0
- firefly_activemd-0.3.0/firefly/simulations/__init__.py +0 -0
- firefly_activemd-0.3.0/firefly/simulations/ensemble.py +265 -0
- firefly_activemd-0.3.0/firefly/utils/DFTCodes.py +111 -0
- firefly_activemd-0.3.0/firefly/utils/MaceUtils.py +138 -0
- firefly_activemd-0.3.0/firefly/utils/ThreshComp.py +97 -0
- firefly_activemd-0.3.0/firefly/utils/__init__.py +0 -0
- firefly_activemd-0.3.0/firefly_activemd.egg-info/PKG-INFO +54 -0
- firefly_activemd-0.3.0/firefly_activemd.egg-info/SOURCES.txt +16 -0
- firefly_activemd-0.3.0/firefly_activemd.egg-info/dependency_links.txt +1 -0
- firefly_activemd-0.3.0/firefly_activemd.egg-info/entry_points.txt +2 -0
- firefly_activemd-0.3.0/firefly_activemd.egg-info/requires.txt +4 -0
- firefly_activemd-0.3.0/firefly_activemd.egg-info/top_level.txt +1 -0
- firefly_activemd-0.3.0/pyproject.toml +23 -0
- firefly_activemd-0.3.0/setup.cfg +4 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: firefly-activemd
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Active Learning Molecular Dynamics with MACE potentials and DFT reference calculations
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: ase>=3.28
|
|
8
|
+
Requires-Dist: mace-torch>=0.3
|
|
9
|
+
Requires-Dist: numpy>=2
|
|
10
|
+
Requires-Dist: sisl>=0.16
|
|
11
|
+
|
|
12
|
+
# Firefly
|
|
13
|
+
|
|
14
|
+
Active Learning Molecular Dynamics with MACE potentials and DFT reference calculations (SIESTA or Quantum ESPRESSO).
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or in editable mode (for development):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install -e .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
firefly parameters.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Parameters (parameters.json)
|
|
35
|
+
|
|
36
|
+
| Key | Required | Description |
|
|
37
|
+
|-----|----------|-------------|
|
|
38
|
+
| `file` | yes | Path to the atomic structure file (.xyz, etc.) |
|
|
39
|
+
| `ensemble` | yes | MD ensemble: `iso`, `aniso`, `nptz`, `nptxy`, `bussi`, `ortho` |
|
|
40
|
+
| `code` | yes | DFT code: `siesta` or `qe` |
|
|
41
|
+
| `cmd` | yes | Command to run the DFT code |
|
|
42
|
+
| `thresh` | yes | Uncertainty threshold for active learning |
|
|
43
|
+
| `n_models` | yes | Number of MACE models in the committee |
|
|
44
|
+
| `atoms` | siesta only | Dict mapping element symbols to atomic numbers |
|
|
45
|
+
| `gpus` | no | Number of GPUs (default: 1) |
|
|
46
|
+
| `T_begin` | no | Initial temperature in K (default: 300) |
|
|
47
|
+
| `T_final` | no | Final temperature in K (default: 300) |
|
|
48
|
+
| `pressure` | no | Pressure in bar (default: 0) |
|
|
49
|
+
| `dt` | no | Time step in fs (default: 0.5) |
|
|
50
|
+
| `time` | no | Total simulation time in fs (default: 100000) |
|
|
51
|
+
| `patience` | no | Steps before forcing DFT call (default: 1000) |
|
|
52
|
+
| `active` | no | Uncertainty metric: `energy` or `force` (default: `energy`) |
|
|
53
|
+
| `epoch` | no | Training epochs per retraining step (default: 10) |
|
|
54
|
+
| `premodel` | no | Model name prefix (default: `model`) |
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Firefly
|
|
2
|
+
|
|
3
|
+
Active Learning Molecular Dynamics with MACE potentials and DFT reference calculations (SIESTA or Quantum ESPRESSO).
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install .
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or in editable mode (for development):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install -e .
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Usage
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
firefly parameters.json
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Parameters (parameters.json)
|
|
24
|
+
|
|
25
|
+
| Key | Required | Description |
|
|
26
|
+
|-----|----------|-------------|
|
|
27
|
+
| `file` | yes | Path to the atomic structure file (.xyz, etc.) |
|
|
28
|
+
| `ensemble` | yes | MD ensemble: `iso`, `aniso`, `nptz`, `nptxy`, `bussi`, `ortho` |
|
|
29
|
+
| `code` | yes | DFT code: `siesta` or `qe` |
|
|
30
|
+
| `cmd` | yes | Command to run the DFT code |
|
|
31
|
+
| `thresh` | yes | Uncertainty threshold for active learning |
|
|
32
|
+
| `n_models` | yes | Number of MACE models in the committee |
|
|
33
|
+
| `atoms` | siesta only | Dict mapping element symbols to atomic numbers |
|
|
34
|
+
| `gpus` | no | Number of GPUs (default: 1) |
|
|
35
|
+
| `T_begin` | no | Initial temperature in K (default: 300) |
|
|
36
|
+
| `T_final` | no | Final temperature in K (default: 300) |
|
|
37
|
+
| `pressure` | no | Pressure in bar (default: 0) |
|
|
38
|
+
| `dt` | no | Time step in fs (default: 0.5) |
|
|
39
|
+
| `time` | no | Total simulation time in fs (default: 100000) |
|
|
40
|
+
| `patience` | no | Steps before forcing DFT call (default: 1000) |
|
|
41
|
+
| `active` | no | Uncertainty metric: `energy` or `force` (default: `energy`) |
|
|
42
|
+
| `epoch` | no | Training epochs per retraining step (default: 10) |
|
|
43
|
+
| `premodel` | no | Model name prefix (default: `model`) |
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
from ase import units
|
|
5
|
+
from ase.io import read
|
|
6
|
+
import firefly.simulations.ensemble as sim
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ActiveMD:
|
|
12
|
+
def __init__(self, config):
|
|
13
|
+
self.config = config
|
|
14
|
+
|
|
15
|
+
erros = []
|
|
16
|
+
|
|
17
|
+
if "file" not in self.config or not self.config["file"]:
|
|
18
|
+
erros.append("The path to the atomic structure file ('file') is mandatory.")
|
|
19
|
+
|
|
20
|
+
if "ensemble" not in self.config or not self.config["ensemble"]:
|
|
21
|
+
erros.append("The simulation ensemble type ('ensemble') is mandatory (e.g. 'iso', 'aniso', 'bussi', etc.).")
|
|
22
|
+
|
|
23
|
+
if "code" not in self.config or not self.config["code"]:
|
|
24
|
+
erros.append("The reference DFT code ('code') is mandatory (use 'siesta' or 'qe').")
|
|
25
|
+
|
|
26
|
+
if "cmd" not in self.config or not self.config["cmd"]:
|
|
27
|
+
erros.append("The execution command for the DFT software ('cmd') is mandatory.")
|
|
28
|
+
|
|
29
|
+
if "thresh" not in self.config or self.config.get("thresh") is None:
|
|
30
|
+
erros.append("The uncertainty threshold ('thresh') is mandatory for Active Learning.")
|
|
31
|
+
|
|
32
|
+
if "n_models" not in self.config or self.config.get("n_models") is None:
|
|
33
|
+
erros.append("The number of models in the ensemble ('n_models') is mandatory.")
|
|
34
|
+
|
|
35
|
+
self.code = self.config.get("code")
|
|
36
|
+
if self.code == 'siesta':
|
|
37
|
+
if "atoms" not in self.config or not self.config["atoms"]:
|
|
38
|
+
erros.append("The 'atoms' dictionary mapping symbols to atomic numbers is mandatory when using Siesta.")
|
|
39
|
+
|
|
40
|
+
if erros:
|
|
41
|
+
logger.error("=" * 60)
|
|
42
|
+
logger.error(" CONFIGURATION ERROR DETECTED IN PARAMETERS.JSON")
|
|
43
|
+
logger.error("=" * 60)
|
|
44
|
+
logger.error("The following mandatory options are missing or invalid:")
|
|
45
|
+
for erro in erros:
|
|
46
|
+
logger.error(f" -> {erro}")
|
|
47
|
+
logger.error("=" * 60)
|
|
48
|
+
sys.exit(1)
|
|
49
|
+
|
|
50
|
+
import os
|
|
51
|
+
caminho_arquivo = self.config["file"]
|
|
52
|
+
if not os.path.exists(caminho_arquivo):
|
|
53
|
+
logger.error("=" * 60)
|
|
54
|
+
logger.error(" ERROR: ATOMIC STRUCTURE FILE NOT FOUND")
|
|
55
|
+
logger.error("=" * 60)
|
|
56
|
+
logger.error(f" The file '{caminho_arquivo}' specified in 'file' does not exist.")
|
|
57
|
+
logger.error("=" * 60)
|
|
58
|
+
sys.exit(1)
|
|
59
|
+
|
|
60
|
+
self.ensemble = self.config["ensemble"]
|
|
61
|
+
self.atoms = read(caminho_arquivo, index='0')
|
|
62
|
+
self.T_beg = self.config.get("T_begin", 300.0)
|
|
63
|
+
self.T_fin = self.config.get("T_final", 300.0)
|
|
64
|
+
self.name = self.config.get("premodel", "model")
|
|
65
|
+
self.n = self.config["n_models"]
|
|
66
|
+
self.dict = self.config.get("atoms", {})
|
|
67
|
+
self.patience = self.config.get("patience", 1000)
|
|
68
|
+
self.thresh = self.config["thresh"]
|
|
69
|
+
self.cmd = self.config["cmd"]
|
|
70
|
+
self.time = self.config.get("time", 100000.0) * units.fs
|
|
71
|
+
self.interval = self.patience
|
|
72
|
+
self.dt = self.config.get("dt", 0.5) * units.fs
|
|
73
|
+
self.active = self.config.get("active", "energy")
|
|
74
|
+
self.epoch = self.config.get("epoch", 10)
|
|
75
|
+
|
|
76
|
+
pressao_valor = self.config.get("pressure")
|
|
77
|
+
if pressao_valor is not None:
|
|
78
|
+
self.press = pressao_valor * units.bar
|
|
79
|
+
else:
|
|
80
|
+
self.press = 0.0 * units.bar
|
|
81
|
+
|
|
82
|
+
def iniciar(self):
|
|
83
|
+
logger.info("Starting molecular dynamics and processing parameters...")
|
|
84
|
+
|
|
85
|
+
mapa_ensembles = {
|
|
86
|
+
'aniso': sim.aniso,
|
|
87
|
+
'iso': sim.iso,
|
|
88
|
+
'nptz': sim.NPTz,
|
|
89
|
+
'nptxy': sim.NPTxy,
|
|
90
|
+
'bussi': sim.bussi,
|
|
91
|
+
'ortho': sim.ortho,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
funcao_simulacao = mapa_ensembles.get(self.ensemble.lower())
|
|
95
|
+
if funcao_simulacao:
|
|
96
|
+
funcao_simulacao(data=self)
|
|
97
|
+
else:
|
|
98
|
+
logger.error(f"The ensemble '{self.ensemble}' is not supported by the program.")
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main():
|
|
103
|
+
logging.basicConfig(
|
|
104
|
+
level=logging.INFO,
|
|
105
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
106
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
if len(sys.argv) < 2:
|
|
110
|
+
logger.error("Correct usage: firefly parameters.json")
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
logger.info(f"Loading configuration file: {sys.argv[1]}")
|
|
115
|
+
with open(sys.argv[1], 'r') as arquivo:
|
|
116
|
+
parametros = json.load(arquivo)
|
|
117
|
+
|
|
118
|
+
app = ActiveMD(parametros)
|
|
119
|
+
app.iniciar()
|
|
120
|
+
|
|
121
|
+
except FileNotFoundError:
|
|
122
|
+
logger.error(f"Error: File '{sys.argv[1]}' was not found.")
|
|
123
|
+
sys.exit(1)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
main()
|
|
File without changes
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import logging
|
|
3
|
+
import numpy as np
|
|
4
|
+
from ase import units
|
|
5
|
+
from ase.cell import Cell
|
|
6
|
+
from ase.io import read, write, Trajectory
|
|
7
|
+
from ase.optimize import LBFGS
|
|
8
|
+
from ase.md.langevinbaoab import LangevinBAOAB
|
|
9
|
+
from ase.md.npt import NPT
|
|
10
|
+
from ase.md.langevin import Langevin
|
|
11
|
+
from ase.md.bussi import Bussi
|
|
12
|
+
from ase.md.nptberendsen import NPTBerendsen
|
|
13
|
+
from ase.md.nptberendsen import Inhomogeneous_NPTBerendsen
|
|
14
|
+
from ase.md.velocitydistribution import MaxwellBoltzmannDistribution, Stationary, ZeroRotation
|
|
15
|
+
from ase.calculators.mixing import AverageCalculator
|
|
16
|
+
from mace.calculators import MACECalculator
|
|
17
|
+
from ase.md.nose_hoover_chain import MaskedMTKNPT
|
|
18
|
+
from ase.md.nose_hoover_chain import MTKNPT
|
|
19
|
+
from ase.md.nose_hoover_chain import IsotropicMTKNPT
|
|
20
|
+
|
|
21
|
+
import firefly.utils.ThreshComp as thresh
|
|
22
|
+
import firefly.utils.MaceUtils as mu
|
|
23
|
+
|
|
24
|
+
logger = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
amu_A3_to_g_cm3 = 1.660539
|
|
27
|
+
GPa_to_atm = 9869.23
|
|
28
|
+
GPa_to_eVA3 = 1. / 160.21766208
|
|
29
|
+
atm_to_eVA3 = (1. / GPa_to_atm) * GPa_to_eVA3
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TemperatureRamp:
|
|
33
|
+
def __init__(self, dyn, T_start, T_end, Nsteps):
|
|
34
|
+
self.dyn = dyn
|
|
35
|
+
self.Nsteps = Nsteps
|
|
36
|
+
self.T_start = T_start
|
|
37
|
+
self.T_end = T_end
|
|
38
|
+
|
|
39
|
+
def __call__(self):
|
|
40
|
+
a = (self.Nsteps - self.dyn.nsteps) / self.Nsteps
|
|
41
|
+
T = self.T_start * a + self.T_end * (1.0 - a)
|
|
42
|
+
if self.dyn.__class__.__name__ == 'Bussi':
|
|
43
|
+
self.dyn.temperature_K = T
|
|
44
|
+
self.dyn.temp = T * units.kB
|
|
45
|
+
self.dyn.target_kinetic_energy = 0.5 * self.dyn.temp * self.dyn.ndof
|
|
46
|
+
elif hasattr(self.dyn, 'set_temperature'):
|
|
47
|
+
self.dyn.set_temperature(temperature_K=T)
|
|
48
|
+
elif hasattr(self.dyn._thermostat, 'set_temperature'):
|
|
49
|
+
self.dyn._thermostat.set_temperature(temperature_K=T)
|
|
50
|
+
else:
|
|
51
|
+
self.dyn._thermostat._kT = T * units.kB
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PrintEnergy:
|
|
55
|
+
def __init__(self, dyn, interval=1):
|
|
56
|
+
self.dyn = dyn
|
|
57
|
+
self.interval = interval
|
|
58
|
+
self.time_start = time.time()
|
|
59
|
+
self.time_last = self.time_start
|
|
60
|
+
|
|
61
|
+
def __call__(self):
|
|
62
|
+
a = self.dyn.atoms
|
|
63
|
+
step = self.dyn.nsteps
|
|
64
|
+
epot = a.get_potential_energy() / len(a)
|
|
65
|
+
ekin = a.get_kinetic_energy() / len(a)
|
|
66
|
+
temp = a.get_temperature()
|
|
67
|
+
stress = a.get_stress() / GPa_to_eVA3
|
|
68
|
+
press = ((stress[0] + stress[1] + stress[2]) / 3) * GPa_to_atm
|
|
69
|
+
dens = (a.get_masses().sum() / a.get_volume()) * amu_A3_to_g_cm3
|
|
70
|
+
cell = a.cell.cellpar()
|
|
71
|
+
time_now = time.time()
|
|
72
|
+
elapsed_time = time_now - self.time_last
|
|
73
|
+
self.time_last = time_now
|
|
74
|
+
spcpu = self.interval / elapsed_time
|
|
75
|
+
|
|
76
|
+
with open("therm.dat", "a") as f:
|
|
77
|
+
f.write('%d: T= %.2f K P= %.1f atm K= %.5e U= %.5e rho= %.3e spcpu= %.1f' % (step, temp, press, ekin, epot, dens, spcpu) + ' [%.3f %.3f %.3f %.2f %.2f %.2f]\n' % tuple(cell))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _get_calculators(data):
|
|
81
|
+
num_gpus = data.config.get("gpus", 1)
|
|
82
|
+
models = [f"model{i+1}.model" for i in range(data.n)]
|
|
83
|
+
calculators = []
|
|
84
|
+
for i, model_path in enumerate(models):
|
|
85
|
+
gpu_id = i % num_gpus
|
|
86
|
+
device = f"cuda:{gpu_id}"
|
|
87
|
+
logger.info(f"Loading {model_path} on device {device}")
|
|
88
|
+
calculators.append(MACECalculator(model_paths=model_path, device=device))
|
|
89
|
+
return calculators
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def aniso(data):
|
|
93
|
+
atoms = data.atoms
|
|
94
|
+
data.calculadoras = _get_calculators(data)
|
|
95
|
+
data.calc = AverageCalculator(data.calculadoras)
|
|
96
|
+
atoms.calc = data.calc
|
|
97
|
+
|
|
98
|
+
press = data.press
|
|
99
|
+
Tdamp = 100 * data.dt
|
|
100
|
+
Pdamp = 1000 * data.dt
|
|
101
|
+
n_steps = int(data.time / data.dt)
|
|
102
|
+
|
|
103
|
+
MaxwellBoltzmannDistribution(atoms, temperature_K=data.T_beg)
|
|
104
|
+
dyn = MTKNPT(data.atoms, timestep=data.dt, temperature_K=data.T_beg, pressure_au=press, tdamp=Tdamp, pdamp=Pdamp)
|
|
105
|
+
data.dyn = dyn
|
|
106
|
+
|
|
107
|
+
if data.active == 'force':
|
|
108
|
+
dyn.attach(lambda: thresh.thresh_forces(data=data), interval=1)
|
|
109
|
+
if data.active == 'energy':
|
|
110
|
+
dyn.attach(lambda: thresh.thresh_energies(data=data), interval=1)
|
|
111
|
+
|
|
112
|
+
dyn.attach(TemperatureRamp(dyn, data.T_beg, data.T_fin, n_steps), interval=100)
|
|
113
|
+
dyn.attach(PrintEnergy(dyn, 1), interval=1)
|
|
114
|
+
traj = Trajectory('run.traj', 'w', atoms)
|
|
115
|
+
dyn.attach(traj.write, interval=100)
|
|
116
|
+
|
|
117
|
+
logger.info("Starting the simulation")
|
|
118
|
+
dyn.run(n_steps)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def iso(data):
|
|
122
|
+
atoms = data.atoms
|
|
123
|
+
data.calculadoras = _get_calculators(data)
|
|
124
|
+
data.calc = AverageCalculator(data.calculadoras)
|
|
125
|
+
atoms.calc = data.calc
|
|
126
|
+
|
|
127
|
+
press = data.press
|
|
128
|
+
n_steps = int(data.time / data.dt)
|
|
129
|
+
|
|
130
|
+
MaxwellBoltzmannDistribution(data.atoms, temperature_K=data.T_beg)
|
|
131
|
+
dyn = IsotropicMTKNPT(data.atoms, timestep=data.dt, temperature_K=data.T_beg, pressure_au=press, tdamp=100 * units.fs, pdamp=1000 * units.fs)
|
|
132
|
+
data.dyn = dyn
|
|
133
|
+
|
|
134
|
+
if data.active == 'force':
|
|
135
|
+
dyn.attach(lambda: thresh.thresh_forces(data=data), interval=1)
|
|
136
|
+
if data.active == 'energy':
|
|
137
|
+
dyn.attach(lambda: thresh.thresh_energies(data=data), interval=1)
|
|
138
|
+
|
|
139
|
+
dyn.attach(TemperatureRamp(dyn, data.T_beg, data.T_fin, n_steps), interval=100)
|
|
140
|
+
dyn.attach(PrintEnergy(dyn, 1), interval=1)
|
|
141
|
+
traj = Trajectory('run.traj', 'w', atoms)
|
|
142
|
+
dyn.attach(traj.write, interval=100)
|
|
143
|
+
|
|
144
|
+
logger.info("Starting the simulation")
|
|
145
|
+
dyn.run(n_steps)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def ortho(data):
|
|
149
|
+
atoms = data.atoms
|
|
150
|
+
data.calculadoras = _get_calculators(data)
|
|
151
|
+
data.calc = AverageCalculator(data.calculadoras)
|
|
152
|
+
atoms.calc = data.calc
|
|
153
|
+
|
|
154
|
+
press = data.press
|
|
155
|
+
Tdamp = 100 * data.dt
|
|
156
|
+
Pdamp = 1000 * data.dt
|
|
157
|
+
n_steps = int(data.time / data.dt)
|
|
158
|
+
|
|
159
|
+
MaxwellBoltzmannDistribution(data.atoms, temperature_K=data.T_beg)
|
|
160
|
+
dyn = MaskedMTKNPT(data.atoms, timestep=data.dt, temperature_K=data.T_beg, pressure_au=press, tdamp=Tdamp, pdamp=Pdamp, mask=(1, 1, 1))
|
|
161
|
+
data.dyn = dyn
|
|
162
|
+
|
|
163
|
+
if data.active == 'force':
|
|
164
|
+
dyn.attach(lambda: thresh.thresh_forces(data=data), interval=1)
|
|
165
|
+
if data.active == 'energy':
|
|
166
|
+
dyn.attach(lambda: thresh.thresh_energies(data=data), interval=1)
|
|
167
|
+
|
|
168
|
+
dyn.attach(TemperatureRamp(dyn, data.T_beg, data.T_fin, n_steps), interval=100)
|
|
169
|
+
dyn.attach(PrintEnergy(dyn, 1), interval=1)
|
|
170
|
+
traj = Trajectory('run.traj', 'w', atoms)
|
|
171
|
+
dyn.attach(traj.write, interval=100)
|
|
172
|
+
|
|
173
|
+
logger.info("Starting the simulation")
|
|
174
|
+
dyn.run(n_steps)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def NPTz(data):
|
|
178
|
+
atoms = data.atoms
|
|
179
|
+
data.calculadoras = _get_calculators(data)
|
|
180
|
+
data.calc = AverageCalculator(data.calculadoras)
|
|
181
|
+
atoms.calc = data.calc
|
|
182
|
+
|
|
183
|
+
press = data.press
|
|
184
|
+
Tdamp = 100 * data.dt
|
|
185
|
+
Pdamp = 1000 * data.dt
|
|
186
|
+
n_steps = int(data.time / data.dt)
|
|
187
|
+
|
|
188
|
+
MaxwellBoltzmannDistribution(data.atoms, temperature_K=data.T_beg)
|
|
189
|
+
Stationary(data.atoms)
|
|
190
|
+
ZeroRotation(data.atoms)
|
|
191
|
+
dyn = MaskedMTKNPT(data.atoms, timestep=data.dt, temperature_K=data.T_beg, pressure_au=press, tdamp=Tdamp, pdamp=Pdamp, mask=[False, False, True])
|
|
192
|
+
data.dyn = dyn
|
|
193
|
+
|
|
194
|
+
if data.active == 'force':
|
|
195
|
+
dyn.attach(lambda: thresh.thresh_forces(data=data), interval=1)
|
|
196
|
+
if data.active == 'energy':
|
|
197
|
+
dyn.attach(lambda: thresh.thresh_energies(data=data), interval=1)
|
|
198
|
+
|
|
199
|
+
dyn.attach(TemperatureRamp(dyn, data.T_beg, data.T_fin, n_steps), interval=100)
|
|
200
|
+
dyn.attach(PrintEnergy(dyn, 1), interval=1)
|
|
201
|
+
traj = Trajectory('run.traj', 'w', atoms)
|
|
202
|
+
dyn.attach(traj.write, interval=100)
|
|
203
|
+
|
|
204
|
+
logger.info("Starting the simulation")
|
|
205
|
+
dyn.run(n_steps)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def NPTxy(data):
|
|
209
|
+
atoms = data.atoms
|
|
210
|
+
data.calculadoras = _get_calculators(data)
|
|
211
|
+
data.calc = AverageCalculator(data.calculadoras)
|
|
212
|
+
atoms.calc = data.calc
|
|
213
|
+
|
|
214
|
+
press = data.press
|
|
215
|
+
Tdamp = 100 * data.dt
|
|
216
|
+
Pdamp = 1000 * data.dt
|
|
217
|
+
n_steps = int(data.time / data.dt)
|
|
218
|
+
|
|
219
|
+
MaxwellBoltzmannDistribution(data.atoms, temperature_K=data.T_beg)
|
|
220
|
+
Stationary(data.atoms)
|
|
221
|
+
ZeroRotation(data.atoms)
|
|
222
|
+
dyn = MaskedMTKNPT(data.atoms, timestep=data.dt, temperature_K=data.T_beg, pressure_au=press, tdamp=Tdamp, pdamp=Pdamp, mask=[True, True, False])
|
|
223
|
+
data.dyn = dyn
|
|
224
|
+
|
|
225
|
+
if data.active == 'force':
|
|
226
|
+
dyn.attach(lambda: thresh.thresh_forces(data=data), interval=1)
|
|
227
|
+
if data.active == 'energy':
|
|
228
|
+
dyn.attach(lambda: thresh.thresh_energies(data=data), interval=1)
|
|
229
|
+
|
|
230
|
+
dyn.attach(TemperatureRamp(dyn, data.T_beg, data.T_fin, n_steps), interval=100)
|
|
231
|
+
dyn.attach(PrintEnergy(dyn, 1), interval=1)
|
|
232
|
+
traj = Trajectory('run.traj', 'w', atoms)
|
|
233
|
+
dyn.attach(traj.write, interval=100)
|
|
234
|
+
|
|
235
|
+
logger.info("Starting the simulation")
|
|
236
|
+
dyn.run(n_steps)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def bussi(data):
|
|
240
|
+
atoms = data.atoms
|
|
241
|
+
data.calculadoras = _get_calculators(data)
|
|
242
|
+
data.calc = AverageCalculator(data.calculadoras)
|
|
243
|
+
atoms.calc = data.calc
|
|
244
|
+
|
|
245
|
+
Tdamp = 100 * data.dt
|
|
246
|
+
n_steps = int(data.time / data.dt)
|
|
247
|
+
|
|
248
|
+
MaxwellBoltzmannDistribution(data.atoms, temperature_K=data.T_beg)
|
|
249
|
+
Stationary(data.atoms)
|
|
250
|
+
ZeroRotation(data.atoms)
|
|
251
|
+
dyn = Bussi(data.atoms, timestep=data.dt, temperature_K=data.T_beg, taut=Tdamp)
|
|
252
|
+
data.dyn = dyn
|
|
253
|
+
|
|
254
|
+
if data.active == 'force':
|
|
255
|
+
dyn.attach(lambda: thresh.thresh_forces(data=data), interval=1)
|
|
256
|
+
if data.active == 'energy':
|
|
257
|
+
dyn.attach(lambda: thresh.thresh_energies(data=data), interval=1)
|
|
258
|
+
|
|
259
|
+
dyn.attach(TemperatureRamp(dyn, data.T_beg, data.T_fin, n_steps), interval=100)
|
|
260
|
+
dyn.attach(PrintEnergy(dyn, 1), interval=1)
|
|
261
|
+
traj = Trajectory('run.traj', 'w', atoms)
|
|
262
|
+
dyn.attach(traj.write, interval=100)
|
|
263
|
+
|
|
264
|
+
logger.info("Starting the simulation")
|
|
265
|
+
dyn.run(n_steps)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import glob
|
|
3
|
+
import logging
|
|
4
|
+
import numpy as np
|
|
5
|
+
from ase import Atoms
|
|
6
|
+
from ase.io import write, read
|
|
7
|
+
from ase.data import atomic_numbers, chemical_symbols
|
|
8
|
+
from ase.calculators.singlepoint import SinglePointCalculator
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sisl
|
|
11
|
+
from sisl.io.siesta import stdoutSileSiesta
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def create_siesta(data):
|
|
17
|
+
logger.info("Starting DFT reference calculation with SIESTA...")
|
|
18
|
+
celli = data.atoms.get_cell()
|
|
19
|
+
symbols = data.atoms.get_chemical_symbols()
|
|
20
|
+
posicoes = data.atoms.get_positions()
|
|
21
|
+
|
|
22
|
+
with open("geom.in", "w") as arq:
|
|
23
|
+
for sym, pos2 in zip(symbols, posicoes):
|
|
24
|
+
x, y, z = pos2
|
|
25
|
+
Z = data.dict[sym]
|
|
26
|
+
arq.write(f"{x:12.6f} {y:12.6f} {z:12.6f} {Z:3d}\n")
|
|
27
|
+
|
|
28
|
+
with open("lattice.fdf", "w") as arq:
|
|
29
|
+
arq.write("%block LatticeVectors\n")
|
|
30
|
+
c = celli.reshape(1, 9)[0]
|
|
31
|
+
arq.write(f"{c[0]:12.6f} {c[1]:12.6f} {c[2]:12.6f}\n")
|
|
32
|
+
arq.write(f"{c[3]:12.6f} {c[4]:12.6f} {c[5]:12.6f}\n")
|
|
33
|
+
arq.write(f"{c[6]:12.6f} {c[7]:12.6f} {c[8]:12.6f}\n")
|
|
34
|
+
arq.write("%endblock LatticeVectors\n")
|
|
35
|
+
|
|
36
|
+
write("last_add.xyz", data.atoms)
|
|
37
|
+
|
|
38
|
+
os.system(data.cmd + " < RUN.fdf > saida.out")
|
|
39
|
+
|
|
40
|
+
geom = sisl.get_sile("siesta.XV").read_geometry()
|
|
41
|
+
out = stdoutSileSiesta("saida.out")
|
|
42
|
+
E = out.read_energy()
|
|
43
|
+
Etot = E['total']
|
|
44
|
+
force = np.loadtxt("siesta.FA", skiprows=1)
|
|
45
|
+
forces = force[:, 1:4]
|
|
46
|
+
symbols_geom = [chemical_symbols[z] for z in geom.atoms.Z]
|
|
47
|
+
stress = out.read_stress()
|
|
48
|
+
|
|
49
|
+
atoms2 = Atoms(symbols=symbols_geom, positions=geom.xyz, cell=geom.cell, pbc=True)
|
|
50
|
+
atoms2.info['REF_energy'] = Etot
|
|
51
|
+
if stress is not None:
|
|
52
|
+
atoms2.info['REF_stress'] = stress
|
|
53
|
+
atoms2.arrays['REF_forces'] = forces
|
|
54
|
+
|
|
55
|
+
write("train.xyz", atoms2, append=True)
|
|
56
|
+
|
|
57
|
+
padroes_temporarios = [
|
|
58
|
+
"siesta*", "BASIS_*", "NON_TRIMMED_KP_LIST", "FORCE_STRESS", "OUTVARS.yml",
|
|
59
|
+
"PARALLEL_DIST", "TIMES", "*ion*", "MESSAGES", "CLOCK", "fdf*", "INPUT_TMP.*",
|
|
60
|
+
"geom.in", "lattice.fdf", "last_add.xyz", "saida.out",
|
|
61
|
+
]
|
|
62
|
+
for padrao in padroes_temporarios:
|
|
63
|
+
for f in glob.glob(padrao):
|
|
64
|
+
try:
|
|
65
|
+
os.remove(f)
|
|
66
|
+
except OSError:
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def create_qe(data):
|
|
71
|
+
logger.info("Starting DFT reference calculation with Quantum ESPRESSO (QE)...")
|
|
72
|
+
celli = data.atoms.get_cell()
|
|
73
|
+
symbols = data.atoms.get_chemical_symbols()
|
|
74
|
+
posicoes = data.atoms.get_positions()
|
|
75
|
+
|
|
76
|
+
with open("geom.in", "w") as arq:
|
|
77
|
+
arq.write("\nATOMIC_POSITIONS angstrom\n")
|
|
78
|
+
for sym, pos2 in zip(symbols, posicoes):
|
|
79
|
+
x, y, z = pos2
|
|
80
|
+
arq.write(f"{sym:4s} {x:12.6f} {y:12.6f} {z:12.6f}\n")
|
|
81
|
+
|
|
82
|
+
with open("lattice.in", "w") as arq:
|
|
83
|
+
arq.write("\nCELL_PARAMETERS angstrom\n")
|
|
84
|
+
c = celli.reshape(1, 9)[0]
|
|
85
|
+
arq.write(f"{c[0]:12.6f} {c[1]:12.6f} {c[2]:12.6f}\n")
|
|
86
|
+
arq.write(f"{c[3]:12.6f} {c[4]:12.6f} {c[5]:12.6f}\n")
|
|
87
|
+
arq.write(f"{c[6]:12.6f} {c[7]:12.6f} {c[8]:12.6f}\n")
|
|
88
|
+
|
|
89
|
+
with open("run.in", "w") as outfile:
|
|
90
|
+
for fname in ["espresso.in", "geom.in", "lattice.in"]:
|
|
91
|
+
with open(fname) as infile:
|
|
92
|
+
outfile.write(infile.read())
|
|
93
|
+
|
|
94
|
+
os.system(data.cmd + " < run.in > saida.out")
|
|
95
|
+
|
|
96
|
+
atoms3 = read('saida.out', format='espresso-out')
|
|
97
|
+
atoms3.info['REF_energy'] = atoms3.get_potential_energy()
|
|
98
|
+
atoms3.arrays['REF_forces'] = atoms3.get_forces()
|
|
99
|
+
if atoms3.get_stress() is not None:
|
|
100
|
+
atoms3.info['REF_stress'] = atoms3.get_stress()
|
|
101
|
+
|
|
102
|
+
write('train.xyz', atoms3, append=True)
|
|
103
|
+
|
|
104
|
+
padroes_temporarios = ["geom.in", "lattice.in", "run.in", "saida.out", "active.save", "active.xml"]
|
|
105
|
+
for f in padroes_temporarios:
|
|
106
|
+
if os.path.exists(f):
|
|
107
|
+
if os.path.isdir(f):
|
|
108
|
+
import shutil
|
|
109
|
+
shutil.rmtree(f)
|
|
110
|
+
else:
|
|
111
|
+
os.remove(f)
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import glob
|
|
2
|
+
import re
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
import logging
|
|
7
|
+
from mace.calculators import MACECalculator
|
|
8
|
+
from ase.calculators.mixing import AverageCalculator
|
|
9
|
+
|
|
10
|
+
logger = logging.getLogger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _get_calculators(data):
|
|
14
|
+
num_gpus = data.config.get("gpus", 1)
|
|
15
|
+
models = [f"model{i+1}.model" for i in range(data.n)]
|
|
16
|
+
calculators = []
|
|
17
|
+
for i, model_path in enumerate(models):
|
|
18
|
+
gpu_id = i % num_gpus
|
|
19
|
+
device = f"cuda:{gpu_id}"
|
|
20
|
+
logger.info(f"Loading {model_path} on device {device}")
|
|
21
|
+
calculators.append(MACECalculator(model_paths=model_path, device=device))
|
|
22
|
+
return calculators
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def renew_model(data):
|
|
26
|
+
data.calculadoras = None
|
|
27
|
+
data.calculadoras = _get_calculators(data)
|
|
28
|
+
new_calc = AverageCalculator(data.calculadoras)
|
|
29
|
+
data.atoms.calc = new_calc
|
|
30
|
+
logger.info("The model was restarted and distributed across GPUs")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def train_parallel(data):
|
|
34
|
+
n = data.n
|
|
35
|
+
ep_train = data.epoch
|
|
36
|
+
ep_swa = data.epoch // 2
|
|
37
|
+
|
|
38
|
+
gpus_config = data.config.get("gpus", 1)
|
|
39
|
+
if isinstance(gpus_config, int):
|
|
40
|
+
gpus_disponiveis = list(range(gpus_config))
|
|
41
|
+
elif isinstance(gpus_config, list):
|
|
42
|
+
gpus_disponiveis = gpus_config
|
|
43
|
+
else:
|
|
44
|
+
gpus_disponiveis = [0]
|
|
45
|
+
|
|
46
|
+
num_gpus = len(gpus_disponiveis)
|
|
47
|
+
logger.info(f"[Parallel Training] Starting training of {n} models on {num_gpus} GPU(s): {gpus_disponiveis}")
|
|
48
|
+
|
|
49
|
+
jobs_para_executar = []
|
|
50
|
+
for i in range(n):
|
|
51
|
+
prefixo = data.name + str(i + 1)
|
|
52
|
+
arquivos = glob.glob(f"checkpoints/{prefixo}_run-*_epoch-*.pt")
|
|
53
|
+
|
|
54
|
+
max_epoch = max((int(re.search(r"epoch-(\d+)", f).group(1)) for f in arquivos), default=0)
|
|
55
|
+
novo_max = max_epoch + ep_train
|
|
56
|
+
novo_max_swa = max_epoch + ep_swa
|
|
57
|
+
|
|
58
|
+
job_file = f"job_{i+1}.sh"
|
|
59
|
+
if os.path.exists(job_file):
|
|
60
|
+
with open(job_file, "r") as f:
|
|
61
|
+
conteudo = f.read()
|
|
62
|
+
conteudo = re.sub(r"(--max_num_epochs=)\d+", f"\\1{novo_max}", conteudo)
|
|
63
|
+
conteudo = re.sub(r"(--start_swa=)\d+", f"\\1{novo_max_swa}", conteudo)
|
|
64
|
+
with open(job_file, "w") as f:
|
|
65
|
+
f.write(conteudo)
|
|
66
|
+
|
|
67
|
+
jobs_para_executar.append({
|
|
68
|
+
"id_modelo": i + 1,
|
|
69
|
+
"cmd": ["sh", job_file],
|
|
70
|
+
"arquivos_para_limpar": arquivos,
|
|
71
|
+
"prefixo": prefixo,
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
processos_ativos = {}
|
|
75
|
+
jobs_restantes = list(jobs_para_executar)
|
|
76
|
+
jobs_concluidos = []
|
|
77
|
+
falhas = []
|
|
78
|
+
|
|
79
|
+
while jobs_restantes or processos_ativos:
|
|
80
|
+
for gpu_id in gpus_disponiveis:
|
|
81
|
+
if gpu_id not in processos_ativos and jobs_restantes:
|
|
82
|
+
job = jobs_restantes.pop(0)
|
|
83
|
+
modelo_id = job["id_modelo"]
|
|
84
|
+
logger.info(f" -> Allocating Model {modelo_id} on GPU {gpu_id}...")
|
|
85
|
+
env_subprocesso = os.environ.copy()
|
|
86
|
+
env_subprocesso["CUDA_VISIBLE_DEVICES"] = str(gpu_id)
|
|
87
|
+
processo = subprocess.Popen(
|
|
88
|
+
job["cmd"],
|
|
89
|
+
stdout=subprocess.PIPE,
|
|
90
|
+
stderr=subprocess.PIPE,
|
|
91
|
+
env=env_subprocesso,
|
|
92
|
+
text=True,
|
|
93
|
+
)
|
|
94
|
+
processos_ativos[gpu_id] = (processo, job)
|
|
95
|
+
|
|
96
|
+
time.sleep(1.0)
|
|
97
|
+
|
|
98
|
+
gpus_liberadas = []
|
|
99
|
+
for gpu_id, (proc, job) in processos_ativos.items():
|
|
100
|
+
status = proc.poll()
|
|
101
|
+
if status is not None:
|
|
102
|
+
modelo_id = job["id_modelo"]
|
|
103
|
+
gpus_liberadas.append(gpu_id)
|
|
104
|
+
if status == 0:
|
|
105
|
+
logger.info(f" [OK] Model {modelo_id} completed on GPU {gpu_id}.")
|
|
106
|
+
modelo_saida = f"{data.name}{modelo_id}_stagetwo.model"
|
|
107
|
+
modelo_destino = f"model{modelo_id}.model"
|
|
108
|
+
if os.path.exists(modelo_saida):
|
|
109
|
+
os.rename(modelo_saida, modelo_destino)
|
|
110
|
+
else:
|
|
111
|
+
logger.warning(f" [Warning] File {modelo_saida} not found post-training.")
|
|
112
|
+
for f in job["arquivos_para_limpar"]:
|
|
113
|
+
if os.path.exists(f):
|
|
114
|
+
os.remove(f)
|
|
115
|
+
jobs_concluidos.append(job)
|
|
116
|
+
else:
|
|
117
|
+
_, stderr_output = proc.communicate()
|
|
118
|
+
logger.error(f" [ERROR] Model {modelo_id} failed on GPU {gpu_id} with code {status}.")
|
|
119
|
+
logger.error(f"Details:\n{stderr_output}")
|
|
120
|
+
falhas.append((modelo_id, stderr_output))
|
|
121
|
+
|
|
122
|
+
for gpu_id in gpus_liberadas:
|
|
123
|
+
del processos_ativos[gpu_id]
|
|
124
|
+
|
|
125
|
+
for i in range(n):
|
|
126
|
+
prefixo = data.name + str(i + 1)
|
|
127
|
+
for temp_file in glob.glob(f"{prefixo}*"):
|
|
128
|
+
if os.path.exists(temp_file):
|
|
129
|
+
os.remove(temp_file)
|
|
130
|
+
|
|
131
|
+
if falhas:
|
|
132
|
+
erros_str = ", ".join([f"Model {m_id}" for m_id, _ in falhas])
|
|
133
|
+
raise RuntimeError(f"Parallel training failed for models: {erros_str}.")
|
|
134
|
+
|
|
135
|
+
logger.info("[Parallel Training] All models have been successfully updated!")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
train_paralell = train_parallel
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import logging
|
|
3
|
+
import firefly.utils.DFTCodes as cd
|
|
4
|
+
import firefly.utils.MaceUtils as mu
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def thresh_forces(data):
|
|
10
|
+
if not hasattr(data, 'calculadoras') or data.calculadoras is None:
|
|
11
|
+
data.calculadoras = mu._get_calculators(data)
|
|
12
|
+
|
|
13
|
+
atoms = data.atoms
|
|
14
|
+
calc_main = atoms.calc
|
|
15
|
+
committee_forces = []
|
|
16
|
+
|
|
17
|
+
for calc in data.calculadoras:
|
|
18
|
+
atoms.calc = calc
|
|
19
|
+
committee_forces.append(atoms.get_forces())
|
|
20
|
+
|
|
21
|
+
committee_forces = np.array(committee_forces)
|
|
22
|
+
std_forces = np.std(committee_forces, axis=0)
|
|
23
|
+
val_incerteza_estrutura = np.sqrt(np.mean(np.square(std_forces)))
|
|
24
|
+
|
|
25
|
+
if val_incerteza_estrutura > data.thresh:
|
|
26
|
+
logger.warning(f"Force uncertainty exceeded threshold! Value: {val_incerteza_estrutura:.6f} > Threshold: {data.thresh:.6f}")
|
|
27
|
+
|
|
28
|
+
if data.interval < data.patience and val_incerteza_estrutura < 3 * data.thresh:
|
|
29
|
+
logger.info(f"Patience active. Current interval: {data.interval}/{data.patience}")
|
|
30
|
+
data.atoms.calc = calc_main
|
|
31
|
+
data.interval += 1
|
|
32
|
+
else:
|
|
33
|
+
logger.warning("Patience limit exceeded or uncertainty too high (>= 3 * thresh). Starting DFT calculation and retraining.")
|
|
34
|
+
data.interval = 0
|
|
35
|
+
data.otf = val_incerteza_estrutura
|
|
36
|
+
passo_atual = data.dyn.get_number_of_steps()
|
|
37
|
+
|
|
38
|
+
with open("thresh.dat", "a") as f:
|
|
39
|
+
f.write(f"{passo_atual}\t{val_incerteza_estrutura:.6f}\n")
|
|
40
|
+
|
|
41
|
+
if data.code == 'siesta':
|
|
42
|
+
cd.create_siesta(data=data)
|
|
43
|
+
mu.train_parallel(data=data)
|
|
44
|
+
mu.renew_model(data=data)
|
|
45
|
+
|
|
46
|
+
if data.code == 'qe':
|
|
47
|
+
cd.create_qe(data=data)
|
|
48
|
+
mu.train_parallel(data=data)
|
|
49
|
+
mu.renew_model(data=data)
|
|
50
|
+
else:
|
|
51
|
+
data.atoms.calc = calc_main
|
|
52
|
+
data.interval += 1
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def thresh_energies(data):
|
|
56
|
+
if not hasattr(data, 'calculadoras') or data.calculadoras is None:
|
|
57
|
+
data.calculadoras = mu._get_calculators(data)
|
|
58
|
+
|
|
59
|
+
atoms = data.atoms
|
|
60
|
+
calc_main = atoms.calc
|
|
61
|
+
committee_energies = []
|
|
62
|
+
|
|
63
|
+
for calc in data.calculadoras:
|
|
64
|
+
atoms.calc = calc
|
|
65
|
+
committee_energies.append(atoms.get_potential_energy())
|
|
66
|
+
|
|
67
|
+
committee_energies = np.array(committee_energies)
|
|
68
|
+
std_energy = np.std(committee_energies)
|
|
69
|
+
|
|
70
|
+
if std_energy > data.thresh:
|
|
71
|
+
logger.warning(f"Energy uncertainty exceeded threshold! Value: {std_energy:.6f} > Threshold: {data.thresh:.6f}")
|
|
72
|
+
|
|
73
|
+
if data.interval < data.patience and std_energy < 3 * data.thresh:
|
|
74
|
+
logger.info(f"Patience active. Current interval: {data.interval}/{data.patience}")
|
|
75
|
+
data.atoms.calc = calc_main
|
|
76
|
+
data.interval += 1
|
|
77
|
+
else:
|
|
78
|
+
logger.warning("Patience limit exceeded or uncertainty too high (>= 3 * thresh). Starting DFT calculation and retraining.")
|
|
79
|
+
data.interval = 0
|
|
80
|
+
data.otf = std_energy
|
|
81
|
+
passo_atual = data.dyn.get_number_of_steps()
|
|
82
|
+
|
|
83
|
+
with open("thresh.dat", "a") as f:
|
|
84
|
+
f.write(f"{passo_atual}\t{std_energy:.6f}\n")
|
|
85
|
+
|
|
86
|
+
if data.code == 'siesta':
|
|
87
|
+
cd.create_siesta(data=data)
|
|
88
|
+
mu.train_parallel(data=data)
|
|
89
|
+
mu.renew_model(data=data)
|
|
90
|
+
|
|
91
|
+
if data.code == 'qe':
|
|
92
|
+
cd.create_qe(data=data)
|
|
93
|
+
mu.train_parallel(data=data)
|
|
94
|
+
mu.renew_model(data=data)
|
|
95
|
+
else:
|
|
96
|
+
data.atoms.calc = calc_main
|
|
97
|
+
data.interval += 1
|
|
File without changes
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: firefly-activemd
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: Active Learning Molecular Dynamics with MACE potentials and DFT reference calculations
|
|
5
|
+
Requires-Python: >=3.8
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: ase>=3.28
|
|
8
|
+
Requires-Dist: mace-torch>=0.3
|
|
9
|
+
Requires-Dist: numpy>=2
|
|
10
|
+
Requires-Dist: sisl>=0.16
|
|
11
|
+
|
|
12
|
+
# Firefly
|
|
13
|
+
|
|
14
|
+
Active Learning Molecular Dynamics with MACE potentials and DFT reference calculations (SIESTA or Quantum ESPRESSO).
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install .
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Or in editable mode (for development):
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install -e .
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
firefly parameters.json
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Parameters (parameters.json)
|
|
35
|
+
|
|
36
|
+
| Key | Required | Description |
|
|
37
|
+
|-----|----------|-------------|
|
|
38
|
+
| `file` | yes | Path to the atomic structure file (.xyz, etc.) |
|
|
39
|
+
| `ensemble` | yes | MD ensemble: `iso`, `aniso`, `nptz`, `nptxy`, `bussi`, `ortho` |
|
|
40
|
+
| `code` | yes | DFT code: `siesta` or `qe` |
|
|
41
|
+
| `cmd` | yes | Command to run the DFT code |
|
|
42
|
+
| `thresh` | yes | Uncertainty threshold for active learning |
|
|
43
|
+
| `n_models` | yes | Number of MACE models in the committee |
|
|
44
|
+
| `atoms` | siesta only | Dict mapping element symbols to atomic numbers |
|
|
45
|
+
| `gpus` | no | Number of GPUs (default: 1) |
|
|
46
|
+
| `T_begin` | no | Initial temperature in K (default: 300) |
|
|
47
|
+
| `T_final` | no | Final temperature in K (default: 300) |
|
|
48
|
+
| `pressure` | no | Pressure in bar (default: 0) |
|
|
49
|
+
| `dt` | no | Time step in fs (default: 0.5) |
|
|
50
|
+
| `time` | no | Total simulation time in fs (default: 100000) |
|
|
51
|
+
| `patience` | no | Steps before forcing DFT call (default: 1000) |
|
|
52
|
+
| `active` | no | Uncertainty metric: `energy` or `force` (default: `energy`) |
|
|
53
|
+
| `epoch` | no | Training epochs per retraining step (default: 10) |
|
|
54
|
+
| `premodel` | no | Model name prefix (default: `model`) |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
firefly/__init__.py
|
|
4
|
+
firefly/cli.py
|
|
5
|
+
firefly/simulations/__init__.py
|
|
6
|
+
firefly/simulations/ensemble.py
|
|
7
|
+
firefly/utils/DFTCodes.py
|
|
8
|
+
firefly/utils/MaceUtils.py
|
|
9
|
+
firefly/utils/ThreshComp.py
|
|
10
|
+
firefly/utils/__init__.py
|
|
11
|
+
firefly_activemd.egg-info/PKG-INFO
|
|
12
|
+
firefly_activemd.egg-info/SOURCES.txt
|
|
13
|
+
firefly_activemd.egg-info/dependency_links.txt
|
|
14
|
+
firefly_activemd.egg-info/entry_points.txt
|
|
15
|
+
firefly_activemd.egg-info/requires.txt
|
|
16
|
+
firefly_activemd.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
firefly
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "firefly-activemd"
|
|
7
|
+
version = "0.3.0"
|
|
8
|
+
description = "Active Learning Molecular Dynamics with MACE potentials and DFT reference calculations"
|
|
9
|
+
readme = {file = "README.md", content-type = "text/markdown"}
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"ase>=3.28",
|
|
13
|
+
"mace-torch>=0.3",
|
|
14
|
+
"numpy>=2",
|
|
15
|
+
"sisl>=0.16",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.scripts]
|
|
19
|
+
firefly = "firefly.cli:main"
|
|
20
|
+
|
|
21
|
+
[tool.setuptools.packages.find]
|
|
22
|
+
where = ["."]
|
|
23
|
+
include = ["firefly*"]
|