calphy 2.1.0__tar.gz → 2.1.2__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.
Files changed (63) hide show
  1. {calphy-2.1.0 → calphy-2.1.2}/PKG-INFO +1 -1
  2. {calphy-2.1.0 → calphy-2.1.2}/calphy/__init__.py +1 -1
  3. {calphy-2.1.0 → calphy-2.1.2}/calphy/alchemy.py +163 -245
  4. {calphy-2.1.0 → calphy-2.1.2}/calphy/helpers.py +75 -46
  5. {calphy-2.1.0 → calphy-2.1.2}/calphy/input.py +29 -7
  6. {calphy-2.1.0 → calphy-2.1.2}/calphy/liquid.py +17 -4
  7. {calphy-2.1.0 → calphy-2.1.2}/calphy/phase.py +114 -33
  8. {calphy-2.1.0 → calphy-2.1.2}/calphy/routines.py +52 -13
  9. {calphy-2.1.0 → calphy-2.1.2}/calphy/runner.py +7 -4
  10. {calphy-2.1.0 → calphy-2.1.2}/calphy/solid.py +34 -7
  11. {calphy-2.1.0 → calphy-2.1.2}/calphy.egg-info/PKG-INFO +1 -1
  12. {calphy-2.1.0 → calphy-2.1.2}/calphy.egg-info/SOURCES.txt +1 -0
  13. {calphy-2.1.0 → calphy-2.1.2}/pyproject.toml +1 -1
  14. {calphy-2.1.0 → calphy-2.1.2}/tests/test_command_vocabulary.py +2 -2
  15. calphy-2.1.2/tests/test_composition_scaling_potential.py +76 -0
  16. {calphy-2.1.0 → calphy-2.1.2}/tests/test_golden_streams.py +103 -0
  17. {calphy-2.1.0 → calphy-2.1.2}/tests/test_helpers.py +54 -0
  18. {calphy-2.1.0 → calphy-2.1.2}/tests/test_input_validation.py +55 -0
  19. {calphy-2.1.0 → calphy-2.1.2}/tests/test_runner.py +35 -0
  20. {calphy-2.1.0 → calphy-2.1.2}/LICENSE +0 -0
  21. {calphy-2.1.0 → calphy-2.1.2}/README.md +0 -0
  22. {calphy-2.1.0 → calphy-2.1.2}/calphy/clitools.py +0 -0
  23. {calphy-2.1.0 → calphy-2.1.2}/calphy/composition_transformation.py +0 -0
  24. {calphy-2.1.0 → calphy-2.1.2}/calphy/errors.py +0 -0
  25. {calphy-2.1.0 → calphy-2.1.2}/calphy/integrators.py +0 -0
  26. {calphy-2.1.0 → calphy-2.1.2}/calphy/kernel.py +0 -0
  27. {calphy-2.1.0 → calphy-2.1.2}/calphy/library_runner.py +0 -0
  28. {calphy-2.1.0 → calphy-2.1.2}/calphy/phase_diagram.py +0 -0
  29. {calphy-2.1.0 → calphy-2.1.2}/calphy/postprocessing.py +0 -0
  30. {calphy-2.1.0 → calphy-2.1.2}/calphy/queuekernel.py +0 -0
  31. {calphy-2.1.0 → calphy-2.1.2}/calphy/range_scan.py +0 -0
  32. {calphy-2.1.0 → calphy-2.1.2}/calphy/scheduler.py +0 -0
  33. {calphy-2.1.0 → calphy-2.1.2}/calphy/splines.py +0 -0
  34. {calphy-2.1.0 → calphy-2.1.2}/calphy/utils.py +0 -0
  35. {calphy-2.1.0 → calphy-2.1.2}/calphy.egg-info/dependency_links.txt +0 -0
  36. {calphy-2.1.0 → calphy-2.1.2}/calphy.egg-info/entry_points.txt +0 -0
  37. {calphy-2.1.0 → calphy-2.1.2}/calphy.egg-info/requires.txt +0 -0
  38. {calphy-2.1.0 → calphy-2.1.2}/calphy.egg-info/top_level.txt +0 -0
  39. {calphy-2.1.0 → calphy-2.1.2}/setup.cfg +0 -0
  40. {calphy-2.1.0 → calphy-2.1.2}/tests/test_composition_transformation_bug.py +0 -0
  41. {calphy-2.1.0 → calphy-2.1.2}/tests/test_dissipation_check.py +0 -0
  42. {calphy-2.1.0 → calphy-2.1.2}/tests/test_element_ordering.py +0 -0
  43. {calphy-2.1.0 → calphy-2.1.2}/tests/test_ex07.py +0 -0
  44. {calphy-2.1.0 → calphy-2.1.2}/tests/test_find_tm_crossing_error.py +0 -0
  45. {calphy-2.1.0 → calphy-2.1.2}/tests/test_fix_paths_user_expansion.py +0 -0
  46. {calphy-2.1.0 → calphy-2.1.2}/tests/test_gather_results_sweep_data.py +0 -0
  47. {calphy-2.1.0 → calphy-2.1.2}/tests/test_integration_lammps.py +0 -0
  48. {calphy-2.1.0 → calphy-2.1.2}/tests/test_integrators.py +0 -0
  49. {calphy-2.1.0 → calphy-2.1.2}/tests/test_library_runner.py +0 -0
  50. {calphy-2.1.0 → calphy-2.1.2}/tests/test_meam_pair_coeff.py +0 -0
  51. {calphy-2.1.0 → calphy-2.1.2}/tests/test_melting_temperature_n_iterations.py +0 -0
  52. {calphy-2.1.0 → calphy-2.1.2}/tests/test_options.py +0 -0
  53. {calphy-2.1.0 → calphy-2.1.2}/tests/test_phase_detection_defaults.py +0 -0
  54. {calphy-2.1.0 → calphy-2.1.2}/tests/test_phase_diagram.py +0 -0
  55. {calphy-2.1.0 → calphy-2.1.2}/tests/test_phase_diagram_validation.py +0 -0
  56. {calphy-2.1.0 → calphy-2.1.2}/tests/test_postprocessing.py +0 -0
  57. {calphy-2.1.0 → calphy-2.1.2}/tests/test_preflight.py +0 -0
  58. {calphy-2.1.0 → calphy-2.1.2}/tests/test_pressure_convergence_pv_fit.py +0 -0
  59. {calphy-2.1.0 → calphy-2.1.2}/tests/test_pressure_coupling.py +0 -0
  60. {calphy-2.1.0 → calphy-2.1.2}/tests/test_qtb_input.py +0 -0
  61. {calphy-2.1.0 → calphy-2.1.2}/tests/test_scheduler.py +0 -0
  62. {calphy-2.1.0 → calphy-2.1.2}/tests/test_seed.py +0 -0
  63. {calphy-2.1.0 → calphy-2.1.2}/tests/test_solid_methods.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: calphy
3
- Version: 2.1.0
3
+ Version: 2.1.2
4
4
  Summary: free energy calculation for python
5
5
  Author: Yury Lysogorskiy, Ralf Drautz
6
6
  Author-email: Sarath Menon <sarath.menon@ruhr-uni-bochum.de>
@@ -28,7 +28,7 @@ from calphy.solid import Solid
28
28
  from calphy.alchemy import Alchemy
29
29
  from calphy.routines import MeltingTemp
30
30
 
31
- __version__ = "2.1.0"
31
+ __version__ = "2.1.2"
32
32
 
33
33
  def addtest(a,b):
34
34
  return a+b
@@ -58,6 +58,91 @@ class Alchemy(cph.Phase):
58
58
  calculation=calculation, simfolder=simfolder, log_to_screen=log_to_screen,
59
59
  )
60
60
 
61
+ def _end_states(self):
62
+ """
63
+ The initial and final potential of the alchemical switch.
64
+
65
+ The pair lists hold both end states back to back: the first half
66
+ describes the initial potential, the second half the final one. Each
67
+ half is one physical potential made of one component (a plain pair
68
+ style) or several (``pair_mode: overlay``, or the component-wise
69
+ rewrite done by composition scaling). A component is the tuple
70
+ (style with options, style name, pair_coeff).
71
+ """
72
+ styles = self.calc._pair_style_with_options
73
+ names = self.calc._pair_style_names
74
+ coeffs = self.calc.pair_coeff
75
+ if len(styles) % 2 or len(styles) != len(coeffs):
76
+ raise ValueError(
77
+ "alchemical switching needs pair_style and pair_coeff lists of "
78
+ "equal, even length (initial components followed by final "
79
+ "components); got %d pair styles and %d pair coeffs"
80
+ % (len(styles), len(coeffs))
81
+ )
82
+ half = len(styles) // 2
83
+ components = list(zip(styles, names, coeffs))
84
+ return components[:half], components[half:]
85
+
86
+ @staticmethod
87
+ def _pure_pair_style_command(components):
88
+ """``pair_style`` for one potential on its own: the plain style for a
89
+ single component, ``hybrid/overlay`` of the components otherwise."""
90
+ if len(components) == 1:
91
+ return "pair_style %s" % components[0][0]
92
+ return "pair_style hybrid/overlay %s" % " ".join(
93
+ style for style, _, _ in components
94
+ )
95
+
96
+ @staticmethod
97
+ def _pure_pair_coeff_commands(components):
98
+ """``pair_coeff`` lines matching :meth:`_pure_pair_style_command`."""
99
+ if len(components) == 1:
100
+ return ["pair_coeff %s" % components[0][2]]
101
+ return ph.hybrid_pair_coeff_commands_for(
102
+ [name for _, name, _ in components], [coeff for _, _, coeff in components]
103
+ )
104
+
105
+ def _scaled_pair_commands(self, initial, final, initial_scale, final_scale):
106
+ """
107
+ Install ``hybrid/scaled`` with every initial component scaled by
108
+ ``initial_scale`` and every final component by ``final_scale``,
109
+ then define computes ``c1``/``c2`` (or ``c1_1, c1_2, ...`` for
110
+ multi-component ends) holding the two potential energies and the
111
+ per-atom variables ``dU1``/``dU2`` recorded during switching.
112
+
113
+ Returns the commands and the compute ids to release afterwards.
114
+ """
115
+ terms = ["%s %s" % (initial_scale, style) for style, _, _ in initial]
116
+ terms += ["%s %s" % (final_scale, style) for style, _, _ in final]
117
+ commands = ["pair_style hybrid/scaled %s" % " ".join(terms)]
118
+
119
+ components = initial + final
120
+ names = [name for _, name, _ in components]
121
+ commands += ph.hybrid_pair_coeff_commands_for(
122
+ names, [coeff for _, _, coeff in components]
123
+ )
124
+
125
+ tags = ph.hybrid_component_tags(names)
126
+ compute_ids, energies = [], []
127
+ for label, part, part_tags in (
128
+ ("c1", initial, tags[: len(initial)]),
129
+ ("c2", final, tags[len(initial) :]),
130
+ ):
131
+ ids = (
132
+ [label]
133
+ if len(part) == 1
134
+ else ["%s_%d" % (label, i) for i in range(1, len(part) + 1)]
135
+ )
136
+ compute_commands, energy = ph.hybrid_pair_compute_commands(ids, part_tags)
137
+ commands += compute_commands
138
+ compute_ids += ids
139
+ energies.append(energy if len(ids) == 1 else "(%s)" % energy)
140
+
141
+ commands.append("variable step equal step")
142
+ commands.append("variable dU1 equal %s/atoms" % energies[0])
143
+ commands.append("variable dU2 equal %s/atoms" % energies[1])
144
+ return commands, compute_ids
145
+
61
146
  def _coupling_pair(self, lmp, ramp="0.0", tag=None):
