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,498 @@
|
|
|
1
|
+
"""Tests for :mod:`polyphys.manage.utils`.
|
|
2
|
+
|
|
3
|
+
The string and filename expectations are worked out by hand from each
|
|
4
|
+
function's stated rule. The bulk-quantity expectations are closed forms: the
|
|
5
|
+
number density ``n / v_avail``, with ``v_avail = (l - d)^3`` in a cube and
|
|
6
|
+
``pi (l - d) (D - d)^2 / 4`` in a cylinder (``l`` in place of ``l - d``
|
|
7
|
+
under periodic boundaries), and the volume fraction ``rho pi d^3 / 6``. They
|
|
8
|
+
are compared at ``rel=1e-12``, far above the float64 round-off of the few
|
|
9
|
+
operations behind them (about 1e-16).
|
|
10
|
+
"""
|
|
11
|
+
import gzip
|
|
12
|
+
import re
|
|
13
|
+
from pathlib import PosixPath
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
from polyphys.manage.utils import (
|
|
19
|
+
read_camel_case,
|
|
20
|
+
to_float_if_possible,
|
|
21
|
+
split_alphanumeric,
|
|
22
|
+
normalize_exts,
|
|
23
|
+
sort_filenames,
|
|
24
|
+
openany,
|
|
25
|
+
openany_context,
|
|
26
|
+
round_down_first_non_zero,
|
|
27
|
+
round_to_multiple,
|
|
28
|
+
invalid_keyword,
|
|
29
|
+
number_density_cube,
|
|
30
|
+
volume_fraction_cube,
|
|
31
|
+
number_density_cylinder,
|
|
32
|
+
volume_fraction_cylinder
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def make_text_file(tmp_path):
|
|
38
|
+
"""Return a factory that writes text to a file in ``tmp_path`` and
|
|
39
|
+
returns its path; a name ending in ``.gz`` is written gzipped."""
|
|
40
|
+
def _make(name, content):
|
|
41
|
+
path = tmp_path / name
|
|
42
|
+
opener = gzip.open if name.endswith(".gz") else open
|
|
43
|
+
with opener(path, "wt") as handle:
|
|
44
|
+
handle.write(content)
|
|
45
|
+
return str(path)
|
|
46
|
+
return _make
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@pytest.mark.parametrize(
|
|
50
|
+
"string,expected",
|
|
51
|
+
[
|
|
52
|
+
pytest.param("camelCase", ["camel", "Case"], id="lower-camel"),
|
|
53
|
+
pytest.param(
|
|
54
|
+
"CamelCaseString", ["Camel", "Case", "String"], id="upper-camel"
|
|
55
|
+
),
|
|
56
|
+
pytest.param("CAMELCase", ["CAMEL", "Case"], id="leading-acronym"),
|
|
57
|
+
pytest.param("CamelCASE", ["Camel", "CASE"], id="trailing-acronym"),
|
|
58
|
+
pytest.param("camel", ["camel"], id="one-lower-word"),
|
|
59
|
+
pytest.param("CAMEL", ["CAMEL"], id="one-acronym"),
|
|
60
|
+
],
|
|
61
|
+
)
|
|
62
|
+
def test_read_camel_case_splits_at_case_changes(string, expected):
|
|
63
|
+
"""A camel-case string splits into its words, and a run of capitals is
|
|
64
|
+
kept as one word."""
|
|
65
|
+
assert read_camel_case(string) == expected
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@pytest.mark.parametrize(
|
|
69
|
+
"string,expected",
|
|
70
|
+
[
|
|
71
|
+
pytest.param("3.14", 3.14, id="decimal"),
|
|
72
|
+
pytest.param("42", 42.0, id="integer"),
|
|
73
|
+
pytest.param("abc", "abc", id="not-a-number"),
|
|
74
|
+
],
|
|
75
|
+
)
|
|
76
|
+
def test_to_float_if_possible_converts_numbers_only(string, expected):
|
|
77
|
+
"""A numeric string becomes a float and any other string comes back
|
|
78
|
+
unchanged."""
|
|
79
|
+
assert to_float_if_possible(string) == expected
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.parametrize(
|
|
83
|
+
"string,expected",
|
|
84
|
+
[
|
|
85
|
+
pytest.param(
|
|
86
|
+
"file20.5name10", ["file", 20.5, "name", 10], id="float-and-int"
|
|
87
|
+
),
|
|
88
|
+
pytest.param("abc123def", ["abc", 123, "def"], id="int-inside"),
|
|
89
|
+
pytest.param("123.45file", [123.45, "file"], id="leading-float"),
|
|
90
|
+
],
|
|
91
|
+
)
|
|
92
|
+
def test_split_alphanumeric_separates_numbers_from_letters(string, expected):
|
|
93
|
+
"""Runs of digits become ints or floats and the letters between them stay
|
|
94
|
+
strings, in order."""
|
|
95
|
+
assert split_alphanumeric(string) == expected
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
@pytest.mark.parametrize(
|
|
99
|
+
"exts,expected",
|
|
100
|
+
[
|
|
101
|
+
pytest.param("npy", (".npy",), id="str-without-dot"),
|
|
102
|
+
pytest.param(".trj", (".trj",), id="str-with-dot"),
|
|
103
|
+
pytest.param(
|
|
104
|
+
("trj", ".lammpstrj"), (".trj", ".lammpstrj"), id="tuple-mixed"
|
|
105
|
+
),
|
|
106
|
+
],
|
|
107
|
+
)
|
|
108
|
+
def test_normalize_exts_gives_dot_prefixed_tuple(exts, expected):
|
|
109
|
+
"""An extension or a tuple of them becomes a tuple of extensions, each
|
|
110
|
+
with exactly one leading dot."""
|
|
111
|
+
assert normalize_exts(exts) == expected
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@pytest.mark.parametrize(
|
|
115
|
+
"filenames,formats,expected",
|
|
116
|
+
[
|
|
117
|
+
pytest.param(
|
|
118
|
+
["file2.trj", "file1.trj", "file1.data", "file2.data",
|
|
119
|
+
"file3.lammpstrj", "file3.data"],
|
|
120
|
+
["data", ("trj", "lammpstrj")],
|
|
121
|
+
[
|
|
122
|
+
(PosixPath("file1.data"), PosixPath("file1.trj")),
|
|
123
|
+
(PosixPath("file2.data"), PosixPath("file2.trj")),
|
|
124
|
+
(PosixPath("file3.data"), PosixPath("file3.lammpstrj")),
|
|
125
|
+
],
|
|
126
|
+
id="equivalent-extensions",
|
|
127
|
+
),
|
|
128
|
+
pytest.param(
|
|
129
|
+
[], ["data", ("trj", "lammpstrj")], [], id="empty-input"
|
|
130
|
+
),
|
|
131
|
+
pytest.param(
|
|
132
|
+
["file1.data", "file2.data", "file3.data"],
|
|
133
|
+
["data"],
|
|
134
|
+
[
|
|
135
|
+
(PosixPath("file1.data"),),
|
|
136
|
+
(PosixPath("file2.data"),),
|
|
137
|
+
(PosixPath("file3.data"),),
|
|
138
|
+
],
|
|
139
|
+
id="one-format",
|
|
140
|
+
),
|
|
141
|
+
pytest.param(
|
|
142
|
+
["file1.data", "file1.trj", "file1.npy",
|
|
143
|
+
"file2.npy", "file2.trj", "file2.data"],
|
|
144
|
+
["data", "trj", "npy"],
|
|
145
|
+
[
|
|
146
|
+
(PosixPath("file1.data"), PosixPath("file1.trj"),
|
|
147
|
+
PosixPath("file1.npy")),
|
|
148
|
+
(PosixPath("file2.data"), PosixPath("file2.trj"),
|
|
149
|
+
PosixPath("file2.npy")),
|
|
150
|
+
],
|
|
151
|
+
id="slot-order-follows-formats",
|
|
152
|
+
),
|
|
153
|
+
pytest.param(
|
|
154
|
+
["sample1.0.tar.gz", "sample1.0.gz",
|
|
155
|
+
"sample2.0.tar.gz", "sample2.0.gz"],
|
|
156
|
+
["tar.gz", "gz"],
|
|
157
|
+
[
|
|
158
|
+
(PosixPath("sample1.0.tar.gz"), PosixPath("sample1.0.gz")),
|
|
159
|
+
(PosixPath("sample2.0.tar.gz"), PosixPath("sample2.0.gz")),
|
|
160
|
+
],
|
|
161
|
+
id="compound-extension-distinct",
|
|
162
|
+
),
|
|
163
|
+
pytest.param(
|
|
164
|
+
["a.data", "b.DATA"],
|
|
165
|
+
["DATA"],
|
|
166
|
+
[(PosixPath("b.DATA"),)],
|
|
167
|
+
id="case-sensitive",
|
|
168
|
+
),
|
|
169
|
+
],
|
|
170
|
+
)
|
|
171
|
+
def test_sort_filenames_pairs_by_basename_in_natural_order(
|
|
172
|
+
filenames, formats, expected
|
|
173
|
+
):
|
|
174
|
+
"""Files sharing a basename are grouped one per format, in the order of
|
|
175
|
+
the formats, and the groups are sorted naturally by basename."""
|
|
176
|
+
assert sort_filenames(filenames, formats) == expected
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_sort_filenames_drops_incomplete_basename_with_warning():
|
|
180
|
+
"""A basename missing one of its formats is left out, with a warning that
|
|
181
|
+
names the missing counterpart."""
|
|
182
|
+
files = ["file2.trj", "file1.trj", "file1.data", "file3.lammpstrj",
|
|
183
|
+
"file3.data"]
|
|
184
|
+
expected_message = (
|
|
185
|
+
"'file2.trj' does not have matching 'file2.data' so it is ignored"
|
|
186
|
+
)
|
|
187
|
+
with pytest.warns(UserWarning, match=re.escape(expected_message)):
|
|
188
|
+
result = sort_filenames(files, ["data", ("trj", "lammpstrj")])
|
|
189
|
+
assert result == [
|
|
190
|
+
(PosixPath("file1.data"), PosixPath("file1.trj")),
|
|
191
|
+
(PosixPath("file3.data"), PosixPath("file3.lammpstrj")),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def test_sort_filenames_missing_format_group_gives_empty_with_warning():
|
|
196
|
+
"""When no file matches one format group, every basename is incomplete,
|
|
197
|
+
so the result is empty and each file draws a warning."""
|
|
198
|
+
files = ["file1.data", "file2.data", "file3.data"]
|
|
199
|
+
with pytest.warns(UserWarning) as record:
|
|
200
|
+
result = sort_filenames(files, ["data", ("trj", "lammpstrj")])
|
|
201
|
+
assert result == []
|
|
202
|
+
assert [str(warning.message) for warning in record] == [
|
|
203
|
+
"'file1.data' does not have matching 'file1.trj' so it is ignored",
|
|
204
|
+
"'file2.data' does not have matching 'file2.trj' so it is ignored",
|
|
205
|
+
"'file3.data' does not have matching 'file3.trj' so it is ignored",
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_sort_filenames_rejects_two_files_in_one_slot():
|
|
210
|
+
"""Two equivalent extensions for one basename fill the same slot twice,
|
|
211
|
+
which is rejected."""
|
|
212
|
+
files = ["file1.trj", "file1.data", "file1.lammpstrj"]
|
|
213
|
+
with pytest.raises(
|
|
214
|
+
ValueError, match="Duplicate files for basename 'file1'"
|
|
215
|
+
):
|
|
216
|
+
sort_filenames(files, ["data", ("trj", "lammpstrj")])
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_sort_filenames_rejects_extension_in_two_format_groups():
|
|
220
|
+
"""An extension listed in two format groups is ambiguous and
|
|
221
|
+
rejected."""
|
|
222
|
+
formats = ["data", ("trj", "data")]
|
|
223
|
+
files = ["file1.trj", "file1.data"]
|
|
224
|
+
expected = (
|
|
225
|
+
"Extension '.data' appears in multiple format groups: "
|
|
226
|
+
"('.data',) and ('.trj', '.data')"
|
|
227
|
+
)
|
|
228
|
+
with pytest.raises(ValueError, match=re.escape(expected)):
|
|
229
|
+
sort_filenames(files, formats)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def test_sort_filenames_keeps_names_that_differ_after_last_dot():
|
|
233
|
+
"""Two names that differ only after their last dot, as every HnsCyl name
|
|
234
|
+
ending in ``epshc1.0nc<n>`` does, stay two groups instead of colliding
|
|
235
|
+
on the part before the dot, with the format the space collectors pass."""
|
|
236
|
+
files = ["N200epshc1.0nc500-nucleoid-rhoMon-ensAvg.csv",
|
|
237
|
+
"N200epshc1.0nc0-nucleoid-rhoMon-ensAvg.csv"]
|
|
238
|
+
|
|
239
|
+
result = sort_filenames(files, ["csv"])
|
|
240
|
+
|
|
241
|
+
assert result == [
|
|
242
|
+
(PosixPath("N200epshc1.0nc0-nucleoid-rhoMon-ensAvg.csv"),),
|
|
243
|
+
(PosixPath("N200epshc1.0nc500-nucleoid-rhoMon-ensAvg.csv"),),
|
|
244
|
+
]
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def test_sort_filenames_pairs_formats_by_full_name():
|
|
248
|
+
"""A .data file pairs with the .lammpstrj file of the same full name, not
|
|
249
|
+
with one that shares only the part before the last dot."""
|
|
250
|
+
files = ["N200D10.0ac2.0nc500.lammpstrj", "N200D10.0ac2.0nc0.data",
|
|
251
|
+
"N200D10.0ac2.0nc500.data", "N200D10.0ac2.0nc0.lammpstrj"]
|
|
252
|
+
|
|
253
|
+
result = sort_filenames(files, ["data", "lammpstrj"])
|
|
254
|
+
|
|
255
|
+
assert result == [
|
|
256
|
+
(PosixPath("N200D10.0ac2.0nc0.data"),
|
|
257
|
+
PosixPath("N200D10.0ac2.0nc0.lammpstrj")),
|
|
258
|
+
(PosixPath("N200D10.0ac2.0nc500.data"),
|
|
259
|
+
PosixPath("N200D10.0ac2.0nc500.lammpstrj")),
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def test_sort_filenames_does_not_pair_different_full_names():
|
|
264
|
+
"""A .data and a .lammpstrj file whose names differ after the last dot
|
|
265
|
+
are both incomplete: no pair, and a warning for each."""
|
|
266
|
+
files = ["N200D10.0ac2.0nc0.data", "N200D10.0ac2.0nc500.lammpstrj"]
|
|
267
|
+
|
|
268
|
+
with pytest.warns(UserWarning) as record:
|
|
269
|
+
result = sort_filenames(files, ["data", "lammpstrj"])
|
|
270
|
+
|
|
271
|
+
assert result == []
|
|
272
|
+
assert [str(warning.message) for warning in record] == [
|
|
273
|
+
"'N200D10.0ac2.0nc0.data' does not have matching "
|
|
274
|
+
"'N200D10.0ac2.0nc0.lammpstrj' so it is ignored",
|
|
275
|
+
"'N200D10.0ac2.0nc500.lammpstrj' does not have matching "
|
|
276
|
+
"'N200D10.0ac2.0nc500.data' so it is ignored",
|
|
277
|
+
]
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
@pytest.mark.parametrize(
|
|
281
|
+
"name,content",
|
|
282
|
+
[
|
|
283
|
+
pytest.param("plain.txt", "test content", id="plain"),
|
|
284
|
+
pytest.param("packed.txt.gz", "gzipped content", id="gzipped"),
|
|
285
|
+
],
|
|
286
|
+
)
|
|
287
|
+
def test_openany_reads_plain_and_gzipped_files(make_text_file, name, content):
|
|
288
|
+
"""A plain file and a gzipped one both read back as their text."""
|
|
289
|
+
path = make_text_file(name, content)
|
|
290
|
+
with openany(path) as handle:
|
|
291
|
+
text = handle.read()
|
|
292
|
+
assert text == content
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
@pytest.mark.parametrize(
|
|
296
|
+
"name,content",
|
|
297
|
+
[
|
|
298
|
+
pytest.param("plain.txt", "test content", id="plain"),
|
|
299
|
+
pytest.param("packed.txt.gz", "gzipped content", id="gzipped"),
|
|
300
|
+
],
|
|
301
|
+
)
|
|
302
|
+
def test_openany_context_reads_plain_and_gzipped_files(
|
|
303
|
+
make_text_file, name, content
|
|
304
|
+
):
|
|
305
|
+
"""Inside the context, a plain file and a gzipped one both read back as
|
|
306
|
+
their text."""
|
|
307
|
+
path = make_text_file(name, content)
|
|
308
|
+
with openany_context(path) as handle:
|
|
309
|
+
text = handle.read()
|
|
310
|
+
assert text == content
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def test_openany_context_lets_body_error_propagate(make_text_file, capsys):
|
|
314
|
+
"""An error raised in the with-block propagates unchanged, unprinted."""
|
|
315
|
+
path = make_text_file("plain.txt", "test content")
|
|
316
|
+
with pytest.raises(RuntimeError, match="raised in the body"):
|
|
317
|
+
with openany_context(path):
|
|
318
|
+
raise RuntimeError("raised in the body")
|
|
319
|
+
assert capsys.readouterr().out == ""
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
@pytest.mark.parametrize(
|
|
323
|
+
"num,expected",
|
|
324
|
+
[
|
|
325
|
+
pytest.param(1234, 1000, id="integer"),
|
|
326
|
+
pytest.param(0.0456, 0.04, id="fraction"),
|
|
327
|
+
pytest.param(0, 0, id="zero"),
|
|
328
|
+
],
|
|
329
|
+
)
|
|
330
|
+
def test_round_down_first_non_zero_keeps_leading_digit(num, expected):
|
|
331
|
+
"""A number rounds down to its leading digit times its power of ten, and
|
|
332
|
+
zero stays zero."""
|
|
333
|
+
result = round_down_first_non_zero(num)
|
|
334
|
+
# float64 round-off left after rounding; atol for the zero case:
|
|
335
|
+
assert result == pytest.approx(expected, rel=1e-12, abs=1e-12)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
@pytest.mark.parametrize(
|
|
339
|
+
"dividend,divider,round_to,expected",
|
|
340
|
+
[
|
|
341
|
+
pytest.param(52, 10, 0, 50.0, id="integers-down"),
|
|
342
|
+
pytest.param(49.5, 0.1, 1, 49.5, id="already-a-multiple"),
|
|
343
|
+
pytest.param(7, 3, 1, 6.0, id="integers-down-to-6"),
|
|
344
|
+
pytest.param(0.0874, 0.025, 3, 0.075, id="phi-below-midpoint"),
|
|
345
|
+
pytest.param(0.0876, 0.025, 3, 0.1, id="phi-above-midpoint"),
|
|
346
|
+
pytest.param(0.1001, 0.025, 3, 0.1, id="phi-just-above-multiple"),
|
|
347
|
+
pytest.param(0.11, 0.025, 3, 0.1, id="label-below-value"),
|
|
348
|
+
],
|
|
349
|
+
)
|
|
350
|
+
def test_round_to_multiple_rounds_to_nearest_multiple(
|
|
351
|
+
dividend, divider, round_to, expected
|
|
352
|
+
):
|
|
353
|
+
"""The result is the multiple of divider nearest to dividend, which can
|
|
354
|
+
lie below dividend."""
|
|
355
|
+
result = round_to_multiple(dividend, divider, round_to)
|
|
356
|
+
# float64 round-off left after rounding to round_to decimals:
|
|
357
|
+
assert result == pytest.approx(expected, rel=0, abs=1e-12)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
@pytest.mark.parametrize(
|
|
361
|
+
"dividend,divider,round_to,expected",
|
|
362
|
+
[
|
|
363
|
+
pytest.param(2.5, 1.0, 0, 2.0, id="tie-down-to-even"),
|
|
364
|
+
pytest.param(3.5, 1.0, 0, 4.0, id="tie-up-to-even"),
|
|
365
|
+
pytest.param(0.0625, 0.025, 3, 0.05, id="phi-tie-to-even"),
|
|
366
|
+
],
|
|
367
|
+
)
|
|
368
|
+
def test_round_to_multiple_breaks_ties_to_even(
|
|
369
|
+
dividend, divider, round_to, expected
|
|
370
|
+
):
|
|
371
|
+
"""A dividend exactly halfway between two multiples goes to the even
|
|
372
|
+
multiple, as Python's round does."""
|
|
373
|
+
result = round_to_multiple(dividend, divider, round_to)
|
|
374
|
+
# float64 round-off left after rounding to round_to decimals:
|
|
375
|
+
assert result == pytest.approx(expected, rel=0, abs=1e-12)
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_invalid_keyword_accepts_valid_keyword():
|
|
379
|
+
"""A keyword in the valid list passes without raising."""
|
|
380
|
+
assert invalid_keyword("opt1", ["opt1", "opt2"]) is None
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def test_invalid_keyword_raises_with_custom_message():
|
|
384
|
+
"""A keyword outside the valid list raises with the given message
|
|
385
|
+
appended to the quoted keyword."""
|
|
386
|
+
with pytest.raises(ValueError, match=re.escape("'invalid' is wrong.")):
|
|
387
|
+
invalid_keyword("invalid", ["opt1", "opt2"], message=" is wrong.")
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@pytest.mark.parametrize(
|
|
391
|
+
"pbc,expected",
|
|
392
|
+
[
|
|
393
|
+
pytest.param(False, 1000 / 9**3, id="walls"),
|
|
394
|
+
pytest.param(True, 1000 / 10**3, id="periodic"),
|
|
395
|
+
],
|
|
396
|
+
)
|
|
397
|
+
def test_number_density_cube_divides_by_available_volume(pbc, expected):
|
|
398
|
+
"""1000 particles of diameter 1 in a cube of side 10 have the density
|
|
399
|
+
1000 / 9^3 with walls and 1000 / 10^3 under periodic boundaries."""
|
|
400
|
+
result = number_density_cube(1000, 1.0, 10.0, pbc=pbc)
|
|
401
|
+
# float64 round-off of a cube and a division:
|
|
402
|
+
assert result == pytest.approx(expected, rel=1e-12)
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
@pytest.mark.parametrize(
|
|
406
|
+
"pbc,expected",
|
|
407
|
+
[
|
|
408
|
+
pytest.param(False, 1000 / 9**3 * np.pi / 6, id="walls"),
|
|
409
|
+
pytest.param(True, 1000 / 10**3 * np.pi / 6, id="periodic"),
|
|
410
|
+
],
|
|
411
|
+
)
|
|
412
|
+
def test_volume_fraction_cube_is_density_times_bead_volume(pbc, expected):
|
|
413
|
+
"""The volume fraction is the number density times pi d^3 / 6."""
|
|
414
|
+
result = volume_fraction_cube(1000, 1.0, 10.0, pbc=pbc)
|
|
415
|
+
# float64 round-off of a few products and divisions:
|
|
416
|
+
assert result == pytest.approx(expected, rel=1e-12)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def test_volume_fraction_cube_allows_nominal_phi_above_one():
|
|
420
|
+
"""1000 unit beads in a cube of side 5 with walls give the nominal
|
|
421
|
+
fraction 1000 / 4^3 * pi / 6, above one, instead of raising."""
|
|
422
|
+
result = volume_fraction_cube(n_atom=1000, d_atom=1.0, l_cube=5.0)
|
|
423
|
+
# float64 round-off of a few products and divisions:
|
|
424
|
+
assert result == pytest.approx(1000 / 4**3 * np.pi / 6, rel=1e-12)
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
@pytest.mark.parametrize(
|
|
428
|
+
"d_atom,l_cube,pbc",
|
|
429
|
+
[
|
|
430
|
+
pytest.param(5.0, 5.0, False, id="as-wide-as-box"),
|
|
431
|
+
pytest.param(6.0, 5.0, False, id="wider-than-box"),
|
|
432
|
+
],
|
|
433
|
+
)
|
|
434
|
+
def test_number_density_cube_rejects_box_without_room(d_atom, l_cube, pbc):
|
|
435
|
+
"""A particle that leaves its center no room in a walled cube raises
|
|
436
|
+
ValueError instead of returning a meaningless density."""
|
|
437
|
+
with pytest.raises(ValueError, match="no room for particle centers"):
|
|
438
|
+
number_density_cube(10, d_atom, l_cube, pbc=pbc)
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
@pytest.mark.parametrize(
|
|
442
|
+
"pbc,expected",
|
|
443
|
+
[
|
|
444
|
+
pytest.param(False, 100 / (np.pi * 9 * 4**2 / 4), id="walls"),
|
|
445
|
+
pytest.param(True, 100 / (np.pi * 10 * 4**2 / 4), id="periodic"),
|
|
446
|
+
],
|
|
447
|
+
)
|
|
448
|
+
def test_number_density_cylinder_divides_by_available_volume(pbc, expected):
|
|
449
|
+
"""100 unit beads in a cylinder of length 10 and diameter 5 have the
|
|
450
|
+
density 100 / (pi 9 4^2 / 4) with end walls and 100 / (pi 10 4^2 / 4)
|
|
451
|
+
under periodic boundaries along the axis."""
|
|
452
|
+
result = number_density_cylinder(100, 1.0, 10.0, 5.0, pbc=pbc)
|
|
453
|
+
# float64 round-off of a few products and a division:
|
|
454
|
+
assert result == pytest.approx(expected, rel=1e-12)
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
@pytest.mark.parametrize(
|
|
458
|
+
"pbc,expected",
|
|
459
|
+
[
|
|
460
|
+
pytest.param(False, 100 / 216, id="walls"),
|
|
461
|
+
pytest.param(True, 100 / 240, id="periodic"),
|
|
462
|
+
],
|
|
463
|
+
)
|
|
464
|
+
def test_volume_fraction_cylinder_is_density_times_bead_volume(pbc, expected):
|
|
465
|
+
"""The volume fraction is the number density times pi d^3 / 6, which
|
|
466
|
+
cancels pi: 100 / (9 * 4^2 * 6 / 4) with end walls and
|
|
467
|
+
100 / (10 * 4^2 * 6 / 4) under periodic boundaries."""
|
|
468
|
+
result = volume_fraction_cylinder(100, 1.0, 10.0, 5.0, pbc=pbc)
|
|
469
|
+
# float64 round-off of a few products and divisions:
|
|
470
|
+
assert result == pytest.approx(expected, rel=1e-12)
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
def test_volume_fraction_cylinder_allows_nominal_phi_above_one():
|
|
474
|
+
"""500 unit beads in a cylinder of length 5 and diameter 3 with end walls
|
|
475
|
+
give the nominal fraction 500 / (4 * 2^2 * 6 / 4) = 500 / 24, above one,
|
|
476
|
+
instead of raising."""
|
|
477
|
+
result = volume_fraction_cylinder(
|
|
478
|
+
n_atom=500, d_atom=1.0, l_cyl=5.0, d_cyl=3.0)
|
|
479
|
+
# float64 round-off of a few products and divisions:
|
|
480
|
+
assert result == pytest.approx(500 / 24, rel=1e-12)
|
|
481
|
+
|
|
482
|
+
|
|
483
|
+
@pytest.mark.parametrize(
|
|
484
|
+
"d_atom,l_cyl,d_cyl,pbc",
|
|
485
|
+
[
|
|
486
|
+
pytest.param(5.0, 20.0, 5.0, True, id="as-wide-as-cylinder"),
|
|
487
|
+
pytest.param(6.0, 20.0, 5.0, True, id="wider-than-cylinder"),
|
|
488
|
+
pytest.param(5.0, 20.0, 5.0, False, id="as-wide-with-end-walls"),
|
|
489
|
+
pytest.param(1.0, 1.0, 5.0, False, id="as-long-as-cylinder"),
|
|
490
|
+
],
|
|
491
|
+
)
|
|
492
|
+
def test_number_density_cylinder_rejects_cylinder_without_room(
|
|
493
|
+
d_atom, l_cyl, d_cyl, pbc):
|
|
494
|
+
"""A particle that leaves its center no room across or along the
|
|
495
|
+
cylinder raises ValueError instead of dividing by zero or returning a
|
|
496
|
+
density from a squared negative diameter."""
|
|
497
|
+
with pytest.raises(ValueError, match="no room for particle centers"):
|
|
498
|
+
number_density_cylinder(10, d_atom, l_cyl, d_cyl, pbc=pbc)
|