From 97d0db3702820d87e088c7f7863dc55332f88193 Mon Sep 17 00:00:00 2001 From: ghammad Date: Thu, 27 Mar 2025 14:33:21 +0100 Subject: [PATCH 1/3] Add reader for generic NSRR files --- pyActigraphy/io/__init__.py | 2 + pyActigraphy/io/nsrr/__init__.py | 11 ++ pyActigraphy/io/nsrr/nsrr.py | 238 +++++++++++++++++++++++++++++++ 3 files changed, 251 insertions(+) create mode 100644 pyActigraphy/io/nsrr/__init__.py create mode 100644 pyActigraphy/io/nsrr/nsrr.py diff --git a/pyActigraphy/io/__init__.py b/pyActigraphy/io/__init__.py index b57764cc..72b5930a 100644 --- a/pyActigraphy/io/__init__.py +++ b/pyActigraphy/io/__init__.py @@ -19,6 +19,7 @@ from .dqt import read_raw_dqt from .mesa import read_raw_mesa from .mtn import read_raw_mtn +from .nsrr import read_raw_nsrr from .rpx import read_raw_rpx from .tal import read_raw_tal @@ -32,6 +33,7 @@ "read_raw_dqt", "read_raw_mesa", "read_raw_mtn", + "read_raw_nsrr" "read_raw_rpx", "read_raw_tal" ] diff --git a/pyActigraphy/io/nsrr/__init__.py b/pyActigraphy/io/nsrr/__init__.py new file mode 100644 index 00000000..a2298079 --- /dev/null +++ b/pyActigraphy/io/nsrr/__init__.py @@ -0,0 +1,11 @@ +"""Module to read generic NSRR files.""" + +# Author: Grégory Hammad +# +# License: BSD (3-clause) + +from .nsrr import RawNSRR + +from .nsrr import read_raw_nsrr + +__all__ = ["RawNSRR", "read_raw_nsrr"] diff --git a/pyActigraphy/io/nsrr/nsrr.py b/pyActigraphy/io/nsrr/nsrr.py new file mode 100644 index 00000000..97b79ad9 --- /dev/null +++ b/pyActigraphy/io/nsrr/nsrr.py @@ -0,0 +1,238 @@ +import pandas as pd +import os +import warnings + +from ..base import BaseRaw +from pyActigraphy.light import LightRecording + + +class RawNSRR(BaseRaw): + r"""Raw object from generic NSRR files + + Parameters + ---------- + input_fname: str + Path to the NSRR file. + time_origin: datetime-like + Time origin of the timestamps. + Required as the NSRR files do not contain date informations. + Default is '2000-01-01' + start_time: datetime-like, optional + Read data from this time. + Default is None. + period: str, optional + Length of the read data. + Cf. #timeseries-offset-aliases in + . + Default is None (i.e all the data). + intervals: dict, optional + Map manually annotated periods to specific scores. + If set to None, the names of the annotated periods is returned instead. + Default is {'EXCLUDED': -1, 'ACTIVE': 1, 'REST': 0.5, 'REST-S': 0}. + check_dayofweek: bool, optional + If set to True, check if the day of the week reported in the original + recoring is aligned with the reconstructed index. + Default is False. + """ + + def __init__( + self, + input_fname, + time_origin='2000-01-01', + start_time=None, + period=None, + intervals={'EXCLUDED': -1, 'ACTIVE': 1, 'REST': 0.5, 'REST-S': 0}, + check_dayofweek=False + ): + + # get absolute file path + input_fname = os.path.abspath(input_fname) + + # read file + data = pd.read_csv(input_fname, index_col='line') + + # extract informations from the header + name = data.iloc[1, 0] + + # set additional informations manually + uuid = None + freq = pd.Timedelta(30, unit='s') + + # day of the week + self.__dayofweek = data['dayofweek'] + + # reconstruct NSRR datetime index + date = pd.to_datetime( + data['daybymidnight'] - 1 + data.loc[1, 'dayofweek'], + unit='D', + origin=time_origin + ).astype(str) + time = data['linetime'] + + index = pd.DatetimeIndex(date + ' ' + time, freq='infer') + + data.set_index(index, inplace=True) + + if check_dayofweek: + # Shift day of the week to match Pandas' convention (0=Monday, etc) + dw = self.__dayofweek - 2 + if (data.index.dayofweek - dw.where(dw >= 0, dw + 7)).sum() != 0: + warnings.warn(( + "Specified time_origin is such that the day of the week in" + " the reconstructed time index is *not* aligned with the" + " day of the week reported in the recording." + )) + + # set start and stop times + if start_time is not None: + start_time = pd.to_datetime(start_time) + else: + start_time = data.index[0] + + if period is not None: + period = pd.Timedelta(period) + stop_time = start_time+period + else: + stop_time = data.index[-1] + period = stop_time - start_time + + data = data[start_time:stop_time] + + # no wear indicator + self.__nowear = data['offwrist'] + + # event marker indicator + self.__marker = data['marker'] + + # LIGHT + self.__white_light = data['whitelight'] + self.__red_light = data['redlight'] + self.__green_light = data['greenlight'] + self.__blue_light = data['bluelight'] + + # wake indicator + self.__wake = data['wake'] + + # intervals + if intervals is not None: + self.__intervals = data['interval'].map(intervals) + else: + self.__intervals = data['interval'] + + # call __init__ function of the base class + super().__init__( + fpath=input_fname, + name=name, + uuid=uuid, + format='NSRR', + axial_mode='tri-axial', + start_time=start_time, + period=period, + frequency=freq, + data=data['activity'], + light=LightRecording( + name=name, + uuid=uuid, + data=data.loc[:, [ + 'whitelight', 'redlight', 'greenlight', 'bluelight' + ] + ], + frequency=data.index.freq + ) + ) + + @property + def marker(self): + r"""Event marker indicator.""" + return self.__marker + + @property + def wake(self): + r"""Awake indicator.""" + return self.__wake + + @property + def nowear(self): + r"""Off-wrist indicator.""" + return self.__nowear + + @property + def intervals(self): + r"""Interval type (manual rest-activty scoring).""" + return self.__intervals + + @property + def dayofweek(self): + r"""Day of the week (1=Sunday, 2=Monday, etc).""" + return self.__dayofweek + + @property + def white_light(self): + r"""Value of the white light illuminance in lux.""" + return self.__white_light + + @property + def red_light(self): + r"""Value of the light intensity in µw/cm².""" + return self.__red_light + + @property + def green_light(self): + r"""Value of the light intensity in µw/cm².""" + return self.__green_light + + @property + def blue_light(self): + r"""Value of the light intensity in µw/cm².""" + return self.__blue_light + + +def read_raw_nsrr( + input_fname, + time_origin='2000-01-01', + start_time=None, + period=None, + intervals={'EXCLUDED': -1, 'ACTIVE': 1, 'REST': 0.5, 'REST-S': 0}, + check_dayofweek=False +): + r"""Reader function for generic NSRR files + + Parameters + ---------- + input_fname: str + Path to the ActTrust file. + time_origin: datetime-like + Time origin of the timestamps. + Required as the NSRR files do not contain date informations. + Default is '2000-01-01' + start_time: datetime-like, optional + Read data from this time. + Default is None. + period: str, optional + Length of the read data. + Cf. #timeseries-offset-aliases in + . + Default is None (i.e all the data). + intervals: dict, optional + Map manually annotated periods to specific scores. + If set to None, the names of the annotated periods is returned instead. + Default is {'EXCLUDED': -1, 'ACTIVE': 1, 'REST': 0.5, 'REST-S': 0}. + check_dayofweek: bool, optional + If set to True, check if the day of the week reported in the original + recoring is aligned with the reconstructed index. + Default is False. + + Returns + ------- + raw : Instance of RawNSRR + An object containing raw NSRR data + """ + + return RawNSRR( + input_fname=input_fname, + time_origin=time_origin, + start_time=start_time, + period=period, + intervals=intervals, + check_dayofweek=check_dayofweek + ) From 8dfa3b8e804ba689a73a6eab1e87a366090628f6 Mon Sep 17 00:00:00 2001 From: ghammad Date: Thu, 27 Mar 2025 14:34:11 +0100 Subject: [PATCH 2/3] Bump version to 1.2.3 --- pyActigraphy/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyActigraphy/__init__.py b/pyActigraphy/__init__.py index 71eb1ba1..30256ccd 100644 --- a/pyActigraphy/__init__.py +++ b/pyActigraphy/__init__.py @@ -41,4 +41,4 @@ "viz" ] -__version__ = '1.2.2' +__version__ = '1.2.3' From 4458a53b16f619392764f9668254432b6831a885 Mon Sep 17 00:00:00 2001 From: ghammad Date: Thu, 27 Mar 2025 14:42:16 +0100 Subject: [PATCH 3/3] Add support for NSRR in batch reader --- pyActigraphy/io/reader/reader.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/pyActigraphy/io/reader/reader.py b/pyActigraphy/io/reader/reader.py index 16b3406f..bda8e54c 100644 --- a/pyActigraphy/io/reader/reader.py +++ b/pyActigraphy/io/reader/reader.py @@ -13,6 +13,7 @@ from ..dqt import read_raw_dqt from ..mesa import read_raw_mesa from ..mtn import read_raw_mtn +from ..nsrr import read_raw_nsrr from ..rpx import read_raw_rpx from ..tal import read_raw_tal from pyActigraphy.log import read_sst_log @@ -238,6 +239,7 @@ def read_raw( * DQT (Daqtometers, Daqtix) * MESA (MESA dataset, NSRR) * MTN (MotionWatch8, CamNtech) + * NSRR (generic file, NSRR) * RPX (Actiwatch, Respironics) * TAL (Tempatilumi, CE Brasil) @@ -262,7 +264,7 @@ def read_raw( """ supported_types = [ - 'AGD', 'ATR', 'AWD', 'BBA', 'DQT', 'MESA', 'MTN', 'RPX', 'TAL' + 'AGD', 'ATR', 'AWD', 'BBA', 'DQT', 'MESA', 'MTN', 'NSRR', 'RPX', 'TAL' ] if reader_type not in supported_types: raise ValueError( @@ -302,6 +304,9 @@ def parallel_reader( 'MTN': lambda files: parallel_reader( n_jobs, read_raw_mtn, files, prefer, verbose, **kwargs ), + 'NSRR': lambda files: parallel_reader( + n_jobs, read_raw_nsrr, files, prefer, verbose, **kwargs + ), 'RPX': lambda files: parallel_reader( n_jobs, read_raw_rpx, files, prefer, verbose, **kwargs ),