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.
Files changed (44) hide show
  1. {actinet-0.7.2 → actinet-0.8.0}/PKG-INFO +9 -2
  2. {actinet-0.7.2 → actinet-0.8.0}/README.md +3 -0
  3. {actinet-0.7.2 → actinet-0.8.0}/pyproject.toml +70 -1
  4. {actinet-0.7.2 → actinet-0.8.0}/setup.py +6 -2
  5. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/_version.py +3 -3
  6. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/accPlot.py +9 -3
  7. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/actinet.py +178 -49
  8. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/circadian.py +49 -21
  9. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/evaluate.py +37 -29
  10. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/hmm.py +98 -42
  11. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/models.py +164 -76
  12. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/prepare.py +152 -70
  13. actinet-0.8.0/src/actinet/py.typed +0 -0
  14. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/sslmodel.py +79 -64
  15. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/summarisation.py +37 -20
  16. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/collate_outputs.py +8 -6
  17. actinet-0.8.0/src/actinet/utils/eval_utils.py +167 -0
  18. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/generate_commands.py +10 -5
  19. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/model_config.py +4 -1
  20. actinet-0.7.2/src/actinet/utils/eval_utils.py → actinet-0.8.0/src/actinet/utils/plot_utils.py +381 -356
  21. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/sleep_utils.py +42 -13
  22. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/summary_utils.py +30 -12
  23. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/utils.py +32 -15
  24. {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/PKG-INFO +9 -2
  25. {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/SOURCES.txt +13 -1
  26. {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/requires.txt +5 -1
  27. actinet-0.8.0/tests/test_actinet_cli_and_io.py +244 -0
  28. actinet-0.8.0/tests/test_command_utils.py +109 -0
  29. actinet-0.8.0/tests/test_models.py +182 -0
  30. actinet-0.8.0/tests/test_plotting.py +175 -0
  31. actinet-0.8.0/tests/test_prepare_and_evaluate.py +258 -0
  32. actinet-0.8.0/tests/test_real_sample_regression.py +64 -0
  33. actinet-0.8.0/tests/test_sleep_and_hmm.py +131 -0
  34. actinet-0.8.0/tests/test_sslmodel.py +163 -0
  35. actinet-0.8.0/tests/test_time_interval.py +299 -0
  36. actinet-0.8.0/tests/test_utils_and_summaries.py +197 -0
  37. {actinet-0.7.2 → actinet-0.8.0}/LICENSE.md +0 -0
  38. {actinet-0.7.2 → actinet-0.8.0}/setup.cfg +0 -0
  39. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/__init__.py +0 -0
  40. {actinet-0.7.2 → actinet-0.8.0}/src/actinet/utils/__init__.py +0 -0
  41. {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/dependency_links.txt +0 -0
  42. {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/entry_points.txt +0 -0
  43. {actinet-0.7.2 → actinet-0.8.0}/src/actinet.egg-info/top_level.txt +0 -0
  44. {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.7.2
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: flake8; extra == "dev"
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", "flake8", "autopep8", "ipdb"]
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
- "flake8",
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-03-31T10:30:51+0100",
11
+ "date": "2026-10-08T18:41:48+0100",
12
12
  "dirty": false,
13
13
  "error": null,
14
- "full-revisionid": "58d134b8be0965b5611aa589bc07a926de68f575",
15
- "version": "0.7.2"
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(data, title=None, showFirstNDays=None): # noqa: C901
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 argparse
7
- import json
8
- import hashlib
11
+ import urllib
9
12
  import warnings
10
- import numpy as np
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 actipy
16
-
17
- from actinet import __version__
18
- from actinet import __classifiers__
19
- from actinet.accPlot import plotTimeSeries
20
- from actinet.models import ActivityClassifier
21
- from actinet.summarisation import get_activity_summary
22
- from actinet.utils.summary_utils import calculate_daily_wear_stats
23
- from actinet.utils.utils import (
24
- infer_freq,
25
- drop_first_last_days,
26
- flag_wear_below_days,
27
- calculate_wear_stats,
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(outputSummaryFile, info, verbose):
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 resolve_path(path):
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
- classifier: ActivityClassifier = joblib.load(classifier_path)
647
+ loaded_classifier: ActivityClassifier = joblib.load(classifier_path)
524
648
  except Exception as e:
525
- raise ValueError(f"Error loading classifier file.")
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
- classifier.load_model(model_repo_path)
654
+ loaded_classifier.load_model(model_repo_path)
531
655
 
532
- return classifier
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):