pack-mm 0.0.21__py3-none-any.whl → 0.0.24__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,8 +139,54 @@ def validate_value(label, x):
138
139
  raise Exception(err)
139
140
 
140
141
 
142
+ def set_random_seed(seed: int) -> None:
143
+ """Set random seed."""
144
+ random.seed(seed)
145
+
146
+
147
+ def set_defaults(
148
+ cell: (float, float, float),
149
+ centre: (float, float, float) | None = None,
150
+ where: str | None = None,
151
+ a: float | None = None,
152
+ b: float | None = None,
153
+ c: float | None = None,
154
+ radius: float | None = None,
155
+ height: float | None = None,
156
+ ) -> tuple(
157
+ (float, float, float),
158
+ float | None,
159
+ float | None,
160
+ float | None,
161
+ float | None,
162
+ float | None,
163
+ ):
164
+ """Set defaults for insertion areas."""
165
+ if centre is None:
166
+ centre = (cell[0] * 0.5, cell[1] * 0.5, cell[2] * 0.5)
167
+
168
+ if where == "anywhere":
169
+ a, b, c = cell[0], cell[1], cell[2]
170
+ elif where == "sphere":
171
+ radius = radius or min(cell) * 0.5
172
+ elif where == "cylinderZ":
173
+ radius = radius or min(cell[0], cell[1]) * 0.5
174
+ height = height or 0.5 * cell[2]
175
+ elif where == "cylinderY":
176
+ radius = radius or min(cell[0], cell[2]) * 0.5
177
+ height = height or 0.5 * cell[1]
178
+ elif where == "cylinderX":
179
+ radius = radius or min(cell[2], cell[1]) * 0.5
180
+ height = height or 0.5 * cell[0]
181
+ elif where == "box":
182
+ a, b, c = a or cell[0], b or cell[1], c or cell[2]
183
+ elif where == "ellipsoid":
184
+ a, b, c = a or cell[0] * 0.5, b or cell[1] * 0.5, c or cell[2] * 0.5
185
+ return (centre, a, b, c, radius, height)
186
+
187
+
141
188
  def pack_molecules(
142
- system: str = None,
189
+ system: str | Atoms = None,
143
190
  molecule: str = "H2O",
144
191
  nmols: int = -1,
145
192
  arch: str = "cpu",
@@ -161,13 +208,19 @@ def pack_molecules(
161
208
  cell_b: float = None,
162
209
  cell_c: float = None,
163
210
  out_path: str = ".",
164
- ) -> float:
211
+ every: int = -1,
212
+ relax_strategy: str = "geometry_optimisation",
213
+ insert_strategy: str = "random",
214
+ md_steps: int = 10,
215
+ md_timestep: float = 1.0,
216
+ md_temperature: float = 100.0,
217
+ ) -> tuple(float, Atoms):
165
218
  """
166
219
  Pack molecules into a system based on the specified parameters.
167
220
 
168
221
  Parameters
169
222
  ----------
170
- system (str): Path to the system file or name of the system.
223
+ system (str|Atoms): Path to the system file or name of the system.
171
224
  molecule (str): Path to the molecule file or name of the molecule.
172
225
  nmols (int): Number of molecules to insert.
173
226
  arch (str): Architecture for the calculator.
@@ -185,6 +238,19 @@ def pack_molecules(
185
238
  geometry (bool): Whether to perform geometry optimization after insertion.
186
239
  cell_a, cell_b, cell_c (float): Cell dimensions if system is empty.
187
240
  out_path (str): path to save various outputs
241
+ every (int): After how many instertions to do a relaxation,
242
+ default -1 means none..
243
+ md_temperature (float): Temperature in Kelvin for MD.
244
+ md_steps (int): Number of steps for MD.
245
+ md_timestep (float): Timestep in fs for MD.
246
+ insert_strategy (str): Insert strategy, "random" or "md"
247
+ relax_strategy (str): Relax strategy, "geometry_optimisation" or "md"
248
+
249
+ Returns
250
+ -------
251
+ tuple: A tuple energy and Atoms object containing
252
+ original system and added molecules..
253
+
188
254
  """
