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 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)