pack-mm 0.0.21__py3-none-any.whl → 0.0.22__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pack_mm/cli/packmm.py CHANGED
@@ -25,6 +25,22 @@ class InsertionMethod(str, Enum):
25
25
  ELLIPSOID = "ellipsoid"
26
26
 
27
27
 
28
+ class InsertionStrategy(str, Enum):
29
+ """Insertion options."""
30
+
31
+ # propose randomly a point
32
+ MC = "mc"
33
+ # hybrid monte carlo
34
+ HMC = "hmc"
35
+
36
+
37
+ class RelaxStrategy(str, Enum):
38
+ """Relaxation options."""
39
+
40
+ GEOMETRY_OPTIMISATION = "geometry_optimisation"
41
+ MD = "md"
42
+
43
+
28
44
  app = typer.Typer(no_args_is_help=True)
29
45
 
30
46
 
@@ -44,12 +60,26 @@ def packmm(
44
60
  ntries: int = typer.Option(
45
61
  50, help="Maximum number of attempts to insert each molecule."
46
62
  ),
63
+ every: int = typer.Option(
64
+ -1, help="Run MD-NVE or Geometry optimisation everyth insertion."
65
+ ),
47
66
  seed: int = typer.Option(2025, help="Random seed for reproducibility."),
67
+ md_steps: int = typer.Option(10, help="Number of steps to run MD."),
68
+ md_timestep: float = typer.Option(1.0, help="Timestep for MD integration, in fs."),
48
69
  where: InsertionMethod = typer.Option(
49
70
  InsertionMethod.ANYWHERE,
50
71
  help="""Where to insert the molecule. Choices: 'anywhere', 'sphere',
51
72
  'box', 'cylinderZ', 'cylinderY', 'cylinderX', 'ellipsoid'.""",
52
73
  ),
74
+ insert_strategy: InsertionStrategy = typer.Option(
75
+ InsertionStrategy.MC,
76
+ help="""How to insert a new molecule. Choices: 'mc', 'hmc',""",
77
+ ),
78
+ relax_strategy: RelaxStrategy = typer.Option(
79
+ RelaxStrategy.GEOMETRY_OPTIMISATION,
80
+ help="""How to relax the system to get more favourable structures.
81
+ Choices: 'geometry_optimisation', 'md',""",
82
+ ),
53
83
  centre: str | None = typer.Option(
54
84
  None,
55
85
  help="""Centre of the insertion zone, coordinates in Å,
@@ -84,6 +114,9 @@ def packmm(
84
114
  temperature: float = typer.Option(
85
115
  300.0, help="Temperature for the Monte Carlo acceptance rule."
86
116
  ),
117
+ md_temperature: float = typer.Option(
118
+ 100.0, help="Temperature for the Molecular dynamics relaxation."
119
+ ),
87
120
  cell_a: float = typer.Option(
88
121
  20.0, help="Side of the empty box along the x-axis in Å."
89
122
  ),
@@ -125,6 +158,12 @@ def packmm(
125
158
  print(f"{fmax=}")
126
159
  print(f"{geometry=}")
127
160
  print(f"{out_path=}")
161
+ print(f"{every=}")
162
+ print(f"insert_strategy={insert_strategy.value}")
163
+ print(f"relax_strategy={relax_strategy.value}")
164
+ print(f"{md_steps=}")
165
+ print(f"{md_timestep=}")
166
+ print(f"{md_temperature=}")
128
167
  if nmols == -1:
129
168
  print("nothing to do, no molecule to insert")
130
169
  raise typer.Exit(0)
@@ -161,8 +200,10 @@ def packmm(
161
200
  cell_b=cell_b,
162
201
  cell_c=cell_c,
163
202
  out_path=out_path,
203
+ every=every,
204
+ relax_strategy=relax_strategy,
205
+ insert_strategy=insert_strategy,
206
+ md_steps=md_steps,
207
+ md_timestep=md_timestep,
208
+ md_temperature=md_temperature,
164
209
  )
165
-
166
-
167
- if __name__ == "__main__":
168
- app()
pack_mm/core/core.py CHANGED
@@ -13,6 +13,7 @@ from ase.build import molecule as build_molecule
13
13
  from ase.io import read, write
14
14
  from ase.units import kB
15
15
  from janus_core.calculations.geom_opt import GeomOpt
16
+ from janus_core.calculations.md import NVE
16
17
  from janus_core.helpers.mlip_calculators import choose_calculator
17
18
  from numpy import cos, exp, pi, random, sin, sqrt
18
19
 
@@ -130,7 +131,7 @@ def random_point_in_cylinder(
130
131
  return (x, y, z)
131
132
 
132
133
 
133
- def validate_value(label, x):
134
+ def validate_value(label: str, x: float | int) -> None:
134
135
  """Validate input value, and raise an exception."""
135
136
  if x is not None and x < 0.0:
136
137
  err = f"Invalid {label}, needs to be positive"
@@ -138,6 +139,52 @@ def validate_value(label, x):
138
139
  raise Exception(err)
139
140
 
140
141
 
142
+ def set_random_seed(seed: int) -> None:
143
+ """Set random seed."""
144
+ random.seed(seed)
145
+
146
+
147
+ def set_defaults(
148
+ cell: (float, float, float),
149
+ centre: (float, float, float) | None = None,
150
+ where: str | None = None,
151
+ a: float | None = None,
152
+ b: float | None = None,
153
+ c: float | None = None,
154
+ radius: float | None = None,
155
+ height: float | None = None,
156
+ ) -> tuple(
157
+ (float, float, float),
158
+ float | None,
159
+ float | None,
160
+ float | None,
161
+ float | None,
162
+ float | None,
163
+ ):
164
+ """Set defaults for insertion areas."""
165
+ if centre is None:
166
+ centre = (cell[0] * 0.5, cell[1] * 0.5, cell[2] * 0.5)
167
+
168
+ if where == "anywhere":
169
+ a, b, c = cell[0], cell[1], cell[2]
170
+ elif where == "sphere":
171
+ radius = radius or min(cell) * 0.5
172
+ elif where == "cylinderZ":
173
+ radius = radius or min(cell[0], cell[1]) * 0.5
174
+ height = height or 0.5 * cell[2]
175
+ elif where == "cylinderY":
176
+ radius = radius or min(cell[0], cell[2]) * 0.5
177
+ height = height or 0.5 * cell[1]
178
+ elif where == "cylinderX":
179
+ radius = radius or min(cell[2], cell[1]) * 0.5
180
+ height = height or 0.5 * cell[0]
181
+ elif where == "box":
182
+ a, b, c = a or cell[0], b or cell[1], c or cell[2]
183
+ elif where == "ellipsoid":
184
+ a, b, c = a or cell[0] * 0.5, b or cell[1] * 0.5, c or cell[2] * 0.5
185
+ return (centre, a, b, c, radius, height)
186
+
187
+
141
188
  def pack_molecules(
142
189
  system: str = None,
143
190
  molecule: str = "H2O",
@@ -161,6 +208,12 @@ def pack_molecules(
161
208
  cell_b: float = None,
162
209
  cell_c: float = None,
163
210
  out_path: str = ".",
211
+ every: int = -1,
212
+ relax_strategy: str = "geometry_optimisation",
213
+ insert_strategy: str = "random",
214
+ md_steps: int = 10,
215
+ md_timestep: float = 1.0,
216
+ md_temperature: float = 100.0,
164
217
  ) -> float:
165
218
  """
166
219
  Pack molecules into a system based on the specified parameters.
@@ -185,6 +238,14 @@ def pack_molecules(
185
238
  geometry (bool): Whether to perform geometry optimization after insertion.
186
239
  cell_a, cell_b, cell_c (float): Cell dimensions if system is empty.
187
240
  out_path (str): path to save various outputs
241
+ every (int): After how many instertions to do a relaxation,
242
+ default -1 means none..
243
+ md_temperature (float): Temperature in Kelvin for MD.
244
+ md_steps (int): Number of steps for MD.
245
+ md_timestep (float): Timestep in fs for MD.
246
+ insert_strategy (str): Insert strategy, "random" or "md"
247
+ relax_strategy (str): Relax strategy, "geometry_optimisation" or "md"
248
+
188
249
  """
189
250
  kbt = temperature * kB
190
251
  validate_value("temperature", temperature)
@@ -198,51 +259,31 @@ def pack_molecules(
198
259
  validate_value("ntries", ntries)
199
260
  validate_value("cell box cell a", cell_a)
200
261
  validate_value("cell box cell b", cell_b)
201
- validate_value("nmols", nmols)
202
262
  validate_value("cell box cell c", cell_c)
263
+ validate_value("nmols", nmols)
264
+ validate_value("MD steps", md_steps)
265
+ validate_value("MD timestep", md_timestep)
266
+ validate_value("MD temperature", md_temperature)
203
267
 
204
- random.seed(seed)
268
+ set_random_seed(seed)
205
269
 
206
- try:
207
- sys = read(system)
208
- sysname = Path(system).stem
209
- except Exception:
270
+ if system is None:
210
271
  sys = Atoms(cell=[cell_a, cell_b, cell_c], pbc=[True, True, True])
211
- sysname = "empty"
212
-
213
- cell = sys.cell.lengths()
272
+ sysname = ""
273
+ else:
274
+ sys = read(system)
275
+ sysname = Path(system).stem + "+"
214
276
 
215
277
  # Print summary
216
278
  print(f"Inserting {nmols} {molecule} molecules in {sysname}.")
217
279
  print(f"Using {arch} model {model} on {device}.")
218
280
  print(f"Insert in {where}.")
219
281
 
220
- if center is None:
221
- center = (cell[0] * 0.5, cell[1] * 0.5, cell[2] * 0.5)
282
+ cell = sys.cell.lengths()
222
283
 
223
- if where == "anywhere":
224
- a, b, c = cell[0], cell[1], cell[2]
225
- elif where == "sphere":
226
- if radius is None:
227
- radius = min(cell) * 0.5
228
- elif where in ["cylinderZ", "cylinderY", "cylinderX"]:
229
- if radius is None:
230
- if where == "cylinderZ":
231
- radius = min(cell[0], cell[1]) * 0.5
232
- if height is None:
233
- height = 0.5 * cell[2]
234
- elif where == "cylinderY":
235
- radius = min(cell[0], cell[2]) * 0.5
236
- if height is None:
237
- height = 0.5 * cell[1]
238
- elif where == "cylinderX":
239
- radius = min(cell[2], cell[1]) * 0.5
240
- if height is None:
241
- height = 0.5 * cell[0]
242
- elif where == "box":
243
- a, b, c = a or cell[0], b or cell[1], c or cell[2]
244
- elif where == "ellipsoid":
245
- a, b, c = a or cell[0], b or cell[1], c or cell[2]
284
+ center, a, b, c, radius, height = set_defaults(
285
+ cell, center, where, a, b, c, radius, height
286
+ )
246
287
 
247
288
  calc = choose_calculator(arch=arch, model_path=model, device=device)
248
289
  sys.calc = calc
@@ -250,7 +291,8 @@ def pack_molecules(
250
291
  e = sys.get_potential_energy() if len(sys) > 0 else 0.0
251
292
 
252
293
  csys = sys.copy()
253
- for i in range(nmols):
294
+ i = 0
295
+ while i < nmols:
254
296
  accept = False
255
297
  for _itry in range(ntries):
256
298
  mol = load_molecule(molecule)
@@ -259,6 +301,11 @@ def pack_molecules(
259
301
  mol.translate(tv)
260
302
 
261
303
  tsys = csys.copy() + mol.copy()
304
+ if insert_strategy == "hmc":
305
+ tsys = run_md_nve(
306
+ tsys, md_temperature, md_steps, md_timestep, arch, model, device
307
+ )
308
+
262
309
  tsys.calc = calc
263
310
  en = tsys.get_potential_energy()
264
311
  de = en - e
@@ -270,34 +317,55 @@ def pack_molecules(
270
317
  if u <= acc:
271
318
  accept = True
272
319
  break
320
+ if every > 0 and _itry / every == 0:
321
+ csys = save_the_day(
322
+ Path(out_path) / f"{sysname}{i}{Path(molecule).stem}.cif",
323
+ device,
324
+ arch,
325
+ model,
326
+ fmax,
327
+ out_path,
328
+ md_temperature,
329
+ md_steps,
330
+ md_timestep,
331
+ relax_strategy,
332
+ )
273
333
 
274
334
  if accept:
275
335
  csys = tsys.copy()
276
336
  e = en
277
- print(f"Inserted particle {i + 1}")
278
- write(Path(out_path) / f"{sysname}+{i + 1}{Path(molecule).stem}.cif", csys)
337
+ i += 1
338
+ print(f"Inserted particle {i}")
339
+ write(Path(out_path) / f"{sysname}{i}{Path(molecule).stem}.cif", csys)
279
340
  else:
280
341
  # Things are bad, maybe geomatry optimisation saves us
342
+ # once you hit here is bad, this can keep looping
281
343
  print(f"Failed to insert particle {i + 1} after {ntries} tries")
282
- _ = optimize_geometry(
283
- f"{sysname}+{i + 1}{Path(molecule).stem}.cif",
344
+ csys = save_the_day(
345
+ Path(out_path) / f"{sysname}{i}{Path(molecule).stem}.cif",
284
346
  device,
285
347
  arch,
286
348
  model,
287
349
  fmax,
288
350
  out_path,
351
+ md_temperature,
352
+ md_steps,
353
+ md_timestep,
354
+ relax_strategy,
289
355
  )
356
+
290
357
  energy_final = e
291
358
 
292
359
  # Perform final geometry optimization if requested
293
360
  if geometry:
294
361
  energy_final = optimize_geometry(
295
- f"{sysname}+{nmols}{Path(molecule).stem}.cif",
362
+ Path(out_path) / f"{sysname}{nmols}{Path(molecule).stem}.cif",
296
363
  device,
297
364
  arch,
298
365
  model,
299
366
  fmax,
300
367
  out_path,
368
+ True,
301
369
  )
302
370
  return energy_final
303
371
 
@@ -329,7 +397,6 @@ def get_insertion_position(
329
397
  if where in ["cylinderZ", "cylinderY", "cylinderX"]:
330
398
  axis = where[-1].lower()
331
399
  return random_point_in_cylinder(center, radius, height, axis)
332
- # now is anywhere
333
400
  return random.random(3) * [a, b, c]
334
401
 
335
402
 
@@ -342,6 +409,72 @@ def rotate_molecule(mol):
342
409
  return mol
343
410
 
344
411
 
412
+ def save_the_day(
413
+ struct_path: str = "",
414
+ device: str = "",
415
+ arch: str = "",
416
+ model: str = "",
417
+ fmax: float = 0.01,
418
+ out_path: str = ".",
419
+ md_temperature: float = 100.0,
420
+ md_steps: int = 10,
421
+ md_timestep: float = 1.0,
422
+ relax_strategy: str = "geometry_optimisation",
423
+ ) -> Atoms:
424
+ """Geometry optimisation or MD to get a better structure."""
425
+ if relax_strategy == "geometry_optimisation":
426
+ _ = optimize_geometry(
427
+ struct_path,
428
+ device,
429
+ arch,
430
+ model,
431
+ fmax,
432
+ out_path,
433
+ )
434
+ return read(Path(out_path) / f"{Path(struct_path).stem}-opt.cif")
435
+ if relax_strategy == "md":
436
+ return run_md_nve(
437
+ struct_path, md_temperature, md_steps, md_timestep, arch, model, device
438
+ )
439
+ return None
440
+
441
+
442
+ def run_md_nve(
443
+ struct_path: str | Atoms,
444
+ temp: float = 100.0,
445
+ steps: int = 10,
446
+ timestep: float = 1.0,
447
+ arch: str = "",
448
+ model: str = "",
449
+ device: str = "",
450
+ ) -> Atoms:
451
+ """Run nve simulation."""
452
+ if isinstance(struct_path, Atoms):
453
+ md = NVE(
454
+ struct=struct_path,
455
+ temp=temp,
456
+ device=device,
457
+ arch=arch,
458
+ calc_kwargs={"model_paths": model},
459
+ stats_every=1,
460
+ steps=steps,
461
+ timestep=timestep,
462
+ )
463
+ else:
464
+ md = NVE(
465
+ struct_path=struct_path,
466
+ temp=temp,
467
+ device=device,
468
+ arch=arch,
469
+ calc_kwargs={"model_paths": model},
470
+ stats_every=1,
471
+ steps=steps,
472
+ timestep=timestep,
473
+ )
474
+ md.run()
475
+ return md.struct
476
+
477
+
345
478
  def optimize_geometry(
346
479
  struct_path: str,
347
480
  device: str,
@@ -349,14 +482,16 @@ def optimize_geometry(
349
482
  model: str,
350
483
  fmax: float,
351
484
  out_path: str = ".",
485
+ opt_cell: bool = False,
352
486
  ) -> float:
353
487
  """Optimize the geometry of a structure."""
354
488
  geo = GeomOpt(
355
489
  struct_path=struct_path,
356
490
  device=device,
491
+ arch=arch,
357
492
  fmax=fmax,
358
493
  calc_kwargs={"model_paths": model},
359
- filter_kwargs={"hydrostatic_strain": True},
494
+ filter_kwargs={"hydrostatic_strain": opt_cell},
360
495
  )
361
496
  geo.run()
362
497
  write(Path(out_path) / f"{Path(struct_path).stem}-opt.cif", geo.struct)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pack-mm
3
- Version: 0.0.21
3
+ Version: 0.0.22
4
4
  Summary: packing materials and molecules in boxes using for machine learnt interatomic potentials
5
5
  Author: Alin M. Elena
6
6
  Classifier: Programming Language :: Python
@@ -42,7 +42,7 @@ It provides both a cli and a python api, with some examples below.
42
42
  uv pip install pack-mm
43
43
 
44
44
  ```
45
- or install the lates
45
+ or install the latest
46
46
 
47
47
  ```bash
48
48
 
@@ -125,7 +125,7 @@ packmm --system Pd-super.cif --molecule H2 --nmols 50 --where anywhere --mode
125
125
 
126
126
  before optimisation
127
127
 
128
- ![](examples/pics/Pd-H2-noopt.webp)
128
+ ![](examples/pics/Pd-super+50H2.webp)
129
129
 
130
130
 
131
131
  after optimisation
@@ -0,0 +1,8 @@
1
+ pack_mm-0.0.22.dist-info/METADATA,sha256=5XjBJexb_jY1kVAZdZSXFcXfdWvfHJFpnsYJ25tTQlc,13509
2
+ pack_mm-0.0.22.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
+ pack_mm-0.0.22.dist-info/entry_points.txt,sha256=ajKA2oehIa_LCVCP2XTRxV0VNgjGl9c2wYkwk0BasrQ,66
4
+ pack_mm-0.0.22.dist-info/licenses/LICENSE,sha256=ZOYkPdn_vQ8wYJqZnjesow79F_grMbVlHcJ9V91G1pE,1100
5
+ pack_mm/__init__.py,sha256=ct7qfCmTDwhLYip6JKYWRLasmmaGYt0ColbK0CpvYZk,150
6
+ pack_mm/cli/packmm.py,sha256=E63MmBrxvn9g5GQMK5vlyq6CmHL8hgQ8CICeX2N9z1E,6398
7
+ pack_mm/core/core.py,sha256=R0d-FTmuTj-5BSwG5S4Fa6f3BFdYrvBUsV2cSwcHi2c,15269
8
+ pack_mm-0.0.22.dist-info/RECORD,,
@@ -1,8 +0,0 @@
1
- pack_mm-0.0.21.dist-info/METADATA,sha256=jW4IZ3HFn-e7AxJ8dAteajsDSs1dsybX3G9l080Gr5Y,13506
2
- pack_mm-0.0.21.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
- pack_mm-0.0.21.dist-info/entry_points.txt,sha256=ajKA2oehIa_LCVCP2XTRxV0VNgjGl9c2wYkwk0BasrQ,66
4
- pack_mm-0.0.21.dist-info/licenses/LICENSE,sha256=ZOYkPdn_vQ8wYJqZnjesow79F_grMbVlHcJ9V91G1pE,1100
5
- pack_mm/__init__.py,sha256=ct7qfCmTDwhLYip6JKYWRLasmmaGYt0ColbK0CpvYZk,150
6
- pack_mm/cli/packmm.py,sha256=VqumDT_f1Nf1LCZ1WsF5D6MoLmKEQPEimYenibQHIU4,4944
7
- pack_mm/core/core.py,sha256=vNMQOVDIXyRCUWosww9sSHUxwqbjPRk5JLsb9muwpVA,11369
8
- pack_mm-0.0.21.dist-info/RECORD,,