From 9db59114cd21c7b6e9d67287d88479658a4d159e Mon Sep 17 00:00:00 2001 From: kleinjohann Date: Wed, 15 Apr 2020 16:22:58 +0200 Subject: [PATCH 01/85] Support empty array annotations --- neo/io/nixio.py | 12 ++++++------ neo/test/iotest/test_nixio.py | 12 ++++++------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/neo/io/nixio.py b/neo/io/nixio.py index 86df28ee1..3c5958219 100644 --- a/neo/io/nixio.py +++ b/neo/io/nixio.py @@ -854,7 +854,7 @@ def _write_analogsignal(self, anasig, nixblock, nixgroup): if anasig.array_annotations: for k, v in anasig.array_annotations.items(): p = self._write_property(metadata, k, v) - p.definition = ARRAYANNOTATION + p.type = ARRAYANNOTATION self._signal_map[nix_name] = nixdas @@ -992,7 +992,7 @@ def _write_irregularlysampledsignal(self, irsig, nixblock, nixgroup): if irsig.array_annotations: for k, v in irsig.array_annotations.items(): p = self._write_property(metadata, k, v) - p.definition = ARRAYANNOTATION + p.type = ARRAYANNOTATION self._signal_map[nix_name] = nixdas @@ -1047,7 +1047,7 @@ def _write_event(self, event, nixblock, nixgroup): if event.array_annotations: for k, v in event.array_annotations.items(): p = self._write_property(metadata, k, v) - p.definition = ARRAYANNOTATION + p.type = ARRAYANNOTATION nixgroup.multi_tags.append(nixmt) @@ -1115,7 +1115,7 @@ def _write_epoch(self, epoch, nixblock, nixgroup): if epoch.array_annotations: for k, v in epoch.array_annotations.items(): p = self._write_property(metadata, k, v) - p.definition = ARRAYANNOTATION + p.type = ARRAYANNOTATION nixgroup.multi_tags.append(nixmt) @@ -1177,7 +1177,7 @@ def _write_spiketrain(self, spiketrain, nixblock, nixgroup): if spiketrain.array_annotations: for k, v in spiketrain.array_annotations.items(): p = self._write_property(metadata, k, v) - p.definition = ARRAYANNOTATION + p.type = ARRAYANNOTATION if nixgroup: nixgroup.multi_tags.append(nixmt) @@ -1390,7 +1390,7 @@ def _nix_attr_to_neo(nix_obj): if prop.definition in (DATEANNOTATION, TIMEANNOTATION, DATETIMEANNOTATION): values = dt_from_nix(values, prop.definition) - if prop.definition == ARRAYANNOTATION: + if prop.type == ARRAYANNOTATION: if 'array_annotations' in neo_attrs: neo_attrs['array_annotations'][prop.name] = values else: diff --git a/neo/test/iotest/test_nixio.py b/neo/test/iotest/test_nixio.py index d216f796b..35c0cfa53 100644 --- a/neo/test/iotest/test_nixio.py +++ b/neo/test/iotest/test_nixio.py @@ -418,7 +418,7 @@ def create_full_nix_file(cls, filename): 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' + asig_md.props[arr_ann_name].type = 'ARRAYANNOTATION' for idx in range(10): da_asig = blk.create_data_array( @@ -452,7 +452,7 @@ def create_full_nix_file(cls, filename): imgseq_md.create_property(arr_ann_name, arr_ann_val.magnitude.flatten()) imgseq_md.props[arr_ann_name].unit = str(arr_ann_val.dimensionality) - imgseq_md.props[arr_ann_name].definition = 'ARRAYANNOTATION' + imgseq_md.props[arr_ann_name].type = 'ARRAYANNOTATION' for idx in range(10): da_imgseq = blk.create_data_array( @@ -485,7 +485,7 @@ def create_full_nix_file(cls, filename): 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' + isig_md.props[arr_ann_name].type = 'ARRAYANNOTATION' for idx in range(7): da_isig = blk.create_data_array( "{}.{}".format(isig_name, idx), @@ -527,7 +527,7 @@ def create_full_nix_file(cls, filename): 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' + mtag_st_md.props[arr_ann_name].type = 'ARRAYANNOTATION' waveforms = cls.rquant((10, 8, 5), 1) wfname = "{}.waveforms".format(mtag_st.name) @@ -579,7 +579,7 @@ def create_full_nix_file(cls, filename): 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' + mtag_ep.metadata.props[arr_ann_name].type = 'ARRAYANNOTATION' label_dim = mtag_ep.positions.append_set_dimension() label_dim.labels = cls.rsentence(5).split(" ") @@ -612,7 +612,7 @@ def create_full_nix_file(cls, filename): 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' + mtag_ev.metadata.props[arr_ann_name].type = 'ARRAYANNOTATION' label_dim = mtag_ev.positions.append_set_dimension() label_dim.labels = cls.rsentence(5).split(" ") From 141fc3dd69009d2c9296e4fca58cd0387b50b2ea Mon Sep 17 00:00:00 2001 From: kleinjohann Date: Wed, 15 Apr 2020 16:42:36 +0200 Subject: [PATCH 02/85] Add test with empty array annotation --- neo/test/iotest/test_nixio.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/neo/test/iotest/test_nixio.py b/neo/test/iotest/test_nixio.py index 35c0cfa53..79fab1cf9 100644 --- a/neo/test/iotest/test_nixio.py +++ b/neo/test/iotest/test_nixio.py @@ -1486,6 +1486,24 @@ def test_annotations_special_cases(self): # TODO: multi dimensional value (GH Issue #501) + def test_empty_array_annotations(self): + wblock = Block("block with spiketrain") + wseg = Segment() + wseg.spiketrains = [SpikeTrain(times=[] * pq.s, t_stop=1 * pq.s, + array_annotations={'empty': []})] + wblock.segments = [wseg] + self.writer.write_block(wblock) + try: + rblock = self.writer.read_block(neoname="block with spiketrain") + except Exception as exc: + self.fail('The following exception was raised when' + + ' reading the block with an empty array annotation:\n' + + str(exc)) + rst = rblock.segments[0].spiketrains[0] + self.assertEqual(len(rst.array_annotations), 1) + self.assertIn('empty', rst.array_annotations.keys()) + self.assertEqual(len(rst.array_annotations['empty']), 0) + def test_write_proxyobjects(self): def generate_complete_block(): From fd1c8c145424407a51257bb47ada9081c8239bdb Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 19 May 2020 00:14:17 +0200 Subject: [PATCH 03/85] Initial implementation of a "View" class (see #456) --- neo/core/__init__.py | 4 +- neo/core/baseneo.py | 2 +- neo/core/view.py | 67 ++++++++++++++++++++++++++++++++++ neo/test/coretest/test_view.py | 60 ++++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 2 deletions(-) create mode 100644 neo/core/view.py create mode 100644 neo/test/coretest/test_view.py diff --git a/neo/core/__init__.py b/neo/core/__init__.py index b09e2937d..6070483b2 100644 --- a/neo/core/__init__.py +++ b/neo/core/__init__.py @@ -45,12 +45,14 @@ from neo.core.imagesequence import ImageSequence from neo.core.regionofinterest import RectangularRegionOfInterest, CircularRegionOfInterest, PolygonRegionOfInterest +from neo.core.view import View + # Block should always be first in this list objectlist = [Block, Segment, ChannelIndex, AnalogSignal, IrregularlySampledSignal, Event, Epoch, Unit, SpikeTrain, ImageSequence, RectangularRegionOfInterest, CircularRegionOfInterest, - PolygonRegionOfInterest] + PolygonRegionOfInterest, View] objectnames = [ob.__name__ for ob in objectlist] class_by_name = dict(zip(objectnames, objectlist)) diff --git a/neo/core/baseneo.py b/neo/core/baseneo.py index 5795d447c..a11c2ed62 100644 --- a/neo/core/baseneo.py +++ b/neo/core/baseneo.py @@ -238,7 +238,7 @@ class attributes. :_recommended_attrs: should append # Parent objects whose children can have multiple parents _multi_parent_objects = () - # Attributes that an instance is requires to have defined + # Attributes that an instance is required to have defined _necessary_attrs = () # Attributes that an instance may or may have defined _recommended_attrs = (('name', str), diff --git a/neo/core/view.py b/neo/core/view.py new file mode 100644 index 000000000..b368249e2 --- /dev/null +++ b/neo/core/view.py @@ -0,0 +1,67 @@ +""" +This module implements :class:`View`, which represents a subset of the +channels in an :class:`AnalogSignal` or :class:`IrregularlySampledSignal`. + +It replaces the indexing function of the former :class:`ChannelIndex`. +""" + +import numpy as np +from .baseneo import BaseNeo +from .basesignal import BaseSignal +from .dataobject import ArrayDict + + +class View(BaseNeo): + """ + docstring goes here + """ + _single_parent_objects = ('Segment',) + _single_parent_attrs = ('segment',) + _necessary_attrs = ( + ('index', np.ndarray, 1, np.dtype('i')), + ('obj', BaseSignal, 1) + ) + # "mask" would be an alternative name, proposing "index" for backwards-compatibility with ChannelIndex + + def __init__(self, obj, index, name=None, description=None, file_origin=None, + array_annotations=None, **annotations): + super(View, self).__init__(name=name, description=description, + file_origin=file_origin, **annotations) + if not isinstance(obj, BaseSignal): + raise ValueError("Can only take a View of an AnalogSignal " + "or an IrregularlySampledSignal") + self.obj = obj + + # check type and dtype of index and convert index to a common form + # (accept list or array of bool or int, convert to int array) + self.index = np.array(index) + if len(self.index.shape) != 1: + raise ValueError("index must be a 1D array") + if self.index.dtype == np.bool: # convert boolean mask to integer index + if self.index.size != self.obj.shape[-1]: + raise ValueError("index size does not match number of channels in signal") + self.index = np.arange(self.obj.shape[-1])[self.index] + elif self.index.dtype != np.integer: + raise ValueError("index must be a boolean or integer list or array") + + if not hasattr(self, 'array_annotations') or not self.array_annotations: + self.array_annotations = ArrayDict(self._get_arr_ann_length()) + if array_annotations is not None: + self.array_annotate(**array_annotations) + + @property + def shape(self): + return (self.obj.shape[0], self.index.size) + + def _get_arr_ann_length(self): + return self.shape[-1] + + def array_annotate(self, **array_annotations): + self.array_annotations.update(array_annotations) + + def resolve(self): + """ + Return a copy of the underlying object containing just the subset of channels + defined by the index. + """ + return self.obj[:, self.index] diff --git a/neo/test/coretest/test_view.py b/neo/test/coretest/test_view.py new file mode 100644 index 000000000..fa009d99b --- /dev/null +++ b/neo/test/coretest/test_view.py @@ -0,0 +1,60 @@ +""" +Tests of the neo.core.view.View class and related functions +""" + + +import unittest + +import numpy as np +import quantities as pq +from numpy.testing import assert_array_equal + +from neo.core.analogsignal import AnalogSignal +from neo.core.irregularlysampledsignal import IrregularlySampledSignal +from neo.core.view import View + + +class TestView(unittest.TestCase): + + def setUp(self): + self.test_data = np.random.rand(100, 8) * pq.mV + channel_names = np.array(["a", "b", "c", "d", "e", "f", "g", "h"]) + self.test_signal = AnalogSignal(self.test_data, + sampling_period=0.1 * pq.ms, + name="test signal", + description="this is a test signal", + array_annotations={"channel_names": channel_names}, + attUQoLtUaE=42) + + def test_create_integer_index(self): + view = View(self.test_signal, [1, 2, 5, 7], + name="view of test signal", + description="this is a view of a test signal", + array_annotations={"something": np.array(["A", "B", "C", "D"])}, + sLaTfat="fish") + + assert view.obj is self.test_signal + assert_array_equal(view.index, np.array([1, 2, 5, 7])) + self.assertEqual(view.shape, (100, 4)) + self.assertEqual(view.name, "view of test signal") + self.assertEqual(view.annotations["sLaTfat"], "fish") + + def test_create_boolean_index(self): + view1 = View(self.test_signal, [1, 2, 5, 7]) + view2 = View(self.test_signal, np.array([0, 1, 1, 0, 0, 1, 0, 1], dtype=bool)) + assert_array_equal(view1.index, view2.index) + self.assertEqual(view1.shape, view2.shape) + + def test_resolve(self): + view = View(self.test_signal, [1, 2, 5, 7], + name="view of test signal", + description="this is a view of a test signal", + array_annotations={"something": np.array(["A", "B", "C", "D"])}, + sLaTfat="fish") + signal2 = view.resolve() + self.assertIsInstance(signal2, AnalogSignal) + self.assertEqual(signal2.shape, (100, 4)) + for attr in ('name', 'description', 'sampling_period', 'units'): + self.assertEqual(getattr(self.test_signal, attr), getattr(signal2, attr)) + assert_array_equal(signal2.array_annotations["channel_names"], np.array(["b", "c", "f", "h"])) + assert_array_equal(self.test_data[:, [1, 2, 5, 7]], signal2.magnitude) From dd39d20349d4f718c684a1146cdf6839257ba69f Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 19 May 2020 20:56:02 +0200 Subject: [PATCH 04/85] Add Group class, start updating docs accordingly --- doc/source/core.rst | 59 +- doc/source/grouping.rst | 145 + doc/source/images/base_schematic.png | Bin 60651 -> 67445 bytes doc/source/images/base_schematic.svg | 6226 ++++++++--------- ...chematic_with_IrregularlySampledSignal.svg | 4147 ----------- doc/source/images/generate_diagram.py | 5 +- doc/source/images/multi_segment_diagram.svg | 766 +- .../multi_segment_diagram_spiketrain.png | Bin 69457 -> 63701 bytes .../multi_segment_diagram_spiketrain.svg | 2497 ++++--- doc/source/scripts/multi_tetrode_example.py | 103 + doc/source/scripts/spike_sorting_example.py | 42 + doc/source/usecases.rst | 143 +- neo/core/__init__.py | 13 +- neo/core/analogsignal.py | 12 +- neo/core/block.py | 21 +- neo/core/channelindex.py | 4 + neo/core/group.py | 58 + neo/core/segment.py | 2 + neo/core/unit.py | 2 + neo/core/view.py | 23 +- neo/io/tools.py | 6 +- neo/test/coretest/test_analogsignal.py | 5 +- neo/test/coretest/test_block.py | 13 +- neo/test/coretest/test_group.py | 60 + neo/test/generate_datasets.py | 2 + 25 files changed, 5280 insertions(+), 9074 deletions(-) create mode 100644 doc/source/grouping.rst delete mode 100644 doc/source/images/base_schematic_with_IrregularlySampledSignal.svg create mode 100644 doc/source/scripts/multi_tetrode_example.py create mode 100644 doc/source/scripts/spike_sorting_example.py create mode 100644 neo/core/group.py create mode 100644 neo/test/coretest/test_group.py diff --git a/doc/source/core.rst b/doc/source/core.rst index 637f212af..f633e3f31 100644 --- a/doc/source/core.rst +++ b/doc/source/core.rst @@ -38,7 +38,7 @@ There is a simple hierarchy of containers: May contain any of the data objects. * :py:class:`Block`: The top-level container gathering all of the data, discrete and continuous, for a given recording session. - Contains :class:`Segment`, :class:`Unit` and :class:`ChannelIndex` objects. + Contains :class:`Segment` and :class:`Group` objects. Grouping/linking objects @@ -49,18 +49,16 @@ were recorded on which electrodes, which spike trains were obtained from which membrane potential signals, etc. They contain references to data objects that cut across the simple container hierarchy. - * :py:class:`ChannelIndex`: A set of indices into :py:class:`AnalogSignal` objects, - representing logical and/or physical recording channels. This has two uses: + * :py:class:`View`: A set of indices into :py:class:`AnalogSignal` objects, + representing logical and/or physical recording channels. + For spike sorting of extracellular signals, where spikes may be recorded on more than one + recording channel, the :py:class:`View` can be used to reference the group of recording channels + from which the spikes were obtained. - 1. for linking :py:class:`AnalogSignal` objects recorded from the same (multi)electrode - across several :py:class:`Segment`\s. - 2. for spike sorting of extracellular signals, where spikes may be recorded on more than one - recording channel, and the :py:class:`ChannelIndex` can be used to associate each - :py:class:`Unit` with the group of recording channels from which it was obtained. - - * :py:class:`Unit`: links the :class:`SpikeTrain` objects within a :class:`Block`, - possibly across multiple Segments, that were emitted by the same cell. - A :class:`Unit` is linked to the :class:`ChannelIndex` object from which the spikes were detected. + * :py:class:`Group`: Can contain any of the data objects, views, or other groups, + outside the hierarchy of the segment and block containers. + A common use is to link the :class:`SpikeTrain` objects within a :class:`Block`, + possibly across multiple Segments, that were emitted by the same neuron. * :py:class:`CircularRegionOfInterest`, :py:class:`RectangularRegionOfInterest` and :py:class:`PolygonRegionOfInterest` are three subclasses that link :class:`ImageSequence` objects to signals (:class:`AnalogSignal` objects) @@ -105,14 +103,14 @@ In general, an object can access its children with an attribute *childname+s* in * :attr:`Block.segments` * :attr:`Segments.analogsignals` * :attr:`Segments.spiketrains` - * :attr:`Block.channel_indexes` + * :attr:`Block.groups` These relationships are bi-directional, i.e. a child object can access its parent: * :attr:`Segment.block` * :attr:`AnalogSignal.segment` * :attr:`SpikeTrain.segment` - * :attr:`ChannelIndex.block` + * :attr:`Group.block` Here is an example showing these relationships in use:: @@ -133,38 +131,39 @@ Here is an example showing these relationships in use:: In some cases, a one-to-many relationship is sufficient. Here is a simple example with tetrodes, in which each tetrode has its own group.:: - from neo import Block, ChannelIndex + from neo import Block, Group bl = Block() # the four tetrodes for i in range(4): - chx = ChannelIndex(name='Tetrode %d' % i, - index=[0, 1, 2, 3]) - bl.channelindexes.append(chx) + group = Group(name='Tetrode %d' % i) + bl.groups.append(group) # now we load the data and associate it with the created channels # ... -Now consider a more complex example: a 1x4 silicon probe, with a neuron on channels 0,1,2 and another neuron on channels 1,2,3. We create a group for each neuron to hold the :class:`Unit` object associated with this spike sorting group. Each group also contains the channels on which that neuron spiked. The relationship is many-to-many because channels 1 and 2 occur in multiple groups.:: +Now consider a more complex example: a 1x4 silicon probe, with a neuron on channels 0,1,2 and another neuron on channels 1,2,3. +We create a group for each neuron to hold the spiketrains for each spike sorting group together with +the channels on which that neuron spiked:: bl = Block(name='probe data') # one group for each neuron - chx0 = ChannelIndex(name='Group 0', - index=[0, 1, 2]) - bl.channelindexes.append(chx0) + view0 = View(recorded_signals, index=[0, 1, 2]) + unit0 = Group(view0, name='Group 0') + bl.groups.append(unit0) - chx1 = ChannelIndex(name='Group 1', - index=[1, 2, 3]) - bl.channelindexes.append(chx1) + view1 = View(recorded_signals, index=[1, 2, 3]) + unit1 = Group(view1, name='Group 1') + bl.groups.append(unit1) - # now we add the spiketrain from Unit 0 to chx0 - # and add the spiketrain from Unit 1 to chx1 + # now we add the spiketrains from Unit 0 to unit0 + # and add the spiketrains from Unit 1 to unit1 # ... -Note that because neurons are sorted from groups of channels in this situation, it is natural that the :py:class:`ChannelIndex` contains a reference to the :py:class:`Unit` object. -That unit then contains references to its spiketrains. Also note that recording channels can be -identified by names/labels as well as, or instead of, integer indices. + +Now each putative neuron is represented by a :class:`Group` containing the spiktrains of that neuron +and a view of the signal selecting only those channels from which the spikes were obtained. See :doc:`usecases` for more examples of how the different objects may be used. diff --git a/doc/source/grouping.rst b/doc/source/grouping.rst new file mode 100644 index 000000000..76a84ed2e --- /dev/null +++ b/doc/source/grouping.rst @@ -0,0 +1,145 @@ +************************* +Grouping and linking data +************************* + + +... to do + + + + +Migrating from ChannelIndex/Unit to View/Group +============================================== + + +Examples +-------- + +A simple example with two tetrodes. Here the :class:`ChannelIndex` was not being +used for grouping, simply to associate a name with each channel. + +Using :class:`ChannelIndex`:: + + import numpy as np + from quantities import kHz, mV + from neo import Block, Segment, ChannelIndex, AnalogSignal + + block = Block() + segment = Segment() + segment.block = block + block.segments.append(segment) + + for i in (0, 1): + signal = AnalogSignal(np.random.rand(1000, 4) * mV, + sampling_rate=1 * kHz,) + segment.analogsignals.append(signal) + chx = ChannelIndex(name=f"Tetrode #{i + 1}", + index=[0, 1, 2, 3], + channel_names=["A", "B", "C", "D"]) + chx.analogsignals.append(signal) + block.channel_indexes.append(chx) + +Using array annotations, we annotate the channels of the :class:`AnalogSignal` directly:: + + import numpy as np + from quantities import kHz, mV + from neo import Block, Segment, AnalogSignal + + block = Block() + segment = Segment() + segment.block = block + block.segments.append(segment) + + for i in (0, 1): + signal = AnalogSignal(np.random.rand(1000, 4) * mV, + sampling_rate=1 * kHz, + channel_names=["A", "B", "C", "D"]) + segment.analogsignals.append(signal) + + +Now a more complex example: a 1x4 silicon probe, with a neuron on channels 0,1,2 and another neuron on channels 1,2,3. +We create a :class:`ChannelIndex` for each neuron to hold the :class:`Unit` object associated with this spike sorting group. +Each :class:`ChannelIndex` also contains the list of channels on which that neuron spiked. + +:: + + import numpy as np + from quantities import ms, mV, kHz + from neo import Block, Segment, ChannelIndex, Unit, SpikeTrain, AnalogSignal + + block = Block(name="probe data") + segment = Segment() + segment.block = block + block.segments.append(segment) + + # create 4-channel AnalogSignal with dummy data + signal = AnalogSignal(np.random.rand(1000, 4) * mV, + sampling_rate=10 * kHz) + # create spike trains with dummy data + # we will pretend the spikes have been extracted from the dummy signal + spiketrains = [ + SpikeTrain(np.arange(5, 100) * ms, t_stop=100 * ms), + SpikeTrain(np.arange(7, 100) * ms, t_stop=100 * ms) + ] + segment.analogsignals.append(signal) + segment.spiketrains.extend(spiketrains) + # assign each spiketrain to a neuron (Unit) + units = [] + for i, spiketrain in enumerate(spiketrains): + unit = Unit(name=f"Neuron #{i + 1}") + unit.spiketrains.append(spiketrain) + units.append(unit) + + # create a ChannelIndex for each unit, to show which channels the spikes come from + chx0 = ChannelIndex(name="Channel Group 1", index=[0, 1, 2]) + chx0.units.append(units[0]) + chx0.analogsignals.append(signal) + units[0].channel_index = chx0 + chx1 = ChannelIndex(name="Channel Group 2", index=[1, 2, 3]) + chx1.units.append(units[1]) + chx1.analogsignals.append(signal) + units[1].channel_index = chx1 + + block.channel_indexes.extend((chx0, chx1)) + + +Using :class:`View` and :class`Group`:: + + import numpy as np + from quantities import ms, mV, kHz + from neo import Block, Segment, View, Group, SpikeTrain, AnalogSignal + + block = Block(name="probe data") + segment = Segment() + segment.block = block + block.segments.append(segment) + + # create 4-channel AnalogSignal with dummy data + signal = AnalogSignal(np.random.rand(1000, 4) * mV, + sampling_rate=10 * kHz) + # create spike trains with dummy data + # we will pretend the spikes have been extracted from the dummy signal + spiketrains = [ + SpikeTrain(np.arange(5, 100) * ms, t_stop=100 * ms), + SpikeTrain(np.arange(7, 100) * ms, t_stop=100 * ms) + ] + segment.analogsignals.append(signal) + segment.spiketrains.extend(spiketrains) + # assign each spiketrain to a neuron (now using Group) + units = [] + for i, spiketrain in enumerate(spiketrains): + unit = Group(spiketrain, name=f"Neuron #{i + 1}") + units.append(unit) + + # create a View of the signal for each unit, to show which channels the spikes come from + # and add it to the relevant Group + view0 = View(signal, index=[0, 1, 2], name="Channel Group 1") + units[0].add(view0) + view1 = View(signal, index=[1, 2, 3], name="Channel Group 2") + units[1].add(view1) + + block.groups.extend(units) + + +Now each putative neuron is represented by a :class:`Group` containing the spiktrains of that neuron +and a view of the signal selecting only those channels from which the spikes were obtained. \ No newline at end of file diff --git a/doc/source/images/base_schematic.png b/doc/source/images/base_schematic.png index d763e80001970aaf1efeb0a6be2d106037d3aef3..f86e3860c6ad0496a92d77530c6c9d24ad955cb9 100644 GIT binary patch literal 67445 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

J~gPw>Af-tkA7Kj{B4TZwWW=7ux4ISBr_vy;E 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 VN)cz7rSL{x5)Q8+HHy literal 60651 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 - - + inkscape:export-filename="/Users/andrew/dev/analysis/neo/doc/source/images/base_schematic.png" + version="1.0" + inkscape:output_extension="org.inkscape.output.svg.inkscape" + sodipodi:docname="base_schematic.svg" + inkscape:version="1.0 (4035a4f, 2020-05-01)" + sodipodi:version="0.32" + id="svg2" + height="610.57245" + width="670.67755"> + showguides="true" + inkscape:window-y="30" + inkscape:window-x="57" + inkscape:window-height="1005" + inkscape:window-width="1363" + showgrid="false" + inkscape:current-layer="layer1" + inkscape:document-units="px" + inkscape:cy="333.66771" + inkscape:cx="403.6858" + inkscape:zoom="1.3995495" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="#ffffff" + id="base" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Mend"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path4650" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path9606" /> - + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1353.75 : 275.625 : 1" + inkscape:persp3d-origin="676.875 : 183.75 : 1" + id="perspective5210" /> + d="M 0,3 0,-3" + id="md0b6aff34ceb4dcf39a042d98ad215e8" /> + width="839.78998" + y="44.099998" + x="135.45" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1LendZ"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path10404" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1LendZ7"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path11032" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1LendZk"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path11669" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lendr"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path12860" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1LendrK"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path13519" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lendrg"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path14196" /> + inkscape:collect="always"> + inkscape:collect="always" /> - - - + id="perspective3983" /> - - - + id="perspective4005" /> - + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective4069" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective4157" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3285" /> + width="446.39999" + y="43.200001" + x="72" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3426" /> + width="446.39999" + y="43.200001" + x="72" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3593" /> + width="446.39999" + height="345.60001" + id="rect3565" /> - - - + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective4579" /> + width="446.39999" + height="345.60001" + id="rect4551" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective4697" /> + width="446.39999" + height="345.60001" + id="rect4669" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective4837" /> + width="446.39999" + height="345.60001" + id="rect4809" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective4977" /> + width="446.39999" + height="345.60001" + id="rect4949" /> - - - + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective5170" /> + width="446.39999" + height="345.60001" + id="rect5142" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective5288" /> + width="446.39999" + height="345.60001" + id="rect5260" /> - + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3315" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lendrg"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path14196-2" /> - + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3408" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Mendd"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path5383" /> + inkscape:vp_z="1 : 0.5 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_x="0 : 0.5 : 1" + sodipodi:type="inkscape:persp3d" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1MendJ"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path7135" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1MendX"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path8064" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Mendw"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path8997" /> - + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective9874" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path9606-6" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective9902" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1MendX"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path8064-3" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective9930" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1MendX"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path8064-1" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective9930-5" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1MendX"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path8064-6" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1MendX9"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path10066" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1MendXg"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path11043" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1MendXx"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path12024" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1MendT"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path13675" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective14561" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lendr"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path12860-9" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1LendrB"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path14695" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend7"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path15696" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend-1C"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path18311" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend-1E"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path19320" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend-1o"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path20333" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend-1e"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path21350" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend-1Cb"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path22371" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend-1v"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path23396" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend-1n"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path24425" /> + inkscape:vp_z="1 : 0.5 : 1" + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_x="0 : 0.5 : 1" + sodipodi:type="inkscape:persp3d" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend-1Cb"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path22371-9" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend-1CbS"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path25489" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective26420" /> + refX="0" + refY="0" + orient="auto" + inkscape:stockid="Arrow1Lend-1CbS"> + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path25489-6" /> + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective26420-9" /> - + + d="M 0,0 5,-5 -12.5,0 5,5 0,0 z" + id="path25489-0" /> - - - + id="perspective4350" /> - - - + id="perspective4393" /> - - - + id="perspective4415" /> - - - + id="perspective4437" /> - - - + id="perspective4459" /> - - - + id="perspective4459-1" /> - - - + id="perspective4459-14" /> - - - + id="perspective4459-8" /> - - - + id="perspective4459-6" /> - - - + id="perspective4517" /> - - - + id="perspective4517-1" /> - - - + id="perspective4517-7" /> - - - + id="perspective4517-72" /> - - - + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective4517-79" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3404-4" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3404-6" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3404-3" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3404-90" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3404-96" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3404-93" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3404-25" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3730-3" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3730-36" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3730-0" /> + + + + inkscape:vp_y="0 : 1000 : 0" + inkscape:vp_z="1 : 0.5 : 1" + inkscape:persp3d-origin="0.5 : 0.33333333 : 1" + id="perspective3730-08" /> + + + + + + + + width="446.39999" + height="345.60001" + id="rect5299" /> + width="446.39999" + height="345.60001" + id="rect4708" /> + width="446.39999" + height="345.60001" + id="rect4848" /> + width="446.39999" + height="345.60001" + id="rect4988" /> + + + @@ -1440,2687 +1452,2639 @@ - + inkscape:groupmode="layer" + inkscape:label="Calque 1"> + height="539.77057" + width="622.87042" + id="rect2383" + style="fill:none;stroke:#000000;stroke-width:1.40584946;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + height="11.455798" + width="128.24529" + id="rect9411" + style="fill:none;stroke:#aad400;stroke-width:1.16330421;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + height="12.585578" + width="389.27277" + id="rect9409" + style="fill:none;stroke:#ff6600;stroke-width:1.16330421;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + height="440.01648" + width="132.81863" + id="rect2385" + style="display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:1.2779;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /> + height="441.10608" + width="180.6758" + id="rect2387" + style="display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:1.28433;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /> - - + style="fill:none;stroke:#aa4400;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" /> + style="fill:none;stroke:#aa4400;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" /> - - - - + id="use4539" /> - - - - + id="use4541" /> - - - - + id="use4543" /> - - - - + id="use4545" /> - - - - + id="use4547" /> - - - - + id="use4549" /> - - - - + id="use4551" /> - - - - + id="use4553" /> - - - - + id="use4555" /> - - - - + id="use4557" /> - - - - + id="use4559" /> - - - - + id="use4561" /> - - - - + id="use4563" /> - - - - + id="use4565" /> - - - - + id="use4567" /> - - - - + id="use4569" /> - - - - + id="use4571" /> - - - - + id="use4573" /> - - - - + id="use4575" /> - - - + id="use4577" /> - - - + id="use4579" /> - - - + id="use4581" /> - - - + id="use4583" /> - - - + id="use4585" /> - - - + id="use4587" /> - - - + id="use4589" /> - - - + id="use4591" /> - - - + id="use4593" /> - - - + id="use4595" /> - - - + id="use4597" /> - - - + id="use4599" /> - - - + id="use4601" /> - - - + id="use4603" /> - - - + id="use4605" /> - - - + id="use4607" /> - - - + id="use4609" /> - - - + id="use4611" /> - - - + id="use4613" /> - - - + id="use4615" /> - - - + id="use4617" /> - - - + id="use4619" /> - - - + + width="744.09448" + style="fill:none;stroke:#000080;stroke-width:1.07452543;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" + xlink:href="#md0b6aff34ceb4dcf39a042d98ad215e8" + x="278.72351" + y="220.5" + id="use4623" /> + + + - - + width="744.09448" + style="fill:none;stroke:#000080;stroke-width:1.07452543;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" + xlink:href="#md0b6aff34ceb4dcf39a042d98ad215e8" + x="291.65042" + y="220.5" + id="use4631" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + d="m 324.39233,211.49661 24.25118,0" + style="fill:none;stroke:#4d0089;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> - + d="M 331.80486,-195.05049 V 241.78272" + style="fill:none;stroke:#ffcc00;stroke-width:1.02018px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" /> + x="513.36914" + height="113.47384" + width="114.4437" + id="rect14857" + style="display:inline;overflow:visible;visibility:visible;fill:#93aca7;fill-opacity:1;fill-rule:nonzero;stroke:#93aca7;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;filter:url(#filter14895);enable-background:accumulate" /> + d="m 617.23213,-208.14961 0,201.480856 0,248.206054" + style="fill:#008000;fill-opacity:1;fill-rule:evenodd;stroke:#008000;stroke-width:1.03548181;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> - + style="fill:none;stroke:#ff00ff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> Block + id="tspan9592" + sodipodi:role="line">Block - RecordingChannel + d="m 190.83413,-283.32788 c 17.40456,0.74979 20.35272,1.03174 30.95109,1.56955 11.33425,0.57515 8.00388,2.47416 11.1612,17.66816" + style="fill:none;stroke:#000000;stroke-width:0.93064338px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow1Lend)" /> Unit + sodipodi:role="line">Group + d="M 269.50093,0.64358273 C 274.49759,-14.984088 283.51222,-16.924958 302.02092,-16.924958" + style="fill:none;stroke:#ff6600;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1LendZ)" /> SpikeTrain - + id="tspan11011" + sodipodi:role="line">SpikeTrain - AnalogSignal - - Epoch + id="path11013" + d="m 276.83759,56.93591 c -0.26167,-9.7659 19.29079,-7.7307 38.43774,-7.7307" + style="fill:none;stroke:#aad400;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1LendZ7)" /> Segment - + id="tspan14158" + sodipodi:role="line">Segment - Event + id="path14160" + d="m 312.09881,-219.84371 c 31.36779,0 33.94452,-17.90827 50.65445,17.80908" + style="fill:none;stroke:#000000;stroke-width:1.24447;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Lend)" /> + d="m 634.93997,-208.01336 0,201.442071 0,248.158269" + style="fill:#008000;fill-opacity:1;fill-rule:evenodd;stroke:#008000;stroke-width:1.03538203;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> - + d="m 588.95603,-207.98187 0,201.3927643 0,248.0975257" + style="fill:#008000;fill-opacity:1;fill-rule:evenodd;stroke:#008000;stroke-width:1.03525543;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + height="9.8696203" + width="9.9539766" + id="rect14851" + style="fill:none;stroke:#0096bb;stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> - - - Spike - Neo 0.2 architecture + id="tspan14903" + sodipodi:role="line">  + d="m 662.76634,-208.8037 0,201.7374156 0,248.5221144" + style="fill:#008000;fill-opacity:1;fill-rule:evenodd;stroke:#008000;stroke-width:1.03614068;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none" /> + d="m 365.23486,223.90264 34.08117,-0.3411" + style="fill:none;stroke:#4d0089;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + d="m 412.69309,234.40253 29.92452,0" + style="fill:none;stroke:#4d0089;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + id="path4214" + d="M 464.22427,-65.213662 548.9421,20.122566" + style="fill:none;stroke:#0096bb;stroke-width:1.17825;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:2.35649, 2.35649;stroke-dashoffset:0;stroke-opacity:1" /> + id="path4216" + d="m 560.55312,19.586083 10.2278,-84.799749" + style="fill:none;stroke:#0096bb;stroke-width:0.737793;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:1.47559, 1.47559;stroke-dashoffset:0;stroke-opacity:1" /> EpochArray + x="245.42741" + y="186.09813">Epoch + id="tspan4245" + sodipodi:role="line">  EventArray + x="576.91998" + y="-237.64931">Event + + + + + waveform + + d="m 570.57322,203.67212 38.50845,0" + style="fill:none;stroke:#aa00d4;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + d="m 601.09178,214.88301 38.50845,0" + style="fill:none;stroke:#aa00d4;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + d="m 633.47882,227.9624 38.50845,0" + style="fill:none;stroke:#aa00d4;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + clip-path="url(#p50431ccdcb28178602d99d9270004dde-70)" + d="m 72,199.44014 0.744,-9.88505 0.744,-8.16304 1.488,-9.36337 1.488,1.87949 0.744,4.51209 1.488,14.91057 2.976,33.84172 0.744,4.07908 0.744,1.42438 2.232,-4.60906 1.488,-0.0347 1.488,4.87221 1.488,5.9514 0.744,1.30441 0.744,-1.102 1.488,-8.48149 1.488,-6.66913 1.488,1.49385 0.744,2.11321 1.488,-1.52428 2.976,-17.3357 0.744,1.00237 2.232,10.64968 1.488,-4.58749 1.488,-22.11071 2.232,-44.27705 0.744,-8.23399 0.744,-2.67097 0.744,1.68533 1.488,9.82042 1.488,15.06494 2.232,34.10097 1.488,19.55812 0.744,4.74052 1.488,-1.04466 2.232,-8.95528 0.744,-1.66071 1.488,1.57539 1.488,9.97787 2.976,25.15023 1.488,9.85045 1.488,16.87157 1.488,20.10934 0.744,4.06258 0.744,-3.2619 0.744,-10.54148 4.464,-88.92547 2.232,-7.57641 0.744,-6.78791 2.976,-42.28856 0.744,-3.70829 0.744,1.93631 1.488,16.51775 1.488,17.60432 0.744,4.60487 0.744,1.46299 1.488,-2.48511 2.232,-8.84485 1.488,3.58933 1.488,17.8884 2.232,44.1192 2.976,69.61286 1.488,16.66005 0.744,1.77475 0.744,-1.73947 0.744,-4.38392 2.232,-21.99984 1.488,-11.63998 2.232,-13.45344 2.232,-20.81468 3.72,-43.83979 0.744,-3.86105 1.488,3.98198 1.488,16.21543 2.232,25.67229 0.744,2.70765 0.744,-1.27609 1.488,-10.12523 1.488,-11.31521 0.744,-3.97331 0.744,-2.00057 1.488,4.1382 1.488,14.931 1.488,16.65152 0.744,3.72521 0.744,-1.59709 0.744,-6.92539 2.976,-47.18876 2.232,-32.9563 1.488,-10.49265 2.976,-7.78144 1.488,1.06199 1.488,10.11014 1.488,8.87559 0.744,1.15151 1.488,0.55343 0.744,1.482 2.232,7.63589 1.488,0.81241 1.488,3.5433 2.976,16.08709 1.488,0.11738 1.488,-4.45597 2.232,-8.75192 0.744,-1.69833 1.488,2.54425 0.744,6.25848 2.976,38.89904 0.744,1.44606 0.744,-3.91149 2.976,-30.90086 1.488,5.82012 2.232,20.64498 0.744,2.85098 0.744,1.52132 1.488,1.61142 0.744,1.6138 0.744,3.74111 2.232,18.7331 0.744,1.82262 0.744,-3.23004 1.488,-19.64156 1.488,-20.95621 0.744,-4.88296 1.488,5.26725 1.488,23.35075 3.72,78.19372 0.744,3.03636 0.744,-3.52594 1.488,-21.42585 5.208,-103.81978 0.744,-6.14833 1.488,3.44713 2.976,30.03524 0.744,3.76533 0.744,1.39758 1.488,-3.36539 0.744,-2.51763 0.744,-1.11263 1.488,3.98873 1.488,10.28867 2.232,16.39558 1.488,-2.65607 0.744,-8.48228 1.488,-29.39002 2.976,-64.39347 0.744,-10.22254 0.744,-5.58917 1.488,6.47734 1.488,29.06624 4.464,126.63805 1.488,19.96354 1.488,8.29161 2.976,7.72263 1.488,-2.20208 0.744,-6.24159 1.488,-23.49537 3.72,-83.10322 0.744,-6.23683 1.488,1.76487 2.232,8.76564 1.488,3.74779 1.488,-0.024 1.488,-3.13723 2.976,-2.18358 1.488,3.46051 2.232,12.14935 0.744,1.04711 0.744,-2.33626 0.744,-5.76889 1.488,-18.68947 2.976,-39.63108 0.744,-4.39655 1.488,5.15274 0.744,11.4399 1.488,40.38546 2.976,93.76422 0.744,13.18135 0.744,6.18746 1.488,-6.65651 1.488,-23.09303 2.232,-39.41937 0.744,-7.5832 0.744,-2.19151 0.744,3.37797 0.744,8.38648 1.488,28.30844 2.232,45.00689 0.744,6.46383 1.488,-7.85238 1.488,-29.28104 2.976,-66.0734 0.744,-9.19186 0.744,-3.97138 0.744,1.29966 0.744,5.71682 4.464,49.47802 3.72,29.2188 1.488,0.59528 0.744,-1.27976 0.744,-2.64938 0.744,-4.89588 0.744,-8.13954 1.488,-26.92991 2.976,-63.3791 1.488,-17.44058 1.488,-6.986 0.744,-2.45078 0.744,-3.75576 4.464,-32.09936 0.744,-1.62975 1.488,-1.46885 1.488,0.82394 0.744,3.15329 1.488,13.24986 3.72,41.72316 0.744,2.85103 1.488,-3.27108 0.744,-2.97088 1.488,2.46798 1.488,15.32221 2.232,27.13907 0.744,5.77709 0.744,1.85304 0.744,-2.94587 0.744,-7.76788 3.72,-58.37263 0.744,-1.83181 0.744,3.37185 2.976,23.05137 0.744,1.45875 0.744,-1.59848 0.744,-5.26625 1.488,-22.90961 3.72,-81.468298 0.744,-7.197684 1.488,6.014559 1.488,30.348323 2.976,69.63821 1.488,15.91528 2.232,10.94183 2.232,18.80032 1.488,15.65283 3.72,44.88057 0.744,1.58038 0.744,-2.28932 1.488,-12.70893 4.464,-54.32107 0.744,-2.68949 0.744,1.09828 0.744,4.68426 1.488,16.41455 2.232,25.92632 0.744,5.98774 0.744,3.10537 1.488,-6.11821 1.488,-19.78223 2.232,-32.4824 1.488,-12.87383 0.744,-3.53748 0.744,-1.52085 1.488,2.65488 0.744,4.96905 1.488,19.96561 3.72,64.27351 0.744,6.03854 0.744,2.11457 0.744,-2.40737 0.744,-6.5507 1.488,-21.65179 3.72,-62.98963 1.488,-13.82365 1.488,-8.53521 0.744,-2.44259 1.488,2.90333 1.488,15.66129 3.72,49.72553 1.488,13.21295 1.488,7.17468 1.488,-1.56624 0.744,-3.64211 1.488,-12.60594 2.232,-30.19264 2.232,-29.12852 2.976,-26.43417 2.232,-16.76631 1.488,-6.35095 1.488,2.60633 1.488,9.02587 0.744,5.30678 0,0" + id="path4671" /> + clip-path="url(#p50431ccdcb28178602d99d9270004dde-1)" + d="m 72,195.32244 2.232,30.33622 5.208,82.6578 0.744,4.19735 1.488,-3.89026 1.488,-19.94804 2.976,-51.5742 1.488,-15.29076 0.744,-3.34251 1.488,3.00996 2.232,13.1893 1.488,-3.78718 1.488,-19.12934 2.232,-34.06841 0.744,-5.24672 1.488,8.23212 1.488,31.43735 2.232,57.23294 1.488,22.07497 0.744,4.115 1.488,-6.28659 1.488,-21.65355 2.232,-36.59323 0.744,-5.1408 1.488,6.30869 1.488,20.61486 2.232,33.49649 2.232,21.86923 1.488,11.09439 0.744,3.17876 1.488,-4.64149 0.744,-9.80413 1.488,-35.10837 5.208,-162.82504 0.744,-4.37728 0.744,2.85359 1.488,18.23989 2.232,26.41871 1.488,8.29516 1.488,-4.16282 1.488,-19.19336 2.976,-49.954665 1.488,-14.127951 0.744,-1.481978 0.744,3.186202 0.744,8.152323 1.488,30.070909 2.976,74.34825 1.488,22.21049 0.744,3.87797 1.488,-4.18932 1.488,-6.76813 0.744,1.1176 0.744,5.54956 2.976,38.1284 0.744,2.07031 0.744,-3.13193 0.744,-7.82764 3.72,-55.0724 0.744,-3.64768 1.488,5.30612 2.976,30.19848 3.72,40.74891 1.488,-6.08927 1.488,-29.84397 5.208,-135.548516 0.744,-4.439519 0.744,2.328802 1.488,18.799123 2.232,31.04385 0.744,3.24254 0.744,-1.35421 1.488,-7.8348 1.488,3.07954 0.744,8.87001 2.976,54.21293 0.744,4.45466 0.744,-3.83675 0.744,-12.36007 1.488,-44.02004 2.232,-67.458014 0.744,-9.807903 0.744,-1.235956 0.744,6.843274 1.488,33.963199 3.72,110.95534 1.488,30.78323 1.488,15.82699 0.744,1.05037 0.744,-2.92336 1.488,-15.42023 2.232,-29.10193 0.744,-4.13997 1.488,3.25476 1.488,14.02677 1.488,13.7527 0.744,3.5736 1.488,-0.0598 1.488,-6.03731 2.232,-11.29102 0.744,-1.71544 1.488,0.83206 1.488,2.74048 1.488,-2.35746 0.744,-4.38614 1.488,-15.79294 2.976,-37.65538 1.488,-9.18414 1.488,-0.397 1.488,0.5789 0.744,-2.07495 2.976,-12.27931 0.744,-1.07803 1.488,3.7658 0.744,7.07097 1.488,22.86684 3.72,62.29229 1.488,10.53592 1.488,-1.53356 0.744,-5.65925 1.488,-20.3937 2.976,-52.3496 0.744,-5.71969 0.744,-1.77482 1.488,2.13314 0.744,1.74903 0.744,2.64263 0.744,4.53901 1.488,16.37177 2.232,37.7415 2.976,57.56895 1.488,16.24963 0.744,4.00541 1.488,-1.54407 0.744,-6.06174 1.488,-22.64763 4.464,-86.76047 0.744,-5.34539 1.488,5.4348 1.488,24.24557 1.488,27.75599 0.744,9.07281 0.744,4.13125 1.488,-5.73562 2.232,-22.1705 0.744,-2.10783 0.744,1.90328 1.488,11.73428 2.232,23.86467 0.744,3.59592 1.488,-4.51945 1.488,-10.58806 1.488,-5.51951 0.744,-1.17679 1.488,0.73573 0.744,2.96847 1.488,11.1741 2.232,25.63503 2.232,28.94129 0.744,5.08943 1.488,-6.45851 1.488,-28.88379 4.464,-110.71625 1.488,-28.21869 1.488,-15.73324 0.744,-1.09211 0.744,4.4715 0.744,10.35307 1.488,34.85257 2.232,59.39486 1.488,25.73433 2.232,24.50853 1.488,11.94282 0.744,4.03252 0.744,1.53676 0.744,-2.10704 0.744,-6.61355 1.488,-25.57901 2.976,-58.38824 0.744,-7.79984 0.744,-3.28947 0.744,1.43053 0.744,6.04059 1.488,23.71545 2.232,42.4171 1.488,15.27687 0.744,1.92636 0.744,-1.1466 2.232,-8.88218 1.488,1.15132 1.488,7.77147 2.232,12.03415 0.744,1.33413 1.488,-2.49175 0.744,-3.71797 0.744,-6.00499 1.488,-21.23441 4.464,-87.4472 1.488,-11.20767 1.488,2.21804 0.744,6.85048 1.488,22.61505 3.72,65.10288 1.488,12.3153 1.488,-1.41618 0.744,-4.74623 1.488,-16.15999 2.976,-38.69355 1.488,-10.16289 0.744,-2.09524 1.488,0.22406 1.488,1.2405 1.488,-1.56728 1.488,2.37064 0.744,5.74578 1.488,21.80702 2.232,36.04628 0.744,7.41719 0.744,3.73415 1.488,-3.52244 1.488,-16.44012 2.976,-39.75004 1.488,-10.86014 2.232,-9.68575 2.976,-8.07549 0.744,-1.90015 1.488,-0.90024 1.488,-2.67097 1.488,-10.76671 1.488,-10.69323 0.744,-1.50011 0.744,2.45728 0.744,6.87963 1.488,25.32681 2.976,58.56911 0.744,5.29605 0.744,-1.31911 0.744,-8.03292 1.488,-32.69795 2.976,-79.02586 1.488,-20.46693 0.744,-1.09612 0.744,5.72965 0.744,12.31994 1.488,40.84565 4.464,142.92149 1.488,22.81465 0.744,5.75133 0.744,2.9772 1.488,-3.98092 0.744,-9.9613 1.488,-37.68173 2.976,-91.91816 1.488,-24.91094 0.744,-5.37188 0.744,-1.86031 1.488,2.85403 1.488,7.85444 1.488,11.91121 1.488,18.20202 2.232,30.27667 1.488,14.22323 1.488,8.26892 1.488,2.78152 1.488,0.84953 1.488,-3.72943 0.744,-5.55318 1.488,-20.61354 1.488,-32.21626 4.464,-113.843703 1.488,-20.141638 0.744,-2.120428 0.744,4.682202 0.744,11.23528 1.488,36.107587 2.232,57.75799 1.488,25.25843 2.232,24.35664 1.488,10.24365 0.744,1.52362 1.488,-1.30286 0.744,-2.08998 0.744,-3.79067 1.488,-15.16222 2.232,-30.2585 0.744,-6.04314 0.744,-2.48551 0.744,1.29102 0.744,5.11122 1.488,20.22618 3.72,64.92923 0.744,7.75707 0.744,3.7673 1.488,-4.86038 1.488,-17.54351 1.488,-30.56327 0,0" + id="path4811" /> + clip-path="url(#p50431ccdcb28178602d99d9270004dde-4)" + d="m 72,163.1451 0.744,-2.27452 1.488,3.69053 1.488,13.83684 1.488,22.76845 2.232,51.95737 1.488,34.5966 0.744,12.10493 0.744,6.4761 1.488,-4.01561 1.488,-17.60438 2.232,-39.1788 1.488,-24.68425 0.744,-7.10813 0.744,-1.58165 0.744,4.08897 3.72,41.4428 0.744,3.48985 0.744,1.4769 1.488,-4.884 1.488,-18.30227 2.976,-48.67307 0.744,-6.86559 0.744,-3.377 1.488,0.86929 1.488,4.36029 0.744,1.13793 1.488,-2.49878 0.744,-2.45691 0.744,-1.28143 0.744,1.2585 0.744,4.6037 1.488,19.62599 1.488,24.18953 0.744,7.91383 0.744,3.48058 1.488,-3.71225 2.232,-11.62052 0.744,-1.80422 1.488,1.17035 1.488,7.85088 2.232,11.21149 2.976,13.6754 1.488,-1.78885 1.488,-8.80563 1.488,-14.75642 2.976,-33.43367 4.464,-31.10787 0.744,-1.14349 0.744,3.96554 0.744,10.21771 1.488,36.2853 1.488,36.87935 0.744,11.54626 0.744,5.98157 1.488,-1.44933 1.488,-5.71535 1.488,0.71294 1.488,-1.19286 1.488,-8.29143 1.488,-7.20299 1.488,0.83889 0.744,3.72078 1.488,13.41834 1.488,13.31066 0.744,2.88616 1.488,-1.52697 0.744,-3.54994 0.744,-5.99536 1.488,-20.37847 2.232,-38.29982 0.744,-7.92207 0.744,-2.75328 0.744,1.8795 2.232,12.81364 0.744,1.03227 0.744,-1.72154 2.232,-12.82547 1.488,2.50925 2.976,22.54865 0.744,-1.25574 0.744,-5.65 2.976,-34.85934 1.488,4.88766 0.744,12.56426 1.488,42.25131 2.976,97.04178 0.744,12.48095 0.744,3.2372 0.744,-6.05124 1.488,-32.37885 1.488,-37.54203 1.488,-20.92636 0.744,-3.7474 0.744,-1.00758 1.488,1.46415 1.488,0.0151 0.744,-2.34547 0.744,-4.56184 1.488,-15.18759 1.488,-16.71724 0.744,-5.34978 0.744,-1.5619 0.744,2.84896 0.744,7.14781 1.488,23.91764 2.232,37.60059 0.744,3.53793 0.744,-4.05031 0.744,-11.05249 1.488,-37.68191 3.72,-119.49675 1.488,-26.478681 0.744,-6.897754 0.744,-3.11891 0.744,1.250277 0.744,6.99151 0.744,13.792158 1.488,47.30283 4.464,177.32434 1.488,33.14416 1.488,17.02368 0.744,2.96724 1.488,-5.51284 1.488,-21.4442 2.232,-41.10146 1.488,-16.05151 0.744,-3.94976 0.744,-1.39129 1.488,2.2463 1.488,-0.3758 0.744,-5.36849 1.488,-23.64804 2.232,-41.03902 1.488,-15.6666 1.488,-8.38273 1.488,5.61339 0.744,10.64396 1.488,34.68651 2.976,76.53698 0.744,9.90657 0.744,4.79924 1.488,-2.56891 0.744,-6.54618 1.488,-23.81025 2.232,-40.27649 0.744,-7.50548 0.744,-3.17536 1.488,2.37205 1.488,6.46522 2.232,10.65168 1.488,-3.60726 1.488,-14.64286 2.232,-24.27417 1.488,-9.22035 0.744,-2.77995 1.488,-0.14636 1.488,3.06904 0.744,-1.23541 0.744,-5.03374 1.488,-19.42625 1.488,-18.74288 0.744,-4.28695 1.488,6.28938 1.488,24.29036 3.72,74.23768 0.744,6.9658 0.744,2.43073 0.744,-1.42117 1.488,-8.96017 2.232,-14.48642 0.744,-1.16321 1.488,0.42834 0.744,-1.29693 0.744,-3.04055 3.72,-23.37479 0.744,-1.02771 0.744,1.55461 0.744,4.20746 1.488,15.48447 4.464,65.01346 0.744,3.1016 0.744,-2.14014 0.744,-8.58213 1.488,-36.71644 2.232,-69.06021 1.488,-29.90818 1.488,-16.47476 0.744,-2.88334 0.744,1.26266 0.744,5.61147 1.488,22.21736 3.72,68.78244 0.744,7.10172 0.744,3.52161 1.488,-2.8778 1.488,-8.405 0.744,-1.84613 0.744,1.64525 1.488,12.44408 1.488,13.28303 0.744,3.36649 1.488,-2.70589 1.488,-13.49759 3.72,-46.57487 4.464,-67.92097 0.744,-1.43634 0.744,3.41625 2.232,22.47254 2.232,22.12998 1.488,7.75163 1.488,4.7448 2.232,4.33509 1.488,5.89054 1.488,5.5381 1.488,-1.04093 0.744,-4.01282 1.488,-16.26713 1.488,-26.75233 2.976,-66.60945 0.744,-9.18019 0.744,-2.57555 0.744,4.67276 1.488,26.63495 2.976,60.57553 2.232,33.23359 1.488,11.55037 1.488,-4.40908 3.72,-38.79848 0.744,-2.37546 1.488,-0.91866 0.744,-2.30007 0.744,-5.56669 2.976,-33.59139 0.744,-2.90548 0.744,1.16037 1.488,10.94604 1.488,9.56728 0.744,1.78836 1.488,1.80099 0.744,2.10887 0.744,3.74749 1.488,13.03245 1.488,19.44637 2.232,33.25631 0.744,6.38156 0.744,1.3175 0.744,-4.56847 1.488,-24.44709 2.232,-40.3528 0.744,-7.59347 0.744,-3.82612 1.488,3.76905 2.232,14.45134 1.488,-0.90748 2.976,-13.16671 1.488,1.82187 2.232,10.12258 1.488,-3.02868 1.488,-14.67378 4.464,-58.70091 0.744,-4.0362 1.488,3.01815 1.488,14.64084 2.232,29.36065 1.488,10.75984 1.488,5.26404 0.744,1.07752 1.488,-2.26771 1.488,-4.07076 1.488,0.0972 0.744,3.57554 1.488,16.756 2.232,31.16986 1.488,9.29941 1.488,3.14758 1.488,2.73359 0.744,2.65369 0.744,4.75089 1.488,17.85895 2.232,34.39264 0.744,6.45355 0.744,2.43797 0.744,-1.47078 0.744,-5.10291 1.488,-18.98022 3.72,-58.41148 1.488,-10.50604 0.744,-1.42735 2.976,0.61828 0.744,1.35708 2.232,8.73947 2.232,9.13244 0.744,1.28294 1.488,-2.9375 2.976,-12.40827 1.488,2.80629 1.488,9.75622 3.72,32.55742 1.488,-0.96532 1.488,-9.5954 3.72,-29.85605 2.976,-22.32126 0.744,-2.60173 1.488,4.89718 1.488,19.46461 1.488,22.06141 0.744,6.24971 0,0" + id="path4951" /> AnalogSignalArray + x="187.07605" + y="-45.020061">AnalogSignal IrregularlySampledSignal + + ChannelIndex + y="-91.022598" + x="264.88303" + sodipodi:role="line">View + d="m 620.86053,-242.09527 c 0,0 7.22292,-0.50171 7.57758,3.148 0.14852,1.5284 0.2121,4.65508 0.2121,4.65508 0.53637,6.73562 -4.1397,9.34953 -12.91086,9.1051 l -16.44056,2e-5 c -6.88078,0.28147 -9.70501,1.126 -10.46922,9.60561 l 0.1551,12.92847" + style="fill:none;stroke:#008000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Lend-1C)" /> + d="m 628.3009,-239.42863 0.37533,5.6443 c -0.38004,6.13036 3.23713,8.0843 7.05592,7.87865 l 18.32216,0.76442 c 8.55567,0.2583 8.29747,6.12977 8.49758,15.22615 l 0.2857,9.01801" + style="fill:none;stroke:#008000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Lend-1e)" /> + d="m 628.6431,-235.66977 c 0,3.14614 -0.0109,2.83036 -0.0109,5.9765 -0.0664,4.076 -1.68849,7.18468 -4.47254,8.34554 l -4.00983,0.89423 c -2.13811,1.05969 -2.86438,2.25204 -2.77604,6.75591 l -0.12281,13.29529" + style="fill:none;stroke:#008000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Lend-1E)" /> - - + d="m 628.64162,-233.2654 c 0,3.33249 0.0897,0.96811 0.0897,4.3006 -0.0657,4.31743 0.61906,5.61074 2.292,6.41941 l 1.75054,1.1674 c 2.23646,1.75389 2.05841,3.11236 2.14594,7.883 l 0.17458,13.4248" + style="fill:none;stroke:#008000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;marker-end:url(#Arrow1Lend-1o)" /> + d="m 279.9382,-50.41412 c 2.25775,-0.12975 7.14425,0.109248 7.14425,-2.356112 -0.0616,-3.18772 -0.27684,-26.097233 -0.27922,-33.804098 l -0.298,-15.83482 c 0.002,-3.67252 -1.03087,-8.97084 8.89419,-9.18469 l 18.26818,0.0456" + style="fill:none;stroke:#aa0000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow1Lend-1Cb)" /> + d="m 284.01311,-50.403359 c 2.34216,-0.129669 9.33168,-2.015209 9.33168,-4.479021 -0.0639,-3.185714 -0.35306,-13.156858 -0.21508,-15.289015 l -0.0593,-4.777778 c 0.34508,-2.074743 2.39506,-4.067729 5.1528,-4.171414 l 16.42636,-9.5e-5" + style="fill:none;stroke:#aa0000;stroke-width:1.01820028px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow1Lend-1v)" /> + + d="m 291.54042,184.59044 c 2.25775,-0.12975 12.20472,0.3428 16.09739,0.36876 9.58,0.3098 14.13358,6.7723 18.1265,11.63379 l 11.33923,13.04714" + style="fill:none;stroke:#4d0089;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow1Lend-1CbS)" /> + d="m 299.17005,184.72518 c 2.25775,-0.12975 18.74442,0.26495 25.90693,0.52447 14.17336,0.46551 38.89099,12.06634 50.04643,28.06087 l 6.43446,8.60949" + style="fill:none;stroke:#4d0089;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow1Lend-1CbS)" /> + d="m 303.45201,184.80304 c 68.4332,1.27161 70.36126,1.43275 88.89038,5.19567 22.81509,8.56227 26.90155,21.87588 29.1817,32.65423 l 4.33241,9.7773" + style="fill:none;stroke:#4d0089;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;marker-end:url(#Arrow1Lend-1CbS)" /> - - - - - - - - - - - - - - - - - - - - - - - - - + id="tspan3337" + sodipodi:role="line"> - - - + id="use4817-2-4" /> - - + + + + width="744.09448" + style="fill:none;stroke:#4d0089;stroke-width:1.28113735;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" + xlink:href="#md0b6aff34ceb4dcf39a042d98ad215e8" + x="660.58362" + y="220.5" + id="use4817-2-4-0" /> + + + + width="744.09448" + style="fill:none;stroke:#aa00d4;stroke-width:1.28113735;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" + xlink:href="#md0b6aff34ceb4dcf39a042d98ad215e8" + x="660.58362" + y="220.5" + id="use4817-2-4-7-4" /> + + + + width="744.09448" + style="fill:none;stroke:#aa00d4;stroke-width:1.28113735;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" + xlink:href="#md0b6aff34ceb4dcf39a042d98ad215e8" + x="660.58362" + y="220.5" + id="use4817-2-4-7-5" /> + + + + + width="744.09448" + style="fill:none;stroke:#005522;stroke-width:1.28113735;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" + xlink:href="#md0b6aff34ceb4dcf39a042d98ad215e8" + x="634.56555" + y="220.5" + id="use4807-9-32" /> + + + + + + width="744.09448" + style="fill:none;stroke:#005522;stroke-width:1.28113735;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" + xlink:href="#md0b6aff34ceb4dcf39a042d98ad215e8" + x="634.56555" + y="220.5" + id="use4807-9-47" /> + + + + + + width="744.09448" + style="fill:none;stroke:#005522;stroke-width:1.28113735;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" + xlink:href="#md0b6aff34ceb4dcf39a042d98ad215e8" + x="634.56555" + y="220.5" + id="use4807-9-33-35" /> + + + + + + width="744.09448" + style="fill:none;stroke:#005522;stroke-width:1.28113735;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" + xlink:href="#md0b6aff34ceb4dcf39a042d98ad215e8" + x="634.56555" + y="220.5" + id="use4807-9-33-76" /> + + + Group + + + Group diff --git a/doc/source/images/base_schematic_with_IrregularlySampledSignal.svg b/doc/source/images/base_schematic_with_IrregularlySampledSignal.svg deleted file mode 100644 index 3b83a3375..000000000 --- a/doc/source/images/base_schematic_with_IrregularlySampledSignal.svg +++ /dev/null @@ -1,4147 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Block - - RecordingChannel - Unit - - SpikeTrain - - - AnalogSignal - - Epoch - Segment - - - Event - - - - - - - - Spike - Neo 0.2 architecture - - - - - - EpochArray - - EventArray - - - - - - - AnalogSignalArray - IrregularlySampledSignal - - ChannelIndex - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/doc/source/images/generate_diagram.py b/doc/source/images/generate_diagram.py index fba3920c3..c25bb4e8d 100644 --- a/doc/source/images/generate_diagram.py +++ b/doc/source/images/generate_diagram.py @@ -207,12 +207,13 @@ def generate_diagram_simple(): 'Segment': (.5 + rw * bf * 1, .5), 'Event': (.5 + rw * bf * 4, 3.0), 'Epoch': (.5 + rw * bf * 4, 1.0), - 'ChannelIndex': (.5 + rw * bf * 1, 7.5), - 'Unit': (.5 + rw * bf * 2., 9.9), + 'Group': (.5 + rw * bf * 1, 7.5), + 'View': (.5 + rw * bf * 2., 9.9), 'SpikeTrain': (.5 + rw * bf * 3, 7.5), 'IrregularlySampledSignal': (.5 + rw * bf * 3, 0.5), 'AnalogSignal': (.5 + rw * bf * 3, 4.9), } + # todo: add ImageSequence, RegionOfInterest generate_diagram('simple_generated_diagram.svg', rect_pos, rect_width, figsize) generate_diagram('simple_generated_diagram.png', diff --git a/doc/source/images/multi_segment_diagram.svg b/doc/source/images/multi_segment_diagram.svg index 94880032b..58236f5cc 100644 --- a/doc/source/images/multi_segment_diagram.svg +++ b/doc/source/images/multi_segment_diagram.svg @@ -1,6 +1,4 @@ - - + inkscape:export-filename="/Users/andrew/dev/neo/doc/source/images/multi_segment_diagram.png" + sodipodi:docname="multi_segment_diagram.svg" + inkscape:version="1.0 (4035a4f, 2020-05-01)" + version="1.1" + id="svg3504" + height="1052.3622047" + width="744.09448819"> + width="446.39999" + height="345.60001" + id="rect4949-2-1" /> + width="446.39999" + height="345.60001" + id="rect4809-6-9" /> + width="446.39999" + height="345.60001" + id="rect4669-5-7" /> + width="446.39999" + height="345.60001" + id="rect4949-4-7" /> + width="446.39999" + height="345.60001" + id="rect4809-1-6" /> + width="446.39999" + height="345.60001" + id="rect4669-9-6" /> + width="446.39999" + height="345.60001" + id="rect4809-6-0-5" /> + width="446.39999" + height="345.60001" + id="rect4949-2-2-4" /> + width="446.39999" + height="345.60001" + id="rect4809-1-7-0" /> + width="446.39999" + height="345.60001" + id="rect4809-6-0" /> + width="446.39999" + height="345.60001" + id="rect4949-2-2" /> + width="446.39999" + height="345.60001" + id="rect4809-1-7" /> + width="446.39999" + height="345.60001" + id="rect4949-2" /> + width="446.39999" + height="345.60001" + id="rect4809-6" /> + width="446.39999" + height="345.60001" + id="rect4669-5" /> + width="446.39999" + height="345.60001" + id="rect4949-4" /> + width="446.39999" + height="345.60001" + id="rect4809-1" /> + width="446.39999" + height="345.60001" + id="rect4669-9" /> + inkscape:window-x="0" + inkscape:window-height="821" + inkscape:window-width="1372" + showgrid="false" + inkscape:current-layer="layer1" + inkscape:document-units="px" + inkscape:cy="258.29969" + inkscape:cx="346.77419" + inkscape:zoom="1.24" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="#ffffff" + id="base" /> @@ -209,402 +208,417 @@ image/svg+xml - + + inkscape:label="Layer 1"> + height="308.31601" + width="115.0112" + id="rect2385" + style="fill:none;stroke:#000000;stroke-width:0.99540734;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> Segment 2 + id="tspan14158" + sodipodi:role="line">Segment 2 ChannelIndex + y="122.99126" + x="-255.30644" + sodipodi:role="line">Group + height="308.55853" + width="115.0112" + id="rect2385-6" + style="fill:none;stroke:#000000;stroke-width:0.99579889;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + d="m 222.70549,271.49472 0.0939,-0.65022 0.28159,1.79838 0.2816,0.0478 0.28164,-3.3574 0.2816,0.91227 0.2816,-3.76504 0.28158,-3.8786 0.28159,0.31826 0.28161,-2.93969 0.28158,2.12976 0.28166,2.91161 0.28158,-3.66694 0.28159,-4.78151 0.2816,3.00418 0.2816,-0.45084 0.28158,3.36208 0.2816,2.38221 0.28165,-1.73022 0.2816,2.24656 0.28159,1.29211 0.2816,-1.02102 0.28159,1.63143 0.2816,0.0785 0.28159,-0.17149 0.28161,1.84832 0.28159,-1.45939 0.28163,2.20814 0.28159,-0.37676 0.28161,-3.22058 0.28159,-4.99985 0.28161,1.80392 0.2816,-0.72018 0.28159,3.61202 0.2816,-0.11021 0.28159,-1.01527 0.28159,-1.53575 0.28162,-2.34306 0.28163,-1.33859 0.28159,1.26216 0.28159,2.81306 0.2816,-3.5147 0.2816,3.15474 0.28158,-1.43211 0.28159,-1.35094 0.28166,-1.35476 0.28158,2.45654 0.2816,-2.25443 0.28159,0.79081 0.2816,-2.33768 0.28159,-1.27333 0.28165,0.0148 0.28159,-2.79968 0.2816,2.96059 0.2816,0.0434 0.28159,2.29501 0.28159,-1.20215 0.2816,1.99338 0.28164,0.25563 0.28159,0.88164 0.28161,1.88327 0.28159,2.48923 0.2816,4.27227 0.28158,-2.67399 0.28159,1.51851 0.28167,-3.91957 0.28157,0.84992 0.2816,0.79747 0.2816,1.65774 0.28159,3.25539 0.28161,-1.05148 0.28156,3.38361 0.28167,-2.56788 0.28158,1.42066 0.2816,-1.64775 0.28161,1.10117 0.28157,-2.48312 0.2816,-0.34185 0.28165,-1.85732 0.28159,-4.3439 0.28161,-1.22529 0.28159,-2.64647 0.28159,0.40261 0.28159,2.30316 0.2816,3.26171 0.28165,3.90675 0.28159,1.20757 0.2816,-1.13937 0.2816,1.8794 0.28157,1.22247 0.2816,0.94872 0.28159,-0.0297 0.28162,-0.677 0.28164,-0.95908 0.28158,-2.79147 0.28159,-0.91336 0.2816,1.34441 0.2816,-0.80308 0.28161,-0.54615 0.28159,2.23517 0.28163,-0.55074 0.28159,-1.05092 0.28158,2.60262 0.2816,1.38288 0.2816,-0.87159 0.28165,0.8822 0.28158,-0.44716 0.28159,5.30315 0.2816,-2.34479 0.2816,-2.53624 0.28159,-0.16236 0.2816,2.55294 0.28163,-1.85789 0.28159,-2.12507 0.2816,1.95738 0.2816,-2.0168 0.2816,-0.71736 0.28159,-3.52875 0.28164,1.87319 0.2816,3.54883 0.2816,1.00207 0.28159,-1.72404 0.28159,-4.11831 0.2816,0.2098 0.28159,3.47672 0.28164,0.59952 0.28161,-0.53344 0.28158,0.92126 0.2816,-1.67634 0.28159,0.0624 0.2816,1.38527 0.28159,0.17587 0.28163,1.98841 0.2816,-3.42549 0.28159,1.03746 0.28161,-0.50364 0.2816,1.59861 0.28158,-1.11643 0.28159,1.67321 0.28165,0.7699 0.28165,1.66817 0.28155,1.33539 0.28162,0.65591 0.28155,2.48906 0.28161,-3.94096 0.28162,0.17908 0.28156,-2.71655 0.28166,1.49826 0.28155,-0.0782 0.28161,-2.04234 0.28165,-4.32407 0.28156,-5.58112 0.28163,-0.10081 0.28161,-1.93655 0.28154,1.11356 0.28163,0.72132 0.28155,0.17371 0.28164,-0.18938 0.28166,-0.45278 0.28158,-1.22887 0.28159,2.05886 0.28161,1.59961 0.28154,-0.0455 0.28165,1.28094 0.28154,-2.95794 0.28168,0.21413 0.2816,1.58617 0.2816,0.31645 0.28159,1.25661 0.28159,-0.99596 0.2816,-2.27949 0.2816,0.49983 0.28163,-2.23732 0.2816,0.89454 0.28159,1.64203 0.28159,-2.01425 0.2816,-0.37659 0.2816,-0.89502 0.28159,0.37718 0.28165,3.04772 0.28158,1.60406 0.28161,-0.97373 0.28159,1.24335 0.28161,-3.31343 0.28158,-0.72102 0.28163,0.43293 0.28161,1.1283 0.2816,0.31134 0.28158,0.6649 0.2816,1.21785 0.28161,2.15913 0.28157,2.5182 0.28164,2.14505 0.28161,3.90048 0.28159,-1.43453 0.28159,0.0936 0.28159,0.45461 0.2816,-1.28066 0.28159,-1.62624 0.28168,-1.51851 0.28156,-2.62506 0.2816,-1.73765 0.2816,1.05838 0.28158,-2.35868 0.28161,2.92044 0.28167,2.04158 0.28156,0.16252 0.28164,-0.85923 0.28154,-2.4334 0.28159,-1.72778 0.28159,0.76047 0.28162,-0.20882 0.28163,-0.47628 0.28154,-0.79049 0.28165,-0.61261 0.28163,-3.2259 0.28156,0.0224 0.28165,3.35336 0.28153,0.75558 0.2816,-0.16432 0.28164,-3.43003 0.28154,0.92457 0.28166,-2.55016 0.28164,1.07406 0.28154,-0.43445 0.28164,4.41529 0.2816,-1.69215 0.28158,-1.51447 0.28161,2.94899 0.28155,0.4539 0.28163,4.24397 0.28154,3.53664 0.2817,3.32527 0.2816,1.68069 0.2816,-1.09731 0.28158,-2.91576 0.28159,2.71903 0.28156,0.0598 0.28164,-0.83655 0.28165,1.69247 0.28159,-1.33134 0.28159,-1.69297 0.2816,-3.42547 0.28159,-2.60579 0.28161,0.79454 0.28158,-1.38681 0.28163,0.12446 0.2816,-0.50704 0.2816,-2.23453 0.28161,-0.16455 0.28157,-0.45981 0.28159,-2.07539 0.28165,-4.76536 0.2816,0.12337 0.2816,-4.92953 0.28158,-0.12442 0.2816,-4.40374 0.2816,0.72553 0.28159,-0.0916 0.28166,-1.80163 0.28159,0.85558 0.28157,-0.32571 0.2816,8.23979 0.2816,0.96718 0.2816,5.6372 0.28161,1.97274 0.2816,3.61145 0.28161,-0.0287 0.2816,2.1735 0.28159,0.14365 0.28159,-3.75557 0.28159,2.83995 0.28171,-1.70416 0.28154,0.46254 0.28163,1.68104 0.28156,-0.89553 0.2816,-0.94178 0.2816,0.62107 0.28158,-1.80433 0.28165,-0.83944 0.28164,4.58588 0.28154,-2.93819 0.28165,1.56934 0.28155,2.37222 0.28159,-1.96843 0.2816,4.09013 0.2816,-2.63005 0.28162,1.37942 0.28165,2.37555 0.28155,1.13014 0.28163,-1.16697 0.28155,1.72014 0.28169,-0.50155 0.28151,-3.36382 0.2816,1.27787 0.28169,-5.58999 0.28151,2.97148 0.2816,-0.46802 0.28168,-2.87843 0.2816,1.74327 0.2816,0.32919 0.2816,-0.68901 0.28152,-2.48547 0.28159,-0.0147 0.2816,0.30121 0.2816,4.05682 0.28169,-2.28124 0.2816,-0.375 0.2816,-0.0952 0.2816,-3.41578 0.2816,0.0962 0.2816,1.09725 0.28151,2.412 0.28168,3.83625 0.2816,-2.5841 0.2816,-1.94555 0.2816,1.16433 0.2816,-1.40041 0.2816,2.0661 0.2816,-0.14777 0.2816,0.90409 0.2816,1.4941 0.2816,-2.03815 0.2816,-3.51139 0.2816,-0.37598 0.2816,-3.20486 0.2816,3.63219 0.2816,-1.03755 0.2816,-0.90889 0.28159,0.66676 0.2816,-0.69522 0.2816,-3.14377 0.2816,-1.38854 0.2816,1.0004 0.2816,1.37037 0.28169,2.9271 0.28151,0.14337 0.2816,-0.25431 0.2816,-0.64014 0.28168,3.21865 0.2816,-1.90861 0.2816,4.05888 0.2816,2.19023 0.2816,0.96478 0.2816,-0.3453 0.28151,0.9265 0.28169,-2.99366 0.2816,-2.92969 0.2816,-0.1183 0.2816,-3.57941 0.2816,0.20556 0.2816,-3.47592 0.2816,2.07596 0.2816,-3.82994 0.28159,-3.3212 0.2816,0.15551 0.2816,2.21495 0.2816,4.35104 0.2816,0.34942 0.2816,1.97568 0.2816,-0.0424 0.2816,1.01363 0.2816,-1.09955 0.28169,1.62205 0.28151,5.40194 0.28168,-2.60878 0.2816,0.18115 0.28152,1.08384 0.28168,0.33661 0.28152,-0.38263 0.28159,0.66049 0.2816,-0.567 0.2816,-0.55418 0.28169,0.35739 0.2816,-0.87595 0.2816,0.97882 0.2816,-1.97601 0.28151,-1.73457 0.28169,-4.55629 0.28151,1.15363 0.28168,-1.19391 0.2816,1.60667 0.2816,-1.5903 0.2816,-0.98102 0.2816,0.85377 0.2816,0.99845 0.2816,-2.84561 0.2816,1.42273 0.2816,7.97131 0.2816,-1.8932 0.2816,1.00487 0.2816,-0.56939 0.2816,-0.2043 0.2816,2.8425 0.2816,-2.32566 0.2816,-1.26199 0.28159,3.57549 0.2816,-2.66773 0.2816,-5.01292 0.2816,1.85462 0.28169,0.78672 0.2816,1.58927 0.2816,4.47557 0.28151,-1.30855 0.28169,-3.14339 0.28151,-0.87456 0.28168,1.52212 0.2816,2.11194 0.2816,4.58331 0.2816,-0.10075 0.20462,-1.93982" + clip-path="none" + style="fill:none;stroke:#aa4400;stroke-width:0.83733952;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" /> + style="fill:none;stroke:#aa0000;stroke-width:5.40178728;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-70-4)" + d="m 72,199.44014 0.744,-9.88505 0.744,-8.16304 1.488,-9.36337 1.488,1.87949 0.744,4.51209 1.488,14.91057 2.976,33.84172 0.744,4.07908 0.744,1.42438 2.232,-4.60906 1.488,-0.0347 1.488,4.87221 1.488,5.9514 0.744,1.30441 0.744,-1.102 1.488,-8.48149 1.488,-6.66913 1.488,1.49385 0.744,2.11321 1.488,-1.52428 2.976,-17.3357 0.744,1.00237 2.232,10.64968 1.488,-4.58749 1.488,-22.11071 2.232,-44.27705 0.744,-8.23399 0.744,-2.67097 0.744,1.68533 1.488,9.82042 1.488,15.06494 2.232,34.10097 1.488,19.55812 0.744,4.74052 1.488,-1.04466 2.232,-8.95528 0.744,-1.66071 1.488,1.57539 1.488,9.97787 2.976,25.15023 1.488,9.85045 1.488,16.87157 1.488,20.10934 0.744,4.06258 0.744,-3.2619 0.744,-10.54148 4.464,-88.92547 2.232,-7.57641 0.744,-6.78791 2.976,-42.28856 0.744,-3.70829 0.744,1.93631 1.488,16.51775 1.488,17.60432 0.744,4.60487 0.744,1.46299 1.488,-2.48511 2.232,-8.84485 1.488,3.58933 1.488,17.8884 2.232,44.1192 2.976,69.61286 1.488,16.66005 0.744,1.77475 0.744,-1.73947 0.744,-4.38392 2.232,-21.99984 1.488,-11.63998 2.232,-13.45344 2.232,-20.81468 3.72,-43.83979 0.744,-3.86105 1.488,3.98198 1.488,16.21543 2.232,25.67229 0.744,2.70765 0.744,-1.27609 1.488,-10.12523 1.488,-11.31521 0.744,-3.97331 0.744,-2.00057 1.488,4.1382 1.488,14.931 1.488,16.65152 0.744,3.72521 0.744,-1.59709 0.744,-6.92539 2.976,-47.18876 2.232,-32.9563 1.488,-10.49265 2.976,-7.78144 1.488,1.06199 1.488,10.11014 1.488,8.87559 0.744,1.15151 1.488,0.55343 0.744,1.482 2.232,7.63589 1.488,0.81241 1.488,3.5433 2.976,16.08709 1.488,0.11738 1.488,-4.45597 2.232,-8.75192 0.744,-1.69833 1.488,2.54425 0.744,6.25848 2.976,38.89904 0.744,1.44606 0.744,-3.91149 2.976,-30.90086 1.488,5.82012 2.232,20.64498 0.744,2.85098 0.744,1.52132 1.488,1.61142 0.744,1.6138 0.744,3.74111 2.232,18.7331 0.744,1.82262 0.744,-3.23004 1.488,-19.64156 1.488,-20.95621 0.744,-4.88296 1.488,5.26725 1.488,23.35075 3.72,78.19372 0.744,3.03636 0.744,-3.52594 1.488,-21.42585 5.208,-103.81978 0.744,-6.14833 1.488,3.44713 2.976,30.03524 0.744,3.76533 0.744,1.39758 1.488,-3.36539 0.744,-2.51763 0.744,-1.11263 1.488,3.98873 1.488,10.28867 2.232,16.39558 1.488,-2.65607 0.744,-8.48228 1.488,-29.39002 2.976,-64.39347 0.744,-10.22254 0.744,-5.58917 1.488,6.47734 1.488,29.06624 4.464,126.63805 1.488,19.96354 1.488,8.29161 2.976,7.72263 1.488,-2.20208 0.744,-6.24159 1.488,-23.49537 3.72,-83.10322 0.744,-6.23683 1.488,1.76487 2.232,8.76564 1.488,3.74779 1.488,-0.024 1.488,-3.13723 2.976,-2.18358 1.488,3.46051 2.232,12.14935 0.744,1.04711 0.744,-2.33626 0.744,-5.76889 1.488,-18.68947 2.976,-39.63108 0.744,-4.39655 1.488,5.15274 0.744,11.4399 1.488,40.38546 2.976,93.76422 0.744,13.18135 0.744,6.18746 1.488,-6.65651 1.488,-23.09303 2.232,-39.41937 0.744,-7.5832 0.744,-2.19151 0.744,3.37797 0.744,8.38648 1.488,28.30844 2.232,45.00689 0.744,6.46383 1.488,-7.85238 1.488,-29.28104 2.976,-66.0734 0.744,-9.19186 0.744,-3.97138 0.744,1.29966 0.744,5.71682 4.464,49.47802 3.72,29.2188 1.488,0.59528 0.744,-1.27976 0.744,-2.64938 0.744,-4.89588 0.744,-8.13954 1.488,-26.92991 2.976,-63.3791 1.488,-17.44058 1.488,-6.986 0.744,-2.45078 0.744,-3.75576 4.464,-32.09936 0.744,-1.62975 1.488,-1.46885 1.488,0.82394 0.744,3.15329 1.488,13.24986 3.72,41.72316 0.744,2.85103 1.488,-3.27108 0.744,-2.97088 1.488,2.46798 1.488,15.32221 2.232,27.13907 0.744,5.77709 0.744,1.85304 0.744,-2.94587 0.744,-7.76788 3.72,-58.37263 0.744,-1.83181 0.744,3.37185 2.976,23.05137 0.744,1.45875 0.744,-1.59848 0.744,-5.26625 1.488,-22.90961 3.72,-81.468298 0.744,-7.197684 1.488,6.014559 1.488,30.348323 2.976,69.63821 1.488,15.91528 2.232,10.94183 2.232,18.80032 1.488,15.65283 3.72,44.88057 0.744,1.58038 0.744,-2.28932 1.488,-12.70893 4.464,-54.32107 0.744,-2.68949 0.744,1.09828 0.744,4.68426 1.488,16.41455 2.232,25.92632 0.744,5.98774 0.744,3.10537 1.488,-6.11821 1.488,-19.78223 2.232,-32.4824 1.488,-12.87383 0.744,-3.53748 0.744,-1.52085 1.488,2.65488 0.744,4.96905 1.488,19.96561 3.72,64.27351 0.744,6.03854 0.744,2.11457 0.744,-2.40737 0.744,-6.5507 1.488,-21.65179 3.72,-62.98963 1.488,-13.82365 1.488,-8.53521 0.744,-2.44259 1.488,2.90333 1.488,15.66129 3.72,49.72553 1.488,13.21295 1.488,7.17468 1.488,-1.56624 0.744,-3.64211 1.488,-12.60594 2.232,-30.19264 2.232,-29.12852 2.976,-26.43417 2.232,-16.76631 1.488,-6.35095 1.488,2.60633 1.488,9.02587 0.744,5.30678 0,0" + id="path4671-0" /> + style="fill:none;stroke:#aa0000;stroke-width:5.78597641;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-1-3)" + d="m 72,195.32244 2.232,30.33622 5.208,82.6578 0.744,4.19735 1.488,-3.89026 1.488,-19.94804 2.976,-51.5742 1.488,-15.29076 0.744,-3.34251 1.488,3.00996 2.232,13.1893 1.488,-3.78718 1.488,-19.12934 2.232,-34.06841 0.744,-5.24672 1.488,8.23212 1.488,31.43735 2.232,57.23294 1.488,22.07497 0.744,4.115 1.488,-6.28659 1.488,-21.65355 2.232,-36.59323 0.744,-5.1408 1.488,6.30869 1.488,20.61486 2.232,33.49649 2.232,21.86923 1.488,11.09439 0.744,3.17876 1.488,-4.64149 0.744,-9.80413 1.488,-35.10837 5.208,-162.82504 0.744,-4.37728 0.744,2.85359 1.488,18.23989 2.232,26.41871 1.488,8.29516 1.488,-4.16282 1.488,-19.19336 2.976,-49.954665 1.488,-14.127951 0.744,-1.481978 0.744,3.186202 0.744,8.152323 1.488,30.070909 2.976,74.34825 1.488,22.21049 0.744,3.87797 1.488,-4.18932 1.488,-6.76813 0.744,1.1176 0.744,5.54956 2.976,38.1284 0.744,2.07031 0.744,-3.13193 0.744,-7.82764 3.72,-55.0724 0.744,-3.64768 1.488,5.30612 2.976,30.19848 3.72,40.74891 1.488,-6.08927 1.488,-29.84397 5.208,-135.548516 0.744,-4.439519 0.744,2.328802 1.488,18.799123 2.232,31.04385 0.744,3.24254 0.744,-1.35421 1.488,-7.8348 1.488,3.07954 0.744,8.87001 2.976,54.21293 0.744,4.45466 0.744,-3.83675 0.744,-12.36007 1.488,-44.02004 2.232,-67.458014 0.744,-9.807903 0.744,-1.235956 0.744,6.843274 1.488,33.963199 3.72,110.95534 1.488,30.78323 1.488,15.82699 0.744,1.05037 0.744,-2.92336 1.488,-15.42023 2.232,-29.10193 0.744,-4.13997 1.488,3.25476 1.488,14.02677 1.488,13.7527 0.744,3.5736 1.488,-0.0598 1.488,-6.03731 2.232,-11.29102 0.744,-1.71544 1.488,0.83206 1.488,2.74048 1.488,-2.35746 0.744,-4.38614 1.488,-15.79294 2.976,-37.65538 1.488,-9.18414 1.488,-0.397 1.488,0.5789 0.744,-2.07495 2.976,-12.27931 0.744,-1.07803 1.488,3.7658 0.744,7.07097 1.488,22.86684 3.72,62.29229 1.488,10.53592 1.488,-1.53356 0.744,-5.65925 1.488,-20.3937 2.976,-52.3496 0.744,-5.71969 0.744,-1.77482 1.488,2.13314 0.744,1.74903 0.744,2.64263 0.744,4.53901 1.488,16.37177 2.232,37.7415 2.976,57.56895 1.488,16.24963 0.744,4.00541 1.488,-1.54407 0.744,-6.06174 1.488,-22.64763 4.464,-86.76047 0.744,-5.34539 1.488,5.4348 1.488,24.24557 1.488,27.75599 0.744,9.07281 0.744,4.13125 1.488,-5.73562 2.232,-22.1705 0.744,-2.10783 0.744,1.90328 1.488,11.73428 2.232,23.86467 0.744,3.59592 1.488,-4.51945 1.488,-10.58806 1.488,-5.51951 0.744,-1.17679 1.488,0.73573 0.744,2.96847 1.488,11.1741 2.232,25.63503 2.232,28.94129 0.744,5.08943 1.488,-6.45851 1.488,-28.88379 4.464,-110.71625 1.488,-28.21869 1.488,-15.73324 0.744,-1.09211 0.744,4.4715 0.744,10.35307 1.488,34.85257 2.232,59.39486 1.488,25.73433 2.232,24.50853 1.488,11.94282 0.744,4.03252 0.744,1.53676 0.744,-2.10704 0.744,-6.61355 1.488,-25.57901 2.976,-58.38824 0.744,-7.79984 0.744,-3.28947 0.744,1.43053 0.744,6.04059 1.488,23.71545 2.232,42.4171 1.488,15.27687 0.744,1.92636 0.744,-1.1466 2.232,-8.88218 1.488,1.15132 1.488,7.77147 2.232,12.03415 0.744,1.33413 1.488,-2.49175 0.744,-3.71797 0.744,-6.00499 1.488,-21.23441 4.464,-87.4472 1.488,-11.20767 1.488,2.21804 0.744,6.85048 1.488,22.61505 3.72,65.10288 1.488,12.3153 1.488,-1.41618 0.744,-4.74623 1.488,-16.15999 2.976,-38.69355 1.488,-10.16289 0.744,-2.09524 1.488,0.22406 1.488,1.2405 1.488,-1.56728 1.488,2.37064 0.744,5.74578 1.488,21.80702 2.232,36.04628 0.744,7.41719 0.744,3.73415 1.488,-3.52244 1.488,-16.44012 2.976,-39.75004 1.488,-10.86014 2.232,-9.68575 2.976,-8.07549 0.744,-1.90015 1.488,-0.90024 1.488,-2.67097 1.488,-10.76671 1.488,-10.69323 0.744,-1.50011 0.744,2.45728 0.744,6.87963 1.488,25.32681 2.976,58.56911 0.744,5.29605 0.744,-1.31911 0.744,-8.03292 1.488,-32.69795 2.976,-79.02586 1.488,-20.46693 0.744,-1.09612 0.744,5.72965 0.744,12.31994 1.488,40.84565 4.464,142.92149 1.488,22.81465 0.744,5.75133 0.744,2.9772 1.488,-3.98092 0.744,-9.9613 1.488,-37.68173 2.976,-91.91816 1.488,-24.91094 0.744,-5.37188 0.744,-1.86031 1.488,2.85403 1.488,7.85444 1.488,11.91121 1.488,18.20202 2.232,30.27667 1.488,14.22323 1.488,8.26892 1.488,2.78152 1.488,0.84953 1.488,-3.72943 0.744,-5.55318 1.488,-20.61354 1.488,-32.21626 4.464,-113.843703 1.488,-20.141638 0.744,-2.120428 0.744,4.682202 0.744,11.23528 1.488,36.107587 2.232,57.75799 1.488,25.25843 2.232,24.35664 1.488,10.24365 0.744,1.52362 1.488,-1.30286 0.744,-2.08998 0.744,-3.79067 1.488,-15.16222 2.232,-30.2585 0.744,-6.04314 0.744,-2.48551 0.744,1.29102 0.744,5.11122 1.488,20.22618 3.72,64.92923 0.744,7.75707 0.744,3.7673 1.488,-4.86038 1.488,-17.54351 1.488,-30.56327 0,0" + id="path4811-6" /> + style="fill:none;stroke:#aa0000;stroke-width:5.83715773;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-4-8)" + d="m 72,163.1451 0.744,-2.27452 1.488,3.69053 1.488,13.83684 1.488,22.76845 2.232,51.95737 1.488,34.5966 0.744,12.10493 0.744,6.4761 1.488,-4.01561 1.488,-17.60438 2.232,-39.1788 1.488,-24.68425 0.744,-7.10813 0.744,-1.58165 0.744,4.08897 3.72,41.4428 0.744,3.48985 0.744,1.4769 1.488,-4.884 1.488,-18.30227 2.976,-48.67307 0.744,-6.86559 0.744,-3.377 1.488,0.86929 1.488,4.36029 0.744,1.13793 1.488,-2.49878 0.744,-2.45691 0.744,-1.28143 0.744,1.2585 0.744,4.6037 1.488,19.62599 1.488,24.18953 0.744,7.91383 0.744,3.48058 1.488,-3.71225 2.232,-11.62052 0.744,-1.80422 1.488,1.17035 1.488,7.85088 2.232,11.21149 2.976,13.6754 1.488,-1.78885 1.488,-8.80563 1.488,-14.75642 2.976,-33.43367 4.464,-31.10787 0.744,-1.14349 0.744,3.96554 0.744,10.21771 1.488,36.2853 1.488,36.87935 0.744,11.54626 0.744,5.98157 1.488,-1.44933 1.488,-5.71535 1.488,0.71294 1.488,-1.19286 1.488,-8.29143 1.488,-7.20299 1.488,0.83889 0.744,3.72078 1.488,13.41834 1.488,13.31066 0.744,2.88616 1.488,-1.52697 0.744,-3.54994 0.744,-5.99536 1.488,-20.37847 2.232,-38.29982 0.744,-7.92207 0.744,-2.75328 0.744,1.8795 2.232,12.81364 0.744,1.03227 0.744,-1.72154 2.232,-12.82547 1.488,2.50925 2.976,22.54865 0.744,-1.25574 0.744,-5.65 2.976,-34.85934 1.488,4.88766 0.744,12.56426 1.488,42.25131 2.976,97.04178 0.744,12.48095 0.744,3.2372 0.744,-6.05124 1.488,-32.37885 1.488,-37.54203 1.488,-20.92636 0.744,-3.7474 0.744,-1.00758 1.488,1.46415 1.488,0.0151 0.744,-2.34547 0.744,-4.56184 1.488,-15.18759 1.488,-16.71724 0.744,-5.34978 0.744,-1.5619 0.744,2.84896 0.744,7.14781 1.488,23.91764 2.232,37.60059 0.744,3.53793 0.744,-4.05031 0.744,-11.05249 1.488,-37.68191 3.72,-119.49675 1.488,-26.478681 0.744,-6.897754 0.744,-3.11891 0.744,1.250277 0.744,6.99151 0.744,13.792158 1.488,47.30283 4.464,177.32434 1.488,33.14416 1.488,17.02368 0.744,2.96724 1.488,-5.51284 1.488,-21.4442 2.232,-41.10146 1.488,-16.05151 0.744,-3.94976 0.744,-1.39129 1.488,2.2463 1.488,-0.3758 0.744,-5.36849 1.488,-23.64804 2.232,-41.03902 1.488,-15.6666 1.488,-8.38273 1.488,5.61339 0.744,10.64396 1.488,34.68651 2.976,76.53698 0.744,9.90657 0.744,4.79924 1.488,-2.56891 0.744,-6.54618 1.488,-23.81025 2.232,-40.27649 0.744,-7.50548 0.744,-3.17536 1.488,2.37205 1.488,6.46522 2.232,10.65168 1.488,-3.60726 1.488,-14.64286 2.232,-24.27417 1.488,-9.22035 0.744,-2.77995 1.488,-0.14636 1.488,3.06904 0.744,-1.23541 0.744,-5.03374 1.488,-19.42625 1.488,-18.74288 0.744,-4.28695 1.488,6.28938 1.488,24.29036 3.72,74.23768 0.744,6.9658 0.744,2.43073 0.744,-1.42117 1.488,-8.96017 2.232,-14.48642 0.744,-1.16321 1.488,0.42834 0.744,-1.29693 0.744,-3.04055 3.72,-23.37479 0.744,-1.02771 0.744,1.55461 0.744,4.20746 1.488,15.48447 4.464,65.01346 0.744,3.1016 0.744,-2.14014 0.744,-8.58213 1.488,-36.71644 2.232,-69.06021 1.488,-29.90818 1.488,-16.47476 0.744,-2.88334 0.744,1.26266 0.744,5.61147 1.488,22.21736 3.72,68.78244 0.744,7.10172 0.744,3.52161 1.488,-2.8778 1.488,-8.405 0.744,-1.84613 0.744,1.64525 1.488,12.44408 1.488,13.28303 0.744,3.36649 1.488,-2.70589 1.488,-13.49759 3.72,-46.57487 4.464,-67.92097 0.744,-1.43634 0.744,3.41625 2.232,22.47254 2.232,22.12998 1.488,7.75163 1.488,4.7448 2.232,4.33509 1.488,5.89054 1.488,5.5381 1.488,-1.04093 0.744,-4.01282 1.488,-16.26713 1.488,-26.75233 2.976,-66.60945 0.744,-9.18019 0.744,-2.57555 0.744,4.67276 1.488,26.63495 2.976,60.57553 2.232,33.23359 1.488,11.55037 1.488,-4.40908 3.72,-38.79848 0.744,-2.37546 1.488,-0.91866 0.744,-2.30007 0.744,-5.56669 2.976,-33.59139 0.744,-2.90548 0.744,1.16037 1.488,10.94604 1.488,9.56728 0.744,1.78836 1.488,1.80099 0.744,2.10887 0.744,3.74749 1.488,13.03245 1.488,19.44637 2.232,33.25631 0.744,6.38156 0.744,1.3175 0.744,-4.56847 1.488,-24.44709 2.232,-40.3528 0.744,-7.59347 0.744,-3.82612 1.488,3.76905 2.232,14.45134 1.488,-0.90748 2.976,-13.16671 1.488,1.82187 2.232,10.12258 1.488,-3.02868 1.488,-14.67378 4.464,-58.70091 0.744,-4.0362 1.488,3.01815 1.488,14.64084 2.232,29.36065 1.488,10.75984 1.488,5.26404 0.744,1.07752 1.488,-2.26771 1.488,-4.07076 1.488,0.0972 0.744,3.57554 1.488,16.756 2.232,31.16986 1.488,9.29941 1.488,3.14758 1.488,2.73359 0.744,2.65369 0.744,4.75089 1.488,17.85895 2.232,34.39264 0.744,6.45355 0.744,2.43797 0.744,-1.47078 0.744,-5.10291 1.488,-18.98022 3.72,-58.41148 1.488,-10.50604 0.744,-1.42735 2.976,0.61828 0.744,1.35708 2.232,8.73947 2.232,9.13244 0.744,1.28294 1.488,-2.9375 2.976,-12.40827 1.488,2.80629 1.488,9.75622 3.72,32.55742 1.488,-0.96532 1.488,-9.5954 3.72,-29.85605 2.976,-22.32126 0.744,-2.60173 1.488,4.89718 1.488,19.46461 1.488,22.06141 0.744,6.24971 0,0" + id="path4951-8" /> + d="m 221.62685,407.65605 0.0939,-0.65022 0.2816,1.79837 0.28159,0.0478 0.28165,-3.35739 0.2816,0.91227 0.2816,-3.76504 0.28158,-3.8786 0.28159,0.31826 0.28161,-2.93969 0.28158,2.12976 0.28166,2.91161 0.28158,-3.66695 0.28159,-4.7815 0.2816,3.00418 0.2816,-0.45085 0.28158,3.36209 0.2816,2.38221 0.28165,-1.73022 0.28159,2.24655 0.28159,1.29211 0.2816,-1.02102 0.2816,1.63144 0.28159,0.0785 0.28159,-0.1715 0.28162,1.84832 0.28159,-1.45939 0.28163,2.20815 0.28159,-0.37677 0.28161,-3.22057 0.28159,-4.99985 0.2816,1.80392 0.2816,-0.72018 0.2816,3.61201 0.28159,-0.11021 0.2816,-1.01527 0.28159,-1.53574 0.28162,-2.34306 0.28163,-1.3386 0.28159,1.26217 0.28159,2.81306 0.2816,-3.5147 0.2816,3.15474 0.28158,-1.43211 0.28159,-1.35094 0.28166,-1.35476 0.28158,2.45653 0.2816,-2.25442 0.28159,0.79081 0.2816,-2.33768 0.28159,-1.27333 0.28165,0.0148 0.28159,-2.79969 0.28159,2.9606 0.28161,0.0434 0.28158,2.29501 0.28159,-1.20215 0.2816,1.99338 0.28165,0.25563 0.28159,0.88163 0.2816,1.88328 0.2816,2.48923 0.28159,4.27226 0.28159,-2.67399 0.28159,1.51852 0.28167,-3.91957 0.28157,0.84992 0.2816,0.79747 0.2816,1.65774 0.28159,3.25539 0.28161,-1.05149 0.28156,3.38362 0.28167,-2.56789 0.28158,1.42067 0.2816,-1.64776 0.28161,1.10117 0.28156,-2.48312 0.28161,-0.34185 0.28165,-1.85731 0.28159,-4.3439 0.2816,-1.22529 0.2816,-2.64647 0.28159,0.40261 0.28159,2.30316 0.2816,3.2617 0.28165,3.90675 0.28159,1.20758 0.2816,-1.13938 0.2816,1.87941 0.28157,1.22247 0.2816,0.94872 0.28159,-0.0297 0.28162,-0.67701 0.28164,-0.95907 0.28158,-2.79147 0.28159,-0.91337 0.2816,1.34442 0.2816,-0.80308 0.28161,-0.54616 0.28159,2.23518 0.28162,-0.55074 0.2816,-1.05092 0.28158,2.60261 0.2816,1.38289 0.2816,-0.87159 0.28165,0.8822 0.28158,-0.44717 0.28159,5.30316 0.2816,-2.34479 0.2816,-2.53624 0.28159,-0.16237 0.2816,2.55295 0.28163,-1.8579 0.28159,-2.12507 0.2816,1.95738 0.2816,-2.01679 0.2816,-0.71736 0.28159,-3.52876 0.28164,1.8732 0.2816,3.54882 0.28159,1.00207 0.2816,-1.72403 0.28159,-4.11831 0.2816,0.2098 0.28159,3.47671 0.28164,0.59952 0.28161,-0.53343 0.28158,0.92125 0.2816,-1.67633 0.28159,0.0624 0.2816,1.38527 0.28159,0.17587 0.28163,1.9884 0.2816,-3.42548 0.28159,1.03745 0.28161,-0.50363 0.2816,1.5986 0.28158,-1.11642 0.28158,1.67321 0.28166,0.7699 0.28165,1.66817 0.28154,1.33538 0.28163,0.65592 0.28155,2.48905 0.28161,-3.94096 0.28162,0.17908 0.28156,-2.71654 0.28166,1.49825 0.28154,-0.0782 0.28162,-2.04234 0.28165,-4.32406 0.28156,-5.58113 0.28163,-0.1008 0.28161,-1.93655 0.28154,1.11356 0.28162,0.72132 0.28156,0.17371 0.28164,-0.18938 0.28166,-0.45278 0.28158,-1.22887 0.28159,2.05886 0.2816,1.5996 0.28154,-0.0455 0.28165,1.28095 0.28155,-2.95794 0.28168,0.21413 0.2816,1.58616 0.2816,0.31646 0.28159,1.2566 0.28159,-0.99596 0.2816,-2.27948 0.2816,0.49983 0.28162,-2.23732 0.28161,0.89454 0.28159,1.64203 0.28159,-2.01425 0.2816,-0.37659 0.2816,-0.89502 0.28158,0.37718 0.28166,3.04772 0.28158,1.60405 0.28161,-0.97373 0.28159,1.24336 0.2816,-3.31343 0.28159,-0.72102 0.28163,0.43293 0.28161,1.1283 0.2816,0.31134 0.28158,0.6649 0.2816,1.21784 0.28161,2.15914 0.28157,2.51819 0.28164,2.14506 0.28161,3.90047 0.28159,-1.43452 0.28159,0.0936 0.28159,0.4546 0.28159,-1.28065 0.28159,-1.62624 0.28169,-1.51852 0.28156,-2.62505 0.2816,-1.73765 0.2816,1.05838 0.28158,-2.35868 0.28161,2.92043 0.28167,2.04159 0.28156,0.16252 0.28164,-0.85923 0.28154,-2.4334 0.28159,-1.72779 0.28159,0.76048 0.28162,-0.20882 0.28163,-0.47629 0.28154,-0.79049 0.28165,-0.6126 0.28163,-3.2259 0.28156,0.0224 0.28165,3.35336 0.28153,0.75558 0.2816,-0.16432 0.28164,-3.43003 0.28154,0.92457 0.28166,-2.55016 0.28164,1.07406 0.28154,-0.43445 0.28164,4.41529 0.2816,-1.69215 0.28158,-1.51447 0.28161,2.94899 0.28155,0.4539 0.28163,4.24396 0.28154,3.53664 0.2817,3.32528 0.2816,1.68069 0.2816,-1.09731 0.28158,-2.91577 0.28159,2.71904 0.28156,0.0598 0.28164,-0.83655 0.28165,1.69247 0.28159,-1.33135 0.28159,-1.69297 0.2816,-3.42546 0.28159,-2.60579 0.2816,0.79454 0.28159,-1.38682 0.28163,0.12447 0.2816,-0.50705 0.2816,-2.23453 0.28161,-0.16455 0.28157,-0.4598 0.28159,-2.07539 0.28165,-4.76536 0.2816,0.12337 0.2816,-4.92953 0.28157,-0.12442 0.2816,-4.40374 0.2816,0.72553 0.2816,-0.0916 0.28165,-1.80163 0.28159,0.85558 0.28158,-0.32571 0.2816,8.23979 0.2816,0.96718 0.2816,5.6372 0.2816,1.97274 0.28161,3.61144 0.28161,-0.0287 0.2816,2.1735 0.28159,0.14366 0.28159,-3.75557 0.28159,2.83995 0.28171,-1.70417 0.28153,0.46254 0.28164,1.68104 0.28155,-0.89553 0.28161,-0.94177 0.2816,0.62107 0.28158,-1.80433 0.28165,-0.83944 0.28164,4.58587 0.28154,-2.93818 0.28165,1.56933 0.28155,2.37223 0.28159,-1.96844 0.2816,4.09014 0.2816,-2.63006 0.28162,1.37943 0.28165,2.37555 0.28155,1.13013 0.28162,-1.16696 0.28156,1.72014 0.28165,-0.50155 0.28158,-3.36383 0.28155,1.27787 0.28166,-5.58999 0.28154,2.97148 0.28165,-0.46802 0.28164,-2.87843 0.28158,1.74327 0.28161,0.32919 0.28159,-0.68901 0.28156,-2.48547 0.28162,-0.0147 0.28155,0.30121 0.28165,4.05682 0.28164,-2.28124 0.2816,-0.375 0.28158,-0.0952 0.28159,-3.41577 0.2816,0.0962 0.28161,1.09725 0.28153,2.412 0.28169,3.83624 0.28159,-2.58409 0.2816,-1.94555 0.28159,1.16433 0.28159,-1.40041 0.2816,2.0661 0.28166,-0.14777 0.28159,0.90408 0.28158,1.49411 0.28159,-2.03815 0.28162,-3.51139 0.28158,-0.37599 0.2816,-3.20485 0.28163,3.63219 0.28159,-1.03756 0.28161,-0.90888 0.28159,0.66675 0.2816,-0.69521 0.28159,-3.14377 0.28161,-1.38854 0.28162,1.0004 0.28159,1.37037 0.28162,2.9271 0.28158,0.14336 0.28159,-0.2543 0.28159,-0.64014 0.28169,3.21864 0.28156,-1.9086 0.2816,4.05887 0.28158,2.19024 0.2816,0.96478 0.28161,-0.34531 0.28158,0.92651 0.28165,-2.99366 0.28162,-2.92969 0.28155,-0.11831 0.28161,-3.5794 0.2816,0.20556 0.28159,-3.47593 0.28159,2.07597 0.28159,-3.82994 0.28163,-3.3212 0.28165,0.15551 0.28156,2.21495 0.28162,4.35104 0.28156,0.34942 0.28159,1.97568 0.28164,-0.0424 0.28155,1.01362 0.28166,-1.09955 0.28163,1.62206 0.28151,5.40193 0.28169,-2.60877 0.2816,0.18115 0.28151,1.08384 0.28169,0.3366 0.28151,-0.38263 0.2816,0.6605 0.2816,-0.567 0.2816,-0.55418 0.28168,0.35738 0.2816,-0.87594 0.2816,0.97881 0.2816,-1.976 0.28151,-1.73457 0.28169,-4.55629 0.28151,1.15363 0.28169,-1.19391 0.2816,1.60667 0.2816,-1.5903 0.2816,-0.98102 0.2816,0.85377 0.2816,0.99845 0.28159,-2.84561 0.2816,1.42273 0.2816,7.9713 0.2816,-1.8932 0.2816,1.00488 0.2816,-0.56939 0.2816,-0.2043 0.2816,2.8425 0.2816,-2.32567 0.2816,-1.26198 0.2816,3.57549 0.2816,-2.66773 0.2816,-5.01292 0.2816,1.85462 0.28168,0.78671 0.2816,1.58928 0.2816,4.47557 0.28151,-1.30855 0.28169,-3.1434 0.28151,-0.87456 0.28169,1.52213 0.2816,2.11193 0.2816,4.58332 0.2816,-0.10076 0.20461,-1.93981" + clip-path="none" + style="fill:none;stroke:#aa4400;stroke-width:0.83733952;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" /> + style="fill:none;stroke:#aa0000;stroke-width:5.40178728;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-70-49)" + d="m 72,199.44014 0.744,-9.88505 0.744,-8.16304 1.488,-9.36337 1.488,1.87949 0.744,4.51209 1.488,14.91057 2.976,33.84172 0.744,4.07908 0.744,1.42438 2.232,-4.60906 1.488,-0.0347 1.488,4.87221 1.488,5.9514 0.744,1.30441 0.744,-1.102 1.488,-8.48149 1.488,-6.66913 1.488,1.49385 0.744,2.11321 1.488,-1.52428 2.976,-17.3357 0.744,1.00237 2.232,10.64968 1.488,-4.58749 1.488,-22.11071 2.232,-44.27705 0.744,-8.23399 0.744,-2.67097 0.744,1.68533 1.488,9.82042 1.488,15.06494 2.232,34.10097 1.488,19.55812 0.744,4.74052 1.488,-1.04466 2.232,-8.95528 0.744,-1.66071 1.488,1.57539 1.488,9.97787 2.976,25.15023 1.488,9.85045 1.488,16.87157 1.488,20.10934 0.744,4.06258 0.744,-3.2619 0.744,-10.54148 4.464,-88.92547 2.232,-7.57641 0.744,-6.78791 2.976,-42.28856 0.744,-3.70829 0.744,1.93631 1.488,16.51775 1.488,17.60432 0.744,4.60487 0.744,1.46299 1.488,-2.48511 2.232,-8.84485 1.488,3.58933 1.488,17.8884 2.232,44.1192 2.976,69.61286 1.488,16.66005 0.744,1.77475 0.744,-1.73947 0.744,-4.38392 2.232,-21.99984 1.488,-11.63998 2.232,-13.45344 2.232,-20.81468 3.72,-43.83979 0.744,-3.86105 1.488,3.98198 1.488,16.21543 2.232,25.67229 0.744,2.70765 0.744,-1.27609 1.488,-10.12523 1.488,-11.31521 0.744,-3.97331 0.744,-2.00057 1.488,4.1382 1.488,14.931 1.488,16.65152 0.744,3.72521 0.744,-1.59709 0.744,-6.92539 2.976,-47.18876 2.232,-32.9563 1.488,-10.49265 2.976,-7.78144 1.488,1.06199 1.488,10.11014 1.488,8.87559 0.744,1.15151 1.488,0.55343 0.744,1.482 2.232,7.63589 1.488,0.81241 1.488,3.5433 2.976,16.08709 1.488,0.11738 1.488,-4.45597 2.232,-8.75192 0.744,-1.69833 1.488,2.54425 0.744,6.25848 2.976,38.89904 0.744,1.44606 0.744,-3.91149 2.976,-30.90086 1.488,5.82012 2.232,20.64498 0.744,2.85098 0.744,1.52132 1.488,1.61142 0.744,1.6138 0.744,3.74111 2.232,18.7331 0.744,1.82262 0.744,-3.23004 1.488,-19.64156 1.488,-20.95621 0.744,-4.88296 1.488,5.26725 1.488,23.35075 3.72,78.19372 0.744,3.03636 0.744,-3.52594 1.488,-21.42585 5.208,-103.81978 0.744,-6.14833 1.488,3.44713 2.976,30.03524 0.744,3.76533 0.744,1.39758 1.488,-3.36539 0.744,-2.51763 0.744,-1.11263 1.488,3.98873 1.488,10.28867 2.232,16.39558 1.488,-2.65607 0.744,-8.48228 1.488,-29.39002 2.976,-64.39347 0.744,-10.22254 0.744,-5.58917 1.488,6.47734 1.488,29.06624 4.464,126.63805 1.488,19.96354 1.488,8.29161 2.976,7.72263 1.488,-2.20208 0.744,-6.24159 1.488,-23.49537 3.72,-83.10322 0.744,-6.23683 1.488,1.76487 2.232,8.76564 1.488,3.74779 1.488,-0.024 1.488,-3.13723 2.976,-2.18358 1.488,3.46051 2.232,12.14935 0.744,1.04711 0.744,-2.33626 0.744,-5.76889 1.488,-18.68947 2.976,-39.63108 0.744,-4.39655 1.488,5.15274 0.744,11.4399 1.488,40.38546 2.976,93.76422 0.744,13.18135 0.744,6.18746 1.488,-6.65651 1.488,-23.09303 2.232,-39.41937 0.744,-7.5832 0.744,-2.19151 0.744,3.37797 0.744,8.38648 1.488,28.30844 2.232,45.00689 0.744,6.46383 1.488,-7.85238 1.488,-29.28104 2.976,-66.0734 0.744,-9.19186 0.744,-3.97138 0.744,1.29966 0.744,5.71682 4.464,49.47802 3.72,29.2188 1.488,0.59528 0.744,-1.27976 0.744,-2.64938 0.744,-4.89588 0.744,-8.13954 1.488,-26.92991 2.976,-63.3791 1.488,-17.44058 1.488,-6.986 0.744,-2.45078 0.744,-3.75576 4.464,-32.09936 0.744,-1.62975 1.488,-1.46885 1.488,0.82394 0.744,3.15329 1.488,13.24986 3.72,41.72316 0.744,2.85103 1.488,-3.27108 0.744,-2.97088 1.488,2.46798 1.488,15.32221 2.232,27.13907 0.744,5.77709 0.744,1.85304 0.744,-2.94587 0.744,-7.76788 3.72,-58.37263 0.744,-1.83181 0.744,3.37185 2.976,23.05137 0.744,1.45875 0.744,-1.59848 0.744,-5.26625 1.488,-22.90961 3.72,-81.468298 0.744,-7.197684 1.488,6.014559 1.488,30.348323 2.976,69.63821 1.488,15.91528 2.232,10.94183 2.232,18.80032 1.488,15.65283 3.72,44.88057 0.744,1.58038 0.744,-2.28932 1.488,-12.70893 4.464,-54.32107 0.744,-2.68949 0.744,1.09828 0.744,4.68426 1.488,16.41455 2.232,25.92632 0.744,5.98774 0.744,3.10537 1.488,-6.11821 1.488,-19.78223 2.232,-32.4824 1.488,-12.87383 0.744,-3.53748 0.744,-1.52085 1.488,2.65488 0.744,4.96905 1.488,19.96561 3.72,64.27351 0.744,6.03854 0.744,2.11457 0.744,-2.40737 0.744,-6.5507 1.488,-21.65179 3.72,-62.98963 1.488,-13.82365 1.488,-8.53521 0.744,-2.44259 1.488,2.90333 1.488,15.66129 3.72,49.72553 1.488,13.21295 1.488,7.17468 1.488,-1.56624 0.744,-3.64211 1.488,-12.60594 2.232,-30.19264 2.232,-29.12852 2.976,-26.43417 2.232,-16.76631 1.488,-6.35095 1.488,2.60633 1.488,9.02587 0.744,5.30678 0,0" + id="path4671-4" /> + style="fill:none;stroke:#aa0000;stroke-width:5.78597641;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-1-6)" + d="m 72,195.32244 2.232,30.33622 5.208,82.6578 0.744,4.19735 1.488,-3.89026 1.488,-19.94804 2.976,-51.5742 1.488,-15.29076 0.744,-3.34251 1.488,3.00996 2.232,13.1893 1.488,-3.78718 1.488,-19.12934 2.232,-34.06841 0.744,-5.24672 1.488,8.23212 1.488,31.43735 2.232,57.23294 1.488,22.07497 0.744,4.115 1.488,-6.28659 1.488,-21.65355 2.232,-36.59323 0.744,-5.1408 1.488,6.30869 1.488,20.61486 2.232,33.49649 2.232,21.86923 1.488,11.09439 0.744,3.17876 1.488,-4.64149 0.744,-9.80413 1.488,-35.10837 5.208,-162.82504 0.744,-4.37728 0.744,2.85359 1.488,18.23989 2.232,26.41871 1.488,8.29516 1.488,-4.16282 1.488,-19.19336 2.976,-49.954665 1.488,-14.127951 0.744,-1.481978 0.744,3.186202 0.744,8.152323 1.488,30.070909 2.976,74.34825 1.488,22.21049 0.744,3.87797 1.488,-4.18932 1.488,-6.76813 0.744,1.1176 0.744,5.54956 2.976,38.1284 0.744,2.07031 0.744,-3.13193 0.744,-7.82764 3.72,-55.0724 0.744,-3.64768 1.488,5.30612 2.976,30.19848 3.72,40.74891 1.488,-6.08927 1.488,-29.84397 5.208,-135.548516 0.744,-4.439519 0.744,2.328802 1.488,18.799123 2.232,31.04385 0.744,3.24254 0.744,-1.35421 1.488,-7.8348 1.488,3.07954 0.744,8.87001 2.976,54.21293 0.744,4.45466 0.744,-3.83675 0.744,-12.36007 1.488,-44.02004 2.232,-67.458014 0.744,-9.807903 0.744,-1.235956 0.744,6.843274 1.488,33.963199 3.72,110.95534 1.488,30.78323 1.488,15.82699 0.744,1.05037 0.744,-2.92336 1.488,-15.42023 2.232,-29.10193 0.744,-4.13997 1.488,3.25476 1.488,14.02677 1.488,13.7527 0.744,3.5736 1.488,-0.0598 1.488,-6.03731 2.232,-11.29102 0.744,-1.71544 1.488,0.83206 1.488,2.74048 1.488,-2.35746 0.744,-4.38614 1.488,-15.79294 2.976,-37.65538 1.488,-9.18414 1.488,-0.397 1.488,0.5789 0.744,-2.07495 2.976,-12.27931 0.744,-1.07803 1.488,3.7658 0.744,7.07097 1.488,22.86684 3.72,62.29229 1.488,10.53592 1.488,-1.53356 0.744,-5.65925 1.488,-20.3937 2.976,-52.3496 0.744,-5.71969 0.744,-1.77482 1.488,2.13314 0.744,1.74903 0.744,2.64263 0.744,4.53901 1.488,16.37177 2.232,37.7415 2.976,57.56895 1.488,16.24963 0.744,4.00541 1.488,-1.54407 0.744,-6.06174 1.488,-22.64763 4.464,-86.76047 0.744,-5.34539 1.488,5.4348 1.488,24.24557 1.488,27.75599 0.744,9.07281 0.744,4.13125 1.488,-5.73562 2.232,-22.1705 0.744,-2.10783 0.744,1.90328 1.488,11.73428 2.232,23.86467 0.744,3.59592 1.488,-4.51945 1.488,-10.58806 1.488,-5.51951 0.744,-1.17679 1.488,0.73573 0.744,2.96847 1.488,11.1741 2.232,25.63503 2.232,28.94129 0.744,5.08943 1.488,-6.45851 1.488,-28.88379 4.464,-110.71625 1.488,-28.21869 1.488,-15.73324 0.744,-1.09211 0.744,4.4715 0.744,10.35307 1.488,34.85257 2.232,59.39486 1.488,25.73433 2.232,24.50853 1.488,11.94282 0.744,4.03252 0.744,1.53676 0.744,-2.10704 0.744,-6.61355 1.488,-25.57901 2.976,-58.38824 0.744,-7.79984 0.744,-3.28947 0.744,1.43053 0.744,6.04059 1.488,23.71545 2.232,42.4171 1.488,15.27687 0.744,1.92636 0.744,-1.1466 2.232,-8.88218 1.488,1.15132 1.488,7.77147 2.232,12.03415 0.744,1.33413 1.488,-2.49175 0.744,-3.71797 0.744,-6.00499 1.488,-21.23441 4.464,-87.4472 1.488,-11.20767 1.488,2.21804 0.744,6.85048 1.488,22.61505 3.72,65.10288 1.488,12.3153 1.488,-1.41618 0.744,-4.74623 1.488,-16.15999 2.976,-38.69355 1.488,-10.16289 0.744,-2.09524 1.488,0.22406 1.488,1.2405 1.488,-1.56728 1.488,2.37064 0.744,5.74578 1.488,21.80702 2.232,36.04628 0.744,7.41719 0.744,3.73415 1.488,-3.52244 1.488,-16.44012 2.976,-39.75004 1.488,-10.86014 2.232,-9.68575 2.976,-8.07549 0.744,-1.90015 1.488,-0.90024 1.488,-2.67097 1.488,-10.76671 1.488,-10.69323 0.744,-1.50011 0.744,2.45728 0.744,6.87963 1.488,25.32681 2.976,58.56911 0.744,5.29605 0.744,-1.31911 0.744,-8.03292 1.488,-32.69795 2.976,-79.02586 1.488,-20.46693 0.744,-1.09612 0.744,5.72965 0.744,12.31994 1.488,40.84565 4.464,142.92149 1.488,22.81465 0.744,5.75133 0.744,2.9772 1.488,-3.98092 0.744,-9.9613 1.488,-37.68173 2.976,-91.91816 1.488,-24.91094 0.744,-5.37188 0.744,-1.86031 1.488,2.85403 1.488,7.85444 1.488,11.91121 1.488,18.20202 2.232,30.27667 1.488,14.22323 1.488,8.26892 1.488,2.78152 1.488,0.84953 1.488,-3.72943 0.744,-5.55318 1.488,-20.61354 1.488,-32.21626 4.464,-113.843703 1.488,-20.141638 0.744,-2.120428 0.744,4.682202 0.744,11.23528 1.488,36.107587 2.232,57.75799 1.488,25.25843 2.232,24.35664 1.488,10.24365 0.744,1.52362 1.488,-1.30286 0.744,-2.08998 0.744,-3.79067 1.488,-15.16222 2.232,-30.2585 0.744,-6.04314 0.744,-2.48551 0.744,1.29102 0.744,5.11122 1.488,20.22618 3.72,64.92923 0.744,7.75707 0.744,3.7673 1.488,-4.86038 1.488,-17.54351 1.488,-30.56327 0,0" + id="path4811-8" /> + style="fill:none;stroke:#aa0000;stroke-width:5.83715773;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-4-9)" + d="m 72,163.1451 0.744,-2.27452 1.488,3.69053 1.488,13.83684 1.488,22.76845 2.232,51.95737 1.488,34.5966 0.744,12.10493 0.744,6.4761 1.488,-4.01561 1.488,-17.60438 2.232,-39.1788 1.488,-24.68425 0.744,-7.10813 0.744,-1.58165 0.744,4.08897 3.72,41.4428 0.744,3.48985 0.744,1.4769 1.488,-4.884 1.488,-18.30227 2.976,-48.67307 0.744,-6.86559 0.744,-3.377 1.488,0.86929 1.488,4.36029 0.744,1.13793 1.488,-2.49878 0.744,-2.45691 0.744,-1.28143 0.744,1.2585 0.744,4.6037 1.488,19.62599 1.488,24.18953 0.744,7.91383 0.744,3.48058 1.488,-3.71225 2.232,-11.62052 0.744,-1.80422 1.488,1.17035 1.488,7.85088 2.232,11.21149 2.976,13.6754 1.488,-1.78885 1.488,-8.80563 1.488,-14.75642 2.976,-33.43367 4.464,-31.10787 0.744,-1.14349 0.744,3.96554 0.744,10.21771 1.488,36.2853 1.488,36.87935 0.744,11.54626 0.744,5.98157 1.488,-1.44933 1.488,-5.71535 1.488,0.71294 1.488,-1.19286 1.488,-8.29143 1.488,-7.20299 1.488,0.83889 0.744,3.72078 1.488,13.41834 1.488,13.31066 0.744,2.88616 1.488,-1.52697 0.744,-3.54994 0.744,-5.99536 1.488,-20.37847 2.232,-38.29982 0.744,-7.92207 0.744,-2.75328 0.744,1.8795 2.232,12.81364 0.744,1.03227 0.744,-1.72154 2.232,-12.82547 1.488,2.50925 2.976,22.54865 0.744,-1.25574 0.744,-5.65 2.976,-34.85934 1.488,4.88766 0.744,12.56426 1.488,42.25131 2.976,97.04178 0.744,12.48095 0.744,3.2372 0.744,-6.05124 1.488,-32.37885 1.488,-37.54203 1.488,-20.92636 0.744,-3.7474 0.744,-1.00758 1.488,1.46415 1.488,0.0151 0.744,-2.34547 0.744,-4.56184 1.488,-15.18759 1.488,-16.71724 0.744,-5.34978 0.744,-1.5619 0.744,2.84896 0.744,7.14781 1.488,23.91764 2.232,37.60059 0.744,3.53793 0.744,-4.05031 0.744,-11.05249 1.488,-37.68191 3.72,-119.49675 1.488,-26.478681 0.744,-6.897754 0.744,-3.11891 0.744,1.250277 0.744,6.99151 0.744,13.792158 1.488,47.30283 4.464,177.32434 1.488,33.14416 1.488,17.02368 0.744,2.96724 1.488,-5.51284 1.488,-21.4442 2.232,-41.10146 1.488,-16.05151 0.744,-3.94976 0.744,-1.39129 1.488,2.2463 1.488,-0.3758 0.744,-5.36849 1.488,-23.64804 2.232,-41.03902 1.488,-15.6666 1.488,-8.38273 1.488,5.61339 0.744,10.64396 1.488,34.68651 2.976,76.53698 0.744,9.90657 0.744,4.79924 1.488,-2.56891 0.744,-6.54618 1.488,-23.81025 2.232,-40.27649 0.744,-7.50548 0.744,-3.17536 1.488,2.37205 1.488,6.46522 2.232,10.65168 1.488,-3.60726 1.488,-14.64286 2.232,-24.27417 1.488,-9.22035 0.744,-2.77995 1.488,-0.14636 1.488,3.06904 0.744,-1.23541 0.744,-5.03374 1.488,-19.42625 1.488,-18.74288 0.744,-4.28695 1.488,6.28938 1.488,24.29036 3.72,74.23768 0.744,6.9658 0.744,2.43073 0.744,-1.42117 1.488,-8.96017 2.232,-14.48642 0.744,-1.16321 1.488,0.42834 0.744,-1.29693 0.744,-3.04055 3.72,-23.37479 0.744,-1.02771 0.744,1.55461 0.744,4.20746 1.488,15.48447 4.464,65.01346 0.744,3.1016 0.744,-2.14014 0.744,-8.58213 1.488,-36.71644 2.232,-69.06021 1.488,-29.90818 1.488,-16.47476 0.744,-2.88334 0.744,1.26266 0.744,5.61147 1.488,22.21736 3.72,68.78244 0.744,7.10172 0.744,3.52161 1.488,-2.8778 1.488,-8.405 0.744,-1.84613 0.744,1.64525 1.488,12.44408 1.488,13.28303 0.744,3.36649 1.488,-2.70589 1.488,-13.49759 3.72,-46.57487 4.464,-67.92097 0.744,-1.43634 0.744,3.41625 2.232,22.47254 2.232,22.12998 1.488,7.75163 1.488,4.7448 2.232,4.33509 1.488,5.89054 1.488,5.5381 1.488,-1.04093 0.744,-4.01282 1.488,-16.26713 1.488,-26.75233 2.976,-66.60945 0.744,-9.18019 0.744,-2.57555 0.744,4.67276 1.488,26.63495 2.976,60.57553 2.232,33.23359 1.488,11.55037 1.488,-4.40908 3.72,-38.79848 0.744,-2.37546 1.488,-0.91866 0.744,-2.30007 0.744,-5.56669 2.976,-33.59139 0.744,-2.90548 0.744,1.16037 1.488,10.94604 1.488,9.56728 0.744,1.78836 1.488,1.80099 0.744,2.10887 0.744,3.74749 1.488,13.03245 1.488,19.44637 2.232,33.25631 0.744,6.38156 0.744,1.3175 0.744,-4.56847 1.488,-24.44709 2.232,-40.3528 0.744,-7.59347 0.744,-3.82612 1.488,3.76905 2.232,14.45134 1.488,-0.90748 2.976,-13.16671 1.488,1.82187 2.232,10.12258 1.488,-3.02868 1.488,-14.67378 4.464,-58.70091 0.744,-4.0362 1.488,3.01815 1.488,14.64084 2.232,29.36065 1.488,10.75984 1.488,5.26404 0.744,1.07752 1.488,-2.26771 1.488,-4.07076 1.488,0.0972 0.744,3.57554 1.488,16.756 2.232,31.16986 1.488,9.29941 1.488,3.14758 1.488,2.73359 0.744,2.65369 0.744,4.75089 1.488,17.85895 2.232,34.39264 0.744,6.45355 0.744,2.43797 0.744,-1.47078 0.744,-5.10291 1.488,-18.98022 3.72,-58.41148 1.488,-10.50604 0.744,-1.42735 2.976,0.61828 0.744,1.35708 2.232,8.73947 2.232,9.13244 0.744,1.28294 1.488,-2.9375 2.976,-12.40827 1.488,2.80629 1.488,9.75622 3.72,32.55742 1.488,-0.96532 1.488,-9.5954 3.72,-29.85605 2.976,-22.32126 0.744,-2.60173 1.488,4.89718 1.488,19.46461 1.488,22.06141 0.744,6.24971 0,0" + id="path4951-7" /> + style="fill:none;stroke:#aa0000;stroke-width:5.78597641;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-1-3-1)" + d="m 72,195.32244 2.232,30.33622 5.208,82.6578 0.744,4.19735 1.488,-3.89026 1.488,-19.94804 2.976,-51.5742 1.488,-15.29076 0.744,-3.34251 1.488,3.00996 2.232,13.1893 1.488,-3.78718 1.488,-19.12934 2.232,-34.06841 0.744,-5.24672 1.488,8.23212 1.488,31.43735 2.232,57.23294 1.488,22.07497 0.744,4.115 1.488,-6.28659 1.488,-21.65355 2.232,-36.59323 0.744,-5.1408 1.488,6.30869 1.488,20.61486 2.232,33.49649 2.232,21.86923 1.488,11.09439 0.744,3.17876 1.488,-4.64149 0.744,-9.80413 1.488,-35.10837 5.208,-162.82504 0.744,-4.37728 0.744,2.85359 1.488,18.23989 2.232,26.41871 1.488,8.29516 1.488,-4.16282 1.488,-19.19336 2.976,-49.954665 1.488,-14.127951 0.744,-1.481978 0.744,3.186202 0.744,8.152323 1.488,30.070909 2.976,74.34825 1.488,22.21049 0.744,3.87797 1.488,-4.18932 1.488,-6.76813 0.744,1.1176 0.744,5.54956 2.976,38.1284 0.744,2.07031 0.744,-3.13193 0.744,-7.82764 3.72,-55.0724 0.744,-3.64768 1.488,5.30612 2.976,30.19848 3.72,40.74891 1.488,-6.08927 1.488,-29.84397 5.208,-135.548516 0.744,-4.439519 0.744,2.328802 1.488,18.799123 2.232,31.04385 0.744,3.24254 0.744,-1.35421 1.488,-7.8348 1.488,3.07954 0.744,8.87001 2.976,54.21293 0.744,4.45466 0.744,-3.83675 0.744,-12.36007 1.488,-44.02004 2.232,-67.458014 0.744,-9.807903 0.744,-1.235956 0.744,6.843274 1.488,33.963199 3.72,110.95534 1.488,30.78323 1.488,15.82699 0.744,1.05037 0.744,-2.92336 1.488,-15.42023 2.232,-29.10193 0.744,-4.13997 1.488,3.25476 1.488,14.02677 1.488,13.7527 0.744,3.5736 1.488,-0.0598 1.488,-6.03731 2.232,-11.29102 0.744,-1.71544 1.488,0.83206 1.488,2.74048 1.488,-2.35746 0.744,-4.38614 1.488,-15.79294 2.976,-37.65538 1.488,-9.18414 1.488,-0.397 1.488,0.5789 0.744,-2.07495 2.976,-12.27931 0.744,-1.07803 1.488,3.7658 0.744,7.07097 1.488,22.86684 3.72,62.29229 1.488,10.53592 1.488,-1.53356 0.744,-5.65925 1.488,-20.3937 2.976,-52.3496 0.744,-5.71969 0.744,-1.77482 1.488,2.13314 0.744,1.74903 0.744,2.64263 0.744,4.53901 1.488,16.37177 2.232,37.7415 2.976,57.56895 1.488,16.24963 0.744,4.00541 1.488,-1.54407 0.744,-6.06174 1.488,-22.64763 4.464,-86.76047 0.744,-5.34539 1.488,5.4348 1.488,24.24557 1.488,27.75599 0.744,9.07281 0.744,4.13125 1.488,-5.73562 2.232,-22.1705 0.744,-2.10783 0.744,1.90328 1.488,11.73428 2.232,23.86467 0.744,3.59592 1.488,-4.51945 1.488,-10.58806 1.488,-5.51951 0.744,-1.17679 1.488,0.73573 0.744,2.96847 1.488,11.1741 2.232,25.63503 2.232,28.94129 0.744,5.08943 1.488,-6.45851 1.488,-28.88379 4.464,-110.71625 1.488,-28.21869 1.488,-15.73324 0.744,-1.09211 0.744,4.4715 0.744,10.35307 1.488,34.85257 2.232,59.39486 1.488,25.73433 2.232,24.50853 1.488,11.94282 0.744,4.03252 0.744,1.53676 0.744,-2.10704 0.744,-6.61355 1.488,-25.57901 2.976,-58.38824 0.744,-7.79984 0.744,-3.28947 0.744,1.43053 0.744,6.04059 1.488,23.71545 2.232,42.4171 1.488,15.27687 0.744,1.92636 0.744,-1.1466 2.232,-8.88218 1.488,1.15132 1.488,7.77147 2.232,12.03415 0.744,1.33413 1.488,-2.49175 0.744,-3.71797 0.744,-6.00499 1.488,-21.23441 4.464,-87.4472 1.488,-11.20767 1.488,2.21804 0.744,6.85048 1.488,22.61505 3.72,65.10288 1.488,12.3153 1.488,-1.41618 0.744,-4.74623 1.488,-16.15999 2.976,-38.69355 1.488,-10.16289 0.744,-2.09524 1.488,0.22406 1.488,1.2405 1.488,-1.56728 1.488,2.37064 0.744,5.74578 1.488,21.80702 2.232,36.04628 0.744,7.41719 0.744,3.73415 1.488,-3.52244 1.488,-16.44012 2.976,-39.75004 1.488,-10.86014 2.232,-9.68575 2.976,-8.07549 0.744,-1.90015 1.488,-0.90024 1.488,-2.67097 1.488,-10.76671 1.488,-10.69323 0.744,-1.50011 0.744,2.45728 0.744,6.87963 1.488,25.32681 2.976,58.56911 0.744,5.29605 0.744,-1.31911 0.744,-8.03292 1.488,-32.69795 2.976,-79.02586 1.488,-20.46693 0.744,-1.09612 0.744,5.72965 0.744,12.31994 1.488,40.84565 4.464,142.92149 1.488,22.81465 0.744,5.75133 0.744,2.9772 1.488,-3.98092 0.744,-9.9613 1.488,-37.68173 2.976,-91.91816 1.488,-24.91094 0.744,-5.37188 0.744,-1.86031 1.488,2.85403 1.488,7.85444 1.488,11.91121 1.488,18.20202 2.232,30.27667 1.488,14.22323 1.488,8.26892 1.488,2.78152 1.488,0.84953 1.488,-3.72943 0.744,-5.55318 1.488,-20.61354 1.488,-32.21626 4.464,-113.843703 1.488,-20.141638 0.744,-2.120428 0.744,4.682202 0.744,11.23528 1.488,36.107587 2.232,57.75799 1.488,25.25843 2.232,24.35664 1.488,10.24365 0.744,1.52362 1.488,-1.30286 0.744,-2.08998 0.744,-3.79067 1.488,-15.16222 2.232,-30.2585 0.744,-6.04314 0.744,-2.48551 0.744,1.29102 0.744,5.11122 1.488,20.22618 3.72,64.92923 0.744,7.75707 0.744,3.7673 1.488,-4.86038 1.488,-17.54351 1.488,-30.56327 0,0" + id="path4811-6-2" /> + d="m 374.21356,205.81779 0.0939,-0.65022 0.28159,1.79838 0.2816,0.0478 0.28164,-3.3574 0.2816,0.91227 0.2816,-3.76504 0.28158,-3.87859 0.28159,0.31825 0.28161,-2.93969 0.28158,2.12976 0.28166,2.91161 0.28159,-3.66694 0.28159,-4.78151 0.2816,3.00419 0.28159,-0.45085 0.28159,3.36209 0.2816,2.3822 0.28165,-1.73021 0.28159,2.24655 0.28159,1.29211 0.2816,-1.02102 0.2816,1.63143 0.28159,0.0785 0.28159,-0.17149 0.28162,1.84832 0.28159,-1.45939 0.28162,2.20815 0.28159,-0.37677 0.28161,-3.22058 0.28159,-4.99985 0.28161,1.80393 0.2816,-0.72019 0.2816,3.61202 0.28159,-0.11021 0.2816,-1.01527 0.28158,-1.53574 0.28163,-2.34306 0.28162,-1.3386 0.28159,1.26216 0.28159,2.81307 0.2816,-3.5147 0.2816,3.15474 0.28158,-1.43211 0.28159,-1.35095 0.28166,-1.35476 0.28159,2.45654 0.28159,-2.25443 0.2816,0.79081 0.28159,-2.33768 0.2816,-1.27332 0.28165,0.0148 0.28159,-2.79968 0.28159,2.96059 0.28161,0.0434 0.28158,2.29501 0.28159,-1.20215 0.2816,1.99338 0.28164,0.25563 0.28159,0.88164 0.28161,1.88327 0.28159,2.48923 0.2816,4.27227 0.28158,-2.67399 0.28159,1.51851 0.28167,-3.91957 0.28157,0.84992 0.2816,0.79747 0.2816,1.65774 0.28159,3.25539 0.28161,-1.05149 0.28157,3.38362 0.28167,-2.56789 0.28158,1.42067 0.2816,-1.64775 0.2816,1.10117 0.28157,-2.48312 0.28161,-0.34186 0.28164,-1.85731 0.28159,-4.3439 0.28161,-1.22529 0.2816,-2.64647 0.28158,0.40261 0.28159,2.30317 0.2816,3.26169 0.28165,3.90676 0.28159,1.20757 0.2816,-1.13937 0.2816,1.8794 0.28157,1.22247 0.2816,0.94872 0.28159,-0.0297 0.28162,-0.677 0.28164,-0.95908 0.28159,-2.79146 0.28159,-0.91337 0.2816,1.34442 0.2816,-0.80308 0.2816,-0.54616 0.28159,2.23517 0.28163,-0.55074 0.28159,-1.05092 0.28158,2.60262 0.2816,1.38289 0.2816,-0.87159 0.28165,0.88219 0.28158,-0.44716 0.28159,5.30315 0.2816,-2.34479 0.2816,-2.53623 0.28159,-0.16237 0.2816,2.55295 0.28164,-1.8579 0.28159,-2.12507 0.2816,1.95738 0.2816,-2.0168 0.2816,-0.71736 0.28159,-3.52875 0.28163,1.87319 0.2816,3.54883 0.2816,1.00207 0.28159,-1.72404 0.28159,-4.11831 0.2816,0.2098 0.28159,3.47672 0.28164,0.59952 0.28161,-0.53343 0.28158,0.92125 0.2816,-1.67634 0.28159,0.0624 0.2816,1.38527 0.28159,0.17587 0.28164,1.98841 0.2816,-3.42549 0.28159,1.03746 0.28161,-0.50364 0.2816,1.59861 0.28158,-1.11643 0.28158,1.67321 0.28165,0.7699 0.28165,1.66817 0.28155,1.33538 0.28162,0.65592 0.28155,2.48906 0.28161,-3.94096 0.28163,0.17908 0.28155,-2.71655 0.28166,1.49825 0.28155,-0.0782 0.28162,-2.04234 0.28165,-4.32407 0.28155,-5.58112 0.28164,-0.10081 0.2816,-1.93655 0.28154,1.11356 0.28163,0.72133 0.28155,0.1737 0.28165,-0.18937 0.28166,-0.45278 0.28157,-1.22888 0.28159,2.05886 0.28161,1.59961 0.28154,-0.0455 0.28165,1.28095 0.28155,-2.95794 0.28167,0.21413 0.2816,1.58616 0.2816,0.31646 0.28159,1.25661 0.28159,-0.99596 0.2816,-2.27949 0.2816,0.49983 0.28163,-2.23732 0.28161,0.89454 0.28159,1.64203 0.28159,-2.01425 0.2816,-0.37659 0.2816,-0.89502 0.28158,0.37718 0.28165,3.04772 0.28158,1.60406 0.28161,-0.97374 0.28159,1.24336 0.28161,-3.31343 0.28158,-0.72102 0.28163,0.43293 0.28161,1.1283 0.2816,0.31134 0.28158,0.6649 0.2816,1.21784 0.28161,2.15914 0.28157,2.51819 0.28165,2.14506 0.28161,3.90047 0.28159,-1.43452 0.28159,0.0936 0.28159,0.45461 0.28159,-1.28066 0.28159,-1.62624 0.28169,-1.51851 0.28155,-2.62506 0.2816,-1.73765 0.2816,1.05838 0.28158,-2.35868 0.28161,2.92044 0.28168,2.04158 0.28155,0.16252 0.28165,-0.85923 0.28153,-2.4334 0.2816,-1.72778 0.28159,0.76047 0.28161,-0.20882 0.28164,-0.47629 0.28154,-0.79049 0.28165,-0.6126 0.28162,-3.2259 0.28157,0.0224 0.28165,3.35336 0.28153,0.75558 0.2816,-0.16432 0.28163,-3.43003 0.28154,0.92457 0.28166,-2.55016 0.28164,1.07406 0.28154,-0.43445 0.28164,4.41529 0.2816,-1.69215 0.28159,-1.51447 0.2816,2.94899 0.28155,0.4539 0.28164,4.24397 0.28153,3.53664 0.28171,3.32527 0.2816,1.68069 0.2816,-1.09731 0.28158,-2.91576 0.28158,2.71903 0.28156,0.0598 0.28165,-0.83654 0.28164,1.69247 0.28159,-1.33135 0.28159,-1.69297 0.2816,-3.42546 0.28159,-2.60579 0.28161,0.79454 0.28158,-1.38682 0.28164,0.12447 0.28159,-0.50705 0.2816,-2.23453 0.28161,-0.16455 0.28158,-0.4598 0.28159,-2.07539 0.28165,-4.76536 0.2816,0.12337 0.2816,-4.92953 0.28157,-0.12442 0.2816,-4.40374 0.2816,0.72553 0.2816,-0.0916 0.28165,-1.80163 0.28159,0.85557 0.28157,-0.32571 0.2816,8.23979 0.2816,0.96718 0.2816,5.63721 0.28161,1.97273 0.28161,3.61145 0.28161,-0.0287 0.28159,2.1735 0.2816,0.14366 0.28159,-3.75557 0.28159,2.83995 0.2817,-1.70417 0.28154,0.46254 0.28163,1.68104 0.28156,-0.89553 0.28161,-0.94177 0.2816,0.62107 0.28158,-1.80433 0.28164,-0.83944 0.28164,4.58587 0.28154,-2.93818 0.28165,1.56933 0.28155,2.37223 0.28159,-1.96844 0.2816,4.09014 0.2816,-2.63005 0.28163,1.37942 0.28165,2.37555 0.28154,1.13013 0.28163,-1.16696 0.28156,1.72014 0.28168,-0.50155 0.28151,-3.36383 0.2816,1.27787 0.28169,-5.58998 0.28151,2.97147 0.2816,-0.46802 0.28169,-2.87842 0.2816,1.74326 0.2816,0.32919 0.2816,-0.68901 0.28151,-2.48547 0.2816,-0.0147 0.2816,0.30121 0.2816,4.05682 0.28168,-2.28124 0.2816,-0.375 0.2816,-0.0952 0.2816,-3.41578 0.2816,0.0962 0.2816,1.09725 0.28151,2.41201 0.28169,3.83624 0.2816,-2.5841 0.2816,-1.94554 0.2816,1.16432 0.2816,-1.40041 0.28159,2.06611 0.2816,-0.14777 0.2816,0.90408 0.2816,1.49411 0.2816,-2.03815 0.2816,-3.5114 0.2816,-0.37598 0.2816,-3.20485 0.2816,3.63218 0.2816,-1.03755 0.2816,-0.90888 0.2816,0.66675 0.2816,-0.69521 0.2816,-3.14377 0.2816,-1.38855 0.2816,1.00041 0.28159,1.37037 0.28169,2.9271 0.28151,0.14336 0.2816,-0.25431 0.2816,-0.64014 0.28169,3.21865 0.2816,-1.90861 0.2816,4.05888 0.2816,2.19024 0.2816,0.96477 0.28159,-0.3453 0.28152,0.9265 0.28168,-2.99365 0.2816,-2.92969 0.2816,-0.11831 0.2816,-3.57941 0.2816,0.20557 0.2816,-3.47593 0.2816,2.07597 0.2816,-3.82994 0.2816,-3.3212 0.2816,0.15551 0.2816,2.21494 0.2816,4.35104 0.2816,0.34943 0.28159,1.97567 0.2816,-0.0424 0.2816,1.01362 0.2816,-1.09955 0.28169,1.62206 0.28151,5.40193 0.28169,-2.60877 0.2816,0.18115 0.28151,1.08383 0.28169,0.33661 0.28151,-0.38263 0.2816,0.66049 0.2816,-0.56699 0.2816,-0.55418 0.28168,0.35738 0.2816,-0.87595 0.2816,0.97882 0.2816,-1.976 0.28151,-1.73458 0.28169,-4.55629 0.28151,1.15364 0.28169,-1.19391 0.2816,1.60667 0.2816,-1.59031 0.2816,-0.98102 0.2816,0.85378 0.28159,0.99844 0.2816,-2.8456 0.2816,1.42273 0.2816,7.9713 0.2816,-1.8932 0.2816,1.00487 0.2816,-0.56938 0.2816,-0.2043 0.2816,2.8425 0.2816,-2.32567 0.2816,-1.26198 0.2816,3.57549 0.2816,-2.66773 0.2816,-5.01292 0.2816,1.85462 0.28168,0.78671 0.2816,1.58927 0.2816,4.47558 0.28151,-1.30855 0.28169,-3.1434 0.28151,-0.87456 0.28169,1.52212 0.2816,2.11194 0.2816,4.58332 0.2816,-0.10076 0.20461,-1.93981" + clip-path="none" + style="fill:none;stroke:#aa4400;stroke-width:0.83733952;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" /> + style="fill:none;stroke:#aa0000;stroke-width:5.83715773;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-4-9-2)" + d="m 72,163.1451 0.744,-2.27452 1.488,3.69053 1.488,13.83684 1.488,22.76845 2.232,51.95737 1.488,34.5966 0.744,12.10493 0.744,6.4761 1.488,-4.01561 1.488,-17.60438 2.232,-39.1788 1.488,-24.68425 0.744,-7.10813 0.744,-1.58165 0.744,4.08897 3.72,41.4428 0.744,3.48985 0.744,1.4769 1.488,-4.884 1.488,-18.30227 2.976,-48.67307 0.744,-6.86559 0.744,-3.377 1.488,0.86929 1.488,4.36029 0.744,1.13793 1.488,-2.49878 0.744,-2.45691 0.744,-1.28143 0.744,1.2585 0.744,4.6037 1.488,19.62599 1.488,24.18953 0.744,7.91383 0.744,3.48058 1.488,-3.71225 2.232,-11.62052 0.744,-1.80422 1.488,1.17035 1.488,7.85088 2.232,11.21149 2.976,13.6754 1.488,-1.78885 1.488,-8.80563 1.488,-14.75642 2.976,-33.43367 4.464,-31.10787 0.744,-1.14349 0.744,3.96554 0.744,10.21771 1.488,36.2853 1.488,36.87935 0.744,11.54626 0.744,5.98157 1.488,-1.44933 1.488,-5.71535 1.488,0.71294 1.488,-1.19286 1.488,-8.29143 1.488,-7.20299 1.488,0.83889 0.744,3.72078 1.488,13.41834 1.488,13.31066 0.744,2.88616 1.488,-1.52697 0.744,-3.54994 0.744,-5.99536 1.488,-20.37847 2.232,-38.29982 0.744,-7.92207 0.744,-2.75328 0.744,1.8795 2.232,12.81364 0.744,1.03227 0.744,-1.72154 2.232,-12.82547 1.488,2.50925 2.976,22.54865 0.744,-1.25574 0.744,-5.65 2.976,-34.85934 1.488,4.88766 0.744,12.56426 1.488,42.25131 2.976,97.04178 0.744,12.48095 0.744,3.2372 0.744,-6.05124 1.488,-32.37885 1.488,-37.54203 1.488,-20.92636 0.744,-3.7474 0.744,-1.00758 1.488,1.46415 1.488,0.0151 0.744,-2.34547 0.744,-4.56184 1.488,-15.18759 1.488,-16.71724 0.744,-5.34978 0.744,-1.5619 0.744,2.84896 0.744,7.14781 1.488,23.91764 2.232,37.60059 0.744,3.53793 0.744,-4.05031 0.744,-11.05249 1.488,-37.68191 3.72,-119.49675 1.488,-26.478681 0.744,-6.897754 0.744,-3.11891 0.744,1.250277 0.744,6.99151 0.744,13.792158 1.488,47.30283 4.464,177.32434 1.488,33.14416 1.488,17.02368 0.744,2.96724 1.488,-5.51284 1.488,-21.4442 2.232,-41.10146 1.488,-16.05151 0.744,-3.94976 0.744,-1.39129 1.488,2.2463 1.488,-0.3758 0.744,-5.36849 1.488,-23.64804 2.232,-41.03902 1.488,-15.6666 1.488,-8.38273 1.488,5.61339 0.744,10.64396 1.488,34.68651 2.976,76.53698 0.744,9.90657 0.744,4.79924 1.488,-2.56891 0.744,-6.54618 1.488,-23.81025 2.232,-40.27649 0.744,-7.50548 0.744,-3.17536 1.488,2.37205 1.488,6.46522 2.232,10.65168 1.488,-3.60726 1.488,-14.64286 2.232,-24.27417 1.488,-9.22035 0.744,-2.77995 1.488,-0.14636 1.488,3.06904 0.744,-1.23541 0.744,-5.03374 1.488,-19.42625 1.488,-18.74288 0.744,-4.28695 1.488,6.28938 1.488,24.29036 3.72,74.23768 0.744,6.9658 0.744,2.43073 0.744,-1.42117 1.488,-8.96017 2.232,-14.48642 0.744,-1.16321 1.488,0.42834 0.744,-1.29693 0.744,-3.04055 3.72,-23.37479 0.744,-1.02771 0.744,1.55461 0.744,4.20746 1.488,15.48447 4.464,65.01346 0.744,3.1016 0.744,-2.14014 0.744,-8.58213 1.488,-36.71644 2.232,-69.06021 1.488,-29.90818 1.488,-16.47476 0.744,-2.88334 0.744,1.26266 0.744,5.61147 1.488,22.21736 3.72,68.78244 0.744,7.10172 0.744,3.52161 1.488,-2.8778 1.488,-8.405 0.744,-1.84613 0.744,1.64525 1.488,12.44408 1.488,13.28303 0.744,3.36649 1.488,-2.70589 1.488,-13.49759 3.72,-46.57487 4.464,-67.92097 0.744,-1.43634 0.744,3.41625 2.232,22.47254 2.232,22.12998 1.488,7.75163 1.488,4.7448 2.232,4.33509 1.488,5.89054 1.488,5.5381 1.488,-1.04093 0.744,-4.01282 1.488,-16.26713 1.488,-26.75233 2.976,-66.60945 0.744,-9.18019 0.744,-2.57555 0.744,4.67276 1.488,26.63495 2.976,60.57553 2.232,33.23359 1.488,11.55037 1.488,-4.40908 3.72,-38.79848 0.744,-2.37546 1.488,-0.91866 0.744,-2.30007 0.744,-5.56669 2.976,-33.59139 0.744,-2.90548 0.744,1.16037 1.488,10.94604 1.488,9.56728 0.744,1.78836 1.488,1.80099 0.744,2.10887 0.744,3.74749 1.488,13.03245 1.488,19.44637 2.232,33.25631 0.744,6.38156 0.744,1.3175 0.744,-4.56847 1.488,-24.44709 2.232,-40.3528 0.744,-7.59347 0.744,-3.82612 1.488,3.76905 2.232,14.45134 1.488,-0.90748 2.976,-13.16671 1.488,1.82187 2.232,10.12258 1.488,-3.02868 1.488,-14.67378 4.464,-58.70091 0.744,-4.0362 1.488,3.01815 1.488,14.64084 2.232,29.36065 1.488,10.75984 1.488,5.26404 0.744,1.07752 1.488,-2.26771 1.488,-4.07076 1.488,0.0972 0.744,3.57554 1.488,16.756 2.232,31.16986 1.488,9.29941 1.488,3.14758 1.488,2.73359 0.744,2.65369 0.744,4.75089 1.488,17.85895 2.232,34.39264 0.744,6.45355 0.744,2.43797 0.744,-1.47078 0.744,-5.10291 1.488,-18.98022 3.72,-58.41148 1.488,-10.50604 0.744,-1.42735 2.976,0.61828 0.744,1.35708 2.232,8.73947 2.232,9.13244 0.744,1.28294 1.488,-2.9375 2.976,-12.40827 1.488,2.80629 1.488,9.75622 3.72,32.55742 1.488,-0.96532 1.488,-9.5954 3.72,-29.85605 2.976,-22.32126 0.744,-2.60173 1.488,4.89718 1.488,19.46461 1.488,22.06141 0.744,6.24971 0,0" + id="path4951-7-6" /> - + + style="fill:none;stroke:#aa0000;stroke-width:5.78597641;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-1-3-1-9)" + d="m 72,195.32244 2.232,30.33622 5.208,82.6578 0.744,4.19735 1.488,-3.89026 1.488,-19.94804 2.976,-51.5742 1.488,-15.29076 0.744,-3.34251 1.488,3.00996 2.232,13.1893 1.488,-3.78718 1.488,-19.12934 2.232,-34.06841 0.744,-5.24672 1.488,8.23212 1.488,31.43735 2.232,57.23294 1.488,22.07497 0.744,4.115 1.488,-6.28659 1.488,-21.65355 2.232,-36.59323 0.744,-5.1408 1.488,6.30869 1.488,20.61486 2.232,33.49649 2.232,21.86923 1.488,11.09439 0.744,3.17876 1.488,-4.64149 0.744,-9.80413 1.488,-35.10837 5.208,-162.82504 0.744,-4.37728 0.744,2.85359 1.488,18.23989 2.232,26.41871 1.488,8.29516 1.488,-4.16282 1.488,-19.19336 2.976,-49.954665 1.488,-14.127951 0.744,-1.481978 0.744,3.186202 0.744,8.152323 1.488,30.070909 2.976,74.34825 1.488,22.21049 0.744,3.87797 1.488,-4.18932 1.488,-6.76813 0.744,1.1176 0.744,5.54956 2.976,38.1284 0.744,2.07031 0.744,-3.13193 0.744,-7.82764 3.72,-55.0724 0.744,-3.64768 1.488,5.30612 2.976,30.19848 3.72,40.74891 1.488,-6.08927 1.488,-29.84397 5.208,-135.548516 0.744,-4.439519 0.744,2.328802 1.488,18.799123 2.232,31.04385 0.744,3.24254 0.744,-1.35421 1.488,-7.8348 1.488,3.07954 0.744,8.87001 2.976,54.21293 0.744,4.45466 0.744,-3.83675 0.744,-12.36007 1.488,-44.02004 2.232,-67.458014 0.744,-9.807903 0.744,-1.235956 0.744,6.843274 1.488,33.963199 3.72,110.95534 1.488,30.78323 1.488,15.82699 0.744,1.05037 0.744,-2.92336 1.488,-15.42023 2.232,-29.10193 0.744,-4.13997 1.488,3.25476 1.488,14.02677 1.488,13.7527 0.744,3.5736 1.488,-0.0598 1.488,-6.03731 2.232,-11.29102 0.744,-1.71544 1.488,0.83206 1.488,2.74048 1.488,-2.35746 0.744,-4.38614 1.488,-15.79294 2.976,-37.65538 1.488,-9.18414 1.488,-0.397 1.488,0.5789 0.744,-2.07495 2.976,-12.27931 0.744,-1.07803 1.488,3.7658 0.744,7.07097 1.488,22.86684 3.72,62.29229 1.488,10.53592 1.488,-1.53356 0.744,-5.65925 1.488,-20.3937 2.976,-52.3496 0.744,-5.71969 0.744,-1.77482 1.488,2.13314 0.744,1.74903 0.744,2.64263 0.744,4.53901 1.488,16.37177 2.232,37.7415 2.976,57.56895 1.488,16.24963 0.744,4.00541 1.488,-1.54407 0.744,-6.06174 1.488,-22.64763 4.464,-86.76047 0.744,-5.34539 1.488,5.4348 1.488,24.24557 1.488,27.75599 0.744,9.07281 0.744,4.13125 1.488,-5.73562 2.232,-22.1705 0.744,-2.10783 0.744,1.90328 1.488,11.73428 2.232,23.86467 0.744,3.59592 1.488,-4.51945 1.488,-10.58806 1.488,-5.51951 0.744,-1.17679 1.488,0.73573 0.744,2.96847 1.488,11.1741 2.232,25.63503 2.232,28.94129 0.744,5.08943 1.488,-6.45851 1.488,-28.88379 4.464,-110.71625 1.488,-28.21869 1.488,-15.73324 0.744,-1.09211 0.744,4.4715 0.744,10.35307 1.488,34.85257 2.232,59.39486 1.488,25.73433 2.232,24.50853 1.488,11.94282 0.744,4.03252 0.744,1.53676 0.744,-2.10704 0.744,-6.61355 1.488,-25.57901 2.976,-58.38824 0.744,-7.79984 0.744,-3.28947 0.744,1.43053 0.744,6.04059 1.488,23.71545 2.232,42.4171 1.488,15.27687 0.744,1.92636 0.744,-1.1466 2.232,-8.88218 1.488,1.15132 1.488,7.77147 2.232,12.03415 0.744,1.33413 1.488,-2.49175 0.744,-3.71797 0.744,-6.00499 1.488,-21.23441 4.464,-87.4472 1.488,-11.20767 1.488,2.21804 0.744,6.85048 1.488,22.61505 3.72,65.10288 1.488,12.3153 1.488,-1.41618 0.744,-4.74623 1.488,-16.15999 2.976,-38.69355 1.488,-10.16289 0.744,-2.09524 1.488,0.22406 1.488,1.2405 1.488,-1.56728 1.488,2.37064 0.744,5.74578 1.488,21.80702 2.232,36.04628 0.744,7.41719 0.744,3.73415 1.488,-3.52244 1.488,-16.44012 2.976,-39.75004 1.488,-10.86014 2.232,-9.68575 2.976,-8.07549 0.744,-1.90015 1.488,-0.90024 1.488,-2.67097 1.488,-10.76671 1.488,-10.69323 0.744,-1.50011 0.744,2.45728 0.744,6.87963 1.488,25.32681 2.976,58.56911 0.744,5.29605 0.744,-1.31911 0.744,-8.03292 1.488,-32.69795 2.976,-79.02586 1.488,-20.46693 0.744,-1.09612 0.744,5.72965 0.744,12.31994 1.488,40.84565 4.464,142.92149 1.488,22.81465 0.744,5.75133 0.744,2.9772 1.488,-3.98092 0.744,-9.9613 1.488,-37.68173 2.976,-91.91816 1.488,-24.91094 0.744,-5.37188 0.744,-1.86031 1.488,2.85403 1.488,7.85444 1.488,11.91121 1.488,18.20202 2.232,30.27667 1.488,14.22323 1.488,8.26892 1.488,2.78152 1.488,0.84953 1.488,-3.72943 0.744,-5.55318 1.488,-20.61354 1.488,-32.21626 4.464,-113.843703 1.488,-20.141638 0.744,-2.120428 0.744,4.682202 0.744,11.23528 1.488,36.107587 2.232,57.75799 1.488,25.25843 2.232,24.35664 1.488,10.24365 0.744,1.52362 1.488,-1.30286 0.744,-2.08998 0.744,-3.79067 1.488,-15.16222 2.232,-30.2585 0.744,-6.04314 0.744,-2.48551 0.744,1.29102 0.744,5.11122 1.488,20.22618 3.72,64.92923 0.744,7.75707 0.744,3.7673 1.488,-4.86038 1.488,-17.54351 1.488,-30.56327 0,0" + id="path4811-6-2-9" /> + d="m 374.23269,341.47482 0.0939,-0.65022 0.28159,1.79838 0.2816,0.0478 0.28164,-3.3574 0.2816,0.91227 0.2816,-3.76504 0.28158,-3.8786 0.28159,0.31826 0.28161,-2.93969 0.28158,2.12976 0.28166,2.91161 0.28158,-3.66694 0.28159,-4.78151 0.2816,3.00418 0.2816,-0.45084 0.28159,3.36208 0.28159,2.38221 0.28166,-1.73022 0.28159,2.24655 0.28159,1.29212 0.2816,-1.02102 0.2816,1.63143 0.28159,0.0785 0.28159,-0.17149 0.28161,1.84831 0.28159,-1.45938 0.28163,2.20814 0.28159,-0.37676 0.28161,-3.22058 0.28159,-4.99985 0.28161,1.80392 0.2816,-0.72018 0.2816,3.61201 0.28159,-0.1102 0.2816,-1.01527 0.28158,-1.53575 0.28162,-2.34305 0.28163,-1.3386 0.28159,1.26216 0.28159,2.81306 0.2816,-3.51469 0.2816,3.15473 0.28158,-1.43211 0.28159,-1.35094 0.28166,-1.35476 0.28158,2.45654 0.2816,-2.25443 0.28159,0.79081 0.2816,-2.33768 0.28159,-1.27333 0.28166,0.0148 0.28159,-2.79968 0.28159,2.96059 0.2816,0.0434 0.28159,2.29501 0.28159,-1.20215 0.2816,1.99338 0.28164,0.25563 0.28159,0.88164 0.28161,1.88327 0.28159,2.48923 0.2816,4.27227 0.28158,-2.67399 0.28159,1.51851 0.28167,-3.91957 0.28157,0.84992 0.2816,0.79747 0.2816,1.65774 0.28159,3.25539 0.28161,-1.05148 0.28156,3.38361 0.28167,-2.56789 0.28159,1.42067 0.28159,-1.64775 0.28161,1.10117 0.28157,-2.48312 0.28161,-0.34186 0.28164,-1.85731 0.28159,-4.3439 0.28161,-1.22529 0.2816,-2.64647 0.28158,0.40261 0.28159,2.30317 0.2816,3.26169 0.28165,3.90676 0.28159,1.20757 0.2816,-1.13937 0.2816,1.8794 0.28157,1.22247 0.2816,0.94872 0.28159,-0.0297 0.28162,-0.677 0.28164,-0.95908 0.28158,-2.79147 0.28159,-0.91337 0.2816,1.34442 0.2816,-0.80308 0.28161,-0.54615 0.28159,2.23517 0.28163,-0.55074 0.28159,-1.05092 0.28158,2.60261 0.2816,1.38289 0.2816,-0.87159 0.28165,0.8822 0.28158,-0.44716 0.28159,5.30315 0.2816,-2.34479 0.2816,-2.53624 0.28159,-0.16237 0.2816,2.55295 0.28164,-1.85789 0.28159,-2.12508 0.2816,1.95739 0.2816,-2.0168 0.28159,-0.71736 0.2816,-3.52875 0.28163,1.87319 0.2816,3.54882 0.2816,1.00208 0.28159,-1.72404 0.28159,-4.11831 0.2816,0.2098 0.28159,3.47671 0.28164,0.59953 0.28161,-0.53344 0.28158,0.92126 0.2816,-1.67634 0.28159,0.0624 0.2816,1.38527 0.28159,0.17587 0.28164,1.9884 0.2816,-3.42548 0.28159,1.03746 0.2816,-0.50364 0.2816,1.5986 0.28159,-1.11642 0.28158,1.67321 0.28165,0.7699 0.28165,1.66817 0.28155,1.33538 0.28162,0.65592 0.28155,2.48906 0.28161,-3.94097 0.28162,0.17909 0.28156,-2.71655 0.28166,1.49825 0.28155,-0.0782 0.28161,-2.04233 0.28166,-4.32407 0.28155,-5.58112 0.28164,-0.10081 0.2816,-1.93655 0.28154,1.11356 0.28163,0.72132 0.28155,0.17371 0.28165,-0.18937 0.28166,-0.45279 0.28157,-1.22887 0.28159,2.05886 0.28161,1.5996 0.28154,-0.0455 0.28165,1.28095 0.28155,-2.95795 0.28167,0.21414 0.2816,1.58616 0.2816,0.31645 0.28159,1.25661 0.28159,-0.99596 0.2816,-2.27949 0.2816,0.49984 0.28163,-2.23732 0.2816,0.89453 0.2816,1.64203 0.28159,-2.01424 0.2816,-0.37659 0.28159,-0.89503 0.28159,0.37718 0.28165,3.04773 0.28158,1.60405 0.28161,-0.97373 0.28159,1.24335 0.28161,-3.31343 0.28158,-0.72102 0.28163,0.43293 0.28161,1.12831 0.2816,0.31134 0.28158,0.6649 0.2816,1.21784 0.28161,2.15913 0.28157,2.5182 0.28165,2.14505 0.2816,3.90048 0.28159,-1.43452 0.2816,0.0936 0.28159,0.4546 0.28159,-1.28065 0.28159,-1.62625 0.28168,-1.51851 0.28156,-2.62506 0.2816,-1.73765 0.2816,1.05838 0.28158,-2.35868 0.28161,2.92044 0.28168,2.04158 0.28155,0.16253 0.28164,-0.85924 0.28154,-2.43339 0.28159,-1.72779 0.28159,0.76048 0.28162,-0.20882 0.28163,-0.47629 0.28154,-0.79049 0.28166,-0.6126 0.28162,-3.2259 0.28157,0.0224 0.28165,3.35336 0.28153,0.75558 0.2816,-0.16431 0.28163,-3.43003 0.28154,0.92457 0.28166,-2.55017 0.28164,1.07407 0.28154,-0.43445 0.28164,4.41528 0.2816,-1.69215 0.28158,-1.51447 0.28161,2.949 0.28155,0.45389 0.28163,4.24397 0.28154,3.53664 0.28171,3.32527 0.28159,1.68069 0.2816,-1.09731 0.28159,-2.91576 0.28158,2.71903 0.28156,0.0598 0.28165,-0.83655 0.28164,1.69247 0.28159,-1.33134 0.28159,-1.69297 0.2816,-3.42547 0.28159,-2.60579 0.28161,0.79454 0.28158,-1.38681 0.28163,0.12446 0.2816,-0.50704 0.2816,-2.23453 0.28161,-0.16455 0.28157,-0.45981 0.28159,-2.07539 0.28166,-4.76536 0.2816,0.12338 0.28159,-4.92954 0.28158,-0.12442 0.2816,-4.40374 0.2816,0.72554 0.2816,-0.0916 0.28165,-1.80163 0.28159,0.85557 0.28157,-0.32571 0.2816,8.23979 0.2816,0.96719 0.2816,5.6372 0.28161,1.97273 0.28161,3.61145 0.2816,-0.0287 0.2816,2.1735 0.28159,0.14366 0.28159,-3.75558 0.28159,2.83995 0.28171,-1.70416 0.28154,0.46254 0.28163,1.68104 0.28156,-0.89553 0.2816,-0.94178 0.2816,0.62107 0.28159,-1.80432 0.28164,-0.83945 0.28164,4.58588 0.28154,-2.93819 0.28165,1.56934 0.28155,2.37222 0.28159,-1.96843 0.2816,4.09014 0.2816,-2.63006 0.28162,1.37942 0.28166,2.37555 0.28154,1.13014 0.28163,-1.16697 0.28155,1.72014 0.28169,-0.50155 0.28151,-3.36382 0.2816,1.27787 0.28169,-5.58999 0.28151,2.97148 0.2816,-0.46802 0.28169,-2.87843 0.2816,1.74327 0.2816,0.32919 0.2816,-0.68901 0.28152,-2.48547 0.28159,-0.0147 0.2816,0.30121 0.2816,4.05682 0.28169,-2.28124 0.2816,-0.375 0.2816,-0.0952 0.2816,-3.41578 0.2816,0.0962 0.2816,1.09726 0.28151,2.412 0.28168,3.83624 0.2816,-2.58409 0.2816,-1.94555 0.2816,1.16433 0.2816,-1.40041 0.2816,2.0661 0.2816,-0.14777 0.2816,0.90408 0.2816,1.49411 0.2816,-2.03815 0.2816,-3.51139 0.2816,-0.37599 0.2816,-3.20485 0.2816,3.63219 0.2816,-1.03756 0.2816,-0.90888 0.28159,0.66675 0.2816,-0.69521 0.2816,-3.14377 0.2816,-1.38854 0.2816,1.0004 0.2816,1.37037 0.28169,2.9271 0.28151,0.14336 0.2816,-0.25431 0.2816,-0.64013 0.28168,3.21864 0.2816,-1.9086 0.2816,4.05887 0.2816,2.19024 0.2816,0.96478 0.2816,-0.34531 0.28151,0.92651 0.28169,-2.99366 0.2816,-2.92969 0.2816,-0.11831 0.2816,-3.5794 0.2816,0.20556 0.2816,-3.47593 0.2816,2.07597 0.2816,-3.82994 0.2816,-3.3212 0.28159,0.15551 0.2816,2.21495 0.2816,4.35104 0.2816,0.34942 0.2816,1.97568 0.2816,-0.0424 0.2816,1.01362 0.2816,-1.09955 0.28169,1.62206 0.28151,5.40194 0.28168,-2.60878 0.2816,0.18115 0.28152,1.08384 0.28168,0.3366 0.28152,-0.38263 0.28159,0.6605 0.2816,-0.567 0.2816,-0.55418 0.28169,0.35738 0.2816,-0.87594 0.2816,0.97882 0.2816,-1.97601 0.28151,-1.73457 0.28169,-4.55629 0.28151,1.15363 0.28168,-1.19391 0.2816,1.60668 0.2816,-1.59031 0.2816,-0.98102 0.2816,0.85377 0.2816,0.99845 0.2816,-2.84561 0.2816,1.42273 0.2816,7.9713 0.2816,-1.8932 0.2816,1.00488 0.2816,-0.56939 0.2816,-0.2043 0.2816,2.8425 0.2816,-2.32566 0.2816,-1.26199 0.28159,3.57549 0.2816,-2.66773 0.2816,-5.01292 0.2816,1.85462 0.28169,0.78672 0.2816,1.58927 0.2816,4.47557 0.28151,-1.30855 0.28169,-3.1434 0.28151,-0.87456 0.28168,1.52213 0.2816,2.11194 0.2816,4.58331 0.2816,-0.10076 0.20462,-1.93981" + clip-path="none" + style="fill:none;stroke:#aa4400;stroke-width:0.83733952;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" /> + style="fill:none;stroke:#aa0000;stroke-width:5.83715773;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-4-9-2-9)" + d="m 72,163.1451 0.744,-2.27452 1.488,3.69053 1.488,13.83684 1.488,22.76845 2.232,51.95737 1.488,34.5966 0.744,12.10493 0.744,6.4761 1.488,-4.01561 1.488,-17.60438 2.232,-39.1788 1.488,-24.68425 0.744,-7.10813 0.744,-1.58165 0.744,4.08897 3.72,41.4428 0.744,3.48985 0.744,1.4769 1.488,-4.884 1.488,-18.30227 2.976,-48.67307 0.744,-6.86559 0.744,-3.377 1.488,0.86929 1.488,4.36029 0.744,1.13793 1.488,-2.49878 0.744,-2.45691 0.744,-1.28143 0.744,1.2585 0.744,4.6037 1.488,19.62599 1.488,24.18953 0.744,7.91383 0.744,3.48058 1.488,-3.71225 2.232,-11.62052 0.744,-1.80422 1.488,1.17035 1.488,7.85088 2.232,11.21149 2.976,13.6754 1.488,-1.78885 1.488,-8.80563 1.488,-14.75642 2.976,-33.43367 4.464,-31.10787 0.744,-1.14349 0.744,3.96554 0.744,10.21771 1.488,36.2853 1.488,36.87935 0.744,11.54626 0.744,5.98157 1.488,-1.44933 1.488,-5.71535 1.488,0.71294 1.488,-1.19286 1.488,-8.29143 1.488,-7.20299 1.488,0.83889 0.744,3.72078 1.488,13.41834 1.488,13.31066 0.744,2.88616 1.488,-1.52697 0.744,-3.54994 0.744,-5.99536 1.488,-20.37847 2.232,-38.29982 0.744,-7.92207 0.744,-2.75328 0.744,1.8795 2.232,12.81364 0.744,1.03227 0.744,-1.72154 2.232,-12.82547 1.488,2.50925 2.976,22.54865 0.744,-1.25574 0.744,-5.65 2.976,-34.85934 1.488,4.88766 0.744,12.56426 1.488,42.25131 2.976,97.04178 0.744,12.48095 0.744,3.2372 0.744,-6.05124 1.488,-32.37885 1.488,-37.54203 1.488,-20.92636 0.744,-3.7474 0.744,-1.00758 1.488,1.46415 1.488,0.0151 0.744,-2.34547 0.744,-4.56184 1.488,-15.18759 1.488,-16.71724 0.744,-5.34978 0.744,-1.5619 0.744,2.84896 0.744,7.14781 1.488,23.91764 2.232,37.60059 0.744,3.53793 0.744,-4.05031 0.744,-11.05249 1.488,-37.68191 3.72,-119.49675 1.488,-26.478681 0.744,-6.897754 0.744,-3.11891 0.744,1.250277 0.744,6.99151 0.744,13.792158 1.488,47.30283 4.464,177.32434 1.488,33.14416 1.488,17.02368 0.744,2.96724 1.488,-5.51284 1.488,-21.4442 2.232,-41.10146 1.488,-16.05151 0.744,-3.94976 0.744,-1.39129 1.488,2.2463 1.488,-0.3758 0.744,-5.36849 1.488,-23.64804 2.232,-41.03902 1.488,-15.6666 1.488,-8.38273 1.488,5.61339 0.744,10.64396 1.488,34.68651 2.976,76.53698 0.744,9.90657 0.744,4.79924 1.488,-2.56891 0.744,-6.54618 1.488,-23.81025 2.232,-40.27649 0.744,-7.50548 0.744,-3.17536 1.488,2.37205 1.488,6.46522 2.232,10.65168 1.488,-3.60726 1.488,-14.64286 2.232,-24.27417 1.488,-9.22035 0.744,-2.77995 1.488,-0.14636 1.488,3.06904 0.744,-1.23541 0.744,-5.03374 1.488,-19.42625 1.488,-18.74288 0.744,-4.28695 1.488,6.28938 1.488,24.29036 3.72,74.23768 0.744,6.9658 0.744,2.43073 0.744,-1.42117 1.488,-8.96017 2.232,-14.48642 0.744,-1.16321 1.488,0.42834 0.744,-1.29693 0.744,-3.04055 3.72,-23.37479 0.744,-1.02771 0.744,1.55461 0.744,4.20746 1.488,15.48447 4.464,65.01346 0.744,3.1016 0.744,-2.14014 0.744,-8.58213 1.488,-36.71644 2.232,-69.06021 1.488,-29.90818 1.488,-16.47476 0.744,-2.88334 0.744,1.26266 0.744,5.61147 1.488,22.21736 3.72,68.78244 0.744,7.10172 0.744,3.52161 1.488,-2.8778 1.488,-8.405 0.744,-1.84613 0.744,1.64525 1.488,12.44408 1.488,13.28303 0.744,3.36649 1.488,-2.70589 1.488,-13.49759 3.72,-46.57487 4.464,-67.92097 0.744,-1.43634 0.744,3.41625 2.232,22.47254 2.232,22.12998 1.488,7.75163 1.488,4.7448 2.232,4.33509 1.488,5.89054 1.488,5.5381 1.488,-1.04093 0.744,-4.01282 1.488,-16.26713 1.488,-26.75233 2.976,-66.60945 0.744,-9.18019 0.744,-2.57555 0.744,4.67276 1.488,26.63495 2.976,60.57553 2.232,33.23359 1.488,11.55037 1.488,-4.40908 3.72,-38.79848 0.744,-2.37546 1.488,-0.91866 0.744,-2.30007 0.744,-5.56669 2.976,-33.59139 0.744,-2.90548 0.744,1.16037 1.488,10.94604 1.488,9.56728 0.744,1.78836 1.488,1.80099 0.744,2.10887 0.744,3.74749 1.488,13.03245 1.488,19.44637 2.232,33.25631 0.744,6.38156 0.744,1.3175 0.744,-4.56847 1.488,-24.44709 2.232,-40.3528 0.744,-7.59347 0.744,-3.82612 1.488,3.76905 2.232,14.45134 1.488,-0.90748 2.976,-13.16671 1.488,1.82187 2.232,10.12258 1.488,-3.02868 1.488,-14.67378 4.464,-58.70091 0.744,-4.0362 1.488,3.01815 1.488,14.64084 2.232,29.36065 1.488,10.75984 1.488,5.26404 0.744,1.07752 1.488,-2.26771 1.488,-4.07076 1.488,0.0972 0.744,3.57554 1.488,16.756 2.232,31.16986 1.488,9.29941 1.488,3.14758 1.488,2.73359 0.744,2.65369 0.744,4.75089 1.488,17.85895 2.232,34.39264 0.744,6.45355 0.744,2.43797 0.744,-1.47078 0.744,-5.10291 1.488,-18.98022 3.72,-58.41148 1.488,-10.50604 0.744,-1.42735 2.976,0.61828 0.744,1.35708 2.232,8.73947 2.232,9.13244 0.744,1.28294 1.488,-2.9375 2.976,-12.40827 1.488,2.80629 1.488,9.75622 3.72,32.55742 1.488,-0.96532 1.488,-9.5954 3.72,-29.85605 2.976,-22.32126 0.744,-2.60173 1.488,4.89718 1.488,19.46461 1.488,22.06141 0.744,6.24971 0,0" + id="path4951-7-6-7" /> + style="fill:none;stroke:#aa0000;stroke-width:5.78597641;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-1-6-1-1)" + d="m 72,195.32244 2.232,30.33622 5.208,82.6578 0.744,4.19735 1.488,-3.89026 1.488,-19.94804 2.976,-51.5742 1.488,-15.29076 0.744,-3.34251 1.488,3.00996 2.232,13.1893 1.488,-3.78718 1.488,-19.12934 2.232,-34.06841 0.744,-5.24672 1.488,8.23212 1.488,31.43735 2.232,57.23294 1.488,22.07497 0.744,4.115 1.488,-6.28659 1.488,-21.65355 2.232,-36.59323 0.744,-5.1408 1.488,6.30869 1.488,20.61486 2.232,33.49649 2.232,21.86923 1.488,11.09439 0.744,3.17876 1.488,-4.64149 0.744,-9.80413 1.488,-35.10837 5.208,-162.82504 0.744,-4.37728 0.744,2.85359 1.488,18.23989 2.232,26.41871 1.488,8.29516 1.488,-4.16282 1.488,-19.19336 2.976,-49.954665 1.488,-14.127951 0.744,-1.481978 0.744,3.186202 0.744,8.152323 1.488,30.070909 2.976,74.34825 1.488,22.21049 0.744,3.87797 1.488,-4.18932 1.488,-6.76813 0.744,1.1176 0.744,5.54956 2.976,38.1284 0.744,2.07031 0.744,-3.13193 0.744,-7.82764 3.72,-55.0724 0.744,-3.64768 1.488,5.30612 2.976,30.19848 3.72,40.74891 1.488,-6.08927 1.488,-29.84397 5.208,-135.548516 0.744,-4.439519 0.744,2.328802 1.488,18.799123 2.232,31.04385 0.744,3.24254 0.744,-1.35421 1.488,-7.8348 1.488,3.07954 0.744,8.87001 2.976,54.21293 0.744,4.45466 0.744,-3.83675 0.744,-12.36007 1.488,-44.02004 2.232,-67.458014 0.744,-9.807903 0.744,-1.235956 0.744,6.843274 1.488,33.963199 3.72,110.95534 1.488,30.78323 1.488,15.82699 0.744,1.05037 0.744,-2.92336 1.488,-15.42023 2.232,-29.10193 0.744,-4.13997 1.488,3.25476 1.488,14.02677 1.488,13.7527 0.744,3.5736 1.488,-0.0598 1.488,-6.03731 2.232,-11.29102 0.744,-1.71544 1.488,0.83206 1.488,2.74048 1.488,-2.35746 0.744,-4.38614 1.488,-15.79294 2.976,-37.65538 1.488,-9.18414 1.488,-0.397 1.488,0.5789 0.744,-2.07495 2.976,-12.27931 0.744,-1.07803 1.488,3.7658 0.744,7.07097 1.488,22.86684 3.72,62.29229 1.488,10.53592 1.488,-1.53356 0.744,-5.65925 1.488,-20.3937 2.976,-52.3496 0.744,-5.71969 0.744,-1.77482 1.488,2.13314 0.744,1.74903 0.744,2.64263 0.744,4.53901 1.488,16.37177 2.232,37.7415 2.976,57.56895 1.488,16.24963 0.744,4.00541 1.488,-1.54407 0.744,-6.06174 1.488,-22.64763 4.464,-86.76047 0.744,-5.34539 1.488,5.4348 1.488,24.24557 1.488,27.75599 0.744,9.07281 0.744,4.13125 1.488,-5.73562 2.232,-22.1705 0.744,-2.10783 0.744,1.90328 1.488,11.73428 2.232,23.86467 0.744,3.59592 1.488,-4.51945 1.488,-10.58806 1.488,-5.51951 0.744,-1.17679 1.488,0.73573 0.744,2.96847 1.488,11.1741 2.232,25.63503 2.232,28.94129 0.744,5.08943 1.488,-6.45851 1.488,-28.88379 4.464,-110.71625 1.488,-28.21869 1.488,-15.73324 0.744,-1.09211 0.744,4.4715 0.744,10.35307 1.488,34.85257 2.232,59.39486 1.488,25.73433 2.232,24.50853 1.488,11.94282 0.744,4.03252 0.744,1.53676 0.744,-2.10704 0.744,-6.61355 1.488,-25.57901 2.976,-58.38824 0.744,-7.79984 0.744,-3.28947 0.744,1.43053 0.744,6.04059 1.488,23.71545 2.232,42.4171 1.488,15.27687 0.744,1.92636 0.744,-1.1466 2.232,-8.88218 1.488,1.15132 1.488,7.77147 2.232,12.03415 0.744,1.33413 1.488,-2.49175 0.744,-3.71797 0.744,-6.00499 1.488,-21.23441 4.464,-87.4472 1.488,-11.20767 1.488,2.21804 0.744,6.85048 1.488,22.61505 3.72,65.10288 1.488,12.3153 1.488,-1.41618 0.744,-4.74623 1.488,-16.15999 2.976,-38.69355 1.488,-10.16289 0.744,-2.09524 1.488,0.22406 1.488,1.2405 1.488,-1.56728 1.488,2.37064 0.744,5.74578 1.488,21.80702 2.232,36.04628 0.744,7.41719 0.744,3.73415 1.488,-3.52244 1.488,-16.44012 2.976,-39.75004 1.488,-10.86014 2.232,-9.68575 2.976,-8.07549 0.744,-1.90015 1.488,-0.90024 1.488,-2.67097 1.488,-10.76671 1.488,-10.69323 0.744,-1.50011 0.744,2.45728 0.744,6.87963 1.488,25.32681 2.976,58.56911 0.744,5.29605 0.744,-1.31911 0.744,-8.03292 1.488,-32.69795 2.976,-79.02586 1.488,-20.46693 0.744,-1.09612 0.744,5.72965 0.744,12.31994 1.488,40.84565 4.464,142.92149 1.488,22.81465 0.744,5.75133 0.744,2.9772 1.488,-3.98092 0.744,-9.9613 1.488,-37.68173 2.976,-91.91816 1.488,-24.91094 0.744,-5.37188 0.744,-1.86031 1.488,2.85403 1.488,7.85444 1.488,11.91121 1.488,18.20202 2.232,30.27667 1.488,14.22323 1.488,8.26892 1.488,2.78152 1.488,0.84953 1.488,-3.72943 0.744,-5.55318 1.488,-20.61354 1.488,-32.21626 4.464,-113.843703 1.488,-20.141638 0.744,-2.120428 0.744,4.682202 0.744,11.23528 1.488,36.107587 2.232,57.75799 1.488,25.25843 2.232,24.35664 1.488,10.24365 0.744,1.52362 1.488,-1.30286 0.744,-2.08998 0.744,-3.79067 1.488,-15.16222 2.232,-30.2585 0.744,-6.04314 0.744,-2.48551 0.744,1.29102 0.744,5.11122 1.488,20.22618 3.72,64.92923 0.744,7.75707 0.744,3.7673 1.488,-4.86038 1.488,-17.54351 1.488,-30.56327 0,0" + id="path4811-8-6-7" /> + height="306.71115" + width="115.0112" + id="rect2385-3" + style="fill:none;stroke:#000000;stroke-width:0.99281323;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:inline;overflow:visible;enable-background:accumulate" /> + d="m 521.49053,270.75621 0.0939,-0.65022 0.28159,1.79837 0.2816,0.0478 0.28165,-3.35739 0.2816,0.91226 0.2816,-3.76503 0.28158,-3.8786 0.28159,0.31826 0.28161,-2.93969 0.28158,2.12976 0.28166,2.91161 0.28158,-3.66695 0.28159,-4.7815 0.2816,3.00418 0.2816,-0.45085 0.28158,3.36209 0.2816,2.3822 0.28165,-1.73021 0.28159,2.24655 0.28159,1.29211 0.2816,-1.02102 0.2816,1.63144 0.28159,0.0785 0.28159,-0.1715 0.28162,1.84832 0.28159,-1.45939 0.28163,2.20815 0.28159,-0.37677 0.28161,-3.22057 0.28159,-4.99985 0.2816,1.80392 0.2816,-0.72018 0.2816,3.61201 0.28159,-0.11021 0.2816,-1.01527 0.28158,-1.53574 0.28163,-2.34306 0.28163,-1.3386 0.28159,1.26217 0.28159,2.81306 0.2816,-3.5147 0.2816,3.15474 0.28158,-1.43211 0.28159,-1.35095 0.28166,-1.35475 0.28158,2.45653 0.2816,-2.25443 0.28159,0.79082 0.2816,-2.33768 0.28159,-1.27333 0.28165,0.0148 0.28159,-2.79969 0.28159,2.9606 0.28161,0.0434 0.28158,2.29501 0.28159,-1.20216 0.2816,1.99339 0.28165,0.25563 0.28159,0.88163 0.2816,1.88328 0.2816,2.48923 0.28159,4.27226 0.28159,-2.67399 0.28159,1.51852 0.28167,-3.91957 0.28157,0.84992 0.2816,0.79747 0.2816,1.65774 0.28159,3.25539 0.28161,-1.05149 0.28156,3.38362 0.28167,-2.56789 0.28158,1.42067 0.2816,-1.64776 0.28161,1.10117 0.28156,-2.48312 0.28161,-0.34185 0.28164,-1.85731 0.2816,-4.3439 0.2816,-1.22529 0.2816,-2.64648 0.28158,0.40262 0.2816,2.30316 0.2816,3.2617 0.28165,3.90675 0.28159,1.20757 0.2816,-1.13937 0.2816,1.8794 0.28157,1.22248 0.2816,0.94872 0.28159,-0.0297 0.28162,-0.67701 0.28164,-0.95907 0.28158,-2.79147 0.28159,-0.91337 0.2816,1.34442 0.2816,-0.80308 0.28161,-0.54616 0.28159,2.23518 0.28162,-0.55074 0.28159,-1.05092 0.28159,2.60261 0.2816,1.38289 0.2816,-0.87159 0.28165,0.8822 0.28158,-0.44717 0.28159,5.30316 0.2816,-2.3448 0.2816,-2.53623 0.28159,-0.16237 0.2816,2.55295 0.28163,-1.8579 0.28159,-2.12507 0.2816,1.95738 0.2816,-2.01679 0.2816,-0.71736 0.28159,-3.52876 0.28164,1.8732 0.2816,3.54882 0.28159,1.00207 0.2816,-1.72404 0.28159,-4.1183 0.2816,0.2098 0.28159,3.47671 0.28164,0.59952 0.28161,-0.53343 0.28158,0.92125 0.2816,-1.67633 0.28159,0.0624 0.2816,1.38526 0.28159,0.17588 0.28163,1.9884 0.2816,-3.42548 0.28159,1.03745 0.28161,-0.50364 0.2816,1.59861 0.28158,-1.11642 0.28158,1.67321 0.28166,0.7699 0.28165,1.66817 0.28154,1.33538 0.28163,0.65592 0.28155,2.48905 0.2816,-3.94096 0.28163,0.17908 0.28156,-2.71655 0.28166,1.49826 0.28154,-0.0782 0.28162,-2.04234 0.28165,-4.32407 0.28156,-5.58112 0.28163,-0.10081 0.28161,-1.93655 0.28154,1.11356 0.28162,0.72133 0.28156,0.1737 0.28164,-0.18937 0.28166,-0.45278 0.28158,-1.22888 0.28159,2.05887 0.2816,1.5996 0.28154,-0.0455 0.28165,1.28095 0.28155,-2.95794 0.28168,0.21413 0.2816,1.58616 0.2816,0.31645 0.28159,1.25661 0.28159,-0.99596 0.2816,-2.27949 0.2816,0.49984 0.28162,-2.23732 0.28161,0.89453 0.28159,1.64204 0.28159,-2.01425 0.2816,-0.37659 0.2816,-0.89503 0.28158,0.37719 0.28165,3.04772 0.28159,1.60405 0.2816,-0.97373 0.2816,1.24335 0.2816,-3.31343 0.28159,-0.72102 0.28163,0.43293 0.28161,1.12831 0.2816,0.31134 0.28158,0.6649 0.2816,1.21784 0.28161,2.15913 0.28157,2.5182 0.28164,2.14505 0.28161,3.90048 0.28159,-1.43452 0.28159,0.0936 0.28159,0.4546 0.28159,-1.28065 0.28159,-1.62625 0.28169,-1.51851 0.28156,-2.62506 0.2816,-1.73764 0.28159,1.05837 0.28159,-2.35867 0.2816,2.92043 0.28168,2.04159 0.28156,0.16252 0.28164,-0.85923 0.28154,-2.4334 0.28159,-1.72779 0.28159,0.76048 0.28162,-0.20882 0.28163,-0.47629 0.28154,-0.79049 0.28165,-0.6126 0.28163,-3.2259 0.28156,0.0224 0.28165,3.35335 0.28153,0.75559 0.2816,-0.16432 0.28164,-3.43003 0.28154,0.92457 0.28166,-2.55016 0.28164,1.07406 0.28154,-0.43445 0.28164,4.41529 0.2816,-1.69215 0.28158,-1.51448 0.28161,2.949 0.28155,0.4539 0.28163,4.24396 0.28154,3.53664 0.2817,3.32528 0.2816,1.68068 0.2816,-1.0973 0.28158,-2.91577 0.28158,2.71903 0.28157,0.0598 0.28164,-0.83654 0.28164,1.69247 0.2816,-1.33135 0.28159,-1.69297 0.2816,-3.42546 0.28159,-2.60579 0.2816,0.79454 0.28159,-1.38682 0.28163,0.12447 0.2816,-0.50705 0.2816,-2.23453 0.28161,-0.16455 0.28157,-0.4598 0.28159,-2.07539 0.28165,-4.76536 0.2816,0.12337 0.2816,-4.92954 0.28157,-0.12441 0.2816,-4.40375 0.2816,0.72554 0.2816,-0.0916 0.28165,-1.80163 0.28159,0.85557 0.28158,-0.3257 0.2816,8.23979 0.2816,0.96718 0.2816,5.6372 0.2816,1.97274 0.28161,3.61144 0.28161,-0.0287 0.2816,2.1735 0.28159,0.14366 0.28159,-3.75557 0.28159,2.83995 0.2817,-1.70417 0.28154,0.46254 0.28164,1.68104 0.28155,-0.89553 0.28161,-0.94177 0.2816,0.62107 0.28158,-1.80433 0.28165,-0.83945 0.28164,4.58588 0.28154,-2.93818 0.28165,1.56933 0.28155,2.37223 0.28159,-1.96844 0.2816,4.09014 0.2816,-2.63006 0.28162,1.37943 0.28165,2.37555 0.28155,1.13013 0.28162,-1.16697 0.28156,1.72015 0.28169,-0.50155 0.28151,-3.36383 0.2816,1.27787 0.28168,-5.58999 0.28152,2.97148 0.2816,-0.46802 0.28168,-2.87843 0.2816,1.74327 0.2816,0.32919 0.2816,-0.68901 0.28151,-2.48547 0.2816,-0.0147 0.2816,0.30121 0.2816,4.05682 0.28169,-2.28124 0.2816,-0.375 0.2816,-0.0952 0.2816,-3.41577 0.28159,0.0962 0.2816,1.09725 0.28152,2.412 0.28168,3.83624 0.2816,-2.58409 0.2816,-1.94555 0.2816,1.16433 0.2816,-1.40041 0.2816,2.0661 0.2816,-0.14777 0.2816,0.90408 0.2816,1.49411 0.2816,-2.03815 0.2816,-3.51139 0.2816,-0.37599 0.2816,-3.20485 0.28159,3.63219 0.2816,-1.03755 0.2816,-0.90889 0.2816,0.66675 0.2816,-0.69521 0.2816,-3.14377 0.2816,-1.38854 0.2816,1.0004 0.2816,1.37037 0.28169,2.9271 0.28151,0.14336 0.2816,-0.2543 0.2816,-0.64014 0.28168,3.21864 0.2816,-1.9086 0.2816,4.05887 0.2816,2.19024 0.2816,0.96478 0.2816,-0.34531 0.28151,0.92651 0.28169,-2.99366 0.2816,-2.92969 0.2816,-0.11831 0.2816,-3.5794 0.2816,0.20556 0.28159,-3.47593 0.2816,2.07597 0.2816,-3.82994 0.2816,-3.3212 0.2816,0.15551 0.2816,2.21495 0.2816,4.35104 0.2816,0.34942 0.2816,1.97568 0.2816,-0.0424 0.2816,1.01362 0.2816,-1.09955 0.28168,1.62206 0.28152,5.40193 0.28168,-2.60877 0.2816,0.18115 0.28151,1.08384 0.28169,0.3366 0.28151,-0.38263 0.2816,0.6605 0.2816,-0.567 0.2816,-0.55418 0.28169,0.35738 0.2816,-0.87594 0.2816,0.97881 0.2816,-1.976 0.28151,-1.73457 0.28168,-4.55629 0.28152,1.15363 0.28168,-1.19391 0.2816,1.60667 0.2816,-1.5903 0.2816,-0.98102 0.2816,0.85377 0.2816,0.99845 0.2816,-2.84561 0.2816,1.42273 0.2816,7.9713 0.2816,-1.8932 0.2816,1.00488 0.2816,-0.56939 0.2816,-0.2043 0.28159,2.8425 0.2816,-2.32567 0.2816,-1.26198 0.2816,3.57549 0.2816,-2.66773 0.2816,-5.01292 0.2816,1.85462 0.28169,0.78671 0.2816,1.58928 0.2816,4.47557 0.28151,-1.30855 0.28168,-3.1434 0.28152,-0.87456 0.28168,1.52213 0.2816,2.11193 0.2816,4.58332 0.2816,-0.10076 0.20462,-1.93981" + clip-path="none" + style="fill:none;stroke:#aa4400;stroke-width:0.83733952;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" /> + style="fill:none;stroke:#aa0000;stroke-width:5.40178728;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-70-4-5)" + d="m 72,199.44014 0.744,-9.88505 0.744,-8.16304 1.488,-9.36337 1.488,1.87949 0.744,4.51209 1.488,14.91057 2.976,33.84172 0.744,4.07908 0.744,1.42438 2.232,-4.60906 1.488,-0.0347 1.488,4.87221 1.488,5.9514 0.744,1.30441 0.744,-1.102 1.488,-8.48149 1.488,-6.66913 1.488,1.49385 0.744,2.11321 1.488,-1.52428 2.976,-17.3357 0.744,1.00237 2.232,10.64968 1.488,-4.58749 1.488,-22.11071 2.232,-44.27705 0.744,-8.23399 0.744,-2.67097 0.744,1.68533 1.488,9.82042 1.488,15.06494 2.232,34.10097 1.488,19.55812 0.744,4.74052 1.488,-1.04466 2.232,-8.95528 0.744,-1.66071 1.488,1.57539 1.488,9.97787 2.976,25.15023 1.488,9.85045 1.488,16.87157 1.488,20.10934 0.744,4.06258 0.744,-3.2619 0.744,-10.54148 4.464,-88.92547 2.232,-7.57641 0.744,-6.78791 2.976,-42.28856 0.744,-3.70829 0.744,1.93631 1.488,16.51775 1.488,17.60432 0.744,4.60487 0.744,1.46299 1.488,-2.48511 2.232,-8.84485 1.488,3.58933 1.488,17.8884 2.232,44.1192 2.976,69.61286 1.488,16.66005 0.744,1.77475 0.744,-1.73947 0.744,-4.38392 2.232,-21.99984 1.488,-11.63998 2.232,-13.45344 2.232,-20.81468 3.72,-43.83979 0.744,-3.86105 1.488,3.98198 1.488,16.21543 2.232,25.67229 0.744,2.70765 0.744,-1.27609 1.488,-10.12523 1.488,-11.31521 0.744,-3.97331 0.744,-2.00057 1.488,4.1382 1.488,14.931 1.488,16.65152 0.744,3.72521 0.744,-1.59709 0.744,-6.92539 2.976,-47.18876 2.232,-32.9563 1.488,-10.49265 2.976,-7.78144 1.488,1.06199 1.488,10.11014 1.488,8.87559 0.744,1.15151 1.488,0.55343 0.744,1.482 2.232,7.63589 1.488,0.81241 1.488,3.5433 2.976,16.08709 1.488,0.11738 1.488,-4.45597 2.232,-8.75192 0.744,-1.69833 1.488,2.54425 0.744,6.25848 2.976,38.89904 0.744,1.44606 0.744,-3.91149 2.976,-30.90086 1.488,5.82012 2.232,20.64498 0.744,2.85098 0.744,1.52132 1.488,1.61142 0.744,1.6138 0.744,3.74111 2.232,18.7331 0.744,1.82262 0.744,-3.23004 1.488,-19.64156 1.488,-20.95621 0.744,-4.88296 1.488,5.26725 1.488,23.35075 3.72,78.19372 0.744,3.03636 0.744,-3.52594 1.488,-21.42585 5.208,-103.81978 0.744,-6.14833 1.488,3.44713 2.976,30.03524 0.744,3.76533 0.744,1.39758 1.488,-3.36539 0.744,-2.51763 0.744,-1.11263 1.488,3.98873 1.488,10.28867 2.232,16.39558 1.488,-2.65607 0.744,-8.48228 1.488,-29.39002 2.976,-64.39347 0.744,-10.22254 0.744,-5.58917 1.488,6.47734 1.488,29.06624 4.464,126.63805 1.488,19.96354 1.488,8.29161 2.976,7.72263 1.488,-2.20208 0.744,-6.24159 1.488,-23.49537 3.72,-83.10322 0.744,-6.23683 1.488,1.76487 2.232,8.76564 1.488,3.74779 1.488,-0.024 1.488,-3.13723 2.976,-2.18358 1.488,3.46051 2.232,12.14935 0.744,1.04711 0.744,-2.33626 0.744,-5.76889 1.488,-18.68947 2.976,-39.63108 0.744,-4.39655 1.488,5.15274 0.744,11.4399 1.488,40.38546 2.976,93.76422 0.744,13.18135 0.744,6.18746 1.488,-6.65651 1.488,-23.09303 2.232,-39.41937 0.744,-7.5832 0.744,-2.19151 0.744,3.37797 0.744,8.38648 1.488,28.30844 2.232,45.00689 0.744,6.46383 1.488,-7.85238 1.488,-29.28104 2.976,-66.0734 0.744,-9.19186 0.744,-3.97138 0.744,1.29966 0.744,5.71682 4.464,49.47802 3.72,29.2188 1.488,0.59528 0.744,-1.27976 0.744,-2.64938 0.744,-4.89588 0.744,-8.13954 1.488,-26.92991 2.976,-63.3791 1.488,-17.44058 1.488,-6.986 0.744,-2.45078 0.744,-3.75576 4.464,-32.09936 0.744,-1.62975 1.488,-1.46885 1.488,0.82394 0.744,3.15329 1.488,13.24986 3.72,41.72316 0.744,2.85103 1.488,-3.27108 0.744,-2.97088 1.488,2.46798 1.488,15.32221 2.232,27.13907 0.744,5.77709 0.744,1.85304 0.744,-2.94587 0.744,-7.76788 3.72,-58.37263 0.744,-1.83181 0.744,3.37185 2.976,23.05137 0.744,1.45875 0.744,-1.59848 0.744,-5.26625 1.488,-22.90961 3.72,-81.468298 0.744,-7.197684 1.488,6.014559 1.488,30.348323 2.976,69.63821 1.488,15.91528 2.232,10.94183 2.232,18.80032 1.488,15.65283 3.72,44.88057 0.744,1.58038 0.744,-2.28932 1.488,-12.70893 4.464,-54.32107 0.744,-2.68949 0.744,1.09828 0.744,4.68426 1.488,16.41455 2.232,25.92632 0.744,5.98774 0.744,3.10537 1.488,-6.11821 1.488,-19.78223 2.232,-32.4824 1.488,-12.87383 0.744,-3.53748 0.744,-1.52085 1.488,2.65488 0.744,4.96905 1.488,19.96561 3.72,64.27351 0.744,6.03854 0.744,2.11457 0.744,-2.40737 0.744,-6.5507 1.488,-21.65179 3.72,-62.98963 1.488,-13.82365 1.488,-8.53521 0.744,-2.44259 1.488,2.90333 1.488,15.66129 3.72,49.72553 1.488,13.21295 1.488,7.17468 1.488,-1.56624 0.744,-3.64211 1.488,-12.60594 2.232,-30.19264 2.232,-29.12852 2.976,-26.43417 2.232,-16.76631 1.488,-6.35095 1.488,2.60633 1.488,9.02587 0.744,5.30678 0,0" + id="path4671-0-4" /> + style="fill:none;stroke:#aa0000;stroke-width:5.78597641;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-1-3-3)" + d="m 72,195.32244 2.232,30.33622 5.208,82.6578 0.744,4.19735 1.488,-3.89026 1.488,-19.94804 2.976,-51.5742 1.488,-15.29076 0.744,-3.34251 1.488,3.00996 2.232,13.1893 1.488,-3.78718 1.488,-19.12934 2.232,-34.06841 0.744,-5.24672 1.488,8.23212 1.488,31.43735 2.232,57.23294 1.488,22.07497 0.744,4.115 1.488,-6.28659 1.488,-21.65355 2.232,-36.59323 0.744,-5.1408 1.488,6.30869 1.488,20.61486 2.232,33.49649 2.232,21.86923 1.488,11.09439 0.744,3.17876 1.488,-4.64149 0.744,-9.80413 1.488,-35.10837 5.208,-162.82504 0.744,-4.37728 0.744,2.85359 1.488,18.23989 2.232,26.41871 1.488,8.29516 1.488,-4.16282 1.488,-19.19336 2.976,-49.954665 1.488,-14.127951 0.744,-1.481978 0.744,3.186202 0.744,8.152323 1.488,30.070909 2.976,74.34825 1.488,22.21049 0.744,3.87797 1.488,-4.18932 1.488,-6.76813 0.744,1.1176 0.744,5.54956 2.976,38.1284 0.744,2.07031 0.744,-3.13193 0.744,-7.82764 3.72,-55.0724 0.744,-3.64768 1.488,5.30612 2.976,30.19848 3.72,40.74891 1.488,-6.08927 1.488,-29.84397 5.208,-135.548516 0.744,-4.439519 0.744,2.328802 1.488,18.799123 2.232,31.04385 0.744,3.24254 0.744,-1.35421 1.488,-7.8348 1.488,3.07954 0.744,8.87001 2.976,54.21293 0.744,4.45466 0.744,-3.83675 0.744,-12.36007 1.488,-44.02004 2.232,-67.458014 0.744,-9.807903 0.744,-1.235956 0.744,6.843274 1.488,33.963199 3.72,110.95534 1.488,30.78323 1.488,15.82699 0.744,1.05037 0.744,-2.92336 1.488,-15.42023 2.232,-29.10193 0.744,-4.13997 1.488,3.25476 1.488,14.02677 1.488,13.7527 0.744,3.5736 1.488,-0.0598 1.488,-6.03731 2.232,-11.29102 0.744,-1.71544 1.488,0.83206 1.488,2.74048 1.488,-2.35746 0.744,-4.38614 1.488,-15.79294 2.976,-37.65538 1.488,-9.18414 1.488,-0.397 1.488,0.5789 0.744,-2.07495 2.976,-12.27931 0.744,-1.07803 1.488,3.7658 0.744,7.07097 1.488,22.86684 3.72,62.29229 1.488,10.53592 1.488,-1.53356 0.744,-5.65925 1.488,-20.3937 2.976,-52.3496 0.744,-5.71969 0.744,-1.77482 1.488,2.13314 0.744,1.74903 0.744,2.64263 0.744,4.53901 1.488,16.37177 2.232,37.7415 2.976,57.56895 1.488,16.24963 0.744,4.00541 1.488,-1.54407 0.744,-6.06174 1.488,-22.64763 4.464,-86.76047 0.744,-5.34539 1.488,5.4348 1.488,24.24557 1.488,27.75599 0.744,9.07281 0.744,4.13125 1.488,-5.73562 2.232,-22.1705 0.744,-2.10783 0.744,1.90328 1.488,11.73428 2.232,23.86467 0.744,3.59592 1.488,-4.51945 1.488,-10.58806 1.488,-5.51951 0.744,-1.17679 1.488,0.73573 0.744,2.96847 1.488,11.1741 2.232,25.63503 2.232,28.94129 0.744,5.08943 1.488,-6.45851 1.488,-28.88379 4.464,-110.71625 1.488,-28.21869 1.488,-15.73324 0.744,-1.09211 0.744,4.4715 0.744,10.35307 1.488,34.85257 2.232,59.39486 1.488,25.73433 2.232,24.50853 1.488,11.94282 0.744,4.03252 0.744,1.53676 0.744,-2.10704 0.744,-6.61355 1.488,-25.57901 2.976,-58.38824 0.744,-7.79984 0.744,-3.28947 0.744,1.43053 0.744,6.04059 1.488,23.71545 2.232,42.4171 1.488,15.27687 0.744,1.92636 0.744,-1.1466 2.232,-8.88218 1.488,1.15132 1.488,7.77147 2.232,12.03415 0.744,1.33413 1.488,-2.49175 0.744,-3.71797 0.744,-6.00499 1.488,-21.23441 4.464,-87.4472 1.488,-11.20767 1.488,2.21804 0.744,6.85048 1.488,22.61505 3.72,65.10288 1.488,12.3153 1.488,-1.41618 0.744,-4.74623 1.488,-16.15999 2.976,-38.69355 1.488,-10.16289 0.744,-2.09524 1.488,0.22406 1.488,1.2405 1.488,-1.56728 1.488,2.37064 0.744,5.74578 1.488,21.80702 2.232,36.04628 0.744,7.41719 0.744,3.73415 1.488,-3.52244 1.488,-16.44012 2.976,-39.75004 1.488,-10.86014 2.232,-9.68575 2.976,-8.07549 0.744,-1.90015 1.488,-0.90024 1.488,-2.67097 1.488,-10.76671 1.488,-10.69323 0.744,-1.50011 0.744,2.45728 0.744,6.87963 1.488,25.32681 2.976,58.56911 0.744,5.29605 0.744,-1.31911 0.744,-8.03292 1.488,-32.69795 2.976,-79.02586 1.488,-20.46693 0.744,-1.09612 0.744,5.72965 0.744,12.31994 1.488,40.84565 4.464,142.92149 1.488,22.81465 0.744,5.75133 0.744,2.9772 1.488,-3.98092 0.744,-9.9613 1.488,-37.68173 2.976,-91.91816 1.488,-24.91094 0.744,-5.37188 0.744,-1.86031 1.488,2.85403 1.488,7.85444 1.488,11.91121 1.488,18.20202 2.232,30.27667 1.488,14.22323 1.488,8.26892 1.488,2.78152 1.488,0.84953 1.488,-3.72943 0.744,-5.55318 1.488,-20.61354 1.488,-32.21626 4.464,-113.843703 1.488,-20.141638 0.744,-2.120428 0.744,4.682202 0.744,11.23528 1.488,36.107587 2.232,57.75799 1.488,25.25843 2.232,24.35664 1.488,10.24365 0.744,1.52362 1.488,-1.30286 0.744,-2.08998 0.744,-3.79067 1.488,-15.16222 2.232,-30.2585 0.744,-6.04314 0.744,-2.48551 0.744,1.29102 0.744,5.11122 1.488,20.22618 3.72,64.92923 0.744,7.75707 0.744,3.7673 1.488,-4.86038 1.488,-17.54351 1.488,-30.56327 0,0" + id="path4811-6-8" /> + style="fill:none;stroke:#aa0000;stroke-width:5.83715773;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-4-8-6)" + d="m 72,163.1451 0.744,-2.27452 1.488,3.69053 1.488,13.83684 1.488,22.76845 2.232,51.95737 1.488,34.5966 0.744,12.10493 0.744,6.4761 1.488,-4.01561 1.488,-17.60438 2.232,-39.1788 1.488,-24.68425 0.744,-7.10813 0.744,-1.58165 0.744,4.08897 3.72,41.4428 0.744,3.48985 0.744,1.4769 1.488,-4.884 1.488,-18.30227 2.976,-48.67307 0.744,-6.86559 0.744,-3.377 1.488,0.86929 1.488,4.36029 0.744,1.13793 1.488,-2.49878 0.744,-2.45691 0.744,-1.28143 0.744,1.2585 0.744,4.6037 1.488,19.62599 1.488,24.18953 0.744,7.91383 0.744,3.48058 1.488,-3.71225 2.232,-11.62052 0.744,-1.80422 1.488,1.17035 1.488,7.85088 2.232,11.21149 2.976,13.6754 1.488,-1.78885 1.488,-8.80563 1.488,-14.75642 2.976,-33.43367 4.464,-31.10787 0.744,-1.14349 0.744,3.96554 0.744,10.21771 1.488,36.2853 1.488,36.87935 0.744,11.54626 0.744,5.98157 1.488,-1.44933 1.488,-5.71535 1.488,0.71294 1.488,-1.19286 1.488,-8.29143 1.488,-7.20299 1.488,0.83889 0.744,3.72078 1.488,13.41834 1.488,13.31066 0.744,2.88616 1.488,-1.52697 0.744,-3.54994 0.744,-5.99536 1.488,-20.37847 2.232,-38.29982 0.744,-7.92207 0.744,-2.75328 0.744,1.8795 2.232,12.81364 0.744,1.03227 0.744,-1.72154 2.232,-12.82547 1.488,2.50925 2.976,22.54865 0.744,-1.25574 0.744,-5.65 2.976,-34.85934 1.488,4.88766 0.744,12.56426 1.488,42.25131 2.976,97.04178 0.744,12.48095 0.744,3.2372 0.744,-6.05124 1.488,-32.37885 1.488,-37.54203 1.488,-20.92636 0.744,-3.7474 0.744,-1.00758 1.488,1.46415 1.488,0.0151 0.744,-2.34547 0.744,-4.56184 1.488,-15.18759 1.488,-16.71724 0.744,-5.34978 0.744,-1.5619 0.744,2.84896 0.744,7.14781 1.488,23.91764 2.232,37.60059 0.744,3.53793 0.744,-4.05031 0.744,-11.05249 1.488,-37.68191 3.72,-119.49675 1.488,-26.478681 0.744,-6.897754 0.744,-3.11891 0.744,1.250277 0.744,6.99151 0.744,13.792158 1.488,47.30283 4.464,177.32434 1.488,33.14416 1.488,17.02368 0.744,2.96724 1.488,-5.51284 1.488,-21.4442 2.232,-41.10146 1.488,-16.05151 0.744,-3.94976 0.744,-1.39129 1.488,2.2463 1.488,-0.3758 0.744,-5.36849 1.488,-23.64804 2.232,-41.03902 1.488,-15.6666 1.488,-8.38273 1.488,5.61339 0.744,10.64396 1.488,34.68651 2.976,76.53698 0.744,9.90657 0.744,4.79924 1.488,-2.56891 0.744,-6.54618 1.488,-23.81025 2.232,-40.27649 0.744,-7.50548 0.744,-3.17536 1.488,2.37205 1.488,6.46522 2.232,10.65168 1.488,-3.60726 1.488,-14.64286 2.232,-24.27417 1.488,-9.22035 0.744,-2.77995 1.488,-0.14636 1.488,3.06904 0.744,-1.23541 0.744,-5.03374 1.488,-19.42625 1.488,-18.74288 0.744,-4.28695 1.488,6.28938 1.488,24.29036 3.72,74.23768 0.744,6.9658 0.744,2.43073 0.744,-1.42117 1.488,-8.96017 2.232,-14.48642 0.744,-1.16321 1.488,0.42834 0.744,-1.29693 0.744,-3.04055 3.72,-23.37479 0.744,-1.02771 0.744,1.55461 0.744,4.20746 1.488,15.48447 4.464,65.01346 0.744,3.1016 0.744,-2.14014 0.744,-8.58213 1.488,-36.71644 2.232,-69.06021 1.488,-29.90818 1.488,-16.47476 0.744,-2.88334 0.744,1.26266 0.744,5.61147 1.488,22.21736 3.72,68.78244 0.744,7.10172 0.744,3.52161 1.488,-2.8778 1.488,-8.405 0.744,-1.84613 0.744,1.64525 1.488,12.44408 1.488,13.28303 0.744,3.36649 1.488,-2.70589 1.488,-13.49759 3.72,-46.57487 4.464,-67.92097 0.744,-1.43634 0.744,3.41625 2.232,22.47254 2.232,22.12998 1.488,7.75163 1.488,4.7448 2.232,4.33509 1.488,5.89054 1.488,5.5381 1.488,-1.04093 0.744,-4.01282 1.488,-16.26713 1.488,-26.75233 2.976,-66.60945 0.744,-9.18019 0.744,-2.57555 0.744,4.67276 1.488,26.63495 2.976,60.57553 2.232,33.23359 1.488,11.55037 1.488,-4.40908 3.72,-38.79848 0.744,-2.37546 1.488,-0.91866 0.744,-2.30007 0.744,-5.56669 2.976,-33.59139 0.744,-2.90548 0.744,1.16037 1.488,10.94604 1.488,9.56728 0.744,1.78836 1.488,1.80099 0.744,2.10887 0.744,3.74749 1.488,13.03245 1.488,19.44637 2.232,33.25631 0.744,6.38156 0.744,1.3175 0.744,-4.56847 1.488,-24.44709 2.232,-40.3528 0.744,-7.59347 0.744,-3.82612 1.488,3.76905 2.232,14.45134 1.488,-0.90748 2.976,-13.16671 1.488,1.82187 2.232,10.12258 1.488,-3.02868 1.488,-14.67378 4.464,-58.70091 0.744,-4.0362 1.488,3.01815 1.488,14.64084 2.232,29.36065 1.488,10.75984 1.488,5.26404 0.744,1.07752 1.488,-2.26771 1.488,-4.07076 1.488,0.0972 0.744,3.57554 1.488,16.756 2.232,31.16986 1.488,9.29941 1.488,3.14758 1.488,2.73359 0.744,2.65369 0.744,4.75089 1.488,17.85895 2.232,34.39264 0.744,6.45355 0.744,2.43797 0.744,-1.47078 0.744,-5.10291 1.488,-18.98022 3.72,-58.41148 1.488,-10.50604 0.744,-1.42735 2.976,0.61828 0.744,1.35708 2.232,8.73947 2.232,9.13244 0.744,1.28294 1.488,-2.9375 2.976,-12.40827 1.488,2.80629 1.488,9.75622 3.72,32.55742 1.488,-0.96532 1.488,-9.5954 3.72,-29.85605 2.976,-22.32126 0.744,-2.60173 1.488,4.89718 1.488,19.46461 1.488,22.06141 0.744,6.24971 0,0" + id="path4951-8-1" /> + d="m 520.41189,406.91753 0.0939,-0.65022 0.28159,1.79838 0.2816,0.0478 0.28165,-3.3574 0.28159,0.91227 0.2816,-3.76504 0.28159,-3.8786 0.28159,0.31826 0.2816,-2.93969 0.28159,2.12976 0.28166,2.91161 0.28158,-3.66694 0.28159,-4.78151 0.2816,3.00418 0.2816,-0.45084 0.28158,3.36208 0.2816,2.38221 0.28165,-1.73022 0.28159,2.24655 0.28159,1.29212 0.2816,-1.02102 0.2816,1.63143 0.28159,0.0785 0.28159,-0.17149 0.28162,1.84831 0.28159,-1.45938 0.28162,2.20814 0.2816,-0.37676 0.2816,-3.22058 0.28159,-4.99985 0.28161,1.80392 0.2816,-0.72018 0.2816,3.61201 0.28159,-0.1102 0.2816,-1.01527 0.28158,-1.53575 0.28163,-2.34305 0.28162,-1.3386 0.28159,1.26216 0.2816,2.81306 0.28159,-3.51469 0.2816,3.15473 0.28159,-1.43211 0.28159,-1.35094 0.28166,-1.35476 0.28158,2.45654 0.2816,-2.25443 0.28159,0.79081 0.2816,-2.33768 0.28159,-1.27333 0.28165,0.0148 0.28159,-2.79968 0.28159,2.96059 0.28161,0.0434 0.28158,2.29501 0.28159,-1.20215 0.2816,1.99338 0.28164,0.25563 0.28159,0.88164 0.28161,1.88327 0.28159,2.48923 0.2816,4.27227 0.28159,-2.67399 0.28159,1.51851 0.28166,-3.91957 0.28158,0.84992 0.2816,0.79747 0.2816,1.65774 0.28159,3.25539 0.28161,-1.05149 0.28156,3.38362 0.28167,-2.56789 0.28158,1.42067 0.2816,-1.64775 0.28161,1.10117 0.28156,-2.48312 0.28161,-0.34186 0.28164,-1.85731 0.28159,-4.3439 0.28161,-1.22529 0.2816,-2.64647 0.28158,0.40261 0.28159,2.30316 0.2816,3.2617 0.28165,3.90676 0.2816,1.20757 0.28159,-1.13937 0.2816,1.8794 0.28158,1.22247 0.2816,0.94872 0.28159,-0.0297 0.28161,-0.677 0.28165,-0.95908 0.28158,-2.79147 0.28159,-0.91337 0.2816,1.34442 0.2816,-0.80308 0.28161,-0.54616 0.28159,2.23518 0.28162,-0.55074 0.28159,-1.05092 0.28159,2.60261 0.28159,1.38289 0.2816,-0.87159 0.28166,0.8822 0.28158,-0.44716 0.28159,5.30315 0.2816,-2.34479 0.2816,-2.53624 0.28159,-0.16237 0.2816,2.55295 0.28163,-1.85789 0.28159,-2.12508 0.2816,1.95739 0.2816,-2.0168 0.2816,-0.71736 0.28159,-3.52875 0.28163,1.87319 0.2816,3.54882 0.2816,1.00208 0.28159,-1.72404 0.28159,-4.11831 0.2816,0.2098 0.28159,3.47671 0.28165,0.59953 0.2816,-0.53344 0.28159,0.92126 0.2816,-1.67634 0.28159,0.0624 0.2816,1.38527 0.28159,0.17587 0.28163,1.9884 0.2816,-3.42548 0.28159,1.03746 0.28161,-0.50364 0.2816,1.5986 0.28158,-1.11642 0.28158,1.67321 0.28165,0.7699 0.28166,1.66817 0.28154,1.33538 0.28163,0.65592 0.28155,2.48905 0.2816,-3.94096 0.28163,0.17908 0.28155,-2.71654 0.28166,1.49825 0.28155,-0.0782 0.28162,-2.04233 0.28165,-4.32407 0.28156,-5.58113 0.28163,-0.1008 0.28161,-1.93655 0.28154,1.11356 0.28162,0.72132 0.28156,0.17371 0.28164,-0.18938 0.28166,-0.45278 0.28157,-1.22887 0.28159,2.05886 0.28161,1.5996 0.28154,-0.0455 0.28165,1.28095 0.28155,-2.95794 0.28168,0.21413 0.2816,1.58616 0.2816,0.31646 0.28159,1.2566 0.28159,-0.99595 0.2816,-2.27949 0.2816,0.49983 0.28162,-2.23732 0.28161,0.89454 0.28159,1.64203 0.28159,-2.01425 0.2816,-0.37659 0.2816,-0.89502 0.28158,0.37718 0.28165,3.04772 0.28159,1.60406 0.2816,-0.97374 0.28159,1.24336 0.28161,-3.31343 0.28158,-0.72102 0.28164,0.43293 0.28161,1.1283 0.2816,0.31134 0.28158,0.6649 0.2816,1.21784 0.2816,2.15914 0.28158,2.51819 0.28164,2.14506 0.28161,3.90047 0.28159,-1.43452 0.28159,0.0936 0.28159,0.45461 0.28159,-1.28066 0.28159,-1.62624 0.28169,-1.51851 0.28155,-2.62506 0.2816,-1.73765 0.2816,1.05838 0.28159,-2.35868 0.2816,2.92044 0.28168,2.04158 0.28156,0.16252 0.28164,-0.85923 0.28154,-2.4334 0.28159,-1.72778 0.28159,0.76047 0.28162,-0.20882 0.28163,-0.47629 0.28154,-0.79049 0.28165,-0.6126 0.28163,-3.2259 0.28156,0.0224 0.28165,3.35336 0.28153,0.75558 0.2816,-0.16432 0.28163,-3.43003 0.28154,0.92457 0.28166,-2.55016 0.28165,1.07406 0.28153,-0.43445 0.28165,4.41529 0.2816,-1.69215 0.28158,-1.51447 0.28161,2.94899 0.28154,0.4539 0.28164,4.24397 0.28154,3.53664 0.2817,3.32527 0.2816,1.68069 0.2816,-1.09731 0.28158,-2.91576 0.28158,2.71903 0.28157,0.0598 0.28164,-0.83655 0.28164,1.69247 0.28159,-1.33134 0.28159,-1.69297 0.2816,-3.42547 0.28159,-2.60579 0.28161,0.79454 0.28158,-1.38681 0.28164,0.12446 0.2816,-0.50705 0.2816,-2.23452 0.28161,-0.16456 0.28157,-0.4598 0.28159,-2.07539 0.28165,-4.76536 0.2816,0.12337 0.2816,-4.92953 0.28157,-0.12442 0.2816,-4.40374 0.2816,0.72554 0.2816,-0.0916 0.28165,-1.80164 0.28159,0.85558 0.28158,-0.32571 0.2816,8.23979 0.28159,0.96718 0.2816,5.6372 0.28161,1.97274 0.28161,3.61145 0.28161,-0.0287 0.2816,2.1735 0.28159,0.14365 0.28159,-3.75557 0.28159,2.83995 0.2817,-1.70416 0.28154,0.46253 0.28164,1.68105 0.28155,-0.89553 0.28161,-0.94178 0.2816,0.62107 0.28158,-1.80433 0.28164,-0.83944 0.28165,4.58588 0.28154,-2.93819 0.28165,1.56934 0.28154,2.37222 0.28159,-1.96844 0.2816,4.09014 0.2816,-2.63005 0.28163,1.37942 0.28165,2.37555 0.28155,1.13014 0.28162,-1.16697 0.28156,1.72014 0.28165,-0.50155 0.28158,-3.36382 0.28155,1.27787 0.28166,-5.58999 0.28154,2.97148 0.28165,-0.46802 0.28163,-2.87843 0.28159,1.74327 0.28161,0.32919 0.28158,-0.68901 0.28157,-2.48547 0.28162,-0.0147 0.28155,0.30121 0.28164,4.05682 0.28165,-2.28124 0.2816,-0.375 0.28158,-0.0952 0.28159,-3.41578 0.2816,0.0962 0.28161,1.09725 0.28153,2.41201 0.28169,3.83624 0.28159,-2.5841 0.2816,-1.94554 0.28159,1.16432 0.28159,-1.40041 0.2816,2.0661 0.28165,-0.14776 0.28159,0.90408 0.28159,1.49411 0.28159,-2.03816 0.28161,-3.51139 0.28159,-0.37598 0.28159,-3.20485 0.28164,3.63218 0.28159,-1.03755 0.28161,-0.90888 0.28159,0.66675 0.2816,-0.69522 0.28159,-3.14376 0.28161,-1.38855 0.28161,1.0004 0.2816,1.37037 0.28162,2.92711 0.28158,0.14336 0.28158,-0.25431 0.2816,-0.64014 0.28169,3.21865 0.28155,-1.90861 0.2816,4.05888 0.28159,2.19024 0.2816,0.96477 0.2816,-0.3453 0.28159,0.9265 0.28165,-2.99365 0.28161,-2.9297 0.28156,-0.1183 0.28161,-3.57941 0.2816,0.20557 0.28159,-3.47593 0.28159,2.07596 0.28159,-3.82993 0.28163,-3.32121 0.28165,0.15551 0.28156,2.21495 0.28162,4.35104 0.28156,0.34943 0.28159,1.97567 0.28164,-0.0424 0.28155,1.01363 0.28165,-1.09955 0.28164,1.62205 0.28151,5.40194 0.28169,-2.60878 0.2816,0.18116 0.28151,1.08383 0.28168,0.33661 0.28152,-0.38263 0.2816,0.66049 0.2816,-0.567 0.2816,-0.55418 0.28168,0.35739 0.2816,-0.87595 0.2816,0.97882 0.2816,-1.97601 0.28151,-1.73457 0.28169,-4.55629 0.28151,1.15363 0.28169,-1.1939 0.2816,1.60667 0.2816,-1.59031 0.28159,-0.98102 0.2816,0.85378 0.2816,0.99844 0.2816,-2.8456 0.2816,1.42272 0.2816,7.97131 0.2816,-1.8932 0.2816,1.00487 0.2816,-0.56939 0.2816,-0.2043 0.2816,2.8425 0.2816,-2.32566 0.2816,-1.26198 0.2816,3.57548 0.2816,-2.66773 0.2816,-5.01291 0.28159,1.85461 0.28169,0.78672 0.2816,1.58927 0.2816,4.47557 0.28151,-1.30855 0.28169,-3.14339 0.28151,-0.87456 0.28169,1.52212 0.2816,2.11194 0.2816,4.58332 0.28159,-0.10076 0.20462,-1.93982" + clip-path="none" + style="fill:none;stroke:#aa4400;stroke-width:0.83733952;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none" /> + style="fill:none;stroke:#aa0000;stroke-width:5.40178728;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-70-49-7)" + d="m 72,199.44014 0.744,-9.88505 0.744,-8.16304 1.488,-9.36337 1.488,1.87949 0.744,4.51209 1.488,14.91057 2.976,33.84172 0.744,4.07908 0.744,1.42438 2.232,-4.60906 1.488,-0.0347 1.488,4.87221 1.488,5.9514 0.744,1.30441 0.744,-1.102 1.488,-8.48149 1.488,-6.66913 1.488,1.49385 0.744,2.11321 1.488,-1.52428 2.976,-17.3357 0.744,1.00237 2.232,10.64968 1.488,-4.58749 1.488,-22.11071 2.232,-44.27705 0.744,-8.23399 0.744,-2.67097 0.744,1.68533 1.488,9.82042 1.488,15.06494 2.232,34.10097 1.488,19.55812 0.744,4.74052 1.488,-1.04466 2.232,-8.95528 0.744,-1.66071 1.488,1.57539 1.488,9.97787 2.976,25.15023 1.488,9.85045 1.488,16.87157 1.488,20.10934 0.744,4.06258 0.744,-3.2619 0.744,-10.54148 4.464,-88.92547 2.232,-7.57641 0.744,-6.78791 2.976,-42.28856 0.744,-3.70829 0.744,1.93631 1.488,16.51775 1.488,17.60432 0.744,4.60487 0.744,1.46299 1.488,-2.48511 2.232,-8.84485 1.488,3.58933 1.488,17.8884 2.232,44.1192 2.976,69.61286 1.488,16.66005 0.744,1.77475 0.744,-1.73947 0.744,-4.38392 2.232,-21.99984 1.488,-11.63998 2.232,-13.45344 2.232,-20.81468 3.72,-43.83979 0.744,-3.86105 1.488,3.98198 1.488,16.21543 2.232,25.67229 0.744,2.70765 0.744,-1.27609 1.488,-10.12523 1.488,-11.31521 0.744,-3.97331 0.744,-2.00057 1.488,4.1382 1.488,14.931 1.488,16.65152 0.744,3.72521 0.744,-1.59709 0.744,-6.92539 2.976,-47.18876 2.232,-32.9563 1.488,-10.49265 2.976,-7.78144 1.488,1.06199 1.488,10.11014 1.488,8.87559 0.744,1.15151 1.488,0.55343 0.744,1.482 2.232,7.63589 1.488,0.81241 1.488,3.5433 2.976,16.08709 1.488,0.11738 1.488,-4.45597 2.232,-8.75192 0.744,-1.69833 1.488,2.54425 0.744,6.25848 2.976,38.89904 0.744,1.44606 0.744,-3.91149 2.976,-30.90086 1.488,5.82012 2.232,20.64498 0.744,2.85098 0.744,1.52132 1.488,1.61142 0.744,1.6138 0.744,3.74111 2.232,18.7331 0.744,1.82262 0.744,-3.23004 1.488,-19.64156 1.488,-20.95621 0.744,-4.88296 1.488,5.26725 1.488,23.35075 3.72,78.19372 0.744,3.03636 0.744,-3.52594 1.488,-21.42585 5.208,-103.81978 0.744,-6.14833 1.488,3.44713 2.976,30.03524 0.744,3.76533 0.744,1.39758 1.488,-3.36539 0.744,-2.51763 0.744,-1.11263 1.488,3.98873 1.488,10.28867 2.232,16.39558 1.488,-2.65607 0.744,-8.48228 1.488,-29.39002 2.976,-64.39347 0.744,-10.22254 0.744,-5.58917 1.488,6.47734 1.488,29.06624 4.464,126.63805 1.488,19.96354 1.488,8.29161 2.976,7.72263 1.488,-2.20208 0.744,-6.24159 1.488,-23.49537 3.72,-83.10322 0.744,-6.23683 1.488,1.76487 2.232,8.76564 1.488,3.74779 1.488,-0.024 1.488,-3.13723 2.976,-2.18358 1.488,3.46051 2.232,12.14935 0.744,1.04711 0.744,-2.33626 0.744,-5.76889 1.488,-18.68947 2.976,-39.63108 0.744,-4.39655 1.488,5.15274 0.744,11.4399 1.488,40.38546 2.976,93.76422 0.744,13.18135 0.744,6.18746 1.488,-6.65651 1.488,-23.09303 2.232,-39.41937 0.744,-7.5832 0.744,-2.19151 0.744,3.37797 0.744,8.38648 1.488,28.30844 2.232,45.00689 0.744,6.46383 1.488,-7.85238 1.488,-29.28104 2.976,-66.0734 0.744,-9.19186 0.744,-3.97138 0.744,1.29966 0.744,5.71682 4.464,49.47802 3.72,29.2188 1.488,0.59528 0.744,-1.27976 0.744,-2.64938 0.744,-4.89588 0.744,-8.13954 1.488,-26.92991 2.976,-63.3791 1.488,-17.44058 1.488,-6.986 0.744,-2.45078 0.744,-3.75576 4.464,-32.09936 0.744,-1.62975 1.488,-1.46885 1.488,0.82394 0.744,3.15329 1.488,13.24986 3.72,41.72316 0.744,2.85103 1.488,-3.27108 0.744,-2.97088 1.488,2.46798 1.488,15.32221 2.232,27.13907 0.744,5.77709 0.744,1.85304 0.744,-2.94587 0.744,-7.76788 3.72,-58.37263 0.744,-1.83181 0.744,3.37185 2.976,23.05137 0.744,1.45875 0.744,-1.59848 0.744,-5.26625 1.488,-22.90961 3.72,-81.468298 0.744,-7.197684 1.488,6.014559 1.488,30.348323 2.976,69.63821 1.488,15.91528 2.232,10.94183 2.232,18.80032 1.488,15.65283 3.72,44.88057 0.744,1.58038 0.744,-2.28932 1.488,-12.70893 4.464,-54.32107 0.744,-2.68949 0.744,1.09828 0.744,4.68426 1.488,16.41455 2.232,25.92632 0.744,5.98774 0.744,3.10537 1.488,-6.11821 1.488,-19.78223 2.232,-32.4824 1.488,-12.87383 0.744,-3.53748 0.744,-1.52085 1.488,2.65488 0.744,4.96905 1.488,19.96561 3.72,64.27351 0.744,6.03854 0.744,2.11457 0.744,-2.40737 0.744,-6.5507 1.488,-21.65179 3.72,-62.98963 1.488,-13.82365 1.488,-8.53521 0.744,-2.44259 1.488,2.90333 1.488,15.66129 3.72,49.72553 1.488,13.21295 1.488,7.17468 1.488,-1.56624 0.744,-3.64211 1.488,-12.60594 2.232,-30.19264 2.232,-29.12852 2.976,-26.43417 2.232,-16.76631 1.488,-6.35095 1.488,2.60633 1.488,9.02587 0.744,5.30678 0,0" + id="path4671-4-9" /> + style="fill:none;stroke:#aa0000;stroke-width:5.78597641;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-1-6-5)" + d="m 72,195.32244 2.232,30.33622 5.208,82.6578 0.744,4.19735 1.488,-3.89026 1.488,-19.94804 2.976,-51.5742 1.488,-15.29076 0.744,-3.34251 1.488,3.00996 2.232,13.1893 1.488,-3.78718 1.488,-19.12934 2.232,-34.06841 0.744,-5.24672 1.488,8.23212 1.488,31.43735 2.232,57.23294 1.488,22.07497 0.744,4.115 1.488,-6.28659 1.488,-21.65355 2.232,-36.59323 0.744,-5.1408 1.488,6.30869 1.488,20.61486 2.232,33.49649 2.232,21.86923 1.488,11.09439 0.744,3.17876 1.488,-4.64149 0.744,-9.80413 1.488,-35.10837 5.208,-162.82504 0.744,-4.37728 0.744,2.85359 1.488,18.23989 2.232,26.41871 1.488,8.29516 1.488,-4.16282 1.488,-19.19336 2.976,-49.954665 1.488,-14.127951 0.744,-1.481978 0.744,3.186202 0.744,8.152323 1.488,30.070909 2.976,74.34825 1.488,22.21049 0.744,3.87797 1.488,-4.18932 1.488,-6.76813 0.744,1.1176 0.744,5.54956 2.976,38.1284 0.744,2.07031 0.744,-3.13193 0.744,-7.82764 3.72,-55.0724 0.744,-3.64768 1.488,5.30612 2.976,30.19848 3.72,40.74891 1.488,-6.08927 1.488,-29.84397 5.208,-135.548516 0.744,-4.439519 0.744,2.328802 1.488,18.799123 2.232,31.04385 0.744,3.24254 0.744,-1.35421 1.488,-7.8348 1.488,3.07954 0.744,8.87001 2.976,54.21293 0.744,4.45466 0.744,-3.83675 0.744,-12.36007 1.488,-44.02004 2.232,-67.458014 0.744,-9.807903 0.744,-1.235956 0.744,6.843274 1.488,33.963199 3.72,110.95534 1.488,30.78323 1.488,15.82699 0.744,1.05037 0.744,-2.92336 1.488,-15.42023 2.232,-29.10193 0.744,-4.13997 1.488,3.25476 1.488,14.02677 1.488,13.7527 0.744,3.5736 1.488,-0.0598 1.488,-6.03731 2.232,-11.29102 0.744,-1.71544 1.488,0.83206 1.488,2.74048 1.488,-2.35746 0.744,-4.38614 1.488,-15.79294 2.976,-37.65538 1.488,-9.18414 1.488,-0.397 1.488,0.5789 0.744,-2.07495 2.976,-12.27931 0.744,-1.07803 1.488,3.7658 0.744,7.07097 1.488,22.86684 3.72,62.29229 1.488,10.53592 1.488,-1.53356 0.744,-5.65925 1.488,-20.3937 2.976,-52.3496 0.744,-5.71969 0.744,-1.77482 1.488,2.13314 0.744,1.74903 0.744,2.64263 0.744,4.53901 1.488,16.37177 2.232,37.7415 2.976,57.56895 1.488,16.24963 0.744,4.00541 1.488,-1.54407 0.744,-6.06174 1.488,-22.64763 4.464,-86.76047 0.744,-5.34539 1.488,5.4348 1.488,24.24557 1.488,27.75599 0.744,9.07281 0.744,4.13125 1.488,-5.73562 2.232,-22.1705 0.744,-2.10783 0.744,1.90328 1.488,11.73428 2.232,23.86467 0.744,3.59592 1.488,-4.51945 1.488,-10.58806 1.488,-5.51951 0.744,-1.17679 1.488,0.73573 0.744,2.96847 1.488,11.1741 2.232,25.63503 2.232,28.94129 0.744,5.08943 1.488,-6.45851 1.488,-28.88379 4.464,-110.71625 1.488,-28.21869 1.488,-15.73324 0.744,-1.09211 0.744,4.4715 0.744,10.35307 1.488,34.85257 2.232,59.39486 1.488,25.73433 2.232,24.50853 1.488,11.94282 0.744,4.03252 0.744,1.53676 0.744,-2.10704 0.744,-6.61355 1.488,-25.57901 2.976,-58.38824 0.744,-7.79984 0.744,-3.28947 0.744,1.43053 0.744,6.04059 1.488,23.71545 2.232,42.4171 1.488,15.27687 0.744,1.92636 0.744,-1.1466 2.232,-8.88218 1.488,1.15132 1.488,7.77147 2.232,12.03415 0.744,1.33413 1.488,-2.49175 0.744,-3.71797 0.744,-6.00499 1.488,-21.23441 4.464,-87.4472 1.488,-11.20767 1.488,2.21804 0.744,6.85048 1.488,22.61505 3.72,65.10288 1.488,12.3153 1.488,-1.41618 0.744,-4.74623 1.488,-16.15999 2.976,-38.69355 1.488,-10.16289 0.744,-2.09524 1.488,0.22406 1.488,1.2405 1.488,-1.56728 1.488,2.37064 0.744,5.74578 1.488,21.80702 2.232,36.04628 0.744,7.41719 0.744,3.73415 1.488,-3.52244 1.488,-16.44012 2.976,-39.75004 1.488,-10.86014 2.232,-9.68575 2.976,-8.07549 0.744,-1.90015 1.488,-0.90024 1.488,-2.67097 1.488,-10.76671 1.488,-10.69323 0.744,-1.50011 0.744,2.45728 0.744,6.87963 1.488,25.32681 2.976,58.56911 0.744,5.29605 0.744,-1.31911 0.744,-8.03292 1.488,-32.69795 2.976,-79.02586 1.488,-20.46693 0.744,-1.09612 0.744,5.72965 0.744,12.31994 1.488,40.84565 4.464,142.92149 1.488,22.81465 0.744,5.75133 0.744,2.9772 1.488,-3.98092 0.744,-9.9613 1.488,-37.68173 2.976,-91.91816 1.488,-24.91094 0.744,-5.37188 0.744,-1.86031 1.488,2.85403 1.488,7.85444 1.488,11.91121 1.488,18.20202 2.232,30.27667 1.488,14.22323 1.488,8.26892 1.488,2.78152 1.488,0.84953 1.488,-3.72943 0.744,-5.55318 1.488,-20.61354 1.488,-32.21626 4.464,-113.843703 1.488,-20.141638 0.744,-2.120428 0.744,4.682202 0.744,11.23528 1.488,36.107587 2.232,57.75799 1.488,25.25843 2.232,24.35664 1.488,10.24365 0.744,1.52362 1.488,-1.30286 0.744,-2.08998 0.744,-3.79067 1.488,-15.16222 2.232,-30.2585 0.744,-6.04314 0.744,-2.48551 0.744,1.29102 0.744,5.11122 1.488,20.22618 3.72,64.92923 0.744,7.75707 0.744,3.7673 1.488,-4.86038 1.488,-17.54351 1.488,-30.56327 0,0" + id="path4811-8-3" /> + style="fill:none;stroke:#aa0000;stroke-width:5.83715773;stroke-linecap:square;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none" + clip-path="url(#p50431ccdcb28178602d99d9270004dde-4-9-1)" + d="m 72,163.1451 0.744,-2.27452 1.488,3.69053 1.488,13.83684 1.488,22.76845 2.232,51.95737 1.488,34.5966 0.744,12.10493 0.744,6.4761 1.488,-4.01561 1.488,-17.60438 2.232,-39.1788 1.488,-24.68425 0.744,-7.10813 0.744,-1.58165 0.744,4.08897 3.72,41.4428 0.744,3.48985 0.744,1.4769 1.488,-4.884 1.488,-18.30227 2.976,-48.67307 0.744,-6.86559 0.744,-3.377 1.488,0.86929 1.488,4.36029 0.744,1.13793 1.488,-2.49878 0.744,-2.45691 0.744,-1.28143 0.744,1.2585 0.744,4.6037 1.488,19.62599 1.488,24.18953 0.744,7.91383 0.744,3.48058 1.488,-3.71225 2.232,-11.62052 0.744,-1.80422 1.488,1.17035 1.488,7.85088 2.232,11.21149 2.976,13.6754 1.488,-1.78885 1.488,-8.80563 1.488,-14.75642 2.976,-33.43367 4.464,-31.10787 0.744,-1.14349 0.744,3.96554 0.744,10.21771 1.488,36.2853 1.488,36.87935 0.744,11.54626 0.744,5.98157 1.488,-1.44933 1.488,-5.71535 1.488,0.71294 1.488,-1.19286 1.488,-8.29143 1.488,-7.20299 1.488,0.83889 0.744,3.72078 1.488,13.41834 1.488,13.31066 0.744,2.88616 1.488,-1.52697 0.744,-3.54994 0.744,-5.99536 1.488,-20.37847 2.232,-38.29982 0.744,-7.92207 0.744,-2.75328 0.744,1.8795 2.232,12.81364 0.744,1.03227 0.744,-1.72154 2.232,-12.82547 1.488,2.50925 2.976,22.54865 0.744,-1.25574 0.744,-5.65 2.976,-34.85934 1.488,4.88766 0.744,12.56426 1.488,42.25131 2.976,97.04178 0.744,12.48095 0.744,3.2372 0.744,-6.05124 1.488,-32.37885 1.488,-37.54203 1.488,-20.92636 0.744,-3.7474 0.744,-1.00758 1.488,1.46415 1.488,0.0151 0.744,-2.34547 0.744,-4.56184 1.488,-15.18759 1.488,-16.71724 0.744,-5.34978 0.744,-1.5619 0.744,2.84896 0.744,7.14781 1.488,23.91764 2.232,37.60059 0.744,3.53793 0.744,-4.05031 0.744,-11.05249 1.488,-37.68191 3.72,-119.49675 1.488,-26.478681 0.744,-6.897754 0.744,-3.11891 0.744,1.250277 0.744,6.99151 0.744,13.792158 1.488,47.30283 4.464,177.32434 1.488,33.14416 1.488,17.02368 0.744,2.96724 1.488,-5.51284 1.488,-21.4442 2.232,-41.10146 1.488,-16.05151 0.744,-3.94976 0.744,-1.39129 1.488,2.2463 1.488,-0.3758 0.744,-5.36849 1.488,-23.64804 2.232,-41.03902 1.488,-15.6666 1.488,-8.38273 1.488,5.61339 0.744,10.64396 1.488,34.68651 2.976,76.53698 0.744,9.90657 0.744,4.79924 1.488,-2.56891 0.744,-6.54618 1.488,-23.81025 2.232,-40.27649 0.744,-7.50548 0.744,-3.17536 1.488,2.37205 1.488,6.46522 2.232,10.65168 1.488,-3.60726 1.488,-14.64286 2.232,-24.27417 1.488,-9.22035 0.744,-2.77995 1.488,-0.14636 1.488,3.06904 0.744,-1.23541 0.744,-5.03374 1.488,-19.42625 1.488,-18.74288 0.744,-4.28695 1.488,6.28938 1.488,24.29036 3.72,74.23768 0.744,6.9658 0.744,2.43073 0.744,-1.42117 1.488,-8.96017 2.232,-14.48642 0.744,-1.16321 1.488,0.42834 0.744,-1.29693 0.744,-3.04055 3.72,-23.37479 0.744,-1.02771 0.744,1.55461 0.744,4.20746 1.488,15.48447 4.464,65.01346 0.744,3.1016 0.744,-2.14014 0.744,-8.58213 1.488,-36.71644 2.232,-69.06021 1.488,-29.90818 1.488,-16.47476 0.744,-2.88334 0.744,1.26266 0.744,5.61147 1.488,22.21736 3.72,68.78244 0.744,7.10172 0.744,3.52161 1.488,-2.8778 1.488,-8.405 0.744,-1.84613 0.744,1.64525 1.488,12.44408 1.488,13.28303 0.744,3.36649 1.488,-2.70589 1.488,-13.49759 3.72,-46.57487 4.464,-67.92097 0.744,-1.43634 0.744,3.41625 2.232,22.47254 2.232,22.12998 1.488,7.75163 1.488,4.7448 2.232,4.33509 1.488,5.89054 1.488,5.5381 1.488,-1.04093 0.744,-4.01282 1.488,-16.26713 1.488,-26.75233 2.976,-66.60945 0.744,-9.18019 0.744,-2.57555 0.744,4.67276 1.488,26.63495 2.976,60.57553 2.232,33.23359 1.488,11.55037 1.488,-4.40908 3.72,-38.79848 0.744,-2.37546 1.488,-0.91866 0.744,-2.30007 0.744,-5.56669 2.976,-33.59139 0.744,-2.90548 0.744,1.16037 1.488,10.94604 1.488,9.56728 0.744,1.78836 1.488,1.80099 0.744,2.10887 0.744,3.74749 1.488,13.03245 1.488,19.44637 2.232,33.25631 0.744,6.38156 0.744,1.3175 0.744,-4.56847 1.488,-24.44709 2.232,-40.3528 0.744,-7.59347 0.744,-3.82612 1.488,3.76905 2.232,14.45134 1.488,-0.90748 2.976,-13.16671 1.488,1.82187 2.232,10.12258 1.488,-3.02868 1.488,-14.67378 4.464,-58.70091 0.744,-4.0362 1.488,3.01815 1.488,14.64084 2.232,29.36065 1.488,10.75984 1.488,5.26404 0.744,1.07752 1.488,-2.26771 1.488,-4.07076 1.488,0.0972 0.744,3.57554 1.488,16.756 2.232,31.16986 1.488,9.29941 1.488,3.14758 1.488,2.73359 0.744,2.65369 0.744,4.75089 1.488,17.85895 2.232,34.39264 0.744,6.45355 0.744,2.43797 0.744,-1.47078 0.744,-5.10291 1.488,-18.98022 3.72,-58.41148 1.488,-10.50604 0.744,-1.42735 2.976,0.61828 0.744,1.35708 2.232,8.73947 2.232,9.13244 0.744,1.28294 1.488,-2.9375 2.976,-12.40827 1.488,2.80629 1.488,9.75622 3.72,32.55742 1.488,-0.96532 1.488,-9.5954 3.72,-29.85605 2.976,-22.32126 0.744,-2.60173 1.488,4.89718 1.488,19.46461 1.488,22.06141 0.744,6.24971 0,0" + id="path4951-7-9" /> 0 + sodipodi:role="line">0  1 + x="206.02428" + sodipodi:role="line">1 2 + x="203.62785" + sodipodi:role="line">2 3 + x="203.47337" + sodipodi:role="line">3 4 + x="203.28874" + sodipodi:role="line">4 5 + y="341.11588" + x="208.27754" + sodipodi:role="line">5 6 + y="375.75681" + x="208.27754" + sodipodi:role="line">6 7 + y="410.3978" + x="208.27754" + sodipodi:role="line">7 AnalogSignal 0 AnalogSignal 0   + x="226.24783" + sodipodi:role="line">  + d="m 157.26274,148.07933 c -20.64887,73.34197 20.17293,132.4804 -9.87155,140.15415 -1.17489,0.30041 -2.48425,0.62963 -3.88386,0.76997 l -0.48514,0 c -1.06289,0.0911 -2.04006,0.15396 -3.23652,0.15396 l 1.29468,0.30838 -1.29468,0.15395 c 2.22835,0 4.05081,0.30228 5.82588,0.61606 0.41623,0.0737 0.90171,0.0634 1.29462,0.15396 l 0.48513,0.15395 c 30.04449,7.67366 -10.77732,66.81209 9.87155,140.15409" + style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:5.05196238px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + inkscape:connector-curvature="0" /> Segment 1 + id="tspan14158-3" + sodipodi:role="line">Segment 1 Segment 0 + id="tspan14158-9" + sodipodi:role="line">Segment 0 AnalogSignal 1 AnalogSignal 1   + dy="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.6593653" + id="tspan3357">  AnalogSignal 2 AnalogSignal 2   + x="524.63495" + sodipodi:role="line">  diff --git a/doc/source/images/multi_segment_diagram_spiketrain.png b/doc/source/images/multi_segment_diagram_spiketrain.png index 0a9ec7508123e5a325483692d3995bf9a396fdff..e2bc696c680df67aac847e8d0a3d2cc659265529 100644 GIT binary patch literal 63701 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 OEGr{CzCPRU-9=TIt(ClCLR9`BE( zD&kY*lq<1Sj0u}s{@>n2@)Mj|nT-r4ck`?rpfvY#9`<>ym#Sp8kf25h;UqYf#C#mf z5#Hs{q_M5U-_mq^<;J%$^6_r65rIQUx!S>%zEO|hp-q^X=tO;Kx}CwZf4}!;{RUS7 zCT}>eI|h+HR2)hJ!g9}yAL!rW9+*RwrtTZ$Yg|St8nz6HoI0Kd_WBzSErJc-5`P z<><~)LuTiFnX}FNYOFn%C&cx7_z!g0eU*;IBb!BA=StE*o6N{sJICq6E@w$?UcHOM zzLnkQXWODH@}`wtlEm+tCv{mNNhx=ca09Z)npLx>MkvR@K-Gi#@x?sxPOXPWeV-on z=7OHEJ7;N_6=ppNNCxt|4EA>xHZZlmp#;Z(54tlWZstlowZHM(m!=<26+eGG(_K~I z0sa&&9iGQ#b-_n^q~f~zNVF(XZ#h!8%95F}Rf|`z^&+q~;hH+1pEOu>?Ho}o9#s7J zSa-|$g^|EO&o$omm1{8Ss;Mri!~+i{K6MiiSJQh@U-7H=Cv54#Hgr-B>skG><*biQ zTw+*94kL%`YB7h%&{wmkw%^rKj zq5r+Sj1@Q6>N?Q<RMg|}rZEKN;q+XG8sQ@B2fqKV>W(8EgI1>jiji>GfU)w(g}|N3zkvglv#Z?Dhjt+ z-os8A+3jto)ySkPtX-8oK9-U#aN?e@TMfpvgvxUHyadH$ApUsKp+ENUX(d6N=0i`) z=+0FIV{{02+!g&McYDuzh2?~7dQ9#HkmF8`P|0`X+S5r?z0UUb3acRHn9A9OONLkL z^d*9zK@56Lamn@aEmsNG)?*;wT>EupJ>g4_uQRQmga?(0>vvv?>@AX;Sbm##=0qaN z>YN|}E(q(P?*WqcM?~}qZu1F#Tjb8iuvehUPy?_goG5K9F%|AdiZ_->^TOf1I7AaffgU!D~W=FJO$rO%ORY4o1U zC(4s=#~9Xm+?mA%afX9Fw1U^^?MuTfW59LRHt`JD5_7QOjIU0itYeLgL?zy-{IMTg%0|MQg{f7mn(vg*hAHNYO#Mtppz$eZN#Bwq0Go@#ph%tQ&DdYNR0woA zyczU(EWK#saU0GN>k0m7_v^uJ_WlfariJ7UQW$b_i-dp@-24WbZQ^{Aj>=_~w}3k=BU2NTK%?F|6LYKgpmXQ%wSi6FK|JwFgf6@`zOQNFr9Xe!!FcY-Ok`wXX8zzB` zc<;lVRY~`@x$8&?K2E*M1Epo|To#SfyqSub(k>|U)dY&`Ayc(ns)Jmex(lxesc9!7gf0V~)J+=_fan6at zny(;N$4e3zdw)<*u40wXNKy(yHkImIdYt-68EfOmiO*t}s*;>#`%5M~syGj(1ul>6 z_J+`c^aRWHj;p42+HBhV*^z|Gz2ULJYoUIT;6X)bM?;80N>_MuQk1T^sO&IOVshh` zkaB6ZhJ=eXO3~%i9WWEJqk~6>Hv28|W)s{I>AOu>;1w@_qEt+bE))At zn4pf*40Jx$&j>9lXo3j+@^i$qh~$boXDZB`3&|Kuf)pDIDM#}3RLg0Y^{eV|_T(MP zRET0hSd|VH@%(vr)TtY_>*hu+c*g#|>gUrlM73^SO)I!QR9SL8?S_LVldqpduDiq7 z09*hj+}Z=z;QW5OZhvHD22Br1AP-4k*2#|DGj_5RkJ^=H$?(cWpl@tCfI(7^K#>1-az>!{ zSfL_kxE`=9#{$~Zy~?84%BJ}pj9=7H!K)L-DaJ` zDu5SqHQrT@^_Pyz$-P-Ec07U4G~08!T~bde4|zGN%72pG*PYM$>RC-q&4owrQ(Y_D zLL|xhf&C=v^2+Xg$;oZq&oSoew0w&fAQ1e_pZac|ZfUrzPf z`SHGo=IYptj*-&s>BbWY(`$RR*zY9niih6{XK1;fE_8W_BaHi0dQH2bk$-*r3BFwZ z4)MnYuvAHr0Dq@;hk>{uC#Ks~mvE9>5yaWB>x5=Y>`Ag5%a?3@_r}a0JYsvHbwn|q zJZu}#d7jdb7Fpg!R;FD?`aQR0(>cVZz5fStJ}!`W-atq%@FO}ogA=}VHLW3bAG!C= z7eU%>N2Fih9JQbvmtD|dtL67an4sz14sTpw4LI*i99=N|f>*QyZ&)}qo30Z5SxFVO6vs%MV$E)ZS?^#4NtrECW+9XUKG`powTfs`UcjI6Y37 zZ3{Gpc|?L;{Tx_NIsIuujHF5&9$-91th4_yDD=W!|!^U94dVsBc7)XDd*ix@J{b4^F1F8 zLMv%t!L&^FQ|m_0d@-G!XD!yZ*UBw5G&Ni1XhEBY*`9DqDqpbJStNY!5X2P3x|& zWoYQyp+z(ey5)K@OOd7~7xRWypIxOvd82!k)6|*+&q^6Ay*EW~NdE3skg~;HZq~w~ zRhMTj^>)#atY-Uh5-+XVwB7^Vsj%t(b{^-EoaVWi2WEwnu<6!KoBMzcbvqg8rEr`F z;v1cmt&t$Ci6XtDZ^&ag%gGS;l;`f>iBem6ovXp4S^UQ!QJG^p&0=i+0;YA+Bg^cg zu_^k8YV(Iv1#EDPq}_mAJkI52)dXKbsb^;1`v~M8F&Zd?3~Y9}Nul^zl$rmfyYBaQ z9Z+Ci#QQkr&VrfKiZ>nDHBE8e3~im%(3iDv`2wZHP$z7 z1c+qW9L&%Tjpu;s2XTyg_VE4}9{vCO=hCB}{jX|i`Pu}|2yOXpZb5G z^4tZctp9dv{5?D=;D5RuOij-hBs2YYXrY-g!~ej$i)fSn#VeaJx6-YDjgdB$`*jbm zw7-}cAOb9RA?0#4RSV$DZ3XVou-|ngM_$UcKCLTtL-A~HXoFwW2}d$hV*0d9>&loI z(9>Gq#6;(pR)CHt4Yq_D*C^*(MXe$AAAQFXdV5en?@N6N0jj8po5 zoW*~R4oo%Ea^{GA`6Z2?whH^Ga#oxzZSJ*(>7?NO#WR1K-_rh)FP}e|nE&@v3vap> zMQNXYt@@eB^1oTu{~ope|Fv61D4ovYkd^@I2OX^uoyIklzbr#zvj#qOSvOiCsL@mP zNj?qNA`KtQnd>cXjXRk0YReG7wMSct3W%9o@MEP@-p^`CrKUn;&AhY0$Cs zJ97KO_(_@@e9=So2`NupFFe}Pa$HiepYr{}<7fqflU&(E`~>;93ZOSCxvL-OQSWm7 zPx*1839Y#V5Wv{a8SBug!1MQ%3k;2oQ>18BiyaaYj_zm8$yw-A+{*s>(?ctqh+R+e z6%i4Uh`_k_?1=nbo+{0$Dtmt~k6H7E!^OuI6Td#Zt)H}HFq-TpnL#C?zW7HDfe-(3 zOy-_At4PsLA1Dus7Vmrc_()Y)Gc*IrIm#>F-}{*(u6JOFH=o=+d1O()=;QBi#XJtc zM^$P?@BQL#y5nO46m%raP=l{w0LO(F!V4{tX`rVO z2Bsu=8c2_SS+bH478yiyM&G=7gYcVEomx|8VCG;i>3M4rXcgW06;KkCARlDyul1i} z|0BhJ1w2@E>GmQ(g2|4N#lg|h9svbZ#s9fPt6|@pd+mO&Z8g`LmZW~u(ec{rv((Rz zPlkJ=cD=m42gz|y%gTmFRcUJf8*9J#nqGfC@a27H*4|qy{{MB|>WUtf7H&;{D-fU}P+3alHwy}YVxY5tCB$3elasn~sb}FJ?slmE^wk zZ}C$XUU=VRA~?}F6gkV_simno_36{Y&QjQ2S`mjt+qa{(TKSTGXdHAYm72i(v30w{ zY)OTW!oB4V&aO97=8R-3)A{LvS2@duJHsvqUx=VDdE7+=puC_ZP|-!e3H=TFw}oNt z5_W_YOKgl29Zs-!?4>iL$}CWsPQECH5pe+_7akwU97#`+-#Ys#eWIhyN_ag!>Bn+# z+8)CRK%cSP^=HTm6JP~NZkH&wCo4hex4g7wI>l_2`xu4*;5sGbr85RSXRPXK&v~i2p0K#9M<)t#kvx_*`dsGs$Pw#)l`2 z<)9gq>+8Dvb9h?zY;S#5>1cNnqaz+#xXxJkY-FHyKi#YfDIX-N8Api48(~K&y4cY% zYm^OwZGV711}11&AN_J_qF8a)R1G6PgQdzt~4GO)2Be@rqD5ejSyB z3P6p!H`~-{Vb4(X5W)gpF#rlXgcmD8E{C528pVPbDI5Wyeh@MQgw%jHXQ?oVl0T1)KJ zuwE$a>c}jCdJ@L(+`Y$-zJMcZ`y83gsxxT804eWLXPT4Zxn`)^jN1H>1_$NsNS8E< zf|y0vy&?FL0!+_tP|@#O;v`&9O$I8c8NoEaSE0Hzuiku11%MUSR)LVoNnex3$DL^B zA0#fI+$91L^aYDC#&QU;i-Gj{9peLaCEMW1=&|$q9T5-akut><>dfODzgpUmiG%Hs zXFp7dad9j?#mBDTXU27n+Ppm;G2&PbDo!G`B4cI0hac$;Eh6>xDvM zMZ5-7SX1-Qo^GJ!g*0?X|I;(e$GVmEGAxs=&yeRL4JI5Qun_@^Rs+V*!NsBa@u^`R z{-%ffHgy@dKUHTBt#RFM;-i_h1s7MaSI|08(5d=b^$(GpT=R*2vO{3z-lfygu``WH zJ;>1SuJC<^@ak}Q>MVP_B4#%-^7J9=tH4`ZAzlyGdDlS2uu;&UPipT2{`bcKyG+FE zv{D}#{mn3a%Rp6O-^&TYmMf?7n(SL#&6+p+Dl#glo^-D+7&fw$^=bj4ejk;R$jHy+z9lmOV28b#0*n3AegRT5Vjc=khj_!_7nefSmya2C^(*CXuuW)LtrV88n*PZ$$7-*B zh6FBk!t)i-tl8G}@QsI+ibwIEw!wYxcOk|o`Q$2BwtyM}0>4Olkace612@VZix|Go z`gkmVGUuFq%ir-(7~n?L>zb3-dMbhnANixawS?+(drkWxiYh40h4FQ$%c#u`+b z(-@KlU}>BK^hZ4(37ybc*4!VHcAWX>v?z%Z7YpN*fWNi1fio{EHMJuApF?{e)_Pby z)E6-~tkJip9rI(%y6J5RJGE*21fH;^pWn)R1a61i?&^=#;F@`FZ+*{wOGPt77ZNrUX>1lfHYC07ZFs7f`UMhl0Za2LJ_4% z@4ZM@=>j51Ni1|o=mJs%gwTVCC<$F^fP4qM@B4Y5Z>{&o%MTW7)?v=foHKi_y{~KU zqmgg0Piy|iR}slQ^Uj$~z)^q6g+D&EGN7p;U9bW@L9aCxqu) z6mcadJ5#0GzM0Yg=d&iQyD7z_ci+7a8MD=n^$-%e(><+&*}trPoq2I(xSBx1u@b6a zQW26HRkwnIFZ26Z$SbGVdv|K?MyCb@X0=^t@P~-)B32YDMc>&aCYB;6=?fKciU>$2 zt;(P!@4FjwVVuu6eoG!xm)2gUh2?*1vVE=!cug}v1l1o9Qjj(Sg9m|a%q1t=d|8(3!RLdPU>h^VI|pI? zz-D3P3{36aQ(rem9CQ*sok(( z)_1!bm&{{TSJiQ{1o9;|!jWsjcAWy*(#RIiZIPKzU=Mo5r~^#`)`?=Q^FpnA9KX9( z<)(LnXgLQOFk6N=$JVuKSZiBdAhIly+iu(J`+2+8=lSsKi+OEtAnNpnv3Vi=Ji^1K z3}muoT`=v?(hwDhM)(Z!fcz|uqdcppR^He{;-1I!tN)Em0yXBuA592HI zJ@SE<-e8YC^L2m%l6?#plO1l80FCJfL2qld$s&w8$!Q8LA#3)-LlG2V`RL*&Tx}Yd z$Jpz`#n_{KyM+eMw;3%zg9v*nJcj(5DoYz;kgIOeV~v)#t>AOKkEg&&$h^;hbmq8q zQ0r@b%w!+rMynZ?L9WmoLDv&IGi99;r^cbig^uIoPZ)%JhlfUNmEG>jj^@|eZ%WapWu3HlOZ9;S2q9` z;M$mBow4@$vWR3Ppb=shri3_3%Sp*~#>H6TY=yRbLL&w2LE6DsYu zw1z9zAz-&O>*Om&-ffr8YjzIjzvCxkEsKuCa=2O8UuI0`_-RPpH}))LSoWt}YZ$%E z3^sErq!lb++;B&g%;ouyH#*S*Z7oH|)M|3FZGP@oT*Ch7`haT{GF?B&W-it+@Y<@P zBfOeI`EQowMAK`aUrcXDgZAfOmt}q&aL|WUB!g*8AtBICJtE*sTmU?G4}j{d^~>{l zYHG@!w=_?z&$Z?6e9w>xcnLplOkMw;8b(#Z6~Qj~Eudj`0Tt29>G>*t+nnPAH5X)$ zG162miz3+^)~M^3CWd{nTm`wYOX1QdS$UV^K6UobZ6@3&@;-;q((r6O_o1Y~H}rv3 zTRnGXAKxom0x~QZtEJyhH@4n3C%+H@5iYy8{0K;Tb$WC`Jw9dW*+xNY;z0c)bFr-1 z{&Uan^MV;4IMiy5h3t6&0TbPZ6%5X?MSfhwg!i5O#ItdG?Eu!R+Tn5fB__e$CV|dw zvg!+W12VyLyH^yn>3%137Wb6Md0I|f0c90F)k|rCG2*BU`Yy4hEKe}-(iM~CJuN8E zj#OTn1T3XM0SYB=TV0R_xicSbVf$>ckOz_X$ChGq8vXH&mGJOZ4*#@n@h(xtgCUpx zRh*uyM!3Uq)15WOl)UTO;dBNiCQ@;a7yA_TN=N?SYhN$y4Jyt0!Y$&nKlf_?6FMpb zrgP!5%5KFPaS%xQpn1IEM-4bBJgK z-Og3rhT}Am!gN5G%vYLA*9j1zD~$%>i@AH1EW9`Oxc@rYbS6Uus#R3LdDPW)qiqhp z&9DkkQ)MGH`fI6jPIr85jsLms&QJjzmz-_DWhB|_uowVwNxiWH0fVNkR2U67S{^DTO$^8B$K0K)%Xy$@tM zip@&{!<2_%LP(BlEenfpd$p2+&o@)ku&|Lw3{iltiN{1R6^3(Uh=@Qi`9e3Y#}(2_ zggw=LE>a-WOd0dX_PW8f=&RQlK_>ccz57QSId)3}xigIp-Zj%}#O=xPX$`d}KMAwB zGr5if&y1crwQX}-duX@(@D|b6)R!>*r&vR|k#_h6mF-beLGUcaubIYOrNww|xv>gb zlk(z%R?HWxzdWaism~%f0yPTBW~Y6mnbdibWwE6{BM9DdZc8G(dl8X zs#n9qudlC*lg&59Ta3Tg7)H(-N?2)ZnRE2XQ(~F0WO&xEyoL-1OzY(9ET9jM#xbf( zzbFYHLL8%xGia9FO$ZlxU-9-ygiTcd%vkOO9768-PlxpZN#6}nyqeD{-`X=FlLRI9 z-Z4zrUD<`2A4?EcW&S;;p?b_K8Rb>V<7=49FF#h^RnDYz1J&sH{^3?05XRLdSc#py zEBwGIZC2yE@%;lZogNjR6+nio`PR$7SXd0q=tmJA%6!?)@KSdsh7mK2xrQ?bvf$4Z zNGaynD+kdW^B`H8a_^QToG+d}_AL6<#_f7peSZiyqCeq;TXg zf06cf5WDsT15AZ$tbuWONE57SC^2^tANAugEs}Cw9w`H=UbMLkV+ARZJ4hI?fNy8{rSiWrr`mJJ{qR?RH`~5E5OSm}X42a*gcr6(^UEP|ei6 zw_?cw19MV7f)k$BkqZQdT_vjScI7Cu!qw)0nQ)WpMgip@Tt0LSvcK69kbGffmpmtG z>R#dk8z1+LRcQD^1;dWNTGE_ZpjHo2ARZo|hObzd8@F;8EuZwXj$cT)uT-RiqUT{= zV0*&xEeXC>9+)-JnH?P%^ycTn-pk-1?TP?1sBNwQCP~U8$10=2u)u9gBys*3tQ0bF zD?3O?FO%@Pbv)*I!DbubQSxuNX|@}53C6toA)0$SXN9qt?igg zx5J=5gBM0^n9V!#poc!%#?T<0Y?)z{?7dN)n{-ypLI@`Y9@{t`tG!;BQ z#uELmZfBcvD^ebeHL&u3VZ-_(%I+(PoEy#jQxdLX(6qj>AxktbaDrM4zo?tQ zhc#vxvh~aK3Ef6*$wnxfyV>mP$?oJ-HFM**?B6Ut9T`u-Qm?ScJ43Kc=#FHe+liS? zd0$o+a2!l?h;+l|v3ZK=YAFGv1PV0xh9^5eh#)#%}LHYZb)5 z-#GMQA9?dyOF`s`0o5y4P}eSgtp4gw4HeR0j4^f(D>pu{wbzI7f!7i*Ogp~#BI(T2 zd*>-vbVZt0WX~f5{TE77n?8l2LGw=}{cLScr)&V6uEWZ!(H(9(aJJs}n=hbAw{lv< zai-*MZeCLAYFFjkA>n^2WH* zJNWQdjS)C{yQOi@89`sk-cqgB)%l@9E8dDXiNBh9%!%a86RweUcctq0uX|Z*DCrjQ zY%}6q+P8hE#oC@mKAT_F*f_7H?&K4nYZFe7u8=%2*%f3^Bz=UySyi0XVs-AeGGXL5(!lWqfSPw>;31FoTKm@tOj+L(}M zW_kgZGYxoc%(bux>$9K%t+ng4Rwbx8t}u7D0E5Zz)$Zoq4={(fl8?rSwt9Ci79#!t z(o39QzX*QNJ~X7$YhR%6yZ?L3Pb3Y?T#{EV9~xlp6fzsxdK=QR?-p>e@wc8NOY+%O)Qk`y-B4 z!)-v|J@{GZXA4k#h*wB{*YYaeA(#dU>qrwDlR35bT>W`M!;oY802*mQsX1`n72;1a z(1-_t;gBA}yeH$KkW`9-wOPYxkRnLgdf4K)tCZBE;I)M_@nA|I`;8Y7(-nZTg6y`E z)rA!FUyKkBwLnVkjN_CU4Byt#Az6_uh7Vq-9||qm@l{i}+v_vj+*_jB39fscu(ZvU z^aP1%uRp#L$VE@Lk_5iMMbxmv864*Yuj^h*wv$aZyc4%M==(6H|+RS<_1FD1fArm=EL zxjH^1-lKvhfiknxlkyhzESc?vi$VK}xW^Kn)8?3MWjAmQxj9%BuJut+6m}zYWws5a ziPS(g%+1kbE>kM=-Gq&Z1!dGS?ZrZ?7#Vi(n)*YJmOF+mnZryytZ5M8+Y**EO-$l9(>s*0W%v5`bthdz+igPFhh z)YU09*oi>AdP0#)fv0SRhPy+IuSb$uO*jSt3h9 zUEjN%BFY#A!IQ?RW=ukaXaDQJOg87q8HMVnb~BckHuf%%*2i? zj(Obp-_i)2UO9nTpk0LjH&A9^ps%`TXdSXa6#q3+pEmAAydS{4t@z7biSz>wQp(Q_ zs*6_&1JeF?AMyU%W%twbAw~gw(qEqyt%DS~&3`x1 zJZg{@b4n*oi?h8r{AXHa+qd1-G9#HC;yzh7#hglyY}C5@trQ0O-NseEp75l%D<0B z|7n>Q#VgD*z^w^*+;+`QePxRYJzld48FayNIYW(cp0HbO@MW$`6BNTdwC;CVP?Z zO>xRy7Q-bKorY+6mu6??*65_qS}nOqpM_a#!3<#+6>ARoan6%@Lr=SnUa1m^8tgmr zY14azb<_kV45Gp0r0->aPobwI~AYV5x4nR~K z7lr($^)jLknjF{?jC3eWb|bIW_ZMN8{1~#F|MJ|7>UEr_sUlh2>`%2eD z!R@GpZOo22f((vax>P>K$g1DEj|K0)=9n6Rf!1Y_3S&=Tvg`VY++UnqEf@+c=a?nN z6~NeQA^idSpMpphMAwH0fefuxy}_UW&Z(Ow$szryTO_H@UVq)=<8$>wZSOE}tTxG& zpo_PMUB}f$9MpnVceZ87@;U+T(_WvdRu6lggUo_Fnv5QmwviRM?}ypJ=wtDI4?_Kh zYoxgdG>uJY@+L+GOhfxxx}BA3*dv>55?kF_e^LviB=E1f;;6(}V$ zlGyNKEM{K_v|sb6n0R>}3nO{G+0bfSFUYVUHD`J}gUB`|#=Xa;az?f$KL}Dy6YVu6 z$m8F{?7_6}&vQ7crAUF5nBNE#85CZTI*9b+nKoG6PS#o)z}T*Q#4AX@HC$)1tSI2MS!@5P`@r<;<^e8LiGYK06`Ie z4s0a)<##uH7vFzn-PX)_VS4U2hWD8OK9Ny7qz&3)vA<~k(db5P7IZcOOlE+h%{<(2 z!5xs}HRiK2T9`T~>G5{*VEm+Q5rmR;Ehjadn zMUhJDHZYge=rdmAvfZ0e9Lp=GUc89OoqFx?0k8ml^{;C4hckr=n=-oo+!ZuUnU2kc zDd_fT@Do%3rG|bU=ruO5I_CDcay6#G^3`fqpV^&mmfq|YJ3rz(J+&6fs`Xk{j3h?j zV;(1EMOIrP_9FH6g;SH6@;=?qz6B%a&jneMM^8s=l2dmQiXdy``TOWv3FPcf(EiIg zDrvPxFFvDk-Yv}IhvKcTb^5XVbjK!)=?fZeP{T4Vxj7q4O?+(*Yp&f9RXP-YM2q-I z^>6Py3VR)LN}@`$TC3)0TSr99HMBY&L%M{r)pyt4z*{p++}A9y z-um9u7kPM(S$!zSrvA}18!U*th7e>vSggVHw9Lu`@Gs^Z=EO3pK-hdKvDAiRXOb9$zmPOIm8EJ{9I&*W z;-u+dFe2P$qM7D}-c-TO(ZSGaWFgrg+@|NIU3L2bC5Tc{ z_30u%Cl1q5PU?9plbV+m+RonFXuoFhcbRTkN82PO&wUqvMTFVBNgHEy-wL&XPi=$G zFIs95W@iC=4`+210Ns)tO4<$mw1sG-$V?V;ZcIj^7LyJax^9xYb75=S$)%lr&Y{Q0 z18v*jeH89X&V2#WzHGt$CGzSwr%1mkz_rhlV3a$ySRhCRSnmG`SB1E^%+RE8Fmxx4 zQ@G4L=SXPHGvsM5y#8n<0QL|im6b-b0mI!TTsRVfDUNL)mSO#rD0bnOB@px#35kXI z@izT=WYgF1znMS}?Wh;zv1Uid*l!2=KTc?mTt}%sIsLWuF)^cAUqyF? zoPSDr4r^{JJb(B)k4hv_M}tv!+4P$~kDiz7)v4cG1{=`(;!o0(93JIuwu*`yaGXAw z9z*T$gDsAs%CMEjl`r_epa4B`;<2Y zfMI?zp-@N&{@+-$i20&O;o<)O#-u3zo)It2w12U)$_b!W_tuvnp7B-r592%SNOF&Z z8^}L|Duv`>JV@ARjQEvD7DZM(5LyUL)4he7(aG|w!7$ePX%O$hA~(kW?5A^N-bNCF z%ykKqe@A-z%>+;co!o_Nmkb`#`lHK-#8<}xp@)=yIj{Wt{5zVO zMMv=`k4}&M(M~}tcaVxkO~P?Aw}&5in_1$?)LS%#6j|2SWBqbbE2=t1<(Jk&Fi|dL z)rjFRsv@9Pewq`B#M+YF5|goAxJ|5_OpQAKk!;ry``02lU+Nc7h@qx`2(wvk)a})z zviCW+-F)V=(zkk%i1C6s7VUVZLBaR=?EY*Lc})#QV!BnLr~)8h@W?kyps z1`l`@u;PUT`CD8H_QfJ|JI z*6Ma>;m`O*&&RADq3Khy??tgg>5Xa0CVObtt|&+*0OH!)PLf2eMoVf;tX{y(13~S0 zF}LhNWt=N+GJTo7EHe=$P>zS`ytNA1MbX>(q!Cx@1d*P!+d<^kHb595v>8S3qJvs% zXAWr$3AUg6R02f1BaUp(L>CM#B5$*i#iwT#ZHFm>P@-jg)MW*hS zF4g--hvmIQ{XS;YPSAaPl&&>d0OOyS^cirIRYy%x=pbr|qxrV9{S^GLLFsz$`Mvk; zaac{pcf&O+NyEX1`dd7q&Eh+9_A`~pdBu6+FBi}J$oUl=Lia$ID0-J}W=HyczgD|v zWCS3V9L}4GuQ-=q6lk-!b17|-C;o_ZFy0_+_6PJkhpaj_Fe|bfO7G2ZJ-J5?xt7wB z9;8ls)3HN|DG8bi?+w0RNg&`GRZ0#-ZQ4nZYFCqP3G-mFn>Cnasbt64jMsYSkf zwPHzjYexx5nko?4wQ~ApXs*GSYDBqBVbF`B^DZgkUcXr}-cNmu62{a(E+PyOJGn|B zE9}elU^UQ0MooTTr>Ld;9=YHu;vqfO?kKRM_6$x~CKX~lgO27v!Aajx%-(#*?7WkFW?y=s;)L2g zC^E;w9gfD|v;!r|py(@0C@Hw%XwItcDDuI!Ai2ZN+Fe!Y;G9%XLmMK$Yju^zYTp4T zDo{m(u_fH=8-4ZjNQ*AFR*~bo{`RqF+a=~Wg0=jtW~!%TOlo4~rq~Q?b}4s?{N^A> zy!PT`TjyL~GEeIhp_6@N+!WBSs^os9bB_KRjEWn?Jno4g;hh7E129^2XS!{w7R6kN zzS+C8OH`LJF()u*q)E1vw#o+0XzBLK{&BF(ebJ;l6|=Q)cEyTE2c^L{@CR=*HsI%P zs{BiRW_;uWmZ~dft(Iij26r^IWk$B6*z4%Gb)JUNF34Fa(1`*Y2jgUkI4s zxxQM>=JNc|nj{w0xA#}))iJJHItRIkYj`L^*Ka zRbXY1iL=L8407Y!6aC`=-w`01UvXU|QS!_050}Rl zAr|Snj}`(es5d>a$by@XrgC)c27m6c6nlDgN3p7UqGfbnnFDVd71@WQhW)Bp=A;Vt zwLIu8kq#&0aA%IBA?VV$|JgnD+4{Ve zaXWO0Qb$l4_q$gbj9tWr;8!MrH}?+C7vIY-t_HS2ZaMaO(gQ*1*dbj)Z%}ITX9vZZ z*KJ4q5!-Bf!x`4zoAG+bb~EJG-$}F4V;V+n%-fB~j<6C4H-tI<%};tB=|5gNi`5j6 z)yPjyfv=mGytheTIhLy(UD5(RZFd2n<@SPx^aSo+h1%j8YQgKm&e zK%%I7nl5=)ds^x8Odu+{SA7N+FgmC^J zu!U*^&K4%aJJU~PGyqR|+8m2rT7o;6O020Uq^^mkYuwt1)x5rGNj8mlFZ>pwgM<@r zV)(Hcy&oer7&aYzS0llUKTWf9=tUAA$}4u@V)Z6q(q-#C5ZTv7NU3lU6Pq>o2y4^< zjJwgOGm6ZH4)N-)m3Oxv%Rs9vTe|9#Ytt;!(1|+)pc2Prd3}nzLqh!X_ipSCdmDWd z6|_Lf$O8PAWajI(?bmD3d(+FEG}Xtq-_*T({-hV%?R$Ue{;!2yUVhSaa?J;^!&pKp z`$N0H)KX-%dc9Q2 zQ3!u1DYb7$l_ooNMgBfD+i)+HmD~;>ouHY{^wKFVd)nS51QbX4Wl|YH9*>m$#!Jro zybsJiIV}gR8a}@M>blP)n(WTXf~9Vp8kbgQOK_vzeoy&Xg*};dY671fHEx9t&l|a| zNr+#(L3E7V*5^0b%zhf7^{BSE2{ACA@lm1WBlO&^_6N7n?Y1w~PePPLR%l8u8#ctv z;{zXr%((xXjzMz z{Snovey7uWdL>K$!`Fyd+cg7U1g~`UmokVPI3Y1kPBaH(sS#k9Ez6ZO9$64-x6VQ~JNqSMVMDapsQFu-&)Tn1!}0||-onP9 z@be{kRKM%aX7|2j9%DR?A;&Xyv>cwl-rMr0&(mmh;Xaami2}$U5M-wlhqEX zqP|mLUO`fu*+Z)Br@T8;KZ!mA(O(6!lPD0kkM*3HrG-aK!TGoCtn6a5^I|h@zo~DO zt&Z=93zinrnv{XC3hv_YwNmjAW|XZ4-riKcGSN%BqH8E(s1tJSEgUs(U2C7Oq1B<8 zJ$r6Hx`zbvCnLRHK}KaDn;4$=X?(b(n8%CA`EB)E0a`5k2SZK`N5Q|_5dvq~)wr0t zQw5D*AK1i3!=G_ry41o0Gb6g^L=m5LM|V+=zmkevJr_%QZiCJW4EEhv&ZcU*=UjB% zV+_>n@>!rE0GCePfhxp_`$L%KYMHc%Mgb+1)HQMAsGomAUhEF|sHMFD;RkV>zL@#lj4-)&n8Z_iNrYM2qce(YVWC zNqV9^G=#uZg@qfV!VnhO<6}A?enI~^zxblUOf^)0c-$2wc)X$56dl;r>D8JPeACmo z6h-TG-QCe_$h^mV4=r8l2rb$pqsN)qiz7b&xRyG%6_3Q>-tS*n&s)b=&kguRTIZEA z&aVtbxaP^NW8GFpef--LO?UwZK>x7T{oG&Pxbn;gat|@@l#6rz-Xdf0`saYJQkqpq z+S^%~`My69w9wbx-D@HHzQv~xQGIWGKYi%kbq&VhCqTma`V*L7aCU6qRfgjATOe-u zSF|QZ=ZDc`qn*Wq;ccVM3yPI}8IXsg@(|q-yXLbEF!7gKLP`q43aq^3z^0A%2~gh< zk$&-fjp9jDuq?M7JXH`wekLDgArC}htL{-C*`r599Pn^05yRX^j~VJ$hwjdE+ekFo zo4PcLrEA=%>WZJ(CWEyxJZ*oAhY*1=x1gWbD~FMBsMwuHu7o* z@X;#rscO(6C(({AkpCe}{1>&IOHGwADg%urUZQZmGudc zKH6;W&!h03-8t`_rO68bVWR_4q>ZvqZZY?60WiplS^5s?d~f79!T>-&-`HVyJM7ZO zT$>u9TK%H7%_spXmwj|RCrliNzcT^U1SimSU;PtmS{AOidXS+IbxU-VHH?ZG-Yk?3 zZrPV~8akBi^aH9O@rfq_Ntj(V{5v_xfV4~0izn}nIG^It{L)J0nalaT4S&wR4DT<$ zbK&N`O~LDjd)fEgvLfF5eD3!X9%kLf_$lQYALQqYq&72#;y>S}l@$4eI_u46YYH55 zu0T@juyO-zWTImx1&HG;e^H0ssRU9{%O>i^7+F{`p3%8utM`ZhjG9>&KSmh2DJH)4 zRkZpWLA-Yo?@_)Xp59L`&|mjgX*sO@Z=*(3f6QN|G!;;3>hkC)NuRwvea;e4ub%v< zGMzNGQ(di&@MOIGH~Lh5l8&2bR}}HS9z$=bRII_IN^uUQC`a+9>t|>nudgstQlx~2 z$(+g@be?Mcaplx$$eKSHAVU*{C7sqqkV1f@Z2^eX$%Pb!I?z|_o?mCixq|{;-QD8( z=8V}5(HY|FIbkzRC{8HKsNw?cnLn32E;7_Ho|%uIr97qxh**T5A!ndg1e;SEfosUT z2bxffm{w2BV9457!>RCL7zHev3CWMl<$O9x2_Gf;7^&c)HI{K~h@-*%B(rX72oF6M z3cuv~Alzf!3YUd-;Tf${Ipt7KZW>~SQU^VjdpPr4UAB#!>Pg5~P*aY3C&o{JZw#}#eMEo8QF({pUUZ0&MSyK@);)@$26G2(qE z#>!GjbfEP<#^-Q`9r(t~YjR)lkP*1o$_kF38Yl%7%4FoVUKg&k!u?#U|N`@v4Vs|KljF)pM} zmIX!z`ci1h)m9m(U}sy!m=4Cv8NcCRIUi z_PtbzUONUZ=+uRR?BGD3-Cg27QEjxr_1g~|2enq>G$|$gvOS(afX2^G)FPiU9T0ci z@;#g9o_N^)IL-^wva!ckF7}NZJt6gfX>~ikA|+L~hErpb)o-+M_y{*6sgEV6+u%$Z z>P%fNo06DnwyVFUew#K%@e!5?uXAHQDra6D>G<{>IqMmsX3GfTZwfzUd`cgPhA_B(zPlYrngj+o{_ZJx~YPf0R5E_6}w7c(KA@#~pe72JOnY(*wU6e$N; zPfK=ZzF4J5H@!YKit~~sKPErajIrjA-2)-X*J;;h$R0*AQ(hcwy^BS#^HOx1YoK8stFku@UNPfj^?YTXVP)yZc?W^zo6FpDGw8NiWHQwBH6WYNx zMRdP=SyopH<;qPlKQX7pwkJku&hLjLNp-~hhmVqni1+J5IO0a=qwKhC0Z1W0;ifBZ zg*c~pE@heh2IF9oFw5ZDq4n4s9*pZOl~D9UBy&$_@gpob2HDLu0>0Mwz;?l7`&bkP95qRlEyzRh#{3XO?4xisHP|uHY+|fLp3?E`i2q& z+Pf0CJFyft7O%s)^paldhfBvE0yOK=@EIW zOCr16Lm=C=y~li_UvTV@f^xesvu_7;;B`%J`ZeQDw~g(G=5SKKG_vryU`?a_@k;3( zYLKSZ$jB4ad+tZ0IjO(~qEeKuvyGx+$53NsE<4*kW=Z+;&1(pw#@3iRLhp-h1<4xk z215nVJon1`DlgnnQNCy=m`xtN*BG3rq241Gt%? z$Wt<>SolPs8fe(-NQRUcG3Q{x;0GGwj(H(e+1?&G;r>Y782XS<%tdLE_Ap0k31RIz zbucIiRON{jQ@mv_%tf;Y9^yR1Jbh;BP@7PW*~H`qJ?a_^F$-x%7N>RQ5YWGIvbZbK zAs?95`3uy%z4jA4^RmM$QLxN3%Lv#6Z7Gc_Xf!0eEOFF|fRw^%EBKx0GYk6LrofG7q za{KhTp?Yy*&2?Z?CY{K0jLID!P#JD|9iRn0Mn+3v-&yYV`rxC(Uk08c;*k<0C>TmB_ojHTC z7)Wij(RY*3NtFrt77EZFWKa-E^5dL{jUf?kaD{OC;owU}C%Off7X{i9NcLZihn49o z=i#*`HP3kWOeLrGRy0KhS;8=EGF+zJBUqU$pr(YGlg+8S9L6RbH(DD2R4jSc`vczU1|UPCkA_bg8j~%7ftt~zTx=uKZLk@|b(w>)O8Xf7p=N9Vhd;@wCu5kZ4tYkq zSV~UuU-OAohwU@Eu8x{t^=zO}5Kb%MUFuh`9;-BQXi22+93fnmF zQ*6FzhuuGJV%82Dx;Y_obCo`^E5E6lKmlf}?Bq<~hfB_b9Iz3(<;*_}-N)k8V-3QR zX8@(TiSdu3Opu%KzJEXHN$6B)3*=(OD12i`WYU9$>!R`Gar>pbxjVQcbwIxsWMdzg znz)6E*S>z&_(WLh0&~Z8V>4hWo`KA2b}y-O7^kCcz`v;xWP~J}LM+=J^}0Lm6NDd6 zK}|LYQtB4SHhZ9!M4r@P^ozu-_S+Ine62=KZ=GZF=R??)(ArQu^4&XAE?N*#!LT=H;k)4U(&bWV552kV=f1kbTN6lIM5g3ixwrt^BNTj15JLfR zk6!oHVp66j9#<7xq{PG&!&C^-ch7v&VE$z~!lkbvSyHNfbv3S406aFuqE%M3f~1vQHXQ- z22en6O18&+_Z!~1@IXw1ag54OG~K5)EUe$ttMMyv^#E%SQ5-d^sP9K!3gjOL%(>!txcD z6ov9*EN5~g#s)m*52||`w}PdikJ5;vpkyN2P{sQVJYQH}V585Ig3mZh7 z&tBXVOe{$4NjrL;+G6Y{zugxM;T0N&Q7N$_wX<0bq(pBB>;llW*)|d*`JSX|f z-E$V{5G~zPg_8S~+?lII!UJRtE?QR`0I2{Xi7ro4mpYnXXC|gL+%p;ys*(dT&*GfH$m>!?^ zLp0hz?F-icMu=jkUkTtaad5##c!LBDpPxC-oO**%bf#tuTEhE>KlM{Z#EG5BOTOJG zv913>Jmx8j{*zr6+7Y* z^m0w~;R8go{dRXyM*wx50G1uEDLH(%zr*NEK}+g;&LSxf&atgu^1~{>S<6bqk33&q zSSzI;V_#QB4_1B#OZGJ$IHX|jhrT}t|J^o zGo6r&Tgonzc;rBUOlQ@o=p?n1Z(@VI14S zw@xX|xR=22L^pz7Jq;Rm;T!M|P`;hKp_4OOjB(tgWZ zZ_U6B*Eo~vb}q%QG>57aAKGj_f)WkQh65F6*ImVND(Ha>@N$`hQFvL=dbHWmJZGKL7aF(%^%v^*6dM?x_zwOnvh*{EG^*j6+5 zD_5b!0&Oj6`iAx>mZpU)Cf>%T2W^V}DTd2qVkWckKNijh^(l20jRHv>apRo$Pwe?e zqIsyJ3v|YfFK!GKh({(jPtZBq z$O5?=D4apX*tRm&$QbXsTP~+1Z64&EvdwSx(7#000H}229L&jDb~#!!J&<-9NKh%3 zyxr=@TM;QHe?Qt_7#BC_9?F%wG?PiusQ5k8)#T?ja-*blJXtcD@6zO?ju6QQ2#SH`WM_YPh&g z=b=#Qi3*0ldj(7KD*{q6df((x_0J0t>ho=Z==39n#n*c5Q=vaLURpRzC#hR2c~FNo za!vE%-h^9`x$)GMw%X9RB~*%6*306&DSg0ovT4TAHx+e&mc;b{>*XAYa5glLLS8y@7p5*V_W*4k_n3 zVH%9zSAnz_*LrxU70P*UmMZA~W9zM=+T6M}&;)lV?(XjH5-0_N6e;df+={zx+^xj} zh2l_J+=~|pZGqw*in|9+y7%{=aW2jnxyZ$vm3PfG*OX^kg670L5tVlb-n)T&J{QXF zoyO>tj31bRG09uLm0Th|+7QLN?Tb<#Vojc1o$>~0g_a&O;Crse}>%o2`i#GikbS)N3S z@ax^jBVMYsW#S+T{ z&?aKzi;Faewkv;{#dm;~gmRBmtaz&K@}&s&v{beMA#$$5n*apBOMl z5zhh*B8&86+UXvJYh%E~X^~x`SgH`MIP=2RjxdYd9AZo&iY+C03O~P600`U-V*FOicP+3jo(L0GoQhKdKs2qi5MQlz^=DP3g2J&==PxQT^yRhc*R9Am zx%)rOoZTp%9|?O*mb>Knm;Jt2DZZ%-BX>K=JyAThRrlouO2;R!p-)7v ziF}=+i?FcrPP2&I@iqDfrb!E=^wnN~mHdM)zmybY5|ZjI;`uaVM&vdxg8O=c&= zZ9i8iB2~BkUy$aNk`S}GM`V?9Ww1*GjmBHL=b8`9gnw;W>u*4H+;rT+Dk zXTJ*vMXK2>gl}(M^&${dd#Bz*0|nsFsF7mKre*ev0}GkmId%%X1skf=^D+Re$cB}onhn#jdbBJuUA z|F$AlE;pk@RK1cLXs`!iOQqLVOh@R4+nr0Vk{Ah2+k}iS!w7l{j4cZ}LVqJWn}8$$ zk)$5Q4bcveTC5aI9$wGnlITWj2BeGAkwLF(VD8{ z-Dc(+vO%|YrAZA!jA3lw%e3{qADaiJ4gCl`BuU972_w}l#Qq}S=^6hPu~*HP^hfpi zMf~bdj;7Da6EFgN`&jLnJ+<>dLW;pIJhYlNx`wH@(~_Uez*0}tPK#!@8{knG{NWxxHq?Lg;yW%uXbBMoAXG=U13z`yjk!D3sqtm$sc2K_abnR+_x*? z(naNgBZEZSkqeXd!0X41o(qd1d-Cd7r_f#%@EB{bR!EQiiOrF*QB21g>C+Ve>q*@{7fI>x6@Q2)a36IbH zN2Y-^@gU2Se7IgUxIAar`UJ-LUBX;U7EoX3%Y>*g`~PW@0mcOK;(`&!@lr13M^A@Q ztfl0sLzn~Gx@lNN*ZToa_(P;g+0kG<(Wz1xY-%BD!}%*-+bZS~9(}u^YE$S@E?8ch z9j1sRMnE;ygn!C#>WXpvspLPb=-;YT0p^1BP@#7{b6Us)nM)Rc)TpJQ`uD2s2xuG- zTr&VSi}!tLLD>deo^6ja1+VK`GS79xr<+{qE+)+T{;UUwqO?X|X|3a07BViL+IU1U zJ(!U$SM&jm@FKTf1*rq10WJwLttUtp5mY%a(Uzq9|COY?>fgo_ul<77{}%wKSJIl;%hUz2HBL;T=az? zxs~=l1KU~?$cqS8?IRxumKnps1llaJt_U|jvivMQG1&Kii3>)QIUvTHCMzKTrG34Z zgG5;vI^|DS07p0+8#IWyVMAKt^tg7B#f&W}WH}v|?e=#-T~RLL;=0+fYt^F%Q?;}h|mOIvS8qDjF=h2rJ zL*)VpiCNv|$(94x=aA;>{<$I|Wa#L_*W5EVUlM{Kw5G=28adCusxt&pHEoT1f+9}$KToe^h!+?GU#d0a!DeNrrL-r}dvhlPG^ zB)@*et+ih=BB?hqa*}2(G&x%*vb;S6X}ajx5KiV=^HFot-`5m@X{)b21{B=b-Zbe;d(x#7yB6FCT=mncnELe(hI zDAdT=n8f#t(nir%_3a%lWD!T+#*^x@`zT>nzZX?Hsi*hkp<^D&EWz=?z(;OEg%1Hg zvib0tVmXO-BRczR#V{NB~C%P8@{GtjH z62J;^xMts2=Ix8I?e)uyfJMZQsuASK?H$l*j-0&hsdERy_hM+psZTlH|I|2bZ~z<- z-}T^?!Hoh5HS>1wnkyS|SdzzI?1i;>bW7=~$f7)%ReViFGln|cvYaxWQVI^l*lg-q z_^~C>O*@nQF0O! zbWj#ltpwViPh)rzT1@?f8v<`979*WzFToE|-UqAR{1S^0wRp!h6!bCf#7UGg4qUcI z30oWFRr%`hI?i)c-i{o^w#ml4MsWY@vmWS&b_$eU*r*$P-Oo}k)+pn_XMBdP*@FJ< ztLB*2x2+L0fC3OMKGo_;G$W(ilgbzNsqdZQwQ5Z&#CnIT4D*9hjT~sp3KTW_8!zPD z8a>~KJ=+qhQ>AsvW=YAFJ#lj~HYo`(0%5O@HU~&YbWslk8B2hoK0w`HU&`PDi>_Rs zzkEaNk{_ZV2p`p|pDx+DSjOsm=~w`t$z@4y4GN}`Vx~UOmzR5o%!@u(4wvx}KA+;) z`Mtrg4r`gd|ApHPMi18Tym5v|=0W@TB^kKVT!f8kiV6ka$9g59HQak&*rgaTjWmgb zP^D&jz|-)d`;N2an+geGX%k<(yck8t9BsclYD1RAA$uX!)P`+qsi#fR^_tn4xkaVn zUM~yU7fpZ*k?_ZF_ZgyibC5!gO$~?3T8Mwg);}D?Aa=MDG|rDF(n3oJaC|%A$*tjj zZ}8%X0wgqpu_a+kXPdN4=->JMD;CAHdrkoEU={O*(W5xHds?mML_Y!XcRzn{&T-M# zO@^v{wi5h0u;TM~>+hosGU)uj|LCg{C4xWU{{&0>sq{Y6@7yx?T6{v9U!?s*(}ibh z5OIwINyiwua1WQgI#f$GLKg=Hs1)eKddNXp+FdtnYT}JAFlACWt>o#Vl}Ugp)$&20 zWC2Kol`uXL;rMq)%hw;8Mzc@lx6iy7Jop32Pl~p>)CCt#5NM&Sj6s9ZS5@9JI>w4W z6A{UchnOJP7T;b_w}jw;8IU?f7>M#OtPfa-vi&(9Gna|GQi0Z3+c04kW-|w$w$F+qYWBj8=Zr z=>$R$p_CTBF67u8nk#3nsVGoM!7-v_qxgxdGAiwOPL`Ohxm8xh z9mrxOK51gW%Ix2(7`C?-ly-sQh5HqmA_%!Q69vxmmh7N0pVp$#RFL>h!Sfv=i_xU?I%(YcH&L|`UAZqo^@nHn3iFC)Hsye# z^JcbfYrD1UIDN;Z(NsGydl8`GP4JS%37y1#&IcI=b^NT3pTJhVJA!WjOiC%+EdKpO zJ1~Ee*0@H760vBVuWCYnd*nF^l|TU`&Kt+0zH|`$5ox3>JFUJCzuKqp1};0P>Fro+ z-rlu&!?jy$M_!mn(Rb(Q`@;6{0H-UWi#?~TvFj>U$DMk169X05xzyqpej5?RMgwU0 zLbv;7)}zn)$sL>Gt>C{$GNgoQNA-jLta1uEFA;h`-6}Og40A-ammD#LJTDb~moIZx{D`PqD3!-2eDx92tpJg0TQybNxqm=(W6 z{YCY>0l!+}{Nhd?YgKZ4=IM$B4V&xu!RU&DRnk zVHW@JYnoq^8qs1Aws`BWh;NA%7_Rn%Xn|U0COBp}CzD-F8o1ue+#471wD>u##HygT zgdCb_vlEobU9f-}FDJP^ZFf0eMN3+wE!lt+#w?>ZFzzTlsUpex@~4pW2EL>8$dA%v z^>>1VFze>yInYtlX|(N>GS|J+R;|>1z0^8bVv$2k+RAsGshsAuM%%$+PuLCAT-@Jx zRtf(r(qbCykWuC4XO*x@44ZKCQjHZkO#n_^*0*ejR;D-XO|0T0FWL18`V!gcuAd{r z`3fM|>Ys(M+Pjb9X|G#Rrs|aZsPZWs%RFRGUlZ_8NCYA+0mw?G&n%`q-3iL=39E#o z(mO7UwmZaunw+s^Bu~X<@z@yN0b)-txl26jA3ao?)gGwFB;=<8&hNF4= z&PN+%s1f5yf=C`bI6Eq}Pz)4)r)P-cQ_v{A1ALX=h(A#oR|Rb8^qwex0tYcN?7-6< z4#_X6tq>gWI=%A`A|9a8ExAPwzBtVw*et!~2UFWc}G2Hz5lLLYm}lHb}h6 zY*0t>rT#0(Q(mRQAD2|xJ%^0Pl`w2gYcCxRaMVkV5RBFe>#N+$ozHvZqP_%zUd&%E z2y-wiiV)8Wasgo_`cLI2N5WZ}3>KUlUNX1?7ZqLc?J)I&yci zYdZ3w#k=;Hx>9lkcj}A|CO+SOiCODiyScO|u3DHJQmAje6B2UgHU(Xg2)s{Ankd*J zg^9}R#E=PE$FnR@BPO3YtjoW97~&%@oKV}QMlOWNohQA?S@8QQ z(IGyPW@?@PYvqZF zQsB0VY-drxKCVv+5Ob#+e#g^vPHIs^`c{{83cA+byrMhlNPrL+ch4rJchf?S(YkzvDItp}YZOqz@D3$Yw39srMAB7>%(tt@0;?K#QtpRYSM8?w*(G z^$+zXJyRwMx2T@aIWNO3>eS~uMkH7bAV<0$DMIk)7HuVM2G=Ps-d}L(+RWPzI%wD( zP)n8PFzKJY<|DnUoyLW=K&i6njjR@|DS;&k-haA62imXAo3zX;KT8&^>Zgp>|g>x7aZqImLyE&sg;8mM!FXLHQ*m=+ZRb5P8F@2mUV?z^>ES4`t{xu!{tR zunRXZ^zI!(;x zJs`z{w;{+~M|>nl4Y~jf!-%}Yhk_*GI!530jSkDLgr$IlW+50cROGQba-yn5XcD;E zAXpj03NP;9=mgEoY6$J8+3S+892Evv4-Q-lyY%Ih!Q>uq*l%H7*d5+UAT(q|)ep*1 z%Zs8@bCl__+n#P{dDHA8mAD{A9yO`>NPC~mQwJMhKHD1z6G!GC;7eTgw?GP|L0LA? z>cl31q_I1QpqAqdR^h%vDz>K3MCdpp6im!8(~}hPFM|jP0Fk(JSA0X)%=P?*(dQwh zW-fP-*wU8Z^u{qZoIbBuNE~OdjnAGRH## z;+^wtbftZ-H{IjCU9t&pP71bGvqXFGkYp!4ClQL;@cRxV6fy9&16F0|RHRUrCI4H% zp%-+P`A}A{{Q#Vn9LW=0`fDm7K1q@Pty|X@MmQd(o{-_1jiKlEOBXKsdVP9_l&pUA zCOieJ<<~%vQgrc4X%?d`mb6B3sXY&GIbP5&I(U8JBTW3{#c2K?t5|m%-4w}&2T9(I1=}$0 zKVhc4ba!8S>lBx==^u-O^D0+u`U;}Y{or?NBjdd{J10Kk5J73Be0a*bnlplv_TTwVa!R^Nh7JbhhB6 z;37j9x4_22qDTn*b=u*eU~7}w7ZE@1qT`aA%azOAT5g1E)CManr&?K#w?mkGV|N4T zD%PnRU_mDwNs4R9GMp=HgH_0?B_|gHSI253cLa7$0!j7`%#;oPAK%(f6$gTp1i?U# zj$tDQd1n%r&5*4uRBI8O^6<~~E)(B6k{Fj8RZ%RPZ zc|*Zgl@S$c4BKsov-k+CC$z>}$$lmoE$9QQNkYZp3c%C-AVZ&2M-NXdvWIOW=*FkgL~A>G3=KpUmJW?{}WL-gL~5dVjbwZ^#|1lN_);~am1MyxD zrwW(*Hr@*8K=yvS3Yi-5+Y^dKJ-9fG_{P>x&b4G6=g7mvG#SHhJk}#y=1TamUpTo7lhA zX%|6meH!79zO>4l&DqwN>-_89k{?$^l0a5=mVB0)%%Rd$F(z8_o8xr1flJ6#q|jq( z=Hr6Yr#^7RRfJu*O89j6L&U4_KPsj|$$(p?M)p%OOLf#mVZ>I)E%hW57$hw}2fiD3 z#84+uG^Le7M4WAIz4@UjP&C0S8;n!--)CZEPOFIVk=&NAEaYBVip^g)5Z+&^h8=&Z=qe??FMFAb zY~Enm33AFKuvSG{_sO#3q?WTlzHpTLIDB^>R@&A?PWg0x=CobVjz9x@LeY7MvnHI< zMGfl^7|9MlgYuw}d;qoP`u)gwt4d9OtPT!5JO~aL7^jPkK)hkrIK6usvE~>;R->#UlB*lB{0t=SMCD5G}rKyWC;XtYY(^(yVj4 z;3e--cu;!c&DJSPj#~&&cQxlFW=kuqwo2ZQB_dj8i0nA4AbnDN{jG8$TkR(zGu1| zv#h!G*fam;BG1&48zSB@eN)h5*5`mcid-utT zGhN!vE9Yng-WM&UCtiA~;hgdH9gl`c7`7=^yjec^VW9l6^ocviOFEt=LJrvwIX@%F z9)VINvR?-nEYl_$>?G;eFrVmoHKNv28tuHj=?JVFDVO$B7UW-?SpMb6qR-v9i@>A zq6`qh5S=`%#|d}(jeg&B9d{~b%0NCsm0D3}CUAKXJ8{#(>y;(?y4aY}l=d{c$0xux zi4Xdz@h6(Ub}#4PrjJJF-c0hS5kCjZjx_`I@lv;pYN3}NZ_oC$h3JM6HWt{if>G95c0?NS{23pzO8-B-GhP!4Tpx0Q}7vy-V_&Q*Vb z^@X*GpC4$KjG@?y@KGUxnL1%OKn~@j*m-uW1P)P6V{y>rfJNL*g*0lqYqcB<+nk*=1k9mHSx3A~vYRcZ$77hkxE^~k$~ zpYfm3PPv|M;zwD$SZQ$a8KU5!pcKhiXze3f%zA-FhgCpWZ^N!XZ}w1xz|%<>J$`w6 zdFn4K|#(jSnRY+pRiStPt-RzKUgoH%Zoe5!MCI(ao&V z|G3?*XfHo2sL`K^tVbe<$hwYeOD|3}#}# z?0S`v{<%rV?SDf_vHMlYW~ZF<#*SQl1!Y%}O@tXg{Yq zy@JmOxxsd<2*aR6FWsFw;rO0dPb{hm`etC8w#HGh8q|lIJ`YcCD#e~962M_n#QrT_ z;JUw@%Ps{VyQ|AuuD>b0xy)66H(n*>oJOrA+BHWbaiKvvZc83`{TsD%P^emo<2{-Q zqfU7{b6D{%aGw5V%rvXU2{8V2W}rydBabipQW|z}Xt*vicX8#;3n>P<@@qzIzM+x1 zoo1Gqx|OxOlRkQ__=EoOxx&?fxxo92oaom+XPnb3i)lk>mE=dKnIhS~c6K72MexX}gh*!9IW@ArNt}JJrqD6_h_xcTs*F6t zp8O?khsO4bOIc=l@6jb+Ta>Jh&@fvgRt0+dB)}gy$33(;nFq|rDP}A!kcyxZqIi|6 zkCBhVNrG45x5s9jFVI$P>*OpC-;<**@=#lRI@{l23;s$QHMPuEX zp9-yRE6{@Nrv(ZHhpg%!2$mHir4*^NlMl#T`OnOK*;yAMWv=U<)H4Pp@|`$(yFsrp z$YL0h{Y#j2$Q*rCd?7)R2x~)1xX6KcNn)@{P$*oEp0DRi`piZdyVA_Oql>h9s9B){ z`$-W=QE}?+XJf~zXQ`M9Nwfe|yz;FD(f9u_LlnTxoWEj>(-JCrdt>2wxg)m1n8-v& zg34l97h+S8Q4ehqEy?)70vqn{H=KEKre;B`59rbcLme4m@v! zMR!}L4Pt0LI&<{>O0a9a_1n>9apoI5pK$s);k;+7qNo?uxV)?DA8*zy4;n~+RgO+y z6)qVg*Ulny!&eGyTGcZD3-MNii?V#2Fy9omPHk&8KpLO~TQY zI==VB8YO|7GaajF-ElIo>D-C^`bfCBU9B_S%nl_U@=fpA6odNUxd#O^ zB1zSA*17CORk1U8}%R;3|3xKWgx15;Yf1D z*79r$D~p?7LtZ1E(GoaJk!^5)N!GrE-7bcom^F!foRCd1IRUs_d9WKl`&^}4aNQ1@ zbzT(Ky5Kv}5G4O5vCsYyS=C+;CppJKur@r@qG!yO9N%Nn2`67FS5rsrN4Z99kPXZw za=8DTJ@+g~q3_6M*uz9uPD?ib%i^LD7sjpQck*NP{k4~DP08Nezh(z6*owsE9^-_j zEH_SibJN!QB1{FmS!Eoicgd#Cm@egRQQCMjUurxX60;S5=X7;l#`x~+v{C8m>SE^h zAz7JK$%450+=^8%OXn-{lT_R5V%gx$WWAD?AOZLrV)DE(;sUp7&Vo?MM8rI2#y+Wh z4N-&3vcx^n0KvhaYujV=6r-h2R+(`Nbd1bj#bg3{L;fW zv)N;P8H8Ww@+aqFnkI&RqEdbD?$OSpALXoA7%bE2sqEpyE(oPbc`6Y~lb zHY2xli#cj3fJ4D&ezqiRyG)+$RRpERQMrSyF|AmhqUrvr!}{^DDe)u@aX^me*4x{eYkqEB z^5r8X{3DJ=&CC}rrEoFW_m|xk?>LQ3+H1dv{1$ifF^>!{3>WLYvk#U0p}ae_gBT$0 z-vV0U%xhh<*AdpL;kfU@Ki%CP%Qln7MY={^J)DiH;QZ|dAqbOS>~r5xdb?*`{LQg^ zZzYm;k*@?EZAFz8 z#5PO3H2lT{QQA3TbQjSUDJwq zvO-RtzH?r)y@nS0Z^n1&mWrZJ7ELCy``wB;a{Dm|)_>I6A#Ax9mcbC`!D#EOOKx{e zN(GAM3&kOhO7v6gmwAb&lmWCUOeAxpK&g?{zTv+9d>pWY;A73gJ9zb7U7uhdrp@rr zBjEC1q!;lCKNrT;0&Sp?WfUO6c(;8U={x{gJIzwqqb1qH<(Pd_>^BZvjo3aOU)r$) zC<{^tsI}M*>Jk6H{2W4vC~|OK%Y(Sb0TUVc{;^=YJmC?JZwD+CFH+!YDaPighFi?Y z@~Be@+6K#aKug4C-?7I`6-ad&a3?*9A+k5p^wUZm)FNPEkzFrY^&#Fb46Yww-gBHO zDrvwISR&8xmFIj@CfI)KtLU}D+E#HA#UNAqU;|=1_!Zp7au^GA6c?P6a)FoYw^awG ztb=koSbaxnRxRuh1y-er7AI5GT5QwwF^{(_QZE0)hbpwIp_YI*JQ2OHsv9|5VRp_Q z;fN0rpS`Kq`cLR0e&+q;N?TO`Z&L|r$WNY&r2m2f zRxOa%kbe4J{F!|x*^XTQLPg9N_jAqR!R+}edE;Ai^*1yYS+(oGciTBZdpZNY2`s_j z&rSNzpi=AoTFkD02BLW(*G^hWg-MEotepMU0EM*rnWN=Lo0X4Rb_62o-3U^>A3K%c z|Jk0RnB*irGNu-O1q0ZO2+{`zUmxBf)vpZLe0C6G$`sg&{O^@?%+y6Fzb76VmSYTE za=wT~-TsgkSLO-jkK~V11}8X}Z=V!ue~b-&y)*%6j!l}ODksiSD8;Sd&zY5>mgEYl zp82Sh|3a(c{EsyDj%Ld_9k?AU9Gp)6ZF~wj)MQws^DdcsLnoaj*^|27=exS0e6bvAPBIn= ziKqTPS28|P=C#oWq47BAW;4nkb(Zf2jSk#KYZwJqrSahrE9zZOJ9$ugt=>od1XlUq zvZ0C1K?O;=#d8@C>271QvU&>W$q}6VBu=v2`34LaM&ovqb}!eStH}w2$9rZD%Etx zDJZR_h2M2mx}L=k!(3v%d%pet;|F2RahsO;mH-0(I<)z2DU9lU{oF)DD7lshtKXji zjbg&{tM^ycJ<{^W4+&6AOr%xs6lW7-FF7U@V5;QEw((Vs_U@D&sYji+K729|yX~t< zYuVuJ#TlB1Yc6kmpqVCkR!Px^q{nc;GaQ!IR3i1>i1Y)xY}co(=8ocTLTWAox$B#{ zwlVOcl&s$U8?H1GW$vu}QITLC;*Xw6FJK0lj{BV=+K2v4U9Sudn~lu5&$^~GMCe9? zN^mITl+ZpvKCMKblU{mAVu(v+T5VCbeecEK{zF}}T}8CZ7Alr$Wo$%_^8I=^`bmF( zDR9qk()VQb*pHPhU!&}+(&a5j-&vn`KF){i`JwyFu~VEz!eVL#CkT(R|Mjc2CZy4dlqInuSFiKe)2zCtWJi2 z5c)kS@TR{?hOKB6!mebjHJtP(ji;XBblOFqrg9bgdR4tu${yz?t$yp*C366wa@`N- zj|E~xZBx%7(!X1rUtX4O$;ovM^Rr7Ii=i7nz}3oSi@tg8!9%qk_{IxE*U-gpl_J*$0}S^mtczLPublXX>^RYyD>J2fRTTJ@!3?VWtS z7#>=R!fyp$h1kl6G?JDDk`Hcu3XZ>gRuV2H@$+%kQ2QS-M;zDJU_~F^y3}GQ^i4iZ zn}Mb3AaOGYG2)S;^f>X3sPP~Dkk_m~pdM*sjYxl-)Vu{lrOm>L;1ivxGGAGLbN#2l za3Iypb)i~{8)&Z>Sgze?Q0(~|rN4(+%Hi2zlSjV(4puT)eJO8YtF}NZPEdyfGW4WX zJzFr-9v}X!^Ztt+^c!r0acv~C$3hD7GXeSW%KrX|2K$2T#mJA zXU|x-uP&GC%>o*~WCx7C+8@vZ?^Mkhu(r1Fm(?Z1!v{IIeKvk(f_JOGin1aJA=bUR zKPZUCQT%LTEdH_J=Ktby$%+}!zS3XK6XA3-p-6HOwa|4NVTvA1&7|o|6GBg?2EUL8 zbU@HO&vpZ`yD)%)4Gl;I={h7U`*pr&B;=&~f4Kl}H-9t3T;zz!V^|&8Zh6MD%Y&%C zgKCE7fggZzcRcaN_0s0@?^J>eCGFeu!j~7l+x@AoDoFfC+)Gt8!f`M<%RHi6^v{k* z!$nd3tE#(E1S#-FZS(%oZNx*Ly4ktVX4h-@dJWyALj|_x_}86)C1T8AlUuTu$q1ux zg^F$Wd*K9zZBhZ=VbNI2Jc;;|zwD@O8^JAZqpCAk5J36>a`B1rd0=LU;AU|8$fvr4!d=F0vHkS-~{?n$W|rc6GwBIRxYV&)`_oYKOAy>oDLF zKu)u5xPR35DS$W(&T)z|%Z3p^S+^?pW8*L-IBJ&m3C*7r)A$O#=OSc@P+V|l8Jy;P zHDs$dI;m)dV*(-hN%fe+OnHvli2A<70zY_ty7=h12ea;w&!q2xJr+N>IAim6w={ky z-Eo#8r#n1}^5Cv#6(HL0#7I3`OCBXn@Kv!$_T{C#xlqMF!K5n<8nK@{{2a)!TKxgZ zW4vlyJt{MJ(AG%E_x`G;T+Lb7sD~no>WI!kMk#mvt@ zN^$AGU|noepYGfDovS2ADmF7w<90Rtl&Q$+O;ao`FIPoG7&1#AC>$C+aO;1xW*sEa zOoVHEY&;7c=RlY<9-+>nD*HNnosFu0op+LTjLVSvd-qo~cLcY7-BMQMB$of*IJH*{ z4;AP$KVGx-#@*f_M~;1#|LXUI>oTUOG;E(w!9bNt%&|iFisL(86avpf+ zPoFqsGZnvl=13dM%2^$*aiaR)Yo{wZh*1i2RwDOuZ#^P^N-imbno6S|zB2G*F2^>u z`r7jLe-By_$57zz52|G4(zO_ogxd6P$f>FM{4lypL7?zeI3W>qjKF1Dwa!hMa+wR) z@>O=M3FQ~(B;!4L7{J-mH~2B7A>qP)A4xsC(-it<1NMWFj#NZ&A(=BY3mEZ?Hy}yv z%o%Uc>l-`Ngp0HS1*cV4D#%)T?kjP!pCcsaKRI?=hxy-*akAT8({D38Qp`35^5|Bk z1sN0=vVTALOxGw&*g}gGj4KWL6R2L39#sE3wq*_>C6gpiUrFv$wm>DmNWKNJ4z6p& zCJIvUNGHXD*7k*RqbwJCJhl3KC)C4; zBb{lrdF6;mrTplmO>wP|nNfc7D7~)3%#TZG&0{YOBWcmJbP8MfpdKi;sfv1elpwe$ zmM|iWdoDz%IrXFG^GSL)KAcmvyT$br(!B>Bp(j}K*=^AQ!j;8$ep$1)u?}+6K!l4) zJ!m!Av1gFqO#0$VVk3YaKYRzOfYNu?&13ONj|9&J^*hI8m#Jkaq~Sr%rRm9Su3nSdm6MMo$B z#VRRq4){)y7J~|S18JG{Ivy-N1Rog9J7t+|OVAtr=0Ov-asZXN)XU1=C-<4| zCbWy^B1JFA_HuE?NIBC(Q2yF+79YZ2v>+yqL8(4v zi2)h0i81at9A3K2!+f;&;g9HT<>MjkYc~!OmaUu?6a19vB9Bjt)V`}oPYvU6|LASZ zC+0AC_`m>ll2*dM@iQn>Mc4i({A8^OSZJ^GRbg7g@eLmf^#g4?^Nmu`i&!gWq7;}) zdX=$Ee*ET_HKpU3*rt*fDI@8=M=EZlR}WwW6bY%%`}j9z5&JQV>@d=nT!6I%{oblC zL~U*TMR~31tDLikmV?;eM6nQpSnFGXSqDdz>NBDUsWkp9D<4)_RdXdsdQQL5I?&Qf zMoQnxKq9mP_wnL|cS9w`kqcszJcbAXz46%+>x%0h`xiY)gnKv}nB8fc zdw}MX%Hpt<2J^yw=IG*sNLsq(!jxi_VzoDrCUwQ%^Lr6v{OUWQ%dP z4bS*Q4-cVeFsc(}R)&3Dk%sb?pM=!5LBmJdnKj$0?C8H|)U-Z3vI_X#Y zbYY%_wHILNO1xG0k-!v#uS}Q~n{Z{ck%lqSTYNOjY7t+JpM*CX*ch-^@+oazSx@R>bwc{@fj%HpxHyNED4LP^ULUR@lEO~Hv;YtY-;-`tv zHD9;DSN((E3b;lMO-RCg_yzH1InQ5H>;d(@#J`&lLV7?Bt9EV0VdC?<$F*HICbDx9 z2P97}X=4o=N56lkf);fIy*LB1e7#s(IN(b||FOi*y+u%c(`ETOZ^}~mcKn3qx6v0q zn+FBQGDFTK2%n#X3-;kZ%VuDX&vmHzauB-N3MMpJm7Q ziF4z`LoRz*kX&As%sJe_c@-P-SqL8!zbC?gffjiR{as}sK2Yzm$9?>4rt|P~*47X| z8Jso=H2h+4@38Bn^+PySI{JW{W5$Z(O}lOzkOJ35nksopOwC~%SnhjD8F;)x96`%_?b?{euBhE2tDW9~qEmB>H~ujrh@e6?S08mky2D3Ew6R zTcc)(;1+%IRZT)od4Pwiku;pet~C^S)M8^5J3{E}BZirm2HCi)|G;ok z9yoK!k501RlxpsfjH(H)1CE*mp(93Z8qRimOjt3rc&x-aQ3~T0jya||Tik?Pb-%NTTV?2`lY=san81`b~z|c31|0Hh>aGXv=v_9 zEv-#{EKarBwl>^uE=ZgiWZ|H(uE4-0GGV2rN|wegQUPtVcWXbl%J2VV0REx%ThVqT z%p)j+wl5H1e%Q69@c|bo+t{!E#z}_lSnsq;JNlglQB8xGJn{aldY5P~ion(R_s=>cWyIREjlc*Tr(GWH|R@PLGc1ZoBukTazL&t#bm zD0c$ZZ7)BNG!wi>ISN09<^C#3jn!fARlF(1Q754cNioNc!Qf6Eh^Z!d+kp|7V*Vnz zcp@!jp!xr)?ybMte4hAGAUGvRpt#fGR=l`tq0koh;u?y(6RZ@6;!Y`6+#xuHLeSz6 z+_kuV!~1@|=l*p6fSZ$ZlI*k3JhPMC*`1wv%~m_?hPqTqe|E0YxgmpI?A(a79XHU1 zNO^XOmzW(xxv@=1)5n&`SniU!?i`S}jraD&wR`zVpW3-_?v^ox@SCx|DhPHkFO36^adv{1=(O{F#zNf08{`F7n&TfI@Mf9L zmJF(-cKAIu}b8ofd8_*KVGBcS@2Kl`Zkt=0H;^W9)pgXQk2*=-C9Q5KHf8g^~cVG@H&@k@T2xZc>!k2zQ{Ajmn!%g1T1u4@s&Yj*i z9ZbF$V3r$_xIg6_w56GlxFkOZ&#*e>@H~ASHXz zAkDh1P}G*nOBj2hQ1&IGBokDV)_F<(c)`c0De%IgQir5ScwRzXq&Y3-1<1L0i)A5b zEzB(CjD4sMUTXd$^hc#VPTaIq7uAs|S{5r>E* z8#>zwSO}-~%$5uv@ulWKbm>1c!#l%?*8E!@J08U}L&aaH^iCBC?Qvw-`uh-v%Y_W7 zDKeR2tt^)fw!+QKbGZ>qs9NhVe-NB$kXwM1?AG{vph~tZr`eO7BUjW&-dJ36o&9Sh zmW9na8Bt9W=+;4p@Ig&1T>AV`%0gBXw;;hju+7R{$`n&2*Q)s{jsR|1mzJs$bFR<# zQssgb7Y0+fzwzNcZ*>-=st;9@jrP{hYwjYnM!tAe7Z?YVndK&rH50{6k;Q8R>UO3^ zQ0_;gXUdda7WjJMeBpY#LgO>s8SAKV)w2IvuU97et{o}|?Tk|~9po@?2VBx;(D=nG z8dIJOhhEhLY+QfsK>X%`D-8Atj!fo?c`C$_fm(^AyEzBwUQYgN6}aZpi&($NMYvao98+#~>TiFat} z5nBT*h%ElPui)?$S_}bRIU+BF$q5)%mPqmP5|IgJS9vJJL?OcGa5xR)svmL8;{?cQ z{_*X!-Glt7^`Bk5N<^DBCPY?zO@C(m;#|cdAbwfVW}5@?M`|Rm_{fF$Sk$6uP;@7I zylqS|1O5$^Ac-+-ipAG2<%yOnB@Q_jQs4qlH*;tKa)eelZ@CI7xl&f{E>ohCvc9&l zdB4uq5YkWdtJ?IGpGu^5b)~7&y$*ErXlzC>_=Q1di>2t)}vwMnZeq zL3f!G4NpXK=fU=T*TUY%H=!#rTU{}T1{91xiBtcy;p4PK@ZSb`DI*dhzZ|TnArH7? zB2nY2ac&yOODA?W07ghm$vqB_|3e0y7jBT4uCjYKux0(}i|?~7EHPB=O&=TuW_{1O0EE1elQ2ADY4VH>e>{D3ve58dWH$q1KC%e;@d zpx<$sP23tGY4wqwON9caJAU;%q?AMY(*ofeo>_s2n5U^opa)sx{2s5%n(xu=FUsTNNZbaY^D2kvVcgpN$$FQ$MQY(UI zeW^R3T!vAQ&~+*3Up`vvk%u#f?7?KK_Z&o^n3Cc-bxDJ zv5cZRqb2dSWnq;DKV>DC?&bkS;tOyFHwO9*&AyYBNviNCp2F zNPFr9eNwAJQhk9*NWx3$D3|Lq;S}7%5&Kdj8xNB;{+pj>;bGBO?Og5G>x_LcVBGfL z{z*x>y?)HkNweHnh?|x+4WI28l5Mz*jX9(nr?d~Vg~kNJvy>a#Y1hDZNRueRzTNrB zEk>vfO*Z*qH!yZh#5n+)`a zH0%5n36*|XJ#I=i;wx78w&aw0LpQ=d5XNZF(jhn!bczBCSX}*?Y}bE;)v4dGi?BqyWS8#1GU9ai5;*>e!RppieDx0Mw-i)%^qupGC z@1vN0nx4Gb^~T=hrmzUbBn#Fo7>P*Y*xL6cU}4(`s38R7)pU16i@sRqDyhYl6b)ly zK2$3;BJIN`e{F<|xwh-BY{C4d5?6ki#)$zp^v5TFMj7eZ6+yKk^bN4e>z0m`l>1|8);ff z1uaQleW!v&3-8KjI&JT+upsb-;NB{JOJ{jt3~e8Ga=DKQOO6cPJ)|7IU~MiNk>VgH zpvuS!Y?R4%7nHt9PnFLZkmu{{$`$;bhb5>%;TKMP?=L;7*Edd+7WSBSfB&GtIu^n@Tg7aJ#D<1if{NORfjUFKI>EiM ziV0yGkn|)x`{6}R#?*1eHhj~u{=ZE|4Glyt2B8mu3J)t}CV8S43jIcqGPufnvK}dB z>?VTUql->i+sj((V}g^o6)x<9v@b{4ESqszq*Mtd)aGGyDi%c8P7#&qPB9&=edUG6 zVbc6BrB=x(3VuN>t$Y&r87iyBFM5(PB6*B}GsCMX@8;fy;(3arYh5Is779&syDc9o zRkt8_F_|aVUvaFp6ZOm?cm0`lqe1|H48mhOk;Be7qIv?bJ&CaUxE{x9qA z5Uf)U(30%LLqp7YNkh*iPt~rFw)hxhn4Dx$h3 z6#99yW7LDQXBZo|9ri;Fs^9%8Un!k_%AxxfrHAj$Tj*78uj=C$wUbi3aB9do&TJyq z2`iEzBnN4EG-V6#eM(Q&VEw>6V*Sfha!i6&N0?Sef|isRpFQeLSm??Xh{~}?+4b-L z-0Ekh>F9l@s9@R?brTM_O;Q$IX2Xj+^Wf*PShY(IdbU(}xJ!Ivw|Suw7`boTFee%n zaI8@FP?Z-sVKo?tAI>3)nu#ych+7JM!m|O025Qoe%HJZ>j(63KL16KaZU^S9wUNhr z!Ep~;vKqFrB3p@2`>0;*mDpr4oIk3f1q>mBwn_(zEgo`%u1OQsspsGX*Tt;9TI|7W|%V zgxYjeR6V3HLN8zZB0nJ^Y8j{TA}J4hfo@E{KBSK0Nn?Y$HozES`kgvBvIhZ)r`3q)gTu*#!iXp8||aarI^eU_|XgNG-i7c%mYmjMq~ zQJ#13UutC4f8!fW zrR)O`c$Mk&bVjdE71lxH^RY!eTmvIka%DSge<_~zjBaA>{|k-?C??jMdq#B$&rvE8 zUQ-wktD&OS2ChF0PX}gDRnB|k3*J6(0vlEDb9*k3{Wp#KCBD7KFiq;=NCzL0C#78= zj@QoIFkXI%&uE!6==>PoacnctD6=X-OZV}AkhdRiE%~@(w#@HBYko7f3TY(=!r#E1 z9yk9&_5I}n+#vh=U@=O^G~+Odtb6IC8`5u!$KC)B;l;J)Uon*GX*LIxm49zk2+roL6D>?OWixzELh_(XJV) zBk-~8{N2Q=_}6wLuFXYWm-Br!h)+l0Nobuz3+&7Lp=}QvABGXVd3@PI8}%czM^fKU9FVDLqwfx-%1TKz*UkG{$asy1K~HJ zuV2%34H*RMuhuVnH1TVsBrFAO#xpN6G7j+z&?hq1^JkASFS!Y;G1yOb~>(B)AgA1p|uGvnQ^mcgeN+s6LRX4&b;cwnqRXZk>rg-P` z8SmWS(~A|&*#{oU-E~NT&p{)z#l3d38f$R;&Rj8^4mebr+TNH zzug%!Lc+1{6m*HX)A!tO?gT`UyoA4BP_Eg9y=9GQ-bTNJykWC0VJDEwo(qkg=a@ zk{eyMXul-LRk)D8jqyCJL_zTaXLfhKU$_T7?1cNvXi4R_By%TKMxEQC)2;U%?s%AC zPAkb+k#2IM`l&-03hq6Lg6DCH7Y*WR;yBBog63Ii+guZEMQLWZ|(6j<%ro%;}tqX>Uh(u>l1HxTd7>?Y;@v6 z1ND$R1o9{PeEPm?@W(IxSa;5ghil!)2c7w=bc;;)UHm#1RfT((rME1NoZ$xtfcE zsbDCIBj4=roTvBSADCyXPxJ#Gcd-U;1oA|Iig z)@QCZXt!3RBa%YdclpUT-Yo|Kk=;SLZ+J#Aa*4;QiLHmZZKZ^$A%E*hns?W)jES5_ z9|r>ifM=R~>6E-tA~4ri{R& z72Bm_&dFz%Ra#* zj9%e>snA{SL03WCW6ek{Z5j|Dmw2#X!yXTvj*nX_Au1uMi(HY2d4ai)q(bkIv^`$i z>yf-)tl{wgq_mR>MSw-}kL;PjrIP~pDVh>AV_)4jhJ zpyPcy*EMYE7^>pG7u!GjrUS&fFE=yLv$MWANknKbcpfY zr~B;0bZBW4N*O5$_*y8HbtEM>^o{|FLX6Cw?DK90Bt? zTRv*8#XP=*cJ;Px9fPds#8obR>Wqexr?s`45eWg_+u8ka-o2nV!FD#`$yOw%l%oV^ z_KxX)Qm)ui%cr@YiKvBSL6K$c>dGQnM7A|^9MMNKmvfBii&+q%q|6!kPi5J(2sqs^ zgHUOdh3wZ*7|4(aTNWekb0km-CvqHI^_#BYU!+a71XBV=J^4G|*pz6{u_Tf7qUgho zPcL6L#feUx!!;yfejwuwBAn*p#gyn30Y|EllyA^?ls~kis<8|maNoM11+H)AKeRrU ziXPYe&CA*xcVBrG&XH>Osr(hIZYmxp`AvFZo2XLe;X*b7rq9e%_$07)aQ_>s>xCwW zx%uFQd_v_tkDnUxGG?HpaVcloRtuDqb4T#I(l1aQNt@t!y%F%QRE*9DZyPy>QJ#qU zH&mW1De)^?#4)D&dCM`DSsm39h1Y@=g?^?yo-dtPRw=l1!DU8lQe?t82VF_yg| z4nQuav!$`0O^PTC(wAaf+a~O}_Fxj9bVpwP8+z!0Ux2Mf-|@L4BrRVEpDj{$P~QBT z_`L{N`}QAa2+oHQ%Lz$a8W3;F(!x1&p}ltTOqDHMfLf`_IMvxWh}m-1r(G|nN^f}L zET~U%^p|2!7k}@a{|9f|$hsDV$$hGyixTLhaxtRJ0;A5a#?FM& zqj1Iv*5u@2qLRPuW}*LMT-FR;U+#l+uCiu7Xlnk=h`G`8{sG>dtT!Q>?=NNS&ItUK zQ(~1Cz%~~&o71>ce?XVd+bdfG3QCMPZVxE*HY6&Y6PH^V_>AT~+Anza&y6=f_Y@Kt z9CKXU;1{4`7U1L7ceXIG&%jH~o1P6a_3voFN<`Q6nGHL5;IE2mA=h1&Wc)=U&~ zqlMVKq^QhN_4*om+n($q9;`q1J%A!nEbxyXwCwtts5FMR)yp&PnF+$E6hKxgjeBk@ zp82;Tvd)5~pE4Cx0Yd2)3K#ZDM1;No4ShHXZd6{PHJ2!vP@*E`)V%42LT`tT?$d@e zy|7?b_GL8wiiH(EQ}@>AD^#VQWu?6&`OB&GLR=r}=Mp_(FB)*53X!XAn^(*KdP!nh zBPy|8nK8R)OTb)oTaF;;WEfC_-G;wNjf7}8IjT8VoHbXGwUc+-7K<}VM4s#fRxD1m z9fRylMzzU*^Az`<0>BJEU2b3s_Ve&J$_7U z1C)k%fT}Dy-->VZo*%f_1!6?(Vm_%5ZF3OZ$=BNm7G5PsXz+z-`1=N=4s|bNo!@iK zK`#7auG@}Y%VQfU!2BOEe_hb7fAY&zuUn{4GNwUK&{NRLOJmwCz2xMdu#k*du{zNA!mBWwO(Dly}m2;%P9LGnobWBR8eYZabA-qLxA;3YWU+dTWB+ z@MMpwnx5uj29mC}qjfr$6$PvD0@9hcJW1p4@5D}U&0NE#lpC0$aH+8l(~2MzO|$2%DUliCvvmCGRSX=MtwL7$59}UF+ekOo&1u&-Kv+vfE!-*FLGr7 z;~!8qbN6aasPnmlXy(TX#>S-&YJX>XXlzA}qKV z0kqhq^TLWlGt6+1(2z+lw+(GQ#F=c^$l9CeAq}l+Kb#8*QlZ;v<>Wy)9q6(#60bQ92 zqf-c$klzN*Y!Rn2DkaFjMo`*kL;B2{(m3?~sk?ip{VwSIlAj9fzNr{oYqHM|Mm|MsaE$_sS^nK<5(V=x8@0-j`(IIrJZbklOVnEt8dZU#_`FhuD( z>I4E5x@3%PO;8wuMo|v*gE0~)--i7O1w@}{qFia~TfIEc37tuSH<+h2x?}<@Gq-zJow32g zDb^ru5|fz`_$aJ+-JRrMCAw08&hF1>y_E@(TRju~zWS1|NM9=?BNr9lI>Y>lwm7E2sjtnmWz8*b6Bp zkMoN5rG3P!W)mCxv@`*)z?Kj6XA4Pv43i3M`%Y(371izzNps zQ(?;_kH8sxjNRxU&_H+F_%ClX%J;#-&``%Fo**Tq&)D*60BTlfUs1$`1#}z=3F^rS zgS|lRX$EF>^0X>zEND;}D@+GoPmTRlJorJIUD z=-dg#no1bKR-0K^Z8)AxzYw0}NP$?3e}qC2It>)#GM;KwpV>ql?5_8)_SFcQd4lSp z3d$!ps2wR&Lgo*mv*ungNl0mJe52qehD1Z^r$*FlMffbLzwi_Ztx^Kx^z*=_P61&| zwzso?lVr7zH{$x;!W%Rin;n8kg<`Txc4FOcsO|^^-3b-=t9n25m@kD&d1Y`h^6)j{ z2&MEh;&KYpwHEh!K@}p*!hPPa^M!2wdL=g57mMnTW1ttv6gd9MQ;a9d)CZpO{nyGl z>FV=i`IDZNaT}(+VRiYLJGEBHj4lTq^0Gmw*QcXaG$FpvH$(p6@YL;^OSds!C+~JE z6bh!V-}8TEW1YaeNgcS;Nq67dIRr0b<&|RjHhu}y1v?5ZqSY;I8{an^Pw6) ze~P+>^|<5bu10y4LA!Sb>s1x4E!BAkJJp)NaL;9#7Q@j_>5+KUj`tXm%}nSv61Ij= zwU(XF!@bU5Y%6F^(A@4wh^4{^27=+zJj!Q9UjEXmB96}!X_m$l=Y4MegBqdBu}8U% zJMzhQIz;N&gK|Bt+$P829!n;LXr!W``rdo}s+_xYcTL9}0b62woHjn_EJDo{BJ(^` zOHm~!g8$VdqVTmqfiRs+zP7)tg1!#xFn%hV+@E+|?OzmLS0+l+&lyyKgngn~@ht-h z#t~`(9gb{4o5rSc9NXVck75T-?=g1^XYdt|pENqA83sSuoD@Hy{5gsMl6y5F=}f`i z;snNyX4|~&nIl~D1FHwHu?Cimpggi z(hd*l*Y4Clp*3|(eUM=ne2rUpd1BsQlNAv%xCqoyJ^|4;%pe)xykSp!iP8D>T*TzA zj>@zcMO76dD(d-#D4v}yAac2lAc!M!6qf?YZlJr$&Tmm;EUWkSSdmNHszN)^aB`;p zC-0q@hKG_Wk8p+W9p825awA34Qb|U2mUx3iYq;AMDAklJk_+1WAjZdS7=KP)$GBI# zQHkS7&xusn=d!Tnm&Uhxl?E;Py4)}6DklXe;0hwKF1WI-AZ+^H#AiTq@4?&fp^Q;2 zd#c{&6Uz2wqZ)~UF3bU#!vEYJ$^M#Z=+OwUun|>@#6-{AWhVKgwzdEy$zYgjM8kvQ zUeV?vV4oiAkJQw#c69cAU5wDvV)L_Aztqz{252hNI>^h?`=3t<(032IxqE?O)!jQJ zlDLz9GJd|Lt`FNTwA zQk_ZMD&{V=BB5yijrR?L$o{|Z{n6TrC>+(`i6D4NL9Wm1gLc+%ZwDDhOwNOcSsw60 zI_dXnWUvE@VS$M=kC{jZ=7-4n7Z1&5=mz>xox?v-4mya`FkTb<4(0-`75R}|Q;s{Y zrI4M3;jTi~O{Uirfg`Aw=qa&L4v0aaglqN8WmzO(s+~2Z%0vDw9+vA!VA!i`@^S1h zdfug4xF<&__cJC9$-vDgr12qVJa}=~ueU)*W|@Z=I*Z63imUWZ;wD#{Q7s8le?x<# zl)MB9mtJ1xIURF!`3wtse+n=_p^TSJjX+j1aUjG>&Il4y0Z%KNSlnX|6qdD*A=4;A zcAkej?)Euqso!EowS8x?f`%N40iL~aARb@^?$OTS=;m8h6mG9!jIxrU2u0Z;R zl!_#H-dMpH)qj9{kD-BMBCL_LEe`)v{p;@hT6n+O3T1!*Z7Bs5Mz~E3_rszzitt8B zMFhtg)p7CQ9d64mcA|_fBDt%;M<3UY_ubsOJJ7C0!Gfo)`tMZLg(04XIOuox2)+^u zSNGR@l@6Cv>B(IFeF~^G*+x~~Eusq}qOew9dpLu6VbD)^{qDWwi)pR%&<6 zZ$8hV&#I{-3N*8xjbzx8xrfH}i=)Zsw(kuBUX+R%W>wn@nkfH|JKyJ8lNPdmA<88J zBKXgwdQzV!12F_ly4u&+V%x{osWwQn|Evn|IfF9#nPJm@M9t+Rj7peJ2k4~q^#DEMK{jd6TDLPXXp?Lh zWKy;j{$HWce}yGMRhI0=NJQ!XyC1RR!6*6${bi8^7kVt97{7w-)iVV|vRA6W!WJE@ zO!T%e>*glD6)@s)jJOyGO}Dz9e*R2R$!+x<_4zX)3Bm94Wu+kmkIw~+7H@2xr$42H zH;C5#cj-G~&VMc_RDF&6Ux6lKQT|_DmPDohB>n#*GG9j#W(tCNTTo&k?mwV3dHx}K zO%0+M#E6^qEX2aUwLaeQj4awucsv@A`Kdpon@EV4lAM}srL;-#{{g1r Bfu8^X literal 69457 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

6UEs5ATsn%;0ipD=YkpmD^o*)etsi)k@|rid5}G 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 - - + inkscape:export-filename="/Users/andrew/dev/analysis/neo/doc/source/images/multi_segment_diagram_spiketrain.png" + sodipodi:docname="multi_segment_diagram_spiketrain.svg" + inkscape:version="1.0 (4035a4f, 2020-05-01)" + version="1.1" + id="svg3504" + height="1052.3622047" + width="744.09448819"> + + + + + + + width="446.39999" + height="345.60001" + id="rect4949-2-1" /> + width="446.39999" + height="345.60001" + id="rect4809-6-9" /> + width="446.39999" + height="345.60001" + id="rect4669-5-7" /> + width="446.39999" + height="345.60001" + id="rect4949-4-7" /> + width="446.39999" + height="345.60001" + id="rect4809-1-6" /> + width="446.39999" + height="345.60001" + id="rect4669-9-6" /> + width="446.39999" + height="345.60001" + id="rect4809-6-0-5" /> + width="446.39999" + height="345.60001" + id="rect4949-2-2-4" /> + width="446.39999" + height="345.60001" + id="rect4809-1-7-0" /> + width="446.39999" + height="345.60001" + id="rect4809-6-0" /> + width="446.39999" + height="345.60001" + id="rect4949-2-2" /> + width="446.39999" + height="345.60001" + id="rect4809-1-7" /> + width="446.39999" + height="345.60001" + id="rect4949-2" /> + width="446.39999" + height="345.60001" + id="rect4809-6" /> + width="446.39999" + height="345.60001" + id="rect4669-5" /> + width="446.39999" + height="345.60001" + id="rect4949-4" /> + width="446.39999" + height="345.60001" + id="rect4809-1" /> + width="446.39999" + height="345.60001" + id="rect4669-9" /> + inkscape:window-width="1397" + showgrid="false" + inkscape:current-layer="layer1" + inkscape:document-units="px" + inkscape:cy="361.68127" + inkscape:cx="202.2455" + inkscape:zoom="0.98994949" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + borderopacity="1.0" + bordercolor="#666666" + pagecolor="#ffffff" + id="base" /> @@ -209,1315 +236,1223 @@ image/svg+xml - + + inkscape:label="Layer 1"> + width="115.0112" + id="rect2385" + style="display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:0.968808;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" /> Segment 2 - ChannelIndex 0 + sodipodi:role="line">Segment 2 - + - 0 - 1 - 2 - 3 - 0 - 1 - 2 - 3 + width="115.0112" + id="rect2385-3" + style="display:inline;overflow:visible;visibility:visible;fill:none;stroke:#000000;stroke-width:0.968808;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;enable-background:accumulate" />  ST = SpikeTrain - + y="66.88073" + x="116.25489" + sodipodi:role="line">ST = SpikeTrain   Segment 1 + sodipodi:role="line">Segment 1 Segment 0 + sodipodi:role="line">Segment 0 - ChannelIndex 1 - + d="m 177.2463,145.45234 c -20.64887,32.9374 20.17293,59.49607 -9.87155,62.9423 -1.17489,0.13491 -2.48425,0.28276 -3.88386,0.34579 h -0.48514 c -1.06289,0.0409 -2.04006,0.0691 -3.23652,0.0691 l 1.29468,0.13849 -1.29468,0.0691 c 2.22835,0 4.05081,0.13575 5.82588,0.27667 0.41623,0.0331 0.90171,0.0285 1.29462,0.0691 l 0.48513,0.0691 c 30.04449,3.44619 -10.77732,30.00486 9.87155,62.94228" + style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:3.38554px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + inkscape:connector-curvature="0" /> + d="m 177.96058,281.88091 c -20.64887,32.9374 20.17293,59.49607 -9.87155,62.9423 -1.17489,0.13491 -2.48425,0.28276 -3.88386,0.34579 h -0.48514 c -1.06289,0.0409 -2.04006,0.0691 -3.23652,0.0691 l 1.29468,0.13849 -1.29468,0.0691 c 2.22835,0 4.05081,0.13575 5.82588,0.27667 0.41623,0.0331 0.90171,0.0285 1.29462,0.0691 l 0.48513,0.0691 c 30.04449,3.44619 -10.77732,30.00486 9.87155,62.94228" + style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:3.38554px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" + inkscape:connector-curvature="0" /> + transform="translate(42.347275)" + id="g5406"> Unit 0 - - - - - - - - - - - - - - - - - - + y="183.23851" + dy="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.6593653" + id="tspan3891-3">Group 0 + + + + + + + + + + + + + + + + + + + transform="translate(42.147583)" + id="g5380"> Unit 1 - - - - - - - - - - - - - - - - - - - - - - + y="253.11589" + dy="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.6593653" + id="tspan3891-3-5">Group 1 + + + + + + + + + + + + + + + + + + + + + + + transform="translate(40.801575)" + id="g5255"> Unit 2 - - - - - - - - - - - - - - - - - + y="302.11591" + dy="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.6593653" + id="tspan3891-3-9">Group 2 + + + + + + + + + + + + + + + + + + transform="translate(42.241776,-1.6428833)" + id="g5276"> Unit 3 - - - - - - - - - - - - - - - - - - - - - - - - - - - + y="327.40158" + dy="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.6593653" + id="tspan3891-3-53">Group 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + transform="translate(42.185257,-0.78570557)" + id="g5328"> Unit 5 - - - - - - - - - - - - - - - - - - - - - - - - - + y="373.83017" + dy="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.6593653" + id="tspan3891-3-8">Group 5 + + + + + + + + + + + + + + + + + + + + + + + + + + transform="translate(42.426407,0.46557617)" + id="g5307"> Unit 4 - - - - - - - - - - - - - - - - - + y="348.93604" + dy="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.6593653" + id="tspan3891-3-3">Group 4 + + + + + + + + + + + + + + + + + + transform="translate(42.373657)" + id="g5357"> Unit 6 - - - - - - - - - - - - - - - - - - - + y="396.68732" + dy="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 -1.6593653" + id="tspan3891-3-6">Group 6 + + + + + + + + + + + + + + + + + + + ST 0,0 + x="265.67014" + id="tspan5430" + sodipodi:role="line">ST 0,0 ST 1,0 + x="421.91724" + id="tspan5430-7" + sodipodi:role="line">ST 1,0 ST 2,0 + x="567.39948" + id="tspan5430-78" + sodipodi:role="line">ST 2,0 ST 0,1 + x="268.04239" + id="tspan5430-4" + sodipodi:role="line">ST 0,1 ST 1,1 + x="424.28952" + id="tspan5430-7-7" + sodipodi:role="line">ST 1,1 ST 2,2 + x="569.77173" + id="tspan5430-78-3" + sodipodi:role="line">ST 2,2 ST 0,2 + x="269.05255" + id="tspan5430-6" + sodipodi:role="line">ST 0,2 ST 1,2 + x="425.29965" + id="tspan5430-7-2" + sodipodi:role="line">ST 1,2 ST 2,2 + x="570.78192" + id="tspan5430-78-2" + sodipodi:role="line">ST 2,2 ST 0,3 + x="271.07285" + id="tspan5430-74" + sodipodi:role="line">ST 0,3 ST 1,3 + x="427.31998" + id="tspan5430-7-4" + sodipodi:role="line">ST 1,3 ST 2,3 + x="572.80219" + id="tspan5430-78-1" + sodipodi:role="line">ST 2,3 ST 0,4 + x="270.06271" + id="tspan5430-3" + sodipodi:role="line">ST 0,4 ST 1,4 + x="426.30981" + id="tspan5430-7-8" + sodipodi:role="line">ST 1,4 ST 2,4 + x="571.79205" + id="tspan5430-78-8" + sodipodi:role="line">ST 2,4 ST 0,5 + x="272.08301" + id="tspan5430-45" + sodipodi:role="line">ST 0,5 ST 1,5 + x="428.33014" + id="tspan5430-7-0" + sodipodi:role="line">ST 1,5 ST 2,5 + x="573.81238" + id="tspan5430-78-0" + sodipodi:role="line">ST 2,5 ST 0,6 + x="272.08301" + id="tspan5430-0" + sodipodi:role="line">ST 0,6 ST 1,6 + ST 1,6 + x="573.81238" + id="tspan5430-78-7" + sodipodi:role="line">ST 2,6 + tetrode_id= "Tetrode #1" ST 2,6 + id="tspan8512-4" + x="22.017519" + y="349.36826" + style="font-size:16px">tetrode_id= "Tetrode #2" + Annotations + diff --git a/doc/source/scripts/multi_tetrode_example.py b/doc/source/scripts/multi_tetrode_example.py new file mode 100644 index 000000000..3c48db1bf --- /dev/null +++ b/doc/source/scripts/multi_tetrode_example.py @@ -0,0 +1,103 @@ +""" +Example for usecases.rst +""" + +from itertools import cycle +import numpy as np +from quantities import ms, mV, kHz +import matplotlib.pyplot as plt +from neo import Block, Segment, View, Group, SpikeTrain, AnalogSignal + +store_signals = False + +block = Block(name="probe data", tetrode_ids=["Tetrode #1", "Tetrode #2"]) +block.segments = [Segment(name="trial #1", index=0), + Segment(name="trial #2", index=1), + Segment(name="trial #3", index=2)] + +n_units = { + "Tetrode #1": 2, + "Tetrode #2": 5 +} + +# Create a group for each neuron, annotate each group with the tetrode from which it was recorded +groups = [] +counter = 0 +for tetrode_id, n in n_units.items(): + groups.extend( + [Group(name=f"neuron #{counter + i + 1}", tetrode_id=tetrode_id) + for i in range(n)] + ) + counter += n +block.groups.extend(groups) + +iter_group = cycle(groups) + +# Create dummy data, one segment at a time +for segment in block.segments: + segment.block = block + + # create two 4-channel AnalogSignals with dummy data + signals = { + "Tetrode #1": AnalogSignal(np.random.rand(1000, 4) * mV, sampling_rate=10 * kHz, tetrode_id="Tetrode #1"), + "Tetrode #2": AnalogSignal(np.random.rand(1000, 4) * mV, sampling_rate=10 * kHz, tetrode_id="Tetrode #2") + } + if store_signals: + segment.analogsignals.extend(signals.values()) + for signal in signals: + signal.segment = segment + + # create spike trains with dummy data + # we will pretend the spikes have been extracted from the dummy signal + for tetrode_id in ("Tetrode #1", "Tetrode #2"): + for i in range(n_units[tetrode_id]): + spiketrain = SpikeTrain(np.random.uniform(0, 100, size=30) * ms, t_stop=100 * ms) + # assign each spiketrain to the appropriate segment + segment.spiketrains.append(spiketrain) + spiketrain.segment = segment + # assign each spiketrain to a given neuron + current_group = next(iter_group) + current_group.add(spiketrain) + if store_signals: + # add to the group a reference to the signal from which the spikes were obtained + # this does not give a 1:1 correspondance between spike trains and signals, + # for that we could use additional groups (and have groups of groups) + current_group.add(signals[tetrode_id]) + + +# Now plot the data + +# .. by trial +plt.figure() +for seg in block.segments: + print(f"Analyzing segment {seg.index}") + stlist = [st - st.t_start for st in seg.spiketrains] + plt.subplot(len(block.segments), 1, seg.index + 1) + count, bins = np.histogram(stlist) + plt.bar(bins[:-1], count, width=bins[1] - bins[0]) + plt.title(f"PSTH in segment {seg.index}") +plt.show() + +# ..by neuron + +plt.figure() +for i, group in enumerate(block.groups): + stlist = [st - st.t_start for st in group.spiketrains] + plt.subplot(len(block.groups), 1, i + 1) + count, bins = np.histogram(stlist) + plt.bar(bins[:-1], count, width=bins[1] - bins[0]) + plt.title(f"PSTH of unit {group.name}") +plt.show() + +# ..by tetrode + +plt.figure() +for i, tetrode_id in enumerate(block.annotations["tetrode_ids"]): + stlist = [] + for unit in block.filter(objects=Group, tetrode_id=tetrode_id): + stlist.extend([st - st.t_start for st in unit.spiketrains]) + plt.subplot(2, 1, i + 1) + count, bins = np.histogram(stlist) + plt.bar(bins[:-1], count, width=bins[1] - bins[0]) + plt.title(f"PSTH blend of tetrode {tetrode_id}") +plt.show() diff --git a/doc/source/scripts/spike_sorting_example.py b/doc/source/scripts/spike_sorting_example.py new file mode 100644 index 000000000..7971c2198 --- /dev/null +++ b/doc/source/scripts/spike_sorting_example.py @@ -0,0 +1,42 @@ +""" +Example for usecases.rst +""" + +import numpy as np +from neo import Segment, AnalogSignal, SpikeTrain, Group, View +from quantities import Hz + +# generate some fake data +seg = Segment() +seg.analogsignals.append( + AnalogSignal([[0.1, 0.1, 0.1, 0.1], + [-2.0, -2.0, -2.0, -2.0], + [0.1, 0.1, 0.1, 0.1], + [-0.1, -0.1, -0.1, -0.1], + [-0.1, -0.1, -0.1, -0.1], + [-3.0, -3.0, -3.0, -3.0], + [0.1, 0.1, 0.1, 0.1], + [0.1, 0.1, 0.1, 0.1]], + sampling_rate=1000*Hz, units='V')) + +# extract spike trains from all channels +st_list = [] +for signal in seg.analogsignals: + # use a simple threshhold detector + spike_mask = np.where(np.min(signal.magnitude, axis=1) < -1.0)[0] + + # create a spike train + spike_times = signal.times[spike_mask] + st = SpikeTrain(spike_times, t_start=signal.t_start, t_stop=signal.t_stop) + + # remember the spike waveforms + wf_list = [] + for spike_idx in np.nonzero(spike_mask)[0]: + wf_list.append(signal[spike_idx-1:spike_idx+2, :]) + st.waveforms = np.array(wf_list) + + st_list.append(st) + +unit = Group() +unit.spiketrains = st_list +unit.analogsignals.extend(seg.analogsignals) \ No newline at end of file diff --git a/doc/source/usecases.rst b/doc/source/usecases.rst index f60cddfe3..fad1b582b 100644 --- a/doc/source/usecases.rst +++ b/doc/source/usecases.rst @@ -12,13 +12,13 @@ that we have recorded three trials/episodes. We therefore have a total of Our entire dataset is contained in a :class:`Block`, which in turn contains: * 3 :class:`Segment` objects, each representing data from a single trial, - * 1 :class:`ChannelIndex`. + * 1 :class:`Group`. .. image:: images/multi_segment_diagram.png :width: 75% :align: center -:class:`Segment` and :class:`ChannelIndex` objects provide two different +:class:`Segment` and :class:`Group` objects provide two different ways to access the data, corresponding respectively, in this scenario, to access by **time** and by **space**. @@ -37,7 +37,7 @@ In this example, we're averaging over the channels. import numpy as np from matplotlib import pyplot as plt - + for seg in block.segments: print("Analyzing segment %d" % seg.index) @@ -49,23 +49,20 @@ In this example, we're averaging over the channels. **Spatial (by channel)** -In this case you want to go through your data by channel location and average over time. +In this case you want to go through your data by channel location and average over time. Perhaps you want to see which physical location produces the strongest response, and every stimulus was the same: - + .. doctest:: - - # We assume that our block has only 1 ChannelIndex - chx = block.channelindexes[0]: - siglist = [sig[:, chx.index] for sig in chx.analogsignals] - avg = np.mean(siglist, axis=0) - + + # We assume that our block has only 1 Group + group = block.groups[0] + avg = np.mean(group.analogsignals, axis=0) + plt.figure() - for index, name in zip(chx.index, chx.channel_names): + for index, name in enumerate(group.annotations["channel_names"]): plt.plot(avg[:, index]) plt.title("Average response on channels %s: %s' % (index, name) - - **Mixed example** Combining simultaneously the two approaches of descending the hierarchy @@ -75,7 +72,7 @@ during the experiment and you want to follow up. What was the average response? .. doctest:: - index = chx.index[5] + index = 5 avg = np.mean([seg.analogsignals[0][:, index] for seg in block.segments[::2]], axis=1) plt.plot(avg) @@ -87,13 +84,11 @@ Here is a similar example in which we have recorded with two tetrodes and extracted spikes from the extra-cellular signals. The spike times are contained in :class:`SpikeTrain` objects. -Again, our data set is contained in a :class:`Block`, which contains: - * 3 :class:`Segments` (one per trial). - * 2 :class:`ChannelIndexes` (one per tetrode), which contain: - - * 2 :class:`Unit` objects (= 2 neurons) for the first :class:`ChannelIndex` - * 5 :class:`Units` for the second :class:`ChannelIndex`. + * 7 :class:`Groups` (one per neuron), which each contain: + + * 3 :class:`SpikeTrain` objects + * an annotation showing which tetrode the spiketrains were recorded from In total we have 3 x 7 = 21 :class:`SpikeTrains` in this :class:`Block`. @@ -101,58 +96,69 @@ In total we have 3 x 7 = 21 :class:`SpikeTrains` in this :class:`Block`. :width: 75% :align: center +.. note:: In this scenario we have discarded the original signals, perhaps to save + space, therefore we use annotations to link the spiketrains to the tetrode + they were recorded from. If we wished to include the original + extracellular signals, we would add a reference to the three :class:`AnalogSignal` + objects for the appropriate tetrode to the :class:`Group` for each neuron. There are three ways to access the :class:`SpikeTrain` data: - * by :class:`Segment` - * by :class:`RecordingChannel` - * by :class:`Unit` + * by trial (:class:`Segment`) + * by neuron (:class:`Group`) + * by tetrode -**By Segment** +**By trial** In this example, each :class:`Segment` represents data from one trial, and we want a PSTH for each trial from all units combined: .. doctest:: + plt.figure() for seg in block.segments: - print("Analyzing segment %d" % seg.index) + print(f"Analyzing segment {seg.index}") stlist = [st - st.t_start for st in seg.spiketrains] - plt.figure() + plt.subplot(len(block.segments), 1, seg.index + 1) count, bins = np.histogram(stlist) plt.bar(bins[:-1], count, width=bins[1] - bins[0]) - plt.title("PSTH in segment %d" % seg.index) + plt.title(f"PSTH in segment {seg.index}") + plt.show() -**By Unit** +**By neuron** Now we can calculate the PSTH averaged over trials for each unit, using the -:attr:`block.list_units` property: +:attr:`block.groups` property: .. doctest:: - for unit in block.list_units: - stlist = [st - st.t_start for st in unit.spiketrains] - plt.figure() + plt.figure() + for i, group in enumerate(block.groups): + stlist = [st - st.t_start for st in group.spiketrains] + plt.subplot(len(block.groups), 1, i + 1) count, bins = np.histogram(stlist) plt.bar(bins[:-1], count, width=bins[1] - bins[0]) - plt.title("PSTH of unit %s" % unit.name) - + plt.title(f"PSTH of unit {group.name}") + plt.show() + -**By ChannelIndex** +**By tetrode** Here we calculate a PSTH averaged over trials by channel location, blending all units: .. doctest:: - for chx in block.channelindexes: + plt.figure() + for i, tetrode_id in enumerate(block.annotations["tetrode_ids"]): stlist = [] - for unit in chx.units: + for unit in block.filter(objects=Group, tetrode_id=tetrode_id): stlist.extend([st - st.t_start for st in unit.spiketrains]) - plt.figure() + plt.subplot(2, 1, i + 1) count, bins = np.histogram(stlist) plt.bar(bins[:-1], count, width=bins[1] - bins[0]) - plt.title("PSTH blend of tetrode %s" % chx.name) + plt.title(f"PSTH blend of tetrode {tetrode_id}") + plt.show() Spike sorting @@ -161,7 +167,7 @@ Spike sorting Spike sorting is the process of detecting and classifying high-frequency deflections ("spikes") on a group of physically nearby recording channels. -For example, let's say you have defined a ChannelIndex for a tetrode +For example, let's say you have recordings from a tetrode containing 4 separate channels. Here is an example showing (with fake data) how you could iterate over the contained signals and extract spike times. (Of course in reality you would use a more sophisticated algorithm.) @@ -172,63 +178,54 @@ how you could iterate over the contained signals and extract spike times. seg = Segment() seg.analogsignals.append( AnalogSignal([[0.1, 0.1, 0.1, 0.1], - [-2.0, -2.0, -2.0, -2.0], - [0.1, 0.1, 0.1, 0.1], - [-0.1, -0.1, -0.1, -0.1], - [-0.1, -0.1, -0.1, -0.1], - [-3.0, -3.0, -3.0, -3.0], - [0.1, 0.1, 0.1, 0.1], - [0.1, 0.1, 0.1, 0.1]], - sampling_rate=1000*Hz, units='V')) - chx = ChannelIndex(channel_indexes=[0, 1, 2, 3]) - chx.analogsignals.append(seg.analogsignals[0]) - - - # extract spike trains from each channel + [-2.0, -2.0, -2.0, -2.0], + [0.1, 0.1, 0.1, 0.1], + [-0.1, -0.1, -0.1, -0.1], + [-0.1, -0.1, -0.1, -0.1], + [-3.0, -3.0, -3.0, -3.0], + [0.1, 0.1, 0.1, 0.1], + [0.1, 0.1, 0.1, 0.1]], + sampling_rate=1000*Hz, units='V')) + + # extract spike trains from all channels st_list = [] - for signal in chx.analogsignals: + for signal in seg.analogsignals: # use a simple threshhold detector spike_mask = np.where(np.min(signal.magnitude, axis=1) < -1.0)[0] - + # create a spike train spike_times = signal.times[spike_mask] - st = neo.SpikeTrain(spike_times, t_start=signal.t_start, t_stop=signal.t_stop) - + st = SpikeTrain(spike_times, t_start=signal.t_start, t_stop=signal.t_stop) + # remember the spike waveforms wf_list = [] for spike_idx in np.nonzero(spike_mask)[0]: wf_list.append(signal[spike_idx-1:spike_idx+2, :]) st.waveforms = np.array(wf_list) - + st_list.append(st) At this point, we have a list of spiketrain objects. We could simply create -a single Unit object, assign all spike trains to it, and then assign the -Unit to the group on which we detected it. +a single :class:`Group` object, assign all spiketrains to it, and then also assign the +:class:`AnalogSignal` on which we detected them. .. doctest:: - - u = Unit() - u.spiketrains = st_list - chx.units.append(u) -Now the recording channel group (tetrode) contains a list of analogsignals, -and a single Unit object containing all of the detected spiketrains from those -signals. + unit = Group() + unit.spiketrains = st_list + unit.analogsignals.extend(seg.analogsignals) Further processing could assign each of the detected spikes to an independent source, a putative single neuron. (This processing is outside the scope of Neo. There are many open-source toolboxes to do it, for instance our sister project OpenElectrophy.) -In that case we would create a separate Unit for each cluster, assign its -spiketrains to it, and then store all the units in the original -recording channel group. +In that case we would create a separate :class:`Group` for each cluster, assign its +spiketrains to it, and still store in each group a reference to the original +recording. .. EEG .. Network simulations - - diff --git a/neo/core/__init__.py b/neo/core/__init__.py index 6070483b2..12e920204 100644 --- a/neo/core/__init__.py +++ b/neo/core/__init__.py @@ -11,12 +11,13 @@ .. autoclass:: Block .. autoclass:: Segment -.. autoclass:: ChannelIndex -.. autoclass:: Unit +.. autoclass:: Group .. autoclass:: AnalogSignal .. autoclass:: IrregularlySampledSignal +.. autoclass:: View + .. autoclass:: Event .. autoclass:: Epoch @@ -27,6 +28,11 @@ .. autoclass:: CircularRegionOfInterest .. autoclass:: PolygonRegionOfInterest +Deprecated classes: + +.. autoclass:: ChannelIndex +.. autoclass:: Unit + """ from neo.core.block import Block @@ -46,13 +52,14 @@ from neo.core.regionofinterest import RectangularRegionOfInterest, CircularRegionOfInterest, PolygonRegionOfInterest from neo.core.view import View +from neo.core.group import Group # Block should always be first in this list objectlist = [Block, Segment, ChannelIndex, AnalogSignal, IrregularlySampledSignal, Event, Epoch, Unit, SpikeTrain, ImageSequence, RectangularRegionOfInterest, CircularRegionOfInterest, - PolygonRegionOfInterest, View] + PolygonRegionOfInterest, View, Group] objectnames = [ob.__name__ for ob in objectlist] class_by_name = dict(zip(objectnames, objectlist)) diff --git a/neo/core/analogsignal.py b/neo/core/analogsignal.py index 74bb7c043..ec8dd8692 100644 --- a/neo/core/analogsignal.py +++ b/neo/core/analogsignal.py @@ -31,7 +31,6 @@ from neo.core.baseneo import BaseNeo, MergeError, merge_annotations from neo.core.dataobject import DataObject -from neo.core.channelindex import ChannelIndex from copy import copy, deepcopy from neo.core.basesignal import BaseSignal @@ -142,7 +141,7 @@ class AnalogSignal(BaseSignal): read-only. (:attr:`t_start` + arange(:attr:`shape`[0])/:attr:`sampling_rate`) :channel_index: - access to the channel_index attribute of the principal ChannelIndex + (deprecated) access to the channel_index attribute of the principal ChannelIndex associated with this signal. *Slicing*: @@ -443,9 +442,12 @@ def _pp(line): with pp.group(indent=1): pp.text(line) - for line in ["sampling rate: {}".format(self.sampling_rate), - "time: {} to {}".format(self.t_start, self.t_stop)]: - _pp(line) + _pp("sampling rate: {} {}".format(self.sampling_rate, + self.sampling_rate.dimensionality.string)) + _pp("time: {} {} to {} {}".format(self.t_start, + self.t_start.dimensionality.string, + self.t_stop, + self.t_stop.dimensionality.string)) def time_index(self, t): """Return the array index corresponding to the time `t`""" diff --git a/neo/core/block.py b/neo/core/block.py index 4b65da346..1c7724019 100644 --- a/neo/core/block.py +++ b/neo/core/block.py @@ -21,28 +21,26 @@ class Block(Container): *Usage*:: - >>> from neo.core import (Block, Segment, ChannelIndex, - ... AnalogSignal) + >>> from neo.core import Block, Segment, Group, AnalogSignal >>> from quantities import nA, kHz >>> import numpy as np >>> - >>> # create a Block with 3 Segment and 2 ChannelIndex objects + >>> # create a Block with 3 Segment and 2 Group objects ,,, blk = Block() >>> for ind in range(3): ... seg = Segment(name='segment %d' % ind, index=ind) ... blk.segments.append(seg) ... >>> for ind in range(2): - ... chx = ChannelIndex(name='Array probe %d' % ind, - ... index=np.arange(64)) - ... blk.channel_indexes.append(chx) + ... group = Group(name='Array probe %d' % ind) + ... blk.groups.append(group) ... >>> # Populate the Block with AnalogSignal objects ... for seg in blk.segments: - ... for chx in blk.channel_indexes: + ... for group in blk.groups: ... a = AnalogSignal(np.random.randn(10000, 64)*nA, ... sampling_rate=10*kHz) - ... chx.analogsignals.append(a) + ... group.analogsignals.append(a) ... seg.analogsignals.append(a) *Required attributes/properties*: @@ -57,7 +55,7 @@ class Block(Container): :rec_datetime: (datetime) The date and time of the original recording. *Properties available on this object*: - :list_units: descends through hierarchy and returns a list of + :list_units: (deprecated) descends through hierarchy and returns a list of :class:`Unit` objects existing in the block. This shortcut exists because a common analysis case is analyzing all neurons that you recorded in a session. @@ -67,11 +65,12 @@ class Block(Container): *Container of*: :class:`Segment` - :class:`ChannelIndex` + :class:`Group` + :class:`ChannelIndex` (deprecated) ''' - _container_child_objects = ('Segment', 'ChannelIndex') + _container_child_objects = ('Segment', 'ChannelIndex', 'Group') _child_properties = ('Unit',) _recommended_attrs = ((('file_datetime', datetime), ('rec_datetime', datetime), diff --git a/neo/core/channelindex.py b/neo/core/channelindex.py index d40f575da..9b8479760 100644 --- a/neo/core/channelindex.py +++ b/neo/core/channelindex.py @@ -16,6 +16,10 @@ class ChannelIndex(Container): ''' A container for indexing/grouping data channels. + Use of :class:`ChannelIndex` is deprecated. Its various uses can be replaced + by the :class:`Group` and :class:`View` classes, or by use of + array annotations. + This container has several purposes: * Grouping all :class:`AnalogSignal`\\s and diff --git a/neo/core/group.py b/neo/core/group.py new file mode 100644 index 000000000..cc81b185c --- /dev/null +++ b/neo/core/group.py @@ -0,0 +1,58 @@ +""" +This module implements :class:`Group`, which represents a subset of the +channels in an :class:`AnalogSignal` or :class:`IrregularlySampledSignal`. + +It replaces and extends the grouping function of the former :class:`ChannelIndex` +and :class:`Unit`. +""" + +from neo.core.container import Container + + + +class Group(Container): + """ + Can contain any of the data objects, views, or other groups, + outside the hierarchy of the segment and block containers. + A common use is to link the :class:`SpikeTrain` objects within a :class:`Block`, + possibly across multiple Segments, that were emitted by the same neuron. + + *Required attributes/properties*: + None + + *Recommended attributes/properties*: + :objects: (Neo object) Objects with which to pre-populate the :class:`Group` + :name: (str) A label for the group. + :description: (str) Text description. + :file_origin: (str) Filesystem path or URL of the original data file. + + Note: Any other additional arguments are assumed to be user-specific + metadata and stored in :attr:`annotations`. + + *Container of*: + :class:`AnalogSignal`, :class:`IrregularlySampledSignal`, :class:`SpikeTrain`, + :class:`Event`, :class:`Epoch`, :class:`View`, :class:`Group + """ + _data_child_objects = ( + 'AnalogSignal', 'IrregularlySampledSignal', 'SpikeTrain', 'Event', 'Epoch', 'View' + ) + _container_child_objects = ('Segment', 'Group') + _single_parent_objects = ('Block',) + + def __init__(self, *objects, name=None, description=None, file_origin=None, **annotations): + super().__init__(name=name, description=description, + file_origin=file_origin, **annotations) + self.add(*objects) + + @property + def _container_lookup(self): + return { + cls_name: getattr(self, container_name) + for cls_name, container_name in zip(self._child_objects, self._child_containers) + } + + def add(self, *objects): + """Add a new Neo object to the Group""" + for obj in objects: + container = self._container_lookup[obj.__class__.__name__] + container.append(obj) diff --git a/neo/core/segment.py b/neo/core/segment.py index 4ed275ba6..44f84299f 100644 --- a/neo/core/segment.py +++ b/neo/core/segment.py @@ -243,6 +243,8 @@ def construct_subsegment_by_unit(self, unit_list=None): 2 ''' + # todo: provide equivalent method using Group/View + # add deprecation message (use decorator)? seg = Segment() seg.spiketrains = self.take_spiketrains_by_unit(unit_list) seg.analogsignals = \ diff --git a/neo/core/unit.py b/neo/core/unit.py index 8d8762e1c..19f068d81 100644 --- a/neo/core/unit.py +++ b/neo/core/unit.py @@ -13,6 +13,8 @@ class Unit(Container): ''' A container of :class:`SpikeTrain` objects from a unit. + Use of :class:`Unit` is deprecated. It can be replaced by the :class:`Group`. + A :class:`Unit` regroups all the :class:`SpikeTrain` objects that were emitted by a single spike source during a :class:`Block`. A spike source is often a single neuron but doesn't have to be. The spikes diff --git a/neo/core/view.py b/neo/core/view.py index b368249e2..7c4c14296 100644 --- a/neo/core/view.py +++ b/neo/core/view.py @@ -13,20 +13,35 @@ class View(BaseNeo): """ - docstring goes here + A tool for indexing a subset of the channels within an :class:`AnalogSignal` + or :class:`IrregularlySampledSignal`\\s; + + *Required attributes/properties*: + :obj: (AnalogSignal or IrregularlySampledSignal) The signal being indexed. + :index: (list/1D-array) boolean or integer mask to select the channels of interest. + + *Recommended attributes/properties*: + :name: (str) A label for the view. + :description: (str) Text description. + :file_origin: (str) Filesystem path or URL of the original data file. + :array_annotations: (dict) Dict mapping strings to numpy arrays containing annotations + for all data points + + Note: Any other additional arguments are assumed to be user-specific + metadata and stored in :attr:`annotations`. """ _single_parent_objects = ('Segment',) _single_parent_attrs = ('segment',) _necessary_attrs = ( ('index', np.ndarray, 1, np.dtype('i')), - ('obj', BaseSignal, 1) + ('obj', ('AnalogSignal', 'IrregularlySampledSignal'), 1) ) # "mask" would be an alternative name, proposing "index" for backwards-compatibility with ChannelIndex def __init__(self, obj, index, name=None, description=None, file_origin=None, array_annotations=None, **annotations): - super(View, self).__init__(name=name, description=description, - file_origin=file_origin, **annotations) + super().__init__(name=name, description=description, + file_origin=file_origin, **annotations) if not isinstance(obj, BaseSignal): raise ValueError("Can only take a View of an AnalogSignal " "or an IrregularlySampledSignal") diff --git a/neo/io/tools.py b/neo/io/tools.py index fc4cf7d84..eab5c41df 100644 --- a/neo/io/tools.py +++ b/neo/io/tools.py @@ -14,7 +14,7 @@ from neo.core import (AnalogSignal, Block, Epoch, Event, IrregularlySampledSignal, - ChannelIndex, + ChannelIndex, Group, View, Segment, SpikeTrain, Unit) @@ -88,9 +88,9 @@ class LazyList(MutableSequence): respective object. """ _container_objects = { - Block, Segment, ChannelIndex, Unit} + Block, Segment, ChannelIndex, Unit, Group} _neo_objects = _container_objects.union( - [AnalogSignal, Epoch, Event, + [AnalogSignal, Epoch, Event, View, IrregularlySampledSignal, SpikeTrain]) def __init__(self, io, lazy, items=None): diff --git a/neo/test/coretest/test_analogsignal.py b/neo/test/coretest/test_analogsignal.py index a2aa7b199..06c68d874 100644 --- a/neo/test/coretest/test_analogsignal.py +++ b/neo/test/coretest/test_analogsignal.py @@ -373,8 +373,9 @@ def test__pretty(self): '' % (signal.shape[1], signal.shape[0], signal.units.dimensionality.unicode, signal.dtype)) + ('annotations: %s\n' % signal.annotations) - + ('sampling rate: {}\n'.format(signal.sampling_rate)) - + ('time: {} to {}'.format(signal.t_start, signal.t_stop))) + + ('sampling rate: {} {}\n'.format(signal.sampling_rate, + signal.sampling_rate.dimensionality.string)) + + ('time: {} ms to {} ms'.format(signal.t_start, signal.t_stop))) self.assertEqual(prepr, targ) diff --git a/neo/test/coretest/test_block.py b/neo/test/coretest/test_block.py index 393dc2e3c..f49ef5384 100644 --- a/neo/test/coretest/test_block.py +++ b/neo/test/coretest/test_block.py @@ -256,7 +256,7 @@ def test__children(self): chxs1a = clone_object(self.chxs1) self.assertEqual(self.blk1._container_child_objects, - ('Segment', 'ChannelIndex')) + ('Segment', 'ChannelIndex', 'Group')) self.assertEqual(self.blk1._data_child_objects, ()) self.assertEqual(self.blk1._single_parent_objects, ()) self.assertEqual(self.blk1._multi_child_objects, ()) @@ -265,21 +265,21 @@ def test__children(self): ('Unit',)) self.assertEqual(self.blk1._single_child_objects, - ('Segment', 'ChannelIndex')) + ('Segment', 'ChannelIndex', 'Group')) self.assertEqual(self.blk1._container_child_containers, - ('segments', 'channel_indexes')) + ('segments', 'channel_indexes', 'groups')) self.assertEqual(self.blk1._data_child_containers, ()) self.assertEqual(self.blk1._single_child_containers, - ('segments', 'channel_indexes')) + ('segments', 'channel_indexes', 'groups')) self.assertEqual(self.blk1._single_parent_containers, ()) self.assertEqual(self.blk1._multi_child_containers, ()) self.assertEqual(self.blk1._multi_parent_containers, ()) self.assertEqual(self.blk1._child_objects, - ('Segment', 'ChannelIndex')) + ('Segment', 'ChannelIndex', 'Group')) self.assertEqual(self.blk1._child_containers, - ('segments', 'channel_indexes')) + ('segments', 'channel_indexes', 'groups')) self.assertEqual(self.blk1._parent_objects, ()) self.assertEqual(self.blk1._parent_containers, ()) @@ -341,6 +341,7 @@ def test__children(self): def test__size(self): targ = {'segments': self.nchildren, + 'groups': 0, # need to update test data generation to handle groups 'channel_indexes': self.nchildren} self.assertEqual(self.targobj.size, targ) diff --git a/neo/test/coretest/test_group.py b/neo/test/coretest/test_group.py new file mode 100644 index 000000000..2b0968bcf --- /dev/null +++ b/neo/test/coretest/test_group.py @@ -0,0 +1,60 @@ +""" +Tests of the neo.core.group.Group class and related functions +""" + + +import unittest + +import numpy as np +import quantities as pq +from numpy.testing import assert_array_equal + +from neo.core.analogsignal import AnalogSignal +from neo.core.irregularlysampledsignal import IrregularlySampledSignal +from neo.core.spiketrain import SpikeTrain +from neo.core.segment import Segment +from neo.core.view import View +from neo.core.group import Group + + +class TestView(unittest.TestCase): + + def setUp(self): + test_data = np.random.rand(100, 8) * pq.mV + channel_names = np.array(["a", "b", "c", "d", "e", "f", "g", "h"]) + self.test_signal = AnalogSignal(test_data, + sampling_period=0.1 * pq.ms, + name="test signal", + description="this is a test signal", + array_annotations={"channel_names": channel_names}, + attUQoLtUaE=42) + self.test_view = View(self.test_signal, [1, 2, 5, 7], + name="view of test signal", + description="this is a view of a test signal", + array_annotations={"something": np.array(["A", "B", "C", "D"])}, + sLaTfat="fish") + self.test_spiketrains = [SpikeTrain(np.arange(100.0), units="ms", t_stop=200), + SpikeTrain(np.arange(0.5, 100.5), units="ms", t_stop=200)] + self.test_segment = Segment() + self.test_segment.analogsignals.append(self.test_signal) + self.test_segment.spiketrains.extend(self.test_spiketrains) + + def test_create_group(self): + group = Group(*self.test_spiketrains, self.test_view, self.test_signal, self.test_segment) + + assert group.analogsignals[0] is self.test_signal + assert group.spiketrains[0] is self.test_spiketrains[0] + assert group.spiketrains[1] is self.test_spiketrains[1] + assert group.views[0] is self.test_view + assert len(group.irregularlysampledsignals) == 0 + assert group.segments[0].analogsignals[0] is self.test_signal + + def test_children(self): + group = Group(*self.test_spiketrains, self.test_view, self.test_signal, self.test_segment) + + # note: ordering is by class name for data children (AnalogSignal, SpikeTrain), + # then container children (Segment) + assert group.children == (self.test_signal, + *self.test_spiketrains, + self.test_view, + self.test_segment) diff --git a/neo/test/generate_datasets.py b/neo/test/generate_datasets.py index 2261de3b0..c1799bf34 100644 --- a/neo/test/generate_datasets.py +++ b/neo/test/generate_datasets.py @@ -395,6 +395,8 @@ def fake_neo(obj_type="Block", cascade=True, seed=None, n=1): cascade = 'block' for i, childname in enumerate(getattr(obj, '_child_objects', [])): # we create a few of each class + if childname == 'Group': + continue # avoid infinite recursion, since Groups can contain Groups for j in range(n): if seed is not None: iseed = 10 * seed + 100 * i + 1000 * j From a3cef45191335525e2655756601d86c723698251 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Mon, 6 Jul 2020 14:43:27 +0200 Subject: [PATCH 05/85] pep8 fixes --- doc/source/images/generate_diagram.py | 2 +- doc/source/scripts/multi_tetrode_example.py | 6 ++++-- doc/source/scripts/spike_sorting_example.py | 6 +++--- neo/core/group.py | 1 - neo/core/view.py | 5 +++-- neo/test/coretest/test_group.py | 4 ++-- neo/test/coretest/test_view.py | 5 +++-- 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/doc/source/images/generate_diagram.py b/doc/source/images/generate_diagram.py index c25bb4e8d..ded179927 100644 --- a/doc/source/images/generate_diagram.py +++ b/doc/source/images/generate_diagram.py @@ -213,7 +213,7 @@ def generate_diagram_simple(): 'IrregularlySampledSignal': (.5 + rw * bf * 3, 0.5), 'AnalogSignal': (.5 + rw * bf * 3, 4.9), } - # todo: add ImageSequence, RegionOfInterest + # todo: add ImageSequence, RegionOfInterest generate_diagram('simple_generated_diagram.svg', rect_pos, rect_width, figsize) generate_diagram('simple_generated_diagram.png', diff --git a/doc/source/scripts/multi_tetrode_example.py b/doc/source/scripts/multi_tetrode_example.py index 3c48db1bf..2ed07a937 100644 --- a/doc/source/scripts/multi_tetrode_example.py +++ b/doc/source/scripts/multi_tetrode_example.py @@ -39,8 +39,10 @@ # create two 4-channel AnalogSignals with dummy data signals = { - "Tetrode #1": AnalogSignal(np.random.rand(1000, 4) * mV, sampling_rate=10 * kHz, tetrode_id="Tetrode #1"), - "Tetrode #2": AnalogSignal(np.random.rand(1000, 4) * mV, sampling_rate=10 * kHz, tetrode_id="Tetrode #2") + "Tetrode #1": AnalogSignal(np.random.rand(1000, 4) * mV, + sampling_rate=10 * kHz, tetrode_id="Tetrode #1"), + "Tetrode #2": AnalogSignal(np.random.rand(1000, 4) * mV, + sampling_rate=10 * kHz, tetrode_id="Tetrode #2") } if store_signals: segment.analogsignals.extend(signals.values()) diff --git a/doc/source/scripts/spike_sorting_example.py b/doc/source/scripts/spike_sorting_example.py index 7971c2198..9fbeaca40 100644 --- a/doc/source/scripts/spike_sorting_example.py +++ b/doc/source/scripts/spike_sorting_example.py @@ -17,7 +17,7 @@ [-3.0, -3.0, -3.0, -3.0], [0.1, 0.1, 0.1, 0.1], [0.1, 0.1, 0.1, 0.1]], - sampling_rate=1000*Hz, units='V')) + sampling_rate=1000 * Hz, units='V')) # extract spike trains from all channels st_list = [] @@ -32,11 +32,11 @@ # remember the spike waveforms wf_list = [] for spike_idx in np.nonzero(spike_mask)[0]: - wf_list.append(signal[spike_idx-1:spike_idx+2, :]) + wf_list.append(signal[spike_idx - 1:spike_idx + 2, :]) st.waveforms = np.array(wf_list) st_list.append(st) unit = Group() unit.spiketrains = st_list -unit.analogsignals.extend(seg.analogsignals) \ No newline at end of file +unit.analogsignals.extend(seg.analogsignals) diff --git a/neo/core/group.py b/neo/core/group.py index cc81b185c..665ffe60f 100644 --- a/neo/core/group.py +++ b/neo/core/group.py @@ -9,7 +9,6 @@ from neo.core.container import Container - class Group(Container): """ Can contain any of the data objects, views, or other groups, diff --git a/neo/core/view.py b/neo/core/view.py index 7c4c14296..81929470e 100644 --- a/neo/core/view.py +++ b/neo/core/view.py @@ -36,7 +36,8 @@ class View(BaseNeo): ('index', np.ndarray, 1, np.dtype('i')), ('obj', ('AnalogSignal', 'IrregularlySampledSignal'), 1) ) - # "mask" would be an alternative name, proposing "index" for backwards-compatibility with ChannelIndex + # "mask" would be an alternative name, proposing "index" for + # backwards-compatibility with ChannelIndex def __init__(self, obj, index, name=None, description=None, file_origin=None, array_annotations=None, **annotations): @@ -52,7 +53,7 @@ def __init__(self, obj, index, name=None, description=None, file_origin=None, self.index = np.array(index) if len(self.index.shape) != 1: raise ValueError("index must be a 1D array") - if self.index.dtype == np.bool: # convert boolean mask to integer index + if self.index.dtype == np.bool: # convert boolean mask to integer index if self.index.size != self.obj.shape[-1]: raise ValueError("index size does not match number of channels in signal") self.index = np.arange(self.obj.shape[-1])[self.index] diff --git a/neo/test/coretest/test_group.py b/neo/test/coretest/test_group.py index 2b0968bcf..1939e76fe 100644 --- a/neo/test/coretest/test_group.py +++ b/neo/test/coretest/test_group.py @@ -17,10 +17,10 @@ from neo.core.group import Group -class TestView(unittest.TestCase): +class TestGroup(unittest.TestCase): def setUp(self): - test_data = np.random.rand(100, 8) * pq.mV + test_data = np.random.rand(100, 8) * pq.mV channel_names = np.array(["a", "b", "c", "d", "e", "f", "g", "h"]) self.test_signal = AnalogSignal(test_data, sampling_period=0.1 * pq.ms, diff --git a/neo/test/coretest/test_view.py b/neo/test/coretest/test_view.py index fa009d99b..9c1afa363 100644 --- a/neo/test/coretest/test_view.py +++ b/neo/test/coretest/test_view.py @@ -17,7 +17,7 @@ class TestView(unittest.TestCase): def setUp(self): - self.test_data = np.random.rand(100, 8) * pq.mV + self.test_data = np.random.rand(100, 8) * pq.mV channel_names = np.array(["a", "b", "c", "d", "e", "f", "g", "h"]) self.test_signal = AnalogSignal(self.test_data, sampling_period=0.1 * pq.ms, @@ -56,5 +56,6 @@ def test_resolve(self): self.assertEqual(signal2.shape, (100, 4)) for attr in ('name', 'description', 'sampling_period', 'units'): self.assertEqual(getattr(self.test_signal, attr), getattr(signal2, attr)) - assert_array_equal(signal2.array_annotations["channel_names"], np.array(["b", "c", "f", "h"])) + assert_array_equal(signal2.array_annotations["channel_names"], + np.array(["b", "c", "f", "h"])) assert_array_equal(self.test_data[:, [1, 2, 5, 7]], signal2.magnitude) From 742a1a14534913a91240a3686dcbac7b7732ead4 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Mon, 6 Jul 2020 14:44:09 +0200 Subject: [PATCH 06/85] Make changes agreed during review --- neo/core/group.py | 16 ++++++++++++++-- neo/core/view.py | 2 +- neo/test/coretest/test_group.py | 18 ++++++++++++++++-- 3 files changed, 31 insertions(+), 5 deletions(-) diff --git a/neo/core/group.py b/neo/core/group.py index 665ffe60f..04244da10 100644 --- a/neo/core/group.py +++ b/neo/core/group.py @@ -25,6 +25,10 @@ class Group(Container): :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. + *Optional arguments*: + :allowed_types: (list or tuple) Types of Neo object that are allowed to be + added to the Group. If not specified, any Neo object can be added. + Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. @@ -38,10 +42,16 @@ class Group(Container): _container_child_objects = ('Segment', 'Group') _single_parent_objects = ('Block',) - def __init__(self, *objects, name=None, description=None, file_origin=None, **annotations): + def __init__(self, objects=None, name=None, description=None, file_origin=None, + allowed_types=None, **annotations): super().__init__(name=name, description=description, file_origin=file_origin, **annotations) - self.add(*objects) + if allowed_types is None: + self.allowed_types = None + else: + self.allowed_types = tuple(allowed_types) + if objects: + self.add(*objects) @property def _container_lookup(self): @@ -53,5 +63,7 @@ def _container_lookup(self): def add(self, *objects): """Add a new Neo object to the Group""" for obj in objects: + if self.allowed_types and not isinstance(obj, self.allowed_types): + raise TypeError("This Group can only contain {}".format(self.allowed_types)) container = self._container_lookup[obj.__class__.__name__] container.append(obj) diff --git a/neo/core/view.py b/neo/core/view.py index 81929470e..f0cf10b3b 100644 --- a/neo/core/view.py +++ b/neo/core/view.py @@ -56,7 +56,7 @@ def __init__(self, obj, index, name=None, description=None, file_origin=None, if self.index.dtype == np.bool: # convert boolean mask to integer index if self.index.size != self.obj.shape[-1]: raise ValueError("index size does not match number of channels in signal") - self.index = np.arange(self.obj.shape[-1])[self.index] + self.index, = np.nonzero(self.index) elif self.index.dtype != np.integer: raise ValueError("index must be a boolean or integer list or array") diff --git a/neo/test/coretest/test_group.py b/neo/test/coretest/test_group.py index 1939e76fe..8e02824f7 100644 --- a/neo/test/coretest/test_group.py +++ b/neo/test/coretest/test_group.py @@ -40,7 +40,9 @@ def setUp(self): self.test_segment.spiketrains.extend(self.test_spiketrains) def test_create_group(self): - group = Group(*self.test_spiketrains, self.test_view, self.test_signal, self.test_segment) + objects = [self.test_view, self.test_signal, self.test_segment] + objects.extend(self.test_spiketrains) + group = Group(objects) assert group.analogsignals[0] is self.test_signal assert group.spiketrains[0] is self.test_spiketrains[0] @@ -49,8 +51,12 @@ def test_create_group(self): assert len(group.irregularlysampledsignals) == 0 assert group.segments[0].analogsignals[0] is self.test_signal + def test_create_empty_group(self): + group = Group() + def test_children(self): - group = Group(*self.test_spiketrains, self.test_view, self.test_signal, self.test_segment) + group = Group(self.test_spiketrains + [self.test_view] + + [self.test_signal] + [self.test_segment]) # note: ordering is by class name for data children (AnalogSignal, SpikeTrain), # then container children (Segment) @@ -58,3 +64,11 @@ def test_children(self): *self.test_spiketrains, self.test_view, self.test_segment) + + def test_with_allowed_types(self): + objects = [self.test_signal] + self.test_spiketrains + group = Group(objects, allowed_types=(AnalogSignal, SpikeTrain)) + assert group.analogsignals[0] is self.test_signal + assert group.spiketrains[0] is self.test_spiketrains[0] + assert group.spiketrains[1] is self.test_spiketrains[1] + self.assertRaises(TypeError, group.add, self.test_view) From ebc4646c1e0e81449b4e30f2d2cbf028b4f80ece Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Mon, 6 Jul 2020 16:04:47 +0200 Subject: [PATCH 07/85] Some change in basefromrawio --- neo/io/basefromrawio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/neo/io/basefromrawio.py b/neo/io/basefromrawio.py index 8c27fad63..2b0170c42 100644 --- a/neo/io/basefromrawio.py +++ b/neo/io/basefromrawio.py @@ -32,6 +32,7 @@ import quantities as pq +# test push to andrew class BaseFromRaw(BaseIO): """ From 1085015dbd6e617df933b16151823781ac208cd9 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 7 Jul 2020 16:11:33 +0200 Subject: [PATCH 08/85] Rename `View` to `ChannelView` --- doc/source/core.rst | 8 ++++---- doc/source/grouping.rst | 12 ++++++------ doc/source/images/base_schematic.svg | 2 +- doc/source/images/generate_diagram.py | 2 +- doc/source/scripts/multi_tetrode_example.py | 2 +- doc/source/scripts/spike_sorting_example.py | 2 +- neo/core/__init__.py | 6 +++--- neo/core/channelindex.py | 2 +- neo/core/group.py | 4 ++-- neo/core/segment.py | 2 +- neo/core/view.py | 6 +++--- neo/io/tools.py | 4 ++-- neo/test/coretest/test_group.py | 6 +++--- neo/test/coretest/test_view.py | 12 ++++++------ 14 files changed, 35 insertions(+), 35 deletions(-) diff --git a/doc/source/core.rst b/doc/source/core.rst index f633e3f31..ff3ffe992 100644 --- a/doc/source/core.rst +++ b/doc/source/core.rst @@ -49,10 +49,10 @@ were recorded on which electrodes, which spike trains were obtained from which membrane potential signals, etc. They contain references to data objects that cut across the simple container hierarchy. - * :py:class:`View`: A set of indices into :py:class:`AnalogSignal` objects, + * :py:class:`ChannelView`: A set of indices into :py:class:`AnalogSignal` objects, representing logical and/or physical recording channels. For spike sorting of extracellular signals, where spikes may be recorded on more than one - recording channel, the :py:class:`View` can be used to reference the group of recording channels + recording channel, the :py:class:`ChannelView` can be used to reference the group of recording channels from which the spikes were obtained. * :py:class:`Group`: Can contain any of the data objects, views, or other groups, @@ -149,11 +149,11 @@ the channels on which that neuron spiked:: bl = Block(name='probe data') # one group for each neuron - view0 = View(recorded_signals, index=[0, 1, 2]) + view0 = ChannelView(recorded_signals, index=[0, 1, 2]) unit0 = Group(view0, name='Group 0') bl.groups.append(unit0) - view1 = View(recorded_signals, index=[1, 2, 3]) + view1 = ChannelView(recorded_signals, index=[1, 2, 3]) unit1 = Group(view1, name='Group 1') bl.groups.append(unit1) diff --git a/doc/source/grouping.rst b/doc/source/grouping.rst index 76a84ed2e..9f9ca11d4 100644 --- a/doc/source/grouping.rst +++ b/doc/source/grouping.rst @@ -8,7 +8,7 @@ Grouping and linking data -Migrating from ChannelIndex/Unit to View/Group +Migrating from ChannelIndex/Unit to ChannelView/Group ============================================== @@ -103,11 +103,11 @@ Each :class:`ChannelIndex` also contains the list of channels on which that neur block.channel_indexes.extend((chx0, chx1)) -Using :class:`View` and :class`Group`:: +Using :class:`ChannelView` and :class`Group`:: import numpy as np from quantities import ms, mV, kHz - from neo import Block, Segment, View, Group, SpikeTrain, AnalogSignal + from neo import Block, Segment, ChannelView, Group, SpikeTrain, AnalogSignal block = Block(name="probe data") segment = Segment() @@ -131,11 +131,11 @@ Using :class:`View` and :class`Group`:: unit = Group(spiketrain, name=f"Neuron #{i + 1}") units.append(unit) - # create a View of the signal for each unit, to show which channels the spikes come from + # create a ChannelView of the signal for each unit, to show which channels the spikes come from # and add it to the relevant Group - view0 = View(signal, index=[0, 1, 2], name="Channel Group 1") + view0 = ChannelView(signal, index=[0, 1, 2], name="Channel Group 1") units[0].add(view0) - view1 = View(signal, index=[1, 2, 3], name="Channel Group 2") + view1 = ChannelView(signal, index=[1, 2, 3], name="Channel Group 2") units[1].add(view1) block.groups.extend(units) diff --git a/doc/source/images/base_schematic.svg b/doc/source/images/base_schematic.svg index 6b4a32b11..e1d58f1a0 100644 --- a/doc/source/images/base_schematic.svg +++ b/doc/source/images/base_schematic.svg @@ -3648,7 +3648,7 @@ id="tspan10382-0" y="-91.022598" x="264.88303" - sodipodi:role="line">View + sodipodi:role="line">ChannelView Date: Thu, 3 Sep 2020 12:57:32 +0200 Subject: [PATCH 09/85] First draft for integrating Group into BaseFromRawIO: * group across segments can controllable per object (AnalogSignal, SpikeTrain, Event, Epoch) * units_group_mode is removed * For better readability BaseRawIO.get_group_channel_indexes renamed to BaseRawIO.get_group_signal_channel_indexes --- neo/io/basefromrawio.py | 167 ++++++++++++------------- neo/rawio/baserawio.py | 2 +- neo/test/rawiotest/rawio_compliance.py | 6 +- 3 files changed, 83 insertions(+), 92 deletions(-) diff --git a/neo/io/basefromrawio.py b/neo/io/basefromrawio.py index 2b0170c42..b6af320c1 100644 --- a/neo/io/basefromrawio.py +++ b/neo/io/basefromrawio.py @@ -20,7 +20,7 @@ from neo.core import (AnalogSignal, Block, Epoch, Event, IrregularlySampledSignal, - ChannelIndex, + Group, Segment, SpikeTrain, Unit) from neo.io.baseio import BaseIO @@ -55,7 +55,7 @@ class BaseFromRaw(BaseIO): is_writable = False supported_objects = [Block, Segment, AnalogSignal, - SpikeTrain, Unit, ChannelIndex, Event, Epoch] + SpikeTrain, Unit, Group, Event, Epoch] readable_objects = [Block, Segment] writeable_objects = [] @@ -68,20 +68,27 @@ class BaseFromRaw(BaseIO): mode = 'file' _prefered_signal_group_mode = 'split-all' # 'group-by-same-units' - _prefered_units_group_mode = 'split-all' # 'all-in-one' def __init__(self, *args, **kargs): BaseIO.__init__(self, *args, **kargs) self.parse_header() - def read_block(self, block_index=0, lazy=False, signal_group_mode=None, - units_group_mode=None, load_waveforms=False): + def read_block(self, block_index=0, lazy=False, + create_group_across_segment=None, + signal_group_mode=None, load_waveforms=False): """ - - :param block_index: int default 0. In case of several block block_index can be specified. :param lazy: False by default. + + :param create_group_across_segment: bool or dict + If True : + * Create a neo.Group to group AnalogSignal segments + * Create a neo.Group to group SpikeTrain across segments + * Create a neo.Group to group Event across segments + * Create a neo.Group to group Epoch across segments + With a dict the behavior can be controlled more finely + create_group_across_segment = { 'AnalogSignal': True, 'SpikeTrain': False, ...} :param signal_group_mode: 'split-all' or 'group-by-same-units' (default depend IO): This control behavior for grouping channels in AnalogSignal. @@ -89,12 +96,6 @@ def read_block(self, block_index=0, lazy=False, signal_group_mode=None, * 'group-by-same-units' all channel sharing the same quantity units ar grouped in a 2D AnalogSignal - :param units_group_mode: 'split-all' or 'all-in-one'(default depend IO) - This control behavior for grouping Unit in ChannelIndex: - * 'split-all': each neo.Unit is assigned to a new neo.ChannelIndex - * 'all-in-one': all neo.Unit are grouped in the same neo.ChannelIndex - (global spike sorting for instance) - :param load_waveforms: False by default. Control SpikeTrains.waveforms is None or not. """ @@ -104,10 +105,27 @@ def read_block(self, block_index=0, lazy=False, signal_group_mode=None, if self._prefered_signal_group_mode == 'split-all': self.logger.warning("the default signal_group_mode will change from "\ "'split-all' to 'group-by-same-units' in next release") - - if units_group_mode is None: - units_group_mode = self._prefered_units_group_mode - + + l = ['AnalogSignal', 'SpikeTrain', 'Event', 'Epoch'] + if create_group_across_segment is None: + # @andrew @ julia @michael ? + # I think here the default None could give this + create_group_across_segment = { + 'AnalogSignal': True, #because mimic the old ChannelIndex for AnalogSignals + 'SpikeTrain': False, # False by default because can create too many object for simulation + 'Event': False, # not implemented yet + 'Epoch': False, # not implemented yet + } + elif isinstance(create_group_across_segment, bool): + # bool to dict + v = create_group_across_segment + create_group_across_segment = { k: v for k in l} + elif isinstance(create_group_across_segment, dict): + # put False to missing keys + create_group_across_segment = {create_group_across_segment.get(k, False) for k in l} + else: + raise ValueError('create_group_across_segment must be bool or dict') + # annotations bl_annotations = dict(self.raw_annotations['blocks'][block_index]) bl_annotations.pop('segments') @@ -115,69 +133,44 @@ def read_block(self, block_index=0, lazy=False, signal_group_mode=None, bl = Block(**bl_annotations) - # ChannelIndex are plit in 2 parts: - # * some for AnalogSignals - # * some for Units - - # ChannelIndex for AnalogSignals - all_channels = self.header['signal_channels'] - channel_indexes_list = self.get_group_channel_indexes() - for channel_index in channel_indexes_list: - for i, (ind_within, ind_abs) in self._make_signal_channel_subgroups( - channel_index, signal_group_mode=signal_group_mode).items(): - if signal_group_mode == "split-all": - chidx_annotations = self.raw_annotations['signal_channels'][i] - elif signal_group_mode == "group-by-same-units": - # this should be done with array_annotation soon: - keys = list(self.raw_annotations['signal_channels'][ind_abs[0]].keys()) - # take key from first channel of the group - chidx_annotations = {key: [] for key in keys} - for j in ind_abs: - for key in keys: - v = self.raw_annotations['signal_channels'][j].get(key, None) - chidx_annotations[key].append(v) - if 'name' in list(chidx_annotations.keys()): - chidx_annotations.pop('name') - chidx_annotations = check_annotations(chidx_annotations) - # this should be done with array_annotation soon: - ch_names = all_channels[ind_abs]['name'].astype('U') - neo_channel_index = ChannelIndex(index=ind_within, - channel_names=ch_names, - channel_ids=all_channels[ind_abs]['id'], - name='Channel group {}'.format(i), - ) - neo_channel_index.annotations.update(chidx_annotations) - - bl.channel_indexes.append(neo_channel_index) - - # ChannelIndex and Unit - # 2 case are possible in neo defifferent IO have choosen one or other: - # * All units are grouped in the same ChannelIndex and indexes are all channels: - # 'all-in-one' - # * Each units is assigned to one ChannelIndex: 'split-all' - # This is kept for compatibility - unit_channels = self.header['unit_channels'] - if units_group_mode == 'all-in-one': - if unit_channels.size > 0: - channel_index = ChannelIndex(index=np.array([], dtype='i'), - name='ChannelIndex for all Unit') - bl.channel_indexes.append(channel_index) + # Group for AnalogSignals + if create_group_across_segment['AnalogSignal']: + all_channels = self.header['signal_channels'] + channel_indexes_list = self.get_group_signal_channel_indexes() + sig_groups = [] + for channel_index in channel_indexes_list: + for i, (ind_within, ind_abs) in self._make_signal_channel_subgroups( + channel_index, signal_group_mode=signal_group_mode).items(): + group = Group(name='AnalogSignal group {}'.format(i)) + # @andrew @ julia @michael : do we annotate group across segment with this arrays ? + group.annotate(ch_names=all_channels[ind_abs]['name'].astype('U')) # ?? + group.annotate(channel_ids=all_channels[ind_abs]['id']) # ?? + bl.groups.append(group) + sig_groups.append(group) + + if create_group_across_segment['SpikeTrain']: + unit_channels = self.header['unit_channels'] + st_groups = [] for c in range(unit_channels.size): + group = Group(name='SpikeTrain group {}'.format(i)) + group.annotate(unit_name=unit_channels[c]['name']) + group.annotate(unit_id=unit_channels[c]['id']) unit_annotations = self.raw_annotations['unit_channels'][c] unit_annotations = check_annotations(unit_annotations) - unit = Unit(**unit_annotations) - channel_index.units.append(unit) - - elif units_group_mode == 'split-all': - for c in range(len(unit_channels)): - unit_annotations = self.raw_annotations['unit_channels'][c] - unit_annotations = check_annotations(unit_annotations) - unit = Unit(**unit_annotations) - channel_index = ChannelIndex(index=np.array([], dtype='i'), - name='ChannelIndex for Unit') - channel_index.units.append(unit) - bl.channel_indexes.append(channel_index) - + group.annotations.annotate(**unit_annotations) + bl.groups.append(group) + st_groups.append(group) + + if create_group_across_segment['Event']: + # @andrew @ julia @michael : + # Do we need this ? I guess yes + raise NotImplementedError() + + if create_group_across_segment['Epoch']: + # @andrew @ julia @michael : + # Do we need this ? I guess yes + raise NotImplementedError() + # Read all segments for seg_index in range(self.segment_count(block_index)): seg = self.read_segment(block_index=block_index, seg_index=seg_index, @@ -185,17 +178,15 @@ def read_block(self, block_index=0, lazy=False, signal_group_mode=None, load_waveforms=load_waveforms) bl.segments.append(seg) - # create link to other containers ChannelIndex and Units + # create link between group (across segment) and data objects for seg in bl.segments: - for c, anasig in enumerate(seg.analogsignals): - bl.channel_indexes[c].analogsignals.append(anasig) - - nsig = len(seg.analogsignals) - for c, sptr in enumerate(seg.spiketrains): - if units_group_mode == 'all-in-one': - bl.channel_indexes[nsig].units[c].spiketrains.append(sptr) - elif units_group_mode == 'split-all': - bl.channel_indexes[nsig + c].units[0].spiketrains.append(sptr) + if create_group_across_segment['AnalogSignal']: + for c, anasig in enumerate(seg.analogsignals): + sig_groups[c].add(anasig) + + if create_group_across_segment['SpikeTrain']: + for c, sptr in enumerate(seg.spiketrains): + st_groups[c].add(sptr) bl.create_many_to_one_relationship() @@ -245,7 +236,7 @@ def read_segment(self, block_index=0, seg_index=0, lazy=False, # AnalogSignal signal_channels = self.header['signal_channels'] if signal_channels.size > 0: - channel_indexes_list = self.get_group_channel_indexes() + channel_indexes_list = self.get_group_signal_channel_indexes() for channel_indexes in channel_indexes_list: for i, (ind_within, ind_abs) in self._make_signal_channel_subgroups( channel_indexes, diff --git a/neo/rawio/baserawio.py b/neo/rawio/baserawio.py index fa0e98750..945484126 100644 --- a/neo/rawio/baserawio.py +++ b/neo/rawio/baserawio.py @@ -402,7 +402,7 @@ def _check_common_characteristics(self, channel_indexes): assert np.unique(characteristics[channel_indexes]).size == 1, \ 'This channel set have differents characteristics' - def get_group_channel_indexes(self): + def get_group_signal_channel_indexes(self): """ Usefull for few IOs (TdtrawIO, NeuroExplorerRawIO, ...). diff --git a/neo/test/rawiotest/rawio_compliance.py b/neo/test/rawiotest/rawio_compliance.py index 6dc4f6298..b8f6cd2ae 100644 --- a/neo/test/rawiotest/rawio_compliance.py +++ b/neo/test/rawiotest/rawio_compliance.py @@ -76,7 +76,7 @@ def count_element(reader): if nb_sig > 0: if reader._several_channel_groups: - channel_indexes_list = reader.get_group_channel_indexes() + channel_indexes_list = reader.get_group_signal_channel_indexes() for channel_indexes in channel_indexes_list: sig_size = reader.get_signal_size(block_index, seg_index, channel_indexes=channel_indexes) @@ -133,7 +133,7 @@ def read_analogsignals(reader): return if reader._several_channel_groups: - channel_indexes_list = reader.get_group_channel_indexes() + channel_indexes_list = reader.get_group_signal_channel_indexes() else: channel_indexes_list = [None] @@ -239,7 +239,7 @@ def benchmark_speed_read_signals(reader): """ if reader._several_channel_groups: - channel_indexes_list = reader.get_group_channel_indexes() + channel_indexes_list = reader.get_group_signal_channel_indexes() else: channel_indexes_list = [None] From 581caf75c604dd0f64be0070cb4e2f853c8cefe1 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Tue, 8 Sep 2020 11:52:10 -0700 Subject: [PATCH 10/85] Use NLX_Base_Class_Type to determine recording type. --- neo/rawio/neuralynxrawio.py | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 305049b18..46f930b06 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -661,6 +661,7 @@ def _to_bool(txt): ('ApplicationName', '', None), # also include version number when present ('AcquisitionSystem', '', None), ('ReferenceChannel', '', None), + ('NLX_Base_Class_Type','',None) # in version 4 and earlier versions of Cheetah ] # Filename and datetime may appear in header lines starting with # at @@ -809,6 +810,37 @@ def buildForFile(filename): return info + def typeOfRecording(self): + """ + Determines type of recording in Ncs file with this header. + + RETURN: + one of 'PRE4','BML','DIGITALLYNX','DIGITALLYNXSX','UNKNOWN' + """ + + if 'NLX_Base_Class_Type' in self: + + # older style standard neuralynx acquisition with rounded sampling frequency + if self['NLX_Base_Class_Type'] == 'CscAcqEnt': + return 'PRE4' + + # BML style with fractional frequency and microsPerSamp + elif self['NLX_Base_Class_Type'] == 'BmlAcq': + return 'BML' + + elif 'HardwareSubsystemType' in self: + + # DigitalLynx + if self['HardwareSubsystemType'] == 'DigitalLynx': + return 'DIGITALLYNX' + + # DigitalLynxSX + elif self['HardwareSubsystemType'] == 'DigitalLynxSX': + return 'DIGITALLYNXSX' + + else: + return 'UNKNOWN' + class NcsHeader(): """ From 2bbc68f0269f6a90771ae549f68c79dc768c5fa2 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Tue, 8 Sep 2020 11:52:45 -0700 Subject: [PATCH 11/85] Initial test of Ncs recording type from header. --- neo/test/rawiotest/test_neuralynxrawio.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 1c5a1aa92..b13c9e689 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -2,6 +2,7 @@ from neo.rawio.neuralynxrawio import NeuralynxRawIO from neo.test.rawiotest.common_rawio_test import BaseTestRawIO +from neo.rawio.neuralynxrawio import NlxHeader import logging @@ -14,7 +15,8 @@ class TestNeuralynxRawIO(BaseTestRawIO, unittest.TestCase, ): 'Cheetah_v5.5.1/original_data', 'Cheetah_v5.6.3/original_data', 'Cheetah_v5.7.4/original_data', - 'Cheetah_v6.3.2/incomplete_blocks' + 'Cheetah_v6.3.2/incomplete_blocks', + 'Cheetah_v4.0.2/original_data' ] files_to_download = [ 'Cheetah_v5.5.1/original_data/CheetahLogFile.txt', @@ -58,7 +60,20 @@ class TestNeuralynxRawIO(BaseTestRawIO, unittest.TestCase, ): 'Cheetah_v5.7.4/README.txt', 'Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs', 'Cheetah_v6.3.2/incomplete_blocks/Events.nev', - 'Cheetah_v6.3.2/incomplete_blocks/README.txt'] + 'Cheetah_v6.3.2/incomplete_blocks/README.txt', + 'Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs'] + +class TestNcsRecordingType(TestNeuralynxRawIO, unittest.TestCase): + """ + Test of decoding of NlxHeader for type of recording. + """ + + def test_recording_types(self): + + filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') + hdr = NlxHeader.buildForFile(filename) + self.assertEqual(hdr.typeOfRecording(),'PRE4') + if __name__ == "__main__": From bfa1af52afa65c90e80ba9156378b8302cb83bf8 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Fri, 2 Oct 2020 15:03:47 +0200 Subject: [PATCH 12/85] support for Group object in NixIO --- neo/core/group.py | 3 +- neo/io/nixio.py | 110 ++++++++++++++++++++++++++++++++-- neo/test/iotest/test_nixio.py | 48 ++++++++++++++- 3 files changed, 151 insertions(+), 10 deletions(-) diff --git a/neo/core/group.py b/neo/core/group.py index bf9e8f727..1691e5314 100644 --- a/neo/core/group.py +++ b/neo/core/group.py @@ -37,7 +37,8 @@ class Group(Container): :class:`Event`, :class:`Epoch`, :class:`ChannelView`, :class:`Group """ _data_child_objects = ( - 'AnalogSignal', 'IrregularlySampledSignal', 'SpikeTrain', 'Event', 'Epoch', 'ChannelView' + 'AnalogSignal', 'IrregularlySampledSignal', 'SpikeTrain', + 'Event', 'Epoch', 'ChannelView', 'ImageSequence' ) _container_child_objects = ('Segment', 'Group') _single_parent_objects = ('Block',) diff --git a/neo/io/nixio.py b/neo/io/nixio.py index bd7aa553f..865aea833 100644 --- a/neo/io/nixio.py +++ b/neo/io/nixio.py @@ -30,6 +30,7 @@ from uuid import uuid4 import warnings from distutils.version import LooseVersion as Version +from itertools import chain import quantities as pq import numpy as np @@ -37,7 +38,7 @@ from .baseio import BaseIO from ..core import (Block, Segment, ChannelIndex, AnalogSignal, IrregularlySampledSignal, Epoch, Event, SpikeTrain, - ImageSequence, Unit) + ImageSequence, Unit, ChannelView, Group) from ..io.proxyobjects import BaseProxy from ..version import version as neover @@ -164,7 +165,7 @@ class NixIO(BaseIO): is_readable = True is_writable = True - supported_objects = [Block, Segment, ChannelIndex, + supported_objects = [Block, Segment, ChannelIndex, Group, ChannelView, AnalogSignal, IrregularlySampledSignal, Epoch, Event, SpikeTrain, Unit] readable_objects = [Block] @@ -304,10 +305,18 @@ def _nix_to_neo_block(self, nix_block): # descend into Groups for grp in nix_block.groups: - newseg = self._nix_to_neo_segment(grp) - neo_block.segments.append(newseg) - # parent reference - newseg.block = neo_block + if grp.type == "neo.segment": + newseg = self._nix_to_neo_segment(grp) + neo_block.segments.append(newseg) + # parent reference + newseg.block = neo_block + elif grp.type == "neo.group": + newgrp = self._nix_to_neo_group(grp) + neo_block.groups.append(newgrp) + # parent reference + newgrp.block = neo_block + else: + raise Exception("Unexpected group type") # find free floating (Groupless) signals and spiketrains blockdas = self._group_signals(nix_block.data_arrays) @@ -395,6 +404,27 @@ def _nix_to_neo_segment(self, nix_group): return neo_segment + def _nix_to_neo_group(self, nix_group): + neo_attrs = self._nix_attr_to_neo(nix_group) + neo_group = Group(**neo_attrs) + self._neo_map[nix_group.name] = neo_group + dataarrays = list(filter( + lambda da: da.type in ("neo.analogsignal", + "neo.irregularlysampledsignal", + "neo.imagesequence",), + nix_group.data_arrays)) + dataarrays = self._group_signals(dataarrays) + # descend into DataArrays + for name in dataarrays: + obj = self._neo_map[name] + neo_group.add(obj) + # descend into MultiTags + for mtag in nix_group.multi_tags: + obj = self._neo_map[mtag.name] + neo_group.add(obj) + # todo: handle sub-groups, once we implement writing those + return neo_group + def _nix_to_neo_channelindex(self, nix_source): neo_attrs = self._nix_attr_to_neo(nix_source) channels = list(self._nix_attr_to_neo(c) for c in nix_source.sources @@ -676,6 +706,10 @@ def write_block(self, block, use_obj_names=False): for chx in block.channel_indexes: self._write_channelindex(chx, nixblock) + # descend into Neo Groups + for group in block.groups: + self._write_group(group, nixblock) + self._create_source_links(block, nixblock) def _write_channelindex(self, chx, nixblock): @@ -784,6 +818,70 @@ def _write_segment(self, segment, nixblock): for imagesequence in segment.imagesequences: self._write_imagesequence(imagesequence, nixblock, nixgroup) + def _write_group(self, neo_group, nixblock): + """ + Convert the provided Neo Group to a NIX Group and write it to the + NIX file. + + :param neo_group: Neo Group to be written + :param nixblock: NIX Block where the NIX Group will be created + """ + if "nix_name" in neo_group.annotations: + nix_name = neo_group.annotations["nix_name"] + else: + nix_name = "neo.group.{}".format(self._generate_nix_name()) + neo_group.annotate(nix_name=nix_name) + + nixgroup = nixblock.create_group(nix_name, "neo.group") + nixgroup.metadata = nixblock.metadata.create_section( + nix_name, "neo.group.metadata" + ) + metadata = nixgroup.metadata + neoname = neo_group.name if neo_group.name is not None else "" + metadata["neo_name"] = neoname + nixgroup.definition = neo_group.description + if neo_group.annotations: + for k, v in neo_group.annotations.items(): + self._write_property(metadata, k, v) + + # link signals and image sequences + objnames = [] + for obj in chain( + neo_group.analogsignals, + neo_group.irregularlysampledsignals, + neo_group.imagesequences, + ): + if not ("nix_name" in obj.annotations + and obj.annotations["nix_name"] in self._signal_map): + # the following restriction could be relaxed later + # but for a first pass this simplifies my mental model + raise Exception("Orphan signals/image sequences cannot be stored, needs to belong to a Segment") + objnames.append(obj.annotations["nix_name"]) + for name in objnames: + for da in self._signal_map[name]: + nixgroup.data_arrays.append(da) + + # link events, epochs and spiketrains + objnames = [] + for obj in chain( + neo_group.events, + neo_group.epochs, + neo_group.spiketrains, + ): + if not ("nix_name" in obj.annotations + and obj.annotations["nix_name"] in nixblock.multi_tags): + # the following restriction could be relaxed later + # but for a first pass this simplifies my mental model + raise Exception("Orphan epochs/events/spiketrains cannot be stored, needs to belong to a Segment") + objnames.append(obj.annotations["nix_name"]) + for name in objnames: + mt = nixblock.multi_tags[name] + nixgroup.multi_tags.append(mt) + + if len(neo_group.groups) > 0: + # todo + raise NotImplementedError("Cannot yet store groups within groups") + def _write_analogsignal(self, anasig, nixblock, nixgroup): """ Convert the provided ``anasig`` (AnalogSignal) to a list of NIX diff --git a/neo/test/iotest/test_nixio.py b/neo/test/iotest/test_nixio.py index 6eaeacae3..f69d46ed8 100644 --- a/neo/test/iotest/test_nixio.py +++ b/neo/test/iotest/test_nixio.py @@ -20,7 +20,7 @@ from datetime import date, time, datetime from tempfile import mkdtemp - +from itertools import chain import unittest import string import numpy as np @@ -28,7 +28,7 @@ from neo.core import (Block, Segment, ChannelIndex, AnalogSignal, IrregularlySampledSignal, Unit, SpikeTrain, - Event, Epoch, ImageSequence) + Event, Epoch, ImageSequence, Group) from neo.test.iotest.common_io_test import BaseTestIO from neo.io.nixio import (NixIO, create_quantity, units_to_string, neover, dt_from_nix, dt_to_nix, DATETIMEANNOTATION) @@ -60,7 +60,10 @@ class NixIOTest(unittest.TestCase): def compare_blocks(self, neoblocks, nixblocks): for neoblock, nixblock in zip(neoblocks, nixblocks): self.compare_attr(neoblock, nixblock) - self.assertEqual(len(neoblock.segments), len(nixblock.groups)) + self.assertEqual(len(neoblock.segments), + len([grp for grp in nixblock.groups if grp.type == "neo.segment"])) + self.assertEqual(len(neoblock.groups), + len([grp for grp in nixblock.groups if grp.type == "neo.group"])) for idx, neoseg in enumerate(neoblock.segments): nixgrp = nixblock.groups[neoseg.annotations["nix_name"]] self.compare_segment_group(neoseg, nixgrp) @@ -1001,6 +1004,45 @@ def test_spiketrain_write(self): spiketrain.left_sweep = pq.Quantity(-10, "ms") self.write_and_compare([block]) + def test_group_write(self): + signals = [ + AnalogSignal(np.random.random(size=(1000, 5)) * pq.mV, + sampling_period=1 * pq.ms, name="sig1"), + AnalogSignal(np.random.random(size=(1000, 3)) * pq.mV, + sampling_period=1 * pq.ms, name="sig2"), + ] + spiketrains = [ + SpikeTrain([0.1, 54.3, 76.6, 464.2], units=pq.ms, + t_stop=1000.0 * pq.ms, t_start=0.0 * pq.ms), + SpikeTrain([30.1, 154.3, 276.6, 864.2], units=pq.ms, + t_stop=1000.0 * pq.ms, t_start=0.0 * pq.ms), + SpikeTrain([120.1, 454.3, 576.6, 764.2], units=pq.ms, + t_stop=1000.0 * pq.ms, t_start=0.0 * pq.ms), + ] + epochs = [ + Epoch(times=[0, 500], durations=[100, 100], units=pq.ms, labels=["A", "B"]) + ] + + seg = Segment(name="seg1") + seg.analogsignals.extend(signals) + seg.spiketrains.extend(spiketrains) + seg.epochs.extend(epochs) + for obj in chain(signals, spiketrains, epochs): + obj.segment = seg + + groups = [ + Group(objects=(signals[0:1] + spiketrains[0:2] + epochs), name="group1"), + Group(objects=(signals[1:2] + spiketrains[1:] + epochs), name="group2") + ] + + block = Block(name="block1") + block.segments.append(seg) + block.groups.extend(groups) + for obj in chain([seg], groups): + obj.block = block + + self.write_and_compare([block]) + def test_metadata_structure_write(self): neoblk = self.create_all_annotated() self.io.write_block(neoblk) From 90db72ee2bbffe1d3aad0b9e588a701d35c5717d Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Mon, 5 Oct 2020 12:11:20 +0200 Subject: [PATCH 13/85] Add support for ChannelView to NixIO --- neo/core/group.py | 2 +- neo/io/nixio.py | 59 +++++++++++++++++++++++++++++++++++ neo/test/iotest/test_nixio.py | 5 +-- 3 files changed, 63 insertions(+), 3 deletions(-) diff --git a/neo/core/group.py b/neo/core/group.py index 1691e5314..84009138d 100644 --- a/neo/core/group.py +++ b/neo/core/group.py @@ -34,7 +34,7 @@ class Group(Container): *Container of*: :class:`AnalogSignal`, :class:`IrregularlySampledSignal`, :class:`SpikeTrain`, - :class:`Event`, :class:`Epoch`, :class:`ChannelView`, :class:`Group + :class:`Event`, :class:`Epoch`, :class:`ChannelView`, :class:`Group` """ _data_child_objects = ( 'AnalogSignal', 'IrregularlySampledSignal', 'SpikeTrain', diff --git a/neo/io/nixio.py b/neo/io/nixio.py index 865aea833..0f8116725 100644 --- a/neo/io/nixio.py +++ b/neo/io/nixio.py @@ -420,8 +420,11 @@ def _nix_to_neo_group(self, nix_group): neo_group.add(obj) # descend into MultiTags for mtag in nix_group.multi_tags: + if mtag.type == "neo.channelview" and mtag.name not in self._neo_map: + self._nix_to_neo_channelview(mtag) obj = self._neo_map[mtag.name] neo_group.add(obj) + # todo: handle sub-groups, once we implement writing those return neo_group @@ -465,6 +468,15 @@ def _nix_to_neo_channelindex(self, nix_source): return neo_chx + def _nix_to_neo_channelview(self, nix_mtag): + neo_attrs = self._nix_attr_to_neo(nix_mtag) + index = nix_mtag.positions + nix_name, = self._group_signals(nix_mtag.references).keys() + obj = self._neo_map[nix_name] + neo_chview = ChannelView(obj, index, **neo_attrs) + self._neo_map[nix_mtag.name] = neo_chview + return neo_chview + def _nix_to_neo_unit(self, nix_source): neo_attrs = self._nix_attr_to_neo(nix_source) neo_unit = Unit(**neo_attrs) @@ -770,6 +782,49 @@ def _write_channelindex(self, chx, nixblock): for unit in chx.units: self._write_unit(unit, nixsource) + def _write_channelview(self, chview, nixblock, nixgroup): + """ + Convert the provided Neo ChannelView to a NIX MultiTag and write it to + the NIX file. + + :param chx: The Neo ChannelView to be written + :param nixblock: NIX Block where the MultiTag will be created + """ + if "nix_name" in chview.annotations: + nix_name = chview.annotations["nix_name"] + else: + nix_name = "neo.channelview.{}".format(self._generate_nix_name()) + chview.annotate(nix_name=nix_name) + + channels = nixblock.create_data_array( + "{}.index".format(nix_name), "neo.channelview.index", data=chview.index + ) + + nixmt = nixblock.create_multi_tag(nix_name, "neo.channelview", + positions=channels) + + nixmt.metadata = nixgroup.metadata.create_section( + nix_name, "neo.channelview.metadata" + ) + metadata = nixmt.metadata + neoname = chview.name if chview.name is not None else "" + metadata["neo_name"] = neoname + nixmt.definition = chview.description + if chview.annotations: + for k, v in chview.annotations.items(): + self._write_property(metadata, k, v) + + # link tag to the data array for the ChannelView's signal + if not ("nix_name" in chview.obj.annotations + and chview.obj.annotations["nix_name"] in self._signal_map): + # the following restriction could be relaxed later + # but for a first pass this simplifies my mental model + raise Exception("Need to save signals before saving views") + nix_name = chview.obj.annotations["nix_name"] + nixmt.references.extend(self._signal_map[nix_name]) + + nixgroup.multi_tags.append(nixmt) + def _write_segment(self, segment, nixblock): """ Convert the provided Neo Segment to a NIX Group and write it to the @@ -878,6 +933,10 @@ def _write_group(self, neo_group, nixblock): mt = nixblock.multi_tags[name] nixgroup.multi_tags.append(mt) + # save channel views + for chview in neo_group.channelviews: + self._write_channelview(chview, nixblock, nixgroup) + if len(neo_group.groups) > 0: # todo raise NotImplementedError("Cannot yet store groups within groups") diff --git a/neo/test/iotest/test_nixio.py b/neo/test/iotest/test_nixio.py index f69d46ed8..0bc90efdf 100644 --- a/neo/test/iotest/test_nixio.py +++ b/neo/test/iotest/test_nixio.py @@ -28,7 +28,7 @@ from neo.core import (Block, Segment, ChannelIndex, AnalogSignal, IrregularlySampledSignal, Unit, SpikeTrain, - Event, Epoch, ImageSequence, Group) + Event, Epoch, ImageSequence, Group, ChannelView) from neo.test.iotest.common_io_test import BaseTestIO from neo.io.nixio import (NixIO, create_quantity, units_to_string, neover, dt_from_nix, dt_to_nix, DATETIMEANNOTATION) @@ -1030,8 +1030,9 @@ def test_group_write(self): for obj in chain(signals, spiketrains, epochs): obj.segment = seg + views = [ChannelView(index=np.array([0, 3, 4]), obj=signals[0], name="view_of_sig1")] groups = [ - Group(objects=(signals[0:1] + spiketrains[0:2] + epochs), name="group1"), + Group(objects=(signals[0:1] + spiketrains[0:2] + epochs + views), name="group1"), Group(objects=(signals[1:2] + spiketrains[1:] + epochs), name="group2") ] From 6026671100ea5b9a8438b7e7cc95f23adb37a1a3 Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Tue, 6 Oct 2020 16:45:34 +0200 Subject: [PATCH 14/85] Limit index attribute of ChannelIndexs in dataset generation ... to only cover realistic values (0 to len(index)) --- neo/test/coretest/test_generate_datasets.py | 2 +- neo/test/generate_datasets.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/neo/test/coretest/test_generate_datasets.py b/neo/test/coretest/test_generate_datasets.py index 568917777..e27b6462e 100644 --- a/neo/test/coretest/test_generate_datasets.py +++ b/neo/test/coretest/test_generate_datasets.py @@ -592,7 +592,7 @@ def subcheck__generate_datasets(self, cls, cascade, seed=None): resattr = get_fake_values(cls, annotate=False, seed=0) if seed is not None: for name, value in resattr.items(): - if name in ['channel_names', 'channel_indexes', 'channel_index', 'coordinates']: + if name in ['channel_names', 'channel_indexes', 'index', 'coordinates']: continue try: try: diff --git a/neo/test/generate_datasets.py b/neo/test/generate_datasets.py index c1799bf34..42952c794 100644 --- a/neo/test/generate_datasets.py +++ b/neo/test/generate_datasets.py @@ -210,6 +210,8 @@ def get_fake_value(name, datatype, dim=0, dtype='float', seed=None, units=None, data = np.arange(n) elif n and name == 'channel_names': data = np.array(["ch%d" % i for i in range(n)]) + elif n and name == 'index': # ChannelIndex.index + data = np.random.randint(0, n, np.random.randint(n)+1) elif n and obj == 'AnalogSignal': if name == 'signal': size = [] @@ -274,8 +276,7 @@ def get_fake_values(cls, annotate=True, seed=None, n=None): If annotate is True (default), also add annotations to the values. """ - if hasattr(cls, - 'lower'): # is this a test that cls is a string? better to use isinstance(cls, + if hasattr(cls, 'lower'): # is this a test that cls is a string? better to use isinstance(cls, # basestring), no? cls = class_by_name[cls] # iseed is needed below for generation of array annotations From 7feb86dc4ae62a6bbec634c7bc0312a7eb735c5c Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Wed, 7 Oct 2020 11:30:34 +0200 Subject: [PATCH 15/85] Fix test dataset generation of ChannelIndex attributes and corresponding tests --- neo/test/coretest/test_block.py | 8 ++++---- neo/test/coretest/test_generate_datasets.py | 2 +- neo/test/generate_datasets.py | 6 ++++-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/neo/test/coretest/test_block.py b/neo/test/coretest/test_block.py index f49ef5384..9e2b3c397 100644 --- a/neo/test/coretest/test_block.py +++ b/neo/test/coretest/test_block.py @@ -243,8 +243,8 @@ def test__merge(self): blk1a.segments.append(self.segs2[0]) blk1a.merge(self.blk2) - segs1a = clone_object(self.blk1).segments - chxs1a = clone_object(self.chxs1) + segs1a = deepcopy(self.blk1.segments) + chxs1a = deepcopy(self.chxs1) assert_same_sub_schema(chxs1a + self.chxs2, blk1a.channel_indexes) @@ -252,8 +252,8 @@ def test__merge(self): blk1a.segments) def test__children(self): - segs1a = clone_object(self.blk1).segments - chxs1a = clone_object(self.chxs1) + segs1a = deepcopy(self.blk1.segments) + chxs1a = deepcopy(self.chxs1) self.assertEqual(self.blk1._container_child_objects, ('Segment', 'ChannelIndex', 'Group')) diff --git a/neo/test/coretest/test_generate_datasets.py b/neo/test/coretest/test_generate_datasets.py index e27b6462e..45409b7a3 100644 --- a/neo/test/coretest/test_generate_datasets.py +++ b/neo/test/coretest/test_generate_datasets.py @@ -592,7 +592,7 @@ def subcheck__generate_datasets(self, cls, cascade, seed=None): resattr = get_fake_values(cls, annotate=False, seed=0) if seed is not None: for name, value in resattr.items(): - if name in ['channel_names', 'channel_indexes', 'index', 'coordinates']: + if name in ['channel_names', 'channel_ids', 'index', 'coordinates']: continue try: try: diff --git a/neo/test/generate_datasets.py b/neo/test/generate_datasets.py index 42952c794..41a81c1db 100644 --- a/neo/test/generate_datasets.py +++ b/neo/test/generate_datasets.py @@ -206,12 +206,14 @@ def get_fake_value(name, datatype, dim=0, dtype='float', seed=None, units=None, data = np.array(0.0) elif name == 't_stop': data = np.array(1.0) - elif n and name == 'channel_indexes': + elif n and name in ['channel_indexes', 'channel_ids']: data = np.arange(n) + elif n and name == 'coordinates': + data = np.arange(0, 2*n).reshape((n, 2)) elif n and name == 'channel_names': data = np.array(["ch%d" % i for i in range(n)]) elif n and name == 'index': # ChannelIndex.index - data = np.random.randint(0, n, np.random.randint(n)+1) + data = np.random.randint(0, n, n) elif n and obj == 'AnalogSignal': if name == 'signal': size = [] From 0dae32b5f14a84396045e8f1da3de26490b0a076 Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Wed, 7 Oct 2020 14:29:58 +0200 Subject: [PATCH 16/85] Relax index type restrictions for View for compatibility with ChannelIndex.index --- neo/core/view.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/neo/core/view.py b/neo/core/view.py index 273536665..0b6b64d81 100644 --- a/neo/core/view.py +++ b/neo/core/view.py @@ -57,8 +57,9 @@ def __init__(self, obj, index, name=None, description=None, file_origin=None, if self.index.size != self.obj.shape[-1]: raise ValueError("index size does not match number of channels in signal") self.index, = np.nonzero(self.index) - elif self.index.dtype != np.integer: - raise ValueError("index must be a boolean or integer list or array") + # allow any type of integer representation + elif self.index.dtype.char not in np.typecodes['AllInteger']: + raise ValueError("index must be of a list or array of data type boolean or integer") if not hasattr(self, 'array_annotations') or not self.array_annotations: self.array_annotations = ArrayDict(self._get_arr_ann_length()) From de0b8591646f55419083c7d0d29e7b02734ef348 Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Wed, 7 Oct 2020 16:40:16 +0200 Subject: [PATCH 17/85] Add version converter --- neo/converter.py | 90 ++++++++++++++++++++++++++++++++++++ neo/test/test_converter.py | 94 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 neo/converter.py create mode 100644 neo/test/test_converter.py diff --git a/neo/converter.py b/neo/converter.py new file mode 100644 index 000000000..175688985 --- /dev/null +++ b/neo/converter.py @@ -0,0 +1,90 @@ +from neo import Group, Unit, ChannelView, SpikeTrain +from neo.core.basesignal import BaseSignal + + +def _convert_unit(unit): + group_unit = Group(unit.spiketrains, + name=unit.name, + file_origin=unit.file_origin, + description=unit.description, + allowed_types=[SpikeTrain], + **unit.annotations) + # clean up references + for st in unit.spiketrains: + delattr(st, 'unit') + return group_unit + + +def _convert_channel_index(channel_index): + # convert child objects + new_child_objects = [] + for child_obj in channel_index.children: + if isinstance(child_obj, Unit): + new_unit = _convert_unit(child_obj) + new_child_objects.append(new_unit) + elif isinstance(child_obj, BaseSignal): + # always generate view, as this might provide specific info regarding the object + new_view = ChannelView(child_obj, channel_index.index, + name=channel_index.name, + description=channel_index.description, + file_origin=channel_index.file_origin, + **channel_index.annotations) + new_view.array_annotate(channel_ids=channel_index.channel_ids, + channel_names=channel_index.channel_names) + + # separate dimenions of coordinates into different 1D array_annotations + if channel_index.coordinates.shape: + if len(channel_index.coordinates.shape) == 1: + new_view.array_annotate(coordinates=channel_index.coordinates) + elif len(channel_index.coordinates.shape) == 2: + for dim in range(channel_index.coordinates.shape[1]): + new_view.array_annotate( + **{f'coordinates_dim{dim}': channel_index.coordinates[:, dim]}) + else: + raise ValueError(f'Incompatible channel index coordinates with wrong ' + f'dimensions: Provided coordinates have shape ' + f'{channel_index.coordinates.shape}.') + + # clean up references + delattr(child_obj, 'channel_index') + + new_child_objects.append(new_view) + + new_channel_group = Group(new_child_objects, + name=channel_index.name, + file_origin=channel_index.file_origin, + description=channel_index.description, + **channel_index.annotations) + + return new_channel_group + + +def convert_channelindex_to_view_group(block): + """ + Convert deprecated ChannelIndex and Unit objects to ChannelView and Group objects + + This conversion is preserving all information stored as attributes and (array) annotations. + The conversion is done in-place. + Each ChannelIndex is represented as a Group. Linked Unit objects are represented as child Group + (subgroup) objects. Linked data objects (neo.AnalogSignal, neo.IrregularlySampledSignal) are + represented by a View object linking to the original data object. + Attributes are as far as possible conserved by the conversion of objects. `channel_ids`, + `channel_names` and `coordinates` are converted to array_annotations. + + :param block: neo.Block structure to be converted + :return: block: updated neo.Block structure + """ + for channel_index in block.channel_indexes: + new_channel_group = _convert_channel_index(channel_index) + block.groups.append(new_channel_group) + + # clean up references + delattr(block, 'channel_indexes') + + # this is a hack to clean up ImageSequence objects that are not properly linked to + # ChannelIndex objects, see also Issue #878 + for seg in block.segments: + for imgseq in seg.imagesequences: + delattr(imgseq, 'channel_index') + + return block diff --git a/neo/test/test_converter.py b/neo/test/test_converter.py new file mode 100644 index 000000000..d2db2c5bd --- /dev/null +++ b/neo/test/test_converter.py @@ -0,0 +1,94 @@ +""" +Tests of the neo.conversion module +""" + +import unittest +import copy +import numpy as np + +from neo.io.proxyobjects import (AnalogSignalProxy, SpikeTrainProxy, + EventProxy, EpochProxy) + +from neo.core import (Epoch, Event, SpikeTrain) +from neo.core.basesignal import BaseSignal + +from neo.test.tools import (assert_arrays_equal, assert_same_attributes) +from neo.test.generate_datasets import fake_neo +from neo.converter import convert_channelindex_to_view_group + +class ConversionTest(unittest.TestCase): + def setUp(self): + block = fake_neo(n=3) + self.old_block = copy.deepcopy(block) + self.new_block = convert_channelindex_to_view_group(block) + + def test_no_deprecated_attributes(self): + self.assertFalse(hasattr(self.new_block, 'channel_indexes')) + # collecting data objects + objs = [] + for seg in self.new_block.segments: + objs.extend(seg.analogsignals) + objs.extend(seg.irregularlysampledsignals) + objs.extend(seg.events) + objs.extend(seg.epochs) + objs.extend(seg.spiketrains) + objs.extend(seg.imagesequences) + + for obj in objs: + if isinstance(obj, BaseSignal): + self.assertFalse(hasattr(obj, 'channel_index')) + elif isinstance(obj, SpikeTrain): + self.assertFalse(hasattr(obj, 'unit')) + elif isinstance(obj, (Event, Epoch)): + pass + else: + raise TypeError(f'Unexpected data type object {type(obj)}') + + def test_block_conversion(self): + # verify that all previous data is present in new structure + groups = self.new_block.groups + for channel_index in self.old_block.channel_indexes: + # check existence of objects and attributes + self.assertIn(channel_index.name, [g.name for g in groups]) + group = groups[[g.name for g in groups].index(channel_index.name)] + + # comparing group attributes to channel_index attributes + assert_same_attributes(group, channel_index) + self.assertDictEqual(channel_index.annotations, group.annotations) + + # comparing views and their attributes + view_names = np.asarray([v.name for v in group.channelviews]) + matching_views = np.asarray(group.channelviews)[view_names == channel_index.name] + for view in matching_views: + self.assertIn('channel_ids', view.array_annotations) + self.assertIn('channel_names', view.array_annotations) + self.assertIn('coordinates_dim0', view.array_annotations) + self.assertIn('coordinates_dim1', view.array_annotations) + + # check content of attributes + assert_arrays_equal(channel_index.index, view.index) + assert_arrays_equal(channel_index.channel_ids, view.array_annotations['channel_ids']) + assert_arrays_equal(channel_index.channel_names, + view.array_annotations['channel_names']) + view_coordinates = np.vstack((view.array_annotations['coordinates_dim0'], + view.array_annotations['coordinates_dim1'])).T + # readd unit lost during stacking of arrays + units = view.array_annotations['coordinates_dim0'].units + view_coordinates = view_coordinates.magnitude * units + assert_arrays_equal(channel_index.coordinates, view_coordinates) + self.assertDictEqual(channel_index.annotations, view.annotations) + + # check linking between objects + self.assertEqual(len(channel_index.data_children), len(matching_views)) + + # check linking between objects + for child in channel_index.data_children: + # comparing names instead of objects as attributes differ + self.assertIn(child.name, [v.obj.name for v in matching_views]) + group_names = np.asarray([g.name for g in group.groups]) + for unit in channel_index.units: + self.assertIn(unit.name, group_names) + + matching_groups = np.asarray(group.groups)[group_names == unit.name] + self.assertEqual(len(channel_index.units), len(matching_groups)) + From c421a307bf6a77ea5d3b6412d2019ffda7c5f350 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Thu, 8 Oct 2020 11:07:16 -0700 Subject: [PATCH 18/85] Decode headers from recent file types and test them. --- neo/rawio/neuralynxrawio.py | 15 ++++++++++--- neo/test/rawiotest/test_neuralynxrawio.py | 27 +++++++++++++++++------ 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 46f930b06..4dd9f4bac 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -828,16 +828,25 @@ def typeOfRecording(self): elif self['NLX_Base_Class_Type'] == 'BmlAcq': return 'BML' - elif 'HardwareSubsystemType' in self: + else: return 'UNKNOWN' + + elif 'HardwareSubSystemType' in self: # DigitalLynx - if self['HardwareSubsystemType'] == 'DigitalLynx': + if self['HardwareSubSystemType'] == 'DigitalLynx': return 'DIGITALLYNX' # DigitalLynxSX - elif self['HardwareSubsystemType'] == 'DigitalLynxSX': + elif self['HardwareSubSystemType'] == 'DigitalLynxSX': return 'DIGITALLYNXSX' + elif 'FileType' in self: + + if self['FileVersion'] in ['3.3','3.4']: + return self['AcquisitionSystem'].split()[1].upper() + + else: return 'UNKNOWN' + else: return 'UNKNOWN' diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index b13c9e689..0985d7635 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -12,13 +12,14 @@ class TestNeuralynxRawIO(BaseTestRawIO, unittest.TestCase, ): rawioclass = NeuralynxRawIO entities_to_test = [ + 'Cheetah_v4.0.2/original_data', 'Cheetah_v5.5.1/original_data', 'Cheetah_v5.6.3/original_data', 'Cheetah_v5.7.4/original_data', - 'Cheetah_v6.3.2/incomplete_blocks', - 'Cheetah_v4.0.2/original_data' + 'Cheetah_v6.3.2/incomplete_blocks' ] files_to_download = [ + 'Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs', 'Cheetah_v5.5.1/original_data/CheetahLogFile.txt', 'Cheetah_v5.5.1/original_data/CheetahLostADRecords.txt', 'Cheetah_v5.5.1/original_data/Events.nev', @@ -60,20 +61,32 @@ class TestNeuralynxRawIO(BaseTestRawIO, unittest.TestCase, ): 'Cheetah_v5.7.4/README.txt', 'Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs', 'Cheetah_v6.3.2/incomplete_blocks/Events.nev', - 'Cheetah_v6.3.2/incomplete_blocks/README.txt', - 'Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs'] + 'Cheetah_v6.3.2/incomplete_blocks/README.txt' + ] + class TestNcsRecordingType(TestNeuralynxRawIO, unittest.TestCase): """ Test of decoding of NlxHeader for type of recording. """ + ncsTypeTestFiles = [ + ('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs','PRE4'), + ('Cheetah_v5.5.1/original_data/STet3a.nse','DIGITALLYNXSX'), + ('Cheetah_v5.5.1/original_data/Tet3a.ncs','DIGITALLYNXSX'), + ('Cheetah_v5.6.3/original_data/CSC1.ncs','DIGITALLYNXSX'), + ('Cheetah_v5.6.3/original_data/TT1.ntt','DIGITALLYNXSX'), + ('Cheetah_v5.7.4/original_data/CSC1.ncs','DIGITALLYNXSX'), + ('Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs','DIGITALLYNXSX') + ] + def test_recording_types(self): - filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') - hdr = NlxHeader.buildForFile(filename) - self.assertEqual(hdr.typeOfRecording(),'PRE4') + for typeTest in self.ncsTypeTestFiles: + filename = self.get_filename_path(typeTest[0]) + hdr = NlxHeader.buildForFile(filename) + self.assertEqual(hdr.typeOfRecording(),typeTest[1]) if __name__ == "__main__": From 5e224b62ab56e64f8fa423d17ab467df1f66fa70 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Thu, 8 Oct 2020 11:08:07 -0700 Subject: [PATCH 19/85] Add self to authors file. --- doc/source/authors.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/authors.rst b/doc/source/authors.rst index f48515458..bddb41844 100644 --- a/doc/source/authors.rst +++ b/doc/source/authors.rst @@ -52,6 +52,7 @@ and may not be the current affiliation of a contributor. * rishidhingra@github * Hugo van Kemenade * Aitor Morales-Gregorio [13] +* Peter N Steinmetz [22] 1. Centre de Recherche en Neuroscience de Lyon, CNRS UMR5292 - INSERM U1028 - Universite Claude Bernard Lyon 1 2. Unité de Neuroscience, Information et Complexité, CNRS UPR 3293, Gif-sur-Yvette, France @@ -74,6 +75,7 @@ and may not be the current affiliation of a contributor. 19. IAL Developmental Neurobiology, Kazan Federal University, Kazan, Russia 20. Harden Technologies, LLC 21. Institut des Neurosciences Paris-Saclay, CNRS UMR 9197 - Université Paris-Sud, Gif-sur-Yvette, France +22. Neurtex Brain Research Institute, Dallas, TX, USAs If we've somehow missed you off the list we're very sorry - please let us know. From 1a5a3fc976c5b6bf1283d5a6ed4787322704c6cc Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Thu, 8 Oct 2020 11:11:08 -0700 Subject: [PATCH 20/85] Add 4.0.2 test data. --- neo/test/iotest/test_neuralynxio.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/neo/test/iotest/test_neuralynxio.py b/neo/test/iotest/test_neuralynxio.py index adc97ba6a..a6b82d5f4 100644 --- a/neo/test/iotest/test_neuralynxio.py +++ b/neo/test/iotest/test_neuralynxio.py @@ -22,12 +22,14 @@ class CommonNeuralynxIOTest(BaseTestIO, unittest.TestCase, ): ioclass = NeuralynxIO files_to_test = [ + 'Cheetah_v4.0.2/original_data', 'Cheetah_v5.5.1/original_data', 'Cheetah_v5.6.3/original_data', 'Cheetah_v5.7.4/original_data', 'Pegasus_v2.1.1', 'Cheetah_v6.3.2/incomplete_blocks'] files_to_download = [ + 'Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs', 'Cheetah_v5.5.1/original_data/CheetahLogFile.txt', 'Cheetah_v5.5.1/original_data/CheetahLostADRecords.txt', 'Cheetah_v5.5.1/original_data/Events.nev', @@ -71,7 +73,8 @@ class CommonNeuralynxIOTest(BaseTestIO, unittest.TestCase, ): 'Pegasus_v2.1.1/Events_0008.nev', 'Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs', 'Cheetah_v6.3.2/incomplete_blocks/Events.nev', - 'Cheetah_v6.3.2/incomplete_blocks/README.txt'] + 'Cheetah_v6.3.2/incomplete_blocks/README.txt' + ] class TestCheetah_v551(CommonNeuralynxIOTest, unittest.TestCase): @@ -336,7 +339,6 @@ def test_gap_handling_v563(self): self.assertEqual(len(block.channel_indexes[0].analogsignals), n_gaps + 1) self.assertEqual(len(block.channel_indexes[-1].units[0].spiketrains), n_gaps + 1) - def compare_old_and_new_neuralynxio(): base = '/tmp/files_for_testing_neo/neuralynx/' dirname = base + 'Cheetah_v5.5.1/original_data/' From 7008f9c5e73e1a3604fcdcc328c177520326a016 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Fri, 9 Oct 2020 09:32:24 -0700 Subject: [PATCH 21/85] Do not fully test the 4.0.2 data yet. Allows full normal testing to run, using the 4.0.2 data only to test the parsing of ncs recording from the header information. --- neo/test/rawiotest/test_neuralynxrawio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 0985d7635..3a8641a97 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -12,7 +12,7 @@ class TestNeuralynxRawIO(BaseTestRawIO, unittest.TestCase, ): rawioclass = NeuralynxRawIO entities_to_test = [ - 'Cheetah_v4.0.2/original_data', + # 'Cheetah_v4.0.2/original_data', 'Cheetah_v5.5.1/original_data', 'Cheetah_v5.6.3/original_data', 'Cheetah_v5.7.4/original_data', From b6f8153b14baa8a2c34b89a4cf6a3e9789648f31 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Fri, 9 Oct 2020 09:43:07 -0700 Subject: [PATCH 22/85] =?UTF-8?q?Don=E2=80=99t=20test=204.0.2=20in=20test?= =?UTF-8?q?=20of=20neuralynxio=20either.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- neo/test/iotest/test_neuralynxio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neo/test/iotest/test_neuralynxio.py b/neo/test/iotest/test_neuralynxio.py index a6b82d5f4..3bf29d12f 100644 --- a/neo/test/iotest/test_neuralynxio.py +++ b/neo/test/iotest/test_neuralynxio.py @@ -22,7 +22,7 @@ class CommonNeuralynxIOTest(BaseTestIO, unittest.TestCase, ): ioclass = NeuralynxIO files_to_test = [ - 'Cheetah_v4.0.2/original_data', + # 'Cheetah_v4.0.2/original_data', 'Cheetah_v5.5.1/original_data', 'Cheetah_v5.6.3/original_data', 'Cheetah_v5.7.4/original_data', From b0deaf287a6fd8d55ce6d11175e6f97e3a238f21 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Fri, 9 Oct 2020 09:46:17 -0700 Subject: [PATCH 23/85] Add blank line for PEP8. --- neo/test/iotest/test_neuralynxio.py | 1 + 1 file changed, 1 insertion(+) diff --git a/neo/test/iotest/test_neuralynxio.py b/neo/test/iotest/test_neuralynxio.py index 3bf29d12f..85667034d 100644 --- a/neo/test/iotest/test_neuralynxio.py +++ b/neo/test/iotest/test_neuralynxio.py @@ -339,6 +339,7 @@ def test_gap_handling_v563(self): self.assertEqual(len(block.channel_indexes[0].analogsignals), n_gaps + 1) self.assertEqual(len(block.channel_indexes[-1].units[0].spiketrains), n_gaps + 1) + def compare_old_and_new_neuralynxio(): base = '/tmp/files_for_testing_neo/neuralynx/' dirname = base + 'Cheetah_v5.5.1/original_data/' From 51daa9325ca5c34ebbfaa4e65199f11ac132cfd7 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Fri, 9 Oct 2020 10:05:21 -0700 Subject: [PATCH 24/85] Clean up more PEP8 style issues. --- neo/rawio/neuralynxrawio.py | 10 ++++++---- neo/test/iotest/test_neuralynxio.py | 3 +-- neo/test/rawiotest/test_neuralynxrawio.py | 21 +++++++++------------ 3 files changed, 16 insertions(+), 18 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 4dd9f4bac..4e08d120c 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -661,7 +661,7 @@ def _to_bool(txt): ('ApplicationName', '', None), # also include version number when present ('AcquisitionSystem', '', None), ('ReferenceChannel', '', None), - ('NLX_Base_Class_Type','',None) # in version 4 and earlier versions of Cheetah + ('NLX_Base_Class_Type', '', None) # in version 4 and earlier versions of Cheetah ] # Filename and datetime may appear in header lines starting with # at @@ -828,7 +828,8 @@ def typeOfRecording(self): elif self['NLX_Base_Class_Type'] == 'BmlAcq': return 'BML' - else: return 'UNKNOWN' + else: + return 'UNKNOWN' elif 'HardwareSubSystemType' in self: @@ -842,10 +843,11 @@ def typeOfRecording(self): elif 'FileType' in self: - if self['FileVersion'] in ['3.3','3.4']: + if self['FileVersion'] in ['3.3', '3.4']: return self['AcquisitionSystem'].split()[1].upper() - else: return 'UNKNOWN' + else: + return 'UNKNOWN' else: return 'UNKNOWN' diff --git a/neo/test/iotest/test_neuralynxio.py b/neo/test/iotest/test_neuralynxio.py index 85667034d..116515940 100644 --- a/neo/test/iotest/test_neuralynxio.py +++ b/neo/test/iotest/test_neuralynxio.py @@ -73,8 +73,7 @@ class CommonNeuralynxIOTest(BaseTestIO, unittest.TestCase, ): 'Pegasus_v2.1.1/Events_0008.nev', 'Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs', 'Cheetah_v6.3.2/incomplete_blocks/Events.nev', - 'Cheetah_v6.3.2/incomplete_blocks/README.txt' - ] + 'Cheetah_v6.3.2/incomplete_blocks/README.txt'] class TestCheetah_v551(CommonNeuralynxIOTest, unittest.TestCase): diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 3a8641a97..ba20bdb83 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -16,8 +16,7 @@ class TestNeuralynxRawIO(BaseTestRawIO, unittest.TestCase, ): 'Cheetah_v5.5.1/original_data', 'Cheetah_v5.6.3/original_data', 'Cheetah_v5.7.4/original_data', - 'Cheetah_v6.3.2/incomplete_blocks' - ] + 'Cheetah_v6.3.2/incomplete_blocks'] files_to_download = [ 'Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs', 'Cheetah_v5.5.1/original_data/CheetahLogFile.txt', @@ -61,8 +60,7 @@ class TestNeuralynxRawIO(BaseTestRawIO, unittest.TestCase, ): 'Cheetah_v5.7.4/README.txt', 'Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs', 'Cheetah_v6.3.2/incomplete_blocks/Events.nev', - 'Cheetah_v6.3.2/incomplete_blocks/README.txt' - ] + 'Cheetah_v6.3.2/incomplete_blocks/README.txt'] class TestNcsRecordingType(TestNeuralynxRawIO, unittest.TestCase): @@ -71,14 +69,13 @@ class TestNcsRecordingType(TestNeuralynxRawIO, unittest.TestCase): """ ncsTypeTestFiles = [ - ('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs','PRE4'), - ('Cheetah_v5.5.1/original_data/STet3a.nse','DIGITALLYNXSX'), - ('Cheetah_v5.5.1/original_data/Tet3a.ncs','DIGITALLYNXSX'), - ('Cheetah_v5.6.3/original_data/CSC1.ncs','DIGITALLYNXSX'), - ('Cheetah_v5.6.3/original_data/TT1.ntt','DIGITALLYNXSX'), - ('Cheetah_v5.7.4/original_data/CSC1.ncs','DIGITALLYNXSX'), - ('Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs','DIGITALLYNXSX') - ] + ('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs', 'PRE4'), + ('Cheetah_v5.5.1/original_data/STet3a.nse', 'DIGITALLYNXSX'), + ('Cheetah_v5.5.1/original_data/Tet3a.ncs', 'DIGITALLYNXSX'), + ('Cheetah_v5.6.3/original_data/CSC1.ncs', 'DIGITALLYNXSX'), + ('Cheetah_v5.6.3/original_data/TT1.ntt', 'DIGITALLYNXSX'), + ('Cheetah_v5.7.4/original_data/CSC1.ncs', 'DIGITALLYNXSX'), + ('Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs', 'DIGITALLYNXSX')] def test_recording_types(self): From ea95d7065c632c722776e28e4a0998ad9e0b6e4b Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Fri, 9 Oct 2020 10:13:13 -0700 Subject: [PATCH 25/85] Clean up another whitespace issue for PEP8. --- neo/test/rawiotest/test_neuralynxrawio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index ba20bdb83..dce7357ac 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -83,7 +83,7 @@ def test_recording_types(self): filename = self.get_filename_path(typeTest[0]) hdr = NlxHeader.buildForFile(filename) - self.assertEqual(hdr.typeOfRecording(),typeTest[1]) + self.assertEqual(hdr.typeOfRecording(), typeTest[1]) if __name__ == "__main__": From 0a2b9590c8cac3a40911927597210d128c4c420c Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Fri, 9 Oct 2020 11:15:52 -0700 Subject: [PATCH 26/85] Move constants into class so accessible for testing. --- neo/rawio/neuralynxrawio.py | 63 ++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 4e08d120c..380e2cc3c 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -29,7 +29,6 @@ import datetime from collections import OrderedDict -BLOCK_SIZE = 512 # nb sample per signal block class NeuralynxRawIO(BaseRawIO): @@ -51,6 +50,10 @@ class NeuralynxRawIO(BaseRawIO): extensions = ['nse', 'ncs', 'nev', 'ntt'] rawmode = 'one-dir' + _BLOCK_SIZE = 512 # nb sample per signal block + _ncs_dtype = [('timestamp', 'uint64'), ('channel_id', 'uint32'), ('sample_rate', 'uint32'), + ('nb_valid', 'uint32'), ('samples', 'int16', (_BLOCK_SIZE,))] + def __init__(self, dirname='', keep_original_times=False, **kargs): """ Parameters @@ -348,8 +351,8 @@ def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, chann if i_stop is None: i_stop = self._sigs_length[seg_index] - block_start = i_start // BLOCK_SIZE - block_stop = i_stop // BLOCK_SIZE + 1 + block_start = i_start // self._BLOCK_SIZE + block_stop = i_stop // self._BLOCK_SIZE + 1 sl0 = i_start % 512 sl1 = sl0 + (i_stop - i_start) @@ -492,11 +495,11 @@ def read_ncs_files(self, ncs_filenames): if len(ncs_filenames) == 0: return None - good_delta = int(BLOCK_SIZE * 1e6 / self._sigs_sampling_rate) + good_delta = int(self._BLOCK_SIZE * 1e6 / self._sigs_sampling_rate) chan_uid0 = list(ncs_filenames.keys())[0] filename0 = ncs_filenames[chan_uid0] - data0 = np.memmap(filename0, dtype=ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) + data0 = np.memmap(filename0, dtype=self.ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) gap_indexes = None lost_indexes = None @@ -535,7 +538,7 @@ def read_ncs_files(self, ncs_filenames): # is not strictly necessary as all channels might have same partially filled # blocks at the end. - lost_indexes, = np.nonzero(data0['nb_valid'] < BLOCK_SIZE) + lost_indexes, = np.nonzero(data0['nb_valid'] < self._BLOCK_SIZE) if self.use_cache: self.add_in_cache(lost_indexes=lost_indexes) @@ -562,7 +565,8 @@ def read_ncs_files(self, ncs_filenames): # create segment with subdata block/t_start/t_stop/length for chan_uid, ncs_filename in self.ncs_filenames.items(): - data = np.memmap(ncs_filename, dtype=ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) + data = np.memmap(ncs_filename, dtype=self.ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) assert data.size == data0.size, 'ncs files do not have the same data length' for seg_index, (i0, i1) in enumerate(gap_pairs): @@ -578,21 +582,20 @@ def read_ncs_files(self, ncs_filenames): if chan_uid == chan_uid0: ts0 = subdata[0]['timestamp'] ts1 = subdata[-1]['timestamp'] +\ - np.uint64(BLOCK_SIZE / self._sigs_sampling_rate * 1e6) + np.uint64(self._BLOCK_SIZE / self._sigs_sampling_rate * 1e6) self._timestamp_limits.append((ts0, ts1)) t_start = ts0 / 1e6 self._sigs_t_start.append(t_start) t_stop = ts1 / 1e6 self._sigs_t_stop.append(t_stop) - length = subdata.size * BLOCK_SIZE + length = subdata.size * self._BLOCK_SIZE self._sigs_length.append(length) class NcsBlocks(): """ Contains information regarding the blocks of records in an Ncs file. - Factory methods perform parsing of this information from an Ncs file or - confirmation that file agrees with block structure. + Factory methods perform parsing of this information from an Ncs file. """ startBlocks = [] @@ -601,6 +604,41 @@ class NcsBlocks(): microsPerSampUsed = 0 +class NcsBlocksFactory(): + """ + Class for factory methods which perform parsing of blocks in Ncs files. + + Moved here since algorithm covering all 3 header styles and types used is + more complicated. Copied from Java code on Sept 7, 2020. + """ + + _tolerance = 0.001 # tolerance for drift of timestamps within one NcsBlock + _maxGapLength = 5 # maximum gap between predicted and actual block timestamps + # still considered within one NcsBlock + + def _parseGivenActualFrequency(sampsMemMap,ncsBlocks,chanNum,reqFreq,blkOnePredTime): + """ + Parse blocks in file when microsPerSampUsed and sampFreqUsed are known, + filling in an NcsBlocks object. + + PARAMETERS + sampsMemMap: + memmap of Ncs file + ncsBlocks: + result with microsPerSamp and sampFreqUsed set correctly + chanNum: + channel number that should be present in all records + reqFreq: + rounded frequency that all records should contain + blkOnePredTime: + predicted starting time of first block + + RETURN + NcsBlocks object with block locations marked + """ + + + class NlxHeader(OrderedDict): """ Representation of basic information in all 16 kbytes Neuralynx file headers, @@ -860,9 +898,6 @@ class NcsHeader(): """ -ncs_dtype = [('timestamp', 'uint64'), ('channel_id', 'uint32'), ('sample_rate', 'uint32'), - ('nb_valid', 'uint32'), ('samples', 'int16', (BLOCK_SIZE,))] - nev_dtype = [ ('reserved', ' Date: Mon, 12 Oct 2020 11:52:31 -0700 Subject: [PATCH 27/85] Test of building NcsBlocks. --- neo/test/rawiotest/test_neuralynxrawio.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index dce7357ac..64a29988b 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -1,8 +1,10 @@ import unittest +import numpy as np + from neo.rawio.neuralynxrawio import NeuralynxRawIO -from neo.test.rawiotest.common_rawio_test import BaseTestRawIO from neo.rawio.neuralynxrawio import NlxHeader +from neo.test.rawiotest.common_rawio_test import BaseTestRawIO import logging @@ -85,6 +87,17 @@ def test_recording_types(self): hdr = NlxHeader.buildForFile(filename) self.assertEqual(hdr.typeOfRecording(), typeTest[1]) +class TestNcsBlocksFactory(TestNeuralynxRawIO, unittest.TestCase): + """ + Test building NcsBlocks for files of different revisions. + """ + + def test_ncsblocks_partial(self): + filename = self.get_filename_path('Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs') + data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) + self.assertEqual(data0.shape[0],6690) + self.assertEqual(data0['timestamp'][6689],8515800549) # timestamp of last record if __name__ == "__main__": unittest.main() From 409ea4117e2ed035fd1364c3896ba08bfc34fe84 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Tue, 13 Oct 2020 11:52:40 -0700 Subject: [PATCH 28/85] Handle old files with truncated frequency in header and test. --- neo/rawio/neuralynxrawio.py | 146 ++++++++++++++++++++-- neo/test/rawiotest/test_neuralynxrawio.py | 18 +++ 2 files changed, 156 insertions(+), 8 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 380e2cc3c..b24cefe2c 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -592,21 +592,71 @@ def read_ncs_files(self, ncs_filenames): self._sigs_length.append(length) +class WholeMicrosTimePositionBlock(): + """ + Map of time to sample positions. + + Times are rounded to nearest microsecond. Model here is that times + from start of a sample until just before the next sample are included, + that is, closed lower bound and open upper bound on intervals. A + channel with no samples is empty and contains no time intervals. + """ + + _sampFrequency = 0 + _startTime = 0 + _size = 0 + _microsPerSamp = 0 + + @staticmethod + def getMicrosPerSampForFreq(sampFreq): + """ + Compute fractional microseconds per sample. + """ + return 1e6 / sampFreq + + @staticmethod + def calcSampleTime(sampFr, startTime, posn): + """ + Calculate time rounded to microseconds for sample given frequency, + start time, and sample position. + """ + return round(startTime+ + WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(sampFr)*posn) + +class CscRecordHeader(): + """ + Information in header of each Ncs record, excluding sample values themselves. + """ + timestamp = 0 + channel_id = 0 + sample_rate = 0 + nb_valid = 0 + + def __init__(self,ncsMemMap,recn): + """ + Construct a record header for a given record in a memory map for an NcsFile. + """ + self.timestamp = ncsMemMap['timestamp'][recn] + self.channel_id = ncsMemMap['channel_id'][recn] + self.sample_rate = ncsMemMap['sample_rate'][recn] + self.nb_valid = ncsMemMap['nb_valid'][recn] + class NcsBlocks(): """ - Contains information regarding the blocks of records in an Ncs file. + Contains information regarding the contiguous blocks of records in an Ncs file. Factory methods perform parsing of this information from an Ncs file. """ startBlocks = [] endBlocks = [] - sampFreqUsed = 0 - microsPerSampUsed = 0 + sampFreqUsed = 0 # actual sampling frequency of samples + microsPerSampUsed = 0 # microseconds per sample class NcsBlocksFactory(): """ - Class for factory methods which perform parsing of blocks in Ncs files. + Class for factory methods which perform parsing of contiguous blocks of records + in Ncs files. Moved here since algorithm covering all 3 header styles and types used is more complicated. Copied from Java code on Sept 7, 2020. @@ -616,13 +666,13 @@ class NcsBlocksFactory(): _maxGapLength = 5 # maximum gap between predicted and actual block timestamps # still considered within one NcsBlock - def _parseGivenActualFrequency(sampsMemMap,ncsBlocks,chanNum,reqFreq,blkOnePredTime): + def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime): """ - Parse blocks in file when microsPerSampUsed and sampFreqUsed are known, + Parse blocks in memory mapped file when microsPerSampUsed and sampFreqUsed are known, filling in an NcsBlocks object. PARAMETERS - sampsMemMap: + ncsMemMap: memmap of Ncs file ncsBlocks: result with microsPerSamp and sampFreqUsed set correctly @@ -636,8 +686,88 @@ def _parseGivenActualFrequency(sampsMemMap,ncsBlocks,chanNum,reqFreq,blkOnePredT RETURN NcsBlocks object with block locations marked """ - + startBlockPredTime = blkOnePredTime + blkLen = 0 + for recn in range(1, ncsMemMap.shape[0]): + hdr = CscRecordHeader(ncsMemMap, recn) + if hdr.channel_id!=chanNum | hdr.sample_rate!=reqFreq: + raise IOError('Channel number or sampling frequency changed in records within file') + predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, + startBlockPredTime, blkLen) + nValidSamps = hdr.nb_valid + if hdr.timestamp != predTime: + ncsBlocks.endBlocks.append(recn-1) + ncsBlocks.startBlocks.append(recn) + startBlockPredTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, + hdr.timestamp, + nValidSamps) + blklen = 0 + else: + blkLen += nValidSamps + ncsBlocks.endBlocks.append(ncsMemMap.shape[0] - 1) + + return ncsBlocks + + + def _buildGivenActualFrequency(ncsMemMap, ncsBlocks, reqFreq): + """ + Build NcsBlocks object for file given actual sampling frequency. + + Requires that frequency in each record agrees with requested frequency. This is + normally obtained by rounding the header frequency; however, this value may be different + from the rounded actual frequency used in the recording, since the underlying + requirement in older Ncs files was that the rounded number of whole microseconds + per sample be the same for all records in a block. + + PARAMETERS + ncsMemMap: + memmap of Ncs file + ncsBlocks: + containing the actual sampling frequency used and microsPerSamp for the result + reqFreq: + frequency to require in records + RETURN: + NcsBlocks object + """ + # check frequency in first record + rh0 = CscRecordHeader(ncsMemMap, 0) + if rh0.sample_rate != reqFreq: + raise IOError("Sampling frequency in first record doesn't agree with header.") + chanNum = rh0.channel_id + + # check if file is one block of records, which is often the case, and avoid full parse + lastBlkI = ncsMemMap.shape[0] - 1 + rhl = CscRecordHeader(ncsMemMap, lastBlkI) + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, rh0.timestamp, + NeuralynxRawIO._BLOCK_SIZE * lastBlkI) + if rhl.channel_id == chanNum and rhl.sample_rate == reqFreq and rhl.timestamp == predLastBlockStartTime: + ncsBlocks.startBlocks.append(0) + ncsBlocks.endBlocks.append(lastBlkI) + return ncsBlocks + + # otherwise need to scan looking for breaks + else: + blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, rh0.timestamp, + rh0.nb_valid) + return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime) + + + def _parseForMaxGap(ncsMemMap, hdr, nomFreq, maxGapLen): + """ + Parse blocks of records from file, allowing a maximum gap in timestamps between records + in blocks. Estimates frequency being used based on timestamps. + + PARAMETERS + ncsMemMap: + memmap of Ncs file + hdr: + CSC record headr information + nomFreq: + nominal frequency to use in computing time for samples (Hz) + maxGapLen: + maximum difference within a block between predicted time of start of record and recorded time + """ class NlxHeader(OrderedDict): """ diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 64a29988b..8c092aed9 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -4,6 +4,8 @@ from neo.rawio.neuralynxrawio import NeuralynxRawIO from neo.rawio.neuralynxrawio import NlxHeader +from neo.rawio.neuralynxrawio import NcsBlocksFactory +from neo.rawio.neuralynxrawio import NcsBlocks from neo.test.rawiotest.common_rawio_test import BaseTestRawIO import logging @@ -99,5 +101,21 @@ def test_ncsblocks_partial(self): self.assertEqual(data0.shape[0],6690) self.assertEqual(data0['timestamp'][6689],8515800549) # timestamp of last record + def testBuildGivenActualFrequency(self): + + # Test early files where the frequency listed in the header is + # floor(1e6/(actual number of microseconds between samples) + filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') + data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) + ncsBlocks = NcsBlocks() + ncsBlocks.sampFreqUsed = 1/(35e-6) + ncsBlocks.microsPerSampUsed = 35 + ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks, 27789) + self.assertEqual(len(ncsBlocks.startBlocks), 1) + self.assertEqual(ncsBlocks.startBlocks[0], 0) + self.assertEqual(len(ncsBlocks.endBlocks), 1) + self.assertEqual(ncsBlocks.endBlocks[0], 9) + if __name__ == "__main__": unittest.main() From 2c3f80b8848c901b5f1af12550eccf1496addabf Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Fri, 16 Oct 2020 11:38:07 -0700 Subject: [PATCH 29/85] Change interface on parse versus build. --- neo/rawio/neuralynxrawio.py | 157 ++++++++++++++++++++-- neo/test/rawiotest/test_neuralynxrawio.py | 2 +- 2 files changed, 144 insertions(+), 15 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index b24cefe2c..be52bfe14 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -28,6 +28,7 @@ import distutils.version import datetime from collections import OrderedDict +import math @@ -110,7 +111,7 @@ def _parse_header(self): self._empty_ncs.append(filename) continue - # All file have more or less the same header structure + # All files have more or less the same header structure info = NlxHeader.buildForFile(filename) chan_names = info['channel_names'] chan_ids = info['channel_ids'] @@ -666,6 +667,7 @@ class NcsBlocksFactory(): _maxGapLength = 5 # maximum gap between predicted and actual block timestamps # still considered within one NcsBlock + @staticmethod def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime): """ Parse blocks in memory mapped file when microsPerSampUsed and sampFreqUsed are known, @@ -675,7 +677,7 @@ def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePre ncsMemMap: memmap of Ncs file ncsBlocks: - result with microsPerSamp and sampFreqUsed set correctly + NcsBlocks with actual sampFreqUsed correct chanNum: channel number that should be present in all records reqFreq: @@ -709,7 +711,8 @@ def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePre return ncsBlocks - def _buildGivenActualFrequency(ncsMemMap, ncsBlocks, reqFreq): + @staticmethod + def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): """ Build NcsBlocks object for file given actual sampling frequency. @@ -736,24 +739,30 @@ def _buildGivenActualFrequency(ncsMemMap, ncsBlocks, reqFreq): raise IOError("Sampling frequency in first record doesn't agree with header.") chanNum = rh0.channel_id + nb = NcsBlocks() + nb.sampFreqUsed = actualSampFreq + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(actualSampFreq) + # check if file is one block of records, which is often the case, and avoid full parse lastBlkI = ncsMemMap.shape[0] - 1 rhl = CscRecordHeader(ncsMemMap, lastBlkI) - predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, rh0.timestamp, + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, NeuralynxRawIO._BLOCK_SIZE * lastBlkI) if rhl.channel_id == chanNum and rhl.sample_rate == reqFreq and rhl.timestamp == predLastBlockStartTime: - ncsBlocks.startBlocks.append(0) - ncsBlocks.endBlocks.append(lastBlkI) - return ncsBlocks + nb = NcsBlocks() + nb.startBlocks.append(0) + nb.endBlocks.append(lastBlkI) + return nb # otherwise need to scan looking for breaks else: - blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, rh0.timestamp, + blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, rh0.nb_valid) - return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime) + return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, nb, chanNum, reqFreq, blkOnePredTime) - def _parseForMaxGap(ncsMemMap, hdr, nomFreq, maxGapLen): + @staticmethod + def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): """ Parse blocks of records from file, allowing a maximum gap in timestamps between records in blocks. Estimates frequency being used based on timestamps. @@ -761,13 +770,133 @@ def _parseForMaxGap(ncsMemMap, hdr, nomFreq, maxGapLen): PARAMETERS ncsMemMap: memmap of Ncs file - hdr: - CSC record headr information - nomFreq: - nominal frequency to use in computing time for samples (Hz) + ncsBlocks: + NcsBlocks object with sampFreqUsed set to nominal frequency to use in computing time for samples (Hz) maxGapLen: maximum difference within a block between predicted time of start of record and recorded time + + RETURN: + NcsBlocks object with sampFreqUsed and microsPerSamp set based on estimate from largest block + """ + + # track frequency of each block and use estimate with longest block + maxBlkLen = 0 + maxBlkFreqEstimate = 0 + + # Parse the record sequence, finding blocks of continuous time with no more than maxGapLength + # and same channel number + rh0 = CscRecordHeader(ncsMemMap, 0) + chanNum = rh0.channel_id + + startBlockTime = rh0.timestamp + blkLen = rh0.nb_valid + lastRecTime = rh0.timestamp + lastRecNumSamps = rh0.nb_valid + recFreq = rh0.sample_rate + + ncsBlocks.startBlocks.append(0) + for recn in range(1, ncsMemMap.shape[0]): + hdr = CscRecordHeader(ncsMemMap, recn) + if hdr.channel_id != chanNum | hdr.sample_rate != recFreq: + raise IOError('Channel number or sampling frequency changed in records within file') + predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, lastRecTime, + lastRecNumSamps) + if (abs(hdr.timestamp - predTime) > maxGapLen): + ncsBlocks.endBlocks.append(recn-1) + ncsBlocks.startBlocks.append(recn) + if blkLen > maxBlkLen: + maxBlkLen = blkLen + maxBlkFreqEstimate = (blkLen - lastRecNumSamps) * 1e6 / (lastRecTime - startBlockTime) + startBlockTime = hdr.timestamp + blkLen = hdr.nb_valid + else: + blkLen += hdr.nb_valid + ncsBlocks.append(ncsMemMap.shape[0] - 1) + + ncsBlocks.sampFreqUsed = maxBlkFreqEstimate + ncsBlocks.setMicrosPerSamp = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(maxBlkFreqEstimate) + + return ncsBlocks + + + @staticmethod + def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): """ + Determine blocks of records in memory mapped Ncs file given a nominal frequency of the file, + using the default values of frequency tolerance and maximum gap between blocks. + + PARAMETERS + ncsMemMap: + memmap of Ncs file + nomFreq: + nominal sampling frequency used, normally from header of file + + RETURN: + NcsBlocks object + """ + nb = NcsBlocks() + + numRecs = ncsMemMap.shape[0] + if numRecs < 1: + return nb + + rh0 = CscRecordHeader(ncsMemMap, 0) + chanNum = rh0.channel_id + + lastBlkI = numRecs - 1 + rhl = CscRecordHeader(ncsMemMap,lastBlkI) + + # check if file is one block of records, to within tolerance, which is often the case + numSampsForPred = NeuralynxRawIO._BLOCK_SIZE * lastBlkI + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq,rh0.timestamp, + numSampsForPred) + freqInFile = math.floor(nomFreq) + if abs(rhl.timestamp - predLastBlockStartTime) / rhl.timestamp < NcsBlocks._tolerance and \ + rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: + nb.endBlocks.append(lastBlkI) + nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) / 1e6 + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) + + # otherwise parse records to determine blocks using default maximum gap length + else: + nb.sampFreqUsed = nomFreq + nb = NcsBlocks._parseForMaxGap(ncsMemMap, nb, NcsBlocks._maxGapLength) + + return nb + + @staticmethod + def buildForNcsFile(ncsMemMap,nlxHdr): + """ + Build an NcsBlocks object for an NcsFile, given as a memmap and NlxHeader, + handling gap detection appropriately given the file type as specified by the header. + + PARAMETERS + ncsMemMap: + memory map of file + acqType: + string specifying type of data acquisition used, one of types returned by NlxHeader.typeOfRecording() + """ + acqType = nlxHdr.typeOfRecording() + + # old Neuralynx style with rounded whole microseconds for the samples + if acqType == "PRE4": + freq = nlxHdr['SamplingFrequency'] + sampFreqUsed = WholeMicrosTimePositionBlock.getFreqForMicrosPerSamp(microsPerSampUsed) + nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) + nb.microsPerSampUsed = math.floor(WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(freq)) + + # digital lynx style with fractional frequency and micros per samp determined from block times + elif acqType == "DIGITALLYNX" or acqType == "DIGITALLYNXSX": + nomFreq = nlxHdr['SamplingFrequency'] + nb = NcsBlocks._buildForToleranceAndMaxGap(ncsMemMap, nomFreq) + + # BML style with fractional frequency and micros per samp + elif acqType == "BML": + sampFreqUsed = nlxHdr['SamplingFrequency'] + nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(sampFreqUsed)) + + else: + raise TypeError("Unknown Ncs file type from header.") class NlxHeader(OrderedDict): """ diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 8c092aed9..653ae2a68 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -111,7 +111,7 @@ def testBuildGivenActualFrequency(self): ncsBlocks = NcsBlocks() ncsBlocks.sampFreqUsed = 1/(35e-6) ncsBlocks.microsPerSampUsed = 35 - ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks, 27789) + ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks.sampFreqUsed, 27789) self.assertEqual(len(ncsBlocks.startBlocks), 1) self.assertEqual(ncsBlocks.startBlocks[0], 0) self.assertEqual(len(ncsBlocks.endBlocks), 1) From 4f27b20be2d1917869ced85ef697a70a7d3dc8f3 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Mon, 19 Oct 2020 20:10:13 +0200 Subject: [PATCH 30/85] Fix container lookup for proxy objects in Group --- neo/core/group.py | 8 +++++++- neo/io/proxyobjects.py | 10 +++++++--- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/neo/core/group.py b/neo/core/group.py index 84009138d..f2bb83a9c 100644 --- a/neo/core/group.py +++ b/neo/core/group.py @@ -6,6 +6,7 @@ and :class:`Unit`. """ +from os import close from neo.core.container import Container @@ -61,10 +62,15 @@ def _container_lookup(self): for cls_name, container_name in zip(self._child_objects, self._child_containers) } + def _get_container(self, cls): + if hasattr(cls, "proxy_for"): + cls = cls.proxy_for + return self._container_lookup[cls.__name__] + def add(self, *objects): """Add a new Neo object to the Group""" for obj in objects: if self.allowed_types and not isinstance(obj, self.allowed_types): raise TypeError("This Group can only contain {}".format(self.allowed_types)) - container = self._container_lookup[obj.__class__.__name__] + container = self._get_container(obj.__class__) container.append(obj) diff --git a/neo/io/proxyobjects.py b/neo/io/proxyobjects.py index bb920e97b..c2ef374b3 100644 --- a/neo/io/proxyobjects.py +++ b/neo/io/proxyobjects.py @@ -70,9 +70,9 @@ class AnalogSignalProxy(BaseProxy): Usage: >>> proxy_anasig = AnalogSignalProxy(rawio=self.reader, - global_channel_indexes=None, - block_index=0, - seg_index=0) + global_channel_indexes=None, + block_index=0, + seg_index=0) >>> anasig = proxy_anasig.load() >>> slice_of_anasig = proxy_anasig.load(time_slice=(1.*pq.s, 2.*pq.s)) >>> some_channel_of_anasig = proxy_anasig.load(channel_indexes=[0,5,10]) @@ -82,6 +82,7 @@ class AnalogSignalProxy(BaseProxy): _necessary_attrs = (('sampling_rate', pq.Quantity, 0), ('t_start', pq.Quantity, 0)) _recommended_attrs = BaseNeo._recommended_attrs + proxy_for = AnalogSignal def __init__(self, rawio=None, global_channel_indexes=None, block_index=0, seg_index=0): self._rawio = rawio @@ -289,6 +290,7 @@ class SpikeTrainProxy(BaseProxy): _necessary_attrs = (('t_start', pq.Quantity, 0), ('t_stop', pq.Quantity, 0)) _recommended_attrs = () + proxy_for = SpikeTrain def __init__(self, rawio=None, unit_index=None, block_index=0, seg_index=0): @@ -471,6 +473,7 @@ class EventProxy(_EventOrEpoch): ''' _necessary_attrs = (('times', pq.Quantity, 1), ('labels', np.ndarray, 1, np.dtype('S'))) + proxy_for = Event class EpochProxy(_EventOrEpoch): @@ -499,6 +502,7 @@ class EpochProxy(_EventOrEpoch): _necessary_attrs = (('times', pq.Quantity, 1), ('durations', pq.Quantity, 1), ('labels', np.ndarray, 1, np.dtype('S'))) + proxy_for = Epoch proxyobjectlist = [AnalogSignalProxy, SpikeTrainProxy, EventProxy, From 60006871a0fe2126adefd1a8d6a93243bd8500c3 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Mon, 19 Oct 2020 11:37:57 -0700 Subject: [PATCH 31/85] Tests for PRE4 type and code corrections. Tests for v5.5.1 still failing. --- neo/rawio/neuralynxrawio.py | 39 +++++++++++++++-------- neo/test/rawiotest/test_neuralynxrawio.py | 31 +++++++++++++++++- 2 files changed, 56 insertions(+), 14 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index be52bfe14..e778f8f02 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -609,11 +609,18 @@ class WholeMicrosTimePositionBlock(): _microsPerSamp = 0 @staticmethod - def getMicrosPerSampForFreq(sampFreq): + def getFreqForMicrosPerSamp(micros): """ - Compute fractional microseconds per sample. + Compute fractional sampling frequency, given microseconds per sample. """ - return 1e6 / sampFreq + return 1e6 / micros + + @staticmethod + def getMicrosPerSampForFreq(sampFr): + """ + Calculate fractional microseconds per sample, given the sampling frequency (Hz). + """ + return 1e6 / sampFr @staticmethod def calcSampleTime(sampFr, startTime, posn): @@ -811,7 +818,7 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): blkLen = hdr.nb_valid else: blkLen += hdr.nb_valid - ncsBlocks.append(ncsMemMap.shape[0] - 1) + ncsBlocks.endBlocks.append(ncsMemMap.shape[0] - 1) ncsBlocks.sampFreqUsed = maxBlkFreqEstimate ncsBlocks.setMicrosPerSamp = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(maxBlkFreqEstimate) @@ -851,7 +858,7 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq,rh0.timestamp, numSampsForPred) freqInFile = math.floor(nomFreq) - if abs(rhl.timestamp - predLastBlockStartTime) / rhl.timestamp < NcsBlocks._tolerance and \ + if abs(rhl.timestamp - predLastBlockStartTime) / rhl.timestamp < NcsBlocksFactory._tolerance and \ rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: nb.endBlocks.append(lastBlkI) nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) / 1e6 @@ -860,7 +867,8 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): # otherwise parse records to determine blocks using default maximum gap length else: nb.sampFreqUsed = nomFreq - nb = NcsBlocks._parseForMaxGap(ncsMemMap, nb, NcsBlocks._maxGapLength) + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) + nb = NcsBlocksFactory._parseForMaxGap(ncsMemMap, nb, NcsBlocksFactory._maxGapLength) return nb @@ -880,24 +888,29 @@ def buildForNcsFile(ncsMemMap,nlxHdr): # old Neuralynx style with rounded whole microseconds for the samples if acqType == "PRE4": - freq = nlxHdr['SamplingFrequency'] + freq = nlxHdr['sampling_rate'] + microsPerSampUsed = math.floor(WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(freq)) sampFreqUsed = WholeMicrosTimePositionBlock.getFreqForMicrosPerSamp(microsPerSampUsed) - nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) - nb.microsPerSampUsed = math.floor(WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(freq)) + nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) + nb.sampFreqUsed = sampFreqUsed + nb.microsPerSampUsed = microsPerSampUsed # digital lynx style with fractional frequency and micros per samp determined from block times elif acqType == "DIGITALLYNX" or acqType == "DIGITALLYNXSX": - nomFreq = nlxHdr['SamplingFrequency'] - nb = NcsBlocks._buildForToleranceAndMaxGap(ncsMemMap, nomFreq) + nomFreq = nlxHdr['sampling_rate'] + nb = NcsBlocksFactory._buildForToleranceAndMaxGap(ncsMemMap, nomFreq) # BML style with fractional frequency and micros per samp elif acqType == "BML": - sampFreqUsed = nlxHdr['SamplingFrequency'] - nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(sampFreqUsed)) + sampFreqUsed = nlxHdr['sampling_rate'] + nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(sampFreqUsed)) else: raise TypeError("Unknown Ncs file type from header.") + return nb + + class NlxHeader(OrderedDict): """ Representation of basic information in all 16 kbytes Neuralynx file headers, diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 653ae2a68..1a0a40302 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -109,7 +109,7 @@ def testBuildGivenActualFrequency(self): data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) ncsBlocks = NcsBlocks() - ncsBlocks.sampFreqUsed = 1/(35e-6) + ncsBlocks.sampFreqUsed = 1/35e-6 ncsBlocks.microsPerSampUsed = 35 ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks.sampFreqUsed, 27789) self.assertEqual(len(ncsBlocks.startBlocks), 1) @@ -117,5 +117,34 @@ def testBuildGivenActualFrequency(self): self.assertEqual(len(ncsBlocks.endBlocks), 1) self.assertEqual(ncsBlocks.endBlocks[0], 9) + + def testBuildUsingHeaderAndScanning(self): + + # Test early files where the frequency listed in the header is + # floor(1e6/(actual number of microseconds between samples) + filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') + hdr = NlxHeader.buildForFile(filename) + data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) + nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) + + self.assertEqual(nb.sampFreqUsed, 1/35e-6) + self.assertEqual(nb.microsPerSampUsed, 35) + self.assertEqual(len(nb.startBlocks), 1) + self.assertEqual(nb.startBlocks[0], 0) + self.assertEqual(len(nb.endBlocks), 1) + self.assertEqual(nb.endBlocks[0], 9) + + # test Cheetah 5.5.1, which is DigitalLynxSX + filename = self.get_filename_path('Cheetah_v5.5.1/original_data/Tet3a.ncs') + hdr = NlxHeader.buildForFile(filename) + data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) + nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) + self.assertEqual(nb.sampFreqUsed, 32000) + self.assertEqual(nb.microsPerSampUsed, 31.25) + + + if __name__ == "__main__": unittest.main() From 865fd29e578250512ae6f9576c3f87ce8d24a8c0 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Tue, 20 Oct 2020 20:46:03 +0200 Subject: [PATCH 32/85] Fixing unit tests --- neo/io/baseio.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/neo/io/baseio.py b/neo/io/baseio.py index b84251c31..f790f9ba0 100644 --- a/neo/io/baseio.py +++ b/neo/io/baseio.py @@ -19,9 +19,9 @@ from neo import logging_handler from neo.core import (AnalogSignal, Block, - Epoch, Event, + Epoch, Event, Group, IrregularlySampledSignal, - ChannelIndex, + ChannelIndex, ChannelView, Segment, SpikeTrain, Unit, ImageSequence, RectangularRegionOfInterest, CircularRegionOfInterest, PolygonRegionOfInterest) @@ -179,12 +179,18 @@ def read_irregularlysampledsignal(self, **kargs): def read_channelindex(self, **kargs): assert (ChannelIndex in self.readable_objects), read_error + def read_channelview(self, **kargs): + assert (ChannelView in self.readable_objects), read_error + def read_event(self, **kargs): assert (Event in self.readable_objects), read_error def read_epoch(self, **kargs): assert (Epoch in self.readable_objects), read_error + def read_group(self, **kargs): + assert (Group in self.readable_objects), read_error + ######## All individual write methods ####################### def write_block(self, bl, **kargs): assert (Block in self.writeable_objects), write_error @@ -219,8 +225,14 @@ def write_irregularlysampledsignal(self, irsig, **kargs): def write_channelindex(self, chx, **kargs): assert (ChannelIndex in self.writeable_objects), write_error + def write_channelview(self, chv, **kargs): + assert (ChannelView in self.writeable_objects), write_error + def write_event(self, ev, **kargs): assert (Event in self.writeable_objects), write_error def write_epoch(self, ep, **kargs): assert (Epoch in self.writeable_objects), write_error + + def write_group(self, group, **kargs): + assert (Group in self.writeable_objects), write_error \ No newline at end of file From 8e73267e24e0e96ad3130711fbcd5084e7352d24 Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Wed, 21 Oct 2020 10:58:20 +0200 Subject: [PATCH 33/85] Permit ChannelView to link to ProxyObjects --- neo/core/view.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/neo/core/view.py b/neo/core/view.py index 0b6b64d81..a43166a8e 100644 --- a/neo/core/view.py +++ b/neo/core/view.py @@ -43,7 +43,9 @@ def __init__(self, obj, index, name=None, description=None, file_origin=None, array_annotations=None, **annotations): super().__init__(name=name, description=description, file_origin=file_origin, **annotations) - if not isinstance(obj, BaseSignal): + + if not (isinstance(obj, BaseSignal) or ( + hasattr(obj, "proxy_for") and issubclass(obj.proxy_for, BaseSignal))): raise ValueError("Can only take a ChannelView of an AnalogSignal " "or an IrregularlySampledSignal") self.obj = obj From 4dba99a2f4b354d9d8681b27dd4e4ef52a94d169 Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Thu, 22 Oct 2020 09:42:22 +0200 Subject: [PATCH 34/85] Fix NixIO writing multiple Groups with single ChannelView --- neo/io/nixio.py | 57 ++++++++++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/neo/io/nixio.py b/neo/io/nixio.py index 0f8116725..c3f5cae74 100644 --- a/neo/io/nixio.py +++ b/neo/io/nixio.py @@ -219,6 +219,7 @@ def __init__(self, filename, mode="rw"): self._neo_map = dict() self._ref_map = dict() self._signal_map = dict() + self._view_map = dict() # _names_ok is used to guard against name check duplication self._names_ok = False @@ -346,6 +347,7 @@ def _nix_to_neo_block(self, nix_block): self._neo_map = dict() self._ref_map = dict() self._signal_map = dict() + self._view_map = dict() return neo_block @@ -796,32 +798,38 @@ def _write_channelview(self, chview, nixblock, nixgroup): nix_name = "neo.channelview.{}".format(self._generate_nix_name()) chview.annotate(nix_name=nix_name) - channels = nixblock.create_data_array( - "{}.index".format(nix_name), "neo.channelview.index", data=chview.index - ) - - nixmt = nixblock.create_multi_tag(nix_name, "neo.channelview", - positions=channels) + # create a new data array if this channelview was not saved yet + if not nix_name in self._view_map: + channels = nixblock.create_data_array( + "{}.index".format(nix_name), "neo.channelview.index", data=chview.index + ) - nixmt.metadata = nixgroup.metadata.create_section( - nix_name, "neo.channelview.metadata" - ) - metadata = nixmt.metadata - neoname = chview.name if chview.name is not None else "" - metadata["neo_name"] = neoname - nixmt.definition = chview.description - if chview.annotations: - for k, v in chview.annotations.items(): - self._write_property(metadata, k, v) + nixmt = nixblock.create_multi_tag(nix_name, "neo.channelview", + positions=channels) - # link tag to the data array for the ChannelView's signal - if not ("nix_name" in chview.obj.annotations - and chview.obj.annotations["nix_name"] in self._signal_map): - # the following restriction could be relaxed later - # but for a first pass this simplifies my mental model - raise Exception("Need to save signals before saving views") - nix_name = chview.obj.annotations["nix_name"] - nixmt.references.extend(self._signal_map[nix_name]) + nixmt.metadata = nixgroup.metadata.create_section( + nix_name, "neo.channelview.metadata" + ) + metadata = nixmt.metadata + neoname = chview.name if chview.name is not None else "" + metadata["neo_name"] = neoname + nixmt.definition = chview.description + if chview.annotations: + for k, v in chview.annotations.items(): + self._write_property(metadata, k, v) + print(nix_name) + self._view_map[nix_name] = nixmt + + # link tag to the data array for the ChannelView's signal + if not ("nix_name" in chview.obj.annotations + and chview.obj.annotations["nix_name"] in self._signal_map): + # the following restriction could be relaxed later + # but for a first pass this simplifies my mental model + raise Exception("Need to save signals before saving views") + nix_name = chview.obj.annotations["nix_name"] + nixmt.references.extend(self._signal_map[nix_name]) + else: + nixmt = self._view_map[nix_name] nixgroup.multi_tags.append(nixmt) @@ -1677,6 +1685,7 @@ def close(self): self._neo_map = None self._ref_map = None self._signal_map = None + self._view_map = None self._block_read_counter = None def __del__(self): From 27b5b821ab58361f713f720a11273b57a13f676d Mon Sep 17 00:00:00 2001 From: Samuel Garcia Date: Fri, 23 Oct 2020 18:15:34 +0200 Subject: [PATCH 35/85] Remove ChannelIndex from brainwaredamioio/brainwaref32io/brainscrio. Still have error due to asyymetric relationship in group but I think we need to fix unitest. --- neo/io/brainwaredamio.py | 15 +++--- neo/io/brainwaref32io.py | 20 +++----- neo/io/brainwaresrcio.py | 63 +++++++++++--------------- neo/test/iotest/test_brainwaredamio.py | 11 ++--- neo/test/iotest/test_brainwaref32io.py | 14 ++---- neo/test/iotest/test_brainwaresrcio.py | 31 +++++-------- 6 files changed, 60 insertions(+), 94 deletions(-) diff --git a/neo/io/brainwaredamio.py b/neo/io/brainwaredamio.py index 89bb52a30..912e799f8 100644 --- a/neo/io/brainwaredamio.py +++ b/neo/io/brainwaredamio.py @@ -35,7 +35,7 @@ # needed core neo modules from neo.core import (AnalogSignal, Block, - ChannelIndex, Segment) + Group, Segment) # need to subclass BaseIO from neo.io.baseio import BaseIO @@ -75,7 +75,7 @@ class BrainwareDamIO(BaseIO): # This class is able to directly or indirectly handle the following objects # You can notice that this greatly simplifies the full Neo object hierarchy - supported_objects = [Block, ChannelIndex, + supported_objects = [Block, Group, Segment, AnalogSignal] readable_objects = [Block] @@ -135,13 +135,10 @@ def read_block(self, lazy=False, **kargs): block = Block(file_origin=self._filename) # create the objects to store other objects - chx = ChannelIndex(file_origin=self._filename, - channel_ids=np.array([1]), - index=np.array([0]), - channel_names=np.array(['Chan1'], dtype='U')) - + gr = Group(file_origin=self._filename) + # load objects into their containers - block.channel_indexes.append(chx) + block.groups.append(gr) # open the file with open(self._path, 'rb') as fobject: @@ -153,8 +150,8 @@ def read_block(self, lazy=False, **kargs): break # store the segment and signals - seg.analogsignals[0].channel_index = chx block.segments.append(seg) + gr.analogsignals.append(seg.analogsignals[0]) # remove the file object self._fsrc = None diff --git a/neo/io/brainwaref32io.py b/neo/io/brainwaref32io.py index c81c0866e..ebacd4d1b 100644 --- a/neo/io/brainwaref32io.py +++ b/neo/io/brainwaref32io.py @@ -32,7 +32,7 @@ import quantities as pq # needed core neo modules -from neo.core import Block, ChannelIndex, Segment, SpikeTrain, Unit +from neo.core import Block, Group, Segment, SpikeTrain, Unit # need to subclass BaseIO from neo.io.baseio import BaseIO @@ -59,8 +59,7 @@ class BrainwareF32IO(BaseIO): reading or closed. Note 1: - There is always only one ChannelIndex. BrainWare stores the - equivalent of ChannelIndexes in separate files. + There is always only one Group. Usage: >>> from neo.io.brainwaref32io import BrainwareF32IO @@ -80,7 +79,7 @@ class BrainwareF32IO(BaseIO): # This class is able to directly or indirectly handle the following objects # You can notice that this greatly simplifies the full Neo object hierarchy - supported_objects = [Block, ChannelIndex, + supported_objects = [Block, Group, Segment, SpikeTrain, Unit] readable_objects = [Block] @@ -117,7 +116,7 @@ def __init__(self, filename=None): self._fsrc = None self._blk = None - self.__unit = None + self.__unit_group = None self.__t_stop = None self.__params = None @@ -149,13 +148,8 @@ def read_block(self, lazy=False, **kargs): block = self._blk # create the objects to store other objects - chx = ChannelIndex(file_origin=self._filename, - index=np.array([], dtype=np.int)) - self.__unit = Unit(file_origin=self._filename) - - # load objects into their containers - block.channel_indexes.append(chx) - chx.units.append(self.__unit) + self.__unit_group = Group(file_origin=self._filename) + block.groups.append(self.__unit_group) # initialize values self.__t_stop = None @@ -282,7 +276,7 @@ def __save_segment(self): file_origin=self._filename) self.__seg.spiketrains = [train] - self.__unit.spiketrains.append(train) + self.__unit_group.spiketrains.append(train) self._blk.segments.append(self.__seg) # set an empty segment diff --git a/neo/io/brainwaresrcio.py b/neo/io/brainwaresrcio.py index 93bf94370..3d3a8aadb 100755 --- a/neo/io/brainwaresrcio.py +++ b/neo/io/brainwaresrcio.py @@ -27,6 +27,11 @@ The code is implemented with the permission of Dr. Jan Schnupp +Note when porting ChannelIndex/Unit to Group (Samuel Garcia). +The ChannelIndex was used as group of units. +To avoid now a "group of group" each units is directly a "Group"'. + + Author: Todd Jennings """ @@ -42,7 +47,7 @@ # needed core neo modules from neo.core import (Block, Event, - ChannelIndex, Segment, SpikeTrain, Unit) + Group, Segment, SpikeTrain, Unit) # need to subclass BaseIO from neo.io.baseio import BaseIO @@ -71,7 +76,7 @@ class BrainwareSrcIO(BaseIO): reading or closed. Note 1: - The first Unit in each ChannelIndex is always + The first Unit in each Group is always UnassignedSpikes, which has a SpikeTrain for each Segment containing all the spikes not assigned to any Unit in that Segment. @@ -85,8 +90,7 @@ class BrainwareSrcIO(BaseIO): a condition, each repetition is stored as a separate Segment. Note 4: - There is always only one ChannelIndex. BrainWare stores the - equivalent of ChannelIndexes in separate files. + There is always only one Group. Usage: >>> from neo.io.brainwaresrcio import BrainwareSrcIO @@ -96,8 +100,8 @@ class BrainwareSrcIO(BaseIO): >>> blks = srcfile.read_all_blocks() >>> print blk1.segments >>> print blk1.segments[0].spiketrains - >>> print blk1.units - >>> print blk1.units[0].name + >>> print blk1.groups + >>> print blk1.groups[0].name >>> print blk2 >>> print blk2[0].segments >>> print blks @@ -108,8 +112,8 @@ class BrainwareSrcIO(BaseIO): is_writable = False # write is not supported # This class is able to directly or indirectly handle the following objects - supported_objects = [Block, ChannelIndex, - Segment, SpikeTrain, Event, Unit] + supported_objects = [Block, Group, + Segment, SpikeTrain, Event] readable_objects = [Block] writeable_objects = [] @@ -156,15 +160,11 @@ def __init__(self, filename=None): # This stores the current Block self._blk = None - # This stores the current ChannelIndex for easy access - # It is equivalent to self._blk.channel_indexes[0] - self._chx = None - # This stores the current Segment for easy access # It is equivalent to self._blk.segments[-1] self._seg0 = None - # this stores a dictionary of the Block's Units by name, + # this stores a dictionary of the Block's Group (Units) by name, # making it easier and faster to retrieve Units by name later # UnassignedSpikes and Units accessed by index are not stored here self._unitdict = {} @@ -271,15 +271,11 @@ def read_next_block(self, **kargs): # create the Block and the contents all Blocks of from IO share self._blk = Block(file_origin=self._file_origin) - self._chx = ChannelIndex(file_origin=self._file_origin, - index=np.array([], dtype=np.int)) self._seg0 = Segment(name='Comments', file_origin=self._file_origin) - self._unit0 = Unit(name='UnassignedSpikes', - file_origin=self._file_origin, + self._unit0 = Group(name='UnassignedSpikes', elliptic=[], boundaries=[], timestamp=[], max_valid=[]) - self._blk.channel_indexes.append(self._chx) - self._chx.units.append(self._unit0) + self._blk.groups.append(self._unit0) self._blk.segments.append(self._seg0) # this actually reads the contents of the Block @@ -299,7 +295,6 @@ def read_next_block(self, **kargs): # reset the per-Block attributes self._blk = None - self._chx = None self._unitdict = {} # combine the comments into one big event @@ -448,9 +443,9 @@ def _assign_sequence(self, data_obj): should go based on its class. Warning are issued if this method is used since manual reorganization may be needed. """ - if isinstance(data_obj, Unit): - self.logger.warning('Unknown Unit found, adding to Units list') - self._chx.units.append(data_obj) + if isinstance(data_obj, Group): + self.logger.warning('Unknown Group found, adding to Group list') + self._blk.groups.append(data_obj) if data_obj.name: self._unitdict[data_obj.name] = data_obj elif isinstance(data_obj, Segment): @@ -948,11 +943,6 @@ def __read_segment_list(self): for _ in range(numelements): self.__read_comment() - # create a channel_index for the numchannels - self._chx.index = np.arange(numchannels) - self._chx.channel_names = np.array(['Chan{}'.format(i) - for i in range(numchannels)], dtype='U') - # store what side of the head we are dealing with for segment in segments: for spiketrain in segment.spiketrains: @@ -1266,7 +1256,6 @@ def __read_unit_list(self): numelements = np.fromfile(self._fsrc, dtype=np.int16, count=1)[0] # {sequence} * numelements1 -- the number of lists of Units to read - self._chx.annotations['max_valid'] = [] for i in range(numelements): # {skip} = byte * 2 (int16) -- skip 2 bytes @@ -1283,19 +1272,19 @@ def __read_unit_list(self): # if there aren't enough Units, create them # remember we need to skip the UnassignedSpikes Unit - if numunits > len(self._chx.units) + 1: - for ind1 in range(len(self._chx.units), numunits + 1): - unit = Unit(name='unit%s' % ind1, + if numunits > len(self._blk.groups) + 1: + for ind1 in range(len(self._blk.groups), numunits + 1): + unit = Group(name='unit%s' % ind1, file_origin=self._file_origin, elliptic=[], boundaries=[], timestamp=[], max_valid=[]) - self._chx.units.append(unit) + self._blk.groups.append(unit) # {Block} * numelements -- Units for ind1 in range(numunits): # get the Unit with the given index # remember we need to skip the UnassignedSpikes Unit - unit = self._chx.units[ind1 + 1] + unit = self._blk.groups[ind1 + 1] # {skip} = byte * 2 (int16) -- skip 2 bytes self._fsrc.seek(2, 1) @@ -1318,7 +1307,7 @@ def __read_unit_list(self): unit.annotations['boundaries'].append(boundaries) unit.annotations['max_valid'].append(max_valid) - return self._chx.units[1:maxunit] + return self._blk.groups[1:maxunit] def __read_unit_list_timestamped(self): """ @@ -1428,9 +1417,9 @@ def __read_unit_unsorted(self): if name in self._unitdict: unit = self._unitdict[name] else: - unit = Unit(name=name, file_origin=self._file_origin, + unit = Group(name=name, file_origin=self._file_origin, elliptic=[], boundaries=[], timestamp=[], max_valid=[]) - self._chx.units.append(unit) + self._blk.groups.append(unit) self._unitdict[name] = unit # convert the individual spikes to SpikeTrains and add them to the Unit diff --git a/neo/test/iotest/test_brainwaredamio.py b/neo/test/iotest/test_brainwaredamio.py index f1170820f..2dc80f3b2 100644 --- a/neo/test/iotest/test_brainwaredamio.py +++ b/neo/test/iotest/test_brainwaredamio.py @@ -10,7 +10,7 @@ import quantities as pq from neo.core import (AnalogSignal, Block, - ChannelIndex, Segment) + Group, Segment) from neo.io import BrainwareDamIO from neo.test.iotest.common_io_test import BaseTestIO from neo.test.tools import (assert_same_sub_schema, @@ -48,12 +48,9 @@ def proc_dam(filename): block = Block(file_origin=filename) - chx = ChannelIndex(file_origin=filename, - index=np.array([0]), - channel_ids=np.array([1]), - channel_names=np.array(['Chan1'], dtype='U')) + gr = Group(file_origin=filename) - block.channel_indexes.append(chx) + block.groups.append(gr) params = [res['params'][0, 0].flatten() for res in damfile['stim']] values = [res['values'][0, 0].flatten() for res in damfile['stim']] @@ -72,6 +69,8 @@ def proc_dam(filename): **stim) segment.analogsignals = [sig] block.segments.append(segment) + gr.analogsignals.append(sig) + sig.group = gr block.create_many_to_one_relationship() diff --git a/neo/test/iotest/test_brainwaref32io.py b/neo/test/iotest/test_brainwaref32io.py index ddc326013..c1ddfe96d 100644 --- a/neo/test/iotest/test_brainwaref32io.py +++ b/neo/test/iotest/test_brainwaref32io.py @@ -9,7 +9,7 @@ import numpy as np import quantities as pq -from neo.core import Block, ChannelIndex, Segment, SpikeTrain, Unit +from neo.core import Block, Segment, SpikeTrain, Group from neo.io import BrainwareF32IO from neo.test.iotest.common_io_test import BaseTestIO from neo.test.tools import (assert_same_sub_schema, @@ -41,14 +41,8 @@ def proc_f32(filename): # create the objects to store other objects block = Block(file_origin=filenameorig) - chx = ChannelIndex(file_origin=filenameorig, - index=np.array([], dtype=np.int), - channel_names=np.array([], dtype='U')) - unit = Unit(file_origin=filenameorig) - - # load objects into their containers - block.channel_indexes.append(chx) - chx.units.append(unit) + gr = Group(file_origin=filenameorig) + block.groups.append(gr) try: with np.load(filename, allow_pickle=True) as f32obj: @@ -81,7 +75,7 @@ def proc_f32(filename): segment = Segment(file_origin=filenameorig, **params) segment.spiketrains = [train] - unit.spiketrains.append(train) + gr.spiketrains.append(train) block.segments.append(segment) block.create_many_to_one_relationship() diff --git a/neo/test/iotest/test_brainwaresrcio.py b/neo/test/iotest/test_brainwaresrcio.py index 073dc947b..bff7f7883 100644 --- a/neo/test/iotest/test_brainwaresrcio.py +++ b/neo/test/iotest/test_brainwaresrcio.py @@ -11,7 +11,7 @@ import quantities as pq from neo.core import (Block, Event, - ChannelIndex, Segment, SpikeTrain, Unit) + Group, Segment, SpikeTrain) from neo.io import BrainwareSrcIO, brainwaresrcio from neo.test.iotest.common_io_test import BaseTestIO from neo.test.tools import (assert_same_sub_schema, @@ -74,12 +74,8 @@ def proc_src(filename): comm_seg = proc_src_comments(srcfile, filename) block.segments.append(comm_seg) - chx = proc_src_units(srcfile, filename) - chan_nums = np.arange(NChannels, dtype='int') - chan_names = ['Chan{}'.format(i) for i in range(NChannels)] - chx.index = chan_nums - chx.channel_names = np.array(chan_names, dtype='U') - block.channel_indexes.append(chx) + all_units = proc_src_units(srcfile, filename) + block.groups.extend(all_units) for rep in srcfile['sets'][0, 0].flatten(): proc_src_condition(rep, filename, ADperiod, side, block) @@ -116,12 +112,11 @@ def proc_src_comments(srcfile, filename): def proc_src_units(srcfile, filename): '''Get the units in an src file that has been processed by the official matlab function. See proc_src for details''' - chx = ChannelIndex(file_origin=filename, - index=np.array([], dtype=int)) - un_unit = Unit(name='UnassignedSpikes', file_origin=filename, + all_units = [] + un_unit = Group(name='UnassignedSpikes', file_origin=filename, elliptic=[], boundaries=[], timestamp=[], max_valid=[]) - chx.units.append(un_unit) + all_units.append(un_unit) sortInfo = srcfile['sortInfo'][0, 0] timeslice = sortInfo['timeslice'][0, 0] @@ -133,20 +128,18 @@ def proc_src_units(srcfile, filename): boundaries = [res.flatten() for res in cluster['boundaries'].flatten()] fullclust = zip(elliptic, boundaries) for ielliptic, iboundaries in fullclust: - unit = Unit(file_origin=filename, + unit = Group(file_origin=filename, boundaries=[iboundaries], elliptic=[ielliptic], timeStamp=[], max_valid=[maxValid]) - chx.units.append(unit) - return chx + all_units.append(unit) + return all_units def proc_src_condition(rep, filename, ADperiod, side, block): '''Get the condition in a src file that has been processed by the official matlab function. See proc_src for details''' - chx = block.channel_indexes[0] - stim = rep['stim'].flatten() params = [str(res[0]) for res in stim['paramName'][0].flatten()] values = [res for res in stim['paramVal'][0].flatten()] @@ -165,7 +158,7 @@ def proc_src_condition(rep, filename, ADperiod, side, block): trains = proc_src_condition_unit(spikeunit, sweepLen, side, ADperiod, respWin, damaIndexes, timeStamps, filename) - chx.units[0].spiketrains.extend(trains) + block.groups[0].spiketrains.extend(trains) atrains = [trains] else: damaIndexes = [] @@ -191,10 +184,10 @@ def proc_src_condition(rep, filename, ADperiod, side, block): respWins = [] spikeunits = [] - for unit, IdString in zip(chx.units[1:], IdStrings): + for unit, IdString in zip(block.groups[1:], IdStrings): unit.name = str(IdString) - fullunit = zip(spikeunits, chx.units[1:], sweepLens, respWins) + fullunit = zip(spikeunits, block.groups[1:], sweepLens, respWins) for spikeunit, unit, sweepLen, respWin in fullunit: trains = proc_src_condition_unit(spikeunit, sweepLen, side, ADperiod, respWin, damaIndexes, timeStamps, From c5858e81575c7d7dda3d3a0d57274e149f9b2482 Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Mon, 26 Oct 2020 13:24:41 +0100 Subject: [PATCH 36/85] Make error message more verbose. --- neo/core/group.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/neo/core/group.py b/neo/core/group.py index f2bb83a9c..22cfc848d 100644 --- a/neo/core/group.py +++ b/neo/core/group.py @@ -71,6 +71,7 @@ def add(self, *objects): """Add a new Neo object to the Group""" for obj in objects: if self.allowed_types and not isinstance(obj, self.allowed_types): - raise TypeError("This Group can only contain {}".format(self.allowed_types)) + raise TypeError("This Group can only contain {}, but not {}" + "".format(self.allowed_types, type(obj))) container = self._get_container(obj.__class__) container.append(obj) From 9df9e3ac9f2e8d612f426735d56fdcb44be7b03e Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Mon, 26 Oct 2020 22:12:08 +0100 Subject: [PATCH 37/85] Remove print statement --- neo/io/nixio.py | 1 - 1 file changed, 1 deletion(-) diff --git a/neo/io/nixio.py b/neo/io/nixio.py index c3f5cae74..bee5be544 100644 --- a/neo/io/nixio.py +++ b/neo/io/nixio.py @@ -817,7 +817,6 @@ def _write_channelview(self, chview, nixblock, nixgroup): if chview.annotations: for k, v in chview.annotations.items(): self._write_property(metadata, k, v) - print(nix_name) self._view_map[nix_name] = nixmt # link tag to the data array for the ChannelView's signal From c4e05ae922209e08cb92a818d4554798cd0888ce Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Mon, 26 Oct 2020 15:35:43 -0700 Subject: [PATCH 38/85] Fix initializer, update loop vars. --- neo/rawio/neuralynxrawio.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index e778f8f02..2905f7225 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -652,13 +652,14 @@ def __init__(self,ncsMemMap,recn): class NcsBlocks(): """ Contains information regarding the contiguous blocks of records in an Ncs file. - Factory methods perform parsing of this information from an Ncs file. + Methods of NcsBlocksFactory perform parsing of this information from an Ncs file. """ - startBlocks = [] - endBlocks = [] - sampFreqUsed = 0 # actual sampling frequency of samples - microsPerSampUsed = 0 # microseconds per sample + def __init__(self): + self.startBlocks = [] + self.endBlocks = [] + self.sampFreqUsed = 0 # actual sampling frequency of samples + self.microsPerSampUsed = 0 # microseconds per sample class NcsBlocksFactory(): @@ -818,6 +819,9 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): blkLen = hdr.nb_valid else: blkLen += hdr.nb_valid + lastRecTime = hdr.timestamp + lastRecNumSamps = hdr.nb_valid + ncsBlocks.endBlocks.append(ncsMemMap.shape[0] - 1) ncsBlocks.sampFreqUsed = maxBlkFreqEstimate @@ -858,7 +862,7 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq,rh0.timestamp, numSampsForPred) freqInFile = math.floor(nomFreq) - if abs(rhl.timestamp - predLastBlockStartTime) / rhl.timestamp < NcsBlocksFactory._tolerance and \ + if abs(rhl.timestamp - predLastBlockStartTime) / (rhl.timestamp - rh0.timestamp) < NcsBlocksFactory._tolerance and \ rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: nb.endBlocks.append(lastBlkI) nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) / 1e6 From 85a05e8244b9a7921958c0ced27b3d784768f4ef Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Mon, 26 Oct 2020 15:40:47 -0700 Subject: [PATCH 39/85] Add additional tests on v5.5.1 with 2 blocks --- neo/test/rawiotest/test_neuralynxrawio.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 1a0a40302..6b999328b 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -135,7 +135,8 @@ def testBuildUsingHeaderAndScanning(self): self.assertEqual(len(nb.endBlocks), 1) self.assertEqual(nb.endBlocks[0], 9) - # test Cheetah 5.5.1, which is DigitalLynxSX + # test Cheetah 5.5.1, which is DigitalLynxSX and has two blocks of records + # with a fairly large gap filename = self.get_filename_path('Cheetah_v5.5.1/original_data/Tet3a.ncs') hdr = NlxHeader.buildForFile(filename) data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', @@ -143,6 +144,12 @@ def testBuildUsingHeaderAndScanning(self): nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) self.assertEqual(nb.sampFreqUsed, 32000) self.assertEqual(nb.microsPerSampUsed, 31.25) + self.assertEqual(len(nb.startBlocks), 2) + self.assertEqual(nb.startBlocks[0], 0) + self.assertEqual(nb.startBlocks[1], 2498) + self.assertEqual(len(nb.endBlocks), 2) + self.assertEqual(nb.endBlocks[0], 2497) + self.assertEqual(nb.endBlocks[1], 3331) From 846edb055b91907519a47980cf1fc7a608a2ab5d Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Mon, 26 Oct 2020 16:22:08 -0700 Subject: [PATCH 40/85] Tests of block construction for incomplete blocks --- neo/test/rawiotest/test_neuralynxrawio.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 6b999328b..c3b3e6bc8 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -101,6 +101,15 @@ def test_ncsblocks_partial(self): self.assertEqual(data0.shape[0],6690) self.assertEqual(data0['timestamp'][6689],8515800549) # timestamp of last record + hdr = NlxHeader.buildForFile(filename) + nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) + self.assertEqual(nb.sampFreqUsed, 32009.05084744305) + self.assertEqual(nb.microsPerSampUsed, 31.241163781021083) + self.assertEqual(len(nb.startBlocks), 1) + self.assertEqual(nb.startBlocks[0], 0) + self.assertEqual(len(nb.endBlocks), 1) + self.assertEqual(nb.endBlocks[0], 6689) + def testBuildGivenActualFrequency(self): # Test early files where the frequency listed in the header is From 5ba611370640164438b5f2eeb96385927d46953e Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Mon, 26 Oct 2020 16:22:29 -0700 Subject: [PATCH 41/85] Fix up single block case. --- neo/rawio/neuralynxrawio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 2905f7225..73e8e4666 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -864,8 +864,9 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): freqInFile = math.floor(nomFreq) if abs(rhl.timestamp - predLastBlockStartTime) / (rhl.timestamp - rh0.timestamp) < NcsBlocksFactory._tolerance and \ rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: + nb.startBlocks.append(0) nb.endBlocks.append(lastBlkI) - nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) / 1e6 + nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) * 1e6 nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) # otherwise parse records to determine blocks using default maximum gap length From 153446dc38d0d2c41ad71fc8add52609876812c8 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Tue, 27 Oct 2020 08:40:18 -0700 Subject: [PATCH 42/85] Remove unneeded classes. Clean up style. --- neo/rawio/neuralynxrawio.py | 78 ++++++++++++++----------------------- 1 file changed, 29 insertions(+), 49 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 73e8e4666..a95b5c05e 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -31,7 +31,6 @@ import math - class NeuralynxRawIO(BaseRawIO): """" Class for reading datasets recorded by Neuralynx. @@ -53,7 +52,7 @@ class NeuralynxRawIO(BaseRawIO): _BLOCK_SIZE = 512 # nb sample per signal block _ncs_dtype = [('timestamp', 'uint64'), ('channel_id', 'uint32'), ('sample_rate', 'uint32'), - ('nb_valid', 'uint32'), ('samples', 'int16', (_BLOCK_SIZE,))] + ('nb_valid', 'uint32'), ('samples', 'int16', (_BLOCK_SIZE,))] def __init__(self, dirname='', keep_original_times=False, **kargs): """ @@ -235,7 +234,7 @@ def _parse_header(self): # :TODO: current algorithm depends on side-effect of read_ncs_files on # self._sigs_memmap, self._sigs_t_start, self._sigs_t_stop, # self._sigs_length, self._nb_segment, self._timestamp_limits - ncsBlocks = self.read_ncs_files(self.ncs_filenames) + self.read_ncs_files(self.ncs_filenames) # Determine timestamp limits in nev, nse file by scanning them. ts0, ts1 = None, None @@ -470,9 +469,9 @@ def _rescale_event_timestamp(self, event_timestamps, dtype): def read_ncs_files(self, ncs_filenames): """ - Given a list of ncs files, return a dictionary of NcsBlocks indexed by channel uid. + Given a list of ncs files, read their basic structure and setup the following + attributes: - :TODO: Current algorithm has side effects on following attributes: * self._sigs_memmap = [ {} for seg_index in range(self._nb_segment) ] * self._sigs_t_start = [] * self._sigs_t_stop = [] @@ -567,7 +566,7 @@ def read_ncs_files(self, ncs_filenames): for chan_uid, ncs_filename in self.ncs_filenames.items(): data = np.memmap(ncs_filename, dtype=self.ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) + offset=NlxHeader.HEADER_SIZE) assert data.size == data0.size, 'ncs files do not have the same data length' for seg_index, (i0, i1) in enumerate(gap_pairs): @@ -595,7 +594,7 @@ def read_ncs_files(self, ncs_filenames): class WholeMicrosTimePositionBlock(): """ - Map of time to sample positions. + Wrapper of static calculations of time to sample positions. Times are rounded to nearest microsecond. Model here is that times from start of a sample until just before the next sample are included, @@ -603,11 +602,6 @@ class WholeMicrosTimePositionBlock(): channel with no samples is empty and contains no time intervals. """ - _sampFrequency = 0 - _startTime = 0 - _size = 0 - _microsPerSamp = 0 - @staticmethod def getFreqForMicrosPerSamp(micros): """ @@ -628,19 +622,16 @@ def calcSampleTime(sampFr, startTime, posn): Calculate time rounded to microseconds for sample given frequency, start time, and sample position. """ - return round(startTime+ + return round(startTime + WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(sampFr)*posn) + class CscRecordHeader(): """ Information in header of each Ncs record, excluding sample values themselves. """ - timestamp = 0 - channel_id = 0 - sample_rate = 0 - nb_valid = 0 - def __init__(self,ncsMemMap,recn): + def __init__(self, ncsMemMap, recn): """ Construct a record header for a given record in a memory map for an NcsFile. """ @@ -649,6 +640,7 @@ def __init__(self,ncsMemMap,recn): self.sample_rate = ncsMemMap['sample_rate'][recn] self.nb_valid = ncsMemMap['nb_valid'][recn] + class NcsBlocks(): """ Contains information regarding the contiguous blocks of records in an Ncs file. @@ -659,10 +651,10 @@ def __init__(self): self.startBlocks = [] self.endBlocks = [] self.sampFreqUsed = 0 # actual sampling frequency of samples - self.microsPerSampUsed = 0 # microseconds per sample + self.microsPerSampUsed = 0 # microseconds per sample -class NcsBlocksFactory(): +class NcsBlocksFactory: """ Class for factory methods which perform parsing of contiguous blocks of records in Ncs files. @@ -671,9 +663,8 @@ class NcsBlocksFactory(): more complicated. Copied from Java code on Sept 7, 2020. """ - _tolerance = 0.001 # tolerance for drift of timestamps within one NcsBlock - _maxGapLength = 5 # maximum gap between predicted and actual block timestamps - # still considered within one NcsBlock + _tolerance = 0.001 # tolerance for drift of timestamps within one NcsBlock + _maxGapLength = 5 # maximum gap between predicted and actual block timestamps still considered within one NcsBlock @staticmethod def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime): @@ -700,7 +691,7 @@ def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePre blkLen = 0 for recn in range(1, ncsMemMap.shape[0]): hdr = CscRecordHeader(ncsMemMap, recn) - if hdr.channel_id!=chanNum | hdr.sample_rate!=reqFreq: + if hdr.channel_id != chanNum | hdr.sample_rate != reqFreq: raise IOError('Channel number or sampling frequency changed in records within file') predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, startBlockPredTime, blkLen) @@ -718,7 +709,6 @@ def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePre return ncsBlocks - @staticmethod def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): """ @@ -755,7 +745,7 @@ def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): lastBlkI = ncsMemMap.shape[0] - 1 rhl = CscRecordHeader(ncsMemMap, lastBlkI) predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, - NeuralynxRawIO._BLOCK_SIZE * lastBlkI) + NeuralynxRawIO._BLOCK_SIZE * lastBlkI) if rhl.channel_id == chanNum and rhl.sample_rate == reqFreq and rhl.timestamp == predLastBlockStartTime: nb = NcsBlocks() nb.startBlocks.append(0) @@ -765,10 +755,9 @@ def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): # otherwise need to scan looking for breaks else: blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, - rh0.nb_valid) + rh0.nb_valid) return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, nb, chanNum, reqFreq, blkOnePredTime) - @staticmethod def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): """ @@ -808,8 +797,8 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): if hdr.channel_id != chanNum | hdr.sample_rate != recFreq: raise IOError('Channel number or sampling frequency changed in records within file') predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, lastRecTime, - lastRecNumSamps) - if (abs(hdr.timestamp - predTime) > maxGapLen): + lastRecNumSamps) + if abs(hdr.timestamp - predTime) > maxGapLen: ncsBlocks.endBlocks.append(recn-1) ncsBlocks.startBlocks.append(recn) if blkLen > maxBlkLen: @@ -829,7 +818,6 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): return ncsBlocks - @staticmethod def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): """ @@ -855,15 +843,16 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): chanNum = rh0.channel_id lastBlkI = numRecs - 1 - rhl = CscRecordHeader(ncsMemMap,lastBlkI) + rhl = CscRecordHeader(ncsMemMap, lastBlkI) # check if file is one block of records, to within tolerance, which is often the case numSampsForPred = NeuralynxRawIO._BLOCK_SIZE * lastBlkI - predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq,rh0.timestamp, + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq, rh0.timestamp, numSampsForPred) freqInFile = math.floor(nomFreq) - if abs(rhl.timestamp - predLastBlockStartTime) / (rhl.timestamp - rh0.timestamp) < NcsBlocksFactory._tolerance and \ - rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: + if abs(rhl.timestamp - predLastBlockStartTime) / \ + (rhl.timestamp - rh0.timestamp) < NcsBlocksFactory._tolerance and \ + rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: nb.startBlocks.append(0) nb.endBlocks.append(lastBlkI) nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) * 1e6 @@ -871,14 +860,14 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): # otherwise parse records to determine blocks using default maximum gap length else: - nb.sampFreqUsed = nomFreq - nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) - nb = NcsBlocksFactory._parseForMaxGap(ncsMemMap, nb, NcsBlocksFactory._maxGapLength) + nb.sampFreqUsed = nomFreq + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) + nb = NcsBlocksFactory._parseForMaxGap(ncsMemMap, nb, NcsBlocksFactory._maxGapLength) return nb @staticmethod - def buildForNcsFile(ncsMemMap,nlxHdr): + def buildForNcsFile(ncsMemMap, nlxHdr): """ Build an NcsBlocks object for an NcsFile, given as a memmap and NlxHeader, handling gap detection appropriately given the file type as specified by the header. @@ -931,7 +920,7 @@ def _to_bool(txt): elif txt == 'False': return False else: - raise Exception('Can not convert %s to bool' % (txt)) + raise Exception('Can not convert %s to bool' % txt) # keys that may be present in header which we parse txt_header_keys = [ @@ -1110,8 +1099,6 @@ def buildForFile(filename): else: hpd = NlxHeader.header_pattern_dicts['def'] - original_filename = re.search(hpd['filename_regex'], txt_header).groupdict()['filename'] - # opening time dt1 = re.search(hpd['datetime1_regex'], txt_header).groupdict() info['recording_opened'] = datetime.datetime.strptime( @@ -1168,13 +1155,6 @@ def typeOfRecording(self): return 'UNKNOWN' -class NcsHeader(): - """ - Representation of information in Ncs file headers, including exact - recording type. - """ - - nev_dtype = [ ('reserved', ' Date: Tue, 27 Oct 2020 09:03:56 -0700 Subject: [PATCH 43/85] Use private dtype by new private name. --- neo/rawio/neuralynxrawio.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index a95b5c05e..9bf80804c 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -499,7 +499,7 @@ def read_ncs_files(self, ncs_filenames): chan_uid0 = list(ncs_filenames.keys())[0] filename0 = ncs_filenames[chan_uid0] - data0 = np.memmap(filename0, dtype=self.ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) + data0 = np.memmap(filename0, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) gap_indexes = None lost_indexes = None @@ -565,7 +565,7 @@ def read_ncs_files(self, ncs_filenames): # create segment with subdata block/t_start/t_stop/length for chan_uid, ncs_filename in self.ncs_filenames.items(): - data = np.memmap(ncs_filename, dtype=self.ncs_dtype, mode='r', + data = np.memmap(ncs_filename, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) assert data.size == data0.size, 'ncs files do not have the same data length' From b6ecbcd0b78147c80e82eb19324acb5693bec228 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Tue, 27 Oct 2020 09:48:42 -0700 Subject: [PATCH 44/85] Add test of side effects of read_ncs_files --- neo/test/rawiotest/test_neuralynxrawio.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index c3b3e6bc8..2a6000bbb 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -66,6 +66,21 @@ class TestNeuralynxRawIO(BaseTestRawIO, unittest.TestCase, ): 'Cheetah_v6.3.2/incomplete_blocks/Events.nev', 'Cheetah_v6.3.2/incomplete_blocks/README.txt'] + def test_read_ncs_files_sideeffects(self): + + # Test Cheetah 5.5.1, which is DigitalLynxSX and has two blocks of records + # with a fairly large gap. + rawio = NeuralynxRawIO(self.get_filename_path('Cheetah_v5.5.1/original_data')) + rawio.parse_header() + self.assertEqual(rawio._nb_segment, 2) + self.assertListEqual(rawio._timestamp_limits,[(26122557633, 26162525633), + (26366360633, 26379704633)]) + self.assertListEqual(rawio._sigs_length,[1278976, 427008]) + self.assertListEqual(rawio._sigs_t_stop,[26162.525633, 26379.704633]) + self.assertListEqual(rawio._sigs_t_start,[26122.557633, 26366.360633]) + self.assertEqual(len(rawio._sigs_memmap),2) # check only that there are 2 memmaps + + class TestNcsRecordingType(TestNeuralynxRawIO, unittest.TestCase): """ From 6982c597f8b56b872e3d781569143609d63f2342 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Tue, 27 Oct 2020 11:34:40 -0700 Subject: [PATCH 45/85] Use NcsBlocksFactory and logical or. --- neo/rawio/neuralynxrawio.py | 121 ++++++++++++++---------------------- 1 file changed, 45 insertions(+), 76 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 9bf80804c..c0dec5e1f 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -479,116 +479,85 @@ def read_ncs_files(self, ncs_filenames): * self._nb_segment * self._timestamp_limits - The first file is read entirely to detect gaps in timestamp. - each gap lead to a new segment. - - Other files are not read entirely but we check than gaps - are at the same place. - - - gap_indexes can be given (when cached) to avoid full read. - + Files will be scanned to determine the blocks of records. If file is a single block of records, + this scan is brief, otherwise it will check each record which may take some time. """ + # :TODO: Needs to account for gaps and start and end times potentially # being different in different groups of channels. These groups typically # correspond to the channels collected by a single ADC card. if len(ncs_filenames) == 0: return None - good_delta = int(self._BLOCK_SIZE * 1e6 / self._sigs_sampling_rate) chan_uid0 = list(ncs_filenames.keys())[0] filename0 = ncs_filenames[chan_uid0] + # parse the structure of the first file data0 = np.memmap(filename0, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) - - gap_indexes = None - lost_indexes = None - - if self.use_cache: - gap_indexes = self._cache.get('gap_indexes') - lost_indexes = self._cache.get('lost_indexes') - - # detect gaps on first file - if (gap_indexes is None) or (lost_indexes is None): - - # this can be long!!!! - timestamps0 = data0['timestamp'] - deltas0 = np.diff(timestamps0) - - # :TODO: This algorithm needs to account for older style files which had a rounded - # off sampling rate in the header. - # - # It should be that: - # gap_indexes, = np.nonzero(deltas0!=good_delta) - # but for a file I have found many deltas0==15999, 16000, 16001 (for sampling at 32000) - # I guess this is a round problem - # So this is the same with a tolerance of 1 or 2 ticks - max_tolerance = 2 - mask = np.abs((deltas0 - good_delta).astype('int64')) > max_tolerance - - gap_indexes, = np.nonzero(mask) - - if self.use_cache: - self.add_in_cache(gap_indexes=gap_indexes) - - # update for lost_indexes - # Sometimes NLX writes a faulty block, but it then validates how much samples it wrote - # the validation field is in delta0['nb_valid'], it should be equal to BLOCK_SIZE - # :TODO: this algorithm ignores samples in partially filled blocks, which - # is not strictly necessary as all channels might have same partially filled - # blocks at the end. - - lost_indexes, = np.nonzero(data0['nb_valid'] < self._BLOCK_SIZE) - - if self.use_cache: - self.add_in_cache(lost_indexes=lost_indexes) - - gap_candidates = np.unique([0] - + [data0.size] - + (gap_indexes + 1).tolist() - + lost_indexes.tolist()) # linear - - gap_pairs = np.vstack([gap_candidates[:-1], gap_candidates[1:]]).T # 2D (n_segments, 2) + hdr0 = NlxHeader.buildForFile(filename0) + nb0 = NcsBlocksFactory.buildForNcsFile(data0,hdr0) # construct proper gap ranges free of lost samples artifacts minimal_segment_length = 1 # in blocks - goodpairs = np.diff(gap_pairs, 1).reshape(-1) > minimal_segment_length - gap_pairs = gap_pairs[goodpairs] # ensures a segment is at least a block wide - self._nb_segment = len(gap_pairs) + self._nb_segment = len(nb0.startBlocks) self._sigs_memmap = [{} for seg_index in range(self._nb_segment)] self._sigs_t_start = [] self._sigs_t_stop = [] self._sigs_length = [] self._timestamp_limits = [] - # create segment with subdata block/t_start/t_stop/length + # create segment with subdata block/t_start/t_stop/length for each channel for chan_uid, ncs_filename in self.ncs_filenames.items(): - data = np.memmap(ncs_filename, dtype=self._ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) - assert data.size == data0.size, 'ncs files do not have the same data length' + if chan_uid == chan_uid0: + data = data0 + hdr = hdr0 + nb = nb0 + else: + data = np.memmap(ncs_filename, dtype=self._ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) + hdr = NlxHeader.buildForFile(ncs_filename) + nb = NcsBlocksFactory.buildForNcsFile(data, hdr) + + # Check that record block structure of each file is identical to the first. + if len(nb.startBlocks) != len(nb0.startBlocks) or len(nb.endBlocks) != len(nb0.endBlocks): + raise IOError('ncs files have different numbers of blocks of records') + + for i, sbi in enumerate(nb.startBlocks): + if (sbi != nb0.startBlocks[i]): + raise IOError('ncs files have different start block structure') + + for i, ebi in enumerate(nb.endBlocks): + if (ebi != nb0.endBlocks[i]): + raise IOError('ncs files have different end block structure') - for seg_index, (i0, i1) in enumerate(gap_pairs): + # create a memmap for each record block + for seg_index in range(len(nb.startBlocks)): - assert data[i0]['timestamp'] == data0[i0][ - 'timestamp'], 'ncs files do not have the same gaps' - assert data[i1 - 1]['timestamp'] == data0[i1 - 1][ - 'timestamp'], 'ncs files do not have the same gaps' + if (data[nb.startBlocks[seg_index]]['timestamp'] != + data0[nb0.startBlocks[seg_index]]['timestamp'] or + data[nb.endBlocks[seg_index]]['timestamp'] != + data0[nb0.endBlocks[seg_index]]['timestamp']) : + raise IOError('ncs files have different timestamp structure') - subdata = data[i0:i1] + subdata = data[nb.startBlocks[seg_index]:nb.endBlocks[seg_index]] self._sigs_memmap[seg_index][chan_uid] = subdata if chan_uid == chan_uid0: + numSampsLastBlock = subdata[-1]['nb_valid'] ts0 = subdata[0]['timestamp'] - ts1 = subdata[-1]['timestamp'] +\ - np.uint64(self._BLOCK_SIZE / self._sigs_sampling_rate * 1e6) + ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, + subdata[-1]['timestamp'], + numSampsLastBlock) self._timestamp_limits.append((ts0, ts1)) t_start = ts0 / 1e6 self._sigs_t_start.append(t_start) t_stop = ts1 / 1e6 self._sigs_t_stop.append(t_stop) - length = subdata.size * self._BLOCK_SIZE + # :TODO: this should really be the total of block lengths, but this allows + # the last block to be shorter, the most common case + length = (subdata.size - 1) * self._BLOCK_SIZE + numSampsLastBlock self._sigs_length.append(length) @@ -794,7 +763,7 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): ncsBlocks.startBlocks.append(0) for recn in range(1, ncsMemMap.shape[0]): hdr = CscRecordHeader(ncsMemMap, recn) - if hdr.channel_id != chanNum | hdr.sample_rate != recFreq: + if hdr.channel_id != chanNum or hdr.sample_rate != recFreq: raise IOError('Channel number or sampling frequency changed in records within file') predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, lastRecTime, lastRecNumSamps) From 7930bb4fbbea79666b1d03e7f71a2358bb5c197b Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Wed, 28 Oct 2020 11:35:23 +0100 Subject: [PATCH 46/85] Deprecate old Blackrock and Neuralynx IOs --- neo/io/blackrockio_v4.py | 11 ++++++++--- neo/io/neuralynxio_v1.py | 4 ++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/neo/io/blackrockio_v4.py b/neo/io/blackrockio_v4.py index 051330beb..3c53e740c 100644 --- a/neo/io/blackrockio_v4.py +++ b/neo/io/blackrockio_v4.py @@ -50,11 +50,12 @@ import datetime import os import re +import warnings import numpy as np import quantities as pq -import neo +import neo.io.blackrockio from neo.io.baseio import BaseIO from neo.core import (Block, Segment, SpikeTrain, Unit, Event, ChannelIndex, AnalogSignal) @@ -142,7 +143,7 @@ class BlackrockIO(BaseIO): is_streameable = False read_params = { - neo.Block: [ + Block: [ ('nsx_to_load', { 'value': 'none', 'label': "List of nsx files (ids, int) to read."}), @@ -168,7 +169,7 @@ class BlackrockIO(BaseIO): ('load_events', { 'value': False, 'label': "States if events should be loaded."})], - neo.Segment: [ + Segment: [ ('n_start', { 'label': "Start time point (Quantity) for segment"}), ('n_stop', { @@ -216,6 +217,10 @@ def __init__(self, filename, nsx_override=None, nev_override=None, """ Initialize the BlackrockIO class. """ + + warnings.warn('{} is deprecated and will be removed in neo version 0.10. Use {} instead.' + ''.format(self.__class__, neo.io.blackrockio.BlackrockIO), FutureWarning) + BaseIO.__init__(self) # Used to avoid unnecessary repetition of verbose messages diff --git a/neo/io/neuralynxio_v1.py b/neo/io/neuralynxio_v1.py index 7eb88553c..2ca5fbe73 100644 --- a/neo/io/neuralynxio_v1.py +++ b/neo/io/neuralynxio_v1.py @@ -28,6 +28,7 @@ import quantities as pq from neo.io.baseio import BaseIO +import neo.io.neuralynxio from neo.core import (Block, Segment, ChannelIndex, AnalogSignal, SpikeTrain, Event, Unit) from os import listdir, sep @@ -146,6 +147,9 @@ def __init__(self, sessiondir=None, cachedir=None, use_cache='hash', has priority over filename. """ + warnings.warn('{} is deprecated and will be removed in neo version 0.10. Use {} instead.' + ''.format(self.__class__, neo.io.neuralynxio.NeuralynxIO), FutureWarning) + BaseIO.__init__(self) # possiblity to provide filename instead of sessiondir for IO From d9ade17b0201883f4e909845232973b30cd63f1f Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 15:36:46 -0700 Subject: [PATCH 47/85] Fix off by one in range for list. Comments. --- neo/rawio/neuralynxrawio.py | 18 ++++++++++-------- neo/test/rawiotest/test_neuralynxrawio.py | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index c0dec5e1f..02cfdbade 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -541,22 +541,24 @@ def read_ncs_files(self, ncs_filenames): data0[nb0.endBlocks[seg_index]]['timestamp']) : raise IOError('ncs files have different timestamp structure') - subdata = data[nb.startBlocks[seg_index]:nb.endBlocks[seg_index]] + subdata = data[nb.startBlocks[seg_index]:(nb.endBlocks[seg_index]+1)] self._sigs_memmap[seg_index][chan_uid] = subdata if chan_uid == chan_uid0: numSampsLastBlock = subdata[-1]['nb_valid'] ts0 = subdata[0]['timestamp'] - ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, - subdata[-1]['timestamp'], - numSampsLastBlock) + ts1 = subdata[-1]['timestamp'] +\ + np.uint64(self._BLOCK_SIZE / self._sigs_sampling_rate * 1e6) + # ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, + # subdata[-1]['timestamp'], + # numSampsLastBlock) self._timestamp_limits.append((ts0, ts1)) t_start = ts0 / 1e6 self._sigs_t_start.append(t_start) t_stop = ts1 / 1e6 self._sigs_t_stop.append(t_stop) - # :TODO: this should really be the total of block lengths, but this allows - # the last block to be shorter, the most common case + # :TODO: this should really be the total of nb_valid in records, but this allows + # the last record of a block to be shorter, the most common case length = (subdata.size - 1) * self._BLOCK_SIZE + numSampsLastBlock self._sigs_length.append(length) @@ -617,8 +619,8 @@ class NcsBlocks(): """ def __init__(self): - self.startBlocks = [] - self.endBlocks = [] + self.startBlocks = [] # index of starting record for each block + self.endBlocks = [] # index of last record (inclusive) for each block self.sampFreqUsed = 0 # actual sampling frequency of samples self.microsPerSampUsed = 0 # microseconds per sample diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 2a6000bbb..c15bfa822 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -73,6 +73,7 @@ def test_read_ncs_files_sideeffects(self): rawio = NeuralynxRawIO(self.get_filename_path('Cheetah_v5.5.1/original_data')) rawio.parse_header() self.assertEqual(rawio._nb_segment, 2) + # test values here from direct inspection of .ncs files self.assertListEqual(rawio._timestamp_limits,[(26122557633, 26162525633), (26366360633, 26379704633)]) self.assertListEqual(rawio._sigs_length,[1278976, 427008]) From 4af130c0bbc2767cfb414407bb3d53b8afcb9798 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 15:40:27 -0700 Subject: [PATCH 48/85] Use standard time calculation for last time of block. --- neo/rawio/neuralynxrawio.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 02cfdbade..6c7ae89fe 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -547,18 +547,17 @@ def read_ncs_files(self, ncs_filenames): if chan_uid == chan_uid0: numSampsLastBlock = subdata[-1]['nb_valid'] ts0 = subdata[0]['timestamp'] - ts1 = subdata[-1]['timestamp'] +\ - np.uint64(self._BLOCK_SIZE / self._sigs_sampling_rate * 1e6) - # ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, - # subdata[-1]['timestamp'], - # numSampsLastBlock) + ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, + subdata[-1]['timestamp'], + numSampsLastBlock) self._timestamp_limits.append((ts0, ts1)) t_start = ts0 / 1e6 self._sigs_t_start.append(t_start) t_stop = ts1 / 1e6 self._sigs_t_stop.append(t_stop) - # :TODO: this should really be the total of nb_valid in records, but this allows - # the last record of a block to be shorter, the most common case + # :TODO: This should really be the total of nb_valid in records, but this allows + # the last record of a block to be shorter, the most common case. Have never + # seen a block of records with not full records before the last. length = (subdata.size - 1) * self._BLOCK_SIZE + numSampsLastBlock self._sigs_length.append(length) From 88880e019607aff8a8a8bedcdc7e38d52d076a12 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 17:52:04 -0700 Subject: [PATCH 49/85] Remove test with tolerance over whole length. Fix microsPerSampUsed assignement. Using a tolerance over a longer experiment is not sensitive enough to detect blocks where perhaps a large amount of samples are dropped and there is a small gap afterwards. --- neo/rawio/neuralynxrawio.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 6c7ae89fe..34ad5975d 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -633,7 +633,6 @@ class NcsBlocksFactory: more complicated. Copied from Java code on Sept 7, 2020. """ - _tolerance = 0.001 # tolerance for drift of timestamps within one NcsBlock _maxGapLength = 5 # maximum gap between predicted and actual block timestamps still considered within one NcsBlock @staticmethod @@ -784,12 +783,12 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): ncsBlocks.endBlocks.append(ncsMemMap.shape[0] - 1) ncsBlocks.sampFreqUsed = maxBlkFreqEstimate - ncsBlocks.setMicrosPerSamp = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(maxBlkFreqEstimate) + ncsBlocks.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(maxBlkFreqEstimate) return ncsBlocks @staticmethod - def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): + def _buildForMaxGap(ncsMemMap, nomFreq): """ Determine blocks of records in memory mapped Ncs file given a nominal frequency of the file, using the default values of frequency tolerance and maximum gap between blocks. @@ -815,13 +814,12 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): lastBlkI = numRecs - 1 rhl = CscRecordHeader(ncsMemMap, lastBlkI) - # check if file is one block of records, to within tolerance, which is often the case + # check if file is one block of records, with exact timestamp match, which may be the case numSampsForPred = NeuralynxRawIO._BLOCK_SIZE * lastBlkI predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq, rh0.timestamp, numSampsForPred) freqInFile = math.floor(nomFreq) - if abs(rhl.timestamp - predLastBlockStartTime) / \ - (rhl.timestamp - rh0.timestamp) < NcsBlocksFactory._tolerance and \ + if abs(rhl.timestamp - predLastBlockStartTime) == 0 and \ rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: nb.startBlocks.append(0) nb.endBlocks.append(lastBlkI) @@ -862,7 +860,7 @@ def buildForNcsFile(ncsMemMap, nlxHdr): # digital lynx style with fractional frequency and micros per samp determined from block times elif acqType == "DIGITALLYNX" or acqType == "DIGITALLYNXSX": nomFreq = nlxHdr['sampling_rate'] - nb = NcsBlocksFactory._buildForToleranceAndMaxGap(ncsMemMap, nomFreq) + nb = NcsBlocksFactory._buildForMaxGap(ncsMemMap, nomFreq) # BML style with fractional frequency and micros per samp elif acqType == "BML": From 6b8b64a3f1638261d9b306c78fb6d48e597edb22 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 17:53:37 -0700 Subject: [PATCH 50/85] Tests of raw io for incomplete records multiple block case. --- neo/test/rawiotest/test_neuralynxrawio.py | 26 +++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index c15bfa822..2feded960 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -72,8 +72,8 @@ def test_read_ncs_files_sideeffects(self): # with a fairly large gap. rawio = NeuralynxRawIO(self.get_filename_path('Cheetah_v5.5.1/original_data')) rawio.parse_header() - self.assertEqual(rawio._nb_segment, 2) # test values here from direct inspection of .ncs files + self.assertEqual(rawio._nb_segment, 2) self.assertListEqual(rawio._timestamp_limits,[(26122557633, 26162525633), (26366360633, 26379704633)]) self.assertListEqual(rawio._sigs_length,[1278976, 427008]) @@ -81,6 +81,19 @@ def test_read_ncs_files_sideeffects(self): self.assertListEqual(rawio._sigs_t_start,[26122.557633, 26366.360633]) self.assertEqual(len(rawio._sigs_memmap),2) # check only that there are 2 memmaps + # Test Cheetah 6.3.2, the incomplete_blocks test. This is a DigitalLynxSX with + # three blocks of records. Gaps are on the order of 16 ms or so. + rawio = NeuralynxRawIO(self.get_filename_path('Cheetah_v6.3.2/incomplete_blocks')) + rawio.parse_header() + # test values here from direct inspection of .ncs file + self.assertEqual(rawio._nb_segment, 3) + self.assertListEqual(rawio._timestamp_limits,[(8408806811, 8427831990), + (8427832053, 8487768498), + (8487768561, 8515816549)]) + self.assertListEqual(rawio._sigs_length,[608806, 1917967, 897536]) + self.assertListEqual(rawio._sigs_t_stop,[8427.831990, 8487.768498, 8515.816549]) + self.assertListEqual(rawio._sigs_t_start,[8408.806811, 8427.832053, 8487.768561]) + self.assertEqual(len(rawio._sigs_memmap),3) # check only that there are 3 memmaps class TestNcsRecordingType(TestNeuralynxRawIO, unittest.TestCase): @@ -105,6 +118,7 @@ def test_recording_types(self): hdr = NlxHeader.buildForFile(filename) self.assertEqual(hdr.typeOfRecording(), typeTest[1]) + class TestNcsBlocksFactory(TestNeuralynxRawIO, unittest.TestCase): """ Test building NcsBlocks for files of different revisions. @@ -119,12 +133,10 @@ def test_ncsblocks_partial(self): hdr = NlxHeader.buildForFile(filename) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) - self.assertEqual(nb.sampFreqUsed, 32009.05084744305) - self.assertEqual(nb.microsPerSampUsed, 31.241163781021083) - self.assertEqual(len(nb.startBlocks), 1) - self.assertEqual(nb.startBlocks[0], 0) - self.assertEqual(len(nb.endBlocks), 1) - self.assertEqual(nb.endBlocks[0], 6689) + self.assertEqual(nb.sampFreqUsed, 32000.012813673042) + self.assertEqual(nb.microsPerSampUsed, 31.249987486652431) + self.assertListEqual(nb.startBlocks, [0, 1190, 4937]) + self.assertListEqual(nb.endBlocks, [1189, 4936, 6689]) def testBuildGivenActualFrequency(self): From 81f14b22cdd2d7f9d09d6bd785218ce3ab168522 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 18:04:40 -0700 Subject: [PATCH 51/85] Update stop times to include time for samples in partially filled records. --- neo/test/iotest/test_neuralynxio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neo/test/iotest/test_neuralynxio.py b/neo/test/iotest/test_neuralynxio.py index 116515940..cfa4dd384 100644 --- a/neo/test/iotest/test_neuralynxio.py +++ b/neo/test/iotest/test_neuralynxio.py @@ -308,7 +308,7 @@ def test_incomplete_block_handling_v632(self): for t, gt in zip(nio._sigs_t_start, [8408.806811, 8427.832053, 8487.768561]): self.assertEqual(np.round(t, 4), np.round(gt, 4)) - for t, gt in zip(nio._sigs_t_stop, [8427.830803, 8487.768029, 8515.816549]): + for t, gt in zip(nio._sigs_t_stop, [8427.831990, 8487.768498, 8515.816549]): self.assertEqual(np.round(t, 4), np.round(gt, 4)) From 4ed56794657ca4481a504869ca41e7c135c752b8 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 18:21:35 -0700 Subject: [PATCH 52/85] PEP and style cleanup. Corrected gap comment. --- neo/rawio/neuralynxrawio.py | 20 +++++------ neo/test/rawiotest/test_neuralynxrawio.py | 42 +++++++++++------------ 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 34ad5975d..1f4edbf38 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -164,7 +164,7 @@ def _parse_header(self): dtype = get_nse_or_ntt_dtype(info, ext) - if (os.path.getsize(filename) <= NlxHeader.HEADER_SIZE): + if os.path.getsize(filename) <= NlxHeader.HEADER_SIZE: self._empty_nse_ntt.append(filename) data = np.zeros((0,), dtype=dtype) else: @@ -197,7 +197,7 @@ def _parse_header(self): # each ('event_id', 'ttl_input') give a new event channel self.nev_filenames[chan_id] = filename - if (os.path.getsize(filename) <= NlxHeader.HEADER_SIZE): + if os.path.getsize(filename) <= NlxHeader.HEADER_SIZE: self._empty_nev.append(filename) data = np.zeros((0,), dtype=nev_dtype) internal_ids = [] @@ -495,7 +495,7 @@ def read_ncs_files(self, ncs_filenames): # parse the structure of the first file data0 = np.memmap(filename0, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) hdr0 = NlxHeader.buildForFile(filename0) - nb0 = NcsBlocksFactory.buildForNcsFile(data0,hdr0) + nb0 = NcsBlocksFactory.buildForNcsFile(data0, hdr0) # construct proper gap ranges free of lost samples artifacts minimal_segment_length = 1 # in blocks @@ -525,11 +525,11 @@ def read_ncs_files(self, ncs_filenames): raise IOError('ncs files have different numbers of blocks of records') for i, sbi in enumerate(nb.startBlocks): - if (sbi != nb0.startBlocks[i]): + if sbi != nb0.startBlocks[i]: raise IOError('ncs files have different start block structure') for i, ebi in enumerate(nb.endBlocks): - if (ebi != nb0.endBlocks[i]): + if ebi != nb0.endBlocks[i]: raise IOError('ncs files have different end block structure') # create a memmap for each record block @@ -538,7 +538,7 @@ def read_ncs_files(self, ncs_filenames): if (data[nb.startBlocks[seg_index]]['timestamp'] != data0[nb0.startBlocks[seg_index]]['timestamp'] or data[nb.endBlocks[seg_index]]['timestamp'] != - data0[nb0.endBlocks[seg_index]]['timestamp']) : + data0[nb0.endBlocks[seg_index]]['timestamp']): raise IOError('ncs files have different timestamp structure') subdata = data[nb.startBlocks[seg_index]:(nb.endBlocks[seg_index]+1)] @@ -548,7 +548,7 @@ def read_ncs_files(self, ncs_filenames): numSampsLastBlock = subdata[-1]['nb_valid'] ts0 = subdata[0]['timestamp'] ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, - subdata[-1]['timestamp'], + subdata[-1]['timestamp'], numSampsLastBlock) self._timestamp_limits.append((ts0, ts1)) t_start = ts0 / 1e6 @@ -562,7 +562,7 @@ def read_ncs_files(self, ncs_filenames): self._sigs_length.append(length) -class WholeMicrosTimePositionBlock(): +class WholeMicrosTimePositionBlock: """ Wrapper of static calculations of time to sample positions. @@ -596,7 +596,7 @@ def calcSampleTime(sampFr, startTime, posn): WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(sampFr)*posn) -class CscRecordHeader(): +class CscRecordHeader: """ Information in header of each Ncs record, excluding sample values themselves. """ @@ -611,7 +611,7 @@ def __init__(self, ncsMemMap, recn): self.nb_valid = ncsMemMap['nb_valid'][recn] -class NcsBlocks(): +class NcsBlocks: """ Contains information regarding the contiguous blocks of records in an Ncs file. Methods of NcsBlocksFactory perform parsing of this information from an Ncs file. diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 2feded960..a11996cef 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -74,26 +74,26 @@ def test_read_ncs_files_sideeffects(self): rawio.parse_header() # test values here from direct inspection of .ncs files self.assertEqual(rawio._nb_segment, 2) - self.assertListEqual(rawio._timestamp_limits,[(26122557633, 26162525633), - (26366360633, 26379704633)]) - self.assertListEqual(rawio._sigs_length,[1278976, 427008]) - self.assertListEqual(rawio._sigs_t_stop,[26162.525633, 26379.704633]) - self.assertListEqual(rawio._sigs_t_start,[26122.557633, 26366.360633]) - self.assertEqual(len(rawio._sigs_memmap),2) # check only that there are 2 memmaps + self.assertListEqual(rawio._timestamp_limits, [(26122557633, 26162525633), + (26366360633, 26379704633)]) + self.assertListEqual(rawio._sigs_length, [1278976, 427008]) + self.assertListEqual(rawio._sigs_t_stop, [26162.525633, 26379.704633]) + self.assertListEqual(rawio._sigs_t_start, [26122.557633, 26366.360633]) + self.assertEqual(len(rawio._sigs_memmap), 2) # check only that there are 2 memmaps # Test Cheetah 6.3.2, the incomplete_blocks test. This is a DigitalLynxSX with - # three blocks of records. Gaps are on the order of 16 ms or so. + # three blocks of records. Gaps are on the order of 60 microseconds or so. rawio = NeuralynxRawIO(self.get_filename_path('Cheetah_v6.3.2/incomplete_blocks')) rawio.parse_header() # test values here from direct inspection of .ncs file self.assertEqual(rawio._nb_segment, 3) - self.assertListEqual(rawio._timestamp_limits,[(8408806811, 8427831990), - (8427832053, 8487768498), - (8487768561, 8515816549)]) - self.assertListEqual(rawio._sigs_length,[608806, 1917967, 897536]) - self.assertListEqual(rawio._sigs_t_stop,[8427.831990, 8487.768498, 8515.816549]) - self.assertListEqual(rawio._sigs_t_start,[8408.806811, 8427.832053, 8487.768561]) - self.assertEqual(len(rawio._sigs_memmap),3) # check only that there are 3 memmaps + self.assertListEqual(rawio._timestamp_limits, [(8408806811, 8427831990), + (8427832053, 8487768498), + (8487768561, 8515816549)]) + self.assertListEqual(rawio._sigs_length, [608806, 1917967, 897536]) + self.assertListEqual(rawio._sigs_t_stop, [8427.831990, 8487.768498, 8515.816549]) + self.assertListEqual(rawio._sigs_t_start, [8408.806811, 8427.832053, 8487.768561]) + self.assertEqual(len(rawio._sigs_memmap), 3) # check only that there are 3 memmaps class TestNcsRecordingType(TestNeuralynxRawIO, unittest.TestCase): @@ -127,9 +127,9 @@ class TestNcsBlocksFactory(TestNeuralynxRawIO, unittest.TestCase): def test_ncsblocks_partial(self): filename = self.get_filename_path('Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs') data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) - self.assertEqual(data0.shape[0],6690) - self.assertEqual(data0['timestamp'][6689],8515800549) # timestamp of last record + offset=NlxHeader.HEADER_SIZE) + self.assertEqual(data0.shape[0], 6690) + self.assertEqual(data0['timestamp'][6689], 8515800549) # timestamp of last record hdr = NlxHeader.buildForFile(filename) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) @@ -144,7 +144,7 @@ def testBuildGivenActualFrequency(self): # floor(1e6/(actual number of microseconds between samples) filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) + offset=NlxHeader.HEADER_SIZE) ncsBlocks = NcsBlocks() ncsBlocks.sampFreqUsed = 1/35e-6 ncsBlocks.microsPerSampUsed = 35 @@ -154,7 +154,6 @@ def testBuildGivenActualFrequency(self): self.assertEqual(len(ncsBlocks.endBlocks), 1) self.assertEqual(ncsBlocks.endBlocks[0], 9) - def testBuildUsingHeaderAndScanning(self): # Test early files where the frequency listed in the header is @@ -162,7 +161,7 @@ def testBuildUsingHeaderAndScanning(self): filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') hdr = NlxHeader.buildForFile(filename) data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) + offset=NlxHeader.HEADER_SIZE) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) self.assertEqual(nb.sampFreqUsed, 1/35e-6) @@ -177,7 +176,7 @@ def testBuildUsingHeaderAndScanning(self): filename = self.get_filename_path('Cheetah_v5.5.1/original_data/Tet3a.ncs') hdr = NlxHeader.buildForFile(filename) data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) + offset=NlxHeader.HEADER_SIZE) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) self.assertEqual(nb.sampFreqUsed, 32000) self.assertEqual(nb.microsPerSampUsed, 31.25) @@ -189,6 +188,5 @@ def testBuildUsingHeaderAndScanning(self): self.assertEqual(nb.endBlocks[1], 3331) - if __name__ == "__main__": unittest.main() From 82c93d93564a28c120991f82b390030347905b1d Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Thu, 29 Oct 2020 08:18:56 -0700 Subject: [PATCH 53/85] Line shortening for PEP8. --- neo/rawio/neuralynxrawio.py | 92 +++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 1f4edbf38..473d7e6a7 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -479,8 +479,9 @@ def read_ncs_files(self, ncs_filenames): * self._nb_segment * self._timestamp_limits - Files will be scanned to determine the blocks of records. If file is a single block of records, - this scan is brief, otherwise it will check each record which may take some time. + Files will be scanned to determine the blocks of records. If file is a single + block of records, this scan is brief, otherwise it will check each record which may + take some time. """ # :TODO: Needs to account for gaps and start and end times potentially @@ -521,7 +522,8 @@ def read_ncs_files(self, ncs_filenames): nb = NcsBlocksFactory.buildForNcsFile(data, hdr) # Check that record block structure of each file is identical to the first. - if len(nb.startBlocks) != len(nb0.startBlocks) or len(nb.endBlocks) != len(nb0.endBlocks): + if len(nb.startBlocks) != len(nb0.startBlocks) or len(nb.endBlocks) != \ + len(nb0.endBlocks): raise IOError('ncs files have different numbers of blocks of records') for i, sbi in enumerate(nb.startBlocks): @@ -555,9 +557,9 @@ def read_ncs_files(self, ncs_filenames): self._sigs_t_start.append(t_start) t_stop = ts1 / 1e6 self._sigs_t_stop.append(t_stop) - # :TODO: This should really be the total of nb_valid in records, but this allows - # the last record of a block to be shorter, the most common case. Have never - # seen a block of records with not full records before the last. + # :TODO: This should really be the total of nb_valid in records, but this + # allows the last record of a block to be shorter, the most common case. + # Have never seen a block of records with not full records before the last. length = (subdata.size - 1) * self._BLOCK_SIZE + numSampsLastBlock self._sigs_length.append(length) @@ -633,7 +635,8 @@ class NcsBlocksFactory: more complicated. Copied from Java code on Sept 7, 2020. """ - _maxGapLength = 5 # maximum gap between predicted and actual block timestamps still considered within one NcsBlock + _maxGapLength = 5 # maximum gap between predicted and actual block timestamps still + # considered within one NcsBlock @staticmethod def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime): @@ -661,16 +664,18 @@ def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePre for recn in range(1, ncsMemMap.shape[0]): hdr = CscRecordHeader(ncsMemMap, recn) if hdr.channel_id != chanNum | hdr.sample_rate != reqFreq: - raise IOError('Channel number or sampling frequency changed in records within file') + raise IOError('Channel number or sampling frequency changed in ' + + 'records within file') predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, startBlockPredTime, blkLen) nValidSamps = hdr.nb_valid if hdr.timestamp != predTime: ncsBlocks.endBlocks.append(recn-1) ncsBlocks.startBlocks.append(recn) - startBlockPredTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, - hdr.timestamp, - nValidSamps) + startBlockPredTime = WholeMicrosTimePositionBlock.calcSampleTime( + ncsBlocks.sampFreqUsed, + hdr.timestamp, + nValidSamps) blklen = 0 else: blkLen += nValidSamps @@ -713,9 +718,11 @@ def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): # check if file is one block of records, which is often the case, and avoid full parse lastBlkI = ncsMemMap.shape[0] - 1 rhl = CscRecordHeader(ncsMemMap, lastBlkI) - predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, - NeuralynxRawIO._BLOCK_SIZE * lastBlkI) - if rhl.channel_id == chanNum and rhl.sample_rate == reqFreq and rhl.timestamp == predLastBlockStartTime: + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, + rh0.timestamp, + NeuralynxRawIO._BLOCK_SIZE * lastBlkI) + if rhl.channel_id == chanNum and rhl.sample_rate == reqFreq and \ + rhl.timestamp == predLastBlockStartTime: nb = NcsBlocks() nb.startBlocks.append(0) nb.endBlocks.append(lastBlkI) @@ -723,9 +730,11 @@ def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): # otherwise need to scan looking for breaks else: - blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, + blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, + rh0.timestamp, rh0.nb_valid) - return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, nb, chanNum, reqFreq, blkOnePredTime) + return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, nb, chanNum, reqFreq, + blkOnePredTime) @staticmethod def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): @@ -737,20 +746,23 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): ncsMemMap: memmap of Ncs file ncsBlocks: - NcsBlocks object with sampFreqUsed set to nominal frequency to use in computing time for samples (Hz) + NcsBlocks object with sampFreqUsed set to nominal frequency to use in computing time + for samples (Hz) maxGapLen: - maximum difference within a block between predicted time of start of record and recorded time + maximum difference within a block between predicted time of start of record and + recorded time RETURN: - NcsBlocks object with sampFreqUsed and microsPerSamp set based on estimate from largest block + NcsBlocks object with sampFreqUsed and microsPerSamp set based on estimate from + largest block """ # track frequency of each block and use estimate with longest block maxBlkLen = 0 maxBlkFreqEstimate = 0 - # Parse the record sequence, finding blocks of continuous time with no more than maxGapLength - # and same channel number + # Parse the record sequence, finding blocks of continuous time with no more than + # maxGapLength and same channel number rh0 = CscRecordHeader(ncsMemMap, 0) chanNum = rh0.channel_id @@ -764,15 +776,17 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): for recn in range(1, ncsMemMap.shape[0]): hdr = CscRecordHeader(ncsMemMap, recn) if hdr.channel_id != chanNum or hdr.sample_rate != recFreq: - raise IOError('Channel number or sampling frequency changed in records within file') - predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, lastRecTime, - lastRecNumSamps) + raise IOError('Channel number or sampling frequency changed in ' + + 'records within file') + predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, + lastRecTime, lastRecNumSamps) if abs(hdr.timestamp - predTime) > maxGapLen: ncsBlocks.endBlocks.append(recn-1) ncsBlocks.startBlocks.append(recn) if blkLen > maxBlkLen: maxBlkLen = blkLen - maxBlkFreqEstimate = (blkLen - lastRecNumSamps) * 1e6 / (lastRecTime - startBlockTime) + maxBlkFreqEstimate = (blkLen - lastRecNumSamps) * 1e6 / \ + (lastRecTime - startBlockTime) startBlockTime = hdr.timestamp blkLen = hdr.nb_valid else: @@ -783,15 +797,16 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): ncsBlocks.endBlocks.append(ncsMemMap.shape[0] - 1) ncsBlocks.sampFreqUsed = maxBlkFreqEstimate - ncsBlocks.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(maxBlkFreqEstimate) + ncsBlocks.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq( + maxBlkFreqEstimate) return ncsBlocks @staticmethod def _buildForMaxGap(ncsMemMap, nomFreq): """ - Determine blocks of records in memory mapped Ncs file given a nominal frequency of the file, - using the default values of frequency tolerance and maximum gap between blocks. + Determine blocks of records in memory mapped Ncs file given a nominal frequency of + the file, using the default values of frequency tolerance and maximum gap between blocks. PARAMETERS ncsMemMap: @@ -816,7 +831,8 @@ def _buildForMaxGap(ncsMemMap, nomFreq): # check if file is one block of records, with exact timestamp match, which may be the case numSampsForPred = NeuralynxRawIO._BLOCK_SIZE * lastBlkI - predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq, rh0.timestamp, + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq, + rh0.timestamp, numSampsForPred) freqInFile = math.floor(nomFreq) if abs(rhl.timestamp - predLastBlockStartTime) == 0 and \ @@ -824,12 +840,14 @@ def _buildForMaxGap(ncsMemMap, nomFreq): nb.startBlocks.append(0) nb.endBlocks.append(lastBlkI) nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) * 1e6 - nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq( + nb.sampFreqUsed) # otherwise parse records to determine blocks using default maximum gap length else: nb.sampFreqUsed = nomFreq - nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq( + nb.sampFreqUsed) nb = NcsBlocksFactory._parseForMaxGap(ncsMemMap, nb, NcsBlocksFactory._maxGapLength) return nb @@ -844,16 +862,19 @@ def buildForNcsFile(ncsMemMap, nlxHdr): ncsMemMap: memory map of file acqType: - string specifying type of data acquisition used, one of types returned by NlxHeader.typeOfRecording() + string specifying type of data acquisition used, one of types returned by + NlxHeader.typeOfRecording() """ acqType = nlxHdr.typeOfRecording() # old Neuralynx style with rounded whole microseconds for the samples if acqType == "PRE4": freq = nlxHdr['sampling_rate'] - microsPerSampUsed = math.floor(WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(freq)) + microsPerSampUsed = math.floor(WholeMicrosTimePositionBlock.getMicrosPerSampForFreq( + freq)) sampFreqUsed = WholeMicrosTimePositionBlock.getFreqForMicrosPerSamp(microsPerSampUsed) - nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) + nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, + math.floor(freq)) nb.sampFreqUsed = sampFreqUsed nb.microsPerSampUsed = microsPerSampUsed @@ -865,7 +886,8 @@ def buildForNcsFile(ncsMemMap, nlxHdr): # BML style with fractional frequency and micros per samp elif acqType == "BML": sampFreqUsed = nlxHdr['sampling_rate'] - nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(sampFreqUsed)) + nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, + math.floor(sampFreqUsed)) else: raise TypeError("Unknown Ncs file type from header.") From ba82efe4580e74c321e04cf110be27c050ce77d5 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Thu, 29 Oct 2020 08:25:20 -0700 Subject: [PATCH 54/85] More PEP8 items. --- neo/rawio/neuralynxrawio.py | 11 ++++++----- neo/test/rawiotest/test_neuralynxrawio.py | 7 ++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 473d7e6a7..8ba6aae49 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -543,7 +543,7 @@ def read_ncs_files(self, ncs_filenames): data0[nb0.endBlocks[seg_index]]['timestamp']): raise IOError('ncs files have different timestamp structure') - subdata = data[nb.startBlocks[seg_index]:(nb.endBlocks[seg_index]+1)] + subdata = data[nb.startBlocks[seg_index]:(nb.endBlocks[seg_index] + 1)] self._sigs_memmap[seg_index][chan_uid] = subdata if chan_uid == chan_uid0: @@ -595,7 +595,7 @@ def calcSampleTime(sampFr, startTime, posn): start time, and sample position. """ return round(startTime + - WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(sampFr)*posn) + WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(sampFr) * posn) class CscRecordHeader: @@ -636,7 +636,7 @@ class NcsBlocksFactory: """ _maxGapLength = 5 # maximum gap between predicted and actual block timestamps still - # considered within one NcsBlock + # considered within one NcsBlock @staticmethod def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime): @@ -781,7 +781,7 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, lastRecTime, lastRecNumSamps) if abs(hdr.timestamp - predTime) > maxGapLen: - ncsBlocks.endBlocks.append(recn-1) + ncsBlocks.endBlocks.append(recn - 1) ncsBlocks.startBlocks.append(recn) if blkLen > maxBlkLen: maxBlkLen = blkLen @@ -878,7 +878,8 @@ def buildForNcsFile(ncsMemMap, nlxHdr): nb.sampFreqUsed = sampFreqUsed nb.microsPerSampUsed = microsPerSampUsed - # digital lynx style with fractional frequency and micros per samp determined from block times + # digital lynx style with fractional frequency and micros per samp determined from + # block times elif acqType == "DIGITALLYNX" or acqType == "DIGITALLYNXSX": nomFreq = nlxHdr['sampling_rate'] nb = NcsBlocksFactory._buildForMaxGap(ncsMemMap, nomFreq) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index a11996cef..10c49a87a 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -146,9 +146,10 @@ def testBuildGivenActualFrequency(self): data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) ncsBlocks = NcsBlocks() - ncsBlocks.sampFreqUsed = 1/35e-6 + ncsBlocks.sampFreqUsed = 1 / 35e-6 ncsBlocks.microsPerSampUsed = 35 - ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks.sampFreqUsed, 27789) + ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks.sampFreqUsed, + 27789) self.assertEqual(len(ncsBlocks.startBlocks), 1) self.assertEqual(ncsBlocks.startBlocks[0], 0) self.assertEqual(len(ncsBlocks.endBlocks), 1) @@ -164,7 +165,7 @@ def testBuildUsingHeaderAndScanning(self): offset=NlxHeader.HEADER_SIZE) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) - self.assertEqual(nb.sampFreqUsed, 1/35e-6) + self.assertEqual(nb.sampFreqUsed, 1 / 35e-6) self.assertEqual(nb.microsPerSampUsed, 35) self.assertEqual(len(nb.startBlocks), 1) self.assertEqual(nb.startBlocks[0], 0) From c995a41b8746a3c31339c40453755c703e691d17 Mon Sep 17 00:00:00 2001 From: dizcza Date: Mon, 2 Nov 2020 12:31:11 +0100 Subject: [PATCH 55/85] fixed a bug in loading hd5py data --- neo/io/hdf5io.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neo/io/hdf5io.py b/neo/io/hdf5io.py index c665a8934..255538112 100644 --- a/neo/io/hdf5io.py +++ b/neo/io/hdf5io.py @@ -308,7 +308,7 @@ def _merge_data_objects(self, objects): return objects def _get_quantity(self, node): - value = node.value + value = node[()] unit_str = [x for x in node.attrs.keys() if "unit" in x][0].split("__")[1] units = getattr(pq, unit_str) return value * units From 4a75a84ef57e12b91148d2037acff6f92a7af4d0 Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Tue, 3 Nov 2020 10:13:27 +0100 Subject: [PATCH 56/85] Fix API change of h5py version 3.0 '.value' was removed in favour of '[()]' --- neo/io/hdf5io.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/neo/io/hdf5io.py b/neo/io/hdf5io.py index 255538112..4a2b0b6bf 100644 --- a/neo/io/hdf5io.py +++ b/neo/io/hdf5io.py @@ -213,7 +213,7 @@ def _read_epocharray(self, node, parent): attributes = self._get_standard_attributes(node) times = self._get_quantity(node["times"]) durations = self._get_quantity(node["durations"]) - labels = node["labels"].value.astype('U') + labels = node["labels"][()].astype('U') epoch = Epoch(times=times, durations=durations, labels=labels, **attributes) epoch.segment = parent return epoch @@ -224,7 +224,7 @@ def _read_epoch(self, node, parent): def _read_eventarray(self, node, parent): attributes = self._get_standard_attributes(node) times = self._get_quantity(node["times"]) - labels = node["labels"].value.astype('U') + labels = node["labels"][()].astype('U') event = Event(times=times, labels=labels, **attributes) event.segment = parent return event @@ -235,8 +235,8 @@ def _read_event(self, node, parent): def _read_recordingchannelgroup(self, node, parent): # todo: handle Units attributes = self._get_standard_attributes(node) - channel_indexes = node["channel_indexes"].value - channel_names = node["channel_names"].value + channel_indexes = node["channel_indexes"][()] + channel_names = node["channel_names"][()] if channel_indexes.size: if len(node['recordingchannels']): From b7178657ab9daabc234734c9bfcbfcbe149116e3 Mon Sep 17 00:00:00 2001 From: Julia Sprenger Date: Tue, 3 Nov 2020 11:10:50 +0100 Subject: [PATCH 57/85] Add version check for h5py --- neo/io/hdf5io.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/neo/io/hdf5io.py b/neo/io/hdf5io.py index 4a2b0b6bf..0c0d48a23 100644 --- a/neo/io/hdf5io.py +++ b/neo/io/hdf5io.py @@ -5,6 +5,7 @@ import logging +from distutils.version import LooseVersion import pickle import numpy as np import quantities as pq @@ -23,7 +24,7 @@ from neo.core.baseneo import MergeError logger = logging.getLogger('Neo') - +min_h5py_version = LooseVersion('2.6.0') def disjoint_groups(groups): """`groups` should be a list of sets""" @@ -55,6 +56,10 @@ class NeoHdf5IO(BaseIO): def __init__(self, filename): if not HAVE_H5PY: raise ImportError("h5py is not available") + if HAVE_H5PY: + if LooseVersion(h5py.__version__) < min_h5py_version: + raise ImportError('h5py version {} is too old. Minimal required version is {}' + ''.format(h5py.__version__, min_h5py_version)) BaseIO.__init__(self, filename=filename) self._data = h5py.File(filename, 'r') self.object_refs = {} From 8f87b460101080f9d18674e8351c531eab41f2ab Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Thu, 5 Nov 2020 08:37:28 +0100 Subject: [PATCH 58/85] test fix: objects in a Group do not keep a reference to the group --- neo/test/tools.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/neo/test/tools.py b/neo/test/tools.py index 66705436b..11643b654 100644 --- a/neo/test/tools.py +++ b/neo/test/tools.py @@ -151,16 +151,17 @@ def assert_neo_object_is_compliant(ob, check_type=True): obattr.dtype.kind, dtp.kind) # test bijectivity : parents and children - for container in getattr(ob, '_single_child_containers', []): - for i, child in enumerate(getattr(ob, container, [])): - assert hasattr(child, _reference_name( - classname)), '%s should have %s attribute (2 way relationship)' \ - '' % (container, _reference_name(classname)) - if hasattr(child, _reference_name(classname)): - parent = getattr(child, _reference_name(classname)) - assert parent == ob, '%s.%s %s is not symmetric with %s.%s' \ - '' % (container, _reference_name(classname), i, classname, - container) + if classname != "Group": # objects in a Group do not keep a reference to the group. + for container in getattr(ob, '_single_child_containers', []): + for i, child in enumerate(getattr(ob, container, [])): + assert hasattr(child, _reference_name( + classname)), '%s should have %s attribute (2 way relationship)' \ + '' % (container, _reference_name(classname)) + if hasattr(child, _reference_name(classname)): + parent = getattr(child, _reference_name(classname)) + assert parent == ob, '%s.%s %s is not symmetric with %s.%s' \ + '' % (container, _reference_name(classname), i, classname, + container) # recursive on one to many rel for i, child in enumerate(getattr(ob, 'children', [])): From f2fd5da908d5c231e07846c2d769e1b524de3896 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Thu, 5 Nov 2020 10:38:42 +0100 Subject: [PATCH 59/85] Support for nested Groups in NixIO --- neo/io/nixio.py | 41 +++++++++++++++++++++++++--------- neo/test/iotest/test_nixio.py | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/neo/io/nixio.py b/neo/io/nixio.py index bee5be544..63778559f 100644 --- a/neo/io/nixio.py +++ b/neo/io/nixio.py @@ -305,6 +305,7 @@ def _nix_to_neo_block(self, nix_block): ) # descend into Groups + groups_to_resolve = [] for grp in nix_block.groups: if grp.type == "neo.segment": newseg = self._nix_to_neo_segment(grp) @@ -312,13 +313,22 @@ def _nix_to_neo_block(self, nix_block): # parent reference newseg.block = neo_block elif grp.type == "neo.group": - newgrp = self._nix_to_neo_group(grp) + newgrp, parent_name = self._nix_to_neo_group(grp) + assert parent_name is None neo_block.groups.append(newgrp) # parent reference newgrp.block = neo_block + elif grp.type == "neo.subgroup": + newgrp, parent_name = self._nix_to_neo_group(grp) + groups_to_resolve.append((newgrp, parent_name)) else: raise Exception("Unexpected group type") + # link subgroups to parents + for newgrp, parent_name in groups_to_resolve: + parent = self._neo_map[parent_name] + parent.groups.append(newgrp) + # find free floating (Groupless) signals and spiketrains blockdas = self._group_signals(nix_block.data_arrays) for name, das in blockdas.items(): @@ -408,6 +418,7 @@ def _nix_to_neo_segment(self, nix_group): def _nix_to_neo_group(self, nix_group): neo_attrs = self._nix_attr_to_neo(nix_group) + parent_name = neo_attrs.pop("neo_parent", None) neo_group = Group(**neo_attrs) self._neo_map[nix_group.name] = neo_group dataarrays = list(filter( @@ -427,8 +438,7 @@ def _nix_to_neo_group(self, nix_group): obj = self._neo_map[mtag.name] neo_group.add(obj) - # todo: handle sub-groups, once we implement writing those - return neo_group + return neo_group, parent_name def _nix_to_neo_channelindex(self, nix_source): neo_attrs = self._nix_attr_to_neo(nix_source) @@ -880,27 +890,38 @@ def _write_segment(self, segment, nixblock): for imagesequence in segment.imagesequences: self._write_imagesequence(imagesequence, nixblock, nixgroup) - def _write_group(self, neo_group, nixblock): + def _write_group(self, neo_group, nixblock, parent=None): """ Convert the provided Neo Group to a NIX Group and write it to the NIX file. :param neo_group: Neo Group to be written :param nixblock: NIX Block where the NIX Group will be created + :param parent: for sub-groups, the parent Neo Group """ + if parent: + label = "neo.subgroup" + # note that the use of a different label for top-level groups and sub-groups is not + # strictly necessary, the presence of the "neo_parent" annotation is sufficient. + # However, I think it adds clarity and helps in debugging and testing. + else: + label = "neo.group" + if "nix_name" in neo_group.annotations: nix_name = neo_group.annotations["nix_name"] else: - nix_name = "neo.group.{}".format(self._generate_nix_name()) + nix_name = "{}.{}".format(label, self._generate_nix_name()) neo_group.annotate(nix_name=nix_name) - nixgroup = nixblock.create_group(nix_name, "neo.group") + nixgroup = nixblock.create_group(nix_name, label) nixgroup.metadata = nixblock.metadata.create_section( - nix_name, "neo.group.metadata" + nix_name, f"{label}.metadata" ) metadata = nixgroup.metadata neoname = neo_group.name if neo_group.name is not None else "" metadata["neo_name"] = neoname + if parent: + metadata["neo_parent"] = parent.annotations["nix_name"] nixgroup.definition = neo_group.description if neo_group.annotations: for k, v in neo_group.annotations.items(): @@ -944,9 +965,9 @@ def _write_group(self, neo_group, nixblock): for chview in neo_group.channelviews: self._write_channelview(chview, nixblock, nixgroup) - if len(neo_group.groups) > 0: - # todo - raise NotImplementedError("Cannot yet store groups within groups") + # save sub-groups + for subgroup in neo_group.groups: + self._write_group(subgroup, nixblock, parent=neo_group) def _write_analogsignal(self, anasig, nixblock, nixgroup): """ diff --git a/neo/test/iotest/test_nixio.py b/neo/test/iotest/test_nixio.py index 0bc90efdf..dd3695002 100644 --- a/neo/test/iotest/test_nixio.py +++ b/neo/test/iotest/test_nixio.py @@ -1044,6 +1044,48 @@ def test_group_write(self): self.write_and_compare([block]) + def test_group_write_nested(self): + signals = [ + AnalogSignal(np.random.random(size=(1000, 5)) * pq.mV, + sampling_period=1 * pq.ms, name="sig1"), + AnalogSignal(np.random.random(size=(1000, 3)) * pq.mV, + sampling_period=1 * pq.ms, name="sig2"), + ] + spiketrains = [ + SpikeTrain([0.1, 54.3, 76.6, 464.2], units=pq.ms, + t_stop=1000.0 * pq.ms, t_start=0.0 * pq.ms), + SpikeTrain([30.1, 154.3, 276.6, 864.2], units=pq.ms, + t_stop=1000.0 * pq.ms, t_start=0.0 * pq.ms), + SpikeTrain([120.1, 454.3, 576.6, 764.2], units=pq.ms, + t_stop=1000.0 * pq.ms, t_start=0.0 * pq.ms), + ] + epochs = [ + Epoch(times=[0, 500], durations=[100, 100], units=pq.ms, labels=["A", "B"]) + ] + + seg = Segment(name="seg1") + seg.analogsignals.extend(signals) + seg.spiketrains.extend(spiketrains) + seg.epochs.extend(epochs) + for obj in chain(signals, spiketrains, epochs): + obj.segment = seg + + views = [ChannelView(index=np.array([0, 3, 4]), obj=signals[0], name="view_of_sig1")] + + subgroup = Group(objects=(signals[0:1] + views), name="subgroup") + groups = [ + Group(objects=([subgroup] + spiketrains[0:2] + epochs), name="group1"), + Group(objects=(signals[1:2] + spiketrains[1:] + epochs), name="group2") + ] + + block = Block(name="block1") + block.segments.append(seg) + block.groups.extend(groups) + for obj in chain([seg], groups): + obj.block = block + + self.write_and_compare([block]) + def test_metadata_structure_write(self): neoblk = self.create_all_annotated() self.io.write_block(neoblk) From 2efc3d8bfcd97d11cf57f9708c80f86681dc3bd4 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Thu, 5 Nov 2020 16:57:18 +0100 Subject: [PATCH 60/85] fix deprecation warnings --- neo/io/brainwaresrcio.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/neo/io/brainwaresrcio.py b/neo/io/brainwaresrcio.py index 3d3a8aadb..e38d3a236 100755 --- a/neo/io/brainwaresrcio.py +++ b/neo/io/brainwaresrcio.py @@ -394,8 +394,7 @@ def _read_by_id(self): try: # uint16 -- the ID code of the next sequence - seqid = np.asscalar(np.fromfile(self._fsrc, - dtype=np.uint16, count=1)) + seqid = np.fromfile(self._fsrc, dtype=np.uint16, count=1).item() except ValueError: # return a None if at EOF. Other methods use None to recognize # an EOF @@ -638,8 +637,7 @@ def __read_str(self, numchars=1, utf=None): """ Read a string of a specific length. """ - rawstr = np.asscalar(np.fromfile(self._fsrc, - dtype='S%s' % numchars, count=1)) + rawstr = np.fromfile(self._fsrc, dtype='S%s' % numchars, count=1).item() return rawstr.decode('utf-8') def __read_annotations(self): @@ -667,8 +665,7 @@ def __read_annotations(self): self._fsrc.seek(1, 1) # uint8 -- length of next string - numchars = np.asscalar(np.fromfile(self._fsrc, - dtype=np.uint8, count=1)) + numchars = np.fromfile(self._fsrc, dtype=np.uint8, count=1).item() # if there is no name, make one up if not numchars: @@ -739,15 +736,13 @@ def __read_comment(self): time = np.fromfile(self._fsrc, dtype=np.double, count=1)[0] # int16 -- length of next string - numchars1 = np.asscalar(np.fromfile(self._fsrc, - dtype=np.int16, count=1)) + numchars1 = np.fromfile(self._fsrc, dtype=np.int16, count=1).item() # char * numchars -- the one who sent the comment sender = self.__read_str(numchars1) # int16 -- length of next string - numchars2 = np.asscalar(np.fromfile(self._fsrc, - dtype=np.int16, count=1)) + numchars2 = np.fromfile(self._fsrc, dtype=np.int16, count=1).item() # char * numchars -- comment text text = self.__read_str(numchars2, utf=False) @@ -1397,8 +1392,7 @@ def __read_unit_unsorted(self): self._fsrc.seek(2, 1) # uint16 -- number of characters in next string - numchars = np.asscalar(np.fromfile(self._fsrc, - dtype=np.uint16, count=1)) + numchars = np.fromfile(self._fsrc, dtype=np.uint16, count=1).item() # char * numchars -- ID string of Unit name = self.__read_str(numchars) From 2ac95965f76ae55bec1656f17db2ed22b8140dd1 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Thu, 5 Nov 2020 17:54:56 +0100 Subject: [PATCH 61/85] Fixing or commenting out tests that involve ChannelIndex and Unit --- neo/test/iotest/test_axographio.py | 4 +- neo/test/iotest/test_blackrockio.py | 36 ++++++------- neo/test/iotest/test_brainwaresrcio.py | 2 +- neo/test/iotest/test_exampleio.py | 4 +- neo/test/iotest/test_neuralynxio.py | 74 +++++++++++++------------- 5 files changed, 59 insertions(+), 61 deletions(-) diff --git a/neo/test/iotest/test_axographio.py b/neo/test/iotest/test_axographio.py index 72d726621..5de997f76 100644 --- a/neo/test/iotest/test_axographio.py +++ b/neo/test/iotest/test_axographio.py @@ -209,7 +209,7 @@ def test_multi_segment(self): reader = AxographIO(filename=filename) blk = reader.read_block() assert_equal(len(blk.segments), 30) - assert_equal(len(blk.channel_indexes), 2) + assert_equal(len(blk.groups), 2) def test_force_single_segment(self): """Test reading an episodic file into one Segment""" @@ -218,7 +218,7 @@ def test_force_single_segment(self): reader = AxographIO(filename=filename, force_single_segment=True) blk = reader.read_block() assert_equal(len(blk.segments), 1) - assert_equal(len(blk.channel_indexes), 60) + assert_equal(len(blk.groups), 60) def test_events_and_epochs(self): """Test loading events and epochs""" diff --git a/neo/test/iotest/test_blackrockio.py b/neo/test/iotest/test_blackrockio.py index 3785356fb..3ca9e123d 100644 --- a/neo/test/iotest/test_blackrockio.py +++ b/neo/test/iotest/test_blackrockio.py @@ -109,13 +109,12 @@ def test_inputs_V23(self): # test 4 Units block = reader.read_block(load_waveforms=True, - signal_group_mode='split-all', - units_group_mode='all-in-one') + signal_group_mode='split-all') self.assertEqual(len(block.segments[0].analogsignals), 10) - self.assertEqual(len(block.channel_indexes[-1].units), 4) - self.assertEqual(len(block.channel_indexes[-1].units), - len(block.segments[0].spiketrains)) + #self.assertEqual(len(block.channel_indexes[-1].units), 4) + #self.assertEqual(len(block.channel_indexes[-1].units), + # len(block.segments[0].spiketrains)) anasig = block.segments[0].analogsignals[0] self.assertIsNotNone(anasig.file_origin) @@ -163,14 +162,13 @@ def test_inputs_V21(self): # test 4 Units block = reader.read_block(load_waveforms=True, - signal_group_mode='split-all', - units_group_mode='all-in-one') + signal_group_mode='split-all', + )#units_group_mode='all-in-one') self.assertEqual(len(block.segments[0].analogsignals), 96) - self.assertEqual(len(block.channel_indexes[-1].units), 218) - self.assertEqual(len(block.channel_indexes[-1].units), - len(block.segments[0].spiketrains)) - + #self.assertEqual(len(block.channel_indexes[-1].units), 218) + #self.assertEqual(len(block.channel_indexes[-1].units), + # len(block.segments[0].spiketrains)) anasig = block.segments[0].analogsignals[0] self.assertIsNotNone(anasig.file_origin) @@ -261,18 +259,18 @@ def test_compare_blackrockio_with_matlabloader_v21(self): verbose=False, **param[1]) block = session.read_block(load_waveforms=True, signal_group_mode='split-all') # Check if analog data are equal - self.assertGreater(len(block.channel_indexes), 0) - for i, chidx in enumerate(block.channel_indexes): + self.assertGreater(len(block.groups), 0) + for i, chidx_grp in enumerate(block.channel_indexes): # Break for ChannelIndexes for Units that don't contain any Analogsignals - if len(chidx.analogsignals) == 0 and len(chidx.units) >= 1: + if len(chidx_grp.analogsignals) == 0 and len(chidx_grp.spiketrains) >= 1: break - # Should only have one AnalogSignal per ChannelIndex - self.assertEqual(len(chidx.analogsignals), 1) + # Should only have one AnalogSignal per ChannelIndex-representing Group + self.assertEqual(len(chidx_grp.analogsignals), 1) # Find out channel_id in order to compare correctly - idx = chidx.analogsignals[0].annotations['channel_id'] + idx = chidx_grp.analogsignals[0].annotations['channel_id'] # Get data of AnalogSignal without pq.units - anasig = np.squeeze(chidx.analogsignals[0].base[:].magnitude) + anasig = np.squeeze(chidx_grp.analogsignals[0].base[:].magnitude) # Test for equality of first nonzero values of AnalogSignal # and matlab file contents # If not equal test if hardcoded gain is responsible for this @@ -280,7 +278,7 @@ def test_compare_blackrockio_with_matlabloader_v21(self): j = 0 while anasig[j] == 0: j += 1 - if lfp_ml[i, j] != np.squeeze(chidx.analogsignals[0].base[j].magnitude): + if lfp_ml[i, j] != np.squeeze(chidx_grp.analogsignals[0].base[j].magnitude): anasig = anasig / 152.592547 anasig = np.round(anasig).astype(int) diff --git a/neo/test/iotest/test_brainwaresrcio.py b/neo/test/iotest/test_brainwaresrcio.py index bff7f7883..806f0c052 100644 --- a/neo/test/iotest/test_brainwaresrcio.py +++ b/neo/test/iotest/test_brainwaresrcio.py @@ -315,7 +315,7 @@ def test_against_reference(self): try: assert_neo_object_is_compliant(obj) assert_neo_object_is_compliant(refobj) - assert_same_sub_schema(obj, refobj) + #assert_same_sub_schema(obj, refobj) # commented out until IO is adapted to use Group except BaseException as exc: exc.args += ('from ' + filename,) raise diff --git a/neo/test/iotest/test_exampleio.py b/neo/test/iotest/test_exampleio.py index 26f7d426e..a4b3c7aef 100644 --- a/neo/test/iotest/test_exampleio.py +++ b/neo/test/iotest/test_exampleio.py @@ -61,8 +61,8 @@ def test_read_segment_lazy(self): def test_read_block(self): r = ExampleIO(filename=None) bl = r.read_block(lazy=True) - assert len(bl.list_units) == 3 - assert len(bl.channel_indexes) == 1 + 3 # signals grouped + units + #assert len(bl.list_units) == 3 + #assert len(bl.channel_indexes) == 1 + 3 # signals grouped + units def test_read_segment_with_time_slice(self): r = ExampleIO(filename=None) diff --git a/neo/test/iotest/test_neuralynxio.py b/neo/test/iotest/test_neuralynxio.py index 6a9bcd1e5..4fb2dbb04 100644 --- a/neo/test/iotest/test_neuralynxio.py +++ b/neo/test/iotest/test_neuralynxio.py @@ -102,13 +102,13 @@ def test_read_block(self): block.segments[0].spiketrains[0].shape[0]) self.assertGreater(len(block.segments[0].events), 0) - self.assertEqual(len(block.channel_indexes[-1].units[0].spiketrains), 2) # 2 segment + # self.assertEqual(len(block.channel_indexes[-1].units[0].spiketrains), 2) # 2 segment - block = nio.read_block(load_waveforms=True, units_group_mode='all-in-one') - self.assertEqual(len(block.channel_indexes[-1].units), 2) # 2 units + # block = nio.read_block(load_waveforms=True, units_group_mode='all-in-one') + # self.assertEqual(len(block.channel_indexes[-1].units), 2) # 2 units - block = nio.read_block(load_waveforms=True, units_group_mode='split-all') - self.assertEqual(len(block.channel_indexes[-1].units), 1) # 1 units by ChannelIndex + # block = nio.read_block(load_waveforms=True, units_group_mode='split-all') + # self.assertEqual(len(block.channel_indexes[-1].units), 1) # 1 units by ChannelIndex def test_read_segment(self): dirname = self.get_filename_path('Cheetah_v5.5.1/original_data') @@ -159,13 +159,13 @@ def test_read_block(self): self.assertEqual(block.segments[0].spiketrains[0].waveforms.shape[-1], 32) self.assertGreater(len(block.segments[0].events), 0) - self.assertEqual(len(block.channel_indexes[-1].units[0].spiketrains), 2) + # self.assertEqual(len(block.channel_indexes[-1].units[0].spiketrains), 2) - block = nio.read_block(load_waveforms=True, units_group_mode='all-in-one') - self.assertEqual(len(block.channel_indexes[-1].units), 8) + # block = nio.read_block(load_waveforms=True, units_group_mode='all-in-one') + # self.assertEqual(len(block.channel_indexes[-1].units), 8) - block = nio.read_block(load_waveforms=True, units_group_mode='split-all') - self.assertEqual(len(block.channel_indexes[-1].units), 1) # 1 units by ChannelIndex + # block = nio.read_block(load_waveforms=True, units_group_mode='split-all') + # self.assertEqual(len(block.channel_indexes[-1].units), 1) # 1 units by ChannelIndex def test_read_segment(self): dirname = self.get_filename_path('Cheetah_v5.5.1/original_data') @@ -211,10 +211,10 @@ def test_read_block(self): self.assertGreater(len(block.segments[0].events), 0) block = nio.read_block(signal_group_mode='split-all') - self.assertEqual(len(block.channel_indexes), 5) + self.assertEqual(len(block.groups), 5) block = nio.read_block(signal_group_mode='group-by-same-units') - self.assertEqual(len(block.channel_indexes), 1) + self.assertEqual(len(block.groups), 1) class TestPegasus_v211(CommonNeuralynxIOTest, unittest.TestCase): @@ -246,26 +246,26 @@ def test_read_block(self): class TestData(CommonNeuralynxIOTest, unittest.TestCase): - def test_ncs(self): - for session in self.files_to_test[1:2]: # in the long run this should include all files - dirname = self.get_filename_path(session) - nio = NeuralynxIO(dirname=dirname, use_cache=False) - block = nio.read_block() - - for anasig_id, anasig in enumerate(block.segments[0].analogsignals): - chid = anasig.channel_index.channel_ids[anasig_id] - - # need to decode, unless keyerror - chname = anasig.channel_index.channel_names[anasig_id] - chuid = (chname, chid) - filename = nio.ncs_filenames[chuid][:-3] + 'txt' - filename = filename.replace('original_data', 'plain_data') - plain_data = np.loadtxt(filename)[:, 5:].flatten() # first columns are meta info - overlap = 512 * 500 - gain_factor_0 = plain_data[0] / anasig.magnitude[0, 0] - np.testing.assert_allclose(plain_data[:overlap], - anasig.magnitude[:overlap, 0] * gain_factor_0, - rtol=0.01) + # def test_ncs(self): + # for session in self.files_to_test[1:2]: # in the long run this should include all files + # dirname = self.get_filename_path(session) + # nio = NeuralynxIO(dirname=dirname, use_cache=False) + # block = nio.read_block() + + # for anasig_id, anasig in enumerate(block.segments[0].analogsignals): + # chid = anasig.channel_index.channel_ids[anasig_id] + + # # need to decode, unless keyerror + # chname = anasig.channel_index.channel_names[anasig_id] + # chuid = (chname, chid) + # filename = nio.ncs_filenames[chuid][:-3] + 'txt' + # filename = filename.replace('original_data', 'plain_data') + # plain_data = np.loadtxt(filename)[:, 5:].flatten() # first columns are meta info + # overlap = 512 * 500 + # gain_factor_0 = plain_data[0] / anasig.magnitude[0, 0] + # np.testing.assert_allclose(plain_data[:overlap], + # anasig.magnitude[:overlap, 0] * gain_factor_0, + # rtol=0.01) def test_keep_original_spike_times(self): for session in self.files_to_test: @@ -302,7 +302,7 @@ def test_incomplete_block_handling_v632(self): n_gaps = 2 # so 3 segments, 3 anasigs by Channelindex self.assertEqual(len(block.segments), n_gaps + 1) - self.assertEqual(len(block.channel_indexes[0].analogsignals), n_gaps + 1) + # self.assertEqual(len(block.channel_indexes[0].analogsignals), n_gaps + 1) for t, gt in zip(nio._sigs_t_start, [8408.806811, 8427.832053, 8487.768561]): self.assertEqual(np.round(t, 4), np.round(gt, 4)) @@ -321,8 +321,8 @@ def test_gap_handling_v551(self): n_gaps = 1 # so 2 segments, 2 anasigs by Channelindex, 2 SpikeTrain by Units self.assertEqual(len(block.segments), n_gaps + 1) - self.assertEqual(len(block.channel_indexes[0].analogsignals), n_gaps + 1) - self.assertEqual(len(block.channel_indexes[-1].units[0].spiketrains), n_gaps + 1) + # self.assertEqual(len(block.channel_indexes[0].analogsignals), n_gaps + 1) + # self.assertEqual(len(block.channel_indexes[-1].units[0].spiketrains), n_gaps + 1) def test_gap_handling_v563(self): dirname = self.get_filename_path('Cheetah_v5.6.3/original_data') @@ -333,8 +333,8 @@ def test_gap_handling_v563(self): n_gaps = 1 # so 2 segments, 2 anasigs by Channelindex, 2 SpikeTrain by Units self.assertEqual(len(block.segments), n_gaps + 1) - self.assertEqual(len(block.channel_indexes[0].analogsignals), n_gaps + 1) - self.assertEqual(len(block.channel_indexes[-1].units[0].spiketrains), n_gaps + 1) + # self.assertEqual(len(block.channel_indexes[0].analogsignals), n_gaps + 1) + # self.assertEqual(len(block.channel_indexes[-1].units[0].spiketrains), n_gaps + 1) def compare_old_and_new_neuralynxio(): From 1ad87a85ebe4c0cc1672125bcab2bc1561797e59 Mon Sep 17 00:00:00 2001 From: Andrew Davison Date: Fri, 6 Nov 2020 13:40:06 +0100 Subject: [PATCH 62/85] remove part of test_converter.py which is failing, and seems incorrect --- neo/test/test_converter.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/neo/test/test_converter.py b/neo/test/test_converter.py index d2db2c5bd..6b36d46fe 100644 --- a/neo/test/test_converter.py +++ b/neo/test/test_converter.py @@ -88,7 +88,3 @@ def test_block_conversion(self): group_names = np.asarray([g.name for g in group.groups]) for unit in channel_index.units: self.assertIn(unit.name, group_names) - - matching_groups = np.asarray(group.groups)[group_names == unit.name] - self.assertEqual(len(channel_index.units), len(matching_groups)) - From 6ed65a219bca18638726ba78ca55b8d4eb8ce6ae Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Fri, 6 Nov 2020 13:46:36 -0700 Subject: [PATCH 63/85] Language and function renaming. --- neo/rawio/neuralynxrawio.py | 8 ++++---- neo/test/rawiotest/test_neuralynxrawio.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index eb86a960f..818a4c270 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -108,7 +108,7 @@ def _parse_header(self): continue # All file have more or less the same header structure - info = NlxHeader.buildForFile(filename) + info = NlxHeader.build_for_file(filename) chan_names = info['channel_names'] chan_ids = info['channel_ids'] @@ -227,7 +227,7 @@ def _parse_header(self): self._timestamp_limits = None self._nb_segment = 1 - # Read ncs files for gaps detection and nb_segment computation. + # Read ncs files for gap detection and nb_segment computation. # :TODO: current algorithm depends on side-effect of read_ncs_files on # self._sigs_memmap, self._sigs_t_start, self._sigs_t_stop, # self._sigs_length, self._nb_segment, self._timestamp_limits @@ -695,7 +695,7 @@ def _to_bool(txt): datetimeformat='%Y/%m/%d %H:%M:%S') } - def buildForFile(filename): + def build_for_file(filename): """ Factory function to build NlxHeader for a given file. """ @@ -810,7 +810,7 @@ def buildForFile(filename): return info - def typeOfRecording(self): + def type_of_recording(self): """ Determines type of recording in Ncs file with this header. diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index dce7357ac..4e0bc40cb 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -82,8 +82,8 @@ def test_recording_types(self): for typeTest in self.ncsTypeTestFiles: filename = self.get_filename_path(typeTest[0]) - hdr = NlxHeader.buildForFile(filename) - self.assertEqual(hdr.typeOfRecording(), typeTest[1]) + hdr = NlxHeader.build_for_file(filename) + self.assertEqual(hdr.type_of_recording(), typeTest[1]) if __name__ == "__main__": From 3907d870d2211f705d04010e2f323160b52feac2 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Fri, 9 Oct 2020 11:15:52 -0700 Subject: [PATCH 64/85] Move constants into class so accessible for testing. --- neo/rawio/neuralynxrawio.py | 63 ++++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 818a4c270..bb88b2c72 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -29,7 +29,6 @@ import datetime from collections import OrderedDict -BLOCK_SIZE = 512 # nb sample per signal block class NeuralynxRawIO(BaseRawIO): @@ -51,6 +50,10 @@ class NeuralynxRawIO(BaseRawIO): extensions = ['nse', 'ncs', 'nev', 'ntt'] rawmode = 'one-dir' + _BLOCK_SIZE = 512 # nb sample per signal block + _ncs_dtype = [('timestamp', 'uint64'), ('channel_id', 'uint32'), ('sample_rate', 'uint32'), + ('nb_valid', 'uint32'), ('samples', 'int16', (_BLOCK_SIZE,))] + def __init__(self, dirname='', keep_original_times=False, **kargs): """ Parameters @@ -348,8 +351,8 @@ def _get_analogsignal_chunk(self, block_index, seg_index, i_start, i_stop, chann if i_stop is None: i_stop = self._sigs_length[seg_index] - block_start = i_start // BLOCK_SIZE - block_stop = i_stop // BLOCK_SIZE + 1 + block_start = i_start // self._BLOCK_SIZE + block_stop = i_stop // self._BLOCK_SIZE + 1 sl0 = i_start % 512 sl1 = sl0 + (i_stop - i_start) @@ -492,11 +495,11 @@ def read_ncs_files(self, ncs_filenames): if len(ncs_filenames) == 0: return None - good_delta = int(BLOCK_SIZE * 1e6 / self._sigs_sampling_rate) + good_delta = int(self._BLOCK_SIZE * 1e6 / self._sigs_sampling_rate) chan_uid0 = list(ncs_filenames.keys())[0] filename0 = ncs_filenames[chan_uid0] - data0 = np.memmap(filename0, dtype=ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) + data0 = np.memmap(filename0, dtype=self.ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) gap_indexes = None lost_indexes = None @@ -535,7 +538,7 @@ def read_ncs_files(self, ncs_filenames): # is not strictly necessary as all channels might have same partially filled # blocks at the end. - lost_indexes, = np.nonzero(data0['nb_valid'] < BLOCK_SIZE) + lost_indexes, = np.nonzero(data0['nb_valid'] < self._BLOCK_SIZE) if self.use_cache: self.add_in_cache(lost_indexes=lost_indexes) @@ -562,7 +565,8 @@ def read_ncs_files(self, ncs_filenames): # create segment with subdata block/t_start/t_stop/length for chan_uid, ncs_filename in self.ncs_filenames.items(): - data = np.memmap(ncs_filename, dtype=ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) + data = np.memmap(ncs_filename, dtype=self.ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) assert data.size == data0.size, 'ncs files do not have the same data length' for seg_index, (i0, i1) in enumerate(gap_pairs): @@ -578,21 +582,20 @@ def read_ncs_files(self, ncs_filenames): if chan_uid == chan_uid0: ts0 = subdata[0]['timestamp'] ts1 = subdata[-1]['timestamp'] +\ - np.uint64(BLOCK_SIZE / self._sigs_sampling_rate * 1e6) + np.uint64(self._BLOCK_SIZE / self._sigs_sampling_rate * 1e6) self._timestamp_limits.append((ts0, ts1)) t_start = ts0 / 1e6 self._sigs_t_start.append(t_start) t_stop = ts1 / 1e6 self._sigs_t_stop.append(t_stop) - length = subdata.size * BLOCK_SIZE + length = subdata.size * self._BLOCK_SIZE self._sigs_length.append(length) class NcsBlocks(): """ Contains information regarding the blocks of records in an Ncs file. - Factory methods perform parsing of this information from an Ncs file or - confirmation that file agrees with block structure. + Factory methods perform parsing of this information from an Ncs file. """ startBlocks = [] @@ -601,6 +604,41 @@ class NcsBlocks(): microsPerSampUsed = 0 +class NcsBlocksFactory(): + """ + Class for factory methods which perform parsing of blocks in Ncs files. + + Moved here since algorithm covering all 3 header styles and types used is + more complicated. Copied from Java code on Sept 7, 2020. + """ + + _tolerance = 0.001 # tolerance for drift of timestamps within one NcsBlock + _maxGapLength = 5 # maximum gap between predicted and actual block timestamps + # still considered within one NcsBlock + + def _parseGivenActualFrequency(sampsMemMap,ncsBlocks,chanNum,reqFreq,blkOnePredTime): + """ + Parse blocks in file when microsPerSampUsed and sampFreqUsed are known, + filling in an NcsBlocks object. + + PARAMETERS + sampsMemMap: + memmap of Ncs file + ncsBlocks: + result with microsPerSamp and sampFreqUsed set correctly + chanNum: + channel number that should be present in all records + reqFreq: + rounded frequency that all records should contain + blkOnePredTime: + predicted starting time of first block + + RETURN + NcsBlocks object with block locations marked + """ + + + class NlxHeader(OrderedDict): """ Representation of basic information in all 16 kbytes Neuralynx file headers, @@ -860,9 +898,6 @@ class NcsHeader(): """ -ncs_dtype = [('timestamp', 'uint64'), ('channel_id', 'uint32'), ('sample_rate', 'uint32'), - ('nb_valid', 'uint32'), ('samples', 'int16', (BLOCK_SIZE,))] - nev_dtype = [ ('reserved', ' Date: Mon, 12 Oct 2020 11:52:31 -0700 Subject: [PATCH 65/85] Test of building NcsBlocks. --- neo/test/rawiotest/test_neuralynxrawio.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 4e0bc40cb..6547c060a 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -1,8 +1,10 @@ import unittest +import numpy as np + from neo.rawio.neuralynxrawio import NeuralynxRawIO -from neo.test.rawiotest.common_rawio_test import BaseTestRawIO from neo.rawio.neuralynxrawio import NlxHeader +from neo.test.rawiotest.common_rawio_test import BaseTestRawIO import logging @@ -85,6 +87,17 @@ def test_recording_types(self): hdr = NlxHeader.build_for_file(filename) self.assertEqual(hdr.type_of_recording(), typeTest[1]) +class TestNcsBlocksFactory(TestNeuralynxRawIO, unittest.TestCase): + """ + Test building NcsBlocks for files of different revisions. + """ + + def test_ncsblocks_partial(self): + filename = self.get_filename_path('Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs') + data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) + self.assertEqual(data0.shape[0],6690) + self.assertEqual(data0['timestamp'][6689],8515800549) # timestamp of last record if __name__ == "__main__": unittest.main() From 6ebcd753385adda72353054408985ed6fd9c86c7 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Tue, 13 Oct 2020 11:52:40 -0700 Subject: [PATCH 66/85] Handle old files with truncated frequency in header and test. --- neo/rawio/neuralynxrawio.py | 146 ++++++++++++++++++++-- neo/test/rawiotest/test_neuralynxrawio.py | 18 +++ 2 files changed, 156 insertions(+), 8 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index bb88b2c72..50f9f4b7a 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -592,21 +592,71 @@ def read_ncs_files(self, ncs_filenames): self._sigs_length.append(length) +class WholeMicrosTimePositionBlock(): + """ + Map of time to sample positions. + + Times are rounded to nearest microsecond. Model here is that times + from start of a sample until just before the next sample are included, + that is, closed lower bound and open upper bound on intervals. A + channel with no samples is empty and contains no time intervals. + """ + + _sampFrequency = 0 + _startTime = 0 + _size = 0 + _microsPerSamp = 0 + + @staticmethod + def getMicrosPerSampForFreq(sampFreq): + """ + Compute fractional microseconds per sample. + """ + return 1e6 / sampFreq + + @staticmethod + def calcSampleTime(sampFr, startTime, posn): + """ + Calculate time rounded to microseconds for sample given frequency, + start time, and sample position. + """ + return round(startTime+ + WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(sampFr)*posn) + +class CscRecordHeader(): + """ + Information in header of each Ncs record, excluding sample values themselves. + """ + timestamp = 0 + channel_id = 0 + sample_rate = 0 + nb_valid = 0 + + def __init__(self,ncsMemMap,recn): + """ + Construct a record header for a given record in a memory map for an NcsFile. + """ + self.timestamp = ncsMemMap['timestamp'][recn] + self.channel_id = ncsMemMap['channel_id'][recn] + self.sample_rate = ncsMemMap['sample_rate'][recn] + self.nb_valid = ncsMemMap['nb_valid'][recn] + class NcsBlocks(): """ - Contains information regarding the blocks of records in an Ncs file. + Contains information regarding the contiguous blocks of records in an Ncs file. Factory methods perform parsing of this information from an Ncs file. """ startBlocks = [] endBlocks = [] - sampFreqUsed = 0 - microsPerSampUsed = 0 + sampFreqUsed = 0 # actual sampling frequency of samples + microsPerSampUsed = 0 # microseconds per sample class NcsBlocksFactory(): """ - Class for factory methods which perform parsing of blocks in Ncs files. + Class for factory methods which perform parsing of contiguous blocks of records + in Ncs files. Moved here since algorithm covering all 3 header styles and types used is more complicated. Copied from Java code on Sept 7, 2020. @@ -616,13 +666,13 @@ class NcsBlocksFactory(): _maxGapLength = 5 # maximum gap between predicted and actual block timestamps # still considered within one NcsBlock - def _parseGivenActualFrequency(sampsMemMap,ncsBlocks,chanNum,reqFreq,blkOnePredTime): + def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime): """ - Parse blocks in file when microsPerSampUsed and sampFreqUsed are known, + Parse blocks in memory mapped file when microsPerSampUsed and sampFreqUsed are known, filling in an NcsBlocks object. PARAMETERS - sampsMemMap: + ncsMemMap: memmap of Ncs file ncsBlocks: result with microsPerSamp and sampFreqUsed set correctly @@ -636,8 +686,88 @@ def _parseGivenActualFrequency(sampsMemMap,ncsBlocks,chanNum,reqFreq,blkOnePredT RETURN NcsBlocks object with block locations marked """ - + startBlockPredTime = blkOnePredTime + blkLen = 0 + for recn in range(1, ncsMemMap.shape[0]): + hdr = CscRecordHeader(ncsMemMap, recn) + if hdr.channel_id!=chanNum | hdr.sample_rate!=reqFreq: + raise IOError('Channel number or sampling frequency changed in records within file') + predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, + startBlockPredTime, blkLen) + nValidSamps = hdr.nb_valid + if hdr.timestamp != predTime: + ncsBlocks.endBlocks.append(recn-1) + ncsBlocks.startBlocks.append(recn) + startBlockPredTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, + hdr.timestamp, + nValidSamps) + blklen = 0 + else: + blkLen += nValidSamps + ncsBlocks.endBlocks.append(ncsMemMap.shape[0] - 1) + + return ncsBlocks + + + def _buildGivenActualFrequency(ncsMemMap, ncsBlocks, reqFreq): + """ + Build NcsBlocks object for file given actual sampling frequency. + + Requires that frequency in each record agrees with requested frequency. This is + normally obtained by rounding the header frequency; however, this value may be different + from the rounded actual frequency used in the recording, since the underlying + requirement in older Ncs files was that the rounded number of whole microseconds + per sample be the same for all records in a block. + + PARAMETERS + ncsMemMap: + memmap of Ncs file + ncsBlocks: + containing the actual sampling frequency used and microsPerSamp for the result + reqFreq: + frequency to require in records + RETURN: + NcsBlocks object + """ + # check frequency in first record + rh0 = CscRecordHeader(ncsMemMap, 0) + if rh0.sample_rate != reqFreq: + raise IOError("Sampling frequency in first record doesn't agree with header.") + chanNum = rh0.channel_id + + # check if file is one block of records, which is often the case, and avoid full parse + lastBlkI = ncsMemMap.shape[0] - 1 + rhl = CscRecordHeader(ncsMemMap, lastBlkI) + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, rh0.timestamp, + NeuralynxRawIO._BLOCK_SIZE * lastBlkI) + if rhl.channel_id == chanNum and rhl.sample_rate == reqFreq and rhl.timestamp == predLastBlockStartTime: + ncsBlocks.startBlocks.append(0) + ncsBlocks.endBlocks.append(lastBlkI) + return ncsBlocks + + # otherwise need to scan looking for breaks + else: + blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, rh0.timestamp, + rh0.nb_valid) + return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime) + + + def _parseForMaxGap(ncsMemMap, hdr, nomFreq, maxGapLen): + """ + Parse blocks of records from file, allowing a maximum gap in timestamps between records + in blocks. Estimates frequency being used based on timestamps. + + PARAMETERS + ncsMemMap: + memmap of Ncs file + hdr: + CSC record headr information + nomFreq: + nominal frequency to use in computing time for samples (Hz) + maxGapLen: + maximum difference within a block between predicted time of start of record and recorded time + """ class NlxHeader(OrderedDict): """ diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 6547c060a..c38a10eea 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -4,6 +4,8 @@ from neo.rawio.neuralynxrawio import NeuralynxRawIO from neo.rawio.neuralynxrawio import NlxHeader +from neo.rawio.neuralynxrawio import NcsBlocksFactory +from neo.rawio.neuralynxrawio import NcsBlocks from neo.test.rawiotest.common_rawio_test import BaseTestRawIO import logging @@ -99,5 +101,21 @@ def test_ncsblocks_partial(self): self.assertEqual(data0.shape[0],6690) self.assertEqual(data0['timestamp'][6689],8515800549) # timestamp of last record + def testBuildGivenActualFrequency(self): + + # Test early files where the frequency listed in the header is + # floor(1e6/(actual number of microseconds between samples) + filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') + data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) + ncsBlocks = NcsBlocks() + ncsBlocks.sampFreqUsed = 1/(35e-6) + ncsBlocks.microsPerSampUsed = 35 + ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks, 27789) + self.assertEqual(len(ncsBlocks.startBlocks), 1) + self.assertEqual(ncsBlocks.startBlocks[0], 0) + self.assertEqual(len(ncsBlocks.endBlocks), 1) + self.assertEqual(ncsBlocks.endBlocks[0], 9) + if __name__ == "__main__": unittest.main() From 1c5cd2ceb16a89f991be3c6995c4e881dc5f0a0c Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Sat, 7 Nov 2020 15:44:10 -0700 Subject: [PATCH 67/85] Change interface on parse versus build. # Conflicts: # neo/rawio/neuralynxrawio.py --- neo/rawio/neuralynxrawio.py | 161 ++++++++++++++++++++-- neo/test/rawiotest/test_neuralynxrawio.py | 2 +- 2 files changed, 147 insertions(+), 16 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 50f9f4b7a..530f42c05 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -28,6 +28,7 @@ import distutils.version import datetime from collections import OrderedDict +import math @@ -499,7 +500,7 @@ def read_ncs_files(self, ncs_filenames): chan_uid0 = list(ncs_filenames.keys())[0] filename0 = ncs_filenames[chan_uid0] - data0 = np.memmap(filename0, dtype=self.ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) + data0 = np.memmap(filename0, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) gap_indexes = None lost_indexes = None @@ -565,7 +566,7 @@ def read_ncs_files(self, ncs_filenames): # create segment with subdata block/t_start/t_stop/length for chan_uid, ncs_filename in self.ncs_filenames.items(): - data = np.memmap(ncs_filename, dtype=self.ncs_dtype, mode='r', + data = np.memmap(ncs_filename, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) assert data.size == data0.size, 'ncs files do not have the same data length' @@ -666,6 +667,7 @@ class NcsBlocksFactory(): _maxGapLength = 5 # maximum gap between predicted and actual block timestamps # still considered within one NcsBlock + @staticmethod def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime): """ Parse blocks in memory mapped file when microsPerSampUsed and sampFreqUsed are known, @@ -675,7 +677,7 @@ def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePre ncsMemMap: memmap of Ncs file ncsBlocks: - result with microsPerSamp and sampFreqUsed set correctly + NcsBlocks with actual sampFreqUsed correct chanNum: channel number that should be present in all records reqFreq: @@ -709,7 +711,8 @@ def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePre return ncsBlocks - def _buildGivenActualFrequency(ncsMemMap, ncsBlocks, reqFreq): + @staticmethod + def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): """ Build NcsBlocks object for file given actual sampling frequency. @@ -736,24 +739,30 @@ def _buildGivenActualFrequency(ncsMemMap, ncsBlocks, reqFreq): raise IOError("Sampling frequency in first record doesn't agree with header.") chanNum = rh0.channel_id + nb = NcsBlocks() + nb.sampFreqUsed = actualSampFreq + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(actualSampFreq) + # check if file is one block of records, which is often the case, and avoid full parse lastBlkI = ncsMemMap.shape[0] - 1 rhl = CscRecordHeader(ncsMemMap, lastBlkI) - predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, rh0.timestamp, + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, NeuralynxRawIO._BLOCK_SIZE * lastBlkI) if rhl.channel_id == chanNum and rhl.sample_rate == reqFreq and rhl.timestamp == predLastBlockStartTime: - ncsBlocks.startBlocks.append(0) - ncsBlocks.endBlocks.append(lastBlkI) - return ncsBlocks + nb = NcsBlocks() + nb.startBlocks.append(0) + nb.endBlocks.append(lastBlkI) + return nb # otherwise need to scan looking for breaks else: - blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, rh0.timestamp, + blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, rh0.nb_valid) - return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime) + return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, nb, chanNum, reqFreq, blkOnePredTime) - def _parseForMaxGap(ncsMemMap, hdr, nomFreq, maxGapLen): + @staticmethod + def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): """ Parse blocks of records from file, allowing a maximum gap in timestamps between records in blocks. Estimates frequency being used based on timestamps. @@ -761,13 +770,135 @@ def _parseForMaxGap(ncsMemMap, hdr, nomFreq, maxGapLen): PARAMETERS ncsMemMap: memmap of Ncs file - hdr: - CSC record headr information - nomFreq: - nominal frequency to use in computing time for samples (Hz) + ncsBlocks: + NcsBlocks object with sampFreqUsed set to nominal frequency to use in computing time for samples (Hz) maxGapLen: maximum difference within a block between predicted time of start of record and recorded time + + RETURN: + NcsBlocks object with sampFreqUsed and microsPerSamp set based on estimate from largest block + """ + + # track frequency of each block and use estimate with longest block + maxBlkLen = 0 + maxBlkFreqEstimate = 0 + + # Parse the record sequence, finding blocks of continuous time with no more than maxGapLength + # and same channel number + rh0 = CscRecordHeader(ncsMemMap, 0) + chanNum = rh0.channel_id + + startBlockTime = rh0.timestamp + blkLen = rh0.nb_valid + lastRecTime = rh0.timestamp + lastRecNumSamps = rh0.nb_valid + recFreq = rh0.sample_rate + + ncsBlocks.startBlocks.append(0) + for recn in range(1, ncsMemMap.shape[0]): + hdr = CscRecordHeader(ncsMemMap, recn) + if hdr.channel_id != chanNum | hdr.sample_rate != recFreq: + raise IOError('Channel number or sampling frequency changed in records within file') + predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, lastRecTime, + lastRecNumSamps) + if (abs(hdr.timestamp - predTime) > maxGapLen): + ncsBlocks.endBlocks.append(recn-1) + ncsBlocks.startBlocks.append(recn) + if blkLen > maxBlkLen: + maxBlkLen = blkLen + maxBlkFreqEstimate = (blkLen - lastRecNumSamps) * 1e6 / (lastRecTime - startBlockTime) + startBlockTime = hdr.timestamp + blkLen = hdr.nb_valid + else: + blkLen += hdr.nb_valid + ncsBlocks.append(ncsMemMap.shape[0] - 1) + + ncsBlocks.sampFreqUsed = maxBlkFreqEstimate + ncsBlocks.setMicrosPerSamp = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(maxBlkFreqEstimate) + + return ncsBlocks + + + @staticmethod + def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): + """ + Determine blocks of records in memory mapped Ncs file given a nominal frequency of the file, + using the default values of frequency tolerance and maximum gap between blocks. + + PARAMETERS + ncsMemMap: + memmap of Ncs file + nomFreq: + nominal sampling frequency used, normally from header of file + + RETURN: + NcsBlocks object + """ + nb = NcsBlocks() + + numRecs = ncsMemMap.shape[0] + if numRecs < 1: + return nb + + rh0 = CscRecordHeader(ncsMemMap, 0) + chanNum = rh0.channel_id + + lastBlkI = numRecs - 1 + rhl = CscRecordHeader(ncsMemMap,lastBlkI) + + # check if file is one block of records, to within tolerance, which is often the case + numSampsForPred = NeuralynxRawIO._BLOCK_SIZE * lastBlkI + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq,rh0.timestamp, + numSampsForPred) + freqInFile = math.floor(nomFreq) + if abs(rhl.timestamp - predLastBlockStartTime) / rhl.timestamp < NcsBlocks._tolerance and \ + rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: + nb.endBlocks.append(lastBlkI) + nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) / 1e6 + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) + + # otherwise parse records to determine blocks using default maximum gap length + else: + nb.sampFreqUsed = nomFreq + nb = NcsBlocks._parseForMaxGap(ncsMemMap, nb, NcsBlocks._maxGapLength) + + return nb + + @staticmethod + def buildForNcsFile(ncsMemMap,nlxHdr): """ + Build an NcsBlocks object for an NcsFile, given as a memmap and NlxHeader, + handling gap detection appropriately given the file type as specified by the header. + + PARAMETERS + ncsMemMap: + memory map of file + acqType: + string specifying type of data acquisition used, one of types returned by NlxHeader.typeOfRecording() + """ + acqType = nlxHdr.typeOfRecording() + + # old Neuralynx style with rounded whole microseconds for the samples + if acqType == "PRE4": + freq = nlxHdr['SamplingFrequency'] + microsPerSampUsed = math.floor( + WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(freq)) + sampFreqUsed = WholeMicrosTimePositionBlock.getFreqForMicrosPerSamp(microsPerSampUsed) + nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) + nb.microsPerSampUsed = microsPerSampUsed + + # digital lynx style with fractional frequency and micros per samp determined from block times + elif acqType == "DIGITALLYNX" or acqType == "DIGITALLYNXSX": + nomFreq = nlxHdr['SamplingFrequency'] + nb = NcsBlocks._buildForToleranceAndMaxGap(ncsMemMap, nomFreq) + + # BML style with fractional frequency and micros per samp + elif acqType == "BML": + sampFreqUsed = nlxHdr['SamplingFrequency'] + nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(sampFreqUsed)) + + else: + raise TypeError("Unknown Ncs file type from header.") class NlxHeader(OrderedDict): """ diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index c38a10eea..58af0206f 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -111,7 +111,7 @@ def testBuildGivenActualFrequency(self): ncsBlocks = NcsBlocks() ncsBlocks.sampFreqUsed = 1/(35e-6) ncsBlocks.microsPerSampUsed = 35 - ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks, 27789) + ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks.sampFreqUsed, 27789) self.assertEqual(len(ncsBlocks.startBlocks), 1) self.assertEqual(ncsBlocks.startBlocks[0], 0) self.assertEqual(len(ncsBlocks.endBlocks), 1) From e307614ae7d6c2d2152e621b87234a979bf2166e Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Sat, 7 Nov 2020 15:45:48 -0700 Subject: [PATCH 68/85] Tests for PRE4 type and code corrections. Tests for v5.5.1 still failing. # Conflicts: # neo/rawio/neuralynxrawio.py --- neo/rawio/neuralynxrawio.py | 39 +++++++++++++++++------ neo/test/rawiotest/test_neuralynxrawio.py | 29 +++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 530f42c05..61ab8a6e2 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -609,11 +609,18 @@ class WholeMicrosTimePositionBlock(): _microsPerSamp = 0 @staticmethod - def getMicrosPerSampForFreq(sampFreq): + def getFreqForMicrosPerSamp(micros): """ - Compute fractional microseconds per sample. + Compute fractional sampling frequency, given microseconds per sample. """ - return 1e6 / sampFreq + return 1e6 / micros + + @staticmethod + def getMicrosPerSampForFreq(sampFr): + """ + Calculate fractional microseconds per sample, given the sampling frequency (Hz). + """ + return 1e6 / sampFr @staticmethod def calcSampleTime(sampFr, startTime, posn): @@ -811,7 +818,7 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): blkLen = hdr.nb_valid else: blkLen += hdr.nb_valid - ncsBlocks.append(ncsMemMap.shape[0] - 1) + ncsBlocks.endBlocks.append(ncsMemMap.shape[0] - 1) ncsBlocks.sampFreqUsed = maxBlkFreqEstimate ncsBlocks.setMicrosPerSamp = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(maxBlkFreqEstimate) @@ -851,7 +858,7 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq,rh0.timestamp, numSampsForPred) freqInFile = math.floor(nomFreq) - if abs(rhl.timestamp - predLastBlockStartTime) / rhl.timestamp < NcsBlocks._tolerance and \ + if abs(rhl.timestamp - predLastBlockStartTime) / rhl.timestamp < NcsBlocksFactory._tolerance and \ rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: nb.endBlocks.append(lastBlkI) nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) / 1e6 @@ -860,7 +867,8 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): # otherwise parse records to determine blocks using default maximum gap length else: nb.sampFreqUsed = nomFreq - nb = NcsBlocks._parseForMaxGap(ncsMemMap, nb, NcsBlocks._maxGapLength) + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) + nb = NcsBlocksFactory._parseForMaxGap(ncsMemMap, nb, NcsBlocksFactory._maxGapLength) return nb @@ -880,26 +888,37 @@ def buildForNcsFile(ncsMemMap,nlxHdr): # old Neuralynx style with rounded whole microseconds for the samples if acqType == "PRE4": +<<<<<<< HEAD freq = nlxHdr['SamplingFrequency'] microsPerSampUsed = math.floor( WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(freq)) sampFreqUsed = WholeMicrosTimePositionBlock.getFreqForMicrosPerSamp(microsPerSampUsed) nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) +======= + freq = nlxHdr['sampling_rate'] + microsPerSampUsed = math.floor(WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(freq)) + sampFreqUsed = WholeMicrosTimePositionBlock.getFreqForMicrosPerSamp(microsPerSampUsed) + nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) + nb.sampFreqUsed = sampFreqUsed +>>>>>>> 60006871... Tests for PRE4 type and code corrections. nb.microsPerSampUsed = microsPerSampUsed # digital lynx style with fractional frequency and micros per samp determined from block times elif acqType == "DIGITALLYNX" or acqType == "DIGITALLYNXSX": - nomFreq = nlxHdr['SamplingFrequency'] - nb = NcsBlocks._buildForToleranceAndMaxGap(ncsMemMap, nomFreq) + nomFreq = nlxHdr['sampling_rate'] + nb = NcsBlocksFactory._buildForToleranceAndMaxGap(ncsMemMap, nomFreq) # BML style with fractional frequency and micros per samp elif acqType == "BML": - sampFreqUsed = nlxHdr['SamplingFrequency'] - nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(sampFreqUsed)) + sampFreqUsed = nlxHdr['sampling_rate'] + nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(sampFreqUsed)) else: raise TypeError("Unknown Ncs file type from header.") + return nb + + class NlxHeader(OrderedDict): """ Representation of basic information in all 16 kbytes Neuralynx file headers, diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 58af0206f..73309bcbe 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -117,5 +117,34 @@ def testBuildGivenActualFrequency(self): self.assertEqual(len(ncsBlocks.endBlocks), 1) self.assertEqual(ncsBlocks.endBlocks[0], 9) + + def testBuildUsingHeaderAndScanning(self): + + # Test early files where the frequency listed in the header is + # floor(1e6/(actual number of microseconds between samples) + filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') + hdr = NlxHeader.buildForFile(filename) + data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) + nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) + + self.assertEqual(nb.sampFreqUsed, 1/35e-6) + self.assertEqual(nb.microsPerSampUsed, 35) + self.assertEqual(len(nb.startBlocks), 1) + self.assertEqual(nb.startBlocks[0], 0) + self.assertEqual(len(nb.endBlocks), 1) + self.assertEqual(nb.endBlocks[0], 9) + + # test Cheetah 5.5.1, which is DigitalLynxSX + filename = self.get_filename_path('Cheetah_v5.5.1/original_data/Tet3a.ncs') + hdr = NlxHeader.buildForFile(filename) + data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) + nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) + self.assertEqual(nb.sampFreqUsed, 32000) + self.assertEqual(nb.microsPerSampUsed, 31.25) + + + if __name__ == "__main__": unittest.main() From c5fac535c81e11f861b2988b476a2b66be7d9edc Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Mon, 26 Oct 2020 15:35:43 -0700 Subject: [PATCH 69/85] Fix initializer, update loop vars. --- neo/rawio/neuralynxrawio.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 61ab8a6e2..d2fb20462 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -652,13 +652,14 @@ def __init__(self,ncsMemMap,recn): class NcsBlocks(): """ Contains information regarding the contiguous blocks of records in an Ncs file. - Factory methods perform parsing of this information from an Ncs file. + Methods of NcsBlocksFactory perform parsing of this information from an Ncs file. """ - startBlocks = [] - endBlocks = [] - sampFreqUsed = 0 # actual sampling frequency of samples - microsPerSampUsed = 0 # microseconds per sample + def __init__(self): + self.startBlocks = [] + self.endBlocks = [] + self.sampFreqUsed = 0 # actual sampling frequency of samples + self.microsPerSampUsed = 0 # microseconds per sample class NcsBlocksFactory(): @@ -818,6 +819,9 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): blkLen = hdr.nb_valid else: blkLen += hdr.nb_valid + lastRecTime = hdr.timestamp + lastRecNumSamps = hdr.nb_valid + ncsBlocks.endBlocks.append(ncsMemMap.shape[0] - 1) ncsBlocks.sampFreqUsed = maxBlkFreqEstimate @@ -858,7 +862,7 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq,rh0.timestamp, numSampsForPred) freqInFile = math.floor(nomFreq) - if abs(rhl.timestamp - predLastBlockStartTime) / rhl.timestamp < NcsBlocksFactory._tolerance and \ + if abs(rhl.timestamp - predLastBlockStartTime) / (rhl.timestamp - rh0.timestamp) < NcsBlocksFactory._tolerance and \ rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: nb.endBlocks.append(lastBlkI) nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) / 1e6 From ddf26c6e6d492d9a1216e03d5b4532608ea60684 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Mon, 26 Oct 2020 15:40:47 -0700 Subject: [PATCH 70/85] Add additional tests on v5.5.1 with 2 blocks --- neo/test/rawiotest/test_neuralynxrawio.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 73309bcbe..ccebe2e60 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -135,7 +135,8 @@ def testBuildUsingHeaderAndScanning(self): self.assertEqual(len(nb.endBlocks), 1) self.assertEqual(nb.endBlocks[0], 9) - # test Cheetah 5.5.1, which is DigitalLynxSX + # test Cheetah 5.5.1, which is DigitalLynxSX and has two blocks of records + # with a fairly large gap filename = self.get_filename_path('Cheetah_v5.5.1/original_data/Tet3a.ncs') hdr = NlxHeader.buildForFile(filename) data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', @@ -143,6 +144,12 @@ def testBuildUsingHeaderAndScanning(self): nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) self.assertEqual(nb.sampFreqUsed, 32000) self.assertEqual(nb.microsPerSampUsed, 31.25) + self.assertEqual(len(nb.startBlocks), 2) + self.assertEqual(nb.startBlocks[0], 0) + self.assertEqual(nb.startBlocks[1], 2498) + self.assertEqual(len(nb.endBlocks), 2) + self.assertEqual(nb.endBlocks[0], 2497) + self.assertEqual(nb.endBlocks[1], 3331) From b75989d6d05d9105458a0401248d755931a7c698 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Mon, 26 Oct 2020 16:22:08 -0700 Subject: [PATCH 71/85] Tests of block construction for incomplete blocks --- neo/test/rawiotest/test_neuralynxrawio.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index ccebe2e60..18d0bde05 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -101,6 +101,15 @@ def test_ncsblocks_partial(self): self.assertEqual(data0.shape[0],6690) self.assertEqual(data0['timestamp'][6689],8515800549) # timestamp of last record + hdr = NlxHeader.buildForFile(filename) + nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) + self.assertEqual(nb.sampFreqUsed, 32009.05084744305) + self.assertEqual(nb.microsPerSampUsed, 31.241163781021083) + self.assertEqual(len(nb.startBlocks), 1) + self.assertEqual(nb.startBlocks[0], 0) + self.assertEqual(len(nb.endBlocks), 1) + self.assertEqual(nb.endBlocks[0], 6689) + def testBuildGivenActualFrequency(self): # Test early files where the frequency listed in the header is From dd09521de0380004f8c9927e4d95ae6a98da5355 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Mon, 26 Oct 2020 16:22:29 -0700 Subject: [PATCH 72/85] Fix up single block case. --- neo/rawio/neuralynxrawio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index d2fb20462..b946e0686 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -864,8 +864,9 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): freqInFile = math.floor(nomFreq) if abs(rhl.timestamp - predLastBlockStartTime) / (rhl.timestamp - rh0.timestamp) < NcsBlocksFactory._tolerance and \ rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: + nb.startBlocks.append(0) nb.endBlocks.append(lastBlkI) - nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) / 1e6 + nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) * 1e6 nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) # otherwise parse records to determine blocks using default maximum gap length From e17a869e6d98be3d4cccdbc3b6a6d97c09bef33b Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Sat, 7 Nov 2020 15:46:16 -0700 Subject: [PATCH 73/85] Remove unneeded classes. Clean up style. # Conflicts: # neo/rawio/neuralynxrawio.py --- neo/rawio/neuralynxrawio.py | 81 +++++++++++++++---------------------- 1 file changed, 33 insertions(+), 48 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index b946e0686..200b32641 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -31,7 +31,6 @@ import math - class NeuralynxRawIO(BaseRawIO): """" Class for reading datasets recorded by Neuralynx. @@ -53,7 +52,7 @@ class NeuralynxRawIO(BaseRawIO): _BLOCK_SIZE = 512 # nb sample per signal block _ncs_dtype = [('timestamp', 'uint64'), ('channel_id', 'uint32'), ('sample_rate', 'uint32'), - ('nb_valid', 'uint32'), ('samples', 'int16', (_BLOCK_SIZE,))] + ('nb_valid', 'uint32'), ('samples', 'int16', (_BLOCK_SIZE,))] def __init__(self, dirname='', keep_original_times=False, **kargs): """ @@ -235,7 +234,7 @@ def _parse_header(self): # :TODO: current algorithm depends on side-effect of read_ncs_files on # self._sigs_memmap, self._sigs_t_start, self._sigs_t_stop, # self._sigs_length, self._nb_segment, self._timestamp_limits - ncsBlocks = self.read_ncs_files(self.ncs_filenames) + self.read_ncs_files(self.ncs_filenames) # Determine timestamp limits in nev, nse file by scanning them. ts0, ts1 = None, None @@ -470,9 +469,9 @@ def _rescale_event_timestamp(self, event_timestamps, dtype): def read_ncs_files(self, ncs_filenames): """ - Given a list of ncs files, return a dictionary of NcsBlocks indexed by channel uid. + Given a list of ncs files, read their basic structure and setup the following + attributes: - :TODO: Current algorithm has side effects on following attributes: * self._sigs_memmap = [ {} for seg_index in range(self._nb_segment) ] * self._sigs_t_start = [] * self._sigs_t_stop = [] @@ -566,8 +565,13 @@ def read_ncs_files(self, ncs_filenames): # create segment with subdata block/t_start/t_stop/length for chan_uid, ncs_filename in self.ncs_filenames.items(): +<<<<<<< HEAD data = np.memmap(ncs_filename, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) +======= + data = np.memmap(ncs_filename, dtype=self.ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) +>>>>>>> 153446dc... Remove unneeded classes. Clean up style. assert data.size == data0.size, 'ncs files do not have the same data length' for seg_index, (i0, i1) in enumerate(gap_pairs): @@ -595,7 +599,7 @@ def read_ncs_files(self, ncs_filenames): class WholeMicrosTimePositionBlock(): """ - Map of time to sample positions. + Wrapper of static calculations of time to sample positions. Times are rounded to nearest microsecond. Model here is that times from start of a sample until just before the next sample are included, @@ -603,11 +607,6 @@ class WholeMicrosTimePositionBlock(): channel with no samples is empty and contains no time intervals. """ - _sampFrequency = 0 - _startTime = 0 - _size = 0 - _microsPerSamp = 0 - @staticmethod def getFreqForMicrosPerSamp(micros): """ @@ -628,19 +627,16 @@ def calcSampleTime(sampFr, startTime, posn): Calculate time rounded to microseconds for sample given frequency, start time, and sample position. """ - return round(startTime+ + return round(startTime + WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(sampFr)*posn) + class CscRecordHeader(): """ Information in header of each Ncs record, excluding sample values themselves. """ - timestamp = 0 - channel_id = 0 - sample_rate = 0 - nb_valid = 0 - def __init__(self,ncsMemMap,recn): + def __init__(self, ncsMemMap, recn): """ Construct a record header for a given record in a memory map for an NcsFile. """ @@ -649,6 +645,7 @@ def __init__(self,ncsMemMap,recn): self.sample_rate = ncsMemMap['sample_rate'][recn] self.nb_valid = ncsMemMap['nb_valid'][recn] + class NcsBlocks(): """ Contains information regarding the contiguous blocks of records in an Ncs file. @@ -659,10 +656,10 @@ def __init__(self): self.startBlocks = [] self.endBlocks = [] self.sampFreqUsed = 0 # actual sampling frequency of samples - self.microsPerSampUsed = 0 # microseconds per sample + self.microsPerSampUsed = 0 # microseconds per sample -class NcsBlocksFactory(): +class NcsBlocksFactory: """ Class for factory methods which perform parsing of contiguous blocks of records in Ncs files. @@ -671,9 +668,8 @@ class NcsBlocksFactory(): more complicated. Copied from Java code on Sept 7, 2020. """ - _tolerance = 0.001 # tolerance for drift of timestamps within one NcsBlock - _maxGapLength = 5 # maximum gap between predicted and actual block timestamps - # still considered within one NcsBlock + _tolerance = 0.001 # tolerance for drift of timestamps within one NcsBlock + _maxGapLength = 5 # maximum gap between predicted and actual block timestamps still considered within one NcsBlock @staticmethod def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime): @@ -700,7 +696,7 @@ def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePre blkLen = 0 for recn in range(1, ncsMemMap.shape[0]): hdr = CscRecordHeader(ncsMemMap, recn) - if hdr.channel_id!=chanNum | hdr.sample_rate!=reqFreq: + if hdr.channel_id != chanNum | hdr.sample_rate != reqFreq: raise IOError('Channel number or sampling frequency changed in records within file') predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, startBlockPredTime, blkLen) @@ -718,7 +714,6 @@ def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePre return ncsBlocks - @staticmethod def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): """ @@ -755,7 +750,7 @@ def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): lastBlkI = ncsMemMap.shape[0] - 1 rhl = CscRecordHeader(ncsMemMap, lastBlkI) predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, - NeuralynxRawIO._BLOCK_SIZE * lastBlkI) + NeuralynxRawIO._BLOCK_SIZE * lastBlkI) if rhl.channel_id == chanNum and rhl.sample_rate == reqFreq and rhl.timestamp == predLastBlockStartTime: nb = NcsBlocks() nb.startBlocks.append(0) @@ -765,10 +760,9 @@ def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): # otherwise need to scan looking for breaks else: blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, - rh0.nb_valid) + rh0.nb_valid) return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, nb, chanNum, reqFreq, blkOnePredTime) - @staticmethod def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): """ @@ -808,8 +802,8 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): if hdr.channel_id != chanNum | hdr.sample_rate != recFreq: raise IOError('Channel number or sampling frequency changed in records within file') predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, lastRecTime, - lastRecNumSamps) - if (abs(hdr.timestamp - predTime) > maxGapLen): + lastRecNumSamps) + if abs(hdr.timestamp - predTime) > maxGapLen: ncsBlocks.endBlocks.append(recn-1) ncsBlocks.startBlocks.append(recn) if blkLen > maxBlkLen: @@ -829,7 +823,6 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): return ncsBlocks - @staticmethod def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): """ @@ -855,15 +848,16 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): chanNum = rh0.channel_id lastBlkI = numRecs - 1 - rhl = CscRecordHeader(ncsMemMap,lastBlkI) + rhl = CscRecordHeader(ncsMemMap, lastBlkI) # check if file is one block of records, to within tolerance, which is often the case numSampsForPred = NeuralynxRawIO._BLOCK_SIZE * lastBlkI - predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq,rh0.timestamp, + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq, rh0.timestamp, numSampsForPred) freqInFile = math.floor(nomFreq) - if abs(rhl.timestamp - predLastBlockStartTime) / (rhl.timestamp - rh0.timestamp) < NcsBlocksFactory._tolerance and \ - rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: + if abs(rhl.timestamp - predLastBlockStartTime) / \ + (rhl.timestamp - rh0.timestamp) < NcsBlocksFactory._tolerance and \ + rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: nb.startBlocks.append(0) nb.endBlocks.append(lastBlkI) nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) * 1e6 @@ -871,14 +865,14 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): # otherwise parse records to determine blocks using default maximum gap length else: - nb.sampFreqUsed = nomFreq - nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) - nb = NcsBlocksFactory._parseForMaxGap(ncsMemMap, nb, NcsBlocksFactory._maxGapLength) + nb.sampFreqUsed = nomFreq + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) + nb = NcsBlocksFactory._parseForMaxGap(ncsMemMap, nb, NcsBlocksFactory._maxGapLength) return nb @staticmethod - def buildForNcsFile(ncsMemMap,nlxHdr): + def buildForNcsFile(ncsMemMap, nlxHdr): """ Build an NcsBlocks object for an NcsFile, given as a memmap and NlxHeader, handling gap detection appropriately given the file type as specified by the header. @@ -939,7 +933,7 @@ def _to_bool(txt): elif txt == 'False': return False else: - raise Exception('Can not convert %s to bool' % (txt)) + raise Exception('Can not convert %s to bool' % txt) # keys that may be present in header which we parse txt_header_keys = [ @@ -1118,8 +1112,6 @@ def build_for_file(filename): else: hpd = NlxHeader.header_pattern_dicts['def'] - original_filename = re.search(hpd['filename_regex'], txt_header).groupdict()['filename'] - # opening time dt1 = re.search(hpd['datetime1_regex'], txt_header).groupdict() info['recording_opened'] = datetime.datetime.strptime( @@ -1176,13 +1168,6 @@ def type_of_recording(self): return 'UNKNOWN' -class NcsHeader(): - """ - Representation of information in Ncs file headers, including exact - recording type. - """ - - nev_dtype = [ ('reserved', ' Date: Tue, 27 Oct 2020 09:48:42 -0700 Subject: [PATCH 74/85] Add test of side effects of read_ncs_files --- neo/test/rawiotest/test_neuralynxrawio.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 18d0bde05..64c7720d8 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -66,6 +66,21 @@ class TestNeuralynxRawIO(BaseTestRawIO, unittest.TestCase, ): 'Cheetah_v6.3.2/incomplete_blocks/Events.nev', 'Cheetah_v6.3.2/incomplete_blocks/README.txt'] + def test_read_ncs_files_sideeffects(self): + + # Test Cheetah 5.5.1, which is DigitalLynxSX and has two blocks of records + # with a fairly large gap. + rawio = NeuralynxRawIO(self.get_filename_path('Cheetah_v5.5.1/original_data')) + rawio.parse_header() + self.assertEqual(rawio._nb_segment, 2) + self.assertListEqual(rawio._timestamp_limits,[(26122557633, 26162525633), + (26366360633, 26379704633)]) + self.assertListEqual(rawio._sigs_length,[1278976, 427008]) + self.assertListEqual(rawio._sigs_t_stop,[26162.525633, 26379.704633]) + self.assertListEqual(rawio._sigs_t_start,[26122.557633, 26366.360633]) + self.assertEqual(len(rawio._sigs_memmap),2) # check only that there are 2 memmaps + + class TestNcsRecordingType(TestNeuralynxRawIO, unittest.TestCase): """ From 182577295cb30fa5a1aafcf772794c2b10cd753f Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Sat, 7 Nov 2020 15:48:56 -0700 Subject: [PATCH 75/85] Use NcsBlocksFactory and logical or. # Conflicts: # neo/rawio/neuralynxrawio.py --- neo/rawio/neuralynxrawio.py | 127 +++++++++++++++--------------------- 1 file changed, 51 insertions(+), 76 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 200b32641..13a58a412 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -479,92 +479,38 @@ def read_ncs_files(self, ncs_filenames): * self._nb_segment * self._timestamp_limits - The first file is read entirely to detect gaps in timestamp. - each gap lead to a new segment. - - Other files are not read entirely but we check than gaps - are at the same place. - - - gap_indexes can be given (when cached) to avoid full read. - + Files will be scanned to determine the blocks of records. If file is a single block of records, + this scan is brief, otherwise it will check each record which may take some time. """ + # :TODO: Needs to account for gaps and start and end times potentially # being different in different groups of channels. These groups typically # correspond to the channels collected by a single ADC card. if len(ncs_filenames) == 0: return None - good_delta = int(self._BLOCK_SIZE * 1e6 / self._sigs_sampling_rate) chan_uid0 = list(ncs_filenames.keys())[0] filename0 = ncs_filenames[chan_uid0] + # parse the structure of the first file data0 = np.memmap(filename0, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) - - gap_indexes = None - lost_indexes = None - - if self.use_cache: - gap_indexes = self._cache.get('gap_indexes') - lost_indexes = self._cache.get('lost_indexes') - - # detect gaps on first file - if (gap_indexes is None) or (lost_indexes is None): - - # this can be long!!!! - timestamps0 = data0['timestamp'] - deltas0 = np.diff(timestamps0) - - # :TODO: This algorithm needs to account for older style files which had a rounded - # off sampling rate in the header. - # - # It should be that: - # gap_indexes, = np.nonzero(deltas0!=good_delta) - # but for a file I have found many deltas0==15999, 16000, 16001 (for sampling at 32000) - # I guess this is a round problem - # So this is the same with a tolerance of 1 or 2 ticks - max_tolerance = 2 - mask = np.abs((deltas0 - good_delta).astype('int64')) > max_tolerance - - gap_indexes, = np.nonzero(mask) - - if self.use_cache: - self.add_in_cache(gap_indexes=gap_indexes) - - # update for lost_indexes - # Sometimes NLX writes a faulty block, but it then validates how much samples it wrote - # the validation field is in delta0['nb_valid'], it should be equal to BLOCK_SIZE - # :TODO: this algorithm ignores samples in partially filled blocks, which - # is not strictly necessary as all channels might have same partially filled - # blocks at the end. - - lost_indexes, = np.nonzero(data0['nb_valid'] < self._BLOCK_SIZE) - - if self.use_cache: - self.add_in_cache(lost_indexes=lost_indexes) - - gap_candidates = np.unique([0] - + [data0.size] - + (gap_indexes + 1).tolist() - + lost_indexes.tolist()) # linear - - gap_pairs = np.vstack([gap_candidates[:-1], gap_candidates[1:]]).T # 2D (n_segments, 2) + hdr0 = NlxHeader.buildForFile(filename0) + nb0 = NcsBlocksFactory.buildForNcsFile(data0,hdr0) # construct proper gap ranges free of lost samples artifacts minimal_segment_length = 1 # in blocks - goodpairs = np.diff(gap_pairs, 1).reshape(-1) > minimal_segment_length - gap_pairs = gap_pairs[goodpairs] # ensures a segment is at least a block wide - self._nb_segment = len(gap_pairs) + self._nb_segment = len(nb0.startBlocks) self._sigs_memmap = [{} for seg_index in range(self._nb_segment)] self._sigs_t_start = [] self._sigs_t_stop = [] self._sigs_length = [] self._timestamp_limits = [] - # create segment with subdata block/t_start/t_stop/length + # create segment with subdata block/t_start/t_stop/length for each channel for chan_uid, ncs_filename in self.ncs_filenames.items(): +<<<<<<< HEAD <<<<<<< HEAD data = np.memmap(ncs_filename, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) @@ -573,27 +519,56 @@ def read_ncs_files(self, ncs_filenames): offset=NlxHeader.HEADER_SIZE) >>>>>>> 153446dc... Remove unneeded classes. Clean up style. assert data.size == data0.size, 'ncs files do not have the same data length' - - for seg_index, (i0, i1) in enumerate(gap_pairs): - - assert data[i0]['timestamp'] == data0[i0][ - 'timestamp'], 'ncs files do not have the same gaps' - assert data[i1 - 1]['timestamp'] == data0[i1 - 1][ - 'timestamp'], 'ncs files do not have the same gaps' - - subdata = data[i0:i1] +======= + if chan_uid == chan_uid0: + data = data0 + hdr = hdr0 + nb = nb0 + else: + data = np.memmap(ncs_filename, dtype=self._ncs_dtype, mode='r', + offset=NlxHeader.HEADER_SIZE) + hdr = NlxHeader.buildForFile(ncs_filename) + nb = NcsBlocksFactory.buildForNcsFile(data, hdr) + + # Check that record block structure of each file is identical to the first. + if len(nb.startBlocks) != len(nb0.startBlocks) or len(nb.endBlocks) != len(nb0.endBlocks): + raise IOError('ncs files have different numbers of blocks of records') + + for i, sbi in enumerate(nb.startBlocks): + if (sbi != nb0.startBlocks[i]): + raise IOError('ncs files have different start block structure') + + for i, ebi in enumerate(nb.endBlocks): + if (ebi != nb0.endBlocks[i]): + raise IOError('ncs files have different end block structure') +>>>>>>> 6982c597... Use NcsBlocksFactory and logical or. + + # create a memmap for each record block + for seg_index in range(len(nb.startBlocks)): + + if (data[nb.startBlocks[seg_index]]['timestamp'] != + data0[nb0.startBlocks[seg_index]]['timestamp'] or + data[nb.endBlocks[seg_index]]['timestamp'] != + data0[nb0.endBlocks[seg_index]]['timestamp']) : + raise IOError('ncs files have different timestamp structure') + + subdata = data[nb.startBlocks[seg_index]:nb.endBlocks[seg_index]] self._sigs_memmap[seg_index][chan_uid] = subdata if chan_uid == chan_uid0: + numSampsLastBlock = subdata[-1]['nb_valid'] ts0 = subdata[0]['timestamp'] - ts1 = subdata[-1]['timestamp'] +\ - np.uint64(self._BLOCK_SIZE / self._sigs_sampling_rate * 1e6) + ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, + subdata[-1]['timestamp'], + numSampsLastBlock) self._timestamp_limits.append((ts0, ts1)) t_start = ts0 / 1e6 self._sigs_t_start.append(t_start) t_stop = ts1 / 1e6 self._sigs_t_stop.append(t_stop) - length = subdata.size * self._BLOCK_SIZE + # :TODO: this should really be the total of block lengths, but this allows + # the last block to be shorter, the most common case + length = (subdata.size - 1) * self._BLOCK_SIZE + numSampsLastBlock self._sigs_length.append(length) @@ -799,7 +774,7 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): ncsBlocks.startBlocks.append(0) for recn in range(1, ncsMemMap.shape[0]): hdr = CscRecordHeader(ncsMemMap, recn) - if hdr.channel_id != chanNum | hdr.sample_rate != recFreq: + if hdr.channel_id != chanNum or hdr.sample_rate != recFreq: raise IOError('Channel number or sampling frequency changed in records within file') predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, lastRecTime, lastRecNumSamps) From 97a694a3470b7709721c3aa322279d0bc5ddd33b Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 15:36:46 -0700 Subject: [PATCH 76/85] Fix off by one in range for list. Comments. --- neo/rawio/neuralynxrawio.py | 18 ++++++++++-------- neo/test/rawiotest/test_neuralynxrawio.py | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 13a58a412..4081afc7b 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -552,22 +552,24 @@ def read_ncs_files(self, ncs_filenames): data0[nb0.endBlocks[seg_index]]['timestamp']) : raise IOError('ncs files have different timestamp structure') - subdata = data[nb.startBlocks[seg_index]:nb.endBlocks[seg_index]] + subdata = data[nb.startBlocks[seg_index]:(nb.endBlocks[seg_index]+1)] self._sigs_memmap[seg_index][chan_uid] = subdata if chan_uid == chan_uid0: numSampsLastBlock = subdata[-1]['nb_valid'] ts0 = subdata[0]['timestamp'] - ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, - subdata[-1]['timestamp'], - numSampsLastBlock) + ts1 = subdata[-1]['timestamp'] +\ + np.uint64(self._BLOCK_SIZE / self._sigs_sampling_rate * 1e6) + # ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, + # subdata[-1]['timestamp'], + # numSampsLastBlock) self._timestamp_limits.append((ts0, ts1)) t_start = ts0 / 1e6 self._sigs_t_start.append(t_start) t_stop = ts1 / 1e6 self._sigs_t_stop.append(t_stop) - # :TODO: this should really be the total of block lengths, but this allows - # the last block to be shorter, the most common case + # :TODO: this should really be the total of nb_valid in records, but this allows + # the last record of a block to be shorter, the most common case length = (subdata.size - 1) * self._BLOCK_SIZE + numSampsLastBlock self._sigs_length.append(length) @@ -628,8 +630,8 @@ class NcsBlocks(): """ def __init__(self): - self.startBlocks = [] - self.endBlocks = [] + self.startBlocks = [] # index of starting record for each block + self.endBlocks = [] # index of last record (inclusive) for each block self.sampFreqUsed = 0 # actual sampling frequency of samples self.microsPerSampUsed = 0 # microseconds per sample diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 64c7720d8..00261d9db 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -73,6 +73,7 @@ def test_read_ncs_files_sideeffects(self): rawio = NeuralynxRawIO(self.get_filename_path('Cheetah_v5.5.1/original_data')) rawio.parse_header() self.assertEqual(rawio._nb_segment, 2) + # test values here from direct inspection of .ncs files self.assertListEqual(rawio._timestamp_limits,[(26122557633, 26162525633), (26366360633, 26379704633)]) self.assertListEqual(rawio._sigs_length,[1278976, 427008]) From 5a6dc6275e5c2a2f1dc539662bfc0c3fad6bb6ca Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 15:40:27 -0700 Subject: [PATCH 77/85] Use standard time calculation for last time of block. --- neo/rawio/neuralynxrawio.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 4081afc7b..6892ed821 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -558,18 +558,17 @@ def read_ncs_files(self, ncs_filenames): if chan_uid == chan_uid0: numSampsLastBlock = subdata[-1]['nb_valid'] ts0 = subdata[0]['timestamp'] - ts1 = subdata[-1]['timestamp'] +\ - np.uint64(self._BLOCK_SIZE / self._sigs_sampling_rate * 1e6) - # ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, - # subdata[-1]['timestamp'], - # numSampsLastBlock) + ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, + subdata[-1]['timestamp'], + numSampsLastBlock) self._timestamp_limits.append((ts0, ts1)) t_start = ts0 / 1e6 self._sigs_t_start.append(t_start) t_stop = ts1 / 1e6 self._sigs_t_stop.append(t_stop) - # :TODO: this should really be the total of nb_valid in records, but this allows - # the last record of a block to be shorter, the most common case + # :TODO: This should really be the total of nb_valid in records, but this allows + # the last record of a block to be shorter, the most common case. Have never + # seen a block of records with not full records before the last. length = (subdata.size - 1) * self._BLOCK_SIZE + numSampsLastBlock self._sigs_length.append(length) From 0837723dd622cedd573a3ba4624f3d0fa0e4de62 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 17:52:04 -0700 Subject: [PATCH 78/85] Remove test with tolerance over whole length. Fix microsPerSampUsed assignement. Using a tolerance over a longer experiment is not sensitive enough to detect blocks where perhaps a large amount of samples are dropped and there is a small gap afterwards. --- neo/rawio/neuralynxrawio.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 6892ed821..bf4cb584b 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -644,7 +644,6 @@ class NcsBlocksFactory: more complicated. Copied from Java code on Sept 7, 2020. """ - _tolerance = 0.001 # tolerance for drift of timestamps within one NcsBlock _maxGapLength = 5 # maximum gap between predicted and actual block timestamps still considered within one NcsBlock @staticmethod @@ -795,12 +794,12 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): ncsBlocks.endBlocks.append(ncsMemMap.shape[0] - 1) ncsBlocks.sampFreqUsed = maxBlkFreqEstimate - ncsBlocks.setMicrosPerSamp = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(maxBlkFreqEstimate) + ncsBlocks.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(maxBlkFreqEstimate) return ncsBlocks @staticmethod - def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): + def _buildForMaxGap(ncsMemMap, nomFreq): """ Determine blocks of records in memory mapped Ncs file given a nominal frequency of the file, using the default values of frequency tolerance and maximum gap between blocks. @@ -826,13 +825,12 @@ def _buildForToleranceAndMaxGap(ncsMemMap, nomFreq): lastBlkI = numRecs - 1 rhl = CscRecordHeader(ncsMemMap, lastBlkI) - # check if file is one block of records, to within tolerance, which is often the case + # check if file is one block of records, with exact timestamp match, which may be the case numSampsForPred = NeuralynxRawIO._BLOCK_SIZE * lastBlkI predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq, rh0.timestamp, numSampsForPred) freqInFile = math.floor(nomFreq) - if abs(rhl.timestamp - predLastBlockStartTime) / \ - (rhl.timestamp - rh0.timestamp) < NcsBlocksFactory._tolerance and \ + if abs(rhl.timestamp - predLastBlockStartTime) == 0 and \ rhl.channel_id == chanNum and rhl.sample_rate == freqInFile: nb.startBlocks.append(0) nb.endBlocks.append(lastBlkI) @@ -881,7 +879,7 @@ def buildForNcsFile(ncsMemMap, nlxHdr): # digital lynx style with fractional frequency and micros per samp determined from block times elif acqType == "DIGITALLYNX" or acqType == "DIGITALLYNXSX": nomFreq = nlxHdr['sampling_rate'] - nb = NcsBlocksFactory._buildForToleranceAndMaxGap(ncsMemMap, nomFreq) + nb = NcsBlocksFactory._buildForMaxGap(ncsMemMap, nomFreq) # BML style with fractional frequency and micros per samp elif acqType == "BML": From 32a26c704c212b3b024761fbfeaea2a4a84d2292 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 17:53:37 -0700 Subject: [PATCH 79/85] Tests of raw io for incomplete records multiple block case. --- neo/test/rawiotest/test_neuralynxrawio.py | 26 +++++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 00261d9db..28c93c088 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -72,8 +72,8 @@ def test_read_ncs_files_sideeffects(self): # with a fairly large gap. rawio = NeuralynxRawIO(self.get_filename_path('Cheetah_v5.5.1/original_data')) rawio.parse_header() - self.assertEqual(rawio._nb_segment, 2) # test values here from direct inspection of .ncs files + self.assertEqual(rawio._nb_segment, 2) self.assertListEqual(rawio._timestamp_limits,[(26122557633, 26162525633), (26366360633, 26379704633)]) self.assertListEqual(rawio._sigs_length,[1278976, 427008]) @@ -81,6 +81,19 @@ def test_read_ncs_files_sideeffects(self): self.assertListEqual(rawio._sigs_t_start,[26122.557633, 26366.360633]) self.assertEqual(len(rawio._sigs_memmap),2) # check only that there are 2 memmaps + # Test Cheetah 6.3.2, the incomplete_blocks test. This is a DigitalLynxSX with + # three blocks of records. Gaps are on the order of 16 ms or so. + rawio = NeuralynxRawIO(self.get_filename_path('Cheetah_v6.3.2/incomplete_blocks')) + rawio.parse_header() + # test values here from direct inspection of .ncs file + self.assertEqual(rawio._nb_segment, 3) + self.assertListEqual(rawio._timestamp_limits,[(8408806811, 8427831990), + (8427832053, 8487768498), + (8487768561, 8515816549)]) + self.assertListEqual(rawio._sigs_length,[608806, 1917967, 897536]) + self.assertListEqual(rawio._sigs_t_stop,[8427.831990, 8487.768498, 8515.816549]) + self.assertListEqual(rawio._sigs_t_start,[8408.806811, 8427.832053, 8487.768561]) + self.assertEqual(len(rawio._sigs_memmap),3) # check only that there are 3 memmaps class TestNcsRecordingType(TestNeuralynxRawIO, unittest.TestCase): @@ -105,6 +118,7 @@ def test_recording_types(self): hdr = NlxHeader.build_for_file(filename) self.assertEqual(hdr.type_of_recording(), typeTest[1]) + class TestNcsBlocksFactory(TestNeuralynxRawIO, unittest.TestCase): """ Test building NcsBlocks for files of different revisions. @@ -119,12 +133,10 @@ def test_ncsblocks_partial(self): hdr = NlxHeader.buildForFile(filename) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) - self.assertEqual(nb.sampFreqUsed, 32009.05084744305) - self.assertEqual(nb.microsPerSampUsed, 31.241163781021083) - self.assertEqual(len(nb.startBlocks), 1) - self.assertEqual(nb.startBlocks[0], 0) - self.assertEqual(len(nb.endBlocks), 1) - self.assertEqual(nb.endBlocks[0], 6689) + self.assertEqual(nb.sampFreqUsed, 32000.012813673042) + self.assertEqual(nb.microsPerSampUsed, 31.249987486652431) + self.assertListEqual(nb.startBlocks, [0, 1190, 4937]) + self.assertListEqual(nb.endBlocks, [1189, 4936, 6689]) def testBuildGivenActualFrequency(self): From 917eb56ba430df1c5808bffa87e0e0021c2597f1 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 18:04:40 -0700 Subject: [PATCH 80/85] Update stop times to include time for samples in partially filled records. --- neo/test/iotest/test_neuralynxio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neo/test/iotest/test_neuralynxio.py b/neo/test/iotest/test_neuralynxio.py index df9f0b986..7e0c2f234 100644 --- a/neo/test/iotest/test_neuralynxio.py +++ b/neo/test/iotest/test_neuralynxio.py @@ -308,7 +308,7 @@ def test_incomplete_block_handling_v632(self): for t, gt in zip(nio._sigs_t_start, [8408.806811, 8427.832053, 8487.768561]): self.assertEqual(np.round(t, 4), np.round(gt, 4)) - for t, gt in zip(nio._sigs_t_stop, [8427.830803, 8487.768029, 8515.816549]): + for t, gt in zip(nio._sigs_t_stop, [8427.831990, 8487.768498, 8515.816549]): self.assertEqual(np.round(t, 4), np.round(gt, 4)) From f28c4ca07eb973aa8a38955aab54dd53a35dd208 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Wed, 28 Oct 2020 18:21:35 -0700 Subject: [PATCH 81/85] PEP and style cleanup. Corrected gap comment. --- neo/rawio/neuralynxrawio.py | 20 +++++------ neo/test/rawiotest/test_neuralynxrawio.py | 42 +++++++++++------------ 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index bf4cb584b..1c9e50231 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -164,7 +164,7 @@ def _parse_header(self): dtype = get_nse_or_ntt_dtype(info, ext) - if (os.path.getsize(filename) <= NlxHeader.HEADER_SIZE): + if os.path.getsize(filename) <= NlxHeader.HEADER_SIZE: self._empty_nse_ntt.append(filename) data = np.zeros((0,), dtype=dtype) else: @@ -197,7 +197,7 @@ def _parse_header(self): # each ('event_id', 'ttl_input') give a new event channel self.nev_filenames[chan_id] = filename - if (os.path.getsize(filename) <= NlxHeader.HEADER_SIZE): + if os.path.getsize(filename) <= NlxHeader.HEADER_SIZE: self._empty_nev.append(filename) data = np.zeros((0,), dtype=nev_dtype) internal_ids = [] @@ -495,7 +495,7 @@ def read_ncs_files(self, ncs_filenames): # parse the structure of the first file data0 = np.memmap(filename0, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) hdr0 = NlxHeader.buildForFile(filename0) - nb0 = NcsBlocksFactory.buildForNcsFile(data0,hdr0) + nb0 = NcsBlocksFactory.buildForNcsFile(data0, hdr0) # construct proper gap ranges free of lost samples artifacts minimal_segment_length = 1 # in blocks @@ -535,11 +535,11 @@ def read_ncs_files(self, ncs_filenames): raise IOError('ncs files have different numbers of blocks of records') for i, sbi in enumerate(nb.startBlocks): - if (sbi != nb0.startBlocks[i]): + if sbi != nb0.startBlocks[i]: raise IOError('ncs files have different start block structure') for i, ebi in enumerate(nb.endBlocks): - if (ebi != nb0.endBlocks[i]): + if ebi != nb0.endBlocks[i]: raise IOError('ncs files have different end block structure') >>>>>>> 6982c597... Use NcsBlocksFactory and logical or. @@ -549,7 +549,7 @@ def read_ncs_files(self, ncs_filenames): if (data[nb.startBlocks[seg_index]]['timestamp'] != data0[nb0.startBlocks[seg_index]]['timestamp'] or data[nb.endBlocks[seg_index]]['timestamp'] != - data0[nb0.endBlocks[seg_index]]['timestamp']) : + data0[nb0.endBlocks[seg_index]]['timestamp']): raise IOError('ncs files have different timestamp structure') subdata = data[nb.startBlocks[seg_index]:(nb.endBlocks[seg_index]+1)] @@ -559,7 +559,7 @@ def read_ncs_files(self, ncs_filenames): numSampsLastBlock = subdata[-1]['nb_valid'] ts0 = subdata[0]['timestamp'] ts1 = WholeMicrosTimePositionBlock.calcSampleTime(nb0.sampFreqUsed, - subdata[-1]['timestamp'], + subdata[-1]['timestamp'], numSampsLastBlock) self._timestamp_limits.append((ts0, ts1)) t_start = ts0 / 1e6 @@ -573,7 +573,7 @@ def read_ncs_files(self, ncs_filenames): self._sigs_length.append(length) -class WholeMicrosTimePositionBlock(): +class WholeMicrosTimePositionBlock: """ Wrapper of static calculations of time to sample positions. @@ -607,7 +607,7 @@ def calcSampleTime(sampFr, startTime, posn): WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(sampFr)*posn) -class CscRecordHeader(): +class CscRecordHeader: """ Information in header of each Ncs record, excluding sample values themselves. """ @@ -622,7 +622,7 @@ def __init__(self, ncsMemMap, recn): self.nb_valid = ncsMemMap['nb_valid'][recn] -class NcsBlocks(): +class NcsBlocks: """ Contains information regarding the contiguous blocks of records in an Ncs file. Methods of NcsBlocksFactory perform parsing of this information from an Ncs file. diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 28c93c088..4aa22222b 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -74,26 +74,26 @@ def test_read_ncs_files_sideeffects(self): rawio.parse_header() # test values here from direct inspection of .ncs files self.assertEqual(rawio._nb_segment, 2) - self.assertListEqual(rawio._timestamp_limits,[(26122557633, 26162525633), - (26366360633, 26379704633)]) - self.assertListEqual(rawio._sigs_length,[1278976, 427008]) - self.assertListEqual(rawio._sigs_t_stop,[26162.525633, 26379.704633]) - self.assertListEqual(rawio._sigs_t_start,[26122.557633, 26366.360633]) - self.assertEqual(len(rawio._sigs_memmap),2) # check only that there are 2 memmaps + self.assertListEqual(rawio._timestamp_limits, [(26122557633, 26162525633), + (26366360633, 26379704633)]) + self.assertListEqual(rawio._sigs_length, [1278976, 427008]) + self.assertListEqual(rawio._sigs_t_stop, [26162.525633, 26379.704633]) + self.assertListEqual(rawio._sigs_t_start, [26122.557633, 26366.360633]) + self.assertEqual(len(rawio._sigs_memmap), 2) # check only that there are 2 memmaps # Test Cheetah 6.3.2, the incomplete_blocks test. This is a DigitalLynxSX with - # three blocks of records. Gaps are on the order of 16 ms or so. + # three blocks of records. Gaps are on the order of 60 microseconds or so. rawio = NeuralynxRawIO(self.get_filename_path('Cheetah_v6.3.2/incomplete_blocks')) rawio.parse_header() # test values here from direct inspection of .ncs file self.assertEqual(rawio._nb_segment, 3) - self.assertListEqual(rawio._timestamp_limits,[(8408806811, 8427831990), - (8427832053, 8487768498), - (8487768561, 8515816549)]) - self.assertListEqual(rawio._sigs_length,[608806, 1917967, 897536]) - self.assertListEqual(rawio._sigs_t_stop,[8427.831990, 8487.768498, 8515.816549]) - self.assertListEqual(rawio._sigs_t_start,[8408.806811, 8427.832053, 8487.768561]) - self.assertEqual(len(rawio._sigs_memmap),3) # check only that there are 3 memmaps + self.assertListEqual(rawio._timestamp_limits, [(8408806811, 8427831990), + (8427832053, 8487768498), + (8487768561, 8515816549)]) + self.assertListEqual(rawio._sigs_length, [608806, 1917967, 897536]) + self.assertListEqual(rawio._sigs_t_stop, [8427.831990, 8487.768498, 8515.816549]) + self.assertListEqual(rawio._sigs_t_start, [8408.806811, 8427.832053, 8487.768561]) + self.assertEqual(len(rawio._sigs_memmap), 3) # check only that there are 3 memmaps class TestNcsRecordingType(TestNeuralynxRawIO, unittest.TestCase): @@ -127,9 +127,9 @@ class TestNcsBlocksFactory(TestNeuralynxRawIO, unittest.TestCase): def test_ncsblocks_partial(self): filename = self.get_filename_path('Cheetah_v6.3.2/incomplete_blocks/CSC1_reduced.ncs') data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) - self.assertEqual(data0.shape[0],6690) - self.assertEqual(data0['timestamp'][6689],8515800549) # timestamp of last record + offset=NlxHeader.HEADER_SIZE) + self.assertEqual(data0.shape[0], 6690) + self.assertEqual(data0['timestamp'][6689], 8515800549) # timestamp of last record hdr = NlxHeader.buildForFile(filename) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) @@ -144,7 +144,7 @@ def testBuildGivenActualFrequency(self): # floor(1e6/(actual number of microseconds between samples) filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) + offset=NlxHeader.HEADER_SIZE) ncsBlocks = NcsBlocks() ncsBlocks.sampFreqUsed = 1/(35e-6) ncsBlocks.microsPerSampUsed = 35 @@ -154,7 +154,6 @@ def testBuildGivenActualFrequency(self): self.assertEqual(len(ncsBlocks.endBlocks), 1) self.assertEqual(ncsBlocks.endBlocks[0], 9) - def testBuildUsingHeaderAndScanning(self): # Test early files where the frequency listed in the header is @@ -162,7 +161,7 @@ def testBuildUsingHeaderAndScanning(self): filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') hdr = NlxHeader.buildForFile(filename) data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) + offset=NlxHeader.HEADER_SIZE) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) self.assertEqual(nb.sampFreqUsed, 1/35e-6) @@ -177,7 +176,7 @@ def testBuildUsingHeaderAndScanning(self): filename = self.get_filename_path('Cheetah_v5.5.1/original_data/Tet3a.ncs') hdr = NlxHeader.buildForFile(filename) data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) + offset=NlxHeader.HEADER_SIZE) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) self.assertEqual(nb.sampFreqUsed, 32000) self.assertEqual(nb.microsPerSampUsed, 31.25) @@ -189,6 +188,5 @@ def testBuildUsingHeaderAndScanning(self): self.assertEqual(nb.endBlocks[1], 3331) - if __name__ == "__main__": unittest.main() From 50958f52f4c45eb71caca31d602620cab01f0142 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Thu, 29 Oct 2020 08:18:56 -0700 Subject: [PATCH 82/85] Line shortening for PEP8. --- neo/rawio/neuralynxrawio.py | 92 +++++++++++++++++++++++-------------- 1 file changed, 57 insertions(+), 35 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 1c9e50231..b0bdbfe61 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -479,8 +479,9 @@ def read_ncs_files(self, ncs_filenames): * self._nb_segment * self._timestamp_limits - Files will be scanned to determine the blocks of records. If file is a single block of records, - this scan is brief, otherwise it will check each record which may take some time. + Files will be scanned to determine the blocks of records. If file is a single + block of records, this scan is brief, otherwise it will check each record which may + take some time. """ # :TODO: Needs to account for gaps and start and end times potentially @@ -531,7 +532,8 @@ def read_ncs_files(self, ncs_filenames): nb = NcsBlocksFactory.buildForNcsFile(data, hdr) # Check that record block structure of each file is identical to the first. - if len(nb.startBlocks) != len(nb0.startBlocks) or len(nb.endBlocks) != len(nb0.endBlocks): + if len(nb.startBlocks) != len(nb0.startBlocks) or len(nb.endBlocks) != \ + len(nb0.endBlocks): raise IOError('ncs files have different numbers of blocks of records') for i, sbi in enumerate(nb.startBlocks): @@ -566,9 +568,9 @@ def read_ncs_files(self, ncs_filenames): self._sigs_t_start.append(t_start) t_stop = ts1 / 1e6 self._sigs_t_stop.append(t_stop) - # :TODO: This should really be the total of nb_valid in records, but this allows - # the last record of a block to be shorter, the most common case. Have never - # seen a block of records with not full records before the last. + # :TODO: This should really be the total of nb_valid in records, but this + # allows the last record of a block to be shorter, the most common case. + # Have never seen a block of records with not full records before the last. length = (subdata.size - 1) * self._BLOCK_SIZE + numSampsLastBlock self._sigs_length.append(length) @@ -644,7 +646,8 @@ class NcsBlocksFactory: more complicated. Copied from Java code on Sept 7, 2020. """ - _maxGapLength = 5 # maximum gap between predicted and actual block timestamps still considered within one NcsBlock + _maxGapLength = 5 # maximum gap between predicted and actual block timestamps still + # considered within one NcsBlock @staticmethod def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime): @@ -672,16 +675,18 @@ def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePre for recn in range(1, ncsMemMap.shape[0]): hdr = CscRecordHeader(ncsMemMap, recn) if hdr.channel_id != chanNum | hdr.sample_rate != reqFreq: - raise IOError('Channel number or sampling frequency changed in records within file') + raise IOError('Channel number or sampling frequency changed in ' + + 'records within file') predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, startBlockPredTime, blkLen) nValidSamps = hdr.nb_valid if hdr.timestamp != predTime: ncsBlocks.endBlocks.append(recn-1) ncsBlocks.startBlocks.append(recn) - startBlockPredTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, - hdr.timestamp, - nValidSamps) + startBlockPredTime = WholeMicrosTimePositionBlock.calcSampleTime( + ncsBlocks.sampFreqUsed, + hdr.timestamp, + nValidSamps) blklen = 0 else: blkLen += nValidSamps @@ -724,9 +729,11 @@ def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): # check if file is one block of records, which is often the case, and avoid full parse lastBlkI = ncsMemMap.shape[0] - 1 rhl = CscRecordHeader(ncsMemMap, lastBlkI) - predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, - NeuralynxRawIO._BLOCK_SIZE * lastBlkI) - if rhl.channel_id == chanNum and rhl.sample_rate == reqFreq and rhl.timestamp == predLastBlockStartTime: + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, + rh0.timestamp, + NeuralynxRawIO._BLOCK_SIZE * lastBlkI) + if rhl.channel_id == chanNum and rhl.sample_rate == reqFreq and \ + rhl.timestamp == predLastBlockStartTime: nb = NcsBlocks() nb.startBlocks.append(0) nb.endBlocks.append(lastBlkI) @@ -734,9 +741,11 @@ def _buildGivenActualFrequency(ncsMemMap, actualSampFreq, reqFreq): # otherwise need to scan looking for breaks else: - blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, rh0.timestamp, + blkOnePredTime = WholeMicrosTimePositionBlock.calcSampleTime(actualSampFreq, + rh0.timestamp, rh0.nb_valid) - return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, nb, chanNum, reqFreq, blkOnePredTime) + return NcsBlocksFactory._parseGivenActualFrequency(ncsMemMap, nb, chanNum, reqFreq, + blkOnePredTime) @staticmethod def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): @@ -748,20 +757,23 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): ncsMemMap: memmap of Ncs file ncsBlocks: - NcsBlocks object with sampFreqUsed set to nominal frequency to use in computing time for samples (Hz) + NcsBlocks object with sampFreqUsed set to nominal frequency to use in computing time + for samples (Hz) maxGapLen: - maximum difference within a block between predicted time of start of record and recorded time + maximum difference within a block between predicted time of start of record and + recorded time RETURN: - NcsBlocks object with sampFreqUsed and microsPerSamp set based on estimate from largest block + NcsBlocks object with sampFreqUsed and microsPerSamp set based on estimate from + largest block """ # track frequency of each block and use estimate with longest block maxBlkLen = 0 maxBlkFreqEstimate = 0 - # Parse the record sequence, finding blocks of continuous time with no more than maxGapLength - # and same channel number + # Parse the record sequence, finding blocks of continuous time with no more than + # maxGapLength and same channel number rh0 = CscRecordHeader(ncsMemMap, 0) chanNum = rh0.channel_id @@ -775,15 +787,17 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): for recn in range(1, ncsMemMap.shape[0]): hdr = CscRecordHeader(ncsMemMap, recn) if hdr.channel_id != chanNum or hdr.sample_rate != recFreq: - raise IOError('Channel number or sampling frequency changed in records within file') - predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, lastRecTime, - lastRecNumSamps) + raise IOError('Channel number or sampling frequency changed in ' + + 'records within file') + predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, + lastRecTime, lastRecNumSamps) if abs(hdr.timestamp - predTime) > maxGapLen: ncsBlocks.endBlocks.append(recn-1) ncsBlocks.startBlocks.append(recn) if blkLen > maxBlkLen: maxBlkLen = blkLen - maxBlkFreqEstimate = (blkLen - lastRecNumSamps) * 1e6 / (lastRecTime - startBlockTime) + maxBlkFreqEstimate = (blkLen - lastRecNumSamps) * 1e6 / \ + (lastRecTime - startBlockTime) startBlockTime = hdr.timestamp blkLen = hdr.nb_valid else: @@ -794,15 +808,16 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): ncsBlocks.endBlocks.append(ncsMemMap.shape[0] - 1) ncsBlocks.sampFreqUsed = maxBlkFreqEstimate - ncsBlocks.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(maxBlkFreqEstimate) + ncsBlocks.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq( + maxBlkFreqEstimate) return ncsBlocks @staticmethod def _buildForMaxGap(ncsMemMap, nomFreq): """ - Determine blocks of records in memory mapped Ncs file given a nominal frequency of the file, - using the default values of frequency tolerance and maximum gap between blocks. + Determine blocks of records in memory mapped Ncs file given a nominal frequency of + the file, using the default values of frequency tolerance and maximum gap between blocks. PARAMETERS ncsMemMap: @@ -827,7 +842,8 @@ def _buildForMaxGap(ncsMemMap, nomFreq): # check if file is one block of records, with exact timestamp match, which may be the case numSampsForPred = NeuralynxRawIO._BLOCK_SIZE * lastBlkI - predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq, rh0.timestamp, + predLastBlockStartTime = WholeMicrosTimePositionBlock.calcSampleTime(nomFreq, + rh0.timestamp, numSampsForPred) freqInFile = math.floor(nomFreq) if abs(rhl.timestamp - predLastBlockStartTime) == 0 and \ @@ -835,12 +851,14 @@ def _buildForMaxGap(ncsMemMap, nomFreq): nb.startBlocks.append(0) nb.endBlocks.append(lastBlkI) nb.sampFreqUsed = numSampsForPred / (rhl.timestamp - rh0.timestamp) * 1e6 - nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq( + nb.sampFreqUsed) # otherwise parse records to determine blocks using default maximum gap length else: nb.sampFreqUsed = nomFreq - nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(nb.sampFreqUsed) + nb.microsPerSampUsed = WholeMicrosTimePositionBlock.getMicrosPerSampForFreq( + nb.sampFreqUsed) nb = NcsBlocksFactory._parseForMaxGap(ncsMemMap, nb, NcsBlocksFactory._maxGapLength) return nb @@ -855,7 +873,8 @@ def buildForNcsFile(ncsMemMap, nlxHdr): ncsMemMap: memory map of file acqType: - string specifying type of data acquisition used, one of types returned by NlxHeader.typeOfRecording() + string specifying type of data acquisition used, one of types returned by + NlxHeader.typeOfRecording() """ acqType = nlxHdr.typeOfRecording() @@ -869,9 +888,11 @@ def buildForNcsFile(ncsMemMap, nlxHdr): nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) ======= freq = nlxHdr['sampling_rate'] - microsPerSampUsed = math.floor(WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(freq)) + microsPerSampUsed = math.floor(WholeMicrosTimePositionBlock.getMicrosPerSampForFreq( + freq)) sampFreqUsed = WholeMicrosTimePositionBlock.getFreqForMicrosPerSamp(microsPerSampUsed) - nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) + nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, + math.floor(freq)) nb.sampFreqUsed = sampFreqUsed >>>>>>> 60006871... Tests for PRE4 type and code corrections. nb.microsPerSampUsed = microsPerSampUsed @@ -884,7 +905,8 @@ def buildForNcsFile(ncsMemMap, nlxHdr): # BML style with fractional frequency and micros per samp elif acqType == "BML": sampFreqUsed = nlxHdr['sampling_rate'] - nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(sampFreqUsed)) + nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, + math.floor(sampFreqUsed)) else: raise TypeError("Unknown Ncs file type from header.") From b2bbb395fb236a84863c0596c04c2c562b0ee37b Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Sat, 7 Nov 2020 15:49:08 -0700 Subject: [PATCH 83/85] More PEP8 items. # Conflicts: # neo/test/rawiotest/test_neuralynxrawio.py --- neo/rawio/neuralynxrawio.py | 11 ++++++----- neo/test/rawiotest/test_neuralynxrawio.py | 9 +++++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index b0bdbfe61..88ce665cb 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -554,7 +554,7 @@ def read_ncs_files(self, ncs_filenames): data0[nb0.endBlocks[seg_index]]['timestamp']): raise IOError('ncs files have different timestamp structure') - subdata = data[nb.startBlocks[seg_index]:(nb.endBlocks[seg_index]+1)] + subdata = data[nb.startBlocks[seg_index]:(nb.endBlocks[seg_index] + 1)] self._sigs_memmap[seg_index][chan_uid] = subdata if chan_uid == chan_uid0: @@ -606,7 +606,7 @@ def calcSampleTime(sampFr, startTime, posn): start time, and sample position. """ return round(startTime + - WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(sampFr)*posn) + WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(sampFr) * posn) class CscRecordHeader: @@ -647,7 +647,7 @@ class NcsBlocksFactory: """ _maxGapLength = 5 # maximum gap between predicted and actual block timestamps still - # considered within one NcsBlock + # considered within one NcsBlock @staticmethod def _parseGivenActualFrequency(ncsMemMap, ncsBlocks, chanNum, reqFreq, blkOnePredTime): @@ -792,7 +792,7 @@ def _parseForMaxGap(ncsMemMap, ncsBlocks, maxGapLen): predTime = WholeMicrosTimePositionBlock.calcSampleTime(ncsBlocks.sampFreqUsed, lastRecTime, lastRecNumSamps) if abs(hdr.timestamp - predTime) > maxGapLen: - ncsBlocks.endBlocks.append(recn-1) + ncsBlocks.endBlocks.append(recn - 1) ncsBlocks.startBlocks.append(recn) if blkLen > maxBlkLen: maxBlkLen = blkLen @@ -897,7 +897,8 @@ def buildForNcsFile(ncsMemMap, nlxHdr): >>>>>>> 60006871... Tests for PRE4 type and code corrections. nb.microsPerSampUsed = microsPerSampUsed - # digital lynx style with fractional frequency and micros per samp determined from block times + # digital lynx style with fractional frequency and micros per samp determined from + # block times elif acqType == "DIGITALLYNX" or acqType == "DIGITALLYNXSX": nomFreq = nlxHdr['sampling_rate'] nb = NcsBlocksFactory._buildForMaxGap(ncsMemMap, nomFreq) diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index 4aa22222b..e27c1d1ba 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -146,9 +146,14 @@ def testBuildGivenActualFrequency(self): data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) ncsBlocks = NcsBlocks() +<<<<<<< HEAD ncsBlocks.sampFreqUsed = 1/(35e-6) +======= + ncsBlocks.sampFreqUsed = 1 / 35e-6 +>>>>>>> ba82efe4... More PEP8 items. ncsBlocks.microsPerSampUsed = 35 - ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks.sampFreqUsed, 27789) + ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks.sampFreqUsed, + 27789) self.assertEqual(len(ncsBlocks.startBlocks), 1) self.assertEqual(ncsBlocks.startBlocks[0], 0) self.assertEqual(len(ncsBlocks.endBlocks), 1) @@ -164,7 +169,7 @@ def testBuildUsingHeaderAndScanning(self): offset=NlxHeader.HEADER_SIZE) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) - self.assertEqual(nb.sampFreqUsed, 1/35e-6) + self.assertEqual(nb.sampFreqUsed, 1 / 35e-6) self.assertEqual(nb.microsPerSampUsed, 35) self.assertEqual(len(nb.startBlocks), 1) self.assertEqual(nb.startBlocks[0], 0) From 76d3bb373d6642d15c654e48e76bd730264ddc83 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Sat, 7 Nov 2020 15:55:44 -0700 Subject: [PATCH 84/85] Remove conflict markers. --- neo/rawio/neuralynxrawio.py | 36 ++---------------------------------- 1 file changed, 2 insertions(+), 34 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 1b82be3bc..90ad70763 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -110,13 +110,8 @@ def _parse_header(self): self._empty_ncs.append(filename) continue -<<<<<<< HEAD # All file have more or less the same header structure info = NlxHeader.build_for_file(filename) -======= - # All files have more or less the same header structure - info = NlxHeader.buildForFile(filename) ->>>>>>> ba82efe4580e74c321e04cf110be27c050ce77d5 chan_names = info['channel_names'] chan_ids = info['channel_ids'] @@ -516,19 +511,11 @@ def read_ncs_files(self, ncs_filenames): # create segment with subdata block/t_start/t_stop/length for each channel for chan_uid, ncs_filename in self.ncs_filenames.items(): -<<<<<<< HEAD -<<<<<<< HEAD -<<<<<<< HEAD data = np.memmap(ncs_filename, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) -======= - data = np.memmap(ncs_filename, dtype=self.ncs_dtype, mode='r', - offset=NlxHeader.HEADER_SIZE) ->>>>>>> 153446dc... Remove unneeded classes. Clean up style. + assert data.size == data0.size, 'ncs files do not have the same data length' -======= -======= ->>>>>>> ba82efe4580e74c321e04cf110be27c050ce77d5 + if chan_uid == chan_uid0: data = data0 hdr = hdr0 @@ -551,10 +538,6 @@ def read_ncs_files(self, ncs_filenames): for i, ebi in enumerate(nb.endBlocks): if ebi != nb0.endBlocks[i]: raise IOError('ncs files have different end block structure') -<<<<<<< HEAD ->>>>>>> 6982c597... Use NcsBlocksFactory and logical or. -======= ->>>>>>> ba82efe4580e74c321e04cf110be27c050ce77d5 # create a memmap for each record block for seg_index in range(len(nb.startBlocks)): @@ -891,27 +874,12 @@ def buildForNcsFile(ncsMemMap, nlxHdr): # old Neuralynx style with rounded whole microseconds for the samples if acqType == "PRE4": -<<<<<<< HEAD -<<<<<<< HEAD freq = nlxHdr['SamplingFrequency'] microsPerSampUsed = math.floor( WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(freq)) sampFreqUsed = WholeMicrosTimePositionBlock.getFreqForMicrosPerSamp(microsPerSampUsed) nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) -======= -======= ->>>>>>> ba82efe4580e74c321e04cf110be27c050ce77d5 - freq = nlxHdr['sampling_rate'] - microsPerSampUsed = math.floor(WholeMicrosTimePositionBlock.getMicrosPerSampForFreq( - freq)) - sampFreqUsed = WholeMicrosTimePositionBlock.getFreqForMicrosPerSamp(microsPerSampUsed) - nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, - math.floor(freq)) nb.sampFreqUsed = sampFreqUsed -<<<<<<< HEAD ->>>>>>> 60006871... Tests for PRE4 type and code corrections. -======= ->>>>>>> ba82efe4580e74c321e04cf110be27c050ce77d5 nb.microsPerSampUsed = microsPerSampUsed # digital lynx style with fractional frequency and micros per samp determined from From c8fa8f22cc466d0ac88216dcf1e03486370dca96 Mon Sep 17 00:00:00 2001 From: "Peter N. Steinmetz" Date: Sat, 7 Nov 2020 16:01:25 -0700 Subject: [PATCH 85/85] More conflict resolution for rebase. --- neo/rawio/neuralynxrawio.py | 10 +++++----- neo/test/rawiotest/test_neuralynxrawio.py | 10 +++------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/neo/rawio/neuralynxrawio.py b/neo/rawio/neuralynxrawio.py index 90ad70763..3cd9df911 100644 --- a/neo/rawio/neuralynxrawio.py +++ b/neo/rawio/neuralynxrawio.py @@ -495,7 +495,7 @@ def read_ncs_files(self, ncs_filenames): # parse the structure of the first file data0 = np.memmap(filename0, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) - hdr0 = NlxHeader.buildForFile(filename0) + hdr0 = NlxHeader.build_for_file(filename0) nb0 = NcsBlocksFactory.buildForNcsFile(data0, hdr0) # construct proper gap ranges free of lost samples artifacts @@ -523,7 +523,7 @@ def read_ncs_files(self, ncs_filenames): else: data = np.memmap(ncs_filename, dtype=self._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) - hdr = NlxHeader.buildForFile(ncs_filename) + hdr = NlxHeader.build_for_file(ncs_filename) nb = NcsBlocksFactory.buildForNcsFile(data, hdr) # Check that record block structure of each file is identical to the first. @@ -870,15 +870,15 @@ def buildForNcsFile(ncsMemMap, nlxHdr): string specifying type of data acquisition used, one of types returned by NlxHeader.typeOfRecording() """ - acqType = nlxHdr.typeOfRecording() + acqType = nlxHdr.type_of_recording() # old Neuralynx style with rounded whole microseconds for the samples if acqType == "PRE4": - freq = nlxHdr['SamplingFrequency'] + freq = nlxHdr['sampling_rate'] microsPerSampUsed = math.floor( WholeMicrosTimePositionBlock.getMicrosPerSampForFreq(freq)) sampFreqUsed = WholeMicrosTimePositionBlock.getFreqForMicrosPerSamp(microsPerSampUsed) - nb = NcsBlocks._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) + nb = NcsBlocksFactory._buildGivenActualFrequency(ncsMemMap, sampFreqUsed, math.floor(freq)) nb.sampFreqUsed = sampFreqUsed nb.microsPerSampUsed = microsPerSampUsed diff --git a/neo/test/rawiotest/test_neuralynxrawio.py b/neo/test/rawiotest/test_neuralynxrawio.py index ea6cfbc6b..c3ec1ee08 100644 --- a/neo/test/rawiotest/test_neuralynxrawio.py +++ b/neo/test/rawiotest/test_neuralynxrawio.py @@ -146,11 +146,7 @@ def testBuildGivenActualFrequency(self): data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) ncsBlocks = NcsBlocks() -<<<<<<< HEAD ncsBlocks.sampFreqUsed = 1/(35e-6) -======= - ncsBlocks.sampFreqUsed = 1 / 35e-6 ->>>>>>> ba82efe4... More PEP8 items. ncsBlocks.microsPerSampUsed = 35 ncsBlocks = NcsBlocksFactory._buildGivenActualFrequency(data0, ncsBlocks.sampFreqUsed, 27789) @@ -205,7 +201,7 @@ def test_ncsblocks_partial(self): self.assertEqual(data0.shape[0], 6690) self.assertEqual(data0['timestamp'][6689], 8515800549) # timestamp of last record - hdr = NlxHeader.buildForFile(filename) + hdr = NlxHeader.build_for_file(filename) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) self.assertEqual(nb.sampFreqUsed, 32000.012813673042) self.assertEqual(nb.microsPerSampUsed, 31.249987486652431) @@ -234,7 +230,7 @@ def testBuildUsingHeaderAndScanning(self): # Test early files where the frequency listed in the header is # floor(1e6/(actual number of microseconds between samples) filename = self.get_filename_path('Cheetah_v4.0.2/original_data/CSC14_trunc.Ncs') - hdr = NlxHeader.buildForFile(filename) + hdr = NlxHeader.build_for_file(filename) data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr) @@ -249,7 +245,7 @@ def testBuildUsingHeaderAndScanning(self): # test Cheetah 5.5.1, which is DigitalLynxSX and has two blocks of records # with a fairly large gap filename = self.get_filename_path('Cheetah_v5.5.1/original_data/Tet3a.ncs') - hdr = NlxHeader.buildForFile(filename) + hdr = NlxHeader.build_for_file(filename) data0 = np.memmap(filename, dtype=NeuralynxRawIO._ncs_dtype, mode='r', offset=NlxHeader.HEADER_SIZE) nb = NcsBlocksFactory.buildForNcsFile(data0, hdr)