actinet 0.7.2__tar.gz → 0.8.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.
- {actinet-0.7.2 → actinet-0.8.0}/PKG-INFO +9 -2
- {actinet-0.7.2 → actinet-0.8.0}/README.md +3 -0
- {actinet-0.7.2 → actinet-0.8.0}/pyproject.toml +70 -1
- {actinet-0.7.2 → actinet-0.8.0}/setup.py +6 -2
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/_version.py +3 -3
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/accPlot.py +9 -3
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/actinet.py +178 -49
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/circadian.py +49 -21
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/evaluate.py +37 -29
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/hmm.py +98 -42
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/models.py +164 -76
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/prepare.py +152 -70
- actinet-0.8.0/src/actinet/py.typed +0 -0
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/sslmodel.py +79 -64
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/summarisation.py +37 -20
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/collate_outputs.py +8 -6
- actinet-0.8.0/src/actinet/utils/eval_utils.py +167 -0
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/generate_commands.py +10 -5
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/model_config.py +4 -1
- actinet-0.7.2/src/actinet/utils/eval_utils.py → actinet-0.8.0/src/actinet/utils/plot_utils.py +381 -356
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/sleep_utils.py +42 -13
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/summary_utils.py +30 -12
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/utils.py +32 -15
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/PKG-INFO +9 -2
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/SOURCES.txt +13 -1
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/requires.txt +5 -1
- actinet-0.8.0/tests/test_actinet_cli_and_io.py +244 -0
- actinet-0.8.0/tests/test_command_utils.py +109 -0
- actinet-0.8.0/tests/test_models.py +182 -0
- actinet-0.8.0/tests/test_plotting.py +175 -0
- actinet-0.8.0/tests/test_prepare_and_evaluate.py +258 -0
- actinet-0.8.0/tests/test_real_sample_regression.py +64 -0
- actinet-0.8.0/tests/test_sleep_and_hmm.py +131 -0
- actinet-0.8.0/tests/test_sslmodel.py +163 -0
- actinet-0.8.0/tests/test_time_interval.py +299 -0
- actinet-0.8.0/tests/test_utils_and_summaries.py +197 -0
- {actinet-0.7.2 → actinet-0.8.0}/LICENSE.md +0 -0
- {actinet-0.7.2 → actinet-0.8.0}/setup.cfg +0 -0
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/__init__.py +0 -0
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/__init__.py +0 -0
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/dependency_links.txt +0 -0
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/entry_points.txt +0 -0
- {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/top_level.txt +0 -0
- {actinet-0.7.2 → actinet-0.8.0}/versioneer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: actinet
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Activity detection algorithm compatible with the UK Biobank Accelerometer Dataset
|
|
5
5
|
Home-page: https://github.com/OxWearables/actinet
|
|
6
6
|
Download-URL: https://github.com/OxWearables/actinet
|
|
@@ -34,10 +34,14 @@ Requires-Dist: versioneer; extra == "dev"
|
|
|
34
34
|
Requires-Dist: twine; extra == "dev"
|
|
35
35
|
Requires-Dist: ipython; extra == "dev"
|
|
36
36
|
Requires-Dist: ipdb; extra == "dev"
|
|
37
|
-
Requires-Dist:
|
|
37
|
+
Requires-Dist: ruff==0.15.20; extra == "dev"
|
|
38
38
|
Requires-Dist: autopep8; extra == "dev"
|
|
39
39
|
Requires-Dist: tomli; extra == "dev"
|
|
40
40
|
Requires-Dist: jupyter; extra == "dev"
|
|
41
|
+
Requires-Dist: mypy==1.14.1; extra == "dev"
|
|
42
|
+
Requires-Dist: pytest<8.4,>=8.3; extra == "dev"
|
|
43
|
+
Requires-Dist: pytest-cov<6,>=5; extra == "dev"
|
|
44
|
+
Requires-Dist: seaborn==0.13.*; extra == "dev"
|
|
41
45
|
Provides-Extra: docs
|
|
42
46
|
Requires-Dist: sphinx>=4.2; extra == "docs"
|
|
43
47
|
Requires-Dist: sphinx_rtd_theme>=1.0; extra == "docs"
|
|
@@ -117,6 +121,9 @@ $ actinet sample.csv --csv-txyz 'timestamp,accel_x,accel_y,accel_z'
|
|
|
117
121
|
|
|
118
122
|
# Or specify columns by index (0-indexed)
|
|
119
123
|
$ actinet sample.csv --csv-txyz-idxs '0,1,2,3'
|
|
124
|
+
|
|
125
|
+
# Process an inclusive time interval from the recording
|
|
126
|
+
$ actinet sample.cwa --start '2024-01-01 10:00:00' --end '2024-01-08 09:59:59'
|
|
120
127
|
```
|
|
121
128
|
|
|
122
129
|
See the [Usage](https://actinet.readthedocs.io/en/latest/usage.html) page for further uses of the tool.
|
|
@@ -56,6 +56,9 @@ $ actinet sample.csv --csv-txyz 'timestamp,accel_x,accel_y,accel_z'
|
|
|
56
56
|
|
|
57
57
|
# Or specify columns by index (0-indexed)
|
|
58
58
|
$ actinet sample.csv --csv-txyz-idxs '0,1,2,3'
|
|
59
|
+
|
|
60
|
+
# Process an inclusive time interval from the recording
|
|
61
|
+
$ actinet sample.cwa --start '2024-01-01 10:00:00' --end '2024-01-08 09:59:59'
|
|
59
62
|
```
|
|
60
63
|
|
|
61
64
|
See the [Usage](https://actinet.readthedocs.io/en/latest/usage.html) page for further uses of the tool.
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
# # Similar to `dependencies` above, these must be valid existing
|
|
117
117
|
# # projects.
|
|
118
118
|
# [project.optional-dependencies] # Optional
|
|
119
|
-
# dev = ["check-manifest", "build", "twine", "versioneer", "
|
|
119
|
+
# dev = ["check-manifest", "build", "twine", "versioneer", "ruff==0.15.20", "autopep8", "ipdb"]
|
|
120
120
|
# test = ["coverage"]
|
|
121
121
|
|
|
122
122
|
# # List URLs that are relevant to your project
|
|
@@ -161,3 +161,72 @@ versionfile_source = "src/actinet/_version.py"
|
|
|
161
161
|
versionfile_build = "actinet/_version.py"
|
|
162
162
|
tag_prefix = "v"
|
|
163
163
|
parentdir_prefix = "actinet-"
|
|
164
|
+
|
|
165
|
+
[tool.ruff]
|
|
166
|
+
target-version = "py38"
|
|
167
|
+
line-length = 120
|
|
168
|
+
extend-exclude = ["venv", "versioneer.py", "src/actinet/_version.py"]
|
|
169
|
+
|
|
170
|
+
[tool.ruff.lint]
|
|
171
|
+
select = [
|
|
172
|
+
"E4", # pycodestyle imports
|
|
173
|
+
"E7", # pycodestyle statements
|
|
174
|
+
"E9", # pycodestyle runtime errors
|
|
175
|
+
"F", # Pyflakes
|
|
176
|
+
"B", # flake8-bugbear
|
|
177
|
+
"C4", # flake8-comprehensions
|
|
178
|
+
"C90", # mccabe
|
|
179
|
+
"I", # isort
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
[tool.ruff.lint.mccabe]
|
|
183
|
+
max-complexity = 10
|
|
184
|
+
|
|
185
|
+
[tool.ruff.lint.per-file-ignores]
|
|
186
|
+
"__init__.py" = ["F401"]
|
|
187
|
+
|
|
188
|
+
[tool.mypy]
|
|
189
|
+
python_version = "3.8"
|
|
190
|
+
files = ["src/actinet"]
|
|
191
|
+
exclude = [
|
|
192
|
+
"^src/actinet/_version\\.py$",
|
|
193
|
+
"^src/actinet/torch_hub_cache/",
|
|
194
|
+
]
|
|
195
|
+
check_untyped_defs = true
|
|
196
|
+
disallow_untyped_defs = true
|
|
197
|
+
no_implicit_optional = true
|
|
198
|
+
show_error_codes = true
|
|
199
|
+
warn_redundant_casts = true
|
|
200
|
+
warn_unused_ignores = true
|
|
201
|
+
|
|
202
|
+
[[tool.mypy.overrides]]
|
|
203
|
+
module = [
|
|
204
|
+
"actipy.*",
|
|
205
|
+
"imblearn.*",
|
|
206
|
+
"joblib.*",
|
|
207
|
+
"matplotlib.*",
|
|
208
|
+
"pandas.*",
|
|
209
|
+
"scipy.*",
|
|
210
|
+
"seaborn.*",
|
|
211
|
+
"sklearn.*",
|
|
212
|
+
"tqdm.*",
|
|
213
|
+
"transforms3d.*",
|
|
214
|
+
"torchvision.*",
|
|
215
|
+
]
|
|
216
|
+
ignore_missing_imports = true
|
|
217
|
+
|
|
218
|
+
[[tool.mypy.overrides]]
|
|
219
|
+
module = "actinet._version"
|
|
220
|
+
follow_imports = "skip"
|
|
221
|
+
|
|
222
|
+
[tool.pytest.ini_options]
|
|
223
|
+
testpaths = ["tests"]
|
|
224
|
+
markers = [
|
|
225
|
+
"slow: real-device or otherwise longer-running regression tests",
|
|
226
|
+
]
|
|
227
|
+
|
|
228
|
+
[tool.coverage.run]
|
|
229
|
+
omit = ["*/actinet/_version.py"]
|
|
230
|
+
|
|
231
|
+
[tool.coverage.report]
|
|
232
|
+
fail_under = 80
|
|
@@ -46,7 +46,7 @@ def main():
|
|
|
46
46
|
packages=find_packages(where="src", exclude=("test", "tests")),
|
|
47
47
|
package_dir={"": "src"},
|
|
48
48
|
# What other files to include, e.g. *.class if the package uses some Java code.
|
|
49
|
-
package_data={"actinet": ["*.txt", "*.rst", "*.md"]},
|
|
49
|
+
package_data={"actinet": ["*.txt", "*.rst", "*.md", "py.typed"]},
|
|
50
50
|
include_package_data=False,
|
|
51
51
|
# Dependencies
|
|
52
52
|
install_requires=[
|
|
@@ -69,10 +69,14 @@ def main():
|
|
|
69
69
|
"twine",
|
|
70
70
|
"ipython",
|
|
71
71
|
"ipdb",
|
|
72
|
-
"
|
|
72
|
+
"ruff==0.15.20",
|
|
73
73
|
"autopep8",
|
|
74
74
|
"tomli",
|
|
75
75
|
"jupyter",
|
|
76
|
+
"mypy==1.14.1",
|
|
77
|
+
"pytest>=8.3,<8.4",
|
|
78
|
+
"pytest-cov>=5,<6",
|
|
79
|
+
"seaborn==0.13.*",
|
|
76
80
|
],
|
|
77
81
|
"docs": [
|
|
78
82
|
"sphinx>=4.2",
|
|
@@ -8,11 +8,11 @@ import json
|
|
|
8
8
|
|
|
9
9
|
version_json = '''
|
|
10
10
|
{
|
|
11
|
-
"date": "2026-
|
|
11
|
+
"date": "2026-10-08T18:41:48+0100",
|
|
12
12
|
"dirty": false,
|
|
13
13
|
"error": null,
|
|
14
|
-
"full-revisionid": "
|
|
15
|
-
"version": "0.
|
|
14
|
+
"full-revisionid": "497eea3d3834609c45be709c9a70cff0a59235ea",
|
|
15
|
+
"version": "0.8.0"
|
|
16
16
|
}
|
|
17
17
|
''' # END VERSION_JSON
|
|
18
18
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"""Script to plot accelerometer traces."""
|
|
2
2
|
|
|
3
3
|
import sys
|
|
4
|
+
from typing import Any, Optional
|
|
5
|
+
|
|
4
6
|
import numpy as np
|
|
5
7
|
import pandas as pd
|
|
6
8
|
from pandas.plotting import register_matplotlib_converters
|
|
@@ -39,7 +41,7 @@ LABELS_AND_COLORS = {
|
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
|
|
42
|
-
def main(): # noqa: C901
|
|
44
|
+
def main() -> None: # noqa: C901
|
|
43
45
|
"""
|
|
44
46
|
Application entry point responsible for parsing command line requests
|
|
45
47
|
"""
|
|
@@ -111,7 +113,11 @@ def main(): # noqa: C901
|
|
|
111
113
|
print("Plot file written to:", args.plotFile)
|
|
112
114
|
|
|
113
115
|
|
|
114
|
-
def plotTimeSeries(
|
|
116
|
+
def plotTimeSeries(
|
|
117
|
+
data: pd.DataFrame,
|
|
118
|
+
title: Optional[str] = None,
|
|
119
|
+
showFirstNDays: Optional[int] = None,
|
|
120
|
+
) -> Any: # noqa: C901
|
|
115
121
|
"""
|
|
116
122
|
Plot acceleration traces and classified activities.
|
|
117
123
|
|
|
@@ -292,7 +298,7 @@ def plotTimeSeries(data, title=None, showFirstNDays=None): # noqa: C901
|
|
|
292
298
|
return fig
|
|
293
299
|
|
|
294
300
|
|
|
295
|
-
def str2bool(v):
|
|
301
|
+
def str2bool(v: str) -> bool:
|
|
296
302
|
"""
|
|
297
303
|
Used to parse true/false values from the command line. E.g. "True" -> True
|
|
298
304
|
"""
|
|
@@ -1,36 +1,39 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import hashlib
|
|
5
|
+
import json
|
|
1
6
|
import os
|
|
2
7
|
import pathlib
|
|
3
|
-
import urllib
|
|
4
8
|
import shutil
|
|
9
|
+
import sys
|
|
5
10
|
import time
|
|
6
|
-
import
|
|
7
|
-
import json
|
|
8
|
-
import hashlib
|
|
11
|
+
import urllib
|
|
9
12
|
import warnings
|
|
10
|
-
import
|
|
11
|
-
import pandas as pd
|
|
12
|
-
import joblib
|
|
13
|
-
import sys
|
|
13
|
+
from typing import TYPE_CHECKING, Any, Dict, Literal, Optional, Tuple, Union
|
|
14
14
|
|
|
15
|
-
import
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
from actinet.models import ActivityClassifier
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
15
|
+
from actinet import __classifiers__, __version__
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
import pandas as pd
|
|
19
|
+
|
|
20
|
+
from actinet.models import ActivityClassifier
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def __getattr__(name: str) -> Any:
|
|
24
|
+
if name == "ActivityClassifier":
|
|
25
|
+
from actinet.models import ActivityClassifier
|
|
26
|
+
|
|
27
|
+
globals()[name] = ActivityClassifier
|
|
28
|
+
return ActivityClassifier
|
|
29
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
29
30
|
|
|
30
31
|
BASE_URL = "https://wearables-files.ndph.ox.ac.uk/files/models/actinet/"
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
def save_and_print_summary(
|
|
34
|
+
def save_and_print_summary(
|
|
35
|
+
outputSummaryFile: str, info: Dict[str, Any], verbose: bool
|
|
36
|
+
) -> None:
|
|
34
37
|
"""Save info to outputSummary.json and print summary stats."""
|
|
35
38
|
with open(outputSummaryFile, "w") as f:
|
|
36
39
|
json.dump(info, f, indent=4, cls=NpEncoder)
|
|
@@ -55,7 +58,7 @@ def save_and_print_summary(outputSummaryFile, info, verbose):
|
|
|
55
58
|
)
|
|
56
59
|
|
|
57
60
|
|
|
58
|
-
def main():
|
|
61
|
+
def main() -> None:
|
|
59
62
|
parser = argparse.ArgumentParser(
|
|
60
63
|
description="A tool to predict activities from accelerometer data using a self-supervised ResNet-18 model",
|
|
61
64
|
add_help=True,
|
|
@@ -126,6 +129,24 @@ def main():
|
|
|
126
129
|
type=str,
|
|
127
130
|
default=None,
|
|
128
131
|
)
|
|
132
|
+
parser.add_argument(
|
|
133
|
+
"--start",
|
|
134
|
+
help=(
|
|
135
|
+
"Specify a start time for the data to be processed (otherwise, process all). "
|
|
136
|
+
"Pass values as strings, e.g.: '2024-01-01 10:00:00'. Default: None"
|
|
137
|
+
),
|
|
138
|
+
type=str,
|
|
139
|
+
default=None,
|
|
140
|
+
)
|
|
141
|
+
parser.add_argument(
|
|
142
|
+
"--end",
|
|
143
|
+
help=(
|
|
144
|
+
"Specify an end time for the data to be processed (otherwise, process all). "
|
|
145
|
+
"Pass values as strings, e.g.: '2024-01-02 09:59:59'. Default: None"
|
|
146
|
+
),
|
|
147
|
+
type=str,
|
|
148
|
+
default=None,
|
|
149
|
+
)
|
|
129
150
|
parser.add_argument(
|
|
130
151
|
"--csv-start-row",
|
|
131
152
|
help="Row number to start reading a CSV file. Default: 1 (First row)",
|
|
@@ -168,7 +189,7 @@ def main():
|
|
|
168
189
|
parser.add_argument(
|
|
169
190
|
"--cache-classifier",
|
|
170
191
|
action="store_true",
|
|
171
|
-
help="Download and cache classifier file and model modules for offline usage",
|
|
192
|
+
help="Download and cache classifier file and model modules for offline usage. This will cause the ActiNet classification model to not run.",
|
|
172
193
|
)
|
|
173
194
|
parser.add_argument(
|
|
174
195
|
"--model-repo-path", "-m", help="Enter repository of ssl model", default=None
|
|
@@ -197,6 +218,13 @@ def main():
|
|
|
197
218
|
if not args.filepath:
|
|
198
219
|
raise ValueError("Please provide a file to process.")
|
|
199
220
|
|
|
221
|
+
from actinet.utils.summary_utils import calculate_daily_wear_stats
|
|
222
|
+
from actinet.utils.utils import (
|
|
223
|
+
calculate_wear_stats,
|
|
224
|
+
drop_first_last_days,
|
|
225
|
+
flag_wear_below_days,
|
|
226
|
+
)
|
|
227
|
+
|
|
200
228
|
# Info contains high-level summary of the data and results
|
|
201
229
|
info = {}
|
|
202
230
|
info["ActiNetVersion"] = __version__
|
|
@@ -211,6 +239,8 @@ def main():
|
|
|
211
239
|
args.calibration_stdtol_min,
|
|
212
240
|
resample_hz="uniform",
|
|
213
241
|
sample_rate=args.sample_rate,
|
|
242
|
+
start_time=args.start,
|
|
243
|
+
end_time=args.end,
|
|
214
244
|
csv_txyz_idxs=args.csv_txyz_idxs,
|
|
215
245
|
verbose=verbose,
|
|
216
246
|
)
|
|
@@ -275,6 +305,11 @@ def main():
|
|
|
275
305
|
args.require_sleep_above,
|
|
276
306
|
args.single_sleep_block,
|
|
277
307
|
)
|
|
308
|
+
if len(Y) < 3:
|
|
309
|
+
raise ValueError(
|
|
310
|
+
"The selected time interval must produce at least three "
|
|
311
|
+
f"{classifier.window_sec}-second activity epochs."
|
|
312
|
+
)
|
|
278
313
|
except Exception as e:
|
|
279
314
|
save_and_print_summary(outputSummaryFile, info, verbose)
|
|
280
315
|
raise ValueError(f"Model failed to make predictions on the data: {str(e)}")
|
|
@@ -290,6 +325,8 @@ def main():
|
|
|
290
325
|
|
|
291
326
|
# Plot activity profile
|
|
292
327
|
if args.plot_activity:
|
|
328
|
+
from actinet.accPlot import plotTimeSeries
|
|
329
|
+
|
|
293
330
|
plotFile = f"{outdir}/{basename}-timeSeries-plot.png"
|
|
294
331
|
fig = plotTimeSeries(Y)
|
|
295
332
|
fig.savefig(plotFile, dpi=200, bbox_inches="tight")
|
|
@@ -298,6 +335,8 @@ def main():
|
|
|
298
335
|
print("Output plot written to:", plotFile)
|
|
299
336
|
|
|
300
337
|
# Summary
|
|
338
|
+
from actinet.summarisation import get_activity_summary
|
|
339
|
+
|
|
301
340
|
summary, daily_summary = get_activity_summary(
|
|
302
341
|
Y, list(classifier.labels), args.exclude_wear_below, True, True, verbose
|
|
303
342
|
)
|
|
@@ -312,6 +351,8 @@ def main():
|
|
|
312
351
|
if verbose:
|
|
313
352
|
print("Output summary written to:", outputSummaryFile)
|
|
314
353
|
|
|
354
|
+
import pandas as pd
|
|
355
|
+
|
|
315
356
|
daily_summary = pd.concat([daily_wear_stats, daily_summary], axis=1)
|
|
316
357
|
|
|
317
358
|
daily_summary.insert(0, "Filename", info["Filename"]) # add filename for reference
|
|
@@ -346,17 +387,27 @@ def main():
|
|
|
346
387
|
|
|
347
388
|
|
|
348
389
|
def read(
|
|
349
|
-
filepath,
|
|
350
|
-
usecols=None,
|
|
351
|
-
skipRows=0,
|
|
352
|
-
dateFormat=None,
|
|
353
|
-
calibration_stdtol_min=None,
|
|
354
|
-
resample_hz="uniform",
|
|
355
|
-
sample_rate=None,
|
|
356
|
-
lowpass_hz=None,
|
|
357
|
-
csv_txyz_idxs=None,
|
|
358
|
-
verbose=True,
|
|
359
|
-
|
|
390
|
+
filepath: str,
|
|
391
|
+
usecols: Optional[str] = None,
|
|
392
|
+
skipRows: int = 0,
|
|
393
|
+
dateFormat: Optional[str] = None,
|
|
394
|
+
calibration_stdtol_min: Optional[float] = None,
|
|
395
|
+
resample_hz: Optional[Union[Literal["uniform"], int, float, bool]] = "uniform",
|
|
396
|
+
sample_rate: Optional[int] = None,
|
|
397
|
+
lowpass_hz: Optional[float] = None,
|
|
398
|
+
csv_txyz_idxs: Optional[str] = None,
|
|
399
|
+
verbose: bool = True,
|
|
400
|
+
start_time: Any = None,
|
|
401
|
+
end_time: Any = None,
|
|
402
|
+
) -> Tuple[pd.DataFrame, Dict[str, Any]]:
|
|
403
|
+
import actipy
|
|
404
|
+
import numpy as np
|
|
405
|
+
import pandas as pd
|
|
406
|
+
|
|
407
|
+
from actinet.utils.utils import infer_freq
|
|
408
|
+
|
|
409
|
+
start_time, end_time = validate_time_interval(start_time, end_time)
|
|
410
|
+
|
|
360
411
|
p = pathlib.Path(filepath)
|
|
361
412
|
fsize = round(p.stat().st_size / (1024 * 1024), 1)
|
|
362
413
|
|
|
@@ -397,6 +448,8 @@ def read(
|
|
|
397
448
|
header[zidx],
|
|
398
449
|
)
|
|
399
450
|
else:
|
|
451
|
+
if usecols is None:
|
|
452
|
+
usecols = "time,x,y,z"
|
|
400
453
|
tcol, xcol, ycol, zcol = usecols.split(",")
|
|
401
454
|
|
|
402
455
|
data = pd.read_csv(
|
|
@@ -420,6 +473,8 @@ def read(
|
|
|
420
473
|
freq = infer_freq(data.index)
|
|
421
474
|
sample_rate = int(np.round(pd.Timedelta("1s") / freq))
|
|
422
475
|
|
|
476
|
+
assert sample_rate is not None
|
|
477
|
+
|
|
423
478
|
# Quick fix: Drop duplicate indices. TODO: Maybe should be handled by actipy.
|
|
424
479
|
data = data[~data.index.duplicated(keep="first")]
|
|
425
480
|
|
|
@@ -466,10 +521,77 @@ def read(
|
|
|
466
521
|
if "ResampleRate" not in info:
|
|
467
522
|
info["ResampleRate"] = info["SampleRate"]
|
|
468
523
|
|
|
524
|
+
start_time, end_time = validate_time_interval(
|
|
525
|
+
start_time, end_time, data.index
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
# Apply bounds after ActiPy processes the complete recording for calibration
|
|
529
|
+
# and non-wear detection.
|
|
530
|
+
if start_time is not None:
|
|
531
|
+
data = data.loc[start_time:]
|
|
532
|
+
if end_time is not None:
|
|
533
|
+
data = data.loc[:end_time]
|
|
534
|
+
|
|
535
|
+
if len(data) == 0 and (start_time is not None or end_time is not None):
|
|
536
|
+
raise ValueError("The selected time interval does not contain any data.")
|
|
537
|
+
|
|
469
538
|
return data, info
|
|
470
539
|
|
|
471
540
|
|
|
472
|
-
def
|
|
541
|
+
def validate_time_interval(
|
|
542
|
+
start_time: Any = None,
|
|
543
|
+
end_time: Any = None,
|
|
544
|
+
index: Optional[pd.Index] = None,
|
|
545
|
+
) -> Tuple[Optional[pd.Timestamp], Optional[pd.Timestamp]]:
|
|
546
|
+
"""Parse optional bounds and validate them against a recording index."""
|
|
547
|
+
|
|
548
|
+
import pandas as pd
|
|
549
|
+
|
|
550
|
+
def parse_bound(value: Any, name: str) -> Optional[pd.Timestamp]:
|
|
551
|
+
if value is None:
|
|
552
|
+
return None
|
|
553
|
+
try:
|
|
554
|
+
value = pd.Timestamp(value)
|
|
555
|
+
except (TypeError, ValueError) as error:
|
|
556
|
+
raise ValueError(f"Invalid {name} time: {value!r}") from error
|
|
557
|
+
if pd.isna(value):
|
|
558
|
+
raise ValueError(f"Invalid {name} time: {value!r}")
|
|
559
|
+
return value
|
|
560
|
+
|
|
561
|
+
start_time = parse_bound(start_time, "start")
|
|
562
|
+
end_time = parse_bound(end_time, "end")
|
|
563
|
+
|
|
564
|
+
if start_time is not None and end_time is not None:
|
|
565
|
+
try:
|
|
566
|
+
reversed_interval = start_time > end_time
|
|
567
|
+
except TypeError as error:
|
|
568
|
+
raise ValueError(
|
|
569
|
+
"Start and end times must use compatible timezones."
|
|
570
|
+
) from error
|
|
571
|
+
if reversed_interval:
|
|
572
|
+
raise ValueError("Start time must be earlier than or equal to end time.")
|
|
573
|
+
|
|
574
|
+
if index is None or len(index) == 0:
|
|
575
|
+
return start_time, end_time
|
|
576
|
+
if not isinstance(index, pd.DatetimeIndex):
|
|
577
|
+
raise ValueError("Time interval selection requires a DatetimeIndex.")
|
|
578
|
+
|
|
579
|
+
index_is_aware = index.tz is not None
|
|
580
|
+
for name, bound in (("start", start_time), ("end", end_time)):
|
|
581
|
+
if bound is not None and (bound.tzinfo is not None) != index_is_aware:
|
|
582
|
+
raise ValueError(
|
|
583
|
+
f"The {name} time and recording timestamps must use compatible timezones."
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
if start_time is not None and start_time > index[-1]:
|
|
587
|
+
raise ValueError("The selected time interval does not overlap the recording.")
|
|
588
|
+
if end_time is not None and end_time < index[0]:
|
|
589
|
+
raise ValueError("The selected time interval does not overlap the recording.")
|
|
590
|
+
|
|
591
|
+
return start_time, end_time
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def resolve_path(path: Union[str, pathlib.Path]) -> Tuple[pathlib.Path, str, str]:
|
|
473
595
|
"""Return parent folder, file name and file extension"""
|
|
474
596
|
p = pathlib.Path(path)
|
|
475
597
|
extension = p.suffixes[0]
|
|
@@ -479,13 +601,15 @@ def resolve_path(path):
|
|
|
479
601
|
|
|
480
602
|
|
|
481
603
|
def load_classifier(
|
|
482
|
-
classifier,
|
|
483
|
-
model_repo_path=None,
|
|
484
|
-
force_download=False,
|
|
485
|
-
verbose=True,
|
|
486
|
-
):
|
|
604
|
+
classifier: str,
|
|
605
|
+
model_repo_path: Optional[str] = None,
|
|
606
|
+
force_download: bool = False,
|
|
607
|
+
verbose: bool = True,
|
|
608
|
+
) -> ActivityClassifier:
|
|
487
609
|
"""Load trained classifier. Download if not exists."""
|
|
488
610
|
|
|
611
|
+
import joblib
|
|
612
|
+
|
|
489
613
|
if classifier in __classifiers__.keys():
|
|
490
614
|
classifier_version = __classifiers__[classifier]["version"]
|
|
491
615
|
classifier_md5 = __classifiers__[classifier]["md5"]
|
|
@@ -514,25 +638,25 @@ def load_classifier(
|
|
|
514
638
|
)
|
|
515
639
|
|
|
516
640
|
elif pathlib.Path(classifier).exists():
|
|
517
|
-
classifier_path = classifier
|
|
641
|
+
classifier_path = pathlib.Path(classifier)
|
|
518
642
|
|
|
519
643
|
else:
|
|
520
644
|
raise ValueError(f"Unknown classifier: {classifier}")
|
|
521
645
|
|
|
522
646
|
try:
|
|
523
|
-
|
|
647
|
+
loaded_classifier: ActivityClassifier = joblib.load(classifier_path)
|
|
524
648
|
except Exception as e:
|
|
525
|
-
raise ValueError(
|
|
649
|
+
raise ValueError("Error loading classifier file.")
|
|
526
650
|
|
|
527
651
|
if model_repo_path and pathlib.Path(model_repo_path).exists() and verbose:
|
|
528
652
|
print(f"Loading model repository from {model_repo_path}.")
|
|
529
653
|
|
|
530
|
-
|
|
654
|
+
loaded_classifier.load_model(model_repo_path)
|
|
531
655
|
|
|
532
|
-
return
|
|
656
|
+
return loaded_classifier
|
|
533
657
|
|
|
534
658
|
|
|
535
|
-
def md5(fname):
|
|
659
|
+
def md5(fname: Union[str, pathlib.Path]) -> str:
|
|
536
660
|
hash_md5 = hashlib.md5()
|
|
537
661
|
with open(fname, "rb") as f:
|
|
538
662
|
for chunk in iter(lambda: f.read(4096), b""):
|
|
@@ -541,7 +665,12 @@ def md5(fname):
|
|
|
541
665
|
|
|
542
666
|
|
|
543
667
|
class NpEncoder(json.JSONEncoder):
|
|
544
|
-
def default(self, obj):
|
|
668
|
+
def default(self, obj: Any) -> Any:
|
|
669
|
+
import numpy as np
|
|
670
|
+
import pandas as pd
|
|
671
|
+
|
|
672
|
+
if isinstance(obj, np.bool_):
|
|
673
|
+
return bool(obj)
|
|
545
674
|
if isinstance(obj, np.integer):
|
|
546
675
|
return int(obj)
|
|
547
676
|
if isinstance(obj, np.floating):
|