189
255
  kbt = temperature * kB
190
256
  validate_value("temperature", temperature)
@@ -198,51 +264,34 @@ def pack_molecules(
198
264
  validate_value("ntries", ntries)
199
265
  validate_value("cell box cell a", cell_a)
200
266
  validate_value("cell box cell b", cell_b)
201
- validate_value("nmols", nmols)
202
267
  validate_value("cell box cell c", cell_c)
268
+ validate_value("nmols", nmols)
269
+ validate_value("MD steps", md_steps)
270
+ validate_value("MD timestep", md_timestep)
271
+ validate_value("MD temperature", md_temperature)
203
272
 
204
- random.seed(seed)
273
+ set_random_seed(seed)
205
274
 
206
- try:
207
- sys = read(system)
208
- sysname = Path(system).stem
209
- except Exception:
275
+ if system is None:
210
276
  sys = Atoms(cell=[cell_a, cell_b, cell_c], pbc=[True, True, True])
211
- sysname = "empty"
212
-
213
- cell = sys.cell.lengths()
277
+ sysname = ""
278
+ elif isinstance(system, Atoms):
279
+ sys = system.copy()
280
+ sysname = sys.get_chemical_formula()
281
+ else:
282
+ sys = read(system)
283
+ sysname = Path(system).stem + "+"
214
284
 
215
285
  # Print summary
216
286
  print(f"Inserting {nmols} {molecule} molecules in {sysname}.")
217
287
  print(f"Using {arch} model {model} on {device}.")
218
288
  print(f"Insert in {where}.")
219
289
 
220
- if center is None:
221
- center = (cell[0] * 0.5, cell[1] * 0.5, cell[2] * 0.5)
290
+ cell = sys.cell.lengths()
222
291
 
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]
292
+ center, a, b, c, radius, height = set_defaults(
293
+ cell, center, where, a, b, c, radius, height
294
+ )
246
295
 
247
296
  calc = choose_calculator(arch=arch, model_path=model, device=device)
248
297
  sys.calc = calc