62
147
  """Coupling mode: base components (all but last) at constant scale
63
148
  1.0, the LAST component scaled by `ramp` (a constant or v_name).
@@ -180,13 +265,16 @@ class Alchemy(cph.Phase):
180
265
  lmp = ph.create_structure(lmp, self.calc)
181
266
  self._coupling_pair(lmp, ramp="0.0")
182
267
  else:
183
- lmp.command(f"pair_style {self.calc._pair_style_with_options[0]}")
268
+ # equilibrate with the initial potential on its own
269
+ initial, _ = self._end_states()
270
+ lmp.command(self._pure_pair_style_command(initial))
184
271
 
185
272
  # set up structure
186
273
  lmp = ph.create_structure(lmp, self.calc)
187
274
 
188
275
  # set up potential
189
- lmp.command(f"pair_coeff {self.calc.pair_coeff[0]}")
276
+ for command in self._pure_pair_coeff_commands(initial):
277
+ lmp.command(command)
190
278
  lmp = ph.set_mass(lmp, self.calc)
191
279
 
192
280
  # add some computes
@@ -254,6 +342,8 @@ class Alchemy(cph.Phase):
254
342
  if self.calc.alchemy_coupling:
255
343
  return self._run_integration_coupling(iteration=iteration)
256
344
 
345
+ initial, final = self._end_states()
346
+
257
347
  # create lammps object
258
348
  lmp = ph.create_object(self.calc, self.simfolder)
259
349
 
@@ -261,22 +351,16 @@ class Alchemy(cph.Phase):
261
351
  lmp.command("variable li equal 1.0")
262
352
  lmp.command("variable lf equal 0.0")
263
353
 
264
- lmp.command(f"pair_style {self.calc._pair_style_with_options[0]}")
265
-
266
- # read dump file
267
- # conf = os.path.join(self.simfolder, "conf.equilibration.dump")
354
+ # the equilibrated configuration is read in with the initial potential
355
+ lmp.command(self._pure_pair_style_command(initial))
268
356
  conf = os.path.join(self.simfolder, "conf.equilibration.data")
269
357
  lmp = ph.read_data(lmp, conf)
270
-
271
- # set up hybrid potential
272
- # here we only need to set one potential
273
- lmp.command(f"pair_coeff {self.calc.pair_coeff[0]}")
358
+ for command in self._pure_pair_coeff_commands(initial):
359
+ lmp.command(command)
274
360
  lmp = ph.set_mass(lmp, self.calc)
275
361
 
276
- # NEW ADDED
277
362
  lmp.command("group g1 type 1")
278
363
  lmp.command("group g2 type 2")
279
- # lmp = ph.set_double_hybrid_potential(lmp, self.options, self.calc._pressureair_style, self.calc._pressureair_coeff)
280
364
 
281
365
  # remap the box to get the correct pressure
282
366
  lmp = ph.remap_box(lmp, self.lx, self.ly, self.lz)
@@ -316,143 +400,20 @@ class Alchemy(cph.Phase):
316
400
  # equilibration run is over
317
401
 
318
402
  # ---------------------------------------------------------------
319
- # FWD cycle
403
+ # FWD cycle: initial potential scaled 1 -> 0, final potential 0 -> 1
320
404
  # ---------------------------------------------------------------
321
405
  lmp.command("variable flambda equal ramp(${li},${lf})")
322
406
  lmp.command("variable blambda equal ramp(${lf},${li})")
323
407
 
324
- # lmp.command("pair_style hybrid/scaled v_flambda %s v_blambda ufm 7.5"%self.options["md"]["pair_style"])
325
-
326
- # Compute pair definitions
327
- if self.calc.pair_style[0] == self.calc.pair_style[1]:
328
- pc = self.calc.pair_coeff[0]
329
- pcraw = pc.split()
330
- pc1 = " ".join(
331
- [
332
- *pcraw[:2],
333
- *[
334
- self.calc._pair_style_names[0],
335
- ],
336
- "1",
337
- *pcraw[2:],
338
- ]
339
- )
340
- pc = self.calc.pair_coeff[1]
341
- pcraw = pc.split()
342
- pc2 = " ".join(
343
- [
344
- *pcraw[:2],
345
- *[
346
- self.calc._pair_style_names[1],
347
- ],
348
- "2",
349
- *pcraw[2:],
350
- ]
351
- )
352
- else:
353
- pc = self.calc.pair_coeff[0]
354
- pcraw = pc.split()
355
- pc1 = " ".join(
356
- [
357
- *pcraw[:2],
358
- *[
359
- self.calc._pair_style_names[0],
360
- ],
361
- *pcraw[2:],
362
- ]
363
- )
364
- pc = self.calc.pair_coeff[1]
365
- pcraw = pc.split()
366
- pc2 = " ".join(
367
- [
368
- *pcraw[:2],
369
- *[
370
- self.calc._pair_style_names[1],
371
- ],
372
- *pcraw[2:],
373
- ]
374
- )
375
-
376
- lmp.command(
377
- "pair_style hybrid/scaled v_flambda %s v_blambda %s"
378
- % (
379
- self.calc._pair_style_with_options[0],
380
- self.calc._pair_style_with_options[1],
381
- )
408
+ commands, compute_ids = self._scaled_pair_commands(
409
+ initial, final, "v_flambda", "v_blambda"
382
410
  )
383
- lmp.command("pair_coeff %s" % pc1)
384
- lmp.command("pair_coeff %s" % pc2)
385
-
386
- # apply pair force commands
387
- if self.calc._pair_style_names[0] == self.calc._pair_style_names[1]:
388
- lmp.command(
389
- "compute c1 all pair %s 1" % self.calc._pair_style_names[0]
390
- )
391
- lmp.command(
392
- "compute c2 all pair %s 2" % self.calc._pair_style_names[1]
393
- )
394
- else:
395
- lmp.command(
396
- "compute c1 all pair %s" % self.calc._pair_style_names[0]
397
- )
398
- lmp.command(
399
- "compute c2 all pair %s" % self.calc._pair_style_names[1]
400
- )
411
+ for command in commands:
412
+ lmp.command(command)
401
413
 
402
- # Output variables.
403
- lmp.command("variable step equal step")
404
- lmp.command(
405
- "variable dU1 equal c_c1/atoms"
406
- ) # Driving-force obtained from NEHI procedure.
407
- lmp.command("variable dU2 equal c_c2/atoms")
408
-
409
- # add swaps if n_swap is > 0 - forward pass
410
- if (
411
- self.calc.monte_carlo.n_swaps > 0
412
- and len(self.calc.monte_carlo.forward_swap_types) >= 2
413
- ):
414
- swap_types = self.calc.monte_carlo.forward_swap_types
415
- swap_combos = list(itertools.combinations(swap_types, 2))
416
- self.logger.info(
417
- f"Forward pass: {self.calc.monte_carlo.n_swaps} swap moves per combo, {len(swap_combos)} combinations every {self.calc.monte_carlo.n_steps}"
418
- )
419
- for combo in swap_combos:
420
- self.logger.info(f" Swapping types: {combo[0]} <-> {combo[1]}")
421
-
422
- for idx, (type1, type2) in enumerate(swap_combos):
423
- swap_str = f"{type1} {type2}"
424
- if self.calc.monte_carlo.use_custom_lammps:
425
- lmp.command(
426
- "fix swap%d all atom/swap %d %d %d %f ke no types %s noforce yes localE yes"
427
- % (
428
- idx,
429
- self.calc.monte_carlo.n_steps,
430
- self.calc.monte_carlo.n_swaps,
431
- np.random.randint(1, 10000),
432
- self.calc._temperature,
433
- swap_str,
434
- )
435
- )
436
- else:
437
- lmp.command(
438
- "fix swap%d all atom/swap %d %d %d %f ke no types %s"
439
- % (
440
- idx,
441
- self.calc.monte_carlo.n_steps,
442
- self.calc.monte_carlo.n_swaps,
443
- np.random.randint(1, 10000),
444
- self.calc._temperature,
445
- swap_str,
446
- )
447
- )
448
-
449
- # Use the first swap fix for output tracking
450
- # lmp.command("variable a equal f_swap0[1]")
451
- # lmp.command("variable b equal f_swap0[2]")
452
- # lmp.command(
453
- # 'fix swap_print all print 1 "${a} ${b} ${flambda}" screen no file swap.forward_%d.dat'
454
- # % iteration
455
- # )
414
+ swap_fixes = self._add_swap_fixes(
415
+ lmp, self.calc.monte_carlo.forward_swap_types, "Forward"
416
+ )
456
417
 
457
418
  # Thermo output.
458
419
  lmp.command("thermo_style custom step v_dU1 v_dU2")
@@ -467,21 +428,16 @@ class Alchemy(cph.Phase):
467
428
  )
468
429
  lmp.command("run %d" % self.calc._n_switching_steps)
469
430
 
470
- # now equilibrate at the second potential
471
431
  lmp.command("unfix f2")
472
- lmp.command("uncompute c1")
473
- lmp.command("uncompute c2")
474
-
475
- # NEW SWAP
476
- if self.calc.monte_carlo.n_swaps > 0:
477
- swap_types = self.calc.monte_carlo.forward_swap_types
478
- swap_combos = list(itertools.combinations(swap_types, 2))
479
- for idx in range(len(swap_combos)):
480
- lmp.command(f"unfix swap{idx}")
481
- # lmp.command("unfix swap_print")
432
+ for compute_id in compute_ids:
433
+ lmp.command("uncompute %s" % compute_id)
434
+ for swap_fix in swap_fixes:
435
+ lmp.command("unfix %s" % swap_fix)
482
436
 
483
- lmp.command("pair_style %s" % self.calc._pair_style_with_options[1])
484
- lmp.command("pair_coeff %s" % self.calc.pair_coeff[1])
437
+ # now equilibrate with the final potential on its own
438
+ lmp.command(self._pure_pair_style_command(final))
439
+ for command in self._pure_pair_coeff_commands(final):
440
+ lmp.command(command)
485
441
 
486
442
  # Thermo output.
487
443
  lmp.command("thermo_style custom step pe")
@@ -490,90 +446,21 @@ class Alchemy(cph.Phase):
490
446
  # run eqbrm run
491
447
  lmp.command("run %d" % self.calc.n_equilibration_steps)
492
448
 
493
- # reverse switching
449
+ # ---------------------------------------------------------------
450
+ # BKD cycle: initial potential scaled 0 -> 1, final potential 1 -> 0
451
+ # ---------------------------------------------------------------
494
452
  lmp.command("variable flambda equal ramp(${lf},${li})")
495
453
  lmp.command("variable blambda equal ramp(${li},${lf})")
496
454
 
497
- lmp.command(
498
- "pair_style hybrid/scaled v_flambda %s v_blambda %s"
499
- % (
500
- self.calc._pair_style_with_options[0],
501
- self.calc._pair_style_with_options[1],
502
- )
455
+ commands, compute_ids = self._scaled_pair_commands(
456
+ initial, final, "v_flambda", "v_blambda"
503
457
  )
504
- lmp.command("pair_coeff %s" % pc1)
505
- lmp.command("pair_coeff %s" % pc2)
506
-
507
- # apply pair force commands
508
- if self.calc._pair_style_names[0] == self.calc._pair_style_names[1]:
509
- lmp.command(
510
- "compute c1 all pair %s 1" % self.calc._pair_style_names[0]
511
- )
512
- lmp.command(
513
- "compute c2 all pair %s 2" % self.calc._pair_style_names[1]
514
- )
515
- else:
516
- lmp.command(
517
- "compute c1 all pair %s" % self.calc._pair_style_names[0]
518
- )
519
- lmp.command(
520
- "compute c2 all pair %s" % self.calc._pair_style_names[1]
521
- )
458
+ for command in commands:
459
+ lmp.command(command)
522
460
 
523
- # Output variables.
524
- lmp.command("variable step equal step")
525
- lmp.command(
526
- "variable dU1 equal c_c1/atoms"
527
- ) # Driving-force obtained from NEHI procedure.
528
- lmp.command("variable dU2 equal c_c2/atoms")
529
-
530
- # add swaps if n_swap is > 0 - reverse pass
531
- if (
532
- self.calc.monte_carlo.n_swaps > 0
533
- and len(self.calc.monte_carlo.reverse_swap_types) >= 2
534
- ):
535
- swap_types = self.calc.monte_carlo.reverse_swap_types
536
- swap_combos = list(itertools.combinations(swap_types, 2))
537
- self.logger.info(
538
- f"Reverse pass: {self.calc.monte_carlo.n_swaps} swap moves per combo, {len(swap_combos)} combinations every {self.calc.monte_carlo.n_steps}"
539
- )
540
- for combo in swap_combos:
541
- self.logger.info(f" Swapping types: {combo[0]} <-> {combo[1]}")
542
-
543
- for idx, (type1, type2) in enumerate(swap_combos):
544
- swap_str = f"{type1} {type2}"
545
- if self.calc.monte_carlo.use_custom_lammps:
546
- lmp.command(
547
- "fix swap%d all atom/swap %d %d %d %f ke no types %s noforce yes localE yes"
548
- % (
549
- idx,
550
- self.calc.monte_carlo.n_steps,
551
- self.calc.monte_carlo.n_swaps,
552
- np.random.randint(1, 10000),
553
- self.calc._temperature,
554
- swap_str,
555
- )
556
- )
557
- else:
558
- lmp.command(
559
- "fix swap%d all atom/swap %d %d %d %f ke no types %s"
560
- % (
561
- idx,
562
- self.calc.monte_carlo.n_steps,
563
- self.calc.monte_carlo.n_swaps,
564
- np.random.randint(1, 10000),
565
- self.calc._temperature,
566
- swap_str,
567
- )
568
- )
569
-
570
- # Use the first swap fix for output tracking
571
- # lmp.command("variable a equal f_swap0[1]")
572
- # lmp.command("variable b equal f_swap0[2]")
573
- # lmp.command(
574
- #'fix swap_print all print 1 "${a} ${b} ${blambda}" screen no file swap.backward_%d.dat'
575
- # % iteration
576
- # )
461
+ swap_fixes = self._add_swap_fixes(
462
+ lmp, self.calc.monte_carlo.reverse_swap_types, "Reverse"
463
+ )
577
464
 
578
465
  # Thermo output.
579
466
  lmp.command("thermo_style custom step v_dU1 v_dU2")
@@ -588,21 +475,52 @@ class Alchemy(cph.Phase):
588
475
  )
589
476
  lmp.command("run %d" % self.calc._n_switching_steps)
590
477
 
591
- # now equilibrate at the second potential
592
478
  lmp.command("unfix f2")
593
- lmp.command("uncompute c1")
594
- lmp.command("uncompute c2")
595
-
596
- if self.calc.monte_carlo.n_swaps > 0:
597
- swap_types = self.calc.monte_carlo.reverse_swap_types
598
- swap_combos = list(itertools.combinations(swap_types, 2))
599
- for idx in range(len(swap_combos)):
600
- lmp.command(f"unfix swap{idx}")
601
- # lmp.command("unfix swap_print")
479
+ for compute_id in compute_ids:
480
+ lmp.command("uncompute %s" % compute_id)
481
+ for swap_fix in swap_fixes:
482
+ lmp.command("unfix %s" % swap_fix)
602
483
 
603
484
  self.lammps_close(lmp=lmp)
604
485
  lmp.rotate_logs("integration")
605
486
 
487
+ def _add_swap_fixes(self, lmp, swap_types, pass_name):
488
+ """
489
+ Add ``fix atom/swap`` moves between every pair of ``swap_types`` when
490
+ Monte Carlo swaps are requested; returns the fix ids that were added.
491
+ """
492
+ mc = self.calc.monte_carlo
493
+ if not (mc.n_swaps > 0 and len(swap_types) >= 2):
494
+ return []
495
+
496
+ swap_combos = list(itertools.combinations(swap_types, 2))
497
+ self.logger.info(
498
+ f"{pass_name} pass: {mc.n_swaps} swap moves per combo, "
499
+ f"{len(swap_combos)} combinations every {mc.n_steps}"
500
+ )
501
+ for combo in swap_combos:
502
+ self.logger.info(f" Swapping types: {combo[0]} <-> {combo[1]}")
503
+
504
+ fix_ids = []
505
+ for idx, (type1, type2) in enumerate(swap_combos):
506
+ fix_id = "swap%d" % idx
507
+ extra = " noforce yes localE yes" if mc.use_custom_lammps else ""
508
+ lmp.command(
509
+ "fix %s all atom/swap %d %d %d %f ke no types %s %s%s"
510
+ % (
511
+ fix_id,
512
+ mc.n_steps,
513
+ mc.n_swaps,
514
+ np.random.randint(1, 10000),
515
+ self.calc._temperature,
516
+ type1,
517
+ type2,
518
+ extra,
519
+ )
520
+ )
521
+ fix_ids.append(fix_id)
522
+ return fix_ids
523
+
606
524
  def thermodynamic_integration(self):
607
525
  """
