polyphys 1.0.0__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.
- polyphys/__init__.py +7 -0
- polyphys/__version__.py +22 -0
- polyphys/analyze/__init__.py +51 -0
- polyphys/analyze/clusters.py +1738 -0
- polyphys/analyze/correlations.py +995 -0
- polyphys/analyze/density.py +1138 -0
- polyphys/analyze/measurer.py +1338 -0
- polyphys/analyze/pipeline.py +1868 -0
- polyphys/analyze/stats.py +810 -0
- polyphys/analyze/sumrule.py +1159 -0
- polyphys/manage/__init__.py +11 -0
- polyphys/manage/organizer.py +2188 -0
- polyphys/manage/parser.py +1264 -0
- polyphys/manage/types.py +171 -0
- polyphys/manage/utils.py +777 -0
- polyphys/probe/__init__.py +25 -0
- polyphys/probe/binders.py +711 -0
- polyphys/probe/chain_size.py +583 -0
- polyphys/probe/core.py +437 -0
- polyphys/probe/histograms.py +863 -0
- polyphys/probe/pairs.py +555 -0
- polyphys/simulate/__init__.py +48 -0
- polyphys/simulate/build.py +1231 -0
- polyphys/simulate/core.py +237 -0
- polyphys/simulate/data.py +977 -0
- polyphys/simulate/dump.py +752 -0
- polyphys/simulate/log.py +788 -0
- polyphys/spec.py +1359 -0
- polyphys/tests/analyze/test_clusters.py +1058 -0
- polyphys/tests/analyze/test_correlations.py +708 -0
- polyphys/tests/analyze/test_density.py +813 -0
- polyphys/tests/analyze/test_measurer.py +785 -0
- polyphys/tests/analyze/test_pipeline.py +1881 -0
- polyphys/tests/analyze/test_stats.py +732 -0
- polyphys/tests/analyze/test_sumrule.py +917 -0
- polyphys/tests/conftest.py +753 -0
- polyphys/tests/data/lammps_build/make_starts.py +70 -0
- polyphys/tests/manage/test_organizer.py +1495 -0
- polyphys/tests/manage/test_parser.py +1222 -0
- polyphys/tests/manage/test_utils.py +498 -0
- polyphys/tests/probe/conftest.py +407 -0
- polyphys/tests/probe/test_binders.py +686 -0
- polyphys/tests/probe/test_chain_size.py +557 -0
- polyphys/tests/probe/test_core.py +391 -0
- polyphys/tests/probe/test_histograms.py +779 -0
- polyphys/tests/probe/test_pairs.py +475 -0
- polyphys/tests/simulate/test_build.py +1042 -0
- polyphys/tests/simulate/test_core.py +106 -0
- polyphys/tests/simulate/test_data.py +858 -0
- polyphys/tests/simulate/test_dump.py +487 -0
- polyphys/tests/simulate/test_log.py +667 -0
- polyphys/tests/strategies.py +120 -0
- polyphys/tests/synthetic.py +410 -0
- polyphys/tests/test_conftest.py +414 -0
- polyphys/tests/test_docs.py +82 -0
- polyphys/tests/test_packaging.py +97 -0
- polyphys/tests/test_spec.py +772 -0
- polyphys/tests/viz/conftest.py +32 -0
- polyphys/tests/viz/test_colors.py +113 -0
- polyphys/tests/viz/test_helpers.py +292 -0
- polyphys/viz/__init__.py +15 -0
- polyphys/viz/colors.py +204 -0
- polyphys/viz/helpers.py +549 -0
- polyphys-1.0.0.dist-info/METADATA +339 -0
- polyphys-1.0.0.dist-info/RECORD +69 -0
- polyphys-1.0.0.dist-info/WHEEL +5 -0
- polyphys-1.0.0.dist-info/licenses/AUTHORS.rst +13 -0
- polyphys-1.0.0.dist-info/licenses/LICENSE +22 -0
- polyphys-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
"""Tests for :mod:`polyphys.simulate.log`.
|
|
2
|
+
|
|
3
|
+
Tier A tests parse synthetic logs assembled in the test; each expected value
|
|
4
|
+
is a number the test writes into the log, so the parser must read it back
|
|
5
|
+
exactly, or, for the wall time, an analytic conversion.
|
|
6
|
+
|
|
7
|
+
Tier B tests read the logs in ``polyphys/tests/data/lammps_log/``, written
|
|
8
|
+
by LAMMPS (22 Jul 2025 - Update 4) from the inputs beside them. Their
|
|
9
|
+
expected values were read from the log text with ``grep`` and ``awk``, not
|
|
10
|
+
from the parser.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import gzip
|
|
14
|
+
import logging
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Callable, Sequence
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
import pytest
|
|
21
|
+
|
|
22
|
+
from polyphys.simulate.log import (
|
|
23
|
+
BrokenLogError,
|
|
24
|
+
_lammps_version,
|
|
25
|
+
parse_log,
|
|
26
|
+
read_log,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def make_log() -> Callable[..., str]:
|
|
32
|
+
"""Return a factory that assembles a synthetic LAMMPS log.
|
|
33
|
+
|
|
34
|
+
Each table, a sequence of lines, becomes one run: a run-start line, the
|
|
35
|
+
table, and that run's summary lines (by default a ``Loop time of`` line
|
|
36
|
+
for 100 steps with 20 atoms on 1 process). The log starts with
|
|
37
|
+
`first_line` and ends with `wall`.
|
|
38
|
+
"""
|
|
39
|
+
def _make(
|
|
40
|
+
*tables: Sequence[str],
|
|
41
|
+
summaries: Sequence[Sequence[str]] | None = None,
|
|
42
|
+
first_line: str = "LAMMPS (22 Jul 2025 - Update 4)",
|
|
43
|
+
run_start: str = ("Per MPI rank memory allocation (min/avg/max) = "
|
|
44
|
+
"4.9 | 4.9 | 4.9 Mbytes"),
|
|
45
|
+
wall: str = "Total wall time: 0:00:01",
|
|
46
|
+
) -> str:
|
|
47
|
+
if summaries is None:
|
|
48
|
+
summaries = [
|
|
49
|
+
["Loop time of 0.5 on 1 procs for 100 steps with 20 atoms"]
|
|
50
|
+
] * len(tables)
|
|
51
|
+
lines = [first_line]
|
|
52
|
+
for table, summary in zip(tables, summaries, strict=True):
|
|
53
|
+
lines += [run_start, *table, *summary]
|
|
54
|
+
lines.append(wall)
|
|
55
|
+
return "\n".join(lines) + "\n"
|
|
56
|
+
return _make
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def log_dir() -> Path:
|
|
61
|
+
"""Return the directory of the LAMMPS log fixtures."""
|
|
62
|
+
return Path(__file__).resolve().parents[1] / "data" / "lammps_log"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Thermo tables --------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_parse_log_one_style_reads_keywords_and_values(make_log):
|
|
69
|
+
"""A one-style table becomes one column per keyword, Step as int64."""
|
|
70
|
+
text = make_log([" Step Temp Press",
|
|
71
|
+
" 0 1.0 0.5",
|
|
72
|
+
" 100 0.9 0.4"])
|
|
73
|
+
|
|
74
|
+
log = parse_log(text)
|
|
75
|
+
|
|
76
|
+
expected = pd.DataFrame({"run": [0, 0], "Step": [0, 100],
|
|
77
|
+
"Temp": [1.0, 0.9], "Press": [0.5, 0.4]})
|
|
78
|
+
pd.testing.assert_frame_equal(
|
|
79
|
+
log.thermo, expected.astype({"run": "int64", "Step": "int64"}))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_parse_log_custom_keywords_kept_verbatim(make_log):
|
|
83
|
+
"""Custom keywords with digits and brackets keep their exact names."""
|
|
84
|
+
text = make_log(["Step c_gyr[1] c_gyr[2] v_x1 c_1 f_3[2]",
|
|
85
|
+
"0 1.5 2.5 3.5 4.5 5.5"])
|
|
86
|
+
|
|
87
|
+
log = parse_log(text)
|
|
88
|
+
|
|
89
|
+
assert list(log.thermo.columns) == [
|
|
90
|
+
"run", "Step", "c_gyr[1]", "c_gyr[2]", "v_x1", "c_1", "f_3[2]"]
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_parse_log_multi_style_reads_sections(make_log):
|
|
94
|
+
"""A multi-style section gives Step, CPU, and each Keyword = value."""
|
|
95
|
+
text = make_log([
|
|
96
|
+
"------------ Step 0 ----- CPU = 0 (sec) "
|
|
97
|
+
"-------------",
|
|
98
|
+
"TotEng = -1.5000 KinEng = 1.5000 Temp = "
|
|
99
|
+
" 1.0000 ",
|
|
100
|
+
"PotEng = -3.0000 Press = 0.2500",
|
|
101
|
+
"------------ Step 100 ----- CPU = 0.01234 (sec) "
|
|
102
|
+
"-------------",
|
|
103
|
+
"TotEng = -1.2500 KinEng = 1.2500 Temp = "
|
|
104
|
+
" 0.8000 ",
|
|
105
|
+
"PotEng = -2.5000 Press = 0.1250",
|
|
106
|
+
])
|
|
107
|
+
|
|
108
|
+
log = parse_log(text)
|
|
109
|
+
|
|
110
|
+
expected = pd.DataFrame({
|
|
111
|
+
"run": [0, 0], "Step": [0, 100], "CPU": [0.0, 0.01234],
|
|
112
|
+
"TotEng": [-1.5, -1.25], "KinEng": [1.5, 1.25], "Temp": [1.0, 0.8],
|
|
113
|
+
"PotEng": [-3.0, -2.5], "Press": [0.25, 0.125],
|
|
114
|
+
})
|
|
115
|
+
pd.testing.assert_frame_equal(
|
|
116
|
+
log.thermo, expected.astype({"run": "int64", "Step": "int64"}))
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def test_parse_log_yaml_style_reads_rows(make_log):
|
|
120
|
+
"""A yaml-style table takes its keywords from the keywords line."""
|
|
121
|
+
text = make_log(["---",
|
|
122
|
+
"keywords: ['Step', 'Temp', 'c_rg[1]', ]",
|
|
123
|
+
"data:",
|
|
124
|
+
" - [0, 1, 2.5, ]",
|
|
125
|
+
" - [100, 0.75, 2.25, ]",
|
|
126
|
+
"..."])
|
|
127
|
+
|
|
128
|
+
log = parse_log(text)
|
|
129
|
+
|
|
130
|
+
expected = pd.DataFrame({"run": [0, 0], "Step": [0, 100],
|
|
131
|
+
"Temp": [1.0, 0.75], "c_rg[1]": [2.5, 2.25]})
|
|
132
|
+
pd.testing.assert_frame_equal(
|
|
133
|
+
log.thermo, expected.astype({"run": "int64", "Step": "int64"}))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@pytest.mark.parametrize(
|
|
137
|
+
("table", "expected_steps"),
|
|
138
|
+
[
|
|
139
|
+
pytest.param(
|
|
140
|
+
["Step Temp",
|
|
141
|
+
"0 1.0",
|
|
142
|
+
"WARNING: Lost atoms: original 20 current 19 "
|
|
143
|
+
"(src/thermo.cpp:532)",
|
|
144
|
+
"For more information see https://docs.lammps.org/err0008 "
|
|
145
|
+
"(src/thermo.cpp:532)",
|
|
146
|
+
"100 0.9"],
|
|
147
|
+
[0, 100], id="warning-in-one-table"),
|
|
148
|
+
pytest.param(
|
|
149
|
+
["WARNING: Communication cutoff is shorter than a bond length "
|
|
150
|
+
"(src/comm.cpp:743)",
|
|
151
|
+
"Step Temp",
|
|
152
|
+
"0 1.0"],
|
|
153
|
+
[0], id="warning-before-header"),
|
|
154
|
+
pytest.param(
|
|
155
|
+
["------------ Step 0 ----- CPU = 0 "
|
|
156
|
+
"(sec) -------------",
|
|
157
|
+
"Temp = 1.0000 ",
|
|
158
|
+
"WARNING: Lost atoms: original 20 current 19 "
|
|
159
|
+
"(src/thermo.cpp:532)",
|
|
160
|
+
"------------ Step 100 ----- CPU = 0.01 "
|
|
161
|
+
"(sec) -------------",
|
|
162
|
+
"Temp = 0.9000 "],
|
|
163
|
+
[0, 100], id="warning-in-multi-table"),
|
|
164
|
+
pytest.param(
|
|
165
|
+
["Step Temp",
|
|
166
|
+
"0 1.0",
|
|
167
|
+
"Fix halt condition for fix-id F_halt met on step 50 with "
|
|
168
|
+
"value 50"],
|
|
169
|
+
[0], id="fix-halt-message"),
|
|
170
|
+
],
|
|
171
|
+
)
|
|
172
|
+
def test_parse_log_skips_messages_in_table(make_log, table, expected_steps):
|
|
173
|
+
"""WARNING and fix halt lines inside a table are not read as data."""
|
|
174
|
+
text = make_log(table)
|
|
175
|
+
|
|
176
|
+
log = parse_log(text)
|
|
177
|
+
|
|
178
|
+
assert log.thermo["Step"].tolist() == expected_steps
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def test_parse_log_logs_skipped_message_with_line_number(make_log, caplog):
|
|
182
|
+
"""A skipped WARNING line is logged at INFO with its line number."""
|
|
183
|
+
text = make_log(["Step Temp", "0 1.0",
|
|
184
|
+
"WARNING: Lost atoms: original 20 current 19"])
|
|
185
|
+
|
|
186
|
+
with caplog.at_level(logging.INFO, logger="polyphys.simulate.log"):
|
|
187
|
+
parse_log(text)
|
|
188
|
+
|
|
189
|
+
assert "line 5" in caplog.text
|
|
190
|
+
assert "WARNING: Lost atoms" in caplog.text
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def test_parse_log_reads_nan_and_inf(make_log):
|
|
194
|
+
"""nan, -nan, inf, and -inf values are read as such, not dropped."""
|
|
195
|
+
text = make_log(["Step A B C D",
|
|
196
|
+
"0 nan -nan inf -inf"])
|
|
197
|
+
|
|
198
|
+
log = parse_log(text)
|
|
199
|
+
|
|
200
|
+
np.testing.assert_array_equal(
|
|
201
|
+
log.thermo.loc[0, ["A", "B", "C", "D"]].to_numpy(dtype=float),
|
|
202
|
+
[np.nan, np.nan, np.inf, -np.inf])
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def test_parse_log_joins_runs_with_run_column(make_log):
|
|
206
|
+
"""Runs are joined in log order, keeping each run's repeated first step."""
|
|
207
|
+
text = make_log(["Step Temp", "0 1.0", "100 0.9"],
|
|
208
|
+
["Step Temp", "100 0.9", "200 0.8"])
|
|
209
|
+
|
|
210
|
+
log = parse_log(text)
|
|
211
|
+
|
|
212
|
+
assert log.thermo["run"].tolist() == [0, 0, 1, 1]
|
|
213
|
+
assert log.thermo["Step"].tolist() == [0, 100, 100, 200]
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def test_parse_log_keyword_absent_from_run_is_nan(make_log):
|
|
217
|
+
"""A keyword missing from one run is NaN in that run's rows."""
|
|
218
|
+
text = make_log(["Step Temp Press", "0 1.0 0.5"],
|
|
219
|
+
["Step Temp", "100 0.9"])
|
|
220
|
+
|
|
221
|
+
log = parse_log(text)
|
|
222
|
+
|
|
223
|
+
np.testing.assert_array_equal(log.thermo["Press"].to_numpy(),
|
|
224
|
+
[0.5, np.nan])
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@pytest.mark.parametrize(
|
|
228
|
+
("first_line", "expected"),
|
|
229
|
+
[
|
|
230
|
+
pytest.param("LAMMPS (22 Jul 2025 - Update 4)",
|
|
231
|
+
"22 Jul 2025 - Update 4", id="recent"),
|
|
232
|
+
pytest.param("LAMMPS (29 Oct 2020)", "29 Oct 2020", id="older"),
|
|
233
|
+
pytest.param("units lj", None, id="opened-by-log-command"),
|
|
234
|
+
],
|
|
235
|
+
)
|
|
236
|
+
def test_parse_log_reads_version_from_first_line(make_log, first_line,
|
|
237
|
+
expected):
|
|
238
|
+
"""The version comes from a first line LAMMPS (<version>), else None."""
|
|
239
|
+
text = make_log(["Step", "0"], first_line=first_line)
|
|
240
|
+
|
|
241
|
+
log = parse_log(text)
|
|
242
|
+
|
|
243
|
+
assert log.lammps_version == expected
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_lammps_version_of_empty_log_is_none():
|
|
247
|
+
"""An empty list of lines has no version line."""
|
|
248
|
+
assert _lammps_version([]) is None
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def test_parse_log_accepts_old_run_start_line(make_log):
|
|
252
|
+
"""The run-start line of old versions also opens a run."""
|
|
253
|
+
text = make_log(["Step Temp", "0 1.0"],
|
|
254
|
+
run_start="Memory usage per processor = 2.1 Mbytes")
|
|
255
|
+
|
|
256
|
+
log = parse_log(text)
|
|
257
|
+
|
|
258
|
+
assert log.thermo["Step"].tolist() == [0]
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# Broken logs -----------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@pytest.mark.parametrize(
|
|
265
|
+
("tables", "options", "match"),
|
|
266
|
+
[
|
|
267
|
+
pytest.param(
|
|
268
|
+
[["Step Temp", "0 1.0"]], {"summaries": [[]]},
|
|
269
|
+
r"Run 0 \(line 2\) has no 'Loop time of' line", id="no-footer"),
|
|
270
|
+
pytest.param(
|
|
271
|
+
[["Step Temp", "0 1.0"]], {"wall": ""},
|
|
272
|
+
r"no 'Total wall time:' line", id="no-wall-time"),
|
|
273
|
+
pytest.param(
|
|
274
|
+
[["Step Temp", "0 1.0"]],
|
|
275
|
+
{"wall": "# run ends with Total wall time: 0:00:01"},
|
|
276
|
+
r"no 'Total wall time:' line", id="wall-time-only-in-comment"),
|
|
277
|
+
pytest.param(
|
|
278
|
+
[], {}, r"The log has no run", id="no-run"),
|
|
279
|
+
pytest.param(
|
|
280
|
+
[["Step Temp", "0 1.0",
|
|
281
|
+
"ERROR: Lost atoms: original 20 current 19"]], {},
|
|
282
|
+
r"Line 5 reports an error", id="error-line"),
|
|
283
|
+
pytest.param(
|
|
284
|
+
[["Step Temp", "0 1.0", "fix print: Rg 5.2"]], {},
|
|
285
|
+
r"Line 5 is inside a thermo table but is not data",
|
|
286
|
+
id="stray-line"),
|
|
287
|
+
pytest.param(
|
|
288
|
+
[["Step Temp", "0 1.0", "100"]], {},
|
|
289
|
+
r"Line 5 is inside a thermo table but is not data",
|
|
290
|
+
id="short-row"),
|
|
291
|
+
pytest.param(
|
|
292
|
+
[["Step Temp", "0 abc"]], {},
|
|
293
|
+
r"Line 4 is inside a thermo table but is not data",
|
|
294
|
+
id="non-numeric-value"),
|
|
295
|
+
pytest.param(
|
|
296
|
+
[["WARNING: Lost atoms: original 20 current 19"]], {},
|
|
297
|
+
r"Run 0 \(line 2\) has no thermo header", id="no-header"),
|
|
298
|
+
pytest.param(
|
|
299
|
+
[["------------ Step 0 ----- CPU = 0 "
|
|
300
|
+
"(sec) -------------",
|
|
301
|
+
"fix print: Rg 5.2"]], {},
|
|
302
|
+
r"Line 4 is inside a thermo table but is not data",
|
|
303
|
+
id="multi-stray-line"),
|
|
304
|
+
pytest.param(
|
|
305
|
+
[["---", "keywords: ['Step', ]", "data:", "fix print: Rg 5.2"]],
|
|
306
|
+
{}, r"Line 6 is inside a thermo table but is not data",
|
|
307
|
+
id="yaml-stray-line"),
|
|
308
|
+
pytest.param(
|
|
309
|
+
[["---", "data:", "..."]], {},
|
|
310
|
+
r"Run 0 \(line 2\) has no 'keywords:' line",
|
|
311
|
+
id="yaml-without-keywords"),
|
|
312
|
+
pytest.param(
|
|
313
|
+
[["Step Temp", "0 1.0"]],
|
|
314
|
+
{"summaries": [["Loop time of 0.01 on 4 procs (2 MPI x 2 OpenMP) "
|
|
315
|
+
"for 100 steps with 20 atoms"]]},
|
|
316
|
+
r"Line 5 starts with 'Loop time of' but is not",
|
|
317
|
+
id="loop-line-of-another-form"),
|
|
318
|
+
pytest.param(
|
|
319
|
+
[["Step Temp", "0 1.0"], ["Step Temp", "100 1.1"]],
|
|
320
|
+
{"summaries": [
|
|
321
|
+
["Loop time of 0.01 on 4 procs (2 MPI x 2 OpenMP) for 100 "
|
|
322
|
+
"steps with 20 atoms"],
|
|
323
|
+
["Loop time of 0.5 on 1 procs for 100 steps with 20 atoms"]]},
|
|
324
|
+
r"Line 5 starts with 'Loop time of' but is not",
|
|
325
|
+
id="loop-line-of-another-form-in-first-run"),
|
|
326
|
+
pytest.param(
|
|
327
|
+
[["Step Temp Temp", "0 1.0 1.0"]], {},
|
|
328
|
+
r"Line 3 repeats the thermo keyword 'Temp'",
|
|
329
|
+
id="repeated-keyword"),
|
|
330
|
+
pytest.param(
|
|
331
|
+
[["Step Temp Temp", "0 1.0 1.0"], ["Step Press", "100 0.5"]], {},
|
|
332
|
+
r"Line 3 repeats the thermo keyword 'Temp'",
|
|
333
|
+
id="repeated-keyword-then-another-header"),
|
|
334
|
+
pytest.param(
|
|
335
|
+
[["---", "keywords: ['Step', 'Temp', 'Temp', ]", "data:",
|
|
336
|
+
" - [0, 1.0, 1.0, ]", "..."]], {},
|
|
337
|
+
r"Line 4 repeats the thermo keyword 'Temp'",
|
|
338
|
+
id="yaml-repeated-keyword"),
|
|
339
|
+
],
|
|
340
|
+
)
|
|
341
|
+
def test_parse_log_raises_broken_log_error(make_log, tables, options, match):
|
|
342
|
+
"""A cut-off or malformed log raises BrokenLogError naming the problem."""
|
|
343
|
+
text = make_log(*tables, **options)
|
|
344
|
+
|
|
345
|
+
with pytest.raises(BrokenLogError, match=match):
|
|
346
|
+
parse_log(text)
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
# Run summary -----------------------------------------------------------------
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def test_parse_log_reads_run_summary(make_log):
|
|
353
|
+
"""Every field of a full run summary lands in its run_stats column."""
|
|
354
|
+
summary = [
|
|
355
|
+
"Loop time of 2.5 on 4 procs for 1000 steps with 2000 atoms",
|
|
356
|
+
"",
|
|
357
|
+
"Performance: 172800.000 tau/day, 400.000 timesteps/s, "
|
|
358
|
+
"800.000 katom-step/s",
|
|
359
|
+
"100.0% CPU use with 4 MPI tasks x 1 OpenMP threads",
|
|
360
|
+
"",
|
|
361
|
+
"MPI task timing breakdown:",
|
|
362
|
+
"Section | min time | avg time | max time |%varavg| %total",
|
|
363
|
+
"---------------------------------------------------------------",
|
|
364
|
+
"Pair | 1.0 | 1.2 | 1.4 | 5.0 | 48.00",
|
|
365
|
+
"Other | | 0.05 | | | 2.00",
|
|
366
|
+
"",
|
|
367
|
+
"Dangerous builds = 3",
|
|
368
|
+
]
|
|
369
|
+
text = make_log(["Step", "0"], summaries=[summary])
|
|
370
|
+
|
|
371
|
+
log = parse_log(text)
|
|
372
|
+
|
|
373
|
+
assert log.run_stats.iloc[0].to_dict() == {
|
|
374
|
+
"run": 0, "loop_time": 2.5, "n_cores": 4, "loop_timesteps": 1000,
|
|
375
|
+
"n_atoms": 2000, "perf_per_day": 172800.0, "perf_unit": "tau/day",
|
|
376
|
+
"timesteps_per_s": 400.0, "cpu_use": 100.0, "mpi_task": 4,
|
|
377
|
+
"openmp_threads": 1, "Pair_avg_time": 1.2, "Pair_total_pct": 48.0,
|
|
378
|
+
"Other_avg_time": 0.05, "Other_total_pct": 2.0,
|
|
379
|
+
"dangerous_builds": 3,
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
@pytest.mark.parametrize(
|
|
384
|
+
("line", "expected"),
|
|
385
|
+
[
|
|
386
|
+
pytest.param(
|
|
387
|
+
"Performance: 172800.000 tau/day, 400.000 timesteps/s, "
|
|
388
|
+
"800.000 katom-step/s",
|
|
389
|
+
(172800.0, "tau/day", 400.0), id="lj"),
|
|
390
|
+
pytest.param(
|
|
391
|
+
"Performance: 172800.000 tau/day, 400.000 timesteps/s",
|
|
392
|
+
(172800.0, "tau/day", 400.0), id="lj-without-atom-steps"),
|
|
393
|
+
pytest.param(
|
|
394
|
+
"Performance: 0.512 ns/day, 46.875 hours/ns, 5.926 timesteps/s, "
|
|
395
|
+
"11.852 katom-step/s",
|
|
396
|
+
(0.512, "ns/day", 5.926), id="real"),
|
|
397
|
+
pytest.param(
|
|
398
|
+
"Performance: 3.200 fs/day, 7.500 hours/fs, 37.037 timesteps/s, "
|
|
399
|
+
"74.074 katom-step/s",
|
|
400
|
+
(3.2, "fs/day", 37.037), id="electron"),
|
|
401
|
+
],
|
|
402
|
+
)
|
|
403
|
+
def test_parse_log_reads_performance_in_any_unit(make_log, line, expected):
|
|
404
|
+
"""The performance line gives its rate, unit, and timesteps per second."""
|
|
405
|
+
summary = ["Loop time of 2.5 on 4 procs for 1000 steps with 2000 atoms",
|
|
406
|
+
"", line]
|
|
407
|
+
text = make_log(["Step", "0"], summaries=[summary])
|
|
408
|
+
|
|
409
|
+
log = parse_log(text)
|
|
410
|
+
|
|
411
|
+
row = log.run_stats.iloc[0]
|
|
412
|
+
assert (row["perf_per_day"], row["perf_unit"],
|
|
413
|
+
row["timesteps_per_s"]) == expected
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
@pytest.mark.parametrize(
|
|
417
|
+
("line", "expected"),
|
|
418
|
+
[
|
|
419
|
+
pytest.param("100.0% CPU use with 4 MPI tasks x 1 OpenMP threads",
|
|
420
|
+
(100.0, 4, 1), id="full-use"),
|
|
421
|
+
pytest.param("9.5% CPU use with 2 MPI tasks x no OpenMP threads",
|
|
422
|
+
(9.5, 2, 0), id="no-openmp"),
|
|
423
|
+
],
|
|
424
|
+
)
|
|
425
|
+
def test_parse_log_reads_cpu_line(make_log, line, expected):
|
|
426
|
+
"""The CPU line gives use, MPI tasks, and threads (0 without OpenMP)."""
|
|
427
|
+
summary = ["Loop time of 2.5 on 4 procs for 1000 steps with 2000 atoms",
|
|
428
|
+
"", line]
|
|
429
|
+
text = make_log(["Step", "0"], summaries=[summary])
|
|
430
|
+
|
|
431
|
+
log = parse_log(text)
|
|
432
|
+
|
|
433
|
+
row = log.run_stats.iloc[0]
|
|
434
|
+
assert (row["cpu_use"], row["mpi_task"],
|
|
435
|
+
row["openmp_threads"]) == expected
|
|
436
|
+
|
|
437
|
+
|
|
438
|
+
@pytest.mark.parametrize(
|
|
439
|
+
("header", "row"),
|
|
440
|
+
[
|
|
441
|
+
pytest.param(
|
|
442
|
+
"Section | min time | avg time | max time |%varavg| %total",
|
|
443
|
+
"Pair | 1.0 | 1.2 | 1.4 | 5.0 | 48.00",
|
|
444
|
+
id="without-cpu-column"),
|
|
445
|
+
pytest.param(
|
|
446
|
+
"Section | min time | avg time | max time |%varavg| %CPU "
|
|
447
|
+
"| %total",
|
|
448
|
+
"Pair | 1.0 | 1.2 | 1.4 | 5.0 | 99.0 "
|
|
449
|
+
"| 48.00",
|
|
450
|
+
id="with-cpu-column"),
|
|
451
|
+
],
|
|
452
|
+
)
|
|
453
|
+
def test_parse_log_finds_timing_columns_by_name(make_log, header, row):
|
|
454
|
+
"""Average time and percent of total are found whichever columns exist."""
|
|
455
|
+
summary = ["Loop time of 2.5 on 4 procs for 1000 steps with 2000 atoms",
|
|
456
|
+
"", "MPI task timing breakdown:", header, "-" * 63, row, ""]
|
|
457
|
+
text = make_log(["Step", "0"], summaries=[summary])
|
|
458
|
+
|
|
459
|
+
log = parse_log(text)
|
|
460
|
+
|
|
461
|
+
row_stats = log.run_stats.iloc[0]
|
|
462
|
+
assert (row_stats["Pair_avg_time"], row_stats["Pair_total_pct"]) == (
|
|
463
|
+
1.2, 48.0)
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def test_parse_log_skips_malformed_timing_rows(make_log):
|
|
467
|
+
"""Timing rows without a section or with too few cells are ignored."""
|
|
468
|
+
summary = [
|
|
469
|
+
"Loop time of 2.5 on 4 procs for 1000 steps with 2000 atoms", "",
|
|
470
|
+
"MPI task timing breakdown:",
|
|
471
|
+
"Section | min time | avg time | max time |%varavg| %total",
|
|
472
|
+
"-" * 63,
|
|
473
|
+
"Pair | 1.0 | 1.2",
|
|
474
|
+
" | 1.0 | 1.2 | 1.4 | 5.0 | 48.00",
|
|
475
|
+
"Neigh | 1.0 | n/a | 1.4 | 5.0 | 12.00",
|
|
476
|
+
"",
|
|
477
|
+
]
|
|
478
|
+
text = make_log(["Step", "0"], summaries=[summary])
|
|
479
|
+
|
|
480
|
+
log = parse_log(text)
|
|
481
|
+
|
|
482
|
+
row = log.run_stats.iloc[0]
|
|
483
|
+
timing = [col for col in log.run_stats.columns
|
|
484
|
+
if col.endswith(("_avg_time", "_total_pct"))]
|
|
485
|
+
assert timing == ["Neigh_avg_time", "Neigh_total_pct"]
|
|
486
|
+
assert np.isnan(row["Neigh_avg_time"])
|
|
487
|
+
assert row["Neigh_total_pct"] == 12.0
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def test_parse_log_missing_summary_fields_are_nan(make_log):
|
|
491
|
+
"""A run without a performance or dangerous-builds count gets NaN."""
|
|
492
|
+
minimize = ["Loop time of 0.1 on 1 procs for 5 steps with 20 atoms",
|
|
493
|
+
"Dangerous builds not checked"]
|
|
494
|
+
run = ["Loop time of 0.5 on 1 procs for 100 steps with 20 atoms",
|
|
495
|
+
"Performance: 172800.000 tau/day, 400.000 timesteps/s",
|
|
496
|
+
"Dangerous builds = 2"]
|
|
497
|
+
text = make_log(["Step", "0"], ["Step", "5"], summaries=[minimize, run])
|
|
498
|
+
|
|
499
|
+
log = parse_log(text)
|
|
500
|
+
|
|
501
|
+
np.testing.assert_array_equal(log.run_stats["perf_per_day"].to_numpy(),
|
|
502
|
+
[np.nan, 172800.0])
|
|
503
|
+
np.testing.assert_array_equal(
|
|
504
|
+
log.run_stats["dangerous_builds"].to_numpy(), [np.nan, 2.0])
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def test_parse_log_wall_time_in_seconds_and_hours(make_log):
|
|
508
|
+
"""1:02:03 is 3723 s; cores and atoms come from the last run."""
|
|
509
|
+
summaries = [["Loop time of 0.5 on 4 procs for 100 steps with 20 atoms"],
|
|
510
|
+
["Loop time of 0.5 on 8 procs for 100 steps with 19 atoms"]]
|
|
511
|
+
text = make_log(["Step", "0"], ["Step", "100"], summaries=summaries,
|
|
512
|
+
wall="Total wall time: 1:02:03")
|
|
513
|
+
|
|
514
|
+
log = parse_log(text)
|
|
515
|
+
|
|
516
|
+
row = log.wall_time.iloc[0]
|
|
517
|
+
assert row["wall_time_s"] == 3723
|
|
518
|
+
# 3723 / 3600 h; tolerance covers float64 round-off only.
|
|
519
|
+
assert row["wall_time_hr"] == pytest.approx(3723 / 3600, rel=1e-12)
|
|
520
|
+
assert (row["n_cores"], row["n_atoms"]) == (8, 19)
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
# Reading files ---------------------------------------------------------------
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
@pytest.mark.parametrize("as_type", [pytest.param(str, id="str"),
|
|
527
|
+
pytest.param(Path, id="path")])
|
|
528
|
+
def test_read_log_accepts_str_and_path(make_log, tmp_path, as_type):
|
|
529
|
+
"""read_log takes a str or a Path and stores the file as a Path."""
|
|
530
|
+
path = tmp_path / "log.lammps"
|
|
531
|
+
path.write_text(make_log(["Step", "0"]))
|
|
532
|
+
|
|
533
|
+
log = read_log(as_type(path))
|
|
534
|
+
|
|
535
|
+
assert log.path == path
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def test_read_log_reads_a_gzipped_log(make_log, tmp_path):
|
|
539
|
+
"""A log whose name ends in .gz is read through gzip."""
|
|
540
|
+
path = tmp_path / "log.lammps.gz"
|
|
541
|
+
with gzip.open(path, "wt") as stream:
|
|
542
|
+
stream.write(make_log(["Step Temp", "0 1.5", "100 0.5"]))
|
|
543
|
+
|
|
544
|
+
log = read_log(path)
|
|
545
|
+
|
|
546
|
+
assert log.thermo["Temp"].tolist() == [1.5, 0.5]
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
# LAMMPS-written fixtures (tier B) -------------------------------------------
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
@pytest.mark.parametrize(
|
|
553
|
+
("name", "expected"),
|
|
554
|
+
[
|
|
555
|
+
pytest.param("log.one", [2, 7, 7, 7, 7, 12], id="one"),
|
|
556
|
+
pytest.param("log.multi", [6, 6, 6, 6, 6], id="multi"),
|
|
557
|
+
pytest.param("log.custom", [6, 6, 3, 7], id="custom"),
|
|
558
|
+
pytest.param("log.yaml", [6, 6], id="yaml"),
|
|
559
|
+
],
|
|
560
|
+
)
|
|
561
|
+
def test_read_log_fixture_rows_per_run(log_dir, name, expected):
|
|
562
|
+
"""Each run of a LAMMPS-written log keeps all its thermo rows."""
|
|
563
|
+
log = read_log(log_dir / name)
|
|
564
|
+
|
|
565
|
+
assert log.thermo.groupby("run").size().tolist() == expected
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
@pytest.mark.parametrize(
|
|
569
|
+
("name", "expected"),
|
|
570
|
+
[
|
|
571
|
+
pytest.param("log.one", [0, 5, 505, 1005, 1505, 2005], id="one"),
|
|
572
|
+
pytest.param("log.multi", [0, 0, 500, 1000, 1500],
|
|
573
|
+
id="multi-reset-timestep"),
|
|
574
|
+
pytest.param("log.custom", [0, 500, 1000, 1250], id="custom"),
|
|
575
|
+
pytest.param("log.yaml", [0, 500], id="yaml"),
|
|
576
|
+
],
|
|
577
|
+
)
|
|
578
|
+
def test_read_log_fixture_first_step_per_run(log_dir, name, expected):
|
|
579
|
+
"""Each run starts at the step LAMMPS printed first for it."""
|
|
580
|
+
log = read_log(log_dir / name)
|
|
581
|
+
|
|
582
|
+
assert log.thermo.groupby("run")["Step"].first().tolist() == expected
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
@pytest.mark.parametrize(
|
|
586
|
+
("name", "expected"),
|
|
587
|
+
[
|
|
588
|
+
pytest.param(
|
|
589
|
+
"log.one",
|
|
590
|
+
["run", "Step", "Temp", "E_pair", "E_mol", "TotEng", "Press"],
|
|
591
|
+
id="one"),
|
|
592
|
+
pytest.param(
|
|
593
|
+
"log.multi",
|
|
594
|
+
["run", "Step", "CPU", "TotEng", "KinEng", "Temp", "PotEng",
|
|
595
|
+
"E_bond", "E_angle", "E_dihed", "E_impro", "E_vdwl", "E_coul",
|
|
596
|
+
"E_long", "Press"],
|
|
597
|
+
id="multi"),
|
|
598
|
+
pytest.param(
|
|
599
|
+
"log.custom",
|
|
600
|
+
["run", "Step", "KinEng", "PotEng", "E_tail", "Ecouple",
|
|
601
|
+
"Econserve", "E_vdwl", "E_bond", "c_rg", "c_rg[1]", "c_rg[3]",
|
|
602
|
+
"v_rg2"],
|
|
603
|
+
id="custom"),
|
|
604
|
+
pytest.param(
|
|
605
|
+
"log.yaml",
|
|
606
|
+
["run", "Step", "Temp", "KinEng", "PotEng", "E_bond", "E_angle",
|
|
607
|
+
"E_dihed", "E_impro", "E_vdwl", "E_coul", "E_long", "Press"],
|
|
608
|
+
id="yaml"),
|
|
609
|
+
],
|
|
610
|
+
)
|
|
611
|
+
def test_read_log_fixture_keywords(log_dir, name, expected):
|
|
612
|
+
"""The thermo columns are the keywords LAMMPS printed, in order."""
|
|
613
|
+
log = read_log(log_dir / name)
|
|
614
|
+
|
|
615
|
+
assert list(log.thermo.columns) == expected
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
@pytest.mark.parametrize(
|
|
619
|
+
("name", "steps", "atoms"),
|
|
620
|
+
[
|
|
621
|
+
pytest.param("log.one", [5, 500, 500, 500, 500, 1000],
|
|
622
|
+
[170, 170, 170, 170, 170, 123], id="one"),
|
|
623
|
+
pytest.param("log.multi", [500] * 5, [320] * 5, id="multi"),
|
|
624
|
+
pytest.param("log.custom", [500, 500, 250, 500], [320] * 4,
|
|
625
|
+
id="custom-fix-halt"),
|
|
626
|
+
pytest.param("log.yaml", [500, 500], [320, 320], id="yaml"),
|
|
627
|
+
],
|
|
628
|
+
)
|
|
629
|
+
def test_read_log_fixture_loop_lines(log_dir, name, steps, atoms):
|
|
630
|
+
"""Each run's steps and atoms come from its Loop time line."""
|
|
631
|
+
log = read_log(log_dir / name)
|
|
632
|
+
|
|
633
|
+
assert log.run_stats["loop_timesteps"].tolist() == steps
|
|
634
|
+
assert log.run_stats["n_atoms"].tolist() == atoms
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def test_read_log_fixture_first_row_values(log_dir):
|
|
638
|
+
"""The first row of log.one equals the numbers LAMMPS printed."""
|
|
639
|
+
log = read_log(log_dir / "log.one")
|
|
640
|
+
|
|
641
|
+
assert log.thermo.iloc[0].tolist() == [
|
|
642
|
+
0, 0, 0.0, 0.020390867, 2.2622954, 2.2826862, -0.0049223331]
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def test_read_log_fixture_unchecked_builds_are_nan(log_dir):
|
|
646
|
+
"""The run with neighbor checks off has no dangerous-builds count."""
|
|
647
|
+
log = read_log(log_dir / "log.custom")
|
|
648
|
+
|
|
649
|
+
np.testing.assert_array_equal(
|
|
650
|
+
log.run_stats["dangerous_builds"].to_numpy(), [0, np.nan, 0, 0])
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
@pytest.mark.parametrize(
|
|
654
|
+
("name", "match"),
|
|
655
|
+
[
|
|
656
|
+
pytest.param("log.error",
|
|
657
|
+
r"Line \d+ reports an error: ERROR: Lost atoms",
|
|
658
|
+
id="error"),
|
|
659
|
+
pytest.param("log.killed",
|
|
660
|
+
r"Run 0 \(line \d+\) has no 'Loop time of' line",
|
|
661
|
+
id="killed"),
|
|
662
|
+
],
|
|
663
|
+
)
|
|
664
|
+
def test_read_log_fixture_broken_log_raises(log_dir, name, match):
|
|
665
|
+
"""A LAMMPS run that stopped with an error or was killed raises."""
|
|
666
|
+
with pytest.raises(BrokenLogError, match=match):
|
|
667
|
+
read_log(log_dir / name)
|