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,1495 @@
|
|
|
1
|
+
"""Tests for :mod:`polyphys.manage.organizer`.
|
|
2
|
+
|
|
3
|
+
Most inputs come from the ``make_lineage_tree`` factory in
|
|
4
|
+
``polyphys/tests/conftest.py``: tiny ``.npy`` files named for the
|
|
5
|
+
``ChainCyl`` parser, under a directory whose name contains hyphens, as
|
|
6
|
+
every lineage directory name does. File ``(e, w, s)`` (ensemble, whole,
|
|
7
|
+
segment, each from 1; ``s = 0`` for a whole file) holds
|
|
8
|
+
``1000 * e + 100 * w + 10 * s + k`` in row ``k``, so every expected sum,
|
|
9
|
+
concatenation, and replicate statistic below is written down by hand from
|
|
10
|
+
that rule. Expected lineage names come from the parser applied to the
|
|
11
|
+
written filename, never from the strings the tree builds.
|
|
12
|
+
|
|
13
|
+
The space tests build a small ``analysis`` phase on disk; their expected
|
|
14
|
+
``t_index`` and ``time`` values come from the ``dt`` field and the dump
|
|
15
|
+
period of the group's trajectory (``adump`` for ``all``, ``bdump`` for
|
|
16
|
+
``bug``) encoded in the filenames. The stamp tests use the column layout of the
|
|
17
|
+
SciOps probe phase's stamp files: genealogy columns, parser attributes, and
|
|
18
|
+
``n_frames``.
|
|
19
|
+
"""
|
|
20
|
+
import logging
|
|
21
|
+
import os
|
|
22
|
+
import re
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import get_args
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
import pandas as pd
|
|
28
|
+
import pytest
|
|
29
|
+
|
|
30
|
+
from polyphys.manage.organizer import (
|
|
31
|
+
BULK_NAME_MAP,
|
|
32
|
+
children_stamps,
|
|
33
|
+
create_fullname,
|
|
34
|
+
ens_avg_from_bin_edge,
|
|
35
|
+
ens_avg_from_df,
|
|
36
|
+
ens_avg_from_ndarray,
|
|
37
|
+
ens_from_bin_edge,
|
|
38
|
+
ens_from_df,
|
|
39
|
+
ens_from_mat_t,
|
|
40
|
+
ens_from_vec,
|
|
41
|
+
ensemble,
|
|
42
|
+
ensemble_avg,
|
|
43
|
+
make_database,
|
|
44
|
+
parents_stamps,
|
|
45
|
+
rename_bulk_columns,
|
|
46
|
+
save_artifact,
|
|
47
|
+
space_hists,
|
|
48
|
+
space_tseries,
|
|
49
|
+
whole_from_file,
|
|
50
|
+
whole_from_segments,
|
|
51
|
+
)
|
|
52
|
+
from polyphys.tests.synthetic import ChainCyl
|
|
53
|
+
from polyphys.manage.types import PhaseT, StageT
|
|
54
|
+
from polyphys.manage.utils import round_to_multiple
|
|
55
|
+
|
|
56
|
+
# A Pandas4Warning marks a deprecated pandas call; fail on it.
|
|
57
|
+
pytestmark = pytest.mark.filterwarnings("error::pandas.errors.Pandas4Warning")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# Fixtures: lineage trees and the names the parser gives their files
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.fixture
|
|
64
|
+
def segment_tree(make_lineage_tree):
|
|
65
|
+
"""Return a segment tree: 2 ensembles x 2 wholes x 2 segments x 3 rows."""
|
|
66
|
+
return make_lineage_tree(ChainCyl, group="bug", n_rows=3)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
@pytest.fixture
|
|
70
|
+
def whole_tree(make_lineage_tree):
|
|
71
|
+
"""Return a whole tree: 2 ensembles x 2 wholes x 3 rows."""
|
|
72
|
+
return make_lineage_tree(
|
|
73
|
+
ChainCyl, group="bug", lineage="whole", n_rows=3
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _parsed_names(tree, attr):
|
|
78
|
+
"""Map each tree file's (ensemble, whole) indices to a parsed name."""
|
|
79
|
+
return {
|
|
80
|
+
index[:2]: getattr(
|
|
81
|
+
ChainCyl(str(path), tree.lineage, tree.group, ispath=True),
|
|
82
|
+
attr,
|
|
83
|
+
)
|
|
84
|
+
for path, index in tree.index.items()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.fixture
|
|
89
|
+
def segment_whole_names(segment_tree):
|
|
90
|
+
"""Return the parsed whole name of each (ensemble, whole) of the tree."""
|
|
91
|
+
return _parsed_names(segment_tree, "whole")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.fixture
|
|
95
|
+
def whole_names(whole_tree):
|
|
96
|
+
"""Return the parsed whole name of each (ensemble, whole) in whole_tree."""
|
|
97
|
+
return _parsed_names(whole_tree, "whole")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.fixture
|
|
101
|
+
def ensemble_names(whole_tree):
|
|
102
|
+
"""Return the parsed ensemble_long name of each ensemble in whole_tree."""
|
|
103
|
+
return {
|
|
104
|
+
e: name for (e, _), name in
|
|
105
|
+
_parsed_names(whole_tree, "ensemble_long").items()
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@pytest.fixture
|
|
110
|
+
def write_segments(tmp_path):
|
|
111
|
+
"""Factory: write arrays as segments j01, j02, ... of one whole."""
|
|
112
|
+
stem = (
|
|
113
|
+
"N20r5.5lz25.0ac1.0nc100dt0.005bdump1000adump5000ens1"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def _write(arrays):
|
|
117
|
+
directory = tmp_path / "ChainCyl-bug-segment"
|
|
118
|
+
directory.mkdir(exist_ok=True)
|
|
119
|
+
paths = []
|
|
120
|
+
for seg_id, array in enumerate(arrays, start=1):
|
|
121
|
+
path = directory / f"{stem}.j{seg_id:02d}.bug.binEdge.npy"
|
|
122
|
+
np.save(path, np.asarray(array))
|
|
123
|
+
paths.append(str(path))
|
|
124
|
+
return paths
|
|
125
|
+
|
|
126
|
+
return _write
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
# Naming and persistence
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_create_fullname_joins_with_hyphens():
|
|
133
|
+
"""A full name is the name, group, and property joined by hyphens."""
|
|
134
|
+
assert create_fullname("w", "bug", "rhoMon") == "w-bug-rhoMon"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_save_artifact_writes_ndarray_to_npy(tmp_path):
|
|
138
|
+
"""An array is saved as <filename>.npy and loads back unchanged."""
|
|
139
|
+
data = np.arange(3.0)
|
|
140
|
+
save_artifact("w-bug-rhoMon", data, tmp_path)
|
|
141
|
+
np.testing.assert_array_equal(np.load(tmp_path / "w-bug-rhoMon.npy"), data)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_save_artifact_writes_dataframe_to_csv_without_index(tmp_path):
|
|
145
|
+
"""A DataFrame is saved as <filename>.csv without its index."""
|
|
146
|
+
data = pd.DataFrame({"a": [1.0, 2.0], "b": [3.0, 4.0]}, index=[5, 6])
|
|
147
|
+
save_artifact("w-bug-rhoMon", data, tmp_path)
|
|
148
|
+
pd.testing.assert_frame_equal(
|
|
149
|
+
pd.read_csv(tmp_path / "w-bug-rhoMon.csv"),
|
|
150
|
+
data.reset_index(drop=True),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_save_artifact_writes_dict_entries_by_measure(tmp_path):
|
|
155
|
+
"""Each '<prop>-<measure>' entry is saved as <filename>-<measure>.npy."""
|
|
156
|
+
data = {"rhoMon-mean": np.array([1.0]), "rhoMon-var": np.array([2.0])}
|
|
157
|
+
save_artifact("w-bug-rhoMon", data, tmp_path)
|
|
158
|
+
assert sorted(os.listdir(tmp_path)) == [
|
|
159
|
+
"w-bug-rhoMon-mean.npy", "w-bug-rhoMon-var.npy"
|
|
160
|
+
]
|
|
161
|
+
np.testing.assert_array_equal(
|
|
162
|
+
np.load(tmp_path / "w-bug-rhoMon-var.npy"), [2.0]
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_save_artifact_rejects_unsupported_type(tmp_path):
|
|
167
|
+
"""Data that is not an array, a DataFrame, or a dict raises TypeError."""
|
|
168
|
+
with pytest.raises(TypeError, match="Unsupported data type list"):
|
|
169
|
+
save_artifact("w-bug-rhoMon", [1.0, 2.0], tmp_path)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@pytest.mark.parametrize(
|
|
173
|
+
"group, directory",
|
|
174
|
+
[
|
|
175
|
+
pytest.param("bug", "N20-bug-ens", id="with-group"),
|
|
176
|
+
pytest.param(None, "N20-ens", id="without-group"),
|
|
177
|
+
],
|
|
178
|
+
)
|
|
179
|
+
def test_make_database_creates_sibling_phase_directory(
|
|
180
|
+
tmp_path, group, directory
|
|
181
|
+
):
|
|
182
|
+
"""The new directory is <phase>/<prefix>-<group>-<stage> beside the old."""
|
|
183
|
+
old = tmp_path / "probes" / "N20-bug-segment"
|
|
184
|
+
new = make_database(old, "analysis", "ens", group)
|
|
185
|
+
assert new == str(tmp_path / "analysis" / directory) + "/"
|
|
186
|
+
assert Path(new).is_dir()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def test_make_database_returns_existing_directory(tmp_path):
|
|
190
|
+
"""Asking twice for the same directory returns the same path."""
|
|
191
|
+
old = tmp_path / "probes" / "N20-bug-segment"
|
|
192
|
+
first = make_database(old, "analysis", "ens", "bug")
|
|
193
|
+
assert make_database(old, "analysis", "ens", "bug") == first
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_make_database_logs_reuse_of_existing_directory(
|
|
197
|
+
tmp_path, caplog, capsys
|
|
198
|
+
):
|
|
199
|
+
"""Reusing a directory logs an INFO record naming it and prints nothing."""
|
|
200
|
+
old = tmp_path / "probes" / "N20-bug-segment"
|
|
201
|
+
make_database(old, "analysis", "ens", "bug")
|
|
202
|
+
caplog.set_level(logging.INFO, logger="polyphys.manage.organizer")
|
|
203
|
+
|
|
204
|
+
make_database(old, "analysis", "ens", "bug")
|
|
205
|
+
|
|
206
|
+
assert [(record.levelname, record.getMessage()) for record in
|
|
207
|
+
caplog.records] == [
|
|
208
|
+
("INFO", f"Reusing the existing directory "
|
|
209
|
+
f"{tmp_path / 'analysis' / 'N20-bug-ens'}.")]
|
|
210
|
+
assert capsys.readouterr().out == ""
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def test_make_database_rejects_unknown_phase(tmp_path):
|
|
214
|
+
"""A phase outside the phase vocabulary raises ValueError."""
|
|
215
|
+
with pytest.raises(ValueError, match="'nope' is an invalid option"):
|
|
216
|
+
make_database(tmp_path / "probes" / "N20-bug-segment", "nope", "ens",
|
|
217
|
+
"bug")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_phase_vocabulary_is_the_settled_set():
|
|
221
|
+
"""PhaseT declares the nine phase names of AGENTS.md in data-flow order."""
|
|
222
|
+
assert get_args(PhaseT) == (
|
|
223
|
+
"simsAll", "simsCont", "logs", "trjs", "probes", "analysis", "viz",
|
|
224
|
+
"galaxy", "allInOne",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
@pytest.mark.parametrize("phase", get_args(PhaseT))
|
|
229
|
+
def test_make_database_accepts_every_declared_phase(tmp_path, phase):
|
|
230
|
+
"""Every phase that PhaseT declares is accepted and names the directory."""
|
|
231
|
+
new = make_database(tmp_path / "probes" / "N20-bug-segment", phase,
|
|
232
|
+
"ens", "bug")
|
|
233
|
+
assert Path(new).parent.name == phase
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def test_make_database_rejects_singular_phase_name(tmp_path):
|
|
237
|
+
"""The singular 'simAll' of the old PhaseT is no longer accepted."""
|
|
238
|
+
with pytest.raises(ValueError, match="'simAll' is an invalid option"):
|
|
239
|
+
make_database(tmp_path / "probes" / "N20-bug-segment", "simAll",
|
|
240
|
+
"ens", "bug")
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def test_make_database_rejects_unknown_stage(tmp_path):
|
|
244
|
+
"""A stage outside StageT raises ValueError and names no directory."""
|
|
245
|
+
with pytest.raises(ValueError, match="'ensemble' is an invalid option"):
|
|
246
|
+
make_database(tmp_path / "probes" / "N20-bug-segment", "analysis",
|
|
247
|
+
"ensemble", "bug")
|
|
248
|
+
assert get_args(StageT) == ("segment", "wholeSim", "ens", "ensAvg",
|
|
249
|
+
"space", "galaxy")
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def test_make_database_rejects_group_with_hyphen(tmp_path):
|
|
253
|
+
"""A group holding the field separator '-' raises ValueError before any
|
|
254
|
+
directory is made, since it would shift every hyphen-split field."""
|
|
255
|
+
old = tmp_path / "probes" / "N20-bug-segment"
|
|
256
|
+
|
|
257
|
+
with pytest.raises(ValueError, match="'bug-x'.*'-'"):
|
|
258
|
+
make_database(old, "analysis", "ens", "bug-x")
|
|
259
|
+
|
|
260
|
+
assert not (tmp_path / "analysis").exists()
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_make_database_rejects_old_directory_outside_a_phase(tmp_path):
|
|
264
|
+
"""An old directory whose parent is not a phase directory raises
|
|
265
|
+
ValueError before any directory is made, instead of placing the new one
|
|
266
|
+
at the wrong depth."""
|
|
267
|
+
old = tmp_path / "root" / "N20-bug-segment"
|
|
268
|
+
|
|
269
|
+
with pytest.raises(ValueError, match="'root' is not a phase"):
|
|
270
|
+
make_database(old, "analysis", "ens", "bug")
|
|
271
|
+
|
|
272
|
+
assert not (tmp_path / "analysis").exists()
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# Building wholes
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def test_whole_from_segments_tseries_concatenates_in_segment_order(
|
|
279
|
+
segment_tree, segment_whole_names
|
|
280
|
+
):
|
|
281
|
+
"""A tseries whole is its segments' rows, j01 first, then j02."""
|
|
282
|
+
paths = [str(p) for p in segment_tree.paths]
|
|
283
|
+
wholes = whole_from_segments("gyrTMon", paths, ChainCyl, "bug",
|
|
284
|
+
"tseries")
|
|
285
|
+
np.testing.assert_array_equal(
|
|
286
|
+
wholes[segment_whole_names[(1, 2)]],
|
|
287
|
+
[1210.0, 1211.0, 1212.0, 1220.0, 1221.0, 1222.0],
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_whole_from_segments_histogram_sums_segments(
|
|
292
|
+
segment_tree, segment_whole_names
|
|
293
|
+
):
|
|
294
|
+
"""A histogram whole is the element-wise sum of its segments."""
|
|
295
|
+
paths = [str(p) for p in segment_tree.paths]
|
|
296
|
+
wholes = whole_from_segments("gyrTMon", paths, ChainCyl, "bug",
|
|
297
|
+
"histogram")
|
|
298
|
+
# (1210 + 1220, 1211 + 1221, 1212 + 1222)
|
|
299
|
+
np.testing.assert_array_equal(
|
|
300
|
+
wholes[segment_whole_names[(1, 2)]], [2430.0, 2432.0, 2434.0]
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def test_whole_from_segments_keys_wholes_by_parsed_name(
|
|
305
|
+
segment_tree, segment_whole_names
|
|
306
|
+
):
|
|
307
|
+
"""There is one whole per parsed whole name, whatever the directory."""
|
|
308
|
+
paths = [str(p) for p in segment_tree.paths]
|
|
309
|
+
wholes = whole_from_segments("gyrTMon", paths, ChainCyl, "bug",
|
|
310
|
+
"tseries")
|
|
311
|
+
assert "-" in segment_tree.root.name
|
|
312
|
+
assert list(wholes) == list(segment_whole_names.values())
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def test_whole_from_segments_bin_edge_keeps_shared_edges(write_segments):
|
|
316
|
+
"""Identical segment edges reduce to one copy of them."""
|
|
317
|
+
edges = np.array([0.0, 1.0, 3.0])
|
|
318
|
+
paths = write_segments([edges, edges])
|
|
319
|
+
wholes = whole_from_segments("binEdge", paths, ChainCyl, "bug",
|
|
320
|
+
"bin_edge")
|
|
321
|
+
(whole,) = wholes.values()
|
|
322
|
+
np.testing.assert_array_equal(whole, edges)
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def test_whole_from_segments_bin_edge_rejects_differing_edges(write_segments):
|
|
326
|
+
"""Segments of one whole with different edges raise ValueError."""
|
|
327
|
+
paths = write_segments([[0.0, 1.0, 3.0], [0.0, 1.0, 4.0]])
|
|
328
|
+
with pytest.raises(ValueError, match="Bin edges are not identical"):
|
|
329
|
+
whole_from_segments("binEdge", paths, ChainCyl, "bug", "bin_edge")
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def test_whole_from_segments_saves_each_whole(
|
|
333
|
+
tmp_path, segment_tree, segment_whole_names
|
|
334
|
+
):
|
|
335
|
+
"""save_to writes each whole to <whole>-<group>-<prop>.npy."""
|
|
336
|
+
out = tmp_path / "out"
|
|
337
|
+
out.mkdir()
|
|
338
|
+
paths = [str(p) for p in segment_tree.paths]
|
|
339
|
+
wholes = whole_from_segments("gyrTMon", paths, ChainCyl, "bug",
|
|
340
|
+
"tseries", save_to=out)
|
|
341
|
+
name = segment_whole_names[(2, 1)]
|
|
342
|
+
np.testing.assert_array_equal(
|
|
343
|
+
np.load(out / f"{name}-bug-gyrTMon.npy"), wholes[name]
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
@pytest.mark.parametrize("relation", ["histogram", "tseries", "bin_edge"])
|
|
348
|
+
def test_whole_from_segments_rejects_descending_segment_ids(
|
|
349
|
+
segment_tree, relation
|
|
350
|
+
):
|
|
351
|
+
"""Segments of a whole given in descending segment_id order raise."""
|
|
352
|
+
paths = [str(p) for p in segment_tree.paths[1::-1]]
|
|
353
|
+
with pytest.raises(ValueError, match=r"segment_id .*\[2, 1\]"):
|
|
354
|
+
whole_from_segments("gyrTMon", paths, ChainCyl, "bug", relation)
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@pytest.mark.parametrize("relation", ["histogram", "tseries", "bin_edge"])
|
|
358
|
+
def test_whole_from_segments_rejects_duplicate_segment(segment_tree, relation):
|
|
359
|
+
"""A segment passed twice raises instead of being counted twice."""
|
|
360
|
+
first = str(segment_tree.paths[0])
|
|
361
|
+
with pytest.raises(ValueError, match=r"segment_id .*\[1, 1\]"):
|
|
362
|
+
whole_from_segments(
|
|
363
|
+
"gyrTMon", [first, first], ChainCyl, "bug", relation
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
@pytest.mark.parametrize("relation", ["histogram", "tseries", "bin_edge"])
|
|
368
|
+
def test_whole_from_segments_rejects_a_gap_in_segment_ids(tmp_path, relation):
|
|
369
|
+
"""Segments j01 and j03 of one whole raise instead of being joined as if
|
|
370
|
+
adjacent, which would shift the time of every later frame."""
|
|
371
|
+
whole = "N20r5.5lz25.0ac1.0nc100dt0.005bdump1000adump5000ens1"
|
|
372
|
+
paths = []
|
|
373
|
+
for seg_id in (1, 3):
|
|
374
|
+
path = tmp_path / f"{whole}.j{seg_id:02d}.bug.gyrTMon.npy"
|
|
375
|
+
np.save(path, np.arange(3.0))
|
|
376
|
+
paths.append(str(path))
|
|
377
|
+
|
|
378
|
+
with pytest.raises(ValueError, match=re.escape(
|
|
379
|
+
f"The segments of whole '{whole}' skip a segment_id: [1, 3]")):
|
|
380
|
+
whole_from_segments("gyrTMon", paths, ChainCyl, "bug", relation)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def test_whole_from_segments_accepts_interleaved_wholes(segment_tree):
|
|
384
|
+
"""Wholes interleaved segment by segment give the same wholes."""
|
|
385
|
+
ordered = [str(p) for p in segment_tree.paths]
|
|
386
|
+
# All j01 files first, then all j02 files; each whole stays in order.
|
|
387
|
+
interleaved = ordered[0::2] + ordered[1::2]
|
|
388
|
+
expected = whole_from_segments("gyrTMon", ordered, ChainCyl, "bug",
|
|
389
|
+
"tseries")
|
|
390
|
+
wholes = whole_from_segments("gyrTMon", interleaved, ChainCyl, "bug",
|
|
391
|
+
"tseries")
|
|
392
|
+
assert list(wholes) == list(expected)
|
|
393
|
+
np.testing.assert_array_equal(
|
|
394
|
+
np.stack(list(wholes.values())), np.stack(list(expected.values()))
|
|
395
|
+
)
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def test_whole_from_segments_rejects_unknown_relation(segment_tree):
|
|
399
|
+
"""A relation other than histogram, tseries, or bin_edge raises."""
|
|
400
|
+
paths = [str(p) for p in segment_tree.paths]
|
|
401
|
+
with pytest.raises(ValueError, match="'nope' is an invalid option"):
|
|
402
|
+
whole_from_segments("gyrTMon", paths, ChainCyl, "bug", "nope")
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def test_whole_from_file_loads_each_whole_by_parsed_name(
|
|
406
|
+
whole_tree, whole_names
|
|
407
|
+
):
|
|
408
|
+
"""Each whole file loads under its parsed whole name."""
|
|
409
|
+
wholes = whole_from_file(whole_tree.paths, ChainCyl, "bug")
|
|
410
|
+
assert "-" in whole_tree.root.name
|
|
411
|
+
assert list(wholes) == list(whole_names.values())
|
|
412
|
+
np.testing.assert_array_equal(
|
|
413
|
+
wholes[whole_names[(2, 1)]], [2100.0, 2101.0, 2102.0]
|
|
414
|
+
)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
def test_whole_from_file_accepts_grouped_paths(whole_tree):
|
|
418
|
+
"""1-tuples of paths, as sort_filenames gives, load like bare paths."""
|
|
419
|
+
grouped = list(zip(map(str, whole_tree.paths)))
|
|
420
|
+
bare = whole_from_file(whole_tree.paths, ChainCyl, "bug")
|
|
421
|
+
wholes = whole_from_file(grouped, ChainCyl, "bug")
|
|
422
|
+
assert list(wholes) == list(bare)
|
|
423
|
+
np.testing.assert_array_equal(
|
|
424
|
+
np.stack(list(wholes.values())), np.stack(list(bare.values()))
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def test_whole_from_file_rejects_two_files_of_one_whole(tmp_path):
|
|
429
|
+
"""Two files that parse to one whole name raise, naming both, instead of
|
|
430
|
+
the second silently replacing the first."""
|
|
431
|
+
whole = "N20r5.5lz25.0ac1.0nc100dt0.005bdump1000adump5000ens1"
|
|
432
|
+
paths = []
|
|
433
|
+
for folder in ("a", "b"):
|
|
434
|
+
(tmp_path / folder).mkdir()
|
|
435
|
+
path = tmp_path / folder / f"{whole}-bug-gyrTMon.npy"
|
|
436
|
+
np.save(path, np.arange(3.0))
|
|
437
|
+
paths.append(path)
|
|
438
|
+
|
|
439
|
+
with pytest.raises(ValueError, match=re.escape(
|
|
440
|
+
f"'{paths[0]}' and '{paths[1]}' are both whole '{whole}'")):
|
|
441
|
+
whole_from_file(paths, ChainCyl, "bug")
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def test_whole_from_file_rejects_unsupported_path_item():
|
|
445
|
+
"""An item that is neither a path nor a sequence of one raises."""
|
|
446
|
+
with pytest.raises(TypeError, match="Unsupported path item"):
|
|
447
|
+
whole_from_file([42], ChainCyl, "bug")
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
# Grouping wholes into ensembles
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
def test_ens_from_vec_puts_one_whole_per_column():
|
|
454
|
+
"""Each whole becomes a column, in the order of the dictionary."""
|
|
455
|
+
name, ens_df = ens_from_vec(
|
|
456
|
+
("ens1", {"w2": np.array([1.0, 2.0]), "w1": np.array([3.0, 4.0])})
|
|
457
|
+
)
|
|
458
|
+
assert name == "ens1"
|
|
459
|
+
pd.testing.assert_frame_equal(
|
|
460
|
+
ens_df, pd.DataFrame({"w2": [1.0, 2.0], "w1": [3.0, 4.0]})
|
|
461
|
+
)
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def test_ens_from_vec_rejects_wholes_of_unequal_length():
|
|
465
|
+
"""Wholes of 3 and 2 values raise, naming the ensemble and each length."""
|
|
466
|
+
with pytest.raises(ValueError, match=re.escape(
|
|
467
|
+
"The wholes of ensemble 'ens1' have unequal lengths: "
|
|
468
|
+
"w1 3, w2 2.")):
|
|
469
|
+
ens_from_vec(("ens1", {"w1": np.arange(3.0), "w2": np.arange(2.0)}))
|
|
470
|
+
|
|
471
|
+
|
|
472
|
+
def test_ens_from_mat_t_stacks_wholes_along_axis_0():
|
|
473
|
+
"""Matrices stack into (n_wholes, n_rows, n_cols) in dictionary order."""
|
|
474
|
+
a = np.array([[1.0, 2.0], [3.0, 4.0]])
|
|
475
|
+
b = np.array([[5.0, 6.0], [7.0, 8.0]])
|
|
476
|
+
name, stacked = ens_from_mat_t(("ens1", {"w1": a, "w2": b}))
|
|
477
|
+
assert name == "ens1"
|
|
478
|
+
np.testing.assert_array_equal(stacked, np.stack([a, b]))
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def test_ens_from_df_averages_wholes_row_by_row():
|
|
482
|
+
"""Whole DataFrames average element-wise into one: (a + b) / 2."""
|
|
483
|
+
a = pd.DataFrame({"x": [1.0, 2.0], "y": [3.0, 4.0]})
|
|
484
|
+
b = pd.DataFrame({"x": [3.0, 6.0], "y": [5.0, 8.0]})
|
|
485
|
+
name, ens_df = ens_from_df(("ens1", {"w1": a, "w2": b}))
|
|
486
|
+
assert name == "ens1"
|
|
487
|
+
pd.testing.assert_frame_equal(ens_df, (a + b) / 2)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def test_ens_from_df_rejects_wholes_of_unequal_length():
|
|
491
|
+
"""A 3-row and a 2-row whole raise instead of averaging the last row over
|
|
492
|
+
one replicate."""
|
|
493
|
+
a = pd.DataFrame({"x": [1.0, 2.0, 3.0]})
|
|
494
|
+
b = pd.DataFrame({"x": [3.0, 6.0]})
|
|
495
|
+
|
|
496
|
+
with pytest.raises(ValueError, match=re.escape(
|
|
497
|
+
"The wholes of ensemble 'ens1' have unequal lengths: "
|
|
498
|
+
"w1 3, w2 2.")):
|
|
499
|
+
ens_from_df(("ens1", {"w1": a, "w2": b}))
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def test_ens_from_bin_edge_returns_shared_edges():
|
|
503
|
+
"""Identical whole edges pass through as a copy of the shared array."""
|
|
504
|
+
edges = np.array([0.0, 1.0, 2.0])
|
|
505
|
+
name, out = ens_from_bin_edge(("ens1", {"w1": edges, "w2": edges.copy()}))
|
|
506
|
+
assert name == "ens1"
|
|
507
|
+
np.testing.assert_array_equal(out, edges)
|
|
508
|
+
assert not np.shares_memory(out, edges)
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
@pytest.mark.parametrize(
|
|
512
|
+
"other",
|
|
513
|
+
[
|
|
514
|
+
pytest.param(np.array([0.0, 1.0, 3.0]), id="different-values"),
|
|
515
|
+
pytest.param(np.array([0.0, 1.0, 2.0, 3.0]), id="different-lengths"),
|
|
516
|
+
],
|
|
517
|
+
)
|
|
518
|
+
def test_ens_from_bin_edge_raises_on_mismatch(other):
|
|
519
|
+
"""Differing whole edges raise ValueError instead of being merged."""
|
|
520
|
+
with pytest.raises(ValueError, match="Bin edges are not identical"):
|
|
521
|
+
ens_from_bin_edge(
|
|
522
|
+
("ens1", {"w1": np.array([0.0, 1.0, 2.0]), "w2": other})
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
def test_ensemble_groups_wholes_by_state_point(
|
|
527
|
+
whole_tree, whole_names, ensemble_names
|
|
528
|
+
):
|
|
529
|
+
"""Wholes of two state points form two ensembles keyed by ensemble_long."""
|
|
530
|
+
wholes = whole_from_file(whole_tree.paths, ChainCyl, "bug")
|
|
531
|
+
ens = ensemble("gyrTMon", wholes, ChainCyl, "bug", "vector")
|
|
532
|
+
assert list(ens) == [ensemble_names[1], ensemble_names[2]]
|
|
533
|
+
assert list(ens[ensemble_names[2]].columns) == [
|
|
534
|
+
whole_names[(2, 1)], whole_names[(2, 2)]
|
|
535
|
+
]
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def test_ensemble_takes_bin_center_from_each_ensemble_edges(
|
|
539
|
+
whole_tree, whole_names, ensemble_names
|
|
540
|
+
):
|
|
541
|
+
"""Each ensemble's bin_center comes from its own wholes' edges."""
|
|
542
|
+
wholes = whole_from_file(whole_tree.paths, ChainCyl, "bug")
|
|
543
|
+
edges_1 = np.array([0.0, 1.0, 3.0, 6.0])
|
|
544
|
+
edges_2 = np.array([0.0, 2.0, 4.0, 8.0])
|
|
545
|
+
whole_edges = {
|
|
546
|
+
whole_names[(1, 1)]: edges_1,
|
|
547
|
+
whole_names[(1, 2)]: edges_1,
|
|
548
|
+
whole_names[(2, 1)]: edges_2,
|
|
549
|
+
whole_names[(2, 2)]: edges_2,
|
|
550
|
+
}
|
|
551
|
+
ens = ensemble("gyrTMon", wholes, ChainCyl, "bug", "vector",
|
|
552
|
+
whole_edges=whole_edges)
|
|
553
|
+
# Analytic midpoints of each ensemble's edges
|
|
554
|
+
np.testing.assert_allclose(
|
|
555
|
+
ens[ensemble_names[1]]["bin_center"], [0.5, 2.0, 4.5]
|
|
556
|
+
)
|
|
557
|
+
np.testing.assert_allclose(
|
|
558
|
+
ens[ensemble_names[2]]["bin_center"], [1.0, 3.0, 6.0]
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
|
|
562
|
+
@pytest.mark.parametrize(
|
|
563
|
+
"whole_type, first, second, expected",
|
|
564
|
+
[
|
|
565
|
+
pytest.param(
|
|
566
|
+
"matrix",
|
|
567
|
+
np.array([[1.0, 2.0]]), np.array([[3.0, 4.0]]),
|
|
568
|
+
np.array([[[1.0, 2.0]], [[3.0, 4.0]]]),
|
|
569
|
+
id="matrix-stacks",
|
|
570
|
+
),
|
|
571
|
+
pytest.param(
|
|
572
|
+
"dataframe",
|
|
573
|
+
pd.DataFrame({"x": [1.0, 2.0]}), pd.DataFrame({"x": [3.0, 6.0]}),
|
|
574
|
+
np.array([[2.0], [4.0]]),
|
|
575
|
+
id="dataframe-averages",
|
|
576
|
+
),
|
|
577
|
+
pytest.param(
|
|
578
|
+
"bin_edge",
|
|
579
|
+
np.array([0.0, 1.0]), np.array([0.0, 1.0]),
|
|
580
|
+
np.array([0.0, 1.0]),
|
|
581
|
+
id="bin-edge-shares",
|
|
582
|
+
),
|
|
583
|
+
],
|
|
584
|
+
)
|
|
585
|
+
def test_ensemble_merges_by_whole_type(
|
|
586
|
+
whole_names, ensemble_names, whole_type, first, second, expected
|
|
587
|
+
):
|
|
588
|
+
"""matrix stacks, dataframe averages, and bin_edge shares the wholes."""
|
|
589
|
+
wholes = {whole_names[(1, 1)]: first, whole_names[(1, 2)]: second}
|
|
590
|
+
ens = ensemble("gyrTMon", wholes, ChainCyl, "bug", whole_type)
|
|
591
|
+
np.testing.assert_array_equal(np.asarray(ens[ensemble_names[1]]),
|
|
592
|
+
expected)
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def test_ensemble_saves_each_ensemble(
|
|
596
|
+
tmp_path, whole_tree, ensemble_names
|
|
597
|
+
):
|
|
598
|
+
"""save_to writes each ensemble to <ensemble_long>-<group>-<prop>.csv."""
|
|
599
|
+
out = tmp_path / "out"
|
|
600
|
+
out.mkdir()
|
|
601
|
+
wholes = whole_from_file(whole_tree.paths, ChainCyl, "bug")
|
|
602
|
+
ens = ensemble("gyrTMon", wholes, ChainCyl, "bug", "vector",
|
|
603
|
+
save_to=out)
|
|
604
|
+
name = ensemble_names[2]
|
|
605
|
+
pd.testing.assert_frame_equal(
|
|
606
|
+
pd.read_csv(out / f"{name}-bug-gyrTMon.csv"), ens[name]
|
|
607
|
+
)
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def test_ensemble_rejects_unknown_whole_type(whole_tree):
|
|
611
|
+
"""A whole_type outside the four accepted values raises ValueError."""
|
|
612
|
+
wholes = whole_from_file(whole_tree.paths, ChainCyl, "bug")
|
|
613
|
+
with pytest.raises(ValueError, match="'nope' is an invalid option"):
|
|
614
|
+
ensemble("gyrTMon", wholes, ChainCyl, "bug", "nope")
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
@pytest.fixture
|
|
618
|
+
def hist_wholes():
|
|
619
|
+
"""Return two replicate histogram wholes of one state point."""
|
|
620
|
+
base = (
|
|
621
|
+
"N2000r10.5lz300.0ac1.0nc117000dt0.005bdump5000adump"
|
|
622
|
+
"10000ens"
|
|
623
|
+
)
|
|
624
|
+
return {
|
|
625
|
+
f"{base}1": np.array([1.0, 2.0, 3.0]),
|
|
626
|
+
f"{base}2": np.array([3.0, 4.0, 5.0]),
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
@pytest.fixture
|
|
631
|
+
def hist_edges(hist_wholes):
|
|
632
|
+
"""Return the same non-uniform bin edges for every whole in hist_wholes.
|
|
633
|
+
|
|
634
|
+
The edges are non-uniform so a wrong center formula cannot pass by
|
|
635
|
+
accident.
|
|
636
|
+
"""
|
|
637
|
+
return {name: np.array([0.0, 1.0, 3.0, 6.0]) for name in hist_wholes}
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def test_ensemble_vector_bin_center_is_edge_midpoints(hist_wholes,
|
|
641
|
+
hist_edges):
|
|
642
|
+
"""With whole_edges, a vector ensemble's bin_center is the midpoints."""
|
|
643
|
+
ens = ensemble(
|
|
644
|
+
"rhoMon", hist_wholes, ChainCyl, "bug", "vector",
|
|
645
|
+
whole_edges=hist_edges,
|
|
646
|
+
)
|
|
647
|
+
(ens_df,) = ens.values()
|
|
648
|
+
# Analytic midpoints of the edges [0, 1, 3, 6]
|
|
649
|
+
np.testing.assert_allclose(ens_df["bin_center"], [0.5, 2.0, 4.5])
|
|
650
|
+
pd.testing.assert_frame_equal(
|
|
651
|
+
ens_df.drop(columns="bin_center"), pd.DataFrame(hist_wholes)
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
|
|
655
|
+
def test_ensemble_without_whole_edges_has_no_bin_center(hist_wholes):
|
|
656
|
+
"""Without whole_edges, a vector ensemble has one column per whole only."""
|
|
657
|
+
ens = ensemble("rhoMon", hist_wholes, ChainCyl, "bug", "vector")
|
|
658
|
+
(ens_df,) = ens.values()
|
|
659
|
+
assert list(ens_df.columns) == list(hist_wholes)
|
|
660
|
+
|
|
661
|
+
|
|
662
|
+
def test_ensemble_avg_keeps_bin_center_from_ensemble(hist_wholes,
|
|
663
|
+
hist_edges):
|
|
664
|
+
"""ensemble_avg passes an ensemble's bin_center through unchanged."""
|
|
665
|
+
ens = ensemble(
|
|
666
|
+
"rhoMon", hist_wholes, ChainCyl, "bug", "vector",
|
|
667
|
+
whole_edges=hist_edges,
|
|
668
|
+
)
|
|
669
|
+
(ens_df,) = ens.values()
|
|
670
|
+
avg = ensemble_avg("rhoMon", ens, "bug", "dataframe")
|
|
671
|
+
(avg_df,) = avg.values()
|
|
672
|
+
np.testing.assert_array_equal(avg_df["bin_center"], ens_df["bin_center"])
|
|
673
|
+
assert list(avg_df.columns) == [
|
|
674
|
+
"bin_center", "rhoMon-mean", "rhoMon-var", "rhoMon-sem"
|
|
675
|
+
]
|
|
676
|
+
|
|
677
|
+
|
|
678
|
+
@pytest.mark.parametrize("whole_type", ["matrix", "dataframe", "bin_edge"])
|
|
679
|
+
def test_ensemble_rejects_whole_edges_for_non_vector(
|
|
680
|
+
whole_type, hist_wholes, hist_edges
|
|
681
|
+
):
|
|
682
|
+
"""whole_edges with any whole_type other than 'vector' raises."""
|
|
683
|
+
with pytest.raises(ValueError, match="'whole_edges' is only valid"):
|
|
684
|
+
ensemble(
|
|
685
|
+
"rhoMon", hist_wholes, ChainCyl, "bug", whole_type,
|
|
686
|
+
whole_edges=hist_edges,
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
|
|
690
|
+
# Averaging over an ensemble
|
|
691
|
+
|
|
692
|
+
|
|
693
|
+
def test_ens_avg_from_ndarray_statistics_along_axis_0():
|
|
694
|
+
"""Two wholes a, b give mean (a+b)/2, var (a-b)^2/2, sem |a-b|/2."""
|
|
695
|
+
a = np.array([[1.0, 2.0], [3.0, 4.0]])
|
|
696
|
+
b = np.array([[2.0, 5.0], [3.0, 8.0]])
|
|
697
|
+
out = ens_avg_from_ndarray("gyrTMon", np.stack([a, b]))
|
|
698
|
+
assert list(out) == ["gyrTMon-mean", "gyrTMon-var", "gyrTMon-sem"]
|
|
699
|
+
# Float64 round-off on values of order 10
|
|
700
|
+
np.testing.assert_allclose(out["gyrTMon-mean"], (a + b) / 2, atol=1e-12)
|
|
701
|
+
np.testing.assert_allclose(out["gyrTMon-var"], (a - b) ** 2 / 2,
|
|
702
|
+
atol=1e-12)
|
|
703
|
+
np.testing.assert_allclose(out["gyrTMon-sem"], np.abs(a - b) / 2,
|
|
704
|
+
atol=1e-12)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def test_ens_avg_from_df_leaves_input_unchanged():
|
|
708
|
+
"""ens_avg_from_df returns a new frame and leaves its input unchanged."""
|
|
709
|
+
ens_data = pd.DataFrame({
|
|
710
|
+
"bin_center": [1.0, 2.0],
|
|
711
|
+
"w1": [4.0, 5.0],
|
|
712
|
+
"w2": [5.0, 7.0],
|
|
713
|
+
})
|
|
714
|
+
before = ens_data.copy()
|
|
715
|
+
out = ens_avg_from_df("density", ens_data, ["bin_center"])
|
|
716
|
+
assert out is not ens_data
|
|
717
|
+
pd.testing.assert_frame_equal(ens_data, before)
|
|
718
|
+
assert list(out.columns) == [
|
|
719
|
+
"bin_center", "density-mean", "density-var", "density-sem"
|
|
720
|
+
]
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
def test_ens_avg_from_df_statistics():
|
|
724
|
+
"""Two wholes a, b give mean (a+b)/2, var (a-b)^2/2, sem |a-b|/2."""
|
|
725
|
+
a = np.array([4.0, 5.0])
|
|
726
|
+
b = np.array([5.0, 7.0])
|
|
727
|
+
ens_data = pd.DataFrame({"bin_center": [1.0, 2.0], "w1": a, "w2": b})
|
|
728
|
+
out = ens_avg_from_df("density", ens_data, ["bin_center"])
|
|
729
|
+
np.testing.assert_allclose(out["density-mean"], (a + b) / 2)
|
|
730
|
+
np.testing.assert_allclose(out["density-var"], (a - b) ** 2 / 2)
|
|
731
|
+
np.testing.assert_allclose(out["density-sem"], np.abs(a - b) / 2)
|
|
732
|
+
np.testing.assert_array_equal(out["bin_center"], [1.0, 2.0])
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def test_ens_avg_from_bin_edge_copies_1d_edges():
|
|
736
|
+
"""A single 1D edge array, as ensemble_avg passes it, is copied."""
|
|
737
|
+
edges = np.array([0.0, 0.5, 1.0])
|
|
738
|
+
out = ens_avg_from_bin_edge(edges)
|
|
739
|
+
np.testing.assert_array_equal(out, edges)
|
|
740
|
+
assert not np.shares_memory(out, edges)
|
|
741
|
+
|
|
742
|
+
|
|
743
|
+
def test_ens_avg_from_bin_edge_reduces_equal_sequence():
|
|
744
|
+
"""A sequence of equal edge arrays reduces to one copy of them."""
|
|
745
|
+
edges = np.array([0.0, 0.5, 1.0])
|
|
746
|
+
np.testing.assert_array_equal(
|
|
747
|
+
ens_avg_from_bin_edge([edges, edges.copy()]), edges
|
|
748
|
+
)
|
|
749
|
+
|
|
750
|
+
|
|
751
|
+
def test_ens_avg_from_bin_edge_rejects_differing_sequence():
|
|
752
|
+
"""A sequence of differing edge arrays raises ValueError."""
|
|
753
|
+
edges = np.array([0.0, 0.5, 1.0])
|
|
754
|
+
with pytest.raises(ValueError, match="Bin edges are not identical"):
|
|
755
|
+
ens_avg_from_bin_edge([edges, np.array([0.0, 0.5, 2.0])])
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def test_ensemble_avg_bin_edge_keeps_edges():
|
|
759
|
+
"""ensemble_avg with 'bin_edge' returns each ensemble's edges intact."""
|
|
760
|
+
edges = np.array([0.0, 0.5, 1.0, 1.5])
|
|
761
|
+
out = ensemble_avg("rhoMon", {"ens1": edges}, "all", "bin_edge")
|
|
762
|
+
np.testing.assert_array_equal(out["ens1"], edges)
|
|
763
|
+
|
|
764
|
+
|
|
765
|
+
def test_ensemble_avg_ndarray_averages_each_ensemble():
|
|
766
|
+
"""ensemble_avg with 'ndarray' gives each ensemble's statistics dict."""
|
|
767
|
+
a = np.array([[1.0, 2.0]])
|
|
768
|
+
b = np.array([[3.0, 6.0]])
|
|
769
|
+
out = ensemble_avg("gyrTMon", {"ens1": np.stack([a, b])}, "bug",
|
|
770
|
+
"ndarray")
|
|
771
|
+
np.testing.assert_allclose(out["ens1"]["gyrTMon-mean"], [[2.0, 4.0]])
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def test_ensemble_avg_rejects_unknown_ens_type():
|
|
775
|
+
"""An ens_type outside dataframe, ndarray, and bin_edge raises."""
|
|
776
|
+
with pytest.raises(ValueError, match="'nope' is an invalid option"):
|
|
777
|
+
ensemble_avg("gyrTMon", {"ens1": np.zeros(2)}, "bug", "nope")
|
|
778
|
+
|
|
779
|
+
|
|
780
|
+
@pytest.mark.parametrize(
|
|
781
|
+
"ensembles, ens_type, filenames",
|
|
782
|
+
[
|
|
783
|
+
pytest.param(
|
|
784
|
+
{"ens1": pd.DataFrame({"w1": [1.0], "w2": [3.0]})},
|
|
785
|
+
"dataframe",
|
|
786
|
+
["ens1-bug-rhoMon-ensAvg.csv"],
|
|
787
|
+
id="dataframe-csv",
|
|
788
|
+
),
|
|
789
|
+
pytest.param(
|
|
790
|
+
{"ens1": np.zeros((2, 1, 1))},
|
|
791
|
+
"ndarray",
|
|
792
|
+
[
|
|
793
|
+
"ens1-bug-rhoMon-ensAvg-mean.npy",
|
|
794
|
+
"ens1-bug-rhoMon-ensAvg-sem.npy",
|
|
795
|
+
"ens1-bug-rhoMon-ensAvg-var.npy",
|
|
796
|
+
],
|
|
797
|
+
id="ndarray-npy-per-measure",
|
|
798
|
+
),
|
|
799
|
+
pytest.param(
|
|
800
|
+
{"ens1": np.array([0.0, 1.0])},
|
|
801
|
+
"bin_edge",
|
|
802
|
+
["ens1-bug-rhoMon-ensAvg.npy"],
|
|
803
|
+
id="bin-edge-npy",
|
|
804
|
+
),
|
|
805
|
+
],
|
|
806
|
+
)
|
|
807
|
+
def test_ensemble_avg_saves_under_ens_avg_name(
|
|
808
|
+
tmp_path, ensembles, ens_type, filenames
|
|
809
|
+
):
|
|
810
|
+
"""save_to writes each result under <ensemble>-<group>-<prop>-ensAvg."""
|
|
811
|
+
ensemble_avg("rhoMon", ensembles, "bug", ens_type, save_to=tmp_path)
|
|
812
|
+
assert sorted(os.listdir(tmp_path)) == filenames
|
|
813
|
+
|
|
814
|
+
|
|
815
|
+
# Stamps
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
@pytest.fixture
|
|
819
|
+
def segment_stamps():
|
|
820
|
+
"""Return stamps of 2 wholes x 3 segments of one state point.
|
|
821
|
+
|
|
822
|
+
n_frames is 10, 20, 30 and gyr is 1, 2, 3 for the segments of w1, and
|
|
823
|
+
gyr is 4, 5, 6 for those of w2.
|
|
824
|
+
"""
|
|
825
|
+
return pd.DataFrame({
|
|
826
|
+
"lineage_name": ["w1.j01", "w1.j02", "w1.j03",
|
|
827
|
+
"w2.j01", "w2.j02", "w2.j03"],
|
|
828
|
+
"segment": ["w1.j01", "w1.j02", "w1.j03",
|
|
829
|
+
"w2.j01", "w2.j02", "w2.j03"],
|
|
830
|
+
"whole": ["w1", "w1", "w1", "w2", "w2", "w2"],
|
|
831
|
+
"ensemble_long": ["s1"] * 6,
|
|
832
|
+
"ensemble": ["e1"] * 6,
|
|
833
|
+
"space": ["N20"] * 6,
|
|
834
|
+
"group": ["bug"] * 6,
|
|
835
|
+
"segment_id": [1, 2, 3, 1, 2, 3],
|
|
836
|
+
"ensemble_id": [1, 1, 1, 2, 2, 2],
|
|
837
|
+
"gyr": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
|
838
|
+
"n_frames": [10, 20, 30, 10, 20, 30],
|
|
839
|
+
})
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
@pytest.fixture
|
|
843
|
+
def whole_stamps():
|
|
844
|
+
"""Return stamps of 2 state points x 2 wholes, each of 60 frames.
|
|
845
|
+
|
|
846
|
+
The second state point keeps replicates ens1 and ens3, as if ens2 had
|
|
847
|
+
failed, so the number of wholes differs from the last ensemble_id.
|
|
848
|
+
"""
|
|
849
|
+
return pd.DataFrame({
|
|
850
|
+
"lineage_name": ["w1", "w2", "w3", "w4"],
|
|
851
|
+
"segment": ["N/A"] * 4,
|
|
852
|
+
"whole": ["w1", "w2", "w3", "w4"],
|
|
853
|
+
"ensemble_long": ["s1", "s1", "s2", "s2"],
|
|
854
|
+
"ensemble": ["e1", "e1", "e2", "e2"],
|
|
855
|
+
"space": ["N20"] * 4,
|
|
856
|
+
"group": ["bug"] * 4,
|
|
857
|
+
"segment_id": [0, 0, 0, 0],
|
|
858
|
+
"ensemble_id": [1, 2, 1, 3],
|
|
859
|
+
"n_frames": [60, 60, 60, 60],
|
|
860
|
+
})
|
|
861
|
+
|
|
862
|
+
|
|
863
|
+
@pytest.fixture
|
|
864
|
+
def write_stamps(tmp_path):
|
|
865
|
+
"""Factory: write each row of a stamps frame to its own CSV file."""
|
|
866
|
+
|
|
867
|
+
def _write(stamps):
|
|
868
|
+
directory = tmp_path / "stamps"
|
|
869
|
+
directory.mkdir()
|
|
870
|
+
paths = []
|
|
871
|
+
for row in range(len(stamps)):
|
|
872
|
+
path = directory / f"{stamps.loc[row, 'lineage_name']}-stamps.csv"
|
|
873
|
+
stamps.iloc[[row]].to_csv(path, index=False)
|
|
874
|
+
paths.append((str(path),))
|
|
875
|
+
return paths
|
|
876
|
+
|
|
877
|
+
return _write
|
|
878
|
+
|
|
879
|
+
|
|
880
|
+
def test_children_stamps_concatenates_in_input_order(
|
|
881
|
+
write_stamps, segment_stamps
|
|
882
|
+
):
|
|
883
|
+
"""Segment stamp files combine row by row, in the order given."""
|
|
884
|
+
paths = write_stamps(segment_stamps)
|
|
885
|
+
combined = children_stamps(paths, "segment")
|
|
886
|
+
pd.testing.assert_frame_equal(combined, segment_stamps)
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def test_children_stamps_whole_drops_segment_columns(
|
|
890
|
+
write_stamps, whole_stamps
|
|
891
|
+
):
|
|
892
|
+
"""Whole stamps lose 'segment' and 'segment_id', with a UserWarning."""
|
|
893
|
+
paths = write_stamps(whole_stamps)
|
|
894
|
+
with pytest.warns(UserWarning, match="'segment' and 'segment_id'"):
|
|
895
|
+
combined = children_stamps(paths, "whole")
|
|
896
|
+
pd.testing.assert_frame_equal(
|
|
897
|
+
combined, whole_stamps.drop(columns=["segment", "segment_id"])
|
|
898
|
+
)
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def test_children_stamps_whole_without_segment_columns_is_unchanged(
|
|
902
|
+
write_stamps, whole_stamps, capsys
|
|
903
|
+
):
|
|
904
|
+
"""Whole stamps without segment columns pass through, printing nothing."""
|
|
905
|
+
stamps = whole_stamps.drop(columns=["segment", "segment_id"])
|
|
906
|
+
paths = write_stamps(stamps)
|
|
907
|
+
|
|
908
|
+
combined = children_stamps(paths, "whole")
|
|
909
|
+
|
|
910
|
+
pd.testing.assert_frame_equal(combined, stamps)
|
|
911
|
+
assert capsys.readouterr().out == ""
|
|
912
|
+
|
|
913
|
+
|
|
914
|
+
@pytest.mark.parametrize(
|
|
915
|
+
"stamps_fixture, child_lineage, filename",
|
|
916
|
+
[
|
|
917
|
+
pytest.param("segment_stamps", "segment",
|
|
918
|
+
"N20-bug-stamps-segment.csv", id="segment"),
|
|
919
|
+
pytest.param("whole_stamps", "whole", "N20-bug-stamps-whole.csv",
|
|
920
|
+
id="whole", marks=pytest.mark.filterwarnings(
|
|
921
|
+
"ignore:'segment' and 'segment_id':UserWarning")),
|
|
922
|
+
],
|
|
923
|
+
)
|
|
924
|
+
def test_children_stamps_saves_under_rows_lineage(
|
|
925
|
+
request, tmp_path, write_stamps, stamps_fixture, child_lineage, filename
|
|
926
|
+
):
|
|
927
|
+
"""save_to writes the result as <space>-<group>-stamps-<level>.csv, with
|
|
928
|
+
the lineage level of its rows."""
|
|
929
|
+
paths = write_stamps(request.getfixturevalue(stamps_fixture))
|
|
930
|
+
|
|
931
|
+
combined = children_stamps(paths, child_lineage, save_to=tmp_path)
|
|
932
|
+
|
|
933
|
+
pd.testing.assert_frame_equal(pd.read_csv(tmp_path / filename), combined)
|
|
934
|
+
|
|
935
|
+
|
|
936
|
+
@pytest.fixture
|
|
937
|
+
def write_research_stamps(tmp_path):
|
|
938
|
+
"""Factory: write each row of a stamps frame, without its 'group'
|
|
939
|
+
column, to its own CSV named <lineage_name><suffix>, as the research
|
|
940
|
+
code's probe did (the group is in the file name, not a column)."""
|
|
941
|
+
|
|
942
|
+
def _write(stamps, suffix="-bug-stamps.csv"):
|
|
943
|
+
directory = tmp_path / "stamps"
|
|
944
|
+
directory.mkdir(exist_ok=True)
|
|
945
|
+
stamps = stamps.drop(columns=["group"])
|
|
946
|
+
paths = []
|
|
947
|
+
for row in range(len(stamps)):
|
|
948
|
+
path = directory / f"{stamps.loc[row, 'lineage_name']}{suffix}"
|
|
949
|
+
stamps.iloc[[row]].to_csv(path, index=False)
|
|
950
|
+
paths.append((str(path),))
|
|
951
|
+
return paths
|
|
952
|
+
|
|
953
|
+
return _write
|
|
954
|
+
|
|
955
|
+
|
|
956
|
+
@pytest.mark.parametrize(
|
|
957
|
+
"stamps_fixture, child_lineage, filename",
|
|
958
|
+
[
|
|
959
|
+
pytest.param("segment_stamps", "segment",
|
|
960
|
+
"N20-bug-stamps-segment.csv", id="segment"),
|
|
961
|
+
pytest.param("whole_stamps", "whole", "N20-bug-stamps-whole.csv",
|
|
962
|
+
id="whole", marks=pytest.mark.filterwarnings(
|
|
963
|
+
"ignore:'segment' and 'segment_id':UserWarning")),
|
|
964
|
+
],
|
|
965
|
+
)
|
|
966
|
+
def test_children_stamps_without_group_column_takes_it_from_the_file_names(
|
|
967
|
+
request, tmp_path, write_research_stamps, stamps_fixture, child_lineage,
|
|
968
|
+
filename
|
|
969
|
+
):
|
|
970
|
+
"""Stamps without a 'group' column, as the research code wrote them, get
|
|
971
|
+
one after 'space' from their <name>-<group>-stamps.csv file names, and
|
|
972
|
+
save under that group."""
|
|
973
|
+
stamps = request.getfixturevalue(stamps_fixture)
|
|
974
|
+
paths = write_research_stamps(stamps)
|
|
975
|
+
|
|
976
|
+
combined = children_stamps(paths, child_lineage, save_to=tmp_path)
|
|
977
|
+
|
|
978
|
+
assert (combined.columns.get_loc("group")
|
|
979
|
+
== combined.columns.get_loc("space") + 1)
|
|
980
|
+
assert combined["group"].tolist() == ["bug"] * len(combined)
|
|
981
|
+
pd.testing.assert_frame_equal(pd.read_csv(tmp_path / filename), combined)
|
|
982
|
+
|
|
983
|
+
|
|
984
|
+
def test_children_stamps_without_group_rejects_a_name_without_a_group(
|
|
985
|
+
write_research_stamps, segment_stamps
|
|
986
|
+
):
|
|
987
|
+
"""Stamps with neither a 'group' column nor <name>-<group>-stamps.csv
|
|
988
|
+
file names raise ValueError naming the file."""
|
|
989
|
+
paths = write_research_stamps(segment_stamps, suffix="-stamps.csv")
|
|
990
|
+
with pytest.raises(ValueError, match="w1.j01-stamps.csv"):
|
|
991
|
+
children_stamps(paths, "segment")
|
|
992
|
+
|
|
993
|
+
|
|
994
|
+
def test_children_stamps_without_group_rejects_mixed_groups(
|
|
995
|
+
write_research_stamps, segment_stamps
|
|
996
|
+
):
|
|
997
|
+
"""File names that give more than one group raise ValueError naming
|
|
998
|
+
them, instead of taking the first."""
|
|
999
|
+
paths = (write_research_stamps(segment_stamps.iloc[:3].reset_index(
|
|
1000
|
+
drop=True))
|
|
1001
|
+
+ write_research_stamps(segment_stamps.iloc[3:].reset_index(
|
|
1002
|
+
drop=True), suffix="-all-stamps.csv"))
|
|
1003
|
+
with pytest.raises(ValueError, match=r"\['all', 'bug'\]"):
|
|
1004
|
+
children_stamps(paths, "segment")
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def test_children_stamps_takes_child_lineage_by_keyword(
|
|
1008
|
+
write_stamps, segment_stamps
|
|
1009
|
+
):
|
|
1010
|
+
"""The level of the stamps is the keyword argument child_lineage."""
|
|
1011
|
+
paths = write_stamps(segment_stamps)
|
|
1012
|
+
|
|
1013
|
+
combined = children_stamps(paths, child_lineage="segment")
|
|
1014
|
+
|
|
1015
|
+
assert combined["segment_id"].tolist() == [1, 2, 3, 1, 2, 3]
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def test_children_stamps_rejects_unknown_lineage():
|
|
1019
|
+
"""A lineage other than segment or whole raises ValueError."""
|
|
1020
|
+
with pytest.raises(ValueError, match="'nope' is an invalid option"):
|
|
1021
|
+
children_stamps([], "nope")
|
|
1022
|
+
|
|
1023
|
+
|
|
1024
|
+
def test_parents_stamps_segment_counts_segments_and_sums_frames(
|
|
1025
|
+
segment_stamps
|
|
1026
|
+
):
|
|
1027
|
+
"""Segment stamps give one row per whole with n_segments and n_frames."""
|
|
1028
|
+
parents = parents_stamps(segment_stamps, "segment")
|
|
1029
|
+
assert parents["whole"].tolist() == ["w1", "w2"]
|
|
1030
|
+
assert parents["n_segments"].tolist() == [3, 3]
|
|
1031
|
+
# 10 + 20 + 30 frames per whole
|
|
1032
|
+
assert parents["n_frames"].tolist() == [60, 60]
|
|
1033
|
+
|
|
1034
|
+
|
|
1035
|
+
def test_parents_stamps_whole_counts_wholes_per_ensemble(whole_stamps):
|
|
1036
|
+
"""Whole stamps give one row per state point with n_ensembles."""
|
|
1037
|
+
parents = parents_stamps(whole_stamps, "whole")
|
|
1038
|
+
assert parents["ensemble_long"].tolist() == ["s1", "s2"]
|
|
1039
|
+
assert parents["n_ensembles"].tolist() == [2, 2]
|
|
1040
|
+
assert parents["n_frames"].tolist() == [60, 60]
|
|
1041
|
+
assert {"segment", "segment_id", "whole"}.isdisjoint(parents.columns)
|
|
1042
|
+
|
|
1043
|
+
|
|
1044
|
+
def test_parents_stamps_whole_drops_n_segments(whole_stamps):
|
|
1045
|
+
"""Whole stamps built from segments lose n_segments in their parents."""
|
|
1046
|
+
stamps = whole_stamps.assign(n_segments=[3, 3, 3, 3])
|
|
1047
|
+
parents = parents_stamps(stamps, "whole")
|
|
1048
|
+
assert "n_segments" not in parents.columns
|
|
1049
|
+
|
|
1050
|
+
|
|
1051
|
+
def test_parents_stamps_applies_given_aggregation(segment_stamps):
|
|
1052
|
+
"""properties replaces the default 'last' aggregation of a column."""
|
|
1053
|
+
parents = parents_stamps(segment_stamps, "segment",
|
|
1054
|
+
properties={"gyr": "mean"})
|
|
1055
|
+
# Means of 1, 2, 3 and of 4, 5, 6
|
|
1056
|
+
assert parents["gyr"].tolist() == pytest.approx([2.0, 5.0])
|
|
1057
|
+
|
|
1058
|
+
|
|
1059
|
+
@pytest.mark.parametrize(
|
|
1060
|
+
"stamps_fixture, lineage, filename",
|
|
1061
|
+
[
|
|
1062
|
+
pytest.param("segment_stamps", "segment", "N20-bug-stamps-whole.csv",
|
|
1063
|
+
id="segment-to-whole"),
|
|
1064
|
+
pytest.param("whole_stamps", "whole",
|
|
1065
|
+
"N20-bug-stamps-ensemble_long.csv",
|
|
1066
|
+
id="whole-to-ensemble_long"),
|
|
1067
|
+
],
|
|
1068
|
+
)
|
|
1069
|
+
def test_parents_stamps_saves_under_parents_lineage(
|
|
1070
|
+
request, tmp_path, stamps_fixture, lineage, filename
|
|
1071
|
+
):
|
|
1072
|
+
"""save_to writes the parent stamps as <space>-<group>-stamps-<level>.csv,
|
|
1073
|
+
with the lineage level of the parent rows."""
|
|
1074
|
+
stamps = request.getfixturevalue(stamps_fixture)
|
|
1075
|
+
parents = parents_stamps(stamps, lineage, save_to=tmp_path)
|
|
1076
|
+
pd.testing.assert_frame_equal(pd.read_csv(tmp_path / filename), parents)
|
|
1077
|
+
|
|
1078
|
+
|
|
1079
|
+
@pytest.mark.parametrize("column", ["space", "group"])
|
|
1080
|
+
def test_parents_stamps_saving_without_a_name_column_raises(
|
|
1081
|
+
tmp_path, whole_stamps, column
|
|
1082
|
+
):
|
|
1083
|
+
"""Saving parent stamps that lack 'space' or 'group' raises ValueError
|
|
1084
|
+
naming the column, since the file name needs both."""
|
|
1085
|
+
stamps = whole_stamps.drop(columns=[column])
|
|
1086
|
+
with pytest.raises(ValueError, match=f"'{column}'"):
|
|
1087
|
+
parents_stamps(stamps, "whole", save_to=tmp_path)
|
|
1088
|
+
|
|
1089
|
+
|
|
1090
|
+
@pytest.mark.parametrize(
|
|
1091
|
+
"stamps_fixture, lineage",
|
|
1092
|
+
[
|
|
1093
|
+
pytest.param("segment_stamps", "segment", id="segment"),
|
|
1094
|
+
pytest.param("whole_stamps", "whole", id="whole"),
|
|
1095
|
+
],
|
|
1096
|
+
)
|
|
1097
|
+
def test_parents_stamps_without_lineage_name_drops_child_column(
|
|
1098
|
+
request, stamps_fixture, lineage
|
|
1099
|
+
):
|
|
1100
|
+
"""Stamps without 'lineage_name' still lose the child lineage column."""
|
|
1101
|
+
stamps = request.getfixturevalue(stamps_fixture).drop(
|
|
1102
|
+
columns="lineage_name")
|
|
1103
|
+
|
|
1104
|
+
parents = parents_stamps(stamps, lineage)
|
|
1105
|
+
|
|
1106
|
+
assert lineage not in parents.columns
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
def test_parents_stamps_without_lineage_name_prints_nothing(
|
|
1110
|
+
segment_stamps, capsys
|
|
1111
|
+
):
|
|
1112
|
+
"""An absent 'lineage_name' column is skipped without printing."""
|
|
1113
|
+
stamps = segment_stamps.drop(columns="lineage_name")
|
|
1114
|
+
|
|
1115
|
+
parents_stamps(stamps, "segment")
|
|
1116
|
+
|
|
1117
|
+
assert capsys.readouterr().out == ""
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def test_parents_stamps_takes_child_lineage_by_keyword(segment_stamps):
|
|
1121
|
+
"""The level of the child stamps is the keyword argument child_lineage."""
|
|
1122
|
+
parents = parents_stamps(segment_stamps, child_lineage="segment")
|
|
1123
|
+
|
|
1124
|
+
assert parents["n_segments"].tolist() == [3, 3]
|
|
1125
|
+
|
|
1126
|
+
|
|
1127
|
+
@pytest.mark.parametrize(
|
|
1128
|
+
"stamps_fixture, child_lineage, key",
|
|
1129
|
+
[
|
|
1130
|
+
pytest.param("segment_stamps", "segment", "whole", id="segment"),
|
|
1131
|
+
pytest.param("whole_stamps", "whole", "ensemble_long", id="whole"),
|
|
1132
|
+
],
|
|
1133
|
+
)
|
|
1134
|
+
def test_parents_stamps_rejects_a_missing_parent_name(
|
|
1135
|
+
request, stamps_fixture, child_lineage, key
|
|
1136
|
+
):
|
|
1137
|
+
"""A child row with no parent name raises instead of being dropped by
|
|
1138
|
+
the group-by."""
|
|
1139
|
+
stamps = request.getfixturevalue(stamps_fixture)
|
|
1140
|
+
stamps.loc[0, key] = np.nan
|
|
1141
|
+
|
|
1142
|
+
with pytest.raises(ValueError, match=re.escape(
|
|
1143
|
+
f"1 stamps row has no {key!r}")):
|
|
1144
|
+
parents_stamps(stamps, child_lineage)
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
@pytest.mark.parametrize("column", ["n_frames", "segment_id", "whole"])
|
|
1148
|
+
def test_parents_stamps_rejects_aggregating_its_own_columns(
|
|
1149
|
+
segment_stamps, column
|
|
1150
|
+
):
|
|
1151
|
+
"""A properties entry for a column the parent aggregates itself raises
|
|
1152
|
+
instead of being overwritten silently."""
|
|
1153
|
+
with pytest.raises(ValueError, match=re.escape(
|
|
1154
|
+
f"properties cannot set {column!r}")):
|
|
1155
|
+
parents_stamps(segment_stamps, "segment", properties={column: "max"})
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
def test_parents_stamps_rejects_unknown_lineage(segment_stamps):
|
|
1159
|
+
"""A lineage other than segment or whole raises ValueError."""
|
|
1160
|
+
with pytest.raises(ValueError, match="'nope' is an invalid option"):
|
|
1161
|
+
parents_stamps(segment_stamps, "nope")
|
|
1162
|
+
|
|
1163
|
+
|
|
1164
|
+
# Space level: ensemble-averaged files of two state points that differ only
|
|
1165
|
+
# in the number of crowders. Both dump every 5000 steps with dt=0.005.
|
|
1166
|
+
|
|
1167
|
+
|
|
1168
|
+
@pytest.fixture
|
|
1169
|
+
def space_states():
|
|
1170
|
+
"""Return the ensemble_long names of the two synthetic state points."""
|
|
1171
|
+
return [
|
|
1172
|
+
"N200r10.5lz25.0ac2.0nc1000dt0.005bdump2000adump5000",
|
|
1173
|
+
"N200r10.5lz25.0ac2.0nc2000dt0.005bdump2000adump5000",
|
|
1174
|
+
]
|
|
1175
|
+
|
|
1176
|
+
|
|
1177
|
+
@pytest.fixture
|
|
1178
|
+
def make_ens_avg_space(tmp_path, space_states):
|
|
1179
|
+
"""Factory: write one ensAvg CSV per state point, plus a decoy file.
|
|
1180
|
+
|
|
1181
|
+
Takes the property, one '-mean' column per state point, and the group
|
|
1182
|
+
(default 'all'); '-var' and '-sem' are constant. Returns the ensAvg
|
|
1183
|
+
directory, ending with '/'.
|
|
1184
|
+
"""
|
|
1185
|
+
|
|
1186
|
+
def _make(prop, means, group="all"):
|
|
1187
|
+
ens_avg_dir = tmp_path / "analysis" / f"N200-{group}-ensAvg"
|
|
1188
|
+
ens_avg_dir.mkdir(parents=True)
|
|
1189
|
+
for state, mean in zip(space_states, means):
|
|
1190
|
+
pd.DataFrame({
|
|
1191
|
+
f"{prop}-mean": mean,
|
|
1192
|
+
f"{prop}-var": np.full(len(mean), 0.1),
|
|
1193
|
+
f"{prop}-sem": np.full(len(mean), 0.05),
|
|
1194
|
+
}).to_csv(ens_avg_dir / f"{state}-{group}-{prop}-ensAvg.csv",
|
|
1195
|
+
index=False)
|
|
1196
|
+
# A file of another property in the same directory must be ignored.
|
|
1197
|
+
pd.DataFrame({"other-mean": [1.0]}).to_csv(
|
|
1198
|
+
ens_avg_dir / f"{space_states[0]}-{group}-other-ensAvg.csv",
|
|
1199
|
+
index=False,
|
|
1200
|
+
)
|
|
1201
|
+
return str(ens_avg_dir) + "/"
|
|
1202
|
+
|
|
1203
|
+
return _make
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
@pytest.fixture
|
|
1207
|
+
def state_attr(space_states):
|
|
1208
|
+
"""Factory: an attribute of each state point, repeated n_rows times."""
|
|
1209
|
+
|
|
1210
|
+
def _values(attr, n_rows):
|
|
1211
|
+
parsers = [ChainCyl(s, "ensemble_long", "all") for s in space_states]
|
|
1212
|
+
return np.repeat([getattr(p, attr) for p in parsers], n_rows)
|
|
1213
|
+
|
|
1214
|
+
return _values
|
|
1215
|
+
|
|
1216
|
+
|
|
1217
|
+
def test_space_tseries_concatenates_ensembles(make_ens_avg_space,
|
|
1218
|
+
state_attr):
|
|
1219
|
+
"""space_tseries reads every '-<prop>-ensAvg.csv' file and no other."""
|
|
1220
|
+
database = make_ens_avg_space(
|
|
1221
|
+
"gyrTMon", [[0.0, 1.0, 2.0], [10.0, 11.0, 12.0]]
|
|
1222
|
+
)
|
|
1223
|
+
space = space_tseries(database, "gyrTMon", ChainCyl, "N*", ["dcyl"],
|
|
1224
|
+
"all")
|
|
1225
|
+
assert list(space.index) == list(range(6))
|
|
1226
|
+
assert "other-mean" not in space.columns
|
|
1227
|
+
# Files are sorted by name, so the nc1000 rows come first.
|
|
1228
|
+
np.testing.assert_allclose(
|
|
1229
|
+
space["gyrTMon-mean"], [0.0, 1.0, 2.0, 10.0, 11.0, 12.0]
|
|
1230
|
+
)
|
|
1231
|
+
np.testing.assert_allclose(space["dcyl"], state_attr("dcyl", 3))
|
|
1232
|
+
|
|
1233
|
+
|
|
1234
|
+
def test_space_tseries_reads_database_without_trailing_separator(
|
|
1235
|
+
make_ens_avg_space):
|
|
1236
|
+
"""The directory is joined to the glob as a path, so it reads the same
|
|
1237
|
+
files with or without a trailing separator."""
|
|
1238
|
+
database = make_ens_avg_space(
|
|
1239
|
+
"gyrTMon", [[0.0, 1.0, 2.0], [10.0, 11.0, 12.0]]
|
|
1240
|
+
)
|
|
1241
|
+
|
|
1242
|
+
space = space_tseries(database.rstrip("/"), "gyrTMon", ChainCyl, "N*",
|
|
1243
|
+
[], "all")
|
|
1244
|
+
|
|
1245
|
+
np.testing.assert_allclose(
|
|
1246
|
+
space["gyrTMon-mean"], [0.0, 1.0, 2.0, 10.0, 11.0, 12.0]
|
|
1247
|
+
)
|
|
1248
|
+
|
|
1249
|
+
|
|
1250
|
+
def test_space_tseries_scales_t_index_and_time(make_ens_avg_space):
|
|
1251
|
+
"""For all, t_index is frame index times adump; time is t_index * dt."""
|
|
1252
|
+
database = make_ens_avg_space("gyrTMon", [[0.0, 1.0, 2.0]] * 2)
|
|
1253
|
+
space = space_tseries(database, "gyrTMon", ChainCyl, "N*", [], "all")
|
|
1254
|
+
frames = np.array([0, 1, 2, 0, 1, 2])
|
|
1255
|
+
# adump = 5000 and dt = 0.005 in the filenames
|
|
1256
|
+
np.testing.assert_array_equal(space["t_index"], frames * 5000)
|
|
1257
|
+
np.testing.assert_allclose(space["time"], frames * 5000 * 0.005)
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def test_space_tseries_scales_bug_by_bdump(make_ens_avg_space):
|
|
1261
|
+
"""For bug, t_index is frame index times bdump, not adump."""
|
|
1262
|
+
database = make_ens_avg_space("gyrTMon", [[0.0, 1.0, 2.0]] * 2,
|
|
1263
|
+
group="bug")
|
|
1264
|
+
space = space_tseries(database, "gyrTMon", ChainCyl, "N*", [], "bug")
|
|
1265
|
+
frames = np.array([0, 1, 2, 0, 1, 2])
|
|
1266
|
+
# bdump = 2000 and dt = 0.005 in the filenames
|
|
1267
|
+
np.testing.assert_array_equal(space["t_index"], frames * 2000)
|
|
1268
|
+
np.testing.assert_allclose(space["time"], frames * 2000 * 0.005)
|
|
1269
|
+
|
|
1270
|
+
|
|
1271
|
+
def test_space_tseries_rejects_dump_key_argument(make_ens_avg_space):
|
|
1272
|
+
"""A dump keyword passed by position fails instead of binding divisor."""
|
|
1273
|
+
database = make_ens_avg_space("gyrTMon", [[0.0, 1.0, 2.0]] * 2)
|
|
1274
|
+
with pytest.raises(TypeError, match="takes 6 positional arguments"):
|
|
1275
|
+
space_tseries(database, "gyrTMon", ChainCyl, "N*", [], "all",
|
|
1276
|
+
"adump")
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def test_space_tseries_adds_phi_bulk_c_by_default(make_ens_avg_space,
|
|
1280
|
+
state_attr):
|
|
1281
|
+
"""The crowder fraction comes from phi_bulk_c without being listed."""
|
|
1282
|
+
database = make_ens_avg_space("gyrTMon", [[0.0, 1.0, 2.0]] * 2)
|
|
1283
|
+
space = space_tseries(database, "gyrTMon", ChainCyl, "N*", ["dcyl"],
|
|
1284
|
+
"all")
|
|
1285
|
+
assert "phi_c_bulk" not in space.columns
|
|
1286
|
+
np.testing.assert_allclose(space["phi_bulk_c"],
|
|
1287
|
+
state_attr("phi_bulk_c", 3))
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def test_space_tseries_rounds_crowder_with_round_to_multiple(
|
|
1291
|
+
make_ens_avg_space
|
|
1292
|
+
):
|
|
1293
|
+
"""phi_bulk_c_round is round_to_multiple of the crowder column."""
|
|
1294
|
+
database = make_ens_avg_space("gyrTMon", [[0.0, 1.0, 2.0]] * 2)
|
|
1295
|
+
space = space_tseries(database, "gyrTMon", ChainCyl, "N*", [], "all",
|
|
1296
|
+
divisor=0.05, round_to=2)
|
|
1297
|
+
expected = space["phi_bulk_c"].apply(round_to_multiple, args=(0.05, 2))
|
|
1298
|
+
np.testing.assert_array_equal(space["phi_bulk_c_round"], expected)
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def test_space_tseries_reads_named_crowder_attr(make_ens_avg_space,
|
|
1302
|
+
state_attr):
|
|
1303
|
+
"""crowder_attr selects the parser attribute that is rounded."""
|
|
1304
|
+
database = make_ens_avg_space("gyrTMon", [[0.0, 1.0, 2.0]] * 2)
|
|
1305
|
+
space = space_tseries(database, "gyrTMon", ChainCyl, "N*", [], "all",
|
|
1306
|
+
crowder_attr="phi_bulk_m")
|
|
1307
|
+
assert "phi_bulk_c" not in space.columns
|
|
1308
|
+
np.testing.assert_allclose(space["phi_bulk_m"],
|
|
1309
|
+
state_attr("phi_bulk_m", 3))
|
|
1310
|
+
|
|
1311
|
+
|
|
1312
|
+
def test_space_tseries_saves_space_csv(tmp_path, make_ens_avg_space):
|
|
1313
|
+
"""is_save writes the returned frame to the analysis space directory."""
|
|
1314
|
+
database = make_ens_avg_space("gyrTMon", [[0.0, 1.0, 2.0]] * 2)
|
|
1315
|
+
space = space_tseries(database, "gyrTMon", ChainCyl, "N*", ["dcyl"],
|
|
1316
|
+
"all", is_save=True)
|
|
1317
|
+
saved = tmp_path / "analysis" / "N200-all-space" / \
|
|
1318
|
+
"N200-all-gyrTMon-space.csv"
|
|
1319
|
+
pd.testing.assert_frame_equal(pd.read_csv(saved), space)
|
|
1320
|
+
|
|
1321
|
+
|
|
1322
|
+
def test_space_hists_concatenates_and_normalizes(make_ens_avg_space,
|
|
1323
|
+
state_attr):
|
|
1324
|
+
"""space_hists adds bin centers and each normalized histogram sums to 1."""
|
|
1325
|
+
database = make_ens_avg_space(
|
|
1326
|
+
"rhoMon", [[1.0, 2.0, 1.0], [2.0, 4.0, 2.0]]
|
|
1327
|
+
)
|
|
1328
|
+
bin_center = np.array([0.5, 1.5, 2.5])
|
|
1329
|
+
space = space_hists(database, "rhoMon", ChainCyl, "N*", ["dcyl"],
|
|
1330
|
+
"all", bin_center=bin_center, normalize=True)
|
|
1331
|
+
assert len(space) == 6
|
|
1332
|
+
assert "other-mean" not in space.columns
|
|
1333
|
+
np.testing.assert_allclose(space["bin_center"], np.tile(bin_center, 2))
|
|
1334
|
+
np.testing.assert_allclose(space["bin_center-norm"],
|
|
1335
|
+
np.tile(bin_center / 2.5, 2))
|
|
1336
|
+
# [1, 2, 1] / 4 and [2, 4, 2] / 8
|
|
1337
|
+
np.testing.assert_allclose(space["rhoMon-norm"],
|
|
1338
|
+
[0.25, 0.5, 0.25, 0.25, 0.5, 0.25])
|
|
1339
|
+
np.testing.assert_allclose(space["dcyl"], state_attr("dcyl", 3))
|
|
1340
|
+
np.testing.assert_allclose(space["phi_bulk_c"],
|
|
1341
|
+
state_attr("phi_bulk_c", 3))
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def test_space_hists_zero_histogram_normalizes_to_zero(make_ens_avg_space):
|
|
1345
|
+
"""An all-zero histogram warns and gets a zero normalized column."""
|
|
1346
|
+
database = make_ens_avg_space("rhoMon", [[0.0, 0.0], [0.0, 0.0]])
|
|
1347
|
+
with pytest.warns(UserWarning, match="All values are zero"):
|
|
1348
|
+
space = space_hists(database, "rhoMon", ChainCyl, "N*", [], "all",
|
|
1349
|
+
normalize=True)
|
|
1350
|
+
np.testing.assert_array_equal(space["rhoMon-norm"], np.zeros(4))
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
def test_space_hists_saves_space_csv(tmp_path, make_ens_avg_space):
|
|
1354
|
+
"""is_save writes the returned frame to the analysis space directory."""
|
|
1355
|
+
database = make_ens_avg_space("rhoMon", [[1.0, 2.0, 1.0]] * 2)
|
|
1356
|
+
space = space_hists(database, "rhoMon", ChainCyl, "N*", [], "all",
|
|
1357
|
+
is_save=True)
|
|
1358
|
+
saved = tmp_path / "analysis" / "N200-all-space" / \
|
|
1359
|
+
"N200-all-rhoMon-space.csv"
|
|
1360
|
+
pd.testing.assert_frame_equal(pd.read_csv(saved), space)
|
|
1361
|
+
|
|
1362
|
+
|
|
1363
|
+
# Integration along the lineage: segment -> whole -> ensemble -> ensAvg
|
|
1364
|
+
# -> space, on one segment tree.
|
|
1365
|
+
|
|
1366
|
+
|
|
1367
|
+
def test_lineage_tseries_ensemble_average_matches_tree_values(
|
|
1368
|
+
segment_tree, ensemble_names
|
|
1369
|
+
):
|
|
1370
|
+
"""Two wholes 100 apart average to 1000e + 150 + 10s + k, var 5000."""
|
|
1371
|
+
paths = [str(p) for p in segment_tree.paths]
|
|
1372
|
+
wholes = whole_from_segments("gyrTMon", paths, ChainCyl, "bug",
|
|
1373
|
+
"tseries")
|
|
1374
|
+
ens = ensemble("gyrTMon", wholes, ChainCyl, "bug", "vector")
|
|
1375
|
+
avg = ensemble_avg("gyrTMon", ens, "bug", "dataframe")
|
|
1376
|
+
ens_1 = avg[ensemble_names[1]]
|
|
1377
|
+
np.testing.assert_allclose(
|
|
1378
|
+
ens_1["gyrTMon-mean"],
|
|
1379
|
+
[1160.0, 1161.0, 1162.0, 1170.0, 1171.0, 1172.0],
|
|
1380
|
+
)
|
|
1381
|
+
# var (ddof=1) of two values 100 apart is 100**2 / 2; sem is
|
|
1382
|
+
# sqrt(5000 / 2). Float64 round-off on values of order 1e3.
|
|
1383
|
+
np.testing.assert_allclose(ens_1["gyrTMon-var"], 5000.0, rtol=1e-12)
|
|
1384
|
+
np.testing.assert_allclose(ens_1["gyrTMon-sem"], 50.0, rtol=1e-12)
|
|
1385
|
+
|
|
1386
|
+
|
|
1387
|
+
def test_lineage_saved_ens_avg_feeds_space_tseries(tmp_path, segment_tree):
|
|
1388
|
+
"""ensAvg files saved by ensemble_avg are the files space_tseries reads."""
|
|
1389
|
+
ens_avg_dir = tmp_path / "analysis" / "N20-bug-ensAvg"
|
|
1390
|
+
ens_avg_dir.mkdir(parents=True)
|
|
1391
|
+
paths = [str(p) for p in segment_tree.paths]
|
|
1392
|
+
wholes = whole_from_segments("gyrTMon", paths, ChainCyl, "bug",
|
|
1393
|
+
"tseries")
|
|
1394
|
+
ens = ensemble("gyrTMon", wholes, ChainCyl, "bug", "vector")
|
|
1395
|
+
ensemble_avg("gyrTMon", ens, "bug", "dataframe", save_to=ens_avg_dir)
|
|
1396
|
+
space = space_tseries(str(ens_avg_dir) + "/", "gyrTMon", ChainCyl,
|
|
1397
|
+
"N*", [], "bug")
|
|
1398
|
+
# 2 ensembles x 6 frames; ensemble 2 is 1000 above ensemble 1
|
|
1399
|
+
np.testing.assert_allclose(
|
|
1400
|
+
space["gyrTMon-mean"],
|
|
1401
|
+
[1160.0, 1161.0, 1162.0, 1170.0, 1171.0, 1172.0,
|
|
1402
|
+
2160.0, 2161.0, 2162.0, 2170.0, 2171.0, 2172.0],
|
|
1403
|
+
)
|
|
1404
|
+
# bdump = 1000, the dump period of the bug trajectory, in the tree's
|
|
1405
|
+
# filenames
|
|
1406
|
+
np.testing.assert_array_equal(
|
|
1407
|
+
space["t_index"], np.tile(np.arange(6), 2) * 1000
|
|
1408
|
+
)
|
|
1409
|
+
|
|
1410
|
+
|
|
1411
|
+
def test_lineage_histogram_conserves_total_counts(segment_tree):
|
|
1412
|
+
"""Histogram counts summed over a space equal those over its segments."""
|
|
1413
|
+
paths = [str(p) for p in segment_tree.paths]
|
|
1414
|
+
wholes = whole_from_segments("gyrTMon", paths, ChainCyl, "bug",
|
|
1415
|
+
"histogram")
|
|
1416
|
+
ens = ensemble("gyrTMon", wholes, ChainCyl, "bug", "vector")
|
|
1417
|
+
avg = ensemble_avg("gyrTMon", ens, "bug", "dataframe")
|
|
1418
|
+
# Ensemble mean times 2 wholes, summed over bins and ensembles
|
|
1419
|
+
total = 2 * pd.concat(avg.values())["gyrTMon-mean"].sum()
|
|
1420
|
+
# Sum of 1000e + 100w + 10s + k over e, w, s in {1, 2} and k in
|
|
1421
|
+
# {0, 1, 2}: 24 * (1500 + 150 + 15) + 8 * (0 + 1 + 2)
|
|
1422
|
+
assert total == pytest.approx(39984.0, rel=1e-12)
|
|
1423
|
+
|
|
1424
|
+
|
|
1425
|
+
# Renaming the bulk-quantity columns of research-code tables
|
|
1426
|
+
|
|
1427
|
+
|
|
1428
|
+
@pytest.fixture
|
|
1429
|
+
def old_bulk_table():
|
|
1430
|
+
"""A stamp-like row with every research-code bulk name and two others."""
|
|
1431
|
+
columns = {"ensemble_long": ["N20ac1nc100"], "nmon": [20]}
|
|
1432
|
+
columns.update({old: [float(i)] for i, old in enumerate(BULK_NAME_MAP)})
|
|
1433
|
+
return pd.DataFrame(columns)
|
|
1434
|
+
|
|
1435
|
+
|
|
1436
|
+
@pytest.mark.parametrize(
|
|
1437
|
+
"old, new",
|
|
1438
|
+
[
|
|
1439
|
+
pytest.param("phi_m_bulk", "phi_bulk_m", id="phi_m"),
|
|
1440
|
+
pytest.param("rho_m_bulk", "rho_bulk_m", id="rho_m"),
|
|
1441
|
+
pytest.param("phi_c_bulk", "phi_bulk_c", id="phi_c"),
|
|
1442
|
+
pytest.param("rho_c_bulk", "rho_bulk_c", id="rho_c"),
|
|
1443
|
+
pytest.param("phi_c_bulk_one", "phi_bulk_c_one", id="phi_c_one"),
|
|
1444
|
+
pytest.param("phi_c_bulk_two", "phi_bulk_c_two", id="phi_c_two"),
|
|
1445
|
+
pytest.param("rho_c_bulk_one", "rho_bulk_c_one", id="rho_c_one"),
|
|
1446
|
+
pytest.param("rho_c_bulk_two", "rho_bulk_c_two", id="rho_c_two"),
|
|
1447
|
+
pytest.param("phi_hns_bulk", "phi_bulk_hns", id="phi_hns"),
|
|
1448
|
+
pytest.param("rho_hns_bulk", "rho_bulk_hns", id="rho_hns"),
|
|
1449
|
+
pytest.param("phi_c_bulk_round", "phi_bulk_c_round", id="phi_c_round"),
|
|
1450
|
+
],
|
|
1451
|
+
)
|
|
1452
|
+
def test_bulk_name_map_pairs_each_old_name_with_its_new_name(old, new):
|
|
1453
|
+
"""Every research-code bulk name maps to its <quantity>_bulk_<species>."""
|
|
1454
|
+
assert BULK_NAME_MAP[old] == new
|
|
1455
|
+
|
|
1456
|
+
|
|
1457
|
+
def test_bulk_name_map_has_only_the_eleven_pairs():
|
|
1458
|
+
"""The map holds exactly the eleven research-code names and no alias."""
|
|
1459
|
+
assert len(BULK_NAME_MAP) == 11
|
|
1460
|
+
|
|
1461
|
+
|
|
1462
|
+
def test_bulk_name_map_is_read_only():
|
|
1463
|
+
"""The map rejects assignment."""
|
|
1464
|
+
with pytest.raises(TypeError, match="does not support item assignment"):
|
|
1465
|
+
BULK_NAME_MAP["phi_c_bulk"] = "other" # type: ignore[index]
|
|
1466
|
+
|
|
1467
|
+
|
|
1468
|
+
def test_rename_bulk_columns_renames_every_mapped_column(old_bulk_table):
|
|
1469
|
+
"""Each mapped column takes its new name and keeps its value."""
|
|
1470
|
+
new = rename_bulk_columns(old_bulk_table)
|
|
1471
|
+
for old, expected in BULK_NAME_MAP.items():
|
|
1472
|
+
assert old not in new.columns
|
|
1473
|
+
assert new[expected].tolist() == old_bulk_table[old].tolist()
|
|
1474
|
+
|
|
1475
|
+
|
|
1476
|
+
def test_rename_bulk_columns_keeps_other_columns_and_order(old_bulk_table):
|
|
1477
|
+
"""Unmapped columns and the column order are unchanged."""
|
|
1478
|
+
new = rename_bulk_columns(old_bulk_table)
|
|
1479
|
+
expected = [BULK_NAME_MAP.get(c, c) for c in old_bulk_table.columns]
|
|
1480
|
+
assert list(new.columns) == expected
|
|
1481
|
+
assert new["ensemble_long"].tolist() == ["N20ac1nc100"]
|
|
1482
|
+
assert new["nmon"].tolist() == [20]
|
|
1483
|
+
|
|
1484
|
+
|
|
1485
|
+
def test_rename_bulk_columns_does_not_modify_its_input(old_bulk_table):
|
|
1486
|
+
"""The input table keeps the research-code names after the call."""
|
|
1487
|
+
before = list(old_bulk_table.columns)
|
|
1488
|
+
rename_bulk_columns(old_bulk_table)
|
|
1489
|
+
assert list(old_bulk_table.columns) == before
|
|
1490
|
+
|
|
1491
|
+
|
|
1492
|
+
def test_rename_bulk_columns_leaves_new_names_unchanged(old_bulk_table):
|
|
1493
|
+
"""A table that already carries the PolyPhys names comes back equal."""
|
|
1494
|
+
new = rename_bulk_columns(old_bulk_table)
|
|
1495
|
+
pd.testing.assert_frame_equal(rename_bulk_columns(new), new)
|