608
526
  Calculate free energy after integration step
@@ -208,26 +208,78 @@ def _component_pair_coeffs(options):
208
208
  return components
209
209
 
210
210
 
211
- def hybrid_pair_coeff_commands(options, repeat_index=0, total_repeats=1):
212
- components = _component_pair_coeffs(options)
213
- active_style_names = []
214
- for _ in range(total_repeats):
215
- active_style_names.extend([style_name for style_name, _ in components])
216
- total_counts = Counter(active_style_names)
211
+ def hybrid_component_tags(style_names):
212
+ """
213
+ Sub-style tags of the components of a hybrid pair_style.
214
+
215
+ LAMMPS addresses a sub-style of a hybrid pair_style by its name, followed
216
+ by a 1-based occurrence index when the same style appears more than once
217
+ (``pair_coeff * * eam/alloy 2 ...``, ``compute c all pair eam/alloy 2``).
218
+
219
+ Parameters
220
+ ----------
221
+ style_names : list of str
222
+ Style names in the order they appear in the hybrid pair_style.
223
+
224
+ Returns
225
+ -------
226
+ list of (str, int or None)
227
+ One (name, index) tag per component; the index is None for a style
228
+ that occurs once.
229
+ """
230
+ counts = Counter(style_names)
217
231
  seen = defaultdict(int)
