Skip to content

Commit 2c5ff7c

Browse files
committed
reimplement code to open agd files
1 parent 16428f2 commit 2c5ff7c

4 files changed

Lines changed: 551 additions & 4147 deletions

File tree

circStudio/io/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from .base import Raw
22
from .atr import read_atr
33
from .awd import read_awd
4+
from .agd import read_agd
45

5-
__all__ = ["Raw", "read_atr", "read_awd"]
6+
__all__ = ["Raw", "read_atr", "read_awd", "read_agd"]

circStudio/io/agd/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from .agd import AGD
2+
3+
from .agd import read_agd
4+
5+
__all__ = ["AGD", "read_agd"]

circStudio/io/agd/agd.py

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
import pandas as pd
2+
import numpy as np
3+
import os
4+
import sqlite3
5+
import warnings
6+
7+
from ..base import Raw
8+
9+
10+
class AGD(Raw):
11+
r"""Raw object from .agd file (recorded by Actigraph)
12+
13+
Parameters
14+
----------
15+
input_fname: str
16+
Path to the AGD file.
17+
start_time: datetime-like, optional
18+
Read data from this time.
19+
Default is None.
20+
period: str, optional
21+
Length of the read data.
22+
Cf. #timeseries-offset-aliases in
23+
<https://pandas.pydata.org/pandas-docs/stable/timeseries.html>.
24+
Default is None (i.e., all the data).
25+
"""
26+
27+
def __init__(
28+
self,
29+
input_fname,
30+
start_time=None,
31+
period=None
32+
):
33+
34+
# get absolute file path
35+
input_fname = os.path.abspath(input_fname)
36+
37+
# create connection to the SQLITE3 .agd file in read-only mode
38+
connection = sqlite3.connect('file:'+input_fname+'?mode=ro', uri=True)
39+
40+
# extract header and data size
41+
settings = pd.read_sql_query(
42+
"SELECT * FROM settings",
43+
connection,
44+
index_col='settingName'
45+
)
46+
47+
# extract information from the header
48+
start = self._to_timestamps(
49+
int(settings.at['startdatetime', 'settingValue'])
50+
)
51+
freq = pd.to_timedelta(
52+
int(settings.at['epochlength', 'settingValue']),
53+
unit='s'
54+
)
55+
self._model = settings.at['devicename', 'settingValue']
56+
57+
# extract proximity (wear/no-wear) information
58+
try:
59+
capsense = pd.read_sql_query(
60+
"SELECT state, timeStamp FROM capsense",
61+
connection,
62+
index_col='timeStamp'
63+
).squeeze()
64+
# convert index to a date time
65+
capsense.index = self._to_timestamps(capsense.index)
66+
# set index frequency
67+
if 'proximityIntervalInSeconds' in settings.index:
68+
self.capsense = capsense.asfreq(
69+
pd.to_timedelta(
70+
int(
71+
settings.at[
72+
'proximityIntervalInSeconds',
73+
'settingValue'
74+
]
75+
),
76+
unit='s'
77+
)
78+
)
79+
elif capsense.index.inferred_freq is not None:
80+
self.capsense = capsense.asfreq(capsense.index.inferred_freq)
81+
else:
82+
warnings.warn(
83+
'Acquisition frequency for the wearing sensor data '
84+
+ '(capsense) could neither be retrieved nor inferred from'
85+
+ ' the data.\n Use this information at your own risk',
86+
UserWarning
87+
)
88+
self.capsense = capsense
89+
except Exception as err:
90+
# TODO: specialize exception (eg;sqlite3.OperationalError)
91+
warnings.warn(
92+
'Could not find wearing sensor data (capsense): {}'.format(
93+
err),
94+
UserWarning
95+
)
96+
self.capsense = None
97+
98+
# extract acceleration and light data
99+
data = pd.read_sql_query(
100+
"SELECT * FROM data",
101+
connection,
102+
index_col='dataTimestamp'
103+
)
104+
# convert index to a date time
105+
data.index = self._to_timestamps(data.index)
106+
# set index frequency
107+
data = data.asfreq(freq=freq)
108+
109+
# calculate the magnitude of the acceleration vector.
110+
data['mag'] = np.sqrt(
111+
data['axis1']*data['axis1']
112+
+ data['axis2']*data['axis2']
113+
+ data['axis3']*data['axis3']
114+
)
115+
116+
if start_time is not None:
117+
start_time = pd.to_datetime(start_time)
118+
else:
119+
start_time = start
120+
121+
if period is not None:
122+
period = pd.Timedelta(period)
123+
stop_time = start_time+period
124+
else:
125+
stop_time = data.index[-1]
126+
period = stop_time - start_time
127+
128+
data = data.loc[start_time:stop_time]
129+
130+
# Extract position info:
131+
self._position = data.filter(regex='incline*', axis='columns')
132+
133+
# call __init__ function of the base class
134+
super().__init__(
135+
df=data,
136+
fpath=input_fname,
137+
start_time=start_time,
138+
period=period,
139+
frequency=freq,
140+
activity=data['mag'],
141+
light=data['lux'].to_frame(name='whitelight') if 'lux' in data.columns else None
142+
)
143+
144+
# Close sqlite3 connection
145+
connection.close()
146+
147+
def _extract_position(self, column):
148+
if column in self._position.columns:
149+
return self._position.loc[:, column]
150+
else:
151+
return None
152+
153+
def _extract_light_channel(self, channel):
154+
if self.light is None:
155+
return None
156+
else:
157+
return self.light.get_channel(channel)
158+
159+
@property
160+
def white_light(self):
161+
r"""White light levels (in lux.)"""
162+
return self._extract_light_channel("whitelight")
163+
164+
@property
165+
def model(self):
166+
r"""Model of the device: devicename"""
167+
return self._model
168+
169+
@staticmethod
170+
def _to_timestamps(ticks):
171+
return pd.to_datetime(
172+
(ticks/10000000) - 62135596800,
173+
unit='s'
174+
)
175+
176+
@property
177+
def incline_off(self):
178+
r"""Hourly positional information: inclineOff """
179+
return self._extract_position('inclineOff')
180+
181+
@property
182+
def incline_standing(self):
183+
r"""Hourly positional information: inclineStanding """
184+
return self._extract_position('inclineStanding')
185+
186+
@property
187+
def incline_sitting(self):
188+
r"""Hourly positional information: inclineSitting """
189+
return self._extract_position('inclineSitting')
190+
191+
@property
192+
def incline_lying(self):
193+
r"""Hourly positional information: inclineLying """
194+
return self._extract_position('inclineLying')
195+
196+
def incline_position(self, pos_map=None):
197+
r"""Reader function for raw AWD file.
198+
199+
Parameters
200+
----------
201+
pos_map: dict, optional
202+
Positional information map.
203+
Can be used to turn positional info (str) into int, for example.
204+
Set to None to keep original information.
205+
Default mapping is:
206+
* 'inclineOff': -1
207+
* 'inclineLying': 0
208+
* 'inclineSitting': 1
209+
* 'inclineStanding': 2
210+
211+
Returns
212+
-------
213+
pos : Pandas.Series
214+
Time series with positional information.
215+
"""
216+
if pos_map is None:
217+
pos_map = {
218+
'inclineOff': -1,
219+
'inclineLying': 0,
220+
'inclineSitting': 1,
221+
'inclineStanding': 2
222+
}
223+
pos = self._position.idxmax(axis=1)
224+
return pos.map(pos_map) if pos_map is not None else pos
225+
226+
227+
def read_agd(input_fname, start_time=None, period=None):
228+
r"""Reader function for raw AWD file.
229+
230+
Parameters
231+
----------
232+
input_fname: str
233+
Path to the AGD file.
234+
start_time: datetime-like, optional
235+
Read data from this time.
236+
Default is None.
237+
period: str, optional
238+
Length of the read data.
239+
Cf. #timeseries-offset-aliases in
240+
<https://pandas.pydata.org/pandas-docs/stable/timeseries.html>.
241+
Default is None (i.e all the data).
242+
243+
Returns
244+
-------
245+
raw : Instance of RawAWD
246+
An object containing raw AWD data
247+
"""
248+
249+
return AGD(
250+
input_fname=input_fname,
251+
start_time=start_time,
252+
period=period
253+
)

0 commit comments

Comments
 (0)