diff --git a/neo/io/nixio.py b/neo/io/nixio.py index e0f4c7c76..d6302be3c 100644 --- a/neo/io/nixio.py +++ b/neo/io/nixio.py @@ -53,6 +53,7 @@ string_types = str EMPTYANNOTATION = "EMPTYLIST" +ARRAYANNOTATION = "ARRAYANNOTATION" def stringify(value): @@ -432,6 +433,7 @@ def _nix_to_neo_irregularlysampledsignal(self, nix_da_group): signaldata = create_quantity(signaldata, unit) timedim = self._get_time_dimension(nix_da_group[0]) times = create_quantity(timedim.ticks, timedim.unit) + neo_signal = IrregularlySampledSignal( signal=signaldata, times=times, **neo_attrs ) @@ -450,6 +452,7 @@ def _nix_to_neo_event(self, nix_mtag): times = create_quantity(nix_mtag.positions, time_unit) labels = np.array(nix_mtag.positions.dimensions[0].labels, dtype="S") + neo_event = Event(times=times, labels=labels, **neo_attrs) self._neo_map[nix_mtag.name] = neo_event return neo_event @@ -460,13 +463,13 @@ def _nix_to_neo_epoch(self, nix_mtag): times = create_quantity(nix_mtag.positions, time_unit) durations = create_quantity(nix_mtag.extents, nix_mtag.extents.unit) + if len(nix_mtag.positions.dimensions[0].labels) > 0: labels = np.array(nix_mtag.positions.dimensions[0].labels, dtype="S") else: labels = None - neo_epoch = Epoch(times=times, durations=durations, labels=labels, - **neo_attrs) + neo_epoch = Epoch(times=times, durations=durations, labels=labels, **neo_attrs) self._neo_map[nix_mtag.name] = neo_epoch return neo_epoch @@ -474,6 +477,7 @@ def _nix_to_neo_spiketrain(self, nix_mtag): neo_attrs = self._nix_attr_to_neo(nix_mtag) time_unit = nix_mtag.positions.unit times = create_quantity(nix_mtag.positions, time_unit) + neo_spiketrain = SpikeTrain(times=times, **neo_attrs) if nix_mtag.features: wfda = nix_mtag.features[0].data @@ -733,6 +737,10 @@ def _write_analogsignal(self, anasig, nixblock, nixgroup): if anasig.annotations: for k, v in anasig.annotations.items(): self._write_property(metadata, k, v) + if anasig.array_annotations: + for k, v in anasig.array_annotations.items(): + p = self._write_property(metadata, k, v) + p.definition = ARRAYANNOTATION self._signal_map[nix_name] = nixdas @@ -796,6 +804,10 @@ def _write_irregularlysampledsignal(self, irsig, nixblock, nixgroup): if irsig.annotations: for k, v in irsig.annotations.items(): self._write_property(metadata, k, v) + if irsig.array_annotations: + for k, v in irsig.array_annotations.items(): + p = self._write_property(metadata, k, v) + p.definition = ARRAYANNOTATION self._signal_map[nix_name] = nixdas @@ -843,6 +855,10 @@ def _write_event(self, event, nixblock, nixgroup): if event.annotations: for k, v in event.annotations.items(): self._write_property(metadata, k, v) + if event.array_annotations: + for k, v in event.array_annotations.items(): + p = self._write_property(metadata, k, v) + p.definition = ARRAYANNOTATION nixgroup.multi_tags.append(nixmt) @@ -905,6 +921,10 @@ def _write_epoch(self, epoch, nixblock, nixgroup): if epoch.annotations: for k, v in epoch.annotations.items(): self._write_property(metadata, k, v) + if epoch.array_annotations: + for k, v in epoch.array_annotations.items(): + p = self._write_property(metadata, k, v) + p.definition = ARRAYANNOTATION nixgroup.multi_tags.append(nixmt) @@ -959,6 +979,10 @@ def _write_spiketrain(self, spiketrain, nixblock, nixgroup): if spiketrain.annotations: for k, v in spiketrain.annotations.items(): self._write_property(metadata, k, v) + if spiketrain.array_annotations: + for k, v in spiketrain.array_annotations.items(): + p = self._write_property(metadata, k, v) + p.definition = ARRAYANNOTATION if nixgroup: nixgroup.multi_tags.append(nixmt) @@ -1155,7 +1179,7 @@ def _nix_attr_to_neo(nix_obj): neo_attrs["description"] = stringify(nix_obj.definition) if nix_obj.metadata: for prop in nix_obj.metadata.inherited_properties(): - values = prop.values + values = list(prop.values) if prop.unit: units = prop.unit values = create_quantity(values, units) @@ -1166,6 +1190,11 @@ def _nix_attr_to_neo(nix_obj): values = "" elif len(values) == 1: values = values[0] + elif prop.definition == ARRAYANNOTATION: + if 'array_annotations' in neo_attrs: + neo_attrs['array_annotations'][prop.name] = values + else: + neo_attrs['array_annotations'] = {prop.name: values} else: values = list(values) neo_attrs[prop.name] = values diff --git a/neo/test/iotest/test_nixio.py b/neo/test/iotest/test_nixio.py index 61da2af12..2913bf17e 100644 --- a/neo/test/iotest/test_nixio.py +++ b/neo/test/iotest/test_nixio.py @@ -329,6 +329,24 @@ def compare_attr(self, neoobj, nixobj): else: self.assertEqual(nixmd[str(k)], v, "Property value mismatch: {}".format(k)) + if hasattr(neoobj, 'array_annotations'): + if neoobj.array_annotations: + nixmd = nixobj.metadata + for k, v, in neoobj.array_annotations.items(): + if k in ['labels', 'durations']: + continue + if isinstance(v, pq.Quantity): + nixunit = nixmd.props[str(k)].unit + self.assertEqual(nixunit, units_to_string(v.units)) + nixvalue = nixmd[str(k)] + if isinstance(nixvalue, Iterable): + nixvalue = np.array(nixvalue) + np.testing.assert_almost_equal(nixvalue, v.magnitude) + if isinstance(v, np.ndarray): + self.assertTrue(np.all(v == nixmd[str(k)])) + else: + self.assertEqual(nixmd[str(k)], v, + "Property value mismatch: {}".format(k)) @classmethod def create_full_nix_file(cls, filename): @@ -373,7 +391,13 @@ def create_full_nix_file(cls, filename): asig_definition = cls.rsentence(5, 5) asig_md = group.metadata.create_section(asig_name, asig_name + ".metadata") - for idx in range(3): + + arr_ann_name, arr_ann_val = 'anasig_arr_ann', cls.rquant(10, pq.uV) + asig_md.create_property(arr_ann_name, arr_ann_val.magnitude.flatten()) + asig_md.props[arr_ann_name].unit = str(arr_ann_val.dimensionality) + asig_md.props[arr_ann_name].definition = 'ARRAYANNOTATION' + + for idx in range(10): da_asig = blk.create_data_array( "{}.{}".format(asig_name, idx), "neo.analogsignal", @@ -402,7 +426,11 @@ def create_full_nix_file(cls, filename): isig_md = group.metadata.create_section(isig_name, isig_name + ".metadata") isig_times = cls.rquant(200, 1, True) - for idx in range(10): + arr_ann_name, arr_ann_val = 'irrsig_arr_ann', cls.rquant(7, pq.uV) + isig_md.create_property(arr_ann_name, arr_ann_val.magnitude.flatten()) + isig_md.props[arr_ann_name].unit = str(arr_ann_val.dimensionality) + isig_md.props[arr_ann_name].definition = 'ARRAYANNOTATION' + for idx in range(7): da_isig = blk.create_data_array( "{}.{}".format(isig_name, idx), "neo.irregularlysampledsignal", @@ -440,6 +468,11 @@ def create_full_nix_file(cls, filename): mtag_st.metadata = mtag_st_md mtag_st_md.create_property("t_stop", times[-1] + 1.0) + arr_ann_name, arr_ann_val = 'st_arr_ann', cls.rquant(40, pq.uV) + mtag_st_md.create_property(arr_ann_name, arr_ann_val.magnitude.flatten()) + mtag_st_md.props[arr_ann_name].unit = str(arr_ann_val.dimensionality) + mtag_st_md.props[arr_ann_name].definition = 'ARRAYANNOTATION' + waveforms = cls.rquant((10, 8, 5), 1) wfname = "{}.waveforms".format(mtag_st.name) wfda = blk.create_data_array(wfname, "neo.waveforms", @@ -486,6 +519,12 @@ def create_full_nix_file(cls, filename): group.multi_tags.append(mtag_ep) mtag_ep.definition = cls.rsentence(2) mtag_ep.extents = extents_da + + arr_ann_name, arr_ann_val = 'ep_arr_ann', cls.rquant(5, pq.uV) + mtag_ep.metadata.create_property(arr_ann_name, arr_ann_val.magnitude.flatten()) + mtag_ep.metadata.props[arr_ann_name].unit = str(arr_ann_val.dimensionality) + mtag_ep.metadata.props[arr_ann_name].definition = 'ARRAYANNOTATION' + label_dim = mtag_ep.positions.append_set_dimension() label_dim.labels = cls.rsentence(5).split(" ") # reference all signals in the group @@ -511,6 +550,12 @@ def create_full_nix_file(cls, filename): ) group.multi_tags.append(mtag_ev) mtag_ev.definition = cls.rsentence(2) + + arr_ann_name, arr_ann_val = 'ev_arr_ann', cls.rquant(5, pq.uV) + mtag_ev.metadata.create_property(arr_ann_name, arr_ann_val.magnitude.flatten()) + mtag_ev.metadata.props[arr_ann_name].unit = str(arr_ann_val.dimensionality) + mtag_ev.metadata.props[arr_ann_name].definition = 'ARRAYANNOTATION' + label_dim = mtag_ev.positions.append_set_dimension() label_dim.labels = cls.rsentence(5).split(" ") # reference all signals in the group @@ -606,8 +651,10 @@ def rquant(shape, unit, incr=False): @classmethod def create_all_annotated(cls): - times = cls.rquant(1, pq.s) - signal = cls.rquant(1, pq.V) + times = cls.rquant(10, pq.s) + times_ann = {cls.rword(6): cls.rquant(10, pq.ms)} + signal = cls.rquant((10, 10), pq.V) + signal_ann = {cls.rword(6): cls.rquant(10, pq.uV)} blk = Block() blk.annotate(**cls.rdict(3)) cls.populate_dates(blk) @@ -617,24 +664,24 @@ def create_all_annotated(cls): cls.populate_dates(seg) blk.segments.append(seg) - asig = AnalogSignal(signal=signal, sampling_rate=pq.Hz) + asig = AnalogSignal(signal=signal, sampling_rate=pq.Hz, array_annotations=signal_ann) asig.annotate(**cls.rdict(2)) seg.analogsignals.append(asig) isig = IrregularlySampledSignal(times=times, signal=signal, - time_units=pq.s) + time_units=pq.s, array_annotations=signal_ann) isig.annotate(**cls.rdict(2)) seg.irregularlysampledsignals.append(isig) - epoch = Epoch(times=times, durations=times) + epoch = Epoch(times=times, durations=times, array_annotations=times_ann) epoch.annotate(**cls.rdict(4)) seg.epochs.append(epoch) - event = Event(times=times) + event = Event(times=times, array_annotations=times_ann) event.annotate(**cls.rdict(4)) seg.events.append(event) - spiketrain = SpikeTrain(times=times, t_stop=pq.s, units=pq.s) + spiketrain = SpikeTrain(times=times, t_stop=pq.s, units=pq.s, array_annotations=times_ann) d = cls.rdict(6) d["quantity"] = pq.Quantity(10, "mV") d["qarray"] = pq.Quantity(range(10), "mA") @@ -1399,6 +1446,56 @@ def test_neo_name_read(self): neoblock = self.io.read_block(neoname=neoname) self.assertEqual(neoblock.annotations["nix_name"], nixblock.name) + def test_array_annotations_read(self): + for bl in self.io.read_all_blocks(): + nix_block = self.nixfile.blocks[bl.annotations['nix_name']] + for seg in bl.segments: + for anasig in seg.analogsignals: + da = nix_block.data_arrays[anasig.annotations['nix_name'] + '.0'] + self.assertIn('anasig_arr_ann', da.metadata) + self.assertIn('anasig_arr_ann', anasig.array_annotations) + nix_ann = da.metadata['anasig_arr_ann'] + neo_ann = anasig.array_annotations['anasig_arr_ann'] + self.assertTrue(np.all(nix_ann == neo_ann.magnitude)) + self.assertEqual(da.metadata.props['anasig_arr_ann'].unit, + units_to_string(neo_ann.units)) + for irrsig in seg.irregularlysampledsignals: + da = nix_block.data_arrays[irrsig.annotations['nix_name'] + '.0'] + self.assertIn('irrsig_arr_ann', da.metadata) + self.assertIn('irrsig_arr_ann', irrsig.array_annotations) + nix_ann = da.metadata['irrsig_arr_ann'] + neo_ann = irrsig.array_annotations['irrsig_arr_ann'] + self.assertTrue(np.all(nix_ann == neo_ann.magnitude)) + self.assertEqual(da.metadata.props['irrsig_arr_ann'].unit, + units_to_string(neo_ann.units)) + for ev in seg.events: + da = nix_block.multi_tags[ev.annotations['nix_name']] + self.assertIn('ev_arr_ann', da.metadata) + self.assertIn('ev_arr_ann', ev.array_annotations) + nix_ann = da.metadata['ev_arr_ann'] + neo_ann = ev.array_annotations['ev_arr_ann'] + self.assertTrue(np.all(nix_ann == neo_ann.magnitude)) + self.assertEqual(da.metadata.props['ev_arr_ann'].unit, + units_to_string(neo_ann.units)) + for ep in seg.epochs: + da = nix_block.multi_tags[ep.annotations['nix_name']] + self.assertIn('ep_arr_ann', da.metadata) + self.assertIn('ep_arr_ann', ep.array_annotations) + nix_ann = da.metadata['ep_arr_ann'] + neo_ann = ep.array_annotations['ep_arr_ann'] + self.assertTrue(np.all(nix_ann == neo_ann.magnitude)) + self.assertEqual(da.metadata.props['ep_arr_ann'].unit, + units_to_string(neo_ann.units)) + for st in seg.spiketrains: + da = nix_block.multi_tags[st.annotations['nix_name']] + self.assertIn('st_arr_ann', da.metadata) + self.assertIn('st_arr_ann', st.array_annotations) + nix_ann = da.metadata['st_arr_ann'] + neo_ann = st.array_annotations['st_arr_ann'] + self.assertTrue(np.all(nix_ann == neo_ann.magnitude)) + self.assertEqual(da.metadata.props['st_arr_ann'].unit, + units_to_string(neo_ann.units)) + @unittest.skipUnless(HAVE_NIX, "Requires NIX") class NixIOContextTests(NixIOTest):