@@ -250,7 +299,8 @@ def pack_molecules(
250
299
  e = sys.get_potential_energy() if len(sys) > 0 else 0.0
251
300
 
252
301
  csys = sys.copy()
253
- for i in range(nmols):
302
+ i = 0
303
+ while i < nmols:
254
304
  accept = False
255
305
  for _itry in range(ntries):
256
306
  mol = load_molecule(molecule)
@@ -259,6 +309,25 @@ def pack_molecules(
259
309
  mol.translate(tv)
260
310
 
261
311
  tsys = csys.copy() + mol.copy()
312
+ if insert_strategy == "hmc":
313
+ tsys = run_md_nve(
314
+ tsys, md_temperature, md_steps, md_timestep, arch, model, device
315
+ )
316
+
317
+ if every > 0 and _itry / every == 0:
318
+ tsys = save_the_day(
319
+ struct_path=tsys,
320
+ device=device,
321
+ arch=arch,
322
+ model=model,
323
+ fmax=fmax,
324
+ out_path=out_path,
325
+ md_temperature=md_temperature,
326
+ md_steps=md_steps,
327
+ md_timestep=md_timestep,
328
+ relax_strategy=relax_strategy,
329
+ )
330
+
262
331
  tsys.calc = calc
263
332
  en = tsys.get_potential_energy()
264
333
  de = en - e
@@ -270,36 +339,43 @@ def pack_molecules(
270
339
  if u <= acc:
271
340
  accept = True
272
341
  break
273
-
274
342
  if accept:
275
343
  csys = tsys.copy()
276
344
  e = en
277
- print(f"Inserted particle {i + 1}")
278
- write(Path(out_path) / f"{sysname}+{i + 1}{Path(molecule).stem}.cif", csys)
345
+ i += 1
346
+ print(f"Inserted particle {i}")
347
+ write(Path(out_path) / f"{sysname}{i}{Path(molecule).stem}.cif", csys)
279
348
  else:
280
349
  # Things are bad, maybe geomatry optimisation saves us
350
+ # once you hit here is bad, this can keep looping
281
351
  print(f"Failed to insert particle {i + 1} after {ntries} tries")
282
- _ = optimize_geometry(
283
- f"{sysname}+{i + 1}{Path(molecule).stem}.cif",
352
+ csys = save_the_day(
353
+ csys,
284
354
  device,
285
355
  arch,
286
356
  model,
287
357
  fmax,
288
358
  out_path,
359
+ md_temperature,
360
+ md_steps,
361
+ md_timestep,
362
+ relax_strategy,
289
363
  )
364
+
290
365
  energy_final = e
291
366
 
292
367
  # Perform final geometry optimization if requested
293
368
  if geometry:
294
- energy_final = optimize_geometry(
295
- f"{sysname}+{nmols}{Path(molecule).stem}.cif",
369
+ energy_final, csys = optimize_geometry(
370
+ Path(out_path) / f"{sysname}{nmols}{Path(molecule).stem}.cif",
296
371
  device,
297
372
  arch,
298
373
  model,
299
374
  fmax,
300
375
  out_path,
376
+ True,
301
377
  )
302
- return energy_final
378
+ return (energy_final, csys)
303
379
 
304
380
 
305
381
  def load_molecule(molecule: str):
@@ -329,7 +405,6 @@ def get_insertion_position(
329
405
  if where in ["cylinderZ", "cylinderY", "cylinderX"]:
330
406
  axis = where[-1].lower()
331
407
  return random_point_in_cylinder(center, radius, height, axis)
332
- # now is anywhere
333
408
  return random.random(3) * [a, b, c]
334
409
 
335
410
 
@@ -342,22 +417,101 @@ def rotate_molecule(mol):
342
417
  return mol
343
418
 
344
419
 
420
+ def save_the_day(
421
+ struct_path: str | Atoms,
422
+ device: str = "",
423
+ arch: str = "",
424
+ model: str = "",
425
+ fmax: float = 0.01,
426
+ out_path: str = ".",
427
+ md_temperature: float = 100.0,
428
+ md_steps: int = 10,
429
+ md_timestep: float = 1.0,
430
+ relax_strategy: str = "geometry_optimisation",
431
+ ) -> Atoms:
432
+ """Geometry optimisation or MD to get a better structure."""
433
+ if relax_strategy == "geometry_optimisation":
434
+ _, a = optimize_geometry(
435
+ struct_path,
436
+ device,
437
+ arch,
438
+ model,
439
+ fmax,
440
+ out_path,
441
+ )
442
+ return a
443
+ if relax_strategy == "md":
444
+ return run_md_nve(
445
+ struct_path, md_temperature, md_steps, md_timestep, arch, model, device
446
+ )
447
+ return None
448
+
449
+
450
+ def run_md_nve(
451
+ struct_path: str | Atoms,
452
+ temp: float = 100.0,
453
+ steps: int = 10,
454
+ timestep: float = 1.0,
455
+ arch: str = "",
456
+ model: str = "",
457
+ device: str = "",
458
+ ) -> Atoms:
459
+ """Run nve simulation."""
460
+ if isinstance(struct_path, Atoms):
461
+ md = NVE(
462
+ struct=struct_path,
463
+ temp=temp,
464
+ device=device,
465
+ arch=arch,
466
+ calc_kwargs={"model_paths": model},
467
+ stats_every=1,
468
+ steps=steps,
469
+ timestep=timestep,
470
+ )
471
+ else:
472
+ md = NVE(
473
+ struct_path=struct_path,
474
+ temp=temp,
475
+ device=device,
476
+ arch=arch,
477
+ calc_kwargs={"model_paths": model},
478
+ stats_every=1,
479
+ steps=steps,
480
+ timestep=timestep,
481
+ )
482
+ md.run()
483
+ return md.struct
484
+
485
+
345
486
  def optimize_geometry(
346
- struct_path: str,
487
+ struct_path: str | Atoms,
347
488
  device: str,
348
489
  arch: str,
349
490
  model: str,
350
491
  fmax: float,
351
492
  out_path: str = ".",
352
- ) -> float:
493
+ opt_cell: bool = False,
494
+ ) -> tuple(float, Atoms):
353
495
  """Optimize the geometry of a structure."""
354
- geo = GeomOpt(
355
- struct_path=struct_path,
356
- device=device,
357
- fmax=fmax,
358
- calc_kwargs={"model_paths": model},
359
- filter_kwargs={"hydrostatic_strain": True},
360
- )
361
- geo.run()
362
- write(Path(out_path) / f"{Path(struct_path).stem}-opt.cif", geo.struct)
363
- return geo.struct.get_potential_energy()
496
+ if isinstance(struct_path, Atoms):
497
+ geo = GeomOpt(
498
+ struct=struct_path,
499
+ device=device,
500
+ arch=arch,
501
+ fmax=fmax,
502
+ calc_kwargs={"model_paths": model},
503
+ filter_kwargs={"hydrostatic_strain": opt_cell},
504
+ )
505
+ geo.run()
506
+ else:
507
+ geo = GeomOpt(
508
+ struct_path=struct_path,
509
+ device=device,
510
+ arch=arch,
511
+ fmax=fmax,
512
+ calc_kwargs={"model_paths": model},
513
+ filter_kwargs={"hydrostatic_strain": opt_cell},
514
+ )
515
+ geo.run()
516
+ write(Path(out_path) / f"{Path(struct_path).stem}-opt.cif", geo.struct)
517
+ return (geo.struct.get_potential_energy(), geo.struct)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pack-mm
3
- Version: 0.0.21
3
+ Version: 0.0.24
4
4
  Summary: packing materials and molecules in boxes using for machine learnt interatomic potentials
5
5
  Author: Alin M. Elena
6
6
  Classifier: Programming Language :: Python
@@ -42,7 +42,7 @@ It provides both a cli and a python api, with some examples below.
42
42
  uv pip install pack-mm
43
43
 
44
44
  ```
45
- or install the lates
45
+ or install the latest
46
46
 
47
47
  ```bash
48
48
 
@@ -50,6 +50,11 @@ or install the lates
50
50
 
51
51
  ```
52
52
 
53
+ ## Jupyter notebook examples
54
+
55
+
56
+ - [Basics](docs/source/tutorials/basics.ipynb) [![badge](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/ddmms/pack-mm/blob/main/docs/source/tutorials/basic.ipynb)
57
+
53
58
  ## CLI examples
54
59
 
55
60
 
@@ -125,7 +130,7 @@ packmm --system Pd-super.cif --molecule H2 --nmols 50 --where anywhere --mode
125
130
 
126
131
  before optimisation
127
132
 
128
- ![](examples/pics/Pd-H2-noopt.webp)
133
+ ![](examples/pics/Pd-super+50H2.webp)
129
134
 
130
135
 
131
136
  after optimisation
@@ -0,0 +1,8 @@
1
+ pack_mm-0.0.24.dist-info/METADATA,sha256=Qdbk-eAfirZQY0G6cOibTNAuy6rZtZbCoSktOTAyYKo,13757
2
+ pack_mm-0.0.24.dist-info/WHEEL,sha256=thaaA2w1JzcGC48WYufAs8nrYZjJm8LqNfnXFOFyCC4,90
3
+ pack_mm-0.0.24.dist-info/entry_points.txt,sha256=ajKA2oehIa_LCVCP2XTRxV0VNgjGl9c2wYkwk0BasrQ,66
4
+ pack_mm-0.0.24.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=vLTdnWaqnIOtkhtLBG9911RxhJMn-DWAWe5GzyFWSdc,15883
8
+ pack_mm-0.0.24.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,,