Skip to content

Commit 0fcc37f

Browse files
authored
Merge pull request #1404 from PeterNSteinmetz/factorHeaderHandling
NeuralynxIO: Factor out header handling.
2 parents c33de92 + 1ae5841 commit 0fcc37f

File tree

3 files changed

+58
-27
lines changed

3 files changed

+58
-27
lines changed

neo/rawio/neuralynxrawio/ncssections.py

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -51,19 +51,13 @@ class NcsSection:
5151

5252
_RECORD_SIZE = 512 # nb sample per signal record
5353

54-
def __init__(self):
55-
self.startRec = -1 # index of starting record
56-
self.startTime = -1 # starttime of first record
57-
self.endRec = -1 # index of last record (inclusive)
58-
self.endTime = -1 # end time of last record, that is, the end time of the last
59-
# sampling period contained in the last record of the section
60-
6154
def __init__(self, sb, st, eb, et, ns):
62-
self.startRec = sb
63-
self.startTime = st
64-
self.endRec = eb
65-
self.endTime = et
66-
self.n_samples = ns
55+
self.startRec = sb # index of starting record
56+
self.startTime = st # starttime of first record
57+
self.endRec = eb # index of last record (inclusive)
58+
self.endTime = et # end time of last record, that is, the end time of the last
59+
# sampling period contained in the last record of the section
60+
self.n_samples = ns # number of samples in record which are valid
6761

6862
def __eq__(self, other):
6963
return (

neo/rawio/neuralynxrawio/nlxheader.py

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -124,18 +124,46 @@ def _to_bool(txt):
124124
),
125125
}
126126

127-
def __init__(self, filename):
127+
def __init__(self, filename, props_only=False):
128128
"""
129129
Factory function to build NlxHeader for a given file.
130+
131+
:param filename: name of Neuralynx file
132+
:param props_only: if true, will not try and read time and date or check start
130133
"""
131134
super(OrderedDict, self).__init__()
132135
with open(filename, "rb") as f:
133136
txt_header = f.read(NlxHeader.HEADER_SIZE)
134137
txt_header = txt_header.strip(b"\x00").decode("latin-1")
135138

136139
# must start with 8 # characters
137-
assert txt_header.startswith("########"), "Neuralynx files must start with 8 # characters."
140+
if not props_only and not txt_header.startswith("########"):
141+
ValueError("Neuralynx files must start with 8 # characters.")
142+
143+
self.read_properties(filename, txt_header)
144+
145+
if not props_only:
146+
self.readTimeDate(txt_header)
147+
148+
@staticmethod
149+
def build_with_properties_only(filename):
150+
"""
151+
Builds a version of the header without time and date or other validity checking.
152+
153+
Intended mostly for utilities but may also be useful for some recalcitrant header formats.
138154
155+
:param filename: name of Neuralynx file.
156+
:return: NlxHeader with properties from header text
157+
"""
158+
res = OrderedDict()
159+
160+
161+
def read_properties(self, filename, txt_header):
162+
"""
163+
Read properties from header and place in OrderedDictionary which this object is.
164+
:param filename: name of ncs file, used for extracting channel number
165+
:param txt_header: header text
166+
"""
139167
# find keys
140168
for k1, k2, type_ in NlxHeader.txt_header_keys:
141169
pattern = r"-(?P<name>" + k1 + r")\s+(?P<value>[\S ]*)"
@@ -149,17 +177,14 @@ def __init__(self, filename):
149177
if type_ is not None:
150178
value = type_(value)
151179
self[name] = value
152-
153180
# if channel_ids or s not in self then the filename is used
154181
name = os.path.splitext(os.path.basename(filename))[0]
155-
156182
# convert channel ids
157183
if "channel_ids" in self:
158184
chid_entries = re.findall(r"\S+", self["channel_ids"])
159185
self["channel_ids"] = [int(c) for c in chid_entries]
160186
else:
161187
self["channel_ids"] = ["unknown"]
162-
163188
# convert channel names
164189
if "channel_names" in self:
165190
name_entries = re.findall(r"\S+", self["channel_names"])
@@ -170,7 +195,6 @@ def __init__(self, filename):
170195
), "Number of channel ids does not match channel names."
171196
else:
172197
self["channel_names"] = ["unknown"] * len(self["channel_ids"])
173-
174198
# version and application name
175199
# older Cheetah versions with CheetahRev property
176200
if "CheetahRev" in self:
@@ -192,11 +216,9 @@ def __init__(self, filename):
192216
else:
193217
self["ApplicationName"] = "Neuraview"
194218
app_version = "2"
195-
196219
if " Development" in app_version:
197220
app_version = app_version.replace(" Development", ".dev0")
198221
self["ApplicationVersion"] = Version(app_version)
199-
200222
# convert bit_to_microvolt
201223
if "bit_to_microVolt" in self:
202224
btm_entries = re.findall(r"\S+", self["bit_to_microVolt"])
@@ -206,7 +228,6 @@ def __init__(self, filename):
206228
assert len(self["bit_to_microVolt"]) == len(
207229
self["channel_ids"]
208230
), "Number of channel ids does not match bit_to_microVolt conversion factors."
209-
210231
if "InputRange" in self:
211232
ir_entries = re.findall(r"\w+", self["InputRange"])
212233
if len(ir_entries) == 1:
@@ -217,9 +238,13 @@ def __init__(self, filename):
217238
chid_entries
218239
), "Number of channel ids does not match input range values."
219240