218
- for _ in range(repeat_index):
219
- for style_name, _ in components:
220
- seen[style_name] += 1
232
+ tags = []
233
+ for name in style_names:
234
+ seen[name] += 1
235
+ tags.append((name, seen[name] if counts[name] > 1 else None))
236
+ return tags
237
+
238
+
239
+ def hybrid_pair_coeff_commands_for(style_names, pair_coeffs):
240
+ """
241
+ ``pair_coeff`` commands for the components of a hybrid pair_style.
242
+
243
+ Parameters
244
+ ----------
245
+ style_names : list of str
246
+ Style names in hybrid order, one per pair_coeff.
247
+ pair_coeffs : list of str
248
+ The pair_coeff arguments, with or without the style name already
249
+ included after the type pair.
250
+ """
251
+ return [
252
+ "pair_coeff %s" % _with_hybrid_pair_coeff_style(pair_coeff, name, index)
253
+ for (name, index), pair_coeff in zip(hybrid_component_tags(style_names), pair_coeffs)
254
+ ]
255
+
256
+
257
+ def hybrid_pair_compute_commands(compute_ids, tags):
258
+ """
259
+ ``compute pair`` commands, one per hybrid sub-style tag.
221
260
 
261
+ Returns the commands and the LAMMPS expression summing the computes.
262
+ """
222
263
  commands = []
