accelerometry-annotator 3.2.2__tar.gz → 3.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/PKG-INFO +3 -1
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/accelerometry_annotator.egg-info/PKG-INFO +3 -1
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/accelerometry_annotator.egg-info/SOURCES.txt +3 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/accelerometry_annotator.egg-info/requires.txt +3 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/pyproject.toml +1 -0
- accelerometry_annotator-3.3.0/tests/test_data_loading.py +212 -0
- accelerometry_annotator-3.3.0/tests/test_plotting.py +88 -0
- accelerometry_annotator-3.3.0/tests/test_state.py +93 -0
- accelerometry_annotator-3.3.0/visualize_accelerometry/__init__.py +1 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/data_loading.py +7 -4
- accelerometry_annotator-3.2.2/visualize_accelerometry/__init__.py +0 -1
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/LICENSE +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/README.md +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/accelerometry_annotator.egg-info/dependency_links.txt +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/accelerometry_annotator.egg-info/top_level.txt +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/setup.cfg +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/app.py +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/callbacks.py +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/config.py +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/js/download.js +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/plotting.py +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/state.py +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/static/favicon.ico +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/static/favicon.svg +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/static/logo-dark.svg +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/static/logo.jpg +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/static/logo.svg +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/templates/index.html +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/templates/login.html +0 -0
- {accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/templates/logout.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: accelerometry-annotator
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Web-based tool for visualizing and annotating wrist-worn accelerometry data from physical performance assessments.
|
|
5
5
|
Author-email: Manu Murugesan <manorathan@uchicago.edu>
|
|
6
6
|
License: MIT
|
|
@@ -33,6 +33,8 @@ Requires-Dist: openpyxl
|
|
|
33
33
|
Requires-Dist: jinja2
|
|
34
34
|
Provides-Extra: lttb
|
|
35
35
|
Requires-Dist: lttbc; extra == "lttb"
|
|
36
|
+
Provides-Extra: test
|
|
37
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
36
38
|
Dynamic: license-file
|
|
37
39
|
|
|
38
40
|
# Accelerometry Annotation Tool
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: accelerometry-annotator
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.3.0
|
|
4
4
|
Summary: Web-based tool for visualizing and annotating wrist-worn accelerometry data from physical performance assessments.
|
|
5
5
|
Author-email: Manu Murugesan <manorathan@uchicago.edu>
|
|
6
6
|
License: MIT
|
|
@@ -33,6 +33,8 @@ Requires-Dist: openpyxl
|
|
|
33
33
|
Requires-Dist: jinja2
|
|
34
34
|
Provides-Extra: lttb
|
|
35
35
|
Requires-Dist: lttbc; extra == "lttb"
|
|
36
|
+
Provides-Extra: test
|
|
37
|
+
Requires-Dist: pytest>=7.0; extra == "test"
|
|
36
38
|
Dynamic: license-file
|
|
37
39
|
|
|
38
40
|
# Accelerometry Annotation Tool
|
|
@@ -6,6 +6,9 @@ accelerometry_annotator.egg-info/SOURCES.txt
|
|
|
6
6
|
accelerometry_annotator.egg-info/dependency_links.txt
|
|
7
7
|
accelerometry_annotator.egg-info/requires.txt
|
|
8
8
|
accelerometry_annotator.egg-info/top_level.txt
|
|
9
|
+
tests/test_data_loading.py
|
|
10
|
+
tests/test_plotting.py
|
|
11
|
+
tests/test_state.py
|
|
9
12
|
visualize_accelerometry/__init__.py
|
|
10
13
|
visualize_accelerometry/app.py
|
|
11
14
|
visualize_accelerometry/callbacks.py
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Tests for visualize_accelerometry.data_loading."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from visualize_accelerometry.config import ANNOTATION_COLUMNS, TIME_FMT
|
|
10
|
+
from visualize_accelerometry.data_loading import (
|
|
11
|
+
clamp_anchor,
|
|
12
|
+
cleanup_annotations,
|
|
13
|
+
get_annotations_from_files,
|
|
14
|
+
get_filedata,
|
|
15
|
+
get_filenames,
|
|
16
|
+
save_annotations,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestGetFilenames:
|
|
21
|
+
"""Tests for file discovery and user assignment."""
|
|
22
|
+
|
|
23
|
+
def test_discovers_h5_files(self, patch_config, sample_h5):
|
|
24
|
+
fnames = get_filenames()
|
|
25
|
+
assert len(fnames) == 1
|
|
26
|
+
assert "900001-20230315093000" in fnames[0]
|
|
27
|
+
|
|
28
|
+
def test_deterministic_assignment(self, patch_config, sample_h5, second_h5):
|
|
29
|
+
"""Same seed produces same assignment across calls."""
|
|
30
|
+
first = get_filenames()
|
|
31
|
+
second = get_filenames()
|
|
32
|
+
assert first == second
|
|
33
|
+
|
|
34
|
+
def test_assigns_all_files(self, patch_config, sample_h5, second_h5):
|
|
35
|
+
fnames = get_filenames()
|
|
36
|
+
assert len(fnames) == 2
|
|
37
|
+
basenames = [f.split("--")[1] for f in fnames]
|
|
38
|
+
assert "900001-20230315093000" in basenames
|
|
39
|
+
assert "900002-20230316140000" in basenames
|
|
40
|
+
|
|
41
|
+
def test_format_is_user_dash_dash_filename(self, patch_config, sample_h5):
|
|
42
|
+
from visualize_accelerometry.config import ANNOTATOR_USERS
|
|
43
|
+
fnames = get_filenames()
|
|
44
|
+
for f in fnames:
|
|
45
|
+
parts = f.split("--")
|
|
46
|
+
assert len(parts) == 2
|
|
47
|
+
assert parts[0] in ANNOTATOR_USERS
|
|
48
|
+
|
|
49
|
+
def test_ignores_non_h5_files(self, patch_config, tmp_data_dir):
|
|
50
|
+
readings = tmp_data_dir / "readings"
|
|
51
|
+
(readings / "not_a_data_file.csv").write_text("x,y,z")
|
|
52
|
+
fnames = get_filenames()
|
|
53
|
+
assert len(fnames) == 0
|
|
54
|
+
|
|
55
|
+
def test_no_global_rng_pollution(self, patch_config, sample_h5):
|
|
56
|
+
"""Ensure get_filenames doesn't pollute global numpy random state."""
|
|
57
|
+
np.random.seed(123)
|
|
58
|
+
before = np.random.random()
|
|
59
|
+
np.random.seed(123)
|
|
60
|
+
get_filenames()
|
|
61
|
+
after = np.random.random()
|
|
62
|
+
assert before == after
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TestGetFiledata:
|
|
66
|
+
"""Tests for HDF5 time-window loading."""
|
|
67
|
+
|
|
68
|
+
def test_first_load_returns_bounds(self, sample_h5):
|
|
69
|
+
fname = str(sample_h5).replace(".h5", "")
|
|
70
|
+
anchor, start, end, pdf = get_filedata(fname, None, 3600)
|
|
71
|
+
assert anchor is not None
|
|
72
|
+
assert start is not None
|
|
73
|
+
assert end is not None
|
|
74
|
+
assert len(pdf) > 0
|
|
75
|
+
|
|
76
|
+
def test_subsequent_load_no_bounds(self, sample_h5):
|
|
77
|
+
fname = str(sample_h5).replace(".h5", "")
|
|
78
|
+
anchor, start, end, pdf = get_filedata(fname, None, 3600)
|
|
79
|
+
anchor2, start2, end2, pdf2 = get_filedata(fname, anchor, 3600)
|
|
80
|
+
assert start2 is None
|
|
81
|
+
assert end2 is None
|
|
82
|
+
|
|
83
|
+
def test_returns_expected_columns(self, sample_h5):
|
|
84
|
+
fname = str(sample_h5).replace(".h5", "")
|
|
85
|
+
_, _, _, pdf = get_filedata(fname, None, 3600)
|
|
86
|
+
assert "timestamp" in pdf.columns
|
|
87
|
+
assert "x" in pdf.columns
|
|
88
|
+
assert "y" in pdf.columns
|
|
89
|
+
assert "z" in pdf.columns
|
|
90
|
+
|
|
91
|
+
def test_windowed_query(self, sample_h5):
|
|
92
|
+
"""Small window should return fewer rows than the full file."""
|
|
93
|
+
fname = str(sample_h5).replace(".h5", "")
|
|
94
|
+
_, _, _, pdf_full = get_filedata(fname, None, 3600)
|
|
95
|
+
anchor = pdf_full["timestamp"].iloc[len(pdf_full) // 2].strftime(TIME_FMT)
|
|
96
|
+
_, _, _, pdf_small = get_filedata(fname, anchor, 1)
|
|
97
|
+
assert len(pdf_small) <= len(pdf_full)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
class TestClampAnchor:
|
|
101
|
+
"""Tests for anchor clamping logic."""
|
|
102
|
+
|
|
103
|
+
def test_anchor_within_bounds_unchanged(self):
|
|
104
|
+
start = "Mar 15 2023 09:00 AM"
|
|
105
|
+
end = "Mar 15 2023 10:00 AM"
|
|
106
|
+
anchor = "Mar 15 2023 09:30 AM"
|
|
107
|
+
result = clamp_anchor(anchor, start, end, 600)
|
|
108
|
+
assert result == anchor
|
|
109
|
+
|
|
110
|
+
def test_anchor_past_end_clamped(self):
|
|
111
|
+
start = "Mar 15 2023 09:00 AM"
|
|
112
|
+
end = "Mar 15 2023 10:00 AM"
|
|
113
|
+
anchor = "Mar 15 2023 10:30 AM"
|
|
114
|
+
result = clamp_anchor(anchor, start, end, 600)
|
|
115
|
+
from datetime import datetime
|
|
116
|
+
result_dt = datetime.strptime(result, TIME_FMT)
|
|
117
|
+
end_dt = datetime.strptime(end, TIME_FMT)
|
|
118
|
+
assert result_dt < end_dt
|
|
119
|
+
|
|
120
|
+
def test_anchor_before_start_clamped(self):
|
|
121
|
+
start = "Mar 15 2023 09:00 AM"
|
|
122
|
+
end = "Mar 15 2023 10:00 AM"
|
|
123
|
+
anchor = "Mar 15 2023 08:00 AM"
|
|
124
|
+
result = clamp_anchor(anchor, start, end, 600)
|
|
125
|
+
from datetime import datetime
|
|
126
|
+
result_dt = datetime.strptime(result, TIME_FMT)
|
|
127
|
+
start_dt = datetime.strptime(start, TIME_FMT)
|
|
128
|
+
assert result_dt > start_dt
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class TestAnnotations:
|
|
132
|
+
"""Tests for annotation I/O and cleanup."""
|
|
133
|
+
|
|
134
|
+
def test_load_annotations(self, patch_config, sample_annotations):
|
|
135
|
+
pdf = get_annotations_from_files()
|
|
136
|
+
assert len(pdf) == 2
|
|
137
|
+
assert set(pdf["artifact"]) == {"chair_stand", "tug"}
|
|
138
|
+
|
|
139
|
+
def test_load_empty_returns_empty_df(self, patch_config):
|
|
140
|
+
pdf = get_annotations_from_files()
|
|
141
|
+
assert len(pdf) == 0
|
|
142
|
+
assert list(pdf.columns) == ANNOTATION_COLUMNS
|
|
143
|
+
|
|
144
|
+
def test_cleanup_sorts_and_fills(self):
|
|
145
|
+
pdf = pd.DataFrame({
|
|
146
|
+
"fname": ["f1", "f2"],
|
|
147
|
+
"artifact": ["tug", "chair_stand"],
|
|
148
|
+
"segment": [1, np.nan],
|
|
149
|
+
"scoring": [np.nan, 1],
|
|
150
|
+
"review": [0, np.nan],
|
|
151
|
+
"start_epoch": [100, np.nan],
|
|
152
|
+
"end_epoch": [200, np.nan],
|
|
153
|
+
"start_time": pd.to_datetime(["2023-01-01", None]),
|
|
154
|
+
"end_time": pd.to_datetime(["2023-01-02", None]),
|
|
155
|
+
"annotated_at": pd.to_datetime(["2023-01-01", "2023-01-01"]),
|
|
156
|
+
"user": ["a", "b"],
|
|
157
|
+
"notes": [None, "test"],
|
|
158
|
+
})
|
|
159
|
+
result = cleanup_annotations(pdf)
|
|
160
|
+
assert result["segment"].isna().sum() == 0
|
|
161
|
+
assert result["scoring"].isna().sum() == 0
|
|
162
|
+
assert result["review"].isna().sum() == 0
|
|
163
|
+
assert result["notes"].dtype == object
|
|
164
|
+
|
|
165
|
+
def test_cleanup_adds_notes_column(self):
|
|
166
|
+
pdf = pd.DataFrame({
|
|
167
|
+
"fname": ["f1"],
|
|
168
|
+
"artifact": ["tug"],
|
|
169
|
+
"segment": [0], "scoring": [0], "review": [0],
|
|
170
|
+
"start_epoch": [100], "end_epoch": [200],
|
|
171
|
+
"start_time": pd.to_datetime(["2023-01-01"]),
|
|
172
|
+
"end_time": pd.to_datetime(["2023-01-02"]),
|
|
173
|
+
"annotated_at": pd.to_datetime(["2023-01-01"]),
|
|
174
|
+
"user": ["a"],
|
|
175
|
+
})
|
|
176
|
+
result = cleanup_annotations(pdf)
|
|
177
|
+
assert "notes" in result.columns
|
|
178
|
+
|
|
179
|
+
def test_save_and_reload(self, patch_config, sample_annotations):
|
|
180
|
+
pdf = get_annotations_from_files()
|
|
181
|
+
pdf = cleanup_annotations(pdf)
|
|
182
|
+
|
|
183
|
+
# Add a new annotation
|
|
184
|
+
new_row = pd.DataFrame({
|
|
185
|
+
"fname": ["900001-20230315093000"],
|
|
186
|
+
"artifact": ["3m_walk"],
|
|
187
|
+
"segment": [0], "scoring": [0], "review": [0],
|
|
188
|
+
"start_epoch": [1678872700.0], "end_epoch": [1678872720.0],
|
|
189
|
+
"start_time": pd.to_datetime(["2023-03-15 09:31:40"]),
|
|
190
|
+
"end_time": pd.to_datetime(["2023-03-15 09:32:00"]),
|
|
191
|
+
"annotated_at": pd.to_datetime(["2023-03-17 11:00:00"]),
|
|
192
|
+
"user": ["test_user"],
|
|
193
|
+
"notes": [""],
|
|
194
|
+
})
|
|
195
|
+
pdf = pd.concat([pdf, new_row], ignore_index=True)
|
|
196
|
+
|
|
197
|
+
result = save_annotations(
|
|
198
|
+
pdf, "test_user", "900001-20230315093000",
|
|
199
|
+
)
|
|
200
|
+
assert len(result) == 3
|
|
201
|
+
assert "3m_walk" in result["artifact"].values
|
|
202
|
+
|
|
203
|
+
def test_save_preserves_other_files(self, patch_config, sample_annotations):
|
|
204
|
+
"""Saving annotations for one file shouldn't delete annotations for other files."""
|
|
205
|
+
pdf = get_annotations_from_files()
|
|
206
|
+
pdf = cleanup_annotations(pdf)
|
|
207
|
+
|
|
208
|
+
# Save with only one file's annotations modified
|
|
209
|
+
result = save_annotations(
|
|
210
|
+
pdf, "test_user", "900001-20230315093000",
|
|
211
|
+
)
|
|
212
|
+
assert len(result) == 2
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Tests for visualize_accelerometry.plotting."""
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from bokeh.models import ColumnDataSource
|
|
8
|
+
|
|
9
|
+
from visualize_accelerometry.plotting import _downsample, make_plot, MAX_POINTS
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestDownsample:
|
|
13
|
+
"""Tests for the LTTB downsampling function."""
|
|
14
|
+
|
|
15
|
+
def test_passthrough_when_short(self):
|
|
16
|
+
ts = np.arange(100, dtype=np.float64)
|
|
17
|
+
vals = np.random.randn(100)
|
|
18
|
+
ds_ts, ds_vals = _downsample(ts, vals, 200)
|
|
19
|
+
np.testing.assert_array_equal(ds_ts, ts)
|
|
20
|
+
np.testing.assert_array_equal(ds_vals, vals)
|
|
21
|
+
|
|
22
|
+
def test_reduces_to_target(self):
|
|
23
|
+
n = 50000
|
|
24
|
+
ts = np.arange(n, dtype=np.float64)
|
|
25
|
+
vals = np.sin(np.linspace(0, 10 * np.pi, n))
|
|
26
|
+
ds_ts, ds_vals = _downsample(ts, vals, 1000)
|
|
27
|
+
assert len(ds_ts) <= 1000 + 10 # allow small tolerance
|
|
28
|
+
assert len(ds_ts) > 0
|
|
29
|
+
|
|
30
|
+
def test_preserves_dtype(self):
|
|
31
|
+
ts = np.arange(
|
|
32
|
+
np.datetime64("2023-01-01"),
|
|
33
|
+
np.datetime64("2023-01-01") + np.timedelta64(10000, "ms"),
|
|
34
|
+
np.timedelta64(1, "ms"),
|
|
35
|
+
)
|
|
36
|
+
vals = np.random.randn(len(ts)).astype(np.float64)
|
|
37
|
+
ds_ts, ds_vals = _downsample(ts, vals, 100)
|
|
38
|
+
assert ds_ts.dtype == ts.dtype
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestMakePlot:
|
|
42
|
+
"""Tests for the main plot creation function."""
|
|
43
|
+
|
|
44
|
+
def _make_annotation_cds(self):
|
|
45
|
+
empty = dict(start_time=[], end_time=[])
|
|
46
|
+
return {
|
|
47
|
+
"chair_stand": ColumnDataSource(data=dict(**empty)),
|
|
48
|
+
"3m_walk": ColumnDataSource(data=dict(**empty)),
|
|
49
|
+
"6min_walk": ColumnDataSource(data=dict(**empty)),
|
|
50
|
+
"tug": ColumnDataSource(data=dict(**empty)),
|
|
51
|
+
"segment": ColumnDataSource(data=dict(**empty)),
|
|
52
|
+
"scoring": ColumnDataSource(data=dict(**empty)),
|
|
53
|
+
"review": ColumnDataSource(data=dict(**empty)),
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
def test_returns_four_elements(self):
|
|
57
|
+
n = 2000
|
|
58
|
+
pdf = pd.DataFrame({
|
|
59
|
+
"timestamp": pd.date_range("2023-01-01", periods=n, freq="12ms"),
|
|
60
|
+
"x": np.random.randn(n),
|
|
61
|
+
"y": np.random.randn(n),
|
|
62
|
+
"z": np.random.randn(n),
|
|
63
|
+
})
|
|
64
|
+
result = make_plot(pdf, self._make_annotation_cds())
|
|
65
|
+
assert len(result) == 4
|
|
66
|
+
|
|
67
|
+
def test_empty_data_returns_placeholders(self):
|
|
68
|
+
result = make_plot(None, self._make_annotation_cds())
|
|
69
|
+
assert len(result) == 4
|
|
70
|
+
|
|
71
|
+
def test_empty_dataframe_returns_placeholders(self):
|
|
72
|
+
pdf = pd.DataFrame(columns=["timestamp", "x", "y", "z"])
|
|
73
|
+
result = make_plot(pdf, self._make_annotation_cds())
|
|
74
|
+
assert len(result) == 4
|
|
75
|
+
|
|
76
|
+
def test_signal_cds_has_expected_keys(self):
|
|
77
|
+
n = 2000
|
|
78
|
+
pdf = pd.DataFrame({
|
|
79
|
+
"timestamp": pd.date_range("2023-01-01", periods=n, freq="12ms"),
|
|
80
|
+
"x": np.random.randn(n),
|
|
81
|
+
"y": np.random.randn(n),
|
|
82
|
+
"z": np.random.randn(n),
|
|
83
|
+
})
|
|
84
|
+
_, _, _, signal_cds = make_plot(pdf, self._make_annotation_cds())
|
|
85
|
+
assert "timestamp" in signal_cds.data
|
|
86
|
+
assert "x" in signal_cds.data
|
|
87
|
+
assert "y" in signal_cds.data
|
|
88
|
+
assert "z" in signal_cds.data
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Tests for visualize_accelerometry.state."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from visualize_accelerometry.state import AppState
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestAppStateInit:
|
|
12
|
+
"""Tests for AppState initialization."""
|
|
13
|
+
|
|
14
|
+
def test_creates_with_username(self, patch_config, sample_h5):
|
|
15
|
+
state = AppState("test_user")
|
|
16
|
+
assert state.username == "test_user"
|
|
17
|
+
|
|
18
|
+
def test_discovers_files(self, patch_config, sample_h5):
|
|
19
|
+
state = AppState("test_user")
|
|
20
|
+
assert len(state.lst_fnames) == 1
|
|
21
|
+
|
|
22
|
+
def test_sets_initial_fname(self, patch_config, sample_h5):
|
|
23
|
+
state = AppState("test_user")
|
|
24
|
+
assert "900001-20230315093000" in state.fname
|
|
25
|
+
|
|
26
|
+
def test_default_windowsize(self, patch_config, sample_h5):
|
|
27
|
+
state = AppState("test_user")
|
|
28
|
+
assert state.windowsize == 3600
|
|
29
|
+
|
|
30
|
+
def test_annotation_cds_keys(self, patch_config, sample_h5):
|
|
31
|
+
state = AppState("test_user")
|
|
32
|
+
expected = {"chair_stand", "3m_walk", "6min_walk", "tug",
|
|
33
|
+
"segment", "scoring", "review"}
|
|
34
|
+
assert set(state.annotation_cds.keys()) == expected
|
|
35
|
+
|
|
36
|
+
def test_initial_selection_bounds_none(self, patch_config, sample_h5):
|
|
37
|
+
state = AppState("test_user")
|
|
38
|
+
assert state.selection_bounds is None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class TestAppStateLoadFile:
|
|
42
|
+
"""Tests for loading signal data."""
|
|
43
|
+
|
|
44
|
+
def test_load_file_data(self, patch_config, sample_h5):
|
|
45
|
+
state = AppState("test_user")
|
|
46
|
+
pdf = state.load_file_data()
|
|
47
|
+
assert pdf is not None
|
|
48
|
+
assert len(pdf) > 0
|
|
49
|
+
assert "timestamp" in pdf.columns
|
|
50
|
+
|
|
51
|
+
def test_sets_anchor_and_bounds(self, patch_config, sample_h5):
|
|
52
|
+
state = AppState("test_user")
|
|
53
|
+
state.load_file_data()
|
|
54
|
+
assert state.anchor_timestamp is not None
|
|
55
|
+
assert state.file_start_timestamp is not None
|
|
56
|
+
assert state.file_end_timestamp is not None
|
|
57
|
+
|
|
58
|
+
def test_stores_signal_data(self, patch_config, sample_h5):
|
|
59
|
+
state = AppState("test_user")
|
|
60
|
+
pdf = state.load_file_data()
|
|
61
|
+
assert state.pdf_signal_to_display is pdf
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class TestAppStateAnnotations:
|
|
65
|
+
"""Tests for annotation management in AppState."""
|
|
66
|
+
|
|
67
|
+
def test_loads_annotations(self, patch_config, sample_h5, sample_annotations):
|
|
68
|
+
state = AppState("test_user")
|
|
69
|
+
assert len(state.pdf_annotations) == 2
|
|
70
|
+
|
|
71
|
+
def test_refresh_annotations(self, patch_config, sample_h5, sample_annotations):
|
|
72
|
+
state = AppState("test_user")
|
|
73
|
+
state.refresh_annotations()
|
|
74
|
+
assert len(state.pdf_annotations) == 2
|
|
75
|
+
|
|
76
|
+
def test_get_displayed_annotations_filters(
|
|
77
|
+
self, patch_config, sample_h5, sample_annotations,
|
|
78
|
+
):
|
|
79
|
+
state = AppState("test_user")
|
|
80
|
+
displayed = state.get_displayed_annotations()
|
|
81
|
+
assert all(displayed["user"] == "test_user")
|
|
82
|
+
assert all(
|
|
83
|
+
displayed["fname"] == os.path.basename(state.fname)
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def test_update_annotation_sources(
|
|
87
|
+
self, patch_config, sample_h5, sample_annotations,
|
|
88
|
+
):
|
|
89
|
+
state = AppState("test_user")
|
|
90
|
+
state.update_annotation_sources()
|
|
91
|
+
# chair_stand annotation should appear in the CDS
|
|
92
|
+
cs_data = state.annotation_cds["chair_stand"].data
|
|
93
|
+
assert len(cs_data["start_time"]) == 1
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "3.3.0"
|
|
@@ -30,10 +30,11 @@ def get_filenames():
|
|
|
30
30
|
uses a fixed random seed so every server restart produces the
|
|
31
31
|
same mapping, distributing files evenly across annotators.
|
|
32
32
|
"""
|
|
33
|
-
# Fixed seed ensures the same user-to-file assignment across restarts
|
|
34
|
-
|
|
33
|
+
# Fixed seed ensures the same user-to-file assignment across restarts.
|
|
34
|
+
# Use a local Generator to avoid polluting global NumPy random state.
|
|
35
|
+
rng = np.random.default_rng(2020)
|
|
35
36
|
users_to_assign = list(ANNOTATOR_USERS)
|
|
36
|
-
|
|
37
|
+
rng.shuffle(users_to_assign)
|
|
37
38
|
users_cycle = cycle(users_to_assign)
|
|
38
39
|
lst_files = sorted(
|
|
39
40
|
next(users_cycle) + "--" + os.path.splitext(f)[0]
|
|
@@ -70,7 +71,9 @@ def get_filedata(fname, anchor_timestamp, windowsize):
|
|
|
70
71
|
if anchor_timestamp is None:
|
|
71
72
|
# First load: read the first and last rows to determine file bounds
|
|
72
73
|
first_row = pd.read_hdf(file_path, "readings", start=0, stop=1)
|
|
73
|
-
|
|
74
|
+
with pd.HDFStore(file_path, mode="r") as store:
|
|
75
|
+
nrows = store.get_storer("readings").nrows
|
|
76
|
+
last_row = pd.read_hdf(file_path, "readings", start=nrows - 1, stop=nrows)
|
|
74
77
|
anchor_timestamp = first_row["timestamp"].dt.strftime(TIME_FMT).values[0]
|
|
75
78
|
file_start = first_row["timestamp"].dt.strftime(TIME_FMT).values[0]
|
|
76
79
|
file_end = last_row["timestamp"].dt.strftime(TIME_FMT).values[0]
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "3.2.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/app.py
RENAMED
|
File without changes
|
{accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/callbacks.py
RENAMED
|
File without changes
|
{accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/config.py
RENAMED
|
File without changes
|
|
File without changes
|
{accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/plotting.py
RENAMED
|
File without changes
|
{accelerometry_annotator-3.2.2 → accelerometry_annotator-3.3.0}/visualize_accelerometry/state.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|