220-
# Format of datetime depends on app name, app version
221-
# :TODO: this works for current examples but is not likely actually related
222-
# to app version in this manner.
241+
def readTimeDate(self, txt_header):
242+
"""
243+
Read time and date from text of header appropriate for app name and version
244+
245+
:TODO: this works for current examples but is not likely actually related
246+
to app version in this manner.
247+
"""
223248
an = self["ApplicationName"]
224249
if an == "Cheetah":
225250
av = self["ApplicationVersion"]
@@ -245,7 +270,6 @@ def __init__(self, filename):
245270
an = "Unknown"
246271
av = "NA"
247272
hpd = NlxHeader.header_pattern_dicts["def"]
248-
249273
# opening time
250274
sr = re.search(hpd["datetime1_regex"], txt_header)
251275
if not sr:
@@ -257,7 +281,6 @@ def __init__(self, filename):
257281
self["recording_opened"] = datetime.datetime.strptime(
258282
dt1["date"] + " " + dt1["time"], hpd["datetimeformat"]
259283
)
260-
261284
# close time, if available
262285
if "datetime2_regex" in hpd:
263286
sr = re.search(hpd["datetime2_regex"], txt_header)

neo/test/rawiotest/test_neuralynxrawio.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ class TestNeuralynxRawIO(
2727
"neuralynx/Cheetah_v5.5.1/original_data",
2828
"neuralynx/Cheetah_v5.6.3/original_data",
2929
"neuralynx/Cheetah_v5.7.4/original_data",
30-
"neuralynx/Cheetah_v6.3.2/incomplete_blocks",
30+
"neuralynx/Cheetah_v6.3.2/incomplete_blocks"
3131
]
3232

3333
def test_scan_ncs_files(self):
@@ -356,5 +356,19 @@ def test_equality(self):
356356
self.assertNotEqual(ns0, ns1)
357357

358358

359+
# I comment this now and will put it back when files will be in gin.g-node
360+
# class TestNlxHeader(TestNeuralynxRawIO, unittest.TestCase):
361+
# def test_no_date_time(self):
362+
# filename = self.get_local_path("neuralynx/NoDateHeader/NoDateHeader.nev")
363+
364+
# with self.assertRaises(IOError):
365+
# hdr = NlxHeader(filename)
366+
367+
# hdr = NlxHeader(filename, props_only=True)
368+
369+
# self.assertEqual(len(hdr), 11)
370+
# self.assertEqual(hdr['ApplicationName'], 'Pegasus')
371+
# self.assertEqual(hdr['FileType'], 'Event')
372+
359373
if __name__ == "__main__":
360374
unittest.main()

0 commit comments

Comments
 (0)