223
- for style_name, pair_coeff in components:
224
- seen[style_name] += 1
225
- style_index = seen[style_name] if total_counts[style_name] > 1 else None
226
- commands.append(
227
- "pair_coeff %s"
228
- % _with_hybrid_pair_coeff_style(pair_coeff, style_name, style_index)
229
- )
230
- return commands
264
+ for compute_id, (name, index) in zip(compute_ids, tags):
265
+ if index is None:
266
+ commands.append("compute %s all pair %s" % (compute_id, name))
267
+ else:
268
+ commands.append(
269
+ "compute %s all pair %s %d" % (compute_id, name, index)
270
+ )
271
+ return commands, "+".join("c_%s" % compute_id for compute_id in compute_ids)
272
+
273
+
274
+ def hybrid_pair_coeff_commands(options, repeat_index=0, total_repeats=1):
275
+ components = _component_pair_coeffs(options)
276
+ n = len(components)
277
+ style_names = [style_name for style_name, _ in components] * total_repeats
278
+ tags = hybrid_component_tags(style_names)[repeat_index * n : (repeat_index + 1) * n]
279
+ return [
280
+ "pair_coeff %s" % _with_hybrid_pair_coeff_style(pair_coeff, name, index)
281
+ for (name, index), (_, pair_coeff) in zip(tags, components)
282
+ ]
231
283
 
