Skip to content

Commit fe30313

Browse files
committed
Merge branch 'main' into nmrpca-fix-egligibility
2 parents de10a41 + 054fb11 commit fe30313

9 files changed

Lines changed: 180 additions & 43 deletions

File tree

src/fairmd/lipids/analib/maicos.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
from maicos.lib.weights import density_weights
2626
from tqdm import tqdm
2727

28-
from fairmd.lipids.auxiliary.jsonEncoders import CompactJSONEncoder
28+
from fairmd.lipids.auxiliary import CompactJSONEncoder
2929
from fairmd.lipids.core import System
3030
from fairmd.lipids.molecules import lipids_set
3131

src/fairmd/lipids/analyze.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,7 @@
4343
traj_centering_for_maicos_mda_parallel,
4444
)
4545
from fairmd.lipids.api import UniverseConstructor, mda_gen_selection_mols
46-
from fairmd.lipids.auxiliary import mollib
47-
from fairmd.lipids.auxiliary.jsonEncoders import CompactJSONEncoder
46+
from fairmd.lipids.auxiliary import CompactJSONEncoder, mollib
4847
from fairmd.lipids.core import System
4948
from fairmd.lipids.molecules import lipids_set
5049

src/fairmd/lipids/api.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
import numpy as np
3434

3535
from fairmd.lipids import FMDL_SIMU_PATH
36+
from fairmd.lipids.auxiliary import block_average_time_series
3637
from fairmd.lipids.core import System
3738
from fairmd.lipids.databankio import download_resource_from_uri, resolve_file_url
3839
from fairmd.lipids.molecules import Molecule, lipids_set
@@ -218,13 +219,14 @@ def get_quality(
218219
return q
219220

220221

221-
def get_mean_ApL(system: System) -> float: # noqa: N802 (API name)
222+
def get_ApL_data(system: System, blocksize: float | None = None) -> np.ndarray: # noqa: N802 (API name)
222223
"""
223-
Calculate average area per lipid for a system.
224+
Return Area-per-lipid data as a numpy array (block-averaging possible).
224225
225-
:param system: Simulation object.
226+
:param system: Simulation object
227+
:param blocksize: Averaged t-series by <blocksize> ps
226228
227-
:return: area per lipid (Å^2)
229+
:return: Array (t, value) with blocksize step.
228230
"""
229231
path = os.path.join(FMDL_SIMU_PATH, system["path"], "apl.json")
230232
try:
@@ -236,8 +238,27 @@ def get_mean_ApL(system: System) -> float: # noqa: N802 (API name)
236238
except json.JSONDecodeError as e:
237239
msg = "Area per lipid data for system #{} in {} is invalid.".format(system["ID"], path)
238240
raise ValueError(msg) from e
239-
vals = np.array(list(data.values()))
240-
return vals.mean()
241+
df = np.vstack(
242+
[
243+
np.array(list(data.keys()), dtype=float),
244+
np.array(list(data.values()), dtype=float),
245+
]
246+
).T
247+
if blocksize is not None:
248+
df = block_average_time_series(df, blocksize)
249+
return df
250+
251+
252+
def get_mean_ApL(system: System) -> float: # noqa: N802 (API name)
253+
"""
254+
Calculate average area per lipid for a system.
255+
256+
:param system: Simulation object.
257+
258+
:return: area per lipid (Å^2)
259+
"""
260+
df = get_ApL_data(system)
261+
return df[:, 1].mean()
241262

242263

243264
def get_total_area(system: System) -> float:
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,9 @@
11
# file is required for fairmd.lipids.settings to be accessible as a subpackage
2+
3+
from .jsonEncoders import CompactJSONEncoder
4+
from .routines import block_average_time_series
5+
6+
__all__ = [
7+
"CompactJSONEncoder",
8+
"block_average_time_series",
9+
]

src/fairmd/lipids/auxiliary/jsonEncoders.py

Lines changed: 11 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
#!/usr/bin/env python3
1+
"""
2+
Compact JSON encoder class.
23
3-
#
4-
# Compact JSON from @jannismain is used here.
5-
# https://gist.github.com/jannismain/e96666ca4f059c3e5bc28abb711b5c92
6-
#
4+
:meta: private
5+
"""
76

87
import json
98
import math
@@ -12,7 +11,11 @@
1211

1312

1413
class CompactJSONEncoder(json.JSONEncoder):
15-
"""A JSON Encoder that puts small containers on single lines."""
14+
"""A JSON Encoder that puts small containers on single lines.
15+
16+
Originally taken from `the GiST of @jannismain
17+
<https://gist.github.com/jannismain/e96666ca4f059c3e5bc28abb711b5c92>`_.
18+
"""
1619

1720
CONTAINER_TYPES = (list, tuple, dict)
1821
"""Container datatypes include primitives or other containers."""
@@ -100,30 +103,5 @@ def indent_str(self) -> str:
100103
return " " * (self.indentation_level * self.indent)
101104
if isinstance(self.indent, str):
102105
return self.indentation_level * self.indent
103-
raise ValueError(
104-
f"indent must either be of type int or str (is: {type(self.indent)})",
105-
)
106-
107-
108-
if __name__ == "__main__":
109-
import sys
110-
111-
if "--example" in sys.argv:
112-
data = {
113-
"compact_object": {"first": "element", "second": 2},
114-
"compact_list": ["first", "second"],
115-
"long_list": [
116-
"this",
117-
"is",
118-
"a",
119-
"rather",
120-
"long\nlist",
121-
"and should be broken up because of its width",
122-
],
123-
"non_ascii": "汉语",
124-
1: 2,
125-
}
126-
json.dump(data, sys.stdout, cls=CompactJSONEncoder, ensure_ascii=False)
127-
exit()
128-
129-
json.dump(json.load(sys.stdin), sys.stdout, cls=CompactJSONEncoder)
106+
msg = f"Indent must either be of type int or str (is: {type(self.indent)})"
107+
raise ValueError(msg)
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
Algorithmic routines. Pure numpy vector stuff.
3+
4+
:meta: private
5+
"""
6+
7+
import numpy as np
8+
9+
10+
def block_average_time_series(arr: np.ndarray, blocksize: float) -> np.ndarray:
11+
"""
12+
Return block-average of (N,2) array.
13+
14+
:param arr: numpy array (N,2)
15+
:param blocksize: timeframe for slice blocks
16+
17+
:return: block-averaged array
18+
"""
19+
# arr: (N, 2) -> [time, value]
20+
t = arr[:, 0]
21+
if t[1] - t[0] > blocksize:
22+
msg = f"Blocksize ({blocksize}) must be greater than write timestep ({t[1] - t[0]})"
23+
raise ValueError(msg)
24+
x = arr[:, 1]
25+
26+
t0 = t.min()
27+
t1 = t.max()
28+
# Bin edges: [t0, t0+blocksize, t0+2*blocksize, ...]
29+
edges = np.arange(t0, t1 + blocksize + 1e-3, blocksize) # +1e-3 required to make t1 // blocksize working!
30+
nbins = len(edges) - 1
31+
32+
# Bin index for each sample: 0..len(edges)
33+
idx = np.digitize(t, edges) - 1
34+
35+
# Prepare output arrays
36+
sums = np.zeros(nbins)
37+
counts = np.zeros(nbins, dtype=int)
38+
39+
# Accumulate sums and counts per bin
40+
np.add.at(sums, idx, x)
41+
np.add.at(counts, idx, 1)
42+
43+
# Avoid division by zero
44+
mask = counts > 0
45+
avg_vals = np.empty(nbins)
46+
avg_vals[:] = np.nan
47+
avg_vals[mask] = sums[mask] / counts[mask]
48+
49+
# Define a representative time for each bin (e.g. center)
50+
bin_times = (edges[:-1] + edges[1:]) / 2.0
51+
52+
return np.column_stack((bin_times, avg_vals))

src/fairmd/lipids/bin/evaluate_quality.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
import fairmd.lipids.quality as qq
2424
from fairmd.lipids import FMDL_EXP_PATH, FMDL_SIMU_PATH
25-
from fairmd.lipids.auxiliary.jsonEncoders import CompactJSONEncoder
25+
from fairmd.lipids.auxiliary import CompactJSONEncoder
2626

2727

2828
def _round_quality_values(obj: dict | list, ndigits: int = 4) -> dict | list:

tests/test_api.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,29 @@ def test_get_mean_apl(systems, systemid, result):
119119
assert apm == pytest.approx(result, abs=6e-4)
120120

121121

122+
@pytest.mark.parametrize(
123+
"systemid, nlines",
124+
[
125+
(281, 1001),
126+
(566, 401), # 1
127+
],
128+
)
129+
def test_get_apl_data(systems, systemid, nlines):
130+
from fairmd.lipids.api import get_ApL_data
131+
132+
s = systems.loc(systemid)
133+
df = get_ApL_data(s)
134+
check.is_true(isinstance(df, np.ndarray))
135+
check.equal(df.shape[1], 2)
136+
check.equal(df.shape[0], nlines)
137+
# block-average behavior
138+
df1k = get_ApL_data(s, blocksize=1000)
139+
df2k = get_ApL_data(s, blocksize=2000)
140+
df3k = get_ApL_data(s, blocksize=3000)
141+
check.almost_equal(df1k[0:2, 1].mean(), df2k[0, 1], abs=1e-7)
142+
check.almost_equal(df1k[0:3, 1].mean(), df3k[0, 1], abs=1e-7)
143+
144+
122145
@pytest.mark.parametrize(
123146
"systemid, result",
124147
[(281, 4142.234), (566, 3923.568), (787, 4694.191), (243, 2241.920), (86, 3869.417)],

tests/test_misc.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,10 @@
1111
from contextlib import contextmanager
1212
import os
1313
import shutil
14+
import json
1415

16+
import numpy as np
17+
import numpy.testing as npt
1518
import pytest
1619
import pytest_check as check
1720

@@ -215,3 +218,56 @@ def test_get_tails():
215218
check.equal(len(tails), 4)
216219
check.is_in("sn-1 1", tails)
217220
check.is_in("sn-2 1", tails)
221+
222+
223+
def test_json_encoder(tmpdir):
224+
"""Fixture for a mock experiment path with no data files."""
225+
from fairmd.lipids.auxiliary import CompactJSONEncoder
226+
227+
exp_dir = tmpdir.mkdir("jsonenc")
228+
229+
data = {
230+
"compact_object": {"first": "element", "second": 2},
231+
"compact_list": ["first", "second"],
232+
"long_list": [
233+
"this",
234+
"is",
235+
"a",
236+
"rather",
237+
"long\nlist",
238+
"and should be broken up because of its width",
239+
],
240+
"non_ascii": "汉语",
241+
1: 2,
242+
}
243+
with open(exp_dir.join("test.yaml"), "w", encoding="utf-8") as fd:
244+
json.dump(data, fd, cls=CompactJSONEncoder, ensure_ascii=False)
245+
246+
247+
def test_average_block():
248+
from fairmd.lipids.auxiliary import block_average_time_series
249+
250+
# times 0..9, values = times
251+
t = np.arange(10, dtype=float)
252+
x = t.copy()
253+
arr = np.column_stack((t, x))
254+
255+
out = block_average_time_series(arr, blocksize=2.0)
256+
# expected bins: [0,2), [2,4), [4,6), [6,8), [8,10]
257+
expected_times = np.array([1, 3, 5, 7, 9], dtype=float)
258+
expected_vals = np.array(
259+
[
260+
(0 + 1) / 2,
261+
(2 + 3) / 2,
262+
(4 + 5) / 2,
263+
(6 + 7) / 2,
264+
(8 + 9) / 2,
265+
],
266+
dtype=float,
267+
)
268+
269+
npt.assert_allclose(out[:, 0], expected_times)
270+
npt.assert_allclose(out[:, 1], expected_vals)
271+
272+
arr = arr[:-1, :]
273+
out = block_average_time_series(arr, blocksize=2.0)

0 commit comments

Comments
 (0)