calphy 2.3.0__tar.gz → 2.3.1__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.
- {calphy-2.3.0 → calphy-2.3.1}/PKG-INFO +1 -1
- {calphy-2.3.0 → calphy-2.3.1}/calphy/__init__.py +1 -1
- {calphy-2.3.0 → calphy-2.3.1}/calphy/alchemy.py +3 -3
- {calphy-2.3.0 → calphy-2.3.1}/calphy/dcci.py +2 -1
- {calphy-2.3.0 → calphy-2.3.1}/calphy/helpers.py +16 -2
- {calphy-2.3.0 → calphy-2.3.1}/calphy/input.py +1 -1
- {calphy-2.3.0 → calphy-2.3.1}/calphy/library_runner.py +8 -1
- {calphy-2.3.0 → calphy-2.3.1}/calphy/liquid.py +2 -2
- {calphy-2.3.0 → calphy-2.3.1}/calphy/phase.py +34 -5
- {calphy-2.3.0 → calphy-2.3.1}/calphy/routines.py +24 -12
- {calphy-2.3.0 → calphy-2.3.1}/calphy/runner.py +28 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/solid.py +34 -18
- {calphy-2.3.0 → calphy-2.3.1}/calphy.egg-info/PKG-INFO +1 -1
- {calphy-2.3.0 → calphy-2.3.1}/calphy.egg-info/SOURCES.txt +2 -0
- {calphy-2.3.0 → calphy-2.3.1}/pyproject.toml +1 -1
- calphy-2.3.1/tests/test_disabled_phase_checks.py +46 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_integration_lammps.py +5 -5
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_library_runner.py +41 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_runner.py +73 -0
- calphy-2.3.1/tests/test_spring_constants.py +131 -0
- {calphy-2.3.0 → calphy-2.3.1}/LICENSE +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/README.md +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/clitools.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/composition_transformation.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/errors.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/integrators.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/kernel.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/phase_diagram.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/postprocessing.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/queuekernel.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/range_scan.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/scheduler.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/splines.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/structures.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy/utils.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy.egg-info/dependency_links.txt +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy.egg-info/entry_points.txt +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy.egg-info/requires.txt +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/calphy.egg-info/top_level.txt +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/setup.cfg +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_command_vocabulary.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_composition_scaling_potential.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_composition_transformation_bug.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_dcci_driver.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_dcci_input.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_dcci_integrator.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_dcci_lammps_e2e.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_dcci_lammps_semantics.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_dissipation_check.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_element_ordering.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_ex07.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_find_tm_crossing_error.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_fix_paths_user_expansion.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_gather_results_sweep_data.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_golden_streams.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_helpers.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_input_validation.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_integrators.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_meam_pair_coeff.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_melting_temperature_n_iterations.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_options.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_phase_detection_defaults.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_phase_diagram.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_phase_diagram_validation.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_postprocessing.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_preflight.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_pressure_convergence_pv_fit.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_pressure_coupling.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_qtb_input.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_scheduler.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_seed.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_solid_methods.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_structures.py +0 -0
- {calphy-2.3.0 → calphy-2.3.1}/tests/test_sweep_barostat_targets.py +0 -0
|
@@ -175,7 +175,7 @@ class Alchemy(cph.Phase):
|
|
|
175
175
|
"""Coupling-mode integration: ramp ONLY the last component 0->1
|
|
176
176
|
(forward) and 1->0 (backward) over the fixed base.
|
|
177
177
|
W = int <U_last>_lam dlam; dF = (W_f + W_b)/2 per iteration."""
|
|
178
|
-
lmp =
|
|
178
|
+
lmp = self._open_lammps()
|
|
179
179
|
conf = os.path.join(self.simfolder, "conf.equilibration.data")
|
|
180
180
|
# style only before read_data (pair_coeff needs the box)
|
|
181
181
|
ns = self.calc._pair_style_with_options
|
|
@@ -253,7 +253,7 @@ class Alchemy(cph.Phase):
|
|
|
253
253
|
Fix lattice option is not implemented at present.
|
|
254
254
|
At the end of the run, the averaged box dimensions are calculated.
|
|
255
255
|
"""
|
|
256
|
-
lmp =
|
|
256
|
+
lmp = self._open_lammps()
|
|
257
257
|
|
|
258
258
|
if self.calc.alchemy_coupling:
|
|
259
259
|
# equilibrate on the BASE system (lam = 0): full base, ramped
|
|
@@ -345,7 +345,7 @@ class Alchemy(cph.Phase):
|
|
|
345
345
|
initial, final = self._end_states()
|
|
346
346
|
|
|
347
347
|
# create lammps object
|
|
348
|
-
lmp =
|
|
348
|
+
lmp = self._open_lammps()
|
|
349
349
|
|
|
350
350
|
# Adiabatic switching parameters.
|
|
351
351
|
lmp.command("variable li equal 1.0")
|
|
@@ -345,7 +345,8 @@ class DynamicCCI:
|
|
|
345
345
|
"""
|
|
346
346
|
def equilibrate(phase):
|
|
347
347
|
self.logger.info("Equilibrating the %s cell at %.2f K, %.2f bar" % (phase, self.t0, self.p_start))
|
|
348
|
-
self.jobs[phase]
|
|
348
|
+
with self.jobs[phase]:
|
|
349
|
+
self.jobs[phase].run_averaging()
|
|
349
350
|
self.logger.info("%s cell: %.4f A^3/atom" % (phase, self.jobs[phase].volatom))
|
|
350
351
|
|
|
351
352
|
self._for_cells(equilibrate)
|
|
@@ -24,6 +24,7 @@ sarath.menon@ruhr-uni-bochum.de
|
|
|
24
24
|
"""
|
|
25
25
|
|
|
26
26
|
import os
|
|
27
|
+
import sys
|
|
27
28
|
import shutil
|
|
28
29
|
import warnings
|
|
29
30
|
import logging
|
|
@@ -96,7 +97,7 @@ def create_object(calc, directory):
|
|
|
96
97
|
cmdargs=calc.md.cmdargs,
|
|
97
98
|
directory=directory,
|
|
98
99
|
)
|
|
99
|
-
return
|
|
100
|
+
return _emit_init_commands_or_close(lmp, calc)
|
|
100
101
|
|
|
101
102
|
binary = resolve_lammps_executable(calc.lammps_executable)
|
|
102
103
|
mpi_command = (
|
|
@@ -113,7 +114,20 @@ def create_object(calc, directory):
|
|
|
113
114
|
directory=directory,
|
|
114
115
|
dry_run=False,
|
|
115
116
|
)
|
|
116
|
-
return
|
|
117
|
+
return _emit_init_commands_or_close(lmp, calc)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _emit_init_commands_or_close(lmp, calc):
|
|
121
|
+
"""Run :func:`emit_init_commands`, closing ``lmp`` if it raises.
|
|
122
|
+
|
|
123
|
+
The caller only gets hold of the runner once :func:`create_object`
|
|
124
|
+
returns, so a failure here would otherwise leave the session (in library
|
|
125
|
+
mode, the executor behind it) open."""
|
|
126
|
+
try:
|
|
127
|
+
return emit_init_commands(lmp, calc)
|
|
128
|
+
except BaseException:
|
|
129
|
+
lmp.__exit__(*sys.exc_info())
|
|
130
|
+
raise
|
|
117
131
|
|
|
118
132
|
|
|
119
133
|
def emit_init_commands(lmp, calc):
|
|
@@ -37,6 +37,7 @@ imported lazily inside :class:`LibraryRunner` -- importing this module (or any
|
|
|
37
37
|
other part of calphy) never requires it.
|
|
38
38
|
"""
|
|
39
39
|
import os
|
|
40
|
+
import sys
|
|
40
41
|
import logging
|
|
41
42
|
|
|
42
43
|
from calphy.runner import BaseRunner, _normalize_cmdargs
|
|
@@ -81,7 +82,13 @@ class LibraryRunner(BaseRunner):
|
|
|
81
82
|
self.lmp = LammpsLibrary(
|
|
82
83
|
cores=cores, working_directory=directory, cmdargs=cmdargs
|
|
83
84
|
)
|
|
84
|
-
|
|
85
|
+
# the executor is live from here on; nobody else holds a reference to
|
|
86
|
+
# this runner yet, so close it ourselves if the rest of setup fails
|
|
87
|
+
try:
|
|
88
|
+
self._activate_mliap()
|
|
89
|
+
except BaseException:
|
|
90
|
+
self.__exit__(*sys.exc_info())
|
|
91
|
+
raise
|
|
85
92
|
|
|
86
93
|
def _activate_mliap(self):
|
|
87
94
|
"""Register the mliappy coupling in the live session when available.
|
|
@@ -162,7 +162,7 @@ class Liquid(cph.Phase):
|
|
|
162
162
|
At the end of the run, the averaged box dimensions are calculated.
|
|
163
163
|
"""
|
|
164
164
|
# create lammps object
|
|
165
|
-
lmp =
|
|
165
|
+
lmp = self._open_lammps()
|
|
166
166
|
|
|
167
167
|
lmp = ph.set_pair_style(lmp, self.calc)
|
|
168
168
|
|
|
@@ -237,7 +237,7 @@ class Liquid(cph.Phase):
|
|
|
237
237
|
Run the integration routine where the initial and final systems are connected using
|
|
238
238
|
the lambda parameter. See algorithm 4 in publication.
|
|
239
239
|
"""
|
|
240
|
-
lmp =
|
|
240
|
+
lmp = self._open_lammps()
|
|
241
241
|
|
|
242
242
|
# Adiabatic switching parameters.
|
|
243
243
|
lmp.command("variable li equal 1.0")
|
|
@@ -554,6 +554,11 @@ class Phase:
|
|
|
554
554
|
aborted with a MeltedError. Detection can be turned off by setting
|
|
555
555
|
``tolerance.solid_fraction: 0``.
|
|
556
556
|
"""
|
|
557
|
+
# Disabled (solid_fraction <= 0): the comparison below can never fire, so do not run the
|
|
558
|
+
# structure analysis at all. It is not free, and pyscal3 can segfault in find.neighbors on
|
|
559
|
+
# some LAMMPS triclinic dumps, killing a run over a check that could not have stopped it.
|
|
560
|
+
if self.calc.tolerance.solid_fraction <= 0:
|
|
561
|
+
return
|
|
557
562
|
solids = ph.find_solid_fraction(os.path.join(self.simfolder, filename))
|
|
558
563
|
if solids / self.natoms < self.calc.tolerance.solid_fraction:
|
|
559
564
|
self.lammps_close(lmp=lmp)
|
|
@@ -569,7 +574,11 @@ class Phase:
|
|
|
569
574
|
|
|
570
575
|
If the solid fraction in ``filename`` exceeds
|
|
571
576
|
``tolerance.liquid_fraction`` the run is aborted with a SolidifiedError.
|
|
577
|
+
Disabled (``liquid_fraction >= 1``), the analysis is skipped, as in
|
|
578
|
+
``check_if_melted``.
|
|
572
579
|
"""
|
|
580
|
+
if self.calc.tolerance.liquid_fraction >= 1:
|
|
581
|
+
return
|
|
573
582
|
solids = ph.find_solid_fraction(os.path.join(self.simfolder, filename))
|
|
574
583
|
if solids / self.natoms > self.calc.tolerance.liquid_fraction:
|
|
575
584
|
self.lammps_close(lmp=lmp)
|
|
@@ -1497,7 +1506,7 @@ class Phase:
|
|
|
1497
1506
|
iteration, t0, tf, li, lf, pi, pf,
|
|
1498
1507
|
)
|
|
1499
1508
|
|
|
1500
|
-
lmp =
|
|
1509
|
+
lmp = self._open_lammps()
|
|
1501
1510
|
|
|
1502
1511
|
lmp.command("echo log")
|
|
1503
1512
|
lmp.command("variable li equal %f" % li)
|
|
@@ -1705,7 +1714,7 @@ class Phase:
|
|
|
1705
1714
|
iteration, tf, t0, lf, li, pf, pi,
|
|
1706
1715
|
)
|
|
1707
1716
|
|
|
1708
|
-
lmp =
|
|
1717
|
+
lmp = self._open_lammps()
|
|
1709
1718
|
|
|
1710
1719
|
lmp.command("echo log")
|
|
1711
1720
|
lmp.command("variable li equal %f" % li)
|
|
@@ -1983,7 +1992,7 @@ class Phase:
|
|
|
1983
1992
|
)
|
|
1984
1993
|
|
|
1985
1994
|
# ── Build the LAMMPS object and load the equilibrated configuration ──
|
|
1986
|
-
lmp =
|
|
1995
|
+
lmp = self._open_lammps()
|
|
1987
1996
|
|
|
1988
1997
|
lmp.command("echo log")
|
|
1989
1998
|
lmp = ph.set_pair_style(lmp, self.calc)
|
|
@@ -2173,7 +2182,7 @@ class Phase:
|
|
|
2173
2182
|
p0 = self.calc._pressure
|
|
2174
2183
|
|
|
2175
2184
|
# create lammps object
|
|
2176
|
-
lmp =
|
|
2185
|
+
lmp = self._open_lammps()
|
|
2177
2186
|
|
|
2178
2187
|
lmp.command("echo log")
|
|
2179
2188
|
lmp.command("variable li equal %f" % li)
|
|
@@ -2323,7 +2332,7 @@ class Phase:
|
|
|
2323
2332
|
pf = self.calc._pressure_stop
|
|
2324
2333
|
|
|
2325
2334
|
# create lammps object
|
|
2326
|
-
lmp =
|
|
2335
|
+
lmp = self._open_lammps()
|
|
2327
2336
|
|
|
2328
2337
|
lmp.command("echo log")
|
|
2329
2338
|
lmp.command("variable li equal %f" % li)
|
|
@@ -2482,5 +2491,25 @@ class Phase:
|
|
|
2482
2491
|
with open(os.path.join(self.simfolder, "metadata.yaml"), "w") as fout:
|
|
2483
2492
|
yaml.safe_dump(metadata, fout)
|
|
2484
2493
|
|
|
2494
|
+
def _open_lammps(self):
|
|
2495
|
+
"""Create a stage's LAMMPS session and remember it, so that a
|
|
2496
|
+
surrounding ``with job:`` can close it if the stage raises."""
|
|
2497
|
+
self._lmp = ph.create_object(self.calc, self.simfolder)
|
|
2498
|
+
return self._lmp
|
|
2499
|
+
|
|
2500
|
+
def __enter__(self):
|
|
2501
|
+
return self
|
|
2502
|
+
|
|
2503
|
+
def __exit__(self, exc_type, exc, tb):
|
|
2504
|
+
"""Close the session opened by the last stage, exception or not.
|
|
2505
|
+
|
|
2506
|
+
On success the stage has already closed it itself; closing again is a
|
|
2507
|
+
no-op. The exception is never suppressed.
|
|
2508
|
+
"""
|
|
2509
|
+
lmp, self._lmp = getattr(self, "_lmp", None), None
|
|
2510
|
+
if lmp is not None:
|
|
2511
|
+
lmp.__exit__(exc_type, exc, tb)
|
|
2512
|
+
return False
|
|
2513
|
+
|
|
2485
2514
|
def lammps_close(self, lmp):
|
|
2486
2515
|
lmp.close()
|
|
@@ -271,7 +271,8 @@ class MeltingTemp:
|
|
|
271
271
|
self.logger.info("Starting solid reversible scaling run")
|
|
272
272
|
for i in range(self.soljob.calc.n_iterations):
|
|
273
273
|
try:
|
|
274
|
-
self.soljob
|
|
274
|
+
with self.soljob:
|
|
275
|
+
self.soljob.reversible_scaling(iteration=(i + 1))
|
|
275
276
|
except MeltedError:
|
|
276
277
|
self.logger.info("Solid system melted during reversible scaling run")
|
|
277
278
|
return 2
|
|
@@ -290,7 +291,8 @@ class MeltingTemp:
|
|
|
290
291
|
self.logger.info("Starting liquid reversible scaling calculation")
|
|
291
292
|
for i in range(self.lqdjob.calc.n_iterations):
|
|
292
293
|
try:
|
|
293
|
-
self.lqdjob
|
|
294
|
+
with self.lqdjob:
|
|
295
|
+
self.lqdjob.reversible_scaling(iteration=(i + 1))
|
|
294
296
|
except SolidifiedError:
|
|
295
297
|
self.logger.info("Liquid froze during reversible scaling calculation")
|
|
296
298
|
return 3
|
|
@@ -527,14 +529,16 @@ def routine_fe(job):
|
|
|
527
529
|
Perform an FE calculation routine
|
|
528
530
|
"""
|
|
529
531
|
ts = time.time()
|
|
530
|
-
job
|
|
532
|
+
with job:
|
|
533
|
+
job.run_averaging()
|
|
531
534
|
te = time.time() - ts
|
|
532
535
|
job.logger.info("Averaging routine finished in %f s" % te)
|
|
533
536
|
|
|
534
537
|
# now run integration loops
|
|
535
538
|
for i in range(job.calc.n_iterations):
|
|
536
539
|
ts = time.time()
|
|
537
|
-
job
|
|
540
|
+
with job:
|
|
541
|
+
job.run_integration(iteration=(i + 1))
|
|
538
542
|
te = time.time() - ts
|
|
539
543
|
job.logger.info("Integration cycle %d finished in %f s" % (i + 1, te))
|
|
540
544
|
|
|
@@ -556,14 +560,16 @@ def routine_ts(job):
|
|
|
556
560
|
# in the single-phase region. Disabled by default (mode='none').
|
|
557
561
|
if job.calc.phase_transition_detection.mode != "none":
|
|
558
562
|
ts = time.time()
|
|
559
|
-
job
|
|
563
|
+
with job:
|
|
564
|
+
job.scan_temperature_range()
|
|
560
565
|
te = time.time() - ts
|
|
561
566
|
job.logger.info("Pre-flight temperature-range scan finished in %f s" % te)
|
|
562
567
|
|
|
563
568
|
# now do rev scale steps
|
|
564
569
|
for i in range(job.calc.n_iterations):
|
|
565
570
|
ts = time.time()
|
|
566
|
-
job
|
|
571
|
+
with job:
|
|
572
|
+
job.reversible_scaling(iteration=(i + 1))
|
|
567
573
|
te = time.time() - ts
|
|
568
574
|
job.logger.info("TS integration cycle %d finished in %f s" % (i + 1, te))
|
|
569
575
|
|
|
@@ -581,7 +587,8 @@ def routine_tscale(job):
|
|
|
581
587
|
# now do rev scale steps
|
|
582
588
|
for i in range(job.calc.n_iterations):
|
|
583
589
|
ts = time.time()
|
|
584
|
-
job
|
|
590
|
+
with job:
|
|
591
|
+
job.temperature_scaling(iteration=(i + 1))
|
|
585
592
|
te = time.time() - ts
|
|
586
593
|
job.logger.info("Temperature scaling cycle %d finished in %f s" % (i + 1, te))
|
|
587
594
|
|
|
@@ -599,7 +606,8 @@ def routine_pscale(job):
|
|
|
599
606
|
# now do rev scale steps
|
|
600
607
|
for i in range(job.calc.n_iterations):
|
|
601
608
|
ts = time.time()
|
|
602
|
-
job
|
|
609
|
+
with job:
|
|
610
|
+
job.pressure_scaling(iteration=(i + 1))
|
|
603
611
|
te = time.time() - ts
|
|
604
612
|
job.logger.info("Pressure scaling cycle %d finished in %f s" % (i + 1, te))
|
|
605
613
|
|
|
@@ -613,14 +621,16 @@ def routine_alchemy(job):
|
|
|
613
621
|
Perform an FE calculation routine
|
|
614
622
|
"""
|
|
615
623
|
ts = time.time()
|
|
616
|
-
job
|
|
624
|
+
with job:
|
|
625
|
+
job.run_averaging()
|
|
617
626
|
te = time.time() - ts
|
|
618
627
|
job.logger.info("Averaging routine finished in %f s" % te)
|
|
619
628
|
|
|
620
629
|
# now run integration loops
|
|
621
630
|
for i in range(job.calc.n_iterations):
|
|
622
631
|
ts = time.time()
|
|
623
|
-
job
|
|
632
|
+
with job:
|
|
633
|
+
job.run_integration(iteration=(i + 1))
|
|
624
634
|
te = time.time() - ts
|
|
625
635
|
job.logger.info("Alchemy integration cycle %d finished in %f s" % (i + 1, te))
|
|
626
636
|
|
|
@@ -742,14 +752,16 @@ def routine_composition_scaling(job):
|
|
|
742
752
|
|
|
743
753
|
# now start cycle
|
|
744
754
|
ts = time.time()
|
|
745
|
-
job
|
|
755
|
+
with job:
|
|
756
|
+
job.run_averaging()
|
|
746
757
|
te = time.time() - ts
|
|
747
758
|
job.logger.info("Averaging routine finished in %f s" % te)
|
|
748
759
|
|
|
749
760
|
# now run integration loops
|
|
750
761
|
for i in range(job.calc.n_iterations):
|
|
751
762
|
ts = time.time()
|
|
752
|
-
job
|
|
763
|
+
with job:
|
|
764
|
+
job.run_integration(iteration=(i + 1))
|
|
753
765
|
te = time.time() - ts
|
|
754
766
|
job.logger.info("Alchemy integration cycle %d finished in %f s" % (i + 1, te))
|
|
755
767
|
|
|
@@ -366,6 +366,34 @@ class BaseRunner:
|
|
|
366
366
|
"""End the session and release backend resources."""
|
|
367
367
|
raise NotImplementedError
|
|
368
368
|
|
|
369
|
+
def __enter__(self):
|
|
370
|
+
"""Enter a runner scope; the runner itself is the ``as`` target."""
|
|
371
|
+
return self
|
|
372
|
+
|
|
373
|
+
def __exit__(self, exc_type, exc, tb):
|
|
374
|
+
"""Close the session on the way out, exception or not.
|
|
375
|
+
|
|
376
|
+
This is what makes a stage's LAMMPS session -- and, in library mode,
|
|
377
|
+
the executorlib executor behind it -- go away deterministically even
|
|
378
|
+
when an exception escapes the driver. The driver's exception is never
|
|
379
|
+
suppressed.
|
|
380
|
+
|
|
381
|
+
If :meth:`close` itself fails *while an exception is propagating*, the
|
|
382
|
+
close error is logged and swallowed rather than replacing the original
|
|
383
|
+
one: the interesting failure is the physics error that got us here, not
|
|
384
|
+
the fallout of shutting a half-dead session down.
|
|
385
|
+
"""
|
|
386
|
+
try:
|
|
387
|
+
self.close()
|
|
388
|
+
except Exception:
|
|
389
|
+
if exc_type is None:
|
|
390
|
+
raise
|
|
391
|
+
logger.exception(
|
|
392
|
+
"closing the LAMMPS session failed while handling %s; the "
|
|
393
|
+
"original exception is propagating", exc_type.__name__,
|
|
394
|
+
)
|
|
395
|
+
return False
|
|
396
|
+
|
|
369
397
|
def rotate_logs(self, stage_name):
|
|
370
398
|
"""Collect the LAMMPS log output produced since the last rotation into
|
|
371
399
|
``<stage_name>.log.lammps`` in the sim folder."""
|
|
@@ -91,20 +91,32 @@ class Solid(cph.Phase):
|
|
|
91
91
|
lmp = ph.compute_msd(lmp, self.calc)
|
|
92
92
|
|
|
93
93
|
if ph.check_if_any_is_none(self.calc.spring_constants):
|
|
94
|
-
#
|
|
95
|
-
|
|
94
|
+
# k only has to be close: the Frenkel-Ladd free energy does not depend
|
|
95
|
+
# on its exact value. Stop once k changes by less than
|
|
96
|
+
# tolerance.spring_constant (relative) between cycles, and fall back
|
|
97
|
+
# to the last estimate rather than iterating without end.
|
|
98
|
+
k_last = np.zeros(self.calc.n_elements)
|
|
96
99
|
for i in range(self.calc.md.n_cycles):
|
|
97
100
|
lmp.command("run %d" % int(self.calc.md.n_small_steps))
|
|
98
101
|
lmp.sync() # flush before analyse_spring_constants reads msd.dat
|
|
99
102
|
k_mean, k_std = self.analyse_spring_constants(lmp)
|
|
100
103
|
self.logger.info(
|
|
101
|
-
"At count %d mean k is %
|
|
104
|
+
"At count %d mean k is %s" % (i + 1, " ".join("%f" % k for k in k_mean))
|
|
102
105
|
)
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
k_now = np.array(k_mean)
|
|
107
|
+
# nan for elements without a usable MSD; those are left out here
|
|
108
|
+
change = np.abs(k_now - k_last) / k_now
|
|
109
|
+
k_last = k_now
|
|
110
|
+
finite = np.isfinite(change)
|
|
111
|
+
if not np.any(finite) or np.max(change[finite]) < self.calc.tolerance.spring_constant:
|
|
106
112
|
break
|
|
107
|
-
|
|
113
|
+
else:
|
|
114
|
+
self.logger.warning(
|
|
115
|
+
"Spring constants not converged to a relative change of %f "
|
|
116
|
+
"after %d cycles; using the last estimate"
|
|
117
|
+
% (self.calc.tolerance.spring_constant, self.calc.md.n_cycles)
|
|
118
|
+
)
|
|
119
|
+
self.assign_spring_constants(k_mean)
|
|
108
120
|
|
|
109
121
|
else:
|
|
110
122
|
if not (len(self.calc.spring_constants) == self.calc.n_elements):
|
|
@@ -137,17 +149,21 @@ class Solid(cph.Phase):
|
|
|
137
149
|
k_std = []
|
|
138
150
|
for i in range(self.calc.n_elements):
|
|
139
151
|
quant = msd[:, i + 1][-ncount + 1 :]
|
|
140
|
-
mean_quant = np.
|
|
141
|
-
std_quant = np.
|
|
142
|
-
if mean_quant
|
|
152
|
+
mean_quant = np.mean(quant)
|
|
153
|
+
std_quant = np.std(quant)
|
|
154
|
+
if not (np.isfinite(mean_quant) and mean_quant > 0):
|
|
155
|
+
# e.g. an element with no atoms. nan is replaced by another
|
|
156
|
+
# element's k in validate_spring_constants, which raises if no
|
|
157
|
+
# element has a usable value. Never substitute a fixed MSD: it
|
|
158
|
+
# gives a k that is wrong by orders of magnitude at low T.
|
|
143
159
|
self.logger.warning(
|
|
144
|
-
"MSD for element index %d
|
|
145
|
-
"
|
|
146
|
-
"
|
|
160
|
+
"MSD for element index %d is %s; cannot estimate its spring "
|
|
161
|
+
"constant. Provide spring_constants explicitly if this "
|
|
162
|
+
"element has atoms." % (i, mean_quant)
|
|
147
163
|
)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
164
|
+
k_mean.append(np.nan)
|
|
165
|
+
else:
|
|
166
|
+
k_mean.append(3 * kb * self.calc._temperature / mean_quant)
|
|
151
167
|
k_std.append(std_quant)
|
|
152
168
|
return k_mean, k_std
|
|
153
169
|
|
|
@@ -192,7 +208,7 @@ class Solid(cph.Phase):
|
|
|
192
208
|
At the end of the run, the averaged box dimensions are calculated.
|
|
193
209
|
"""
|
|
194
210
|
|
|
195
|
-
lmp =
|
|
211
|
+
lmp = self._open_lammps()
|
|
196
212
|
|
|
197
213
|
# set up potential
|
|
198
214
|
lmp = ph.set_pair_style(lmp, self.calc)
|
|
@@ -268,7 +284,7 @@ class Solid(cph.Phase):
|
|
|
268
284
|
Run the integration routine where the initial and final systems are connected using
|
|
269
285
|
the lambda parameter. See algorithm 4 in publication.
|
|
270
286
|
"""
|
|
271
|
-
lmp =
|
|
287
|
+
lmp = self._open_lammps()
|
|
272
288
|
|
|
273
289
|
# set up potential
|
|
274
290
|
lmp = ph.set_pair_style(lmp, self.calc)
|
|
@@ -39,6 +39,7 @@ tests/test_dcci_input.py
|
|
|
39
39
|
tests/test_dcci_integrator.py
|
|
40
40
|
tests/test_dcci_lammps_e2e.py
|
|
41
41
|
tests/test_dcci_lammps_semantics.py
|
|
42
|
+
tests/test_disabled_phase_checks.py
|
|
42
43
|
tests/test_dissipation_check.py
|
|
43
44
|
tests/test_element_ordering.py
|
|
44
45
|
tests/test_ex07.py
|
|
@@ -66,5 +67,6 @@ tests/test_runner.py
|
|
|
66
67
|
tests/test_scheduler.py
|
|
67
68
|
tests/test_seed.py
|
|
68
69
|
tests/test_solid_methods.py
|
|
70
|
+
tests/test_spring_constants.py
|
|
69
71
|
tests/test_structures.py
|
|
70
72
|
tests/test_sweep_barostat_targets.py
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""A disabled phase-stability check must not run the structure analysis.
|
|
2
|
+
|
|
3
|
+
`tolerance.solid_fraction: 0` (or `liquid_fraction: 1`) makes the comparison in
|
|
4
|
+
check_if_melted / check_if_solidfied impossible to trigger, but the solid fraction
|
|
5
|
+
was still computed with pyscal3 -- which segfaults in find.neighbors on some LAMMPS
|
|
6
|
+
triclinic dumps (Mg2Si antifluorite, 60-degree primitive cell), killing the run.
|
|
7
|
+
"""
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
|
|
12
|
+
import calphy.phase as phase_mod
|
|
13
|
+
from calphy.phase import Phase
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _fake(solid_fraction=0.0, liquid_fraction=1.0):
|
|
17
|
+
tol = SimpleNamespace(solid_fraction=solid_fraction, liquid_fraction=liquid_fraction)
|
|
18
|
+
return SimpleNamespace(calc=SimpleNamespace(tolerance=tol), simfolder="/nonexistent", natoms=10)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _explode(_):
|
|
22
|
+
raise AssertionError("find_solid_fraction must not be called when the check is disabled")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_disabled_melt_check_skips_structure_analysis(monkeypatch):
|
|
26
|
+
monkeypatch.setattr(phase_mod.ph, "find_solid_fraction", _explode)
|
|
27
|
+
Phase.check_if_melted(_fake(solid_fraction=0.0), lmp=None, filename="traj.temp.dat")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_disabled_solidification_check_skips_structure_analysis(monkeypatch):
|
|
31
|
+
monkeypatch.setattr(phase_mod.ph, "find_solid_fraction", _explode)
|
|
32
|
+
Phase.check_if_solidfied(_fake(liquid_fraction=1.0), lmp=None, filename="traj.temp.dat")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_enabled_melt_check_still_runs(monkeypatch):
|
|
36
|
+
called = []
|
|
37
|
+
monkeypatch.setattr(phase_mod.ph, "find_solid_fraction", lambda f: called.append(f) or 10)
|
|
38
|
+
Phase.check_if_melted(_fake(solid_fraction=0.7), lmp=None, filename="traj.temp.dat")
|
|
39
|
+
assert called == ["/nonexistent/traj.temp.dat"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_enabled_solidification_check_still_runs(monkeypatch):
|
|
43
|
+
called = []
|
|
44
|
+
monkeypatch.setattr(phase_mod.ph, "find_solid_fraction", lambda f: called.append(f) or 0)
|
|
45
|
+
Phase.check_if_solidfied(_fake(liquid_fraction=0.05), lmp=None, filename="traj.temp.dat")
|
|
46
|
+
assert called == ["/nonexistent/traj.temp.dat"]
|
|
@@ -296,11 +296,11 @@ def _check_free_energy(scenario, rundir):
|
|
|
296
296
|
"%s free energy delta %.2e eV/atom exceeds tol %.2e "
|
|
297
297
|
"(new=%.6f base=%.6f)" % (scenario, delta, tol, fe_new, fe_base)
|
|
298
298
|
)
|
|
299
|
-
# Spring constant is informational only:
|
|
300
|
-
#
|
|
301
|
-
# run-to-run (e.g. B1
|
|
302
|
-
# The free energy is insensitive to the exact k (Frenkel-Ladd),
|
|
303
|
-
# on fe only and just report the spring delta.
|
|
299
|
+
# Spring constant is informational only: the baselines were made when calphy
|
|
300
|
+
# rounded the mean MSD to 2 decimals, which made k = 3 kB T / MSD bistable
|
|
301
|
+
# run-to-run (e.g. B1 flipped between ~1.85 and ~2.15), and the RNG seed is
|
|
302
|
+
# not pinned. The free energy is insensitive to the exact k (Frenkel-Ladd),
|
|
303
|
+
# so we assert on fe only and just report the spring delta.
|
|
304
304
|
k_new = new.get("average", {}).get("spring_constant")
|
|
305
305
|
k_base = base.get("average", {}).get("spring_constant")
|
|
306
306
|
kinfo = ""
|
|
@@ -254,6 +254,47 @@ def test_create_object_library_mode(tmp_path, fake_pylammpsmpi, make_calc):
|
|
|
254
254
|
assert len(lmp.logical_commands) == 4
|
|
255
255
|
|
|
256
256
|
|
|
257
|
+
@pytest.fixture
|
|
258
|
+
def recorded_libraries(fake_pylammpsmpi, monkeypatch):
|
|
259
|
+
"""Every FakeLammpsLibrary created, so a test can check it got closed."""
|
|
260
|
+
created = []
|
|
261
|
+
|
|
262
|
+
class Recording(FakeLammpsLibrary):
|
|
263
|
+
def __init__(self, *args, **kwargs):
|
|
264
|
+
super().__init__(*args, **kwargs)
|
|
265
|
+
created.append(self)
|
|
266
|
+
|
|
267
|
+
monkeypatch.setattr(fake_pylammpsmpi, "LammpsLibrary", Recording)
|
|
268
|
+
return created
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def test_failed_mliap_activation_closes_the_session(
|
|
272
|
+
tmp_path, recorded_libraries, fake_lammps_mliap, monkeypatch
|
|
273
|
+
):
|
|
274
|
+
def boom(self):
|
|
275
|
+
raise RuntimeError("activation failed")
|
|
276
|
+
|
|
277
|
+
monkeypatch.setattr(_FakeConcurrent, "activate_mliappy", boom)
|
|
278
|
+
with pytest.raises(RuntimeError, match="activation failed"):
|
|
279
|
+
make_runner(tmp_path)
|
|
280
|
+
assert [lib.closed for lib in recorded_libraries] == [True]
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_create_object_closes_the_session_if_init_commands_fail(
|
|
284
|
+
tmp_path, recorded_libraries, make_calc, monkeypatch
|
|
285
|
+
):
|
|
286
|
+
import calphy.helpers as ph
|
|
287
|
+
|
|
288
|
+
def boom(lmp, calc):
|
|
289
|
+
raise ValueError("bad init command")
|
|
290
|
+
|
|
291
|
+
monkeypatch.setattr(ph, "emit_init_commands", boom)
|
|
292
|
+
calc = make_calc("B1", execution_mode="library")
|
|
293
|
+
with pytest.raises(ValueError, match="bad init command"):
|
|
294
|
+
ph.create_object(calc, str(tmp_path))
|
|
295
|
+
assert [lib.closed for lib in recorded_libraries] == [True]
|
|
296
|
+
|
|
297
|
+
|
|
257
298
|
def test_execution_mode_validation(make_calc):
|
|
258
299
|
with pytest.raises(Exception, match="execution_mode"):
|
|
259
300
|
make_calc("B1", execution_mode="banana")
|
|
@@ -633,3 +633,76 @@ def test_imports_without_pylammpsmpi():
|
|
|
633
633
|
text=True, cwd=REPO_ROOT)
|
|
634
634
|
assert proc.returncode == 0, proc.stdout + proc.stderr
|
|
635
635
|
assert "OK" in proc.stdout
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
# --------------------------------------------------------------------------- #
|
|
639
|
+
# BaseRunner as a context manager
|
|
640
|
+
# --------------------------------------------------------------------------- #
|
|
641
|
+
class _RecordingRunner(R.BaseRunner):
|
|
642
|
+
"""Minimal BaseRunner subclass that records close() calls."""
|
|
643
|
+
|
|
644
|
+
def __init__(self, directory=".", close_error=None):
|
|
645
|
+
super().__init__(directory)
|
|
646
|
+
self.closes = 0
|
|
647
|
+
self.close_error = close_error
|
|
648
|
+
|
|
649
|
+
def close(self):
|
|
650
|
+
self.closes += 1
|
|
651
|
+
if self.close_error is not None:
|
|
652
|
+
raise self.close_error
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def test_context_manager_yields_the_runner_and_closes():
|
|
656
|
+
runner = _RecordingRunner()
|
|
657
|
+
with runner as lmp:
|
|
658
|
+
assert lmp is runner
|
|
659
|
+
assert runner.closes == 0
|
|
660
|
+
assert runner.closes == 1
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def test_context_manager_closes_on_exception_and_does_not_suppress():
|
|
664
|
+
runner = _RecordingRunner()
|
|
665
|
+
with pytest.raises(ValueError, match="physics"):
|
|
666
|
+
with runner:
|
|
667
|
+
raise ValueError("physics went wrong")
|
|
668
|
+
assert runner.closes == 1
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def test_close_failure_propagates_when_no_exception_is_in_flight():
|
|
672
|
+
runner = _RecordingRunner(close_error=RuntimeError("close failed"))
|
|
673
|
+
with pytest.raises(RuntimeError, match="close failed"):
|
|
674
|
+
with runner:
|
|
675
|
+
pass
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
def test_close_failure_does_not_mask_the_original_exception():
|
|
679
|
+
"""A dead LAMMPS worker can make close() fail; the physics error must win."""
|
|
680
|
+
runner = _RecordingRunner(close_error=RuntimeError("worker already dead"))
|
|
681
|
+
with pytest.raises(ValueError, match="physics"):
|
|
682
|
+
with runner:
|
|
683
|
+
raise ValueError("physics went wrong")
|
|
684
|
+
assert runner.closes == 1
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
def test_phase_context_closes_the_session_of_a_failed_stage(monkeypatch):
|
|
688
|
+
"""``with job: job.stage()`` closes the session the stage opened if it raises."""
|
|
689
|
+
import calphy.phase as cph
|
|
690
|
+
|
|
691
|
+
runner = _RecordingRunner()
|
|
692
|
+
monkeypatch.setattr(cph.ph, "create_object", lambda calc, simfolder: runner)
|
|
693
|
+
job = cph.Phase.__new__(cph.Phase)
|
|
694
|
+
job.calc, job.simfolder = None, "."
|
|
695
|
+
|
|
696
|
+
def stage():
|
|
697
|
+
job._open_lammps()
|
|
698
|
+
raise ValueError("physics went wrong")
|
|
699
|
+
|
|
700
|
+
with pytest.raises(ValueError, match="physics"):
|
|
701
|
+
with job:
|
|
702
|
+
stage()
|
|
703
|
+
assert runner.closes == 1
|
|
704
|
+
|
|
705
|
+
# a context with no stage in it is a no-op
|
|
706
|
+
with job:
|
|
707
|
+
pass
|
|
708
|
+
assert runner.closes == 1
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Spring-constant estimate from the MSD in Solid.
|
|
2
|
+
|
|
3
|
+
A small MSD (low T, heavy or stiff elements) used to be rounded to 0 Ų and
|
|
4
|
+
replaced by 1 Ų, giving a k that was orders of magnitude too soft. These tests
|
|
5
|
+
feed a synthetic msd.dat through the estimate and the convergence loop.
|
|
6
|
+
"""
|
|
7
|
+
import logging
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from calphy.integrators import kb
|
|
14
|
+
from calphy.solid import Solid
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class FakeLmp:
|
|
18
|
+
"""Serves msd.dat as `rows` more samples per run command."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, columns, rows):
|
|
21
|
+
self.columns = [np.asarray(c, dtype=float) for c in columns]
|
|
22
|
+
self.rows = rows
|
|
23
|
+
self.nruns = 0
|
|
24
|
+
|
|
25
|
+
def command(self, cmd):
|
|
26
|
+
if cmd.startswith("run"):
|
|
27
|
+
self.nruns += 1
|
|
28
|
+
|
|
29
|
+
def sync(self):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
def read_timeseries(self, name):
|
|
33
|
+
assert name == "msd.dat"
|
|
34
|
+
n = self.nruns * self.rows
|
|
35
|
+
steps = np.arange(n, dtype=float)
|
|
36
|
+
return np.column_stack([steps] + [c[:n] for c in self.columns])
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def make_solid(elements, temperature, spring_constants=None, n_cycles=20,
|
|
40
|
+
tolerance=0.1):
|
|
41
|
+
job = Solid.__new__(Solid)
|
|
42
|
+
job.logger = logging.getLogger("test_spring_constants")
|
|
43
|
+
job.calc = SimpleNamespace(
|
|
44
|
+
element=elements,
|
|
45
|
+
n_elements=len(elements),
|
|
46
|
+
_temperature=temperature,
|
|
47
|
+
_qtb=False,
|
|
48
|
+
spring_constants=spring_constants,
|
|
49
|
+
n_equilibration_steps=25000,
|
|
50
|
+
md=SimpleNamespace(
|
|
51
|
+
n_every_steps=10,
|
|
52
|
+
n_repeat_steps=10,
|
|
53
|
+
n_small_steps=10000,
|
|
54
|
+
n_cycles=n_cycles,
|
|
55
|
+
thermostat_damping=[0.1, 0.1],
|
|
56
|
+
),
|
|
57
|
+
tolerance=SimpleNamespace(spring_constant=tolerance),
|
|
58
|
+
)
|
|
59
|
+
return job
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def noisy_msd(mean, n, rel_noise=0.05, seed=0):
|
|
63
|
+
rng = np.random.default_rng(seed)
|
|
64
|
+
return mean * (1 + rel_noise * rng.standard_normal(n))
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_small_msd_gives_stiff_spring():
|
|
68
|
+
# bcc W at 100 K: MSD ~0.004 Ų must give k = 3kT/0.004 ~ 6.46, not 3kT/1
|
|
69
|
+
job = make_solid(["W"], 100.0)
|
|
70
|
+
lmp = FakeLmp([np.full(300, 0.004)], rows=100)
|
|
71
|
+
lmp.nruns = 3
|
|
72
|
+
k_mean, _ = job.analyse_spring_constants(lmp)
|
|
73
|
+
assert k_mean[0] == pytest.approx(3 * kb * 100.0 / 0.004)
|
|
74
|
+
assert k_mean[0] == pytest.approx(6.46, abs=0.01)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_msd_is_not_rounded():
|
|
78
|
+
# 0.0149 and 0.0051 both rounded to 0.01 before
|
|
79
|
+
job = make_solid(["A", "B"], 300.0)
|
|
80
|
+
lmp = FakeLmp([np.full(300, 0.0149), np.full(300, 0.0051)], rows=100)
|
|
81
|
+
lmp.nruns = 3
|
|
82
|
+
k_mean, _ = job.analyse_spring_constants(lmp)
|
|
83
|
+
assert k_mean[0] == pytest.approx(3 * kb * 300.0 / 0.0149)
|
|
84
|
+
assert k_mean[1] == pytest.approx(3 * kb * 300.0 / 0.0051)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_zero_msd_single_element_raises():
|
|
88
|
+
job = make_solid(["W"], 100.0)
|
|
89
|
+
lmp = FakeLmp([np.zeros(300)], rows=100)
|
|
90
|
+
with pytest.raises(ValueError):
|
|
91
|
+
job.run_spring_constant_convergence(lmp)
|
|
92
|
+
# no usable estimate at all: stop on the first cycle instead of looping
|
|
93
|
+
assert lmp.nruns == 1
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_zero_msd_empty_element_takes_other_k():
|
|
97
|
+
# an element with no atoms has MSD 0; it gets the other element's k
|
|
98
|
+
job = make_solid(["Cu", "Zr"], 100.0)
|
|
99
|
+
lmp = FakeLmp([noisy_msd(0.004, 2000), np.zeros(2000)], rows=100)
|
|
100
|
+
job.run_spring_constant_convergence(lmp)
|
|
101
|
+
assert np.isfinite(job.k).all()
|
|
102
|
+
assert job.k[0] == pytest.approx(6.46, rel=0.05)
|
|
103
|
+
assert job.k[1] == job.k[0]
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_user_spring_constant_overrides_estimate():
|
|
107
|
+
job = make_solid(["Cu", "Zr"], 100.0, spring_constants=[None, 2.0])
|
|
108
|
+
lmp = FakeLmp([noisy_msd(0.004, 2000), np.zeros(2000)], rows=100)
|
|
109
|
+
job.run_spring_constant_convergence(lmp)
|
|
110
|
+
assert job.k[0] == pytest.approx(6.46, rel=0.05)
|
|
111
|
+
assert job.k[1] == 2.0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_convergence_stops_after_two_cycles():
|
|
115
|
+
# a stationary MSD converges as soon as two estimates can be compared
|
|
116
|
+
job = make_solid(["W"], 100.0)
|
|
117
|
+
lmp = FakeLmp([noisy_msd(0.004, 2000)], rows=100)
|
|
118
|
+
job.run_spring_constant_convergence(lmp)
|
|
119
|
+
assert lmp.nruns == 2
|
|
120
|
+
assert job.k[0] == pytest.approx(6.46, rel=0.05)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_convergence_is_bounded_by_n_cycles():
|
|
124
|
+
# an MSD that keeps drifting never meets the tolerance: use the last
|
|
125
|
+
# estimate after n_cycles instead of iterating further or failing
|
|
126
|
+
job = make_solid(["W"], 100.0, n_cycles=5)
|
|
127
|
+
drifting = np.repeat(0.004 * 1.5 ** np.arange(5), 100)
|
|
128
|
+
lmp = FakeLmp([drifting], rows=100)
|
|
129
|
+
job.run_spring_constant_convergence(lmp)
|
|
130
|
+
assert lmp.nruns == 5
|
|
131
|
+
assert np.isfinite(job.k[0])
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|