From 981adb227adc6143b6f72854260dee828cd6de56 Mon Sep 17 00:00:00 2001 From: Adam Santorelli <148909356+Asanto32@users.noreply.github.com> Date: Mon, 9 Dec 2024 09:38:11 -0600 Subject: [PATCH] Feat/issue 102/handle idle sleep mode (#143) * Initial support for reading idle_sleep_mode Get idle_sleep_mode flag from metadata Only available for .gt3x files. Add it to watch_Data class to access later * Create idle_sleep_mode_imputation.py Initial version of the idle_sleep_mode data imputation, resamples for unevenly spaced samples * Create test_idle_sleep_mode.py Initial unit tests to check effective sampling rate is correct * Update test_idle_sleep_mode.py Adding more unit tests * Update test_idle_sleep_mode.py Check filled value is assigned Spacing for AAA testing * Update idle_sleep_mode_imputation.py Fix mypy error with this ugly monstrosity * Update test_idle_sleep_mode.py Fix to ensure numerical result from time.diff().mean() * Update orchestrator.py Adding check for idle_sleep_mode into run_file() Might be hard to hit this line for testing * Update orchestrator.py Fix ruff format error * New sample data Added new sample data for idle_sleep_mode = true smoke test * Addressing PR comments Moving idle_sleep_mode imputation to after the calibration step Imputing acceleration to exactly (0,0,-1) * Update test_orchestrator.py Path error for the full_dir processing * Update README.md Added info about handling idle_sleep_mode * Update readers.py Added info to docstring about idle_sleep_mode_flag settings * Update idle_sleep_mode_imputation.py Fix docstring for new fill value * Update pyproject.toml Update version number * Update README.md Fix zenodo badge for DOI that resolves to latest version --- README.md | 17 ++-- pyproject.toml | 2 +- src/wristpy/core/models.py | 1 + src/wristpy/core/orchestrator.py | 15 +++- src/wristpy/io/readers/readers.py | 9 ++ .../processing/idle_sleep_mode_imputation.py | 81 ++++++++++++++++++ tests/conftest.py | 10 +++ .../example_actigraph_idle_sleep_mode.gt3x | Bin 0 -> 21356 bytes tests/smoke/test_orchestrator_smoke.py | 18 ++++ tests/unit/test_idle_sleep_mode.py | 78 +++++++++++++++++ tests/unit/test_models.py | 5 +- tests/unit/test_orchestrator.py | 1 + tests/unit/test_readers.py | 2 + 13 files changed, 227 insertions(+), 12 deletions(-) create mode 100644 src/wristpy/processing/idle_sleep_mode_imputation.py create mode 100644 tests/sample_data/example_actigraph_idle_sleep_mode.gt3x create mode 100644 tests/unit/test_idle_sleep_mode.py diff --git a/README.md b/README.md index 8be8fa6b..2221e923 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.13883191.svg)](https://doi.org/10.5281/zenodo.13883191) +[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.13883190.svg)](https://doi.org/10.5281/zenodo.13883190) # `wristpy` @@ -25,18 +25,19 @@ The package currently supports the following formats: | BIN | GENEActiv | GENEActiv | ✅ | **Special Note** - The `idle_sleep_mode` for Actigraph watches will lead to uneven sampling rates during periods of no motion (read about this [here](https://actigraphcorp.my.site.com/support/s/article/Idle-Sleep-Mode-Explained)). Consequently, this causes issues when implementing wristpy's non-wear and sleep detection. As of this moment, the authors of this package do not take any steps to impute data during these time gaps and would caution to not use data collected with this mode enabled. Of course, users can make use of the readers within wristpy for their own analysis with this type of data. + The `idle_sleep_mode` for Actigraph watches will lead to uneven sampling rates during periods of no motion (read about this [here](https://actigraphcorp.my.site.com/support/s/article/Idle-Sleep-Mode-Explained)). Consequently, this causes issues when implementing wristpy's non-wear and sleep detection. As of this moment, we fill in the missing acceleration data with the assumption that the watch is perfeclty idle in the face-up position (Acceleration vector = [0, 0, -1]). The data is filled in at the same sampling rate as the raw acceleration data. In the special circumstance when acceleration samples are not evenly spaced, the data is resampled to the highest effective sampling rate to ensure linearly sampled data. ## Processing pipeline implementation The main processing pipeline of the wristpy module can be described as follows: -- Data loading: sensor data is loaded using [`actfast`](https://github.com/childmindresearch/actfast), and a `WatchData` object is created to store all sensor data -- Data calibration: A post-manufacturer calibration step can be applied, to ensure that the acceleration sensor is measuring 1*g* force during periods of no motion. There are three possible options: `None`, `gradient`, `ggir`. -- Metrics Calculation: Calculates various metrics on the calibrated data, namely ENMO (euclidean norm , minus one) and angle-Z (angle of acceleration relative to the *x-y* axis). -- Non-wear detection: We find periods of non-wear based on the acceleration data. Specifically, the standard deviation of the acceleration values in a given time window, along each axis, is used as a threshold to decide `wear` or `not wear`. -- Sleep Detection: Using the HDCZ1 and HSPT2 algorithms to analyze changes in arm angle we are able to find periods of sleep. We find the sleep onset-wakeup times for all sleep windows detected. -- Physical activity levels: Using the enmo data (aggreagated into epoch 1 time bins, 5 second default) we compute activity levels into the following categories: inactivity, light activity, moderate activity, vigorous activity. The default threshold values have been chosen based on the values presented in the Hildenbrand 2014 study3. +- **Data loading**: sensor data is loaded using [`actfast`](https://github.com/childmindresearch/actfast), and a `WatchData` object is created to store all sensor data +- **Data calibration**: A post-manufacturer calibration step can be applied, to ensure that the acceleration sensor is measuring 1*g* force during periods of no motion. There are three possible options: `None`, `gradient`, `ggir`. +- ***Data imputation*** In the special case when dealing with the Actigraph `idle_sleep_mode == enabled`, the gaps in acceleration are filled in after calibration, to avoid biasing the calibration phase. +- **Metrics Calculation**: Calculates various metrics on the calibrated data, namely ENMO (euclidean norm , minus one) and angle-Z (angle of acceleration relative to the *x-y* axis). +- **Non-wear detection**: We find periods of non-wear based on the acceleration data. Specifically, the standard deviation of the acceleration values in a given time window, along each axis, is used as a threshold to decide `wear` or `not wear`. +- **Sleep Detection**: Using the HDCZ1 and HSPT2 algorithms to analyze changes in arm angle we are able to find periods of sleep. We find the sleep onset-wakeup times for all sleep windows detected. +- **Physical activity levels**: Using the enmo data (aggreagated into epoch 1 time bins, 5 second default) we compute activity levels into the following categories: inactivity, light activity, moderate activity, vigorous activity. The default threshold values have been chosen based on the values presented in the Hildenbrand 2014 study3. ## Installation diff --git a/pyproject.toml b/pyproject.toml index 7bbbb89b..939788ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "wristpy" -version = "0.1.0" +version = "0.1.1" description = "wristpy is a Python package designed for processing and analyzing wrist-worn accelerometer data." authors = [ "Adam Santorelli ", diff --git a/src/wristpy/core/models.py b/src/wristpy/core/models.py index 668b7a3e..5f3246fe 100644 --- a/src/wristpy/core/models.py +++ b/src/wristpy/core/models.py @@ -113,6 +113,7 @@ class WatchData(BaseModel): battery: Optional[Measurement] = None capsense: Optional[Measurement] = None temperature: Optional[Measurement] = None + idle_sleep_mode_flag: Optional[bool] = None @field_validator("acceleration") def validate_acceleration(cls, v: Measurement) -> Measurement: diff --git a/src/wristpy/core/orchestrator.py b/src/wristpy/core/orchestrator.py index 9cb60e51..b3f50a9e 100644 --- a/src/wristpy/core/orchestrator.py +++ b/src/wristpy/core/orchestrator.py @@ -11,7 +11,12 @@ from wristpy.core import computations, config, exceptions, models from wristpy.io.readers import readers -from wristpy.processing import analytics, calibration, metrics +from wristpy.processing import ( + analytics, + calibration, + idle_sleep_mode_imputation, + metrics, +) logger = config.get_logger() @@ -350,7 +355,13 @@ def _run_file( "Calibration FAILED: %s. Proceeding without calibration.", exc_info ) calibrated_acceleration = watch_data.acceleration - + if watch_data.idle_sleep_mode_flag: + logger.debug("Imputing idle sleep mode gaps.") + calibrated_acceleration = ( + idle_sleep_mode_imputation.impute_idle_sleep_mode_gaps( + calibrated_acceleration + ) + ) enmo = metrics.euclidean_norm_minus_one(calibrated_acceleration) anglez = metrics.angle_relative_to_horizontal(calibrated_acceleration) sleep_detector = analytics.GgirSleepDetection(anglez) diff --git a/src/wristpy/io/readers/readers.py b/src/wristpy/io/readers/readers.py index b79615b9..b3896c0a 100644 --- a/src/wristpy/io/readers/readers.py +++ b/src/wristpy/io/readers/readers.py @@ -1,5 +1,6 @@ """Function to read accelerometer data from a file.""" +import os import pathlib from typing import Literal, Union @@ -14,6 +15,8 @@ def read_watch_data(file_name: Union[pathlib.Path, str]) -> models.WatchData: """Read watch data from a file. Currently supported watch types are Actigraph .gt3x and GeneActiv .bin. + Assigns the idle_sleep_mode_flag to false unless the watchtype is .gt3x and + sleep_mode is enabled (based on watch metadata). Args: file_name: The filename to read the watch data from. @@ -36,6 +39,11 @@ def read_watch_data(file_name: Union[pathlib.Path, str]) -> models.WatchData: measurements[sensor_name] = models.Measurement( measurements=sensor_values, time=time ) + idle_sleep_mode_flag = False + if os.path.splitext(file_name)[1] == ".gt3x": + idle_sleep_mode_flag = ( + data["metadata"]["device_feature_enabled"]["sleep_mode"].lower() == "true" + ) return models.WatchData( acceleration=measurements["acceleration"], @@ -43,6 +51,7 @@ def read_watch_data(file_name: Union[pathlib.Path, str]) -> models.WatchData: battery=measurements.get("battery_voltage"), capsense=measurements.get("capsense"), temperature=measurements.get("temperature"), + idle_sleep_mode_flag=idle_sleep_mode_flag, ) diff --git a/src/wristpy/processing/idle_sleep_mode_imputation.py b/src/wristpy/processing/idle_sleep_mode_imputation.py new file mode 100644 index 00000000..a403ff5f --- /dev/null +++ b/src/wristpy/processing/idle_sleep_mode_imputation.py @@ -0,0 +1,81 @@ +"""Handle idle sleep mode special case.""" + +import numpy as np +import polars as pl + +from wristpy.core import models + + +def impute_idle_sleep_mode_gaps(acceleration: models.Measurement) -> models.Measurement: + """This function imputes the gaps in the idle sleep mode data. + + Gaps in the acceleration data are filled by assuming the watch is idle in a face up + position. The acceleration data is filled in at a linear sampling rate, estimated + based on the first 100 samples timestamps, with (0, 0, -1). + + In cases when the sampling rate leads to unevenly spaced samples within one second, + eg. 30Hz sampling rate has samples spaced at 33333333ns and 33333343ns within one + second, the entire data set will be resampled at the highest effective sampling rate + that allows for for linearly spaced samples within one second, + to nanosecond precision. + + Args: + acceleration: The raw acceleration data. + + Returns: + A Measurement object with the modified acceleration data. + """ + + def _find_effective_sampling_rate(sampling_rate: int) -> int: + """Helper function to find the effective sampling rate. + + This function finds the new sampling rate that allows for linearly spaced + samples within one second, to nanosecond precision. + + Args: + sampling_rate: The original sampling rate. + + Returns: + The new effective sampling rate. + """ + for effective_sr in range(sampling_rate, 1, -1): + if 1e9 % (1e9 / effective_sr) == 0: + return effective_sr + return 1 + + acceleration_polars_df = pl.DataFrame( + { + "X": acceleration.measurements[:, 0], + "Y": acceleration.measurements[:, 1], + "Z": acceleration.measurements[:, 2], + "time": acceleration.time, + } + ) + + sampling_space_nanosec = np.mean( + acceleration.time[:100] + .diff() + .drop_nulls() + .dt.total_nanoseconds() + .to_numpy() + .astype(dtype=float) + ) + + sampling_rate = int(1e9 / sampling_space_nanosec) + + effective_sampling_rate = _find_effective_sampling_rate(sampling_rate) + effective_sampling_interval = int(1e9 / effective_sampling_rate) + + filled_acceleration = ( + acceleration_polars_df.set_sorted("time") + .group_by_dynamic("time", every=f"{effective_sampling_interval}ns") + .agg(pl.exclude("time").mean()) + .upsample("time", every=f"{effective_sampling_interval}ns", maintain_order=True) + .with_columns( + pl.col("X").fill_null(value=0), + pl.col("Y").fill_null(value=0), + pl.col("Z").fill_null(value=-1), + ) + ) + + return models.Measurement.from_data_frame(filled_acceleration) diff --git a/tests/conftest.py b/tests/conftest.py index 026ec424..63787d0c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -11,6 +11,16 @@ def sample_data_gt3x() -> pathlib.Path: return pathlib.Path(__file__).parent / "sample_data" / "example_actigraph.gt3x" +@pytest.fixture +def sample_data_gt3x_idle_sleep_mode() -> pathlib.Path: + """Test data for .gt3x data file.""" + return ( + pathlib.Path(__file__).parent + / "sample_data" + / "example_actigraph_idle_sleep_mode.gt3x" + ) + + @pytest.fixture def sample_data_bin() -> pathlib.Path: """Test data for .bin data file.""" diff --git a/tests/sample_data/example_actigraph_idle_sleep_mode.gt3x b/tests/sample_data/example_actigraph_idle_sleep_mode.gt3x new file mode 100644 index 0000000000000000000000000000000000000000..6112ca415e1020ddc4e1d5520e7d7ecdd086a512 GIT binary patch literal 21356 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 literal 0 HcmV?d00001 diff --git a/tests/smoke/test_orchestrator_smoke.py b/tests/smoke/test_orchestrator_smoke.py index 4904e871..6638574f 100644 --- a/tests/smoke/test_orchestrator_smoke.py +++ b/tests/smoke/test_orchestrator_smoke.py @@ -47,3 +47,21 @@ def test_orchestrator_different_epoch( assert isinstance(results.nonwear_epoch, models.Measurement) assert isinstance(results.sleep_windows_epoch, models.Measurement) assert isinstance(results.physical_activity_levels, models.Measurement) + + +def test_orchestrator_idle_sleep_mode_run( + tmp_path: pathlib.Path, + sample_data_gt3x_idle_sleep_mode: pathlib.Path, +) -> None: + """Idle sleep mode path for orchestrator.""" + results = orchestrator.run( + input=sample_data_gt3x_idle_sleep_mode, output=tmp_path / "good_file.csv" + ) + + assert (tmp_path / "good_file.csv").exists() + assert isinstance(results, models.OrchestratorResults) + assert isinstance(results.enmo, models.Measurement) + assert isinstance(results.anglez, models.Measurement) + assert isinstance(results.nonwear_epoch, models.Measurement) + assert isinstance(results.sleep_windows_epoch, models.Measurement) + assert isinstance(results.physical_activity_levels, models.Measurement) diff --git a/tests/unit/test_idle_sleep_mode.py b/tests/unit/test_idle_sleep_mode.py new file mode 100644 index 00000000..69a0ec39 --- /dev/null +++ b/tests/unit/test_idle_sleep_mode.py @@ -0,0 +1,78 @@ +"""Testing the idle_sleep_mode functions.""" + +from datetime import datetime, timedelta + +import numpy as np +import polars as pl +import pytest + +from wristpy.core import models +from wristpy.processing import idle_sleep_mode_imputation + + +@pytest.mark.parametrize( + "sampling_rate, effective_sampling_rate", [(30, 25), (20, 20), (1, 1)] +) +def test_idle_sleep_mode_resampling( + sampling_rate: int, effective_sampling_rate: int +) -> None: + """Test the idle_sleep_mode function.""" + num_samples = 10000 + dummy_date = datetime(2024, 5, 2) + dummy_datetime_list = [ + dummy_date + timedelta(seconds=i / sampling_rate) for i in range(num_samples) + ] + test_time = pl.Series("time", dummy_datetime_list, dtype=pl.Datetime("ns")) + acceleration = models.Measurement( + measurements=np.ones((num_samples, 3)), time=test_time + ) + + filled_acceleration = idle_sleep_mode_imputation.impute_idle_sleep_mode_gaps( + acceleration + ) + + assert ( + np.mean( + filled_acceleration.time.diff() + .drop_nulls() + .dt.total_nanoseconds() + .to_numpy() + .astype(dtype=float) + ) + == 1e9 / effective_sampling_rate + ) + + +def test_idle_sleep_mode_gap_fill() -> None: + """Test the idle_sleep_mode gap fill functionality.""" + num_samples = 10000 + dummy_date = datetime(2024, 5, 2) + dummy_datetime_list = [ + dummy_date + timedelta(seconds=i) for i in range(num_samples // 2) + ] + time_gap = dummy_date + timedelta(seconds=(1000)) + dummy_datetime_list += [ + time_gap + timedelta(seconds=i) for i in range(num_samples // 2, num_samples) + ] + test_time = pl.Series("time", dummy_datetime_list, dtype=pl.Datetime("ns")) + acceleration = models.Measurement( + measurements=np.ones((num_samples, 3)), time=test_time + ) + expected_acceleration = (0, 0, -1) + + filled_acceleration = idle_sleep_mode_imputation.impute_idle_sleep_mode_gaps( + acceleration + ) + + assert len(filled_acceleration.measurements) > len(acceleration.measurements) + assert ( + np.mean( + filled_acceleration.time.diff() + .drop_nulls() + .dt.total_nanoseconds() + .to_numpy() + .astype(dtype=float) + ) + == 1e9 + ) + assert np.all(filled_acceleration.measurements[5010] == expected_acceleration) diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index 24c5eac4..a659f6ee 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -42,7 +42,9 @@ def test_watchdata_model() -> None: lux = models.Measurement(measurements=sensor_data, time=time) temp = models.Measurement(measurements=sensor_data, time=time) - watch_data = models.WatchData(acceleration=acceleration, lux=lux, temperature=temp) + watch_data = models.WatchData( + acceleration=acceleration, lux=lux, temperature=temp, idle_sleep_mode_flag=True + ) if watch_data.lux is not None: assert np.array_equal(watch_data.lux.measurements, sensor_data) @@ -53,6 +55,7 @@ def test_watchdata_model() -> None: np.array([1, 2, 3]) * 1000000, ) assert isinstance(watch_data.battery, type(None)) + assert watch_data.idle_sleep_mode_flag def test_measurement_model_time_type() -> None: diff --git a/tests/unit/test_orchestrator.py b/tests/unit/test_orchestrator.py index 5161d2c2..f02266a7 100644 --- a/tests/unit/test_orchestrator.py +++ b/tests/unit/test_orchestrator.py @@ -148,6 +148,7 @@ def test_run_dir(tmp_path: pathlib.Path, sample_data_gt3x: pathlib.Path) -> None expected_files = { tmp_path / "example_actigraph.csv", tmp_path / "example_geneactiv.csv", + tmp_path / "example_actigraph_idle_sleep_mode.csv", } results = orchestrator.run(input=input_dir, output=tmp_path, output_filetype=".csv") diff --git a/tests/unit/test_readers.py b/tests/unit/test_readers.py index 5ffc4ac5..319b37e9 100644 --- a/tests/unit/test_readers.py +++ b/tests/unit/test_readers.py @@ -23,6 +23,7 @@ def test_gt3x_loader(sample_data_gt3x: pathlib.Path) -> None: assert isinstance(watch_data.battery, models.Measurement) assert isinstance(watch_data.capsense, models.Measurement) assert watch_data.temperature is None + assert watch_data.idle_sleep_mode_flag is False def test_geneactiv_bin_loader(sample_data_bin: pathlib.Path) -> None: @@ -34,6 +35,7 @@ def test_geneactiv_bin_loader(sample_data_bin: pathlib.Path) -> None: assert isinstance(watch_data.battery, models.Measurement) assert isinstance(watch_data.temperature, models.Measurement) assert watch_data.capsense is None + assert watch_data.idle_sleep_mode_flag is False def test_nonexistent_file() -> None: