lorem-jax 0.1.0__py3-none-any.whl
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.
- lorem/__init__.py +21 -0
- lorem/_install_ipi_driver.py +31 -0
- lorem/_version.py +24 -0
- lorem/batching.py +113 -0
- lorem/calculator.py +245 -0
- lorem/ipi.py +42 -0
- lorem/models/__init__.py +4 -0
- lorem/models/backbone.py +247 -0
- lorem/models/bec.py +378 -0
- lorem/models/mlip.py +299 -0
- lorem/neighborlist.py +111 -0
- lorem/train.py +1034 -0
- lorem/transforms.py +176 -0
- lorem_jax-0.1.0.dist-info/METADATA +226 -0
- lorem_jax-0.1.0.dist-info/RECORD +19 -0
- lorem_jax-0.1.0.dist-info/WHEEL +5 -0
- lorem_jax-0.1.0.dist-info/entry_points.txt +3 -0
- lorem_jax-0.1.0.dist-info/licenses/LICENSE +28 -0
- lorem_jax-0.1.0.dist-info/top_level.txt +1 -0
lorem/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from opsis.comms import Comms
|
|
2
|
+
|
|
3
|
+
from ._version import __version__
|
|
4
|
+
from .batching import to_batch, to_sample
|
|
5
|
+
from .calculator import Calculator as LOREMCalculator
|
|
6
|
+
from .models.bec import LoremBEC
|
|
7
|
+
from .models.mlip import Lorem
|
|
8
|
+
from .transforms import ToBatch, ToSample
|
|
9
|
+
|
|
10
|
+
comms = Comms("lorem")
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"__version__",
|
|
14
|
+
"Lorem",
|
|
15
|
+
"LoremBEC",
|
|
16
|
+
"LOREMCalculator",
|
|
17
|
+
"to_sample",
|
|
18
|
+
"to_batch",
|
|
19
|
+
"ToSample",
|
|
20
|
+
"ToBatch",
|
|
21
|
+
]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Helper to install the LOREM driver into i-PI."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from importlib.util import find_spec
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def install_ipi_driver():
|
|
9
|
+
"""Copy the installed `lorem.py` into the i-PI `pes` directory."""
|
|
10
|
+
|
|
11
|
+
# Locate i-PI installation
|
|
12
|
+
ipi_spec = find_spec("ipi")
|
|
13
|
+
if ipi_spec is None or not ipi_spec.submodule_search_locations:
|
|
14
|
+
raise RuntimeError(
|
|
15
|
+
"i-PI installation not found. Install i-PI and rerun "
|
|
16
|
+
"`lorem-install-ipi-driver`.",
|
|
17
|
+
)
|
|
18
|
+
pes_dir = Path(ipi_spec.submodule_search_locations[0]) / "pes"
|
|
19
|
+
|
|
20
|
+
# Locate source driver module
|
|
21
|
+
source_spec = find_spec("lorem.ipi")
|
|
22
|
+
if source_spec is None or source_spec.origin is None:
|
|
23
|
+
raise FileNotFoundError("Could not locate lorem.ipi module")
|
|
24
|
+
source_path = Path(source_spec.origin)
|
|
25
|
+
|
|
26
|
+
target_path = pes_dir / "lorem.py"
|
|
27
|
+
shutil.copy(source_path, target_path)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if __name__ == "__main__":
|
|
31
|
+
install_ipi_driver()
|
lorem/_version.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# file generated by vcs-versioning
|
|
2
|
+
# don't change, don't track in version control
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
"__version__",
|
|
7
|
+
"__version_tuple__",
|
|
8
|
+
"version",
|
|
9
|
+
"version_tuple",
|
|
10
|
+
"__commit_id__",
|
|
11
|
+
"commit_id",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
version: str
|
|
15
|
+
__version__: str
|
|
16
|
+
__version_tuple__: tuple[int | str, ...]
|
|
17
|
+
version_tuple: tuple[int | str, ...]
|
|
18
|
+
commit_id: str | None
|
|
19
|
+
__commit_id__: str | None
|
|
20
|
+
|
|
21
|
+
__version__ = version = '0.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 1, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
lorem/batching.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
from collections import namedtuple
|
|
4
|
+
|
|
5
|
+
from jaxpme.batched_mixed.batching import get_batch as jaxpme_batcher
|
|
6
|
+
from jaxpme.batched_mixed.batching import prepare as jaxpme_prepare
|
|
7
|
+
from marathon.data.batching import batch_labels
|
|
8
|
+
from marathon.data.properties import DEFAULT_PROPERTIES
|
|
9
|
+
from marathon.data.sample import to_labels
|
|
10
|
+
from marathon.utils import next_size
|
|
11
|
+
|
|
12
|
+
Batch = namedtuple(
|
|
13
|
+
"Batch",
|
|
14
|
+
(
|
|
15
|
+
"atomic_numbers",
|
|
16
|
+
"sr",
|
|
17
|
+
"nopbc",
|
|
18
|
+
"pbc",
|
|
19
|
+
"labels",
|
|
20
|
+
),
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
Sample = namedtuple(
|
|
25
|
+
"Sample",
|
|
26
|
+
(
|
|
27
|
+
"structure",
|
|
28
|
+
"labels",
|
|
29
|
+
),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def to_batch(
|
|
34
|
+
samples,
|
|
35
|
+
keys,
|
|
36
|
+
batch_size=None,
|
|
37
|
+
strategies={"default": "powers_of_2"},
|
|
38
|
+
shapes=None,
|
|
39
|
+
properties=DEFAULT_PROPERTIES,
|
|
40
|
+
):
|
|
41
|
+
if batch_size is not None:
|
|
42
|
+
assert batch_size > len(samples)
|
|
43
|
+
else:
|
|
44
|
+
batch_size = next_size(len(samples) + 1, strategy="powers_of_2")
|
|
45
|
+
|
|
46
|
+
labels, structures = [], []
|
|
47
|
+
|
|
48
|
+
for sample in samples:
|
|
49
|
+
labels.append(sample.labels)
|
|
50
|
+
structures.append(sample.structure)
|
|
51
|
+
|
|
52
|
+
if shapes is None:
|
|
53
|
+
default = strategies.pop("default", "powers_of_2")
|
|
54
|
+
_, sr, nopbc, pbc = jaxpme_batcher(
|
|
55
|
+
structures,
|
|
56
|
+
strategy=default,
|
|
57
|
+
num_structures_pbc=strategies.get("fine", default),
|
|
58
|
+
num_pairs_nonpbc=strategies.get("coarse", default),
|
|
59
|
+
num_pairs=strategies.get("coarse", default),
|
|
60
|
+
num_structures=batch_size,
|
|
61
|
+
)
|
|
62
|
+
else:
|
|
63
|
+
kwargs = {
|
|
64
|
+
"num_structures": batch_size,
|
|
65
|
+
"num_structures_pbc": shapes["pbc"],
|
|
66
|
+
"num_atoms": shapes["atoms"],
|
|
67
|
+
"num_atoms_pbc": shapes["atoms_pbc"],
|
|
68
|
+
"num_pairs": shapes["pairs"],
|
|
69
|
+
"num_pairs_nonpbc": shapes["pairs_nonpbc"],
|
|
70
|
+
"num_k": shapes["k"],
|
|
71
|
+
"strategy": "multiples",
|
|
72
|
+
}
|
|
73
|
+
_, sr, nopbc, pbc = jaxpme_batcher(
|
|
74
|
+
structures,
|
|
75
|
+
**kwargs,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
num_structures = sr.cell.shape[0]
|
|
79
|
+
num_atoms = sr.positions.shape[0]
|
|
80
|
+
|
|
81
|
+
atomic_numbers = np.zeros(num_atoms, dtype=int)
|
|
82
|
+
Z = np.concatenate([sample.structure["atomic_numbers"] for sample in samples])
|
|
83
|
+
atomic_numbers[: len(Z)] = Z
|
|
84
|
+
|
|
85
|
+
labels = batch_labels(labels, num_structures, num_atoms, keys, properties=properties)
|
|
86
|
+
|
|
87
|
+
return Batch(atomic_numbers, sr, nopbc, pbc, labels)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def to_sample(
|
|
91
|
+
atoms,
|
|
92
|
+
cutoff,
|
|
93
|
+
keys=None,
|
|
94
|
+
energy=True,
|
|
95
|
+
forces=True,
|
|
96
|
+
stress=False,
|
|
97
|
+
lr_wavelength=None,
|
|
98
|
+
smearing=None,
|
|
99
|
+
properties=DEFAULT_PROPERTIES,
|
|
100
|
+
):
|
|
101
|
+
structure = jaxpme_prepare(
|
|
102
|
+
atoms, cutoff, lr_wavelength=lr_wavelength, smearing=smearing, dtype=np.float32
|
|
103
|
+
)
|
|
104
|
+
labels = to_labels(
|
|
105
|
+
atoms,
|
|
106
|
+
keys=keys,
|
|
107
|
+
energy=energy,
|
|
108
|
+
forces=forces,
|
|
109
|
+
stress=stress,
|
|
110
|
+
properties=properties,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return Sample(structure, labels)
|
lorem/calculator.py
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import jax
|
|
3
|
+
import jax.numpy as jnp
|
|
4
|
+
|
|
5
|
+
jax.config.update("jax_default_matmul_precision", "float32")
|
|
6
|
+
|
|
7
|
+
from ase.calculators.calculator import (
|
|
8
|
+
BaseCalculator,
|
|
9
|
+
PropertyNotImplementedError,
|
|
10
|
+
)
|
|
11
|
+
from marathon.emit.checkpoint import read_msgpack
|
|
12
|
+
from marathon.io import from_dict, read_yaml
|
|
13
|
+
|
|
14
|
+
from lorem.neighborlist import NeighborListCache
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Calculator(BaseCalculator):
|
|
18
|
+
name = "lorem"
|
|
19
|
+
parameters = {}
|
|
20
|
+
|
|
21
|
+
def todict(self):
|
|
22
|
+
return self.parameters
|
|
23
|
+
|
|
24
|
+
# default; rebuilt per-instance in __init__ from what the model predicts
|
|
25
|
+
implemented_properties = ["energy", "forces"]
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
pred_fn,
|
|
30
|
+
species_weights,
|
|
31
|
+
params,
|
|
32
|
+
cutoff,
|
|
33
|
+
atoms=None,
|
|
34
|
+
stress=False,
|
|
35
|
+
bec=False,
|
|
36
|
+
add_offset=True,
|
|
37
|
+
double_precision=False,
|
|
38
|
+
skin=0.25,
|
|
39
|
+
):
|
|
40
|
+
self.params = params
|
|
41
|
+
self.cutoff = cutoff
|
|
42
|
+
self.skin = skin
|
|
43
|
+
self.add_offset = add_offset
|
|
44
|
+
self.double_precision = double_precision
|
|
45
|
+
|
|
46
|
+
self._nl_cache = NeighborListCache(skin=skin)
|
|
47
|
+
|
|
48
|
+
# only advertise what the model actually produces
|
|
49
|
+
self.implemented_properties = ["energy", "forces"]
|
|
50
|
+
if stress:
|
|
51
|
+
self.implemented_properties.append("stress")
|
|
52
|
+
if bec:
|
|
53
|
+
self.implemented_properties.append("born_effective_charges")
|
|
54
|
+
|
|
55
|
+
predict_fn = lambda params, batch: pred_fn(params, batch, stress=stress)
|
|
56
|
+
|
|
57
|
+
self.predict_fn = jax.jit(predict_fn)
|
|
58
|
+
self.species_weights = species_weights
|
|
59
|
+
|
|
60
|
+
self.atoms = None
|
|
61
|
+
self.batch = None
|
|
62
|
+
self.results = {}
|
|
63
|
+
if atoms is not None:
|
|
64
|
+
self.setup(atoms)
|
|
65
|
+
|
|
66
|
+
@classmethod
|
|
67
|
+
def from_model(cls, model, params=None, species_weights=None, **kwargs):
|
|
68
|
+
"""Create a Calculator from a Lorem model instance."""
|
|
69
|
+
if params is None:
|
|
70
|
+
params = model.init(jax.random.key(0), *model.dummy_inputs())
|
|
71
|
+
if species_weights is None:
|
|
72
|
+
species_weights = {}
|
|
73
|
+
kwargs.setdefault("add_offset", False)
|
|
74
|
+
kwargs.setdefault("bec", _model_predicts_bec(model))
|
|
75
|
+
return cls(model.predict, species_weights, params, model.cutoff, **kwargs)
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_checkpoint(
|
|
79
|
+
cls,
|
|
80
|
+
folder,
|
|
81
|
+
**kwargs,
|
|
82
|
+
):
|
|
83
|
+
from pathlib import Path
|
|
84
|
+
|
|
85
|
+
folder = Path(folder)
|
|
86
|
+
|
|
87
|
+
model = from_dict(read_yaml(folder / "model/model.yaml"))
|
|
88
|
+
|
|
89
|
+
_ = model.init(jax.random.key(1), *model.dummy_inputs())
|
|
90
|
+
|
|
91
|
+
baseline = read_yaml(folder / "model/baseline.yaml")
|
|
92
|
+
species_to_weight = baseline["elemental"]
|
|
93
|
+
|
|
94
|
+
params = read_msgpack(folder / "model/model.msgpack")
|
|
95
|
+
|
|
96
|
+
kwargs.setdefault("bec", _model_predicts_bec(model))
|
|
97
|
+
return cls(model.predict, species_to_weight, params, model.cutoff, **kwargs)
|
|
98
|
+
|
|
99
|
+
def update(self, atoms):
|
|
100
|
+
if self._nl_cache.needs_update(atoms):
|
|
101
|
+
# Structural change or combined displacement beyond skin
|
|
102
|
+
self.results = {}
|
|
103
|
+
self.atoms = atoms.copy()
|
|
104
|
+
self.setup(atoms)
|
|
105
|
+
elif self.atoms is None or not self._geometry_unchanged(atoms):
|
|
106
|
+
# Positions and/or cell changed but within skin budget
|
|
107
|
+
self.results = {}
|
|
108
|
+
self.atoms = atoms.copy()
|
|
109
|
+
self._update_geometry(atoms)
|
|
110
|
+
|
|
111
|
+
def _geometry_unchanged(self, atoms):
|
|
112
|
+
return np.array_equal(
|
|
113
|
+
atoms.get_positions(), self.atoms.get_positions()
|
|
114
|
+
) and np.array_equal(atoms.get_cell()[:], self.atoms.get_cell()[:])
|
|
115
|
+
|
|
116
|
+
def setup(self, atoms):
|
|
117
|
+
from lorem.batching import to_batch, to_sample
|
|
118
|
+
|
|
119
|
+
nl_cutoff = self.cutoff + self.skin
|
|
120
|
+
|
|
121
|
+
# Derive Ewald parameters from physical cutoff so the long-range
|
|
122
|
+
# decomposition is unchanged when using the extended cutoff.
|
|
123
|
+
lr_wavelength = self.cutoff / 8.0
|
|
124
|
+
smearing = lr_wavelength * 2.0
|
|
125
|
+
|
|
126
|
+
sample = to_sample(
|
|
127
|
+
atoms,
|
|
128
|
+
nl_cutoff,
|
|
129
|
+
lr_wavelength=lr_wavelength,
|
|
130
|
+
smearing=smearing,
|
|
131
|
+
energy=False,
|
|
132
|
+
forces=False,
|
|
133
|
+
stress=False,
|
|
134
|
+
)
|
|
135
|
+
batch = to_batch([sample], [])
|
|
136
|
+
self.batch = jax.tree.map(lambda x: jnp.array(x), batch)
|
|
137
|
+
|
|
138
|
+
max_cell_shift = int(np.abs(np.array(self.batch.sr.cell_shifts)).max())
|
|
139
|
+
self._nl_cache.save_reference(atoms, max_cell_shift=max_cell_shift)
|
|
140
|
+
|
|
141
|
+
def _update_geometry(self, atoms):
|
|
142
|
+
"""Update positions and cell in cached batch without rebuilding
|
|
143
|
+
the neighbor list. The model recomputes R_ij from the updated
|
|
144
|
+
sr.positions and sr.cell, and the Ewald calculator recomputes
|
|
145
|
+
k-vectors from sr.cell (pbc.k_grid stores only integer frequency
|
|
146
|
+
indices). So forces, energy, and stress remain correct."""
|
|
147
|
+
sr = self.batch.sr
|
|
148
|
+
n_atoms = len(atoms)
|
|
149
|
+
|
|
150
|
+
positions = np.zeros(np.array(sr.positions).shape, dtype=np.float32)
|
|
151
|
+
positions[:n_atoms] = atoms.get_positions()
|
|
152
|
+
|
|
153
|
+
cell = np.array(sr.cell)
|
|
154
|
+
new_cell = atoms.get_cell()[:].astype(np.float32)
|
|
155
|
+
if atoms.get_pbc().sum() == 2:
|
|
156
|
+
from jaxpme.batched_mixed.batching import shrink_2d_cell
|
|
157
|
+
|
|
158
|
+
new_cell = shrink_2d_cell(new_cell, atoms.get_pbc(), positions[:n_atoms])
|
|
159
|
+
cell[0] = new_cell
|
|
160
|
+
|
|
161
|
+
new_sr = sr._replace(
|
|
162
|
+
positions=jnp.array(positions),
|
|
163
|
+
cell=jnp.array(cell),
|
|
164
|
+
)
|
|
165
|
+
self.batch = self.batch._replace(sr=new_sr)
|
|
166
|
+
|
|
167
|
+
def calculate(
|
|
168
|
+
self,
|
|
169
|
+
atoms=None,
|
|
170
|
+
properties=None,
|
|
171
|
+
system_changes=None,
|
|
172
|
+
**kwargs,
|
|
173
|
+
):
|
|
174
|
+
self.update(atoms)
|
|
175
|
+
|
|
176
|
+
results = self.predict_fn(self.params, self.batch)
|
|
177
|
+
|
|
178
|
+
actual_results = {}
|
|
179
|
+
for key in self.implemented_properties:
|
|
180
|
+
if key == "energy":
|
|
181
|
+
actual_results[key] = float(
|
|
182
|
+
results[key][self.batch.sr.structure_mask].squeeze()
|
|
183
|
+
)
|
|
184
|
+
elif key == "forces":
|
|
185
|
+
actual_results[key] = np.array(
|
|
186
|
+
results[key][self.batch.sr.atom_mask].reshape(-1, 3),
|
|
187
|
+
dtype=np.float32,
|
|
188
|
+
)
|
|
189
|
+
elif key == "stress":
|
|
190
|
+
virial = np.array(
|
|
191
|
+
results[key][self.batch.sr.structure_mask].reshape(3, 3),
|
|
192
|
+
dtype=np.float32,
|
|
193
|
+
)
|
|
194
|
+
volume = atoms.get_volume()
|
|
195
|
+
from ase.stress import full_3x3_to_voigt_6_stress
|
|
196
|
+
|
|
197
|
+
actual_results[key] = full_3x3_to_voigt_6_stress(virial / volume)
|
|
198
|
+
|
|
199
|
+
# BEC passthrough: when model outputs "apt" (e.g. LoremBEC), expose as
|
|
200
|
+
# "born_effective_charges" in (natoms, 3, 3) layout for ase compatibility
|
|
201
|
+
if "apt" in results:
|
|
202
|
+
actual_results["born_effective_charges"] = np.array(
|
|
203
|
+
results["apt"][self.batch.sr.atom_mask].reshape(-1, 3, 3),
|
|
204
|
+
dtype=np.float32,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if self.add_offset:
|
|
208
|
+
energy_offset = np.sum(
|
|
209
|
+
[self.species_weights[Z] for Z in atoms.get_atomic_numbers()]
|
|
210
|
+
)
|
|
211
|
+
actual_results["energy"] += energy_offset
|
|
212
|
+
|
|
213
|
+
self.results = actual_results
|
|
214
|
+
return actual_results
|
|
215
|
+
|
|
216
|
+
def get_property(self, name, atoms=None, allow_calculation=True):
|
|
217
|
+
if name not in self.implemented_properties:
|
|
218
|
+
raise PropertyNotImplementedError(f"{name} property not implemented")
|
|
219
|
+
|
|
220
|
+
self.update(atoms)
|
|
221
|
+
|
|
222
|
+
if name not in self.results:
|
|
223
|
+
if not allow_calculation:
|
|
224
|
+
return None
|
|
225
|
+
self.calculate(atoms=atoms)
|
|
226
|
+
|
|
227
|
+
if name not in self.results:
|
|
228
|
+
# For some reason the calculator was not able to do what we want,
|
|
229
|
+
# and that is OK.
|
|
230
|
+
raise PropertyNotImplementedError(f"{name} property not present in results!")
|
|
231
|
+
|
|
232
|
+
result = self.results[name]
|
|
233
|
+
if isinstance(result, np.ndarray):
|
|
234
|
+
result = result.copy()
|
|
235
|
+
return result
|
|
236
|
+
|
|
237
|
+
def get_potential_energy(self, atoms=None, force_consistent=True):
|
|
238
|
+
# force_consistent is ignored; we are always consistent
|
|
239
|
+
return self.get_property(name="energy", atoms=atoms)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _model_predicts_bec(model):
|
|
243
|
+
from lorem.models.bec import LoremBEC
|
|
244
|
+
|
|
245
|
+
return isinstance(model, LoremBEC)
|
lorem/ipi.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""LOREM driver for i-PI."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from ipi.pes._ase import ASEDriver
|
|
8
|
+
|
|
9
|
+
from lorem.calculator import Calculator
|
|
10
|
+
|
|
11
|
+
__DRIVER_NAME__ = "lorem"
|
|
12
|
+
__DRIVER_CLASS__ = "LOREM_driver"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class LOREM_driver(ASEDriver):
|
|
16
|
+
def __init__(self, template, model_path, *args, skin=0.25, **kwargs):
|
|
17
|
+
self.model_path = model_path
|
|
18
|
+
self.skin = skin
|
|
19
|
+
super().__init__(template, *args, **kwargs)
|
|
20
|
+
# only advertise BEC if the model predicts it
|
|
21
|
+
if "born_effective_charges" in self.ase_calculator.implemented_properties:
|
|
22
|
+
self.capabilities.append("born_effective_charges")
|
|
23
|
+
|
|
24
|
+
def check_parameters(self):
|
|
25
|
+
super().check_parameters()
|
|
26
|
+
has_stress = "stress" in self.capabilities
|
|
27
|
+
self.ase_calculator = Calculator.from_checkpoint(
|
|
28
|
+
self.model_path, stress=has_stress, skin=self.skin
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def compute_structure(self, cell, pos):
|
|
32
|
+
pot_ipi, force_ipi, vir_ipi, extras = super().compute_structure(cell, pos)
|
|
33
|
+
|
|
34
|
+
# BEC passthrough: when model outputs "born_effective_charges" (e.g. LoremBEC),
|
|
35
|
+
# expose as "BEC" in (3*natoms, 3) layout for i-PI compatibility
|
|
36
|
+
if extras and "born_effective_charges" in extras:
|
|
37
|
+
extras = json.loads(extras)
|
|
38
|
+
BEC = np.reshape(extras.pop("born_effective_charges"), (-1, 3))
|
|
39
|
+
extras["BEC"] = BEC.tolist()
|
|
40
|
+
extras = json.dumps(extras)
|
|
41
|
+
|
|
42
|
+
return pot_ipi, force_ipi, vir_ipi, extras
|
lorem/models/__init__.py
ADDED