Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion meson.build
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
project(
'scikit-digital-health',
'c',
version: '0.17.10',
version: '0.17.11',
license: 'MIT',
meson_version: '>=1.1',
)
Expand Down
16 changes: 14 additions & 2 deletions src/skdh/activity/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ class ActivityLevelClassification(BaseProcess):
If True then count breaks in a bout towards the bout duration. If False
then only count time spent above the threshold towards the bout duration.
Only used if `bout_metric=1`. Default is False.
wear_wake_times_round : {None, int}, optional
A value in seconds to round the wear/wake start/end times to, relative to the
start of the day. For example, if 15 is passed, and the wear/wake period
starts at xx:yy:10, (and the day starts at 00:00:00), the start time will
be rounded to xx:yy:15.
min_wear_time : int, optional
Minimum wear time in hours for a day to be analyzed. Default is 10 hours.
cutpoints : {str, dict, list}, optional
Expand Down Expand Up @@ -136,6 +141,7 @@ def __init__(
bout_criteria=0.8,
bout_metric=4,
closed_bout=False,
wear_wake_times_round=None,
min_wear_time=10,
cutpoints="migueles_wrist_adult",
day_window=(0, 24),
Expand Down Expand Up @@ -165,6 +171,7 @@ def __init__(
bout_criteria=bout_criteria,
bout_metric=bout_metric,
closed_bout=closed_bout,
wear_wake_times_round=wear_wake_times_round,
min_wear_time=min_wear_time,
cutpoints=cutpoints_,
day_window=day_window,
Expand All @@ -177,6 +184,7 @@ def __init__(
self.boutcrit = bout_criteria
self.boutmetric = bout_metric
self.closedbout = closed_bout
self.wear_wake_times_round = wear_wake_times_round
self.min_wear = min_wear_time
self.cutpoints = cutpoints_

Expand Down Expand Up @@ -468,7 +476,7 @@ def predict(self, *, time, accel, fs=None, wear=None, tz_name=None, **kwargs):

# get the intersection of wear time and day
dwear_starts, dwear_stops = get_day_index_intersection(
*self.wear_idx, True, day_start, day_stop # include wear time
*self.wear_idx, True, day_start, day_stop, ends_round=self.wear_wake_times_round, fs=fs # include wear time
)

# PLOTTING. handle here before returning for minimal wear hours, etc
Expand Down Expand Up @@ -502,13 +510,17 @@ def predict(self, *, time, accel, fs=None, wear=None, tz_name=None, **kwargs):
(True, False), # include wear time, exclude sleeping time
day_start,
day_stop,
ends_round=self.wear_wake_times_round,
fs=fs,
)
sleep_wear_starts, sleep_wear_stops = get_day_index_intersection(
(self.wear_idx[0], sleep_starts),
(self.wear_idx[1], sleep_stops),
(True, True), # now we want only sleep
day_start,
day_stop,
ends_round=self.wear_wake_times_round,
fs=fs,
)

res["N wear wake hours"][iday] = around(
Expand Down Expand Up @@ -762,7 +774,7 @@ def _plot_day_sleep(
start_hr -= 24
# get day-sleep intersection
day_sleep_starts, day_sleep_stops = get_day_index_intersection(
sleep_starts, sleep_stops, True, day_start, day_stop
sleep_starts, sleep_stops, True, day_start, day_stop, ends_round=self.wear_wake_times_round, fs=fs
)

sleep = []
Expand Down
21 changes: 13 additions & 8 deletions src/skdh/gait/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
"""

from collections.abc import Iterable
from sys import exception
from warnings import warn

from numpy import ndarray, asarray, mean, diff, sum, nan
Expand Down Expand Up @@ -616,14 +617,18 @@ def predict(

for ibout, bout in enumerate(gait_bouts):
# run the bout processing pipeline
bout_res = self.bout_pipeline.run(
time=time_rs[bout],
accel=accel_rs[bout],
gyro=gyro_rs[bout] if gyro_rs is not None else None,
fs=goal_fs,
v_axis=v_axis,
ap_axis=ap_axis,
)
try:
bout_res = self.bout_pipeline.run(
time=time_rs[bout],
accel=accel_rs[bout],
gyro=gyro_rs[bout] if gyro_rs is not None else None,
fs=goal_fs,
v_axis=v_axis,
ap_axis=ap_axis,
)
except Exception as e:
warn(f"Caught error processing bout no. {ibout}: {e}")
continue

# get the data we need
n_strides = bout_res["qc_initial_contacts"].size
Expand Down
2 changes: 1 addition & 1 deletion src/skdh/preprocessing/wear_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -814,7 +814,7 @@ def predict(self, *, time, accel, fs=None, **kwargs):
**kwargs,
)
# dont start at zero due to timestamp weirdness with some devices
fs = 1 / mean(diff(time[1000:5000]))
fs = 1 / mean(diff(time[1000:5000])) if fs is None else fs
n_wlen = int(self.wlen * 60 * fs) # samples in wlen minutes
n_wskip = int(self.wskip * 60 * fs) # samples in wskip minutes

Expand Down
20 changes: 19 additions & 1 deletion src/skdh/utility/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
all,
asarray,
argsort,
around,
array,
clip,
nonzero,
Expand All @@ -30,7 +31,7 @@
from scipy.signal import cheby1, sosfiltfilt


def get_day_index_intersection(starts, stops, for_inclusion, day_start, day_stop):
def get_day_index_intersection(starts, stops, for_inclusion, day_start, day_stop, ends_round=None, fs=None):
"""
Get the intersection between day start and stop indices and various start and stop indices
that may or may not happen during the day, and are not necessarily for inclusion.
Expand All @@ -50,6 +51,11 @@ def get_day_index_intersection(starts, stops, for_inclusion, day_start, day_stop
Day start index.
day_stop : int
Day stop index.
ends_round : {None, int}, optional
If provided, will round the start and stop indices to the nearest multiple of this value
away from the day start.
fs : float, optional
Sampling frequency of the data. Required if `ends_round` is provided.

Returns
-------
Expand Down Expand Up @@ -158,6 +164,18 @@ def get_day_index_intersection(starts, stops, for_inclusion, day_start, day_stop
valid_starts = asarray(valid_starts)
valid_stops = asarray(valid_stops)

# handle rounding to nearest multiple of ends_round away from day_start
if ends_round is not None:
if fs is None:
raise ValueError("Sampling frequency `fs` must be provided if `ends_round` is provided.")
factor = int(ends_round * fs)
valid_starts = around((valid_starts - day_start) / factor, 0) * factor + day_start
valid_stops = around((valid_stops - day_start) / factor, 0) * factor + day_start

# make sure they stay as integers
valid_starts = valid_starts.astype(int_)
valid_stops = valid_stops.astype(int_)

return (
valid_starts[valid_starts != valid_stops],
valid_stops[valid_starts != valid_stops],
Expand Down
15 changes: 15 additions & 0 deletions tests/utility/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,21 @@ def true_intersect_ends():
return starts, stops


@pytest.fixture(scope="module")
def true_intersect_ends_15s():
starts = {
1: np.array([2795, 3005]),
2: np.array([2495, 3005]),
3: np.array([2300, 3005]),
}
stops = {
1: np.array([2900, 3905]),
2: np.array([2900, 3800]),
3: np.array([2900, 3905]),
}
return starts, stops


@pytest.fixture(scope="module")
def true_sleep_only_ends():
starts = {1: np.array([2000, 2800]), 2: np.array([2500]), 3: np.array([2000])}
Expand Down
23 changes: 23 additions & 0 deletions tests/utility/test_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,29 @@ def test(self, day_ends, sleep_ends, wear_ends, true_intersect_ends):
assert allclose(p_starts, true_starts[i])
assert allclose(p_stops, true_stops[i])

def test_round_15s(self, day_ends, sleep_ends, wear_ends, true_intersect_ends_15s):
day_start, day_stop = day_ends
sleep_starts, sleep_stops = sleep_ends
wear_starts, wear_stops = wear_ends
true_starts, true_stops = true_intersect_ends_15s

for i in range(1, 4):
p_starts, p_stops = get_day_index_intersection(
(sleep_starts[i], wear_starts),
(sleep_stops[i], wear_stops),
(False, True),
day_start,
day_stop,
fs=1.0,
ends_round=15,
)

assert p_starts.size == true_starts[i].size
assert p_stops.size == true_stops[i].size

assert allclose(p_starts, true_starts[i])
assert allclose(p_stops, true_stops[i])

def test_sleep_only(self, day_ends, sleep_ends, true_sleep_only_ends):
day_start, day_stop = day_ends
sleep_starts, sleep_stops = sleep_ends
Expand Down
Loading