pack-mm 0.0.21__tar.gz → 0.0.24__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.
- {pack_mm-0.0.21 → pack_mm-0.0.24}/PKG-INFO +8 -3
- {pack_mm-0.0.21 → pack_mm-0.0.24}/README.md +7 -2
- {pack_mm-0.0.21 → pack_mm-0.0.24}/pack_mm/cli/packmm.py +45 -4
- {pack_mm-0.0.21 → pack_mm-0.0.24}/pack_mm/core/core.py +214 -60
- {pack_mm-0.0.21 → pack_mm-0.0.24}/pyproject.toml +3 -1
- pack_mm-0.0.24/tests/test_advanced.py +85 -0
- pack_mm-0.0.21/tests/test_packmm.py → pack_mm-0.0.24/tests/test_cli.py +77 -10
- pack_mm-0.0.24/tests/test_core.py +416 -0
- pack_mm-0.0.21/tests/test_core.py +0 -178
- {pack_mm-0.0.21 → pack_mm-0.0.24}/LICENSE +0 -0
- {pack_mm-0.0.21 → pack_mm-0.0.24}/pack_mm/__init__.py +0 -0
- {pack_mm-0.0.21 → pack_mm-0.0.24}/tests/__init__.py +0 -0
- {pack_mm-0.0.21 → pack_mm-0.0.24}/tests/utils.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: pack-mm
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.24
|
4
4
|
Summary: packing materials and molecules in boxes using for machine learnt interatomic potentials
|
5
5
|
Author: Alin M. Elena
|
6
6
|
Classifier: Programming Language :: Python
|
@@ -42,7 +42,7 @@ It provides both a cli and a python api, with some examples below.
|
|
42
42
|
uv pip install pack-mm
|
43
43
|
|
44
44
|
```
|
45
|
-
or install the
|
45
|
+
or install the latest
|
46
46
|
|
47
47
|
```bash
|
48
48
|
|
@@ -50,6 +50,11 @@ or install the lates
|
|
50
50
|
|
51
51
|
```
|
52
52
|
|
53
|
+
## Jupyter notebook examples
|
54
|
+
|
55
|
+
|
56
|
+
- [Basics](docs/source/tutorials/basics.ipynb) [](https://colab.research.google.com/github/ddmms/pack-mm/blob/main/docs/source/tutorials/basic.ipynb)
|
57
|
+
|
53
58
|
## CLI examples
|
54
59
|
|
55
60
|
|
@@ -125,7 +130,7 @@ packmm --system Pd-super.cif --molecule H2 --nmols 50 --where anywhere --mode
|
|
125
130
|
|
126
131
|
before optimisation
|
127
132
|
|
128
|
-

|
129
134
|
|
130
135
|
|
131
136
|
after optimisation
|
@@ -21,7 +21,7 @@ It provides both a cli and a python api, with some examples below.
|
|
21
21
|
uv pip install pack-mm
|
22
22
|
|
23
23
|
```
|
24
|
-
or install the
|
24
|
+
or install the latest
|
25
25
|
|
26
26
|
```bash
|
27
27
|
|
@@ -29,6 +29,11 @@ or install the lates
|
|
29
29
|
|
30
30
|
```
|
31
31
|
|
32
|
+
## Jupyter notebook examples
|
33
|
+
|
34
|
+
|
35
|
+
- [Basics](docs/source/tutorials/basics.ipynb) [](https://colab.research.google.com/github/ddmms/pack-mm/blob/main/docs/source/tutorials/basic.ipynb)
|
36
|
+
|
32
37
|
## CLI examples
|
33
38
|
|
34
39
|
|
@@ -104,7 +109,7 @@ packmm --system Pd-super.cif --molecule H2 --nmols 50 --where anywhere --mode
|
|
104
109
|
|
105
110
|
before optimisation
|
106
111
|
|
107
|
-

|
108
113
|
|
109
114
|
|
110
115
|
after optimisation
|
@@ -25,6 +25,22 @@ class InsertionMethod(str, Enum):
|
|
25
25
|
ELLIPSOID = "ellipsoid"
|
26
26
|
|
27
27
|
|
28
|
+
class InsertionStrategy(str, Enum):
|
29
|
+
"""Insertion options."""
|
30
|
+
|
31
|
+
# propose randomly a point
|
32
|
+
MC = "mc"
|
33
|
+
# hybrid monte carlo
|
34
|
+
HMC = "hmc"
|
35
|
+
|
36
|
+
|
37
|
+
class RelaxStrategy(str, Enum):
|
38
|
+
"""Relaxation options."""
|
39
|
+
|
40
|
+
GEOMETRY_OPTIMISATION = "geometry_optimisation"
|
41
|
+
MD = "md"
|
42
|
+
|
43
|
+
|
28
44
|
app = typer.Typer(no_args_is_help=True)
|
29
45
|
|
30
46
|
|
@@ -44,12 +60,26 @@ def packmm(
|
|
44
60
|
ntries: int = typer.Option(
|
45
61
|
50, help="Maximum number of attempts to insert each molecule."
|
46
62
|
),
|
63
|
+
every: int = typer.Option(
|
64
|
+
-1, help="Run MD-NVE or Geometry optimisation everyth insertion."
|
65
|
+
),
|
47
66
|
seed: int = typer.Option(2025, help="Random seed for reproducibility."),
|
67
|
+
md_steps: int = typer.Option(10, help="Number of steps to run MD."),
|
68
|
+
md_timestep: float = typer.Option(1.0, help="Timestep for MD integration, in fs."),
|
48
69
|
where: InsertionMethod = typer.Option(
|
49
70
|
InsertionMethod.ANYWHERE,
|
50
71
|
help="""Where to insert the molecule. Choices: 'anywhere', 'sphere',
|
51
72
|
'box', 'cylinderZ', 'cylinderY', 'cylinderX', 'ellipsoid'.""",
|
52
73
|
),
|
74
|
+
insert_strategy: InsertionStrategy = typer.Option(
|
75
|
+
InsertionStrategy.MC,
|
76
|
+
help="""How to insert a new molecule. Choices: 'mc', 'hmc',""",
|
77
|
+
),
|
78
|
+
relax_strategy: RelaxStrategy = typer.Option(
|
79
|
+
RelaxStrategy.GEOMETRY_OPTIMISATION,
|
80
|
+
help="""How to relax the system to get more favourable structures.
|
81
|
+
Choices: 'geometry_optimisation', 'md',""",
|
82
|
+
),
|
53
83
|
centre: str | None = typer.Option(
|
54
84
|
None,
|
55
85
|
help="""Centre of the insertion zone, coordinates in Å,
|
@@ -84,6 +114,9 @@ def packmm(
|
|
84
114
|
temperature: float = typer.Option(
|
85
115
|
300.0, help="Temperature for the Monte Carlo acceptance rule."
|
86
116
|
),
|
117
|
+
md_temperature: float = typer.Option(
|
118
|
+
100.0, help="Temperature for the Molecular dynamics relaxation."
|
119
|
+
),
|
87
120
|
cell_a: float = typer.Option(
|
88
121
|
20.0, help="Side of the empty box along the x-axis in Å."
|
89
122
|
),
|
@@ -125,6 +158,12 @@ def packmm(
|
|
125
158
|
print(f"{fmax=}")
|
126
159
|
print(f"{geometry=}")
|
127
160
|
print(f"{out_path=}")
|
161
|
+
print(f"{every=}")
|
162
|
+
print(f"insert_strategy={insert_strategy.value}")
|
163
|
+
print(f"relax_strategy={relax_strategy.value}")
|
164
|
+
print(f"{md_steps=}")
|
165
|
+
print(f"{md_timestep=}")
|
166
|
+
print(f"{md_temperature=}")
|
128
167
|
if nmols == -1:
|
129
168
|
print("nothing to do, no molecule to insert")
|
130
169
|
raise typer.Exit(0)
|
@@ -161,8 +200,10 @@ def packmm(
|
|
161
200
|
cell_b=cell_b,
|
162
201
|
cell_c=cell_c,
|
163
202
|
out_path=out_path,
|
203
|
+
every=every,
|
204
|
+
relax_strategy=relax_strategy,
|
205
|
+
insert_strategy=insert_strategy,
|
206
|
+
md_steps=md_steps,
|
207
|
+
md_timestep=md_timestep,
|
208
|
+
md_temperature=md_temperature,
|
164
209
|
)
|
165
|
-
|
166
|
-
|
167
|
-
if __name__ == "__main__":
|
168
|
-
app()
|
@@ -13,6 +13,7 @@ from ase.build import molecule as build_molecule
|
|
13
13
|
from ase.io import read, write
|
14
14
|
from ase.units import kB
|
15
15
|
from janus_core.calculations.geom_opt import GeomOpt
|
16
|
+
from janus_core.calculations.md import NVE
|
16
17
|
from janus_core.helpers.mlip_calculators import choose_calculator
|
17
18
|
from numpy import cos, exp, pi, random, sin, sqrt
|
18
19
|
|
@@ -130,7 +131,7 @@ def random_point_in_cylinder(
|
|
130
131
|
return (x, y, z)
|
131
132
|
|
132
133
|
|
133
|
-
def validate_value(label, x):
|
134
|
+
def validate_value(label: str, x: float | int) -> None:
|
134
135
|
"""Validate input value, and raise an exception."""
|
135
136
|
if x is not None and x < 0.0:
|
136
137
|
err = f"Invalid {label}, needs to be positive"
|
@@ -138,8 +139,54 @@ def validate_value(label, x):
|
|
138
139
|
raise Exception(err)
|
139
140
|
|
140
141
|
|
142
|
+
def set_random_seed(seed: int) -> None:
|
143
|
+
"""Set random seed."""
|
144
|
+
random.seed(seed)
|
145
|
+
|
146
|
+
|
147
|
+
def set_defaults(
|
148
|
+
cell: (float, float, float),
|
149
|
+
centre: (float, float, float) | None = None,
|
150
|
+
where: str | None = None,
|
151
|
+
a: float | None = None,
|
152
|
+
b: float | None = None,
|
153
|
+
c: float | None = None,
|
154
|
+
radius: float | None = None,
|
155
|
+
height: float | None = None,
|
156
|
+
) -> tuple(
|
157
|
+
(float, float, float),
|
158
|
+
float | None,
|
159
|
+
float | None,
|
160
|
+
float | None,
|
161
|
+
float | None,
|
162
|
+
float | None,
|
163
|
+
):
|
164
|
+
"""Set defaults for insertion areas."""
|
165
|
+
if centre is None:
|
166
|
+
centre = (cell[0] * 0.5, cell[1] * 0.5, cell[2] * 0.5)
|
167
|
+
|
168
|
+
if where == "anywhere":
|
169
|
+
a, b, c = cell[0], cell[1], cell[2]
|
170
|
+
elif where == "sphere":
|
171
|
+
radius = radius or min(cell) * 0.5
|
172
|
+
elif where == "cylinderZ":
|
173
|
+
radius = radius or min(cell[0], cell[1]) * 0.5
|
174
|
+
height = height or 0.5 * cell[2]
|
175
|
+
elif where == "cylinderY":
|
176
|
+
radius = radius or min(cell[0], cell[2]) * 0.5
|
177
|
+
height = height or 0.5 * cell[1]
|
178
|
+
elif where == "cylinderX":
|
179
|
+
radius = radius or min(cell[2], cell[1]) * 0.5
|
180
|
+
height = height or 0.5 * cell[0]
|
181
|
+
elif where == "box":
|
182
|
+
a, b, c = a or cell[0], b or cell[1], c or cell[2]
|
183
|
+
elif where == "ellipsoid":
|
184
|
+
a, b, c = a or cell[0] * 0.5, b or cell[1] * 0.5, c or cell[2] * 0.5
|
185
|
+
return (centre, a, b, c, radius, height)
|
186
|
+
|
187
|
+
|
141
188
|
def pack_molecules(
|
142
|
-
system: str = None,
|
189
|
+
system: str | Atoms = None,
|
143
190
|
molecule: str = "H2O",
|
144
191
|
nmols: int = -1,
|
145
192
|
arch: str = "cpu",
|
@@ -161,13 +208,19 @@ def pack_molecules(
|
|
161
208
|
cell_b: float = None,
|
162
209
|
cell_c: float = None,
|
163
210
|
out_path: str = ".",
|
164
|
-
|
211
|
+
every: int = -1,
|
212
|
+
relax_strategy: str = "geometry_optimisation",
|
213
|
+
insert_strategy: str = "random",
|
214
|
+
md_steps: int = 10,
|
215
|
+
md_timestep: float = 1.0,
|
216
|
+
md_temperature: float = 100.0,
|
217
|
+
) -> tuple(float, Atoms):
|
165
218
|
"""
|
166
219
|
Pack molecules into a system based on the specified parameters.
|
167
220
|
|
168
221
|
Parameters
|
169
222
|
----------
|
170
|
-
system (str): Path to the system file or name of the system.
|
223
|
+
system (str|Atoms): Path to the system file or name of the system.
|
171
224
|
molecule (str): Path to the molecule file or name of the molecule.
|
172
225
|
nmols (int): Number of molecules to insert.
|
173
226
|
arch (str): Architecture for the calculator.
|
@@ -185,6 +238,19 @@ def pack_molecules(
|
|
185
238
|
geometry (bool): Whether to perform geometry optimization after insertion.
|
186
239
|
cell_a, cell_b, cell_c (float): Cell dimensions if system is empty.
|
187
240
|
out_path (str): path to save various outputs
|
241
|
+
every (int): After how many instertions to do a relaxation,
|
242
|
+
default -1 means none..
|
243
|
+
md_temperature (float): Temperature in Kelvin for MD.
|
244
|
+
md_steps (int): Number of steps for MD.
|
245
|
+
md_timestep (float): Timestep in fs for MD.
|
246
|
+
insert_strategy (str): Insert strategy, "random" or "md"
|
247
|
+
relax_strategy (str): Relax strategy, "geometry_optimisation" or "md"
|
248
|
+
|
249
|
+
Returns
|
250
|
+
-------
|
251
|
+
tuple: A tuple energy and Atoms object containing
|
252
|
+
original system and added molecules..
|
253
|
+
|
188
254
|
"""
|
189
255
|
kbt = temperature * kB
|
190
256
|
validate_value("temperature", temperature)
|
@@ -198,51 +264,34 @@ def pack_molecules(
|
|
198
264
|
validate_value("ntries", ntries)
|
199
265
|
validate_value("cell box cell a", cell_a)
|
200
266
|
validate_value("cell box cell b", cell_b)
|
201
|
-
validate_value("nmols", nmols)
|
202
267
|
validate_value("cell box cell c", cell_c)
|
268
|
+
validate_value("nmols", nmols)
|
269
|
+
validate_value("MD steps", md_steps)
|
270
|
+
validate_value("MD timestep", md_timestep)
|
271
|
+
validate_value("MD temperature", md_temperature)
|
203
272
|
|
204
|
-
|
273
|
+
set_random_seed(seed)
|
205
274
|
|
206
|
-
|
207
|
-
sys = read(system)
|
208
|
-
sysname = Path(system).stem
|
209
|
-
except Exception:
|
275
|
+
if system is None:
|
210
276
|
sys = Atoms(cell=[cell_a, cell_b, cell_c], pbc=[True, True, True])
|
211
|
-
sysname = "
|
212
|
-
|
213
|
-
|
277
|
+
sysname = ""
|
278
|
+
elif isinstance(system, Atoms):
|
279
|
+
sys = system.copy()
|
280
|
+
sysname = sys.get_chemical_formula()
|
281
|
+
else:
|
282
|
+
sys = read(system)
|
283
|
+
sysname = Path(system).stem + "+"
|
214
284
|
|
215
285
|
# Print summary
|
216
286
|
print(f"Inserting {nmols} {molecule} molecules in {sysname}.")
|
217
287
|
print(f"Using {arch} model {model} on {device}.")
|
218
288
|
print(f"Insert in {where}.")
|
219
289
|
|
220
|
-
|
221
|
-
center = (cell[0] * 0.5, cell[1] * 0.5, cell[2] * 0.5)
|
290
|
+
cell = sys.cell.lengths()
|
222
291
|
|
223
|
-
|
224
|
-
a, b, c
|
225
|
-
|
226
|
-
if radius is None:
|
227
|
-
radius = min(cell) * 0.5
|
228
|
-
elif where in ["cylinderZ", "cylinderY", "cylinderX"]:
|
229
|
-
if radius is None:
|
230
|
-
if where == "cylinderZ":
|
231
|
-
radius = min(cell[0], cell[1]) * 0.5
|
232
|
-
if height is None:
|
233
|
-
height = 0.5 * cell[2]
|
234
|
-
elif where == "cylinderY":
|
235
|
-
radius = min(cell[0], cell[2]) * 0.5
|
236
|
-
if height is None:
|
237
|
-
height = 0.5 * cell[1]
|
238
|
-
elif where == "cylinderX":
|
239
|
-
radius = min(cell[2], cell[1]) * 0.5
|
240
|
-
if height is None:
|
241
|
-
height = 0.5 * cell[0]
|
242
|
-
elif where == "box":
|
243
|
-
a, b, c = a or cell[0], b or cell[1], c or cell[2]
|
244
|
-
elif where == "ellipsoid":
|
245
|
-
a, b, c = a or cell[0], b or cell[1], c or cell[2]
|
292
|
+
center, a, b, c, radius, height = set_defaults(
|
293
|
+
cell, center, where, a, b, c, radius, height
|
294
|
+
)
|
246
295
|
|
247
296
|
calc = choose_calculator(arch=arch, model_path=model, device=device)
|
248
297
|
sys.calc = calc
|
@@ -250,7 +299,8 @@ def pack_molecules(
|
|
250
299
|
e = sys.get_potential_energy() if len(sys) > 0 else 0.0
|
251
300
|
|
252
301
|
csys = sys.copy()
|
253
|
-
|
302
|
+
i = 0
|
303
|
+
while i < nmols:
|
254
304
|
accept = False
|
255
305
|
for _itry in range(ntries):
|
256
306
|
mol = load_molecule(molecule)
|
@@ -259,6 +309,25 @@ def pack_molecules(
|
|
259
309
|
mol.translate(tv)
|
260
310
|
|
261
311
|
tsys = csys.copy() + mol.copy()
|
312
|
+
if insert_strategy == "hmc":
|
313
|
+
tsys = run_md_nve(
|
314
|
+
tsys, md_temperature, md_steps, md_timestep, arch, model, device
|
315
|
+
)
|
316
|
+
|
317
|
+
if every > 0 and _itry / every == 0:
|
318
|
+
tsys = save_the_day(
|
319
|
+
struct_path=tsys,
|
320
|
+
device=device,
|
321
|
+
arch=arch,
|
322
|
+
model=model,
|
323
|
+
fmax=fmax,
|
324
|
+
out_path=out_path,
|
325
|
+
md_temperature=md_temperature,
|
326
|
+
md_steps=md_steps,
|
327
|
+
md_timestep=md_timestep,
|
328
|
+
relax_strategy=relax_strategy,
|
329
|
+
)
|
330
|
+
|
262
331
|
tsys.calc = calc
|
263
332
|
en = tsys.get_potential_energy()
|
264
333
|
de = en - e
|
@@ -270,36 +339,43 @@ def pack_molecules(
|
|
270
339
|
if u <= acc:
|
271
340
|
accept = True
|
272
341
|
break
|
273
|
-
|
274
342
|
if accept:
|
275
343
|
csys = tsys.copy()
|
276
344
|
e = en
|
277
|
-
|
278
|
-
|
345
|
+
i += 1
|
346
|
+
print(f"Inserted particle {i}")
|
347
|
+
write(Path(out_path) / f"{sysname}{i}{Path(molecule).stem}.cif", csys)
|
279
348
|
else:
|
280
349
|
# Things are bad, maybe geomatry optimisation saves us
|
350
|
+
# once you hit here is bad, this can keep looping
|
281
351
|
print(f"Failed to insert particle {i + 1} after {ntries} tries")
|
282
|
-
|
283
|
-
|
352
|
+
csys = save_the_day(
|
353
|
+
csys,
|
284
354
|
device,
|
285
355
|
arch,
|
286
356
|
model,
|
287
357
|
fmax,
|
288
358
|
out_path,
|
359
|
+
md_temperature,
|
360
|
+
md_steps,
|
361
|
+
md_timestep,
|
362
|
+
relax_strategy,
|
289
363
|
)
|
364
|
+
|
290
365
|
energy_final = e
|
291
366
|
|
292
367
|
# Perform final geometry optimization if requested
|
293
368
|
if geometry:
|
294
|
-
energy_final = optimize_geometry(
|
295
|
-
f"{sysname}
|
369
|
+
energy_final, csys = optimize_geometry(
|
370
|
+
Path(out_path) / f"{sysname}{nmols}{Path(molecule).stem}.cif",
|
296
371
|
device,
|
297
372
|
arch,
|
298
373
|
model,
|
299
374
|
fmax,
|
300
375
|
out_path,
|
376
|
+
True,
|
301
377
|
)
|
302
|
-
return energy_final
|
378
|
+
return (energy_final, csys)
|
303
379
|
|
304
380
|
|
305
381
|
def load_molecule(molecule: str):
|
@@ -329,7 +405,6 @@ def get_insertion_position(
|
|
329
405
|
if where in ["cylinderZ", "cylinderY", "cylinderX"]:
|
330
406
|
axis = where[-1].lower()
|
331
407
|
return random_point_in_cylinder(center, radius, height, axis)
|
332
|
-
# now is anywhere
|
333
408
|
return random.random(3) * [a, b, c]
|
334
409
|
|
335
410
|
|
@@ -342,22 +417,101 @@ def rotate_molecule(mol):
|
|
342
417
|
return mol
|
343
418
|
|
344
419
|
|
420
|
+
def save_the_day(
|
421
|
+
struct_path: str | Atoms,
|
422
|
+
device: str = "",
|
423
|
+
arch: str = "",
|
424
|
+
model: str = "",
|
425
|
+
fmax: float = 0.01,
|
426
|
+
out_path: str = ".",
|
427
|
+
md_temperature: float = 100.0,
|
428
|
+
md_steps: int = 10,
|
429
|
+
md_timestep: float = 1.0,
|
430
|
+
relax_strategy: str = "geometry_optimisation",
|
431
|
+
) -> Atoms:
|
432
|
+
"""Geometry optimisation or MD to get a better structure."""
|
433
|
+
if relax_strategy == "geometry_optimisation":
|
434
|
+
_, a = optimize_geometry(
|
435
|
+
struct_path,
|
436
|
+
device,
|
437
|
+
arch,
|
438
|
+
model,
|
439
|
+
fmax,
|
440
|
+
out_path,
|
441
|
+
)
|
442
|
+
return a
|
443
|
+
if relax_strategy == "md":
|
444
|
+
return run_md_nve(
|
445
|
+
struct_path, md_temperature, md_steps, md_timestep, arch, model, device
|
446
|
+
)
|
447
|
+
return None
|
448
|
+
|
449
|
+
|
450
|
+
def run_md_nve(
|
451
|
+
struct_path: str | Atoms,
|
452
|
+
temp: float = 100.0,
|
453
|
+
steps: int = 10,
|
454
|
+
timestep: float = 1.0,
|
455
|
+
arch: str = "",
|
456
|
+
model: str = "",
|
457
|
+
device: str = "",
|
458
|
+
) -> Atoms:
|
459
|
+
"""Run nve simulation."""
|
460
|
+
if isinstance(struct_path, Atoms):
|
461
|
+
md = NVE(
|
462
|
+
struct=struct_path,
|
463
|
+
temp=temp,
|
464
|
+
device=device,
|
465
|
+
arch=arch,
|
466
|
+
calc_kwargs={"model_paths": model},
|
467
|
+
stats_every=1,
|
468
|
+
steps=steps,
|
469
|
+
timestep=timestep,
|
470
|
+
)
|
471
|
+
else:
|
472
|
+
md = NVE(
|
473
|
+
struct_path=struct_path,
|
474
|
+
temp=temp,
|
475
|
+
device=device,
|
476
|
+
arch=arch,
|
477
|
+
calc_kwargs={"model_paths": model},
|
478
|
+
stats_every=1,
|
479
|
+
steps=steps,
|
480
|
+
timestep=timestep,
|
481
|
+
)
|
482
|
+
md.run()
|
483
|
+
return md.struct
|
484
|
+
|
485
|
+
|
345
486
|
def optimize_geometry(
|
346
|
-
struct_path: str,
|
487
|
+
struct_path: str | Atoms,
|
347
488
|
device: str,
|
348
489
|
arch: str,
|
349
490
|
model: str,
|
350
491
|
fmax: float,
|
351
492
|
out_path: str = ".",
|
352
|
-
|
493
|
+
opt_cell: bool = False,
|
494
|
+
) -> tuple(float, Atoms):
|
353
495
|
"""Optimize the geometry of a structure."""
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
|
496
|
+
if isinstance(struct_path, Atoms):
|
497
|
+
geo = GeomOpt(
|
498
|
+
struct=struct_path,
|
499
|
+
device=device,
|
500
|
+
arch=arch,
|
501
|
+
fmax=fmax,
|
502
|
+
calc_kwargs={"model_paths": model},
|
503
|
+
filter_kwargs={"hydrostatic_strain": opt_cell},
|
504
|
+
)
|
505
|
+
geo.run()
|
506
|
+
else:
|
507
|
+
geo = GeomOpt(
|
508
|
+
struct_path=struct_path,
|
509
|
+
device=device,
|
510
|
+
arch=arch,
|
511
|
+
fmax=fmax,
|
512
|
+
calc_kwargs={"model_paths": model},
|
513
|
+
filter_kwargs={"hydrostatic_strain": opt_cell},
|
514
|
+
)
|
515
|
+
geo.run()
|
516
|
+
write(Path(out_path) / f"{Path(struct_path).stem}-opt.cif", geo.struct)
|
517
|
+
return (geo.struct.get_potential_energy(), geo.struct)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "pack-mm"
|
3
|
-
version = "0.0.
|
3
|
+
version = "0.0.24"
|
4
4
|
description = "packing materials and molecules in boxes using for machine learnt interatomic potentials"
|
5
5
|
authors = [
|
6
6
|
{ name = "Alin M. Elena" },
|
@@ -53,6 +53,8 @@ docs = [
|
|
53
53
|
"sphinx-autodoc-typehints<3.0.0,>=2.5.0",
|
54
54
|
"sphinx-collapse>=0.1.3",
|
55
55
|
"sphinx-copybutton<1.0.0,>=0.5.2",
|
56
|
+
"data-tutorials",
|
57
|
+
"weas-widget",
|
56
58
|
]
|
57
59
|
pre-commit = [
|
58
60
|
"pre-commit<4.0.0,>=3.6.0",
|
@@ -0,0 +1,85 @@
|
|
1
|
+
"""Advanced tests for packmm."""
|
2
|
+
|
3
|
+
# Author; alin m elena, alin@elena.re
|
4
|
+
# Contribs;
|
5
|
+
# Date: 23-02-2025
|
6
|
+
# ©alin m elena,
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
from ase.build import bulk
|
10
|
+
from ase.io import read, write
|
11
|
+
import pytest
|
12
|
+
from typer.testing import CliRunner
|
13
|
+
|
14
|
+
from pack_mm.cli.packmm import app
|
15
|
+
|
16
|
+
runner = CliRunner()
|
17
|
+
|
18
|
+
err = 1.0e-8
|
19
|
+
|
20
|
+
|
21
|
+
def test_packmm_hmc(tmp_path):
|
22
|
+
"""Check values."""
|
23
|
+
result = runner.invoke(
|
24
|
+
app,
|
25
|
+
[
|
26
|
+
"--nmols",
|
27
|
+
"2",
|
28
|
+
"--model",
|
29
|
+
"small-0b2",
|
30
|
+
"--insert-strategy",
|
31
|
+
"hmc",
|
32
|
+
"--seed",
|
33
|
+
"2042",
|
34
|
+
"--cell-a",
|
35
|
+
"10",
|
36
|
+
"--cell-b",
|
37
|
+
"10",
|
38
|
+
"--cell-c",
|
39
|
+
"10",
|
40
|
+
"--out-path",
|
41
|
+
tmp_path,
|
42
|
+
],
|
43
|
+
)
|
44
|
+
assert result.exit_code == 0
|
45
|
+
assert (tmp_path / "1H2O.cif").exists()
|
46
|
+
assert (tmp_path / "2H2O.cif").exists()
|
47
|
+
f = read(tmp_path / "2H2O.cif")
|
48
|
+
assert f[0].position == pytest.approx([7.83420049, 6.21661184, 5.21814887], abs=err)
|
49
|
+
assert (tmp_path / "2H2O-opt.cif").exists()
|
50
|
+
f = read(tmp_path / "2H2O-opt.cif")
|
51
|
+
assert f[0].position == pytest.approx([7.83120709, 6.21742874, 5.22597213], abs=err)
|
52
|
+
|
53
|
+
|
54
|
+
def test_packmm_every(tmp_path):
|
55
|
+
"""Check values."""
|
56
|
+
na = bulk("NaCl", "rocksalt", a=5.61, cubic=True)
|
57
|
+
write(tmp_path / "system.cif", na)
|
58
|
+
write("system.cif", na)
|
59
|
+
|
60
|
+
result = runner.invoke(
|
61
|
+
app,
|
62
|
+
[
|
63
|
+
"--system",
|
64
|
+
tmp_path / "system.cif",
|
65
|
+
"--molecule",
|
66
|
+
"H2",
|
67
|
+
"--every",
|
68
|
+
"1",
|
69
|
+
"--nmols",
|
70
|
+
"2",
|
71
|
+
"--model",
|
72
|
+
"small-0b2",
|
73
|
+
"--seed",
|
74
|
+
"2042",
|
75
|
+
"--out-path",
|
76
|
+
tmp_path,
|
77
|
+
"--no-geometry",
|
78
|
+
],
|
79
|
+
)
|
80
|
+
assert result.exit_code == 0
|
81
|
+
assert (tmp_path / "system.cif").exists()
|
82
|
+
assert (tmp_path / "system+1H2.cif").exists()
|
83
|
+
assert (tmp_path / "system+2H2.cif").exists()
|
84
|
+
f = read(tmp_path / "system+2H2.cif")
|
85
|
+
assert f[0].position == pytest.approx([1.24701529, 0.024216, 0.11632905], abs=err)
|
@@ -25,14 +25,14 @@ def test_packmm_custom_molecule():
|
|
25
25
|
"""Check molecule."""
|
26
26
|
result = runner.invoke(app, ["--molecule", "CO2"])
|
27
27
|
assert result.exit_code == 0
|
28
|
-
assert "CO2" in strip_ansi_codes(result.output)
|
28
|
+
assert "molecule='CO2'" in strip_ansi_codes(result.output)
|
29
29
|
|
30
30
|
|
31
31
|
def test_packmm_custom_nmols():
|
32
32
|
"""Check nmols."""
|
33
|
-
result = runner.invoke(app, ["--nmols", "
|
34
|
-
assert result.exit_code ==
|
35
|
-
assert "nmols
|
33
|
+
result = runner.invoke(app, ["--nmols", "-2"])
|
34
|
+
assert result.exit_code == 1
|
35
|
+
assert "nmols=-2" in strip_ansi_codes(result.output)
|
36
36
|
|
37
37
|
|
38
38
|
def test_packmm_custom_ntries():
|
@@ -49,6 +49,13 @@ def test_packmm_custom_seed():
|
|
49
49
|
assert "seed=1234" in strip_ansi_codes(result.output)
|
50
50
|
|
51
51
|
|
52
|
+
def test_packmm_custom_every():
|
53
|
+
"""Check seed."""
|
54
|
+
result = runner.invoke(app, ["--every", "10"])
|
55
|
+
assert result.exit_code == 0
|
56
|
+
assert "every=10" in strip_ansi_codes(result.output)
|
57
|
+
|
58
|
+
|
52
59
|
def test_packmm_custom_insertion_method():
|
53
60
|
"""Check insertion."""
|
54
61
|
result = runner.invoke(app, ["--where", "sphere"])
|
@@ -56,6 +63,20 @@ def test_packmm_custom_insertion_method():
|
|
56
63
|
assert "where=sphere" in strip_ansi_codes(result.output)
|
57
64
|
|
58
65
|
|
66
|
+
def test_packmm_custom_insert_strategy():
|
67
|
+
"""Check insertion."""
|
68
|
+
result = runner.invoke(app, ["--insert-strategy", "hmc"])
|
69
|
+
assert result.exit_code == 0
|
70
|
+
assert "insert_strategy=hmc" in strip_ansi_codes(result.output)
|
71
|
+
|
72
|
+
|
73
|
+
def test_packmm_custom_relax_strategy():
|
74
|
+
"""Check relax."""
|
75
|
+
result = runner.invoke(app, ["--relax-strategy", "md"])
|
76
|
+
assert result.exit_code == 0
|
77
|
+
assert "relax_strategy=md" in strip_ansi_codes(result.output)
|
78
|
+
|
79
|
+
|
59
80
|
def test_packmm_custom_center():
|
60
81
|
"""Check centre."""
|
61
82
|
result = runner.invoke(app, ["--centre", "0.5,0.5,0.5"])
|
@@ -122,6 +143,27 @@ def test_packmm_custom_temperature():
|
|
122
143
|
assert "temperature=400.0" in strip_ansi_codes(result.output)
|
123
144
|
|
124
145
|
|
146
|
+
def test_packmm_md_temperature():
|
147
|
+
"""Check md temperature."""
|
148
|
+
result = runner.invoke(app, ["--md-temperature", "300.0"])
|
149
|
+
assert result.exit_code == 0
|
150
|
+
assert "md_temperature=300.0" in strip_ansi_codes(result.output)
|
151
|
+
|
152
|
+
|
153
|
+
def test_packmm_md_timestep():
|
154
|
+
"""Check md temperature."""
|
155
|
+
result = runner.invoke(app, ["--md-timestep", "1.0"])
|
156
|
+
assert result.exit_code == 0
|
157
|
+
assert "md_timestep=1.0" in strip_ansi_codes(result.output)
|
158
|
+
|
159
|
+
|
160
|
+
def test_packmm_md_steps():
|
161
|
+
"""Check md steps."""
|
162
|
+
result = runner.invoke(app, ["--md-steps", "10"])
|
163
|
+
assert result.exit_code == 0
|
164
|
+
assert "md_steps=10" in strip_ansi_codes(result.output)
|
165
|
+
|
166
|
+
|
125
167
|
def test_packmm_custom_fmax():
|
126
168
|
"""Check fmax."""
|
127
169
|
result = runner.invoke(app, ["--fmax", "0.05"])
|
@@ -143,16 +185,41 @@ def test_packmm_invalid_insertion_method():
|
|
143
185
|
assert "Invalid value for '--where'" in strip_ansi_codes(result.output)
|
144
186
|
|
145
187
|
|
146
|
-
def
|
147
|
-
"""Check
|
148
|
-
result = runner.invoke(app, ["--
|
188
|
+
def test_packmm_invalid_insertion_strategy():
|
189
|
+
"""Check insertion strategt."""
|
190
|
+
result = runner.invoke(app, ["--insert-strategy", "invalid_method"])
|
149
191
|
assert result.exit_code != 0
|
150
|
-
assert "Invalid
|
192
|
+
assert "Invalid value for '--insert-strategy'" in strip_ansi_codes(result.output)
|
193
|
+
|
194
|
+
|
195
|
+
def test_packmm_invalid_relax_strategy():
|
196
|
+
"""Check insertion strategt."""
|
197
|
+
result = runner.invoke(app, ["--relax-strategy", "invalid_method"])
|
198
|
+
assert result.exit_code != 0
|
199
|
+
assert "Invalid value for '--relax-strategy'" in strip_ansi_codes(result.output)
|
200
|
+
|
201
|
+
|
202
|
+
def test_packmm_invalid_md_steps():
|
203
|
+
"""Check md steps."""
|
204
|
+
result = runner.invoke(app, ["--nmols", "1", "--md-steps", "-10"])
|
205
|
+
assert "Invalid MD steps" in strip_ansi_codes(result.output)
|
206
|
+
|
207
|
+
|
208
|
+
def test_packmm_invalid_md_temperature():
|
209
|
+
"""Check md steps."""
|
210
|
+
result = runner.invoke(app, ["--nmols", "1", "--md-temperature", "-10.0"])
|
211
|
+
assert "Invalid MD temperature" in strip_ansi_codes(result.output)
|
151
212
|
|
152
213
|
|
153
|
-
def
|
214
|
+
def test_packmm_invalid_md_timestep():
|
215
|
+
"""Check md timestep."""
|
216
|
+
result = runner.invoke(app, ["--nmols", "1", "--md-timestep", "-10.0"])
|
217
|
+
assert "Invalid MD timestep" in strip_ansi_codes(result.output)
|
218
|
+
|
219
|
+
|
220
|
+
def test_packmm_invalid_centre_format():
|
154
221
|
"""Check centre."""
|
155
|
-
result = runner.invoke(app, ["--nmols", "1", "--centre", "
|
222
|
+
result = runner.invoke(app, ["--nmols", "1", "--centre", "0.5,0.5"])
|
156
223
|
assert result.exit_code != 0
|
157
224
|
assert "Invalid centre" in strip_ansi_codes(result.output)
|
158
225
|
|
@@ -0,0 +1,416 @@
|
|
1
|
+
"""Tests for core."""
|
2
|
+
|
3
|
+
# -*- coding: utf-8 -*-
|
4
|
+
# Author; alin m elena, alin@elena.re
|
5
|
+
# Contribs;
|
6
|
+
# Date: 22-02-2025
|
7
|
+
# ©alin m elena,
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
from ase import Atoms
|
11
|
+
from ase.build import molecule as build_molecule
|
12
|
+
from ase.io import write
|
13
|
+
from numpy import random
|
14
|
+
import pytest
|
15
|
+
|
16
|
+
from pack_mm.core.core import (
|
17
|
+
get_insertion_position,
|
18
|
+
load_molecule,
|
19
|
+
optimize_geometry,
|
20
|
+
pack_molecules,
|
21
|
+
random_point_in_box,
|
22
|
+
random_point_in_cylinder,
|
23
|
+
random_point_in_ellipsoid,
|
24
|
+
random_point_in_sphere,
|
25
|
+
rotate_molecule,
|
26
|
+
run_md_nve,
|
27
|
+
save_the_day,
|
28
|
+
set_defaults,
|
29
|
+
validate_value,
|
30
|
+
)
|
31
|
+
|
32
|
+
err = 1.0e-8
|
33
|
+
|
34
|
+
|
35
|
+
# Set a fixed seed for reproducibility in tests
|
36
|
+
@pytest.fixture(autouse=True)
|
37
|
+
def set_random_seed():
|
38
|
+
"""Set random seed."""
|
39
|
+
random.seed(2042)
|
40
|
+
|
41
|
+
|
42
|
+
def test_random_point_in_sphere():
|
43
|
+
"""Test point in sphere."""
|
44
|
+
center = (2, 2, 2)
|
45
|
+
radius = 8.0
|
46
|
+
x, y, z = random_point_in_sphere(center, radius)
|
47
|
+
assert x == pytest.approx(-3.299696236298196, abs=err)
|
48
|
+
assert y == pytest.approx(-3.046619861327052, abs=err)
|
49
|
+
assert z == pytest.approx(1.1884891239565165, abs=err)
|
50
|
+
|
51
|
+
|
52
|
+
def test_random_point_in_ellipsoid():
|
53
|
+
"""Test point in ellipsoid."""
|
54
|
+
center = (0, 0, 0)
|
55
|
+
a, b, c = 2.0, 2.0, 4.0
|
56
|
+
x, y, z = random_point_in_ellipsoid(center, a, b, c)
|
57
|
+
assert x == pytest.approx(0.27914659851849705, abs=err)
|
58
|
+
assert y == pytest.approx(-1.4815802529721946, abs=err)
|
59
|
+
assert z == pytest.approx(-1.2059956538672925, abs=err)
|
60
|
+
|
61
|
+
|
62
|
+
def test_random_point_in_box():
|
63
|
+
"""Test point in box."""
|
64
|
+
center = (0, 0, 0)
|
65
|
+
a, b, c = 1.0, 2.0, 3.0
|
66
|
+
x, y, z = random_point_in_box(center, a, b, c)
|
67
|
+
assert x == pytest.approx(0.2796391455704136, abs=err)
|
68
|
+
assert y == pytest.approx(0.24221552377157196, abs=err)
|
69
|
+
assert z == pytest.approx(0.10546160764197499, abs=err)
|
70
|
+
|
71
|
+
|
72
|
+
def test_random_point_in_cylinder():
|
73
|
+
"""Test point in cylinder."""
|
74
|
+
center = (0, 0, 0)
|
75
|
+
radius = 2.0
|
76
|
+
height = 5.0
|
77
|
+
|
78
|
+
direction = "z"
|
79
|
+
x, y, z = random_point_in_cylinder(center, radius, height, direction)
|
80
|
+
assert x == pytest.approx(0.29184067570623795, abs=err)
|
81
|
+
assert y == pytest.approx(-1.5489545079008842, abs=err)
|
82
|
+
assert z == pytest.approx(0.1757693460699583, abs=err)
|
83
|
+
|
84
|
+
direction = "y"
|
85
|
+
x, y, z = random_point_in_cylinder(center, radius, height, direction)
|
86
|
+
assert x == pytest.approx(-1.417921218944154, abs=err)
|
87
|
+
assert y == pytest.approx(-0.8295636754899149, abs=err)
|
88
|
+
assert z == pytest.approx(-1.2828806053300128, abs=err)
|
89
|
+
|
90
|
+
direction = "x"
|
91
|
+
x, y, z = random_point_in_cylinder(center, radius, height, direction)
|
92
|
+
assert x == pytest.approx(0.5063181926884202, abs=err)
|
93
|
+
assert y == pytest.approx(-0.5839696769917422, abs=err)
|
94
|
+
assert z == pytest.approx(1.10139156645954, abs=err)
|
95
|
+
|
96
|
+
|
97
|
+
def test_validate_value_positive():
|
98
|
+
"""Test point in test value."""
|
99
|
+
validate_value("test_value", 1.0) # Should not raise an exception
|
100
|
+
|
101
|
+
|
102
|
+
def test_validate_value_negative():
|
103
|
+
"""Test point in test value."""
|
104
|
+
with pytest.raises(Exception, match="Invalid test_value, needs to be positive"):
|
105
|
+
validate_value("test_value", -1.0)
|
106
|
+
|
107
|
+
|
108
|
+
def test_load_molecule_from_file(tmp_path):
|
109
|
+
"""Test point in load molecule."""
|
110
|
+
molecule = build_molecule("H2O")
|
111
|
+
molecule_file = tmp_path / "water.xyz"
|
112
|
+
write(molecule_file, molecule)
|
113
|
+
loaded_molecule = load_molecule(str(molecule_file))
|
114
|
+
assert len(loaded_molecule) == 3
|
115
|
+
|
116
|
+
|
117
|
+
def test_load_molecule_from_name():
|
118
|
+
"""Test point in load molecule."""
|
119
|
+
molecule = load_molecule("H2O")
|
120
|
+
assert len(molecule) == 3
|
121
|
+
|
122
|
+
|
123
|
+
def test_get_insertion_position_sphere():
|
124
|
+
"""Test point in sphere."""
|
125
|
+
center = (5, 5, 5)
|
126
|
+
radius = 10.0
|
127
|
+
x, y, z = get_insertion_position("sphere", center, radius=radius)
|
128
|
+
assert x == pytest.approx(-1.624620295372745, abs=err)
|
129
|
+
assert y == pytest.approx(-1.3082748266588151, abs=err)
|
130
|
+
assert z == pytest.approx(3.9856114049456455, abs=err)
|
131
|
+
|
132
|
+
|
133
|
+
def test_get_insertion_position_box():
|
134
|
+
"""Test point in box."""
|
135
|
+
center = (5, 5, 5)
|
136
|
+
a = 10.0
|
137
|
+
x, y, z = get_insertion_position("box", center, a=a, b=a, c=a)
|
138
|
+
assert x == pytest.approx(7.796391455704136, abs=err)
|
139
|
+
assert y == pytest.approx(6.21107761885786, abs=err)
|
140
|
+
assert z == pytest.approx(5.351538692139917, abs=err)
|
141
|
+
|
142
|
+
|
143
|
+
def test_get_insertion_position_ellipsoid():
|
144
|
+
"""Test point in ellipsoid."""
|
145
|
+
center = (5, 5, 5)
|
146
|
+
x, y, z = get_insertion_position("ellipsoid", center, a=2.0, b=2.0, c=4.0)
|
147
|
+
assert x == pytest.approx(5.279146598518497, abs=err)
|
148
|
+
assert y == pytest.approx(3.5184197470278056, abs=err)
|
149
|
+
assert z == pytest.approx(3.7940043461327075, abs=err)
|
150
|
+
|
151
|
+
|
152
|
+
def test_get_insertion_position_cylinder():
|
153
|
+
"""Test point in cylinder."""
|
154
|
+
center = (5, 5, 5)
|
155
|
+
x, y, z = get_insertion_position("cylinderZ", center, radius=3.0, height=10.0)
|
156
|
+
assert x == pytest.approx(5.437761013559357, abs=err)
|
157
|
+
assert y == pytest.approx(2.6765682381486737, abs=err)
|
158
|
+
assert z == pytest.approx(5.351538692139917, abs=err)
|
159
|
+
|
160
|
+
|
161
|
+
def test_set_defaults_centre():
|
162
|
+
"""Test centre."""
|
163
|
+
cell = [10, 10, 10]
|
164
|
+
centre, _, _, _, _, _ = set_defaults(cell, centre=None)
|
165
|
+
assert centre[0] == pytest.approx(5.0, abs=err)
|
166
|
+
assert centre[1] == pytest.approx(5.0, abs=err)
|
167
|
+
assert centre[2] == pytest.approx(5.0, abs=err)
|
168
|
+
|
169
|
+
|
170
|
+
def test_set_defaults_box():
|
171
|
+
"""Test box."""
|
172
|
+
cell = [10, 10, 10]
|
173
|
+
centre, a, b, c, _, _ = set_defaults(cell, where="box", b=5.0, centre=None)
|
174
|
+
assert centre[0] == pytest.approx(5.0, abs=err)
|
175
|
+
assert a == pytest.approx(10.0, abs=err)
|
176
|
+
assert b == pytest.approx(5.0, abs=err)
|
177
|
+
assert c == pytest.approx(10.0, abs=err)
|
178
|
+
|
179
|
+
|
180
|
+
def test_set_defaults_anywhere():
|
181
|
+
"""Test box."""
|
182
|
+
cell = [10, 10, 10]
|
183
|
+
centre, a, b, c, _, _ = set_defaults(cell, where="anywhere", centre=None)
|
184
|
+
assert centre[0] == pytest.approx(5.0, abs=err)
|
185
|
+
assert a == pytest.approx(10.0, abs=err)
|
186
|
+
assert b == pytest.approx(10.0, abs=err)
|
187
|
+
assert c == pytest.approx(10.0, abs=err)
|
188
|
+
|
189
|
+
|
190
|
+
def test_set_defaults_ellipsoid():
|
191
|
+
"""Test ellipsoid."""
|
192
|
+
cell = [10, 10, 10]
|
193
|
+
centre, a, b, c, _, _ = set_defaults(
|
194
|
+
cell, where="ellipsoid", b=3.0, a=3.0, centre=None
|
195
|
+
)
|
196
|
+
assert centre[0] == pytest.approx(5.0, abs=err)
|
197
|
+
assert a == pytest.approx(3.0, abs=err)
|
198
|
+
assert b == pytest.approx(3.0, abs=err)
|
199
|
+
assert c == pytest.approx(5.0, abs=err)
|
200
|
+
|
201
|
+
|
202
|
+
def test_set_defaults_cylinder():
|
203
|
+
"""Test ellipsoid."""
|
204
|
+
cell = [10, 12, 14]
|
205
|
+
centre, _, _, _, radius, height = set_defaults(cell, where="cylinderZ", centre=None)
|
206
|
+
assert centre[0] == pytest.approx(5.0, abs=err)
|
207
|
+
assert radius == pytest.approx(5.0, abs=err)
|
208
|
+
assert height == pytest.approx(7.0, abs=err)
|
209
|
+
|
210
|
+
centre, _, _, _, radius, height = set_defaults(cell, where="cylinderX", centre=None)
|
211
|
+
assert centre[0] == pytest.approx(5.0, abs=err)
|
212
|
+
assert radius == pytest.approx(6.0, abs=err)
|
213
|
+
assert height == pytest.approx(5.0, abs=err)
|
214
|
+
|
215
|
+
centre, _, _, _, radius, height = set_defaults(cell, where="cylinderY", centre=None)
|
216
|
+
assert centre[0] == pytest.approx(5.0, abs=err)
|
217
|
+
assert radius == pytest.approx(5.0, abs=err)
|
218
|
+
assert height == pytest.approx(6.0, abs=err)
|
219
|
+
|
220
|
+
|
221
|
+
def test_rotate_molecule():
|
222
|
+
"""Test rotate molecule."""
|
223
|
+
molecule = build_molecule("H2O")
|
224
|
+
a1 = molecule.get_angle(2, 0, 1)
|
225
|
+
a2 = molecule.get_angle(2, 1, 0)
|
226
|
+
|
227
|
+
rotated_molecule = rotate_molecule(molecule)
|
228
|
+
assert a1 == pytest.approx(rotated_molecule.get_angle(2, 0, 1))
|
229
|
+
assert a2 == pytest.approx(rotated_molecule.get_angle(2, 1, 0))
|
230
|
+
|
231
|
+
|
232
|
+
def test_optimize_geometry(tmp_path):
|
233
|
+
"""Test go."""
|
234
|
+
molecule = build_molecule("H2O")
|
235
|
+
molecule.set_cell([10, 10, 10])
|
236
|
+
molecule.set_pbc([True, True, True])
|
237
|
+
structure_file = tmp_path / "water.cif"
|
238
|
+
write(structure_file, molecule)
|
239
|
+
optimized_energy, _ = optimize_geometry(
|
240
|
+
str(structure_file),
|
241
|
+
device="cpu",
|
242
|
+
arch="mace_mp",
|
243
|
+
model="small-0b2",
|
244
|
+
fmax=0.01,
|
245
|
+
out_path=tmp_path,
|
246
|
+
opt_cell=True,
|
247
|
+
)
|
248
|
+
assert optimized_energy == pytest.approx(-14.17098106193308, abs=err)
|
249
|
+
|
250
|
+
|
251
|
+
def test_pack_molecules(tmp_path):
|
252
|
+
"""Test pack molecule."""
|
253
|
+
system = Atoms(
|
254
|
+
"Ca", positions=[(5.0, 5.0, 5.0)], cell=[10, 10, 10], pbc=[True, True, True]
|
255
|
+
)
|
256
|
+
system_file = tmp_path / "system.cif"
|
257
|
+
write(system_file, system)
|
258
|
+
|
259
|
+
e, _ = pack_molecules(
|
260
|
+
system=str(system_file),
|
261
|
+
molecule="H2O",
|
262
|
+
nmols=2,
|
263
|
+
arch="mace_mp",
|
264
|
+
model="small-0b2",
|
265
|
+
device="cpu",
|
266
|
+
where="sphere",
|
267
|
+
center=(5.0, 5.0, 5.0),
|
268
|
+
radius=5.0,
|
269
|
+
seed=2042,
|
270
|
+
temperature=300,
|
271
|
+
ntries=10,
|
272
|
+
geometry=False,
|
273
|
+
fmax=0.1,
|
274
|
+
out_path=tmp_path,
|
275
|
+
)
|
276
|
+
|
277
|
+
assert (tmp_path / "system+1H2O.cif").exists()
|
278
|
+
assert (tmp_path / "system+2H2O.cif").exists()
|
279
|
+
assert e == pytest.approx(-29.21589570470306, abs=err)
|
280
|
+
|
281
|
+
|
282
|
+
def test_pack_molecules_atoms(tmp_path):
|
283
|
+
"""Test pack molecule."""
|
284
|
+
system = Atoms(
|
285
|
+
"Ca", positions=[(5.0, 5.0, 5.0)], cell=[10, 10, 10], pbc=[True, True, True]
|
286
|
+
)
|
287
|
+
|
288
|
+
e, _ = pack_molecules(
|
289
|
+
system=system,
|
290
|
+
molecule="H2O",
|
291
|
+
nmols=2,
|
292
|
+
arch="mace_mp",
|
293
|
+
model="small-0b2",
|
294
|
+
device="cpu",
|
295
|
+
where="sphere",
|
296
|
+
center=(5.0, 5.0, 5.0),
|
297
|
+
radius=5.0,
|
298
|
+
seed=2042,
|
299
|
+
temperature=300,
|
300
|
+
ntries=10,
|
301
|
+
geometry=False,
|
302
|
+
fmax=0.1,
|
303
|
+
out_path=tmp_path,
|
304
|
+
)
|
305
|
+
|
306
|
+
assert e == pytest.approx(-29.21589570470306, abs=err)
|
307
|
+
|
308
|
+
|
309
|
+
def test_pack_molecules_2(tmp_path, capsys):
|
310
|
+
"""Test pack molecule."""
|
311
|
+
system = Atoms(
|
312
|
+
"Ca", positions=[(2.5, 2.5, 2.5)], cell=[5, 5, 5], pbc=[True, True, True]
|
313
|
+
)
|
314
|
+
system_file = tmp_path / "system.cif"
|
315
|
+
write(system_file, system)
|
316
|
+
|
317
|
+
e, _ = pack_molecules(
|
318
|
+
system=str(system_file),
|
319
|
+
molecule="H2O",
|
320
|
+
nmols=3,
|
321
|
+
arch="mace_mp",
|
322
|
+
model="small-0b2",
|
323
|
+
device="cpu",
|
324
|
+
where="sphere",
|
325
|
+
center=(2.5, 2.5, 2.5),
|
326
|
+
radius=2.5,
|
327
|
+
seed=2042,
|
328
|
+
temperature=300,
|
329
|
+
ntries=2,
|
330
|
+
geometry=False,
|
331
|
+
fmax=0.1,
|
332
|
+
out_path=tmp_path,
|
333
|
+
)
|
334
|
+
captured = capsys.readouterr()
|
335
|
+
|
336
|
+
assert "Failed to insert particle 3 after 2 tries" in captured.out
|
337
|
+
assert e == pytest.approx(-47.194755808249454, abs=err)
|
338
|
+
|
339
|
+
|
340
|
+
def test_save_the_day(tmp_path):
|
341
|
+
"""Test save the day."""
|
342
|
+
molecule = build_molecule("H2O")
|
343
|
+
molecule.set_cell([10, 10, 10])
|
344
|
+
molecule.set_pbc([True, True, True])
|
345
|
+
molecule.center()
|
346
|
+
structure_file = tmp_path / "water.cif"
|
347
|
+
write(structure_file, molecule)
|
348
|
+
s = save_the_day(
|
349
|
+
str(structure_file),
|
350
|
+
device="cpu",
|
351
|
+
arch="mace_mp",
|
352
|
+
model="small-0b2",
|
353
|
+
fmax=0.01,
|
354
|
+
out_path=tmp_path,
|
355
|
+
)
|
356
|
+
assert s[0].position == pytest.approx([5.0, 4.99619815, 5.30704738], abs=err)
|
357
|
+
|
358
|
+
|
359
|
+
def test_save_the_day_md(tmp_path):
|
360
|
+
"""Test save the day."""
|
361
|
+
molecule = build_molecule("H2O")
|
362
|
+
molecule.set_cell([10, 10, 10])
|
363
|
+
molecule.set_pbc([True, True, True])
|
364
|
+
molecule.center()
|
365
|
+
structure_file = tmp_path / "water.cif"
|
366
|
+
write(structure_file, molecule)
|
367
|
+
s = save_the_day(
|
368
|
+
str(structure_file),
|
369
|
+
device="cpu",
|
370
|
+
arch="mace_mp",
|
371
|
+
model="small-0b2",
|
372
|
+
relax_strategy="md",
|
373
|
+
md_timestep=1.0,
|
374
|
+
md_steps=10.0,
|
375
|
+
md_temperature=100.0,
|
376
|
+
)
|
377
|
+
assert s[0].position == pytest.approx([4.99684244, 5.00440785, 5.2987255], abs=err)
|
378
|
+
|
379
|
+
|
380
|
+
def test_save_the_day_invalid(tmp_path):
|
381
|
+
"""Test save the day."""
|
382
|
+
molecule = build_molecule("H2O")
|
383
|
+
molecule.set_cell([10, 10, 10])
|
384
|
+
molecule.set_pbc([True, True, True])
|
385
|
+
molecule.center()
|
386
|
+
structure_file = tmp_path / "water.cif"
|
387
|
+
write(structure_file, molecule)
|
388
|
+
s = save_the_day(
|
389
|
+
str(structure_file),
|
390
|
+
device="cpu",
|
391
|
+
arch="mace_mp",
|
392
|
+
model="small-0b2",
|
393
|
+
relax_strategy="invalid_stratregy",
|
394
|
+
md_timestep=1.0,
|
395
|
+
md_steps=10.0,
|
396
|
+
md_temperature=100.0,
|
397
|
+
)
|
398
|
+
assert s is None
|
399
|
+
|
400
|
+
|
401
|
+
def test_run_md(tmp_path):
|
402
|
+
"""Test md."""
|
403
|
+
molecule = build_molecule("H2O")
|
404
|
+
molecule.set_cell([10, 10, 10])
|
405
|
+
molecule.set_pbc([True, True, True])
|
406
|
+
molecule.center()
|
407
|
+
s = run_md_nve(
|
408
|
+
molecule,
|
409
|
+
device="cpu",
|
410
|
+
arch="mace_mp",
|
411
|
+
model="small-0b2",
|
412
|
+
timestep=1.0,
|
413
|
+
steps=10.0,
|
414
|
+
temp=100.0,
|
415
|
+
)
|
416
|
+
assert s[0].position == pytest.approx([4.99684244, 5.00440785, 5.2987255], abs=err)
|
@@ -1,178 +0,0 @@
|
|
1
|
-
"""Test cli for core."""
|
2
|
-
|
3
|
-
# -*- coding: utf-8 -*-
|
4
|
-
# Author; alin m elena, alin@elena.re
|
5
|
-
# Contribs;
|
6
|
-
# Date: 22-02-2025
|
7
|
-
# ©alin m elena,
|
8
|
-
from __future__ import annotations
|
9
|
-
|
10
|
-
from ase import Atoms
|
11
|
-
from ase.build import molecule as build_molecule
|
12
|
-
from ase.io import write
|
13
|
-
import numpy as np
|
14
|
-
from numpy import random
|
15
|
-
import pytest
|
16
|
-
|
17
|
-
from pack_mm.core.core import (
|
18
|
-
get_insertion_position,
|
19
|
-
load_molecule,
|
20
|
-
optimize_geometry,
|
21
|
-
pack_molecules,
|
22
|
-
random_point_in_box,
|
23
|
-
random_point_in_cylinder,
|
24
|
-
random_point_in_ellipsoid,
|
25
|
-
random_point_in_sphere,
|
26
|
-
rotate_molecule,
|
27
|
-
validate_value,
|
28
|
-
)
|
29
|
-
|
30
|
-
|
31
|
-
# Set a fixed seed for reproducibility in tests
|
32
|
-
@pytest.fixture(autouse=True)
|
33
|
-
def set_random_seed():
|
34
|
-
"""Set random seed."""
|
35
|
-
random.seed(2042)
|
36
|
-
|
37
|
-
|
38
|
-
def test_random_point_in_sphere():
|
39
|
-
"""Test point in sphere."""
|
40
|
-
center = (0, 0, 0)
|
41
|
-
radius = 10.0
|
42
|
-
point = random_point_in_sphere(center, radius)
|
43
|
-
assert len(point) == 3
|
44
|
-
distance = np.linalg.norm(np.array(point) - np.array(center))
|
45
|
-
assert distance <= radius
|
46
|
-
|
47
|
-
|
48
|
-
def test_random_point_in_ellipsoid():
|
49
|
-
"""Test point in ellipsoid."""
|
50
|
-
center = (0, 0, 0)
|
51
|
-
a, b, c = 1.0, 2.0, 3.0
|
52
|
-
point = random_point_in_ellipsoid(center, a, b, c)
|
53
|
-
assert len(point) == 3
|
54
|
-
x, y, z = point
|
55
|
-
assert (x**2 / a**2) + (y**2 / b**2) + (z**2 / c**2) <= 1.0
|
56
|
-
|
57
|
-
|
58
|
-
def test_random_point_in_box():
|
59
|
-
"""Test point in box."""
|
60
|
-
center = (0, 0, 0)
|
61
|
-
a, b, c = 1.0, 2.0, 3.0
|
62
|
-
point = random_point_in_box(center, a, b, c)
|
63
|
-
assert len(point) == 3
|
64
|
-
x, y, z = point
|
65
|
-
assert center[0] - a * 0.5 <= x <= center[0] + a * 0.5
|
66
|
-
assert center[1] - b * 0.5 <= y <= center[1] + b * 0.5
|
67
|
-
assert center[2] - c * 0.5 <= z <= center[2] + c * 0.5
|
68
|
-
|
69
|
-
|
70
|
-
def test_random_point_in_cylinder():
|
71
|
-
"""Test point in cylinder."""
|
72
|
-
center = (0, 0, 0)
|
73
|
-
radius = 1.0
|
74
|
-
height = 2.0
|
75
|
-
direction = "z"
|
76
|
-
point = random_point_in_cylinder(center, radius, height, direction)
|
77
|
-
assert len(point) == 3
|
78
|
-
x, y, z = point
|
79
|
-
assert x**2 + y**2 <= radius**2
|
80
|
-
assert center[2] - height * 0.5 <= z <= center[2] + height * 0.5
|
81
|
-
|
82
|
-
|
83
|
-
def test_validate_value_positive():
|
84
|
-
"""Test point in test value."""
|
85
|
-
validate_value("test_value", 1.0) # Should not raise an exception
|
86
|
-
|
87
|
-
|
88
|
-
def test_validate_value_negative():
|
89
|
-
"""Test point in test value."""
|
90
|
-
with pytest.raises(Exception, match="Invalid test_value, needs to be positive"):
|
91
|
-
validate_value("test_value", -1.0)
|
92
|
-
|
93
|
-
|
94
|
-
def test_load_molecule_from_file(tmp_path):
|
95
|
-
"""Test point in load molecule."""
|
96
|
-
molecule = build_molecule("H2O")
|
97
|
-
molecule_file = tmp_path / "water.xyz"
|
98
|
-
molecule.write(molecule_file)
|
99
|
-
loaded_molecule = load_molecule(str(molecule_file))
|
100
|
-
assert isinstance(loaded_molecule, Atoms)
|
101
|
-
assert len(loaded_molecule) == 3 # H2O has 3 atoms
|
102
|
-
|
103
|
-
|
104
|
-
def test_load_molecule_from_name():
|
105
|
-
"""Test point in load molecule."""
|
106
|
-
molecule = load_molecule("H2O")
|
107
|
-
assert isinstance(molecule, Atoms)
|
108
|
-
assert len(molecule) == 3 # H2O has 3 atoms
|
109
|
-
|
110
|
-
|
111
|
-
def test_get_insertion_position_sphere():
|
112
|
-
"""Test point in sphere."""
|
113
|
-
center = (0, 0, 0)
|
114
|
-
radius = 10.0
|
115
|
-
point = get_insertion_position("sphere", center, radius=radius)
|
116
|
-
assert len(point) == 3
|
117
|
-
distance = np.linalg.norm(np.array(point) - np.array(center))
|
118
|
-
assert distance <= radius
|
119
|
-
|
120
|
-
|
121
|
-
def test_rotate_molecule():
|
122
|
-
"""Test rotate molecule."""
|
123
|
-
molecule = build_molecule("H2O")
|
124
|
-
rotated_molecule = rotate_molecule(molecule)
|
125
|
-
assert isinstance(rotated_molecule, Atoms)
|
126
|
-
assert len(rotated_molecule) == 3 # H2O has 3 atoms
|
127
|
-
|
128
|
-
|
129
|
-
def test_optimize_geometry(tmp_path):
|
130
|
-
"""Test go."""
|
131
|
-
# Create a temporary structure file
|
132
|
-
molecule = build_molecule("H2O")
|
133
|
-
molecule.set_cell([10, 10, 10])
|
134
|
-
molecule.set_pbc([True, True, True])
|
135
|
-
structure_file = tmp_path / "water.cif"
|
136
|
-
write(structure_file, molecule)
|
137
|
-
optimized_energy = optimize_geometry(
|
138
|
-
str(structure_file),
|
139
|
-
device="cpu",
|
140
|
-
arch="mace_mp",
|
141
|
-
model="medium-omat-0",
|
142
|
-
fmax=0.01,
|
143
|
-
)
|
144
|
-
assert optimized_energy == pytest.approx(-13.759273983276572, abs=1.0e-8)
|
145
|
-
|
146
|
-
|
147
|
-
# Test pack_molecules with a simple case
|
148
|
-
def test_pack_molecules(tmp_path):
|
149
|
-
"""Test pack molecule."""
|
150
|
-
# Create a temporary system file
|
151
|
-
system = Atoms(
|
152
|
-
"Ca", positions=[(5.0, 5.0, 5.0)], cell=[10, 10, 10], pbc=[True, True, True]
|
153
|
-
)
|
154
|
-
system_file = tmp_path / "system.cif"
|
155
|
-
write(system_file, system)
|
156
|
-
|
157
|
-
# Test packing molecules
|
158
|
-
e = pack_molecules(
|
159
|
-
system=str(system_file),
|
160
|
-
molecule="H2O",
|
161
|
-
nmols=2,
|
162
|
-
arch="mace_mp",
|
163
|
-
model="medium-omat-0",
|
164
|
-
device="cpu",
|
165
|
-
where="sphere",
|
166
|
-
center=(5.0, 5.0, 5.0),
|
167
|
-
radius=5.0,
|
168
|
-
seed=2042,
|
169
|
-
temperature=300,
|
170
|
-
ntries=10,
|
171
|
-
geometry=False,
|
172
|
-
fmax=0.1,
|
173
|
-
out_path=tmp_path,
|
174
|
-
)
|
175
|
-
|
176
|
-
assert (tmp_path / "system+1H2O.cif").exists()
|
177
|
-
assert (tmp_path / "system+2H2O.cif").exists()
|
178
|
-
assert e == pytest.approx(-28.251229837533085, abs=1.0e-6)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|