232
284
 
233
285
  def set_pair_coeff(lmp, options):
@@ -253,35 +305,12 @@ def real_pair_compute_commands(
253
305
  options, prefix="c_real", total_repeats=1, repeat_index=0
254
306
  ):
255
307
  components = _component_pair_coeffs(options)
256
- active_style_names = []
257
- for _ in range(total_repeats):
258
- active_style_names.extend([style_name for style_name, _ in components])
259
- total_counts = Counter(active_style_names)
260
- seen = defaultdict(int)
261
- for _ in range(repeat_index):
262
- for style_name, _ in components:
263
- seen[style_name] += 1
264
-
265
- commands = []
266
- terms = []
267
- for idx, (style_name, _) in enumerate(components, start=1):
268
- seen[style_name] += 1
269
- compute_id = "%s%d" % (prefix, idx)
270
- if total_counts[style_name] > 1:
271
- commands.append(
272
- "compute %s all pair %s %d"
273
- % (compute_id, style_name, seen[style_name])
274
- )
275
- else:
276
- commands.append(
277
- "compute %s all pair %s" % (compute_id, style_name)
278
- )
279
- terms.append("c_%s" % compute_id)
280
- return (
281
- commands,
282
- "+".join(terms),
283
- ["%s%d" % (prefix, idx) for idx in range(1, len(components) + 1)],
284
- )
308
+ n = len(components)
309
+ style_names = [style_name for style_name, _ in components] * total_repeats
310
+ tags = hybrid_component_tags(style_names)[repeat_index * n : (repeat_index + 1) * n]
311
+ compute_ids = ["%s%d" % (prefix, idx) for idx in range(1, n + 1)]
312
+ commands, energy = hybrid_pair_compute_commands(compute_ids, tags)
313
+ return commands, energy, compute_ids
285
314
 
286
315
 
287
316
  def set_potential(lmp, options):