pack-mm 0.0.14__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.14/LICENSE +21 -0
- pack_mm-0.0.14/PKG-INFO +36 -0
- pack_mm-0.0.14/README.md +15 -0
- pack_mm-0.0.14/pack_mm/__init__.py +7 -0
- pack_mm-0.0.14/pack_mm/cli/packmm.py +161 -0
- pack_mm-0.0.14/pack_mm/core/core.py +345 -0
- pack_mm-0.0.14/pyproject.toml +138 -0
- pack_mm-0.0.14/tests/__init__.py +7 -0
- pack_mm-0.0.14/tests/test_packmm.py +206 -0
- pack_mm-0.0.14/tests/utils.py +41 -0
pack_mm-0.0.14/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2024 data-driven materials and molecular science
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
pack_mm-0.0.14/PKG-INFO
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
Metadata-Version: 2.1
|
2
|
+
Name: pack-mm
|
3
|
+
Version: 0.0.14
|
4
|
+
Summary: packing materials and molecules in boxes using for machine learnt interatomic potentials
|
5
|
+
Author: Alin M. Elena
|
6
|
+
Classifier: Programming Language :: Python
|
7
|
+
Classifier: Programming Language :: Python :: 3.10
|
8
|
+
Classifier: Programming Language :: Python :: 3.11
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
12
|
+
Classifier: Natural Language :: English
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
14
|
+
Project-URL: Repository, https://github.com/ddmms/pack-mm/
|
15
|
+
Project-URL: Documentation, https://ddmms.github.io/pack-mm/
|
16
|
+
Requires-Python: >=3.10
|
17
|
+
Requires-Dist: janus-core>=0.7.2
|
18
|
+
Requires-Dist: typer<1.0.0,>=0.12.5
|
19
|
+
Requires-Dist: typer-config<2.0.0,>=1.4.2
|
20
|
+
Description-Content-Type: text/markdown
|
21
|
+
|
22
|
+
# pack materials and molecules
|
23
|
+
|
24
|
+
[![Python versions][python-badge]][python-link]
|
25
|
+
[![Build Status][ci-badge]][ci-link]
|
26
|
+
[![Coverage Status][cov-badge]][cov-link]
|
27
|
+
[![License][license-badge]][license-link]
|
28
|
+
|
29
|
+
[python-badge]: https://img.shields.io/pypi/pyversions/pack-mm.svg
|
30
|
+
[python-link]: https://pypi.org/project/pack-mm/
|
31
|
+
[ci-badge]: https://github.com/ddmms/pack-mm/actions/workflows/build.yml/badge.svg?branch=main
|
32
|
+
[ci-link]: https://github.com/ddmms/pack-mm/actions
|
33
|
+
[cov-badge]: https://coveralls.io/repos/github/ddmms/pack-mm/badge.svg?branch=main
|
34
|
+
[cov-link]: https://coveralls.io/github/ddmms/pack-mm?branch=main
|
35
|
+
[license-badge]: https://img.shields.io/badge/License-MIT-yellow.svg
|
36
|
+
[license-link]: https://opensource.org/license/MIT
|
pack_mm-0.0.14/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# pack materials and molecules
|
2
|
+
|
3
|
+
[![Python versions][python-badge]][python-link]
|
4
|
+
[![Build Status][ci-badge]][ci-link]
|
5
|
+
[![Coverage Status][cov-badge]][cov-link]
|
6
|
+
[![License][license-badge]][license-link]
|
7
|
+
|
8
|
+
[python-badge]: https://img.shields.io/pypi/pyversions/pack-mm.svg
|
9
|
+
[python-link]: https://pypi.org/project/pack-mm/
|
10
|
+
[ci-badge]: https://github.com/ddmms/pack-mm/actions/workflows/build.yml/badge.svg?branch=main
|
11
|
+
[ci-link]: https://github.com/ddmms/pack-mm/actions
|
12
|
+
[cov-badge]: https://coveralls.io/repos/github/ddmms/pack-mm/badge.svg?branch=main
|
13
|
+
[cov-link]: https://coveralls.io/github/ddmms/pack-mm?branch=main
|
14
|
+
[license-badge]: https://img.shields.io/badge/License-MIT-yellow.svg
|
15
|
+
[license-link]: https://opensource.org/license/MIT
|
@@ -0,0 +1,161 @@
|
|
1
|
+
"""Command line for packmm."""
|
2
|
+
|
3
|
+
# Author; alin m elena, alin@elena.re
|
4
|
+
# Contribs;
|
5
|
+
# Date: 22-02-2025
|
6
|
+
# ©alin m elena,
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
from enum import Enum
|
10
|
+
|
11
|
+
import typer
|
12
|
+
|
13
|
+
from pack_mm.core.core import pack_molecules
|
14
|
+
|
15
|
+
|
16
|
+
class InsertionMethod(str, Enum):
|
17
|
+
"""Insertion options."""
|
18
|
+
|
19
|
+
ANYWHERE = "anywhere"
|
20
|
+
SPHERE = "sphere"
|
21
|
+
BOX = "box"
|
22
|
+
CYLINDER_Z = "cylinderZ"
|
23
|
+
CYLINDER_Y = "cylinderY"
|
24
|
+
CYLINDER_X = "cylinderX"
|
25
|
+
ELLIPSOID = "ellipsoid"
|
26
|
+
|
27
|
+
|
28
|
+
app = typer.Typer(no_args_is_help=True)
|
29
|
+
|
30
|
+
|
31
|
+
@app.command()
|
32
|
+
def packmm(
|
33
|
+
system: str | None = typer.Option(
|
34
|
+
None,
|
35
|
+
help="""The original box in which you want to add particles.
|
36
|
+
If not provided, an empty box will be created.""",
|
37
|
+
),
|
38
|
+
molecule: str = typer.Option(
|
39
|
+
"H2O",
|
40
|
+
help="""Name of the molecule to be processed, ASE-recognizable or
|
41
|
+
ASE-readable file.""",
|
42
|
+
),
|
43
|
+
nmols: int = typer.Option(-1, help="Target number of molecules to insert."),
|
44
|
+
ntries: int = typer.Option(
|
45
|
+
50, help="Maximum number of attempts to insert each molecule."
|
46
|
+
),
|
47
|
+
seed: int = typer.Option(2025, help="Random seed for reproducibility."),
|
48
|
+
where: InsertionMethod = typer.Option(
|
49
|
+
InsertionMethod.ANYWHERE,
|
50
|
+
help="""Where to insert the molecule. Choices: 'anywhere', 'sphere',
|
51
|
+
'box', 'cylinderZ', 'cylinderY', 'cylinderX', 'ellipsoid'.""",
|
52
|
+
),
|
53
|
+
centre: str | None = typer.Option(
|
54
|
+
None,
|
55
|
+
help="""Centre of the insertion zone in fractional coordinates,
|
56
|
+
e.g., '0.12,0.4,0.5'.""",
|
57
|
+
),
|
58
|
+
radius: float | None = typer.Option(
|
59
|
+
None,
|
60
|
+
help="""Radius of the sphere or cylinder in Å,
|
61
|
+
depending on the insertion volume.""",
|
62
|
+
),
|
63
|
+
height: float | None = typer.Option(
|
64
|
+
None, help="Height of the cylinder in fractional coordinates."
|
65
|
+
),
|
66
|
+
a: float | None = typer.Option(
|
67
|
+
None,
|
68
|
+
help="""Side of the box or semi-axis of the ellipsoid, fractional,
|
69
|
+
depends on the insertion method.""",
|
70
|
+
),
|
71
|
+
b: float | None = typer.Option(
|
72
|
+
None,
|
73
|
+
help="""Side of the box or semi-axis of the ellipsoid, fractional,
|
74
|
+
depends on the insertion method.""",
|
75
|
+
),
|
76
|
+
c: float | None = typer.Option(
|
77
|
+
None,
|
78
|
+
help="""Side of the box or semi-axis of the ellipsoid, fractional,
|
79
|
+
depends on the insertion method.""",
|
80
|
+
),
|
81
|
+
device: str = typer.Option(
|
82
|
+
"cpu", help="Device to run calculations on (e.g., 'cpu' or 'cuda')."
|
83
|
+
),
|
84
|
+
model: str = typer.Option("medium-omat-0", help="ML model to use."),
|
85
|
+
arch: str = typer.Option("mace_mp", help="MLIP architecture to use."),
|
86
|
+
temperature: float = typer.Option(
|
87
|
+
300.0, help="Temperature for the Monte Carlo acceptance rule."
|
88
|
+
),
|
89
|
+
cell_a: float = typer.Option(20.0, help="Side of the empty box along the x-axis."),
|
90
|
+
cell_b: float = typer.Option(20.0, help="Side of the empty box along the y-axis."),
|
91
|
+
cell_c: float = typer.Option(20.0, help="Side of the empty box along the z-axis."),
|
92
|
+
fmax: float = typer.Option(
|
93
|
+
0.1, help="force tollerance for optimisation if needed."
|
94
|
+
),
|
95
|
+
geometry: bool = typer.Option(
|
96
|
+
True, help="Perform geometry optimization at the end."
|
97
|
+
),
|
98
|
+
):
|
99
|
+
"""Pack molecules into a system based on the specified parameters."""
|
100
|
+
print("Script called with following input")
|
101
|
+
print(f"{system=}")
|
102
|
+
print(f"{nmols=}")
|
103
|
+
print(f"{molecule=}")
|
104
|
+
print(f"{ntries=}")
|
105
|
+
print(f"{seed=}")
|
106
|
+
print(f"where={where.value}")
|
107
|
+
print(f"{centre=}")
|
108
|
+
print(f"{radius=}")
|
109
|
+
print(f"{height=}")
|
110
|
+
print(f"{a=}")
|
111
|
+
print(f"{b=}")
|
112
|
+
print(f"{c=}")
|
113
|
+
print(f"{cell_a=}")
|
114
|
+
print(f"{cell_b=}")
|
115
|
+
print(f"{cell_c=}")
|
116
|
+
print(f"{arch=}")
|
117
|
+
print(f"{model=}")
|
118
|
+
print(f"{device=}")
|
119
|
+
print(f"{temperature=}")
|
120
|
+
print(f"{fmax=}")
|
121
|
+
print(f"{geometry=}")
|
122
|
+
if nmols == -1:
|
123
|
+
print("nothing to do, no molecule to insert")
|
124
|
+
raise typer.Exit(0)
|
125
|
+
|
126
|
+
center = centre
|
127
|
+
if centre:
|
128
|
+
center = tuple(map(float, centre.split(",")))
|
129
|
+
lc = [x < 0.0 for x in center]
|
130
|
+
if len(center) != 3 or any(lc):
|
131
|
+
err = "Invalid centre 3 coordinates expected!"
|
132
|
+
print(f"{err}")
|
133
|
+
raise Exception("Invalid centre 3 coordinates expected!")
|
134
|
+
|
135
|
+
pack_molecules(
|
136
|
+
system=system,
|
137
|
+
molecule=molecule,
|
138
|
+
nmols=nmols,
|
139
|
+
arch=arch,
|
140
|
+
model=model,
|
141
|
+
device=device,
|
142
|
+
where=where,
|
143
|
+
center=center,
|
144
|
+
radius=radius,
|
145
|
+
height=height,
|
146
|
+
a=a,
|
147
|
+
b=b,
|
148
|
+
c=c,
|
149
|
+
seed=seed,
|
150
|
+
temperature=temperature,
|
151
|
+
ntries=ntries,
|
152
|
+
fmax=fmax,
|
153
|
+
geometry=geometry,
|
154
|
+
ca=cell_a,
|
155
|
+
cb=cell_b,
|
156
|
+
cc=cell_c,
|
157
|
+
)
|
158
|
+
|
159
|
+
|
160
|
+
if __name__ == "__main__":
|
161
|
+
app()
|
@@ -0,0 +1,345 @@
|
|
1
|
+
# Author; alin m elena, alin@elena.re
|
2
|
+
# Contribs;
|
3
|
+
# Date: 16-11-2024
|
4
|
+
# ©alin m elena,
|
5
|
+
"""pack molecules inside various shapes."""
|
6
|
+
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
from pathlib import Path
|
10
|
+
|
11
|
+
from ase import Atoms
|
12
|
+
from ase.build import molecule as build_molecule
|
13
|
+
from ase.io import read, write
|
14
|
+
from janus_core.calculations.geom_opt import GeomOpt
|
15
|
+
from janus_core.helpers.mlip_calculators import choose_calculator
|
16
|
+
from numpy import cos, exp, pi, random, sin, sqrt
|
17
|
+
|
18
|
+
|
19
|
+
def random_point_in_sphere(c: (float, float, float), r: float) -> (float, float, float):
|
20
|
+
"""
|
21
|
+
Generate a random point inside a sphere of radius r, centered at c.
|
22
|
+
|
23
|
+
Parameters
|
24
|
+
----------
|
25
|
+
c (tuple): The center of the sphere as (x, y, z).
|
26
|
+
r (float): The radius of the sphere.
|
27
|
+
|
28
|
+
Returns
|
29
|
+
-------
|
30
|
+
tuple: A point (x, y, z) inside the sphere.
|
31
|
+
"""
|
32
|
+
rad = r * random.rand() ** (1 / 3)
|
33
|
+
|
34
|
+
theta = random.uniform(0, 2 * pi)
|
35
|
+
phi = random.uniform(0, pi)
|
36
|
+
|
37
|
+
x = c[0] + rad * sin(phi) * cos(theta)
|
38
|
+
y = c[1] + rad * sin(phi) * sin(theta)
|
39
|
+
z = c[2] + rad * cos(phi)
|
40
|
+
|
41
|
+
return (x, y, z)
|
42
|
+
|
43
|
+
|
44
|
+
def random_point_in_ellipsoid(
|
45
|
+
d: (float, float, float), a: float, b: float, c: float
|
46
|
+
) -> (float, float, float):
|
47
|
+
"""
|
48
|
+
Generate a random point inside an ellipsoid with axes a, b, c, centered at d.
|
49
|
+
|
50
|
+
Parameters
|
51
|
+
----------
|
52
|
+
d (tuple): The center of the ellipsoid as (x, y, z).
|
53
|
+
a (float): The semi-axis length of the ellipsoid along the x-axis.
|
54
|
+
b (float): The semi-axis length of the ellipsoid along the y-axis.
|
55
|
+
c (float): The semi-axis length of the ellipsoid along the z-axis.
|
56
|
+
|
57
|
+
Returns
|
58
|
+
-------
|
59
|
+
tuple: A point (x, y, z) inside the ellipsoid.
|
60
|
+
"""
|
61
|
+
theta = random.uniform(0, 2 * pi)
|
62
|
+
phi = random.uniform(0, pi)
|
63
|
+
rad = random.rand() ** (1 / 3)
|
64
|
+
|
65
|
+
x = d[0] + a * rad * sin(phi) * cos(theta)
|
66
|
+
y = d[1] + b * rad * sin(phi) * sin(theta)
|
67
|
+
z = d[2] + c * rad * cos(phi)
|
68
|
+
|
69
|
+
return (x, y, z)
|
70
|
+
|
71
|
+
|
72
|
+
def random_point_in_box(
|
73
|
+
d: (float, float, float), a: float, b: float, c: float
|
74
|
+
) -> (float, float, float):
|
75
|
+
"""
|
76
|
+
Generate a random point inside a box with sides a, b, c, centered at d.
|
77
|
+
|
78
|
+
Parameters
|
79
|
+
----------
|
80
|
+
d (tuple): The center of the box as (x, y, z).
|
81
|
+
a (float): The length of the box along the x-axis.
|
82
|
+
b (float): The length of the box along the y-axis.
|
83
|
+
c (float): The length of the box along the z-axis.
|
84
|
+
|
85
|
+
Returns
|
86
|
+
-------
|
87
|
+
tuple: A point (x, y, z) inside the box.
|
88
|
+
"""
|
89
|
+
x = d[0] + random.uniform(-a * 0.5, a * 0.5)
|
90
|
+
y = d[1] + random.uniform(-b * 0.5, b * 0.5)
|
91
|
+
z = d[2] + random.uniform(-c * 0.5, c * 0.5)
|
92
|
+
|
93
|
+
return (x, y, z)
|
94
|
+
|
95
|
+
|
96
|
+
def random_point_in_cylinder(
|
97
|
+
c: (float, float, float), r: float, h: float, d: str
|
98
|
+
) -> (float, float, float):
|
99
|
+
"""
|
100
|
+
Generate a random point inside a cylinder with radius r and height h, centered at c.
|
101
|
+
|
102
|
+
Parameters
|
103
|
+
----------
|
104
|
+
c (tuple): The center of the cylinder as (x, y, z).
|
105
|
+
r (float): The radius of the cylinder's base.
|
106
|
+
h (float): The height of the cylinder.
|
107
|
+
direction (str): direction along which cylinger is oriented
|
108
|
+
|
109
|
+
Returns
|
110
|
+
-------
|
111
|
+
tuple: A point (x, y, z) inside the cylinder.
|
112
|
+
"""
|
113
|
+
theta = random.uniform(0, 2 * pi)
|
114
|
+
rad = r * sqrt(random.rand())
|
115
|
+
|
116
|
+
if d == "z":
|
117
|
+
z = c[2] + random.uniform(-h * 0.5, h * 0.5)
|
118
|
+
x = c[0] + rad * cos(theta)
|
119
|
+
y = c[1] + rad * sin(theta)
|
120
|
+
elif d == "y":
|
121
|
+
y = c[1] + random.uniform(-h * 0.5, h * 0.5)
|
122
|
+
x = c[0] + rad * cos(theta)
|
123
|
+
z = c[2] + rad * sin(theta)
|
124
|
+
elif d == "x":
|
125
|
+
x = c[0] + random.uniform(-h * 0.5, h * 0.5)
|
126
|
+
y = c[1] + rad * sin(theta)
|
127
|
+
z = c[2] + rad * cos(theta)
|
128
|
+
|
129
|
+
return (x, y, z)
|
130
|
+
|
131
|
+
|
132
|
+
def validate_value(label, x):
|
133
|
+
"""Validate input value, and raise an exception."""
|
134
|
+
if x is not None and x < 0.0:
|
135
|
+
err = f"Invalid {label}, needs to be positive"
|
136
|
+
print(err)
|
137
|
+
raise Exception(err)
|
138
|
+
|
139
|
+
|
140
|
+
def pack_molecules(
|
141
|
+
system: str | None,
|
142
|
+
molecule: str,
|
143
|
+
nmols: int,
|
144
|
+
arch: str,
|
145
|
+
model: str,
|
146
|
+
device: str,
|
147
|
+
where: str,
|
148
|
+
center: tuple[float, float, float] | None,
|
149
|
+
radius: float | None,
|
150
|
+
height: float | None,
|
151
|
+
a: float | None,
|
152
|
+
b: float | None,
|
153
|
+
c: float | None,
|
154
|
+
seed: int,
|
155
|
+
temperature: float,
|
156
|
+
ntries: int,
|
157
|
+
geometry: bool,
|
158
|
+
fmax: float,
|
159
|
+
ca: float,
|
160
|
+
cb: float,
|
161
|
+
cc: float,
|
162
|
+
) -> None:
|
163
|
+
"""
|
164
|
+
Pack molecules into a system based on the specified parameters.
|
165
|
+
|
166
|
+
Parameters
|
167
|
+
----------
|
168
|
+
system (str): Path to the system file or name of the system.
|
169
|
+
molecule (str): Path to the molecule file or name of the molecule.
|
170
|
+
nmols (int): Number of molecules to insert.
|
171
|
+
arch (str): Architecture for the calculator.
|
172
|
+
model (str): Path to the model file.
|
173
|
+
device (str): Device to run calculations on (e.g., "cpu" or "cuda").
|
174
|
+
where (str): Region to insert molecules ("anywhere",
|
175
|
+
"sphere", "cylinderZ", etc.).
|
176
|
+
center (Optional[Tuple[float, float, float]]): Center of the insertion region.
|
177
|
+
radius (Optional[float]): Radius for spherical or cylindrical insertion.
|
178
|
+
height (Optional[float]): Height for cylindrical insertion.
|
179
|
+
a, b, c (Optional[float]): Parameters for box or ellipsoid insertion.
|
180
|
+
seed (int): Random seed for reproducibility.
|
181
|
+
temperature (float): Temperature in Kelvin for acceptance probability.
|
182
|
+
ntries (int): Maximum number of attempts to insert each molecule.
|
183
|
+
geometry (bool): Whether to perform geometry optimization after insertion.
|
184
|
+
ca, cb, cc (float): Cell dimensions if system is empty.
|
185
|
+
"""
|
186
|
+
kbt = temperature * 8.6173303e-5 # Boltzmann constant in eV/K
|
187
|
+
validate_value("temperature", temperature)
|
188
|
+
validate_value("radius", radius)
|
189
|
+
validate_value("height", height)
|
190
|
+
validate_value("fmax", fmax)
|
191
|
+
validate_value("seed", seed)
|
192
|
+
validate_value("box a", a)
|
193
|
+
validate_value("box b", b)
|
194
|
+
validate_value("box c", c)
|
195
|
+
validate_value("ntries", ntries)
|
196
|
+
validate_value("cell box a", ca)
|
197
|
+
validate_value("cell box b", cb)
|
198
|
+
validate_value("nmols", nmols)
|
199
|
+
validate_value("cell box c", cc)
|
200
|
+
|
201
|
+
random.seed(seed)
|
202
|
+
|
203
|
+
try:
|
204
|
+
sys = read(system)
|
205
|
+
sysname = Path(system).stem
|
206
|
+
except Exception:
|
207
|
+
sys = Atoms(cell=[ca, cb, cc], pbc=[True, True, True])
|
208
|
+
sysname = "empty"
|
209
|
+
|
210
|
+
cell = sys.cell.lengths()
|
211
|
+
|
212
|
+
# Print initial information
|
213
|
+
print(f"Inserting {nmols} {molecule} molecules in {sysname}.")
|
214
|
+
print(f"Using {arch} model {model} on {device}.")
|
215
|
+
print(f"Insert in {where}.")
|
216
|
+
|
217
|
+
# Set center of insertion region
|
218
|
+
if center is None:
|
219
|
+
center = (cell[0] * 0.5, cell[1] * 0.5, cell[2] * 0.5)
|
220
|
+
else:
|
221
|
+
center = tuple(ci * cell[i] for i, ci in enumerate(center))
|
222
|
+
|
223
|
+
# Set parameters based on insertion region
|
224
|
+
if where == "anywhere":
|
225
|
+
a, b, c = 1, 1, 1
|
226
|
+
elif where == "sphere":
|
227
|
+
if radius is None:
|
228
|
+
radius = min(cell) * 0.5
|
229
|
+
elif where in ["cylinderZ", "cylinderY", "cylinderX"]:
|
230
|
+
if radius is None:
|
231
|
+
if where == "cylinderZ":
|
232
|
+
radius = min(cell[0], cell[1]) * 0.5
|
233
|
+
elif where == "cylinderY":
|
234
|
+
radius = min(cell[0], cell[2]) * 0.5
|
235
|
+
elif where == "cylinderX":
|
236
|
+
radius = min(cell[2], cell[1]) * 0.5
|
237
|
+
if height is None:
|
238
|
+
height = 0.5
|
239
|
+
elif where == "box":
|
240
|
+
a, b, c = a or 1, b or 1, c or 1
|
241
|
+
elif where == "ellipsoid":
|
242
|
+
a, b, c = a or 0.5, b or 0.5, c or 0.5
|
243
|
+
|
244
|
+
calc = choose_calculator(
|
245
|
+
arch=arch, model_path=model, device=device, default_dtype="float64"
|
246
|
+
)
|
247
|
+
sys.calc = calc
|
248
|
+
|
249
|
+
e = sys.get_potential_energy() if len(sys) > 0 else 0.0
|
250
|
+
|
251
|
+
csys = sys.copy()
|
252
|
+
for i in range(nmols):
|
253
|
+
accept = False
|
254
|
+
for _itry in range(ntries):
|
255
|
+
mol = load_molecule(molecule)
|
256
|
+
tv = get_insertion_position(where, center, cell, a, b, c, radius, height)
|
257
|
+
mol = rotate_molecule(mol)
|
258
|
+
mol.translate(tv)
|
259
|
+
|
260
|
+
tsys = csys.copy() + mol.copy()
|
261
|
+
tsys.calc = calc
|
262
|
+
en = tsys.get_potential_energy()
|
263
|
+
de = en - e
|
264
|
+
|
265
|
+
acc = exp(-de / kbt)
|
266
|
+
u = random.random()
|
267
|
+
print(f"Old energy={e}, new energy={en}, {de=}, {acc=}, random={u}")
|
268
|
+
|
269
|
+
if u <= acc:
|
270
|
+
accept = True
|
271
|
+
break
|
272
|
+
|
273
|
+
if accept:
|
274
|
+
csys = tsys.copy()
|
275
|
+
e = en
|
276
|
+
print(f"Inserted particle {i + 1}")
|
277
|
+
write(f"{sysname}+{i + 1}{Path(molecule).stem}.cif", csys)
|
278
|
+
else:
|
279
|
+
print(f"Failed to insert particle {i + 1} after {ntries} tries")
|
280
|
+
optimize_geometry(
|
281
|
+
f"{sysname}+{i + 1}{Path(molecule).stem}.cif", device, arch, model, fmax
|
282
|
+
)
|
283
|
+
|
284
|
+
# Perform final geometry optimization if requested
|
285
|
+
if geometry:
|
286
|
+
optimize_geometry(
|
287
|
+
f"{sysname}+{nmols}{Path(molecule).stem}.cif", device, arch, model, fmax
|
288
|
+
)
|
289
|
+
|
290
|
+
|
291
|
+
def load_molecule(molecule: str):
|
292
|
+
"""Load a molecule from a file or build it."""
|
293
|
+
try:
|
294
|
+
return build_molecule(molecule)
|
295
|
+
except KeyError:
|
296
|
+
return read(molecule)
|
297
|
+
|
298
|
+
|
299
|
+
def get_insertion_position(
|
300
|
+
where: str,
|
301
|
+
center: tuple[float, float, float],
|
302
|
+
cell: list[float],
|
303
|
+
a: float,
|
304
|
+
b: float,
|
305
|
+
c: float,
|
306
|
+
radius: float | None,
|
307
|
+
height: float | None,
|
308
|
+
) -> tuple[float, float, float]:
|
309
|
+
"""Get a random insertion position based on the region."""
|
310
|
+
if where == "sphere":
|
311
|
+
return random_point_in_sphere(center, radius)
|
312
|
+
if where == "box":
|
313
|
+
return random_point_in_box(center, cell[0] * a, cell[1] * b, cell[2] * c)
|
314
|
+
if where == "ellipsoid":
|
315
|
+
return random_point_in_ellipsoid(center, cell[0] * a, cell[1] * b, cell[2] * c)
|
316
|
+
if where in ["cylinderZ", "cylinderY", "cylinderX"]:
|
317
|
+
axis = where[-1].lower()
|
318
|
+
return random_point_in_cylinder(center, radius, cell[2] * height, axis)
|
319
|
+
# now is anywhere
|
320
|
+
return random.random(3) * [a, b, c] * cell
|
321
|
+
|
322
|
+
|
323
|
+
def rotate_molecule(mol):
|
324
|
+
"""Rotate a molecule randomly."""
|
325
|
+
ang = random.random(3)
|
326
|
+
mol.euler_rotate(
|
327
|
+
phi=ang[0] * 360, theta=ang[1] * 180, psi=ang[2] * 360, center=(0.0, 0.0, 0.0)
|
328
|
+
)
|
329
|
+
return mol
|
330
|
+
|
331
|
+
|
332
|
+
def optimize_geometry(
|
333
|
+
struct_path: str, device: str, arch: str, model: str, fmax: float
|
334
|
+
) -> float:
|
335
|
+
"""Optimize the geometry of a structure."""
|
336
|
+
geo = GeomOpt(
|
337
|
+
struct_path=struct_path,
|
338
|
+
device=device,
|
339
|
+
fmax=fmax,
|
340
|
+
calc_kwargs={"model_paths": model, "default_dtype": "float64"},
|
341
|
+
filter_kwargs={"hydrostatic_strain": True},
|
342
|
+
)
|
343
|
+
geo.run()
|
344
|
+
write(f"{struct_path}-opt.cif", geo.struct)
|
345
|
+
return geo.struct.get_potential_energy()
|
@@ -0,0 +1,138 @@
|
|
1
|
+
[project]
|
2
|
+
name = "pack-mm"
|
3
|
+
version = "0.0.14"
|
4
|
+
description = "packing materials and molecules in boxes using for machine learnt interatomic potentials"
|
5
|
+
authors = [
|
6
|
+
{ name = "Alin M. Elena" },
|
7
|
+
]
|
8
|
+
requires-python = ">=3.10"
|
9
|
+
classifiers = [
|
10
|
+
"Programming Language :: Python",
|
11
|
+
"Programming Language :: Python :: 3.10",
|
12
|
+
"Programming Language :: Python :: 3.11",
|
13
|
+
"Programming Language :: Python :: 3.12",
|
14
|
+
"Intended Audience :: Science/Research",
|
15
|
+
"License :: OSI Approved :: MIT License",
|
16
|
+
"Natural Language :: English",
|
17
|
+
"Development Status :: 3 - Alpha",
|
18
|
+
]
|
19
|
+
readme = "README.md"
|
20
|
+
dependencies = [
|
21
|
+
"janus-core>=0.7.2",
|
22
|
+
"typer<1.0.0,>=0.12.5",
|
23
|
+
"typer-config<2.0.0,>=1.4.2",
|
24
|
+
]
|
25
|
+
|
26
|
+
[project.scripts]
|
27
|
+
packmm = "pack_mm.cli.packmm:app"
|
28
|
+
|
29
|
+
[project.urls]
|
30
|
+
Repository = "https://github.com/ddmms/pack-mm/"
|
31
|
+
Documentation = "https://ddmms.github.io/pack-mm/"
|
32
|
+
|
33
|
+
[dependency-groups]
|
34
|
+
dev = [
|
35
|
+
"coverage[toml]<8.0.0,>=7.4.1",
|
36
|
+
"ipykernel>=6.29.5",
|
37
|
+
"pgtest<2.0.0,>=1.3.2",
|
38
|
+
"pytest<9.0,>=8.0",
|
39
|
+
"pytest-cov<5.0.0,>=4.1.0",
|
40
|
+
"tox-uv<2.0,>=1.16.1",
|
41
|
+
"wheel<1.0,>=0.42",
|
42
|
+
]
|
43
|
+
docs = [
|
44
|
+
"furo<2025.0.0,>=2024.1.29",
|
45
|
+
"jupyter>=1.1.1",
|
46
|
+
"markupsafe<2.1",
|
47
|
+
"nbsphinx>=0.9.6",
|
48
|
+
"numpydoc<2.0.0,>=1.6.0",
|
49
|
+
"sphinx<9.0.0,>=8.0.2",
|
50
|
+
"sphinxcontrib-contentui<1.0.0,>=0.2.5",
|
51
|
+
"sphinxcontrib-details-directive<1.0,>=0.1",
|
52
|
+
"sphinx-autodoc-typehints<3.0.0,>=2.5.0",
|
53
|
+
"sphinx-collapse>=0.1.3",
|
54
|
+
"sphinx-copybutton<1.0.0,>=0.5.2",
|
55
|
+
]
|
56
|
+
pre-commit = [
|
57
|
+
"pre-commit<4.0.0,>=3.6.0",
|
58
|
+
"ruff<1.0.0,>=0.9.3",
|
59
|
+
]
|
60
|
+
|
61
|
+
[build-system]
|
62
|
+
requires = [
|
63
|
+
"pdm-backend",
|
64
|
+
]
|
65
|
+
build-backend = "pdm.backend"
|
66
|
+
|
67
|
+
[tool.pytest.ini_options]
|
68
|
+
python_files = "test_*.py"
|
69
|
+
addopts = "--cov-report xml"
|
70
|
+
pythonpath = [
|
71
|
+
".",
|
72
|
+
]
|
73
|
+
|
74
|
+
[tool.coverage.run]
|
75
|
+
source = [
|
76
|
+
"pack_mm",
|
77
|
+
]
|
78
|
+
|
79
|
+
[tool.ruff]
|
80
|
+
exclude = [
|
81
|
+
"conf.py",
|
82
|
+
"*ipynb",
|
83
|
+
]
|
84
|
+
target-version = "py310"
|
85
|
+
|
86
|
+
[tool.ruff.lint]
|
87
|
+
ignore = [
|
88
|
+
"C901",
|
89
|
+
"B008",
|
90
|
+
]
|
91
|
+
select = [
|
92
|
+
"B",
|
93
|
+
"C",
|
94
|
+
"R",
|
95
|
+
"D",
|
96
|
+
"E",
|
97
|
+
"W",
|
98
|
+
"F",
|
99
|
+
"FA",
|
100
|
+
"I",
|
101
|
+
"N",
|
102
|
+
"UP",
|
103
|
+
]
|
104
|
+
|
105
|
+
[tool.ruff.lint.isort]
|
106
|
+
force-sort-within-sections = true
|
107
|
+
required-imports = [
|
108
|
+
"from __future__ import annotations",
|
109
|
+
]
|
110
|
+
|
111
|
+
[tool.ruff.lint.pydocstyle]
|
112
|
+
convention = "numpy"
|
113
|
+
|
114
|
+
[tool.ruff.lint.pylint]
|
115
|
+
max-args = 10
|
116
|
+
|
117
|
+
[tool.ruff.lint.pyupgrade]
|
118
|
+
keep-runtime-typing = false
|
119
|
+
|
120
|
+
[tool.numpydoc_validation]
|
121
|
+
checks = [
|
122
|
+
"all",
|
123
|
+
"EX01",
|
124
|
+
"SA01",
|
125
|
+
"ES01",
|
126
|
+
"PR04",
|
127
|
+
]
|
128
|
+
exclude = [
|
129
|
+
".__weakref__$",
|
130
|
+
".__repr__$",
|
131
|
+
]
|
132
|
+
|
133
|
+
[tool.uv]
|
134
|
+
default-groups = [
|
135
|
+
"dev",
|
136
|
+
"docs",
|
137
|
+
"pre-commit",
|
138
|
+
]
|
@@ -0,0 +1,206 @@
|
|
1
|
+
"""Test cli for packmm."""
|
2
|
+
|
3
|
+
# Author; alin m elena, alin@elena.re
|
4
|
+
# Contribs;
|
5
|
+
# Date: 22-02-2025
|
6
|
+
# ©alin m elena, GPL v3 https://www.gnu.org/licenses/gpl-3.0.en.html
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
from typer.testing import CliRunner
|
10
|
+
|
11
|
+
from pack_mm.cli.packmm import app
|
12
|
+
from tests.utils import strip_ansi_codes
|
13
|
+
|
14
|
+
runner = CliRunner()
|
15
|
+
|
16
|
+
|
17
|
+
def test_packmm_default_values():
|
18
|
+
"""Check values."""
|
19
|
+
result = runner.invoke(app)
|
20
|
+
assert result.exit_code == 0
|
21
|
+
assert "nothing to do" in strip_ansi_codes(result.output)
|
22
|
+
|
23
|
+
|
24
|
+
def test_packmm_custom_molecule():
|
25
|
+
"""Check molecule."""
|
26
|
+
result = runner.invoke(app, ["--molecule", "CO2"])
|
27
|
+
assert result.exit_code == 0
|
28
|
+
assert "CO2" in strip_ansi_codes(result.output)
|
29
|
+
|
30
|
+
|
31
|
+
def test_packmm_custom_nmols():
|
32
|
+
"""Check nmols."""
|
33
|
+
result = runner.invoke(app, ["--nmols", "1"])
|
34
|
+
assert result.exit_code == 0
|
35
|
+
assert "nmols=1" in strip_ansi_codes(result.output)
|
36
|
+
|
37
|
+
|
38
|
+
def test_packmm_custom_ntries():
|
39
|
+
"""Check ntries."""
|
40
|
+
result = runner.invoke(app, ["--ntries", "1"])
|
41
|
+
assert result.exit_code == 0
|
42
|
+
assert "ntries=1" in strip_ansi_codes(result.output)
|
43
|
+
|
44
|
+
|
45
|
+
def test_packmm_custom_seed():
|
46
|
+
"""Check seed."""
|
47
|
+
result = runner.invoke(app, ["--seed", "1234"])
|
48
|
+
assert result.exit_code == 0
|
49
|
+
assert "seed=1234" in strip_ansi_codes(result.output)
|
50
|
+
|
51
|
+
|
52
|
+
def test_packmm_custom_insertion_method():
|
53
|
+
"""Check insertion."""
|
54
|
+
result = runner.invoke(app, ["--where", "sphere"])
|
55
|
+
assert result.exit_code == 0
|
56
|
+
assert "where=sphere" in strip_ansi_codes(result.output)
|
57
|
+
|
58
|
+
|
59
|
+
def test_packmm_custom_center():
|
60
|
+
"""Check centre."""
|
61
|
+
result = runner.invoke(app, ["--centre", "0.5,0.5,0.5"])
|
62
|
+
assert result.exit_code == 0
|
63
|
+
assert "centre='0.5,0.5,0.5'" in strip_ansi_codes(result.output)
|
64
|
+
|
65
|
+
|
66
|
+
def test_packmm_custom_radius():
|
67
|
+
"""Check radius."""
|
68
|
+
result = runner.invoke(app, ["--radius", "10.0"])
|
69
|
+
assert result.exit_code == 0
|
70
|
+
assert "radius=10.0" in strip_ansi_codes(result.output)
|
71
|
+
|
72
|
+
|
73
|
+
def test_packmm_custom_height():
|
74
|
+
"""Check height."""
|
75
|
+
result = runner.invoke(app, ["--height", "5.0"])
|
76
|
+
assert result.exit_code == 0
|
77
|
+
assert "height=5.0" in strip_ansi_codes(result.output)
|
78
|
+
|
79
|
+
|
80
|
+
def test_packmm_mlip():
|
81
|
+
"""Check mlip."""
|
82
|
+
result = runner.invoke(
|
83
|
+
app, ["--arch", "mace", "--model", "some", "--device", "cuda"]
|
84
|
+
)
|
85
|
+
assert result.exit_code == 0
|
86
|
+
assert "arch='mace'" in strip_ansi_codes(result.output)
|
87
|
+
assert "model='some'" in strip_ansi_codes(result.output)
|
88
|
+
assert "device='cuda'" in strip_ansi_codes(result.output)
|
89
|
+
|
90
|
+
|
91
|
+
def test_packmm_custom_box_dimensions():
|
92
|
+
"""Check box."""
|
93
|
+
result = runner.invoke(app, ["--a", "30.0", "--b", "30.0", "--c", "30.0"])
|
94
|
+
assert result.exit_code == 0
|
95
|
+
assert "a=30.0" in strip_ansi_codes(result.output)
|
96
|
+
assert "b=30.0" in strip_ansi_codes(result.output)
|
97
|
+
assert "c=30.0" in strip_ansi_codes(result.output)
|
98
|
+
|
99
|
+
|
100
|
+
def test_packmm_empty_box_dimensions():
|
101
|
+
"""Check box empty."""
|
102
|
+
result = runner.invoke(
|
103
|
+
app, ["--cell-a", "30.0", "--cell-b", "30.0", "--cell-c", "30.0"]
|
104
|
+
)
|
105
|
+
assert result.exit_code == 0
|
106
|
+
assert "cell_a=30.0" in strip_ansi_codes(result.output)
|
107
|
+
assert "cell_b=30.0" in strip_ansi_codes(result.output)
|
108
|
+
assert "cell_c=30.0" in strip_ansi_codes(result.output)
|
109
|
+
|
110
|
+
|
111
|
+
def test_packmm_custom_temperature():
|
112
|
+
"""Check temperature."""
|
113
|
+
result = runner.invoke(app, ["--temperature", "400.0"])
|
114
|
+
assert result.exit_code == 0
|
115
|
+
assert "temperature=400.0" in strip_ansi_codes(result.output)
|
116
|
+
|
117
|
+
|
118
|
+
def test_packmm_custom_fmax():
|
119
|
+
"""Check fmax."""
|
120
|
+
result = runner.invoke(app, ["--fmax", "0.05"])
|
121
|
+
assert result.exit_code == 0
|
122
|
+
assert "fmax=0.05" in strip_ansi_codes(result.output)
|
123
|
+
|
124
|
+
|
125
|
+
def test_packmm_no_geometry_optimization():
|
126
|
+
"""Check optimisation."""
|
127
|
+
result = runner.invoke(app, ["--no-geometry"])
|
128
|
+
assert result.exit_code == 0
|
129
|
+
assert "geometry=False" in strip_ansi_codes(result.output)
|
130
|
+
|
131
|
+
|
132
|
+
def test_packmm_invalid_insertion_method():
|
133
|
+
"""Check insertion."""
|
134
|
+
result = runner.invoke(app, ["--where", "invalid_method"])
|
135
|
+
assert result.exit_code != 0
|
136
|
+
assert "Invalid value for '--where'" in strip_ansi_codes(result.output)
|
137
|
+
|
138
|
+
|
139
|
+
def test_packmm_invalid_centre_format():
|
140
|
+
"""Check centre."""
|
141
|
+
result = runner.invoke(app, ["--nmols", "1", "--centre", "0.5,0.5"])
|
142
|
+
assert result.exit_code != 0
|
143
|
+
assert "Invalid centre" in strip_ansi_codes(result.output)
|
144
|
+
|
145
|
+
|
146
|
+
def test_packmm_invalid_centre_value():
|
147
|
+
"""Check centre."""
|
148
|
+
result = runner.invoke(app, ["--nmols", "1", "--centre", "-0.6,0.5,0.5"])
|
149
|
+
assert result.exit_code != 0
|
150
|
+
assert "Invalid centre" in strip_ansi_codes(result.output)
|
151
|
+
|
152
|
+
|
153
|
+
def test_packmm_invalid_radius():
|
154
|
+
"""Check box radius."""
|
155
|
+
result = runner.invoke(app, ["--nmols", "1", "--radius", "-10.0"])
|
156
|
+
assert result.exit_code != 0
|
157
|
+
assert "Invalid radius" in strip_ansi_codes(result.output)
|
158
|
+
|
159
|
+
|
160
|
+
def test_packmm_invalid_height():
|
161
|
+
"""Check box height."""
|
162
|
+
result = runner.invoke(app, ["--nmols", "1", "--height", "-5.0"])
|
163
|
+
assert result.exit_code != 0
|
164
|
+
assert "Invalid height" in strip_ansi_codes(result.output)
|
165
|
+
|
166
|
+
|
167
|
+
def test_packmm_invalid_box_dimensions_a():
|
168
|
+
"""Check box dimension."""
|
169
|
+
result = runner.invoke(app, ["--nmols", "1", "--a", "-30.0"])
|
170
|
+
assert result.exit_code != 0
|
171
|
+
assert "Invalid box a" in strip_ansi_codes(result.output)
|
172
|
+
|
173
|
+
|
174
|
+
def test_packmm_invalid_box_dimensions_b():
|
175
|
+
"""Check box dimension."""
|
176
|
+
result = runner.invoke(app, ["--nmols", "1", "--b", "-30.0"])
|
177
|
+
assert result.exit_code != 0
|
178
|
+
assert "Invalid box b" in strip_ansi_codes(result.output)
|
179
|
+
|
180
|
+
|
181
|
+
def test_packmm_invalid_box_dimensions_c():
|
182
|
+
"""Check box dimension."""
|
183
|
+
result = runner.invoke(app, ["--nmols", "1", "--c", "-30.0"])
|
184
|
+
assert result.exit_code != 0
|
185
|
+
assert "Invalid box c" in strip_ansi_codes(result.output)
|
186
|
+
|
187
|
+
|
188
|
+
def test_packmm_invalid_temperature():
|
189
|
+
"""Check temperature."""
|
190
|
+
result = runner.invoke(app, ["--nmols", "1", "--temperature", "-400.0"])
|
191
|
+
assert result.exit_code != 0
|
192
|
+
assert "Invalid temperature" in strip_ansi_codes(result.output)
|
193
|
+
|
194
|
+
|
195
|
+
def test_packmm_invalid_fmax():
|
196
|
+
"""Check fmax."""
|
197
|
+
result = runner.invoke(app, ["--nmols", "1", "--fmax", "-0.05"])
|
198
|
+
assert result.exit_code != 0
|
199
|
+
assert "Invalid fmax" in strip_ansi_codes(result.output)
|
200
|
+
|
201
|
+
|
202
|
+
def test_packmm_invalid_ntries():
|
203
|
+
"""Check ntries."""
|
204
|
+
result = runner.invoke(app, ["--nmols", "1", "--ntries", "-1"])
|
205
|
+
assert result.exit_code != 0
|
206
|
+
assert "Invalid ntries" in strip_ansi_codes(result.output)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
"""Utility functions for tests."""
|
2
|
+
|
3
|
+
# lifted from janus_core github.com/stfc/janus-core/
|
4
|
+
from __future__ import annotations
|
5
|
+
|
6
|
+
import re
|
7
|
+
|
8
|
+
|
9
|
+
def strip_ansi_codes(output: str) -> str:
|
10
|
+
"""
|
11
|
+
Remove any ANSI sequences from output string.
|
12
|
+
|
13
|
+
Based on:
|
14
|
+
https://stackoverflow.com/questions/14693701/how-can-i-remove-the-ansi-escape-sequences-from-a-string-in-python/14693789#14693789
|
15
|
+
|
16
|
+
Parameters
|
17
|
+
----------
|
18
|
+
output
|
19
|
+
Output that may contain ANSI sequences to be removed.
|
20
|
+
|
21
|
+
Returns
|
22
|
+
-------
|
23
|
+
str
|
24
|
+
Output with ANSI sequences removed.
|
25
|
+
"""
|
26
|
+
# 7-bit C1 ANSI sequences
|
27
|
+
ansi_escape = re.compile(
|
28
|
+
r"""
|
29
|
+
\x1B # ESC
|
30
|
+
(?: # 7-bit C1 Fe (except CSI)
|
31
|
+
[@-Z\\-_]
|
32
|
+
| # or [ for CSI, followed by a control sequence
|
33
|
+
\[
|
34
|
+
[0-?]* # Parameter bytes
|
35
|
+
[ -/]* # Intermediate bytes
|
36
|
+
[@-~] # Final byte
|
37
|
+
)
|
38
|
+
""",
|
39
|
+
re.VERBOSE,
|
40
|
+
)
|
41
|
+
return ansi_escape.sub("", output)
|