pack-mm 0.0.14__tar.gz

Sign up to get free protection for your applications and to get access to all the features.
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.
@@ -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
@@ -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,7 @@
1
+ """Pack molecules in various shapes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from importlib.metadata import version
6
+
7
+ __version__ = version("pack-mm")
@@ -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,7 @@
1
+ """Tests for pack me."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ TEST_DIR = Path(__file__).resolve().parent
@@ -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)