neo-0.3.3/0000755000175000017500000000000012273723667013361 5ustar sgarciasgarcia00000000000000neo-0.3.3/setup.py0000755000175000017500000000270112273723542015066 0ustar sgarciasgarcia00000000000000#!/usr/bin/env python # -*- coding: utf-8 -*- from setuptools import setup import os long_description = open("README.rst").read() install_requires = ['numpy>=1.3.0', 'quantities>=0.9.0'] if os.environ.get('TRAVIS') == 'true' and \ os.environ.get('TRAVIS_PYTHON_VERSION').startswith('2.6'): install_requires.append('unittest2>=0.5.1') setup( name = "neo", version = '0.3.3', packages = ['neo', 'neo.core', 'neo.io', 'neo.test', 'neo.test.iotest'], install_requires=install_requires, author = "Neo authors and contributors", author_email = "sgarcia at olfac.univ-lyon1.fr", description = "Neo is a package for representing electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats", long_description = long_description, license = "BSD-3-Clause", url='http://neuralensemble.org/neo', classifiers = [ 'Development Status :: 4 - Beta', 'Intended Audience :: Science/Research', 'License :: OSI Approved :: BSD License', 'Natural Language :: English', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.3', 'Topic :: Scientific/Engineering'] ) neo-0.3.3/neo.egg-info/0000755000175000017500000000000012273723667015634 5ustar sgarciasgarcia00000000000000neo-0.3.3/neo.egg-info/top_level.txt0000644000175000017500000000000412273723667020360 0ustar sgarciasgarcia00000000000000neo neo-0.3.3/neo.egg-info/requires.txt0000644000175000017500000000003612273723667020233 0ustar sgarciasgarcia00000000000000numpy>=1.3.0 quantities>=0.9.0neo-0.3.3/neo.egg-info/SOURCES.txt0000644000175000017500000000676312273723667017534 0ustar sgarciasgarcia00000000000000MANIFEST.in README.rst setup.py doc/Makefile doc/make.bat doc/source/api_reference.rst doc/source/authors.rst doc/source/conf.py doc/source/core.rst doc/source/developers_guide.rst doc/source/examples.rst doc/source/gif2011workshop.rst doc/source/index.rst doc/source/install.rst doc/source/io.rst doc/source/io_developers_guide.rst doc/source/specific_annotations.rst doc/source/usecases.rst doc/source/whatisnew.rst doc/source/images/base_schematic.png doc/source/images/generate_diagram.py doc/source/images/multi_segment_diagram.png doc/source/images/multi_segment_diagram_spiketrain.png doc/source/images/neo_UML_French_workshop.png doc/source/images/neologo.png doc/source/images/neologo_light.png doc/source/images/simple_generated_diagram.png examples/generated_data.py examples/read_files.py examples/simple_plot_with_matplotlib.py neo/__init__.py neo/description.py neo/version.py neo.egg-info/PKG-INFO neo.egg-info/SOURCES.txt neo.egg-info/dependency_links.txt neo.egg-info/requires.txt neo.egg-info/top_level.txt neo/core/__init__.py neo/core/analogsignal.py neo/core/analogsignalarray.py neo/core/baseneo.py neo/core/block.py neo/core/epoch.py neo/core/epocharray.py neo/core/event.py neo/core/eventarray.py neo/core/irregularlysampledsignal.py neo/core/recordingchannel.py neo/core/recordingchannelgroup.py neo/core/segment.py neo/core/spike.py neo/core/spiketrain.py neo/core/unit.py neo/io/__init__.py neo/io/alphaomegaio.py neo/io/asciisignalio.py neo/io/asciispiketrainio.py neo/io/axonio.py neo/io/baseio.py neo/io/blackrockio.py neo/io/brainvisionio.py neo/io/brainwaredamio.py neo/io/brainwaref32io.py neo/io/brainwaresrcio.py neo/io/elanio.py neo/io/elphyio.py neo/io/exampleio.py neo/io/hdf5io.py neo/io/klustakwikio.py neo/io/micromedio.py neo/io/neomatlabio.py neo/io/neuroexplorerio.py neo/io/neuroscopeio.py neo/io/neuroshareio.py neo/io/pickleio.py neo/io/plexonio.py neo/io/pynnio.py neo/io/rawbinarysignalio.py neo/io/spike2io.py neo/io/tdtio.py neo/io/tools.py neo/io/winedrio.py neo/io/winwcpio.py neo/test/__init__.py neo/test/test_analogsignal.py neo/test/test_analogsignalarray.py neo/test/test_base.py neo/test/test_block.py neo/test/test_epoch.py neo/test/test_epocharray.py neo/test/test_event.py neo/test/test_eventarray.py neo/test/test_irregularysampledsignal.py neo/test/test_recordingchannel.py neo/test/test_recordingchannelgroup.py neo/test/test_segment.py neo/test/test_spike.py neo/test/test_spiketrain.py neo/test/test_unit.py neo/test/tools.py neo/test/iotest/__init__.py neo/test/iotest/common_io_test.py neo/test/iotest/generate_datasets.py neo/test/iotest/test_alphaomegaio.py neo/test/iotest/test_asciisignalio.py neo/test/iotest/test_asciispiketrainio.py neo/test/iotest/test_axonio.py neo/test/iotest/test_baseio.py neo/test/iotest/test_blackrockio.py neo/test/iotest/test_brainvisionio.py neo/test/iotest/test_brainwaredamio.py neo/test/iotest/test_brainwaref32io.py neo/test/iotest/test_brainwaresrcio.py neo/test/iotest/test_elanio.py neo/test/iotest/test_elphyio.py neo/test/iotest/test_exampleio.py neo/test/iotest/test_hdf5io.py neo/test/iotest/test_klustakwikio.py neo/test/iotest/test_micromedio.py neo/test/iotest/test_neomatlabio.py neo/test/iotest/test_neuroexplorerio.py neo/test/iotest/test_neuroscopeio.py neo/test/iotest/test_neuroshareio.py neo/test/iotest/test_plexonio.py neo/test/iotest/test_pynnio.py neo/test/iotest/test_rawbinarysignalio.py neo/test/iotest/test_spike2io.py neo/test/iotest/test_tdtio.py neo/test/iotest/test_winedrio.py neo/test/iotest/test_winwcpio.py neo/test/iotest/tools.pyneo-0.3.3/neo.egg-info/PKG-INFO0000644000175000017500000000577012273723667016742 0ustar sgarciasgarcia00000000000000Metadata-Version: 1.1 Name: neo Version: 0.3.3 Summary: Neo is a package for representing electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats Home-page: http://neuralensemble.org/neo Author: Neo authors and contributors Author-email: sgarcia at olfac.univ-lyon1.fr License: BSD-3-Clause Description: === Neo === Neo is a package for representing electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats, including Spike2, NeuroExplorer, AlphaOmega, Axon, Blackrock, Plexon, Tdt, and support for writing to a subset of these formats plus non-proprietary formats including HDF5. The goal of Neo is to improve interoperability between Python tools for analyzing, visualizing and generating electrophysiology data (such as OpenElectrophy, NeuroTools, G-node, Helmholtz, PyNN) by providing a common, shared object model. In order to be as lightweight a dependency as possible, Neo is deliberately limited to represention of data, with no functions for data analysis or visualization. Neo implements a hierarchical data model well adapted to intracellular and extracellular electrophysiology and EEG data with support for multi-electrodes (for example tetrodes). Neo's data objects build on the quantities_ package, which in turn builds on NumPy by adding support for physical dimensions. Thus neo objects behave just like normal NumPy arrays, but with additional metadata, checks for dimensional consistency and automatic unit conversion. Code status ----------- .. image:: https://secure.travis-ci.org/NeuralEnsemble/python-neo.png?branch=master :target: https://travis-ci.org/NeuralEnsemble/python-neo.png More information ---------------- - Home page: http://neuralensemble.org/neo - Mailing list: https://groups.google.com/forum/?fromgroups#!forum/neuralensemble - Documentation: http://packages.python.org/neo/ - Bug reports: https://github.com/NeuralEnsemble/python-neo/issues For installation instructions, see doc/source/install.rst :copyright: Copyright 2010-2014 by the Neo team, see AUTHORS. :license: 3-Clause Revised BSD License, see LICENSE.txt for details. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Topic :: Scientific/Engineering neo-0.3.3/neo.egg-info/dependency_links.txt0000644000175000017500000000000112273723667021702 0ustar sgarciasgarcia00000000000000 neo-0.3.3/examples/0000755000175000017500000000000012273723667015177 5ustar sgarciasgarcia00000000000000neo-0.3.3/examples/simple_plot_with_matplotlib.py0000644000175000017500000000204112265516260023345 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ This is an example for plotting neo object with maplotlib. """ import urllib import numpy as np import quantities as pq from matplotlib import pyplot import neo url = 'https://portal.g-node.org/neo/' distantfile = url + 'neuroexplorer/File_neuroexplorer_2.nex' localfile = 'File_neuroexplorer_2.nex' urllib.urlretrieve(distantfile, localfile) reader = neo.io.NeuroExplorerIO(filename='File_neuroexplorer_2.nex') bl = reader.read(cascade=True, lazy=False)[0] for seg in bl.segments: fig = pyplot.figure() ax1 = fig.add_subplot(2, 1, 1) ax2 = fig.add_subplot(2, 1, 2) ax1.set_title(seg.file_origin) mint = 0 * pq.s maxt = np.inf * pq.s for i, asig in enumerate(seg.analogsignals): times = asig.times.rescale('s').magnitude asig = asig.rescale('mV').magnitude ax1.plot(times, asig) trains = [st.rescale('s').magnitude for st in seg.spiketrains] colors = pyplot.cm.jet(np.linspace(0, 1, len(seg.spiketrains))) ax2.eventplot(trains, colors=colors) pyplot.show() neo-0.3.3/examples/generated_data.py0000644000175000017500000001142512273723542020473 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ This is an example for creating simple plots from various Neo structures. It includes a function that generates toy data. """ from __future__ import division # Use same division in Python 2 and 3 import numpy as np import quantities as pq from matplotlib import pyplot as plt import neo def generate_block(n_segments=3, n_channels=8, n_units=3, data_samples=1000, feature_samples=100): """ Generate a block with a single recording channel group and a number of segments, recording channels and units with associated analog signals and spike trains. """ feature_len = feature_samples / data_samples # Create container and grouping objects segments = [neo.Segment(index=i) for i in range(n_segments)] rcg = neo.RecordingChannelGroup(name='T0') for i in range(n_channels): rc = neo.RecordingChannel(name='C%d' % i, index=i) rc.recordingchannelgroups = [rcg] rcg.recordingchannels.append(rc) units = [neo.Unit('U%d' % i) for i in range(n_units)] rcg.units = units block = neo.Block() block.segments = segments block.recordingchannelgroups = [rcg] # Create synthetic data for seg in segments: feature_pos = np.random.randint(0, data_samples - feature_samples) # Analog signals: Noise with a single sinewave feature wave = 3 * np.sin(np.linspace(0, 2 * np.pi, feature_samples)) for rc in rcg.recordingchannels: sig = np.random.randn(data_samples) sig[feature_pos:feature_pos + feature_samples] += wave signal = neo.AnalogSignal(sig * pq.mV, sampling_rate=1 * pq.kHz) seg.analogsignals.append(signal) rc.analogsignals.append(signal) # Spike trains: Random spike times with elevated rate in short period feature_time = feature_pos / data_samples for u in units: random_spikes = np.random.rand(20) feature_spikes = np.random.rand(5) * feature_len + feature_time spikes = np.hstack([random_spikes, feature_spikes]) train = neo.SpikeTrain(spikes * pq.s, 1 * pq.s) seg.spiketrains.append(train) u.spiketrains.append(train) neo.io.tools.create_many_to_one_relationship(block) return block block = generate_block() # In this example, we treat each segment in turn, averaging over the channels # in each: for seg in block.segments: print("Analysing segment %d" % seg.index) siglist = seg.analogsignals time_points = siglist[0].times avg = np.mean(siglist, axis=0) # Average over signals of Segment plt.figure() plt.plot(time_points, avg) plt.title("Peak response in segment %d: %f" % (seg.index, avg.max())) # The second alternative is spatial traversal of the data (by channel), with # averaging over trials. For example, perhaps you wish to see which physical # location produces the strongest response, and each stimulus was the same: # We assume that our block has only 1 RecordingChannelGroup and each # RecordingChannel only has 1 AnalogSignal. rcg = block.recordingchannelgroups[0] for rc in rcg.recordingchannels: print("Analysing channel %d: %s" % (rc.index, rc.name)) siglist = rc.analogsignals time_points = siglist[0].times avg = np.mean(siglist, axis=0) # Average over signals of RecordingChannel plt.figure() plt.plot(time_points, avg) plt.title("Average response on channel %d" % rc.index) # There are three ways to access the spike train data: by Segment, # by RecordingChannel or by Unit. # By Segment. In this example, each Segment represents data from one trial, # and we want a peristimulus time histogram (PSTH) for each trial from all # Units combined: for seg in block.segments: print("Analysing segment %d" % seg.index) stlist = [st - st.t_start for st in seg.spiketrains] count, bins = np.histogram(np.hstack(stlist)) plt.figure() plt.bar(bins[:-1], count, width=bins[1] - bins[0]) plt.title("PSTH in segment %d" % seg.index) # By Unit. Now we can calculate the PSTH averaged over trials for each Unit: for unit in block.list_units: stlist = [st - st.t_start for st in unit.spiketrains] count, bins = np.histogram(np.hstack(stlist)) plt.figure() plt.bar(bins[:-1], count, width=bins[1] - bins[0]) plt.title("PSTH of unit %s" % unit.name) # By RecordingChannelGroup. Here we calculate a PSTH averaged over trials by # channel location, blending all Units: for rcg in block.recordingchannelgroups: stlist = [] for unit in rcg.units: stlist.extend([st - st.t_start for st in unit.spiketrains]) count, bins = np.histogram(np.hstack(stlist)) plt.figure() plt.bar(bins[:-1], count, width=bins[1] - bins[0]) plt.title("PSTH blend of recording channel group %s" % rcg.name) plt.show() neo-0.3.3/examples/read_files.py0000644000175000017500000000211012265516260017626 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ This is an example for reading files with neo.io """ import urllib import neo # Plexon files distantfile = 'https://portal.g-node.org/neo/plexon/File_plexon_3.plx' localfile = './File_plexon_3.plx' urllib.urlretrieve(distantfile, localfile) #create a reader reader = neo.io.PlexonIO(filename='File_plexon_3.plx') # read the blocks blks = reader.read(cascade=True, lazy=False) print blks # acces to segments for blk in blks: for seg in blk.segments: print seg for asig in seg.analogsignals: print asig for st in seg.spiketrains: print st # CED Spike2 files distantfile = 'https://portal.g-node.org/neo/spike2/File_spike2_1.smr' localfile = './File_spike2_1.smr' urllib.urlretrieve(distantfile, localfile) #create a reader reader = neo.io.Spike2IO(filename='File_spike2_1.smr') # read the block bl = reader.read(cascade=True, lazy=False)[0] print bl # acces to segments for seg in bl.segments: print seg for asig in seg.analogsignals: print asig for st in seg.spiketrains: print st neo-0.3.3/README.rst0000644000175000017500000000345312273723542015045 0ustar sgarciasgarcia00000000000000=== Neo === Neo is a package for representing electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats, including Spike2, NeuroExplorer, AlphaOmega, Axon, Blackrock, Plexon, Tdt, and support for writing to a subset of these formats plus non-proprietary formats including HDF5. The goal of Neo is to improve interoperability between Python tools for analyzing, visualizing and generating electrophysiology data (such as OpenElectrophy, NeuroTools, G-node, Helmholtz, PyNN) by providing a common, shared object model. In order to be as lightweight a dependency as possible, Neo is deliberately limited to represention of data, with no functions for data analysis or visualization. Neo implements a hierarchical data model well adapted to intracellular and extracellular electrophysiology and EEG data with support for multi-electrodes (for example tetrodes). Neo's data objects build on the quantities_ package, which in turn builds on NumPy by adding support for physical dimensions. Thus neo objects behave just like normal NumPy arrays, but with additional metadata, checks for dimensional consistency and automatic unit conversion. Code status ----------- .. image:: https://secure.travis-ci.org/NeuralEnsemble/python-neo.png?branch=master :target: https://travis-ci.org/NeuralEnsemble/python-neo.png More information ---------------- - Home page: http://neuralensemble.org/neo - Mailing list: https://groups.google.com/forum/?fromgroups#!forum/neuralensemble - Documentation: http://packages.python.org/neo/ - Bug reports: https://github.com/NeuralEnsemble/python-neo/issues For installation instructions, see doc/source/install.rst :copyright: Copyright 2010-2014 by the Neo team, see AUTHORS. :license: 3-Clause Revised BSD License, see LICENSE.txt for details. neo-0.3.3/neo/0000755000175000017500000000000012273723667014142 5ustar sgarciasgarcia00000000000000neo-0.3.3/neo/io/0000755000175000017500000000000012273723667014551 5ustar sgarciasgarcia00000000000000neo-0.3.3/neo/io/blackrockio.py0000644000175000017500000004305112273723542017401 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Module for reading binary file from Blackrock format. """ import logging import struct import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import (Block, Segment, RecordingChannel, RecordingChannelGroup, AnalogSignal) from neo.io import tools class BlackrockIO(BaseIO): """ Class for reading/writing data in a BlackRock Neuroshare ns5 files. """ # Class variables demonstrating capabilities of this IO is_readable = True # This a only reading class is_writable = True # write is not supported # This IO can only manipulate continuous data, not spikes or events supported_objects = [Block, Segment, AnalogSignal, RecordingChannelGroup, RecordingChannel] # Keep things simple by always returning a block readable_objects = [Block] # And write a block writeable_objects = [Block] # Not sure what these do, if anything has_header = False is_streameable = False # The IO name and the file extensions it uses name = 'Blackrock' extensions = ['ns5'] # Operates on *.ns5 files mode = 'file' # GUI defaults for reading # Most information is acquired from the file header. read_params = { Block: [ #('rangemin' , { 'value' : -10 } ), #('rangemax' , { 'value' : 10 } ), ] } # GUI defaults for writing (not supported) write_params = None def __init__(self, filename, full_range=8192.*pq.mV) : """Initialize Blackrock reader. **Arguments** filename: string, the filename to read full_range: Quantity, the full-scale analog range of the data. This is set by your digitizing hardware. It should be in volts or millivolts. """ BaseIO.__init__(self) self.filename = filename self.full_range = full_range # The reading methods. The `lazy` and `cascade` parameters are imposed # by neo.io API def read_block(self, lazy=False, cascade=True, n_starts=None, n_stops=None, channel_list=None): """Reads the file and returns contents as a Block. The Block contains one Segment for each entry in zip(n_starts, n_stops). If these parameters are not specified, the default is to store all data in one Segment. The Block also contains one RecordingChannelGroup for all channels. n_starts: list or array of starting times of each Segment in samples from the beginning of the file. n_stops: similar, stopping times of each Segment channel_list: list of channel numbers to get. The neural data channels are 1 - 128. The analog inputs are 129 - 144. The default is to acquire all channels. Returns: Block object containing the data. """ # Create block block = Block(file_origin=self.filename) if not cascade: return block self.loader = Loader(self.filename) self.loader.load_file() self.header = self.loader.header # If channels not specified, get all if channel_list is None: channel_list = self.loader.get_neural_channel_numbers() # If not specified, load all as one Segment if n_starts is None: n_starts = [0] n_stops = [self.loader.header.n_samples] #~ # Add channel hierarchy #~ rcg = RecordingChannelGroup(name='allchannels', #~ description='group of all channels', file_origin=self.filename) #~ block.recordingchannelgroups.append(rcg) #~ self.channel_number_to_recording_channel = {} #~ # Add each channel at a time to hierarchy #~ for ch in channel_list: #~ ch_object = RecordingChannel(name='channel%d' % ch, #~ file_origin=self.filename, index=ch) #~ rcg.channel_indexes.append(ch_object.index) #~ rcg.channel_names.append(ch_object.name) #~ rcg.recordingchannels.append(ch_object) #~ self.channel_number_to_recording_channel[ch] = ch_object # Iterate through n_starts and n_stops and add one Segment # per each. for n, (t1, t2) in enumerate(zip(n_starts, n_stops)): # Create segment and add metadata seg = self.read_segment(n_start=t1, n_stop=t2, chlist=channel_list, lazy=lazy, cascade=cascade) seg.name = 'Segment %d' % n seg.index = n t1sec = t1 / self.loader.header.f_samp t2sec = t2 / self.loader.header.f_samp seg.description = 'Segment %d from %f to %f' % (n, t1sec, t2sec) # Link to block block.segments.append(seg) # Create hardware view, and bijectivity tools.populate_RecordingChannel(block) tools.create_many_to_one_relationship(block) return block def read_segment(self, n_start, n_stop, chlist=None, lazy=False, cascade=True): """Reads a Segment from the file and stores in database. The Segment will contain one AnalogSignal for each channel and will go from n_start to n_stop (in samples). Arguments: n_start : time in samples that the Segment begins n_stop : time in samples that the Segment ends Python indexing is used, so n_stop is not inclusive. Returns a Segment object containing the data. """ # If no channel numbers provided, get all of them if chlist is None: chlist = self.loader.get_neural_channel_numbers() # Conversion from bits to full_range units conversion = self.full_range / 2**(8*self.header.sample_width) # Create the Segment seg = Segment(file_origin=self.filename) t_start = float(n_start) / self.header.f_samp t_stop = float(n_stop) / self.header.f_samp seg.annotate(t_start=t_start) seg.annotate(t_stop=t_stop) # Load data from each channel and store for ch in chlist: if lazy: sig = np.array([]) * conversion else: # Get the data from the loader sig = np.array(\ self.loader._get_channel(ch)[n_start:n_stop]) * conversion # Create an AnalogSignal with the data in it anasig = AnalogSignal(signal=sig, sampling_rate=self.header.f_samp*pq.Hz, t_start=t_start*pq.s, file_origin=self.filename, description='Channel %d from %f to %f' % (ch, t_start, t_stop), channel_index=int(ch)) if lazy: anasig.lazy_shape = n_stop-n_start # Link the signal to the segment seg.analogsignals.append(anasig) # Link the signal to the recording channel from which it came #rc = self.channel_number_to_recording_channel[ch] #rc.analogsignals.append(anasig) return seg def write_block(self, block): """Writes block to `self.filename`. *.ns5 BINARY FILE FORMAT The following information is contained in the first part of the header file. The size in bytes, the variable name, the data type, and the meaning are given below. Everything is little-endian. 8B. File_Type_ID. char. Always "NEURALSG" 16B. File_Spec. char. Always "30 kS/s\0" 4B. Period. uint32. Always 1. 4B. Channel_Count. uint32. Generally 32 or 34. Channel_Count*4B. uint32. Channel_ID. One uint32 for each channel. Thus the total length of the header is 8+16+4+4+Channel_Count*4. Immediately after this header, the raw data begins. Each sample is a 2B signed int16. For our hardware, the conversion factor is 4096.0 / 2**16 mV/bit. The samples for each channel are interleaved, so the first Channel_Count samples correspond to the first sample from each channel, in the same order as the channel id's in the header. Variable names are consistent with the Neuroshare specification. """ fi = open(self.filename, 'wb') self._write_header(block, fi) # Write each segment in order for seg in block.segments: # Create a 2d numpy array of analogsignals converted to bytes all_signals = np.array([ np.rint(sig * 2**16 / self.full_range) for sig in seg.analogsignals], dtype=np.int) # Write to file. We transpose because channel changes faster # than time in this format. for vals in all_signals.transpose(): fi.write(struct.pack('<%dh' % len(vals), *vals)) fi.close() def _write_header(self, block, fi): """Write header info about block to fi""" if len(block.segments) > 0: channel_indexes = channel_indexes_in_segment(block.segments[0]) else: channel_indexes = [] # type of file fi.write('NEURALSG') # sampling rate, in text and integer fi.write('30 kS/s\0') for _ in range(8): fi.write('\0') fi.write(struct.pack(' 128) and (x <= 144), self.header.Channel_ID)) - 128 def get_neural_channel_numbers(self): return np.array(filter(lambda x: x <= 128, self.header.Channel_ID)) neo-0.3.3/neo/io/baseio.py0000644000175000017500000002014112273723542016353 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ baseio ====== Classes ------- BaseIO - abstract class which should be overridden, managing how a file will load/write its data If you want a model for developing a new IO start from exampleIO. """ import collections from neo.core import (AnalogSignal, AnalogSignalArray, Block, Epoch, EpochArray, Event, EventArray, IrregularlySampledSignal, RecordingChannel, RecordingChannelGroup, Segment, Spike, SpikeTrain, Unit) from neo.io.tools import create_many_to_one_relationship read_error = "This type is not supported by this file format for reading" write_error = "This type is not supported by this file format for writing" class BaseIO(object): """ Generic class to handle all the file read/write methods for the key objects of the core class. This template is file-reading/writing oriented but it can also handle data read from/written to a database such as TDT sytem tanks or SQLite files. This is an abstract class that will be subclassed for each format The key methods of the class are: - ``read()`` - Read the whole object structure, return a list of Block objects - ``read_block(lazy=True, cascade=True, **params)`` - Read Block object from file with some parameters - ``read_segment(lazy=True, cascade=True, **params)`` - Read Segment object from file with some parameters - ``read_spiketrainlist(lazy=True, cascade=True, **params)`` - Read SpikeTrainList object from file with some parameters - ``write()`` - Write the whole object structure - ``write_block(**params)`` - Write Block object to file with some parameters - ``write_segment(**params)`` - Write Segment object to file with some parameters - ``write_spiketrainlist(**params)`` - Write SpikeTrainList object to file with some parameters The class can also implement these methods: - ``read_XXX(lazy=True, cascade=True, **params)`` - ``write_XXX(**params)`` where XXX could be one of the objects supported by the IO Each class is able to declare what can be accessed or written directly discribed by **readable_objects** and **readable_objects**. The object types can be one of the classes defined in neo.core (Block, Segment, AnalogSignal, ...) Each class do not necessary support all the whole neo hierarchy but part of it. This is discribe with **supported_objects**. All IOs must support at least Block with a read_block() ** start a new IO ** If you want to implement your own file format, you should create a class that will inherit from this BaseFile class and implement the previous methods. See ExampleIO in exampleio.py """ is_readable = False is_writable = False supported_objects = [] readable_objects = [] writeable_objects = [] has_header = False is_streameable = False read_params = {} write_params = {} name = 'BaseIO' description = 'This IO does not read or write anything' extentions = [] mode = 'file' # or 'fake' or 'dir' or 'database' def __init__(self, filename=None, **kargs): self.filename = filename ######## General read/write methods ####################### def read(self, lazy=False, cascade=True, **kargs): if Block in self.readable_objects: if (hasattr(self, 'read_all_blocks') and callable(getattr(self, 'read_all_blocks'))): return self.read_all_blocks(lazy=lazy, cascade=cascade, **kargs) return [self.read_block(lazy=lazy, cascade=cascade, **kargs)] elif Segment in self.readable_objects: bl = Block(name='One segment only') if not cascade: return bl seg = self.read_segment(lazy=lazy, cascade=cascade, **kargs) bl.segments.append(seg) create_many_to_one_relationship(bl) return [bl] else: raise NotImplementedError def write(self, bl, **kargs): if Block in self.writeable_objects: if isinstance(bl, collections.Sequence): assert hasattr(self, 'write_all_blocks'), \ '%s does not offer to store a sequence of blocks' % \ self.__class__.__name__ self.write_all_blocks(bl, **kargs) else: self.write_block(bl, **kargs) elif Segment in self.writeable_objects: assert len(bl.segments) == 1, \ '%s is based on segment so if you try to write a block it ' + \ 'must contain only one Segment' % self.__class__.__name__ self.write_segment(bl.segments[0], **kargs) else: raise NotImplementedError ######## All individual read methods ####################### def read_block(self, **kargs): assert(Block in self.readable_objects), read_error def read_segment(self, **kargs): assert(Segment in self.readable_objects), read_error def read_unit(self, **kargs): assert(Unit in self.readable_objects), read_error def read_spiketrain(self, **kargs): assert(SpikeTrain in self.readable_objects), read_error def read_spike(self, **kargs): assert(Spike in self.readable_objects), read_error def read_analogsignal(self, **kargs): assert(AnalogSignal in self.readable_objects), read_error def read_irregularlysampledsignal(self, **kargs): assert(IrregularlySampledSignal in self.readable_objects), read_error def read_analogsignalarray(self, **kargs): assert(AnalogSignalArray in self.readable_objects), read_error def read_recordingchannelgroup(self, **kargs): assert(RecordingChannelGroup in self.readable_objects), read_error def read_recordingchannel(self, **kargs): assert(RecordingChannel in self.readable_objects), read_error def read_event(self, **kargs): assert(Event in self.readable_objects), read_error def read_eventarray(self, **kargs): assert(EventArray in self.readable_objects), read_error def read_epoch(self, **kargs): assert(Epoch in self.readable_objects), read_error def read_epocharray(self, **kargs): assert(EpochArray in self.readable_objects), read_error ######## All individual write methods ####################### def write_block(self, bl, **kargs): assert(Block in self.writeable_objects), write_error def write_segment(self, seg, **kargs): assert(Segment in self.writeable_objects), write_error def write_unit(self, ut, **kargs): assert(Unit in self.writeable_objects), write_error def write_spiketrain(self, sptr, **kargs): assert(SpikeTrain in self.writeable_objects), write_error def write_spike(self, sp, **kargs): assert(Spike in self.writeable_objects), write_error def write_analogsignal(self, anasig, **kargs): assert(AnalogSignal in self.writeable_objects), write_error def write_irregularlysampledsignal(self, irsig, **kargs): assert(IrregularlySampledSignal in self.writeable_objects), write_error def write_analogsignalarray(self, anasigar, **kargs): assert(AnalogSignalArray in self.writeable_objects), write_error def write_recordingchannelgroup(self, rcg, **kargs): assert(RecordingChannelGroup in self.writeable_objects), write_error def write_recordingchannel(self, rc, **kargs): assert(RecordingChannel in self.writeable_objects), write_error def write_event(self, ev, **kargs): assert(Event in self.writeable_objects), write_error def write_eventarray(self, ea, **kargs): assert(EventArray in self.writeable_objects), write_error def write_epoch(self, ep, **kargs): assert(Epoch in self.writeable_objects), write_error def write_epocharray(self, epa, **kargs): assert(EpochArray in self.writeable_objects), write_error neo-0.3.3/neo/io/tools.py0000644000175000017500000001507012273723542016256 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tools for IO coder: * For creating parent (many_to_one_relationship) * Creating RecordingChannel and making links with AnalogSignals and SPikeTrains """ import collections import numpy as np from neo.core import (AnalogSignal, AnalogSignalArray, Block, Epoch, EpochArray, Event, EventArray, IrregularlySampledSignal, RecordingChannel, RecordingChannelGroup, Segment, Spike, SpikeTrain, Unit) from neo.description import one_to_many_relationship #def finalize_block(block): # populate_RecordingChannel(block) # create_many_to_one_relationship(block) # Special case this tricky many-to-many relationship # we still need links from recordingchannel to analogsignal # for rcg in block.recordingchannelgroups: # for rc in rcg.recordingchannels: # create_many_to_one_relationship(rc) def create_many_to_one_relationship(ob, force=False): """ Create many_to_one relationship when one_to_many relationships exist. Ex: For each Segment in block.segments it sets segment.block to the parent Block. It is a utility at the end of creating a Block for IO. Note: This is recursive. It works on Block but also work on others neo objects. Usage: >>> create_many_to_one_relationship(a_block) >>> create_many_to_one_relationship(a_block, force=True) You want to run populate_RecordingChannel first, because this will create new objects that this method will link up. If force is True overwrite any existing relationships """ # Determine what class was passed, and whether it has children classname = ob.__class__.__name__ if classname not in one_to_many_relationship: # No children return # Iterate through children and build backward links for childname in one_to_many_relationship[classname]: # Doesn't have links to children if not hasattr(ob, childname.lower()+'s'): continue # get a list of children of type childname and iterate through sub = getattr(ob, childname.lower()+'s') for child in sub: # set a link to parent `ob`, of class `classname` if getattr(child, classname.lower()) is None or force: setattr(child, classname.lower(), ob) # recursively: create_many_to_one_relationship(child, force=force) def populate_RecordingChannel(bl, remove_from_annotation=True): """ When a Block is Block>Segment>AnalogSIgnal this function auto create all RecordingChannel following these rules: * when 'channel_index ' is in AnalogSIgnal the corresponding RecordingChannel is created. * 'channel_index ' is then set to None if remove_from_annotation * only one RecordingChannelGroup is created It is a utility at the end of creating a Block for IO. Usage: >>> populate_RecordingChannel(a_block) """ recordingchannels = {} for seg in bl.segments: for anasig in seg.analogsignals: if getattr(anasig, 'channel_index', None) is not None: ind = int(anasig.channel_index) if ind not in recordingchannels: recordingchannels[ind] = RecordingChannel(index=ind) if 'channel_name' in anasig.annotations: channel_name = anasig.annotations['channel_name'] recordingchannels[ind].name = channel_name if remove_from_annotation: anasig.annotations.pop('channel_name') recordingchannels[ind].analogsignals.append(anasig) anasig.recordingchannel = recordingchannels[ind] if remove_from_annotation: anasig.channel_index = None indexes = np.sort(list(recordingchannels.keys())).astype('i') names = np.array([recordingchannels[idx].name for idx in indexes], dtype='S') rcg = RecordingChannelGroup(name='all channels', channel_indexes=indexes, channel_names=names) bl.recordingchannelgroups.append(rcg) for ind in indexes: # many to many relationship rcg.recordingchannels.append(recordingchannels[ind]) recordingchannels[ind].recordingchannelgroups.append(rcg) def iteritems(D): try: return D.iteritems() # Python 2 except AttributeError: return D.items() # Python 3 class LazyList(collections.MutableSequence): """ An enhanced list that can load its members on demand. Behaves exactly like a regular list for members that are Neo objects. Each item should contain the information that ``load_lazy_cascade`` needs to load the respective object. """ _container_objects = set( [Block, Segment, RecordingChannelGroup, RecordingChannel, Unit]) _neo_objects = _container_objects.union( [AnalogSignal, AnalogSignalArray, Epoch, EpochArray, Event, EventArray, IrregularlySampledSignal, Spike, SpikeTrain]) def __init__(self, io, lazy, items=None): """ :param io: IO instance that can load items. :param lazy: Lazy parameter with which the container object using the list was loaded. :param items: Optional, initial list of items. """ if items is None: self._data = [] else: self._data = items self._lazy = lazy self._io = io def __getitem__(self, index): item = self._data.__getitem__(index) if isinstance(index, slice): return LazyList(self._io, item) if type(item) in self._neo_objects: return item loaded = self._io.load_lazy_cascade(item, self._lazy) self._data[index] = loaded return loaded def __delitem__(self, index): self._data.__delitem__(index) def __len__(self): return self._data.__len__() def __setitem__(self, index, value): self._data.__setitem__(index, value) def insert(self, index, value): self._data.insert(index, value) def append(self, value): self._data.append(value) def reverse(self): self._data.reverse() def extend(self, values): self._data.extend(values) def remove(self, value): self._data.remove(value) def __str__(self): return '<' + self.__class__.__name__ + '>' + self._data.__str__() def __repr__(self): return '<' + self.__class__.__name__ + '>' + self._data.__repr__() neo-0.3.3/neo/io/winedrio.py0000644000175000017500000001073512273723542016741 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Classe for reading data from WinEdr, a software tool written by John Dempster. WinEdr is free: http://spider.science.strath.ac.uk/sipbs/software.htm Depend on: Supported : Read Author: sgarcia """ import os import struct import sys import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import Segment, AnalogSignal from neo.io.tools import create_many_to_one_relationship PY3K = (sys.version_info[0] == 3) class WinEdrIO(BaseIO): """ Class for reading data from WinEDR. Usage: >>> from neo import io >>> r = io.WinEdrIO(filename='File_WinEDR_1.EDR') >>> seg = r.read_segment(lazy=False, cascade=True,) >>> print seg.analogsignals [] """ is_readable = True is_writable = False supported_objects = [ Segment , AnalogSignal ] readable_objects = [Segment] writeable_objects = [] has_header = False is_streameable = False read_params = { Segment : [ ], } write_params = None name = 'WinEDR' extensions = [ 'EDR' ] mode = 'file' def __init__(self , filename = None) : """ This class read a WinEDR file. Arguments: filename : the filename """ BaseIO.__init__(self) self.filename = filename def read_segment(self , lazy = False, cascade = True): seg = Segment( file_origin = os.path.basename(self.filename), ) if not cascade: return seg fid = open(self.filename , 'rb') headertext = fid.read(2048) if PY3K: headertext = headertext.decode('ascii') header = {} for line in headertext.split('\r\n'): if '=' not in line : continue #print '#' , line , '#' key,val = line.split('=') if key in ['NC', 'NR','NBH','NBA','NBD','ADCMAX','NP','NZ','ADCMAX' ] : val = int(val) elif key in ['AD', 'DT', ] : val = val.replace(',','.') val = float(val) header[key] = val if not lazy: data = np.memmap(self.filename , np.dtype('i2') , 'r', #shape = (header['NC'], header['NP']) , shape = (header['NP']/header['NC'],header['NC'], ) , offset = header['NBH']) for c in range(header['NC']): YCF = float(header['YCF%d'%c].replace(',','.')) YAG = float(header['YAG%d'%c].replace(',','.')) YZ = float(header['YZ%d'%c].replace(',','.')) ADCMAX = header['ADCMAX'] AD = header['AD'] DT = header['DT'] if 'TU' in header: if header['TU'] == 'ms': DT *= .001 unit = header['YU%d'%c] try : unit = pq.Quantity(1., unit) except: unit = pq.Quantity(1., '') if lazy: signal = [ ] * unit else: signal = (data[:,header['YO%d'%c]].astype('f4')-YZ) *AD/( YCF*YAG*(ADCMAX+1)) * unit ana = AnalogSignal(signal, sampling_rate=pq.Hz / DT, t_start=0. * pq.s, name=header['YN%d' % c], channel_index=c) if lazy: ana.lazy_shape = header['NP']/header['NC'] seg.analogsignals.append(ana) create_many_to_one_relationship(seg) return seg AnalysisDescription = [ ('RecordStatus','8s'), ('RecordType','4s'), ('GroupNumber','f'), ('TimeRecorded','f'), ('SamplingInterval','f'), ('VMax','8f'), ] class HeaderReader(): def __init__(self,fid ,description ): self.fid = fid self.description = description def read_f(self, offset =0): self.fid.seek(offset) d = { } for key, fmt in self.description : val = struct.unpack(fmt , self.fid.read(struct.calcsize(fmt))) if len(val) == 1: val = val[0] else : val = list(val) d[key] = val return d neo-0.3.3/neo/io/tdtio.py0000644000175000017500000003443612273723542016250 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Class for reading data from from Tucker Davis TTank format. Terminology: TDT hold data with tanks (actually a directory). And tanks hold sub block (sub directories). Tanks correspond to neo.Block and tdt block correspond to neo.Segment. Note the name Block is ambiguous because it does not refer to same thing in TDT terminilogy and neo. Depend on: Supported : Read Author: sgarcia """ import os import struct import sys import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import Block, Segment, AnalogSignal, SpikeTrain, EventArray from neo.io.tools import create_many_to_one_relationship, iteritems PY3K = (sys.version_info[0] == 3) class TdtIO(BaseIO): """ Class for reading data from from Tucker Davis TTank format. Usage: >>> from neo import io >>> r = io.TdtIO(dirname='aep_05') >>> bl = r.read_block(lazy=False, cascade=True) >>> print bl.segments [] >>> print bl.segments[0].analogsignals [] >>> print bl.segments[0].eventarrays [] """ is_readable = True is_writable = False supported_objects = [Block, Segment , AnalogSignal, EventArray ] readable_objects = [Block] writeable_objects = [] has_header = False is_streameable = False read_params = { Block : [ ], } write_params = None name = 'TDT' extensions = [ ] mode = 'dir' def __init__(self , dirname = None) : """ This class read a WinEDR wcp file. **Arguments** Arguments: dirname: path of the TDT tank (a directory) """ BaseIO.__init__(self) self.dirname = dirname if self.dirname.endswith('/'): self.dirname = self.dirname[:-1] def read_block(self, lazy = False, cascade = True, ): bl = Block() tankname = os.path.basename(self.dirname) bl.file_origin = tankname if not cascade : return bl for blockname in os.listdir(self.dirname): if blockname == 'TempBlk': continue subdir = os.path.join(self.dirname,blockname) if not os.path.isdir(subdir): continue seg = Segment(name = blockname) bl.segments.append( seg) global_t_start = None # Step 1 : first loop for counting - tsq file tsq = open(os.path.join(subdir, tankname+'_'+blockname+'.tsq'), 'rb') hr = HeaderReader(tsq, TsqDescription) allsig = { } allspiketr = { } allevent = { } while 1: h= hr.read_f() if h==None:break channel, code , evtype = h['channel'], h['code'], h['evtype'] if Types[evtype] == 'EVTYPE_UNKNOWN': pass elif Types[evtype] == 'EVTYPE_MARK' : if global_t_start is None: global_t_start = h['timestamp'] elif Types[evtype] == 'EVTYPE_SCALER' : # TODO pass elif Types[evtype] == 'EVTYPE_STRON' or \ Types[evtype] == 'EVTYPE_STROFF': # EVENTS if code not in allevent: allevent[code] = { } if channel not in allevent[code]: ea = EventArray(name = code , channel_index = channel) # for counting: ea.lazy_shape = 0 ea.maxlabelsize = 0 allevent[code][channel] = ea allevent[code][channel].lazy_shape += 1 strobe, = struct.unpack('d' , struct.pack('q' , h['eventoffset'])) strobe = str(strobe) if len(strobe)>= allevent[code][channel].maxlabelsize: allevent[code][channel].maxlabelsize = len(strobe) #~ ev = Event() #~ ev.time = h['timestamp'] - global_t_start #~ ev.name = code #~ # it the strobe attribute masked with eventoffset #~ strobe, = struct.unpack('d' , struct.pack('q' , h['eventoffset'])) #~ ev.label = str(strobe) #~ seg._events.append( ev ) elif Types[evtype] == 'EVTYPE_SNIP' : if code not in allspiketr: allspiketr[code] = { } if channel not in allspiketr[code]: allspiketr[code][channel] = { } if h['sortcode'] not in allspiketr[code][channel]: sptr = SpikeTrain([ ], units = 's', name = str(h['sortcode']), #t_start = global_t_start, t_start = 0.*pq.s, t_stop = 0.*pq.s, # temporary left_sweep = (h['size']-10.)/2./h['frequency'] * pq.s, sampling_rate = h['frequency'] * pq.Hz, ) #~ sptr.channel = channel #sptr.annotations['channel_index'] = channel sptr.annotate(channel_index = channel) # for counting: sptr.lazy_shape = 0 sptr.pos = 0 sptr.waveformsize = h['size']-10 #~ sptr.name = str(h['sortcode']) #~ sptr.t_start = global_t_start #~ sptr.sampling_rate = h['frequency'] #~ sptr.left_sweep = (h['size']-10.)/2./h['frequency'] #~ sptr.right_sweep = (h['size']-10.)/2./h['frequency'] #~ sptr.waveformsize = h['size']-10 allspiketr[code][channel][h['sortcode']] = sptr allspiketr[code][channel][h['sortcode']].lazy_shape += 1 elif Types[evtype] == 'EVTYPE_STREAM': if code not in allsig: allsig[code] = { } if channel not in allsig[code]: #~ print 'code', code, 'channel', channel anaSig = AnalogSignal([] * pq.V, name=code, sampling_rate= h['frequency'] * pq.Hz, t_start=(h['timestamp'] - global_t_start) * pq.s, channel_index=channel) anaSig.lazy_dtype = np.dtype(DataFormats[h['dataformat']]) anaSig.pos = 0 # for counting: anaSig.lazy_shape = 0 #~ anaSig.pos = 0 allsig[code][channel] = anaSig allsig[code][channel].lazy_shape += (h['size']*4-40)/anaSig.dtype.itemsize if not lazy: # Step 2 : allocate memory for code, v in iteritems(allsig): for channel, anaSig in iteritems(v): v[channel] = anaSig.duplicate_with_new_array(np.zeros((anaSig.lazy_shape) , dtype = anaSig.lazy_dtype)*pq.V ) v[channel].pos = 0 for code, v in iteritems(allevent): for channel, ea in iteritems(v): ea.times = np.empty( (ea.lazy_shape) ) * pq.s ea.labels = np.empty( (ea.lazy_shape), dtype = 'S'+str(ea.maxlabelsize) ) ea.pos = 0 for code, v in iteritems(allspiketr): for channel, allsorted in iteritems(v): for sortcode, sptr in iteritems(allsorted): new = SpikeTrain(np.zeros( (sptr.lazy_shape), dtype = 'f8' ) *pq.s , name = sptr.name, t_start = sptr.t_start, t_stop = sptr.t_stop, left_sweep = sptr.left_sweep, sampling_rate = sptr.sampling_rate, waveforms = np.ones( (sptr.lazy_shape, 1, sptr.waveformsize) , dtype = 'f') * pq.mV , ) new.annotations.update(sptr.annotations) new.pos = 0 new.waveformsize = sptr.waveformsize allsorted[sortcode] = new # Step 3 : searh sev (individual data files) or tev (common data file) # sev is for version > 70 if os.path.exists(os.path.join(subdir, tankname+'_'+blockname+'.tev')): tev = open(os.path.join(subdir, tankname+'_'+blockname+'.tev'), 'rb') else: tev = None for code, v in iteritems(allsig): for channel, anaSig in iteritems(v): if PY3K: signame = anaSig.name.decode('ascii') else: signame = anaSig.name filename = os.path.join(subdir, tankname+'_'+blockname+'_'+signame+'_ch'+str(anaSig.channel_index)+'.sev') if os.path.exists(filename): anaSig.fid = open(filename, 'rb') else: anaSig.fid = tev for code, v in iteritems(allspiketr): for channel, allsorted in iteritems(v): for sortcode, sptr in iteritems(allsorted): sptr.fid = tev # Step 4 : second loop for copyin chunk of data tsq.seek(0) while 1: h= hr.read_f() if h==None:break channel, code , evtype = h['channel'], h['code'], h['evtype'] if Types[evtype] == 'EVTYPE_STREAM': a = allsig[code][channel] dt = a.dtype s = int((h['size']*4-40)/dt.itemsize) a.fid.seek(h['eventoffset']) a[ a.pos:a.pos+s ] = np.fromstring( a.fid.read( s*dt.itemsize ), dtype = a.dtype) a.pos += s elif Types[evtype] == 'EVTYPE_STRON' or \ Types[evtype] == 'EVTYPE_STROFF': ea = allevent[code][channel] ea.times[ea.pos] = (h['timestamp'] - global_t_start) * pq.s strobe, = struct.unpack('d' , struct.pack('q' , h['eventoffset'])) ea.labels[ea.pos] = str(strobe) ea.pos += 1 elif Types[evtype] == 'EVTYPE_SNIP': sptr = allspiketr[code][channel][h['sortcode']] sptr.t_stop = (h['timestamp'] - global_t_start) * pq.s sptr[sptr.pos] = (h['timestamp'] - global_t_start) * pq.s sptr.waveforms[sptr.pos, 0, :] = np.fromstring( sptr.fid.read( sptr.waveformsize*4 ), dtype = 'f4') * pq.V sptr.pos += 1 # Step 5 : populating segment for code, v in iteritems(allsig): for channel, anaSig in iteritems(v): seg.analogsignals.append( anaSig ) for code, v in iteritems(allevent): for channel, ea in iteritems(v): seg.eventarrays.append( ea ) for code, v in iteritems(allspiketr): for channel, allsorted in iteritems(v): for sortcode, sptr in iteritems(allsorted): seg.spiketrains.append( sptr ) create_many_to_one_relationship(bl) return bl TsqDescription = [ ('size','i'), ('evtype','i'), ('code','4s'), ('channel','H'), ('sortcode','H'), ('timestamp','d'), ('eventoffset','q'), ('dataformat','i'), ('frequency','f'), ] Types = { 0x0 : 'EVTYPE_UNKNOWN', 0x101:'EVTYPE_STRON', 0x102:'EVTYPE_STROFF', 0x201:'EVTYPE_SCALER', 0x8101:'EVTYPE_STREAM', 0x8201:'EVTYPE_SNIP', 0x8801: 'EVTYPE_MARK', } DataFormats = { 0 : np.float32, 1 : np.int32, 2 : np.int16, 3 : np.int8, 4 : np.float64, #~ 5 : '' } class HeaderReader(): def __init__(self,fid ,description ): self.fid = fid self.description = description def read_f(self, offset =None): if offset is not None : self.fid.seek(offset) d = { } for key, fmt in self.description : buf = self.fid.read(struct.calcsize(fmt)) if len(buf) != struct.calcsize(fmt) : return None val = struct.unpack(fmt , buf) if len(val) == 1: val = val[0] else : val = list(val) #~ if 's' in fmt : #~ val = val.replace('\x00','') d[key] = val return d neo-0.3.3/neo/io/spike2io.py0000644000175000017500000004553412273723542016653 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Classe for reading data in CED spike2 files (.smr). This code is based on: - sonpy, written by Antonio Gonzalez Disponible here :: http://www.neuro.ki.se/broberger/ and sonpy come from : - SON Library 2.0 for MATLAB, written by Malcolm Lidierth at King's College London. See http://www.kcl.ac.uk/depsta/biomedical/cfnr/lidierth.html This IO support old (v7) of spike2 Depend on: Supported : Read Author: sgarcia """ import os import sys import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import Segment, AnalogSignal, SpikeTrain, EventArray from neo.io.tools import create_many_to_one_relationship PY3K = (sys.version_info[0] == 3) class Spike2IO(BaseIO): """ Class for reading data from CED spike2. Usage: >>> from neo import io >>> r = io.Spike2IO( filename = 'File_spike2_1.smr') >>> seg = r.read_segment(lazy = False, cascade = True,) >>> print seg.analogsignals >>> print seg.spiketrains >>> print seg.eventarrays """ is_readable = True is_writable = False supported_objects = [ Segment , AnalogSignal , EventArray, SpikeTrain] readable_objects = [Segment] writeable_objects = [ ] has_header = False is_streameable = False read_params = { Segment : [ ('take_ideal_sampling_rate' , { 'value' : False })] } write_params = None name = 'Spike 2 CED' extensions = [ 'smr' ] mode = 'file' def __init__(self , filename = None) : """ This class read a smr file. Arguments: filename : the filename """ BaseIO.__init__(self) self.filename = filename def read_segment(self , take_ideal_sampling_rate = False, lazy = False, cascade = True, ): """ Arguments: """ header = self.read_header(filename = self.filename) #~ print header fid = open(self.filename, 'rb') seg = Segment( file_origin = os.path.basename(self.filename), ced_version = str(header.system_id), ) if not cascade: return seg def addannotations(ob, channelHeader): ob.annotate(title = channelHeader.title) ob.annotate(physical_channel_index = channelHeader.phy_chan) ob.annotate(comment = channelHeader.comment) for i in range(header.channels) : channelHeader = header.channelHeaders[i] #~ print 'channel' , i , 'kind' , channelHeader.kind if channelHeader.kind !=0: #~ print '####' #~ print 'channel' , i, 'kind' , channelHeader.kind , channelHeader.type , channelHeader.phy_chan #~ print channelHeader pass if channelHeader.kind in [1, 9]: #~ print 'analogChanel' anaSigs = self.readOneChannelContinuous( fid, i, header, take_ideal_sampling_rate, lazy = lazy) #~ print 'nb sigs', len(anaSigs) , ' sizes : ', for anaSig in anaSigs : addannotations(anaSig, channelHeader) anaSig.name = str(anaSig.annotations['title']) seg.analogsignals.append( anaSig ) #~ print sig.signal.size, #~ print '' elif channelHeader.kind in [2, 3, 4, 5, 8] : ea = self.readOneChannelEventOrSpike( fid, i, header , lazy = lazy) if ea is not None: addannotations(ea, channelHeader) seg.eventarrays.append(ea) elif channelHeader.kind in [6,7] : sptr = self.readOneChannelEventOrSpike( fid, i, header, lazy = lazy ) if sptr is not None: addannotations(sptr, channelHeader) seg.spiketrains.append(sptr) fid.close() create_many_to_one_relationship(seg) return seg def read_header(self , filename = ''): fid = open(filename, 'rb') header = HeaderReader(fid, np.dtype(headerDescription)) #~ print 'chan_size' , header.chan_size if header.system_id < 6: header.dtime_base = 1e-6 header.datetime_detail = 0 header.datetime_year = 0 channelHeaders = [ ] for i in range(header.channels): # read global channel header fid.seek(512 + 140*i) # TODO verifier i ou i-1 channelHeader = HeaderReader(fid, np.dtype(channelHeaderDesciption1)) if channelHeader.kind in [1, 6]: dt = [('scale' , 'f4'), ('offset' , 'f4'), ('unit' , 'S6'),] channelHeader += HeaderReader(fid, np.dtype(dt)) if header.system_id < 6: channelHeader += HeaderReader(fid, np.dtype([ ('divide' , 'i4')]) )#i8 else : channelHeader +=HeaderReader(fid, np.dtype([ ('interleave' , 'i4')]) )#i8 if channelHeader.kind in [7, 9]: dt = [('min' , 'f4'), ('max' , 'f4'), ('unit' , 'S6'),] channelHeader += HeaderReader(fid, np.dtype(dt)) if header.system_id < 6: channelHeader += HeaderReader(fid, np.dtype([ ('divide' , 'i4')]))#i8 else : channelHeader += HeaderReader(fid, np.dtype([ ('interleave' , 'i4')]) )#i8 if channelHeader.kind in [4]: dt = [('init_low' , 'u1'), ('next_low' , 'u1'),] channelHeader += HeaderReader(fid, np.dtype(dt)) channelHeader.type = dict_kind[channelHeader.kind] #~ print i, channelHeader channelHeaders.append(channelHeader) header.channelHeaders = channelHeaders fid.close() return header def readOneChannelContinuous(self , fid, channel_num, header, take_ideal_sampling_rate, lazy = True): # read AnalogSignal channelHeader = header.channelHeaders[channel_num] # data type if channelHeader.kind == 1: dt = np.dtype('i2') elif channelHeader.kind == 9: dt = np.dtype('f4') # sample rate if take_ideal_sampling_rate: sampling_rate = channelHeader.ideal_rate*pq.Hz else: if header.system_id in [1,2,3,4,5]: # Before version 5 #~ print channel_num, channelHeader.divide, header.us_per_time, header.time_per_adc sample_interval = (channelHeader.divide*header.us_per_time*header.time_per_adc)*1e-6 else : sample_interval = (channelHeader.l_chan_dvd*header.us_per_time*header.dtime_base) sampling_rate = (1./sample_interval)*pq.Hz # read blocks header to preallocate memory by jumping block to block fid.seek(channelHeader.firstblock) blocksize = [ 0 ] starttimes = [ ] for b in range(channelHeader.blocks) : blockHeader = HeaderReader(fid, np.dtype(blockHeaderDesciption)) if len(blocksize) > len(starttimes): starttimes.append(blockHeader.start_time) blocksize[-1] += blockHeader.items if blockHeader.succ_block > 0 : # this is ugly but CED do not garanty continuity in AnalogSignal fid.seek(blockHeader.succ_block) nextBlockHeader = HeaderReader(fid, np.dtype(blockHeaderDesciption)) sample_interval = (blockHeader.end_time-blockHeader.start_time)/(blockHeader.items-1) interval_with_next = nextBlockHeader.start_time - blockHeader.end_time if interval_with_next > sample_interval: blocksize.append(0) fid.seek(blockHeader.succ_block) anaSigs = [ ] if channelHeader.unit in unit_convert: unit = pq.Quantity(1, unit_convert[channelHeader.unit] ) else: #print channelHeader.unit try: unit = pq.Quantity(1, channelHeader.unit ) except: unit = pq.Quantity(1, '') for b,bs in enumerate(blocksize ): if lazy: signal = [ ]*unit else: signal = pq.Quantity(np.empty( bs , dtype = 'f4'), units=unit) anaSig = AnalogSignal(signal, sampling_rate=sampling_rate, t_start=(starttimes[b] * header.us_per_time * header.dtime_base * pq.s), channel_index=channel_num) anaSigs.append( anaSig ) if lazy: for s, anaSig in enumerate(anaSigs): anaSig.lazy_shape = blocksize[s] else: # read data by jumping block to block fid.seek(channelHeader.firstblock) pos = 0 numblock = 0 for b in range(channelHeader.blocks) : blockHeader = HeaderReader(fid, np.dtype(blockHeaderDesciption)) # read data sig = np.fromstring( fid.read(blockHeader.items*dt.itemsize) , dtype = dt) anaSigs[numblock][pos:pos+sig.size] = sig.astype('f4')*unit pos += sig.size if pos >= blocksize[numblock] : numblock += 1 pos = 0 # jump to next block if blockHeader.succ_block > 0 : fid.seek(blockHeader.succ_block) # convert for int16 if dt.kind == 'i' : for anaSig in anaSigs : anaSig *= channelHeader.scale/ 6553.6 anaSig += channelHeader.offset*unit return anaSigs def readOneChannelEventOrSpike(self , fid, channel_num, header ,lazy = True): # return SPikeTrain or EventArray channelHeader = header.channelHeaders[channel_num] if channelHeader.firstblock <0: return if channelHeader.kind not in [2, 3, 4 , 5 , 6 ,7, 8]: return ## Step 1 : type of blocks if channelHeader.kind in [2, 3, 4]: # Event data fmt = [('tick' , 'i4') ] elif channelHeader.kind in [5]: # Marker data fmt = [('tick' , 'i4') , ('marker' , 'i4') ] elif channelHeader.kind in [6]: # AdcMark data fmt = [('tick' , 'i4') , ('marker' , 'i4') , ('adc' , 'S%d' %channelHeader.n_extra )] elif channelHeader.kind in [7]: # RealMark data fmt = [('tick' , 'i4') , ('marker' , 'i4') , ('real' , 'S%d' %channelHeader.n_extra )] elif channelHeader.kind in [8]: # TextMark data fmt = [('tick' , 'i4') , ('marker' , 'i4') , ('label' , 'S%d'%channelHeader.n_extra)] dt = np.dtype(fmt) ## Step 2 : first read for allocating mem fid.seek(channelHeader.firstblock) totalitems = 0 for _ in range(channelHeader.blocks) : blockHeader = HeaderReader(fid, np.dtype(blockHeaderDesciption)) totalitems += blockHeader.items if blockHeader.succ_block > 0 : fid.seek(blockHeader.succ_block) #~ print 'totalitems' , totalitems if lazy : if channelHeader.kind in [2, 3, 4 , 5 , 8]: ea = EventArray( ) ea.annotate(channel_index = channel_num) ea.lazy_shape = totalitems return ea elif channelHeader.kind in [6 ,7]: sptr = SpikeTrain([ ]*pq.s, t_stop=1e99) # correct value for t_stop to be put in later sptr.annotate(channel_index = channel_num) sptr.lazy_shape = totalitems return sptr else: alltrigs = np.zeros( totalitems , dtype = dt) ## Step 3 : read fid.seek(channelHeader.firstblock) pos = 0 for _ in range(channelHeader.blocks) : blockHeader = HeaderReader(fid, np.dtype(blockHeaderDesciption)) # read all events in block trigs = np.fromstring( fid.read( blockHeader.items*dt.itemsize) , dtype = dt) alltrigs[pos:pos+trigs.size] = trigs pos += trigs.size if blockHeader.succ_block > 0 : fid.seek(blockHeader.succ_block) ## Step 3 convert in neo standard class : eventarrays or spiketrains alltimes = alltrigs['tick'].astype('f')*header.us_per_time * header.dtime_base*pq.s if channelHeader.kind in [2, 3, 4 , 5 , 8]: #events ea = EventArray( ) ea.annotate(channel_index = channel_num) ea.times = alltimes if channelHeader.kind >= 5: # Spike2 marker is closer to label sens of neo ea.labels = alltrigs['marker'].astype('S32') if channelHeader.kind == 8: ea.annotate(extra_labels = alltrigs['label']) return ea elif channelHeader.kind in [6 ,7]: # spiketrains # waveforms if channelHeader.kind == 6 : waveforms = np.fromstring(alltrigs['adc'].tostring() , dtype = 'i2') waveforms = waveforms.astype('f4') *channelHeader.scale/ 6553.6 + channelHeader.offset elif channelHeader.kind == 7 : waveforms = np.fromstring(alltrigs['real'].tostring() , dtype = 'f4') if header.system_id>=6 and channelHeader.interleave>1: waveforms = waveforms.reshape((alltimes.size,-1,channelHeader.interleave)) waveforms = waveforms.swapaxes(1,2) else: waveforms = waveforms.reshape(( alltimes.size,1, -1)) if header.system_id in [1,2,3,4,5]: sample_interval = (channelHeader.divide*header.us_per_time*header.time_per_adc)*1e-6 else : sample_interval = (channelHeader.l_chan_dvd*header.us_per_time*header.dtime_base) if channelHeader.unit in unit_convert: unit = pq.Quantity(1, unit_convert[channelHeader.unit] ) else: #print channelHeader.unit try: unit = pq.Quantity(1, channelHeader.unit ) except: unit = pq.Quantity(1, '') if len(alltimes) > 0: t_stop = alltimes.max() # can get better value from associated AnalogSignal(s) ? else: t_stop = 0.0 sptr = SpikeTrain(alltimes, waveforms = waveforms*unit, sampling_rate = (1./sample_interval)*pq.Hz, t_stop = t_stop ) sptr.annotate(channel_index = channel_num) return sptr class HeaderReader(object): def __init__(self , fid , dtype): if fid is not None : array = np.fromstring( fid.read(dtype.itemsize) , dtype)[0] else : array = np.zeros( (1) , dtype = dtype)[0] super(HeaderReader, self).__setattr__('dtype', dtype) super(HeaderReader, self).__setattr__('array', array) def __setattr__(self, name , val): if name in self.dtype.names : self.array[name] = val else : super(HeaderReader, self).__setattr__(name, val) def __getattr__(self , name): #~ print name if name in self.dtype.names : if self.dtype[name].kind == 'S': if PY3K: l = np.fromstring(self.array[name].decode('iso-8859-1')[0], 'u1') else: l = np.fromstring(self.array[name][0], 'u1') return self.array[name][1:l+1] else: return self.array[name] def names(self): return self.array.dtype.names def __repr__(self): s = 'HEADER' for name in self.dtype.names : #~ if self.dtype[name].kind != 'S' : #~ s += name + self.__getattr__(name) s += '{}: {}\n'.format(name, getattr(self, name)) return s def __add__(self, header2): # print 'add' , self.dtype, header2.dtype newdtype = [ ] for name in self.dtype.names : newdtype.append( (name , self.dtype[name].str) ) for name in header2.dtype.names : newdtype.append( (name , header2.dtype[name].str) ) newdtype = np.dtype(newdtype) newHeader = HeaderReader(None , newdtype ) newHeader.array = np.fromstring( self.array.tostring()+header2.array.tostring() , newdtype)[0] return newHeader # headers structures : headerDescription = [ ( 'system_id', 'i2' ), ( 'copyright', 'S10' ), ( 'creator', 'S8' ), ( 'us_per_time', 'i2' ), ( 'time_per_adc', 'i2' ), ( 'filestate', 'i2' ), ( 'first_data', 'i4' ),#i8 ( 'channels', 'i2' ), ( 'chan_size', 'i2' ), ( 'extra_data', 'i2' ), ( 'buffersize', 'i2' ), ( 'os_format', 'i2' ), ( 'max_ftime', 'i4' ),#i8 ( 'dtime_base', 'f8' ), ( 'datetime_detail', 'u1' ), ( 'datetime_year', 'i2' ), ( 'pad', 'S52' ), ( 'comment1', 'S80' ), ( 'comment2', 'S80' ), ( 'comment3', 'S80' ), ( 'comment4', 'S80' ), ( 'comment5', 'S80' ), ] channelHeaderDesciption1 = [ ('del_size','i2'), ('next_del_block','i4'),#i8 ('firstblock','i4'),#i8 ('lastblock','i4'),#i8 ('blocks','i2'), ('n_extra','i2'), ('pre_trig','i2'), ('free0','i2'), ('py_sz','i2'), ('max_data','i2'), ('comment','S72'), ('max_chan_time','i4'),#i8 ('l_chan_dvd','i4'),#i8 ('phy_chan','i2'), ('title','S10'), ('ideal_rate','f4'), ('kind','u1'), ('unused1','i1'), ] dict_kind = { 0 : 'empty', 1: 'Adc', 2: 'EventFall', 3: 'EventRise', 4: 'EventBoth', 5: 'Marker', 6: 'AdcMark', 7: 'RealMark', 8: 'TextMark', 9: 'RealWave', } blockHeaderDesciption =[ ('pred_block','i4'),#i8 ('succ_block','i4'),#i8 ('start_time','i4'),#i8 ('end_time','i4'),#i8 ('channel_num','i2'), ('items','i2'), ] unit_convert = { 'Volts' : 'V' , } neo-0.3.3/neo/io/brainvisionio.py0000644000175000017500000001133612273723542017772 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Class for reading data from BrainVision product. This code was originally made by L. Pezard (2010), modified B. Burle and S. More. Supported : Read Author: sgarcia """ import os import re import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import Segment, AnalogSignal, EventArray from neo.io.tools import create_many_to_one_relationship class BrainVisionIO(BaseIO): """ Class for reading/writing data from BrainVision product (brainAmp, brain analyser...) Usage: >>> from neo import io >>> r = io.BrainVisionIO( filename = 'File_brainvision_1.eeg') >>> seg = r.read_segment(lazy = False, cascade = True,) """ is_readable = True is_writable = False supported_objects = [Segment, AnalogSignal, EventArray] readable_objects = [Segment] writeable_objects = [ ] has_header = False is_streameable = False read_params = { Segment : [ ] } write_params = { Segment : [ ] } name = None extensions = ['vhdr'] mode = 'file' def __init__(self , filename = None) : """ This class read/write a elan based file. **Arguments** filename : the filename to read or write """ BaseIO.__init__(self) self.filename = filename def read_segment(self, lazy = False, cascade = True): ## Read header file (vhdr) header = readBrainSoup(self.filename) assert header['Common Infos']['DataFormat'] == 'BINARY', NotImplementedError assert header['Common Infos']['DataOrientation'] == 'MULTIPLEXED', NotImplementedError nb_channel = int(header['Common Infos']['NumberOfChannels']) sampling_rate = 1.e6/float(header['Common Infos']['SamplingInterval']) * pq.Hz fmt = header['Binary Infos']['BinaryFormat'] fmts = { 'INT_16':np.int16, 'IEEE_FLOAT_32':np.float32,} assert fmt in fmts, NotImplementedError dt = fmts[fmt] seg = Segment(file_origin = os.path.basename(self.filename), ) if not cascade : return seg # read binary if not lazy: binary_file = os.path.splitext(self.filename)[0]+'.eeg' sigs = np.memmap(binary_file , dt, 'r', ).astype('f') n = int(sigs.size/nb_channel) sigs = sigs[:n*nb_channel] sigs = sigs.reshape(n, nb_channel) for c in range(nb_channel): name, ref, res, units = header['Channel Infos']['Ch%d' % (c+1,)].split(',') units = pq.Quantity(1, units.replace('µ', 'u') ) if lazy: signal = [ ]*units else: signal = sigs[:,c]*units anasig = AnalogSignal(signal = signal, channel_index = c, name = name, sampling_rate = sampling_rate, ) if lazy: anasig.lazy_shape = -1 seg.analogsignals.append(anasig) # read marker marker_file = os.path.splitext(self.filename)[0]+'.vmrk' all_info = readBrainSoup(marker_file)['Marker Infos'] all_types = [ ] times = [ ] labels = [ ] for i in range(len(all_info)): type_, label, pos, size, channel = all_info['Mk%d' % (i+1,)].split(',')[:5] all_types.append(type_) times.append(float(pos)/sampling_rate.magnitude) labels.append(label) all_types = np.array(all_types) times = np.array(times) * pq.s labels = np.array(labels, dtype = 'S') for type_ in np.unique(all_types): ind = type_ == all_types if lazy: ea = EventArray(name = str(type_)) ea.lazy_shape = -1 else: ea = EventArray( times = times[ind], labels = labels[ind], name = str(type_), ) seg.eventarrays.append(ea) create_many_to_one_relationship(seg) return seg def readBrainSoup(filename): section = None all_info = { } for line in open(filename , 'rU'): line = line.strip('\n').strip('\r') if line.startswith('['): section = re.findall('\[([\S ]+)\]', line)[0] all_info[section] = { } continue if line.startswith(';'): continue if '=' in line and len(line.split('=')) ==2: k,v = line.split('=') all_info[section][k] = v return all_info neo-0.3.3/neo/io/__init__.py0000644000175000017500000000647112265516260016660 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ :mod:`neo.io` provides classes for reading and/or writing electrophysiological data files. Note that if the package dependency is not satisfied for one io, it does not raise an error but a warning. neo.io.iolist provides a list of succesfully imported io classes. Classes: .. autoclass:: neo.io.AlphaOmegaIO .. autoclass:: neo.io.AsciiSignalIO .. autoclass:: neo.io.AsciiSpikeTrainIO .. autoclass:: neo.io.AxonIO .. autoclass:: neo.io.BlackrockIO .. autoclass:: neo.io.BrainVisionIO .. autoclass:: neo.io.BrainwareDamIO .. autoclass:: neo.io.BrainwareF32IO .. autoclass:: neo.io.BrainwareSrcIO .. autoclass:: neo.io.ElanIO .. autoclass:: neo.io.ElphyIO .. autoclass:: neo.io.KlustaKwikIO .. autoclass:: neo.io.MicromedIO .. autoclass:: neo.io.NeoHdf5IO .. autoclass:: neo.io.NeoMatlabIO .. autoclass:: neo.io.NeuroExplorerIO .. autoclass:: neo.io.NeuroScopeIO .. autoclass:: neo.io.NeuroshareIO .. autoclass:: neo.io.PickleIO .. autoclass:: neo.io.PlexonIO .. autoclass:: neo.io.PyNNNumpyIO .. autoclass:: neo.io.PyNNTextIO .. autoclass:: neo.io.RawBinarySignalIO .. autoclass:: neo.io.TdtIO .. autoclass:: neo.io.WinEdrIO .. autoclass:: neo.io.WinWcpIO """ import os.path from neo.io.alphaomegaio import AlphaOmegaIO from neo.io.asciisignalio import AsciiSignalIO from neo.io.asciispiketrainio import AsciiSpikeTrainIO from neo.io.axonio import AxonIO from neo.io.blackrockio import BlackrockIO from neo.io.brainvisionio import BrainVisionIO from neo.io.brainwaredamio import BrainwareDamIO from neo.io.brainwaref32io import BrainwareF32IO from neo.io.brainwaresrcio import BrainwareSrcIO from neo.io.elanio import ElanIO from neo.io.elphyio import ElphyIO from neo.io.exampleio import ExampleIO from neo.io.klustakwikio import KlustaKwikIO from neo.io.micromedio import MicromedIO from neo.io.hdf5io import NeoHdf5IO from neo.io.neomatlabio import NeoMatlabIO from neo.io.neuroexplorerio import NeuroExplorerIO from neo.io.neuroscopeio import NeuroScopeIO from neo.io.neuroshareio import NeuroshareIO from neo.io.pickleio import PickleIO from neo.io.plexonio import PlexonIO from neo.io.pynnio import PyNNNumpyIO from neo.io.pynnio import PyNNTextIO from neo.io.rawbinarysignalio import RawBinarySignalIO from neo.io.spike2io import Spike2IO from neo.io.tdtio import TdtIO from neo.io.winedrio import WinEdrIO from neo.io.winwcpio import WinWcpIO iolist = [AlphaOmegaIO, AsciiSignalIO, AsciiSpikeTrainIO, AxonIO, BlackrockIO, BrainVisionIO, BrainwareDamIO, BrainwareF32IO, BrainwareSrcIO, ElanIO, ElphyIO, ExampleIO, KlustaKwikIO, MicromedIO, NeoHdf5IO, NeoMatlabIO, NeuroExplorerIO, NeuroScopeIO, NeuroshareIO, PickleIO, PlexonIO, PyNNNumpyIO, PyNNTextIO, RawBinarySignalIO, Spike2IO, TdtIO, WinEdrIO, WinWcpIO] def get_io(filename): """ Return a Neo IO instance, guessing the type based on the filename suffix. """ extension = os.path.splitext(filename)[1][1:] for io in iolist: if extension in io.extensions: return io(filename=filename) raise IOError("file extension %s not registered" % extension) neo-0.3.3/neo/io/micromedio.py0000644000175000017500000001554312273723542017252 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Class for reading/writing data from micromed (.trc). Inspired by the Matlab code for EEGLAB from Rami K. Niazy. Completed with matlab Guillaume BECQ code. Supported : Read Author: sgarcia """ import datetime import os import struct # file no longer exists in Python3 try: file except NameError: import io file = io.BufferedReader import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import Segment, AnalogSignal, EpochArray, EventArray from neo.io.tools import create_many_to_one_relationship class struct_file(file): def read_f(self, fmt): return struct.unpack(fmt , self.read(struct.calcsize(fmt))) class MicromedIO(BaseIO): """ Class for reading data from micromed (.trc). Usage: >>> from neo import io >>> r = io.MicromedIO(filename='File_micromed_1.TRC') >>> seg = r.read_segment(lazy=False, cascade=True) >>> print seg.analogsignals # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [=triggers['pos'][0]) & (triggers['pos']0) & (epochs['start']>> from neo import io >>> r = io.NeuroExplorerIO(filename='File_neuroexplorer_1.nex') >>> seg = r.read_segment(lazy=False, cascade=True) >>> print seg.analogsignals # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [>> print seg.spiketrains # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [>> print seg.eventarrays # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [>> print seg.epocharrays # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [, ] """ is_readable = True is_writable = False supported_objects = [Segment , AnalogSignal, SpikeTrain, EventArray, EpochArray] readable_objects = [ Segment] writeable_objects = [] has_header = False is_streameable = False # This is for GUI stuf : a definition for parameters when reading. read_params = { Segment : [ ] } write_params = None name = 'NeuroExplorer' extensions = [ 'nex' ] mode = 'file' def __init__(self , filename = None) : """ This class read a nex file. Arguments: filename : the filename to read you can pu what ever it do not read anythings """ BaseIO.__init__(self) self.filename = filename def read_segment(self, lazy = False, cascade = True, ): fid = open(self.filename, 'rb') globalHeader = HeaderReader(fid , GlobalHeader ).read_f(offset = 0) #~ print globalHeader #~ print 'version' , globalHeader['version'] seg = Segment() seg.file_origin = os.path.basename(self.filename) seg.annotate(neuroexplorer_version = globalHeader['version']) seg.annotate(comment = globalHeader['comment']) if not cascade : return seg offset = 544 for i in range(globalHeader['nvar']): entityHeader = HeaderReader(fid , EntityHeader ).read_f(offset = offset+i*208) entityHeader['name'] = entityHeader['name'].replace('\x00','') #print 'i',i, entityHeader['type'] if entityHeader['type'] == 0: # neuron if lazy: spike_times = [ ]*pq.s else: spike_times= np.memmap(self.filename , np.dtype('i4') ,'r' , shape = (entityHeader['n'] ), offset = entityHeader['offset'], ) spike_times = spike_times.astype('f8')/globalHeader['freq']*pq.s sptr = SpikeTrain( times= spike_times, t_start = globalHeader['tbeg']/globalHeader['freq']*pq.s, t_stop = globalHeader['tend']/globalHeader['freq']*pq.s, name = entityHeader['name'], ) if lazy: sptr.lazy_shape = entityHeader['n'] sptr.annotate(channel_index = entityHeader['WireNumber']) seg.spiketrains.append(sptr) if entityHeader['type'] == 1: # event if lazy: event_times = [ ]*pq.s else: event_times= np.memmap(self.filename , np.dtype('i4') ,'r' , shape = (entityHeader['n'] ), offset = entityHeader['offset'], ) event_times = event_times.astype('f8')/globalHeader['freq'] * pq.s labels = np.array(['']*event_times.size, dtype = 'S') evar = EventArray(times = event_times, labels=labels, channel_name = entityHeader['name'] ) if lazy: evar.lazy_shape = entityHeader['n'] seg.eventarrays.append(evar) if entityHeader['type'] == 2: # interval if lazy: start_times = [ ]*pq.s stop_times = [ ]*pq.s else: start_times= np.memmap(self.filename , np.dtype('i4') ,'r' , shape = (entityHeader['n'] ), offset = entityHeader['offset'], ) start_times = start_times.astype('f8')/globalHeader['freq']*pq.s stop_times= np.memmap(self.filename , np.dtype('i4') ,'r' , shape = (entityHeader['n'] ), offset = entityHeader['offset']+entityHeader['n']*4, ) stop_times = stop_times.astype('f')/globalHeader['freq']*pq.s epar = EpochArray(times = start_times, durations = stop_times - start_times, labels = np.array(['']*start_times.size, dtype = 'S'), channel_name = entityHeader['name']) if lazy: epar.lazy_shape = entityHeader['n'] seg.epocharrays.append(epar) if entityHeader['type'] == 3: # spiketrain and wavefoms if lazy: spike_times = [ ]*pq.s waveforms = None else: spike_times= np.memmap(self.filename , np.dtype('i4') ,'r' , shape = (entityHeader['n'] ), offset = entityHeader['offset'], ) spike_times = spike_times.astype('f8')/globalHeader['freq'] * pq.s waveforms = np.memmap(self.filename , np.dtype('i2') ,'r' , shape = (entityHeader['n'] , 1,entityHeader['NPointsWave']), offset = entityHeader['offset']+entityHeader['n'] *4, ) waveforms = (waveforms.astype('f')* entityHeader['ADtoMV'] + entityHeader['MVOffset'])*pq.mV t_stop = globalHeader['tend']/globalHeader['freq']*pq.s if spike_times.size>0: t_stop = max(t_stop, max(spike_times)) sptr = SpikeTrain( times = spike_times, t_start = globalHeader['tbeg']/globalHeader['freq']*pq.s, #~ t_stop = max(globalHeader['tend']/globalHeader['freq']*pq.s,max(spike_times)), t_stop = t_stop, name = entityHeader['name'], waveforms = waveforms, sampling_rate = entityHeader['WFrequency']*pq.Hz, left_sweep = 0*pq.ms, ) if lazy: sptr.lazy_shape = entityHeader['n'] sptr.annotate(channel_index = entityHeader['WireNumber']) seg.spiketrains.append(sptr) if entityHeader['type'] == 4: # popvectors pass if entityHeader['type'] == 5: # analog timestamps= np.memmap(self.filename , np.dtype('i4') ,'r' , shape = (entityHeader['n'] ), offset = entityHeader['offset'], ) timestamps = timestamps.astype('f8')/globalHeader['freq'] fragmentStarts = np.memmap(self.filename , np.dtype('i4') ,'r' , shape = (entityHeader['n'] ), offset = entityHeader['offset'], ) fragmentStarts = fragmentStarts.astype('f8')/globalHeader['freq'] t_start = timestamps[0] - fragmentStarts[0]/float(entityHeader['WFrequency']) del timestamps, fragmentStarts if lazy : signal = [ ]*pq.mV else: signal = np.memmap(self.filename , np.dtype('i2') ,'r' , shape = (entityHeader['NPointsWave'] ), offset = entityHeader['offset'], ) signal = signal.astype('f') signal *= entityHeader['ADtoMV'] signal += entityHeader['MVOffset'] signal = signal*pq.mV anaSig = AnalogSignal(signal=signal, t_start=t_start * pq.s, sampling_rate= entityHeader['WFrequency'] * pq.Hz, name=entityHeader['name'], channel_index=entityHeader['WireNumber']) if lazy: anaSig.lazy_shape = entityHeader['NPointsWave'] seg.analogsignals.append( anaSig ) if entityHeader['type'] == 6: # markers : TO TEST if lazy: times = [ ]*pq.s labels = np.array([ ], dtype = 'S') markertype = None else: times= np.memmap(self.filename , np.dtype('i4') ,'r' , shape = (entityHeader['n'] ), offset = entityHeader['offset'], ) times = times.astype('f8')/globalHeader['freq'] * pq.s fid.seek(entityHeader['offset'] + entityHeader['n']*4) markertype = fid.read(64).replace('\x00','') labels = np.memmap(self.filename, np.dtype('S' + str(entityHeader['MarkerLength'])) ,'r', shape = (entityHeader['n'] ), offset = entityHeader['offset'] + entityHeader['n']*4 + 64 ) ea = EventArray( times = times, labels = labels.view(np.ndarray), name = entityHeader['name'], channel_index = entityHeader['WireNumber'], marker_type = markertype ) if lazy: ea.lazy_shape = entityHeader['n'] seg.eventarrays.append(ea) create_many_to_one_relationship(seg) return seg GlobalHeader = [ ('signature' , '4s'), ('version','i'), ('comment','256s'), ('freq','d'), ('tbeg','i'), ('tend','i'), ('nvar','i'), ] EntityHeader = [ ('type' , 'i'), ('varVersion','i'), ('name','64s'), ('offset','i'), ('n','i'), ('WireNumber','i'), ('UnitNumber','i'), ('Gain','i'), ('Filter','i'), ('XPos','d'), ('YPos','d'), ('WFrequency','d'), ('ADtoMV','d'), ('NPointsWave','i'), ('NMarkers','i'), ('MarkerLength','i'), ('MVOffset','d'), ('dummy','60s'), ] MarkerHeader = [ ('type' , 'i'), ('varVersion','i'), ('name','64s'), ('offset','i'), ('n','i'), ('WireNumber','i'), ('UnitNumber','i'), ('Gain','i'), ('Filter','i'), ] class HeaderReader(): def __init__(self,fid ,description ): self.fid = fid self.description = description def read_f(self, offset =0): self.fid.seek(offset) d = { } for key, fmt in self.description : val = struct.unpack(fmt , self.fid.read(struct.calcsize(fmt))) if len(val) == 1: val = val[0] else : val = list(val) d[key] = val return d neo-0.3.3/neo/io/neomatlabio.py0000644000175000017500000003467112273723542017420 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Module for reading/writing Neo objects in MATLAB format (.mat) versions 5 to 7.2. This module is a bridge for MATLAB users who want to adopt the Neo object representation. The nomenclature is the same but using Matlab structs and cell arrays. With this module MATLAB users can use neo.io to read a format and convert it to .mat. Supported : Read/Write Author: sgarcia """ from datetime import datetime from distutils import version import re import numpy as np import quantities as pq # check scipy try: import scipy.io import scipy.version except ImportError as err: HAVE_SCIPY = False SCIPY_ERR = err else: if version.LooseVersion(scipy.version.version) < '0.8': HAVE_SCIPY = False SCIPY_ERR = ImportError("your scipy version is too old to support " + "MatlabIO, you need at least 0.8. " + "You have %s" % scipy.version.version) else: HAVE_SCIPY = True SCIPY_ERR = None from neo.io.baseio import BaseIO from neo.core import Block, Segment, AnalogSignal, EventArray, SpikeTrain from neo.io.tools import create_many_to_one_relationship from neo import description classname_lower_to_upper = { } for k in description.class_by_name.keys(): classname_lower_to_upper[k.lower()] = k class NeoMatlabIO(BaseIO): """ Class for reading/writing Neo objects in MATLAB format (.mat) versions 5 to 7.2. This module is a bridge for MATLAB users who want to adopt the Neo object representation. The nomenclature is the same but using Matlab structs and cell arrays. With this module MATLAB users can use neo.io to read a format and convert it to .mat. Rules of conversion: * Neo classes are converted to MATLAB structs. e.g., a Block is a struct with attributes "name", "file_datetime", ... * Neo one_to_many relationships are cellarrays in MATLAB. e.g., ``seg.analogsignals[2]`` in Python Neo will be ``seg.analogsignals{3}`` in MATLAB. * Quantity attributes are represented by 2 fields in MATLAB. e.g., ``anasig.t_start = 1.5 * s`` in Python will be ``anasig.t_start = 1.5`` and ``anasig.t_start_unit = 's'`` in MATLAB. * classes that inherit from Quantity (AnalogSignal, SpikeTrain, ...) in Python will have 2 fields (array and units) in the MATLAB struct. e.g.: ``AnalogSignal( [1., 2., 3.], 'V')`` in Python will be ``anasig.array = [1. 2. 3]`` and ``anasig.units = 'V'`` in MATLAB. 1 - **Scenario 1: create data in MATLAB and read them in Python** This MATLAB code generates a block:: block = struct(); block.segments = { }; block.name = 'my block with matlab'; for s = 1:3 seg = struct(); seg.name = strcat('segment ',num2str(s)); seg.analogsignals = { }; for a = 1:5 anasig = struct(); anasig.array = rand(100,1); anasig.units = 'mV'; anasig.t_start = 0; anasig.t_start_units = 's'; anasig.sampling_rate = 100; anasig.sampling_rate_units = 'Hz'; seg.analogsignals{a} = anasig; end seg.spiketrains = { }; for t = 1:7 sptr = struct(); sptr.array = rand(30,1)*10; sptr.units = 'ms'; sptr.t_start = 0; sptr.t_start_units = 'ms'; sptr.t_stop = 10; sptr.t_stop_units = 'ms'; seg.spiketrains{t} = sptr; end block.segments{s} = seg; end save 'myblock.mat' block -V7 This code reads it in Python:: import neo r = neo.io.NeoMatlabIO(filename='myblock.mat') bl = r.read_block() print bl.segments[1].analogsignals[2] print bl.segments[1].spiketrains[4] 2 - **Scenario 2: create data in Python and read them in MATLAB** This Python code generates the same block as in the previous scenario:: import neo import quantities as pq from scipy import rand bl = neo.Block(name='my block with neo') for s in range(3): seg = neo.Segment(name='segment' + str(s)) bl.segments.append(seg) for a in range(5): anasig = neo.AnalogSignal(rand(100), units='mV', t_start=0*pq.s, sampling_rate=100*pq.Hz) seg.analogsignals.append(anasig) for t in range(7): sptr = neo.SpikeTrain(rand(30), units='ms', t_start=0*pq.ms, t_stop=10*pq.ms) seg.spiketrains.append(sptr) w = neo.io.NeoMatlabIO(filename='myblock.mat') w.write_block(bl) This MATLAB code reads it:: load 'myblock.mat' block.name block.segments{2}.analogsignals{3}.array block.segments{2}.analogsignals{3}.units block.segments{2}.analogsignals{3}.t_start block.segments{2}.analogsignals{3}.t_start_units 3 - **Scenario 3: conversion** This Python code converts a Spike2 file to MATLAB:: from neo import Block from neo.io import Spike2IO, NeoMatlabIO r = Spike2IO(filename='myspike2file.smr') w = NeoMatlabIO(filename='convertedfile.mat') seg = r.read_segment() bl = Block(name='a block') bl.segments.append(seg) w.write_block(bl) """ is_readable = True is_writable = True supported_objects = [ Block, Segment , AnalogSignal , EventArray, SpikeTrain ] readable_objects = [Block, ] writeable_objects = [Block, ] has_header = False is_streameable = False read_params = { Block : [ ] } write_params = { Block : [ ] } name = 'neomatlab' extensions = [ 'mat' ] mode = 'file' def __init__(self , filename = None) : """ This class read/write neo objects in matlab 5 to 7.2 format. Arguments: filename : the filename to read """ if not HAVE_SCIPY: raise SCIPY_ERR BaseIO.__init__(self) self.filename = filename def read_block(self, cascade = True, lazy = False,): """ Arguments: """ d = scipy.io.loadmat(self.filename, struct_as_record=False, squeeze_me=True) assert'block' in d, 'no block in'+self.filename bl_struct = d['block'] bl = self.create_ob_from_struct(bl_struct, 'Block', cascade = cascade, lazy = lazy) create_many_to_one_relationship(bl) return bl def write_block(self, bl,): """ Arguments:: bl: the block to b saved """ bl_struct = self.create_struct_from_obj(bl) for seg in bl.segments: seg_struct = self.create_struct_from_obj(seg) bl_struct['segments'].append(seg_struct) for anasig in seg.analogsignals: anasig_struct = self.create_struct_from_obj(anasig) seg_struct['analogsignals'].append(anasig_struct) for ea in seg.eventarrays: ea_struct = self.create_struct_from_obj(ea) seg_struct['eventarrays'].append(ea_struct) for sptr in seg.spiketrains: sptr_struct = self.create_struct_from_obj(sptr) seg_struct['spiketrains'].append(sptr_struct) scipy.io.savemat(self.filename, {'block':bl_struct}, oned_as = 'row') def create_struct_from_obj(self, ob, ): classname = ob.__class__.__name__ struct = { } # relationship rel = description.one_to_many_relationship if classname in rel: for childname in rel[classname]: if description.class_by_name[childname] in self.supported_objects: struct[childname.lower()+'s'] = [ ] # attributes necess = description.classes_necessary_attributes[classname] recomm = description.classes_recommended_attributes[classname] attributes = necess + recomm for i, attr in enumerate(attributes): attrname, attrtype = attr[0], attr[1] #~ if attrname =='': #~ struct['array'] = ob.magnitude #~ struct['units'] = ob.dimensionality.string #~ continue if classname in description.classes_inheriting_quantities and \ description.classes_inheriting_quantities[classname] == attrname: struct[attrname] = ob.magnitude struct[attrname+'_units'] = ob.dimensionality.string continue if not(attrname in ob.annotations or hasattr(ob, attrname)): continue if getattr(ob, attrname) is None : continue if attrtype == pq.Quantity: #ndim = attr[2] struct[attrname] = getattr(ob,attrname).magnitude struct[attrname+'_units'] = getattr(ob,attrname).dimensionality.string elif attrtype ==datetime: struct[attrname] = str(getattr(ob,attrname)) else: struct[attrname] = getattr(ob,attrname) return struct def create_ob_from_struct(self, struct, classname, cascade = True, lazy = False,): cl = description.class_by_name[classname] # check if hinerits Quantity #~ is_quantity = False #~ for attr in description.classes_necessary_attributes[classname]: #~ if attr[0] == '' and attr[1] == pq.Quantity: #~ is_quantity = True #~ break #~ is_quantiy = classname in description.classes_inheriting_quantities #~ if is_quantity: if classname in description.classes_inheriting_quantities: quantity_attr = description.classes_inheriting_quantities[classname] arr = getattr(struct,quantity_attr) #~ data_complement = dict(units=str(struct.units)) data_complement = dict(units=str(getattr(struct,quantity_attr+'_units'))) if "sampling_rate" in (at[0] for at in description.classes_necessary_attributes[classname]): data_complement["sampling_rate"] = 0*pq.kHz # put fake value for now, put correct value later if "t_stop" in (at[0] for at in description.classes_necessary_attributes[classname]): if len(arr) > 0: data_complement["t_stop"] =arr.max() else: data_complement["t_stop"] = 0.0 if "t_start" in (at[0] for at in description.classes_necessary_attributes[classname]): if len(arr) > 0: data_complement["t_start"] =arr.min() else: data_complement["t_start"] = 0.0 if lazy: ob = cl([ ], **data_complement) ob.lazy_shape = arr.shape else: ob = cl(arr, **data_complement) else: ob = cl() for attrname in struct._fieldnames: # check children rel = description.one_to_many_relationship if classname in rel and attrname[:-1] in [ r.lower() for r in rel[classname] ]: try: for c in range(len(getattr(struct,attrname))): if cascade: child = self.create_ob_from_struct(getattr(struct,attrname)[c] , classname_lower_to_upper[attrname[:-1]], cascade = cascade, lazy = lazy) getattr(ob, attrname.lower()).append(child) except TypeError: # strange behavior in scipy.io: if len is 1 so there is no len() if cascade: child = self.create_ob_from_struct(getattr(struct,attrname) , classname_lower_to_upper[attrname[:-1]], cascade = cascade, lazy = lazy) getattr(ob, attrname.lower()).append(child) continue # attributes if attrname.endswith('_units') or attrname =='units' :#or attrname == 'array': # linked with another field continue if classname in description.classes_inheriting_quantities and \ description.classes_inheriting_quantities[classname] == attrname: continue item = getattr(struct, attrname) # put the good type necess = description.classes_necessary_attributes[classname] recomm = description.classes_recommended_attributes[classname] attributes = necess + recomm dict_attributes = dict( [ (a[0], a[1:]) for a in attributes]) if attrname in dict_attributes: attrtype = dict_attributes[attrname][0] if attrtype == datetime: m = '(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+).(\d+)' r = re.findall(m, str(item)) if len(r)==1: item = datetime( *[ int(e) for e in r[0] ] ) else: item = None elif attrtype == np.ndarray: dt = dict_attributes[attrname][2] if lazy: item = np.array([ ], dtype = dt) ob.lazy_shape = item.shape else: item = item.astype( dt ) elif attrtype == pq.Quantity: ndim = dict_attributes[attrname][1] units = str(getattr(struct, attrname+'_units')) if ndim == 0: item = pq.Quantity(item, units) else: if lazy: item = pq.Quantity([ ], units) item.lazy_shape = item.shape else: item = pq.Quantity(item, units) else: item = attrtype(item) setattr(ob, attrname, item) return ob neo-0.3.3/neo/io/brainwaref32io.py0000644000175000017500000002511412273723542017733 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' Class for reading from Brainware F32 files F32 files are simplified binary files for holding spike data. Unlike SRC files, F32 files carry little metadata. This also means, however, that the file format does not change, unlike SRC files whose format changes periodically (although ideally SRC files are backwards-compatible). Each F32 file only holds a single Block. The only metadata stored in the file is the length of a single repetition of the stimulus and the values of the stimulus parameters (but not the names of the parameters). Brainware was developed by Dr. Jan Schnupp and is availabe from Tucker Davis Technologies, Inc. http://www.tdt.com/downloads.htm Neither Dr. Jan Schnupp nor Tucker Davis Technologies, Inc. had any part in the development of this code The code is implemented with the permission of Dr. Jan Schnupp Author: Todd Jennings ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function # import needed core python modules from os import path # numpy and quantities are already required by neo import numpy as np import quantities as pq # needed core neo modules from neo.core import Block, RecordingChannelGroup, Segment, SpikeTrain, Unit # need to subclass BaseIO from neo.io.baseio import BaseIO # some tools to finalize the hierachy from neo.io.tools import create_many_to_one_relationship class BrainwareF32IO(BaseIO): ''' Class for reading Brainware Spike ReCord files with the extension '.f32' The read_block method returns the first Block of the file. It will automatically close the file after reading. The read method is the same as read_block. The read_all_blocks method automatically reads all Blocks. It will automatically close the file after reading. The read_next_block method will return one Block each time it is called. It will automatically close the file and reset to the first Block after reading the last block. Call the close method to close the file and reset this method back to the first Block. The isopen property tells whether the file is currently open and reading or closed. Note 1: There is always only one RecordingChannelGroup. BrainWare stores the equivalent of RecordingChannelGroups in separate files. Usage: >>> from neo.io.brainwaref32io import BrainwareF32IO >>> f32file = BrainwareF32IO(filename='multi_500ms_mulitrep_ch1.f32') >>> blk1 = f32file.read() >>> blk2 = f32file.read_block() >>> print blk1.segments >>> print blk1.segments[0].spiketrains >>> print blk1.units >>> print blk1.units[0].name >>> print blk2 >>> print blk2[0].segments ''' is_readable = True # This class can only read data is_writable = False # write is not supported # 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, RecordingChannelGroup, Segment, SpikeTrain, Unit] readable_objects = [Block] writeable_objects = [] has_header = False is_streameable = False # This is for GUI stuff: a definition for parameters when reading. # This dict should be keyed by object (`Block`). Each entry is a list # of tuple. The first entry in each tuple is the parameter name. The # second entry is a dict with keys 'value' (for default value), # and 'label' (for a descriptive name). # Note that if the highest-level object requires parameters, # common_io_test will be skipped. read_params = {Block: [], RecordingChannelGroup: [], Segment: [], SpikeTrain: [], Unit: [], } # does not support write so no GUI stuff write_params = None name = 'Brainware F32 File' extensions = ['f32'] mode = 'file' def __init__(self, filename=None): ''' Arguments: filename: the filename ''' BaseIO.__init__(self) self._path = filename self._filename = path.basename(filename) self._fsrc = None self.__lazy = False self._blk = None self.__unit = None self.__t_stop = None self.__params = None self.__seg = None self.__spiketimes = None def read(self, lazy=False, cascade=True, **kargs): ''' Reads simple spike data file "fname" generated with BrainWare ''' return self.read_block(lazy=lazy, cascade=cascade) def read_block(self, lazy=False, cascade=True, **kargs): ''' Reads a block from the simple spike data file "fname" generated with BrainWare ''' # there are no keyargs implemented to so far. If someone tries to pass # them they are expecting them to do something or making a mistake, # neither of which should pass silently if kargs: raise NotImplementedError('This method does not have any ' 'argument implemented yet') self._fsrc = None self.__lazy = lazy self._blk = Block(file_origin=self._filename) block = self._blk # if we aren't doing cascade, don't load anything if not cascade: return block # create the objects to store other objects rcg = RecordingChannelGroup(file_origin=self._filename) self.__unit = Unit(file_origin=self._filename) # load objects into their containers block.recordingchannelgroups.append(rcg) rcg.units.append(self.__unit) # initialize values self.__t_stop = None self.__params = None self.__seg = None self.__spiketimes = None # open the file with open(self._path, 'rb') as self._fsrc: res = True # while the file is not done keep reading segments while res: res = self.__read_id() create_many_to_one_relationship(block) # cleanup attributes self._fsrc = None self.__lazy = False self._blk = None self.__t_stop = None self.__params = None self.__seg = None self.__spiketimes = None return block # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- # IMPORTANT!!! # These are private methods implementing the internal reading mechanism. # Due to the way BrainWare DAM files are structured, they CANNOT be used # on their own. Calling these manually will almost certainly alter your # position in the file in an unrecoverable manner, whether they throw # an exception or not. # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- def __read_id(self): ''' Read the next ID number and do the appropriate task with it. Returns nothing. ''' try: # float32 -- ID of the first data sequence objid = np.fromfile(self._fsrc, dtype=np.float32, count=1)[0] except IndexError: # if we have a previous segment, save it self.__save_segment() # if there are no more Segments, return return False if objid == -2: self.__read_condition() elif objid == -1: self.__read_segment() else: self.__spiketimes.append(objid) return True def __read_condition(self): ''' Read the parameter values for a single stimulus condition. Returns nothing. ''' # float32 -- SpikeTrain length in ms self.__t_stop = np.fromfile(self._fsrc, dtype=np.float32, count=1)[0] # float32 -- number of stimulus parameters numelements = int(np.fromfile(self._fsrc, dtype=np.float32, count=1)[0]) # [float32] * numelements -- stimulus parameter values paramvals = np.fromfile(self._fsrc, dtype=np.float32, count=numelements).tolist() # organize the parameers into a dictionary with arbitrary names paramnames = ['Param%s' % i for i in range(len(paramvals))] self.__params = dict(zip(paramnames, paramvals)) def __read_segment(self): ''' Setup the next Segment. Returns nothing. ''' # if we have a previous segment, save it self.__save_segment() # create the segment self.__seg = Segment(file_origin=self._filename, **self.__params) # create an empy array to save the spike times # this needs to be converted to a SpikeTrain before it can be used self.__spiketimes = [] def __save_segment(self): ''' Write the segment to the Block if it exists ''' # if this is the beginning of the first condition, then we don't want # to save, so exit # but set __seg from None to False so we know next time to create a # segment even if there are no spike in the condition if self.__seg is None: self.__seg = False return if not self.__seg: # create dummy values if there are no SpikeTrains in this condition self.__seg = Segment(file_origin=self._filename, **self.__params) self.__spiketimes = [] if self.__lazy: train = SpikeTrain(pq.Quantity([], dtype=np.float32, units=pq.ms), t_start=0*pq.ms, t_stop=self.__t_stop * pq.ms, file_origin=self._filename) train.lazy_shape = len(self.__spiketimes) else: times = pq.Quantity(self.__spiketimes, dtype=np.float32, units=pq.ms) train = SpikeTrain(times, t_start=0*pq.ms, t_stop=self.__t_stop * pq.ms, file_origin=self._filename) self.__seg.spiketrains = [train] self.__unit.spiketrains.append(train) self._blk.segments.append(self.__seg) # set an empty segment # from now on, we need to set __seg to False rather than None so # that if there is a condition with no SpikeTrains we know # to create an empty Segment self.__seg = False neo-0.3.3/neo/io/asciispiketrainio.py0000644000175000017500000001031712273723542020627 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Classe for reading/writing SpikeTrains in a text file. It is the simple case where different spiketrains are written line by line. Supported : Read/Write Author: sgarcia """ import os import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import Segment, SpikeTrain from neo.io.tools import create_many_to_one_relationship class AsciiSpikeTrainIO(BaseIO): """ Classe for reading/writing SpikeTrain in a text file. Each Spiketrain is a line. Usage: >>> from neo import io >>> r = io.AsciiSpikeTrainIO( filename = 'File_ascii_spiketrain_1.txt') >>> seg = r.read_segment(lazy = False, cascade = True,) >>> print seg.spiketrains # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [>> from neo import io >>> r = io.AlphaOmegaIO( filename = 'File_AlphaOmega_1.map') >>> blck = r.read_block(lazy = False, cascade = True) >>> print blck.segments[0].analogsignals """ is_readable = True # This is a reading only class is_writable = False # writting is not supported # This class is able to directly or inderectly read the following kind of # objects supported_objects = [ Block, Segment , AnalogSignal] # TODO: Add support for other objects that should be extractable from .map # files (AnalogSignalArray, Event, EventArray, Epoch?, Epoch Array?, # Spike?, SpikeTrain?) # This class can only return a Block readable_objects = [ Block ] # TODO : create readers for different type of objects (Segment, # AnalogSignal,...) # This class is not able to write objects writeable_objects = [ ] # This is for GUI stuff : a definition for parameters when reading. read_params = { Block : [ ] } # Writing is not supported, so no GUI stuff write_params = None name = 'AlphaOmega' extensions = [ 'map' ] mode = 'file' def __init__(self , filename = None) : """ Arguments: filename : the .map Alpha Omega file name """ BaseIO.__init__(self) self.filename = filename # write is not supported so I do not overload write method from BaseIO def read_block(self, # the 2 first keyword arguments are imposed by neo.io API lazy = False, cascade = True): """ Return a Block. """ def count_samples(m_length): """ Count the number of signal samples available in a type 5 data block of length m_length """ # for information about type 5 data block, see [1] count = int((m_length-6)/2-2) # -6 corresponds to the header of block 5, and the -2 take into # account the fact that last 2 values are not available as the 4 # corresponding bytes are coding the time stamp of the beginning # of the block return count # create the neo Block that will be returned at the end blck = Block(file_origin = os.path.basename(self.filename)) blck.file_origin = os.path.basename(self.filename) fid = open(self.filename, 'rb') # NOTE: in the following, the word "block" is used in the sense used in # the alpha-omega specifications (ie a data chunk in the file), rather # than in the sense of the usual Block object in neo # step 1: read the headers of all the data blocks to load the file # structure pos_block = 0 # position of the current block in the file file_blocks = [] # list of data blocks available in the file if not cascade: # we read only the main header m_length, m_TypeBlock = struct.unpack('Hcx' , fid.read(4)) # m_TypeBlock should be 'h', as we read the first block block = HeaderReader(fid, dict_header_type.get(m_TypeBlock, Type_Unknown)).read_f() block.update({'m_length': m_length, 'm_TypeBlock': m_TypeBlock, 'pos': pos_block}) file_blocks.append(block) else: # cascade == True seg = Segment(file_origin = os.path.basename(self.filename)) seg.file_origin = os.path.basename(self.filename) blck.segments.append(seg) while True: first_4_bytes = fid.read(4) if len(first_4_bytes) < 4: # we have reached the end of the file break else: m_length, m_TypeBlock = struct.unpack('Hcx', first_4_bytes) block = HeaderReader(fid, dict_header_type.get(m_TypeBlock, Type_Unknown)).read_f() block.update({'m_length': m_length, 'm_TypeBlock': m_TypeBlock, 'pos': pos_block}) if m_TypeBlock == '2': # The beggining of the block of type '2' is identical for # all types of channels, but the following part depends on # the type of channel. So we need a special case here. # WARNING: How to check the type of channel is not # described in the documentation. So here I use what is # proposed in the C code [2]. # According to this C code, it seems that the 'm_isAnalog' # is used to distinguished analog and digital channels, and # 'm_Mode' encodes the type of analog channel: # 0 for continuous, 1 for level, 2 for external trigger. # But in some files, I found channels that seemed to be # continuous channels with 'm_Modes' = 128 or 192. So I # decided to consider every channel with 'm_Modes' # different from 1 or 2 as continuous. I also couldn't # check that values of 1 and 2 are really for level and # external trigger as I had no test files containing data # of this types. type_subblock = 'unknown_channel_type(m_Mode=' \ + str(block['m_Mode'])+ ')' description = Type2_SubBlockUnknownChannels block.update({'m_Name': 'unknown_name'}) if block['m_isAnalog'] == 0: # digital channel type_subblock = 'digital' description = Type2_SubBlockDigitalChannels elif block['m_isAnalog'] == 1: # analog channel if block['m_Mode'] == 1: # level channel type_subblock = 'level' description = Type2_SubBlockLevelChannels elif block['m_Mode'] == 2: # external trigger channel type_subblock = 'external_trigger' description = Type2_SubBlockExtTriggerChannels else: # continuous channel type_subblock = 'continuous(Mode' \ + str(block['m_Mode']) +')' description = Type2_SubBlockContinuousChannels subblock = HeaderReader(fid, description).read_f() block.update(subblock) block.update({'type_subblock': type_subblock}) file_blocks.append(block) pos_block += m_length fid.seek(pos_block) # step 2: find the available channels list_chan = [] # list containing indexes of channel blocks for ind_block, block in enumerate(file_blocks): if block['m_TypeBlock'] == '2': list_chan.append(ind_block) # step 3: find blocks containing data for the available channels list_data = [] # list of lists of indexes of data blocks # corresponding to each channel for ind_chan, chan in enumerate(list_chan): list_data.append([]) num_chan = file_blocks[chan]['m_numChannel'] for ind_block, block in enumerate(file_blocks): if block['m_TypeBlock'] == '5': if block['m_numChannel'] == num_chan: list_data[ind_chan].append(ind_block) # step 4: compute the length (number of samples) of the channels chan_len = np.zeros(len(list_data), dtype = np.int) for ind_chan, list_blocks in enumerate(list_data): for ind_block in list_blocks: chan_len[ind_chan] += count_samples( file_blocks[ind_block]['m_length']) # step 5: find channels for which data are available ind_valid_chan = np.nonzero(chan_len)[0] # step 6: load the data # TODO give the possibility to load data as AnalogSignalArrays for ind_chan in ind_valid_chan: list_blocks = list_data[ind_chan] ind = 0 # index in the data vector # read time stamp for the beginning of the signal form = '>> from neo import io >>> r = io.PlexonIO(filename='File_plexon_1.plx') >>> seg = r.read_segment(lazy=False, cascade=True) >>> print seg.analogsignals [] >>> print seg.spiketrains # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [>> print seg.eventarrays [] """ is_readable = True is_writable = False supported_objects = [Segment , AnalogSignal, SpikeTrain, EventArray, EpochArray] readable_objects = [ Segment] writeable_objects = [] has_header = False is_streameable = False # This is for GUI stuf : a definition for parameters when reading. read_params = { Segment : [ ('load_spike_waveform' , { 'value' : False } ) , ] } write_params = None name = 'Plexon' extensions = [ 'plx' ] mode = 'file' def __init__(self , filename = None) : """ This class read a plx file. Arguments: filename : the filename load_spike_waveform : load or not waveform of spikes (default True) """ BaseIO.__init__(self) self.filename = filename def read_segment(self, lazy = False, cascade = True, load_spike_waveform = True, ): """ """ fid = open(self.filename, 'rb') globalHeader = HeaderReader(fid , GlobalHeader ).read_f(offset = 0) # metadatas seg = Segment() seg.rec_datetime = datetime.datetime( globalHeader['Year'] , globalHeader['Month'] , globalHeader['Day'] , globalHeader['Hour'] , globalHeader['Minute'] , globalHeader['Second'] ) seg.file_origin = os.path.basename(self.filename) seg.annotate(plexon_version = globalHeader['Version']) if not cascade: return seg ## Step 1 : read headers # dsp channels header = sipkes and waveforms dspChannelHeaders = { } maxunit=0 maxchan = 0 for _ in range(globalHeader['NumDSPChannels']): # channel is 1 based channelHeader = HeaderReader(fid , ChannelHeader ).read_f(offset = None) channelHeader['Template'] = np.array(channelHeader['Template']).reshape((5,64)) channelHeader['Boxes'] = np.array(channelHeader['Boxes']).reshape((5,2,4)) dspChannelHeaders[channelHeader['Channel']]=channelHeader maxunit = max(channelHeader['NUnits'],maxunit) maxchan = max(channelHeader['Channel'],maxchan) # event channel header eventHeaders = { } for _ in range(globalHeader['NumEventChannels']): eventHeader = HeaderReader(fid , EventHeader ).read_f(offset = None) eventHeaders[eventHeader['Channel']] = eventHeader # slow channel header = signal slowChannelHeaders = { } for _ in range(globalHeader['NumSlowChannels']): slowChannelHeader = HeaderReader(fid , SlowChannelHeader ).read_f(offset = None) slowChannelHeaders[slowChannelHeader['Channel']] = slowChannelHeader ## Step 2 : a first loop for counting size # signal nb_samples = np.zeros(len(slowChannelHeaders)) sample_positions = np.zeros(len(slowChannelHeaders)) t_starts = np.zeros(len(slowChannelHeaders), dtype = 'f') #spiketimes and waveform nb_spikes = np.zeros((maxchan+1, maxunit+1) ,dtype='i') wf_sizes = np.zeros((maxchan+1, maxunit+1, 2) ,dtype='i') # eventarrays nb_events = { } #maxstrsizeperchannel = { } for chan, h in iteritems(eventHeaders): nb_events[chan] = 0 #maxstrsizeperchannel[chan] = 0 start = fid.tell() while fid.tell() !=-1 : # read block header dataBlockHeader = HeaderReader(fid , DataBlockHeader ).read_f(offset = None) if dataBlockHeader is None : break chan = dataBlockHeader['Channel'] unit = dataBlockHeader['Unit'] n1,n2 = dataBlockHeader['NumberOfWaveforms'] , dataBlockHeader['NumberOfWordsInWaveform'] time = (dataBlockHeader['UpperByteOf5ByteTimestamp']*2.**32 + dataBlockHeader['TimeStamp']) if dataBlockHeader['Type'] == 1: nb_spikes[chan,unit] +=1 wf_sizes[chan,unit,:] = [n1,n2] fid.seek(n1*n2*2,1) elif dataBlockHeader['Type'] ==4: #event nb_events[chan] += 1 elif dataBlockHeader['Type'] == 5: #continuous signal fid.seek(n2*2, 1) if n2> 0: nb_samples[chan] += n2 if nb_samples[chan] ==0: t_starts[chan] = time ## Step 3: allocating memory and 2 loop for reading if not lazy if not lazy: # allocating mem for signal sigarrays = { } for chan, h in iteritems(slowChannelHeaders): sigarrays[chan] = np.zeros(nb_samples[chan]) # allocating mem for SpikeTrain stimearrays = np.zeros((maxchan+1, maxunit+1) ,dtype=object) swfarrays = np.zeros((maxchan+1, maxunit+1) ,dtype=object) for (chan, unit), _ in np.ndenumerate(nb_spikes): stimearrays[chan,unit] = np.zeros(nb_spikes[chan,unit], dtype = 'f') if load_spike_waveform: n1,n2 = wf_sizes[chan, unit,:] swfarrays[chan, unit] = np.zeros( (nb_spikes[chan, unit], n1, n2 ) , dtype = 'f4' ) pos_spikes = np.zeros(nb_spikes.shape, dtype = 'i') # allocating mem for event eventpositions = { } evarrays = { } for chan, nb in iteritems(nb_events): evarrays[chan] = np.zeros(nb, dtype = 'f' ) eventpositions[chan]=0 fid.seek(start) while fid.tell() !=-1 : dataBlockHeader = HeaderReader(fid , DataBlockHeader ).read_f(offset = None) if dataBlockHeader is None : break chan = dataBlockHeader['Channel'] n1,n2 = dataBlockHeader['NumberOfWaveforms'] , dataBlockHeader['NumberOfWordsInWaveform'] time = dataBlockHeader['UpperByteOf5ByteTimestamp']*2.**32 + dataBlockHeader['TimeStamp'] time/= globalHeader['ADFrequency'] if n2 <0: break if dataBlockHeader['Type'] == 1: #spike unit = dataBlockHeader['Unit'] pos = pos_spikes[chan,unit] stimearrays[chan, unit][pos] = time if load_spike_waveform and n1*n2 != 0 : swfarrays[chan,unit][pos,:,:] = np.fromstring( fid.read(n1*n2*2) , dtype = 'i2').reshape(n1,n2).astype('f4') else: fid.seek(n1*n2*2,1) pos_spikes[chan,unit] +=1 elif dataBlockHeader['Type'] == 4: # event pos = eventpositions[chan] evarrays[chan][pos] = time eventpositions[chan]+= 1 elif dataBlockHeader['Type'] == 5: #signal data = np.fromstring( fid.read(n2*2) , dtype = 'i2').astype('f4') sigarrays[chan][sample_positions[chan] : sample_positions[chan]+data.size] = data sample_positions[chan] += data.size ## Step 3: create neo object for chan, h in iteritems(eventHeaders): if lazy: times = [ ] else: times = evarrays[chan] ea = EventArray(times*pq.s, channel_name= eventHeaders[chan]['Name'], channel_index = chan) if lazy: ea.lazy_shape = nb_events[chan] seg.eventarrays.append(ea) for chan, h in iteritems(slowChannelHeaders): if lazy: signal = [ ] else: if globalHeader['Version'] ==100 or globalHeader['Version'] ==101 : gain = 5000./(2048*slowChannelHeaders[chan]['Gain']*1000.) elif globalHeader['Version'] ==102 : gain = 5000./(2048*slowChannelHeaders[chan]['Gain']*slowChannelHeaders[chan]['PreampGain']) elif globalHeader['Version'] >= 103: gain = globalHeader['SlowMaxMagnitudeMV']/(.5*(2**globalHeader['BitsPerSpikeSample'])*\ slowChannelHeaders[chan]['Gain']*slowChannelHeaders[chan]['PreampGain']) signal = sigarrays[chan]*gain anasig = AnalogSignal(signal*pq.V, sampling_rate = float(slowChannelHeaders[chan]['ADFreq'])*pq.Hz, t_start = t_starts[chan]*pq.s, channel_index = slowChannelHeaders[chan]['Channel'], channel_name = slowChannelHeaders[chan]['Name'], ) if lazy: anasig.lazy_shape = nb_samples[chan] seg.analogsignals.append(anasig) for (chan, unit), value in np.ndenumerate(nb_spikes): if nb_spikes[chan, unit] == 0: continue if lazy: times = [ ] waveforms = None t_stop = 0 else: times = stimearrays[chan,unit] t_stop = times.max() if load_spike_waveform: if globalHeader['Version'] <103: gain = 3000./(2048*dspChannelHeaders[chan]['Gain']*1000.) elif globalHeader['Version'] >=103 and globalHeader['Version'] <105: gain = globalHeader['SpikeMaxMagnitudeMV']/(.5*2.**(globalHeader['BitsPerSpikeSample'])*1000.) elif globalHeader['Version'] >105: gain = globalHeader['SpikeMaxMagnitudeMV']/(.5*2.**(globalHeader['BitsPerSpikeSample'])*globalHeader['SpikePreAmpGain']) waveforms = swfarrays[chan, unit] * gain * pq.V else: waveforms = None sptr = SpikeTrain(times, units='s', t_stop=t_stop*pq.s, waveforms = waveforms, ) sptr.annotate(unit_name = dspChannelHeaders[chan]['Name']) sptr.annotate(channel_index = chan) if lazy: sptr.lazy_shape = nb_spikes[chan,unit] seg.spiketrains.append(sptr) create_many_to_one_relationship(seg) return seg GlobalHeader = [ ('MagicNumber' , 'I'), ('Version','i'), ('Comment','128s'), ('ADFrequency','i'), ('NumDSPChannels','i'), ('NumEventChannels','i'), ('NumSlowChannels','i'), ('NumPointsWave','i'), ('NumPointsPreThr','i'), ('Year','i'), ('Month','i'), ('Day','i'), ('Hour','i'), ('Minute','i'), ('Second','i'), ('FastRead','i'), ('WaveformFreq','i'), ('LastTimestamp','d'), #version >103 ('Trodalness' , 'b'), ('DataTrodalness' , 'b'), ('BitsPerSpikeSample' , 'b'), ('BitsPerSlowSample' , 'b'), ('SpikeMaxMagnitudeMV' , 'H'), ('SlowMaxMagnitudeMV' , 'H'), #version 105 ('SpikePreAmpGain' , 'H'), #version 106 ('AcquiringSoftware','18s'), ('ProcessingSoftware','18s'), ('Padding','10s'), # all version ('TSCounts','650i'), ('WFCounts','650i'), ('EVCounts','512i'), ] ChannelHeader = [ ('Name' , '32s'), ('SIGName','32s'), ('Channel','i'), ('WFRate','i'), ('SIG','i'), ('Ref','i'), ('Gain','i'), ('Filter','i'), ('Threshold','i'), ('Method','i'), ('NUnits','i'), ('Template','320h'), ('Fit','5i'), ('SortWidth','i'), ('Boxes','40h'), ('SortBeg','i'), #version 105 ('Comment','128s'), #version 106 ('SrcId','b'), ('reserved','b'), ('ChanId','H'), ('Padding','10i'), ] EventHeader = [ ('Name' , '32s'), ('Channel','i'), #version 105 ('Comment' , '128s'), #version 106 ('SrcId','b'), ('reserved','b'), ('ChanId','H'), ('Padding','32i'), ] SlowChannelHeader = [ ('Name' , '32s'), ('Channel','i'), ('ADFreq','i'), ('Gain','i'), ('Enabled','i'), ('PreampGain','i'), #version 104 ('SpikeChannel','i'), #version 105 ('Comment','128s'), #version 106 ('SrcId','b'), ('reserved','b'), ('ChanId','H'), ('Padding','27i'), ] DataBlockHeader = [ ('Type','h'), ('UpperByteOf5ByteTimestamp','h'), ('TimeStamp','i'), ('Channel','h'), ('Unit','h'), ('NumberOfWaveforms','h'), ('NumberOfWordsInWaveform','h'), ]# 16 bytes class HeaderReader(): def __init__(self,fid ,description ): self.fid = fid self.description = description def read_f(self, offset =None): if offset is not None : self.fid.seek(offset) d = { } for key, fmt in self.description : buf = self.fid.read(struct.calcsize(fmt)) if len(buf) != struct.calcsize(fmt) : return None val = list(struct.unpack(fmt , buf)) for i, ival in enumerate(val): if hasattr(ival, 'replace'): val[i] = ival.replace('\x00','') if len(val) == 1: val = val[0] d[key] = val return d neo-0.3.3/neo/io/brainwaresrcio.py0000755000175000017500000016603712273723542020145 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' Class for reading from Brainware SRC files SRC files are binary files for holding spike data. They are broken up into nested data sequences of different types, with each type of sequence identified by a unique ID number. This allows new versions of sequences to be included without breaking backwards compatibility, since new versions can just be given a new ID number. The ID numbers and the format of the data they contain were taken from the Matlab-based reader function supplied with BrainWare. The python code, however, was implemented from scratch in Python using Python idioms. There are some situations where BrainWare data can overflow the SRC file, resulting in a corrupt file. Neither BrainWare nor the Matlab-based reader can read such files. This software, however, will try to recover the data, and in most cases can do so successfully. Each SRC file can hold the equivalent of multiple Neo Blocks. Brainware was developed by Dr. Jan Schnupp and is availabe from Tucker Davis Technologies, Inc. http://www.tdt.com/downloads.htm Neither Dr. Jan Schnupp nor Tucker Davis Technologies, Inc. had any part in the development of this code The code is implemented with the permission of Dr. Jan Schnupp Author: Todd Jennings ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function # import needed core python modules from datetime import datetime, timedelta from os import path from warnings import warn # numpy and quantities are already required by neo import numpy as np import quantities as pq # needed core neo modules from neo.core import (Block, Event, RecordingChannel, RecordingChannelGroup, Segment, SpikeTrain, Unit) # need to subclass BaseIO from neo.io.baseio import BaseIO # some tools to finalize the hierachy from neo.io.tools import create_many_to_one_relationship class BrainwareSrcIO(BaseIO): ''' Class for reading Brainware Spike ReCord files with the extension '.src' The read_block method returns the first Block of the file. It will automatically close the file after reading. The read method is the same as read_block. The read_all_blocks method automatically reads all Blocks. It will automatically close the file after reading. The read_next_block method will return one Block each time it is called. It will automatically close the file and reset to the first Block after reading the last block. Call the close method to close the file and reset this method back to the first Block. The isopen property tells whether the file is currently open and reading or closed. Note 1: The first Unit in each RecordingChannelGroup is always UnassignedSpikes, which has a SpikeTrain for each Segment containing all the spikes not assigned to any Unit in that Segment. Note 2: The first Segment in each Block is always Comments, which stores all comments as Event objects. The Event times are the timestamps of the comments as the number of days since dec 30th 1899, while the timestamp attribute has the same value in python datetime format Note 3: The parameters from the BrainWare table for each condition are stored in the Segment annotations. If there are multiple repetitions of a condition, each repetition is stored as a separate Segment. Note 4: There is always only one RecordingChannelGroup. BrainWare stores the equivalent of RecordingChannelGroups in separate files. Usage: >>> from neo.io.brainwaresrcio import BrainwareSrcIO >>> srcfile = BrainwareSrcIO(filename='multi_500ms_mulitrep_ch1.src') >>> blk1 = srcfile.read() >>> blk2 = srcfile.read_block() >>> blks = srcfile.read_all_blocks() >>> print blk1.segments >>> print blk1.segments[0].spiketrains >>> print blk1.units >>> print blk1.units[0].name >>> print blk2 >>> print blk2[0].segments >>> print blks >>> print blks[0].segments ''' is_readable = True # This class can only read data is_writable = False # write is not supported # 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, RecordingChannel, RecordingChannelGroup, Segment, SpikeTrain, Event, Unit] readable_objects = [Block] writeable_objects = [] has_header = False is_streameable = False # This is for GUI stuff: a definition for parameters when reading. # This dict should be keyed by object (`Block`). Each entry is a list # of tuple. The first entry in each tuple is the parameter name. The # second entry is a dict with keys 'value' (for default value), # and 'label' (for a descriptive name). # Note that if the highest-level object requires parameters, # common_io_test will be skipped. read_params = {Block: [], Event: [('sender', {'value': '', 'type': str, 'label': 'The ones who sent the comments', } ), ('timestamp', {'value': datetime(1, 1, 1), 'type': datetime, 'label': 'The time of the comment', } ) ], RecordingChannel: [], RecordingChannelGroup: [], Segment: [('feature_type', {'value': -1, 'type': int}), ('go_by_closest_unit_center', {'value': False, 'type': bool}), ('include_unit_bounds', {'value': False, 'type': bool}) ], SpikeTrain: [('dama_index', {'value': -1, 'type': int, 'label': 'index of analogsignalarray in ' 'corresponding .dam file, if any'}), ('respwin', {'value': np.asarray([], dtype=np.int32), 'type': np.ndarray, 'label': 'response and spon period ' 'boundaries'}), ('trig2', {'value': pq.Quantity([], dtype=np.uint8, units=pq.ms), 'type': pq.quantity.Quantity, 'label': 'point of return to noise'}), ('side', {'value': '', 'type': str, 'label': 'side of the brain'}), ('timestamp', {'value': datetime(1, 1, 1), 'type': datetime, 'label': 'Start time of the SpikeTrain'}) ], Unit: [('boundaries', {'value': [], 'type': list, 'label': 'unit boundaries'}), ('elliptic', {'value': [], 'type': list, 'label': 'elliptic feature'}), ('timestamp', {'value': [], 'type': list, 'label': 'Start time of each unit list'}), ('max_valid', {'value': [], 'type': list}) ] } # does not support write so no GUI stuff write_params = None name = 'Brainware SRC File' extensions = ['src'] mode = 'file' def __init__(self, filename=None): """ Arguments: filename: the filename """ BaseIO.__init__(self) # this stores the filename of the current object, exactly as it is # provided when the instance is initialized. self.__filename = filename # this store the filename without the path self.__file_origin = filename # This stores the file object for the current file self.__fsrc = None # This stores the current Block self.__blk = None # This stores the current RecordingChannelGroup for easy access # It is equivalent to self.__blk.recordingchannelgroups[0] self.__rcg = None # This stores the current Segment for easy access # It is equivalent to self.__blk.segments[-1] self.__seg = None # this stores a dictionary of the Block's 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 = {} # this stores the current Unit self.__unit = None # if the file has a list with negative length, the rest of the file's # list lengths are unreliable, so we need to store this value for the # whole file self.__damaged = False # this stores whether the current file is lazy loaded self.__lazy = False # this stores whether the current file is cascading # this is false by default so if we use read_block on its own it works self.__cascade = False @property def isopen(self): ''' This property tells whether the SRC file associated with the IO object is open. ''' return self.__fsrc is not None def opensrc(self): ''' Open the file if it isn't already open. ''' # if the file isn't already open, open it and clear the Blocks if not self.__fsrc or self.__fsrc.closed: self.__fsrc = open(self.__filename, 'rb') # figure out the filename of the current file self.__file_origin = path.basename(self.__filename) def close(self): ''' Close the currently-open file and reset the current reading point. ''' if self.isopen and not self.__fsrc.closed: self.__fsrc.close() # we also need to reset all per-file attributes self.__damaged = False self.__fsrc = None self.__seg = None self.__cascade = False self.__file_origin = None self.__lazy = False def read(self, lazy=False, cascade=True, **kargs): ''' Reads the first Block from the Spike ReCording file "filename" generated with BrainWare. If you wish to read more than one Block, please use read_all_blocks. ''' return self.read_block(lazy=lazy, cascade=cascade, **kargs) def read_block(self, lazy=False, cascade=True, **kargs): ''' Reads the first Block from the Spike ReCording file "filename" generated with BrainWare. If you wish to read more than one Block, please use read_all_blocks. ''' # there are no keyargs implemented to so far. If someone tries to pass # them they are expecting them to do something or making a mistake, # neither of which should pass silently if kargs: raise NotImplementedError('This method does not have any ' 'argument implemented yet') blockobj = self.read_next_block(cascade=cascade, lazy=lazy, warnlast=False) self.close() return blockobj def read_next_block(self, cascade=True, lazy=False, warnlast=True, **kargs): ''' Reads a single Block from the Spike ReCording file "filename" generated with BrainWare. Each call of read will return the next Block until all Blocks are loaded. After the last Block, the file will be automatically closed and the progress reset. Call the close method manually to reset back to the first Block. If "warnlast" is set to True (default), print a warning after reading the last Block. ''' # there are no keyargs implemented to so far. If someone tries to pass # them they are expecting them to do something or making a mistake, # neither of which should pass silently if kargs: raise NotImplementedError('This method does not have any ' 'argument implemented yet') self.__lazy = lazy self.opensrc() # create the Block and the contents all Blocks of from IO share self.__blk = Block(file_origin=self.__file_origin) if not cascade: return self.__blk self.__rcg = RecordingChannelGroup(file_origin=self.__file_origin) self.__seg = Segment(name='Comments', file_origin=self.__file_origin) self.__unit = Unit(name='UnassignedSpikes', file_origin=self.__file_origin, elliptic=[], boundaries=[], timestamp=[], max_valid=[]) self.__blk.recordingchannelgroups.append(self.__rcg) self.__rcg.units.append(self.__unit) self.__blk.segments.append(self.__seg) # this actually reads the contents of the Block result = [] while hasattr(result, '__iter__'): try: result = self._read_by_id() except: self.close() raise # set the recorging channel group names and indices chans = self.__rcg.recordingchannels chan_inds = np.arange(len(chans), dtype='int') chan_names = np.array(['Chan'+str(i) for i in chan_inds], dtype='string_') self.__rcg.channel_indexes = chan_inds self.__rcg.channel_names = chan_names # since we read at a Block level we always do this create_many_to_one_relationship(self.__blk) # put the Block in a local object so it can be gargabe collected blockobj = self.__blk # reset the per-Block attributes self.__blk = None self.__rcg = None self.__unitdict = {} # result is None iff the end of the file is reached, so we can # close the file # this notification is not helpful if using the read method with # cascade==True, since the user will know it is done when the method # returns a value if result is None: if warnlast: print('Last Block read. Closing file') self.close() return blockobj def read_all_blocks(self, cascade=True, lazy=False, **kargs): ''' Reads all Blocks from the Spike ReCording file "filename" generated with BrainWare. The progress in the file is reset and the file closed then opened again prior to reading. The file is automatically closed after reading completes. ''' # there are no keyargs implemented to so far. If someone tries to pass # them they are expecting them to do something or making a mistake, # neither of which should pass silently if kargs: raise NotImplementedError('This method does not have any ' 'argument implemented yet') self.__lazy = lazy self.__cascade = True self.close() self.opensrc() # Read each Block. # After the last Block self.isopen is set to False, so this make a # good way to determine when to stop blocks = [] while self.isopen: try: blocks.append(self.read_next_block(cascade=cascade, lazy=lazy, warnlast=False)) except: self.close() raise return blocks @staticmethod def convert_timestamp(timestamp, start_date=datetime(1899, 12, 30)): ''' convert_timestamp(timestamp, start_date) - convert a timestamp in brainware src file units to a python datetime object. start_date defaults to 1899.12.30 (ISO format), which is the start date used by all BrainWare SRC data Blocks so far. If manually specified it should be a datetime object or any other object that can be added to a timedelta object. ''' return convert_brainwaresrc_timestamp(timestamp, start_date=start_date) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- # All methods from here on are private. They are not intended to be used # on their own, although methods that could theoretically be called on # their own are marked as such. All private methods could be renamed, # combined, or split at any time. All private methods prefixed by # "__read" or "__skip" will alter the current place in the file. # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- def _read_by_id(self): ''' Reader for generic data BrainWare SRC files are broken up into data sequences that are identified by an ID code. This method determines the ID code and calls the method to read the data sequence with that ID code. See the __ID_DICT attribute for a dictionary of code/method pairs. IMPORTANT!!! This is the only private method that can be called directly. The rest of the private methods can only safely be called by this method or by other private methods, since they depend on the current position in the file. ''' try: # uint16 -- the ID code of the next sequence seqid = np.fromfile(self.__fsrc, dtype=np.uint16, count=1)[0] except IndexError: # return a None if at EOF. Other methods use None to recognize # an EOF return None # using the seqid, get the reader function from the reader dict readfunc = self.__ID_DICT.get(seqid) if readfunc is None: if seqid <= 0: # return if end-of-sequence ID code. This has to be 0. # just calling "return" will return a None which is used as an # EOF indicator return 0 else: # return a warning if the key is invalid # (this is consistent with the official behavior, # even the official reference files have invalid keys # when using the official reference reader matlab # scripts warn('unknown ID: %s' % seqid) return [] try: # run the function to get the data return readfunc(self) except (EOFError, UnicodeDecodeError) as err: # return a warning if the EOF is reached in the middle of a method warn(str(err)) return None # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- # These are helper methods. They don't read from the file, so it # won't harm the reading process to call them, but they are only relevant # when used in other private methods. # # These are tuned to the particular needs of this IO class, they are # unlikely to work properly if used with another file format. # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- def _assign_sequence(self, data_obj): ''' _assign_sequence(data_obj) - Try to guess where an unknown sequence 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): warn('Unknown Unit found, adding to Units list') self.__rcg.units.append(data_obj) if data_obj.name: self.__unitdict[data_obj.name] = data_obj elif isinstance(data_obj, Segment): warn('Unknown Segment found, adding to Segments list') self.__blk.segments.append(data_obj) elif isinstance(data_obj, Event): warn('Unknown Event found, adding to comment Events list') self.__blk.segments[0].events.append(data_obj) elif isinstance(data_obj, SpikeTrain): warn('Unknown SpikeTrain found, ' + 'adding to the UnassignedSpikes Unit') self.__rcg.units[0].spiketrains.append(data_obj) elif hasattr(data_obj, '__iter__') and not isinstance(data_obj, str): for sub_obj in data_obj: self._assign_sequence(sub_obj) else: warn('Unrecognized sequence of type %s found, skipping', type(data_obj)) _default_datetime = datetime(1, 1, 1) _default_spiketrain = SpikeTrain(times=pq.Quantity([], units=pq.ms, dtype=np.float32), t_start=pq.Quantity(0, units=pq.ms, dtype=np.float32), t_stop=pq.Quantity(1, units=pq.ms, dtype=np.float32), waveforms=pq.Quantity([[[]]], dtype=np.int8, units=pq.mV), dtype=np.float32, copy=False, timestamp=_default_datetime, respwin=np.array([], dtype=np.int32), dama_index=-1, trig2=pq.Quantity([], units=pq.ms, dtype=np.uint8), side='') def _combine_spiketrains(self, spiketrains): ''' _combine_spiketrains(spiketrains) - combine a list of SpikeTrains with single spikes into one long SpikeTrain ''' if not spiketrains: train = self._default_spiketrain.copy() train.file_origin = self.__file_origin if self.__lazy: train.lazy_shape = (0,) return train if hasattr(spiketrains[0], 'waveforms') and len(spiketrains) == 1: train = spiketrains[0] if self.__lazy and not hasattr(train, 'lazy_shape'): train.lazy_shape = train.shape train = train[:0] return train if hasattr(spiketrains[0], 't_stop'): # workaround for bug in some broken files istrain = [hasattr(utrain, 'waveforms') for utrain in spiketrains] if not all(istrain): goodtrains = [itrain for i, itrain in enumerate(spiketrains) if istrain[i]] badtrains = [itrain for i, itrain in enumerate(spiketrains) if not istrain[i]] spiketrains = (goodtrains + [self._combine_spiketrains(badtrains)]) spiketrains = [itrain for itrain in spiketrains if itrain.size > 0] if not spiketrains: train = self._default_spiketrain.copy() train.file_origin = self.__file_origin if self.__lazy: train.lazy_shape = (0,) return train # get the times of the spiketrains and combine them waveforms = [itrain.waveforms for itrain in spiketrains] rawtrains = np.array(np.hstack(spiketrains)) times = pq.Quantity(rawtrains, units=pq.ms, copy=False) lens1 = np.array([wave.shape[1] for wave in waveforms]) lens2 = np.array([wave.shape[2] for wave in waveforms]) if lens1.max() != lens1.min() or lens2.max() != lens2.min(): lens1 = lens1.max() - lens1 lens2 = lens2.max() - lens2 waveforms = [np.pad(waveform, ((0, 0), (0, len1), (0, len2)), 'constant') for waveform, len1, len2 in zip(waveforms, lens1, lens2)] waveforms = np.vstack(waveforms) # extract the trig2 annotation trig2 = np.array(np.hstack([itrain.annotations['trig2'] for itrain in spiketrains])) trig2 = pq.Quantity(trig2, units=pq.ms) elif hasattr(spiketrains[0], 'units'): return self._combine_spiketrains([spiketrains]) else: times, waveforms, trig2 = zip(*spiketrains) times = np.hstack(times) # get the times of the SpikeTrains and combine them times = pq.Quantity(times, units=pq.ms, copy=False) # get the waveforms of the SpikeTrains and combine them # these should be a 3D array with the first axis being the spike, # the second axis being the recording channel (there is only one), # and the third axis being the actual waveform waveforms = np.vstack(waveforms)[np.newaxis].swapaxes(0, 1) # extract the trig2 annotation trig2 = pq.Quantity(np.hstack(trig2), units=pq.ms, copy=False) if not times.size: train = self._default_spiketrain.copy() train.file_origin = self.__file_origin if self.__lazy: train.lazy_shape = (0,) return train # get the maximum time t_stop = times.max() * 2 t_start = pq.Quantity(0, units=pq.ms, dtype=times.dtype) if self.__lazy: timesshape = times.shape times = pq.Quantity([], units=pq.ms, copy=False) waveforms = pq.Quantity([[[]]], units=pq.mV) else: waveforms = pq.Quantity(np.asarray(waveforms), units=pq.mV) train = SpikeTrain(times=times, copy=False, t_start=t_start, t_stop=t_stop, file_origin=self.__file_origin, waveforms=waveforms, timestamp=self._default_datetime, respwin=np.array([], dtype=np.int32), dama_index=-1, trig2=trig2, side='') if self.__lazy: train.lazy_shape = timesshape return train # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- # IMPORTANT!!! # These are private methods implementing the internal reading mechanism. # Due to the way BrainWare SRC files are structured, they CANNOT be used # on their own. Calling these manually will almost certainly alter your # position in the file in an unrecoverable manner, whether they throw # an exception or not. # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- def __read_annotations(self): ''' Read the stimulus grid properties. ------------------------------------------------------------------- Returns a dictionary containing the parameter names as keys and the parameter values as values. The returned object must be added to the Block. ID: 29109 ''' # int16 -- number of stimulus parameters numelements = np.fromfile(self.__fsrc, dtype=np.int16, count=1)[0] if not numelements: return {} # [data sequence] * numelements -- parameter names names = [] for i in range(numelements): # {skip} = byte (char) -- skip one byte self.__fsrc.seek(1, 1) # uint8 -- length of next string numchars = np.fromfile(self.__fsrc, dtype=np.uint8, count=1)[0] # if there is no name, make one up if not numchars: name = 'param%s' % i else: # char * numchars -- parameter name string name = str(np.fromfile(self.__fsrc, dtype='S%s' % numchars, count=1)[0].decode('UTF-8')) # if the name is already in there, add a unique number to it # so it isn't overwritten if name in names: name = name + str(i) names.append(name) # float32 * numelements -- an array of parameter values values = np.fromfile(self.__fsrc, dtype=np.float32, count=numelements) # combine the names and values into a dict # the dict will be added to the annotations annotations = dict(zip(names, values)) return annotations def __read_annotations_old(self): ''' Read the stimulus grid properties. Returns a dictionary containing the parameter names as keys and the parameter values as values. ------------------------------------------------ The returned objects must be added to the Block. This reads an old version of the format that does not store paramater names, so placeholder names are created instead. ID: 29099 ''' # int16 * 14 -- an array of parameter values values = np.fromfile(self.__fsrc, dtype=np.int16, count=14) # create dummy names and combine them with the values in a dict # the dict will be added to the annotations params = ['param%s' % i for i in range(len(values))] annotations = dict(zip(params, values)) return annotations def __read_comment(self): ''' Read a single comment. The comment is stored as an Event in Segment 0, which is specifically for comments. ---------------------- Returns an empty list. The returned object is already added to the Block. No ID number: always called from another method ''' # float64 -- timestamp (number of days since dec 30th 1899) time = np.fromfile(self.__fsrc, dtype=np.double, count=1)[0] # convert the timestamp to a python datetime object # then convert that to a quantity containing a unix timestamp timestamp = self.convert_timestamp(time) time = pq.Quantity(time, units=pq.d) # int16 -- length of next string numchars1 = np.fromfile(self.__fsrc, dtype=np.int16, count=1)[0] # char * numchars -- the one who sent the comment sender = str(np.fromfile(self.__fsrc, dtype='S%s' % numchars1, count=1)[0].decode('UTF-8')) # int16 -- length of next string numchars2 = np.fromfile(self.__fsrc, dtype=np.int16, count=1)[0] # char * numchars -- comment text text = str(np.fromfile(self.__fsrc, dtype='S%s' % numchars2, count=1)[0].decode('UTF-8')) comment = Event(time=time, label=text, sender=sender, name='Comment', description='container for a comment', file_origin=self.__file_origin, timestamp=timestamp) self.__blk.segments[0].events.append(comment) return [] def __read_list(self): ''' Read a list of arbitrary data sequences It only says how many data sequences should be read. These sequences are then read by their ID number. Note that lists can be nested. If there are too many sequences (for instance if there are a large number of spikes in a Segment) then a negative number will be returned for the number of data sequences to read. In this case the method tries to guess. This also means that all future list data sequences have unreliable lengths as well. ------------------------------------------- Returns a list of objects. Whether these objects need to be added to the Block depends on the object in question. There are several data sequences that have identical formats but are used in different situations. That means this data sequences has multiple ID numbers. ID: 29082 ID: 29083 ID: 29091 ID: 29093 ''' # int16 -- number of sequences to read numelements = np.fromfile(self.__fsrc, dtype=np.int16, count=1)[0] # {skip} = bytes * 4 (int16 * 2) -- skip four bytes self.__fsrc.seek(4, 1) if numelements == 0: return [] if not self.__damaged and numelements < 0: self.__damaged = True warn('Negative sequence count, file may be damaged') if not self.__damaged: # read the sequences into a list seq_list = [self._read_by_id() for _ in range(numelements)] else: # read until we get some indication we should stop seq_list = [] # uint16 -- the ID of the next sequence seqidinit = np.fromfile(self.__fsrc, dtype=np.uint16, count=1)[0] # {rewind} = byte * 2 (int16) -- move back 2 bytes, i.e. go back to # before the beginning of the seqid self.__fsrc.seek(-2, 1) while 1: # uint16 -- the ID of the next sequence seqid = np.fromfile(self.__fsrc, dtype=np.uint16, count=1)[0] # {rewind} = byte * 2 (int16) -- move back 2 bytes, i.e. go # back to before the beginning of the seqid self.__fsrc.seek(-2, 1) # if we come across a new sequence, we are at the end of the # list so we should stop if seqidinit != seqid: break # otherwise read the next sequence seq_list.append(self._read_by_id()) return seq_list def __read_segment(self): ''' Read an individual Segment. A Segment contains a dictionary of parameters, the length of the recording, a list of Units with their Spikes, and a list of Spikes not assigned to any Unit. The unassigned spikes are always stored in Unit 0, which is exclusively for storing these spikes. ------------------------------------------------- Returns the Segment object created by the method. The returned object is already added to the Block. ID: 29106 ''' # (data_obj) -- the stimulus parameters for this segment annotations = self._read_by_id() annotations['feature_type'] = -1 annotations['go_by_closest_unit_center'] = False annotations['include_unit_bounds'] = False # (data_obj) -- SpikeTrain list of unassigned spikes # these go in the first Unit since it is for unassigned spikes unassigned_spikes = self._read_by_id() self.__rcg.units[0].spiketrains.extend(unassigned_spikes) # read a list of units and grab the second return value, which is the # SpikeTrains from this Segment (if we use the Unit we will get all the # SpikeTrains from that Unit, resuling in duplicates if we are past # the first Segment trains = self._read_by_id() if not trains: if unassigned_spikes: # if there are no assigned spikes, # just use the unassigned spikes trains = zip(unassigned_spikes) else: # if there are no spiketrains at all, # create an empty spike train train = self._default_spiketrain.copy() train.file_origin = self.__file_origin if self.__lazy: train.lazy_shape = (0,) trains = [[train]] elif hasattr(trains[0], 'dtype'): #workaround for some broken files trains = [unassigned_spikes + [self._combine_spiketrains([trains])]] else: # get the second element from each returned value, # which is the actual SpikeTrains trains = [unassigned_spikes] + [train[1] for train in trains] # re-organize by sweeps trains = zip(*trains) # int32 -- SpikeTrain length in ms spiketrainlen = pq.Quantity(np.fromfile(self.__fsrc, dtype=np.int32, count=1)[0], units=pq.ms, copy=False) segments = [] for train in trains: # create the Segment and add everything to it segment = Segment(file_origin=self.__file_origin, **annotations) segment.spiketrains = train self.__blk.segments.append(segment) segments.append(segment) for itrain in train: # use the SpikeTrain length to figure out the stop time # t_start is always 0 so we can ignore it itrain.t_stop = spiketrainlen return segments def __read_segment_list(self): ''' Read a list of Segments with comments. Since comments can occur at any point, whether a recording is happening or not, it is impossible to reliably assign them to a specific Segment. For this reason they are always assigned to Segment 0, which is exclusively used to store comments. -------------------------------------------------------- Returns a list of the Segments created with this method. The returned objects are already added to the Block. ID: 29112 ''' # uint8 -- number of electrode channels in the Segment numchannels = np.fromfile(self.__fsrc, dtype=np.uint8, count=1)[0] # [list of sequences] -- individual Segments segments = self.__read_list() while not hasattr(segments[0], 'spiketrains'): segments = sum(segments, []) # char -- "side of brain" info side = str(np.fromfile(self.__fsrc, dtype='S1', count=1)[0].decode('UTF-8')) # int16 -- number of comments numelements = np.fromfile(self.__fsrc, dtype=np.int16, count=1)[0] # comment_obj * numelements -- comments about the Segments # we don't know which Segment specifically, though for _ in range(numelements): self.__read_comment() # create an empty RecordingChannel for each of the numchannels for i in range(numchannels): chan = RecordingChannel(file_origin=self.__file_origin, index=int(i), name='Chan'+str(int(i))) self.__rcg.recordingchannels.append(chan) # store what side of the head we are dealing with for segment in segments: for spiketrain in segment.spiketrains: spiketrain.annotations['side'] = side return segments def __read_segment_list_v8(self): ''' Read a list of Segments with comments. This is version 8 of the data sequence. This is the same as __read_segment_list_var, but can also contain one or more arbitrary sequences. The class makes an attempt to assign the sequences when possible, and warns the user when this happens (see the _assign_sequence method) -------------------------------------------------------- Returns a list of the Segments created with this method. The returned objects are already added to the Block. ID: 29117 ''' # segment_collection_var -- this is based off a segment_collection_var segments = self.__read_segment_list_var() # uint16 -- the ID of the next sequence seqid = np.fromfile(self.__fsrc, dtype=np.uint16, count=1)[0] # {rewind} = byte * 2 (int16) -- move back 2 bytes, i.e. go back to # before the beginning of the seqid self.__fsrc.seek(-2, 1) if seqid in self.__ID_DICT: # if it is a valid seqid, read it and try to figure out where # to put it self._assign_sequence(self._read_by_id()) else: # otherwise it is a Unit list self.__read_unit_list() # {skip} = byte * 2 (int16) -- skip 2 bytes self.__fsrc.seek(2, 1) return segments def __read_segment_list_v9(self): ''' Read a list of Segments with comments. This is version 9 of the data sequence. This is the same as __read_segment_list_v8, but contains some additional annotations. These annotations are added to the Segment. -------------------------------------------------------- Returns a list of the Segments created with this method. The returned objects are already added to the Block. ID: 29120 ''' # segment_collection_v8 -- this is based off a segment_collection_v8 segments = self.__read_segment_list_v8() # uint8 feature_type = np.fromfile(self.__fsrc, dtype=np.uint8, count=1)[0] # uint8 go_by_closest_unit_center = np.fromfile(self.__fsrc, dtype=np.bool8, count=1)[0] # uint8 include_unit_bounds = np.fromfile(self.__fsrc, dtype=np.bool8, count=1)[0] # create a dictionary of the annotations annotations = {'feature_type': feature_type, 'go_by_closest_unit_center': go_by_closest_unit_center, 'include_unit_bounds': include_unit_bounds} # add the annotations to each Segment for segment in segments: segment.annotations.update(annotations) return segments def __read_segment_list_var(self): ''' Read a list of Segments with comments. This is the same as __read_segment_list, but contains information regarding the sampling period. This information is added to the SpikeTrains in the Segments. -------------------------------------------------------- Returns a list of the Segments created with this method. The returned objects are already added to the Block. ID: 29114 ''' # float32 -- DA conversion clock period in microsec sampling_period = pq.Quantity(np.fromfile(self.__fsrc, dtype=np.float32, count=1), units=pq.us, copy=False)[0] # segment_collection -- this is based off a segment_collection segments = self.__read_segment_list() # add the sampling period to each SpikeTrain for segment in segments: for spiketrain in segment.spiketrains: spiketrain.sampling_period = sampling_period return segments def __read_spike_fixed(self, numpts=40): ''' Read a spike with a fixed waveform length (40 time bins) ------------------------------------------- Returns the time, waveform and trig2 value. The returned objects must be converted to a SpikeTrain then added to the Block. ID: 29079 ''' # float32 -- spike time stamp in ms since start of SpikeTrain time = np.fromfile(self.__fsrc, dtype=np.float32, count=1) # int8 * 40 -- spike shape -- use numpts for spike_var waveform = np.fromfile(self.__fsrc, dtype=np.int8, count=numpts) # uint8 -- point of return to noise trig2 = np.fromfile(self.__fsrc, dtype=np.uint8, count=1) return time, waveform, trig2 def __read_spike_fixed_old(self): ''' Read a spike with a fixed waveform length (40 time bins) This is an old version of the format. The time is stored as ints representing 1/25 ms time steps. It has no trigger information. ------------------------------------------- Returns the time, waveform and trig2 value. The returned objects must be converted to a SpikeTrain then added to the Block. ID: 29081 ''' # int32 -- spike time stamp in ms since start of SpikeTrain time = np.fromfile(self.__fsrc, dtype=np.int32, count=1) / 25. # int8 * 40 -- spike shape # This needs to be a 2D array, one for each channel. BrainWare # only ever has a single channel per file. waveform = np.fromfile(self.__fsrc, dtype=np.int8, count=40) # create a dummy trig2 value trig2 = np.array([-1], dtype=np.uint8) return time, waveform, trig2 def __read_spike_var(self): ''' Read a spike with a variable waveform length ------------------------------------------- Returns the time, waveform and trig2 value. The returned objects must be converted to a SpikeTrain then added to the Block. ID: 29115 ''' # uint8 -- number of points in spike shape numpts = np.fromfile(self.__fsrc, dtype=np.uint8, count=1)[0] # spike_fixed is the same as spike_var if you don't read the numpts # byte and set numpts = 40 return self.__read_spike_fixed(numpts) def __read_spiketrain_indexed(self): ''' Read a SpikeTrain This is the same as __read_spiketrain_timestamped except it also contains the index of the Segment in the dam file. The index is stored as an annotation in the SpikeTrain. ------------------------------------------------- Returns a SpikeTrain object with multiple spikes. The returned object must be added to the Block. ID: 29121 ''' #int32 -- index of the analogsignalarray in corresponding .dam file dama_index = np.fromfile(self.__fsrc, dtype=np.int32, count=1)[0] # spiketrain_timestamped -- this is based off a spiketrain_timestamped spiketrain = self.__read_spiketrain_timestamped() # add the property to the dict spiketrain.annotations['dama_index'] = dama_index return spiketrain def __read_spiketrain_timestamped(self): ''' Read a SpikeTrain This SpikeTrain contains a time stamp for when it was recorded The timestamp is stored as an annotation in the SpikeTrain. ------------------------------------------------- Returns a SpikeTrain object with multiple spikes. The returned object must be added to the Block. ID: 29110 ''' # float64 -- timeStamp (number of days since dec 30th 1899) timestamp = np.fromfile(self.__fsrc, dtype=np.double, count=1)[0] # convert to datetime object timestamp = self.convert_timestamp(timestamp) # seq_list -- spike list # combine the spikes into a single SpikeTrain spiketrain = self._combine_spiketrains(self.__read_list()) # add the timestamp spiketrain.annotations['timestamp'] = timestamp return spiketrain def __read_unit(self): ''' Read all SpikeTrains from a single Segment and Unit This is the same as __read_unit_unsorted except it also contains information on the spike sorting boundaries. ------------------------------------------------------------------ Returns a single Unit and a list of SpikeTrains from that Unit and current Segment, in that order. The SpikeTrains must be returned since it is not possible to determine from the Unit which SpikeTrains are from the current Segment. The returned objects are already added to the Block. The SpikeTrains must be added to the current Segment. ID: 29116 ''' # same as unsorted Unit unit, trains = self.__read_unit_unsorted() # float32 * 18 -- Unit boundaries (IEEE 32-bit floats) unit.annotations['boundaries'] = [np.fromfile(self.__fsrc, dtype=np.float32, count=18)] # uint8 * 9 -- boolean values indicating elliptic feature boundary # dimensions unit.annotations['elliptic'] = [np.fromfile(self.__fsrc, dtype=np.uint8, count=9)] return unit, trains def __read_unit_list(self): ''' A list of a list of Units ----------------------------------------------- Returns a list of Units modified in the method. The returned objects are already added to the Block. No ID number: only called by other methods ''' # this is used to figure out which Units to return maxunit = 1 # int16 -- number of time slices numelements = np.fromfile(self.__fsrc, dtype=np.int16, count=1)[0] # {sequence} * numelements1 -- the number of lists of Units to read self.__rcg.annotations['max_valid'] = [] for _ in range(numelements): # {skip} = byte * 2 (int16) -- skip 2 bytes self.__fsrc.seek(2, 1) # double max_valid = np.fromfile(self.__fsrc, dtype=np.double, count=1)[0] # int16 - the number of Units to read numunits = np.fromfile(self.__fsrc, dtype=np.int16, count=1)[0] # update tha maximum Unit so far maxunit = max(maxunit, numunits + 1) # if there aren't enough Units, create them # remember we need to skip the UnassignedSpikes Unit if numunits > len(self.__rcg.units) + 1: for ind1 in range(len(self.__rcg.units), numunits + 1): unit = Unit(name='unit%s' % ind1, file_origin=self.__file_origin, elliptic=[], boundaries=[], timestamp=[], max_valid=[]) self.__rcg.units.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.__rcg.units[ind1 + 1] # {skip} = byte * 2 (int16) -- skip 2 bytes self.__fsrc.seek(2, 1) # int16 -- a multiplier for the elliptic and boundaries # properties numelements3 = np.fromfile(self.__fsrc, dtype=np.int16, count=1)[0] # uint8 * 10 * numelements3 -- boolean values indicating # elliptic feature boundary dimensions elliptic = np.fromfile(self.__fsrc, dtype=np.uint8, count=10 * numelements3) # float32 * 20 * numelements3 -- feature boundaries boundaries = np.fromfile(self.__fsrc, dtype=np.float32, count=20 * numelements3) unit.annotations['elliptic'].append(elliptic) unit.annotations['boundaries'].append(boundaries) unit.annotations['max_valid'].append(max_valid) return self.__rcg.units[1:maxunit] def __read_unit_list_timestamped(self): ''' A list of a list of Units. This is the same as __read_unit_list, except that it also has a timestamp. This is added ad an annotation to all Units. ----------------------------------------------- Returns a list of Units modified in the method. The returned objects are already added to the Block. ID: 29119 ''' # double -- time zero (number of days since dec 30th 1899) timestamp = np.fromfile(self.__fsrc, dtype=np.double, count=1)[0] # convert to to days since UNIX epoc time: timestamp = self.convert_timestamp(timestamp) # sorter -- this is based off a sorter units = self.__read_unit_list() for unit in units: unit.annotations['timestamp'].append(timestamp) return units def __read_unit_old(self): ''' Read all SpikeTrains from a single Segment and Unit This is the same as __read_unit_unsorted except it also contains information on the spike sorting boundaries. This is an old version of the format that used 48-bit floating-point numbers for the boundaries. These cannot easily be read and so are skipped. ------------------------------------------------------------------ Returns a single Unit and a list of SpikeTrains from that Unit and current Segment, in that order. The SpikeTrains must be returned since it is not possible to determine from the Unit which SpikeTrains are from the current Segment. The returned objects are already added to the Block. The SpikeTrains must be added to the current Segment. ID: 29107 ''' # same as Unit unit, trains = self.__read_unit_unsorted() # bytes * 108 (float48 * 18) -- Unit boundaries (48-bit floating # point numbers are not supported so we skip them) self.__fsrc.seek(108, 1) # uint8 * 9 -- boolean values indicating elliptic feature boundary # dimensions unit.annotations['elliptic'] = np.fromfile(self.__fsrc, dtype=np.uint8, count=9).tolist() return unit, trains def __read_unit_unsorted(self): ''' Read all SpikeTrains from a single Segment and Unit This does not contain Unit boundaries. ------------------------------------------------------------------ Returns a single Unit and a list of SpikeTrains from that Unit and current Segment, in that order. The SpikeTrains must be returned since it is not possible to determine from the Unit which SpikeTrains are from the current Segment. The returned objects are already added to the Block. The SpikeTrains must be added to the current Segment. ID: 29084 ''' # {skip} = bytes * 2 (uint16) -- skip two bytes self.__fsrc.seek(2, 1) # uint16 -- number of characters in next string numchars = np.fromfile(self.__fsrc, dtype=np.uint16, count=1)[0] # char * numchars -- ID string of Unit name = str(np.fromfile(self.__fsrc, dtype='S%s' % numchars, count=1)[0].decode('UTF-8')) # int32 -- SpikeTrain length in ms t_stop = pq.Quantity(np.fromfile(self.__fsrc, dtype=np.int32, count=1)[0].astype('float32'), units=pq.ms, copy=False) # int32 * 4 -- response and spon period boundaries respwin = np.fromfile(self.__fsrc, dtype=np.int32, count=4) # (data_obj) -- list of SpikeTrains spikeslists = self._read_by_id() # use the Unit if it already exists, otherwise create it if name in self.__unitdict: unit = self.__unitdict[name] else: unit = Unit(name=name, file_origin=self.__file_origin, elliptic=[], boundaries=[], timestamp=[], max_valid=[]) self.__rcg.units.append(unit) self.__unitdict[name] = unit # convert the individual spikes to SpikeTrains and add them to the Unit trains = [self._combine_spiketrains(spikes) for spikes in spikeslists] unit.spiketrains.extend(trains) for train in trains: train.t_stop = t_stop train.annotations['respwin'] = respwin return unit, trains def __skip_information(self): ''' Read an information sequence. This is data sequence is skipped both here and in the Matlab reference implementation. ---------------------- Returns an empty list Nothing is created so nothing is added to the Block. ID: 29113 ''' # {skip} char * 34 -- display information self.__fsrc.seek(34, 1) return [] def __skip_information_old(self): ''' Read an information sequence This is data sequence is skipped both here and in the Matlab reference implementation This is an old version of the format ---------------------- Returns an empty list. Nothing is created so nothing is added to the Block. ID: 29100 ''' # {skip} char * 4 -- display information self.__fsrc.seek(4, 1) return [] # This dictionary maps the numeric data sequence ID codes to the data # sequence reading functions. # # Since functions are first-class objects in Python, the functions returned # from this dictionary are directly callable. # # If new data sequence ID codes are added in the future please add the code # here in numeric order and the method above in alphabetical order # # The naming of any private method may change at any time __ID_DICT = {29079: __read_spike_fixed, 29081: __read_spike_fixed_old, 29082: __read_list, 29083: __read_list, 29084: __read_unit_unsorted, 29091: __read_list, 29093: __read_list, 29099: __read_annotations_old, 29100: __skip_information_old, 29106: __read_segment, 29107: __read_unit_old, 29109: __read_annotations, 29110: __read_spiketrain_timestamped, 29112: __read_segment_list, 29113: __skip_information, 29114: __read_segment_list_var, 29115: __read_spike_var, 29116: __read_unit, 29117: __read_segment_list_v8, 29119: __read_unit_list_timestamped, 29120: __read_segment_list_v9, 29121: __read_spiketrain_indexed } def convert_brainwaresrc_timestamp(timestamp, start_date=datetime(1899, 12, 30)): ''' convert_brainwaresrc_timestamp(timestamp, start_date) - convert a timestamp in brainware units to a python datetime object. start_date defaults to 1899.12.30 (ISO format), which is the start date used by all BrainWare SRC data blocks so far. If manually specified it should be a datetime object or any other object that can be added to a timedelta object. ''' # datetime + timedelta = datetime again. try: timestamp = start_date + timedelta(days=timestamp) except OverflowError as err: timestamp = start_date warn(str(err)) return timestamp neo-0.3.3/neo/io/elanio.py0000644000175000017500000002725712273723542016377 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Class for reading/writing data from Elan. Elan is software for studying time-frequency maps of EEG data. Elan is developed in Lyon, France, at INSERM U821 An Elan dataset is separated into 3 files : - .eeg raw data file - .eeg.ent hearder file - .eeg.pos event file Depend on: Supported : Read and Write Author: sgarcia """ import datetime import os import re import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import Segment, AnalogSignal, EventArray from neo.io.tools import create_many_to_one_relationship class VersionError(Exception): def __init__(self, value): self.value = value def __str__(self): return repr(self.value) class ElanIO(BaseIO): """ Classe for reading/writing data from Elan. Usage: >>> from neo import io >>> r = io.ElanIO( filename = 'File_elan_1.eeg') >>> seg = r.read_segment(lazy = False, cascade = True,) >>> print seg.analogsignals # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [] >>> print seg.spiketrains # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [] >>> print seg.eventarrays # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [] """ is_readable = True is_writable = False supported_objects = [Segment, AnalogSignal, EventArray] readable_objects = [Segment] writeable_objects = [ ] has_header = False is_streameable = False read_params = { Segment : [ ] } write_params = { Segment : [ ] } name = None extensions = ['eeg'] mode = 'file' def __init__(self , filename = None) : """ This class read/write a elan based file. **Arguments** filename : the filename to read or write """ BaseIO.__init__(self) self.filename = filename def read_segment(self, lazy = False, cascade = True): ## Read header file f = open(self.filename+'.ent' , 'rU') #version version = f.readline() if version[:2] != 'V2' and version[:2] != 'V3': # raise('read only V2 .eeg.ent files') raise VersionError('Read only V2 or V3 .eeg.ent files. %s given' % version[:2]) return #info info1 = f.readline()[:-1] info2 = f.readline()[:-1] # strange 2 line for datetime #line1 l = f.readline() r1 = re.findall('(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)',l) r2 = re.findall('(\d+):(\d+):(\d+)',l) r3 = re.findall('(\d+)-(\d+)-(\d+)',l) YY, MM, DD, hh, mm, ss = (None, )*6 if len(r1) != 0 : DD , MM, YY, hh ,mm ,ss = r1[0] elif len(r2) != 0 : hh ,mm ,ss = r2[0] elif len(r3) != 0: DD , MM, YY= r3[0] #line2 l = f.readline() r1 = re.findall('(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)',l) r2 = re.findall('(\d+):(\d+):(\d+)',l) r3 = re.findall('(\d+)-(\d+)-(\d+)',l) if len(r1) != 0 : DD , MM, YY, hh ,mm ,ss = r1[0] elif len(r2) != 0 : hh ,mm ,ss = r2[0] elif len(r3) != 0: DD , MM, YY= r3[0] try: fulldatetime = datetime.datetime(int(YY) , int(MM) , int(DD) , int(hh) , int(mm) , int(ss) ) except: fulldatetime = None seg = Segment( file_origin = os.path.basename(self.filename), elan_version = version, info1 = info1, info2 = info2, rec_datetime = fulldatetime, ) if not cascade : return seg l = f.readline() l = f.readline() l = f.readline() # sampling rate sample l = f.readline() sampling_rate = 1./float(l) * pq.Hz # nb channel l = f.readline() nbchannel = int(l)-2 #channel label labels = [ ] for c in range(nbchannel+2) : labels.append(f.readline()[:-1]) # channel type types = [ ] for c in range(nbchannel+2) : types.append(f.readline()[:-1]) # channel unit units = [ ] for c in range(nbchannel+2) : units.append(f.readline()[:-1]) #print units #range min_physic = [] for c in range(nbchannel+2) : min_physic.append( float(f.readline()) ) max_physic = [] for c in range(nbchannel+2) : max_physic.append( float(f.readline()) ) min_logic = [] for c in range(nbchannel+2) : min_logic.append( float(f.readline()) ) max_logic = [] for c in range(nbchannel+2) : max_logic.append( float(f.readline()) ) #info filter info_filter = [] for c in range(nbchannel+2) : info_filter.append(f.readline()[:-1]) f.close() #raw data n = int(round(np.log(max_logic[0]-min_logic[0])/np.log(2))/8) data = np.fromfile(self.filename,dtype = 'i'+str(n) ) data = data.byteswap().reshape( (data.size/(nbchannel+2) ,nbchannel+2) ).astype('f4') for c in range(nbchannel) : if lazy: sig = [ ] else: sig = (data[:,c]-min_logic[c])/(max_logic[c]-min_logic[c])*\ (max_physic[c]-min_physic[c])+min_physic[c] try: unit = pq.Quantity(1, units[c] ) except: unit = pq.Quantity(1, '' ) anaSig = AnalogSignal(sig * unit, sampling_rate=sampling_rate, t_start=0. * pq.s, name=labels[c], channel_index=c) if lazy: anaSig.lazy_shape = data.shape[0] anaSig.annotate(channel_name= labels[c]) seg.analogsignals.append( anaSig ) # triggers f = open(self.filename+'.pos') times =[ ] labels = [ ] reject_codes = [ ] for l in f.readlines() : r = re.findall(' *(\d+) *(\d+) *(\d+) *',l) times.append( float(r[0][0])/sampling_rate.magnitude ) labels.append(str(r[0][1]) ) reject_codes.append( str(r[0][2]) ) if lazy: times = [ ]*pq.S labels = np.array([ ], dtype = 'S') reject_codes = [ ] else: times = np.array(times) * pq.s labels = np.array(labels) reject_codes = np.array(reject_codes) ea = EventArray( times = times, labels = labels, reject_codes = reject_codes, ) if lazy: ea.lazy_shape = len(times) seg.eventarrays.append(ea) f.close() create_many_to_one_relationship(seg) return seg #~ def write_segment(self, segment, ): #~ """ #~ Arguments: #~ segment : the segment to write. Only analog signals and events will be written. #~ """ #~ assert self.filename.endswith('.eeg') #~ fid_ent = open(self.filename+'.ent' ,'wt') #~ fid_eeg = open(self.filename ,'wt') #~ fid_pos = open(self.filename+'.pos' ,'wt') #~ seg = segment #~ sampling_rate = seg._analogsignals[0].sampling_rate #~ N = len(seg._analogsignals) #~ # #~ # header file #~ # #~ fid_ent.write('V2\n') #~ fid_ent.write('OpenElectrophyImport\n') #~ fid_ent.write('ELAN\n') #~ t = datetime.datetime.now() #~ fid_ent.write(t.strftime('%d-%m-%Y %H:%M:%S')+'\n') #~ fid_ent.write(t.strftime('%d-%m-%Y %H:%M:%S')+'\n') #~ fid_ent.write('-1\n') #~ fid_ent.write('reserved\n') #~ fid_ent.write('-1\n') #~ fid_ent.write('%g\n' % (1./sampling_rate)) #~ fid_ent.write( '%d\n' % (N+2) ) #~ # channel label #~ for i, anaSig in enumerate(seg.analogsignals) : #~ try : #~ fid_ent.write('%s.%d\n' % (anaSig.label, i+1 )) #~ except : #~ fid_ent.write('%s.%d\n' % ('nolabel', i+1 )) #~ fid_ent.write('Num1\n') #~ fid_ent.write('Num2\n') #~ #channel type #~ for i, anaSig in enumerate(seg.analogsignals) : #~ fid_ent.write('Electrode\n') #~ fid_ent.write( 'dateur echantillon\n') #~ fid_ent.write( 'type evenement et byte info\n') #~ #units #~ for i, anaSig in enumerate(seg._analogsignals) : #~ unit_txt = str(anaSig.units).split(' ')[1] #~ fid_ent.write('%s\n' % unit_txt) #~ fid_ent.write('sans\n') #~ fid_ent.write('sans\n') #~ #range and data #~ list_range = [] #~ data = np.zeros( (seg._analogsignals[0].size , N+2) , 'i2') #~ for i, anaSig in enumerate(seg._analogsignals) : #~ # in elan file unit is supposed to be in microV to have a big range #~ # so auto translate #~ if anaSig.units == pq.V or anaSig.units == pq.mV: #~ s = anaSig.rescale('uV').magnitude #~ elif anaSig.units == pq.uV: #~ s = anaSig.magnitude #~ else: #~ # automatic range in arbitrry unit #~ s = anaSig.magnitude #~ s*= 10**(int(np.log10(abs(s).max()))+1) #~ list_range.append( int(abs(s).max()) +1 ) #~ s2 = s*65535/(2*list_range[i]) #~ data[:,i] = s2.astype('i2') #~ for r in list_range : #~ fid_ent.write('-%.0f\n'% r) #~ fid_ent.write('-1\n') #~ fid_ent.write('-1\n') #~ for r in list_range : #~ fid_ent.write('%.0f\n'% r) #~ fid_ent.write('+1\n') #~ fid_ent.write('+1\n') #~ for i in range(N+2) : #~ fid_ent.write('-32768\n') #~ for i in range(N+2) : #~ fid_ent.write('+32767\n') #~ #info filter #~ for i in range(N+2) : #~ fid_ent.write('passe-haut ? Hz passe-bas ? Hz\n') #~ fid_ent.write('sans\n') #~ fid_ent.write('sans\n') #~ for i in range(N+2) : #~ fid_ent.write('1\n') #~ for i in range(N+2) : #~ fid_ent.write('reserved\n') #~ # raw file .eeg #~ if len(seg._eventarrays) == 1: #~ ea = seg._eventarrays[0] #~ trigs = (ea.times*sampling_rate).magnitude #~ trigs = trigs.astype('i') #~ trigs2 = trigs[ (trigs>0) & (trigs= 2.2 - quantities Quick reference: ================================================================================ Class NeoHdf5IO() with methods get(), save(), delete() is implemented. This class represents a connection manager with the HDF5 file with the possibility to put (save()) or retrieve (get()) runtime NEO objects from the file. Start by initializing IO: >>> from neo.io.hdf5io import NeoHdf5IO >>> iom = NeoHdf5IO('myfile.h5') >>> iom Now you may save any of your neo objects into the file: >>> b = Block() >>> iom.write_block(b) or just do >>> iom.save(b) After you stored an object it receives a unique "path" in the hdf5 file. This is exactly the place in the HDF5 hierarchy, where it was written. This information is now accessible by "hdf5_path" property: >>> b.hdf5_path '/block_0' You may save more complicated NEO stuctures, with relations and arrays: >>> import numpy as np >>> import quantities as pq >>> s = Segment() >>> b.segments.append(s) >>> a1 = AnalogSignal(signal=np.random.rand(300), t_start=42*pq.ms) >>> s.analogsignals.append(a1) and then >>> iom.write_block(b) or just >>> iom.save(b) If you already have hdf5 file in NEO format, or you just created one, then you may want to read NEO data (providing the path to what to read): >>> b1 = iom.read_block("/block_0") >>> b1 or just use >>> b1 = iom.get("/block_0") You may notice, by default the reading function retrieves all available data, with all downstream relations and arrays: >>> b1._segments [] >>> b1._segments[0]._analogsignals[0].signal array([ 3.18987819e-01, 1.08448284e-01, 1.03858980e-01, ... 3.78908705e-01, 3.08669731e-02, 9.48965785e-01]) * dimensionless When you need to save time and performance, you may load an object without relations >>> b2 = iom.get("/block_0", cascade=False) >>> b2._segments [] and/or even without arrays >>> a2 = iom.get("/block_0/_segments/segment_0/_analogsignals/analogsignal_0", lazy=True) >>> a2.signal [] These functions return "pure" NEO objects. They are completely "detached" from the HDF5 file - changes to the runtime objects will not cause any changes in the file: >>> a2.t_start array(42.0) * ms >>> a2.t_start = 32 * pq.ms >>> a2.t_start array(32.0) * ms >>> iom.get("/block_0/_segments/segment_0/_analogsignals/analogsignal_0").t_start array(42.0) * ms However, if you want to work directly with HDF5 storage making instant modifications, you may use the native PyTables functionality, where all objects are accessible through "._data.root": >>> iom._data.root / (RootGroup) 'neo.h5' children := ['block_0' (Group)] >>> b3 = iom._data.root.block_0 >>> b3 /block_0 (Group) '' children := ['_recordingchannelgroups' (Group), '_segments' (Group)] To understand more about this "direct" way of working with data, please refer to http://www.pytables.org/ Finally, you may get an overview of the contents of the file by running >>> iom.get_info() This is a neo.HDF5 file. it contains: {'spiketrain': 0, 'irsaanalogsignal': 0, 'analogsignalarray': 0, 'recordingchannelgroup': 0, 'eventarray': 0, 'analogsignal': 1, 'epoch': 0, 'unit': 0, 'recordingchannel': 0, 'spike': 0, 'epocharray': 0, 'segment': 1, 'event': 0, 'block': 1} The general structure of the file: ================================================================================ \'Block_1' \ \'Block_2' \ \---'_recordingchannelgroups' \ \ \ \---'RecordingChannelGroup_1' \ \ \ \---'RecordingChannelGroup_2' \ \ \ \---'_recordingchannels' \ \ \ \---'RecordingChannel_1' \ \ \ \---'RecordingChannel_2' \ \ \ \---'_units' \ \ \ \---'Unit_1' \ \ \ \---'Unit_2' \ \---'_segments' \ \--'Segment_1' \ \--'Segment_2' \ \---'_epochs' \ \ \ \---'Epoch_1' \ \---'_epochs' etc. Plans for future extensions: ================================================================================ #FIXME - implement logging mechanism (probably in general for NEO) #FIXME - implement actions history (probably in general for NEO) #FIXME - implement callbacks in functions for GUIs #FIXME - no performance testing yet IMPORTANT things: ================================================================================ 1. Every NEO node object in HDF5 has a "_type" attribute. Please don't modify. 2. There are reserved attributes "unit__" or "__" in objects, containing quantities. 3. Don't use "__" in attribute names, as this symbol is reserved for quantities. Author: asobolev """ # needed for python 3 compatibility from __future__ import absolute_import import logging import uuid #version checking from distutils import version import numpy as np import quantities as pq # check tables try: import tables as tb except ImportError as err: HAVE_TABLES = False TABLES_ERR = err else: if version.LooseVersion(tb.__version__) < '2.2': HAVE_TABLES = False TABLES_ERR = ImportError("your pytables version is too old to " + "support NeoHdf5IO, you need at least 2.2. " + "You have %s" % tb.__version__) else: HAVE_TABLES = True TABLES_ERR = None from neo.core import Block from neo.description import (class_by_name, name_by_class, classes_inheriting_quantities, classes_necessary_attributes, classes_recommended_attributes, many_to_many_relationship, many_to_one_relationship, one_to_many_relationship) from neo.io.baseio import BaseIO from neo.io.tools import create_many_to_one_relationship, LazyList logger = logging.getLogger("Neo") def _func_wrapper(func): try: return func except IOError: raise IOError("There is no connection with the file or the file was recently corrupted. \ Please reload the IO manager.") #--------------------------------------------------------------- # Basic I/O manager, implementing basic I/O functionality #--------------------------------------------------------------- all_objects = list(class_by_name.values()) all_objects.remove(Block) # the order is important all_objects = [Block] + all_objects # Types where an object might have to be loaded multiple times to create # all realtionships complex_relationships = ["Unit", "Segment", "RecordingChannel"] # Data objects which have multiple parents (Segment and one other) multi_parent = {'AnalogSignal': 'RecordingChannel', 'AnalogSignalArray': 'RecordingChannelGroup', 'IrregularlySampledSignal': 'RecordingChannel', 'Spike': 'Unit', 'SpikeTrain': 'Unit'} # Arrays node names for lazy shapes lazy_shape_arrays = {'SpikeTrain': 'times', 'Spike': 'waveform', 'AnalogSignal': 'signal', 'AnalogSignalArray': 'signal', 'EventArray': 'times', 'EpochArray': 'times'} class NeoHdf5IO(BaseIO): """ The IO Manager is the core I/O class for HDF5 / NEO. It handles the connection with the HDF5 file, and uses PyTables for data operations. Use this class to get (load), insert or delete NEO objects to HDF5 file. """ supported_objects = all_objects readable_objects = all_objects writeable_objects = all_objects read_params = dict(zip(all_objects, [] * len(all_objects))) write_params = dict(zip(all_objects, [] * len(all_objects))) name = 'NeoHdf5 IO' extensions = ['h5'] mode = 'file' is_readable = True is_writable = True def __init__(self, filename=None, **kwargs): if not HAVE_TABLES: raise TABLES_ERR BaseIO.__init__(self, filename=filename) self.connected = False self.objects_by_ref = {} # Loaded objects by reference id self.parent_paths = {} # Tuples of (Segment, other parent) paths self.name_indices = {} if filename: self.connect(filename=filename) def _read_entity(self, path="/", cascade=True, lazy=False): """ Wrapper for base io "reader" functions. """ ob = self.get(path, cascade, lazy) if cascade and cascade != 'lazy': create_many_to_one_relationship(ob) return ob def _write_entity(self, obj, where="/", cascade=True, lazy=False): """ Wrapper for base io "writer" functions. """ self.save(obj, where, cascade, lazy) #------------------------------------------- # IO connectivity / Session management #------------------------------------------- def connect(self, filename): """ Opens / initialises new HDF5 file. We rely on PyTables and keep all session management staff there. """ if not self.connected: try: if tb.isHDF5File(filename): self._data = tb.openFile(filename, mode = "a", title = filename) self.connected = True else: raise TypeError('"%s" is not an HDF5 file format.' % filename) except IOError: # create a new file if specified file not found self._data = tb.openFile(filename, mode = "w", title = filename) self.connected = True except: raise NameError("Incorrect file path, couldn't find or create a file.") self.objects_by_ref = {} self.name_indices = {} else: logger.info("Already connected.") def close(self): """ Closes the connection. """ self.objects_by_ref = {} self.parent_paths = {} self.name_indices = {} self._data.close() self.connected = False #------------------------------------------- # some internal IO functions #------------------------------------------- def _get_class_by_node(self, node): """ Returns the type of the object (string) depending on node. """ try: obj_type = node._f_getAttr("_type") return class_by_name[obj_type] except: return None # that's an alien node def _update_path(self, obj, node): setattr(obj, "hdf5_path", node._v_pathname) def _get_next_name(self, obj_type, where): """ Returns the next possible name within a given container (group) """ if not (obj_type, where) in self.name_indices: self.name_indices[(obj_type, where)] = 0 index_num = self.name_indices[(obj_type, where)] prefix = str(obj_type) + "_" if where + '/' + prefix + str(index_num) not in self._data: self.name_indices[(obj_type, where)] = index_num + 1 return prefix + str(index_num) nodes = [] for node in self._data.iterNodes(where): index = node._v_name[node._v_name.find(prefix) + len(prefix):] if len(index) > 0: try: nodes.append(int(index)) except ValueError: pass # index was changed by user, but then we don't care nodes.sort(reverse=True) if len(nodes) > 0: self.name_indices[(obj_type, where)] = nodes[0] + 2 return prefix + str(nodes[0] + 1) else: self.name_indices[(obj_type, where)] = 1 return prefix + "0" #------------------------------------------- # general IO functions, for all NEO objects #------------------------------------------- @_func_wrapper def save(self, obj, where="/", cascade=True, lazy=False): """ Saves changes of a given object to the file. Saves object as new at location "where" if it is not in the file yet. Returns saved node. cascade: True/False process downstream relationships lazy: True/False process any quantity/ndarray attributes """ def assign_attribute(obj_attr, attr_name, path, node): """ subfunction to serialize a given attribute """ if isinstance(obj_attr, pq.Quantity) or isinstance(obj_attr, np.ndarray): if not lazy: # we need to simplify custom quantities if isinstance(obj_attr, pq.Quantity): for un in obj_attr.dimensionality.keys(): if not un.name in pq.units.__dict__ or \ not isinstance(pq.units.__dict__[un.name], pq.Quantity): obj_attr = obj_attr.simplified break # we try to create new array first, so not to loose the # data in case of any failure if obj_attr.size == 0: atom = tb.Float64Atom(shape=(1,)) new_arr = self._data.createEArray(path, attr_name + "__temp", atom, shape=(0,), expectedrows=1) else: new_arr = self._data.createArray(path, attr_name + "__temp", obj_attr) if hasattr(obj_attr, "dimensionality"): for un in obj_attr.dimensionality.items(): new_arr._f_setAttr("unit__" + un[0].name, un[1]) try: self._data.removeNode(path, attr_name) except: pass # there is no array yet or object is new self._data.renameNode(path, attr_name, name=attr_name + "__temp") elif obj_attr is not None: node._f_setAttr(attr_name, obj_attr) #assert_neo_object_is_compliant(obj) obj_type = name_by_class[obj.__class__] if self._data.mode != 'w' and hasattr(obj, "hdf5_path"): # this is an update case path = str(obj.hdf5_path) try: node = self._data.getNode(obj.hdf5_path) except tb.NoSuchNodeError: # create a new node? raise LookupError("A given object has a path %s attribute, \ but such an object does not exist in the file. Please \ correct these values or delete this attribute \ (.__delattr__('hdf5_path')) to create a new object in \ the file." % path) else: # create new object node = self._data.createGroup(where, self._get_next_name(obj_type, where)) node._f_setAttr("_type", obj_type) path = node._v_pathname # processing attributes if obj_type in multi_parent: # Initialize empty parent paths node._f_setAttr('segment', '') node._f_setAttr(multi_parent[obj_type].lower(), '') attrs = classes_necessary_attributes[obj_type] + classes_recommended_attributes[obj_type] for attr in attrs: # we checked already obj is compliant, loop over all safely if hasattr(obj, attr[0]): # save an attribute if exists assign_attribute(getattr(obj, attr[0]), attr[0], path, node) # not forget to save AS, ASA or ST - NEO "stars" if obj_type in classes_inheriting_quantities.keys(): assign_attribute(obj, classes_inheriting_quantities[obj_type], path, node) if hasattr(obj, "annotations"): # annotations should be just a dict node._f_setAttr("annotations", getattr(obj, "annotations")) node._f_setAttr("object_ref", uuid.uuid4().hex) if one_to_many_relationship.has_key(obj_type) and cascade: rels = list(one_to_many_relationship[obj_type]) if obj_type == "RecordingChannelGroup": rels += many_to_many_relationship[obj_type] for child_name in rels: # child_name like "Segment", "Event" etc. container = child_name.lower() + "s" # like "units" try: ch = self._data.getNode(node, container) except tb.NoSuchNodeError: ch = self._data.createGroup(node, container) saved = [] # keeps track of saved object names for removal for child in getattr(obj, container): new_name = None child_node = None if hasattr(child, "hdf5_path"): if not child.hdf5_path.startswith(ch._v_pathname): # create a Hard Link if object exists already somewhere try: target = self._data.getNode(child.hdf5_path) new_name = self._get_next_name( name_by_class[child.__class__], ch._v_pathname) if not hasattr(ch, new_name): # Only link if path does not exist child_node = self._data.createHardLink(ch._v_pathname, new_name, target) except tb.NoSuchNodeError: pass if child_node is None: child_node = self.save(child, where=ch._v_pathname) if child_name in multi_parent: # Save parent for multiparent objects child_node._f_setAttr(obj_type.lower(), path) elif child_name == 'RecordingChannel': parents = [] if 'recordingchannelgroups' in child_node._v_attrs: parents = child_node._v_attrs['recordingchannelgroups'] parents.append(path) child_node._f_setAttr('recordingchannelgroups', parents) if not new_name: new_name = child.hdf5_path.split('/')[-1] saved.append(new_name) for child in self._data.iterNodes(ch._v_pathname): if child._v_name not in saved: # clean-up self._data.removeNode(ch._v_pathname, child._v_name, recursive=True) self._update_path(obj, node) return node def _get_parent(self, path, ref, parent_type): """ Return the path of the parent of type "parent_type" for the object in "path" with id "ref". Returns an empty string if no parent extists. """ parts = path.split('/') if parent_type == 'Block' or parts[-4] == parent_type.lower() + 's': return '/'.join(parts[:-2]) object_folder = parts[-2] parent_folder = parts[-4] if parent_folder in ('recordingchannels', 'units'): block_path = '/'.join(parts[:-6]) else: block_path = '/'.join(parts[:-4]) if parent_type in ('RecordingChannel', 'Unit'): # We need to search all recording channels path = block_path + '/recordingchannelgroups' for n in self._data.iterNodes(path): if not '_type' in n._v_attrs: continue p = self._search_parent( '%s/%ss' % (n._v_pathname, parent_type.lower()), object_folder, ref) if p != '': return p return '' if parent_type == 'Segment': path = block_path + '/segments' elif parent_type == 'RecordingChannelGroup': path = block_path + '/recordingchannelgroups' else: return '' return self._search_parent(path, object_folder, ref) def _get_rcgs(self, path, ref): """ Get RecordingChannelGroup parents for a RecordingChannel """ parts = path.split('/') object_folder = parts[-2] block_path = '/'.join(parts[:-4]) path = block_path + '/recordingchannelgroups' return self._search_parent(path, object_folder, ref, True) def _search_parent(self, path, object_folder, ref, multi=False): """ Searches a folder for an object with a given reference and returns the path of the parent node. :param str path: Path to search :param str object_folder: The name of the folder within the parent object containing the objects to search. :param ref: Object reference """ if multi: ret = [] else: ret = '' for n in self._data.iterNodes(path): if not '_type' in n._v_attrs: continue for c in self._data.iterNodes(n._f_getChild(object_folder)): try: if c._f_getAttr("object_ref") == ref: if not multi: return n._v_pathname else: ret.append(n._v_pathname) except AttributeError: # alien node pass # not an error return ret _second_parent = { # Second parent type apart from Segment 'AnalogSignal': 'RecordingChannel', 'AnalogSignalArray': 'RecordingChannelGroup', 'IrregularlySampledSignal': 'RecordingChannel', 'Spike': 'Unit', 'SpikeTrain': 'Unit'} def load_lazy_cascade(self, path, lazy): """ Load an object with the given path in lazy cascade mode. """ o = self.get(path, cascade='lazy', lazy=lazy) t = type(o).__name__ node = self._data.getNode(path) if t in multi_parent: # Try to read parent objects from attributes if not path in self.parent_paths: ppaths = [None, None] if 'segment' in node._v_attrs: ppaths[0] = node._f_getAttr('segment') if multi_parent[t] in node._v_attrs: ppaths[1] = node._f_getAttr(multi_parent[t]) self.parent_paths[path] = ppaths elif t == 'RecordingChannel': if not path in self.parent_paths: if 'recordingchannelgroups' in node._v_attrs: self.parent_paths[path] = node._f_getAttr('recordingchannelgroups') # Set parent objects if path in self.parent_paths: paths = self.parent_paths[path] if t == 'RecordingChannel': # Set list of parnet channel groups for rcg in self.parent_paths[path]: o.recordingchannelgroups.append(self.get(rcg, cascade='lazy', lazy=lazy)) else: # Set parents: Segment and another parent if paths[0] is None: paths[0] = self._get_parent( path, self._data.getNodeAttr(path, 'object_ref'), 'Segment') if paths[0]: o.segment = self.get(paths[0], cascade='lazy', lazy=lazy) parent = self._second_parent[t] if paths[1] is None: paths[1] = self._get_parent( path, self._data.getNodeAttr(path, 'object_ref'), parent) if paths[1]: setattr(o, parent.lower(), self.get(paths[1], cascade='lazy', lazy=lazy)) elif t != 'Block': ref = self._data.getNodeAttr(path, 'object_ref') if t == 'RecordingChannel': rcg_paths = self._get_rcgs(path, ref) for rcg in rcg_paths: o.recordingchannelgroups.append(self.get(rcg, cascade='lazy', lazy=lazy)) self.parent_paths[path] = rcg_paths else: for p in many_to_one_relationship[t]: parent = self._get_parent(path, ref, p) if parent: setattr(o, p.lower(), self.get(parent, cascade='lazy', lazy=lazy)) return o def load_lazy_object(self, obj): """ Return the fully loaded version of a lazily loaded object. Does not set links to parent objects. """ return self.get(obj.hdf5_path, cascade=False, lazy=False, lazy_loaded=True) @_func_wrapper def get(self, path="/", cascade=True, lazy=False, lazy_loaded=False): """ Returns a requested NEO object as instance of NEO class. Set lazy_loaded to True to load a previously lazily loaded object (cache is ignored in this case).""" def fetch_attribute(attr_name, attr, node): """ fetch required attribute from the corresp. node in the file """ try: if attr[1] == pq.Quantity: arr = self._data.getNode(node, attr_name) units = "" for unit in arr._v_attrs._f_list(attrset='user'): if unit.startswith("unit__"): units += " * " + str(unit[6:]) + " ** " + str(arr._f_getAttr(unit)) units = units.replace(" * ", "", 1) if not lazy or sum(arr.shape) <= 1: nattr = pq.Quantity(arr.read(), units) else: # making an empty array nattr = pq.Quantity(np.empty(tuple([0 for _ in range(attr[2])])), units) elif attr[1] == np.ndarray: arr = self._data.getNode(node, attr_name) if not lazy: nattr = np.array(arr.read(), attr[3]) if nattr.shape == (0, 1): # Fix: Empty arrays should have only one dimension nattr = nattr.reshape(-1) else: # making an empty array nattr = np.empty(0, attr[3]) else: nattr = node._f_getAttr(attr_name) if attr[1] == str or attr[1] == int: nattr = attr[1](nattr) # compliance with NEO attr types except (AttributeError, tb.NoSuchNodeError): # not assigned, continue nattr = None return nattr def get_lazy_shape(obj, node): attr = lazy_shape_arrays[type(obj).__name__] arr = self._data.getNode(node, attr) return arr.shape if path == "/": # this is just for convenience. Try to return any object found = False for n in self._data.iterNodes(path): for obj_type in class_by_name.keys(): if obj_type.lower() in str(n._v_name).lower(): path = n._v_pathname found = True if found: break try: if path == "/": raise ValueError() # root is not a NEO object node = self._data.getNode(path) except (tb.NoSuchNodeError, ValueError): # create a new node? raise LookupError("There is no valid object with a given path " + str(path) + ' . Please give correct path or just browse the file ' '(e.g. NeoHdf5IO()._data.root.._segments...) to find an ' 'appropriate name.') classname = self._get_class_by_node(node) if not classname: raise LookupError("The requested object with the path " + str(path) + " exists, but is not of a NEO type. Please check the '_type' attribute.") obj_type = name_by_class[classname] try: object_ref = self._data.getNodeAttr(node, 'object_ref') except AttributeError: # Object does not have reference, e.g. because this is an old file format object_ref = None if object_ref in self.objects_by_ref and not lazy_loaded: obj = self.objects_by_ref[object_ref] if cascade == 'lazy' or obj_type not in complex_relationships: return obj else: kwargs = {} # load attributes (inherited *-ed attrs are also here) attrs = classes_necessary_attributes[obj_type] + classes_recommended_attributes[obj_type] for i, attr in enumerate(attrs): attr_name = attr[0] nattr = fetch_attribute(attr_name, attr, node) if nattr is not None: kwargs[attr_name] = nattr obj = class_by_name[obj_type](**kwargs) # instantiate new object if lazy and obj_type in lazy_shape_arrays: obj.lazy_shape = get_lazy_shape(obj, node) self._update_path(obj, node) # set up HDF attributes: name, path try: setattr(obj, "annotations", node._f_getAttr("annotations")) except AttributeError: pass # not assigned, continue if object_ref and not lazy_loaded: self.objects_by_ref[object_ref] = obj # load relationships if cascade: if obj_type in one_to_many_relationship: rels = list(one_to_many_relationship[obj_type]) if obj_type == "RecordingChannelGroup": rels += many_to_many_relationship[obj_type] for child in rels: # 'child' is like 'Segment', 'Event' etc. if cascade == 'lazy': relatives = LazyList(self, lazy) else: relatives = [] container = self._data.getNode(node, child.lower() + "s") for n in self._data.iterNodes(container): if cascade == 'lazy': relatives.append(n._v_pathname) else: try: if n._f_getAttr("_type") == child: relatives.append(self.get(n._v_pathname, lazy=lazy)) except AttributeError: # alien node pass # not an error setattr(obj, child.lower() + "s", relatives) if not cascade == 'lazy': # RC -> AnalogSignal relationship will not be created later, do it now if obj_type == "RecordingChannel" and child == "AnalogSignal": for r in relatives: r.recordingchannel = obj # Cannot create Many-to-Many relationship with old format, create at least One-to-Many if obj_type == "RecordingChannelGroup" and not object_ref: for r in relatives: r.recordingchannelgroups = [obj] # special processor for RC -> RCG if obj_type == "RecordingChannel": if hasattr(node, '_v_parent'): parent = node._v_parent if hasattr(parent, '_v_parent'): parent = parent._v_parent if 'object_ref' in parent._v_attrs: obj.recordingchannelgroups.append(self.get( parent._v_pathname, lazy=lazy)) return obj @_func_wrapper def read_all_blocks(self, lazy=False, cascade=True, **kargs): """ Loads all blocks in the file that are attached to the root (which happens when they are saved with save() or write_block()). """ blocks = [] for n in self._data.iterNodes(self._data.root): if self._get_class_by_node(n) == Block: blocks.append(self.read_block(n._v_pathname, lazy=lazy, cascade=cascade, **kargs)) return blocks @_func_wrapper def write_all_blocks(self, blocks, **kargs): """ Writes a sequence of blocks. Just calls write_block() for each element. """ for b in blocks: self.write_block(b) @_func_wrapper def delete(self, path, cascade=False): """ Deletes an object in the file. Just a simple alternative of removeNode(). """ self._data.removeNode(path, recursive=cascade) @_func_wrapper def reset(self, obj): """ Resets runtime changes made to the object. TBD. """ pass @_func_wrapper def get_info(self): """ Returns a quantitative information about the contents of the file. """ logger.info("This is a neo.HDF5 file. it contains:") info = {} info = info.fromkeys(class_by_name.keys(), 0) for node in self._data.walkNodes(): try: t = node._f_getAttr("_type") info[t] += 1 except: # node is not of NEO type pass return info for obj_type in NeoHdf5IO.writeable_objects: setattr(NeoHdf5IO, "write_" + obj_type.__name__.lower(), NeoHdf5IO._write_entity) for obj_type in NeoHdf5IO.readable_objects: setattr(NeoHdf5IO, "read_" + obj_type.__name__.lower(), NeoHdf5IO._read_entity) neo-0.3.3/neo/io/winwcpio.py0000644000175000017500000001231512273723542016754 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Class for reading data from WinWCP, a software tool written by John Dempster. WinWCP is free: http://spider.science.strath.ac.uk/sipbs/software.htm Supported : Read Author : sgarcia """ import os import struct import sys import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import Block, Segment, AnalogSignal from neo.io.tools import create_many_to_one_relationship PY3K = (sys.version_info[0] == 3) class WinWcpIO(BaseIO): """ Class for reading from a WinWCP file. Usage: >>> from neo import io >>> r = io.WinWcpIO( filename = 'File_winwcp_1.wcp') >>> bl = r.read_block(lazy = False, cascade = True,) >>> print bl.segments # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [, , ... >>> print bl.segments[0].analogsignals [>> from neo import io >>> r = io.RawBinarySignalIO( filename = 'File_ascii_signal_2.txt') >>> seg = r.read_segment(lazy = False, cascade = True,) >>> print seg.analogsignals # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE ... """ is_readable = True is_writable = True supported_objects = [Segment , AnalogSignal] readable_objects = [ Segment] writeable_objects = [Segment] has_header = False is_streameable = False read_params = { Segment : [ ('sampling_rate' , { 'value' : 1000. } ) , ('nbchannel' , { 'value' : 16 } ), ('bytesoffset' , { 'value' : 0 } ), ('t_start' , { 'value' : 0. } ), ('dtype' , { 'value' : 'float32' , 'possible' : ['float32' , 'float64', 'int16' , 'uint16', 'int32' , 'uint32', ] } ), ('rangemin' , { 'value' : -10 } ), ('rangemax' , { 'value' : 10 } ), ] } write_params = { Segment : [ ('bytesoffset' , { 'value' : 0 } ), ('dtype' , { 'value' : 'float32' , 'possible' : ['float32' , 'float64', 'int16' , 'uint16', 'int32' , 'uint32', ] } ), ('rangemin' , { 'value' : -10 } ), ('rangemax' , { 'value' : 10 } ), ] } name = None extensions = [ 'raw' ] mode = 'file' def __init__(self , filename = None) : """ This class read a binary file. **Arguments** filename : the filename to read """ BaseIO.__init__(self) self.filename = filename def read_segment(self, cascade = True, lazy = False, sampling_rate = 1.*pq.Hz, t_start = 0.*pq.s, unit = pq.V, nbchannel = 1, bytesoffset = 0, dtype = 'f4', rangemin = -10, rangemax = 10, ): """ Reading signal in a raw binary interleaved compact file. Arguments: sampling_rate : sample rate t_start : time of the first sample sample of each channel unit: unit of AnalogSignal can be a str or directly a Quantities nbchannel : number of channel bytesoffset : nb of bytes offset at the start of file dtype : dtype of the data rangemin , rangemax : if the dtype is integer, range can give in volt the min and the max of the range """ seg = Segment(file_origin = os.path.basename(self.filename)) if not cascade: return seg dtype = np.dtype(dtype) if type(sampling_rate) == float or type(sampling_rate)==int: # if not quantitities Hz by default sampling_rate = sampling_rate*pq.Hz if type(t_start) == float or type(t_start)==int: # if not quantitities s by default t_start = t_start*pq.s unit = pq.Quantity(1, unit) if not lazy: sig = np.memmap(self.filename, dtype = dtype, mode = 'r', offset = bytesoffset) if sig.size % nbchannel != 0 : sig = sig[:- sig.size%nbchannel] sig = sig.reshape((sig.size/nbchannel,nbchannel)) if dtype.kind == 'i' : sig = sig.astype('f') sig /= 2**(8*dtype.itemsize) sig *= ( rangemax-rangemin ) sig += ( rangemax+rangemin )/2. elif dtype.kind == 'u' : sig = sig.astype('f') sig /= 2**(8*dtype.itemsize) sig *= ( rangemax-rangemin ) sig += rangemin sig_with_units = pq.Quantity(sig, units=unit, copy = False) for i in range(nbchannel) : if lazy: signal = [ ]*unit else: signal = sig_with_units[:,i] anaSig = AnalogSignal(signal, sampling_rate=sampling_rate, t_start=t_start, channel_index=i, copy = False) if lazy: # TODO anaSig.lazy_shape = None seg.analogsignals.append(anaSig) create_many_to_one_relationship(seg) return seg def write_segment(self, segment, dtype='f4', rangemin=-10, rangemax=10, bytesoffset=0): """ **Arguments** segment : the segment to write. Only analog signals will be written. dtype : dtype of the data rangemin , rangemax : if the dtype is integer, range can give in volt the min and the max of the range """ if bytesoffset: raise NotImplementedError('bytesoffset values other than 0 ' + 'not supported') dtype = np.dtype(dtype) # all AnaologSignal from Segment must have the same length for anasig in segment.analogsignals[1:]: assert anasig.size == segment.analogsignals[0].size sigs = np.empty((segment.analogsignals[0].size, len(segment.analogsignals))) for i, anasig in enumerate(segment.analogsignals): sigs[:, i] = anasig.magnitude if dtype.kind == 'i': sigs -= ( rangemax+rangemin )/2. sigs /= (rangemax - rangemin) sigs *= 2 ** (8 * dtype.itemsize ) elif dtype.kind == 'u' : sigs -= rangemin sigs /= (rangemax - rangemin) sigs *= 2 ** (8 * dtype.itemsize) sigs = sigs.astype(dtype) f = open(self.filename, 'wb') f.write(sigs.tostring()) f.close() neo-0.3.3/neo/io/neuroscopeio.py0000644000175000017500000001011612273723542017624 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Reading from neuroscope format files. Ref: http://neuroscope.sourceforge.net/ It is an old format from Buzsaki's lab. Supported: Read #TODO: SpikeTrain file '.clu' '.res' EventArray '.ext.evt' or '.evt.ext' Author: sgarcia """ # needed for python 3 compatibility from __future__ import absolute_import import os from xml.etree import ElementTree import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.io.rawbinarysignalio import RawBinarySignalIO from neo.core import (Block, Segment, RecordingChannel, RecordingChannelGroup, AnalogSignal) from neo.io.tools import create_many_to_one_relationship class NeuroScopeIO(BaseIO): """ """ is_readable = True is_writable = False supported_objects = [ Block, Segment , AnalogSignal, RecordingChannel, RecordingChannelGroup] readable_objects = [ Block ] writeable_objects = [ ] has_header = False is_streameable = False read_params = { Segment : [ ] } # do not supported write so no GUI stuff write_params = None name = 'NeuroScope' extensions = [ 'xml' ] mode = 'file' def __init__(self , filename = None) : """ Arguments: filename : the filename """ BaseIO.__init__(self) self.filename = filename def read_block(self, lazy = False, cascade = True, ): """ """ tree = ElementTree.parse(self.filename) root = tree.getroot() acq = root.find('acquisitionSystem') nbits = int(acq.find('nBits').text) nbchannel = int(acq.find('nChannels').text) sampling_rate = float(acq.find('samplingRate').text)*pq.Hz voltage_range = float(acq.find('voltageRange').text) #offset = int(acq.find('offset').text) amplification = float(acq.find('amplification').text) bl = Block(file_origin = os.path.basename(self.filename).replace('.xml', '')) if cascade: seg = Segment() bl.segments.append(seg) # RC and RCG rc_list = [ ] for i, xml_rcg in enumerate(root.find('anatomicalDescription').find('channelGroups').findall('group')): rcg = RecordingChannelGroup(name = 'Group {}'.format(i)) bl.recordingchannelgroups.append(rcg) for xml_rc in xml_rcg: rc = RecordingChannel(index = int(xml_rc.text)) rc_list.append(rc) rcg.recordingchannels.append(rc) rc.recordingchannelgroups.append(rcg) rcg.channel_indexes = np.array([rc.index for rc in rcg.recordingchannels], dtype = int) rcg.channel_names = np.array(['Channel{}'.format(rc.index) for rc in rcg.recordingchannels], dtype = 'S') # AnalogSignals reader = RawBinarySignalIO(filename = self.filename.replace('.xml', '.dat')) seg2 = reader.read_segment(cascade = True, lazy = lazy, sampling_rate = sampling_rate, t_start = 0.*pq.s, unit = pq.V, nbchannel = nbchannel, bytesoffset = 0, dtype = np.int16 if nbits<=16 else np.int32, rangemin = -voltage_range/2., rangemax = voltage_range/2.,) for s, sig in enumerate(seg2.analogsignals): if not lazy: sig /= amplification sig.segment = seg seg.analogsignals.append(sig) rc_list[s].analogsignals.append(sig) create_many_to_one_relationship(bl) return bl neo-0.3.3/neo/io/klustakwikio.py0000644000175000017500000004255312273723542017645 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Reading and writing from KlustaKwik-format files. Ref: http://klusters.sourceforge.net/UserManual/data-files.html Supported : Read, Write Author : Chris Rodgers TODO: * When reading, put the Unit into the RCG, RC hierarchy * When writing, figure out how to get group and cluster if those annotations weren't set. Consider removing those annotations if they are redundant. * Load features in addition to spiketimes. """ import glob import logging import os.path import shutil # note neo.core need only numpy and quantitie import numpy as np try: import matplotlib.mlab as mlab except ImportError as err: HAVE_MLAB = False MLAB_ERR = err else: HAVE_MLAB = True MLAB_ERR = None # I need to subclass BaseIO from neo.io.baseio import BaseIO from neo.core import Block, Segment, Unit, SpikeTrain from neo.io.tools import create_many_to_one_relationship # Pasted version of feature file format spec """ The Feature File Generic file name: base.fet.n Format: ASCII, integer values The feature file lists for each spike the PCA coefficients for each electrode, followed by the timestamp of the spike (more features can be inserted between the PCA coefficients and the timestamp). The first line contains the number of dimensions. Assuming N1 spikes (spike1...spikeN1), N2 electrodes (e1...eN2) and N3 coefficients (c1...cN3), this file looks like: nbDimensions c1_e1_spike1 c2_e1_spike1 ... cN3_e1_spike1 c1_e2_spike1 ... cN3_eN2_spike1 timestamp_spike1 c1_e1_spike2 c2_e1_spike2 ... cN3_e1_spike2 c1_e2_spike2 ... cN3_eN2_spike2 timestamp_spike2 ... c1_e1_spikeN1 c2_e1_spikeN1 ... cN3_e1_spikeN1 c1_e2_spikeN1 ... cN3_eN2_spikeN1 timestamp_spikeN1 The timestamp is expressed in multiples of the sampling interval. For instance, for a 20kHz recording (50 microsecond sampling interval), a timestamp of 200 corresponds to 200x0.000050s=0.01s from the beginning of the recording session. Notice that the last line must end with a newline or carriage return. """ class KlustaKwikIO(BaseIO): """Reading and writing from KlustaKwik-format files.""" # Class variables demonstrating capabilities of this IO is_readable = True is_writable = True # This IO can only manipulate objects relating to spike times supported_objects = [Block, SpikeTrain, Unit] # Keep things simple by always returning a block readable_objects = [Block] # And write a block writeable_objects = [Block] # Not sure what these do, if anything has_header = False is_streameable = False # GUI params read_params = {} # GUI params write_params = {} # The IO name and the file extensions it uses name = 'KlustaKwik' extensions = ['fet', 'clu', 'res', 'spk'] # Operates on directories mode = 'file' def __init__(self, filename, sampling_rate=30000.): """Create a new IO to operate on a directory filename : the directory to contain the files basename : string, basename of KlustaKwik format, or None sampling_rate : in Hz, necessary because the KlustaKwik files stores data in samples. """ if not HAVE_MLAB: raise MLAB_ERR BaseIO.__init__(self) #self.filename = os.path.normpath(filename) self.filename, self.basename = os.path.split(os.path.abspath(filename)) self.sampling_rate = float(sampling_rate) # error check if not os.path.isdir(self.filename): raise ValueError("filename must be a directory") # initialize a helper object to parse filenames self._fp = FilenameParser(dirname=self.filename, basename=self.basename) # The reading methods. The `lazy` and `cascade` parameters are imposed # by neo.io API def read_block(self, lazy=False, cascade=True): """Returns a Block containing spike information. There is no obvious way to infer the segment boundaries from raw spike times, so for now all spike times are returned in one big segment. The way around this would be to specify the segment boundaries, and then change this code to put the spikes in the right segments. """ # Create block and segment to hold all the data block = Block() # Search data directory for KlustaKwik files. # If nothing found, return empty block self._fetfiles = self._fp.read_filenames('fet') self._clufiles = self._fp.read_filenames('clu') if len(self._fetfiles) == 0 or not cascade: return block # Create a single segment to hold all of the data seg = Segment(name='seg0', index=0, file_origin=self.filename) block.segments.append(seg) # Load spike times from each group and store in a dict, keyed # by group number self.spiketrains = dict() for group in sorted(self._fetfiles.keys()): # Load spike times fetfile = self._fetfiles[group] spks, features = self._load_spike_times(fetfile) # Load cluster ids or generate if group in self._clufiles: clufile = self._clufiles[group] uids = self._load_unit_id(clufile) else: # unclustered data, assume all zeros uids = np.zeros(spks.shape, dtype=np.int32) # error check if len(spks) != len(uids): raise ValueError("lengths of fet and clu files are different") # Create Unit for each cluster unique_unit_ids = np.unique(uids) for unit_id in sorted(unique_unit_ids): # Initialize the unit u = Unit(name=('unit %d from group %d' % (unit_id, group)), index=unit_id, group=group) # Initialize a new SpikeTrain for the spikes from this unit if lazy: st = SpikeTrain( times=[], units='sec', t_start=0.0, t_stop=spks.max() / self.sampling_rate, name=('unit %d from group %d' % (unit_id, group))) st.lazy_shape = len(spks[uids==unit_id]) else: st = SpikeTrain( times=spks[uids==unit_id] / self.sampling_rate, units='sec', t_start=0.0, t_stop=spks.max() / self.sampling_rate, name=('unit %d from group %d' % (unit_id, group))) st.annotations['cluster'] = unit_id st.annotations['group'] = group # put features in if not lazy and len(features) != 0: st.annotations['waveform_features'] = features # Link u.spiketrains.append(st) seg.spiketrains.append(st) create_many_to_one_relationship(block) return block # Helper hidden functions for reading def _load_spike_times(self, fetfilename): """Reads and returns the spike times and features""" f = file(fetfilename, 'r') # Number of clustering features is integer on first line nbFeatures = int(f.readline().strip()) # Each subsequent line consists of nbFeatures values, followed by # the spike time in samples. names = ['fet%d' % n for n in xrange(nbFeatures)] names.append('spike_time') # Load into recarray data = mlab.csv2rec(f, names=names, skiprows=1, delimiter=' ') f.close() # get features features = np.array([data['fet%d' % n] for n in xrange(nbFeatures)]) # Return the spike_time column return data['spike_time'], features.transpose() def _load_unit_id(self, clufilename): """Reads and return the cluster ids as int32""" f = file(clufilename, 'r') # Number of clusters on this tetrode is integer on first line nbClusters = int(f.readline().strip()) # Read each cluster name as a string cluster_names = f.readlines() f.close() # Convert names to integers # I think the spec requires cluster names to be integers, but # this code could be modified to support string names which are # auto-numbered. try: cluster_ids = [int(name) for name in cluster_names] except ValueError: raise ValueError( "Could not convert cluster name to integer in %s" % clufilename) # convert to numpy array and error check cluster_ids = np.array(cluster_ids, dtype=np.int32) if len(np.unique(cluster_ids)) != nbClusters: logging.warning("warning: I got %d clusters instead of %d in %s" % ( len(np.unique(cluster_ids)), nbClusters, clufilename)) return cluster_ids # writing functions def write_block(self, block): """Write spike times and unit ids to disk. Currently descends hierarchy from block to segment to spiketrain. Then gets group and cluster information from spiketrain. Then writes the time and cluster info to the file associated with that group. The group and cluster information are extracted from annotations, eg `sptr.annotations['group']`. If no cluster information exists, it is assigned to cluster 0. Note that all segments are essentially combined in this process, since the KlustaKwik format does not allow for segment boundaries. As implemented currently, does not use the `Unit` object at all. We first try to use the sampling rate of each SpikeTrain, or if this is not set, we use `self.sampling_rate`. If the files already exist, backup copies are created by appending the filenames with a "~". """ # set basename if self.basename is None: logging.warning("warning: no basename provided, using `basename`") self.basename = 'basename' # First create file handles for each group which will be stored self._make_all_file_handles(block) # We'll detect how many features belong in each group self._group2features = {} # Iterate through segments in this block for seg in block.segments: # Write each spiketrain of the segment for st in seg.spiketrains: # Get file handles for this spiketrain using its group group = self.st2group(st) fetfilehandle = self._fetfilehandles[group] clufilehandle = self._clufilehandles[group] # Get the id to write to clu file for this spike train cluster = self.st2cluster(st) # Choose sampling rate to convert to samples try: sr = st.annotations['sampling_rate'] except KeyError: sr = self.sampling_rate # Convert to samples spike_times_in_samples = np.rint( np.array(st) * sr).astype(np.int) # Try to get features from spiketrain try: all_features = st.annotations['waveform_features'] except KeyError: # Use empty all_features = [ [] for _ in range(len(spike_times_in_samples))] all_features = np.asarray(all_features) if all_features.ndim != 2: raise ValueError("waveform features should be 2d array") # Check number of features we're supposed to have try: n_features = self._group2features[group] except KeyError: # First time through .. set number of features n_features = all_features.shape[1] self._group2features[group] = n_features # and write to first line of file fetfilehandle.write("%d\n" % n_features) if n_features != all_features.shape[1]: raise ValueError("inconsistent number of features: " + "supposed to be %d but I got %d" %\ (n_features, all_features.shape[1])) # Write features and time for each spike for stt, features in zip(spike_times_in_samples, all_features): # first features for val in features: fetfilehandle.write(str(val)) fetfilehandle.write(" ") # now time fetfilehandle.write("%d\n" % stt) # and cluster id clufilehandle.write("%d\n" % cluster) # We're done, so close the files self._close_all_files() # Helper functions for writing def st2group(self, st): # Not sure this is right so make it a method in case we change it try: return st.annotations['group'] except KeyError: return 0 def st2cluster(self, st): # Not sure this is right so make it a method in case we change it try: return st.annotations['cluster'] except KeyError: return 0 def _make_all_file_handles(self, block): """Get the tetrode (group) of each neuron (cluster) by descending the hierarchy through segment and block. Store in a dict {group_id: list_of_clusters_in_that_group} """ group2clusters = {} for seg in block.segments: for st in seg.spiketrains: group = self.st2group(st) cluster = self.st2cluster(st) if group in group2clusters: if cluster not in group2clusters[group]: group2clusters[group].append(cluster) else: group2clusters[group] = [cluster] # Make new file handles for each group self._fetfilehandles, self._clufilehandles = {}, {} for group, clusters in group2clusters.items(): self._new_group(group, nbClusters=len(clusters)) def _new_group(self, id_group, nbClusters): # generate filenames fetfilename = os.path.join(self.filename, self.basename + ('.fet.%d' % id_group)) clufilename = os.path.join(self.filename, self.basename + ('.clu.%d' % id_group)) # back up before overwriting if os.path.exists(fetfilename): shutil.copyfile(fetfilename, fetfilename + '~') if os.path.exists(clufilename): shutil.copyfile(clufilename, clufilename + '~') # create file handles self._fetfilehandles[id_group] = file(fetfilename, 'w') self._clufilehandles[id_group] = file(clufilename, 'w') # write out first line #self._fetfilehandles[id_group].write("0\n") # Number of features self._clufilehandles[id_group].write("%d\n" % nbClusters) def _close_all_files(self): for val in self._fetfilehandles.values(): val.close() for val in self._clufilehandles.values(): val.close() class FilenameParser: """Simple class to interpret user's requests into KlustaKwik filenames""" def __init__(self, dirname, basename=None): """Initialize a new parser for a directory containing files dirname: directory containing files basename: basename in KlustaKwik format spec If basename is left None, then files with any basename in the directory will be used. An error is raised if files with multiple basenames exist in the directory. """ self.dirname = os.path.normpath(dirname) self.basename = basename # error check if not os.path.isdir(self.dirname): raise ValueError("filename must be a directory") def read_filenames(self, typestring='fet'): """Returns filenames in the data directory matching the type. Generally, `typestring` is one of the following: 'fet', 'clu', 'spk', 'res' Returns a dict {group_number: filename}, e.g.: { 0: 'basename.fet.0', 1: 'basename.fet.1', 2: 'basename.fet.2'} 'basename' can be any string not containing whitespace. Only filenames that begin with "basename.typestring." and end with a sequence of digits are valid. The digits are converted to an integer and used as the group number. """ all_filenames = glob.glob(os.path.join(self.dirname, '*')) # Fill the dict with valid filenames d = {} for v in all_filenames: # Test whether matches format, ie ends with digits split_fn = os.path.split(v)[1] m = glob.re.search(('^(\w+)\.%s\.(\d+)$' % typestring), split_fn) if m is not None: # get basename from first hit if not specified if self.basename is None: self.basename = m.group(1) # return files with correct basename if self.basename == m.group(1): # Key the group number to the filename # This conversion to int should always work since only # strings of digits will match the regex tetn = int(m.group(2)) d[tetn] = v return d neo-0.3.3/neo/io/pickleio.py0000644000175000017500000000216512265516260016714 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Module for reading/writing data from/to Python pickle format. Class: PickleIO Supported: Read/Write Authors: Andrew Davison """ try: import cPickle as pickle # Python 2 except ImportError: import pickle # Python 3 from neo.io.baseio import BaseIO from neo.core import (Block, Segment, AnalogSignal, AnalogSignalArray, SpikeTrain) class PickleIO(BaseIO): """ """ is_readable = True is_writable = True has_header = False is_streameable = False # TODO - correct spelling to "is_streamable" supported_objects = [Block, Segment, AnalogSignal, AnalogSignalArray, SpikeTrain] # should extend to Epoch, etc. readable_objects = supported_objects writeable_objects = supported_objects mode = 'file' name = "Python pickle file" extensions = ['pkl', 'pickle'] def read_block(self, lazy=False, cascade=True): with open(self.filename, "rb") as fp: block = pickle.load(fp) return block def write_block(self, block): with open(self.filename, "wb") as fp: pickle.dump(block, fp) neo-0.3.3/neo/io/elphyio.py0000644000175000017500000046702412273723542016601 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ README ===================================================================================== This is the implementation of the NEO IO for Elphy files. IO dependencies: - NEO - types - numpy - quantities Quick reference: ===================================================================================== Class ElphyIO() with methods read_block() and write_block() are implemented. This classes represent the way to access and produce Elphy files from NEO objects. As regards reading an existing Elphy file, start by initializing a IO class with it: >>> import neo >>> r = neo.io.ElphyIO( filename="Elphy.DAT" ) >>> r Read the file content into NEO object Block: >>> bl = r.read_block(lazy=False, cascade=True) >>> bl Now you can then read all Elphy data as NEO objects: >>> b1.segments [, , , ] >>> bl.segments[0].analogsignals[0] These functions return NEO objects, completely "detached" from the original Elphy file. Changes to the runtime objects will not cause any changes in the file. Having already existing NEO structures, it is possible to write them as an Elphy file. For example, given a segment: >>> s = neo.Segment() filled with other NEO structures: >>> import numpy as np >>> import quantities as pq >>> a = AnalogSignal( signal=np.random.rand(300), t_start=42*pq.ms) >>> s.analogsignals.append( a ) and added to a newly created NEO Block: >>> bl = neo.Block() >>> bl.segments.append( s ) Then, it's easy to create an Elphy file: >>> r = neo.io.ElphyIO( filename="ElphyNeoTest.DAT" ) >>> r.write_block( bl ) Author: Thierry Brizzi Domenico Guarino """ # needed for python 3 compatibility from __future__ import absolute_import # python commons: from datetime import datetime from fractions import gcd from os import path import re import struct from time import time # note neo.core needs only numpy and quantities import numpy as np import quantities as pq # I need to subclass BaseIO from neo.io.baseio import BaseIO # to import from core from neo.core import (Block, Segment, RecordingChannelGroup, RecordingChannel, AnalogSignal, AnalogSignalArray, EventArray, SpikeTrain) # some tools to finalize the hierachy from neo.io.tools import create_many_to_one_relationship # -------------------------------------------------------- # OBJECTS class ElphyScaleFactor(object): """ Useful to retrieve real values from integer ones that are stored in an Elphy file : ``scale`` : compute the actual value of a sample with this following formula : ``delta`` * value + ``offset`` """ def __init__(self, delta, offset): self.delta = delta self.offset = offset def scale(self, value): return value * self.delta + self.offset class BaseSignal(object): """ A descriptor storing main signal properties : ``layout`` : the :class:``ElphyLayout` object that extracts data from a file. ``episode`` : the episode in which the signal has been acquired. ``sampling_frequency`` : the sampling frequency of the analog to digital converter. ``sampling_period`` : the sampling period of the analog to digital converter computed from sampling_frequency. ``t_start`` : the start time of the signal acquisition. ``t_stop`` : the end time of the signal acquisition. ``duration`` : the duration of the signal acquisition computed from t_start and t_stop. ``n_samples`` : the number of sample acquired during the recording computed from the duration and the sampling period. ``name`` : a label to identify the signal. ``data`` : a property triggering data extraction. """ def __init__(self, layout, episode, sampling_frequency, start, stop, name=None): self.layout = layout self.episode = episode self.sampling_frequency = sampling_frequency self.sampling_period = 1 / sampling_frequency self.t_start = start self.t_stop = stop self.duration = self.t_stop - self.t_start self.n_samples = int(self.duration / self.sampling_period) self.name = name @property def data(self): raise NotImplementedError('must be overloaded in subclass') class ElphySignal(BaseSignal): """ Subclass of :class:`BaseSignal` corresponding to Elphy's analog channels : ``channel`` : the identifier of the analog channel providing the signal. ``units`` : an array containing x and y coordinates units. ``x_unit`` : a property to access the x-coordinates unit. ``y_unit`` : a property to access the y-coordinates unit. ``data`` : a property that delegate data extraction to the ``get_signal_data`` function of the ```layout`` object. """ def __init__(self, layout, episode, channel, x_unit, y_unit, sampling_frequency, start, stop, name=None): super(ElphySignal, self).__init__(layout, episode, sampling_frequency, start, stop, name) self.channel = channel self.units = [x_unit, y_unit] def __str__(self): return "%s ep_%s ch_%s [%s, %s]" % (self.layout.file.name, self.episode, self.channel, self.x_unit, self.y_unit) def __repr__(self): return self.__str__() @property def x_unit(self): """ Return the x-coordinate of the signal. """ return self.units[0] @property def y_unit(self): """ Return the y-coordinate of the signal. """ return self.units[1] @property def data(self): return self.layout.get_signal_data(self.episode, self.channel) class ElphyTag(BaseSignal): """ Subclass of :class:`BaseSignal` corresponding to Elphy's tag channels : ``number`` : the identifier of the tag channel. ``x_unit`` : the unit of the x-coordinate. """ def __init__(self, layout, episode, number, x_unit, sampling_frequency, start, stop, name=None): super(ElphyTag, self).__init__(layout, episode, sampling_frequency, start, stop, name) self.number = number self.units = [x_unit, None] def __str__(self): return "%s : ep_%s tag_ch_%s [%s]" % (self.layout.file.name, self.episode, self.number, self.x_unit) def __repr__(self): return self.__str__() @property def x_unit(self): """ Return the x-coordinate of the signal. """ return self.units[0] @property def data(self): return self.layout.get_tag_data(self.episode, self.number) @property def channel(self): return self.number class ElphyEvent(object): """ A descriptor that store a set of events properties : ``layout`` : the :class:``ElphyLayout` object that extracts data from a file. ``episode`` : the episode in which the signal has been acquired. ``number`` : the identifier of the channel. ``x_unit`` : the unit of the x-coordinate. ``n_events`` : the number of events. ``name`` : a label to identify the event. ``times`` : a property triggering event times extraction. """ def __init__(self, layout, episode, number, x_unit, n_events, ch_number=None, name=None): self.layout = layout self.episode = episode self.number = number self.x_unit = x_unit self.n_events = n_events self.name = name self.ch_number = ch_number def __str__(self): return "%s : ep_%s evt_ch_%s [%s]" % (self.layout.file.name, self.episode, self.number, self.x_unit) def __repr__(self): return self.__str__() @property def channel(self): return self.number @property def times(self): return self.layout.get_event_data(self.episode, self.number) @property def data(self): return self.times class ElphySpikeTrain(ElphyEvent): """ A descriptor that store spiketrain properties : ``wf_samples`` : number of samples composing waveforms. ``wf_sampling_frequency`` : sampling frequency of waveforms. ``wf_sampling_period`` : sampling period of waveforms. ``wf_units`` : the units of the x and y coordinates of waveforms. ``t_start`` : the time before the arrival of the spike which corresponds to the starting time of a waveform. ``name`` : a label to identify the event. ``times`` : a property triggering event times extraction. ``waveforms`` : a property triggering waveforms extraction. """ def __init__(self, layout, episode, number, x_unit, n_events, wf_sampling_frequency, wf_samples, unit_x_wf, unit_y_wf, t_start, name=None): super(ElphySpikeTrain, self).__init__(layout, episode, number, x_unit, n_events, name) self.wf_samples = wf_samples self.wf_sampling_frequency = wf_sampling_frequency assert wf_sampling_frequency, "bad sampling frequency" self.wf_sampling_period = 1.0 / wf_sampling_frequency self.wf_units = [unit_x_wf, unit_y_wf] self.t_start = t_start @property def x_unit_wf(self): """ Return the x-coordinate of waveforms. """ return self.wf_units[0] @property def y_unit_wf(self): """ Return the y-coordinate of waveforms. """ return self.wf_units[1] @property def times(self): return self.layout.get_spiketrain_data(self.episode, self.number) @property def waveforms(self): return self.layout.get_waveform_data(self.episode, self.number) if self.wf_samples else None # -------------------------------------------------------- # BLOCKS class BaseBlock(object): """ Represent a chunk of file storing metadata or raw data. A convenient class to break down the structure of an Elphy file to several building blocks : ``layout`` : the layout containing the block. ``identifier`` : the label that identified the block. ``size`` : the size of the block. ``start`` : the file index corresponding to the starting byte of the block. ``end`` : the file index corresponding to the ending byte of the block NB : Subclassing this class is a convenient way to set the properties using polymorphism rather than a conditional structure. By this way each :class:`BaseBlock` type know how to iterate through the Elphy file and store interesting data. """ def __init__(self, layout, identifier, start, size): self.layout = layout self.identifier = identifier self.size = size self.start = start self.end = self.start + self.size - 1 class ElphyBlock(BaseBlock): """ A subclass of :class:`BaseBlock`. Useful to store the location and size of interesting data within a block : ``parent_block`` : the parent block containing the block. ``header_size`` : the size of the header permitting the identification of the type of the block. ``data_offset`` : the file index located after the block header. ``data_size`` : the size of data located after the header. ``sub_blocks`` : the sub-blocks contained by the block. """ def __init__(self, layout, identifier, start, size, fixed_length=None, size_format="i", parent_block=None): super(ElphyBlock, self).__init__(layout, identifier, start, size) # a block may be a sub-block of another block self.parent_block = parent_block # pascal language store strings in 2 different ways # ... first, if in the program the size of the string is # specified (fixed) then the file stores the length # of the string and allocate a number of bytes equal # to the specified size # ... if this size is not specified the length of the # string is also stored but the file allocate dynamically # a number of bytes equal to the actual size of the string l_ident = len(self.identifier) if fixed_length : l_ident += (fixed_length - l_ident) self.header_size = l_ident + 1 + type_dict[size_format] # starting point of data located in the block self.data_offset = self.start + self.header_size self.data_size = self.size - self.header_size # a block may have sub-blocks # it is to subclasses to initialize # this property self.sub_blocks = list() def __repr__(self): return "%s : size = %s, start = %s, end = %s" % (self.identifier, self.size, self.start, self.end) def add_sub_block(self, block): """ Append a block to the sub-block list. """ self.sub_blocks.append(block) class FileInfoBlock(ElphyBlock): """ Base class of all subclasses whose the purpose is to extract user file info stored into an Elphy file : ``header`` : the header block relative to the block. ``file`` : the file containing the block. NB : User defined metadata are not really practical. An Elphy script must know the order of metadata storage to know exactly how to retrieve these data. That's why it is necessary to subclass and reproduce elphy script commands to extract metadata relative to a protocol. Consequently managing a new protocol implies to refactor the file info extraction. """ def __init__(self, layout, identifier, start, size, fixed_length=None, size_format="i", parent_block=None): super(FileInfoBlock, self).__init__(layout, identifier, start, size, fixed_length, size_format, parent_block=parent_block) self.header = None self.file = self.layout.file def get_protocol_and_version(self): """ Return a tuple useful to identify the kind of protocol that has generated a file during data acquisition. """ raise Exception("must be overloaded in a subclass") def get_user_file_info(self): """ Return a dictionary containing all user file info stored in the file. """ raise Exception("must be overloaded in a subclass") def get_sparsenoise_revcor(self): """ Return 'REVCOR' user file info. This method is common to :class:`ClassicFileInfo` and :class:`MultistimFileInfo` because the last one is able to store this kind of metadata. """ header = dict() header['n_div_x'] = read_from_char(self.file, 'h') header['n_div_y'] = read_from_char(self.file, 'h') header['gray_levels'] = read_from_char(self.file, 'h') header['position_x'] = read_from_char(self.file, 'ext') header['position_y'] = read_from_char(self.file, 'ext') header['length'] = read_from_char(self.file, 'ext') header['width'] = read_from_char(self.file, 'ext') header['orientation'] = read_from_char(self.file, 'ext') header['expansion'] = read_from_char(self.file, 'h') header['scotoma'] = read_from_char(self.file, 'h') header['seed'] = read_from_char(self.file, 'h') #dt_on and dt_off may not exist in old revcor formats rollback = self.file.tell() header['dt_on'] = read_from_char(self.file, 'ext') if header['dt_on'] is None : self.file.seek(rollback) rollback = self.file.tell() header['dt_off'] = read_from_char(self.file, 'ext') if header['dt_off'] is None : self.file.seek(rollback) return header class ClassicFileInfo(FileInfoBlock): """ Extract user file info stored into an Elphy file corresponding to sparse noise (revcor), moving bar and flashbar protocols. """ def detect_protocol_from_name(self, path): pattern = "\d{4}(\d+|\D)\D" codes = { 'r':'sparsenoise', 'o':'movingbar', 'f':'flashbar', 'm':'multistim' # here just for assertion } filename = path.split(path)[1] match = re.search(pattern, path) if hasattr(match, 'end') : code = codes.get(path[match.end() - 1].lower(), None) assert code != 'm', "multistim file detected" return code elif 'spt' in filename.lower() : return 'spontaneousactivity' else : return None def get_protocol_and_version(self): if self.layout and self.layout.info_block : self.file.seek(self.layout.info_block.data_offset) version = self.get_title() if version in ['REVCOR1', 'REVCOR2', 'REVCOR + PAIRING'] : name = "sparsenoise" elif version in ['BARFLASH'] : name = "flashbar" elif version in ['ORISTIM', 'ORISTM', 'ORISTM1', 'ORITUN'] : name = "movingbar" else : name = self.detect_protocol_from_name(self.file.name) self.file.seek(0) return name, version return None, None def get_title(self): title_length, title = struct.unpack('= 2) : name = None version = None else : if center == 2 : name = "sparsenoise" elif center == 3 : name = "densenoise" elif center == 4 : name = "densenoise" elif center == 5 : name = "grating" else : name = None version = None self.file.seek(0) return name, version return None, None def get_title(self): title_length = read_from_char(self.file, 'B') title, = struct.unpack('<%ss' % title_length, self.file.read(title_length)) self.file.seek(self.file.tell() + 255 - title_length) return unicode(title) def get_user_file_info(self): header = dict() if self.layout and self.layout.info_block : # go to the info_block sub_block = self.layout.info_block self.file.seek(sub_block.data_offset) #get the first four parameters acqLGN = read_from_char(self.file, 'i') center = read_from_char(self.file, 'i') surround = read_from_char(self.file, 'i') #store info in the header header['acqLGN'] = acqLGN header['center'] = center header['surround'] = surround if not (header['surround'] >= 2) : header.update(self.get_center_header(center)) self.file.seek(0) return header def get_center_header(self, code): #get file info corresponding #to the executed protocol #for the center first ... if code == 0 : return self.get_sparsenoise_revcor() elif code == 2 : return self.get_sparsenoise_center() elif code == 3 : return self.get_densenoise_center(True) elif code == 4 : return self.get_densenoise_center(False) elif code == 5 : return dict() # return self.get_grating_center() else : return dict() def get_surround_header(self, code): #then the surround if code == 2 : return self.get_sparsenoise_surround() elif code == 3 : return self.get_densenoise_surround(True) elif code == 4 : return self.get_densenoise_surround(False) elif code == 5 : raise NotImplementedError() return self.get_grating_center() else : return dict() def get_center_surround(self, center, surround): header = dict() header['stim_center'] = self.get_center_header(center) header['stim_surround'] = self.get_surround_header(surround) return header def get_sparsenoise_center(self): header = dict() header['title'] = self.get_title() header['number_of_sequences'] = read_from_char(self.file, 'i') header['pretrigger_duration'] = read_from_char(self.file, 'ext') header['n_div_x'] = read_from_char(self.file, 'h') header['n_div_y'] = read_from_char(self.file, 'h') header['gray_levels'] = read_from_char(self.file, 'h') header['position_x'] = read_from_char(self.file, 'ext') header['position_y'] = read_from_char(self.file, 'ext') header['length'] = read_from_char(self.file, 'ext') header['width'] = read_from_char(self.file, 'ext') header['orientation'] = read_from_char(self.file, 'ext') header['expansion'] = read_from_char(self.file, 'h') header['scotoma'] = read_from_char(self.file, 'h') header['seed'] = read_from_char(self.file, 'h') header['luminance_1'] = read_from_char(self.file, 'ext') header['luminance_2'] = read_from_char(self.file, 'ext') header['dt_count'] = read_from_char(self.file, 'i') dt_array = list() for _ in range(0, header['dt_count']) : dt_array.append(read_from_char(self.file, 'ext')) header['dt_on'] = dt_array if dt_array else None header['dt_off'] = read_from_char(self.file, 'ext') return header def get_sparsenoise_surround(self): header = dict() header['title_surround'] = self.get_title() header['gap'] = read_from_char(self.file, 'ext') header['n_div_x'] = read_from_char(self.file, 'h') header['n_div_y'] = read_from_char(self.file, 'h') header['gray_levels'] = read_from_char(self.file, 'h') header['expansion'] = read_from_char(self.file, 'h') header['scotoma'] = read_from_char(self.file, 'h') header['seed'] = read_from_char(self.file, 'h') header['luminance_1'] = read_from_char(self.file, 'ext') header['luminance_2'] = read_from_char(self.file, 'ext') header['dt_on'] = read_from_char(self.file, 'ext') header['dt_off'] = read_from_char(self.file, 'ext') return header def get_densenoise_center(self, is_binary): header = dict() header['stimulus_type'] = "B" if is_binary else "T" header['title'] = self.get_title() _tmp = read_from_char(self.file, 'i') header['number_of_sequences'] = _tmp if _tmp < 0 else None rollback = self.file.tell() header['stimulus_duration'] = read_from_char(self.file, 'ext') if header['stimulus_duration'] is None : self.file.seek(rollback) header['pretrigger_duration'] = read_from_char(self.file, 'ext') header['n_div_x'] = read_from_char(self.file, 'h') header['n_div_y'] = read_from_char(self.file, 'h') header['position_x'] = read_from_char(self.file, 'ext') header['position_y'] = read_from_char(self.file, 'ext') header['length'] = read_from_char(self.file, 'ext') header['width'] = read_from_char(self.file, 'ext') header['orientation'] = read_from_char(self.file, 'ext') header['expansion'] = read_from_char(self.file, 'h') header['seed'] = read_from_char(self.file, 'h') header['luminance_1'] = read_from_char(self.file, 'ext') header['luminance_2'] = read_from_char(self.file, 'ext') header['dt_on'] = read_from_char(self.file, 'ext') header['dt_off'] = read_from_char(self.file, 'ext') return header def get_densenoise_surround(self, is_binary): header = dict() header['title_surround'] = self.get_title() header['gap'] = read_from_char(self.file, 'ext') header['n_div_x'] = read_from_char(self.file, 'h') header['n_div_y'] = read_from_char(self.file, 'h') header['expansion'] = read_from_char(self.file, 'h') header['seed'] = read_from_char(self.file, 'h') header['luminance_1'] = read_from_char(self.file, 'ext') header['luminance_2'] = read_from_char(self.file, 'ext') header['dt_on'] = read_from_char(self.file, 'ext') header['dt_off'] = read_from_char(self.file, 'ext') return header def get_grating_center(self): pass def get_grating_surround(self): pass class Header(ElphyBlock): """ A convenient subclass of :class:`Block` to store Elphy file header properties. NB : Subclassing this class is a convenient way to set the properties of the header using polymorphism rather than a conditional structure. """ def __init__(self, layout, identifier, size, fixed_length=None, size_format="i"): super(Header, self).__init__(layout, identifier, 0, size, fixed_length, size_format) class Acquis1Header(Header): """ A subclass of :class:`Header` used to identify the 'ACQUIS1/GS/1991' format. Whereas more recent format, the header contains all data relative to episodes, channels and traces : ``n_channels`` : the number of acquisition channels. ``nbpt`` and ``nbptEx`` : parameters useful to compute the number of samples by episodes. ``tpData`` : the data format identifier used to compute sample size. ``x_unit`` : the x-coordinate unit for all channels in an episode. ``y_units`` : an array containing y-coordinate units for each channel in the episode. ``dX`` and ``X0`` : the scale factors necessary to retrieve the actual times relative to each sample in a channel. ``dY_ar`` and ``Y0_ar``: arrays of scale factors necessary to retrieve the actual values relative to samples. ``continuous`` : a boolean telling if the file has been acquired in continuous mode. ``preSeqI`` : the size in bytes of the data preceding raw data. ``postSeqI`` : the size in bytes of the data preceding raw data. ``dat_length`` : the length in bytes of the data in the file. ``sample_size`` : the size in bytes of a sample. ``n_samples`` : the number of samples. ``ep_size`` : the size in bytes of an episode. ``n_episodes`` : the number of recording sequences store in the file. NB : The size is read from the file, the identifier is a string containing 15 characters and the size is encoded as small integer. See file 'FicDefAc1.pas' to identify the parsed parameters. """ def __init__(self, layout): fileobj = layout.file super(Acquis1Header, self).__init__(layout, "ACQUIS1/GS/1991", 1024, 15, "h") #parse the header to store interesting data about episodes and channels fileobj.seek(18) #extract episode properties n_channels = read_from_char(fileobj, 'B') assert not ((n_channels < 1) or (n_channels > 16)), "bad number of channels" nbpt = read_from_char(fileobj, 'h') l_xu, x_unit = struct.unpack('= self.end : tagShift = 0 else : tagShift = read_from_char(layout.file, 'B') #setup object properties self.n_channels = n_channels self.nbpt = nbpt self.tpData = tpData self.x_unit = xu[0:l_xu] self.dX = dX self.X0 = X0 self.y_units = y_units[0:n_channels] self.dY_ar = dY_ar[0:n_channels] self.Y0_ar = Y0_ar[0:n_channels] self.continuous = continuous if self.continuous : self.preSeqI = 0 self.postSeqI = 0 else : self.preSeqI = preSeqI self.postSeqI = postSeqI self.varEp = varEp self.withTags = withTags if not self.withTags : self.tagShift = 0 else : if tagShift == 0 : self.tagShift = 4 else : self.tagShift = tagShift self.sample_size = type_dict[types[self.tpData]] self.dat_length = self.layout.file_size - self.layout.data_offset if self.continuous : if self.n_channels > 0 : self.n_samples = self.dat_length / (self.n_channels * self.sample_size) else : self.n_samples = 0 else : self.n_samples = self.nbpt self.ep_size = self.preSeqI + self.postSeqI + self.n_samples * self.sample_size * self.n_channels self.n_episodes = self.dat_length / self.ep_size if (self.n_samples != 0) else 0 class DAC2GSEpisodeBlock(ElphyBlock): """ Subclass of :class:`Block` useful to store data corresponding to 'DAC2SEQ' blocks stored in the DAC2/GS/2000 format. ``n_channels`` : the number of acquisition channels. ``nbpt`` : the number of samples by episodes. ``tpData`` : the data format identifier used to compute the sample size. ``x_unit`` : the x-coordinate unit for all channels in an episode. ``y_units`` : an array containing y-coordinate units for each channel in the episode. ``dX`` and ``X0`` : the scale factors necessary to retrieve the actual times relative to each sample in a channel. ``dY_ar`` and ``Y0_ar``: arrays of scale factors necessary to retrieve the actual values relative to samples. ``postSeqI`` : the size in bytes of the data preceding raw data. NB : see file 'FdefDac2.pas' to identify the parsed parameters. """ def __init__(self, layout, identifier, start, size, fixed_length=None, size_format="i"): main = layout.main_block n_channels, nbpt, tpData, postSeqI = struct.unpack(' 0] for data_block in blocks : self.file.seek(data_block.start) raw = self.file.read(data_block.size)[0:expected_size] databytes = np.frombuffer(raw, dtype=dtype) chunks.append(databytes) # concatenate all chunks and return # the specified slice if len(chunks)>0 : databytes = np.concatenate(chunks) return databytes[start:end] else : return np.array([]) def reshape_bytes(self, databytes, reshape, datatypes, order='<'): """ Reshape a numpy array containing a set of databytes. """ assert datatypes and len(datatypes) == len(reshape), "datatypes are not well defined" l_bytes = len(databytes) #create the mask for each shape shape_mask = list() for shape in reshape : for _ in xrange(1, shape + 1) : shape_mask.append(shape) #create a set of masks to extract data bit_masks = list() for shape in reshape : bit_mask = list() for value in shape_mask : bit = 1 if (value == shape) else 0 bit_mask.append(bit) bit_masks.append(np.array(bit_mask)) #extract data n_samples = l_bytes / np.sum(reshape) data = np.empty([len(reshape), n_samples], dtype=(int, int)) for index, bit_mask in enumerate(bit_masks) : tmp = self.filter_bytes(databytes, bit_mask) tp = '%s%s%s' % (order, datatypes[index], reshape[index]) data[index] = np.frombuffer(tmp, dtype=tp) return data.T def filter_bytes(self, databytes, bit_mask): """ Detect from a bit mask which bits to keep to recompose the signal. """ n_bytes = len(databytes) mask = np.ones(n_bytes, dtype=int) np.putmask(mask, mask, bit_mask) to_keep = np.where(mask > 0)[0] return databytes.take(to_keep) def load_channel_data(self, ep, ch): """ Return a numpy array containing the list of bytes corresponding to the specified episode and channel. """ #memorise the sample size and symbol sample_size = self.sample_size(ep, ch) sample_symbol = self.sample_symbol(ep, ch) #create a bit mask to define which #sample to keep from the file bit_mask = self.create_bit_mask(ep, ch) #load all bytes contained in an episode data_blocks = self.get_data_blocks(ep) databytes = self.load_bytes(data_blocks) raw = self.filter_bytes(databytes, bit_mask) #reshape bytes from the sample size dt = np.dtype(numpy_map[sample_symbol]) dt.newbyteorder('<') return np.frombuffer(raw.reshape([len(raw) / sample_size, sample_size]), dt) def apply_op(self, np_array, value, op_type): """ A convenient function to apply an operator over all elements of a numpy array. """ if op_type == "shift_right" : return np_array >> value elif op_type == "shift_left" : return np_array << value elif op_type == "mask" : return np_array & value else : return np_array def get_tag_mask(self, tag_ch, tag_mode): """ Return a mask useful to retrieve bits that encode a tag channel. """ if tag_mode == 1 : tag_mask = 0b01 if (tag_ch == 1) else 0b10 elif tag_mode in [2, 3] : ar_mask = np.zeros(16, dtype=int) ar_mask[tag_ch - 1] = 1 st = "0b" + ''.join(np.array(np.flipud(ar_mask), dtype=str)) tag_mask = eval(st) return tag_mask def load_encoded_tags(self, ep, tag_ch): """ Return a numpy array containing bytes corresponding to the specified episode and channel. """ tag_mode = self.tag_mode(ep) tag_mask = self.get_tag_mask(tag_ch, tag_mode) if tag_mode in [1, 2] : #digidata or itc mode #available for all formats ch = self.get_channel_for_tags(ep) raw = self.load_channel_data(ep, ch) return self.apply_op(raw, tag_mask, "mask") elif tag_mode == 3 : #cyber k mode #only available for DAC2 objects format #store bytes corresponding to the blocks #containing tags in a numpy array and reshape #it to have a set of tuples (time, value) ck_blocks = self.get_blocks_of_type(ep, 'RCyberTag') databytes = self.load_bytes(ck_blocks) raw = self.reshape_bytes(databytes, reshape=(4, 2), datatypes=('u', 'u'), order='<') #keep only items that are compatible #with the specified tag channel raw[:, 1] = self.apply_op(raw[:, 1], tag_mask, "mask") #computing numpy.diff is useful to know #how many times a value is maintained #and necessary to reconstruct the #compressed signal ... repeats = np.array(np.diff(raw[:, 0]), dtype=int) data = np.repeat(raw[:-1, 1], repeats, axis=0) # ... note that there is always #a transition at t=0 for synchronisation #purpose, consequently it is not necessary #to complete with zeros when the first #transition arrive ... return data def load_encoded_data(self, ep, ch): """ Get encoded value of raw data from the elphy file. """ tag_shift = self.tag_shift(ep) data = self.load_channel_data(ep, ch) if tag_shift : return self.apply_op(data, tag_shift, "shift_right") else : return data def get_signal_data(self, ep, ch): """ Return a numpy array containing all samples of a signal, acquired on an Elphy analog channel, formatted as a list of (time, value) tuples. """ #get data from the file y_data = self.load_encoded_data(ep, ch) x_data = np.arange(0, len(y_data)) #create a recarray data = np.recarray(len(y_data), dtype=[('x', b_float), ('y', b_float)]) #put in the recarray the scaled data x_factors = self.x_scale_factors(ep, ch) y_factors = self.y_scale_factors(ep, ch) data['x'] = x_factors.scale(x_data) data['y'] = y_factors.scale(y_data) return data def get_tag_data(self, ep, tag_ch): """ Return a numpy array containing all samples of a signal, acquired on an Elphy tag channel, formatted as a list of (time, value) tuples. """ #get data from the file y_data = self.load_encoded_tags(ep, tag_ch) x_data = np.arange(0, len(y_data)) #create a recarray data = np.recarray(len(y_data), dtype=[('x', b_float), ('y', b_int)]) #put in the recarray the scaled data factors = self.x_tag_scale_factors(ep) data['x'] = factors.scale(x_data) data['y'] = y_data return data class Acquis1Layout(ElphyLayout): """ A subclass of :class:`ElphyLayout` to know how the 'ACQUIS1/GS/1991' format is organised. Extends :class:`ElphyLayout` to store the offset used to retrieve directly raw data : ``data_offset`` : an offset to jump directly to the raw data. """ def __init__(self, fileobj, data_offset): super(Acquis1Layout, self).__init__(fileobj) self.data_offset = data_offset self.data_blocks = None def get_blocks_end(self): return self.data_offset def is_continuous(self): return self.header.continuous def get_episode_blocks(self): raise NotImplementedError() def set_info_block(self): i_blks = self.get_blocks_of_type('USER INFO') assert len(i_blks) < 2, 'too many info blocks' if len(i_blks) : self.info_block = i_blks[0] def set_data_blocks(self): data_blocks = list() size = self.header.n_samples * self.header.sample_size * self.header.n_channels for ep in range(0, self.header.n_episodes) : start = self.data_offset + ep * self.header.ep_size + self.header.preSeqI data_blocks.append(DummyDataBlock(self, 'Acquis1Data', start, size)) self.data_blocks = data_blocks def get_data_blocks(self, ep): return [self.data_blocks[ep - 1]] @property def n_episodes(self): return self.header.n_episodes def n_channels(self, episode): return self.header.n_channels def n_tags(self, episode): return 0 def tag_mode(self, ep): return 0 def tag_shift(self, ep): return 0 def get_channel_for_tags(self, ep): return None @property def no_analog_data(self): return True if (self.n_episodes == 0) else self.header.no_analog_data def sample_type(self, ep, ch): return self.header.tpData def sampling_period(self, ep, ch): return self.header.dX def n_samples(self, ep, ch): return self.header.n_samples def x_tag_scale_factors(self, ep): return ElphyScaleFactor( self.header.dX, self.header.X0 ) def x_scale_factors(self, ep, ch): return ElphyScaleFactor( self.header.dX, self.header.X0 ) def y_scale_factors(self, ep, ch): dY = self.header.dY_ar[ch - 1] Y0 = self.header.Y0_ar[ch - 1] # TODO: see why this kind of exception exists if dY is None or Y0 is None : raise Exception('bad Y-scale factors for episode %s channel %s' % (ep, ch)) return ElphyScaleFactor(dY, Y0) def x_unit(self, ep, ch): return self.header.x_unit def y_unit(self, ep, ch): return self.header.y_units[ch - 1] @property def ep_size(self): return self.header.ep_size @property def file_duration(self): return self.header.dX * self.n_samples def get_tag(self, episode, tag_channel): return None def create_channel_mask(self, ep): return np.arange(1, self.header.n_channels + 1) class DAC2GSLayout(ElphyLayout): """ A subclass of :class:`ElphyLayout` to know how the 'DAC2 / GS / 2000' format is organised. Extends :class:`ElphyLayout` to store the offset used to retrieve directly raw data : ``data_offset`` : an offset to jump directly after the 'MAIN' block where 'DAC2SEQ' blocks start. ``main_block```: a shortcut to access 'MAIN' block. ``episode_blocks`` : a shortcut to access blocks corresponding to episodes. """ def __init__(self, fileobj, data_offset): super(DAC2GSLayout, self).__init__(fileobj) self.data_offset = data_offset self.main_block = None self.episode_blocks = None def get_blocks_end(self): return self.file_size #data_offset def is_continuous(self): main_block = self.main_block return main_block.continuous if main_block else False def get_episode_blocks(self): raise NotImplementedError() def set_main_block(self): main_block = self.get_blocks_of_type('MAIN') self.main_block = main_block[0] if main_block else None def set_episode_blocks(self): ep_blocks = self.get_blocks_of_type('DAC2SEQ') self.episode_blocks = ep_blocks if ep_blocks else None def set_info_block(self): i_blks = self.get_blocks_of_type('USER INFO') assert len(i_blks) < 2, "too many info blocks" if len(i_blks) : self.info_block = i_blks[0] def set_data_blocks(self): data_blocks = list() identifier = 'DAC2GSData' size = self.main_block.n_samples * self.main_block.sample_size * self.main_block.n_channels if not self.is_continuous() : blocks = self.get_blocks_of_type('DAC2SEQ') for block in blocks : start = block.start + self.main_block.preSeqI data_blocks.append(DummyDataBlock(self, identifier, start, size)) else : start = self.blocks[-1].end + 1 + self.main_block.preSeqI data_blocks.append(DummyDataBlock(self, identifier, start, size)) self.data_blocks = data_blocks def get_data_blocks(self, ep): return [self.data_blocks[ep - 1]] def episode_block(self, ep): return self.main_block if self.is_continuous() else self.episode_blocks[ep - 1] def tag_mode(self, ep): return 1 if self.main_block.withTags else 0 def tag_shift(self, ep): return self.main_block.tagShift def get_channel_for_tags(self, ep): return 1 def sample_type(self, ep, ch): return self.main_block.tpData def sample_size(self, ep, ch): size = super(DAC2GSLayout, self).sample_size(ep, ch) assert size == 2, "sample size is always 2 bytes for DAC2/GS/2000 format" return size def sampling_period(self, ep, ch): block = self.episode_block(ep) return block.dX def x_tag_scale_factors(self, ep): block = self.episode_block(ep) return ElphyScaleFactor( block.dX, block.X0, ) def x_scale_factors(self, ep, ch): block = self.episode_block(ep) return ElphyScaleFactor( block.dX, block.X0, ) def y_scale_factors(self, ep, ch): block = self.episode_block(ep) return ElphyScaleFactor( block.dY_ar[ch - 1], block.Y0_ar[ch - 1] ) def x_unit(self, ep, ch): block = self.episode_block(ep) return block.x_unit def y_unit(self, ep, ch): block = self.episode_block(ep) return block.y_units[ch - 1] def n_samples(self, ep, ch): return self.main_block.n_samples def ep_size(self, ep): return self.main_block.ep_size @property def n_episodes(self): return self.main_block.n_episodes def n_channels(self, episode): return self.main_block.n_channels def n_tags(self, episode): return 2 if self.main_block.withTags else 0 @property def file_duration(self): return self.main_block.dX * self.n_samples def get_tag(self, episode, tag_channel): assert episode in range(1, self.n_episodes + 1) # there are none or 2 tag channels if self.tag_mode(episode) == 1 : assert tag_channel in range(1, 3), "DAC2/GS/2000 format support only 2 tag channels" block = self.episode_block(episode) t_stop = self.main_block.n_samples * block.dX return ElphyTag(self, episode, tag_channel, block.x_unit, 1.0 / block.dX, 0, t_stop) else : return None def n_tag_samples(self, ep, tag_channel): return self.main_block.n_samples def get_tag_data(self, episode, tag_channel): #memorise some useful properties block = self.episode_block(episode) sample_size = self.sample_size(episode, tag_channel) sample_symbol = self.sample_symbol(episode, tag_channel) #create a bit mask to define which #sample to keep from the file channel_mask = self.create_channel_mask(episode) bit_mask = self.create_bit_mask(channel_mask, 1) #get bytes from the file data_block = self.data_blocks[episode - 1] n_bytes = data_block.size self.file.seek(data_block.start) databytes = np.frombuffer(self.file.read(n_bytes), ' 0)[0] raw = databytes.take(to_keep) raw = raw.reshape([len(raw) / sample_size, sample_size]) #create a recarray containing data dt = np.dtype(numpy_map[sample_symbol]) dt.newbyteorder('<') tag_mask = 0b01 if (tag_channel == 1) else 0b10 y_data = np.frombuffer(raw, dt) & tag_mask x_data = np.arange(0, len(y_data)) * block.dX + block.X0 data = np.recarray(len(y_data), dtype=[('x', b_float), ('y', b_int)]) data['x'] = x_data data['y'] = y_data return data def create_channel_mask(self, ep): return np.arange(1, self.main_block.n_channels + 1) class DAC2Layout(ElphyLayout): """ A subclass of :class:`ElphyLayout` to know how the Elphy format is organised. Whereas other formats storing raw data at the end of the file, 'DAC2 objects' format spreads them over multiple blocks : ``episode_blocks`` : a shortcut to access blocks corresponding to episodes. """ def __init__(self, fileobj): super(DAC2Layout, self).__init__(fileobj) self.episode_blocks = None def get_blocks_end(self): return self.file_size def is_continuous(self): ep_blocks = [k for k in self.blocks if k.identifier.startswith('B_Ep')] if ep_blocks : ep_block = ep_blocks[0] ep_sub_block = ep_block.sub_blocks[0] return ep_sub_block.continuous else : return False def set_episode_blocks(self): self.episode_blocks = [k for k in self.blocks if k.identifier.startswith('B_Ep')] def set_info_block(self): #in fact the file info are contained into a single sub-block with an USR identifier i_blks = self.get_blocks_of_type('B_Finfo') assert len(i_blks) < 2, "too many info blocks" if len(i_blks) : i_blk = i_blks[0] sub_blocks = i_blk.sub_blocks if len(sub_blocks) : self.info_block = sub_blocks[0] def set_data_blocks(self): data_blocks = list() blocks = self.get_blocks_of_type('RDATA') for block in blocks : start = block.data_start size = block.end + 1 - start data_blocks.append(DummyDataBlock(self, 'RDATA', start, size)) self.data_blocks = data_blocks def get_data_blocks(self, ep): return self.group_blocks_of_type(ep, 'RDATA') def group_blocks_of_type(self, ep, identifier): ep_blocks = list() blocks = [k for k in self.get_blocks_stored_in_episode(ep) if k.identifier == identifier] for block in blocks : start = block.data_start size = block.end + 1 - start ep_blocks.append(DummyDataBlock(self, identifier, start, size)) return ep_blocks def get_blocks_stored_in_episode(self, ep): data_blocks = [k for k in self.blocks if k.identifier == 'RDATA'] n_ep = self.n_episodes blk_1 = self.episode_block(ep) blk_2 = self.episode_block((ep + 1) % n_ep) i_1 = self.blocks.index(blk_1) i_2 = self.blocks.index(blk_2) if (blk_1 == blk_2) or (i_2 < i_1) : return [k for k in data_blocks if self.blocks.index(k) > i_1] else : return [k for k in data_blocks if self.blocks.index(k) in xrange(i_1, i_2)] def set_cyberk_blocks(self): ck_blocks = list() blocks = self.get_blocks_of_type('RCyberTag') for block in blocks : start = block.data_start size = block.end + 1 - start ck_blocks.append(DummyDataBlock(self, 'RCyberTag', start, size)) self.ck_blocks = ck_blocks def episode_block(self, ep): return self.episode_blocks[ep - 1] @property def n_episodes(self): return len(self.episode_blocks) def analog_index(self, episode): """ Return indices relative to channels used for analog signals. """ block = self.episode_block(episode) tag_mode = block.ep_block.tag_mode an_index = np.where(np.array(block.ks_block.k_sampling) > 0) if tag_mode == 2 : an_index = an_index[:-1] return an_index def n_channels(self, episode): """ Return the number of channels used for analog signals but also events. NB : in Elphy this 2 kinds of channels are not differenciated. """ block = self.episode_block(episode) tag_mode = block.ep_block.tag_mode n_channels = len(block.ks_block.k_sampling) return n_channels if tag_mode != 2 else n_channels - 1 def n_tags(self, episode): block = self.episode_block(episode) tag_mode = block.ep_block.tag_mode tag_map = {0:0, 1:2, 2:16, 3:16} return tag_map.get(tag_mode, 0) def n_events(self, episode): """ Return the number of channels dedicated to events. """ block = self.episode_block(episode) return block.ks_block.k_sampling.count(0) def n_spiketrains(self, episode): spk_blocks = [k for k in self.blocks if k.identifier == 'RSPK'] return spk_blocks[0].n_evt_channels if spk_blocks else 0 def sub_sampling(self, ep, ch): """ Return the sub-sampling factor for the specified episode and channel. """ block = self.episode_block(ep) return block.ks_block.k_sampling[ch - 1] if block.ks_block else 1 def aggregate_size(self, block, ep): ag_count = self.aggregate_sample_count(block) ag_size = 0 for ch in range(1, ag_count + 1) : if (block.ks_block.k_sampling[ch - 1] != 0) : ag_size += self.sample_size(ep, ch) return ag_size def n_samples(self, ep, ch): block = self.episode_block(ep) if not block.ep_block.continuous : return block.ep_block.nbpt / self.sub_sampling(ep, ch) else : # for continuous case there isn't any place # in the file that contains the number of # samples unlike the episode case ... data_blocks = self.get_data_blocks(ep) total_size = np.sum([k.size for k in data_blocks]) # count the number of samples in an # aggregate and compute its size in order # to determine the size of an aggregate ag_count = self.aggregate_sample_count(block) ag_size = self.aggregate_size(block, ep) n_ag = total_size / ag_size # the number of samples is equal # to the number of aggregates ... n_samples = n_ag n_chunks = total_size % ag_size # ... but not when there exists # a incomplete aggregate at the # end of the file, consequently # the preeceeding computed number # of samples must be incremented # by one only if the channel map # to a sample in the last aggregate # ... maybe this last part should be # deleted because the n_chunks is always # null in continuous mode if n_chunks : last_ag_size = total_size - n_ag * ag_count size = 0 for i in range(0, ch) : size += self.sample_size(ep, i + 1) if size <= last_ag_size : n_samples += 1 return n_samples def sample_type(self, ep, ch): block = self.episode_block(ep) return block.kt_block.k_types[ch - 1] if block.kt_block else block.ep_block.tpData def sampling_period(self, ep, ch): block = self.episode_block(ep) return block.ep_block.dX * self.sub_sampling(ep, ch) def x_tag_scale_factors(self, ep): block = self.episode_block(ep) return ElphyScaleFactor( block.ep_block.dX, block.ep_block.X0 ) def x_scale_factors(self, ep, ch): block = self.episode_block(ep) return ElphyScaleFactor( block.ep_block.dX * block.ks_block.k_sampling[ch - 1], block.ep_block.X0, ) def y_scale_factors(self, ep, ch): block = self.episode_block(ep) return ElphyScaleFactor( block.ch_block.dY_ar[ch - 1], block.ch_block.Y0_ar[ch - 1] ) def x_unit(self, ep, ch): block = self.episode_block(ep) return block.ep_block.x_unit def y_unit(self, ep, ch): block = self.episode_block(ep) return block.ch_block.y_units[ch - 1] def tag_mode(self, ep): block = self.episode_block(ep) return block.ep_block.tag_mode def tag_shift(self, ep): block = self.episode_block(ep) return block.ep_block.tag_shift def get_channel_for_tags(self, ep): block = self.episode_block(ep) tag_mode = self.tag_mode(ep) if tag_mode == 1 : ks = np.array(block.ks_block.k_sampling) mins = np.where(ks == ks.min())[0] + 1 return mins[0] elif tag_mode == 2 : return block.ep_block.n_channels else : return None def aggregate_sample_count(self, block): """ Return the number of sample in an aggregate. """ # compute the least common multiple # for channels having block.ks_block.k_sampling[ch] > 0 lcm0 = 1 for i in range(0, block.ep_block.n_channels) : if block.ks_block.k_sampling[i] > 0 : lcm0 = least_common_multiple(lcm0, block.ks_block.k_sampling[i]) # sum quotients lcm / KSampling count = 0 for i in range(0, block.ep_block.n_channels) : if block.ks_block.k_sampling[i] > 0 : count += lcm0 / block.ks_block.k_sampling[i] return count def create_channel_mask(self, ep): """ Return the minimal pattern of channel numbers representing the succession of channels in the multiplexed data. It is useful to do the mapping between a sample stored in the file and its relative channel. NB : This function has been converted from the 'TseqBlock.BuildMask' method of the file 'ElphyFormat.pas' stored in Elphy source code. """ block = self.episode_block(ep) ag_count = self.aggregate_sample_count(block) mask_ar = np.zeros(ag_count, dtype='i') ag_size = 0 i = 0 k = 0 while k < ag_count : for j in range(0, block.ep_block.n_channels) : if (block.ks_block.k_sampling[j] != 0) and (i % block.ks_block.k_sampling[j] == 0) : mask_ar[k] = j + 1 ag_size += self.sample_size(ep, j + 1) k += 1 if k >= ag_count : break i += 1 return mask_ar def get_signal(self, episode, channel): block = self.episode_block(episode) k_sampling = np.array(block.ks_block.k_sampling) evt_channels = np.where(k_sampling == 0)[0] if not channel in evt_channels : return super(DAC2Layout, self).get_signal(episode, channel) else : k_sampling[channel - 1] = -1 return self.get_event(episode, channel, k_sampling) def get_tag(self, episode, tag_channel): """ Return a :class:`ElphyTag` which is a descriptor of the specified event channel. """ assert episode in range(1, self.n_episodes + 1) # there are none, 2 or 16 tag # channels depending on tag_mode tag_mode = self.tag_mode(episode) if tag_mode : block = self.episode_block(episode) x_unit = block.ep_block.x_unit # verify the validity of the tag channel if tag_mode == 1 : assert tag_channel in range(1, 3), "Elphy format support only 2 tag channels for tag_mode == 1" elif tag_mode == 2 : assert tag_channel in range(1, 17), "Elphy format support only 16 tag channels for tag_mode == 2" elif tag_mode == 3 : assert tag_channel in range(1, 17), "Elphy format support only 16 tag channels for tag_mode == 3" smp_period = block.ep_block.dX smp_freq = 1.0 / smp_period if tag_mode != 3 : ch = self.get_channel_for_tags(episode) n_samples = self.n_samples(episode, ch) t_stop = (n_samples - 1) * smp_freq else : # get the max of n_samples multiplied by the sampling # period done on every analog channels in order to avoid # the selection of a channel without concrete signals t_max = list() for ch in self.analog_index(episode) : n_samples = self.n_samples(episode, ch) factors = self.x_scale_factors(episode, ch) chtime = n_samples * factors.delta t_max.append(chtime) time_max = max(t_max) # as (n_samples_tag - 1) * dX_tag # and time_max = n_sample_tag * dX_tag # it comes the following duration t_stop = time_max - smp_period return ElphyTag(self, episode, tag_channel, x_unit, smp_freq, 0, t_stop) else : return None def get_event(self, ep, ch, marked_ks): """ Return a :class:`ElphyEvent` which is a descriptor of the specified event channel. """ assert ep in range(1, self.n_episodes + 1) assert ch in range(1, self.n_channels + 1) # find the event channel number evt_channel = np.where(marked_ks == -1)[0][0] assert evt_channel in range(1, self.n_events(ep) + 1) block = self.episode_block(ep) ep_blocks = self.get_blocks_stored_in_episode(ep) evt_blocks = [k for k in ep_blocks if k.identifier == 'REVT'] n_events = np.sum([k.n_events[evt_channel - 1] for k in evt_blocks], dtype=int) x_unit = block.ep_block.x_unit return ElphyEvent(self, ep, evt_channel, x_unit, n_events, ch_number=ch) def load_encoded_events(self, episode, evt_channel, identifier): """ Return times stored as a 4-bytes integer in the specified event channel. """ data_blocks = self.group_blocks_of_type(episode, identifier) ep_blocks = self.get_blocks_stored_in_episode(episode) evt_blocks = [k for k in ep_blocks if k.identifier == identifier] #compute events on each channel n_events = np.sum([k.n_events for k in evt_blocks], dtype=int, axis=0) pre_events = np.sum(n_events[0:evt_channel - 1], dtype=int) start = pre_events end = start + n_events[evt_channel - 1] expected_size = 4 * np.sum(n_events, dtype=int) return self.load_bytes(data_blocks, dtype=' 0 : name = names[episode-1] start = name.size+1 - name.data_size+1 end = name.end - name.start+1 chars = self.load_bytes([name], dtype='uint8', start=start, end=end, expected_size=name.size ).tolist() #print "chars[%s:%s]: %s" % (start,end,chars) episode_name = ''.join([chr(k) for k in chars]) return episode_name def get_event_data(self, episode, evt_channel): """ Return times contained in the specified event channel. This function is triggered when the 'times' property of an :class:`ElphyEvent` descriptor instance is accessed. """ times = self.load_encoded_events(episode, evt_channel, "REVT") block = self.episode_block(episode) return times * block.ep_block.dX / len(block.ks_block.k_sampling) def get_spiketrain(self, episode, electrode_id): """ Return a :class:`Spike` which is a descriptor of the specified spike channel. """ assert episode in range(1, self.n_episodes + 1) assert electrode_id in range(1, self.n_spiketrains(episode) + 1) # get some properties stored in the episode sub-block block = self.episode_block(episode) x_unit = block.ep_block.x_unit x_unit_wf = getattr(block.ep_block, 'x_unit_wf', None) y_unit_wf = getattr(block.ep_block, 'y_unit_wf', None) # number of spikes in the entire episode spk_blocks = [k for k in self.blocks if k.identifier == 'RSPK'] n_events = np.sum([k.n_events[electrode_id - 1] for k in spk_blocks], dtype=int) # number of samples in a waveform wf_sampling_frequency = 1.0 / block.ep_block.dX wf_blocks = [k for k in self.blocks if k.identifier == 'RspkWave'] if wf_blocks : wf_samples = wf_blocks[0].wavelength t_start = wf_blocks[0].pre_trigger * block.ep_block.dX else: wf_samples = 0 t_start = 0 return ElphySpikeTrain(self, episode, electrode_id, x_unit, n_events, wf_sampling_frequency, wf_samples, x_unit_wf, y_unit_wf, t_start) def get_spiketrain_data(self, episode, electrode_id): """ Return times contained in the specified spike channel. This function is triggered when the 'times' property of an :class:`Spike` descriptor instance is accessed. NB : The 'RSPK' block is not actually identical to the 'EVT' one, because all units relative to a time are stored directly after all event times, 1 byte for each. This function doesn't return these units. But, they could be retrieved from the 'RspkWave' block with the 'get_waveform_data function' """ block = self.episode_block(episode) times = self.load_encoded_spikes(episode, electrode_id, "RSPK") return times * block.ep_block.dX def load_encoded_waveforms(self, episode, electrode_id): """ Return times on which waveforms are defined and a numpy recarray containing all the data stored in the RspkWave block. """ # load data corresponding to the RspkWave block identifier = "RspkWave" data_blocks = self.group_blocks_of_type(episode, identifier) databytes = self.load_bytes(data_blocks) # select only data corresponding # to the specified spk_channel ep_blocks = self.get_blocks_stored_in_episode(episode) wf_blocks = [k for k in ep_blocks if k.identifier == identifier] wf_samples = wf_blocks[0].wavelength events = np.sum([k.n_spikes for k in wf_blocks], dtype=int, axis=0) n_events = events[electrode_id - 1] pre_events = np.sum(events[0:electrode_id - 1], dtype=int) start = pre_events end = start + n_events # data must be reshaped before dtype = [ # the time of the spike arrival ('elphy_time', 'u4', (1,)), ('device_time', 'u4', (1,)), # the identifier of the electrode # would also be the 'trodalness' # but this tetrode devices are not # implemented in Elphy ('channel_id', 'u2', (1,)), # the 'category' of the waveform ('unit_id', 'u1', (1,)), #do not used ('dummy', 'u1', (13,)), # samples of the waveform ('waveform', 'i2', (wf_samples,)) ] x_start = wf_blocks[0].pre_trigger x_stop = wf_samples - x_start return np.arange(-x_start, x_stop), np.frombuffer(databytes, dtype=dtype)[start:end] def get_waveform_data(self, episode, electrode_id): """ Return waveforms corresponding to the specified spike channel. This function is triggered when the ``waveforms`` property of an :class:`Spike` descriptor instance is accessed. """ block = self.episode_block(episode) times, databytes = self.load_encoded_waveforms(episode, electrode_id) n_events, = databytes.shape wf_samples = databytes['waveform'].shape[1] dtype = [ ('time', float), ('electrode_id', int), ('unit_id', int), ('waveform', float, (wf_samples, 2)) ] data = np.empty(n_events, dtype=dtype) data['electrode_id'] = databytes['channel_id'][:, 0] data['unit_id'] = databytes['unit_id'][:, 0] data['time'] = databytes['elphy_time'][:, 0] * block.ep_block.dX data['waveform'][:, :, 0] = times * block.ep_block.dX data['waveform'][:, :, 1] = databytes['waveform'] * block.ep_block.dY_wf + block.ep_block.Y0_wf return data def get_rspk_data(self, spk_channel): """ Return times stored as a 4-bytes integer in the specified event channel. """ evt_blocks = self.get_blocks_of_type('RSPK') #compute events on each channel n_events = np.sum([k.n_events for k in evt_blocks], dtype=int, axis=0) pre_events = np.sum(n_events[0:spk_channel], dtype=int) # sum of array values up to spk_channel-1!!!! start = pre_events + (7 + len(n_events))# rspk header end = start + n_events[spk_channel] expected_size = 4 * np.sum(n_events, dtype=int) # constant return self.load_bytes(evt_blocks, dtype='= layout.data_offset)) : block = self.factory.create_block(layout, offset) # create the sub blocks if it is DAC2 objects format # this is only done for B_Ep and B_Finfo blocks for # DAC2 objects format, maybe it could be useful to # spread this to other block types. #if isinstance(header, DAC2Header) and (block.identifier in ['B_Ep']) : if isinstance(header, DAC2Header) and (block.identifier in ['B_Ep', 'B_Finfo']) : sub_offset = block.data_offset while sub_offset < block.start + block.size : sub_block = self.factory.create_sub_block(block, sub_offset) block.add_sub_block(sub_block) sub_offset += sub_block.size # set up some properties of some DAC2Layout sub-blocks if isinstance(sub_block, (DAC2EpSubBlock, DAC2AdcSubBlock, DAC2KSampSubBlock, DAC2KTypeSubBlock)) : block.set_episode_block() block.set_channel_block() block.set_sub_sampling_block() block.set_sample_size_block() # SpikeTrain #if isinstance(header, DAC2Header) and (block.identifier in ['RSPK']) : #print "\nElphyFile.create_layout() - RSPK" #print "ElphyFile.create_layout() - n_events",block.n_events #print "ElphyFile.create_layout() - n_evt_channels",block.n_evt_channels layout.add_block(block) offset += block.size # set up as soon as possible the shortcut # to the main block of a DAC2GSLayout if not detect_main and isinstance(layout, DAC2GSLayout) and isinstance(block, DAC2GSMainBlock) : layout.set_main_block() detect_main = True # detect if the file is continuous when # the 'MAIN' block has been parsed if not detect_continuous : is_continuous = isinstance(header, DAC2GSHeader) and layout.is_continuous() # set up the shortcut to blocks corresponding # to episodes, only available for DAC2Layout # and also DAC2GSLayout if not continuous if isinstance(layout, DAC2Layout) or (isinstance(layout, DAC2GSLayout) and not layout.is_continuous()) : layout.set_episode_blocks() layout.set_data_blocks() # finally set up the user info block of the layout layout.set_info_block() self.file.seek(0) return layout def is_continuous(self): return self.layout.is_continuous() @property def n_episodes(self): """ Return the number of recording sequences. """ return self.layout.n_episodes def n_channels(self, episode): """ Return the number of recording channels involved in data acquisition and relative to the specified episode : ``episode`` : the recording sequence identifier. """ return self.layout.n_channels(episode) def n_tags(self, episode): """ Return the number of tag channels relative to the specified episode : ``episode`` : the recording sequence identifier. """ return self.layout.n_tags(episode) def n_events(self, episode): """ Return the number of event channels relative to the specified episode : ``episode`` : the recording sequence identifier. """ return self.layout.n_events(episode) def n_spiketrains(self, episode): """ Return the number of event channels relative to the specified episode : ``episode`` : the recording sequence identifier. """ return self.layout.n_spiketrains(episode) def n_waveforms(self, episode): """ Return the number of waveform channels : """ return self.layout.n_waveforms(episode) def get_signal(self, episode, channel): """ Return the signal or event descriptor relative to the specified episode and channel : ``episode`` : the recording sequence identifier. ``channel`` : the analog channel identifier. NB : For 'DAC2 objects' format, it could be also used to retrieve events. """ return self.layout.get_signal(episode, channel) def get_tag(self, episode, tag_channel): """ Return the tag descriptor relative to the specified episode and tag channel : ``episode`` : the recording sequence identifier. ``tag_channel`` : the tag channel identifier. NB : There isn't any tag channels for 'Acquis1' format. ElphyTag channels appeared after 'DAC2/GS/2000' release. They are also present in 'DAC2 objects' format. """ return self.layout.get_tag(episode, tag_channel) def get_event(self, episode, evt_channel): """ Return the event relative the specified episode and event channel. `episode`` : the recording sequence identifier. ``tag_channel`` : the tag channel identifier. """ return self.layout.get_event(episode, evt_channel) def get_spiketrain(self, episode, electrode_id): """ Return the spiketrain relative to the specified episode and electrode_id. ``episode`` : the recording sequence identifier. ``electrode_id`` : the identifier of the electrode providing the spiketrain. NB : Available only for 'DAC2 objects' format. This descriptor can return the times of a spiketrain and waveforms relative to each of these times. """ return self.layout.get_spiketrain(episode, electrode_id) @property def comments(self): raise NotImplementedError() def get_user_file_info(self): """ Return user defined file metadata. """ if not self.layout.info_block : return dict() else : return self.layout.info_block.get_user_file_info() @property def episode_info(self, ep_number): raise NotImplementedError() def get_signals(self): """ Get all available analog or event channels stored into an Elphy file. """ signals = list() for ep in range(1, self.n_episodes + 1) : for ch in range(1, self.n_channels(ep) + 1) : signal = self.get_signal(ep, ch) signals.append(signal) return signals def get_tags(self): """ Get all available tag channels stored into an Elphy file. """ tags = list() for ep in range(1, self.n_episodes + 1) : for tg in range(1, self.n_tags(ep) + 1) : tag = self.get_tag(ep, tg) tags.append(tag) return tags def get_spiketrains(self): """ Get all available spiketrains stored into an Elphy file. """ spiketrains = list() for ep in range(1, self.n_episodes + 1) : for ch in range(1, self.n_spiketrains(ep) + 1) : spiketrain = self.get_spiketrain(ep, ch) spiketrains.append(spiketrain) return spiketrains def get_rspk_spiketrains(self): """ Get all available spiketrains stored into an Elphy file. """ spiketrains = list() spk_blocks = self.layout.get_blocks_of_type('RSPK') for bl in spk_blocks : #print "ElphyFile.get_spiketrains() - identifier:",bl.identifier for ch in range(0,bl.n_evt_channels) : spiketrain = self.layout.get_rspk_data(ch) spiketrains.append(spiketrain) return spiketrains def get_names( self ) : com_blocks = list() com_blocks = self.layout.get_blocks_of_type('COM') return com_blocks # -------------------------------------------------------- class ElphyIO(BaseIO): """ Class for reading from and writing to an Elphy file. It enables reading: - :class:`Block` - :class:`Segment` - :class:`RecordingChannel` - :class:`RecordingChannelGroup` - :class:`EventArray` - :class:`SpikeTrain` Usage: >>> from neo import io >>> r = io.ElphyIO(filename='ElphyExample.DAT') >>> seg = r.read_block(lazy=False, cascade=True) >>> print(seg.analogsignals) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE >>> print(seg.spiketrains) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE >>> print(seg.eventarrays) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE >>> print(anasig._data_description) >>> anasig = r.read_analogsignal(lazy=False, cascade=False) >>> bl = Block() >>> # creating segments, their contents and append to bl >>> r.write_block( bl ) """ is_readable = True # This class can read data is_writable = False # This class can write data # This class is able to directly or indirectly handle the following objects supported_objects = [ Block, Segment, AnalogSignalArray, SpikeTrain ] #, AnalogSignal # This class can return a Block readable_objects = [ Block ] # This class is not able to write objects writeable_objects = [ ] has_header = False is_streameable = False # This is for GUI stuff : a definition for parameters when reading. # This dict should be keyed by object (`Block`). Each entry is a list # of tuple. The first entry in each tuple is the parameter name. The # second entry is a dict with keys 'value' (for default value), # and 'label' (for a descriptive name). # Note that if the highest-level object requires parameters, # common_io_test will be skipped. read_params = { } # do not supported write so no GUI stuff write_params = { } name = 'Elphy IO' extensions = [ 'DAT' ] # mode can be 'file' or 'dir' or 'fake' or 'database' mode = 'file' # internal serialized representation of neo data serialized = None def __init__(self , filename = None) : """ Arguments: filename : the filename to read """ BaseIO.__init__(self) self.filename = filename self.elphy_file = ElphyFile(self.filename) def read_block(self, # the 2 first key arguments are imposed by neo.io API lazy = False, cascade = True ): """ Return :class:`Block` filled or not depending on 'cascade' parameter. Parameters: lazy : postpone actual reading of the file. cascade : normally you want this True, otherwise method will only ready Block label. """ # basic block = Block(name=None) # laziness if lazy: return block else: # get analog and tag channels try : self.elphy_file.open() except Exception as e: self.elphy_file.close() raise Exception("cannot open file %s : %s" % (self.filename, e)) # cascading #print "\n\n==========================================\n" #print "read_block() - n_episodes:",self.elphy_file.n_episodes if cascade: # create a segment containing all analog, # tag and event channels for the episode if self.elphy_file.n_episodes == None : print("File '%s' appears to have no episodes" % (self.filename)) return block for episode in range(1, self.elphy_file.n_episodes+1) : segment = self.read_segment(episode) segment.block = block block.segments.append(segment) # close file self.elphy_file.close() # result return block def write_block( self, block ): """ Write a given Neo Block to an Elphy file, its structure being, for example: Neo -> Elphy -------------------------------------------------------------- Block File Segment Episode Block (B_Ep) AnalogSignalArray Episode Descriptor (Ep + Adc + Ksamp + Ktype) multichannel RDATA (with a ChannelMask multiplexing channels) 2D NumPy Array ... AnalogSignalArray AnalogSignal AnalogSignal ... ... SpikeTrain Event Block (RSPK) SpikeTrain ... Arguments:: block: the block to be saved """ # Serialize Neo structure into Elphy file # each analog signal will be serialized as elphy Episode Block (with its subblocks) # then all spiketrains will be serialized into an Rspk Block (an Event Block with addons). # Serialize (and size) all Neo structures before writing them to file # Since to write each Elphy Block is required to know in advance its size, # which includes that of its subblocks, it is necessary to # serialize first the lowest structures. # Iterate over block structures elphy_limit = 256 All = '' #print "\n\n--------------------------------------------\n" #print "write_block() - n_segments:",len(block.segments) for seg in block.segments: analogsignals = 0 # init nbchan = 0 nbpt = 0 chls = 0 Dxu = 1e-8 #0.0000001 Rxu = 1e+8 #10000000.0 X0uSpk = 0.0 CyberTime = 0.0 aa_units = [] NbEv = [] serialized_analog_data = '' serialized_spike_data = '' # AnalogSignals # Neo signalarrays are 2D numpy array where each row is an array of samples for a channel: # signalarray A = [[ 1, 2, 3, 4 ], # [ 5, 6, 7, 8 ]] # signalarray B = [[ 9, 10, 11, 12 ], # [ 13, 14, 15, 16 ]] # Neo Segments can have more than one signalarray. # To be converted in Elphy analog channels they need to be all in a 2D array, not in several 2D arrays. # Concatenate all analogsignalarrays into one and then flatten it. # Elphy RDATA blocks contain Fortran styled samples: # 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15, 4, 8, 12, 16 # AnalogSignalArrays -> analogsignals # get the first to have analogsignals with the right shape # Annotations for analogsignals array come as a list of int being source ids # here, put each source id on a separate dict entry in order to have a matching afterwards idx = 0 annotations = dict( ) # get all the others #print "write_block() - n_analogsignals:",len(seg.analogsignals) #print "write_block() - n_analogsignalarrays:",len(seg.analogsignalarrays) for asigar in seg.analogsignalarrays : idx,annotations = self.get_annotations_dict( annotations, "analogsignal", asigar.annotations.items(), asigar.name, idx ) # array structure _,chls = asigar.shape # units for _ in range(chls) : aa_units.append( asigar.units ) Dxu = asigar.sampling_period Rxu = asigar.sampling_rate if isinstance(analogsignals, np.ndarray) : analogsignals = np.hstack( (analogsignals,asigar) ) else : analogsignals = asigar # first time # collect and reshape all analogsignals if isinstance(analogsignals, np.ndarray) : # transpose matrix since in Neo channels are column-wise while in Elphy are row-wise analogsignals = analogsignals.T # get dimensions nbchan,nbpt = analogsignals.shape # serialize AnalogSignal analog_data_fmt = '<' + str(analogsignals.size) + 'f' # serialized flattened numpy channels in 'F'ortran style analog_data_64 = analogsignals.flatten('F') # elphy normally uses float32 values (for performance reasons) analog_data = np.array( analog_data_64, dtype=np.float32 ) serialized_analog_data += struct.pack( analog_data_fmt, *analog_data ) # SpikeTrains # Neo spiketrains are stored as a one-dimensional array of times # [ 0.11, 1.23, 2.34, 3.45, 4.56, 5.67, 6.78, 7.89 ... ] # These are converted into Elphy Rspk Block which will contain all of them # RDATA + NbVeV:integer for the number of channels (spiketrains) # + NbEv:integer[] for the number of event per channel # followed by the actual arrays of integer containing spike times #spiketrains = seg.spiketrains # ... but consider elphy loading limitation: NbVeV = len( seg.spiketrains ) #print "write_block() - n_spiketrains:",NbVeV if len(seg.spiketrains) > elphy_limit : NbVeV = elphy_limit # serialize format spiketrain_data_fmt = '<' spiketrains = [] for idx,train in enumerate(seg.spiketrains[:NbVeV]) : #print "write_block() - train.size:", train.size,idx #print "write_block() - train:", train fake,annotations = self.get_annotations_dict( annotations,"spiketrain", train.annotations.items(), '', idx ) #annotations.update( dict( [("spiketrain-"+str(idx),train.annotations['source_id'])] ) ) #print "write_block() - train[%s].annotation['source_id']:%s" % (idx,train.annotations['source_id']) # total number of events format + blackrock sorting mark (0 for neo) spiketrain_data_fmt += str(train.size) + "i" + str(train.size) + "B" # get starting time X0uSpk = train.t_start.item() CyberTime = train.t_stop.item() # count number of events per train NbEv.append( train.size ) # multiply by sampling period train = train * Rxu # all flattened spike train # blackrock acquisition card also adds a byte for each event to sort it spiketrains.extend( [spike.item() for spike in train] + [0 for _ in range(train.size)]) # Annotations #print annotations # using DBrecord elphy block, they will be available as values in elphy environment # separate keys and values in two separate serialized strings ST_sub = '' st_fmt = '' st_data = [] BUF_sub = '' serialized_ST_data = '' serialized_BUF_data = '' for key in sorted(annotations.iterkeys()) : # take all values, get their type and concatenate fmt = '' data = [] value = annotations[key] if isinstance( value, (int,np.int32,np.int64) ) : # elphy type 2 fmt = '0 else "episode %s" % str(episode + 1) segment = Segment( name=name ) # create an analog signal for # each channel in the episode for channel in range(1, self.elphy_file.n_channels(episode)+1) : signal = self.elphy_file.get_signal(episode, channel) analog_signal = AnalogSignal( signal.data['y'], units = signal.y_unit, t_start = signal.t_start * getattr(pq, signal.x_unit.strip()), t_stop = signal.t_stop * getattr(pq, signal.x_unit.strip()), #sampling_rate = signal.sampling_frequency * pq.kHz, sampling_period = signal.sampling_period * getattr(pq, signal.x_unit.strip()), channel_name="episode %s, channel %s" % ( int(episode+1), int(channel+1) ) ) analog_signal.segment = segment create_many_to_one_relationship( analog_signal ) segment.analogsignals.append(analog_signal) # create a spiketrain for each # spike channel in the episode # in case of multi-electrode # acquisition context n_spikes = self.elphy_file.n_spiketrains(episode) #print "read_segment() - n_spikes:",n_spikes if n_spikes>0 : for spk in range(1, n_spikes+1) : spiketrain = self.read_spiketrain(episode, spk) spiketrain.segment = segment create_many_to_one_relationship( spiketrain ) segment.spiketrains.append( spiketrain ) # segment return segment def read_recordingchannelgroup( self, episode ): """ Internal method used to return :class:`RecordingChannelGroup` info. Parameters: elphy_file : is the elphy object. episode : number of elphy episode, roughly corresponding to a segment """ n_spikes = self.elphy_file.n_spikes group = RecordingChannelGroup( name="episode %s, group of %s electrodes" % (episode, n_spikes) ) for spk in range(0, n_spikes) : channel = self.read_recordingchannel(episode, spk) group.recordingchannels.append(channel) return group def read_recordingchannel( self, episode, chl ): """ Internal method used to return a :class:`RecordingChannel` label. Parameters: elphy_file : is the elphy object. episode : number of elphy episode, roughly corresponding to a segment. chl : electrode number. """ channel = RecordingChannel( name="episode %s, electrodes %s" % (episode, chl) ) return channel def read_eventarray( self, episode, evt ): """ Internal method used to return a list of elphy :class:`EventArray` acquired from event channels. Parameters: elphy_file : is the elphy object. episode : number of elphy episode, roughly corresponding to a segment. evt : index of the event. """ event = self.elphy_file.get_event(episode, evt) event_array = EventArray( times=event.times * pq.s, channel_name="episode %s, event channel %s" % (episode + 1, evt + 1) ) return event_array def read_spiketrain( self, episode, spk ): """ Internal method used to return an elphy object :class:`SpikeTrain`. Parameters: elphy_file : is the elphy object. episode : number of elphy episode, roughly corresponding to a segment. spk : index of the spike array. """ block = self.elphy_file.layout.episode_block(episode) spike = self.elphy_file.get_spiketrain(episode, spk) spikes = spike.times * pq.s #print "read_spiketrain() - spikes: %s" % (len(spikes)) #print "read_spiketrain() - spikes:",spikes dct = { 'times':spikes, 't_start': block.ep_block.X0_wf if block.ep_block.X0_wf < spikes[0] else spikes[0], #check 't_stop': block.ep_block.cyber_time if block.ep_block.cyber_time > spikes[-1] else spikes[-1], 'units':'s', # special keywords to identify the # electrode providing the spiketrain # event though it is redundant with # waveforms 'label':"episode %s, electrode %s" % (episode, spk), 'electrode_id':spk } # new spiketrain return SpikeTrain(**dct) neo-0.3.3/neo/io/brainwaredamio.py0000644000175000017500000002242612273723542020105 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' Class for reading from Brainware DAM files DAM files are binary files for holding raw data. They are broken up into sequence of Segments, each containing a single raw trace and parameters. The DAM file does NOT contain a sampling rate, nor can it be reliably calculated from any of the parameters. You can calculate it from the "sweep length" attribute if it is present, but it isn't always present. It is more reliable to get it from the corresponding SRC file or F32 file if you have one. The DAM file also does not divide up data into Blocks, so only a single Block is returned.. Brainware was developed by Dr. Jan Schnupp and is availabe from Tucker Davis Technologies, Inc. http://www.tdt.com/downloads.htm Neither Dr. Jan Schnupp nor Tucker Davis Technologies, Inc. had any part in the development of this code The code is implemented with the permission of Dr. Jan Schnupp Author: Todd Jennings ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function # import needed core python modules import os import os.path # numpy and quantities are already required by neo import numpy as np import quantities as pq # needed core neo modules from neo.core import (AnalogSignal, Block, RecordingChannel, RecordingChannelGroup, Segment) # need to subclass BaseI from neo.io.baseio import BaseIO # some tools to finalize the hierachy from neo.io.tools import create_many_to_one_relationship class BrainwareDamIO(BaseIO): """ Class for reading Brainware raw data files with the extension '.dam'. The read_block method returns the first Block of the file. It will automatically close the file after reading. The read method is the same as read_block. Note: The file format does not contain a sampling rate. The sampling rate is set to 1 Hz, but this is arbitrary. If you have a corresponding .src or .f32 file, you can get the sampling rate from that. It may also be possible to infer it from the attributes, such as "sweep length", if present. Usage: >>> from neo.io.brainwaredamio import BrainwareDamIO >>> damfile = BrainwareDamIO(filename='multi_500ms_mulitrep_ch1.dam') >>> blk1 = damfile.read() >>> blk2 = damfile.read_block() >>> print blk1.segments >>> print blk1.segments[0].analogsignals >>> print blk1.units >>> print blk1.units[0].name >>> print blk2 >>> print blk2[0].segments """ is_readable = True # This class can only read data is_writable = False # write is not supported # 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, RecordingChannelGroup, RecordingChannel, Segment, AnalogSignal] readable_objects = [Block] writeable_objects = [] has_header = False is_streameable = False # This is for GUI stuff: a definition for parameters when reading. # This dict should be keyed by object (`Block`). Each entry is a list # of tuple. The first entry in each tuple is the parameter name. The # second entry is a dict with keys 'value' (for default value), # and 'label' (for a descriptive name). # Note that if the highest-level object requires parameters, # common_io_test will be skipped. read_params = {Block: [], RecordingChannelGroup: [], RecordingChannel: [], Segment: [], AnalogSignal: [], } # do not support write so no GUI stuff write_params = None name = 'Brainware DAM File' extensions = ['dam'] mode = 'file' def __init__(self, filename=None): ''' Arguments: filename: the filename ''' BaseIO.__init__(self) self._path = filename self._filename = os.path.basename(filename) self._fsrc = None def read(self, lazy=False, cascade=True, **kargs): ''' Reads raw data file "fname" generated with BrainWare ''' return self.read_block(lazy=lazy, cascade=cascade) def read_block(self, lazy=False, cascade=True, **kargs): ''' Reads a block from the raw data file "fname" generated with BrainWare ''' # there are no keyargs implemented to so far. If someone tries to pass # them they are expecting them to do something or making a mistake, # neither of which should pass silently if kargs: raise NotImplementedError('This method does not have any ' 'argument implemented yet') self._fsrc = None block = Block(file_origin=self._filename) # if we aren't doing cascade, don't load anything if not cascade: return block # create the objects to store other objects rcg = RecordingChannelGroup(file_origin=self._filename) rchan = RecordingChannel(file_origin=self._filename, index=1, name='Chan1') # load objects into their containers rcg.recordingchannels.append(rchan) block.recordingchannelgroups.append(rcg) rcg.channel_indexes = np.array([1]) rcg.channel_names = np.array(['Chan1'], dtype='S') # open the file with open(self._path, 'rb') as fobject: # while the file is not done keep reading segments while True: seg = self._read_segment(fobject, lazy) # if there are no more Segments, stop if not seg: break # store the segment and signals block.segments.append(seg) rchan.analogsignals.append(seg.analogsignals[0]) # remove the file object self._fsrc = None create_many_to_one_relationship(block) return block # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- # IMPORTANT!!! # These are private methods implementing the internal reading mechanism. # Due to the way BrainWare DAM files are structured, they CANNOT be used # on their own. Calling these manually will almost certainly alter your # position in the file in an unrecoverable manner, whether they throw # an exception or not. # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- def _read_segment(self, fobject, lazy): ''' Read a single segment with a single analogsignal Returns the segment or None if there are no more segments ''' try: # float64 -- start time of the AnalogSignal t_start = np.fromfile(fobject, dtype=np.float64, count=1)[0] except IndexError: # if there are no more Segments, return return False # int16 -- index of the stimulus parameters seg_index = np.fromfile(fobject, dtype=np.int16, count=1)[0].tolist() # int16 -- number of stimulus parameters numelements = np.fromfile(fobject, dtype=np.int16, count=1)[0] # read the name strings for the stimulus parameters paramnames = [] for _ in range(numelements): # unit8 -- the number of characters in the string numchars = np.fromfile(fobject, dtype=np.uint8, count=1)[0] # char * numchars -- a single name string name = np.fromfile(fobject, dtype=np.uint8, count=numchars) # exclude invalid characters name = str(name[name >= 32].view('c').tostring()) # add the name to the list of names paramnames.append(name) # float32 * numelements -- the values for the stimulus parameters paramvalues = np.fromfile(fobject, dtype=np.float32, count=numelements) # combine parameter names and the parameters as a dict params = dict(zip(paramnames, paramvalues)) # int32 -- the number elements in the AnalogSignal numpts = np.fromfile(fobject, dtype=np.int32, count=1)[0] # int16 * numpts -- the AnalogSignal itself signal = np.fromfile(fobject, dtype=np.int16, count=numpts) # handle lazy loading if lazy: sig = AnalogSignal([], t_start=t_start*pq.d, file_origin=self._filename, sampling_period=1.*pq.s, units=pq.mV, dtype=np.float) sig.lazy_shape = len(signal) else: sig = AnalogSignal(signal.astype(np.float)*pq.mV, t_start=t_start*pq.d, file_origin=self._filename, sampling_period=1.*pq.s, copy=False) # Note: setting the sampling_period to 1 s is arbitrary # load the AnalogSignal and parameters into a new Segment seg = Segment(file_origin=self._filename, index=seg_index, **params) seg.analogsignals = [sig] return seg neo-0.3.3/neo/io/neuroshareio.py0000644000175000017500000004113512273723542017622 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ NeuroshareIO is a wrap with ctypes of neuroshare DLLs. Neuroshare is a C API for reading neural data. Neuroshare also provides a Matlab and a Python API on top of that. Neuroshare is an open source API but each dll is provided directly by the vendor. The neo user have to download separtatly the dll on neurosharewebsite: http://neuroshare.sourceforge.net/ For some vendors (Spike2/CED , Clampfit/Abf, ...), neo.io also provides pure Python Neo users you should prefer them of course :) Supported : Read Author: sgarcia """ import ctypes import os # file no longer exists in Python3 try: file except NameError: import io file = io.BufferedReader import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import Segment, AnalogSignal, SpikeTrain, EventArray from neo.io.tools import create_many_to_one_relationship class NeuroshareIO(BaseIO): """ Class for reading file trougth neuroshare API. The user need the DLLs in the path of the file format. Usage: >>> from neo import io >>> r = io.NeuroshareIO(filename='a_file', dllname=the_name_of_dll) >>> seg = r.read_segment(lazy=False, cascade=True, import_neuroshare_segment=True) >>> print seg.analogsignals # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [>> print seg.spiketrains [] >>> print seg.eventarrays [] Note: neuroshare.ns_ENTITY_EVENT: are converted to neo.EventArray neuroshare.ns_ENTITY_ANALOG: are converted to neo.AnalogSignal neuroshare.ns_ENTITY_NEURALEVENT: are converted to neo.SpikeTrain neuroshare.ns_ENTITY_SEGMENT: is something between serie of small AnalogSignal and Spiketrain with associated waveforms. It is arbitrarily converted as SpikeTrain. """ is_readable = True is_writable = False supported_objects = [Segment , AnalogSignal, EventArray, SpikeTrain ] readable_objects = [Segment] writeable_objects = [ ] has_header = False is_streameable = False read_params = { Segment : [] } write_params = None name = 'neuroshare' extensions = [ ] mode = 'file' def __init__(self , filename = '', dllname = '') : """ Arguments: filename: the file to read ddlname: the name of neuroshare dll to be used for this file """ self.dllname = dllname self.filename = filename BaseIO.__init__(self) def read_segment(self, import_neuroshare_segment = True, lazy=False, cascade=True): """ Arguments: import_neuroshare_segment: import neuroshare segment as SpikeTrain with associated waveforms or not imported at all. """ seg = Segment( file_origin = os.path.basename(self.filename), ) neuroshare = ctypes.windll.LoadLibrary(self.dllname) # API version info = ns_LIBRARYINFO() neuroshare.ns_GetLibraryInfo(ctypes.byref(info) , ctypes.sizeof(info)) seg.annotate(neuroshare_version = str(info.dwAPIVersionMaj)+'.'+str(info.dwAPIVersionMin)) if not cascade: return seg # open file hFile = ctypes.c_uint32(0) neuroshare.ns_OpenFile(ctypes.c_char_p(self.filename) ,ctypes.byref(hFile)) fileinfo = ns_FILEINFO() neuroshare.ns_GetFileInfo(hFile, ctypes.byref(fileinfo) , ctypes.sizeof(fileinfo)) # read all entities for dwEntityID in range(fileinfo.dwEntityCount): entityInfo = ns_ENTITYINFO() neuroshare.ns_GetEntityInfo( hFile, dwEntityID, ctypes.byref(entityInfo), ctypes.sizeof(entityInfo)) #~ print 'type', entityInfo.dwEntityType,entity_types[entityInfo.dwEntityType], 'count', entityInfo.dwItemCount #~ print entityInfo.szEntityLabel # EVENT if entity_types[entityInfo.dwEntityType] == 'ns_ENTITY_EVENT': pEventInfo = ns_EVENTINFO() neuroshare.ns_GetEventInfo ( hFile, dwEntityID, ctypes.byref(pEventInfo), ctypes.sizeof(pEventInfo)) #~ print pEventInfo.szCSVDesc, pEventInfo.dwEventType, pEventInfo.dwMinDataLength, pEventInfo.dwMaxDataLength if pEventInfo.dwEventType == 0: #TEXT pData = ctypes.create_string_buffer(pEventInfo.dwMaxDataLength) elif pEventInfo.dwEventType == 1:#CVS pData = ctypes.create_string_buffer(pEventInfo.dwMaxDataLength) elif pEventInfo.dwEventType == 2:# 8bit pData = ctypes.c_byte(0) elif pEventInfo.dwEventType == 3:# 16bit pData = ctypes.c_int16(0) elif pEventInfo.dwEventType == 4:# 32bit pData = ctypes.c_int32(0) pdTimeStamp = ctypes.c_double(0.) pdwDataRetSize = ctypes.c_uint32(0) ea = EventArray(name = str(entityInfo.szEntityLabel),) if not lazy: times = [ ] labels = [ ] for dwIndex in range(entityInfo.dwItemCount ): neuroshare.ns_GetEventData ( hFile, dwEntityID, dwIndex, ctypes.byref(pdTimeStamp), ctypes.byref(pData), ctypes.sizeof(pData), ctypes.byref(pdwDataRetSize) ) times.append(pdTimeStamp.value) labels.append(str(pData)) ea.times = times*pq.s ea.labels = np.array(labels, dtype ='S') else : ea.lazy_shape = entityInfo.dwItemCount seg.eventarrays.append(ea) # analog if entity_types[entityInfo.dwEntityType] == 'ns_ENTITY_ANALOG': pAnalogInfo = ns_ANALOGINFO() neuroshare.ns_GetAnalogInfo( hFile, dwEntityID,ctypes.byref(pAnalogInfo),ctypes.sizeof(pAnalogInfo) ) #~ print 'dSampleRate' , pAnalogInfo.dSampleRate , pAnalogInfo.szUnits dwStartIndex = ctypes.c_uint32(0) dwIndexCount = entityInfo.dwItemCount if lazy: signal = [ ]*pq.Quantity(1, pAnalogInfo.szUnits) else: pdwContCount = ctypes.c_uint32(0) pData = np.zeros( (entityInfo.dwItemCount,), dtype = 'f8') neuroshare.ns_GetAnalogData ( hFile, dwEntityID, dwStartIndex, dwIndexCount, ctypes.byref( pdwContCount) , pData.ctypes.data_as(ctypes.POINTER(ctypes.c_double))) pszMsgBuffer = ctypes.create_string_buffer(" "*256) neuroshare.ns_GetLastErrorMsg(ctypes.byref(pszMsgBuffer), 256) #~ print 'pszMsgBuffer' , pszMsgBuffer.value signal = pData[:pdwContCount.value]*pq.Quantity(1, pAnalogInfo.szUnits) #t_start dwIndex = 0 pdTime = ctypes.c_double(0) neuroshare.ns_GetTimeByIndex( hFile, dwEntityID, dwIndex, ctypes.byref(pdTime)) anaSig = AnalogSignal(signal, sampling_rate = pAnalogInfo.dSampleRate*pq.Hz, t_start = pdTime.value * pq.s, name = str(entityInfo.szEntityLabel), ) if lazy: anaSig.lazy_shape = entityInfo.dwItemCount seg.analogsignals.append( anaSig ) #segment if entity_types[entityInfo.dwEntityType] == 'ns_ENTITY_SEGMENT' and import_neuroshare_segment: pdwSegmentInfo = ns_SEGMENTINFO() neuroshare.ns_GetSegmentInfo( hFile, dwEntityID, ctypes.byref(pdwSegmentInfo), ctypes.sizeof(pdwSegmentInfo) ) nsource = pdwSegmentInfo.dwSourceCount pszMsgBuffer = ctypes.create_string_buffer(" "*256) neuroshare.ns_GetLastErrorMsg(ctypes.byref(pszMsgBuffer), 256) #~ print 'pszMsgBuffer' , pszMsgBuffer.value #~ print 'pdwSegmentInfo.dwSourceCount' , pdwSegmentInfo.dwSourceCount for dwSourceID in range(pdwSegmentInfo.dwSourceCount) : pSourceInfo = ns_SEGSOURCEINFO() neuroshare.ns_GetSegmentSourceInfo( hFile, dwEntityID, dwSourceID, ctypes.byref(pSourceInfo), ctypes.sizeof(pSourceInfo) ) if lazy: sptr = SpikeTrain(times, name = str(entityInfo.szEntityLabel)) sptr.lazy_shape = entityInfo.dwItemCount else: pdTimeStamp = ctypes.c_double(0.) dwDataBufferSize = pdwSegmentInfo.dwMaxSampleCount*pdwSegmentInfo.dwSourceCount pData = np.zeros( (dwDataBufferSize), dtype = 'f8') pdwSampleCount = ctypes.c_uint32(0) pdwUnitID= ctypes.c_uint32(0) nsample = pdwSampleCount.value times = np.empty( (entityInfo.dwItemCount), drtype = 'f') waveforms = np.empty( (entityInfo.dwItemCount, nsource, nsample), drtype = 'f') for dwIndex in range(entityInfo.dwItemCount ): neuroshare.ns_GetSegmentData ( hFile, dwEntityID, dwIndex, ctypes.byref(pdTimeStamp), pData.ctypes.data_as(ctypes.POINTER(ctypes.c_double)), dwDataBufferSize * 8, ctypes.byref(pdwSampleCount), ctypes.byref(pdwUnitID ) ) #print 'dwDataBufferSize' , dwDataBufferSize,pdwSampleCount , pdwUnitID times[dwIndex] = pdTimeStamp.value waveforms[dwIndex, :,:] = pData[:nsample*nsource].reshape(nsample ,nsource).transpose() sptr = SpikeTrain(times*pq.s, waveforms = waveforms*pq.Quantity(1., str(pdwSegmentInfo.szUnits) ), left_sweep = nsample/2./float(pdwSegmentInfo.dSampleRate)*pq.s, sampling_rate = float(pdwSegmentInfo.dSampleRate)*pq.Hz, name = str(entityInfo.szEntityLabel), ) seg.spiketrains.append(sptr) # neuralevent if entity_types[entityInfo.dwEntityType] == 'ns_ENTITY_NEURALEVENT': pNeuralInfo = ns_NEURALINFO() neuroshare.ns_GetNeuralInfo ( hFile, dwEntityID, ctypes.byref(pNeuralInfo), ctypes.sizeof(pNeuralInfo)) #print pNeuralInfo.dwSourceUnitID , pNeuralInfo.szProbeInfo if lazy: times = [ ]*pq.s else: pData = np.zeros( (entityInfo.dwItemCount,), dtype = 'f8') dwStartIndex = 0 dwIndexCount = entityInfo.dwItemCount neuroshare.ns_GetNeuralData( hFile, dwEntityID, dwStartIndex, dwIndexCount, pData.ctypes.data_as(ctypes.POINTER(ctypes.c_double))) times = pData*pq.s sptr = SpikeTrain(times, name = str(entityInfo.szEntityLabel),) if lazy: sptr.lazy_shape = entityInfo.dwItemCount seg.spiketrains.append(sptr) # close neuroshare.ns_CloseFile(hFile) create_many_to_one_relationship(seg) return seg # neuroshare structures class ns_FILEDESC(ctypes.Structure): _fields_ = [('szDescription', ctypes.c_char*32), ('szExtension', ctypes.c_char*8), ('szMacCodes', ctypes.c_char*8), ('szMagicCode', ctypes.c_char*16), ] class ns_LIBRARYINFO(ctypes.Structure): _fields_ = [('dwLibVersionMaj', ctypes.c_uint32), ('dwLibVersionMin', ctypes.c_uint32), ('dwAPIVersionMaj', ctypes.c_uint32), ('dwAPIVersionMin', ctypes.c_uint32), ('szDescription', ctypes.c_char*64), ('szCreator',ctypes.c_char*64), ('dwTime_Year',ctypes.c_uint32), ('dwTime_Month',ctypes.c_uint32), ('dwTime_Day',ctypes.c_uint32), ('dwFlags',ctypes.c_uint32), ('dwMaxFiles',ctypes.c_uint32), ('dwFileDescCount',ctypes.c_uint32), ('FileDesc',ns_FILEDESC*16), ] class ns_FILEINFO(ctypes.Structure): _fields_ = [('szFileType', ctypes.c_char*32), ('dwEntityCount', ctypes.c_uint32), ('dTimeStampResolution', ctypes.c_double), ('dTimeSpan', ctypes.c_double), ('szAppName', ctypes.c_char*64), ('dwTime_Year',ctypes.c_uint32), ('dwTime_Month',ctypes.c_uint32), ('dwReserved',ctypes.c_uint32), ('dwTime_Day',ctypes.c_uint32), ('dwTime_Hour',ctypes.c_uint32), ('dwTime_Min',ctypes.c_uint32), ('dwTime_Sec',ctypes.c_uint32), ('dwTime_MilliSec',ctypes.c_uint32), ('szFileComment',ctypes.c_char*256), ] class ns_ENTITYINFO(ctypes.Structure): _fields_ = [('szEntityLabel', ctypes.c_char*32), ('dwEntityType',ctypes.c_uint32), ('dwItemCount',ctypes.c_uint32), ] entity_types = { 0 : 'ns_ENTITY_UNKNOWN' , 1 : 'ns_ENTITY_EVENT' , 2 : 'ns_ENTITY_ANALOG' , 3 : 'ns_ENTITY_SEGMENT' , 4 : 'ns_ENTITY_NEURALEVENT' , } class ns_EVENTINFO(ctypes.Structure): _fields_ = [ ('dwEventType',ctypes.c_uint32), ('dwMinDataLength',ctypes.c_uint32), ('dwMaxDataLength',ctypes.c_uint32), ('szCSVDesc', ctypes.c_char*128), ] class ns_ANALOGINFO(ctypes.Structure): _fields_ = [ ('dSampleRate',ctypes.c_double), ('dMinVal',ctypes.c_double), ('dMaxVal',ctypes.c_double), ('szUnits', ctypes.c_char*16), ('dResolution',ctypes.c_double), ('dLocationX',ctypes.c_double), ('dLocationY',ctypes.c_double), ('dLocationZ',ctypes.c_double), ('dLocationUser',ctypes.c_double), ('dHighFreqCorner',ctypes.c_double), ('dwHighFreqOrder',ctypes.c_uint32), ('szHighFilterType', ctypes.c_char*16), ('dLowFreqCorner',ctypes.c_double), ('dwLowFreqOrder',ctypes.c_uint32), ('szLowFilterType', ctypes.c_char*16), ('szProbeInfo', ctypes.c_char*128), ] class ns_SEGMENTINFO(ctypes.Structure): _fields_ = [ ('dwSourceCount',ctypes.c_uint32), ('dwMinSampleCount',ctypes.c_uint32), ('dwMaxSampleCount',ctypes.c_uint32), ('dSampleRate',ctypes.c_double), ('szUnits', ctypes.c_char*32), ] class ns_SEGSOURCEINFO(ctypes.Structure): _fields_ = [ ('dMinVal',ctypes.c_double), ('dMaxVal',ctypes.c_double), ('dResolution',ctypes.c_double), ('dSubSampleShift',ctypes.c_double), ('dLocationX',ctypes.c_double), ('dLocationY',ctypes.c_double), ('dLocationZ',ctypes.c_double), ('dLocationUser',ctypes.c_double), ('dHighFreqCorner',ctypes.c_double), ('dwHighFreqOrder',ctypes.c_uint32), ('szHighFilterType', ctypes.c_char*16), ('dLowFreqCorner',ctypes.c_double), ('dwLowFreqOrder',ctypes.c_uint32), ('szLowFilterType', ctypes.c_char*16), ('szProbeInfo', ctypes.c_char*128), ] class ns_NEURALINFO(ctypes.Structure): _fields_ = [ ('dwSourceEntityID',ctypes.c_uint32), ('dwSourceUnitID',ctypes.c_uint32), ('szProbeInfo',ctypes.c_char*128), ] neo-0.3.3/neo/io/axonio.py0000644000175000017500000007543312273723542016424 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Classe for reading data from pCLAMP and AxoScope files (.abf version 1 and 2), developed by Molecular device/Axon technologies. - abf = Axon binary file - atf is a text file based format from axon that could be read by AsciiIO (but this file is less efficient.) This code is a port of abfload and abf2load written in Matlab (BSD-2-Clause licence) by : - Copyright (c) 2009, Forrest Collman, fcollman@princeton.edu - Copyright (c) 2004, Harald Hentschke and available here : http://www.mathworks.com/matlabcentral/fileexchange/22114-abf2load Information on abf 1 and 2 formats is available here : http://www.moleculardevices.com/pages/software/developer_info.html This file supports the old (ABF1) and new (ABF2) format. ABF1 (clampfit <=9) and ABF2 (clampfit >10) All possible mode are possible : - event-driven variable-length mode (mode 1) -> return several Segment in the Block - event-driven fixed-length mode (mode 2 or 5) -> return several Segment in the Block - gap free mode -> return one (or sevral) Segment in the Block Supported : Read Author: sgarcia, jnowacki Note: j.s.nowacki@gmail.com has a C++ library with SWIG bindings which also reads abf files - would be good to cross-check """ import datetime import os import struct # file no longer exists in Python3 try: file except NameError: import io file = io.BufferedReader import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import Block, Segment, AnalogSignal, EventArray from neo.io.tools import create_many_to_one_relationship, iteritems class struct_file(file): def read_f(self, fmt , offset = None): if offset is not None: self.seek(offset) return struct.unpack(fmt , self.read(struct.calcsize(fmt))) def write_f(self, fmt , offset = None , *args ): if offset is not None: self.seek(offset) self.write( struct.pack( fmt , *args ) ) def reformat_integer_V1(data, nbchannel , header): """ reformat when dtype is int16 for ABF version 1 """ for i in range(nbchannel): data[:,i] /= header['fInstrumentScaleFactor'][i] data[:,i] /= header['fSignalGain'][i] data[:,i] /= header['fADCProgrammableGain'][i] if header['nTelegraphEnable'][i] : data[:,i] /= header['fTelegraphAdditGain'][i] data[:,i] *= header['fADCRange'] data[:,i] /= header['lADCResolution'] data[:,i] += header['fInstrumentOffset'][i] data[:,i] -= header['fSignalOffset'][i] def reformat_integer_V2(data, nbchannel , header): """ reformat when dtype is int16 for ABF version 2 """ for i in range(nbchannel): data[:,i] /= header['listADCInfo'][i]['fInstrumentScaleFactor'] data[:,i] /= header['listADCInfo'][i]['fSignalGain'] data[:,i] /= header['listADCInfo'][i]['fADCProgrammableGain'] if header['listADCInfo'][i]['nTelegraphEnable'] : data[:,i] /= header['listADCInfo'][i]['fTelegraphAdditGain'] data[:,i] *= header['protocol']['fADCRange'] data[:,i] /= header['protocol']['lADCResolution'] data[:,i] += header['listADCInfo'][i]['fInstrumentOffset'] data[:,i] -= header['listADCInfo'][i]['fSignalOffset'] def clean_string(s): while s.endswith('\x00') : s = s[:-1] while s.endswith(' ') : s = s[:-1] return s class AxonIO(BaseIO): """ Class for reading abf (axon binary file) file. Usage: >>> from neo import io >>> r = io.AxonIO(filename='File_axon_1.abf') >>> bl = r.read_block(lazy=False, cascade=True) >>> print bl.segments [] >>> print bl.segments[0].analogsignals [] >>> print bl.segments[0].eventarrays [] """ is_readable = True is_writable = False supported_objects = [ Block , Segment , AnalogSignal , EventArray ] readable_objects = [ Block ] writeable_objects = [ ] has_header = False is_streameable = False read_params = { Block : [ ] } write_params = None name = 'Axon' extensions = [ 'abf' ] mode = 'file' def __init__(self , filename = None) : """ This class read a abf file. Arguments: filename : the filename to read """ BaseIO.__init__(self) self.filename = filename def read_block(self, lazy = False, cascade = True ): header = self.read_header() version = header['fFileVersionNumber'] bl = Block() bl.file_origin = os.path.basename(self.filename) bl.annotate(abf_version = version) # date and time if version <2. : YY = 1900 MM = 1 DD = 1 hh = int(header['lFileStartTime']/3600.) mm = int((header['lFileStartTime']-hh*3600)/60) ss = header['lFileStartTime']-hh*3600-mm*60 ms = int(np.mod(ss,1)*1e6) ss = int(ss) elif version >=2. : YY = int(header['uFileStartDate']/10000) MM = int((header['uFileStartDate']-YY*10000)/100) DD = int(header['uFileStartDate']-YY*10000-MM*100) hh = int(header['uFileStartTimeMS']/1000./3600.) mm = int((header['uFileStartTimeMS']/1000.-hh*3600)/60) ss = header['uFileStartTimeMS']/1000.-hh*3600-mm*60 ms = int(np.mod(ss,1)*1e6) ss = int(ss) bl.rec_datetime = datetime.datetime( YY , MM , DD , hh , mm , ss , ms) if not cascade: return bl # file format if header['nDataFormat'] == 0 : dt = np.dtype('i2') elif header['nDataFormat'] == 1 : dt = np.dtype('f4') if version <2. : nbchannel = header['nADCNumChannels'] headOffset = header['lDataSectionPtr']*BLOCKSIZE+header['nNumPointsIgnored']*dt.itemsize totalsize = header['lActualAcqLength'] elif version >=2. : nbchannel = header['sections']['ADCSection']['llNumEntries'] headOffset = header['sections']['DataSection']['uBlockIndex']*BLOCKSIZE totalsize = header['sections']['DataSection']['llNumEntries'] data = np.memmap(self.filename , dt , 'r', shape = (totalsize,) , offset = headOffset) # 3 possible modes if version <2. : mode = header['nOperationMode'] elif version >=2. : mode = header['protocol']['nOperationMode'] #~ print 'mode' , mode if (mode == 1) or (mode == 2) or (mode == 5) or (mode == 3): # event-driven variable-length mode (mode 1) # event-driven fixed-length mode (mode 2 or 5) # gap free mode (mode 3) can be in several episod (strange but possible) # read sweep pos if version <2. : nbepisod = header['lSynchArraySize'] offsetEpisod = header['lSynchArrayPtr']*BLOCKSIZE elif version >=2. : nbepisod = header['sections']['SynchArraySection']['llNumEntries'] offsetEpisod = header['sections']['SynchArraySection']['uBlockIndex']*BLOCKSIZE if nbepisod>0: episodArray = np.memmap(self.filename , [('offset','i4'), ('len', 'i4') ] , 'r', shape = (nbepisod), offset = offsetEpisod ) else: episodArray = np.empty( (1) , [('offset','i4'), ('len', 'i4') ] ,) episodArray[0]['len'] = data.size episodArray[0]['offset'] = 0 # sampling_rate if version <2. : sampling_rate = 1./(header['fADCSampleInterval']*nbchannel*1.e-6) * pq.Hz elif version >=2. : sampling_rate = 1.e6/header['protocol']['fADCSequenceInterval'] * pq.Hz # construct block # one sweep = one segment in a block pos = 0 for j in range(episodArray.size): seg = Segment(index = j) length = episodArray[j]['len'] if version <2. : fSynchTimeUnit = header['fSynchTimeUnit'] elif version >=2. : fSynchTimeUnit = header['protocol']['fSynchTimeUnit'] if (fSynchTimeUnit != 0) and (mode == 1) : length /= fSynchTimeUnit subdata = data[pos:pos+length] pos += length subdata = subdata.reshape( (subdata.size/nbchannel, nbchannel )).astype('f') if dt == np.dtype('i2'): if version <2. : reformat_integer_V1(subdata, nbchannel , header) elif version >=2. : reformat_integer_V2(subdata, nbchannel , header) for i in range(nbchannel): if version <2. : name = header['sADCChannelName'][i].replace('\x00','') unit = header['sADCUnits'][i].replace('\xb5', 'u').replace('\x00','')#\xb5 is µ num = header['nADCPtoLChannelMap'][i] elif version >=2. : name = header['listADCInfo'][i]['ADCChNames'].replace('\x00','') unit = header['listADCInfo'][i]['ADCChUnits'].replace('\xb5', 'u').replace('\x00','')#\xb5 is µ num = header['listADCInfo'][i]['nADCNum'] t_start = float(episodArray[j]['offset'])/sampling_rate t_start = t_start.rescale('s') try: pq.Quantity(1, unit) except: #~ print 'bug units', i, unit unit = '' if lazy: signal = [ ] * pq.Quantity(1, unit) else: signal = subdata[:,i] * pq.Quantity(1, unit) anaSig = AnalogSignal(signal, sampling_rate=sampling_rate, t_start=t_start, name=str(name), channel_index=int(num)) if lazy: anaSig.lazy_shape = subdata.shape[0] seg.analogsignals.append( anaSig ) bl.segments.append(seg) if mode in [3,5]:# TODO check if tags exits in other mode # tag is EventArray that should be attached to Block # It is attched to the first Segment times = [ ] labels = [ ] comments = [ ] for i,tag in enumerate(header['listTag']) : times.append(tag['lTagTime']/sampling_rate ) labels.append( str(tag['nTagType']) ) comments.append(clean_string(tag['sComment'])) times = np.array(times) labels = np.array(labels, dtype='S') comments = np.array(comments, dtype='S') # attach all tags to the first segment. seg = bl.segments[0] if lazy : ea = EventArray( times =[ ] * pq.s , labels=np.array([ ], dtype = 'S')) ea.lazy_shape = len(times) else: ea = EventArray( times = times*pq.s, labels = labels, comments = comments ) seg.eventarrays.append(ea) create_many_to_one_relationship(bl) return bl def read_header(self, ): """ read the header of the file The strategy differ here from the original script under Matlab. In the original script for ABF2, it complete the header with informations that are located in other structures. In ABF2 this function return header with sub dict : sections (ABF2) protocol (ABF2) listTags (ABF1&2) listADCInfo (ABF2) listDACInfo (ABF2) dictEpochInfoPerDAC (ABF2) that contain more information. """ fid = struct_file(self.filename,'rb') # version fFileSignature = fid.read(4) if fFileSignature == 'ABF ' : headerDescription = headerDescriptionV1 elif fFileSignature == 'ABF2' : headerDescription = headerDescriptionV2 else : return None # construct dict header = { } for key, offset , fmt in headerDescription : val = fid.read_f(fmt , offset = offset) if len(val) == 1: header[key] = val[0] else : header[key] = np.array(val) # correction of version number and starttime if fFileSignature == 'ABF ' : header['lFileStartTime'] = header['lFileStartTime'] + header['nFileStartMillisecs']*.001 elif fFileSignature == 'ABF2' : n = header['fFileVersionNumber'] header['fFileVersionNumber'] = n[3]+0.1*n[2]+0.01*n[1]+0.001*n[0] header['lFileStartTime'] = header['uFileStartTimeMS']*.001 if header['fFileVersionNumber'] < 2. : # tags listTag = [ ] for i in range(header['lNumTagEntries']) : fid.seek(header['lTagSectionPtr']+i*64) tag = { } for key, fmt in TagInfoDescription : val = fid.read_f(fmt ) if len(val) == 1: tag[key] = val[0] else : tag[key] = np.array(val) listTag.append(tag) header['listTag'] = listTag elif header['fFileVersionNumber'] >= 2. : # in abf2 some info are in other place # sections sections = { } for s,sectionName in enumerate(sectionNames) : uBlockIndex,uBytes,llNumEntries= fid.read_f( 'IIl' , offset = 76 + s * 16 ) sections[sectionName] = { } sections[sectionName]['uBlockIndex'] = uBlockIndex sections[sectionName]['uBytes'] = uBytes sections[sectionName]['llNumEntries'] = llNumEntries header['sections'] = sections # strings sections # hack for reading channels names and units fid.seek(sections['StringsSection']['uBlockIndex']*BLOCKSIZE) bigString = fid.read(sections['StringsSection']['uBytes']) goodstart = bigString.lower().find('clampex') if goodstart == -1 : goodstart = bigString.lower().find('axoscope') bigString = bigString[goodstart:] strings = bigString.split('\x00') # ADC sections header['listADCInfo'] = [ ] for i in range(sections['ADCSection']['llNumEntries']) : # read ADCInfo fid.seek(sections['ADCSection']['uBlockIndex']*\ BLOCKSIZE+sections['ADCSection']['uBytes']*i) ADCInfo = { } for key, fmt in ADCInfoDescription : val = fid.read_f(fmt ) if len(val) == 1: ADCInfo[key] = val[0] else : ADCInfo[key] = np.array(val) ADCInfo['ADCChNames'] = strings[ADCInfo['lADCChannelNameIndex']-1] ADCInfo['ADCChUnits'] = strings[ADCInfo['lADCUnitsIndex']-1] header['listADCInfo'].append( ADCInfo ) # protocol sections protocol = { } fid.seek(sections['ProtocolSection']['uBlockIndex']*BLOCKSIZE) for key, fmt in protocolInfoDescription : val = fid.read_f(fmt ) if len(val) == 1: protocol[key] = val[0] else : protocol[key] = np.array(val) header['protocol'] = protocol # tags listTag = [ ] for i in range(sections['TagSection']['llNumEntries']) : fid.seek(sections['TagSection']['uBlockIndex']*\ BLOCKSIZE+sections['TagSection']['uBytes']*i) tag = { } for key, fmt in TagInfoDescription : val = fid.read_f(fmt ) if len(val) == 1: tag[key] = val[0] else : tag[key] = np.array(val) listTag.append(tag) header['listTag'] = listTag # DAC sections header['listDACInfo'] = [ ] for i in range(sections['DACSection']['llNumEntries']) : # read DACInfo fid.seek(sections['DACSection']['uBlockIndex']*\ BLOCKSIZE+sections['DACSection']['uBytes']*i) DACInfo = { } for key, fmt in DACInfoDescription : val = fid.read_f(fmt ) if len(val) == 1: DACInfo[key] = val[0] else : DACInfo[key] = np.array(val) DACInfo['DACChNames'] = strings[DACInfo['lDACChannelNameIndex']-1] DACInfo['DACChUnits'] = strings[DACInfo['lDACChannelUnitsIndex']-1] header['listDACInfo'].append( DACInfo ) # EpochPerDAC sections # header['dictEpochInfoPerDAC'] is dict of dicts: # - the first index is the DAC number # - the second index is the epoch number # It has to be done like that because data may not exist and may not be in sorted order header['dictEpochInfoPerDAC'] = { } for i in range(sections['EpochPerDACSection']['llNumEntries']) : # read DACInfo fid.seek(sections['EpochPerDACSection']['uBlockIndex']*\ BLOCKSIZE+sections['EpochPerDACSection']['uBytes']*i) EpochInfoPerDAC = { } for key, fmt in EpochInfoPerDACDescription : val = fid.read_f(fmt ) if len(val) == 1: EpochInfoPerDAC[key] = val[0] else : EpochInfoPerDAC[key] = np.array(val) DACNum = EpochInfoPerDAC['nDACNum'] EpochNum = EpochInfoPerDAC['nEpochNum'] # Checking if the key exists, if not, the value is empty # so we have to create empty dict to populate if not header['dictEpochInfoPerDAC'].has_key(DACNum): header['dictEpochInfoPerDAC'][DACNum] = { } header['dictEpochInfoPerDAC'][DACNum][EpochNum] = EpochInfoPerDAC fid.close() return header def read_protocol(self): """ Read the protocol waveform of the file, if present; function works with ABF2 only. Returns: list of segments (one for every episode) with list of analog signls (one for every DAC). """ header = self.read_header() if header['fFileVersionNumber'] < 2. : raise IOError("Protocol is only present in ABF2 files.") nADC = header['sections']['ADCSection']['llNumEntries'] # Number of ADC channels nDAC = header['sections']['DACSection']['llNumEntries'] # Number of DAC channels nSam = header['protocol']['lNumSamplesPerEpisode']/nADC # Number of samples per episode nEpi = header['lActualEpisodes'] # Actual number of episodes sampling_rate = 1.e6/header['protocol']['fADCSequenceInterval'] * pq.Hz # Creating a list of segments with analog signals with just holding levels # List of segments relates to number of episodes, as for recorded data segments = [] for epiNum in range(nEpi): seg = Segment(index=epiNum) # One analog signal for each DAC in segment (episode) for DACNum in range(nDAC): t_start = 0 * pq.s# TODO: Possibly check with episode array name = header['listDACInfo'][DACNum]['DACChNames'] unit = header['listDACInfo'][DACNum]['DACChUnits'].replace('\xb5', 'u')#\xb5 is µ signal = np.ones(nSam)*header['listDACInfo'][DACNum]['fDACHoldingLevel']*pq.Quantity(1, unit) anaSig = AnalogSignal(signal, sampling_rate=sampling_rate, t_start=t_start, name=str(name), channel_index=DACNum) # If there are epoch infos for this DAC if header['dictEpochInfoPerDAC'].has_key(DACNum): # Save last sample index i_last = int(nSam*15625/10**6) # TODO guess for first holding # Go over EpochInfoPerDAC and change the analog signal according to the epochs for epochNum,epoch in iteritems(header['dictEpochInfoPerDAC'][DACNum]): i_begin = i_last i_end = i_last + epoch['lEpochInitDuration'] + epoch['lEpochDurationInc'] * epiNum anaSig[i_begin:i_end] = np.ones(len(range(i_end-i_begin)))*pq.Quantity(1, unit)* \ (epoch['fEpochInitLevel']+epoch['fEpochLevelInc'] * epiNum); i_last += epoch['lEpochInitDuration'] seg.analogsignals.append(anaSig) segments.append(seg) return segments BLOCKSIZE = 512 headerDescriptionV1= [ ('fFileSignature',0,'4s'), ('fFileVersionNumber',4,'f' ), ('nOperationMode',8,'h' ), ('lActualAcqLength',10,'i' ), ('nNumPointsIgnored',14,'h' ), ('lActualEpisodes',16,'i' ), ('lFileStartTime',24,'i' ), ('lDataSectionPtr',40,'i' ), ('lTagSectionPtr',44,'i' ), ('lNumTagEntries',48,'i' ), ('lSynchArrayPtr',92,'i' ), ('lSynchArraySize',96,'i' ), ('nDataFormat',100,'h' ), ('nADCNumChannels', 120, 'h'), ('fADCSampleInterval',122,'f'), ('fSynchTimeUnit',130,'f' ), ('lNumSamplesPerEpisode',138,'i' ), ('lPreTriggerSamples',142,'i' ), ('lEpisodesPerRun',146,'i' ), ('fADCRange', 244, 'f' ), ('lADCResolution', 252, 'i'), ('nFileStartMillisecs', 366, 'h'), ('nADCPtoLChannelMap', 378, '16h'), ('nADCSamplingSeq', 410, '16h'), ('sADCChannelName',442, '10s'*16), ('sADCUnits',602, '8s'*16) , ('fADCProgrammableGain', 730, '16f'), ('fInstrumentScaleFactor', 922, '16f'), ('fInstrumentOffset', 986, '16f'), ('fSignalGain', 1050, '16f'), ('fSignalOffset', 1114, '16f'), ('nTelegraphEnable',4512, '16h'), ('fTelegraphAdditGain',4576,'16f'), ] headerDescriptionV2 =[ ('fFileSignature',0,'4s' ), ('fFileVersionNumber',4,'4b') , ('uFileInfoSize',8,'I' ) , ('lActualEpisodes',12,'I' ) , ('uFileStartDate',16,'I' ) , ('uFileStartTimeMS',20,'I' ) , ('uStopwatchTime',24,'I' ) , ('nFileType',28,'H' ) , ('nDataFormat',30,'H' ) , ('nSimultaneousScan',32,'H' ) , ('nCRCEnable',34,'H' ) , ('uFileCRC',36,'I' ) , ('FileGUID',40,'I' ) , ('uCreatorVersion',56,'I' ) , ('uCreatorNameIndex',60,'I' ) , ('uModifierVersion',64,'I' ) , ('uModifierNameIndex',68,'I' ) , ('uProtocolPathIndex',72,'I' ) , ] sectionNames= ['ProtocolSection', 'ADCSection', 'DACSection', 'EpochSection', 'ADCPerDACSection', 'EpochPerDACSection', 'UserListSection', 'StatsRegionSection', 'MathSection', 'StringsSection', 'DataSection', 'TagSection', 'ScopeSection', 'DeltaSection', 'VoiceTagSection', 'SynchArraySection', 'AnnotationSection', 'StatsSection', ] protocolInfoDescription = [ ('nOperationMode','h'), ('fADCSequenceInterval','f'), ('bEnableFileCompression','b'), ('sUnused1','3s'), ('uFileCompressionRatio','I'), ('fSynchTimeUnit','f'), ('fSecondsPerRun','f'), ('lNumSamplesPerEpisode','i'), ('lPreTriggerSamples','i'), ('lEpisodesPerRun','i'), ('lRunsPerTrial','i'), ('lNumberOfTrials','i'), ('nAveragingMode','h'), ('nUndoRunCount','h'), ('nFirstEpisodeInRun','h'), ('fTriggerThreshold','f'), ('nTriggerSource','h'), ('nTriggerAction','h'), ('nTriggerPolarity','h'), ('fScopeOutputInterval','f'), ('fEpisodeStartToStart','f'), ('fRunStartToStart','f'), ('lAverageCount','i'), ('fTrialStartToStart','f'), ('nAutoTriggerStrategy','h'), ('fFirstRunDelayS','f'), ('nChannelStatsStrategy','h'), ('lSamplesPerTrace','i'), ('lStartDisplayNum','i'), ('lFinishDisplayNum','i'), ('nShowPNRawData','h'), ('fStatisticsPeriod','f'), ('lStatisticsMeasurements','i'), ('nStatisticsSaveStrategy','h'), ('fADCRange','f'), ('fDACRange','f'), ('lADCResolution','i'), ('lDACResolution','i'), ('nExperimentType','h'), ('nManualInfoStrategy','h'), ('nCommentsEnable','h'), ('lFileCommentIndex','i'), ('nAutoAnalyseEnable','h'), ('nSignalType','h'), ('nDigitalEnable','h'), ('nActiveDACChannel','h'), ('nDigitalHolding','h'), ('nDigitalInterEpisode','h'), ('nDigitalDACChannel','h'), ('nDigitalTrainActiveLogic','h'), ('nStatsEnable','h'), ('nStatisticsClearStrategy','h'), ('nLevelHysteresis','h'), ('lTimeHysteresis','i'), ('nAllowExternalTags','h'), ('nAverageAlgorithm','h'), ('fAverageWeighting','f'), ('nUndoPromptStrategy','h'), ('nTrialTriggerSource','h'), ('nStatisticsDisplayStrategy','h'), ('nExternalTagType','h'), ('nScopeTriggerOut','h'), ('nLTPType','h'), ('nAlternateDACOutputState','h'), ('nAlternateDigitalOutputState','h'), ('fCellID','3f'), ('nDigitizerADCs','h'), ('nDigitizerDACs','h'), ('nDigitizerTotalDigitalOuts','h'), ('nDigitizerSynchDigitalOuts','h'), ('nDigitizerType','h'), ] ADCInfoDescription = [ ('nADCNum','h'), ('nTelegraphEnable','h'), ('nTelegraphInstrument','h'), ('fTelegraphAdditGain','f'), ('fTelegraphFilter','f'), ('fTelegraphMembraneCap','f'), ('nTelegraphMode','h'), ('fTelegraphAccessResistance','f'), ('nADCPtoLChannelMap','h'), ('nADCSamplingSeq','h'), ('fADCProgrammableGain','f'), ('fADCDisplayAmplification','f'), ('fADCDisplayOffset','f'), ('fInstrumentScaleFactor','f'), ('fInstrumentOffset','f'), ('fSignalGain','f'), ('fSignalOffset','f'), ('fSignalLowpassFilter','f'), ('fSignalHighpassFilter','f'), ('nLowpassFilterType','b'), ('nHighpassFilterType','b'), ('fPostProcessLowpassFilter','f'), ('nPostProcessLowpassFilterType','c'), ('bEnabledDuringPN','b'), ('nStatsChannelPolarity','h'), ('lADCChannelNameIndex','i'), ('lADCUnitsIndex','i'), ] TagInfoDescription = [ ('lTagTime','i'), ('sComment','56s'), ('nTagType','h'), ('nVoiceTagNumber_or_AnnotationIndex','h'), ] DACInfoDescription = [ ('nDACNum','h'), ('nTelegraphDACScaleFactorEnable','h'), ('fInstrumentHoldingLevel', 'f'), ('fDACScaleFactor','f'), ('fDACHoldingLevel','f'), ('fDACCalibrationFactor','f'), ('fDACCalibrationOffset','f'), ('lDACChannelNameIndex','i'), ('lDACChannelUnitsIndex','i'), ('lDACFilePtr','i'), ('lDACFileNumEpisodes','i'), ('nWaveformEnable','h'), ('nWaveformSource','h'), ('nInterEpisodeLevel','h'), ('fDACFileScale','f'), ('fDACFileOffset','f'), ('lDACFileEpisodeNum','i'), ('nDACFileADCNum','h'), ('nConditEnable','h'), ('lConditNumPulses','i'), ('fBaselineDuration','f'), ('fBaselineLevel','f'), ('fStepDuration','f'), ('fStepLevel','f'), ('fPostTrainPeriod','f'), ('fPostTrainLevel','f'), ('nMembTestEnable','h'), ('nLeakSubtractType','h'), ('nPNPolarity','h'), ('fPNHoldingLevel','f'), ('nPNNumADCChannels','h'), ('nPNPosition','h'), ('nPNNumPulses','h'), ('fPNSettlingTime','f'), ('fPNInterpulse','f'), ('nLTPUsageOfDAC','h'), ('nLTPPresynapticPulses','h'), ('lDACFilePathIndex','i'), ('fMembTestPreSettlingTimeMS','f'), ('fMembTestPostSettlingTimeMS','f'), ('nLeakSubtractADCIndex','h'), ('sUnused','124s'), ] EpochInfoPerDACDescription = [ ('nEpochNum','h'), ('nDACNum','h'), ('nEpochType','h'), ('fEpochInitLevel','f'), ('fEpochLevelInc','f'), ('lEpochInitDuration','i'), ('lEpochDurationInc','i'), ('lEpochPulsePeriod','i'), ('lEpochPulseWidth','i'), ('sUnused','18s'), ] EpochInfoDescription = [ ('nEpochNum','h'), ('nDigitalValue','h'), ('nDigitalTrainValue','h'), ('nAlternateDigitalValue','h'), ('nAlternateDigitalTrainValue','h'), ('bEpochCompression','b'), ('sUnused','21s'), ] neo-0.3.3/neo/io/asciisignalio.py0000644000175000017500000001734412273723542017742 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Class for reading/writing analog signals in a text file. Each columns represents a AnalogSignal. All AnalogSignal have the same sampling rate. Covers many case when part of a file can be viewed as a CSV format. Supported : Read/Write Author: sgarcia """ import csv import os import numpy as np import quantities as pq from neo.io.baseio import BaseIO from neo.core import AnalogSignal, Segment from neo.io.tools import create_many_to_one_relationship class AsciiSignalIO(BaseIO): """ Class for reading signal in generic ascii format. Columns respresents signal. They share all the same sampling rate. The sampling rate is externally known or the first columns could hold the time vector. Usage: >>> from neo import io >>> r = io.AsciiSignalIO(filename='File_asciisignal_2.txt') >>> seg = r.read_segment(lazy=False, cascade=True) >>> print seg.analogsignals [>> from neo import io >>> r = io.ExampleIO(filename='itisafake.nof') >>> seg = r.read_segment(lazy=False, cascade=True) >>> print(seg.analogsignals) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [>> print(seg.spiketrains) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [>> print(seg.eventarrays) # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE [>> anasig = r.read_analogsignal(lazy=True, cascade=False) >>> print(anasig._data_description) {'shape': (150000,)} >>> anasig = r.read_analogsignal(lazy=False, cascade=False) """ is_readable = True # This class can only read data is_writable = False # write is not supported # 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 = [ Segment , AnalogSignal, SpikeTrain, EventArray ] # This class can return either a Block or a Segment # The first one is the default ( self.read ) # These lists should go from highest object to lowest object because # common_io_test assumes it. readable_objects = [ Segment , AnalogSignal, SpikeTrain ] # This class is not able to write objects writeable_objects = [ ] has_header = False is_streameable = False # This is for GUI stuff : a definition for parameters when reading. # This dict should be keyed by object (`Block`). Each entry is a list # of tuple. The first entry in each tuple is the parameter name. The # second entry is a dict with keys 'value' (for default value), # and 'label' (for a descriptive name). # Note that if the highest-level object requires parameters, # common_io_test will be skipped. read_params = { Segment : [ ('segment_duration', {'value' : 15., 'label' : 'Segment size (s.)'}), ('num_analogsignal', {'value' : 8, 'label' : 'Number of recording points'}), ('num_spiketrain_by_channel', {'value' : 3, 'label' : 'Num of spiketrains'}), ], } # do not supported write so no GUI stuff write_params = None name = 'example' extensions = [ 'nof' ] # mode can be 'file' or 'dir' or 'fake' or 'database' # the main case is 'file' but some reader are base on a directory or a database # this info is for GUI stuff also mode = 'fake' def __init__(self , filename = None) : """ Arguments: filename : the filename Note: - filename is here just for exampe because it will not be take in account - if mode=='dir' the argument should be dirname (See TdtIO) """ BaseIO.__init__(self) self.filename = filename # Seed so all instances can return the same values np.random.seed(1234) # Segment reading is supported so I define this : def read_segment(self, # the 2 first keyword arguments are imposed by neo.io API lazy = False, cascade = True, # all following arguments are decied by this IO and are free segment_duration = 15., num_analogsignal = 4, num_spiketrain_by_channel = 3, ): """ Return a fake Segment. The self.filename does not matter. In this IO read by default a Segment. This is just a example to be adapted to each ClassIO. In this case these 3 paramters are taken in account because this function return a generated segment with fake AnalogSignal and fake SpikeTrain. Parameters: segment_duration :is the size in secend of the segment. num_analogsignal : number of AnalogSignal in this segment num_spiketrain : number of SpikeTrain in this segment """ sampling_rate = 10000. #Hz t_start = -1. #time vector for generated signal timevect = np.arange(t_start, t_start+ segment_duration , 1./sampling_rate) # create an empty segment seg = Segment( name = 'it is a seg from exampleio') if cascade: # read nested analosignal for i in range(num_analogsignal): ana = self.read_analogsignal( lazy = lazy , cascade = cascade , channel_index = i ,segment_duration = segment_duration, t_start = t_start) seg.analogsignals += [ ana ] # read nested spiketrain for i in range(num_analogsignal): for _ in range(num_spiketrain_by_channel): sptr = self.read_spiketrain(lazy = lazy , cascade = cascade , segment_duration = segment_duration, t_start = t_start , channel_index = i) seg.spiketrains += [ sptr ] # create an EventArray that mimic triggers. # note that ExampleIO do not allow to acess directly to EventArray # for that you need read_segment(cascade = True) eva = EventArray() if lazy: # in lazy case no data are readed # eva is empty pass else: # otherwise it really contain data n = 1000 # neo.io support quantities my vector use second for unit eva.times = timevect[(np.random.rand(n)*timevect.size).astype('i')]* pq.s # all duration are the same eva.durations = np.ones(n)*500*pq.ms # label l = [ ] for i in range(n): if np.random.rand()>.6: l.append( 'TriggerA' ) else : l.append( 'TriggerB' ) eva.labels = np.array( l ) seg.eventarrays += [ eva ] create_many_to_one_relationship(seg) return seg def read_analogsignal(self , # the 2 first key arguments are imposed by neo.io API lazy = False, cascade = True, channel_index = 0, segment_duration = 15., t_start = -1, ): """ With this IO AnalogSignal can e acces directly with its channel number """ sr = 10000. sinus_freq = 3. # Hz #time vector for generated signal: tvect = np.arange(t_start, t_start+ segment_duration , 1./sr) if lazy: anasig = AnalogSignal([], units='V', sampling_rate=sr * pq.Hz, t_start=t_start * pq.s, channel_index=channel_index) # we add the attribute lazy_shape with the size if loaded anasig.lazy_shape = tvect.shape else: # create analogsignal (sinus of 3 Hz) sig = np.sin(2*np.pi*tvect*sinus_freq + channel_index/5.*2*np.pi)+np.random.rand(tvect.size) anasig = AnalogSignal(sig, units= 'V', sampling_rate=sr * pq.Hz, t_start=t_start * pq.s, channel_index=channel_index) # for attributes out of neo you can annotate anasig.annotate(info = 'it is a sinus of %f Hz' %sinus_freq ) return anasig def read_spiketrain(self , # the 2 first key arguments are imposed by neo.io API lazy = False, cascade = True, segment_duration = 15., t_start = -1, channel_index = 0, ): """ With this IO SpikeTrain can e acces directly with its channel number """ # There are 2 possibles behaviour for a SpikeTrain # holding many Spike instance or directly holding spike times # we choose here the first : if not HAVE_SCIPY: raise SCIPY_ERR num_spike_by_spiketrain = 40 sr = 10000. if lazy: times = [ ] else: times = (np.random.rand(num_spike_by_spiketrain)*segment_duration + t_start) # create a spiketrain spiketr = SpikeTrain(times, t_start = t_start*pq.s, t_stop = (t_start+segment_duration)*pq.s , units = pq.s, name = 'it is a spiketrain from exampleio', ) if lazy: # we add the attribute lazy_shape with the size if loaded spiketr.lazy_shape = (num_spike_by_spiketrain,) # ours spiketrains also hold the waveforms: # 1 generate a fake spike shape (2d array if trodness >1) w1 = -stats.nct.pdf(np.arange(11,60,4), 5,20)[::-1]/3. w2 = stats.nct.pdf(np.arange(11,60,2), 5,20) w = np.r_[ w1 , w2 ] w = -w/max(w) if not lazy: # in the neo API the waveforms attr is 3 D in case tetrode # in our case it is mono electrode so dim 1 is size 1 waveforms = np.tile( w[np.newaxis,np.newaxis,:], ( num_spike_by_spiketrain ,1, 1) ) waveforms *= np.random.randn(*waveforms.shape)/6+1 spiketr.waveforms = waveforms*pq.mV spiketr.sampling_rate = sr * pq.Hz spiketr.left_sweep = 1.5* pq.s # for attributes out of neo you can annotate spiketr.annotate(channel_index = channel_index) return spiketr neo-0.3.3/neo/io/pynnio.py0000644000175000017500000002221612273723542016432 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Module for reading/writing data from/to legacy PyNN formats. PyNN is available at http://neuralensemble.org/PyNN Classes: PyNNNumpyIO PyNNTextIO Supported: Read/Write Authors: Andrew Davison, Pierre Yger """ import numpy import quantities as pq from neo.io.baseio import BaseIO from neo.core import Segment, AnalogSignal, AnalogSignalArray, SpikeTrain from neo.io.tools import create_many_to_one_relationship UNITS_MAP = { 'spikes': pq.ms, 'v': pq.mV, 'gsyn': pq.UnitQuantity('microsiemens', 1e-6*pq.S, 'uS', 'µS'), # checked } class BasePyNNIO(BaseIO): """ Base class for PyNN IO classes """ is_readable = True is_writable = True has_header = True is_streameable = False # TODO - correct spelling to "is_streamable" supported_objects = [Segment, AnalogSignal, AnalogSignalArray, SpikeTrain] readable_objects = supported_objects writeable_objects = supported_objects mode = 'file' def _read_file_contents(self): raise NotImplementedError def _extract_array(self, data, channel_index): idx = numpy.where(data[:, 1] == channel_index)[0] return data[idx, 0] def _determine_units(self, metadata): if 'units' in metadata: return metadata['units'] elif 'variable' in metadata and metadata['variable'] in UNITS_MAP: return UNITS_MAP[metadata['variable']] else: raise IOError("Cannot determine units") def _extract_signal(self, data, metadata, channel_index, lazy): signal = None if lazy: if channel_index in data[:, 1]: signal = AnalogSignal([], units=self._determine_units(metadata), sampling_period=metadata['dt']*pq.ms, channel_index=channel_index) signal.lazy_shape = None else: arr = self._extract_array(data, channel_index) if len(arr) > 0: signal = AnalogSignal(arr, units=self._determine_units(metadata), sampling_period=metadata['dt']*pq.ms, channel_index=channel_index) if signal is not None: signal.annotate(label=metadata["label"], variable=metadata["variable"]) return signal def _extract_spikes(self, data, metadata, channel_index, lazy): spiketrain = None if lazy: if channel_index in data[:, 1]: spiketrain = SpikeTrain([], units=pq.ms, t_stop=0.0) spiketrain.lazy_shape = None else: spike_times = self._extract_array(data, channel_index) if len(spike_times) > 0: spiketrain = SpikeTrain(spike_times, units=pq.ms, t_stop=spike_times.max()) if spiketrain is not None: spiketrain.annotate(label=metadata["label"], channel_index=channel_index, dt=metadata["dt"]) return spiketrain def _write_file_contents(self, data, metadata): raise NotImplementedError def read_segment(self, lazy=False, cascade=True): data, metadata = self._read_file_contents() annotations = dict((k, metadata.get(k, 'unknown')) for k in ("label", "variable", "first_id", "last_id")) seg = Segment(**annotations) if cascade: if metadata['variable'] == 'spikes': for i in range(metadata['first_index'], metadata['last_index']): spiketrain = self._extract_spikes(data, metadata, i, lazy) if spiketrain is not None: seg.spiketrains.append(spiketrain) seg.annotate(dt=metadata['dt']) # store dt for SpikeTrains only, as can be retrieved from sampling_period for AnalogSignal else: for i in range(metadata['first_index'], metadata['last_index']): # probably slow. Replace with numpy-based version from 0.1 signal = self._extract_signal(data, metadata, i, lazy) if signal is not None: seg.analogsignals.append(signal) create_many_to_one_relationship(seg) return seg def write_segment(self, segment): source = segment.analogsignals or segment.analogsignalarrays or segment.spiketrains assert len(source) > 0, "Segment contains neither analog signals nor spike trains." metadata = segment.annotations.copy() metadata['size'] = len(source) metadata['first_index'] = 0 metadata['last_index'] = metadata['size'] if 'label' not in metadata: metadata['label'] = 'unknown' s0 = source[0] if 'dt' not in metadata: # dt not included in annotations if Segment contains only AnalogSignals metadata['dt'] = s0.sampling_period.rescale(pq.ms).magnitude n = sum(s.size for s in source) metadata['n'] = n data = numpy.empty((n, 2)) # if the 'variable' annotation is a standard one from PyNN, we rescale # to use standard PyNN units # we take the units from the first element of source and scale all # the signals to have the same units if 'variable' in segment.annotations: units = UNITS_MAP.get(segment.annotations['variable'], source[0].dimensionality) else: units = source[0].dimensionality metadata['variable'] = 'unknown' try: metadata['units'] = units.unicode except AttributeError: metadata['units'] = units.u_symbol start = 0 if isinstance(s0, AnalogSignalArray): assert len(source) == 1, "Cannot handle multiple analog signal arrays" source = s0.T for i, signal in enumerate(source): # here signal may be AnalogSignal or SpikeTrain end = start + signal.size data[start:end, 0] = numpy.array(signal.rescale(units)) data[start:end, 1] = i*numpy.ones((signal.size,), dtype=float) # index (what about channel_indexes, if it's an AnalogSignalArray?) start = end self._write_file_contents(data, metadata) def read_analogsignal(self, lazy=False, channel_index=0): # channel_index should be positional arg, no? data, metadata = self._read_file_contents() if metadata['variable'] == 'spikes': raise TypeError("File contains spike data, not analog signals") else: signal = self._extract_signal(data, metadata, channel_index, lazy) if signal is None: raise IndexError("File does not contain a signal with channel index %d" % channel_index) else: return signal def read_analogsignalarray(self, lazy=False): raise NotImplementedError def read_spiketrain(self, lazy=False, channel_index=0): data, metadata = self._read_file_contents() if metadata['variable'] != 'spikes': raise TypeError("File contains analog signals, not spike data") else: spiketrain = self._extract_spikes(data, metadata, channel_index, lazy) if spiketrain is None: raise IndexError("File does not contain any spikes with channel index %d" % channel_index) else: return spiketrain class PyNNNumpyIO(BasePyNNIO): """ Reads/writes data from/to PyNN NumpyBinaryFile format """ name = "PyNN NumpyBinaryFile" extensions = ['npz'] def _read_file_contents(self): contents = numpy.load(self.filename) data = contents["data"] metadata = {} for name,value in contents['metadata']: try: metadata[name] = eval(value) except Exception: metadata[name] = value return data, metadata def _write_file_contents(self, data, metadata): metadata_array = numpy.array(sorted(metadata.items())) numpy.savez(self.filename, data=data, metadata=metadata_array) class PyNNTextIO(BasePyNNIO): """ Reads/writes data from/to PyNN StandardTextFile format """ name = "PyNN StandardTextFile" extensions = ['v', 'ras', 'gsyn'] def _read_metadata(self): metadata = {} with open(self.filename) as f: for line in f: if line[0] == "#": name, value = line[1:].strip().split("=") name = name.strip() try: metadata[name] = eval(value) except Exception: metadata[name] = value.strip() else: break return metadata def _read_file_contents(self): data = numpy.loadtxt(self.filename) metadata = self._read_metadata() return data, metadata def _write_file_contents(self, data, metadata): with open(self.filename, 'wb') as f: for item in sorted(metadata.items()): f.write(("# %s = %s\n" % item).encode('utf8')) numpy.savetxt(f, data) neo-0.3.3/neo/__init__.py0000644000175000017500000000041712265516260016243 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' Neo is a package for representing electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats ''' from neo.core import * from neo.io import * from neo.version import version as __version__ neo-0.3.3/neo/test/0000755000175000017500000000000012273723667015121 5ustar sgarciasgarcia00000000000000neo-0.3.3/neo/test/test_recordingchannel.py0000644000175000017500000002015612273723542022033 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.recordingchannel.RecordingChannel class """ try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.recordingchannel import RecordingChannel from neo.core.analogsignal import AnalogSignal from neo.core.irregularlysampledsignal import IrregularlySampledSignal from neo.test.tools import assert_neo_object_is_compliant, assert_arrays_equal from neo.io.tools import create_many_to_one_relationship class TestRecordingChannel(unittest.TestCase): def setUp(self): self.setup_analogsignals() self.setup_irregularlysampledsignals() self.setup_recordingchannels() def setup_recordingchannels(self): params = {'testarg2': 'yes', 'testarg3': True} self.rchan1 = RecordingChannel(index=10, coordinate=[1.1, 1.5, 1.7]*pq.mm, name='test', description='tester 1', file_origin='test.file', testarg1=1, **params) self.rchan2 = RecordingChannel(index=100, coordinate=[11., 15., 17.]*pq.mm, name='test', description='tester 2', file_origin='test.file', testarg1=1, **params) self.rchan1.annotate(testarg1=1.1, testarg0=[1, 2, 3]) self.rchan2.annotate(testarg11=1.1, testarg10=[1, 2, 3]) self.rchan1.analogsignals = self.sig1 self.rchan2.analogsignals = self.sig2 self.rchan1.irregularlysampledsignals = self.irsig1 self.rchan2.irregularlysampledsignals = self.irsig2 create_many_to_one_relationship(self.rchan1) create_many_to_one_relationship(self.rchan2) def setup_analogsignals(self): signame11 = 'analogsignal 1 1' signame12 = 'analogsignal 1 2' signame21 = 'analogsignal 2 1' signame22 = 'analogsignal 2 2' sigdata11 = np.arange(0, 10) * pq.mV sigdata12 = np.arange(10, 20) * pq.mV sigdata21 = np.arange(20, 30) * pq.V sigdata22 = np.arange(30, 40) * pq.V self.signames1 = [signame11, signame12] self.signames2 = [signame21, signame22] self.signames = [signame11, signame12, signame21, signame22] sig11 = AnalogSignal(sigdata11, name=signame11, sampling_rate=1*pq.Hz) sig12 = AnalogSignal(sigdata12, name=signame12, sampling_rate=1*pq.Hz) sig21 = AnalogSignal(sigdata21, name=signame21, sampling_rate=1*pq.Hz) sig22 = AnalogSignal(sigdata22, name=signame22, sampling_rate=1*pq.Hz) self.sig1 = [sig11, sig12] self.sig2 = [sig21, sig22] self.sig = [sig11, sig12, sig21, sig22] def setup_irregularlysampledsignals(self): irsigname11 = 'irregularsignal 1 1' irsigname12 = 'irregularsignal 1 2' irsigname21 = 'irregularsignal 2 1' irsigname22 = 'irregularsignal 2 2' irsigdata11 = np.arange(0, 10) * pq.mA irsigdata12 = np.arange(10, 20) * pq.mA irsigdata21 = np.arange(20, 30) * pq.A irsigdata22 = np.arange(30, 40) * pq.A irsigtimes11 = np.arange(0, 10) * pq.ms irsigtimes12 = np.arange(10, 20) * pq.ms irsigtimes21 = np.arange(20, 30) * pq.s irsigtimes22 = np.arange(30, 40) * pq.s self.irsignames1 = [irsigname11, irsigname12] self.irsignames2 = [irsigname21, irsigname22] self.irsignames = [irsigname11, irsigname12, irsigname21, irsigname22] irsig11 = IrregularlySampledSignal(irsigtimes11, irsigdata11, name=irsigname11) irsig12 = IrregularlySampledSignal(irsigtimes12, irsigdata12, name=irsigname12) irsig21 = IrregularlySampledSignal(irsigtimes21, irsigdata21, name=irsigname21) irsig22 = IrregularlySampledSignal(irsigtimes22, irsigdata22, name=irsigname22) self.irsig1 = [irsig11, irsig12] self.irsig2 = [irsig21, irsig22] self.irsig = [irsig11, irsig12, irsig21, irsig22] def test_recordingchannel_creation(self): assert_neo_object_is_compliant(self.rchan1) assert_neo_object_is_compliant(self.rchan2) self.assertEqual(self.rchan1.index, 10) self.assertEqual(self.rchan2.index, 100) assert_arrays_equal(self.rchan1.coordinate, [1.1, 1.5, 1.7]*pq.mm) assert_arrays_equal(self.rchan2.coordinate, [11., 15., 17.]*pq.mm) self.assertEqual(self.rchan1.name, 'test') self.assertEqual(self.rchan2.name, 'test') self.assertEqual(self.rchan1.description, 'tester 1') self.assertEqual(self.rchan2.description, 'tester 2') self.assertEqual(self.rchan1.file_origin, 'test.file') self.assertEqual(self.rchan2.file_origin, 'test.file') self.assertEqual(self.rchan1.annotations['testarg0'], [1, 2, 3]) self.assertEqual(self.rchan2.annotations['testarg10'], [1, 2, 3]) self.assertEqual(self.rchan1.annotations['testarg1'], 1.1) self.assertEqual(self.rchan2.annotations['testarg1'], 1) self.assertEqual(self.rchan2.annotations['testarg11'], 1.1) self.assertEqual(self.rchan1.annotations['testarg2'], 'yes') self.assertEqual(self.rchan2.annotations['testarg2'], 'yes') self.assertTrue(self.rchan1.annotations['testarg3']) self.assertTrue(self.rchan2.annotations['testarg3']) self.assertTrue(hasattr(self.rchan1, 'analogsignals')) self.assertTrue(hasattr(self.rchan2, 'analogsignals')) self.assertEqual(len(self.rchan1.analogsignals), 2) self.assertEqual(len(self.rchan2.analogsignals), 2) for res, targ in zip(self.rchan1.analogsignals, self.sig1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.rchan2.analogsignals, self.sig2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.rchan1, 'irregularlysampledsignals')) self.assertTrue(hasattr(self.rchan2, 'irregularlysampledsignals')) self.assertEqual(len(self.rchan1.irregularlysampledsignals), 2) self.assertEqual(len(self.rchan2.irregularlysampledsignals), 2) for res, targ in zip(self.rchan1.irregularlysampledsignals, self.irsig1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.rchan2.irregularlysampledsignals, self.irsig2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.rchan1, 'recordingchannelgroups')) self.assertTrue(hasattr(self.rchan2, 'recordingchannelgroups')) def test_recordingchannel_merge(self): self.rchan1.merge(self.rchan2) sigres1 = [sig.name for sig in self.rchan1.analogsignals] sigres2 = [sig.name for sig in self.rchan2.analogsignals] irsigres1 = [sig.name for sig in self.rchan1.irregularlysampledsignals] irsigres2 = [sig.name for sig in self.rchan2.irregularlysampledsignals] self.assertEqual(sigres1, self.signames) self.assertEqual(sigres2, self.signames2) self.assertEqual(irsigres1, self.irsignames) self.assertEqual(irsigres2, self.irsignames2) for res, targ in zip(self.rchan1.analogsignals, self.sig): assert_arrays_equal(res, targ) for res, targ in zip(self.rchan2.analogsignals, self.sig2): assert_arrays_equal(res, targ) for res, targ in zip(self.rchan1.irregularlysampledsignals, self.irsig): assert_arrays_equal(res, targ) for res, targ in zip(self.rchan2.irregularlysampledsignals, self.irsig2): assert_arrays_equal(res, targ) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/tools.py0000644000175000017500000004753512273723542016641 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' Tools for use with neo tests. ''' import hashlib import os import numpy as np import quantities as pq import neo from neo import description def assert_arrays_equal(a, b): ''' Check if two arrays have the same shape and contents ''' assert isinstance(a, np.ndarray), "a is a %s" % type(a) assert isinstance(b, np.ndarray), "b is a %s" % type(b) assert a.shape == b.shape, "%s != %s" % (a, b) #assert a.dtype == b.dtype, "%s and %s not same dtype %s %s" % (a, b, # a.dtype, # b.dtype) try: assert (a.flatten() == b.flatten()).all(), "%s != %s" % (a, b) except (AttributeError, ValueError): try: ar = np.array(a) br = np.array(b) assert (ar.flatten() == br.flatten()).all(), "%s != %s" % (ar, br) except (AttributeError, ValueError): assert np.all(a.flatten() == b.flatten()), "%s != %s" % (a, b) def assert_arrays_almost_equal(a, b, threshold): ''' Check if two arrays have the same shape and contents that differ by abs(a - b) <= threshold for all elements. ''' assert isinstance(a, np.ndarray), "a is a %s" % type(a) assert isinstance(b, np.ndarray), "b is a %s" % type(b) assert a.shape == b.shape, "%s != %s" % (a, b) #assert a.dtype == b.dtype, "%s and %b not same dtype %s %s" % (a, b, # a.dtype, # b.dtype) if a.dtype.kind in ['f', 'c', 'i']: assert (abs(a - b) < threshold).all(), \ "abs(%s - %s) max(|a - b|) = %s threshold:%s" % \ (a, b, (abs(a - b)).max(), threshold) def file_digest(filename): ''' Get the sha1 hash of the file with the given filename. ''' with open(filename, 'rb') as fobj: return hashlib.sha1(fobj.read()).hexdigest() def assert_file_contents_equal(a, b): ''' Assert that two files have the same size and hash. ''' def generate_error_message(a, b): ''' This creates the error message for the assertion error ''' size_a = os.stat(a).st_size size_b = os.stat(b).st_size if size_a == size_b: return "Files have the same size but different contents" else: return "Files have different sizes: a:%d b: %d" % (size_a, size_b) assert file_digest(a) == file_digest(b), generate_error_message(a, b) def assert_neo_object_is_compliant(ob): ''' Test neo compliance of one object and sub objects (one_to_many_relation only): * check types and/or presence of necessary and recommended attribute. * If attribute is Quantities or numpy.ndarray it also check ndim. * If attribute is numpy.ndarray also check dtype.kind. ''' assert type(ob) in description.objectlist, \ '%s is not a neo object' % (type(ob)) classname = ob.__class__.__name__ necess = description.classes_necessary_attributes[classname] recomm = description.classes_recommended_attributes[classname] # test presence of necessary attributes attributes = necess for ioattr in attributes: attrname, attrtype = ioattr[0], ioattr[1] #~ if attrname != '': if classname not in description.classes_inheriting_quantities: assert hasattr(ob, attrname), '%s neo obect does not have %s' % \ (classname, attrname) # test attributes types attributes = necess + recomm for ioattr in attributes: attrname, attrtype = ioattr[0], ioattr[1] if (classname in description.classes_inheriting_quantities and description.classes_inheriting_quantities[classname] == attrname and (attrtype == pq.Quantity or attrtype == np.ndarray)): # object is hinerited from Quantity (AnalogSIgnal, SpikeTrain, ...) ndim = ioattr[2] assert ob.ndim == ndim, \ '%s dimension is %d should be %d' % (classname, ob.ndim, ndim) if attrtype == np.ndarray: dtp = ioattr[3] assert ob.dtype.kind == dtp.kind, \ '%s dtype.kind is %s should be %s' % (classname, ob.dtype.kind, dtp.kind) elif hasattr(ob, attrname): if getattr(ob, attrname) is not None: obattr = getattr(ob, attrname) assert issubclass(type(obattr), attrtype), \ '%s in %s is %s should be %s' % \ (attrname, classname, type(obattr), attrtype) if attrtype == pq.Quantity or attrtype == np.ndarray: ndim = ioattr[2] assert obattr.ndim == ndim, \ '%s.%s dimension is %d should be %d' % \ (classname, attrname, obattr.ndim, ndim) if attrtype == np.ndarray: dtp = ioattr[3] assert obattr.dtype.kind == dtp.kind, \ '%s.%s dtype.kind is %s should be %s' % \ (classname, attrname, obattr.dtype.kind, dtp.kind) # test bijectivity : one_to_many_relationship and many_to_one_relationship if classname in description.one_to_many_relationship: for childname in description.one_to_many_relationship[classname]: if not hasattr(ob, childname.lower()+'s'): continue sub = getattr(ob, childname.lower()+'s') for i, child in enumerate(sub): assert hasattr(child, classname.lower()), \ '%s should have %s attribute (2 way relationship)' % \ (childname, classname.lower()) if hasattr(child, classname.lower()): assert getattr(child, classname.lower()) == ob, \ '%s.%s %s is not symetric with %s.%s s' % \ (childname, classname.lower(), i, classname, childname.lower()) # recursive on one to many rel if classname in description.one_to_many_relationship: for childname in description.one_to_many_relationship[classname]: if not hasattr(ob, childname.lower()+'s'): continue sub = getattr(ob, childname.lower()+'s') for i, child in enumerate(sub): try: assert_neo_object_is_compliant(child) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from %s %s of %s' % (childname, i, classname),) raise def assert_same_sub_schema(ob1, ob2, equal_almost=False, threshold=1e-10): ''' Test if ob1 and ob2 has the same sub schema. Explore all one_to_many_relationship. Many_to_many_relationship is not tested because of infinite recursive loops. Arguments: equal_almost: if False do a strict arrays_equal if True do arrays_almost_equal ''' assert type(ob1) == type(ob2), 'type(%s) != type(%s)' % (type(ob1), type(ob2)) classname = ob1.__class__.__name__ if isinstance(ob1, list): assert len(ob1) == len(ob2), \ 'lens %s and %s not equal for %s and %s' % \ (len(ob1), len(ob2), ob1, ob2) for i, (sub1, sub2) in enumerate(zip(ob1, ob2)): try: assert_same_sub_schema(sub1, sub2, equal_almost=equal_almost, threshold=threshold) # intercept exceptions and add more information except BaseException as exc: exc.args += ('%s[%s]' % (classname, i),) raise return if classname in description.one_to_many_relationship: # test one_to_many_relationship for child in description.one_to_many_relationship[classname]: if not hasattr(ob1, child.lower()+'s'): assert not hasattr(ob2, child.lower()+'s'), \ '%s 2 does have %s but not %s 1' % (classname, child, classname) continue else: assert hasattr(ob2, child.lower()+'s'), \ '%s 1 has %s but not %s 2' % (classname, child, classname) sub1 = getattr(ob1, child.lower()+'s') sub2 = getattr(ob2, child.lower()+'s') assert len(sub1) == len(sub2), \ 'theses two %s do not have the same %s number: %s and %s' % \ (classname, child, len(sub1), len(sub2)) for i in range(len(getattr(ob1, child.lower()+'s'))): # previously lacking parameter try: assert_same_sub_schema(sub1[i], sub2[i], equal_almost, threshold) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from %s[%s] of %s' % (child, i, classname),) raise # check if all attributes are equal if equal_almost: def assert_arrays_equal_and_dtype(a, b): assert_arrays_equal(a, b) assert a.dtype == b.dtype, \ "%s and %s not same dtype %s and %s" % (a, b, a.dtype, b.dtype) assert_eg = assert_arrays_equal_and_dtype else: def assert_arrays_almost_and_dtype(a, b): assert_arrays_almost_equal(a, b, threshold) #assert a.dtype == b.dtype, \ #"%s and %s not same dtype %s %s" % (a, b, a.dtype, b.dtype) assert_eg = assert_arrays_almost_and_dtype necess = description.classes_necessary_attributes[classname] recomm = description.classes_recommended_attributes[classname] attributes = necess + recomm for ioattr in attributes: attrname, attrtype = ioattr[0], ioattr[1] #~ if attrname =='': if (classname in description.classes_inheriting_quantities and description.classes_inheriting_quantities[classname] == attrname): # object is hinerited from Quantity (AnalogSIgnal, SpikeTrain, ...) try: assert_eg(ob1.magnitude, ob2.magnitude) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from %s' % classname,) raise assert ob1.dimensionality.string == ob2.dimensionality.string, \ 'Units of %s are not the same: %s and %s' % \ (classname, ob1.dimensionality.string, ob2.dimensionality.string) continue if not hasattr(ob1, attrname): assert not hasattr(ob2, attrname), \ '%s 2 does have %s but not %s 1' % (classname, attrname, classname) continue else: assert hasattr(ob2, attrname), \ '%s 1 has %s but not %s 2' % (classname, attrname, classname) if getattr(ob1, attrname) is None: assert getattr(ob2, attrname) is None, \ 'In %s.%s %s and %s differed' % (classname, attrname, getattr(ob1, attrname), getattr(ob2, attrname)) continue if getattr(ob2, attrname) is None: assert getattr(ob1, attrname) is None, \ 'In %s.%s %s and %s differed' % (classname, attrname, getattr(ob1, attrname), getattr(ob2, attrname)) continue if attrtype == pq.Quantity: # Compare magnitudes mag1 = getattr(ob1, attrname).magnitude mag2 = getattr(ob2, attrname).magnitude #print "2. ob1(%s) %s:%s\n ob2(%s) %s:%s" % \ #(ob1,attrname,mag1,ob2,attrname,mag2) try: assert_eg(mag1, mag2) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from %s of %s' % (attrname, classname),) raise # Compare dimensionalities dim1 = getattr(ob1, attrname).dimensionality.simplified dim2 = getattr(ob2, attrname).dimensionality.simplified dimstr1 = getattr(ob1, attrname).dimensionality.string dimstr2 = getattr(ob2, attrname).dimensionality.string assert dim1 == dim2, \ 'Attribute %s of %s are not the same: %s != %s' % \ (attrname, classname, dimstr1, dimstr2) elif attrtype == np.ndarray: try: assert_eg(getattr(ob1, attrname), getattr(ob2, attrname)) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from %s of %s' % (attrname, classname),) raise else: #~ print 'yep', getattr(ob1, attrname), getattr(ob2, attrname) assert getattr(ob1, attrname) == getattr(ob2, attrname), \ 'Attribute %s.%s are not the same %s %s %s %s' % \ (classname, attrname, type(getattr(ob1, attrname)), getattr(ob1, attrname), type(getattr(ob2, attrname)), getattr(ob2, attrname)) def assert_sub_schema_is_lazy_loaded(ob): ''' This is util for testing lazy load. All object must load with ndarray.size or Quantity.size ==0 ''' classname = ob.__class__.__name__ if classname in description.one_to_many_relationship: for childname in description.one_to_many_relationship[classname]: if not hasattr(ob, childname.lower()+'s'): continue sub = getattr(ob, childname.lower()+'s') for i, child in enumerate(sub): try: assert_sub_schema_is_lazy_loaded(child) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from %s %s of %s' % (childname, i, classname),) raise necess = description.classes_necessary_attributes[classname] recomm = description.classes_recommended_attributes[classname] attributes = necess + recomm for ioattr in attributes: attrname, attrtype = ioattr[0], ioattr[1] #~ print 'xdsd', classname, attrname #~ if attrname == '': if (classname in description.classes_inheriting_quantities and description.classes_inheriting_quantities[classname] == attrname): assert ob.size == 0, \ 'Lazy loaded error %s.size = %s' % (classname, ob.size) assert hasattr(ob, 'lazy_shape'), \ 'Lazy loaded error, %s should have lazy_shape attribute' % \ classname continue if not hasattr(ob, attrname) or getattr(ob, attrname) is None: continue #~ print 'hjkjh' if (attrtype == pq.Quantity or attrtype == np.ndarray): # FIXME: it is a workaround for recordingChannelGroup.channel_names # which is nupy.array but allowed to be loaded when lazy == True if ob.__class__ == neo.RecordingChannelGroup: continue ndim = ioattr[2] #~ print 'ndim', ndim #~ print getattr(ob, attrname).size if ndim >= 1: assert getattr(ob, attrname).size == 0, \ 'Lazy loaded error %s.%s.size = %s' % \ (classname, attrname, getattr(ob, attrname).size) assert hasattr(ob, 'lazy_shape'), \ 'Lazy loaded error ' +\ '%s should have lazy_shape attribute ' % classname +\ 'because of %s attribute' % attrname lazy_shape_arrays = {'SpikeTrain': 'times', 'Spike': 'waveform', 'AnalogSignal': 'signal', 'AnalogSignalArray': 'signal', 'EventArray': 'times', 'EpochArray': 'times'} def assert_lazy_sub_schema_can_be_loaded(ob, io): ''' This is util for testing lazy load. All object must load with ndarray.size or Quantity.size ==0 ''' classname = ob.__class__.__name__ if classname in lazy_shape_arrays: new_load = io.load_lazy_object(ob) assert hasattr(ob, 'lazy_shape'), \ 'Object %s was not lazy loaded' % classname assert not hasattr(new_load, 'lazy_shape'), \ 'Newly loaded object from %s was also lazy loaded' % classname if classname in description.classes_inheriting_quantities: assert ob.lazy_shape == new_load.shape, \ 'Shape of loaded object %sis not equal to lazy shape' % \ classname else: assert ob.lazy_shape == \ getattr(new_load, lazy_shape_arrays[classname]).shape, \ 'Shape of loaded object %s not equal to lazy shape' %\ classname elif classname in description.one_to_many_relationship: for childname in description.one_to_many_relationship[classname]: if not hasattr(ob, childname.lower() + 's'): continue sub = getattr(ob, childname.lower() + 's') for i, child in enumerate(sub): try: assert_lazy_sub_schema_can_be_loaded(child, io) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from of %s %s of %s' % (childname, i, classname),) raise def assert_objects_equivalent(obj1, obj2): ''' Compares two NEO objects by looping over the attributes and annotations and asserting their hashes. No relationships involved. ''' def assert_attr(obj1, obj2, attr_name): ''' Assert a single attribute and annotation are the same ''' assert hasattr(obj1, attr_name) attr1 = hashlib.md5(getattr(obj1, attr_name)).hexdigest() assert hasattr(obj2, attr_name) attr2 = hashlib.md5(getattr(obj2, attr_name)).hexdigest() assert attr1 == attr2, "Attribute %s for class %s is not equal." % \ (attr_name, description.name_by_class[obj1.__class__]) obj_type = description.name_by_class[obj1.__class__] assert obj_type == description.name_by_class[obj2.__class__] for ioattr in description.classes_necessary_attributes[obj_type]: assert_attr(obj1, obj2, ioattr[0]) for ioattr in description.classes_recommended_attributes[obj_type]: if hasattr(obj1, ioattr[0]) or hasattr(obj2, ioattr[0]): assert_attr(obj1, obj2, ioattr[0]) if hasattr(obj1, "annotations"): assert hasattr(obj2, "annotations") for key, value in obj1.annotations: assert hasattr(obj2.annotations, key) assert obj2.annotations[key] == value def assert_children_empty(obj, parent): ''' Check that the children of a neo object are empty. Used to check the cascade is implemented properly ''' classname = obj.__class__.__name__ errmsg = '''%s reader with cascade=False should return empty children''' % parent.__name__ try: childlist = description.one_to_many_relationship[classname] except KeyError: childlist = [] for childname in childlist: children = getattr(obj, childname.lower() + 's') assert len(children) == 0, errmsg neo-0.3.3/neo/test/test_recordingchannelgroup.py0000644000175000017500000002376612273723542023122 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.recordingchannelgroup.RecordingChannelGroup class """ try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.recordingchannelgroup import RecordingChannelGroup from neo.core.analogsignalarray import AnalogSignalArray from neo.core.recordingchannel import RecordingChannel from neo.core.unit import Unit from neo.test.tools import assert_arrays_equal, assert_neo_object_is_compliant from neo.io.tools import create_many_to_one_relationship class TestRecordingChannelGroup(unittest.TestCase): def setUp(self): self.setup_unit() self.setup_analogsignalarrays() self.setup_recordingchannels() self.setup_recordingchannelgroups() def setup_recordingchannelgroups(self): params = {'testarg2': 'yes', 'testarg3': True} self.rcg1 = RecordingChannelGroup(name='test', description='tester 1', file_origin='test.file', testarg1=1, **params) self.rcg2 = RecordingChannelGroup(name='test', description='tester 2', file_origin='test.file', testarg1=1, **params) self.rcg1.annotate(testarg1=1.1, testarg0=[1, 2, 3]) self.rcg2.annotate(testarg11=1.1, testarg10=[1, 2, 3]) self.rcg1.units = self.units1 self.rcg2.units = self.units2 self.rcg1.recordingchannels = self.rchan1 self.rcg2.recordingchannels = self.rchan2 self.rcg1.analogsignalarrays = self.sigarr1 self.rcg2.analogsignalarrays = self.sigarr2 create_many_to_one_relationship(self.rcg1) create_many_to_one_relationship(self.rcg2) def setup_unit(self): unitname11 = 'unit 1 1' unitname12 = 'unit 1 2' unitname21 = 'unit 2 1' unitname22 = 'unit 2 2' self.unitnames1 = [unitname11, unitname12] self.unitnames2 = [unitname21, unitname22, unitname11] self.unitnames = [unitname11, unitname12, unitname21, unitname22] unit11 = Unit(name=unitname11, channel_indexes=np.array([1])) unit12 = Unit(name=unitname12, channel_indexes=np.array([2])) unit21 = Unit(name=unitname21, channel_indexes=np.array([1])) unit22 = Unit(name=unitname22, channel_indexes=np.array([2])) unit23 = Unit(name=unitname11, channel_indexes=np.array([1])) self.units1 = [unit11, unit12] self.units2 = [unit21, unit22, unit23] self.units = [unit11, unit12, unit21, unit22] def setup_recordingchannels(self): rchanname11 = 'chan 1 1' rchanname12 = 'chan 1 2' rchanname21 = 'chan 2 1' rchanname22 = 'chan 2 2' self.rchannames1 = [rchanname11, rchanname12] self.rchannames2 = [rchanname21, rchanname22, rchanname11] self.rchannames = [rchanname11, rchanname12, rchanname21, rchanname22] rchan11 = RecordingChannel(name=rchanname11) rchan12 = RecordingChannel(name=rchanname12) rchan21 = RecordingChannel(name=rchanname21) rchan22 = RecordingChannel(name=rchanname22) rchan23 = RecordingChannel(name=rchanname11) self.rchan1 = [rchan11, rchan12] self.rchan2 = [rchan21, rchan22, rchan23] self.rchan = [rchan11, rchan12, rchan21, rchan22] def setup_analogsignalarrays(self): sigarrname11 = 'analogsignalarray 1 1' sigarrname12 = 'analogsignalarray 1 2' sigarrname21 = 'analogsignalarray 2 1' sigarrname22 = 'analogsignalarray 2 2' sigarrdata11 = np.arange(0, 10).reshape(5, 2) * pq.mV sigarrdata12 = np.arange(10, 20).reshape(5, 2) * pq.mV sigarrdata21 = np.arange(20, 30).reshape(5, 2) * pq.V sigarrdata22 = np.arange(30, 40).reshape(5, 2) * pq.V sigarrdata112 = np.hstack([sigarrdata11, sigarrdata11]) * pq.mV self.sigarrnames1 = [sigarrname11, sigarrname12] self.sigarrnames2 = [sigarrname21, sigarrname22, sigarrname11] self.sigarrnames = [sigarrname11, sigarrname12, sigarrname21, sigarrname22] sigarr11 = AnalogSignalArray(sigarrdata11, name=sigarrname11, sampling_rate=1*pq.Hz, channel_index=np.array([1])) sigarr12 = AnalogSignalArray(sigarrdata12, name=sigarrname12, sampling_rate=1*pq.Hz, channel_index=np.array([2])) sigarr21 = AnalogSignalArray(sigarrdata21, name=sigarrname21, sampling_rate=1*pq.Hz, channel_index=np.array([1])) sigarr22 = AnalogSignalArray(sigarrdata22, name=sigarrname22, sampling_rate=1*pq.Hz, channel_index=np.array([2])) sigarr23 = AnalogSignalArray(sigarrdata11, name=sigarrname11, sampling_rate=1*pq.Hz, channel_index=np.array([1])) sigarr112 = AnalogSignalArray(sigarrdata112, name=sigarrname11, sampling_rate=1*pq.Hz, channel_index=np.array([1])) self.sigarr1 = [sigarr11, sigarr12] self.sigarr2 = [sigarr21, sigarr22, sigarr23] self.sigarr = [sigarr112, sigarr12, sigarr21, sigarr22] def test__recordingchannelgroup__init_defaults(self): rcg = RecordingChannelGroup() assert_neo_object_is_compliant(rcg) self.assertEqual(rcg.name, None) self.assertEqual(rcg.file_origin, None) self.assertEqual(rcg.recordingchannels, []) self.assertEqual(rcg.analogsignalarrays, []) assert_arrays_equal(rcg.channel_names, np.array([], dtype='S')) assert_arrays_equal(rcg.channel_indexes, np.array([])) def test_recordingchannelgroup__init(self): rcg = RecordingChannelGroup(file_origin='temp.dat', channel_indexes=np.array([1])) assert_neo_object_is_compliant(rcg) self.assertEqual(rcg.file_origin, 'temp.dat') self.assertEqual(rcg.name, None) self.assertEqual(rcg.recordingchannels, []) self.assertEqual(rcg.analogsignalarrays, []) assert_arrays_equal(rcg.channel_names, np.array([], dtype='S')) assert_arrays_equal(rcg.channel_indexes, np.array([1])) def test_recordingchannelgroup__compliance(self): assert_neo_object_is_compliant(self.rcg1) assert_neo_object_is_compliant(self.rcg2) self.assertEqual(self.rcg1.name, 'test') self.assertEqual(self.rcg2.name, 'test') self.assertEqual(self.rcg1.description, 'tester 1') self.assertEqual(self.rcg2.description, 'tester 2') self.assertEqual(self.rcg1.file_origin, 'test.file') self.assertEqual(self.rcg2.file_origin, 'test.file') self.assertEqual(self.rcg1.annotations['testarg0'], [1, 2, 3]) self.assertEqual(self.rcg2.annotations['testarg10'], [1, 2, 3]) self.assertEqual(self.rcg1.annotations['testarg1'], 1.1) self.assertEqual(self.rcg2.annotations['testarg1'], 1) self.assertEqual(self.rcg2.annotations['testarg11'], 1.1) self.assertEqual(self.rcg1.annotations['testarg2'], 'yes') self.assertEqual(self.rcg2.annotations['testarg2'], 'yes') self.assertTrue(self.rcg1.annotations['testarg3']) self.assertTrue(self.rcg2.annotations['testarg3']) self.assertTrue(hasattr(self.rcg1, 'units')) self.assertTrue(hasattr(self.rcg2, 'units')) self.assertEqual(len(self.rcg1.units), 2) self.assertEqual(len(self.rcg2.units), 3) self.assertEqual(self.rcg1.units, self.units1) self.assertEqual(self.rcg2.units, self.units2) self.assertTrue(hasattr(self.rcg1, 'recordingchannels')) self.assertTrue(hasattr(self.rcg2, 'recordingchannels')) self.assertEqual(len(self.rcg1.recordingchannels), 2) self.assertEqual(len(self.rcg2.recordingchannels), 3) for res, targ in zip(self.rcg1.recordingchannels, self.rchan1): self.assertEqual(res.name, targ.name) for res, targ in zip(self.rcg2.recordingchannels, self.rchan2): self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.rcg1, 'analogsignalarrays')) self.assertTrue(hasattr(self.rcg2, 'analogsignalarrays')) self.assertEqual(len(self.rcg1.analogsignalarrays), 2) self.assertEqual(len(self.rcg2.analogsignalarrays), 3) for res, targ in zip(self.rcg1.analogsignalarrays, self.sigarr1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.rcg2.analogsignalarrays, self.sigarr2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) def test_recordingchannelgroup__merge(self): self.rcg1.merge(self.rcg2) chanres1 = [chan.name for chan in self.rcg1.recordingchannels] chanres2 = [chan.name for chan in self.rcg2.recordingchannels] unitres1 = [unit.name for unit in self.rcg1.units] unitres2 = [unit.name for unit in self.rcg2.units] sigarrres1 = [sigarr.name for sigarr in self.rcg1.analogsignalarrays] sigarrres2 = [sigarr.name for sigarr in self.rcg2.analogsignalarrays] self.assertEqual(chanres1, self.rchannames) self.assertEqual(chanres2, self.rchannames2) self.assertEqual(unitres1, self.unitnames) self.assertEqual(unitres2, self.unitnames2) self.assertEqual(sigarrres1, self.sigarrnames) self.assertEqual(sigarrres2, self.sigarrnames2) for res, targ in zip(self.rcg1.analogsignalarrays, self.sigarr): assert_arrays_equal(res, targ) for res, targ in zip(self.rcg2.analogsignalarrays, self.sigarr2): assert_arrays_equal(res, targ) if __name__ == '__main__': unittest.main() neo-0.3.3/neo/test/__init__.py0000644000175000017500000000015712265516260017223 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Contains tests for neo.core The subdirectory iotest contains tests for neo.io """ neo-0.3.3/neo/test/test_block.py0000644000175000017500000001336512273723542017624 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.block.Block class """ try: import unittest2 as unittest except ImportError: import unittest from neo.core.block import Block from neo.core.recordingchannelgroup import RecordingChannelGroup from neo.core.recordingchannel import RecordingChannel from neo.core.segment import Segment from neo.core.unit import Unit from neo.io.tools import create_many_to_one_relationship from neo.test.tools import assert_neo_object_is_compliant class TestBlock(unittest.TestCase): def setUp(self): unitname11 = 'unit 1 1' unitname12 = 'unit 1 2' unitname21 = 'unit 2 1' unitname22 = 'unit 2 2' channame11 = 'chan 1 1' channame12 = 'chan 1 2' channame21 = 'chan 2 1' channame22 = 'chan 2 2' segname11 = 'seg 1 1' segname12 = 'seg 1 2' segname21 = 'seg 2 1' segname22 = 'seg 2 2' self.rcgname1 = 'rcg 1' self.rcgname2 = 'rcg 2' self.unitnames1 = [unitname11, unitname12] self.unitnames2 = [unitname21, unitname22, unitname11] self.unitnames = [unitname11, unitname12, unitname21, unitname22] self.channames1 = [channame11, channame12] self.channames2 = [channame21, channame22, channame11] self.channames = [channame11, channame12, channame21, channame22] self.segnames1 = [segname11, segname12] self.segnames2 = [segname21, segname22, segname11] self.segnames = [segname11, segname12, segname21, segname22] unit11 = Unit(name=unitname11) unit12 = Unit(name=unitname12) unit21 = Unit(name=unitname21) unit22 = Unit(name=unitname22) unit23 = unit11 chan11 = RecordingChannel(name=channame11) chan12 = RecordingChannel(name=channame12) chan21 = RecordingChannel(name=channame21) chan22 = RecordingChannel(name=channame22) chan23 = chan11 seg11 = Segment(name=segname11) seg12 = Segment(name=segname12) seg21 = Segment(name=segname21) seg22 = Segment(name=segname22) seg23 = seg11 self.units1 = [unit11, unit12] self.units2 = [unit21, unit22, unit23] self.units = [unit11, unit12, unit21, unit22] self.chan1 = [chan11, chan12] self.chan2 = [chan21, chan22, chan23] self.chan = [chan11, chan12, chan21, chan22] self.seg1 = [seg11, seg12] self.seg2 = [seg21, seg22, seg23] self.seg = [seg11, seg12, seg21, seg22] self.rcg1 = RecordingChannelGroup(name=self.rcgname1) self.rcg2 = RecordingChannelGroup(name=self.rcgname2) self.rcg1.units = self.units1 self.rcg2.units = self.units2 self.rcg1.recordingchannels = self.chan1 self.rcg2.recordingchannels = self.chan2 def test_block_init(self): blk = Block(name='a block') assert_neo_object_is_compliant(blk) self.assertEqual(blk.name, 'a block') self.assertEqual(blk.file_origin, None) def test_block_list_units(self): blk = Block(name='a block') blk.recordingchannelgroups = [self.rcg1, self.rcg2] create_many_to_one_relationship(blk) #assert_neo_object_is_compliant(blk) unitres1 = [unit.name for unit in blk.recordingchannelgroups[0].units] unitres2 = [unit.name for unit in blk.recordingchannelgroups[1].units] unitres = [unit.name for unit in blk.list_units] self.assertEqual(self.unitnames1, unitres1) self.assertEqual(self.unitnames2, unitres2) self.assertEqual(self.unitnames, unitres) def test_block_list_recordingchannel(self): blk = Block(name='a block') blk.recordingchannelgroups = [self.rcg1, self.rcg2] create_many_to_one_relationship(blk) #assert_neo_object_is_compliant(blk) chanres1 = [chan.name for chan in blk.recordingchannelgroups[0].recordingchannels] chanres2 = [chan.name for chan in blk.recordingchannelgroups[1].recordingchannels] chanres = [chan.name for chan in blk.list_recordingchannels] self.assertEqual(self.channames1, chanres1) self.assertEqual(self.channames2, chanres2) self.assertEqual(self.channames, chanres) def test_block_merge(self): blk1 = Block(name='block 1') blk2 = Block(name='block 2') rcg3 = RecordingChannelGroup(name=self.rcgname1) rcg3.units = self.units1 + [self.units2[0]] rcg3.recordingchannels = self.chan1 + [self.chan2[1]] blk1.recordingchannelgroups = [self.rcg1] blk2.recordingchannelgroups = [self.rcg2, rcg3] blk1.segments = self.seg1 blk2.segments = self.seg2 blk1.merge(blk2) rcgres1 = [rcg.name for rcg in blk1.recordingchannelgroups] rcgres2 = [rcg.name for rcg in blk2.recordingchannelgroups] segres1 = [seg.name for seg in blk1.segments] segres2 = [seg.name for seg in blk2.segments] chanres1 = [chan.name for chan in blk1.list_recordingchannels] chanres2 = [chan.name for chan in blk2.list_recordingchannels] unitres1 = [unit.name for unit in blk1.list_units] unitres2 = [unit.name for unit in blk2.list_units] self.assertEqual(rcgres1, [self.rcgname1, self.rcgname2]) self.assertEqual(rcgres2, [self.rcgname2, self.rcgname1]) self.assertEqual(segres1, self.segnames) self.assertEqual(segres2, self.segnames2) self.assertEqual(chanres1, self.channames1 + self.channames2[-2::-1]) self.assertEqual(chanres2, self.channames2[:-1] + self.channames1) self.assertEqual(unitres1, self.unitnames) self.assertEqual(unitres2, self.unitnames2[:-1] + self.unitnames1) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/0000755000175000017500000000000012273723667016430 5ustar sgarciasgarcia00000000000000neo-0.3.3/neo/test/iotest/test_klustakwikio.py0000644000175000017500000003570712265516260022564 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.klustakwikio """ # needed for python 3 compatibility from __future__ import absolute_import import glob import os.path import sys import tempfile try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq import neo from neo.test.iotest.common_io_test import BaseTestIO from neo.test.tools import assert_arrays_almost_equal from neo.io.klustakwikio import KlustaKwikIO, HAVE_MLAB @unittest.skipUnless(HAVE_MLAB, "requires matplotlib") @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class testFilenameParser(unittest.TestCase): """Tests that filenames can be loaded with or without basename. The test directory contains two basenames and some decoy files with malformed group numbers.""" def setUp(self): self.dirname = os.path.join(tempfile.gettempdir(), 'files_for_testing_neo', 'klustakwik/test1') if not os.path.exists(self.dirname): raise unittest.SkipTest('data directory does not exist: ' + self.dirname) def test1(self): """Tests that files can be loaded by basename""" kio = KlustaKwikIO(filename=os.path.join(self.dirname, 'basename')) if not BaseTestIO.use_network: raise unittest.SkipTest("Requires download of data from the web") fetfiles = kio._fp.read_filenames('fet') self.assertEqual(len(fetfiles), 2) self.assertEqual(os.path.abspath(fetfiles[0]), os.path.abspath(os.path.join(self.dirname, 'basename.fet.0'))) self.assertEqual(os.path.abspath(fetfiles[1]), os.path.abspath(os.path.join(self.dirname, 'basename.fet.1'))) def test2(self): """Tests that files are loaded even without basename""" pass # this test is in flux, should probably have it default to # basename = os.path.split(dirname)[1] when dirname is a directory #~ dirname = os.path.normpath('./files_for_tests/klustakwik/test1') #~ kio = KlustaKwikIO(filename=dirname) #~ fetfiles = kio._fp.read_filenames('fet') #~ # It will just choose one of the two basenames, depending on which #~ # is first, so just assert that it did something without error. #~ self.assertNotEqual(len(fetfiles), 0) def test3(self): """Tests that files can be loaded by basename2""" kio = KlustaKwikIO(filename=os.path.join(self.dirname, 'basename2')) if not BaseTestIO.use_network: raise unittest.SkipTest("Requires download of data from the web") clufiles = kio._fp.read_filenames('clu') self.assertEqual(len(clufiles), 1) self.assertEqual(os.path.abspath(clufiles[1]), os.path.abspath(os.path.join(self.dirname, 'basename2.clu.1'))) @unittest.skipUnless(HAVE_MLAB, "requires matplotlib") @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class testRead(unittest.TestCase): """Tests that data can be read from KlustaKwik files""" def setUp(self): self.dirname = os.path.join(tempfile.gettempdir(), 'files_for_testing_neo', 'klustakwik/test2') if not os.path.exists(self.dirname): raise unittest.SkipTest('data directory does not exist: ' + self.dirname) def test1(self): """Tests that data and metadata are read correctly""" kio = KlustaKwikIO(filename=os.path.join(self.dirname, 'base'), sampling_rate=1000.) block = kio.read()[0] seg = block.segments[0] self.assertEqual(len(seg.spiketrains), 4) for st in seg.spiketrains: self.assertEqual(st.units, np.array(1.0) * pq.s) self.assertEqual(st.t_start, 0.0) self.assertEqual(seg.spiketrains[0].name, 'unit 1 from group 0') self.assertEqual(seg.spiketrains[0].annotations['cluster'], 1) self.assertEqual(seg.spiketrains[0].annotations['group'], 0) self.assertTrue(np.all(seg.spiketrains[0].times == np.array([.100, .200]))) self.assertEqual(seg.spiketrains[1].name, 'unit 2 from group 0') self.assertEqual(seg.spiketrains[1].annotations['cluster'], 2) self.assertEqual(seg.spiketrains[1].annotations['group'], 0) self.assertEqual(seg.spiketrains[1].t_start, 0.0) self.assertTrue(np.all(seg.spiketrains[1].times == np.array([.305]))) self.assertEqual(seg.spiketrains[2].name, 'unit -1 from group 1') self.assertEqual(seg.spiketrains[2].annotations['cluster'], -1) self.assertEqual(seg.spiketrains[2].annotations['group'], 1) self.assertEqual(seg.spiketrains[2].t_start, 0.0) self.assertTrue(np.all(seg.spiketrains[2].times == np.array([.253]))) self.assertEqual(seg.spiketrains[3].name, 'unit 2 from group 1') self.assertEqual(seg.spiketrains[3].annotations['cluster'], 2) self.assertEqual(seg.spiketrains[3].annotations['group'], 1) self.assertEqual(seg.spiketrains[3].t_start, 0.0) self.assertTrue(np.all(seg.spiketrains[3].times == np.array([.050, .152]))) def test2(self): """Checks that cluster id autosets to 0 without clu file""" kio = KlustaKwikIO(filename=os.path.join(self.dirname, 'base2'), sampling_rate=1000.) block = kio.read()[0] seg = block.segments[0] self.assertEqual(len(seg.spiketrains), 1) self.assertEqual(seg.spiketrains[0].name, 'unit 0 from group 5') self.assertEqual(seg.spiketrains[0].annotations['cluster'], 0) self.assertEqual(seg.spiketrains[0].annotations['group'], 5) self.assertEqual(seg.spiketrains[0].t_start, 0.0) self.assertTrue(np.all(seg.spiketrains[0].times == np.array([0.026, 0.122, 0.228]))) @unittest.skipUnless(HAVE_MLAB, "requires matplotlib") @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class testWrite(unittest.TestCase): def setUp(self): self.dirname = os.path.join(tempfile.gettempdir(), 'files_for_testing_neo', 'klustakwik/test3') if not os.path.exists(self.dirname): raise unittest.SkipTest('data directory does not exist: ' + self.dirname) def test1(self): """Create clu and fet files based on spiketrains in a block. Checks that Files are created Converted to samples correctly Missing sampling rate are taken from IO reader default Spiketrains without cluster info are assigned to cluster 0 Spiketrains across segments are concatenated """ block = neo.Block() segment = neo.Segment() segment2 = neo.Segment() block.segments.append(segment) block.segments.append(segment2) # Fake spiketrain 1, will be sorted st1 = neo.SpikeTrain(times=[.002, .004, .006], units='s', t_stop=1.) st1.annotations['cluster'] = 0 st1.annotations['group'] = 0 segment.spiketrains.append(st1) # Fake spiketrain 1B, on another segment. No group specified, # default is 0. st1B = neo.SpikeTrain(times=[.106], units='s', t_stop=1.) st1B.annotations['cluster'] = 0 segment2.spiketrains.append(st1B) # Fake spiketrain 2 on same group, no sampling rate specified st2 = neo.SpikeTrain(times=[.001, .003, .011], units='s', t_stop=1.) st2.annotations['cluster'] = 1 st2.annotations['group'] = 0 segment.spiketrains.append(st2) # Fake spiketrain 3 on new group, with different sampling rate st3 = neo.SpikeTrain(times=[.05, .09, .10], units='s', t_stop=1.) st3.annotations['cluster'] = -1 st3.annotations['group'] = 1 segment.spiketrains.append(st3) # Fake spiketrain 4 on new group, without cluster info st4 = neo.SpikeTrain(times=[.005, .009], units='s', t_stop=1.) st4.annotations['group'] = 2 segment.spiketrains.append(st4) # Create empty directory for writing delete_test_session() # Create writer with default sampling rate kio = KlustaKwikIO(filename=os.path.join(self.dirname, 'base1'), sampling_rate=1000.) kio.write_block(block) # Check files were created for fn in ['.fet.0', '.fet.1', '.clu.0', '.clu.1']: self.assertTrue(os.path.exists(os.path.join(self.dirname, 'base1' + fn))) # Check files contain correct content # Spike times on group 0 data = file(os.path.join(self.dirname, 'base1.fet.0')).readlines() data = [int(d) for d in data] self.assertEqual(data, [0, 2, 4, 6, 1, 3, 11, 106]) # Clusters on group 0 data = file(os.path.join(self.dirname, 'base1.clu.0')).readlines() data = [int(d) for d in data] self.assertEqual(data, [2, 0, 0, 0, 1, 1, 1, 0]) # Spike times on group 1 data = file(os.path.join(self.dirname, 'base1.fet.1')).readlines() data = [int(d) for d in data] self.assertEqual(data, [0, 50, 90, 100]) # Clusters on group 1 data = file(os.path.join(self.dirname, 'base1.clu.1')).readlines() data = [int(d) for d in data] self.assertEqual(data, [1, -1, -1, -1]) # Spike times on group 2 data = file(os.path.join(self.dirname, 'base1.fet.2')).readlines() data = [int(d) for d in data] self.assertEqual(data, [0, 5, 9]) # Clusters on group 2 data = file(os.path.join(self.dirname, 'base1.clu.2')).readlines() data = [int(d) for d in data] self.assertEqual(data, [1, 0, 0]) # Empty out test session again delete_test_session() @unittest.skipUnless(HAVE_MLAB, "requires matplotlib") @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class testWriteWithFeatures(unittest.TestCase): def setUp(self): self.dirname = os.path.join(tempfile.gettempdir(), 'files_for_testing_neo', 'klustakwik/test4') if not os.path.exists(self.dirname): raise unittest.SkipTest('data directory does not exist: ' + self.dirname) def test1(self): """Create clu and fet files based on spiketrains in a block. Checks that Files are created Converted to samples correctly Missing sampling rate are taken from IO reader default Spiketrains without cluster info are assigned to cluster 0 Spiketrains across segments are concatenated """ block = neo.Block() segment = neo.Segment() segment2 = neo.Segment() block.segments.append(segment) block.segments.append(segment2) # Fake spiketrain 1 st1 = neo.SpikeTrain(times=[.002, .004, .006], units='s', t_stop=1.) st1.annotations['cluster'] = 0 st1.annotations['group'] = 0 wff = np.array([ [11.3, 0.2], [-0.3, 12.3], [3.0, -2.5]]) st1.annotations['waveform_features'] = wff segment.spiketrains.append(st1) # Create empty directory for writing if not os.path.exists(self.dirname): os.mkdir(self.dirname) delete_test_session(self.dirname) # Create writer kio = KlustaKwikIO(filename=os.path.join(self.dirname, 'base2'), sampling_rate=1000.) kio.write_block(block) # Check files were created for fn in ['.fet.0', '.clu.0']: self.assertTrue(os.path.exists(os.path.join(self.dirname, 'base2' + fn))) # Check files contain correct content fi = file(os.path.join(self.dirname, 'base2.fet.0')) # first line is nbFeatures self.assertEqual(fi.readline(), '2\n') # Now check waveforms and times are same data = fi.readlines() new_wff = [] new_times = [] for line in data: line_split = line.split() new_wff.append([float(val) for val in line_split[:-1]]) new_times.append(int(line_split[-1])) self.assertEqual(new_times, [2, 4, 6]) assert_arrays_almost_equal(wff, np.array(new_wff), .00001) # Clusters on group 0 data = file(os.path.join(self.dirname, 'base2.clu.0')).readlines() data = [int(d) for d in data] self.assertEqual(data, [1, 0, 0, 0]) # Now read the features and test same block = kio.read_block() train = block.segments[0].spiketrains[0] assert_arrays_almost_equal(wff, train.annotations['waveform_features'], .00001) # Empty out test session again delete_test_session(self.dirname) @unittest.skipUnless(HAVE_MLAB, "requires matplotlib") @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class CommonTests(BaseTestIO, unittest.TestCase): ioclass = KlustaKwikIO # These are the files it tries to read and test for compliance files_to_test = [ 'test2/base', 'test2/base2', ] # Will fetch from g-node if they don't already exist locally # How does it know to do this before any of the other tests? files_to_download = [ 'test1/basename.clu.0', 'test1/basename.fet.-1', 'test1/basename.fet.0', 'test1/basename.fet.1', 'test1/basename.fet.1a', 'test1/basename.fet.a1', 'test1/basename2.clu.1', 'test1/basename2.fet.1', 'test1/basename2.fet.1a', 'test2/base2.fet.5', 'test2/base.clu.0', 'test2/base.clu.1', 'test2/base.fet.0', 'test2/base.fet.1', 'test3/base1.clu.0', 'test3/base1.clu.1', 'test3/base1.clu.2', 'test3/base1.fet.0', 'test3/base1.fet.1', 'test3/base1.fet.2' ] def delete_test_session(dirname=None): """Removes all file in directory so we can test writing to it""" if dirname is None: dirname = os.path.join(os.path.dirname(__file__), 'files_for_tests/klustakwik/test3') for fi in glob.glob(os.path.join(dirname, '*')): os.remove(fi) if __name__ == '__main__': unittest.main() neo-0.3.3/neo/test/iotest/test_elphyio.py0000644000175000017500000000133512265516260021502 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.elphyio """ # needed for python 3 compatibility from __future__ import division import sys try: import unittest2 as unittest except ImportError: import unittest from neo.io import ElphyIO from neo.test.iotest.common_io_test import BaseTestIO @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class TestElphyIO(BaseTestIO, unittest.TestCase): ioclass = ElphyIO files_to_test = ['ElphyExample.DAT', 'ElphyExample_Mode1.dat', 'ElphyExample_Mode2.dat', 'ElphyExample_Mode3.dat', ] files_to_download = files_to_test if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/tools.py0000644000175000017500000003465312265516260020143 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' Common tools that are useful for neo.io object tests ''' # needed for python 3 compatibility from __future__ import absolute_import import logging import os import shutil import tempfile try: from urllib import urlretrieve # Py2 except ImportError: from urllib.request import urlretrieve # Py3 from neo.core import Block, Segment from neo.test.iotest.generate_datasets import generate_from_supported_objects def can_use_network(): ''' Return True if network access is allowed ''' if os.environ.get('NOSETESTS_NO_NETWORK', False): return False if os.environ.get('TRAVIS') == 'true': return False return True def make_all_directories(filename, localdir): ''' Make the directories needed to store test files ''' # handle case of multiple filenames if not hasattr(filename, 'lower'): for ifilename in filename: make_all_directories(ifilename, localdir) return fullpath = os.path.join(localdir, os.path.dirname(filename)) if os.path.dirname(filename) != '' and not os.path.exists(fullpath): if not os.path.exists(os.path.dirname(fullpath)): make_all_directories(os.path.dirname(filename), localdir) os.mkdir(fullpath) def download_test_file(filename, localdir, url): ''' Download a test file from a server if it isn't already available. filename is the name of the file. localdir is the local directory to store the file in. url is the remote url that the file should be downloaded from. ''' # handle case of multiple filenames if not hasattr(filename, 'lower'): for ifilename in filename: download_test_file(ifilename, localdir, url) return localfile = os.path.join(localdir, filename) distantfile = url + '/' + filename if not os.path.exists(localfile): logging.info('Downloading %s here %s', distantfile, localfile) urlretrieve(distantfile, localfile) def create_local_temp_dir(name, directory=None): ''' Create a directory for storing temporary files needed for testing neo If directory is None or not specified, automatically create the directory in {tempdir}/files_for_testing_neo on linux/unix/mac or {tempdir}\files_for_testing_neo on windows, where {tempdir} is the system temporary directory returned by tempfile.gettempdir(). ''' if directory is None: directory = os.path.join(tempfile.gettempdir(), 'files_for_testing_neo') if not os.path.exists(directory): os.mkdir(directory) directory = os.path.join(directory, name) if not os.path.exists(directory): os.mkdir(directory) return directory def close_object_safe(obj): ''' Close an object safely, ignoring errors For some io types, like HDF5IO, the file should be closed before being opened again in a test. Call this after the test is done to make sure the file is closed. ''' try: obj.close() except: pass def cleanup_test_file(mode, path, directory=None): ''' Remove test files or directories safely. mode is the mode of the io class, either 'file' or 'directory'. It can also be an io class object, or any other object with a 'mode' attribute. If that is the case, use the 'mode' attribute from the object. If directory is not None and path is not an absolute path already, use the file from the given directory. ''' if directory is not None and not os.path.isabs(path): path = os.path.join(directory, path) if hasattr(mode, 'mode'): mode = mode.mode if mode == 'file': if os.path.exists(path): os.remove(path) elif mode == 'dir': if os.path.exists(path): shutil.rmtree(path) def create_generic_io_object(ioclass, filename=None, directory=None, return_path=False, clean=False): ''' Create an io object in a generic way that can work with both file-based and directory-based io objects If filename is None, create a filename. If return_path is True, also return the full path to the file. If directory is not None and path is not an absolute path already, use the file from the given directory. If return_path is True, return the full path of the file along with the io object. return reader, path. Default is False. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. ''' # create a filename if none is provided if filename is None: filename = 'Generated0_%s' % ioclass.__name__ if (ioclass.mode == 'file' and len(ioclass.extensions) >= 1): filename += '.' + ioclass.extensions[0] # if a directory is provided add it if directory is not None and not os.path.isabs(filename): filename = os.path.join(directory, filename) if clean: cleanup_test_file(ioclass, filename) try: # actually create the object if ioclass.mode == 'file': ioobj = ioclass(filename=filename) elif ioclass.mode == 'dir': ioobj = ioclass(dirname=filename) else: ioobj = None except: print(filename) raise # return the full path if requested, otherwise don't if return_path: return ioobj, filename return ioobj def iter_generic_io_objects(ioclass, filenames, directory=None, return_path=False, clean=False): ''' Return an iterable over the io objects created from a list of filenames. The objects are automatically cleaned up afterwards. If directory is not None and path is not an absolute path already, use the file from the given directory. If return_path is True, yield the full path of the file along with the io object. yield reader, path. Default is False. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. ''' for filename in filenames: ioobj, path = create_generic_io_object(ioclass, filename=filename, directory=directory, return_path=True, clean=clean) if ioobj is None: continue if return_path: yield ioobj, path else: yield ioobj close_object_safe(ioobj) def create_generic_reader(ioobj, target=None, readall=False): ''' Create a function that can read the target object from a file. If target is None, use the first supported_objects from ioobj If target is False, use the 'read' method. If target is the Block or Segment class, use read_block or read_segment, respectively. If target is a string, use 'read_'+target. If readall is True, use the read_all_ method instead of the read_ method. Default is False. ''' if target is None: target = ioobj.supported_objects[0].__name__ if target == Block: if readall: return ioobj.read_all_blocks return ioobj.read_block elif target == Segment: if readall: return ioobj.read_all_segments return ioobj.read_segment elif not target: if readall: raise ValueError('readall cannot be True if target is False') return ioobj.read elif hasattr(target, 'lower'): if readall: return getattr(ioobj, 'read_all_%ss' % target.lower()) return getattr(ioobj, 'read_%s' % target.lower()) def iter_generic_readers(ioclass, filenames, directory=None, target=None, return_path=False, return_ioobj=False, clean=False, readall=False): ''' Iterate over functions that can read the target object from a list of filenames. If target is None, use the first supported_objects from ioobj If target is False, use the 'read' method. If target is the Block or Segment class, use read_block or read_segment, respectively. If target is a string, use 'read_'+target. If directory is not None and path is not an absolute path already, use the file from the given directory. If return_path is True, return the full path of the file along with the reader object. return reader, path. If return_ioobj is True, return the io object as well as the reader. return reader, ioobj. Default is False. If both return_path and return_ioobj is True, return reader, path, ioobj. Default is False. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. If readall is True, use the read_all_ method instead of the read_ method. Default is False. ''' for ioobj, path in iter_generic_io_objects(ioclass=ioclass, filenames=filenames, directory=directory, return_path=True, clean=clean): res = create_generic_reader(ioobj, target=target, readall=readall) if not return_path and not return_ioobj: yield res else: res = (res, ) if return_path: res = res + (path,) if return_ioobj: res = res + (ioobj,) yield res def create_generic_writer(ioobj, target=None): ''' Create a function that can write the target object to a file using the neo io object ioobj. If target is None, use the first supported_objects from ioobj If target is False, use the 'write' method. If target is the Block or Segment class, use write_block or write_segment, respectively. If target is a string, use 'write_'+target. ''' if target is None: target = ioobj.supported_objects[0].__name__ if target == Block: return ioobj.write_block elif target == Segment: return ioobj.write_segment elif not target: return ioobj.write elif hasattr(target, 'lower'): return getattr(ioobj, 'write_' + target.lower()) def read_generic(ioobj, target=None, cascade=True, lazy=False, readall=False, return_reader=False): ''' Read the target object from a file using the given neo io object ioobj. If target is None, use the first supported_objects from ioobj If target is False, use the 'write' method. If target is the Block or Segment class, use write_block or write_segment, respectively. If target is a string, use 'write_'+target. The cascade and lazy parameters are passed to the reader. Defaults are True and False, respectively. If readall is True, use the read_all_ method instead of the read_ method. Default is False. If return_reader is True, yield the io reader function as well as the object. yield obj, reader. Default is False. ''' obj_reader = create_generic_reader(ioobj, target=target, readall=readall) obj = obj_reader(cascade=cascade, lazy=lazy) if return_reader: return obj, obj_reader return obj def iter_read_objects(ioclass, filenames, directory=None, target=None, return_path=False, return_ioobj=False, return_reader=False, clean=False, readall=False, cascade=True, lazy=False): ''' Iterate over objects read from a list of filenames. If target is None, use the first supported_objects from ioobj If target is False, use the 'read' method. If target is the Block or Segment class, use read_block or read_segment, respectively. If target is a string, use 'read_'+target. If directory is not None and path is not an absolute path already, use the file from the given directory. If return_path is True, yield the full path of the file along with the object. yield obj, path. If return_ioobj is True, yield the io object as well as the object. yield obj, ioobj. Default is False. If return_reader is True, yield the io reader function as well as the object. yield obj, reader. Default is False. If some combination of return_path, return_ioobj, and return_reader is True, they are yielded in the order: obj, path, ioobj, reader. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. The cascade and lazy parameters are passed to the reader. Defaults are True and False, respectively. If readall is True, use the read_all_ method instead of the read_ method. Default is False. ''' for obj_reader, path, ioobj in iter_generic_readers(ioclass, filenames, directory=directory, target=target, return_path=True, return_ioobj=True, clean=clean, readall=readall): obj = obj_reader(cascade=cascade, lazy=lazy) if not return_path and not return_ioobj and not return_reader: yield obj else: obj = (obj, ) if return_path: obj = obj + (path,) if return_ioobj: obj = obj + (ioobj,) if return_reader: obj = obj + (obj_reader,) yield obj def write_generic(ioobj, target=None, obj=None, return_writer=False): ''' Write the target object to a file using the given neo io object ioobj. If target is None, use the first supported_objects from ioobj If target is False, use the 'write' method. If target is the Block or Segment class, use write_block or write_segment, respectively. If target is a string, use 'write_'+target. obj is the object to write. If obj is None, an object is created automatically for the io class. If return_writer is True, yield the io writer function as well as the object. yield obj, writer. Default is False. ''' if obj is None: supported_objects = ioobj.supported_objects obj = generate_from_supported_objects(supported_objects) obj_writer = create_generic_writer(ioobj, target=target) obj_writer(obj) if return_writer: return obj, obj_writer return obj neo-0.3.3/neo/test/iotest/test_neuroshareio.py0000644000175000017500000000056012265516260022533 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.neuroshareio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest #from neo.io import NeuroshareIO #~ class TestNeuroshareIO(unittest.TestCase): #~ pass if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_tdtio.py0000644000175000017500000000246312265516260021157 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.tdtio """ # needed for python 3 compatibility from __future__ import absolute_import, division import sys try: import unittest2 as unittest except ImportError: import unittest from neo.io import TdtIO from neo.test.iotest.common_io_test import BaseTestIO @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class TestTdtIOIO(BaseTestIO, unittest.TestCase, ): ioclass = TdtIO files_to_test = ['aep_05'] files_to_download = ['aep_05/Block-1/aep_05_Block-1.Tbk', 'aep_05/Block-1/aep_05_Block-1.Tdx', 'aep_05/Block-1/aep_05_Block-1.tev', 'aep_05/Block-1/aep_05_Block-1.tsq', #~ 'aep_05/Block-2/aep_05_Block-2.Tbk', #~ 'aep_05/Block-2/aep_05_Block-2.Tdx', #~ 'aep_05/Block-2/aep_05_Block-2.tev', #~ 'aep_05/Block-2/aep_05_Block-2.tsq', #~ 'aep_05/Block-3/aep_05_Block-3.Tbk', #~ 'aep_05/Block-3/aep_05_Block-3.Tdx', #~ 'aep_05/Block-3/aep_05_Block-3.tev', #~ 'aep_05/Block-3/aep_05_Block-3.tsq', ] if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_alphaomegaio.py0000644000175000017500000000110012265516260022445 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.alphaomegaio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.io import AlphaOmegaIO from neo.test.iotest.common_io_test import BaseTestIO class TestAlphaOmegaIO(BaseTestIO, unittest.TestCase): files_to_test = ['File_AlphaOmega_1.map', 'File_AlphaOmega_2.map'] files_to_download = files_to_test ioclass = AlphaOmegaIO if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/generate_datasets.py0000644000175000017500000001324112273723542022455 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' Generate datasets for testing ''' # needed for python 3 compatibility from __future__ import absolute_import import numpy as np from numpy.random import rand import quantities as pq from neo.core import (AnalogSignal, Block, EpochArray, EventArray, RecordingChannel, Segment, SpikeTrain) from neo.io.tools import (create_many_to_one_relationship, populate_RecordingChannel, iteritems) def generate_one_simple_block(block_name='block_0', nb_segment=3, supported_objects=[], **kws): bl = Block() # name = block_name) objects = supported_objects if Segment in objects: for s in range(nb_segment): seg = generate_one_simple_segment(seg_name="seg" + str(s), supported_objects=objects, **kws) bl.segments.append(seg) if RecordingChannel in objects: populate_RecordingChannel(bl) return bl def generate_one_simple_segment(seg_name='segment 0', supported_objects=[], nb_analogsignal=4, t_start=0.*pq.s, sampling_rate=10*pq.kHz, duration=6.*pq.s, nb_spiketrain=6, spikerate_range=[.5*pq.Hz, 12*pq.Hz], event_array_types={'stim': ['a', 'b', 'c', 'd'], 'enter_zone': ['one', 'two'], 'color': ['black', 'yellow', 'green'], }, event_array_size_range=[5, 20], epoch_array_types={'animal state': ['Sleep', 'Freeze', 'Escape'], 'light': ['dark', 'lighted'] }, epoch_array_duration_range=[.5, 3.], ): seg = Segment(name=seg_name) if AnalogSignal in supported_objects: for a in range(nb_analogsignal): anasig = AnalogSignal(rand(int(sampling_rate * duration)), sampling_rate=sampling_rate, t_start=t_start, units=pq.mV, channel_index=a, name='sig %d for segment %s' % (a, seg.name)) seg.analogsignals.append(anasig) if SpikeTrain in supported_objects: for s in range(nb_spiketrain): spikerate = rand()*np.diff(spikerate_range) spikerate += spikerate_range[0].magnitude #spikedata = rand(int((spikerate*duration).simplified))*duration #sptr = SpikeTrain(spikedata, # t_start=t_start, t_stop=t_start+duration) # #, name = 'spiketrain %d'%s) spikes = rand(int((spikerate*duration).simplified)) spikes.sort() # spikes are supposed to be an ascending sequence sptr = SpikeTrain(spikes*duration, t_start=t_start, t_stop=t_start+duration) sptr.annotations['channel_index'] = s seg.spiketrains.append(sptr) if EventArray in supported_objects: for name, labels in iteritems(event_array_types): ea_size = rand()*np.diff(event_array_size_range) ea_size += event_array_size_range[0] labels = np.array(labels, dtype='S') labels = labels[(rand(ea_size)*len(labels)).astype('i')] ea = EventArray(times=rand(ea_size)*duration, labels=labels) seg.eventarrays.append(ea) if EpochArray in supported_objects: for name, labels in iteritems(epoch_array_types): t = 0 times = [] durations = [] while t < duration: times.append(t) dur = rand()*np.diff(epoch_array_duration_range) dur += epoch_array_duration_range[0] durations.append(dur) t = t+dur labels = np.array(labels, dtype='S') labels = labels[(rand(len(times))*len(labels)).astype('i')] epa = EpochArray(times=pq.Quantity(times, units=pq.s), durations=pq.Quantity([x[0] for x in durations], units=pq.s), labels=labels, ) seg.epocharrays.append(epa) # TODO : Spike, Event, Epoch return seg def generate_from_supported_objects(supported_objects): #~ create_many_to_one_relationship objects = supported_objects if Block in objects: higher = generate_one_simple_block(supported_objects=objects) # Chris we do not create RC and RCG if it is not in objects # there is a test in generate_one_simple_block so I removed #finalize_block(higher) elif Segment in objects: higher = generate_one_simple_segment(supported_objects=objects) else: #TODO return None create_many_to_one_relationship(higher) return higher neo-0.3.3/neo/test/iotest/__init__.py0000644000175000017500000000007212265516260020526 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Contains tests for neo.io """ neo-0.3.3/neo/test/iotest/test_neomatlabio.py0000644000175000017500000000106712265516260022325 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.neomatlabio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.test.iotest.common_io_test import BaseTestIO from neo.io.neomatlabio import NeoMatlabIO, HAVE_SCIPY @unittest.skipUnless(HAVE_SCIPY, "requires scipy") class TestNeoMatlabIO(BaseTestIO, unittest.TestCase): ioclass = NeoMatlabIO files_to_test = [] files_to_download = [] if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_hdf5io.py0000644000175000017500000002453612273723542021221 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.hdf5io Usually I run these tests like that. I add neo root folder to the pythonpath (usually by adding the neo.pth with the path to the cloned repository to, say, /usr/lib/python2.6/dist-packages/) and run python /test/io/test_hdf5io.py For the moment only basic tests are active. #TODO add performance testing!! """ # needed for python 3 compatibility from __future__ import absolute_import from datetime import datetime from hashlib import md5 import logging import os import sys try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core import SpikeTrain, Segment, Block from neo.test.tools import (assert_neo_object_is_compliant, assert_objects_equivalent, assert_same_sub_schema) from neo.test.iotest.common_io_test import BaseTestIO from neo.description import (class_by_name, classes_necessary_attributes, classes_recommended_attributes, implicit_relationship, many_to_many_relationship, name_by_class, one_to_many_relationship) from neo.io.hdf5io import NeoHdf5IO, HAVE_TABLES #============================================================================== TEST_ANNOTATIONS = [1, 0, 1.5, "this is a test", datetime.now(), None] def get_fake_value(attr): # attr = (name, type, [dim, [dtype]]) """ returns default value for a given attribute based on description.py """ if attr[1] == pq.Quantity or attr[1] == np.ndarray: size = [] for i in range(int(attr[2])): size.append(np.random.randint(100) + 1) to_set = np.random.random(size) * pq.millisecond # let it be ms if attr[0] == 't_start': to_set = 0.0 * pq.millisecond if attr[0] == 't_stop': to_set = 1.0 * pq.millisecond if attr[0] == 'sampling_rate': to_set = 10000.0 * pq.Hz if attr[1] == np.ndarray: to_set = np.array(to_set, dtype=attr[3]) if attr[1] == str: to_set = str(np.random.randint(100000)) if attr[1] == int: to_set = np.random.randint(100) if attr[1] == datetime: to_set = datetime.now() return to_set def fake_NEO(obj_type="Block", cascade=True, _follow_links=True): """ Create a fake NEO object of a given type. Follows one-to-many and many-to-many relationships if cascade. RC, when requested cascade, will not create RGCs to avoid dead-locks. _follow_links - an internal variable, indicates whether to create objects with 'implicit' relationships, to avoid duplications. Do not use it. """ kwargs = {} # assign attributes attrs = classes_necessary_attributes[obj_type] + \ classes_recommended_attributes[obj_type] for attr in attrs: kwargs[attr[0]] = get_fake_value(attr) obj = class_by_name[obj_type](**kwargs) if cascade: if obj_type == "Block": _follow_links = False if obj_type in one_to_many_relationship: rels = one_to_many_relationship[obj_type] if obj_type == "RecordingChannelGroup": rels += many_to_many_relationship[obj_type] if not _follow_links and obj_type in implicit_relationship: for i in implicit_relationship[obj_type]: if not i in rels: logging.debug("LOOK HERE!!!" + str(obj_type)) rels.remove(i) for child in rels: setattr(obj, child.lower() + "s", [fake_NEO(child, cascade, _follow_links)]) if obj_type == "Block": # need to manually create 'implicit' connections # connect a unit to the spike and spike train u = obj.recordingchannelgroups[0].units[0] st = obj.segments[0].spiketrains[0] sp = obj.segments[0].spikes[0] u.spiketrains.append(st) u.spikes.append(sp) # connect RCG with ASA asa = obj.segments[0].analogsignalarrays[0] obj.recordingchannelgroups[0].analogsignalarrays.append(asa) # connect RC to AS, IrSAS and back to RGC rc = obj.recordingchannelgroups[0].recordingchannels[0] rc.recordingchannelgroups.append(obj.recordingchannelgroups[0]) rc.analogsignals.append(obj.segments[0].analogsignals[0]) seg = obj.segments[0] rc.irregularlysampledsignals.append(seg.irregularlysampledsignals[0]) # add some annotations, 80% at = dict([(str(x), TEST_ANNOTATIONS[x]) for x in range(len(TEST_ANNOTATIONS))]) obj.annotate(**at) return obj class HDF5Commontests(BaseTestIO, unittest.TestCase): ioclass = NeoHdf5IO files_to_test = ['test.h5'] files_to_download = files_to_test @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") @unittest.skipUnless(HAVE_TABLES, "requires PyTables") def setUp(self): BaseTestIO.setUp(self) class hdf5ioTest: # inherit this class from unittest.TestCase when ready """ Tests for the hdf5 library. """ #@unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") #@unittest.skipUnless(HAVE_TABLES, "requires PyTables") def setUp(self): self.test_file = "test.h5" def tearDown(self): if os.path.exists(self.test_file): os.remove(self.test_file) def test_create(self): """ Create test file with signals, segments, blocks etc. """ iom = NeoHdf5IO(filename=self.test_file) b1 = fake_NEO() # creating a structure iom.save(b1) # saving # must be assigned after save self.assertTrue(hasattr(b1, "hdf5_path")) iom.close() iom.connect(filename=self.test_file) b2 = iom.get(b1.hdf5_path) # new object assert_neo_object_is_compliant(b2) assert_same_sub_schema(b1, b2) def test_property_change(self): """ Make sure all attributes are saved properly after the change, including quantities, units, types etc.""" iom = NeoHdf5IO(filename=self.test_file) for obj_type in class_by_name.keys(): obj = fake_NEO(obj_type, cascade=False) iom.save(obj) self.assertTrue(hasattr(obj, "hdf5_path")) replica = iom.get(obj.hdf5_path, cascade=False) assert_objects_equivalent(obj, replica) def test_relations(self): """ make sure the change in relationships is saved properly in the file, including correct M2M, no redundancy etc. RC -> RCG not tested. """ def assert_children(self, obj, replica): obj_type = name_by_class[obj] self.assertEqual(md5(str(obj)).hexdigest(), md5(str(replica)).hexdigest()) if obj_type in one_to_many_relationship: rels = one_to_many_relationship[obj_type] if obj_type == "RecordingChannelGroup": rels += many_to_many_relationship[obj_type] for child_type in rels: ch1 = getattr(obj, child_type.lower() + "s") ch2 = getattr(replica, child_type.lower() + "s") self.assertEqual(len(ch1), len(ch2)) for i, v in enumerate(ch1): self.assert_children(ch1[i], ch2[i]) iom = NeoHdf5IO(filename=self.test_file) for obj_type in class_by_name.keys(): obj = fake_NEO(obj_type, cascade=True) iom.save(obj) self.assertTrue(hasattr(obj, "hdf5_path")) replica = iom.get(obj.hdf5_path, cascade=True) self.assert_children(obj, replica) def test_errors(self): """ some tests for specific errors """ f = open("thisisafakehdf.h5", "w") # wrong file type f.write("this is not an HDF5 file. sorry.") f.close() self.assertRaises(TypeError, NeoHdf5IO(filename="thisisafakehdf.h5")) iom = NeoHdf5IO(filename=self.test_file) # wrong object path test self.assertRaises(LookupError, iom.get("/wrong_path")) some_object = np.array([1, 2, 3]) # non NEO object test self.assertRaises(AssertionError, iom.save(some_object)) def test_attr_changes(self): """ gets an object, changes its attributes, saves it, then compares how good the changes were saved. """ iom = NeoHdf5IO(filename=self.test_file) for obj_type in class_by_name.keys(): obj = fake_NEO(obj_type=obj_type, cascade=False) iom.save(obj) orig_obj = iom.get(obj.hdf5_path) attrs = (classes_necessary_attributes[obj_type] + classes_recommended_attributes[obj_type]) for attr in attrs: if hasattr(orig_obj, attr[0]): setattr(obj, attr[0], get_fake_value(attr)) iom.save(orig_obj) test_obj = iom.get(orig_obj.hdf5_path) assert_objects_equivalent(orig_obj, test_obj) # changes!!! in attr AS WELL AS in relations!! # test annotations # test naming - paths # unicode!! # add a child, then remove, then check it's removed # update/removal of relations b/w RC and AS which are/not are in the # same segment class HDF5MoreTests(unittest.TestCase): @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") @unittest.skipUnless(HAVE_TABLES, "requires PyTables") def test_store_empty_spike_train(self): spiketrain0 = SpikeTrain([], t_start=0.0, t_stop=100.0, units="ms") spiketrain1 = SpikeTrain([23.4, 45.6, 67.8], t_start=0.0, t_stop=100.0, units="ms") segment = Segment(name="a_segment") segment.spiketrains.append(spiketrain0) segment.spiketrains.append(spiketrain1) block = Block(name="a_block") block.segments.append(segment) iom = NeoHdf5IO(filename="test987.h5") iom.save(block) iom.close() iom = NeoHdf5IO(filename="test987.h5") block1 = iom.get("/Block_0") self.assertEqual(block1.segments[0].spiketrains[0].t_stop, 100.0) self.assertEqual(len(block1.segments[0].spiketrains[0]), 0) self.assertEqual(len(block1.segments[0].spiketrains[1]), 3) iom.close() os.remove("test987.h5") if __name__ == '__main__': unittest.main() neo-0.3.3/neo/test/iotest/test_baseio.py0000644000175000017500000000152512265516260021274 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.baseio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.core import objectlist from neo.io.baseio import BaseIO class TestIOObjects(unittest.TestCase): def test__raise_error_when_not_readable_or_writable(self): reader = BaseIO() for ob in objectlist: if ob not in BaseIO.readable_objects: meth = getattr(reader, 'read_'+ob.__name__.lower()) self.assertRaises(AssertionError, meth, ) if ob not in BaseIO.writeable_objects: meth = getattr(reader, 'write_'+ob.__name__.lower()) self.assertRaises(AssertionError, meth, ()) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/common_io_test.py0000644000175000017500000005431212265516260022013 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' Common tests for IOs: * check presence of all necessary attr * check types * write/read consistency See BaseTestIO. The public URL is in url_for_tests. The private url for writing is ssh://gate.g-node.org/groups/neo/io_test_files/ ''' # needed for python 3 compatibility from __future__ import absolute_import __test__ = False url_for_tests = "https://portal.g-node.org/neo/" import os try: import unittest2 as unittest except ImportError: import unittest from neo.core import Block, Segment from neo.test.tools import (assert_same_sub_schema, assert_neo_object_is_compliant, assert_sub_schema_is_lazy_loaded, assert_lazy_sub_schema_can_be_loaded, assert_children_empty) from neo.test.iotest.tools import (can_use_network, cleanup_test_file, close_object_safe, create_generic_io_object, create_generic_reader, create_generic_writer, create_local_temp_dir, download_test_file, iter_generic_io_objects, iter_generic_readers, iter_read_objects, make_all_directories, read_generic, write_generic) from neo.test.iotest.generate_datasets import generate_from_supported_objects class BaseTestIO(object): ''' This class make common tests for all IOs. Several startegies: * for IO able to read write : test_write_then_read * for IO able to read write with hash conservation (optional): test_read_then_write * for all IOs : test_assert_readed_neo_object_is_compliant 2 cases: * files are at G-node and downloaded: download_test_files_if_not_present * files are generated by MyIO.write() ''' #~ __test__ = False # all IO test need to modify this: ioclass = None # the IOclass to be tested files_to_test = [] # list of files to test compliances files_to_download = [] # when files are at G-Node # when reading then writing produces files with identical hashes hash_conserved_when_write_read = False # when writing then reading creates an identical neo object read_and_write_is_bijective = True # allow environment to tell avoid using network use_network = can_use_network() local_test_dir = None def setUp(self): ''' Set up the test fixture. This is run for every test ''' self.higher = self.ioclass.supported_objects[0] self.shortname = self.ioclass.__name__.lower().strip('io') # these objects can both be written and read self.io_readandwrite = list(set(self.ioclass.readable_objects) & set(self.ioclass.writeable_objects)) # these objects can be either written or read self.io_readorwrite = list(set(self.ioclass.readable_objects) | set(self.ioclass.writeable_objects)) self.create_local_dir_if_not_exists() self.download_test_files_if_not_present() self.files_generated = [] self.generate_files_for_io_able_to_write() self.files_to_test.extend(self.files_generated) self.cascade_modes = [True] if hasattr(self.ioclass, 'load_lazy_cascade'): self.cascade_modes.append('lazy') def create_local_dir_if_not_exists(self): ''' Create a local directory to store testing files and return it. The directory path is also written to self.local_test_dir ''' self.local_test_dir = create_local_temp_dir(self.shortname) return self.local_test_dir def download_test_files_if_not_present(self): ''' Download %s file at G-node for testing url_for_tests is global at beginning of this file. ''' % self.ioclass.__name__ if not self.use_network: raise unittest.SkipTest("Requires download of data from the web") url = url_for_tests+self.shortname try: make_all_directories(self.files_to_download, self.local_test_dir) download_test_file(self.files_to_download, self.local_test_dir, url) except IOError as exc: raise unittest.SkipTest(exc) download_test_files_if_not_present.__test__ = False def cleanup_file(self, path): ''' Remove test files or directories safely. ''' cleanup_test_file(self.ioclass, path, directory=self.local_test_dir) def able_to_write_or_read(self, writeread=False, readwrite=False): ''' Return True if generalized writing or reading is possible. If writeread=True, return True if writing then reading is possible and produces identical neo objects. If readwrite=True, return True if reading then writing is possible and produces files with identical hashes. ''' # Find the highest object that is supported by the IO # Test only if it is a Block or Segment, and if it can both read # and write this object. if self.higher not in self.io_readandwrite: return False if self.higher not in [Block, Segment]: return False # when io need external knowldge for writting or read such as # sampling_rate (RawBinaryIO...) the test is too much complex to design # genericaly. if (self.higher in self.ioclass.read_params and len(self.ioclass.read_params[self.higher]) != 0): return False # handle cases where the test should write then read if writeread and not self.read_and_write_is_bijective: return False # handle cases where the test should read then write if readwrite and not self.hash_conserved_when_write_read: return False return True def get_filename_path(self, filename): ''' Get the path to a filename in the current temporary file directory ''' return os.path.join(self.local_test_dir, filename) def generic_io_object(self, filename=None, return_path=False, clean=False): ''' Create an io object in a generic way that can work with both file-based and directory-based io objects. If filename is None, create a filename (default). If return_path is True, return the full path of the file along with the io object. return ioobj, path. Default is False. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. ''' return create_generic_io_object(ioclass=self.ioclass, filename=filename, directory=self.local_test_dir, return_path=return_path, clean=clean) def create_file_reader(self, filename=None, return_path=False, clean=False, target=None, readall=False): ''' Create a function that can read from the specified filename. If filename is None, create a filename (default). If return_path is True, return the full path of the file along with the reader function. return reader, path. Default is False. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. If target is None, use the first supported_objects from ioobj If target is False, use the 'read' method. If target is the Block or Segment class, use read_block or read_segment, respectively. If target is a string, use 'read_'+target. If readall is True, use the read_all_ method instead of the read_ method. Default is False. ''' ioobj, path = self.generic_io_object(filename=filename, return_path=True, clean=clean) res = create_generic_reader(ioobj, target=target, readall=readall) if return_path: return res, path return res def create_file_writer(self, filename=None, return_path=False, clean=False, target=None): ''' Create a function that can write from the specified filename. If filename is None, create a filename (default). If return_path is True, return the full path of the file along with the writer function. return writer, path. Default is False. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. If target is None, use the first supported_objects from ioobj If target is False, use the 'write' method. If target is the Block or Segment class, use write_block or write_segment, respectively. If target is a string, use 'write_'+target. ''' ioobj, path = self.generic_io_object(filename=filename, return_path=True, clean=clean) res = create_generic_writer(ioobj, target=target) if return_path: return res, path return res def read_file(self, filename=None, return_path=False, clean=False, target=None, readall=False, cascade=True, lazy=False): ''' Read from the specified filename. If filename is None, create a filename (default). If return_path is True, return the full path of the file along with the object. return obj, path. Default is False. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. If target is None, use the first supported_objects from ioobj If target is False, use the 'read' method. If target is the Block or Segment class, use read_block or read_segment, respectively. If target is a string, use 'read_'+target. The cascade and lazy parameters are passed to the reader. Defaults are True and False, respectively. If readall is True, use the read_all_ method instead of the read_ method. Default is False. ''' ioobj, path = self.generic_io_object(filename=filename, return_path=True, clean=clean) obj = read_generic(ioobj, target=target, cascade=cascade, lazy=lazy, readall=readall, return_reader=False) if return_path: return obj, path return obj def write_file(self, obj=None, filename=None, return_path=False, clean=False, target=None): ''' Write the target object to a file using the given neo io object ioobj. If filename is None, create a filename (default). If return_path is True, return the full path of the file along with the object. return obj, path. Default is False. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. If target is None, use the first supported_objects from ioobj If target is False, use the 'read' method. If target is the Block or Segment class, use read_block or read_segment, respectively. If target is a string, use 'read_'+target. obj is the object to write. If obj is None, an object is created automatically for the io class. ''' ioobj, path = self.generic_io_object(filename=filename, return_path=True, clean=clean) obj = write_generic(ioobj, target=target, return_reader=False) if return_path: return obj, path return obj def iter_io_objects(self, return_path=False, clean=False): ''' Return an iterable over the io objects created from files_to_test If return_path is True, yield the full path of the file along with the io object. yield ioobj, path Default is False. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. ''' return iter_generic_io_objects(ioclass=self.ioclass, filenames=self.files_to_test, directory=self.local_test_dir, return_path=return_path, clean=clean) def iter_readers(self, target=None, readall=False, return_path=False, return_ioobj=False, clean=False): ''' Return an iterable over readers created from files_to_test. If return_path is True, return the full path of the file along with the reader object. return reader, path. If return_ioobj is True, return the io object as well as the reader. return reader, ioobj. Default is False. If both return_path and return_ioobj is True, return reader, path, ioobj. Default is False. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. If readall is True, use the read_all_ method instead of the read_ method. Default is False. ''' return iter_generic_readers(ioclass=self.ioclass, filenames=self.files_to_test, directory=self.local_test_dir, return_path=return_path, return_ioobj=return_ioobj, target=target, clean=clean, readall=readall) def iter_objects(self, target=None, return_path=False, return_ioobj=False, return_reader=False, clean=False, readall=False, cascade=True, lazy=False): ''' Iterate over objects read from the list of filenames in files_to_test. If target is None, use the first supported_objects from ioobj If target is False, use the 'read' method. If target is the Block or Segment class, use read_block or read_segment, respectively. If target is a string, use 'read_'+target. If return_path is True, yield the full path of the file along with the object. yield obj, path. If return_ioobj is True, yield the io object as well as the object. yield obj, ioobj. Default is False. If return_reader is True, yield the io reader function as well as the object. yield obj, reader. Default is False. If some combination of return_path, return_ioobj, and return_reader is True, they are yielded in the order: obj, path, ioobj, reader. If clean is True, try to delete existing versions of the file before creating the io object. Default is False. The cascade and lazy parameters are passed to the reader. Defaults are True and False, respectively. If readall is True, use the read_all_ method instead of the read_ method. Default is False. ''' return iter_read_objects(ioclass=self.ioclass, filenames=self.files_to_test, directory=self.local_test_dir, target=target, return_path=return_path, return_ioobj=return_ioobj, return_reader=return_reader, clean=clean, readall=readall, cascade=cascade, lazy=lazy) def generate_files_for_io_able_to_write(self): ''' Write files for use in testing. ''' self.files_generated = [] if not self.able_to_write_or_read(): return generate_from_supported_objects(self.ioclass.supported_objects) ioobj, path = self.generic_io_object(return_path=True, clean=True) if ioobj is None: return self.files_generated.append(path) write_generic(ioobj, target=self.higher) close_object_safe(ioobj) def test_write_then_read(self): ''' Test for IO that are able to write and read - here %s: 1 - Generate a full schema with supported objects. 2 - Write to a file 3 - Read from the file 4 - Check the hierachy 5 - Check data Work only for IO for Block and Segment for the highest object (main cases). ''' % self.ioclass.__name__ if not self.able_to_write_or_read(writeread=True): return for cascade in self.cascade_modes: ioobj1 = self.generic_io_object(clean=True) if ioobj1 is None: return ob1 = write_generic(ioobj1, target=self.higher) close_object_safe(ioobj1) ioobj2 = self.generic_io_object() # Read the highest supported object from the file obj_reader = create_generic_reader(ioobj2, target=False) ob2 = obj_reader(cascade=cascade)[0] if self.higher == Segment: ob2 = ob2.segments[0] # some formats (e.g. elphy) do not support double floating # point spiketrains try: assert_same_sub_schema(ob1, ob2, False, 1e-8) assert_neo_object_is_compliant(ob1) assert_neo_object_is_compliant(ob2) # intercept exceptions and add more information except BaseException as exc: exc.args += ('with cascade=%s ' % cascade,) raise close_object_safe(ioobj2) def test_read_then_write(self): ''' Test for IO that are able to read and write, here %s: 1 - Read a file 2 Write object set in another file 3 Compare the 2 files hash NOTE: TODO: Not implemented yet ''' % self.ioclass.__name__ if not self.able_to_write_or_read(readwrite=True): return #assert_file_contents_equal(a, b) def test_assert_readed_neo_object_is_compliant(self): ''' Reading %s files in `files_to_test` produces compliant objects. Compliance test: neo.test.tools.assert_neo_object_is_compliant for all cascade and lazy modes ''' % self.ioclass.__name__ # This is for files presents at G-Node or generated for cascade in self.cascade_modes: for lazy in [True, False]: for obj, path in self.iter_objects(cascade=cascade, lazy=lazy, return_path=True): try: # Check compliance of the block assert_neo_object_is_compliant(obj) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from %s with cascade=%s and lazy=%s' % (os.path.basename(path), cascade, lazy),) raise def test_readed_with_cascade_is_compliant(self): ''' Reading %s files in `files_to_test` with `cascade` is compliant. A reader with cascade = False should return empty children. ''' % self.ioclass.__name__ # This is for files presents at G-Node or generated for obj, path in self.iter_objects(cascade=False, lazy=False, return_path=True): try: # Check compliance of the block or segment assert_neo_object_is_compliant(obj) assert_children_empty(obj, self.ioclass) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from %s ' % os.path.basename(path),) raise def test_readed_with_lazy_is_compliant(self): ''' Reading %s files in `files_to_test` with `lazy` is compliant. Test the reader with lazy = True. All objects derived from ndarray or Quantity should have a size of 0. Also, AnalogSignal, AnalogSignalArray, SpikeTrain, Epocharray, and EventArray should contain the lazy_shape attribute. ''' % self.ioclass.__name__ # This is for files presents at G-Node or generated for cascade in self.cascade_modes: for obj, path in self.iter_objects(cascade=cascade, lazy=True, return_path=True): try: assert_sub_schema_is_lazy_loaded(obj) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from %s with cascade=%s ' % (os.path.basename(path), cascade),) raise def test_load_lazy_objects(self): ''' Reading %s files in `files_to_test` with `lazy` works. Test the reader with lazy = True. All objects derived from ndarray or Quantity should have a size of 0. Also, AnalogSignal, AnalogSignalArray, SpikeTrain, Epocharray, and EventArray should contain the lazy_shape attribute. ''' % self.ioclass.__name__ if not hasattr(self.ioclass, 'load_lazy_object'): return # This is for files presents at G-Node or generated for cascade in self.cascade_modes: for obj, path, ioobj in self.iter_objects(cascade=cascade, lazy=True, return_ioobj=True, return_path=True): try: assert_lazy_sub_schema_can_be_loaded(obj, ioobj) # intercept exceptions and add more information except BaseException as exc: exc.args += ('from %s with cascade=%s ' % (os.path.basename(path), cascade),) raise neo-0.3.3/neo/test/iotest/test_brainwaresrcio.py0000644000175000017500000003325112273723542023047 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.brainwaresrcio """ # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import os.path import sys import warnings try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core import (Block, Event, RecordingChannel, RecordingChannelGroup, Segment, SpikeTrain, Unit) from neo.io import BrainwareSrcIO, brainwaresrcio from neo.io.tools import create_many_to_one_relationship from neo.test.iotest.common_io_test import BaseTestIO from neo.test.tools import (assert_same_sub_schema, assert_neo_object_is_compliant) from neo.test.iotest.tools import create_generic_reader PY_VER = sys.version_info[0] def proc_src(filename): '''Load an src file that has already been processed by the official matlab file converter. That matlab data is saved to an m-file, which is then converted to a numpy '.npz' file. This numpy file is the file actually loaded. This function converts it to a neo block and returns the block. This block can be compared to the block produced by BrainwareSrcIO to make sure BrainwareSrcIO is working properly block = proc_src(filename) filename: The file name of the numpy file to load. It should end with '*_src_py?.npz'. This will be converted to a neo 'file_origin' property with the value '*.src', so the filename to compare should fit that pattern. 'py?' should be 'py2' for the python 2 version of the numpy file or 'py3' for the python 3 version of the numpy file. example: filename = 'file1_src_py2.npz' src file name = 'file1.src' ''' with np.load(filename) as srcobj: srcfile = srcobj.items()[0][1] filename = os.path.basename(filename[:-12]+'.src') block = Block(file_origin=filename) NChannels = srcfile['NChannels'][0, 0][0, 0] side = str(srcfile['side'][0, 0][0]) ADperiod = srcfile['ADperiod'][0, 0][0, 0] comm_seg = proc_src_comments(srcfile, filename) block.segments.append(comm_seg) rcg = proc_src_units(srcfile, filename) chan_nums = np.arange(NChannels, dtype='int') chan_names = [] for i in chan_nums: name = 'Chan'+str(i) chan_names.append(name) chan = RecordingChannel(file_origin='filename', name=name, index=i) rcg.recordingchannels.append(chan) rcg.channel_indexes = chan_nums rcg.channel_names = np.array(chan_names, dtype='string_') block.recordingchannelgroups.append(rcg) for rep in srcfile['sets'][0, 0].flatten(): proc_src_condition(rep, filename, ADperiod, side, block) create_many_to_one_relationship(block) return block def proc_src_comments(srcfile, filename): '''Get the comments in an src file that has been#!N processed by the official matlab function. See proc_src for details''' comm_seg = Segment(name='Comments', file_origin=filename) commentarray = srcfile['comments'].flatten()[0] senders = [res[0] for res in commentarray['sender'].flatten()] texts = [res[0] for res in commentarray['text'].flatten()] timeStamps = [res[0, 0] for res in commentarray['timeStamp'].flatten()] for sender, text, timeStamp in zip(senders, texts, timeStamps): time = pq.Quantity(timeStamp, units=pq.d) timeStamp = brainwaresrcio.convert_brainwaresrc_timestamp(timeStamp) commentevent = Event(time=time, label=str(text), sender=str(sender), name='Comment', file_origin=filename, description='container for a comment', timestamp=timeStamp) comm_seg.events.append(commentevent) return comm_seg 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''' rcg = RecordingChannelGroup(file_origin=filename) un_unit = Unit(name='UnassignedSpikes', file_origin=filename, elliptic=[], boundaries=[], timestamp=[], max_valid=[]) rcg.units.append(un_unit) sortInfo = srcfile['sortInfo'][0, 0] timeslice = sortInfo['timeslice'][0, 0] maxValid = timeslice['maxValid'][0, 0] cluster = timeslice['cluster'][0, 0] if len(cluster): maxValid = maxValid[0, 0] elliptic = [res.flatten() for res in cluster['elliptic'].flatten()] boundaries = [res.flatten() for res in cluster['boundaries'].flatten()] fullclust = zip(elliptic, boundaries) for ielliptic, iboundaries in fullclust: unit = Unit(file_origin=filename, boundaries=[iboundaries], elliptic=[ielliptic], timeStamp=[], max_valid=[maxValid]) rcg.units.append(unit) return rcg 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''' rcg = block.recordingchannelgroups[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()] stim = dict(zip(params, values)) sweepLen = rep['sweepLen'][0, 0] if not len(rep): return unassignedSpikes = rep['unassignedSpikes'].flatten() if len(unassignedSpikes): damaIndexes = [res[0, 0] for res in unassignedSpikes['damaIndex']] timeStamps = [res[0, 0] for res in unassignedSpikes['timeStamp']] spikeunit = [res.flatten() for res in unassignedSpikes['spikes']] respWin = np.array([], dtype=np.int32) trains = proc_src_condition_unit(spikeunit, sweepLen, side, ADperiod, respWin, damaIndexes, timeStamps, filename) rcg.units[0].spiketrains.extend(trains) atrains = [trains] else: damaIndexes = [] timeStamps = [] atrains = [] clusters = rep['clusters'].flatten() if len(clusters): IdStrings = [res[0] for res in clusters['IdString']] sweepLens = [res[0, 0] for res in clusters['sweepLen']] respWins = [res.flatten() for res in clusters['respWin']] spikeunits = [] for cluster in clusters['sweeps']: if len(cluster): spikes = [res.flatten() for res in cluster['spikes'].flatten()] else: spikes = [] spikeunits.append(spikes) else: IdStrings = [] sweepLens = [] respWins = [] spikeunits = [] for unit, IdString in zip(rcg.units[1:], IdStrings): unit.name = str(IdString) fullunit = zip(spikeunits, rcg.units[1:], sweepLens, respWins) for spikeunit, unit, sweepLen, respWin in fullunit: trains = proc_src_condition_unit(spikeunit, sweepLen, side, ADperiod, respWin, damaIndexes, timeStamps, filename) atrains.append(trains) unit.spiketrains.extend(trains) atrains = zip(*atrains) for trains in atrains: segment = Segment(file_origin=filename, feature_type=-1, go_by_closest_unit_center=False, include_unit_bounds=False, **stim) block.segments.append(segment) segment.spiketrains = trains def proc_src_condition_unit(spikeunit, sweepLen, side, ADperiod, respWin, damaIndexes, timeStamps, filename): '''Get the unit in a condition in a src file that has been processed by the official matlab function. See proc_src for details''' if not damaIndexes: damaIndexes = [0]*len(spikeunit) timeStamps = [0]*len(spikeunit) trains = [] for sweep, damaIndex, timeStamp in zip(spikeunit, damaIndexes, timeStamps): timeStamp = brainwaresrcio.convert_brainwaresrc_timestamp(timeStamp) train = proc_src_condition_unit_repetition(sweep, damaIndex, timeStamp, sweepLen, side, ADperiod, respWin, filename) trains.append(train) return trains def proc_src_condition_unit_repetition(sweep, damaIndex, timeStamp, sweepLen, side, ADperiod, respWin, filename): '''Get the repetion for a unit in a condition in a src file that has been processed by the official matlab function. See proc_src for details''' damaIndex = damaIndex.astype('int32') if len(sweep): times = np.array([res[0, 0] for res in sweep['time']]) shapes = np.concatenate([res.flatten()[np.newaxis][np.newaxis] for res in sweep['shape']], axis=0) trig2 = np.array([res[0, 0] for res in sweep['trig2']]) else: times = np.array([]) shapes = np.array([[[]]]) trig2 = np.array([]) times = pq.Quantity(times, units=pq.ms, dtype=np.float32) t_start = pq.Quantity(0, units=pq.ms, dtype=np.float32) t_stop = pq.Quantity(sweepLen, units=pq.ms, dtype=np.float32) trig2 = pq.Quantity(trig2, units=pq.ms, dtype=np.uint8) waveforms = pq.Quantity(shapes, dtype=np.int8, units=pq.mV) sampling_period = pq.Quantity(ADperiod, units=pq.us) train = SpikeTrain(times=times, t_start=t_start, t_stop=t_stop, trig2=trig2, dtype=np.float32, timestamp=timeStamp, dama_index=damaIndex, side=side, copy=True, respwin=respWin, waveforms=waveforms, file_origin=filename) train.annotations['side'] = side train.sampling_period = sampling_period return train class BrainwareSrcIOTestCase(BaseTestIO, unittest.TestCase): ''' Unit test testcase for neo.io.BrainwareSrcIO ''' ioclass = BrainwareSrcIO read_and_write_is_bijective = False # These are the files it tries to read and test for compliance files_to_test = ['block_300ms_4rep_1clust_part_ch1.src', 'block_500ms_5rep_empty_fullclust_ch1.src', 'block_500ms_5rep_empty_partclust_ch1.src', 'interleaved_500ms_5rep_ch2.src', 'interleaved_500ms_5rep_nospikes_ch1.src', 'interleaved_500ms_7rep_noclust_ch1.src', 'long_170s_1rep_1clust_ch2.src', 'multi_500ms_mulitrep_ch1.src', 'random_500ms_12rep_noclust_part_ch2.src', 'sequence_500ms_5rep_ch2.src'] # these are reference files to compare to files_to_compare = ['block_300ms_4rep_1clust_part_ch1', 'block_500ms_5rep_empty_fullclust_ch1', 'block_500ms_5rep_empty_partclust_ch1', 'interleaved_500ms_5rep_ch2', 'interleaved_500ms_5rep_nospikes_ch1', 'interleaved_500ms_7rep_noclust_ch1', '', 'multi_500ms_mulitrep_ch1', 'random_500ms_12rep_noclust_part_ch2', 'sequence_500ms_5rep_ch2'] # add the appropriate suffix depending on the python version for i, fname in enumerate(files_to_compare): if fname: files_to_compare[i] += '_src_py%s.npz' % PY_VER # Will fetch from g-node if they don't already exist locally # How does it know to do this before any of the other tests? files_to_download = files_to_test + files_to_compare def setUp(self): warnings.filterwarnings('ignore', message='Negative sequence count.*') warnings.filterwarnings('ignore', message='unknown ID:*') super(BrainwareSrcIOTestCase, self).setUp() def test_reading_same(self): for ioobj, path in self.iter_io_objects(return_path=True): obj_reader_all = create_generic_reader(ioobj, readall=True) obj_reader_base = create_generic_reader(ioobj, target=False) obj_reader_next = create_generic_reader(ioobj, target='next_block') obj_reader_single = create_generic_reader(ioobj) obj_all = obj_reader_all() obj_base = obj_reader_base() obj_single = obj_reader_single() obj_next = [obj_reader_next(warnlast=False)] while ioobj.isopen: obj_next.append(obj_reader_next(warnlast=False)) try: assert_same_sub_schema(obj_all[0], obj_base) assert_same_sub_schema(obj_all[0], obj_single) assert_same_sub_schema(obj_all, obj_next) except BaseException as exc: exc.args += ('from ' + os.path.basename(path),) raise self.assertEqual(len(obj_all), len(obj_next)) def test_against_reference(self): for filename, refname in zip(self.files_to_test, self.files_to_compare): if not refname: continue obj = self.read_file(filename=filename, readall=True)[0] refobj = proc_src(self.get_filename_path(refname)) try: assert_neo_object_is_compliant(obj) assert_neo_object_is_compliant(refobj) assert_same_sub_schema(obj, refobj) except BaseException as exc: exc.args += ('from ' + filename,) raise if __name__ == '__main__': unittest.main() neo-0.3.3/neo/test/iotest/test_exampleio.py0000644000175000017500000000366512265516260022024 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.exampleio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.io.exampleio import ExampleIO, HAVE_SCIPY from neo.test.iotest.common_io_test import BaseTestIO class TestExampleIO(BaseTestIO, unittest.TestCase, ): ioclass = ExampleIO files_to_test = ['fake1', 'fake2', ] files_to_download = [] class TestExample2IO(unittest.TestCase): @unittest.skipUnless(HAVE_SCIPY, "requires scipy") def test_read_segment_lazy(self): r = ExampleIO(filename=None) seg = r.read_segment(cascade=True, lazy=True) for ana in seg.analogsignals: self.assertEqual(ana.size, 0) assert hasattr(ana, 'lazy_shape') for st in seg.spiketrains: self.assertEqual(st.size, 0) assert hasattr(st, 'lazy_shape') seg = r.read_segment(cascade=True, lazy=False) for ana in seg.analogsignals: self.assertNotEqual(ana.size, 0) for st in seg.spiketrains: self.assertNotEqual(st.size, 0) @unittest.skipUnless(HAVE_SCIPY, "requires scipy") def test_read_segment_cascade(self): r = ExampleIO(filename=None) seg = r.read_segment(cascade=False) self.assertEqual(len(seg.analogsignals), 0) seg = r.read_segment(cascade=True, num_analogsignal=4) self.assertEqual(len(seg.analogsignals), 4) @unittest.skipUnless(HAVE_SCIPY, "requires scipy") def test_read_analogsignal(self): r = ExampleIO(filename=None) r.read_analogsignal(lazy=False, segment_duration=15., t_start=-1) @unittest.skipUnless(HAVE_SCIPY, "requires scipy") def read_spiketrain(self): r = ExampleIO(filename=None) r.read_spiketrain(lazy=False,) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_rawbinarysignalio.py0000644000175000017500000000106512265516260023555 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of io.rawbinarysignal """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.io import RawBinarySignalIO from neo.test.iotest.common_io_test import BaseTestIO class TestRawBinarySignalIO(BaseTestIO, unittest.TestCase, ): ioclass = RawBinarySignalIO files_to_test = ['File_rawbinary_10kHz_2channels_16bit.raw'] files_to_download = files_to_test if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_elanio.py0000644000175000017500000000131012265516260021271 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.elanio """ # needed for python 3 compatibility from __future__ import absolute_import, division import sys try: import unittest2 as unittest except ImportError: import unittest from neo.io import ElanIO from neo.test.iotest.common_io_test import BaseTestIO @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class TestElanIO(BaseTestIO, unittest.TestCase, ): ioclass = ElanIO files_to_test = ['File_elan_1.eeg'] files_to_download = ['File_elan_1.eeg', 'File_elan_1.eeg.ent', 'File_elan_1.eeg.pos', ] if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_asciispiketrainio.py0000644000175000017500000000106212265516260023540 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.asciispiketrainio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.io import AsciiSpikeTrainIO from neo.test.iotest.common_io_test import BaseTestIO class TestAsciiSpikeTrainIO(BaseTestIO, unittest.TestCase, ): ioclass = AsciiSpikeTrainIO files_to_download = ['File_ascii_spiketrain_1.txt'] files_to_test = files_to_download if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_winwcpio.py0000644000175000017500000000101112265516260021657 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.winwcpio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.io import WinWcpIO from neo.test.iotest.common_io_test import BaseTestIO class TestRawBinarySignalIO(BaseTestIO, unittest.TestCase, ): ioclass = WinWcpIO files_to_test = ['File_winwcp_1.wcp'] files_to_download = files_to_test if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_axonio.py0000644000175000017500000000143212265516260021324 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.axonio """ # needed for python 3 compatibility from __future__ import absolute_import import sys try: import unittest2 as unittest except ImportError: import unittest from neo.io import AxonIO from neo.test.iotest.common_io_test import BaseTestIO @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class TestAxonIO(BaseTestIO, unittest.TestCase): files_to_test = ['File_axon_1.abf', 'File_axon_2.abf', 'File_axon_3.abf', 'File_axon_4.abf', 'File_axon_5.abf', 'File_axon_6.abf', ] files_to_download = files_to_test ioclass = AxonIO if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_asciisignalio.py0000644000175000017500000000123612265516260022647 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.asciisignalio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.io import AsciiSignalIO from neo.test.iotest.common_io_test import BaseTestIO class TestAsciiSignalIO(BaseTestIO, unittest.TestCase, ): ioclass = AsciiSignalIO files_to_download = ['File_asciisignal_1.asc', 'File_asciisignal_2.txt', 'File_asciisignal_3.txt', ] files_to_test = files_to_download if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_micromedio.py0000644000175000017500000000113412265516260022155 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.neomatlabio """ # needed for python 3 compatibility from __future__ import absolute_import, division import sys try: import unittest2 as unittest except ImportError: import unittest from neo.io import MicromedIO from neo.test.iotest.common_io_test import BaseTestIO @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class TestMicromedIO(BaseTestIO, unittest.TestCase, ): ioclass = MicromedIO files_to_test = ['File_micromed_1.TRC'] files_to_download = files_to_test if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_blackrockio.py0000644000175000017500000002060412273723542022316 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.blackrockio """ # needed for python 3 compatibility from __future__ import absolute_import import os import struct import sys import tempfile try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq import neo.io.blackrockio from neo.test.iotest.common_io_test import BaseTestIO from neo.io import tools from neo.test.tools import assert_arrays_almost_equal #~ class testRead(unittest.TestCase): #~ """Tests that data can be read from KlustaKwik files""" #~ def test1(self): #~ """Tests that data and metadata are read correctly""" #~ pass #~ def test2(self): #~ """Checks that cluster id autosets to 0 without clu file""" #~ pass #~ dirname = os.path.normpath('./files_for_tests/klustakwik/test2') #~ kio = neo.io.KlustaKwikIO(filename=os.path.join(dirname, 'base2'), #~ sampling_rate=1000.) #~ block = kio.read() #~ seg = block.segments[0] #~ self.assertEqual(len(seg.spiketrains), 1) #~ self.assertEqual(seg.spiketrains[0].name, 'unit 0 from group 5') #~ self.assertEqual(seg.spiketrains[0].annotations['cluster'], 0) #~ self.assertEqual(seg.spiketrains[0].annotations['group'], 5) #~ self.assertEqual(seg.spiketrains[0].t_start, 0.0) #~ self.assertTrue(np.all(seg.spiketrains[0].times == np.array( #~ [0.026, 0.122, 0.228]))) @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class testWrite(unittest.TestCase): def setUp(self): self.datadir = os.path.join(tempfile.gettempdir(), 'files_for_testing_neo', 'blackrock/test2/') self.fn = os.path.join(self.datadir, 'test.write.ns5') if not os.path.exists(self.datadir): raise unittest.SkipTest('data directory does not exist: ' + self.datadir) def test1(self): """Write data to binary file, then read it back in and verify""" # delete temporary file before trying to write to it if os.path.exists(self.fn): os.remove(self.fn) block = neo.Block() full_range = 234 * pq.mV # Create segment1 with analogsignals segment1 = neo.Segment() sig1 = neo.AnalogSignal([3, 4, 5], units='mV', channel_index=3, sampling_rate=30000.*pq.Hz) sig2 = neo.AnalogSignal([6, -4, -5], units='mV', channel_index=4, sampling_rate=30000.*pq.Hz) segment1.analogsignals.append(sig1) segment1.analogsignals.append(sig2) # Create segment2 with analogsignals segment2 = neo.Segment() sig3 = neo.AnalogSignal([-3, -4, -5], units='mV', channel_index=3, sampling_rate=30000.*pq.Hz) sig4 = neo.AnalogSignal([-6, 4, 5], units='mV', channel_index=4, sampling_rate=30000.*pq.Hz) segment2.analogsignals.append(sig3) segment2.analogsignals.append(sig4) # Link segments to block block.segments.append(segment1) block.segments.append(segment2) # Create hardware view, and bijectivity #tools.populate_RecordingChannel(block) #print "problem happening" #print block.recordingchannelgroups[0].recordingchannels #chan = block.recordingchannelgroups[0].recordingchannels[0] #print chan.analogsignals #tools.create_many_to_one_relationship(block) #print "here: " #print block.segments[0].analogsignals[0].recordingchannel # Chris I prefer that: #tools.finalize_block(block) tools.populate_RecordingChannel(block) tools.create_many_to_one_relationship(block) # Check that blackrockio is correctly extracting channel indexes self.assertEqual(neo.io.blackrockio.channel_indexes_in_segment( segment1), [3, 4]) self.assertEqual(neo.io.blackrockio.channel_indexes_in_segment( segment2), [3, 4]) # Create writer. Write block, then read back in. bio = neo.io.BlackrockIO(filename=self.fn, full_range=full_range) bio.write_block(block) fi = file(self.fn) # Text header self.assertEqual(fi.read(16), 'NEURALSG30 kS/s\x00') self.assertEqual(fi.read(8), '\x00\x00\x00\x00\x00\x00\x00\x00') # Integers: period, channel count, channel index1, channel index2 self.assertEqual(struct.unpack('<4I', fi.read(16)), (1, 2, 3, 4)) # What should the signals be after conversion? conv = float(full_range) / 2**16 sigs = np.array([np.concatenate((sig1, sig3)), np.concatenate((sig2, sig4))]) sigs_converted = np.rint(sigs / conv).astype(np.int) # Check that each time point is the same for time_slc in sigs_converted.transpose(): written_data = struct.unpack('<2h', fi.read(4)) self.assertEqual(list(time_slc), list(written_data)) # Check that we read to the end currentpos = fi.tell() fi.seek(0, 2) truelen = fi.tell() self.assertEqual(currentpos, truelen) fi.close() # Empty out test session again #~ delete_test_session() @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class testRead(unittest.TestCase): def setUp(self): self.fn = os.path.join(tempfile.gettempdir(), 'files_for_testing_neo', 'blackrock/test2/test.ns5') if not os.path.exists(self.fn): raise unittest.SkipTest('data file does not exist:' + self.fn) def test1(self): """Read data into one big segment (default)""" full_range = 8192 * pq.mV bio = neo.io.BlackrockIO(filename=self.fn, full_range=full_range) block = bio.read_block(n_starts=[0], n_stops=[6]) self.assertEqual(bio.header.Channel_Count, 2) self.assertEqual(bio.header.n_samples, 6) # Everything put in one segment self.assertEqual(len(block.segments), 1) seg = block.segments[0] self.assertEqual(len(seg.analogsignals), 2) assert_arrays_almost_equal(seg.analogsignals[0], [3., 4., 5., -3., -4., -5.] * pq.mV, .0001) assert_arrays_almost_equal(seg.analogsignals[1], [6., -4., -5., -6., 4., 5.] * pq.mV, .0001) def test2(self): """Read data into two segments instead of just one""" full_range = 8192 * pq.mV bio = neo.io.BlackrockIO(filename=self.fn, full_range=full_range) block = bio.read_block(n_starts=[0, 3], n_stops=[2, 6]) self.assertEqual(bio.header.Channel_Count, 2) self.assertEqual(bio.header.n_samples, 6) # Everything in two segments self.assertEqual(len(block.segments), 2) # Test first seg seg = block.segments[0] self.assertEqual(len(seg.analogsignals), 2) assert_arrays_almost_equal(seg.analogsignals[0], [3., 4.] * pq.mV, .0001) assert_arrays_almost_equal(seg.analogsignals[1], [6., -4.] * pq.mV, .0001) # Test second seg seg = block.segments[1] self.assertEqual(len(seg.analogsignals), 2) assert_arrays_almost_equal(seg.analogsignals[0], [-3., -4., -5.] * pq.mV, .0001) assert_arrays_almost_equal(seg.analogsignals[1], [-6., 4., 5.] * pq.mV, .0001) @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class CommonTests(BaseTestIO, unittest.TestCase): ioclass = neo.io.BlackrockIO read_and_write_is_bijective = False # These are the files it tries to read and test for compliance files_to_test = [ 'test2/test.ns5' ] # Will fetch from g-node if they don't already exist locally # How does it know to do this before any of the other tests? files_to_download = [ 'test2/test.ns5' ] #~ def delete_test_session(): #~ """Removes all file in directory so we can test writing to it""" #~ for fi in glob.glob(os.path.join( #~ './files_for_tests/klustakwik/test3', '*')): #~ os.remove(fi) if __name__ == '__main__': unittest.main() neo-0.3.3/neo/test/iotest/test_brainvisionio.py0000644000175000017500000000161512265516260022705 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.brainvisionio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.io import BrainVisionIO from neo.test.iotest.common_io_test import BaseTestIO class TestBrainVisionIO(BaseTestIO, unittest.TestCase, ): ioclass = BrainVisionIO files_to_test = ['File_brainvision_1.vhdr', 'File_brainvision_2.vhdr', ] files_to_download = ['File_brainvision_1.eeg', 'File_brainvision_1.vhdr', 'File_brainvision_1.vmrk', 'File_brainvision_2.eeg', 'File_brainvision_2.vhdr', 'File_brainvision_2.vmrk', ] if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_neuroscopeio.py0000644000175000017500000000113312265516260022537 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.neuroscopeio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.io import NeuroScopeIO from neo.test.iotest.common_io_test import BaseTestIO class TestNeuroScopeIO(BaseTestIO, unittest.TestCase, ): ioclass = NeuroScopeIO files_to_test = ['test1/test1.xml'] files_to_download = ['test1/test1.xml', 'test1/test1.dat', ] if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_neuroexplorerio.py0000644000175000017500000000127412265516260023274 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.neuroexplorerio """ # needed for python 3 compatibility from __future__ import absolute_import, division import sys try: import unittest2 as unittest except ImportError: import unittest from neo.io import NeuroExplorerIO from neo.test.iotest.common_io_test import BaseTestIO @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class TestNeuroExplorerIO(BaseTestIO, unittest.TestCase, ): ioclass = NeuroExplorerIO files_to_test = ['File_neuroexplorer_1.nex', 'File_neuroexplorer_2.nex', ] files_to_download = files_to_test if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_plexonio.py0000644000175000017500000000127412265516260021670 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.plexonio """ # needed for python 3 compatibility from __future__ import absolute_import, division import sys try: import unittest2 as unittest except ImportError: import unittest from neo.io import PlexonIO from neo.test.iotest.common_io_test import BaseTestIO @unittest.skipIf(sys.version_info[0] > 2, "not Python 3 compatible") class TestPlexonIO(BaseTestIO, unittest.TestCase, ): ioclass = PlexonIO files_to_test = ['File_plexon_1.plx', 'File_plexon_2.plx', 'File_plexon_3.plx', ] files_to_download = files_to_test if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_winedrio.py0000644000175000017500000000115412265516260021650 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.wineedrio """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.io import WinEdrIO from neo.test.iotest.common_io_test import BaseTestIO class TestWinedrIO(BaseTestIO, unittest.TestCase, ): ioclass = WinEdrIO files_to_test = ['File_WinEDR_1.EDR', 'File_WinEDR_2.EDR', 'File_WinEDR_3.EDR', ] files_to_download = files_to_test if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_brainwaredamio.py0000644000175000017500000001333412273723542023021 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.brainwaredamio """ # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import os.path import sys try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core import (AnalogSignal, Block, RecordingChannel, RecordingChannelGroup, Segment) from neo.io import BrainwareDamIO from neo.io.tools import create_many_to_one_relationship from neo.test.iotest.common_io_test import BaseTestIO from neo.test.tools import (assert_same_sub_schema, assert_neo_object_is_compliant) from neo.test.iotest.tools import create_generic_reader PY_VER = sys.version_info[0] def proc_dam(filename): '''Load an dam file that has already been processed by the official matlab file converter. That matlab data is saved to an m-file, which is then converted to a numpy '.npz' file. This numpy file is the file actually loaded. This function converts it to a neo block and returns the block. This block can be compared to the block produced by BrainwareDamIO to make sure BrainwareDamIO is working properly block = proc_dam(filename) filename: The file name of the numpy file to load. It should end with '*_dam_py?.npz'. This will be converted to a neo 'file_origin' property with the value '*.dam', so the filename to compare should fit that pattern. 'py?' should be 'py2' for the python 2 version of the numpy file or 'py3' for the python 3 version of the numpy file. example: filename = 'file1_dam_py2.npz' dam file name = 'file1.dam' ''' with np.load(filename) as damobj: damfile = damobj.items()[0][1].flatten() filename = os.path.basename(filename[:-12]+'.dam') signals = [res.flatten() for res in damfile['signal']] stimIndexes = [int(res[0, 0].tolist()) for res in damfile['stimIndex']] timestamps = [res[0, 0] for res in damfile['timestamp']] block = Block(file_origin=filename) rcg = RecordingChannelGroup(file_origin=filename) chan = RecordingChannel(file_origin=filename, index=0, name='Chan1') rcg.channel_indexes = np.array([1]) rcg.channel_names = np.array(['Chan1'], dtype='S') block.recordingchannelgroups.append(rcg) rcg.recordingchannels.append(chan) params = [res['params'][0, 0].flatten() for res in damfile['stim']] values = [res['values'][0, 0].flatten() for res in damfile['stim']] params = [[res1[0] for res1 in res] for res in params] values = [[res1 for res1 in res] for res in values] stims = [dict(zip(param, value)) for param, value in zip(params, values)] fulldam = zip(stimIndexes, timestamps, signals, stims) for stimIndex, timestamp, signal, stim in fulldam: sig = AnalogSignal(signal=signal*pq.mV, t_start=timestamp*pq.d, file_origin=filename, sampling_period=1.*pq.s) segment = Segment(file_origin=filename, index=stimIndex, **stim) segment.analogsignals = [sig] block.segments.append(segment) create_many_to_one_relationship(block) return block class BrainwareDamIOTestCase(BaseTestIO, unittest.TestCase): ''' Unit test testcase for neo.io.BrainwareDamIO ''' ioclass = BrainwareDamIO read_and_write_is_bijective = False # These are the files it tries to read and test for compliance files_to_test = ['block_300ms_4rep_1clust_part_ch1.dam', 'interleaved_500ms_5rep_ch2.dam', 'long_170s_1rep_1clust_ch2.dam', 'multi_500ms_mulitrep_ch1.dam', 'random_500ms_12rep_noclust_part_ch2.dam', 'sequence_500ms_5rep_ch2.dam'] # these are reference files to compare to files_to_compare = ['block_300ms_4rep_1clust_part_ch1', 'interleaved_500ms_5rep_ch2', '', 'multi_500ms_mulitrep_ch1', 'random_500ms_12rep_noclust_part_ch2', 'sequence_500ms_5rep_ch2'] # add the appropriate suffix depending on the python version for i, fname in enumerate(files_to_compare): if fname: files_to_compare[i] += '_dam_py%s.npz' % PY_VER # Will fetch from g-node if they don't already exist locally # How does it know to do this before any of the other tests? files_to_download = files_to_test + files_to_compare def test_reading_same(self): for ioobj, path in self.iter_io_objects(return_path=True): obj_reader_base = create_generic_reader(ioobj, target=False) obj_reader_single = create_generic_reader(ioobj) obj_base = obj_reader_base() obj_single = obj_reader_single() try: assert_same_sub_schema(obj_base, obj_single) except BaseException as exc: exc.args += ('from ' + os.path.basename(path),) raise def test_against_reference(self): for filename, refname in zip(self.files_to_test, self.files_to_compare): if not refname: continue obj = self.read_file(filename=filename) refobj = proc_dam(self.get_filename_path(refname)) try: assert_neo_object_is_compliant(obj) assert_neo_object_is_compliant(refobj) assert_same_sub_schema(obj, refobj) except BaseException as exc: exc.args += ('from ' + filename,) raise if __name__ == '__main__': unittest.main() neo-0.3.3/neo/test/iotest/test_pynnio.py0000644000175000017500000002013312265516260021342 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.io.pynnio.PyNNNumpyIO and neo.io.pynnio.PyNNTextIO classes """ # needed for python 3 compatibility from __future__ import absolute_import, division import os try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core import Segment, AnalogSignal, SpikeTrain from neo.io import PyNNNumpyIO, PyNNTextIO from neo.test.tools import assert_arrays_equal, assert_file_contents_equal #TODO: common test fails. from neo.test.iotest.common_io_test import BaseTestIO #class CommonTestPyNNNumpyIO(BaseTestIO, unittest.TestCase): # ioclass = PyNNNumpyIO NCELLS = 5 class CommonTestPyNNTextIO(BaseTestIO, unittest.TestCase): ioclass = PyNNTextIO read_and_write_is_bijective = False def read_test_file(filename): contents = np.load(filename) data = contents["data"] metadata = {} for name, value in contents['metadata']: try: metadata[name] = eval(value) except Exception: metadata[name] = value return data, metadata read_test_file.__test__ = False class BaseTestPyNNIO(object): __test__ = False def tearDown(self): if os.path.exists(self.test_file): os.remove(self.test_file) def test_write_segment(self): in_ = self.io_cls(self.test_file) write_test_file = "write_test.%s" % self.file_extension out = self.io_cls(write_test_file) out.write_segment(in_.read_segment(lazy=False, cascade=True)) assert_file_contents_equal(self.test_file, write_test_file) if os.path.exists(write_test_file): os.remove(write_test_file) def build_test_data(self, variable='v'): metadata = { 'size': NCELLS, 'first_index': 0, 'first_id': 0, 'n': 505, 'variable': variable, 'last_id': 4, 'last_index': 5, 'dt': 0.1, 'label': "population0", } if variable == 'v': metadata['units'] = 'mV' elif variable == 'spikes': metadata['units'] = 'ms' data = np.empty((505, 2)) for i in range(NCELLS): # signal data[i*101:(i+1)*101, 0] = np.arange(i, i+101, dtype=float) # index data[i*101:(i+1)*101, 1] = i*np.ones((101,), dtype=float) return data, metadata build_test_data.__test__ = False class BaseTestPyNNIO_Signals(BaseTestPyNNIO): def setUp(self): self.test_file = "test_file_v.%s" % self.file_extension self.write_test_file("v") def test_read_segment_containing_analogsignals_using_eager_cascade(self): # eager == not lazy io = self.io_cls(self.test_file) segment = io.read_segment(lazy=False, cascade=True) self.assertIsInstance(segment, Segment) self.assertEqual(len(segment.analogsignals), NCELLS) as0 = segment.analogsignals[0] self.assertIsInstance(as0, AnalogSignal) assert_arrays_equal(as0, AnalogSignal(np.arange(0, 101, dtype=float), sampling_period=0.1*pq.ms, t_start=0*pq.s, units=pq.mV)) as4 = segment.analogsignals[4] self.assertIsInstance(as4, AnalogSignal) assert_arrays_equal(as4, AnalogSignal(np.arange(4, 105, dtype=float), sampling_period=0.1*pq.ms, t_start=0*pq.s, units=pq.mV)) # test annotations (stuff from file metadata) def test_read_analogsignal_using_eager(self): io = self.io_cls(self.test_file) as3 = io.read_analogsignal(lazy=False, channel_index=3) self.assertIsInstance(as3, AnalogSignal) assert_arrays_equal(as3, AnalogSignal(np.arange(3, 104, dtype=float), sampling_period=0.1*pq.ms, t_start=0*pq.s, units=pq.mV)) # should test annotations: 'channel_index', etc. def test_read_spiketrain_should_fail_with_analogsignal_file(self): io = self.io_cls(self.test_file) self.assertRaises(TypeError, io.read_spiketrain, channel_index=0) class BaseTestPyNNIO_Spikes(BaseTestPyNNIO): def setUp(self): self.test_file = "test_file_spikes.%s" % self.file_extension self.write_test_file("spikes") def test_read_segment_containing_spiketrains_using_eager_cascade(self): io = self.io_cls(self.test_file) segment = io.read_segment(lazy=False, cascade=True) self.assertIsInstance(segment, Segment) self.assertEqual(len(segment.spiketrains), NCELLS) st0 = segment.spiketrains[0] self.assertIsInstance(st0, SpikeTrain) assert_arrays_equal(st0, SpikeTrain(np.arange(0, 101, dtype=float), t_start=0*pq.s, t_stop=101*pq.ms, units=pq.ms)) st4 = segment.spiketrains[4] self.assertIsInstance(st4, SpikeTrain) assert_arrays_equal(st4, SpikeTrain(np.arange(4, 105, dtype=float), t_start=0*pq.s, t_stop=105*pq.ms, units=pq.ms)) # test annotations (stuff from file metadata) def test_read_spiketrain_using_eager(self): io = self.io_cls(self.test_file) st3 = io.read_spiketrain(lazy=False, channel_index=3) self.assertIsInstance(st3, SpikeTrain) assert_arrays_equal(st3, SpikeTrain(np.arange(3, 104, dtype=float), t_start=0*pq.s, t_stop=104*pq.s, units=pq.ms)) # should test annotations: 'channel_index', etc. def test_read_analogsignal_should_fail_with_spiketrain_file(self): io = self.io_cls(self.test_file) self.assertRaises(TypeError, io.read_analogsignal, channel_index=2) class BaseTestPyNNNumpyIO(object): io_cls = PyNNNumpyIO file_extension = "npz" def write_test_file(self, variable='v', check=False): data, metadata = self.build_test_data(variable) metadata_array = np.array(sorted(metadata.items())) np.savez(self.test_file, data=data, metadata=metadata_array) if check: data1, metadata1 = read_test_file(self.test_file) assert metadata == metadata1, "%s != %s" % (metadata, metadata1) assert data.shape == data1.shape == (505, 2), \ "%s, %s, (505, 2)" % (data.shape, data1.shape) assert (data == data1).all() assert metadata["n"] == 505 write_test_file.__test__ = False class BaseTestPyNNTextIO(object): io_cls = PyNNTextIO file_extension = "txt" def write_test_file(self, variable='v', check=False): data, metadata = self.build_test_data(variable) with open(self.test_file, 'wb') as f: for item in sorted(metadata.items()): f.write(("# %s = %s\n" % item).encode('utf8')) np.savetxt(f, data) if check: raise NotImplementedError write_test_file.__test__ = False class TestPyNNNumpyIO_Signals(BaseTestPyNNNumpyIO, BaseTestPyNNIO_Signals, unittest.TestCase): __test__ = True class TestPyNNNumpyIO_Spikes(BaseTestPyNNNumpyIO, BaseTestPyNNIO_Spikes, unittest.TestCase): __test__ = True class TestPyNNTextIO_Signals(BaseTestPyNNTextIO, BaseTestPyNNIO_Signals, unittest.TestCase): __test__ = True class TestPyNNTextIO_Spikes(BaseTestPyNNTextIO, BaseTestPyNNIO_Spikes, unittest.TestCase): __test__ = True if __name__ == '__main__': unittest.main() neo-0.3.3/neo/test/iotest/test_spike2io.py0000644000175000017500000000115312265516260021554 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.spike2io """ # needed for python 3 compatibility from __future__ import absolute_import, division try: import unittest2 as unittest except ImportError: import unittest from neo.io import Spike2IO from neo.test.iotest.common_io_test import BaseTestIO class TestSpike2IO(BaseTestIO, unittest.TestCase, ): ioclass = Spike2IO files_to_test = ['File_spike2_1.smr', 'File_spike2_2.smr', 'File_spike2_3.smr', ] files_to_download = files_to_test if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/iotest/test_brainwaref32io.py0000644000175000017500000001334712273723542022656 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of neo.io.brainwaref32io """ # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import os.path import sys try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core import Block, RecordingChannelGroup, Segment, SpikeTrain, Unit from neo.io import BrainwareF32IO from neo.io.tools import create_many_to_one_relationship from neo.test.iotest.common_io_test import BaseTestIO from neo.test.tools import (assert_same_sub_schema, assert_neo_object_is_compliant) from neo.test.iotest.tools import create_generic_reader PY_VER = sys.version_info[0] def proc_f32(filename): '''Load an f32 file that has already been processed by the official matlab file converter. That matlab data is saved to an m-file, which is then converted to a numpy '.npz' file. This numpy file is the file actually loaded. This function converts it to a neo block and returns the block. This block can be compared to the block produced by BrainwareF32IO to make sure BrainwareF32IO is working properly block = proc_f32(filename) filename: The file name of the numpy file to load. It should end with '*_f32_py?.npz'. This will be converted to a neo 'file_origin' property with the value '*.f32', so the filename to compare should fit that pattern. 'py?' should be 'py2' for the python 2 version of the numpy file or 'py3' for the python 3 version of the numpy file. example: filename = 'file1_f32_py2.npz' f32 file name = 'file1.f32' ''' filenameorig = os.path.basename(filename[:-12]+'.f32') # create the objects to store other objects block = Block(file_origin=filenameorig) rcg = RecordingChannelGroup(file_origin=filenameorig) rcg.channel_indexes = np.array([], dtype=np.int) rcg.channel_names = np.array([], dtype='S') unit = Unit(file_origin=filenameorig) # load objects into their containers block.recordingchannelgroups.append(rcg) rcg.units.append(unit) try: with np.load(filename) as f32obj: f32file = f32obj.items()[0][1].flatten() except IOError as exc: if 'as a pickle' in exc.message: create_many_to_one_relationship(block) return block else: raise sweeplengths = [res[0, 0].tolist() for res in f32file['sweeplength']] stims = [res.flatten().tolist() for res in f32file['stim']] sweeps = [res['spikes'].flatten() for res in f32file['sweep'] if res.size] fullf32 = zip(sweeplengths, stims, sweeps) for sweeplength, stim, sweep in fullf32: for trainpts in sweep: if trainpts.size: trainpts = trainpts.flatten().astype('float32') else: trainpts = [] paramnames = ['Param%s' % i for i in range(len(stim))] params = dict(zip(paramnames, stim)) train = SpikeTrain(trainpts, units=pq.ms, t_start=0, t_stop=sweeplength, file_origin=filenameorig) segment = Segment(file_origin=filenameorig, **params) segment.spiketrains = [train] unit.spiketrains.append(train) block.segments.append(segment) create_many_to_one_relationship(block) return block class BrainwareF32IOTestCase(BaseTestIO, unittest.TestCase): ''' Unit test testcase for neo.io.BrainwareF32IO ''' ioclass = BrainwareF32IO read_and_write_is_bijective = False # These are the files it tries to read and test for compliance files_to_test = ['block_300ms_4rep_1clust_part_ch1.f32', 'block_500ms_5rep_empty_fullclust_ch1.f32', 'block_500ms_5rep_empty_partclust_ch1.f32', 'interleaved_500ms_5rep_ch2.f32', 'interleaved_500ms_5rep_nospikes_ch1.f32', 'multi_500ms_mulitrep_ch1.f32', 'random_500ms_12rep_noclust_part_ch2.f32', 'sequence_500ms_5rep_ch2.f32'] # add the appropriate suffix depending on the python version suffix = '_f32_py%s.npz' % PY_VER files_to_download = files_to_test[:] # add the reference files to the list of files to download files_to_compare = [] for fname in files_to_test: if fname: files_to_compare.append(os.path.splitext(fname)[0] + suffix) # Will fetch from g-node if they don't already exist locally # How does it know to do this before any of the other tests? files_to_download = files_to_test + files_to_compare def test_reading_same(self): for ioobj, path in self.iter_io_objects(return_path=True): obj_reader_base = create_generic_reader(ioobj, target=False) obj_reader_single = create_generic_reader(ioobj) obj_base = obj_reader_base() obj_single = obj_reader_single() try: assert_same_sub_schema(obj_base, obj_single) except BaseException as exc: exc.args += ('from ' + os.path.basename(path),) raise def test_against_reference(self): for obj, path in self.iter_objects(return_path=True): filename = os.path.basename(path) refpath = os.path.splitext(path)[0] + self.suffix refobj = proc_f32(refpath) try: assert_neo_object_is_compliant(obj) assert_neo_object_is_compliant(refobj) assert_same_sub_schema(obj, refobj) except BaseException as exc: exc.args += ('from ' + filename,) raise if __name__ == '__main__': unittest.main() neo-0.3.3/neo/test/test_epoch.py0000644000175000017500000000333112273723542017620 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.epoch.Epoch class """ try: import unittest2 as unittest except ImportError: import unittest import quantities as pq from neo.core.epoch import Epoch from neo.test.tools import assert_neo_object_is_compliant class TestEpoch(unittest.TestCase): def test_epoch_creation(self): params = {'testarg2': 'yes', 'testarg3': True} epc = Epoch(1.5*pq.ms, duration=20*pq.ns, label='test epoch', name='test', description='tester', file_origin='test.file', testarg1=1, **params) epc.annotate(testarg1=1.1, testarg0=[1, 2, 3]) assert_neo_object_is_compliant(epc) self.assertEqual(epc.time, 1.5*pq.ms) self.assertEqual(epc.duration, 20*pq.ns) self.assertEqual(epc.label, 'test epoch') self.assertEqual(epc.name, 'test') self.assertEqual(epc.description, 'tester') self.assertEqual(epc.file_origin, 'test.file') self.assertEqual(epc.annotations['testarg0'], [1, 2, 3]) self.assertEqual(epc.annotations['testarg1'], 1.1) self.assertEqual(epc.annotations['testarg2'], 'yes') self.assertTrue(epc.annotations['testarg3']) def test_epoch_merge_NotImplementedError(self): epc1 = Epoch(1.5*pq.ms, duration=20*pq.ns, label='test epoch', name='test', description='tester', file_origin='test.file') epc2 = Epoch(1.5*pq.ms, duration=20*pq.ns, label='test epoch', name='test', description='tester', file_origin='test.file') self.assertRaises(NotImplementedError, epc1.merge, epc2) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_unit.py0000644000175000017500000001462512273723542017511 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.unit.Unit class """ try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.unit import Unit from neo.core.spiketrain import SpikeTrain from neo.core.spike import Spike from neo.test.tools import assert_neo_object_is_compliant, assert_arrays_equal from neo.io.tools import create_many_to_one_relationship class TestUnit(unittest.TestCase): def setUp(self): self.setup_spikes() self.setup_spiketrains() self.setup_units() def setup_units(self): params = {'testarg2': 'yes', 'testarg3': True} self.unit1 = Unit(name='test', description='tester 1', file_origin='test.file', channels_indexes=[1], testarg1=1, **params) self.unit2 = Unit(name='test', description='tester 2', file_origin='test.file', channels_indexes=[2], testarg1=1, **params) self.unit1.annotate(testarg1=1.1, testarg0=[1, 2, 3]) self.unit2.annotate(testarg11=1.1, testarg10=[1, 2, 3]) self.unit1.spiketrains = self.train1 self.unit2.spiketrains = self.train2 self.unit1.spikes = self.spike1 self.unit2.spikes = self.spike2 create_many_to_one_relationship(self.unit1) create_many_to_one_relationship(self.unit2) def setup_spikes(self): spikename11 = 'spike 1 1' spikename12 = 'spike 1 2' spikename21 = 'spike 2 1' spikename22 = 'spike 2 2' spikedata11 = 10 * pq.ms spikedata12 = 20 * pq.ms spikedata21 = 30 * pq.s spikedata22 = 40 * pq.s self.spikenames1 = [spikename11, spikename12] self.spikenames2 = [spikename21, spikename22] self.spikenames = [spikename11, spikename12, spikename21, spikename22] spike11 = Spike(spikedata11, t_stop=100*pq.s, name=spikename11) spike12 = Spike(spikedata12, t_stop=100*pq.s, name=spikename12) spike21 = Spike(spikedata21, t_stop=100*pq.s, name=spikename21) spike22 = Spike(spikedata22, t_stop=100*pq.s, name=spikename22) self.spike1 = [spike11, spike12] self.spike2 = [spike21, spike22] self.spike = [spike11, spike12, spike21, spike22] def setup_spiketrains(self): trainname11 = 'spiketrain 1 1' trainname12 = 'spiketrain 1 2' trainname21 = 'spiketrain 2 1' trainname22 = 'spiketrain 2 2' traindata11 = np.arange(0, 10) * pq.ms traindata12 = np.arange(10, 20) * pq.ms traindata21 = np.arange(20, 30) * pq.s traindata22 = np.arange(30, 40) * pq.s self.trainnames1 = [trainname11, trainname12] self.trainnames2 = [trainname21, trainname22] self.trainnames = [trainname11, trainname12, trainname21, trainname22] train11 = SpikeTrain(traindata11, t_stop=100*pq.s, name=trainname11) train12 = SpikeTrain(traindata12, t_stop=100*pq.s, name=trainname12) train21 = SpikeTrain(traindata21, t_stop=100*pq.s, name=trainname21) train22 = SpikeTrain(traindata22, t_stop=100*pq.s, name=trainname22) self.train1 = [train11, train12] self.train2 = [train21, train22] self.train = [train11, train12, train21, train22] def test_unit_creation(self): assert_neo_object_is_compliant(self.unit1) assert_neo_object_is_compliant(self.unit2) self.assertEqual(self.unit1.name, 'test') self.assertEqual(self.unit2.name, 'test') self.assertEqual(self.unit1.description, 'tester 1') self.assertEqual(self.unit2.description, 'tester 2') self.assertEqual(self.unit1.file_origin, 'test.file') self.assertEqual(self.unit2.file_origin, 'test.file') self.assertEqual(self.unit1.annotations['testarg0'], [1, 2, 3]) self.assertEqual(self.unit2.annotations['testarg10'], [1, 2, 3]) self.assertEqual(self.unit1.annotations['testarg1'], 1.1) self.assertEqual(self.unit2.annotations['testarg1'], 1) self.assertEqual(self.unit2.annotations['testarg11'], 1.1) self.assertEqual(self.unit1.annotations['testarg2'], 'yes') self.assertEqual(self.unit2.annotations['testarg2'], 'yes') self.assertTrue(self.unit1.annotations['testarg3']) self.assertTrue(self.unit2.annotations['testarg3']) self.assertTrue(hasattr(self.unit1, 'spikes')) self.assertTrue(hasattr(self.unit2, 'spikes')) self.assertEqual(len(self.unit1.spikes), 2) self.assertEqual(len(self.unit2.spikes), 2) for res, targ in zip(self.unit1.spikes, self.spike1): self.assertEqual(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.unit2.spikes, self.spike2): self.assertEqual(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.unit1, 'spiketrains')) self.assertTrue(hasattr(self.unit2, 'spiketrains')) self.assertEqual(len(self.unit1.spiketrains), 2) self.assertEqual(len(self.unit2.spiketrains), 2) for res, targ in zip(self.unit1.spiketrains, self.train1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.unit2.spiketrains, self.train2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) def test_unit_merge(self): self.unit1.merge(self.unit2) spikeres1 = [sig.name for sig in self.unit1.spikes] spikeres2 = [sig.name for sig in self.unit2.spikes] trainres1 = [sig.name for sig in self.unit1.spiketrains] trainres2 = [sig.name for sig in self.unit2.spiketrains] self.assertEqual(spikeres1, self.spikenames) self.assertEqual(spikeres2, self.spikenames2) self.assertEqual(trainres1, self.trainnames) self.assertEqual(trainres2, self.trainnames2) for res, targ in zip(self.unit1.spikes, self.spike): self.assertEqual(res, targ) for res, targ in zip(self.unit2.spikes, self.spike2): self.assertEqual(res, targ) for res, targ in zip(self.unit1.spiketrains, self.train): assert_arrays_equal(res, targ) for res, targ in zip(self.unit2.spiketrains, self.train2): assert_arrays_equal(res, targ) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_spike.py0000644000175000017500000001014212273723542017633 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.spike.Spike class """ try: import unittest2 as unittest except ImportError: import unittest import quantities as pq from neo.core.spike import Spike from neo.test.tools import assert_arrays_equal, assert_neo_object_is_compliant class TestSpike(unittest.TestCase): def setUp(self): params = {'testarg2': 'yes', 'testarg3': True} self.sampling_rate1 = .1*pq.Hz self.left_sweep1 = 2.*pq.s self.spike1 = Spike(1.5*pq.ms, waveform=[[1.1, 1.5, 1.7], [2.2, 2.6, 2.8]]*pq.mV, sampling_rate=self.sampling_rate1, left_sweep=self.left_sweep1, name='test', description='tester', file_origin='test.file', testarg1=1, **params) self.spike1.annotate(testarg1=1.1, testarg0=[1, 2, 3]) def test_spike_creation(self): assert_neo_object_is_compliant(self.spike1) self.assertEqual(self.spike1.time, 1.5*pq.ms) assert_arrays_equal(self.spike1.waveform, [[1.1, 1.5, 1.7], [2.2, 2.6, 2.8]]*pq.mV) self.assertEqual(self.spike1.sampling_rate, .1*pq.Hz) self.assertEqual(self.spike1.left_sweep, 2.*pq.s) self.assertEqual(self.spike1.description, 'tester') self.assertEqual(self.spike1.file_origin, 'test.file') self.assertEqual(self.spike1.annotations['testarg0'], [1, 2, 3]) self.assertEqual(self.spike1.annotations['testarg1'], 1.1) self.assertEqual(self.spike1.annotations['testarg2'], 'yes') self.assertTrue(self.spike1.annotations['testarg3']) def test__duration(self): result1 = self.spike1.duration self.spike1.sampling_rate = None assert_neo_object_is_compliant(self.spike1) result2 = self.spike1.duration self.spike1.sampling_rate = self.sampling_rate1 self.spike1.waveform = None assert_neo_object_is_compliant(self.spike1) result3 = self.spike1.duration self.assertEqual(result1, 30./pq.Hz) self.assertEqual(result1.units, 1./pq.Hz) self.assertEqual(result2, None) self.assertEqual(result3, None) def test__sampling_period(self): result1 = self.spike1.sampling_period self.spike1.sampling_rate = None assert_neo_object_is_compliant(self.spike1) result2 = self.spike1.sampling_period self.spike1.sampling_rate = self.sampling_rate1 self.spike1.sampling_period = 10.*pq.ms assert_neo_object_is_compliant(self.spike1) result3a = self.spike1.sampling_period result3b = self.spike1.sampling_rate self.spike1.sampling_period = None result4a = self.spike1.sampling_period result4b = self.spike1.sampling_rate self.assertEqual(result1, 10./pq.Hz) self.assertEqual(result1.units, 1./pq.Hz) self.assertEqual(result2, None) self.assertEqual(result3a, 10.*pq.ms) self.assertEqual(result3a.units, 1.*pq.ms) self.assertEqual(result3b, .1/pq.ms) self.assertEqual(result3b.units, 1./pq.ms) self.assertEqual(result4a, None) self.assertEqual(result4b, None) def test__right_sweep(self): result1 = self.spike1.right_sweep self.spike1.left_sweep = None assert_neo_object_is_compliant(self.spike1) result2 = self.spike1.right_sweep self.spike1.left_sweep = self.left_sweep1 self.spike1.sampling_rate = None assert_neo_object_is_compliant(self.spike1) result3 = self.spike1.right_sweep self.spike1.sampling_rate = self.sampling_rate1 self.spike1.waveform = None assert_neo_object_is_compliant(self.spike1) result4 = self.spike1.right_sweep self.assertEqual(result1, 32.*pq.s) self.assertEqual(result1.units, 1.*pq.s) self.assertEqual(result2, None) self.assertEqual(result3, None) self.assertEqual(result4, None) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_eventarray.py0000644000175000017500000001076612273723542020714 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.eventarray.EventArray class """ try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.eventarray import EventArray from neo.test.tools import (assert_neo_object_is_compliant, assert_arrays_equal, assert_same_sub_schema) class TestEventArray(unittest.TestCase): def test_EventArray_creation(self): params = {'testarg2': 'yes', 'testarg3': True} evta = EventArray([1.1, 1.5, 1.7]*pq.ms, labels=np.array(['test event 1', 'test event 2', 'test event 3'], dtype='S'), name='test', description='tester', file_origin='test.file', testarg1=1, **params) evta.annotate(testarg1=1.1, testarg0=[1, 2, 3]) assert_neo_object_is_compliant(evta) assert_arrays_equal(evta.times, [1.1, 1.5, 1.7]*pq.ms) assert_arrays_equal(evta.labels, np.array(['test event 1', 'test event 2', 'test event 3'], dtype='S')) self.assertEqual(evta.name, 'test') self.assertEqual(evta.description, 'tester') self.assertEqual(evta.file_origin, 'test.file') self.assertEqual(evta.annotations['testarg0'], [1, 2, 3]) self.assertEqual(evta.annotations['testarg1'], 1.1) self.assertEqual(evta.annotations['testarg2'], 'yes') self.assertTrue(evta.annotations['testarg3']) def test_EventArray_repr(self): params = {'testarg2': 'yes', 'testarg3': True} evta = EventArray([1.1, 1.5, 1.7]*pq.ms, labels=np.array(['test event 1', 'test event 2', 'test event 3'], dtype='S'), name='test', description='tester', file_origin='test.file', testarg1=1, **params) evta.annotate(testarg1=1.1, testarg0=[1, 2, 3]) assert_neo_object_is_compliant(evta) targ = ('') res = repr(evta) self.assertEqual(targ, res) def test_EventArray_merge(self): params1 = {'testarg2': 'yes', 'testarg3': True} params2 = {'testarg2': 'no', 'testarg4': False} paramstarg = {'testarg2': 'yes;no', 'testarg3': True, 'testarg4': False} epca1 = EventArray([1.1, 1.5, 1.7]*pq.ms, labels=np.array(['test event 1 1', 'test event 1 2', 'test event 1 3'], dtype='S'), name='test', description='tester 1', file_origin='test.file', testarg1=1, **params1) epca2 = EventArray([2.1, 2.5, 2.7]*pq.us, labels=np.array(['test event 2 1', 'test event 2 2', 'test event 2 3'], dtype='S'), name='test', description='tester 2', file_origin='test.file', testarg1=1, **params2) epcatarg = EventArray([1.1, 1.5, 1.7, .0021, .0025, .0027]*pq.ms, labels=np.array(['test event 1 1', 'test event 1 2', 'test event 1 3', 'test event 2 1', 'test event 2 2', 'test event 2 3'], dtype='S'), name='test', description='merge(tester 1, tester 2)', file_origin='test.file', testarg1=1, **paramstarg) assert_neo_object_is_compliant(epca1) assert_neo_object_is_compliant(epca2) assert_neo_object_is_compliant(epcatarg) epcares = epca1.merge(epca2) assert_neo_object_is_compliant(epcares) assert_same_sub_schema(epcatarg, epcares) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_base.py0000644000175000017500000010342112273723542017435 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.baseneo.BaseNeo class and related functions """ from datetime import datetime, date, time, timedelta from decimal import Decimal from fractions import Fraction import sys try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.baseneo import (BaseNeo, _check_annotations, merge_annotation, merge_annotations) from neo.test.tools import assert_arrays_equal if sys.version_info[0] >= 3: _bytes = bytes long = int def bytes(s): return _bytes(s, encoding='ascii') class Test_check_annotations(unittest.TestCase): ''' TestCase to make sure _check_annotations works ''' def setUp(self): self.values = [1, 2.2, 3 + 2j, 'test', r'test', b'test', None, datetime(year=2008, month=12, day=3, hour=10, minute=4), timedelta(weeks=2, days=7, hours=18, minutes=28, seconds=18, milliseconds=28, microseconds=45), time(hour=10, minute=4), Decimal("3.14"), Fraction(13, 21), np.array([1.1, 1.2, 1.3]), np.array([1, 2, 3]), np.array('test', dtype='S'), np.array([True, False])] def test__check_annotations__invalid_ValueError(self): value = set([]) self.assertRaises(ValueError, _check_annotations, value) def test__check_annotations__invalid_dtype_ValueError(self): value = np.array([], dtype='O') self.assertRaises(ValueError, _check_annotations, value) def test__check_annotations__valid_dtypes(self): for value in self.values: _check_annotations(value) def test__check_annotations__list(self): _check_annotations(self.values) def test__check_annotations__tuple(self): _check_annotations(tuple(self.values)) _check_annotations((self.values, self.values)) def test__check_annotations__dict(self): names = ['value%s' % i for i in range(len(self.values))] values = dict(zip(names, self.values)) _check_annotations(values) class Test_merge_annotation_annotations(unittest.TestCase): ''' TestCase to make sure merge_annotation and merge_annotations work ''' def test_merge_annotation__different_type_AssertionError(self): value1 = 'test' value2 = 5.5 self.assertRaises(AssertionError, merge_annotation, value1, value2) def test_merge_annotation__unmergable_unequal_AssertionError(self): value1 = 5.6 value2 = 5.5 self.assertRaises(AssertionError, merge_annotation, value1, value2) def test_merge_annotation__str_unequal(self): value1 = 'test1' value2 = 'test2' targ = 'test1;test2' res = merge_annotation(value1, value2) self.assertEqual(targ, res) def test_merge_annotation__str_equal(self): value1 = 'test1' value2 = 'test1' targ = 'test1' res = merge_annotation(value1, value2) self.assertEqual(targ, res) def test_merge_annotation__ndarray(self): value1 = np.array([1, 2, 3]) value2 = np.array([4, 5]) targ = np.array([1, 2, 3, 4, 5]) res = merge_annotation(value1, value2) assert_arrays_equal(targ, res) def test_merge_annotation__float_equal(self): value1 = 5.5 value2 = 5.5 targ = 5.5 res = merge_annotation(value1, value2) self.assertEqual(targ, res) def test_merge_annotation__dict(self): value1 = {'val1': 1, 'val2': 2.2, 'val3': 'test1'} value2 = {'val2': 2.2, 'val3': 'test2', 'val4': [4, 4.4], 'val5': True} targ = {'val1': 1, 'val2': 2.2, 'val3': 'test1;test2', 'val4': [4, 4.4], 'val5': True} res = merge_annotation(value1, value2) self.assertEqual(targ, res) def test_merge_annotations__dict(self): value1 = {'val1': 1, 'val2': 2.2, 'val3': 'test1'} value2 = {'val2': 2.2, 'val3': 'test2', 'val4': [4, 4.4], 'val5': True} targ = {'val1': 1, 'val2': 2.2, 'val3': 'test1;test2', 'val4': [4, 4.4], 'val5': True} res = merge_annotations(value1, value2) self.assertEqual(targ, res) def test_merge_annotations__different_type_AssertionError(self): value1 = {'val1': 1, 'val2': 2.2, 'val3': 'tester'} value2 = {'val3': False, 'val4': [4, 4.4], 'val5': True} self.assertRaises(AssertionError, merge_annotations, value1, value2) def test_merge_annotations__unmergable_unequal_AssertionError(self): value1 = {'val1': 1, 'val2': 2.2, 'val3': True} value2 = {'val3': False, 'val4': [4, 4.4], 'val5': True} self.assertRaises(AssertionError, merge_annotation, value1, value2) class TestBaseNeo(unittest.TestCase): ''' TestCase to make sure basic initialization and methods work ''' def test_init(self): '''test to make sure initialization works properly''' base = BaseNeo(name='a base', description='this is a test') self.assertEqual(base.name, 'a base') self.assertEqual(base.description, 'this is a test') self.assertEqual(base.file_origin, None) def test_annotate(self): '''test to make sure annotation works properly''' base = BaseNeo() base.annotate(test1=1, test2=1) result1 = {'test1': 1, 'test2': 1} self.assertDictEqual(result1, base.annotations) base.annotate(test3=2, test4=3) result2 = {'test3': 2, 'test4': 3} result2a = dict(list(result1.items()) + list(result2.items())) self.assertDictContainsSubset(result1, base.annotations) self.assertDictContainsSubset(result2, base.annotations) self.assertDictEqual(result2a, base.annotations) base.annotate(test1=5, test2=8) result3 = {'test1': 5, 'test2': 8} result3a = dict(list(result3.items()) + list(result2.items())) self.assertDictContainsSubset(result2, base.annotations) self.assertDictContainsSubset(result3, base.annotations) self.assertDictEqual(result3a, base.annotations) self.assertNotEqual(base.annotations['test1'], result1['test1']) self.assertNotEqual(base.annotations['test2'], result1['test2']) class TestBaseNeoCoreTypes(unittest.TestCase): ''' TestCase to make sure annotations are properly checked for core built-in python data types ''' def setUp(self): '''create the instance to be tested, called before every test''' self.base = BaseNeo() def test_python_nonetype(self): '''test to make sure None type data is accepted''' value = None self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_int(self): '''test to make sure int type data is accepted''' value = 10 self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_long(self): '''test to make sure long type data is accepted''' value = long(7) self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_float(self): '''test to make sure float type data is accepted''' value = 9.2 self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_complex(self): '''test to make sure complex type data is accepted''' value = complex(23.17, 11.29) self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_string(self): '''test to make sure string type data is accepted''' value = 'this is a test' self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_unicode(self): '''test to make sure unicode type data is accepted''' value = u'this is also a test' self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_bytes(self): '''test to make sure bytes type data is accepted''' value = bytes('1,2,3,4,5') self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) class TestBaseNeoStandardLibraryTypes(unittest.TestCase): ''' TestCase to make sure annotations are properly checked for data types from the python standard library that are not core built-in data types ''' def setUp(self): '''create the instance to be tested, called before every test''' self.base = BaseNeo() def test_python_fraction(self): '''test to make sure Fraction type data is accepted''' value = Fraction(13, 21) self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_decimal(self): '''test to make sure Decimal type data is accepted''' value = Decimal("3.14") self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_datetime(self): '''test to make sure datetime type data is accepted''' value = datetime(year=2008, month=12, day=3, hour=10, minute=4) self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_date(self): '''test to make sure date type data is accepted''' value = date(year=2008, month=12, day=3) self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_time(self): '''test to make sure time type data is accepted''' value = time(hour=10, minute=4) self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_timedelta(self): '''test to make sure timedelta type data is accepted''' value = timedelta(weeks=2, days=7, hours=18, minutes=28, seconds=18, milliseconds=28, microseconds=45) self.base.annotate(data=value) result = {'data': value} self.assertEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) class TestBaseNeoContainerTypes(unittest.TestCase): ''' TestCase to make sure annotations are properly checked for data type inside python built-in container types ''' def setUp(self): '''create the instance to be tested, called before every test''' self.base = BaseNeo() def test_python_list(self): '''test to make sure list type data is accepted''' value = [None, 10, 9.2, complex(23, 11), ['this is a test', bytes('1,2,3,4,5')], [Fraction(13, 21), Decimal("3.14")]] self.base.annotate(data=value) result = {'data': value} self.assertListEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_tuple(self): '''test to make sure tuple type data is accepted''' value = (None, 10, 9.2, complex(23, 11), ('this is a test', bytes('1,2,3,4,5')), (Fraction(13, 21), Decimal("3.14"))) self.base.annotate(data=value) result = {'data': value} self.assertTupleEqual(value, self.base.annotations['data']) self.assertDictEqual(result, self.base.annotations) def test_python_dict(self): '''test to make sure dict type data is accepted''' value = {'NoneType': None, 'int': 10, 'float': 9.2, 'complex': complex(23, 11), 'dict1': {'string': 'this is a test', 'bytes': bytes('1,2,3,4,5')}, 'dict2': {'Fraction': Fraction(13, 21), 'Decimal': Decimal("3.14")}} self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_python_set(self): '''test to make sure set type data is rejected''' value = set([None, 10, 9.2, complex(23, 11)]) self.assertRaises(ValueError, self.base.annotate, data=value) def test_python_frozenset(self): '''test to make sure frozenset type data is rejected''' value = frozenset([None, 10, 9.2, complex(23, 11)]) self.assertRaises(ValueError, self.base.annotate, data=value) def test_python_iter(self): '''test to make sure iter type data is rejected''' value = iter([None, 10, 9.2, complex(23, 11)]) self.assertRaises(ValueError, self.base.annotate, data=value) class TestBaseNeoNumpyArrayTypes(unittest.TestCase): ''' TestCase to make sure annotations are properly checked for numpy arrays ''' def setUp(self): '''create the instance to be tested, called before every test''' self.base = BaseNeo() def test_numpy_array_int(self): '''test to make sure int type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.int) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_uint(self): '''test to make sure uint type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.uint) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_int0(self): '''test to make sure int0 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.int0) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_uint0(self): '''test to make sure uint0 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.uint0) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_int8(self): '''test to make sure int8 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.int8) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_uint8(self): '''test to make sure uint8 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.uint8) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_int16(self): '''test to make sure int16 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.int16) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_uint16(self): '''test to make sure uint16 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.uint16) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_int32(self): '''test to make sure int32 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.int32) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_uint32(self): '''test to make sure uint32 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.uint32) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_int64(self): '''test to make sure int64 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.int64) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_uint64(self): '''test to make sure uint64 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.uint64) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_float(self): '''test to make sure float type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.float) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_floating(self): '''test to make sure floating type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.floating) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_double(self): '''test to make sure double type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.double) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_float16(self): '''test to make sure float16 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.float16) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_float32(self): '''test to make sure float32 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.float32) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_float64(self): '''test to make sure float64 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.float64) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) @unittest.skipUnless(hasattr(np, "float128"), "float128 not available") def test_numpy_array_float128(self): '''test to make sure float128 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.float128) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_complex(self): '''test to make sure complex type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.complex) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_complex64(self): '''test to make sure complex64 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.complex64) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_complex128(self): '''test to make sure complex128 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.complex128) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) @unittest.skipUnless(hasattr(np, "complex256"), "complex256 not available") def test_numpy_scalar_complex256(self): '''test to make sure complex256 type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.complex256) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_bool(self): '''test to make sure bool type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.bool) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_str(self): '''test to make sure str type numpy arrays are accepted''' value = np.array([1, 2, 3, 4, 5], dtype=np.str) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_string0(self): '''test to make sure string0 type numpy arrays are accepted''' if sys.version_info[0] >= 3: dtype = np.str0 else: dtype = np.string0 value = np.array([1, 2, 3, 4, 5], dtype=dtype) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) class TestBaseNeoNumpyScalarTypes(unittest.TestCase): ''' TestCase to make sure annotations are properly checked for numpy scalars ''' def setUp(self): '''create the instance to be tested, called before every test''' self.base = BaseNeo() def test_numpy_scalar_int(self): '''test to make sure int type numpy scalars are accepted''' value = np.array(99, dtype=np.int) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_uint(self): '''test to make sure uint type numpy scalars are accepted''' value = np.array(99, dtype=np.uint) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_int0(self): '''test to make sure int0 type numpy scalars are accepted''' value = np.array(99, dtype=np.int0) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_uint0(self): '''test to make sure uint0 type numpy scalars are accepted''' value = np.array(99, dtype=np.uint0) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_int8(self): '''test to make sure int8 type numpy scalars are accepted''' value = np.array(99, dtype=np.int8) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_uint8(self): '''test to make sure uint8 type numpy scalars are accepted''' value = np.array(99, dtype=np.uint8) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_int16(self): '''test to make sure int16 type numpy scalars are accepted''' value = np.array(99, dtype=np.int16) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_uint16(self): '''test to make sure uint16 type numpy scalars are accepted''' value = np.array(99, dtype=np.uint16) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_int32(self): '''test to make sure int32 type numpy scalars are accepted''' value = np.array(99, dtype=np.int32) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_uint32(self): '''test to make sure uint32 type numpy scalars are accepted''' value = np.array(99, dtype=np.uint32) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_int64(self): '''test to make sure int64 type numpy scalars are accepted''' value = np.array(99, dtype=np.int64) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_uint64(self): '''test to make sure uint64 type numpy scalars are accepted''' value = np.array(99, dtype=np.uint64) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_float(self): '''test to make sure float type numpy scalars are accepted''' value = np.array(99, dtype=np.float) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_floating(self): '''test to make sure floating type numpy scalars are accepted''' value = np.array(99, dtype=np.floating) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_double(self): '''test to make sure double type numpy scalars are accepted''' value = np.array(99, dtype=np.double) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_float16(self): '''test to make sure float16 type numpy scalars are accepted''' value = np.array(99, dtype=np.float16) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_float32(self): '''test to make sure float32 type numpy scalars are accepted''' value = np.array(99, dtype=np.float32) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_float64(self): '''test to make sure float64 type numpy scalars are accepted''' value = np.array(99, dtype=np.float64) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) @unittest.skipUnless(hasattr(np, "float128"), "float128 not available") def test_numpy_scalar_float128(self): '''test to make sure float128 type numpy scalars are accepted''' value = np.array(99, dtype=np.float128) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_complex(self): '''test to make sure complex type numpy scalars are accepted''' value = np.array(99, dtype=np.complex) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_complex64(self): '''test to make sure complex64 type numpy scalars are accepted''' value = np.array(99, dtype=np.complex64) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_complex128(self): '''test to make sure complex128 type numpy scalars are accepted''' value = np.array(99, dtype=np.complex128) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) @unittest.skipUnless(hasattr(np, "complex256"), "complex256 not available") def test_numpy_scalar_complex256(self): '''test to make sure complex256 type numpy scalars are accepted''' value = np.array(99, dtype=np.complex256) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_bool(self): '''test to make sure bool type numpy scalars are rejected''' value = np.array(99, dtype=np.bool) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_array_str(self): '''test to make sure str type numpy scalars are accepted''' value = np.array(99, dtype=np.str) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_numpy_scalar_string0(self): '''test to make sure string0 type numpy scalars are rejected''' if sys.version_info[0] >= 3: dtype = np.str0 else: dtype = np.string0 value = np.array(99, dtype=dtype) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) class TestBaseNeoQuantitiesArrayTypes(unittest.TestCase): ''' TestCase to make sure annotations are properly checked for quantities arrays ''' def setUp(self): '''create the instance to be tested, called before every test''' self.base = BaseNeo() def test_quantities_array_int(self): '''test to make sure int type quantites arrays are accepted''' value = pq.Quantity([1, 2, 3, 4, 5], dtype=np.int, units=pq.s) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_quantities_array_uint(self): '''test to make sure uint type quantites arrays are accepted''' value = pq.Quantity([1, 2, 3, 4, 5], dtype=np.uint, units=pq.meter) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_quantities_array_float(self): '''test to make sure float type quantites arrays are accepted''' value = [1, 2, 3, 4, 5] * pq.kg self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_quantities_array_str(self): '''test to make sure str type quantites arrays are accepted''' value = pq.Quantity([1, 2, 3, 4, 5], dtype=np.str, units=pq.meter) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) class TestBaseNeoQuantitiesScalarTypes(unittest.TestCase): ''' TestCase to make sure annotations are properly checked for quantities scalars ''' def setUp(self): '''create the instance to be tested, called before every test''' self.base = BaseNeo() def test_quantities_scalar_int(self): '''test to make sure int type quantites scalars are accepted''' value = pq.Quantity(99, dtype=np.int, units=pq.s) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_quantities_scalar_uint(self): '''test to make sure uint type quantites scalars are accepted''' value = pq.Quantity(99, dtype=np.uint, units=pq.meter) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_quantities_scalar_float(self): '''test to make sure float type quantites scalars are accepted''' value = 99 * pq.kg self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) def test_quantities_scalar_str(self): '''test to make sure str type quantites scalars are accepted''' value = pq.Quantity(99, dtype=np.str, units=pq.meter) self.base.annotate(data=value) result = {'data': value} self.assertDictEqual(result, self.base.annotations) class TestBaseNeoUserDefinedTypes(unittest.TestCase): ''' TestCase to make sure annotations are properly checked for arbitrary objects ''' def setUp(self): '''create the instance to be tested, called before every test''' self.base = BaseNeo() def test_my_class(self): '''test to make sure user defined class type data is rejected''' class Foo(object): pass value = Foo() self.assertRaises(ValueError, self.base.annotate, data=value) def test_my_class_list(self): '''test to make sure user defined class type data is rejected''' class Foo(object): pass value = [Foo(), Foo(), Foo()] self.assertRaises(ValueError, self.base.annotate, data=value) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_epocharray.py0000644000175000017500000001155012273723542020661 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.epocharray.EpochArray class """ try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.epocharray import EpochArray from neo.test.tools import (assert_neo_object_is_compliant, assert_arrays_equal, assert_same_sub_schema) class TestEpochArray(unittest.TestCase): def test_EpochArray_creation(self): params = {'testarg2': 'yes', 'testarg3': True} epca = EpochArray([1.1, 1.5, 1.7]*pq.ms, durations=[20, 40, 60]*pq.ns, labels=np.array(['test epoch 1', 'test epoch 2', 'test epoch 3'], dtype='S'), name='test', description='tester', file_origin='test.file', testarg1=1, **params) epca.annotate(testarg1=1.1, testarg0=[1, 2, 3]) assert_neo_object_is_compliant(epca) assert_arrays_equal(epca.times, [1.1, 1.5, 1.7]*pq.ms) assert_arrays_equal(epca.durations, [20, 40, 60]*pq.ns) assert_arrays_equal(epca.labels, np.array(['test epoch 1', 'test epoch 2', 'test epoch 3'], dtype='S')) self.assertEqual(epca.name, 'test') self.assertEqual(epca.description, 'tester') self.assertEqual(epca.file_origin, 'test.file') self.assertEqual(epca.annotations['testarg0'], [1, 2, 3]) self.assertEqual(epca.annotations['testarg1'], 1.1) self.assertEqual(epca.annotations['testarg2'], 'yes') self.assertTrue(epca.annotations['testarg3']) def test_EpochArray_repr(self): params = {'testarg2': 'yes', 'testarg3': True} epca = EpochArray([1.1, 1.5, 1.7]*pq.ms, durations=[20, 40, 60]*pq.ns, labels=np.array(['test epoch 1', 'test epoch 2', 'test epoch 3'], dtype='S'), name='test', description='tester', file_origin='test.file', testarg1=1, **params) epca.annotate(testarg1=1.1, testarg0=[1, 2, 3]) assert_neo_object_is_compliant(epca) targ = ('') res = repr(epca) self.assertEqual(targ, res) def test_EpochArray_merge(self): params1 = {'testarg2': 'yes', 'testarg3': True} params2 = {'testarg2': 'no', 'testarg4': False} paramstarg = {'testarg2': 'yes;no', 'testarg3': True, 'testarg4': False} epca1 = EpochArray([1.1, 1.5, 1.7]*pq.ms, durations=[20, 40, 60]*pq.us, labels=np.array(['test epoch 1 1', 'test epoch 1 2', 'test epoch 1 3'], dtype='S'), name='test', description='tester 1', file_origin='test.file', testarg1=1, **params1) epca2 = EpochArray([2.1, 2.5, 2.7]*pq.us, durations=[3, 5, 7]*pq.ms, labels=np.array(['test epoch 2 1', 'test epoch 2 2', 'test epoch 2 3'], dtype='S'), name='test', description='tester 2', file_origin='test.file', testarg1=1, **params2) epcatarg = EpochArray([1.1, 1.5, 1.7, .0021, .0025, .0027]*pq.ms, durations=[20, 40, 60, 3000, 5000, 7000]*pq.ns, labels=np.array(['test epoch 1 1', 'test epoch 1 2', 'test epoch 1 3', 'test epoch 2 1', 'test epoch 2 2', 'test epoch 2 3'], dtype='S'), name='test', description='merge(tester 1, tester 2)', file_origin='test.file', testarg1=1, **paramstarg) assert_neo_object_is_compliant(epca1) assert_neo_object_is_compliant(epca2) assert_neo_object_is_compliant(epcatarg) epcares = epca1.merge(epca2) assert_neo_object_is_compliant(epcares) assert_same_sub_schema(epcatarg, epcares) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_segment.py0000644000175000017500000011054712273723542020174 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.segment.Segment class """ try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.segment import Segment from neo.core import (AnalogSignal, AnalogSignalArray, Block, Epoch, EpochArray, Event, EventArray, IrregularlySampledSignal, RecordingChannelGroup, Spike, SpikeTrain, Unit) from neo.io.tools import create_many_to_one_relationship from neo.test.tools import assert_neo_object_is_compliant, assert_arrays_equal class TestSegment(unittest.TestCase): def setUp(self): self.setup_analogsignals() self.setup_analogsignalarrays() self.setup_epochs() self.setup_epocharrays() self.setup_events() self.setup_eventarrays() self.setup_irregularlysampledsignals() self.setup_spikes() self.setup_spiketrains() self.setup_units() self.setup_segments() def setup_segments(self): params = {'testarg2': 'yes', 'testarg3': True} self.segment1 = Segment(name='test', description='tester 1', file_origin='test.file', testarg1=1, **params) self.segment2 = Segment(name='test', description='tester 2', file_origin='test.file', testarg1=1, **params) self.segment1.annotate(testarg1=1.1, testarg0=[1, 2, 3]) self.segment2.annotate(testarg11=1.1, testarg10=[1, 2, 3]) self.segment1.analogsignals = self.sig1 self.segment2.analogsignals = self.sig2 self.segment1.analogsignalarrays = self.sigarr1 self.segment2.analogsignalarrays = self.sigarr2 self.segment1.epochs = self.epoch1 self.segment2.epochs = self.epoch2 self.segment1.epocharrays = self.epocharr1 self.segment2.epocharrays = self.epocharr2 self.segment1.events = self.event1 self.segment2.events = self.event2 self.segment1.eventarrays = self.eventarr1 self.segment2.eventarrays = self.eventarr2 self.segment1.irregularlysampledsignals = self.irsig1 self.segment2.irregularlysampledsignals = self.irsig2 self.segment1.spikes = self.spike1 self.segment2.spikes = self.spike2 self.segment1.spiketrains = self.train1 self.segment2.spiketrains = self.train2 create_many_to_one_relationship(self.segment1) create_many_to_one_relationship(self.segment2) def setup_units(self): params = {'testarg2': 'yes', 'testarg3': True} self.unit1 = Unit(name='test', description='tester 1', file_origin='test.file', channel_indexes=np.array([1]), testarg1=1, **params) self.unit2 = Unit(name='test', description='tester 2', file_origin='test.file', channel_indexes=np.array([2]), testarg1=1, **params) self.unit1.annotate(testarg1=1.1, testarg0=[1, 2, 3]) self.unit2.annotate(testarg11=1.1, testarg10=[1, 2, 3]) self.unit1train = [self.train1[0], self.train2[1]] self.unit2train = [self.train1[1], self.train2[0]] self.unit1.spiketrains = self.unit1train self.unit2.spiketrains = self.unit2train self.unit1spike = [self.spike1[0], self.spike2[1]] self.unit2spike = [self.spike1[1], self.spike2[0]] self.unit1.spikes = self.unit1spike self.unit2.spikes = self.unit2spike create_many_to_one_relationship(self.unit1) create_many_to_one_relationship(self.unit2) def setup_analogsignals(self): signame11 = 'analogsignal 1 1' signame12 = 'analogsignal 1 2' signame21 = 'analogsignal 2 1' signame22 = 'analogsignal 2 2' sigdata11 = np.arange(0, 10) * pq.mV sigdata12 = np.arange(10, 20) * pq.mV sigdata21 = np.arange(20, 30) * pq.V sigdata22 = np.arange(30, 40) * pq.V self.signames1 = [signame11, signame12] self.signames2 = [signame21, signame22] self.signames = [signame11, signame12, signame21, signame22] sig11 = AnalogSignal(sigdata11, name=signame11, channel_index=1, sampling_rate=1*pq.Hz) sig12 = AnalogSignal(sigdata12, name=signame12, channel_index=2, sampling_rate=1*pq.Hz) sig21 = AnalogSignal(sigdata21, name=signame21, channel_index=1, sampling_rate=1*pq.Hz) sig22 = AnalogSignal(sigdata22, name=signame22, channel_index=2, sampling_rate=1*pq.Hz) self.sig1 = [sig11, sig12] self.sig2 = [sig21, sig22] self.sig = [sig11, sig12, sig21, sig22] self.chan1sig = [self.sig1[0], self.sig2[0]] self.chan2sig = [self.sig1[1], self.sig2[1]] def setup_analogsignalarrays(self): sigarrname11 = 'analogsignalarray 1 1' sigarrname12 = 'analogsignalarray 1 2' sigarrname21 = 'analogsignalarray 2 1' sigarrname22 = 'analogsignalarray 2 2' sigarrdata11 = np.arange(0, 10).reshape(5, 2) * pq.mV sigarrdata12 = np.arange(10, 20).reshape(5, 2) * pq.mV sigarrdata21 = np.arange(20, 30).reshape(5, 2) * pq.V sigarrdata22 = np.arange(30, 40).reshape(5, 2) * pq.V sigarrdata112 = np.hstack([sigarrdata11, sigarrdata11]) * pq.mV self.sigarrnames1 = [sigarrname11, sigarrname12] self.sigarrnames2 = [sigarrname21, sigarrname22, sigarrname11] self.sigarrnames = [sigarrname11, sigarrname12, sigarrname21, sigarrname22] sigarr11 = AnalogSignalArray(sigarrdata11, name=sigarrname11, sampling_rate=1*pq.Hz, channel_index=np.array([1, 2])) sigarr12 = AnalogSignalArray(sigarrdata12, name=sigarrname12, sampling_rate=1*pq.Hz, channel_index=np.array([2, 1])) sigarr21 = AnalogSignalArray(sigarrdata21, name=sigarrname21, sampling_rate=1*pq.Hz, channel_index=np.array([1, 2])) sigarr22 = AnalogSignalArray(sigarrdata22, name=sigarrname22, sampling_rate=1*pq.Hz, channel_index=np.array([2, 1])) sigarr23 = AnalogSignalArray(sigarrdata11, name=sigarrname11, sampling_rate=1*pq.Hz, channel_index=np.array([1, 2])) sigarr112 = AnalogSignalArray(sigarrdata112, name=sigarrname11, sampling_rate=1*pq.Hz, channel_index=np.array([1, 2])) self.sigarr1 = [sigarr11, sigarr12] self.sigarr2 = [sigarr21, sigarr22, sigarr23] self.sigarr = [sigarr112, sigarr12, sigarr21, sigarr22] self.chan1sigarr1 = [sigarr11[:, 0:1], sigarr12[:, 1:2]] self.chan2sigarr1 = [sigarr11[:, 1:2], sigarr12[:, 0:1]] self.chan1sigarr2 = [sigarr21[:, 0:1], sigarr22[:, 1:2], sigarr23[:, 0:1]] self.chan2sigarr2 = [sigarr21[:, 1:2], sigarr22[:, 0:1], sigarr23[:, 0:1]] def setup_epochs(self): epochname11 = 'epoch 1 1' epochname12 = 'epoch 1 2' epochname21 = 'epoch 2 1' epochname22 = 'epoch 2 2' epochtime11 = 10 * pq.ms epochtime12 = 20 * pq.ms epochtime21 = 30 * pq.s epochtime22 = 40 * pq.s epochdur11 = 11 * pq.s epochdur12 = 21 * pq.s epochdur21 = 31 * pq.ms epochdur22 = 41 * pq.ms self.epochnames1 = [epochname11, epochname12] self.epochnames2 = [epochname21, epochname22] self.epochnames = [epochname11, epochname12, epochname21, epochname22] epoch11 = Epoch(epochtime11, epochdur11, label=epochname11, name=epochname11, channel_index=1, testattr=True) epoch12 = Epoch(epochtime12, epochdur12, label=epochname12, name=epochname12, channel_index=2, testattr=False) epoch21 = Epoch(epochtime21, epochdur21, label=epochname21, name=epochname21, channel_index=1) epoch22 = Epoch(epochtime22, epochdur22, label=epochname22, name=epochname22, channel_index=2) self.epoch1 = [epoch11, epoch12] self.epoch2 = [epoch21, epoch22] self.epoch = [epoch11, epoch12, epoch21, epoch22] def setup_epocharrays(self): epocharrname11 = 'epocharr 1 1' epocharrname12 = 'epocharr 1 2' epocharrname21 = 'epocharr 2 1' epocharrname22 = 'epocharr 2 2' epocharrtime11 = np.arange(0, 10) * pq.ms epocharrtime12 = np.arange(10, 20) * pq.ms epocharrtime21 = np.arange(20, 30) * pq.s epocharrtime22 = np.arange(30, 40) * pq.s epocharrdur11 = np.arange(1, 11) * pq.s epocharrdur12 = np.arange(11, 21) * pq.s epocharrdur21 = np.arange(21, 31) * pq.ms epocharrdur22 = np.arange(31, 41) * pq.ms self.epocharrnames1 = [epocharrname11, epocharrname12] self.epocharrnames2 = [epocharrname21, epocharrname22] self.epocharrnames = [epocharrname11, epocharrname12, epocharrname21, epocharrname22] epocharr11 = EpochArray(epocharrtime11, epocharrdur11, label=epocharrname11, name=epocharrname11) epocharr12 = EpochArray(epocharrtime12, epocharrdur12, label=epocharrname12, name=epocharrname12) epocharr21 = EpochArray(epocharrtime21, epocharrdur21, label=epocharrname21, name=epocharrname21) epocharr22 = EpochArray(epocharrtime22, epocharrdur22, label=epocharrname22, name=epocharrname22) self.epocharr1 = [epocharr11, epocharr12] self.epocharr2 = [epocharr21, epocharr22] self.epocharr = [epocharr11, epocharr12, epocharr21, epocharr22] def setup_events(self): eventname11 = 'event 1 1' eventname12 = 'event 1 2' eventname21 = 'event 2 1' eventname22 = 'event 2 2' eventtime11 = 10 * pq.ms eventtime12 = 20 * pq.ms eventtime21 = 30 * pq.s eventtime22 = 40 * pq.s self.eventnames1 = [eventname11, eventname12] self.eventnames2 = [eventname21, eventname22] self.eventnames = [eventname11, eventname12, eventname21, eventname22] params1 = {'testattr': True} params2 = {'testattr': 5} event11 = Event(eventtime11, label=eventname11, name=eventname11, **params1) event12 = Event(eventtime12, label=eventname12, name=eventname12, **params2) event21 = Event(eventtime21, label=eventname21, name=eventname21) event22 = Event(eventtime22, label=eventname22, name=eventname22) self.event1 = [event11, event12] self.event2 = [event21, event22] self.event = [event11, event12, event21, event22] def setup_eventarrays(self): eventarrname11 = 'eventarr 1 1' eventarrname12 = 'eventarr 1 2' eventarrname21 = 'eventarr 2 1' eventarrname22 = 'eventarr 2 2' eventarrtime11 = np.arange(0, 10) * pq.ms eventarrtime12 = np.arange(10, 20) * pq.ms eventarrtime21 = np.arange(20, 30) * pq.s eventarrtime22 = np.arange(30, 40) * pq.s self.eventarrnames1 = [eventarrname11, eventarrname12] self.eventarrnames2 = [eventarrname21, eventarrname22] self.eventarrnames = [eventarrname11, eventarrname12, eventarrname21, eventarrname22] eventarr11 = EventArray(eventarrtime11, label=eventarrname11, name=eventarrname11) eventarr12 = EventArray(eventarrtime12, label=eventarrname12, name=eventarrname12) eventarr21 = EventArray(eventarrtime21, label=eventarrname21, name=eventarrname21) eventarr22 = EventArray(eventarrtime22, label=eventarrname22, name=eventarrname22) self.eventarr1 = [eventarr11, eventarr12] self.eventarr2 = [eventarr21, eventarr22] self.eventarr = [eventarr11, eventarr12, eventarr21, eventarr22] def setup_irregularlysampledsignals(self): irsigname11 = 'irregularsignal 1 1' irsigname12 = 'irregularsignal 1 2' irsigname21 = 'irregularsignal 2 1' irsigname22 = 'irregularsignal 2 2' irsigdata11 = np.arange(0, 10) * pq.mA irsigdata12 = np.arange(10, 20) * pq.mA irsigdata21 = np.arange(20, 30) * pq.A irsigdata22 = np.arange(30, 40) * pq.A irsigtimes11 = np.arange(0, 10) * pq.ms irsigtimes12 = np.arange(10, 20) * pq.ms irsigtimes21 = np.arange(20, 30) * pq.s irsigtimes22 = np.arange(30, 40) * pq.s self.irsignames1 = [irsigname11, irsigname12] self.irsignames2 = [irsigname21, irsigname22] self.irsignames = [irsigname11, irsigname12, irsigname21, irsigname22] irsig11 = IrregularlySampledSignal(irsigtimes11, irsigdata11, name=irsigname11) irsig12 = IrregularlySampledSignal(irsigtimes12, irsigdata12, name=irsigname12) irsig21 = IrregularlySampledSignal(irsigtimes21, irsigdata21, name=irsigname21) irsig22 = IrregularlySampledSignal(irsigtimes22, irsigdata22, name=irsigname22) self.irsig1 = [irsig11, irsig12] self.irsig2 = [irsig21, irsig22] self.irsig = [irsig11, irsig12, irsig21, irsig22] def setup_spikes(self): spikename11 = 'spike 1 1' spikename12 = 'spike 1 2' spikename21 = 'spike 2 1' spikename22 = 'spike 2 2' spikedata11 = 10 * pq.ms spikedata12 = 20 * pq.ms spikedata21 = 30 * pq.s spikedata22 = 40 * pq.s self.spikenames1 = [spikename11, spikename12] self.spikenames2 = [spikename21, spikename22] self.spikenames = [spikename11, spikename12, spikename21, spikename22] spike11 = Spike(spikedata11, t_stop=100*pq.s, name=spikename11) spike12 = Spike(spikedata12, t_stop=100*pq.s, name=spikename12) spike21 = Spike(spikedata21, t_stop=100*pq.s, name=spikename21) spike22 = Spike(spikedata22, t_stop=100*pq.s, name=spikename22) self.spike1 = [spike11, spike12] self.spike2 = [spike21, spike22] self.spike = [spike11, spike12, spike21, spike22] def setup_spiketrains(self): trainname11 = 'spiketrain 1 1' trainname12 = 'spiketrain 1 2' trainname21 = 'spiketrain 2 1' trainname22 = 'spiketrain 2 2' traindata11 = np.arange(0, 10) * pq.ms traindata12 = np.arange(10, 20) * pq.ms traindata21 = np.arange(20, 30) * pq.s traindata22 = np.arange(30, 40) * pq.s self.trainnames1 = [trainname11, trainname12] self.trainnames2 = [trainname21, trainname22] self.trainnames = [trainname11, trainname12, trainname21, trainname22] train11 = SpikeTrain(traindata11, t_stop=100*pq.s, name=trainname11) train12 = SpikeTrain(traindata12, t_stop=100*pq.s, name=trainname12) train21 = SpikeTrain(traindata21, t_stop=100*pq.s, name=trainname21) train22 = SpikeTrain(traindata22, t_stop=100*pq.s, name=trainname22) self.train1 = [train11, train12] self.train2 = [train21, train22] self.train = [train11, train12, train21, train22] def test_init(self): seg = Segment(name='a segment', index=3) assert_neo_object_is_compliant(seg) self.assertEqual(seg.name, 'a segment') self.assertEqual(seg.file_origin, None) self.assertEqual(seg.index, 3) def test__construct_subsegment_by_unit(self): nb_seg = 3 nb_unit = 7 unit_with_sig = np.array([0, 2, 5]) signal_types = ['Vm', 'Conductances'] sig_len = 100 #recordingchannelgroups rcgs = [RecordingChannelGroup(name='Vm', channel_indexes=unit_with_sig), RecordingChannelGroup(name='Conductance', channel_indexes=unit_with_sig)] # Unit all_unit = [] for u in range(nb_unit): un = Unit(name='Unit #%d' % u, channel_indexes=np.array([u])) assert_neo_object_is_compliant(un) all_unit.append(un) blk = Block() blk.recordingchannelgroups = rcgs for s in range(nb_seg): seg = Segment(name='Simulation %s' % s) for j in range(nb_unit): st = SpikeTrain([1, 2, 3], units='ms', t_start=0., t_stop=10) st.unit = all_unit[j] for t in signal_types: anasigarr = AnalogSignalArray(np.zeros((sig_len, len(unit_with_sig))), units='nA', sampling_rate=1000.*pq.Hz, channel_indexes=unit_with_sig) seg.analogsignalarrays.append(anasigarr) create_many_to_one_relationship(blk) for unit in all_unit: assert_neo_object_is_compliant(unit) for rcg in rcgs: assert_neo_object_is_compliant(rcg) assert_neo_object_is_compliant(blk) # what you want newseg = seg.construct_subsegment_by_unit(all_unit[:4]) assert_neo_object_is_compliant(newseg) def test_segment_creation(self): assert_neo_object_is_compliant(self.segment1) assert_neo_object_is_compliant(self.segment2) assert_neo_object_is_compliant(self.unit1) assert_neo_object_is_compliant(self.unit2) self.assertEqual(self.segment1.name, 'test') self.assertEqual(self.segment2.name, 'test') self.assertEqual(self.segment1.description, 'tester 1') self.assertEqual(self.segment2.description, 'tester 2') self.assertEqual(self.segment1.file_origin, 'test.file') self.assertEqual(self.segment2.file_origin, 'test.file') self.assertEqual(self.segment1.annotations['testarg0'], [1, 2, 3]) self.assertEqual(self.segment2.annotations['testarg10'], [1, 2, 3]) self.assertEqual(self.segment1.annotations['testarg1'], 1.1) self.assertEqual(self.segment2.annotations['testarg1'], 1) self.assertEqual(self.segment2.annotations['testarg11'], 1.1) self.assertEqual(self.segment1.annotations['testarg2'], 'yes') self.assertEqual(self.segment2.annotations['testarg2'], 'yes') self.assertTrue(self.segment1.annotations['testarg3']) self.assertTrue(self.segment2.annotations['testarg3']) self.assertTrue(hasattr(self.segment1, 'analogsignals')) self.assertTrue(hasattr(self.segment2, 'analogsignals')) self.assertEqual(len(self.segment1.analogsignals), 2) self.assertEqual(len(self.segment2.analogsignals), 2) for res, targ in zip(self.segment1.analogsignals, self.sig1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.analogsignals, self.sig2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'analogsignalarrays')) self.assertTrue(hasattr(self.segment2, 'analogsignalarrays')) self.assertEqual(len(self.segment1.analogsignalarrays), 2) self.assertEqual(len(self.segment2.analogsignalarrays), 3) for res, targ in zip(self.segment1.analogsignalarrays, self.sigarr1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.analogsignalarrays, self.sigarr2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'epochs')) self.assertTrue(hasattr(self.segment2, 'epochs')) self.assertEqual(len(self.segment1.epochs), 2) self.assertEqual(len(self.segment2.epochs), 2) for res, targ in zip(self.segment1.epochs, self.epoch1): self.assertEqual(res.time, targ.time) self.assertEqual(res.duration, targ.duration) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.epochs, self.epoch2): self.assertEqual(res.time, targ.time) self.assertEqual(res.duration, targ.duration) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'epocharrays')) self.assertTrue(hasattr(self.segment2, 'epocharrays')) self.assertEqual(len(self.segment1.epocharrays), 2) self.assertEqual(len(self.segment2.epocharrays), 2) for res, targ in zip(self.segment1.epocharrays, self.epocharr1): assert_arrays_equal(res.times, targ.times) assert_arrays_equal(res.durations, targ.durations) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.epocharrays, self.epocharr2): assert_arrays_equal(res.times, targ.times) assert_arrays_equal(res.durations, targ.durations) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'events')) self.assertTrue(hasattr(self.segment2, 'events')) self.assertEqual(len(self.segment1.events), 2) self.assertEqual(len(self.segment2.events), 2) for res, targ in zip(self.segment1.events, self.event1): self.assertEqual(res.time, targ.time) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.events, self.event2): self.assertEqual(res.time, targ.time) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'eventarrays')) self.assertTrue(hasattr(self.segment2, 'eventarrays')) self.assertEqual(len(self.segment1.eventarrays), 2) self.assertEqual(len(self.segment2.eventarrays), 2) for res, targ in zip(self.segment1.eventarrays, self.eventarr1): assert_arrays_equal(res.times, targ.times) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.eventarrays, self.eventarr2): assert_arrays_equal(res.times, targ.times) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'irregularlysampledsignals')) self.assertTrue(hasattr(self.segment2, 'irregularlysampledsignals')) self.assertEqual(len(self.segment1.irregularlysampledsignals), 2) self.assertEqual(len(self.segment2.irregularlysampledsignals), 2) for res, targ in zip(self.segment1.irregularlysampledsignals, self.irsig1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.irregularlysampledsignals, self.irsig2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'spikes')) self.assertTrue(hasattr(self.segment2, 'spikes')) self.assertEqual(len(self.segment1.spikes), 2) self.assertEqual(len(self.segment2.spikes), 2) for res, targ in zip(self.segment1.spikes, self.spike1): self.assertEqual(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.spikes, self.spike2): self.assertEqual(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'spiketrains')) self.assertTrue(hasattr(self.segment2, 'spiketrains')) self.assertEqual(len(self.segment1.spiketrains), 2) self.assertEqual(len(self.segment2.spiketrains), 2) for res, targ in zip(self.segment1.spiketrains, self.train1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.spiketrains, self.train2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) def test_segment_merge(self): self.segment1.merge(self.segment2) create_many_to_one_relationship(self.segment1, force=True) assert_neo_object_is_compliant(self.segment1) self.assertEqual(self.segment1.name, 'test') self.assertEqual(self.segment2.name, 'test') self.assertEqual(self.segment1.description, 'tester 1') self.assertEqual(self.segment2.description, 'tester 2') self.assertEqual(self.segment1.file_origin, 'test.file') self.assertEqual(self.segment2.file_origin, 'test.file') self.assertEqual(self.segment1.annotations['testarg0'], [1, 2, 3]) self.assertEqual(self.segment2.annotations['testarg10'], [1, 2, 3]) self.assertEqual(self.segment1.annotations['testarg1'], 1.1) self.assertEqual(self.segment2.annotations['testarg1'], 1) self.assertEqual(self.segment2.annotations['testarg11'], 1.1) self.assertEqual(self.segment1.annotations['testarg2'], 'yes') self.assertEqual(self.segment2.annotations['testarg2'], 'yes') self.assertTrue(self.segment1.annotations['testarg3']) self.assertTrue(self.segment2.annotations['testarg3']) self.assertTrue(hasattr(self.segment1, 'analogsignals')) self.assertTrue(hasattr(self.segment2, 'analogsignals')) self.assertEqual(len(self.segment1.analogsignals), 4) self.assertEqual(len(self.segment2.analogsignals), 2) for res, targ in zip(self.segment1.analogsignals, self.sig): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.analogsignals, self.sig2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'analogsignalarrays')) self.assertTrue(hasattr(self.segment2, 'analogsignalarrays')) self.assertEqual(len(self.segment1.analogsignalarrays), 4) self.assertEqual(len(self.segment2.analogsignalarrays), 3) for res, targ in zip(self.segment1.analogsignalarrays, self.sigarr): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.analogsignalarrays, self.sigarr2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'epochs')) self.assertTrue(hasattr(self.segment2, 'epochs')) self.assertEqual(len(self.segment1.epochs), 4) self.assertEqual(len(self.segment2.epochs), 2) for res, targ in zip(self.segment1.epochs, self.epoch): self.assertEqual(res.time, targ.time) self.assertEqual(res.duration, targ.duration) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.epochs, self.epoch2): self.assertEqual(res.time, targ.time) self.assertEqual(res.duration, targ.duration) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'epocharrays')) self.assertTrue(hasattr(self.segment2, 'epocharrays')) self.assertEqual(len(self.segment1.epocharrays), 4) self.assertEqual(len(self.segment2.epocharrays), 2) for res, targ in zip(self.segment1.epocharrays, self.epocharr): assert_arrays_equal(res.times, targ.times) assert_arrays_equal(res.durations, targ.durations) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.epocharrays, self.epocharr2): assert_arrays_equal(res.times, targ.times) assert_arrays_equal(res.durations, targ.durations) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'events')) self.assertTrue(hasattr(self.segment2, 'events')) self.assertEqual(len(self.segment1.events), 4) self.assertEqual(len(self.segment2.events), 2) for res, targ in zip(self.segment1.events, self.event): self.assertEqual(res.time, targ.time) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.events, self.event2): self.assertEqual(res.time, targ.time) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'eventarrays')) self.assertTrue(hasattr(self.segment2, 'eventarrays')) self.assertEqual(len(self.segment1.eventarrays), 4) self.assertEqual(len(self.segment2.eventarrays), 2) for res, targ in zip(self.segment1.eventarrays, self.eventarr): assert_arrays_equal(res.times, targ.times) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.eventarrays, self.eventarr2): assert_arrays_equal(res.times, targ.times) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'irregularlysampledsignals')) self.assertTrue(hasattr(self.segment2, 'irregularlysampledsignals')) self.assertEqual(len(self.segment1.irregularlysampledsignals), 4) self.assertEqual(len(self.segment2.irregularlysampledsignals), 2) for res, targ in zip(self.segment1.irregularlysampledsignals, self.irsig): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.irregularlysampledsignals, self.irsig2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'spikes')) self.assertTrue(hasattr(self.segment2, 'spikes')) self.assertEqual(len(self.segment1.spikes), 4) self.assertEqual(len(self.segment2.spikes), 2) for res, targ in zip(self.segment1.spikes, self.spike): self.assertEqual(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.spikes, self.spike2): self.assertEqual(res, targ) self.assertEqual(res.name, targ.name) self.assertTrue(hasattr(self.segment1, 'spiketrains')) self.assertTrue(hasattr(self.segment2, 'spiketrains')) self.assertEqual(len(self.segment1.spiketrains), 4) self.assertEqual(len(self.segment2.spiketrains), 2) for res, targ in zip(self.segment1.spiketrains, self.train): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(self.segment2.spiketrains, self.train2): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) def test_segment_all_data(self): result1 = self.segment1.all_data targs = (self.epoch1 + self.epocharr1 + self.event1 + self.eventarr1 + self.sig1 + self.sigarr1 + self.irsig1 + self.spike1 + self.train1) for res, targ in zip(result1, targs): if hasattr(res, 'ndim') and res.ndim: assert_arrays_equal(res, targ) else: self.assertEqual(res, targ) self.assertEqual(res.name, targ.name) def test_segment_take_spikes_by_unit(self): result1 = self.segment1.take_spikes_by_unit() result21 = self.segment1.take_spikes_by_unit([self.unit1]) result22 = self.segment1.take_spikes_by_unit([self.unit2]) self.assertEqual(result1, []) for res, targ in zip(result21, self.unit1spike): self.assertEqual(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(result22, self.unit2spike): self.assertEqual(res, targ) self.assertEqual(res.name, targ.name) def test_segment_take_spiketrains_by_unit(self): result1 = self.segment1.take_spiketrains_by_unit() result21 = self.segment1.take_spiketrains_by_unit([self.unit1]) result22 = self.segment1.take_spiketrains_by_unit([self.unit2]) self.assertEqual(result1, []) for res, targ in zip(result21, self.unit1train): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(result22, self.unit2train): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) def test_segment_take_analogsignal_by_unit(self): result1 = self.segment1.take_analogsignal_by_unit() result21 = self.segment1.take_analogsignal_by_unit([self.unit1]) result22 = self.segment1.take_analogsignal_by_unit([self.unit2]) self.assertEqual(result1, []) for res, targ in zip(result21, self.chan1sig): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(result22, self.chan2sig): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) def test_segment_take_analogsignal_by_channelindex(self): result1 = self.segment1.take_analogsignal_by_channelindex() result21 = self.segment1.take_analogsignal_by_channelindex([1]) result22 = self.segment1.take_analogsignal_by_channelindex([2]) self.assertEqual(result1, []) for res, targ in zip(result21, self.chan1sig): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(result22, self.chan2sig): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) def test_segment_take_slice_of_analogsignalarray_by_unit(self): segment = self.segment1 unit1 = self.unit1 unit2 = self.unit2 result1 = segment.take_slice_of_analogsignalarray_by_unit() result21 = segment.take_slice_of_analogsignalarray_by_unit([unit1]) result22 = segment.take_slice_of_analogsignalarray_by_unit([unit2]) self.assertEqual(result1, []) for res, targ in zip(result21, self.chan1sigarr1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(result22, self.chan2sigarr1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) def test_segment_take_slice_of_analogsignalarray_by_channelindex(self): segment = self.segment1 result1 = segment.take_slice_of_analogsignalarray_by_channelindex() result21 = segment.take_slice_of_analogsignalarray_by_channelindex([1]) result22 = segment.take_slice_of_analogsignalarray_by_channelindex([2]) self.assertEqual(result1, []) for res, targ in zip(result21, self.chan1sigarr1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) for res, targ in zip(result22, self.chan2sigarr1): assert_arrays_equal(res, targ) self.assertEqual(res.name, targ.name) def test_segment_size(self): result1 = self.segment1.size() targ1 = {"epochs": 2, "events": 2, "analogsignals": 2, "irregularlysampledsignals": 2, "spikes": 2, "spiketrains": 2, "epocharrays": 2, "eventarrays": 2, "analogsignalarrays": 2} self.assertEqual(result1, targ1) def test_segment_filter(self): result1 = self.segment1.filter() result2 = self.segment1.filter(name='analogsignal 1 1') result3 = self.segment1.filter(testattr=True) self.assertEqual(result1, []) self.assertEqual(len(result2), 1) assert_arrays_equal(result2[0], self.sig1[0]) self.assertEqual(len(result3), 2) self.assertEqual(result3[0], self.epoch1[0]) self.assertEqual(result3[1], self.event1[0]) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_event.py0000644000175000017500000000321012273723542017637 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.event.Event class """ try: import unittest2 as unittest except ImportError: import unittest import quantities as pq from neo.core.event import Event from neo.test.tools import assert_neo_object_is_compliant class TestEvent(unittest.TestCase): def test_Event_creation(self): params = {'testarg2': 'yes', 'testarg3': True} evt = Event(1.5*pq.ms, label='test epoch', name='test', description='tester', file_origin='test.file', testarg1=1, **params) evt.annotate(testarg1=1.1, testarg0=[1, 2, 3]) assert_neo_object_is_compliant(evt) self.assertEqual(evt.time, 1.5*pq.ms) self.assertEqual(evt.label, 'test epoch') self.assertEqual(evt.name, 'test') self.assertEqual(evt.description, 'tester') self.assertEqual(evt.file_origin, 'test.file') self.assertEqual(evt.annotations['testarg0'], [1, 2, 3]) self.assertEqual(evt.annotations['testarg1'], 1.1) self.assertEqual(evt.annotations['testarg2'], 'yes') self.assertTrue(evt.annotations['testarg3']) def test_epoch_merge_NotImplementedError(self): evt1 = Event(1.5*pq.ms, label='test epoch', name='test', description='tester', file_origin='test.file', testarg1=1) evt2 = Event(1.5*pq.ms, label='test epoch', name='test', description='tester', file_origin='test.file', testarg1=1) self.assertRaises(NotImplementedError, evt1.merge, evt2) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_analogsignal.py0000644000175000017500000006024212273723542021165 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.analogsignal.AnalogSignal class and related functions """ # needed for python 3 compatibility from __future__ import division import os import pickle from pprint import pformat try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.analogsignal import AnalogSignal, _get_sampling_rate from neo.test.tools import (assert_arrays_almost_equal, assert_arrays_equal, assert_neo_object_is_compliant, assert_same_sub_schema) class TestAnalogSignalConstructor(unittest.TestCase): def test__create_from_list(self): data = range(10) rate = 1000*pq.Hz signal = AnalogSignal(data, sampling_rate=rate, units="mV") assert_neo_object_is_compliant(signal) self.assertEqual(signal.t_start, 0*pq.ms) self.assertEqual(signal.t_stop, len(data)/rate) self.assertEqual(signal[9], 9000*pq.uV) def test__create_from_np_array(self): data = np.arange(10.0) rate = 1*pq.kHz signal = AnalogSignal(data, sampling_rate=rate, units="uV") assert_neo_object_is_compliant(signal) self.assertEqual(signal.t_start, 0*pq.ms) self.assertEqual(signal.t_stop, data.size/rate) self.assertEqual(signal[9], 0.009*pq.mV) def test__create_from_quantities_array(self): data = np.arange(10.0) * pq.mV rate = 5000*pq.Hz signal = AnalogSignal(data, sampling_rate=rate) assert_neo_object_is_compliant(signal) self.assertEqual(signal.t_start, 0*pq.ms) self.assertEqual(signal.t_stop, data.size/rate) self.assertEqual(signal[9], 0.009*pq.V) def test__create_from_array_no_units_ValueError(self): data = np.arange(10.0) self.assertRaises(ValueError, AnalogSignal, data, sampling_rate=1 * pq.kHz) def test__create_from_quantities_array_inconsistent_units_ValueError(self): data = np.arange(10.0) * pq.mV self.assertRaises(ValueError, AnalogSignal, data, sampling_rate=1 * pq.kHz, units="nA") def test__create_without_sampling_rate_or_period_ValueError(self): data = np.arange(10.0) * pq.mV self.assertRaises(ValueError, AnalogSignal, data) def test__create_with_None_sampling_rate_should_raise_ValueError(self): data = np.arange(10.0) * pq.mV self.assertRaises(ValueError, AnalogSignal, data, sampling_rate=None) def test__create_with_None_t_start_should_raise_ValueError(self): data = np.arange(10.0) * pq.mV rate = 5000 * pq.Hz self.assertRaises(ValueError, AnalogSignal, data, sampling_rate=rate, t_start=None) def test__create_inconsistent_sampling_rate_and_period_ValueError(self): data = np.arange(10.0) * pq.mV self.assertRaises(ValueError, AnalogSignal, data, sampling_rate=1 * pq.kHz, sampling_period=5 * pq.s) def test__create_with_copy_true_should_return_copy(self): data = np.arange(10.0) * pq.mV rate = 5000*pq.Hz signal = AnalogSignal(data, copy=True, sampling_rate=rate) data[3] = 99*pq.mV assert_neo_object_is_compliant(signal) self.assertNotEqual(signal[3], 99*pq.mV) def test__create_with_copy_false_should_return_view(self): data = np.arange(10.0) * pq.mV rate = 5000*pq.Hz signal = AnalogSignal(data, copy=False, sampling_rate=rate) data[3] = 99*pq.mV assert_neo_object_is_compliant(signal) self.assertEqual(signal[3], 99*pq.mV) def test__create_with_additional_argument(self): signal = AnalogSignal([1, 2, 3], units="mV", sampling_rate=1*pq.kHz, file_origin='crack.txt', ratname='Nicolas') assert_neo_object_is_compliant(signal) self.assertEqual(signal.annotations, {'ratname': 'Nicolas'}) # This one is universally recommended and handled by BaseNeo self.assertEqual(signal.file_origin, 'crack.txt') # signal must be 1D - should raise Exception if not 1D class TestAnalogSignalProperties(unittest.TestCase): def setUp(self): self.t_start = [0.0*pq.ms, 100*pq.ms, -200*pq.ms] self.rates = [1*pq.kHz, 420*pq.Hz, 999*pq.Hz] self.rates2 = [2*pq.kHz, 290*pq.Hz, 1111*pq.Hz] self.data = [np.arange(10.0)*pq.nA, np.arange(-100.0, 100.0, 10.0)*pq.mV, np.random.uniform(size=100)*pq.uV] self.signals = [AnalogSignal(D, sampling_rate=r, t_start=t, testattr='test') for r, D, t in zip(self.rates, self.data, self.t_start)] def test__compliant(self): for signal in self.signals: assert_neo_object_is_compliant(signal) def test__t_stop_getter(self): for i, signal in enumerate(self.signals): self.assertEqual(signal.t_stop, self.t_start[i] + self.data[i].size/self.rates[i]) def test__duration_getter(self): for signal in self.signals: self.assertAlmostEqual(signal.duration, signal.t_stop - signal.t_start, delta=1e-15) def test__sampling_rate_getter(self): for signal, rate in zip(self.signals, self.rates): self.assertEqual(signal.sampling_rate, rate) def test__sampling_period_getter(self): for signal, rate in zip(self.signals, self.rates): self.assertEqual(signal.sampling_period, 1 / rate) def test__sampling_rate_setter(self): for signal, rate in zip(self.signals, self.rates2): signal.sampling_rate = rate assert_neo_object_is_compliant(signal) self.assertEqual(signal.sampling_rate, rate) self.assertEqual(signal.sampling_period, 1 / rate) def test__sampling_period_setter(self): for signal, rate in zip(self.signals, self.rates2): signal.sampling_period = 1 / rate assert_neo_object_is_compliant(signal) self.assertEqual(signal.sampling_rate, rate) self.assertEqual(signal.sampling_period, 1 / rate) def test__sampling_rate_setter_None_ValueError(self): self.assertRaises(ValueError, setattr, self.signals[0], 'sampling_rate', None) def test__sampling_rate_setter_not_quantity_ValueError(self): self.assertRaises(ValueError, setattr, self.signals[0], 'sampling_rate', 5.5) def test__sampling_period_setter_None_ValueError(self): signal = self.signals[0] assert_neo_object_is_compliant(signal) self.assertRaises(ValueError, setattr, signal, 'sampling_period', None) def test__sampling_period_setter_not_quantity_ValueError(self): self.assertRaises(ValueError, setattr, self.signals[0], 'sampling_period', 5.5) def test__t_start_setter_None_ValueError(self): signal = self.signals[0] assert_neo_object_is_compliant(signal) self.assertRaises(ValueError, setattr, signal, 't_start', None) def test__times_getter(self): for i, signal in enumerate(self.signals): targ = np.arange(self.data[i].size) targ = targ/self.rates[i] + self.t_start[i] assert_neo_object_is_compliant(signal) assert_arrays_almost_equal(signal.times, targ, 1e-12*pq.ms) def test__pprint(self): for i, signal in enumerate(self.signals): prepr = pformat(signal) targ = '' % \ (pformat(self.data[i]), self.t_start[i], self.t_start[i] + len(self.data[i])/self.rates[i], self.rates[i]) self.assertEqual(prepr, targ) def test__duplicate_with_new_array(self): signal1 = self.signals[1] signal2 = self.signals[2] data2 = self.data[2] signal1b = signal1.duplicate_with_new_array(data2) assert_arrays_almost_equal(np.asarray(signal1b), np.asarray(signal2/1000.), 1e-12) self.assertEqual(signal1b.t_start, signal1.t_start) self.assertEqual(signal1b.sampling_rate, signal1.sampling_rate) class TestAnalogSignalArrayMethods(unittest.TestCase): def setUp(self): self.data1 = np.arange(10.0) self.data1quant = self.data1 * pq.nA self.signal1 = AnalogSignal(self.data1quant, sampling_rate=1*pq.kHz, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') def test__compliant(self): assert_neo_object_is_compliant(self.signal1) def test__slice_should_return_AnalogSignal(self): # slice result = self.signal1[3:8] self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(result.size, 5) self.assertEqual(result.sampling_period, self.signal1.sampling_period) self.assertEqual(result.sampling_rate, self.signal1.sampling_rate) self.assertEqual(result.t_start, self.signal1.t_start+3*result.sampling_period) self.assertEqual(result.t_stop, result.t_start + 5*result.sampling_period) assert_arrays_equal(result, self.data1[3:8]) # Test other attributes were copied over (in this case, defaults) self.assertEqual(result.file_origin, self.signal1.file_origin) self.assertEqual(result.name, self.signal1.name) self.assertEqual(result.description, self.signal1.description) self.assertEqual(result.annotations, self.signal1.annotations) def test__slice_should_change_sampling_period(self): result1 = self.signal1[:2] result2 = self.signal1[::2] result3 = self.signal1[1:7:2] self.assertIsInstance(result1, AnalogSignal) assert_neo_object_is_compliant(result1) self.assertEqual(result1.name, 'spam') self.assertEqual(result1.description, 'eggs') self.assertEqual(result1.file_origin, 'testfile.txt') self.assertEqual(result1.annotations, {'arg1': 'test'}) self.assertIsInstance(result2, AnalogSignal) assert_neo_object_is_compliant(result2) self.assertEqual(result2.name, 'spam') self.assertEqual(result2.description, 'eggs') self.assertEqual(result2.file_origin, 'testfile.txt') self.assertEqual(result2.annotations, {'arg1': 'test'}) self.assertIsInstance(result3, AnalogSignal) assert_neo_object_is_compliant(result3) self.assertEqual(result3.name, 'spam') self.assertEqual(result3.description, 'eggs') self.assertEqual(result3.file_origin, 'testfile.txt') self.assertEqual(result3.annotations, {'arg1': 'test'}) self.assertEqual(result1.sampling_period, self.signal1.sampling_period) self.assertEqual(result2.sampling_period, self.signal1.sampling_period * 2) self.assertEqual(result3.sampling_period, self.signal1.sampling_period * 2) assert_arrays_equal(result1, self.data1[:2]) assert_arrays_equal(result2, self.data1[::2]) assert_arrays_equal(result3, self.data1[1:7:2]) def test__getitem_should_return_single_quantity(self): result1 = self.signal1[0] result2 = self.signal1[9] self.assertIsInstance(result1, pq.Quantity) self.assertFalse(hasattr(result1, 'name')) self.assertFalse(hasattr(result1, 'description')) self.assertFalse(hasattr(result1, 'file_origin')) self.assertFalse(hasattr(result1, 'annotations')) self.assertIsInstance(result2, pq.Quantity) self.assertFalse(hasattr(result2, 'name')) self.assertFalse(hasattr(result2, 'description')) self.assertFalse(hasattr(result2, 'file_origin')) self.assertFalse(hasattr(result2, 'annotations')) self.assertEqual(result1, 0*pq.nA) self.assertEqual(result2, 9*pq.nA) def test__getitem_out_of_bounds_IndexError(self): self.assertRaises(IndexError, self.signal1.__getitem__, 10) def test_comparison_operators(self): assert_arrays_equal(self.signal1 >= 5*pq.nA, np.array([False, False, False, False, False, True, True, True, True, True])) assert_arrays_equal(self.signal1 >= 5*pq.pA, np.array([False, True, True, True, True, True, True, True, True, True])) def test__comparison_with_inconsistent_units_should_raise_Exception(self): self.assertRaises(ValueError, self.signal1.__gt__, 5*pq.mV) def test__simple_statistics(self): self.assertEqual(self.signal1.max(), 9*pq.nA) self.assertEqual(self.signal1.min(), 0*pq.nA) self.assertEqual(self.signal1.mean(), 4.5*pq.nA) def test__rescale_same(self): result = self.signal1.copy() result = result.rescale(pq.nA) self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(result.units, 1*pq.nA) assert_arrays_equal(result, self.data1) assert_same_sub_schema(result, self.signal1) def test__rescale_new(self): result = self.signal1.copy() result = result.rescale(pq.pA) self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(result.units, 1*pq.pA) assert_arrays_almost_equal(np.array(result), self.data1*1000., 1e-10) def test__rescale_new_incompatible_ValueError(self): self.assertRaises(ValueError, self.signal1.rescale, pq.mV) class TestAnalogSignalEquality(unittest.TestCase): def test__signals_with_different_data_complement_should_be_not_equal(self): signal1 = AnalogSignal(np.arange(10.0), units="mV", sampling_rate=1*pq.kHz) signal2 = AnalogSignal(np.arange(10.0), units="mV", sampling_rate=2*pq.kHz) assert_neo_object_is_compliant(signal1) assert_neo_object_is_compliant(signal2) self.assertNotEqual(signal1, signal2) class TestAnalogSignalCombination(unittest.TestCase): def setUp(self): self.data1 = np.arange(10.0) self.data1quant = self.data1 * pq.mV self.signal1 = AnalogSignal(self.data1quant, sampling_rate=1*pq.kHz, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') def test__compliant(self): assert_neo_object_is_compliant(self.signal1) self.assertEqual(self.signal1.name, 'spam') self.assertEqual(self.signal1.description, 'eggs') self.assertEqual(self.signal1.file_origin, 'testfile.txt') self.assertEqual(self.signal1.annotations, {'arg1': 'test'}) def test__add_const_quantity_should_preserve_data_complement(self): result = self.signal1 + 0.065*pq.V self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) assert_arrays_equal(result, self.data1 + 65) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], 74*pq.mV) self.assertEqual(self.signal1.t_start, result.t_start) self.assertEqual(self.signal1.sampling_rate, result.sampling_rate) def test__add_quantity_should_preserve_data_complement(self): data2 = np.arange(10.0, 20.0) data2quant = data2*pq.mV result = self.signal1 + data2quant self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) targ = AnalogSignal(np.arange(10.0, 30.0, 2.0), units="mV", sampling_rate=1*pq.kHz, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') assert_neo_object_is_compliant(targ) assert_arrays_equal(result, targ) assert_same_sub_schema(result, targ) def test__add_two_consistent_signals_should_preserve_data_complement(self): data2 = np.arange(10.0, 20.0) data2quant = data2*pq.mV signal2 = AnalogSignal(data2quant, sampling_rate=1*pq.kHz) assert_neo_object_is_compliant(signal2) result = self.signal1 + signal2 self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) targ = AnalogSignal(np.arange(10.0, 30.0, 2.0), units="mV", sampling_rate=1*pq.kHz, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') assert_neo_object_is_compliant(targ) assert_arrays_equal(result, targ) assert_same_sub_schema(result, targ) def test__add_signals_with_inconsistent_data_complement_ValueError(self): self.signal1.t_start = 0.0*pq.ms assert_neo_object_is_compliant(self.signal1) signal2 = AnalogSignal(np.arange(10.0), units="mV", t_start=100.0*pq.ms, sampling_rate=0.5*pq.kHz) assert_neo_object_is_compliant(signal2) self.assertRaises(ValueError, self.signal1.__add__, signal2) def test__subtract_const_should_preserve_data_complement(self): result = self.signal1 - 65*pq.mV self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], -56*pq.mV) assert_arrays_equal(result, self.data1 - 65) self.assertEqual(self.signal1.sampling_rate, result.sampling_rate) def test__subtract_from_const_should_return_signal(self): result = 10*pq.mV - self.signal1 self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], 1*pq.mV) assert_arrays_equal(result, 10 - self.data1) self.assertEqual(self.signal1.sampling_rate, result.sampling_rate) def test__mult_by_const_float_should_preserve_data_complement(self): result = self.signal1*2 self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], 18*pq.mV) assert_arrays_equal(result, self.data1*2) self.assertEqual(self.signal1.sampling_rate, result.sampling_rate) def test__divide_by_const_should_preserve_data_complement(self): result = self.signal1/0.5 self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], 18*pq.mV) assert_arrays_equal(result, self.data1/0.5) self.assertEqual(self.signal1.sampling_rate, result.sampling_rate) def test__merge_NotImplementedError(self): self.assertRaises(NotImplementedError, self.signal1.merge, self.signal1) class TestAnalogSignalFunctions(unittest.TestCase): def test__pickle(self): signal1 = AnalogSignal([1, 2, 3, 4], sampling_period=1*pq.ms, units=pq.S, channel_index=42) signal1.annotations['index'] = 2 fobj = open('./pickle', 'wb') pickle.dump(signal1, fobj) fobj.close() fobj = open('./pickle', 'rb') try: signal2 = pickle.load(fobj) except ValueError: signal2 = None assert_arrays_equal(signal1, signal2) self.assertEqual(signal1.channel_index, signal2.channel_index, 42) fobj.close() os.remove('./pickle') class TestAnalogSignalSampling(unittest.TestCase): def test___get_sampling_rate__period_none_rate_none_ValueError(self): sampling_rate = None sampling_period = None self.assertRaises(ValueError, _get_sampling_rate, sampling_rate, sampling_period) def test___get_sampling_rate__period_quant_rate_none(self): sampling_rate = None sampling_period = pq.Quantity(10., units=pq.s) targ_rate = 1/sampling_period out_rate = _get_sampling_rate(sampling_rate, sampling_period) self.assertEqual(targ_rate, out_rate) def test___get_sampling_rate__period_none_rate_quant(self): sampling_rate = pq.Quantity(10., units=pq.Hz) sampling_period = None targ_rate = sampling_rate out_rate = _get_sampling_rate(sampling_rate, sampling_period) self.assertEqual(targ_rate, out_rate) def test___get_sampling_rate__period_rate_equivalent(self): sampling_rate = pq.Quantity(10., units=pq.Hz) sampling_period = pq.Quantity(0.1, units=pq.s) targ_rate = sampling_rate out_rate = _get_sampling_rate(sampling_rate, sampling_period) self.assertEqual(targ_rate, out_rate) def test___get_sampling_rate__period_rate_not_equivalent_ValueError(self): sampling_rate = pq.Quantity(10., units=pq.Hz) sampling_period = pq.Quantity(10, units=pq.s) self.assertRaises(ValueError, _get_sampling_rate, sampling_rate, sampling_period) def test___get_sampling_rate__period_none_rate_float_TypeError(self): sampling_rate = 10. sampling_period = None self.assertRaises(TypeError, _get_sampling_rate, sampling_rate, sampling_period) def test___get_sampling_rate__period_array_rate_none_TypeError(self): sampling_rate = None sampling_period = np.array(10.) self.assertRaises(TypeError, _get_sampling_rate, sampling_rate, sampling_period) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_spiketrain.py0000644000175000017500000020671112273723542020702 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.spiketrain.SpikeTrain class and related functions """ # needed for python 3 compatibility from __future__ import absolute_import import sys try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.spiketrain import (check_has_dimensions_time, SpikeTrain, _check_time_in_range, _new_spiketrain) from neo.test.tools import assert_arrays_equal, assert_neo_object_is_compliant class Testcheck_has_dimensions_time(unittest.TestCase): def test__check_has_dimensions_time(self): a = np.arange(3) * pq.ms b = np.arange(3) * pq.mV c = np.arange(3) * pq.mA d = np.arange(3) * pq.minute check_has_dimensions_time(a) self.assertRaises(ValueError, check_has_dimensions_time, b) self.assertRaises(ValueError, check_has_dimensions_time, c) check_has_dimensions_time(d) self.assertRaises(ValueError, check_has_dimensions_time, a, b, c, d) class Testcheck_time_in_range(unittest.TestCase): def test__check_time_in_range_empty_array(self): value = np.array([]) t_start = 0*pq.s t_stop = 10*pq.s _check_time_in_range(value, t_start=t_start, t_stop=t_stop) _check_time_in_range(value, t_start=t_start, t_stop=t_stop, view=False) _check_time_in_range(value, t_start=t_start, t_stop=t_stop, view=True) def test__check_time_in_range_exact(self): value = np.array([0., 5., 10.])*pq.s t_start = 0.*pq.s t_stop = 10.*pq.s _check_time_in_range(value, t_start=t_start, t_stop=t_stop) _check_time_in_range(value, t_start=t_start, t_stop=t_stop, view=False) _check_time_in_range(value, t_start=t_start, t_stop=t_stop, view=True) def test__check_time_in_range_scale(self): value = np.array([0., 5000., 10000.])*pq.ms t_start = 0.*pq.s t_stop = 10.*pq.s _check_time_in_range(value, t_start=t_start, t_stop=t_stop) _check_time_in_range(value, t_start=t_start, t_stop=t_stop, view=False) def test__check_time_in_range_inside(self): value = np.array([0.1, 5., 9.9])*pq.s t_start = 0.*pq.s t_stop = 10.*pq.s _check_time_in_range(value, t_start=t_start, t_stop=t_stop) _check_time_in_range(value, t_start=t_start, t_stop=t_stop, view=False) _check_time_in_range(value, t_start=t_start, t_stop=t_stop, view=True) def test__check_time_in_range_below(self): value = np.array([-0.1, 5., 10.])*pq.s t_start = 0.*pq.s t_stop = 10.*pq.s self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop, view=False) self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop, view=True) def test__check_time_in_range_below_scale(self): value = np.array([-1., 5000., 10000.])*pq.ms t_start = 0.*pq.s t_stop = 10.*pq.s self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop, view=False) def test__check_time_in_range_above(self): value = np.array([0., 5., 10.1])*pq.s t_start = 0.*pq.s t_stop = 10.*pq.s self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop, view=False) self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop, view=True) def test__check_time_in_range_above_scale(self): value = np.array([0., 5000., 10001.])*pq.ms t_start = 0.*pq.s t_stop = 10.*pq.s self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop, view=False) def test__check_time_in_range_above_below(self): value = np.array([-0.1, 5., 10.1])*pq.s t_start = 0.*pq.s t_stop = 10.*pq.s self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop, view=False) self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop, view=True) def test__check_time_in_range_above_below_scale(self): value = np.array([-1., 5000., 10001.])*pq.ms t_start = 0.*pq.s t_stop = 10.*pq.s self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _check_time_in_range, value, t_start=t_start, t_stop=t_stop, view=False) class TestConstructor(unittest.TestCase): def result_spike_check(self, train, st_out, t_start_out, t_stop_out, dtype, units): assert_arrays_equal(train, st_out) assert_arrays_equal(train, train.times) assert_neo_object_is_compliant(train) self.assertEqual(train.t_start, t_start_out) self.assertEqual(train.t_start, train.times.t_start) self.assertEqual(train.t_stop, t_stop_out) self.assertEqual(train.t_stop, train.times.t_stop) self.assertEqual(train.units, units) self.assertEqual(train.units, train.times.units) self.assertEqual(train.t_start.units, units) self.assertEqual(train.t_start.units, train.times.t_start.units) self.assertEqual(train.t_stop.units, units) self.assertEqual(train.t_stop.units, train.times.t_stop.units) self.assertEqual(train.dtype, dtype) self.assertEqual(train.dtype, train.times.dtype) self.assertEqual(train.t_stop.dtype, dtype) self.assertEqual(train.t_stop.dtype, train.times.t_stop.dtype) self.assertEqual(train.t_start.dtype, dtype) self.assertEqual(train.t_start.dtype, train.times.t_start.dtype) def test__create_minimal(self): t_start = 0.0 t_stop = 10.0 train1 = SpikeTrain([]*pq.s, t_stop) train2 = _new_spiketrain(SpikeTrain, []*pq.s, t_stop) dtype = np.float64 units = 1 * pq.s t_start_out = t_start * units t_stop_out = t_stop * units st_out = [] * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_empty(self): t_start = 0.0 t_stop = 10.0 train1 = SpikeTrain([], t_start=t_start, t_stop=t_stop, units='s') train2 = _new_spiketrain(SpikeTrain, [], t_start=t_start, t_stop=t_stop, units='s') dtype = np.float64 units = 1 * pq.s t_start_out = t_start * units t_stop_out = t_stop * units st_out = [] * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_empty_no_t_start(self): t_start = 0.0 t_stop = 10.0 train1 = SpikeTrain([], t_stop=t_stop, units='s') train2 = _new_spiketrain(SpikeTrain, [], t_stop=t_stop, units='s') dtype = np.float64 units = 1 * pq.s t_start_out = t_start * units t_stop_out = t_stop * units st_out = [] * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_list(self): times = range(10) t_start = 0.0*pq.s t_stop = 10000.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units="ms") train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units="ms") dtype = np.float64 units = 1 * pq.ms t_start_out = t_start t_stop_out = t_stop st_out = times * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_list_set_dtype(self): times = range(10) t_start = 0.0*pq.s t_stop = 10000.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units="ms", dtype='f4') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units="ms", dtype='f4') dtype = np.float32 units = 1 * pq.ms t_start_out = t_start.astype(dtype) t_stop_out = t_stop.astype(dtype) st_out = pq.Quantity(times, units=units, dtype=dtype) self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_list_no_start_stop_units(self): times = range(10) t_start = 0.0 t_stop = 10000.0 train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units="ms") train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units="ms") dtype = np.float64 units = 1 * pq.ms t_start_out = t_start * units t_stop_out = t_stop * units st_out = times * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_list_no_start_stop_units_set_dtype(self): times = range(10) t_start = 0.0 t_stop = 10000.0 train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units="ms", dtype='f4') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units="ms", dtype='f4') dtype = np.float32 units = 1 * pq.ms t_start_out = pq.Quantity(t_start, units=units, dtype=dtype) t_stop_out = pq.Quantity(t_stop, units=units, dtype=dtype) st_out = pq.Quantity(times, units=units, dtype=dtype) self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_array(self): times = np.arange(10) t_start = 0.0*pq.s t_stop = 10000.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units="s") train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units="s") dtype = np.int units = 1 * pq.s t_start_out = t_start t_stop_out = t_stop st_out = times * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_array_with_dtype(self): times = np.arange(10, dtype='f4') t_start = 0.0*pq.s t_stop = 10000.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units="s") train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units="s") dtype = np.float32 units = 1 * pq.s t_start_out = t_start t_stop_out = t_stop st_out = times * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_array_set_dtype(self): times = np.arange(10) t_start = 0.0*pq.s t_stop = 10000.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units="s", dtype='f4') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units="s", dtype='f4') dtype = np.float32 units = 1 * pq.s t_start_out = t_start.astype(dtype) t_stop_out = t_stop.astype(dtype) st_out = times.astype(dtype) * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_array_no_start_stop_units(self): times = np.arange(10) t_start = 0.0 t_stop = 10000.0 train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units="s") train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units="s") dtype = np.int units = 1 * pq.s t_start_out = t_start * units t_stop_out = t_stop * units st_out = times * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_array_no_start_stop_units_with_dtype(self): times = np.arange(10, dtype='f4') t_start = 0.0 t_stop = 10000.0 train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units="s") train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units="s") dtype = np.float32 units = 1 * pq.s t_start_out = t_start * units t_stop_out = t_stop * units st_out = times * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_array_no_start_stop_units_set_dtype(self): times = np.arange(10) t_start = 0.0 t_stop = 10000.0 train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units="s", dtype='f4') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units="s", dtype='f4') dtype = np.float32 units = 1 * pq.s t_start_out = pq.Quantity(t_start, units=units, dtype=dtype) t_stop_out = pq.Quantity(t_stop, units=units, dtype=dtype) st_out = times.astype(dtype) * units self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_array(self): times = np.arange(10) * pq.ms t_start = 0.0*pq.s t_stop = 12.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop) train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop) dtype = np.float64 units = 1 * pq.ms t_start_out = t_start t_stop_out = t_stop st_out = times self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_array_with_dtype(self): times = np.arange(10, dtype='f4') * pq.ms t_start = 0.0*pq.s t_stop = 12.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop) train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop) dtype = np.float32 units = 1 * pq.ms t_start_out = t_start.astype(dtype) t_stop_out = t_stop.astype(dtype) st_out = times.astype(dtype) self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_array_set_dtype(self): times = np.arange(10) * pq.ms t_start = 0.0*pq.s t_stop = 12.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, dtype='f4') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, dtype='f4') dtype = np.float32 units = 1 * pq.ms t_start_out = t_start.astype(dtype) t_stop_out = t_stop.astype(dtype) st_out = times.astype(dtype) self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_array_no_start_stop_units(self): times = np.arange(10) * pq.ms t_start = 0.0 t_stop = 12.0 train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop) train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop) dtype = np.float64 units = 1 * pq.ms t_start_out = t_start * units t_stop_out = t_stop * units st_out = times self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_array_no_start_stop_units_with_dtype(self): times = np.arange(10, dtype='f4') * pq.ms t_start = 0.0 t_stop = 12.0 train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop) train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop) dtype = np.float32 units = 1 * pq.ms t_start_out = pq.Quantity(t_start, units=units, dtype=dtype) t_stop_out = pq.Quantity(t_stop, units=units, dtype=dtype) st_out = times.astype(dtype) self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_array_no_start_stop_units_set_dtype(self): times = np.arange(10) * pq.ms t_start = 0.0 t_stop = 12.0 train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, dtype='f4') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, dtype='f4') dtype = np.float32 units = 1 * pq.ms t_start_out = pq.Quantity(t_start, units=units, dtype=dtype) t_stop_out = pq.Quantity(t_stop, units=units, dtype=dtype) st_out = times.astype(dtype) self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_array_units(self): times = np.arange(10) * pq.ms t_start = 0.0*pq.s t_stop = 12.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units='s') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units='s') dtype = np.float64 units = 1 * pq.s t_start_out = t_start t_stop_out = t_stop st_out = times self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_array_units_with_dtype(self): times = np.arange(10, dtype='f4') * pq.ms t_start = 0.0*pq.s t_stop = 12.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units='s') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units='s') dtype = np.float32 units = 1 * pq.s t_start_out = t_start.astype(dtype) t_stop_out = t_stop.rescale(units).astype(dtype) st_out = times.rescale(units).astype(dtype) self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_array_units_set_dtype(self): times = np.arange(10) * pq.ms t_start = 0.0*pq.s t_stop = 12.0*pq.ms train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units='s', dtype='f4') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units='s', dtype='f4') dtype = np.float32 units = 1 * pq.s t_start_out = t_start.astype(dtype) t_stop_out = t_stop.rescale(units).astype(dtype) st_out = times.rescale(units).astype(dtype) self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_array_units_no_start_stop_units(self): times = np.arange(10) * pq.ms t_start = 0.0 t_stop = 12.0 train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units='s') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units='s') dtype = np.float64 units = 1 * pq.s t_start_out = pq.Quantity(t_start, units=units, dtype=dtype) t_stop_out = pq.Quantity(t_stop, units=units, dtype=dtype) st_out = times self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_quantity_units_no_start_stop_units_set_dtype(self): times = np.arange(10) * pq.ms t_start = 0.0 t_stop = 12.0 train1 = SpikeTrain(times, t_start=t_start, t_stop=t_stop, units='s', dtype='f4') train2 = _new_spiketrain(SpikeTrain, times, t_start=t_start, t_stop=t_stop, units='s', dtype='f4') dtype = np.float32 units = 1 * pq.s t_start_out = pq.Quantity(t_start, units=units, dtype=dtype) t_stop_out = pq.Quantity(t_stop, units=units, dtype=dtype) st_out = times.rescale(units).astype(dtype) self.result_spike_check(train1, st_out, t_start_out, t_stop_out, dtype, units) self.result_spike_check(train2, st_out, t_start_out, t_stop_out, dtype, units) def test__create_from_list_without_units_should_raise_ValueError(self): times = range(10) t_start = 0.0*pq.s t_stop = 10000.0*pq.ms self.assertRaises(ValueError, SpikeTrain, times, t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _new_spiketrain, SpikeTrain, times, t_start=t_start, t_stop=t_stop) def test__create_from_array_without_units_should_raise_ValueError(self): times = np.arange(10) t_start = 0.0*pq.s t_stop = 10000.0*pq.ms self.assertRaises(ValueError, SpikeTrain, times, t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _new_spiketrain, SpikeTrain, times, t_start=t_start, t_stop=t_stop) def test__create_from_array_with_incompatible_units_ValueError(self): times = np.arange(10) * pq.km t_start = 0.0*pq.s t_stop = 10000.0*pq.ms self.assertRaises(ValueError, SpikeTrain, times, t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _new_spiketrain, SpikeTrain, times, t_start=t_start, t_stop=t_stop) def test__create_with_times_outside_tstart_tstop_ValueError(self): t_start = 23 t_stop = 77 train1 = SpikeTrain(np.arange(t_start, t_stop), units='ms', t_start=t_start, t_stop=t_stop) train2 = _new_spiketrain(SpikeTrain, np.arange(t_start, t_stop), units='ms', t_start=t_start, t_stop=t_stop) assert_neo_object_is_compliant(train1) assert_neo_object_is_compliant(train2) self.assertRaises(ValueError, SpikeTrain, np.arange(t_start-5, t_stop), units='ms', t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _new_spiketrain, SpikeTrain, np.arange(t_start-5, t_stop), units='ms', t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, SpikeTrain, np.arange(t_start, t_stop+5), units='ms', t_start=t_start, t_stop=t_stop) self.assertRaises(ValueError, _new_spiketrain, SpikeTrain, np.arange(t_start, t_stop+5), units='ms', t_start=t_start, t_stop=t_stop) def test_defaults(self): # default recommended attributes train1 = SpikeTrain([3, 4, 5], units='sec', t_stop=10.0) train2 = _new_spiketrain(SpikeTrain, [3, 4, 5], units='sec', t_stop=10.0) assert_neo_object_is_compliant(train1) assert_neo_object_is_compliant(train2) self.assertEqual(train1.dtype, np.float) self.assertEqual(train2.dtype, np.float) self.assertEqual(train1.sampling_rate, 1.0 * pq.Hz) self.assertEqual(train2.sampling_rate, 1.0 * pq.Hz) self.assertEqual(train1.waveforms, None) self.assertEqual(train2.waveforms, None) self.assertEqual(train1.left_sweep, None) self.assertEqual(train2.left_sweep, None) def test_default_tstart(self): # t start defaults to zero train11 = SpikeTrain([3, 4, 5]*pq.s, t_stop=8000*pq.ms) train21 = _new_spiketrain(SpikeTrain, [3, 4, 5]*pq.s, t_stop=8000*pq.ms) assert_neo_object_is_compliant(train11) assert_neo_object_is_compliant(train21) self.assertEqual(train11.t_start, 0.*pq.s) self.assertEqual(train21.t_start, 0.*pq.s) # unless otherwise specified train12 = SpikeTrain([3, 4, 5]*pq.s, t_start=2.0, t_stop=8) train22 = _new_spiketrain(SpikeTrain, [3, 4, 5]*pq.s, t_start=2.0, t_stop=8) assert_neo_object_is_compliant(train12) assert_neo_object_is_compliant(train22) self.assertEqual(train12.t_start, 2.*pq.s) self.assertEqual(train22.t_start, 2.*pq.s) def test_tstop_units_conversion(self): train11 = SpikeTrain([3, 5, 4]*pq.s, t_stop=10) train21 = _new_spiketrain(SpikeTrain, [3, 5, 4]*pq.s, t_stop=10) assert_neo_object_is_compliant(train11) assert_neo_object_is_compliant(train21) self.assertEqual(train11.t_stop, 10.*pq.s) self.assertEqual(train21.t_stop, 10.*pq.s) train12 = SpikeTrain([3, 5, 4]*pq.s, t_stop=10000.*pq.ms) train22 = _new_spiketrain(SpikeTrain, [3, 5, 4]*pq.s, t_stop=10000.*pq.ms) assert_neo_object_is_compliant(train12) assert_neo_object_is_compliant(train22) self.assertEqual(train12.t_stop, 10.*pq.s) self.assertEqual(train22.t_stop, 10.*pq.s) train13 = SpikeTrain([3, 5, 4], units='sec', t_stop=10000.*pq.ms) train23 = _new_spiketrain(SpikeTrain, [3, 5, 4], units='sec', t_stop=10000.*pq.ms) assert_neo_object_is_compliant(train13) assert_neo_object_is_compliant(train23) self.assertEqual(train13.t_stop, 10.*pq.s) self.assertEqual(train23.t_stop, 10.*pq.s) class TestSorting(unittest.TestCase): def test_sort(self): waveforms = np.array([[[0., 1.]], [[2., 3.]], [[4., 5.]]]) * pq.mV train = SpikeTrain([3, 4, 5]*pq.s, waveforms=waveforms, name='n', t_stop=10.0) assert_neo_object_is_compliant(train) train.sort() assert_neo_object_is_compliant(train) assert_arrays_equal(train, [3, 4, 5]*pq.s) assert_arrays_equal(train.waveforms, waveforms) self.assertEqual(train.name, 'n') self.assertEqual(train.t_stop, 10.0 * pq.s) train = SpikeTrain([3, 5, 4]*pq.s, waveforms=waveforms, name='n', t_stop=10.0) assert_neo_object_is_compliant(train) train.sort() assert_neo_object_is_compliant(train) assert_arrays_equal(train, [3, 4, 5]*pq.s) assert_arrays_equal(train.waveforms, waveforms[[0, 2, 1]]) self.assertEqual(train.name, 'n') self.assertEqual(train.t_start, 0.0 * pq.s) self.assertEqual(train.t_stop, 10.0 * pq.s) class TestSlice(unittest.TestCase): def setUp(self): self.waveforms1 = np.array([[[0., 1.], [0.1, 1.1]], [[2., 3.], [2.1, 3.1]], [[4., 5.], [4.1, 5.1]]]) * pq.mV self.data1 = np.array([3, 4, 5]) self.data1quant = self.data1*pq.s self.train1 = SpikeTrain(self.data1quant, waveforms=self.waveforms1, name='n', arb='arbb', t_stop=10.0) def test_compliant(self): assert_neo_object_is_compliant(self.train1) def test_slice(self): # slice spike train, keep sliced spike times result = self.train1[1:2] assert_arrays_equal(self.train1[1:2], result) targwaveforms = np.array([[[2., 3.], [2.1, 3.1]]]) # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(self.train1.t_start, result.t_start) self.assertEqual(self.train1.t_stop, result.t_stop) # except we update the waveforms assert_arrays_equal(self.train1.waveforms[1:2], result.waveforms) assert_arrays_equal(targwaveforms, result.waveforms) def test_slice_to_end(self): # slice spike train, keep sliced spike times result = self.train1[1:] assert_arrays_equal(self.train1[1:], result) targwaveforms = np.array([[[2., 3.], [2.1, 3.1]], [[4., 5.], [4.1, 5.1]]]) * pq.mV # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(self.train1.t_start, result.t_start) self.assertEqual(self.train1.t_stop, result.t_stop) # except we update the waveforms assert_arrays_equal(self.train1.waveforms[1:], result.waveforms) assert_arrays_equal(targwaveforms, result.waveforms) def test_slice_from_beginning(self): # slice spike train, keep sliced spike times result = self.train1[:2] assert_arrays_equal(self.train1[:2], result) targwaveforms = np.array([[[0., 1.], [0.1, 1.1]], [[2., 3.], [2.1, 3.1]]]) * pq.mV # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(self.train1.t_start, result.t_start) self.assertEqual(self.train1.t_stop, result.t_stop) # except we update the waveforms assert_arrays_equal(self.train1.waveforms[:2], result.waveforms) assert_arrays_equal(targwaveforms, result.waveforms) def test_slice_negative_idxs(self): # slice spike train, keep sliced spike times result = self.train1[:-1] assert_arrays_equal(self.train1[:-1], result) targwaveforms = np.array([[[0., 1.], [0.1, 1.1]], [[2., 3.], [2.1, 3.1]]]) * pq.mV # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(self.train1.t_start, result.t_start) self.assertEqual(self.train1.t_stop, result.t_stop) # except we update the waveforms assert_arrays_equal(self.train1.waveforms[:-1], result.waveforms) assert_arrays_equal(targwaveforms, result.waveforms) class TestTimeSlice(unittest.TestCase): def setUp(self): self.waveforms1 = np.array([[[0., 1.], [0.1, 1.1]], [[2., 3.], [2.1, 3.1]], [[4., 5.], [4.1, 5.1]], [[6., 7.], [6.1, 7.1]], [[8., 9.], [8.1, 9.1]], [[10., 11.], [10.1, 11.1]]]) * pq.mV self.data1 = np.array([0.1, 0.5, 1.2, 3.3, 6.4, 7]) self.data1quant = self.data1*pq.ms self.train1 = SpikeTrain(self.data1quant, t_stop=10.0*pq.ms, waveforms=self.waveforms1) def test_compliant(self): assert_neo_object_is_compliant(self.train1) def test_time_slice_typical(self): # time_slice spike train, keep sliced spike times # this is the typical time slice falling somewhere # in the middle of spikes t_start = 0.12 * pq.ms t_stop = 3.5 * pq.ms result = self.train1.time_slice(t_start, t_stop) targ = SpikeTrain([0.5, 1.2, 3.3] * pq.ms, t_stop=3.3) assert_arrays_equal(result, targ) targwaveforms = np.array([[[2., 3.], [2.1, 3.1]], [[4., 5.], [4.1, 5.1]], [[6., 7.], [6.1, 7.1]]]) * pq.mV assert_arrays_equal(targwaveforms, result.waveforms) # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(t_start, result.t_start) self.assertEqual(t_stop, result.t_stop) def test_time_slice_differnt_units(self): # time_slice spike train, keep sliced spike times t_start = 0.00012 * pq.s t_stop = 0.0035 * pq.s result = self.train1.time_slice(t_start, t_stop) targ = SpikeTrain([0.5, 1.2, 3.3] * pq.ms, t_stop=3.3) assert_arrays_equal(result, targ) targwaveforms = np.array([[[2., 3.], [2.1, 3.1]], [[4., 5.], [4.1, 5.1]], [[6., 7.], [6.1, 7.1]]]) * pq.mV assert_arrays_equal(targwaveforms, result.waveforms) # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(t_start, result.t_start) self.assertEqual(t_stop, result.t_stop) def test_time_slice_matching_ends(self): # time_slice spike train, keep sliced spike times t_start = 0.1 * pq.ms t_stop = 7.0 * pq.ms result = self.train1.time_slice(t_start, t_stop) assert_arrays_equal(self.train1, result) assert_arrays_equal(self.waveforms1, result.waveforms) # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(t_start, result.t_start) self.assertEqual(t_stop, result.t_stop) def test_time_slice_out_of_boundries(self): self.train1.t_start = 0.1*pq.ms assert_neo_object_is_compliant(self.train1) # time_slice spike train, keep sliced spike times t_start = 0.01 * pq.ms t_stop = 70.0 * pq.ms result = self.train1.time_slice(t_start, t_stop) assert_arrays_equal(self.train1, result) assert_arrays_equal(self.waveforms1, result.waveforms) # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(self.train1.t_start, result.t_start) self.assertEqual(self.train1.t_stop, result.t_stop) def test_time_slice_empty(self): waveforms = np.array([[[]]]) * pq.mV train = SpikeTrain([] * pq.ms, t_stop=10.0, waveforms=waveforms) assert_neo_object_is_compliant(train) # time_slice spike train, keep sliced spike times t_start = 0.01 * pq.ms t_stop = 70.0 * pq.ms result = train.time_slice(t_start, t_stop) assert_arrays_equal(train, result) assert_arrays_equal(waveforms[:-1], result.waveforms) # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(train.name, result.name) self.assertEqual(train.description, result.description) self.assertEqual(train.annotations, result.annotations) self.assertEqual(train.file_origin, result.file_origin) self.assertEqual(train.dtype, result.dtype) self.assertEqual(t_start, result.t_start) self.assertEqual(train.t_stop, result.t_stop) def test_time_slice_none_stop(self): # time_slice spike train, keep sliced spike times t_start = 1 * pq.ms result = self.train1.time_slice(t_start, None) assert_arrays_equal([1.2, 3.3, 6.4, 7] * pq.ms, result) targwaveforms = np.array([[[4., 5.], [4.1, 5.1]], [[6., 7.], [6.1, 7.1]], [[8., 9.], [8.1, 9.1]], [[10., 11.], [10.1, 11.1]]]) * pq.mV assert_arrays_equal(targwaveforms, result.waveforms) # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(t_start, result.t_start) self.assertEqual(self.train1.t_stop, result.t_stop) def test_time_slice_none_start(self): # time_slice spike train, keep sliced spike times t_stop = 1 * pq.ms result = self.train1.time_slice(None, t_stop) assert_arrays_equal([0.1, 0.5] * pq.ms, result) targwaveforms = np.array([[[0., 1.], [0.1, 1.1]], [[2., 3.], [2.1, 3.1]]]) * pq.mV assert_arrays_equal(targwaveforms, result.waveforms) # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(self.train1.t_start, result.t_start) self.assertEqual(t_stop, result.t_stop) def test_time_slice_none_both(self): self.train1.t_start = 0.1*pq.ms assert_neo_object_is_compliant(self.train1) # time_slice spike train, keep sliced spike times result = self.train1.time_slice(None, None) assert_arrays_equal(self.train1, result) assert_arrays_equal(self.waveforms1, result.waveforms) # but keep everything else pristine assert_neo_object_is_compliant(result) self.assertEqual(self.train1.name, result.name) self.assertEqual(self.train1.description, result.description) self.assertEqual(self.train1.annotations, result.annotations) self.assertEqual(self.train1.file_origin, result.file_origin) self.assertEqual(self.train1.dtype, result.dtype) self.assertEqual(self.train1.t_start, result.t_start) self.assertEqual(self.train1.t_stop, result.t_stop) class TestAttributesAnnotations(unittest.TestCase): def test_set_universally_recommended_attributes(self): train = SpikeTrain([3, 4, 5], units='sec', name='Name', description='Desc', file_origin='crack.txt', t_stop=99.9) assert_neo_object_is_compliant(train) self.assertEqual(train.name, 'Name') self.assertEqual(train.description, 'Desc') self.assertEqual(train.file_origin, 'crack.txt') def test_autoset_universally_recommended_attributes(self): train = SpikeTrain([3, 4, 5]*pq.s, t_stop=10.0) assert_neo_object_is_compliant(train) self.assertEqual(train.name, None) self.assertEqual(train.description, None) self.assertEqual(train.file_origin, None) def test_annotations(self): train = SpikeTrain([3, 4, 5]*pq.s, t_stop=11.1) assert_neo_object_is_compliant(train) self.assertEqual(train.annotations, {}) train = SpikeTrain([3, 4, 5]*pq.s, t_stop=11.1, ratname='Phillippe') assert_neo_object_is_compliant(train) self.assertEqual(train.annotations, {'ratname': 'Phillippe'}) class TestChanging(unittest.TestCase): def test_change_with_copy_default(self): # Default is copy = True # Changing spike train does not change data # Data source is quantity data = [3, 4, 5] * pq.s train = SpikeTrain(data, t_stop=100.0) train[0] = 99 * pq.s assert_neo_object_is_compliant(train) self.assertEqual(train[0], 99*pq.s) self.assertEqual(data[0], 3*pq.s) def test_change_with_copy_false(self): # Changing spike train also changes data, because it is a view # Data source is quantity data = [3, 4, 5] * pq.s train = SpikeTrain(data, copy=False, t_stop=100.0) train[0] = 99 * pq.s assert_neo_object_is_compliant(train) self.assertEqual(train[0], 99*pq.s) self.assertEqual(data[0], 99*pq.s) def test_change_with_copy_false_and_fake_rescale(self): # Changing spike train also changes data, because it is a view # Data source is quantity data = [3000, 4000, 5000] * pq.ms # even though we specify units, it still returns a view train = SpikeTrain(data, units='ms', copy=False, t_stop=100000) train[0] = 99000 * pq.ms assert_neo_object_is_compliant(train) self.assertEqual(train[0], 99000*pq.ms) self.assertEqual(data[0], 99000*pq.ms) def test_change_with_copy_false_and_rescale_true(self): # When rescaling, a view cannot be returned # Changing spike train also changes data, because it is a view data = [3, 4, 5] * pq.s self.assertRaises(ValueError, SpikeTrain, data, units='ms', copy=False, t_stop=10000) def test_init_with_rescale(self): data = [3, 4, 5] * pq.s train = SpikeTrain(data, units='ms', t_stop=6000) assert_neo_object_is_compliant(train) self.assertEqual(train[0], 3000*pq.ms) self.assertEqual(train._dimensionality, pq.ms._dimensionality) self.assertEqual(train.t_stop, 6000*pq.ms) def test_change_with_copy_true(self): # Changing spike train does not change data # Data source is quantity data = [3, 4, 5] * pq.s train = SpikeTrain(data, copy=True, t_stop=100) train[0] = 99 * pq.s assert_neo_object_is_compliant(train) self.assertEqual(train[0], 99*pq.s) self.assertEqual(data[0], 3*pq.s) def test_change_with_copy_default_and_data_not_quantity(self): # Default is copy = True # Changing spike train does not change data # Data source is array # Array and quantity are tested separately because copy default # is different for these two. data = [3, 4, 5] train = SpikeTrain(data, units='sec', t_stop=100) train[0] = 99 * pq.s assert_neo_object_is_compliant(train) self.assertEqual(train[0], 99*pq.s) self.assertEqual(data[0], 3*pq.s) def test_change_with_copy_false_and_data_not_quantity(self): # Changing spike train also changes data, because it is a view # Data source is array # Array and quantity are tested separately because copy default # is different for these two. data = np.array([3, 4, 5]) train = SpikeTrain(data, units='sec', copy=False, dtype=np.int, t_stop=101) train[0] = 99 * pq.s assert_neo_object_is_compliant(train) self.assertEqual(train[0], 99*pq.s) self.assertEqual(data[0], 99) def test_change_with_copy_false_and_dtype_change(self): # You cannot change dtype and request a view data = np.array([3, 4, 5]) self.assertRaises(ValueError, SpikeTrain, data, units='sec', copy=False, t_stop=101, dtype=np.float64) def test_change_with_copy_true_and_data_not_quantity(self): # Changing spike train does not change data # Data source is array # Array and quantity are tested separately because copy default # is different for these two. data = [3, 4, 5] train = SpikeTrain(data, units='sec', copy=True, t_stop=123.4) train[0] = 99 * pq.s assert_neo_object_is_compliant(train) self.assertEqual(train[0], 99*pq.s) self.assertEqual(data[0], 3) def test_changing_slice_changes_original_spiketrain(self): # If we slice a spiketrain and then change the slice, the # original spiketrain should change. # Whether the original data source changes is dependent on the # copy parameter. # This is compatible with both np and quantity default behavior. data = [3, 4, 5] * pq.s train = SpikeTrain(data, copy=True, t_stop=99.9) result = train[1:3] result[0] = 99 * pq.s assert_neo_object_is_compliant(train) self.assertEqual(train[1], 99*pq.s) self.assertEqual(result[0], 99*pq.s) self.assertEqual(data[1], 4*pq.s) def test_changing_slice_changes_original_spiketrain_with_copy_false(self): # If we slice a spiketrain and then change the slice, the # original spiketrain should change. # Whether the original data source changes is dependent on the # copy parameter. # This is compatible with both np and quantity default behavior. data = [3, 4, 5] * pq.s train = SpikeTrain(data, copy=False, t_stop=100.0) result = train[1:3] result[0] = 99 * pq.s assert_neo_object_is_compliant(train) assert_neo_object_is_compliant(result) self.assertEqual(train[1], 99*pq.s) self.assertEqual(result[0], 99*pq.s) self.assertEqual(data[1], 99*pq.s) def test__changing_spiketime_should_check_time_in_range(self): data = [3, 4, 5] * pq.ms train = SpikeTrain(data, copy=False, t_start=0.5, t_stop=10.0) assert_neo_object_is_compliant(train) self.assertRaises(ValueError, train.__setitem__, 0, 10.1*pq.ms) self.assertRaises(ValueError, train.__setitem__, 1, 5.0*pq.s) self.assertRaises(ValueError, train.__setitem__, 2, 5.0*pq.s) self.assertRaises(ValueError, train.__setitem__, 0, 0) def test__changing_multiple_spiketimes(self): data = [3, 4, 5] * pq.ms train = SpikeTrain(data, copy=False, t_start=0.5, t_stop=10.0) train[:] = [7, 8, 9] * pq.ms assert_neo_object_is_compliant(train) assert_arrays_equal(train, np.array([7, 8, 9])) def test__changing_multiple_spiketimes_should_check_time_in_range(self): data = [3, 4, 5] * pq.ms train = SpikeTrain(data, copy=False, t_start=0.5, t_stop=10.0) assert_neo_object_is_compliant(train) if sys.version_info[0] == 2: self.assertRaises(ValueError, train.__setslice__, 0, 3, [3, 4, 11] * pq.ms) self.assertRaises(ValueError, train.__setslice__, 0, 3, [0, 4, 5] * pq.ms) def test__rescale(self): data = [3, 4, 5] * pq.ms train = SpikeTrain(data, t_start=0.5, t_stop=10.0) result = train.rescale(pq.s) assert_neo_object_is_compliant(train) assert_neo_object_is_compliant(result) assert_arrays_equal(train, result) self.assertEqual(result.units, 1 * pq.s) def test__rescale_same_units(self): data = [3, 4, 5] * pq.ms train = SpikeTrain(data, t_start=0.5, t_stop=10.0) result = train.rescale(pq.ms) assert_neo_object_is_compliant(train) assert_arrays_equal(train, result) self.assertEqual(result.units, 1 * pq.ms) def test__rescale_incompatible_units_ValueError(self): data = [3, 4, 5] * pq.ms train = SpikeTrain(data, t_start=0.5, t_stop=10.0) assert_neo_object_is_compliant(train) self.assertRaises(ValueError, train.rescale, pq.m) class TestPropertiesMethods(unittest.TestCase): def setUp(self): self.data1 = [3, 4, 5] self.data1quant = self.data1 * pq.ms self.waveforms1 = np.array([[[0., 1.], [0.1, 1.1]], [[2., 3.], [2.1, 3.1]], [[4., 5.], [4.1, 5.1]]]) * pq.mV self.t_start1 = 0.5 self.t_stop1 = 10.0 self.t_start1quant = self.t_start1 * pq.ms self.t_stop1quant = self.t_stop1 * pq.ms self.sampling_rate1 = .1*pq.Hz self.left_sweep1 = 2.*pq.s self.train1 = SpikeTrain(self.data1quant, t_start=self.t_start1, t_stop=self.t_stop1, waveforms=self.waveforms1, left_sweep=self.left_sweep1, sampling_rate=self.sampling_rate1) def test__compliant(self): assert_neo_object_is_compliant(self.train1) def test__repr(self): result = repr(self.train1) targ = '' self.assertEqual(result, targ) def test__duration(self): result1 = self.train1.duration self.train1.t_start = None assert_neo_object_is_compliant(self.train1) result2 = self.train1.duration self.train1.t_start = self.t_start1quant self.train1.t_stop = None assert_neo_object_is_compliant(self.train1) result3 = self.train1.duration self.assertEqual(result1, 9.5 * pq.ms) self.assertEqual(result1.units, 1. * pq.ms) self.assertEqual(result2, None) self.assertEqual(result3, None) def test__spike_duration(self): result1 = self.train1.spike_duration self.train1.sampling_rate = None assert_neo_object_is_compliant(self.train1) result2 = self.train1.spike_duration self.train1.sampling_rate = self.sampling_rate1 self.train1.waveforms = None assert_neo_object_is_compliant(self.train1) result3 = self.train1.spike_duration self.assertEqual(result1, 20./pq.Hz) self.assertEqual(result1.units, 1./pq.Hz) self.assertEqual(result2, None) self.assertEqual(result3, None) def test__sampling_period(self): result1 = self.train1.sampling_period self.train1.sampling_rate = None assert_neo_object_is_compliant(self.train1) result2 = self.train1.sampling_period self.train1.sampling_rate = self.sampling_rate1 self.train1.sampling_period = 10.*pq.ms assert_neo_object_is_compliant(self.train1) result3a = self.train1.sampling_period result3b = self.train1.sampling_rate self.train1.sampling_period = None result4a = self.train1.sampling_period result4b = self.train1.sampling_rate self.assertEqual(result1, 10./pq.Hz) self.assertEqual(result1.units, 1./pq.Hz) self.assertEqual(result2, None) self.assertEqual(result3a, 10.*pq.ms) self.assertEqual(result3a.units, 1.*pq.ms) self.assertEqual(result3b, .1/pq.ms) self.assertEqual(result3b.units, 1./pq.ms) self.assertEqual(result4a, None) self.assertEqual(result4b, None) def test__right_sweep(self): result1 = self.train1.right_sweep self.train1.left_sweep = None assert_neo_object_is_compliant(self.train1) result2 = self.train1.right_sweep self.train1.left_sweep = self.left_sweep1 self.train1.sampling_rate = None assert_neo_object_is_compliant(self.train1) result3 = self.train1.right_sweep self.train1.sampling_rate = self.sampling_rate1 self.train1.waveforms = None assert_neo_object_is_compliant(self.train1) result4 = self.train1.right_sweep self.assertEqual(result1, 22.*pq.s) self.assertEqual(result1.units, 1.*pq.s) self.assertEqual(result2, None) self.assertEqual(result3, None) self.assertEqual(result4, None) class TestMiscellaneous(unittest.TestCase): def test__different_dtype_for_t_start_and_array(self): data = np.array([0, 9.9999999], dtype=np.float64) * pq.s data16 = data.astype(np.float16) data32 = data.astype(np.float32) data64 = data.astype(np.float64) t_start = data[0] t_stop = data[1] t_start16 = data[0].astype(dtype=np.float16) t_stop16 = data[1].astype(dtype=np.float16) t_start32 = data[0].astype(dtype=np.float32) t_stop32 = data[1].astype(dtype=np.float32) t_start64 = data[0].astype(dtype=np.float64) t_stop64 = data[1].astype(dtype=np.float64) t_start_custom = 0.0 t_stop_custom = 10.0 t_start_custom16 = np.array(t_start_custom, dtype=np.float16) t_stop_custom16 = np.array(t_stop_custom, dtype=np.float16) t_start_custom32 = np.array(t_start_custom, dtype=np.float32) t_stop_custom32 = np.array(t_stop_custom, dtype=np.float32) t_start_custom64 = np.array(t_start_custom, dtype=np.float64) t_stop_custom64 = np.array(t_stop_custom, dtype=np.float64) #This is OK. train = SpikeTrain(data64, copy=True, t_start=t_start, t_stop=t_stop) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start, t_stop=t_stop, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start, t_stop=t_stop, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start, t_stop=t_stop, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start, t_stop=t_stop, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start16, t_stop=t_stop16) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start16, t_stop=t_stop16, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start16, t_stop=t_stop16, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start16, t_stop=t_stop16, dtype=np.float64) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start32, t_stop=t_stop32) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start32, t_stop=t_stop32, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start32, t_stop=t_stop32, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start32, t_stop=t_stop32, dtype=np.float64) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start64, t_stop=t_stop64, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start64, t_stop=t_stop64, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start_custom, t_stop=t_stop_custom) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start_custom, t_stop=t_stop_custom, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start_custom, t_stop=t_stop_custom, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start_custom, t_stop=t_stop_custom, dtype=np.float64) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom, t_stop=t_stop_custom) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom, t_stop=t_stop_custom, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom, t_stop=t_stop_custom, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom, t_stop=t_stop_custom, dtype=np.float64) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start_custom, t_stop=t_stop_custom) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start_custom, t_stop=t_stop_custom, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start_custom, t_stop=t_stop_custom, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start_custom, t_stop=t_stop_custom, dtype=np.float64) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom16, t_stop=t_stop_custom16) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom16, t_stop=t_stop_custom16, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom16, t_stop=t_stop_custom16, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom16, t_stop=t_stop_custom16, dtype=np.float64) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom32, t_stop=t_stop_custom32) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom32, t_stop=t_stop_custom32, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom32, t_stop=t_stop_custom32, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom32, t_stop=t_stop_custom32, dtype=np.float64) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom64, t_stop=t_stop_custom64) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom64, t_stop=t_stop_custom64, dtype=np.float16) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom64, t_stop=t_stop_custom64, dtype=np.float32) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start_custom64, t_stop=t_stop_custom64, dtype=np.float64) assert_neo_object_is_compliant(train) #This use to bug - see ticket #38 train = SpikeTrain(data16, copy=True, t_start=t_start, t_stop=t_stop) assert_neo_object_is_compliant(train) train = SpikeTrain(data16, copy=True, t_start=t_start, t_stop=t_stop, dtype=np.float64) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start, t_stop=t_stop) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start, t_stop=t_stop, dtype=np.float64) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start64, t_stop=t_stop64) assert_neo_object_is_compliant(train) train = SpikeTrain(data32, copy=True, t_start=t_start64, t_stop=t_stop64, dtype=np.float64) assert_neo_object_is_compliant(train) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_analogsignalarray.py0000644000175000017500000007135112273723542022227 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.analogsignalarray.AnalogSignalArrayArray class """ import os import pickle try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.analogsignalarray import AnalogSignalArray from neo.core.analogsignal import AnalogSignal from neo.test.tools import (assert_arrays_almost_equal, assert_arrays_equal, assert_neo_object_is_compliant, assert_same_sub_schema) class TestAnalogSignalArrayConstructor(unittest.TestCase): def test__create_from_list(self): data = [(i, i, i) for i in range(10)] # 3 signals each with 10 samples rate = 1000*pq.Hz signal = AnalogSignalArray(data, sampling_rate=rate, units="mV") assert_neo_object_is_compliant(signal) self.assertEqual(signal.shape, (10, 3)) self.assertEqual(signal.t_start, 0*pq.ms) self.assertEqual(signal.t_stop, len(data)/rate) self.assertEqual(signal[9, 0], 9000*pq.uV) def test__create_from_numpy_array(self): data = np.arange(20.0).reshape((10, 2)) rate = 1*pq.kHz signal = AnalogSignalArray(data, sampling_rate=rate, units="uV") assert_neo_object_is_compliant(signal) self.assertEqual(signal.t_start, 0*pq.ms) self.assertEqual(signal.t_stop, data.shape[0]/rate) self.assertEqual(signal[9, 0], 0.018*pq.mV) self.assertEqual(signal[9, 1], 19*pq.uV) def test__create_from_quantities_array(self): data = np.arange(20.0).reshape((10, 2)) * pq.mV rate = 5000*pq.Hz signal = AnalogSignalArray(data, sampling_rate=rate) assert_neo_object_is_compliant(signal) self.assertEqual(signal.t_start, 0*pq.ms) self.assertEqual(signal.t_stop, data.shape[0]/rate) self.assertEqual(signal[9, 0], 18000*pq.uV) def test__create_from_quantities_with_inconsistent_units_ValueError(self): data = np.arange(20.0).reshape((10, 2)) * pq.mV self.assertRaises(ValueError, AnalogSignalArray, data, sampling_rate=1*pq.kHz, units="nA") def test__create_with_copy_true_should_return_copy(self): data = np.arange(20.0).reshape((10, 2)) * pq.mV rate = 5000*pq.Hz signal = AnalogSignalArray(data, copy=True, sampling_rate=rate) assert_neo_object_is_compliant(signal) data[3, 0] = 0.099*pq.V self.assertNotEqual(signal[3, 0], 99*pq.mV) def test__create_with_copy_false_should_return_view(self): data = np.arange(20.0).reshape((10, 2)) * pq.mV rate = 5000*pq.Hz signal = AnalogSignalArray(data, copy=False, sampling_rate=rate) assert_neo_object_is_compliant(signal) data[3, 0] = 99*pq.mV self.assertEqual(signal[3, 0], 99000*pq.uV) # signal must not be 1D - should raise Exception if 1D class TestAnalogSignalArrayProperties(unittest.TestCase): def setUp(self): self.t_start = [0.0*pq.ms, 100*pq.ms, -200*pq.ms] self.rates = [1*pq.kHz, 420*pq.Hz, 999*pq.Hz] self.data = [np.arange(10.0).reshape((5, 2))*pq.nA, np.arange(-100.0, 100.0, 10.0).reshape((4, 5))*pq.mV, np.random.uniform(size=(100, 4))*pq.uV] self.signals = [AnalogSignalArray(D, sampling_rate=r, t_start=t) for r, D, t in zip(self.rates, self.data, self.t_start)] def test__compliant(self): for signal in self.signals: assert_neo_object_is_compliant(signal) def test__t_stop(self): for i, signal in enumerate(self.signals): targ = self.t_start[i] + self.data[i].shape[0]/self.rates[i] self.assertEqual(signal.t_stop, targ) def test__duration(self): for signal in self.signals: self.assertAlmostEqual(signal.duration, signal.t_stop - signal.t_start, delta=1e-15) def test__sampling_period(self): for signal, rate in zip(self.signals, self.rates): self.assertEqual(signal.sampling_period, 1/rate) def test__times(self): for i, signal in enumerate(self.signals): targ = np.arange(self.data[i].shape[0]) targ = targ/self.rates[i] + self.t_start[i] assert_arrays_almost_equal(signal.times, targ, 1e-12*pq.ms) class TestAnalogSignalArrayArrayMethods(unittest.TestCase): def setUp(self): self.data1 = np.arange(55.0).reshape((11, 5)) self.data1quant = self.data1 * pq.nA self.signal1 = AnalogSignalArray(self.data1quant, sampling_rate=1*pq.kHz, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') self.data2 = np.array([[0, 1, 2, 3, 4, 5], [0, 1, 2, 3, 4, 5]]).T self.data2quant = self.data2 * pq.mV self.signal2 = AnalogSignalArray(self.data2quant, sampling_rate=1.0*pq.Hz, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') def test__compliant(self): assert_neo_object_is_compliant(self.signal1) self.assertEqual(self.signal1.name, 'spam') self.assertEqual(self.signal1.description, 'eggs') self.assertEqual(self.signal1.file_origin, 'testfile.txt') self.assertEqual(self.signal1.annotations, {'arg1': 'test'}) assert_neo_object_is_compliant(self.signal2) self.assertEqual(self.signal2.name, 'spam') self.assertEqual(self.signal2.description, 'eggs') self.assertEqual(self.signal2.file_origin, 'testfile.txt') self.assertEqual(self.signal2.annotations, {'arg1': 'test'}) def test__index_dim1_should_return_analogsignal(self): result = self.signal1[:, 0] self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, None) self.assertEqual(result.description, None) self.assertEqual(result.file_origin, None) self.assertEqual(result.annotations, {}) self.assertEqual(result.t_stop, self.signal1.t_stop) self.assertEqual(result.t_start, self.signal1.t_start) self.assertEqual(result.sampling_rate, self.signal1.sampling_rate) assert_arrays_equal(result, self.data1[:, 0]) def test__index_dim1_and_slice_dim0_should_return_analogsignal(self): result = self.signal1[2:7, 0] self.assertIsInstance(result, AnalogSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, None) self.assertEqual(result.description, None) self.assertEqual(result.file_origin, None) self.assertEqual(result.annotations, {}) self.assertEqual(result.t_start, self.signal1.t_start+2*self.signal1.sampling_period) self.assertEqual(result.t_stop, self.signal1.t_start+7*self.signal1.sampling_period) self.assertEqual(result.sampling_rate, self.signal1.sampling_rate) assert_arrays_equal(result, self.data1[2:7, 0]) def test__index_dim0_should_return_quantity_array(self): # i.e. values from all signals for a single point in time result = self.signal1[3, :] self.assertIsInstance(result, pq.Quantity) self.assertFalse(hasattr(result, 'name')) self.assertFalse(hasattr(result, 'description')) self.assertFalse(hasattr(result, 'file_origin')) self.assertFalse(hasattr(result, 'annotations')) self.assertEqual(result.shape, (5,)) self.assertFalse(hasattr(result, "t_start")) self.assertEqual(result.units, pq.nA) assert_arrays_equal(result, self.data1[3, :]) def test__index_dim0_and_slice_dim1_should_return_quantity_array(self): # i.e. values from a subset of signals for a single point in time result = self.signal1[3, 2:5] self.assertIsInstance(result, pq.Quantity) self.assertFalse(hasattr(result, 'name')) self.assertFalse(hasattr(result, 'description')) self.assertFalse(hasattr(result, 'file_origin')) self.assertFalse(hasattr(result, 'annotations')) self.assertEqual(result.shape, (3,)) self.assertFalse(hasattr(result, "t_start")) self.assertEqual(result.units, pq.nA) assert_arrays_equal(result, self.data1[3, 2:5]) def test__index_as_string_IndexError(self): self.assertRaises(IndexError, self.signal1.__getitem__, 5.) def test__slice_both_dimensions_should_return_analogsignalarray(self): result = self.signal1[0:3, 0:3] self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) targ = AnalogSignalArray([[0, 1, 2], [5, 6, 7], [10, 11, 12]], dtype=float, units="nA", sampling_rate=1*pq.kHz, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') assert_neo_object_is_compliant(targ) self.assertEqual(result.t_stop, targ.t_stop) self.assertEqual(result.t_start, targ.t_start) self.assertEqual(result.sampling_rate, targ.sampling_rate) self.assertEqual(result.shape, targ.shape) assert_same_sub_schema(result, targ) assert_arrays_equal(result, self.data1[0:3, 0:3]) def test__slice_only_first_dimension_should_return_analogsignalarray(self): result = self.signal1[2:7] self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(result.shape, (5, 5)) self.assertEqual(result.t_start, self.signal1.t_start+2*self.signal1.sampling_period) self.assertEqual(result.t_stop, self.signal1.t_start+7*self.signal1.sampling_period) self.assertEqual(result.sampling_rate, self.signal1.sampling_rate) assert_arrays_equal(result, self.data1[2:7]) def test__getitem_should_return_single_quantity(self): # quantities drops the units in this case self.assertEqual(self.signal1[9, 3], 48000*pq.pA) self.assertEqual(self.signal1[9][3], self.signal1[9, 3]) self.assertTrue(hasattr(self.signal1[9, 3], 'units')) self.assertRaises(IndexError, self.signal1.__getitem__, (99, 73)) def test_comparison_operators(self): assert_arrays_equal(self.signal1[0:3, 0:3] >= 5*pq.nA, np.array([[False, False, False], [True, True, True], [True, True, True]])) assert_arrays_equal(self.signal1[0:3, 0:3] >= 5*pq.pA, np.array([[False, True, True], [True, True, True], [True, True, True]])) def test__comparison_with_inconsistent_units_should_raise_Exception(self): self.assertRaises(ValueError, self.signal1.__gt__, 5*pq.mV) def test__simple_statistics(self): self.assertEqual(self.signal1.max(), 54000*pq.pA) self.assertEqual(self.signal1.min(), 0*pq.nA) self.assertEqual(self.signal1.mean(), 27*pq.nA) def test__rescale_same(self): result = self.signal1.copy() result = result.rescale(pq.nA) self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(result.units, 1*pq.nA) assert_arrays_equal(result, self.data1) assert_same_sub_schema(result, self.signal1) def test__rescale_new(self): result = self.signal1.copy() result = result.rescale(pq.pA) self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(result.units, 1*pq.pA) assert_arrays_almost_equal(np.array(result), self.data1*1000., 1e-10) def test__time_slice(self): t_start = 2 * pq.s t_stop = 4 * pq.s result = self.signal2.time_slice(t_start, t_stop) self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) targ = AnalogSignalArray(np.array([[2, 3], [2, 3]]).T, sampling_rate=1.0*pq.Hz, units='mV', t_start=t_start, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') assert_neo_object_is_compliant(result) self.assertEqual(result.t_stop, t_stop) self.assertEqual(result.t_start, t_start) self.assertEqual(result.sampling_rate, targ.sampling_rate) assert_arrays_equal(result, targ) assert_same_sub_schema(result, targ) def test__time_slice__out_of_bounds_ValueError(self): t_start_good = 2 * pq.s t_stop_good = 4 * pq.s t_start_bad = -2 * pq.s t_stop_bad = 40 * pq.s self.assertRaises(ValueError, self.signal2.time_slice, t_start_good, t_stop_bad) self.assertRaises(ValueError, self.signal2.time_slice, t_start_bad, t_stop_good) self.assertRaises(ValueError, self.signal2.time_slice, t_start_bad, t_stop_bad) def test__time_equal(self): t_start = 0 * pq.s t_stop = 6 * pq.s result = self.signal2.time_slice(t_start, t_stop) self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(result.t_stop, t_stop) self.assertEqual(result.t_start, t_start) assert_arrays_equal(result, self.signal2) assert_same_sub_schema(result, self.signal2) def test__time_slice__offset(self): self.signal2.t_start = 10.0 * pq.s assert_neo_object_is_compliant(self.signal2) t_start = 12 * pq.s t_stop = 14 * pq.s result = self.signal2.time_slice(t_start, t_stop) self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) targ = AnalogSignalArray(np.array([[2, 3], [2, 3]]).T, t_start=12.0*pq.ms, sampling_rate=1.0*pq.Hz, units='mV', name='spam', description='eggs', file_origin='testfile.txt', arg1='test') assert_neo_object_is_compliant(result) self.assertEqual(self.signal2.t_start, 10.0 * pq.s) self.assertEqual(result.t_stop, t_stop) self.assertEqual(result.t_start, t_start) self.assertEqual(result.sampling_rate, targ.sampling_rate) assert_arrays_equal(result, targ) assert_same_sub_schema(result, targ) def test__time_slice__different_units(self): self.signal2.t_start = 10.0 * pq.ms assert_neo_object_is_compliant(self.signal2) t_start = 2 * pq.s + 10.0 * pq.ms t_stop = 4 * pq.s + 10.0 * pq.ms result = self.signal2.time_slice(t_start, t_stop) self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) targ = AnalogSignalArray(np.array([[2, 3], [2, 3]]).T, t_start=t_start.rescale(pq.ms), sampling_rate=1.0*pq.Hz, units='mV', name='spam', description='eggs', file_origin='testfile.txt', arg1='test') assert_neo_object_is_compliant(result) assert_neo_object_is_compliant(self.signal2) self.assertEqual(self.signal2.t_start, 10.0 * pq.ms) self.assertAlmostEqual(result.t_stop, t_stop, delta=1e-12*pq.ms) self.assertAlmostEqual(result.t_start, t_start, delta=1e-12*pq.ms) assert_arrays_almost_equal(result.times, targ.times, 1e-12*pq.ms) self.assertEqual(result.sampling_rate, targ.sampling_rate) assert_arrays_equal(result, targ) assert_same_sub_schema(result, targ) class TestAnalogSignalArrayEquality(unittest.TestCase): def test__signals_with_different_data_complement_should_be_not_equal(self): signal1 = AnalogSignalArray(np.arange(55.0).reshape((11, 5)), units="mV", sampling_rate=1*pq.kHz) signal2 = AnalogSignalArray(np.arange(55.0).reshape((11, 5)), units="mV", sampling_rate=2*pq.kHz) self.assertNotEqual(signal1, signal2) assert_neo_object_is_compliant(signal1) assert_neo_object_is_compliant(signal2) class TestAnalogSignalArrayCombination(unittest.TestCase): def setUp(self): self.data1 = np.arange(55.0).reshape((11, 5)) self.data1quant = self.data1 * pq.mV self.signal1 = AnalogSignalArray(self.data1quant, sampling_rate=1*pq.kHz, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') self.data2 = np.arange(100.0, 155.0).reshape((11, 5)) self.data2quant = self.data2 * pq.mV self.signal2 = AnalogSignalArray(self.data2quant, sampling_rate=1*pq.kHz, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') def test__compliant(self): assert_neo_object_is_compliant(self.signal1) self.assertEqual(self.signal1.name, 'spam') self.assertEqual(self.signal1.description, 'eggs') self.assertEqual(self.signal1.file_origin, 'testfile.txt') self.assertEqual(self.signal1.annotations, {'arg1': 'test'}) assert_neo_object_is_compliant(self.signal2) self.assertEqual(self.signal2.name, 'spam') self.assertEqual(self.signal2.description, 'eggs') self.assertEqual(self.signal2.file_origin, 'testfile.txt') self.assertEqual(self.signal2.annotations, {'arg1': 'test'}) def test__add_const_quantity_should_preserve_data_complement(self): result = self.signal1 + 0.065*pq.V self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) # time zero, signal index 4 assert_arrays_equal(result, self.data1 + 65) self.assertEqual(self.signal1[0, 4], 4*pq.mV) self.assertEqual(result[0, 4], 69000*pq.uV) self.assertEqual(self.signal1.t_start, result.t_start) self.assertEqual(self.signal1.sampling_rate, result.sampling_rate) def test__add_two_consistent_signals_should_preserve_data_complement(self): result = self.signal1 + self.signal2 self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) targdata = np.arange(100.0, 210.0, 2.0).reshape((11, 5)) targ = AnalogSignalArray(targdata, units="mV", sampling_rate=1*pq.kHz, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') assert_neo_object_is_compliant(targ) assert_arrays_equal(result, targdata) assert_same_sub_schema(result, targ) def test__add_signals_with_inconsistent_data_complement_ValueError(self): self.signal2.sampling_rate = 0.5*pq.kHz assert_neo_object_is_compliant(self.signal2) self.assertRaises(ValueError, self.signal1.__add__, self.signal2) def test__subtract_const_should_preserve_data_complement(self): result = self.signal1 - 65*pq.mV self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(np.array(self.signal1[1, 4]), 9) self.assertEqual(np.array(result[1, 4]), -56) assert_arrays_equal(result, self.data1 - 65) self.assertEqual(self.signal1.sampling_rate, result.sampling_rate) def test__subtract_from_const_should_return_signal(self): result = 10*pq.mV - self.signal1 self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(np.array(self.signal1[1, 4]), 9) self.assertEqual(np.array(result[1, 4]), 1) assert_arrays_equal(result, 10 - self.data1) self.assertEqual(self.signal1.sampling_rate, result.sampling_rate) def test__mult_by_const_float_should_preserve_data_complement(self): result = self.signal1*2 self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(np.array(self.signal1[1, 4]), 9) self.assertEqual(np.array(result[1, 4]), 18) assert_arrays_equal(result, self.data1*2) self.assertEqual(self.signal1.sampling_rate, result.sampling_rate) def test__divide_by_const_should_preserve_data_complement(self): result = self.signal1/0.5 self.assertIsInstance(result, AnalogSignalArray) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(np.array(self.signal1[1, 4]), 9) self.assertEqual(np.array(result[1, 4]), 18) assert_arrays_equal(result, self.data1/0.5) self.assertEqual(self.signal1.sampling_rate, result.sampling_rate) def test__merge(self): self.signal1.description = None self.signal1.file_origin = None assert_neo_object_is_compliant(self.signal1) data3 = np.arange(1000.0, 1066.0).reshape((11, 6)) * pq.uV data3scale = data3.rescale(self.data1quant.units) signal2 = AnalogSignalArray(self.data1quant, sampling_rate=1*pq.kHz, channel_index=np.arange(5), name='signal2', description='test signal', file_origin='testfile.txt') signal3 = AnalogSignalArray(data3, units="uV", sampling_rate=1*pq.kHz, channel_index=np.arange(5, 11), name='signal3', description='test signal', file_origin='testfile.txt') signal4 = AnalogSignalArray(data3, units="uV", sampling_rate=1*pq.kHz, name='signal4', description='test signal', file_origin='testfile.txt') merged13 = self.signal1.merge(signal3) merged23 = signal2.merge(signal3) merged24 = signal2.merge(signal4) mergeddata13 = np.array(merged13) mergeddata23 = np.array(merged23) mergeddata24 = np.array(merged24) targdata13 = np.hstack([self.data1quant, data3scale]) targdata23 = np.hstack([self.data1quant, data3scale]) targdata24 = np.hstack([self.data1quant, data3scale]) assert_neo_object_is_compliant(signal2) assert_neo_object_is_compliant(signal3) assert_neo_object_is_compliant(merged13) assert_neo_object_is_compliant(merged23) assert_neo_object_is_compliant(merged24) self.assertEqual(merged13[0, 4], 4*pq.mV) self.assertEqual(merged23[0, 4], 4*pq.mV) self.assertEqual(merged13[0, 5], 1*pq.mV) self.assertEqual(merged23[0, 5], 1*pq.mV) self.assertEqual(merged13[10, 10], 1.065*pq.mV) self.assertEqual(merged23[10, 10], 1.065*pq.mV) self.assertEqual(merged13.t_stop, self.signal1.t_stop) self.assertEqual(merged23.t_stop, self.signal1.t_stop) self.assertEqual(merged13.name, 'merge(spam, signal3)') self.assertEqual(merged23.name, 'merge(signal2, signal3)') self.assertEqual(merged13.description, 'merge(None, test signal)') self.assertEqual(merged23.description, 'test signal') self.assertEqual(merged13.file_origin, 'merge(None, testfile.txt)') self.assertEqual(merged23.file_origin, 'testfile.txt') assert_arrays_equal(mergeddata13, targdata13) assert_arrays_equal(mergeddata23, targdata23) assert_arrays_equal(mergeddata24, targdata24) assert_arrays_equal(merged13.channel_indexes, np.arange(5, 11)) assert_arrays_equal(merged23.channel_indexes, np.arange(11)) assert_arrays_equal(merged24.channel_indexes, np.arange(5)) class TestAnalogSignalArrayFunctions(unittest.TestCase): def test__pickle(self): signal1 = AnalogSignalArray(np.arange(55.0).reshape((11, 5)), units="mV", sampling_rate=1*pq.kHz, channel_index=np.arange(5)) fobj = open('./pickle', 'wb') pickle.dump(signal1, fobj) fobj.close() fobj = open('./pickle', 'rb') try: signal2 = pickle.load(fobj) except ValueError: signal2 = None assert_arrays_equal(signal1, signal2) assert_neo_object_is_compliant(signal1) assert_neo_object_is_compliant(signal2) self.assertEqual(list(signal1.channel_indexes), [0, 1, 2, 3, 4]) self.assertEqual(list(signal1.channel_indexes), list(signal2.channel_indexes)) fobj.close() os.remove('./pickle') if __name__ == "__main__": unittest.main() neo-0.3.3/neo/test/test_irregularysampledsignal.py0000644000175000017500000004525212273723542023463 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ Tests of the neo.core.irregularlysampledsignal.IrregularySampledSignal class """ try: import unittest2 as unittest except ImportError: import unittest import numpy as np import quantities as pq from neo.core.irregularlysampledsignal import IrregularlySampledSignal from neo.test.tools import (assert_arrays_almost_equal, assert_arrays_equal, assert_neo_object_is_compliant, assert_same_sub_schema) class TestIrregularlySampledSignalConstruction(unittest.TestCase): def test_IrregularlySampledSignal_creation_times_units_signal_units(self): params = {'testarg2': 'yes', 'testarg3': True} sig = IrregularlySampledSignal([1.1, 1.5, 1.7]*pq.ms, signal=[20., 40., 60.]*pq.mV, name='test', description='tester', file_origin='test.file', testarg1=1, **params) sig.annotate(testarg1=1.1, testarg0=[1, 2, 3]) assert_neo_object_is_compliant(sig) assert_arrays_equal(sig.times, [1.1, 1.5, 1.7]*pq.ms) assert_arrays_equal(np.asarray(sig), np.array([20., 40., 60.])) self.assertEqual(sig.units, pq.mV) self.assertEqual(sig.name, 'test') self.assertEqual(sig.description, 'tester') self.assertEqual(sig.file_origin, 'test.file') self.assertEqual(sig.annotations['testarg0'], [1, 2, 3]) self.assertEqual(sig.annotations['testarg1'], 1.1) self.assertEqual(sig.annotations['testarg2'], 'yes') self.assertTrue(sig.annotations['testarg3']) def test_IrregularlySampledSignal_creation_units_arg(self): params = {'testarg2': 'yes', 'testarg3': True} sig = IrregularlySampledSignal([1.1, 1.5, 1.7], signal=[20., 40., 60.], units=pq.V, time_units=pq.s, name='test', description='tester', file_origin='test.file', testarg1=1, **params) sig.annotate(testarg1=1.1, testarg0=[1, 2, 3]) assert_neo_object_is_compliant(sig) assert_arrays_equal(sig.times, [1.1, 1.5, 1.7]*pq.s) assert_arrays_equal(np.asarray(sig), np.array([20., 40., 60.])) self.assertEqual(sig.units, pq.V) self.assertEqual(sig.name, 'test') self.assertEqual(sig.description, 'tester') self.assertEqual(sig.file_origin, 'test.file') self.assertEqual(sig.annotations['testarg0'], [1, 2, 3]) self.assertEqual(sig.annotations['testarg1'], 1.1) self.assertEqual(sig.annotations['testarg2'], 'yes') self.assertTrue(sig.annotations['testarg3']) def test_IrregularlySampledSignal_creation_units_rescale(self): params = {'testarg2': 'yes', 'testarg3': True} sig = IrregularlySampledSignal([1.1, 1.5, 1.7]*pq.s, signal=[2., 4., 6.]*pq.V, units=pq.mV, time_units=pq.ms, name='test', description='tester', file_origin='test.file', testarg1=1, **params) sig.annotate(testarg1=1.1, testarg0=[1, 2, 3]) assert_neo_object_is_compliant(sig) assert_arrays_equal(sig.times, [1100, 1500, 1700]*pq.ms) assert_arrays_equal(np.asarray(sig), np.array([2000., 4000., 6000.])) self.assertEqual(sig.units, pq.mV) self.assertEqual(sig.name, 'test') self.assertEqual(sig.description, 'tester') self.assertEqual(sig.file_origin, 'test.file') self.assertEqual(sig.annotations['testarg0'], [1, 2, 3]) self.assertEqual(sig.annotations['testarg1'], 1.1) self.assertEqual(sig.annotations['testarg2'], 'yes') self.assertTrue(sig.annotations['testarg3']) def test_IrregularlySampledSignal_different_lens_ValueError(self): times = [1.1, 1.5, 1.7]*pq.ms signal = [20., 40., 60., 70.]*pq.mV self.assertRaises(ValueError, IrregularlySampledSignal, times, signal) def test_IrregularlySampledSignal_no_signal_units_ValueError(self): times = [1.1, 1.5, 1.7]*pq.ms signal = [20., 40., 60.] self.assertRaises(ValueError, IrregularlySampledSignal, times, signal) def test_IrregularlySampledSignal_no_time_units_ValueError(self): times = [1.1, 1.5, 1.7] signal = [20., 40., 60.]*pq.mV self.assertRaises(ValueError, IrregularlySampledSignal, times, signal) class TestIrregularlySampledSignalProperties(unittest.TestCase): def setUp(self): self.times = [np.arange(10.0)*pq.s, np.arange(-100.0, 100.0, 10.0)*pq.ms, np.arange(100)*pq.ns] self.data = [np.arange(10.0)*pq.nA, np.arange(-100.0, 100.0, 10.0)*pq.mV, np.random.uniform(size=100)*pq.uV] self.signals = [IrregularlySampledSignal(t, signal=D, testattr='test') for D, t in zip(self.data, self.times)] def test__compliant(self): for signal in self.signals: assert_neo_object_is_compliant(signal) def test__t_start_getter(self): for signal, times in zip(self.signals, self.times): self.assertAlmostEqual(signal.t_start, times[0], delta=1e-15) def test__t_stop_getter(self): for signal, times in zip(self.signals, self.times): self.assertAlmostEqual(signal.t_stop, times[-1], delta=1e-15) def test__duration_getter(self): for signal, times in zip(self.signals, self.times): self.assertAlmostEqual(signal.duration, times[-1] - times[0], delta=1e-15) def test__sampling_intervals_getter(self): for signal, times in zip(self.signals, self.times): assert_arrays_almost_equal(signal.sampling_intervals, np.diff(times), threshold=1e-15) def test_IrregularlySampledSignal_repr(self): sig = IrregularlySampledSignal([1.1, 1.5, 1.7]*pq.s, signal=[2., 4., 6.]*pq.V, name='test', description='tester', file_origin='test.file', testarg1=1) assert_neo_object_is_compliant(sig) targ = ('') res = repr(sig) self.assertEqual(targ, res) class TestIrregularlySampledSignalArrayMethods(unittest.TestCase): def setUp(self): self.data1 = np.arange(10.0) self.data1quant = self.data1 * pq.mV self.time1 = np.logspace(1, 5, 10) self.time1quant = self.time1*pq.ms self.signal1 = IrregularlySampledSignal(self.time1quant, signal=self.data1quant, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') def test__compliant(self): assert_neo_object_is_compliant(self.signal1) self.assertEqual(self.signal1.name, 'spam') self.assertEqual(self.signal1.description, 'eggs') self.assertEqual(self.signal1.file_origin, 'testfile.txt') self.assertEqual(self.signal1.annotations, {'arg1': 'test'}) def test__slice_should_return_IrregularlySampledSignal(self): result = self.signal1[3:8] self.assertIsInstance(result, IrregularlySampledSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(result.size, 5) self.assertEqual(result.t_start, self.time1quant[3]) self.assertEqual(result.t_stop, self.time1quant[7]) assert_arrays_equal(self.time1quant[3:8], result.times) assert_arrays_equal(self.data1[3:8], result) # Test other attributes were copied over (in this case, defaults) self.assertEqual(result.file_origin, self.signal1.file_origin) self.assertEqual(result.name, self.signal1.name) self.assertEqual(result.description, self.signal1.description) self.assertEqual(result.annotations, self.signal1.annotations) def test__getitem_should_return_single_quantity(self): self.assertEqual(self.signal1[0], 0*pq.mV) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertRaises(IndexError, self.signal1.__getitem__, 10) def test__getitem_out_of_bounds_IndexError(self): self.assertRaises(IndexError, self.signal1.__getitem__, 10) def test_comparison_operators(self): assert_arrays_equal(self.signal1 >= 5*pq.mV, np.array([False, False, False, False, False, True, True, True, True, True])) def test__comparison_with_inconsistent_units_should_raise_Exception(self): self.assertRaises(ValueError, self.signal1.__gt__, 5*pq.nA) def test_simple_statistics(self): targmean = self.signal1[:-1]*np.diff(self.time1quant) targmean = targmean.sum()/(self.time1quant[-1]-self.time1quant[0]) self.assertEqual(self.signal1.max(), 9*pq.mV) self.assertEqual(self.signal1.min(), 0*pq.mV) self.assertEqual(self.signal1.mean(), targmean) def test_mean_interpolation_NotImplementedError(self): self.assertRaises(NotImplementedError, self.signal1.mean, True) def test_resample_NotImplementedError(self): self.assertRaises(NotImplementedError, self.signal1.resample, True) def test__rescale_same(self): result = self.signal1.copy() result = result.rescale(pq.mV) self.assertIsInstance(result, IrregularlySampledSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(result.units, 1*pq.mV) assert_arrays_equal(result, self.data1) assert_arrays_equal(result.times, self.time1quant) assert_same_sub_schema(result, self.signal1) def test__rescale_new(self): result = self.signal1.copy() result = result.rescale(pq.uV) self.assertIsInstance(result, IrregularlySampledSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(result.units, 1*pq.uV) assert_arrays_almost_equal(np.array(result), self.data1*1000., 1e-10) assert_arrays_equal(result.times, self.time1quant) def test__rescale_new_incompatible_ValueError(self): self.assertRaises(ValueError, self.signal1.rescale, pq.nA) class TestIrregularlySampledSignalCombination(unittest.TestCase): def setUp(self): self.data1 = np.arange(10.0) self.data1quant = self.data1 * pq.mV self.time1 = np.logspace(1, 5, 10) self.time1quant = self.time1*pq.ms self.signal1 = IrregularlySampledSignal(self.time1quant, signal=self.data1quant, name='spam', description='eggs', file_origin='testfile.txt', arg1='test') def test__compliant(self): assert_neo_object_is_compliant(self.signal1) self.assertEqual(self.signal1.name, 'spam') self.assertEqual(self.signal1.description, 'eggs') self.assertEqual(self.signal1.file_origin, 'testfile.txt') self.assertEqual(self.signal1.annotations, {'arg1': 'test'}) def test__add_const_quantity_should_preserve_data_complement(self): result = self.signal1 + 0.065*pq.V self.assertIsInstance(result, IrregularlySampledSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) assert_arrays_equal(result, self.data1 + 65) assert_arrays_equal(result.times, self.time1quant) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], 74*pq.mV) def test__add_two_consistent_signals_should_preserve_data_complement(self): data2 = np.arange(10.0, 20.0) data2quant = data2*pq.mV signal2 = IrregularlySampledSignal(self.time1quant, signal=data2quant) assert_neo_object_is_compliant(signal2) result = self.signal1 + signal2 self.assertIsInstance(result, IrregularlySampledSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) targ = IrregularlySampledSignal(self.time1quant, signal=np.arange(10.0, 30.0, 2.0), units="mV", name='spam', description='eggs', file_origin='testfile.txt', arg1='test') assert_neo_object_is_compliant(targ) assert_arrays_equal(result, targ) assert_arrays_equal(self.time1quant, targ.times) assert_arrays_equal(result.times, targ.times) assert_same_sub_schema(result, targ) def test__add_signals_with_inconsistent_times_AssertionError(self): signal2 = IrregularlySampledSignal(self.time1quant*2., signal=np.arange(10.0), units="mV") assert_neo_object_is_compliant(signal2) self.assertRaises(ValueError, self.signal1.__add__, signal2) def test__add_signals_with_inconsistent_dimension_ValueError(self): signal2 = np.arange(20).reshape(2, 10) self.assertRaises(ValueError, self.signal1.__add__, signal2) def test__subtract_const_should_preserve_data_complement(self): result = self.signal1 - 65*pq.mV self.assertIsInstance(result, IrregularlySampledSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], -56*pq.mV) assert_arrays_equal(result, self.data1 - 65) assert_arrays_equal(result.times, self.time1quant) def test__subtract_from_const_should_return_signal(self): result = 10*pq.mV - self.signal1 self.assertIsInstance(result, IrregularlySampledSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], 1*pq.mV) assert_arrays_equal(result, 10 - self.data1) assert_arrays_equal(result.times, self.time1quant) def test__mult_signal_by_const_float_should_preserve_data_complement(self): result = self.signal1*2. self.assertIsInstance(result, IrregularlySampledSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], 18*pq.mV) assert_arrays_equal(result, self.data1*2) assert_arrays_equal(result.times, self.time1quant) def test__mult_signal_by_const_array_should_preserve_data_complement(self): result = self.signal1*np.array(2.) self.assertIsInstance(result, IrregularlySampledSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], 18*pq.mV) assert_arrays_equal(result, self.data1*2) assert_arrays_equal(result.times, self.time1quant) def test__divide_signal_by_const_should_preserve_data_complement(self): result = self.signal1/0.5 self.assertIsInstance(result, IrregularlySampledSignal) assert_neo_object_is_compliant(result) self.assertEqual(result.name, 'spam') self.assertEqual(result.description, 'eggs') self.assertEqual(result.file_origin, 'testfile.txt') self.assertEqual(result.annotations, {'arg1': 'test'}) self.assertEqual(self.signal1[9], 9*pq.mV) self.assertEqual(result[9], 18*pq.mV) assert_arrays_equal(result, self.data1/0.5) assert_arrays_equal(result.times, self.time1quant) class TestIrregularlySampledSignalEquality(unittest.TestCase): def test__signals_with_different_times_should_be_not_equal(self): signal1 = IrregularlySampledSignal(np.arange(10.0)/100*pq.s, np.arange(10.0), units="mV") signal2 = IrregularlySampledSignal(np.arange(10.0)/100*pq.ms, np.arange(10.0), units="mV") self.assertNotEqual(signal1, signal2) if __name__ == "__main__": unittest.main() neo-0.3.3/neo/description.py0000644000175000017500000001457712273723542017045 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ This file is a bundle of utilities to describe Neo object representation (attributes and relationships). It can be used to: * generate diagrams of neo * some generics IO like (databases mapper, hdf5, neomatlab, ...) * tests * external SQL mappers (Cf OpenElectrophy, Gnode) **classes_necessary_attributes** This dict descibes attributes that are necessary to initialize an instance. It a dict of list of tuples. Each attribute is described by a tuple: * for standard type, the tuple is: (name + python type) * for np.ndarray type, the tuple is: (name + np.ndarray + ndim + dtype) * for pq.Quantities, the tuple is: (name + pq.Quantity + ndim) ndim is the dimensionaly of the array: 1=vector, 2=matrix, 3=cube, ... Special case: ndim=0 means that neo expects a scalar, so Quantity.shape=(1,). That is in fact a vector (ndim=1) with one element only in Quantities package. For some neo.object, the data is not held by a field, but by the object itself. This is the case for AnalogSignal, SpikeTrain: they inherit from Quantity, which itself inherits from numpy.array. In theses cases, the classes_inheriting_quantities dict provide a list of classes inhiriting Quantity and there attribute that will become Quantity itself. **classes_recommended_attributes** This dict describes recommended attributes, which are optional at initialization. If present, they will be stored as attributes of the object. The notation is the same as classes_necessary_attributes. """ from datetime import datetime import numpy as np import quantities as pq from neo.core import objectlist class_by_name = {} name_by_class = {} for ob in objectlist: class_by_name[ob.__name__] = ob name_by_class[ob] = ob.__name__ # parent to children one_to_many_relationship = { 'Block': ['Segment', 'RecordingChannelGroup'], 'Segment': ['AnalogSignal', 'AnalogSignalArray', 'IrregularlySampledSignal', 'Event', 'EventArray', 'Epoch', 'EpochArray', 'SpikeTrain', 'Spike'], 'RecordingChannel': ['AnalogSignal', 'IrregularlySampledSignal'], 'RecordingChannelGroup': ['Unit', 'AnalogSignalArray'], 'Unit': ['SpikeTrain', 'Spike'] } # reverse: child to parent many_to_one_relationship = {} for p, children in one_to_many_relationship.items(): for c in children: if c not in many_to_one_relationship: many_to_one_relationship[c] = [] if p not in many_to_one_relationship[c]: many_to_one_relationship[c].append(p) many_to_many_relationship = { 'RecordingChannel': ['RecordingChannelGroup'], 'RecordingChannelGroup': ['RecordingChannel'], } # check bijectivity for p, children in many_to_many_relationship.items(): for c in children: if c not in many_to_many_relationship: many_to_many_relationship[c] = [] if p not in many_to_many_relationship[c]: many_to_many_relationship[c].append(p) # Some relationship shortcuts are accesible througth properties property_relationship = { 'Block': ['Unit', 'RecordingChannel'] } # these relationships are used by IOs which do not natively support non-tree # structures like NEO to avoid object duplications when saving/retrieving # objects from the data source. We can call em "secondary" connections implicit_relationship = { 'RecordingChannel': ['AnalogSignal', 'IrregularlySampledSignal'], 'RecordingChannelGroup': ['AnalogSignalArray'], 'Unit': ['SpikeTrain', 'Spike'] } classes_necessary_attributes = { 'Block': [], 'Segment': [], 'Event': [('time', pq.Quantity, 0), ('label', str)], 'EventArray': [('times', pq.Quantity, 1), ('labels', np.ndarray, 1, np.dtype('S'))], 'Epoch': [('time', pq.Quantity, 0), ('duration', pq.Quantity, 0), ('label', str)], 'EpochArray': [('times', pq.Quantity, 1), ('durations', pq.Quantity, 1), ('labels', np.ndarray, 1, np.dtype('S'))], 'Unit': [], 'SpikeTrain': [('times', pq.Quantity, 1), ('t_start', pq.Quantity, 0), ('t_stop', pq.Quantity, 0)], 'Spike': [('time', pq.Quantity, 0)], 'AnalogSignal': [('signal', pq.Quantity, 1), ('sampling_rate', pq.Quantity, 0), ('t_start', pq.Quantity, 0)], 'AnalogSignalArray': [('signal', pq.Quantity, 2), ('sampling_rate', pq.Quantity, 0), ('t_start', pq.Quantity, 0)], 'IrregularlySampledSignal': [('times', pq.Quantity, 1), ('signal', pq.Quantity, 1)], 'RecordingChannelGroup': [], 'RecordingChannel': [('index', int)], } classes_recommended_attributes = { 'Block': [('file_datetime', datetime), ('rec_datetime', datetime), ('index', int), ], 'Segment': [('file_datetime', datetime), ('rec_datetime', datetime), ('index', int)], 'Event': [], 'EventArray': [], 'Epoch': [], 'EpochArray': [], 'Unit': [], 'SpikeTrain': [('waveforms', pq.Quantity, 3), ('left_sweep', pq.Quantity, 0), ('sampling_rate', pq.Quantity, 0)], 'Spike': [('waveform', pq.Quantity, 2), ('left_sweep', pq.Quantity, 0), ('sampling_rate', pq.Quantity, 0)], 'AnalogSignal': [('channel_index', int)], 'Unit': [('channel_indexes', np.ndarray, 1, np.dtype('i'))], 'AnalogSignalArray': [('channel_indexes', np.ndarray, 1, np.dtype('i'))], 'IrregularlySampledSignal': [], 'RecordingChannelGroup': [('channel_indexes', np.ndarray, 1, np.dtype('i')), ('channel_names', np.ndarray, 1, np.dtype('S'))], 'RecordingChannel': [('coordinate', pq.Quantity, 1)], } # this list classes inheriting quantities with arguments that will become # the quantity array classes_inheriting_quantities = { 'SpikeTrain': 'times', 'AnalogSignal': 'signal', 'AnalogSignalArray': 'signal', 'IrregularlySampledSignal': 'signal', } # all classes can have name, description, file_origin for k in classes_recommended_attributes.keys(): classes_recommended_attributes[k] += [('name', str), ('description', str), ('file_origin', str)] neo-0.3.3/neo/core/0000755000175000017500000000000012273723667015072 5ustar sgarciasgarcia00000000000000neo-0.3.3/neo/core/recordingchannel.py0000644000175000017500000001032612273723542020743 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`RecordingChannel`, a container for recordings coming from a single data channel. :class:`RecordingChannel` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function from neo.core.baseneo import BaseNeo class RecordingChannel(BaseNeo): ''' A container for recordings coming from a single data channel. A :class:`RecordingChannel` is a container for :class:`AnalogSignal` and :class:`IrregularlySampledSignal`objects that come from the same logical and/or physical channel inside a :class:`Block`. Note that a :class:`RecordingChannel` can belong to several :class:`RecordingChannelGroup` objects. *Usage* one :class:`Block` with 3 :class:`Segment` objects, 16 :class:`RecordingChannel` objects, and 48 :class:`AnalogSignal` objects:: >>> from neo.core import (Block, Segment, RecordingChannelGroup, ... RecordingChannel, AnalogSignal) >>> from quantities import mA, Hz >>> import numpy as np >>> >>> # Create a Block ... blk = Block() >>> >>> # Create a new RecordingChannelGroup and add it to the Block ... rcg = RecordingChannelGroup(name='all channels') >>> blk.recordingchannelgroups.append(rcg) >>> >>> # Create 3 Segment and 16 RecordingChannel objects and add them to ... # the Block ... for ind in range(3): ... seg = Segment(name='segment %d' % ind, index=ind) ... blk.segments.append(seg) ... >>> for ind in range(16): ... chan = RecordingChannel(index=ind) ... rcg.recordingchannels.append(chan) # <- many to many ... # relationship ... chan.recordingchannelgroups.append(rcg) # <- many to many ... # relationship ... >>> # Populate the Block with AnalogSignal objects ... for seg in blk.segments: ... for chan in rcg.recordingchannels: ... sig = AnalogSignal(np.random.rand(100000)*mA, ... sampling_rate=20*Hz) ... seg.analogsignals.append(sig) ... chan.analogsignals.append(sig) *Required attributes/properties*: :index: (int) You can use this to order :class:`RecordingChannel` objects in an way you want. *Recommended attributes/properties*: :name: (str) A label for the dataset. :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. :coordinate: (quantity array 1D (x, y, z)) Coordinates of the channel in the brain. Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. *Container of*: :class:`AnalogSignal` :class:`IrregularlySampledSignal` ''' def __init__(self, index=0, coordinate=None, name=None, description=None, file_origin=None, **annotations): ''' Initialize a new :class:`RecordingChannel` instance. ''' # Inherited initialization # Sets universally recommended attributes, and places all others # in annotations BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) # Store required and recommended attributes self.index = index self.coordinate = coordinate # Initialize contianers self.analogsignals = [] self.irregularlysampledsignals = [] # Many to many relationship self.recordingchannelgroups = [] def merge(self, other): ''' Merge the contents of another RecordingChannel into this one. Objects from the other RecordingChannel will be added to this one. ''' for container in ("analogsignals", "irregularlysampledsignals"): getattr(self, container).extend(getattr(other, container)) # TODO: merge annotations neo-0.3.3/neo/core/unit.py0000644000175000017500000000616312273723542016421 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`Unit`, a container of :class:`Spike` and :class:`SpikeTrain` objects from a unit. :class:`Unit` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function from neo.core.baseneo import BaseNeo class Unit(BaseNeo): ''' A container of :class:`Spike` and :class:`SpikeTrain` objects from a unit. A :class:`Unit` regroups all the :class:`SpikeTrain` and :class:`Spike` 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 may come from different :class:`Segment` objects within the :class:`Block`, so this object is not contained in the usual :class:`Block`/ :class:`Segment`/:class:`SpikeTrain` hierarchy. A :class:`Unit` is linked to :class:`RecordingChannelGroup` objects from which it was detected. With tetrodes, for instance, multiple channels may record the same :class:`Unit`. *Usage*:: >>> from neo.core import Unit, SpikeTrain >>> >>> unit = Unit(name='pyramidal neuron') >>> >>> train0 = SpikeTrain(times=[.01, 3.3, 9.3], units='sec', t_stop=10) >>> unit.spiketrains.append(train0) >>> >>> train1 = SpikeTrain(times=[100.01, 103.3, 109.3], units='sec', ... t_stop=110) >>> unit.spiketrains.append(train1) *Required attributes/properties*: None *Recommended attributes/properties*: :name: (str) A label for the dataset. :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. :channel_index: (numpy array 1D dtype='i') You can use this to order :class:`Unit` objects in an way you want. It can have any number of elements. :class:`AnalogSignal` and :class:`AnalogSignalArray` objects can be given indexes as well so related objects can be linked together. Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. *Container of*: :class:`SpikeTrain` :class:`Spike` ''' def __init__(self, name=None, description=None, file_origin=None, channel_indexes=None, **annotations): ''' Initialize a new :clas:`Unit` instance (spike source) ''' BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) self.channel_indexes = channel_indexes self.spiketrains = [] self.spikes = [] self.recordingchannelgroup = None def merge(self, other): ''' Merge the contents of another :class:`Unit` into this one. Child objects of the other :class:`Unit` will be added to this one. ''' for container in ("spikes", "spiketrains"): getattr(self, container).extend(getattr(other, container)) # TODO: merge annotations neo-0.3.3/neo/core/spike.py0000644000175000017500000000715012273723542016552 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`Spike`, a single spike with an optional waveform. :class:`Spike` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import quantities as pq from neo.core.baseneo import BaseNeo class Spike(BaseNeo): ''' A single spike. Object to represent one spike emitted by a :class:`Unit` and represented by its time occurence and optional waveform. *Usage*:: >>> from quantities import s >>> spk = Spike(3*s) >>> spk.time array(3.0) * s *Required attributes/properties*: :time: (quantity) The time of the spike. *Recommended attributes/properties*: :name: (str) A label for the dataset. :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. :waveforms: (quantity array 2D (channel_index, time)) The waveform of the spike. :sampling_rate: (quantity scalar) Number of samples per unit time for the waveform. :left_sweep: (quantity scalar) Time from the beginning of the waveform to the trigger time of the spike. Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. *Properties available on this object*: :sampling_period: (quantity scalar) Interval between two samples. (1/:attr:`sampling_rate`) :duration: (quantity scalar) Duration of :attr:`waveform`, read-only. (:attr:`waveform`.shape[1] * :attr:`sampling_period`) :right_sweep: (quantity scalar) Time from the trigger time of the spike to the end of the :attr:`waveform`, read-only. (:attr:`left_sweep` + :attr:`duration`) ''' def __init__(self, time=0 * pq.s, waveform=None, sampling_rate=None, left_sweep=None, name=None, description=None, file_origin=None, **annotations): ''' Initialize a new :class:`Spike` instance. ''' BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) self.time = time self.waveform = waveform self.left_sweep = left_sweep self.sampling_rate = sampling_rate self.segment = None self.unit = None @property def duration(self): ''' Returns the duration of the :attr:`waveform`. (:attr:`waveform`.shape[1] * :attr:`sampling_period`) ''' if self.waveform is None or self.sampling_rate is None: return None return self.waveform.shape[1] / self.sampling_rate @property def right_sweep(self): ''' Time from the trigger time of the spike to the end of the :attr:`waveform`. (:attr:`left_sweep` + :attr:`duration`) ''' dur = self.duration if dur is None or self.left_sweep is None: return None return self.left_sweep + dur @property def sampling_period(self): ''' Interval between two samples. (1/:attr:`sampling_rate`) ''' if self.sampling_rate is None: return None return 1.0 / self.sampling_rate @sampling_period.setter def sampling_period(self, period): ''' Setter for :attr:`sampling_period`. ''' if period is None: self.sampling_rate = None else: self.sampling_rate = 1.0 / period neo-0.3.3/neo/core/epocharray.py0000644000175000017500000001047012273723542017573 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`EpochArray`, an array of epochs. Introduced for performance reasons. :class:`EpochArray` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import sys import numpy as np import quantities as pq from neo.core.baseneo import BaseNeo, merge_annotations PY_VER = sys.version_info[0] class EpochArray(BaseNeo): ''' Array of epochs. Introduced for performance reason. An :class:`EpochArray` is prefered to a list of :class:`Epoch` objects. *Usage*:: >>> from neo.core import EpochArray >>> from quantities import s, ms >>> import numpy as np >>> >>> epcarr = EpochArray(times=np.arange(0, 30, 10)*s, ... durations=[10, 5, 7]*ms, ... labels=np.array(['btn0', 'btn1', 'btn2'], ... dtype='S')) >>> >>> epcarr.times array([ 0., 10., 20.]) * s >>> epcarr.durations array([ 10., 5., 7.]) * ms >>> epcarr.labels array(['btn0', 'btn1', 'btn2'], dtype='|S4') *Required attributes/properties*: :times: (quantity array 1D) The starts of the time periods. :durations: (quantity array 1D) The length of the time period. :labels: (numpy.array 1D dtype='S') Names or labels for the time periods. *Recommended attributes/properties*: :name: (str) A label for the dataset, :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`, ''' def __init__(self, times=None, durations=None, labels=None, name=None, description=None, file_origin=None, **annotations): ''' Initialize a new :class:`EpochArray` instance. ''' BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) if times is None: times = np.array([]) * pq.s if durations is None: durations = np.array([]) * pq.s if labels is None: labels = np.array([], dtype='S') self.times = times self.durations = durations self.labels = labels self.segment = None def __repr__(self): ''' Returns a string representing the :class:`EpochArray`. ''' # need to convert labels to unicode for python 3 or repr is messed up if PY_VER == 3: labels = self.labels.astype('U') else: labels = self.labels objs = ['%s@%s for %s' % (label, time, dur) for label, time, dur in zip(labels, self.times, self.durations)] return '' % ', '.join(objs) def merge(self, other): ''' Merge the another :class:`EpochArray` into this one. The :class:`EpochArray` objects are concatenated horizontally (column-wise), :func:`np.hstack`). If the attributes of the two :class:`EpochArray` are not compatible, and Exception is raised. ''' othertimes = other.times.rescale(self.times.units) otherdurations = other.durations.rescale(self.durations.units) times = np.hstack([self.times, othertimes]) * self.times.units durations = np.hstack([self.durations, otherdurations]) * self.durations.units labels = np.hstack([self.labels, other.labels]) kwargs = {} for name in ("name", "description", "file_origin"): attr_self = getattr(self, name) attr_other = getattr(other, name) if attr_self == attr_other: kwargs[name] = attr_self else: kwargs[name] = "merge(%s, %s)" % (attr_self, attr_other) merged_annotations = merge_annotations(self.annotations, other.annotations) kwargs.update(merged_annotations) return EpochArray(times=times, durations=durations, labels=labels, **kwargs) neo-0.3.3/neo/core/block.py0000644000175000017500000001373712273723542016541 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`Block`, the main container gathering all the data, whether discrete or continous, for a given recording session. base class used by all :module:`neo.core` classes. :class:`Block` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function from neo.core.baseneo import BaseNeo class Block(BaseNeo): ''' Main container for data. Main container gathering all the data, whether discrete or continous, for a given recording session. A block is not necessarily temporally homogeneous, in contrast to Segment. *Usage*:: >>> from neo.core import (Block, Segment, RecordingChannelGroup, ... AnalogSignalArray) >>> from quantities import nA, kHz >>> import numpy as np >>> >>> # create a Block with 3 Segment and 2 RecordingChannelGroup 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): ... rcg = RecordingChannelGroup(name='Array probe %d' % ind, ... channel_indexes=np.arange(64)) ... blk.recordingchannelgroups.append(rcg) ... >>> # Populate the Block with AnalogSignalArray objects ... for seg in blk.segments: ... for rcg in blk.recordingchannelgroups: ... a = AnalogSignalArray(np.random.randn(10000, 64)*nA, ... sampling_rate=10*kHz) ... rcg.analogsignalarrays.append(a) ... seg.analogsignalarrays.append(a) *Required attributes/properties*: None *Recommended attributes/properties*: :name: (str) A label for the dataset. :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. :file_datetime: (datetime) The creation date and time of the original data file. :rec_datetime: (datetime) The date and time of the original recording. :index: (int) You can use this to define an ordering of your Block. It is not used by Neo in any way. *Properties available on this object*: :list_units: 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. :list_recordingchannels: descends through hierarchy and returns a list of :class:`RecordingChannel` objects existing in the block. Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. *Container of*: :class:`Segment` :class:`RecordingChannelGroup` ''' def __init__(self, name=None, description=None, file_origin=None, file_datetime=None, rec_datetime=None, index=None, **annotations): ''' Initalize a new :class:`Block` instance. ''' BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) self.file_datetime = file_datetime self.rec_datetime = rec_datetime self.index = index self.segments = [] self.recordingchannelgroups = [] @property def list_units(self): ''' Return a list of all :class:`Unit` objects in the :class:`Block`. ''' units = [] for rcg in self.recordingchannelgroups: for unit in rcg.units: if unit not in units: units.append(unit) return units @property def list_recordingchannels(self): ''' Return a list of all :class:`RecordingChannel` objects in the :class:`Block`. ''' all_rc = [] for rcg in self.recordingchannelgroups: for rc in rcg.recordingchannels: if rc not in all_rc: all_rc.append(rc) return all_rc def merge(self, other): ''' Merge the contents of another block into this one. For each :class:`Segment` in the other block, if its name matches that of a :class:`Segment` in this block, the two segments will be merged, otherwise it will be added as a new segment. The equivalent procedure is then applied to each :class:`RecordingChannelGroup`. ''' for container in ("segments", "recordingchannelgroups"): lookup = dict((obj.name, obj) for obj in getattr(self, container)) for obj in getattr(other, container): if obj.name in lookup: lookup[obj.name].merge(obj) else: lookup[obj.name] = obj getattr(self, container).append(obj) # TODO: merge annotations _repr_pretty_attrs_keys_ = [ "name", "description", "annotations", "file_origin", "file_datetime", "rec_datetime", "index"] def _repr_pretty_(self, pp, cycle): ''' Handle pretty-printing the :class:`Block`. ''' pp.text("{0} with {1} segments and {1} groups".format( self.__class__.__name__, len(self.segments), len(self.recordingchannelgroups), )) if self._has_repr_pretty_attrs_(): pp.breakable() self._repr_pretty_attrs_(pp, cycle) if self.segments: pp.breakable() pp.text("# Segments") pp.breakable() for (i, seg) in enumerate(self.segments): if i > 0: pp.breakable() pp.text("{0}: ".format(i)) with pp.indent(3): pp.pretty(seg) neo-0.3.3/neo/core/__init__.py0000644000175000017500000000335612265516260017200 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- """ :mod:`neo.core` provides classes for storing common electrophysiological data types. Some of these classes contain raw data, such as spike trains or analog signals, while others are containers to organize other classes (including both data classes and other container classes). Classes from :mod:`neo.io` return nested data structures containing one or more class from this module. Classes: .. autoclass:: Block .. autoclass:: Segment .. autoclass:: RecordingChannelGroup .. autoclass:: RecordingChannel .. autoclass:: Unit .. autoclass:: AnalogSignal .. autoclass:: AnalogSignalArray .. autoclass:: IrregularlySampledSignal .. autoclass:: Event .. autoclass:: EventArray .. autoclass:: Epoch .. autoclass:: EpochArray .. autoclass:: Spike .. autoclass:: SpikeTrain """ # needed for python 3 compatibility from __future__ import absolute_import, division, print_function from neo.core.block import Block from neo.core.segment import Segment from neo.core.recordingchannelgroup import RecordingChannelGroup from neo.core.recordingchannel import RecordingChannel from neo.core.unit import Unit from neo.core.analogsignal import AnalogSignal from neo.core.analogsignalarray import AnalogSignalArray from neo.core.irregularlysampledsignal import IrregularlySampledSignal from neo.core.event import Event from neo.core.eventarray import EventArray from neo.core.epoch import Epoch from neo.core.epocharray import EpochArray from neo.core.spike import Spike from neo.core.spiketrain import SpikeTrain objectlist = [Block, Segment, RecordingChannelGroup, RecordingChannel, AnalogSignal, AnalogSignalArray, IrregularlySampledSignal, Event, EventArray, Epoch, EpochArray, Unit, Spike, SpikeTrain ] neo-0.3.3/neo/core/baseneo.py0000644000175000017500000001421512273723542017053 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`BaseNeo`, the abstract base class used by all :module:`neo.core` classes. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function from datetime import datetime, date, time, timedelta from decimal import Decimal import logging from numbers import Number import numpy as np ALLOWED_ANNOTATION_TYPES = (int, float, complex, str, bytes, type(None), datetime, date, time, timedelta, Number, Decimal, np.number, np.bool_) # handle both Python 2 and Python 3 try: ALLOWED_ANNOTATION_TYPES += (long, unicode) except NameError: pass try: basestring except NameError: basestring = str logger = logging.getLogger("Neo") def _check_annotations(value): ''' Recursively check that value is either of a "simple" type (number, string, date/time) or is a (possibly nested) dict, list or numpy array containing only simple types. ''' if isinstance(value, np.ndarray): if not issubclass(value.dtype.type, ALLOWED_ANNOTATION_TYPES): raise ValueError("Invalid annotation. NumPy arrays with dtype %s" "are not allowed" % value.dtype.type) elif isinstance(value, dict): for element in value.values(): _check_annotations(element) elif isinstance(value, (list, tuple)): for element in value: _check_annotations(element) elif not isinstance(value, ALLOWED_ANNOTATION_TYPES): raise ValueError("Invalid annotation. Annotations of type %s are not" "allowed" % type(value)) def merge_annotation(a, b): ''' First attempt at a policy for merging annotations (intended for use with parallel computations using MPI). This policy needs to be discussed further, or we could allow the user to specify a policy. Current policy: for arrays: concatenate the two arrays otherwise: fail if the annotations are not equal ''' assert type(a) == type(b) if isinstance(a, dict): return merge_annotations(a, b) elif isinstance(a, np.ndarray): # concatenate b to a return np.append(a, b) elif isinstance(a, basestring): if a == b: return a else: return a + ";" + b else: assert a == b return a def merge_annotations(A, B): ''' Merge two sets of annotations. ''' merged = {} for name in A: if name in B: merged[name] = merge_annotation(A[name], B[name]) else: merged[name] = A[name] for name in B: if name not in merged: merged[name] = B[name] logger.debug("Merging annotations: A=%s B=%s merged=%s", A, B, merged) return merged class BaseNeo(object): '''This is the base class from which all Neo objects inherit. This class implements support for universally recommended arguments, and also sets up the :attr:`annotations` dict for additional arguments. The following "universal" methods are available: :__init__: Grabs the universally recommended arguments :attr:`name`, :attr:`file_origin`, and :attr:`description` and stores them as attributes. Also takes every additional argument (that is, every argument that is not handled by :class:`BaseNeo` or the child class), and puts in the dict :attr:`annotations`. :annotate(**args): Updates :attr:`annotations` with keyword/value pairs. Each child class should: 0) call BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) with the universal recommended arguments, plus optional annotations 1) process its required arguments in its __new__ or __init__ method 2) process its non-universal recommended arguments (in its __new__ or __init__ method Non-keyword arguments should only be used for required arguments. The required and recommended arguments for each child class (Neo object) are specified in :module:`neo.description` and the documentation for the child object. ''' def __init__(self, name=None, file_origin=None, description=None, **annotations): ''' This is the base constructor for all Neo objects. Stores universally recommended attributes and creates :attr:`annotations` from additional arguments not processed by :class:`BaseNeo` or the child class. ''' # create `annotations` for additional arguments _check_annotations(annotations) self.annotations = annotations # these attributes are recommended for all objects. self.name = name self.description = description self.file_origin = file_origin def annotate(self, **annotations): ''' Add annotations (non-standardized metadata) to a Neo object. Example: >>> obj.annotate(key1=value0, key2=value1) >>> obj.key2 value2 ''' _check_annotations(annotations) self.annotations.update(annotations) _repr_pretty_attrs_keys_ = ["name", "description", "annotations"] def _has_repr_pretty_attrs_(self): return any(getattr(self, k) for k in self._repr_pretty_attrs_keys_) def _repr_pretty_attrs_(self, pp, cycle): first = True for key in self._repr_pretty_attrs_keys_: value = getattr(self, key) if value: if first: first = False else: pp.breakable() with pp.group(indent=1): pp.text("{0}: ".format(key)) pp.pretty(value) def _repr_pretty_(self, pp, cycle): ''' Handle pretty-printing the :class:`BaseNeo`. ''' pp.text(self.__class__.__name__) if self._has_repr_pretty_attrs_(): pp.breakable() self._repr_pretty_attrs_(pp, cycle) neo-0.3.3/neo/core/irregularlysampledsignal.py0000644000175000017500000003215312273723542022545 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module implements :class:`IrregularlySampledSignal` an analog signal with samples taken at arbitrary time points. :class:`IrregularlySampledSignal` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`, and from :class:`quantites.Quantity`, which inherits from :class:`numpy.array`. Inheritance from :class:`numpy.array` is explained here: http://docs.scipy.org/doc/numpy/user/basics.subclassing.html In brief: * Initialization of a new object from constructor happens in :meth:`__new__`. This is where user-specified attributes are set. * :meth:`__array_finalize__` is called for all new objects, including those created by slicing. This is where attributes are copied over from the old object. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import numpy as np import quantities as pq from neo.core.baseneo import BaseNeo class IrregularlySampledSignal(BaseNeo, pq.Quantity): ''' An analog signal with samples taken at arbitrary time points. A representation of a continuous, analog signal acquired at time :attr:`t_start` with a varying sampling interval. *Usage*:: >>> from neo.core import IrregularlySampledSignal >>> from quantities import s, nA >>> >>> irsig0 = IrregularlySampledSignal([0.0, 1.23, 6.78], [1, 2, 3], ... units='mV', time_units='ms') >>> irsig1 = IrregularlySampledSignal([0.01, 0.03, 0.12]*s, ... [4, 5, 6]*nA) *Required attributes/properties*: :times: (quantity array 1D, numpy array 1D, or list) The data itself. :signal: (quantity array 1D, numpy array 1D, or list) The time of each data point. Must have the same size as :attr:`times`. :units: (quantity units) Required if the signal is a list or NumPy array, not if it is a :class:`Quantity`. :time_units: (quantity units) Required if :attr`times` is a list or NumPy array, not if it is a :class:`Quantity`. *Recommended attributes/properties*:. :name: (str) A label for the dataset :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. *Optional attributes/properties*: :dtype: (numpy dtype or str) Override the dtype of the signal array. (times are always floats). :copy: (bool) True by default. Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. *Properties available on this object*: :sampling_intervals: (quantity array 1D) Interval between each adjacent pair of samples. (:attr:`times[1:]` - :attr:`times`[:-1]) :duration: (quantity scalar) Signal duration, read-only. (:attr:`times`[-1] - :attr:`times`[0]) :t_start: (quantity scalar) Time when signal begins, read-only. (:attr:`times`[0]) :t_stop: (quantity scalar) Time when signal ends, read-only. (:attr:`times`[-1]) *Slicing*: :class:`IrregularlySampledSignal` objects can be sliced. When this occurs, a new :class:`IrregularlySampledSignal` (actually a view) is returned, with the same metadata, except that :attr:`times` is also sliced in the same way. *Operations available on this object*: == != + * / ''' def __new__(cls, times, signal, units=None, time_units=None, dtype=None, copy=True, name=None, description=None, file_origin=None, **annotations): ''' Construct a new :class:`IrregularlySampledSignal` instance. This is called whenever a new :class:`IrregularlySampledSignal` is created from the constructor, but not when slicing. ''' if len(times) != len(signal): raise ValueError("times array and signal array must " + "have same length") if units is None: if hasattr(signal, "units"): units = signal.units else: raise ValueError("Units must be specified") elif isinstance(signal, pq.Quantity): # could improve this test, what if units is a string? if units != signal.units: signal = signal.rescale(units) if time_units is None: if hasattr(times, "units"): time_units = times.units else: raise ValueError("Time units must be specified") elif isinstance(times, pq.Quantity): # could improve this test, what if units is a string? if time_units != times.units: times = times.rescale(time_units) # should check time units have correct dimensions obj = pq.Quantity.__new__(cls, signal, units=units, dtype=dtype, copy=copy) obj.times = pq.Quantity(times, units=time_units, dtype=float, copy=copy) obj.segment = None obj.recordingchannel = None return obj def __init__(self, times, signal, units=None, time_units=None, dtype=None, copy=True, name=None, description=None, file_origin=None, **annotations): ''' Initializes a newly constructed :class:`IrregularlySampledSignal` instance. ''' BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) def __array_finalize__(self, obj): ''' This is called every time a new :class:`IrregularlySampledSignal` is created. It is the appropriate place to set default values for attributes for :class:`IrregularlySampledSignal` constructed by slicing or viewing. User-specified values are only relevant for construction from constructor, and these are set in __new__. Then they are just copied over here. ''' super(IrregularlySampledSignal, self).__array_finalize__(obj) self.times = getattr(obj, 'times', None) # The additional arguments self.annotations = getattr(obj, 'annotations', None) # Globally recommended attributes self.name = getattr(obj, 'name', None) self.file_origin = getattr(obj, 'file_origin', None) self.description = getattr(obj, 'description', None) def __repr__(self): ''' Returns a string representing the :class:`IrregularlySampledSignal`. ''' return '<%s(%s at times %s)>' % (self.__class__.__name__, super(IrregularlySampledSignal, self).__repr__(), self.times) def __getslice__(self, i, j): ''' Get a slice from :attr:`i` to :attr:`j`. Doesn't get called in Python 3, :meth:`__getitem__` is called instead ''' obj = super(IrregularlySampledSignal, self).__getslice__(i, j) obj.times = self.times.__getslice__(i, j) return obj def __getitem__(self, i): ''' Get the item or slice :attr:`i`. ''' obj = super(IrregularlySampledSignal, self).__getitem__(i) if isinstance(obj, IrregularlySampledSignal): obj.times = self.times.__getitem__(i) return obj @property def duration(self): ''' Signal duration. (:attr:`times`[-1] - :attr:`times`[0]) ''' return self.times[-1] - self.times[0] @property def t_start(self): ''' Time when signal begins. (:attr:`times`[0]) ''' return self.times[0] @property def t_stop(self): ''' Time when signal ends. (:attr:`times`[-1]) ''' return self.times[-1] def __eq__(self, other): ''' Equality test (==) ''' return (super(IrregularlySampledSignal, self).__eq__(other).all() and (self.times == other.times).all()) def __ne__(self, other): ''' Non-equality test (!=) ''' return not self.__eq__(other) def _apply_operator(self, other, op, *args): ''' Handle copying metadata to the new :class:`IrregularlySampledSignal` after a mathematical operation. ''' self._check_consistency(other) f = getattr(super(IrregularlySampledSignal, self), op) new_signal = f(other, *args) new_signal._copy_data_complement(self) return new_signal def _check_consistency(self, other): ''' Check if the attributes of another :class:`IrregularlySampledSignal` are compatible with this one. ''' # if not an array, then allow the calculation if not hasattr(other, 'ndim'): return # if a scalar array, then allow the calculation if not other.ndim: return # dimensionality should match if self.ndim != other.ndim: raise ValueError('Dimensionality does not match: %s vs %s' % (self.ndim, other.ndim)) # if if the other array does not have a times property, # then it should be okay to add it directly if not hasattr(other, 'times'): return # if there is a times property, the times need to be the same if not (self.times == other.times).all(): raise ValueError('Times do not match: %s vs %s' % (self.times, other.times)) def _copy_data_complement(self, other): ''' Copy the metadata from another :class:`IrregularlySampledSignal`. ''' for attr in ("times", "name", "file_origin", "description", "channel_index", "annotations"): setattr(self, attr, getattr(other, attr, None)) def __add__(self, other, *args): ''' Addition (+) ''' return self._apply_operator(other, "__add__", *args) def __sub__(self, other, *args): ''' Subtraction (-) ''' return self._apply_operator(other, "__sub__", *args) def __mul__(self, other, *args): ''' Multiplication (*) ''' return self._apply_operator(other, "__mul__", *args) def __truediv__(self, other, *args): ''' Float division (/) ''' return self._apply_operator(other, "__truediv__", *args) def __div__(self, other, *args): ''' Integer division (//) ''' return self._apply_operator(other, "__div__", *args) __radd__ = __add__ __rmul__ = __sub__ def __rsub__(self, other, *args): ''' Backwards subtraction (other-self) ''' return self.__mul__(-1) + other @property def sampling_intervals(self): ''' Interval between each adjacent pair of samples. (:attr:`times[1:]` - :attr:`times`[:-1]) ''' return self.times[1:] - self.times[:-1] def mean(self, interpolation=None): ''' Calculates the mean, optionally using interpolation between sampling times. If :attr:`interpolation` is None, we assume that values change stepwise at sampling times. ''' if interpolation is None: return (self[:-1]*self.sampling_intervals).sum()/self.duration else: raise NotImplementedError def resample(self, at=None, interpolation=None): ''' Resample the signal, returning either an :class:`AnalogSignal` object or another :class:`IrregularlySampledSignal` object. Arguments: :at: either a :class:`Quantity` array containing the times at which samples should be created (times must be within the signal duration, there is no extrapolation), a sampling rate with dimensions (1/Time) or a sampling interval with dimensions (Time). :interpolation: one of: None, 'linear' ''' # further interpolation methods could be added raise NotImplementedError def rescale(self, units): ''' Return a copy of the :class:`IrregularlySampledSignal` converted to the specified units ''' to_dims = pq.quantity.validate_dimensionality(units) if self.dimensionality == to_dims: to_u = self.units signal = np.array(self) else: to_u = pq.Quantity(1.0, to_dims) from_u = pq.Quantity(1.0, self.dimensionality) try: cf = pq.quantity.get_conversion_factor(from_u, to_u) except AssertionError: raise ValueError('Unable to convert between units of "%s" \ and "%s"' % (from_u._dimensionality, to_u._dimensionality)) signal = cf * self.magnitude new = self.__class__(times=self.times, signal=signal, units=to_u) new._copy_data_complement(self) new.annotations.update(self.annotations) return new neo-0.3.3/neo/core/analogsignalarray.py0000644000175000017500000002572012273723542021140 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module implements :class:`AnalogSignalArray`, an array of analog signals. :class:`AnalogSignalArray` derives from :class:`BaseAnalogSignal`, from :module:`neo.core.analogsignal`. :class:`BaseAnalogSignal` inherits from :class:`quantites.Quantity`, which inherits from :class:`numpy.array`. Inheritance from :class:`numpy.array` is explained here: http://docs.scipy.org/doc/numpy/user/basics.subclassing.html In brief: * Initialization of a new object from constructor happens in :meth:`__new__`. This is where user-specified attributes are set. * :meth:`__array_finalize__` is called for all new objects, including those created by slicing. This is where attributes are copied over from the old object. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import logging import numpy as np import quantities as pq from neo.core.analogsignal import (BaseAnalogSignal, AnalogSignal, _get_sampling_rate) from neo.core.baseneo import BaseNeo, merge_annotations logger = logging.getLogger("Neo") class AnalogSignalArray(BaseAnalogSignal): ''' Several continuous analog signals A representation of several continuous, analog signals that have the same duration, sampling rate and start time. Basically, it is a 2D array like AnalogSignal: dim 0 is time, dim 1 is channel index Inherits from :class:`quantities.Quantity`, which in turn inherits from :class:`numpy.ndarray`. *Usage*:: >>> from neo.core import AnalogSignalArray >>> import quantities as pq >>> >>> sigarr = AnalogSignalArray([[1, 2, 3], [4, 5, 6]], units='V', ... sampling_rate=1*pq.Hz) >>> >>> sigarr >>> sigarr[:,1] >>> sigarr[1, 1] array(5) * V *Required attributes/properties*: :signal: (quantity array 2D, numpy array 2D, or list (data, chanel)) The data itself. :units: (quantity units) Required if the signal is a list or NumPy array, not if it is a :class:`Quantity` :t_start: (quantity scalar) Time when signal begins :sampling_rate: *or* :sampling_period: (quantity scalar) Number of samples per unit time or interval between two samples. If both are specified, they are checked for consistency. *Recommended attributes/properties*: :name: (str) A label for the dataset. :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. :channel_index: (numpy array 1D dtype='i') You can use this to order the columns of the signal in any way you want. It should have the same number of elements as the signal has columns. :class:`AnalogSignal` and :class:`Unit` objects can be given indexes as well so related objects can be linked together. *Optional attributes/properties*: :dtype: (numpy dtype or str) Override the dtype of the signal array. :copy: (bool) True by default. Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. *Properties available on this object*: :sampling_rate: (quantity scalar) Number of samples per unit time. (1/:attr:`sampling_period`) :sampling_period: (quantity scalar) Interval between two samples. (1/:attr:`quantity scalar`) :duration: (Quantity) Signal duration, read-only. (size * :attr:`sampling_period`) :t_stop: (quantity scalar) Time when signal ends, read-only. (:attr:`t_start` + :attr:`duration`) :times: (quantity 1D) The time points of each sample of the signal, read-only. (:attr:`t_start` + arange(:attr:`shape`[0])/:attr:`sampling_rate`) :channel_indexes: (numpy array 1D dtype='i') The same as :attr:`channel_index`, read-only. *Slicing*: :class:`AnalogSignalArray` objects can be sliced. When taking a single row (dimension 1, e.g. [:, 0]), a :class:`AnalogSignal` is returned. When taking a single element, a :class:`~quantities.Quantity` is returned. Otherwise a :class:`AnalogSignalArray` (actually a view) is returned, with the same metadata, except that :attr:`t_start` is changed if the start index along dimension 1 is greater than 1. Getting a single item returns a :class:`~quantity.Quantity` scalar. *Operations available on this object*: == != + * / ''' def __new__(cls, signal, units=None, dtype=None, copy=True, t_start=0 * pq.s, sampling_rate=None, sampling_period=None, name=None, file_origin=None, description=None, channel_index=None, **annotations): ''' Constructs new :class:`AnalogSignalArray` from data. This is called whenever a new class:`AnalogSignalArray` is created from the constructor, but not when slicing. ''' if (isinstance(signal, pq.Quantity) and units is not None and units != signal.units): signal = signal.rescale(units) if not units and hasattr(signal, "units"): units = signal.units obj = pq.Quantity.__new__(cls, signal, units=units, dtype=dtype, copy=copy) obj.t_start = t_start obj.sampling_rate = _get_sampling_rate(sampling_rate, sampling_period) obj.channel_index = channel_index obj.segment = None obj.recordingchannelgroup = None return obj def __init__(self, signal, units=None, dtype=None, copy=True, t_start=0 * pq.s, sampling_rate=None, sampling_period=None, name=None, file_origin=None, description=None, channel_index=None, **annotations): ''' Initializes a newly constructed :class:`AnalogSignalArray` instance. ''' BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) @property def channel_indexes(self): ''' The same as :attr:`channel_index`. ''' return self.channel_index def __getslice__(self, i, j): ''' Get a slice from :attr:`i` to :attr:`j`. Doesn't get called in Python 3, :meth:`__getitem__` is called instead ''' return self.__getitem__(slice(i, j)) def __getitem__(self, i): ''' Get the item or slice :attr:`i`. ''' obj = super(BaseAnalogSignal, self).__getitem__(i) if isinstance(i, int): return obj elif isinstance(i, tuple): j, k = i if isinstance(k, int): if isinstance(j, slice): # extract an AnalogSignal obj = AnalogSignal(obj, sampling_rate=self.sampling_rate) if j.start: obj.t_start = (self.t_start + j.start * self.sampling_period) # return a Quantity (for some reason quantities does not # return a Quantity in this case) elif isinstance(j, int): obj = pq.Quantity(obj, units=self.units) return obj elif isinstance(j, int): # extract a quantity array # should be a better way to do this obj = pq.Quantity(np.array(obj), units=obj.units) return obj else: return obj elif isinstance(i, slice): if i.start: obj.t_start = self.t_start + i.start * self.sampling_period return obj else: raise IndexError("index should be an integer, tuple or slice") def time_slice(self, t_start, t_stop): ''' Creates a new AnalogSignal corresponding to the time slice of the original AnalogSignal between times t_start, t_stop. Note, that for numerical stability reasons if t_start, t_stop do not fall exactly on the time bins defined by the sampling_period they will be rounded to the nearest sampling bins. ''' t_start = t_start.rescale(self.sampling_period.units) t_stop = t_stop.rescale(self.sampling_period.units) i = (t_start - self.t_start) / self.sampling_period j = (t_stop - self.t_start) / self.sampling_period i = int(np.rint(i.magnitude)) j = int(np.rint(j.magnitude)) if (i < 0) or (j > len(self)): raise ValueError('t_start, t_stop have to be withing the analog \ signal duration') # we're going to send the list of indicies so that we get *copy* of the # sliced data obj = super(BaseAnalogSignal, self).__getitem__(np.arange(i, j, 1)) obj.t_start = self.t_start + i * self.sampling_period return obj def merge(self, other): ''' Merge the another :class:`AnalogSignalArray` into this one. The :class:`AnalogSignalArray` objects are concatenated horizontally (column-wise, :func:`np.hstack`). If the attributes of the two :class:`AnalogSignalArray` are not compatible, and Exception is raised. ''' assert self.sampling_rate == other.sampling_rate assert self.t_start == other.t_start other.units = self.units stack = np.hstack(map(np.array, (self, other))) kwargs = {} for name in ("name", "description", "file_origin"): attr_self = getattr(self, name) attr_other = getattr(other, name) if attr_self == attr_other: kwargs[name] = attr_self else: kwargs[name] = "merge(%s, %s)" % (attr_self, attr_other) if self.channel_index is None: channel_index = other.channel_index elif other.channel_index is None: channel_index = self.channel_index else: channel_index = np.append(self.channel_index, other.channel_index) merged_annotations = merge_annotations(self.annotations, other.annotations) kwargs.update(merged_annotations) return AnalogSignalArray(stack, units=self.units, dtype=self.dtype, copy=False, t_start=self.t_start, sampling_rate=self.sampling_rate, channel_index=channel_index, **kwargs) neo-0.3.3/neo/core/epoch.py0000644000175000017500000000403412273723542016533 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`Epoch`, a period of time with a start point and duration. :class:`Epoch` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function from neo.core.baseneo import BaseNeo class Epoch(BaseNeo): ''' A period of time with a start point and duration. Similar to :class:`Event` but with a duration. Useful for describing a period, the state of a subject, etc. *Usage*:: >>> from neo.core import Epoch >>> from quantities import s, ms >>> >>> epc = Epoch(time=50*s, duration=200*ms, label='button pressed') >>> >>> epc.time array(50.0) * s >>> epc.duration array(200.0) * ms >>> epc.label 'button pressed' *Required attributes/properties*: :time: (quantity scalar) The start of the time period. :duration: (quantity scalar) The length of the time period. :label: (str) A name or label for the time period. *Recommended attributes/properties*: :name: (str) A label for the dataset. :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`. ''' def __init__(self, time, duration, label, name=None, description=None, file_origin=None, **annotations): ''' Initialize a new :class:`Epoch` instance. ''' BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) self.time = time self.duration = duration self.label = label self.segment = None def merge(self, other): ''' Merging is not supported in :class:`Epoch`. ''' raise NotImplementedError('Cannot merge Epoch objects') neo-0.3.3/neo/core/eventarray.py0000644000175000017500000000737212273723542017625 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`EventArray`, an array of events. Introduced for performance reasons. :class:`EventArray` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import sys import numpy as np import quantities as pq from neo.core.baseneo import BaseNeo, merge_annotations PY_VER = sys.version_info[0] class EventArray(BaseNeo): ''' Array of events. Introduced for performance reasons. An :class:`EventArray` is prefered to a list of :class:`Event` objects. *Usage*:: >>> from neo.core import EventArray >>> from quantities import s >>> import numpy as np >>> >>> evtarr = EventArray(np.arange(0, 30, 10)*s, ... labels=np.array(['trig0', 'trig1', 'trig2'], ... dtype='S')) >>> >>> evtarr.times array([ 0., 10., 20.]) * s >>> evtarr.labels array(['trig0', 'trig1', 'trig2'], dtype='|S5') *Required attributes/properties*: :times: (quantity array 1D) The time of the events. :labels: (numpy.array 1D dtype='S') Names or labels for the events. *Recommended attributes/properties*: :name: (str) A label for the dataset. :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`. ''' def __init__(self, times=None, labels=None, name=None, description=None, file_origin=None, **annotations): ''' Initialize a new :class:`EventArray` instance. ''' BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) if times is None: times = np.array([]) * pq.s if labels is None: labels = np.array([], dtype='S') self.times = times self.labels = labels self.segment = None def __repr__(self): ''' Returns a string representing the :class:`EventArray`. ''' # need to convert labels to unicode for python 3 or repr is messed up if PY_VER == 3: labels = self.labels.astype('U') else: labels = self.labels objs = ['%s@%s' % (label, time) for label, time in zip(labels, self.times)] return '' % ', '.join(objs) def merge(self, other): ''' Merge the another :class:`EventArray` into this one. The :class:`EventArray` objects are concatenated horizontally (column-wise), :func:`np.hstack`). If the attributes of the two :class:`EventArray` are not compatible, and Exception is raised. ''' othertimes = other.times.rescale(self.times.units) times = np.hstack([self.times, othertimes]) * self.times.units labels = np.hstack([self.labels, other.labels]) kwargs = {} for name in ("name", "description", "file_origin"): attr_self = getattr(self, name) attr_other = getattr(other, name) if attr_self == attr_other: kwargs[name] = attr_self else: kwargs[name] = "merge(%s, %s)" % (attr_self, attr_other) merged_annotations = merge_annotations(self.annotations, other.annotations) kwargs.update(merged_annotations) return EventArray(times=times, labels=labels, **kwargs) neo-0.3.3/neo/core/analogsignal.py0000644000175000017500000004773112273723542020107 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module implements objects relating to analog signals, :class:`BaseAnalogSignal` and its child :class:`AnalogSignal`. :class:`AnalogSignalArray` is derived from :class:`BaseAnalogSignal` but is defined in :module:`neo.core.analogsignalarray`. :class:`IrregularlySampledSignal` is not derived from :class:`BaseAnalogSignal` and is defined in :module:`neo.core.irregularlysampledsignal`. :class:`BaseAnalogSignal` inherits from :class:`quantites.Quantity`, which inherits from :class:`numpy.array`. Inheritance from :class:`numpy.array` is explained here: http://docs.scipy.org/doc/numpy/user/basics.subclassing.html In brief: * Initialization of a new object from constructor happens in :meth:`__new__`. This is where user-specified attributes are set. * :meth:`__array_finalize__` is called for all new objects, including those created by slicing. This is where attributes are copied over from the old object. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import numpy as np import quantities as pq from neo.core.baseneo import BaseNeo def _get_sampling_rate(sampling_rate, sampling_period): ''' Gets the sampling_rate from either the sampling_period or the sampling_rate, or makes sure they match if both are specified ''' if sampling_period is None: if sampling_rate is None: raise ValueError("You must provide either the sampling rate or " + "sampling period") elif sampling_rate is None: sampling_rate = 1.0 / sampling_period elif sampling_period != 1.0 / sampling_rate: raise ValueError('The sampling_rate has to be 1/sampling_period') if not hasattr(sampling_rate, 'units'): raise TypeError("Sampling rate/sampling period must have units") return sampling_rate def _new_BaseAnalogSignal(cls, signal, units=None, dtype=None, copy=True, t_start=0*pq.s, sampling_rate=None, sampling_period=None, name=None, file_origin=None, description=None, channel_index=None, annotations=None): ''' A function to map BaseAnalogSignal.__new__ to function that does not do the unit checking. This is needed for pickle to work. ''' return cls(signal=signal, units=units, dtype=dtype, copy=copy, t_start=t_start, sampling_rate=sampling_rate, sampling_period=sampling_period, name=name, file_origin=file_origin, description=description, channel_index=channel_index, **annotations) class BaseAnalogSignal(BaseNeo, pq.Quantity): ''' Base class for AnalogSignal and AnalogSignalArray ''' def __new__(cls, signal, units=None, dtype=None, copy=True, t_start=0 * pq.s, sampling_rate=None, sampling_period=None, name=None, file_origin=None, description=None, channel_index=None, **annotations): ''' Constructs new :class:`BaseAnalogSignal` from data. This is called whenever a new class:`BaseAnalogSignal` is created from the constructor, but not when slicing. __array_finalize__ is called on the new object. ''' if units is None: if hasattr(signal, "units"): units = signal.units else: raise ValueError("Units must be specified") elif isinstance(signal, pq.Quantity): # could improve this test, what if units is a string? if units != signal.units: signal = signal.rescale(units) obj = pq.Quantity.__new__(cls, signal, units=units, dtype=dtype, copy=copy) if t_start is None: raise ValueError('t_start cannot be None') obj._t_start = t_start obj._sampling_rate = _get_sampling_rate(sampling_rate, sampling_period) obj.channel_index = channel_index obj.segment = None obj.recordingchannel = None return obj def __init__(self, signal, units=None, dtype=None, copy=True, t_start=0 * pq.s, sampling_rate=None, sampling_period=None, name=None, file_origin=None, description=None, channel_index=None, **annotations): ''' Initializes a newly constructed :class:`BaseAnalogSignal` instance. ''' # This method is only called when constructing a new BaseAnalogSignal, # not when slicing or viewing. We use the same call signature # as __new__ for documentation purposes. Anything not in the call # signature is stored in annotations. # Calls parent __init__, which grabs universally recommended # attributes and sets up self.annotations BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) def __reduce__(self): ''' Map the __new__ function onto _new_BaseAnalogSignal, so that pickle works ''' return _new_BaseAnalogSignal, (self.__class__, np.array(self), self.units, self.dtype, True, self.t_start, self.sampling_rate, self.sampling_period, self.name, self.file_origin, self.description, self.channel_index, self.annotations) def __array_finalize__(self, obj): ''' This is called every time a new :class:`BaseAnalogSignal` is created. It is the appropriate place to set default values for attributes for :class:`BaseAnalogSignal` constructed by slicing or viewing. User-specified values are only relevant for construction from constructor, and these are set in __new__. Then they are just copied over here. ''' super(BaseAnalogSignal, self).__array_finalize__(obj) self._t_start = getattr(obj, '_t_start', 0 * pq.s) self._sampling_rate = getattr(obj, '_sampling_rate', None) # The additional arguments self.annotations = getattr(obj, 'annotations', None) # Globally recommended attributes self.name = getattr(obj, 'name', None) self.file_origin = getattr(obj, 'file_origin', None) self.description = getattr(obj, 'description', None) self.channel_index = getattr(obj, 'channel_index', None) def __repr__(self): ''' Returns a string representing the :class:`BaseAnalogSignal`. ''' return ('<%s(%s, [%s, %s], sampling rate: %s)>' % (self.__class__.__name__, super(BaseAnalogSignal, self).__repr__(), self.t_start, self.t_stop, self.sampling_rate)) def __getslice__(self, i, j): ''' Get a slice from :attr:`i` to :attr:`j`. Doesn't get called in Python 3, :meth:`__getitem__` is called instead ''' obj = super(BaseAnalogSignal, self).__getslice__(i, j) obj.t_start = self.t_start + i * self.sampling_period return obj def __getitem__(self, i): ''' Get the item or slice :attr:`i`. ''' obj = super(BaseAnalogSignal, self).__getitem__(i) if isinstance(obj, BaseAnalogSignal): # update t_start and sampling_rate slice_start = None slice_step = None if isinstance(i, slice): slice_start = i.start slice_step = i.step elif isinstance(i, tuple) and len(i) == 2: slice_start = i[0].start slice_step = i[0].step if slice_start: obj.t_start = self.t_start + slice_start * self.sampling_period if slice_step: obj.sampling_period *= slice_step return obj # sampling_rate attribute is handled as a property so type checking can # be done @property def sampling_rate(self): ''' Number of samples per unit time. (1/:attr:`sampling_period`) ''' return self._sampling_rate @sampling_rate.setter def sampling_rate(self, rate): ''' Setter for :attr:`sampling_rate` ''' if rate is None: raise ValueError('sampling_rate cannot be None') elif not hasattr(rate, 'units'): raise ValueError('sampling_rate must have units') self._sampling_rate = rate # sampling_period attribute is handled as a property on underlying rate @property def sampling_period(self): ''' Interval between two samples. (1/:attr:`sampling_rate`) ''' return 1. / self.sampling_rate @sampling_period.setter def sampling_period(self, period): ''' Setter for :attr:`sampling_period` ''' if period is None: raise ValueError('sampling_period cannot be None') elif not hasattr(period, 'units'): raise ValueError('sampling_period must have units') self.sampling_rate = 1. / period # t_start attribute is handled as a property so type checking can be done @property def t_start(self): ''' Time when signal begins. ''' return self._t_start @t_start.setter def t_start(self, start): ''' Setter for :attr:`t_start` ''' if start is None: raise ValueError('t_start cannot be None') self._t_start = start @property def duration(self): ''' Signal duration (:attr:`size` * :attr:`sampling_period`) ''' return self.shape[0] / self.sampling_rate @property def t_stop(self): ''' Time when signal ends. (:attr:`t_start` + :attr:`duration`) ''' return self.t_start + self.duration @property def times(self): ''' The time points of each sample of the signal (:attr:`t_start` + arange(:attr:`shape`)/:attr:`sampling_rate`) ''' return self.t_start + np.arange(self.shape[0]) / self.sampling_rate def rescale(self, units): ''' Return a copy of the AnalogSignal(Array) converted to the specified units ''' to_dims = pq.quantity.validate_dimensionality(units) if self.dimensionality == to_dims: to_u = self.units signal = np.array(self) else: to_u = pq.Quantity(1.0, to_dims) from_u = pq.Quantity(1.0, self.dimensionality) try: cf = pq.quantity.get_conversion_factor(from_u, to_u) except AssertionError: raise ValueError('Unable to convert between units of "%s" \ and "%s"' % (from_u._dimensionality, to_u._dimensionality)) signal = cf * self.magnitude new = self.__class__(signal=signal, units=to_u, sampling_rate=self.sampling_rate) new._copy_data_complement(self) new.annotations.update(self.annotations) return new def duplicate_with_new_array(self, signal): ''' Create a new :class:`BaseAnalogSignal` with the same metadata but different data ''' #signal is the new signal new = self.__class__(signal=signal, units=self.units, sampling_rate=self.sampling_rate) new._copy_data_complement(self) new.annotations.update(self.annotations) return new def __eq__(self, other): ''' Equality test (==) ''' if (self.t_start != other.t_start or self.sampling_rate != other.sampling_rate): return False return super(BaseAnalogSignal, self).__eq__(other) def __ne__(self, other): ''' Non-equality test (!=) ''' return not self.__eq__(other) def _check_consistency(self, other): ''' Check if the attributes of another :class:`BaseAnalogSignal` are compatible with this one. ''' if isinstance(other, BaseAnalogSignal): for attr in "t_start", "sampling_rate": if getattr(self, attr) != getattr(other, attr): raise ValueError("Inconsistent values of %s" % attr) # how to handle name and annotations? def _copy_data_complement(self, other): ''' Copy the metadata from another :class:`BaseAnalogSignal`. ''' for attr in ("t_start", "sampling_rate", "name", "file_origin", "description", "channel_index", "annotations"): setattr(self, attr, getattr(other, attr, None)) def _apply_operator(self, other, op, *args): ''' Handle copying metadata to the new :class:`BaseAnalogSignal` after a mathematical operation. ''' self._check_consistency(other) f = getattr(super(BaseAnalogSignal, self), op) new_signal = f(other, *args) new_signal._copy_data_complement(self) return new_signal def __add__(self, other, *args): ''' Addition (+) ''' return self._apply_operator(other, "__add__", *args) def __sub__(self, other, *args): ''' Subtraction (-) ''' return self._apply_operator(other, "__sub__", *args) def __mul__(self, other, *args): ''' Multiplication (*) ''' return self._apply_operator(other, "__mul__", *args) def __truediv__(self, other, *args): ''' Float division (/) ''' return self._apply_operator(other, "__truediv__", *args) def __div__(self, other, *args): ''' Integer division (//) ''' return self._apply_operator(other, "__div__", *args) __radd__ = __add__ __rmul__ = __sub__ def __rsub__(self, other, *args): ''' Backwards subtraction (other-self) ''' return self.__mul__(-1, *args) + other def _repr_pretty_(self, pp, cycle): ''' Handle pretty-printing the :class:`BaseAnalogSignal`. ''' pp.text(" ".join([self.__class__.__name__, "in", str(self.units), "with", "x".join(map(str, self.shape)), str(self.dtype), "values", ])) if self._has_repr_pretty_attrs_(): pp.breakable() self._repr_pretty_attrs_(pp, cycle) def _pp(line): pp.breakable() with pp.group(indent=1): pp.text(line) if hasattr(self, "channel_index"): _pp("channel index: {0}".format(self.channel_index)) for line in ["sampling rate: {0}".format(self.sampling_rate), "time: {0} to {1}".format(self.t_start, self.t_stop) ]: _pp(line) class AnalogSignal(BaseAnalogSignal): ''' A continuous analog signal. A representation of a continuous, analog signal acquired at time :attr:`t_start` at a certain sampling rate. Inherits from :class:`quantities.Quantity`, which in turn inherits from :class:`numpy.ndarray`. *Usage*:: >>> from neo.core import AnalogSignal >>> from quantities import kHz, ms, nA, s, uV >>> import numpy as np >>> >>> sig0 = AnalogSignal([1, 2, 3], sampling_rate=0.42*kHz, ... units='mV') >>> sig1 = AnalogSignal([4, 5, 6]*nA, sampling_period=42*ms) >>> sig2 = AnalogSignal(np.array([1.0, 2.0, 3.0]), t_start=42*ms, ... sampling_rate=0.42*kHz, units=uV) >>> sig3 = AnalogSignal([1], units='V', day='Monday', ... sampling_period=1*s) >>> >>> sig3 >>> sig3.annotations['day'] 'Monday' >>> sig3[0] array(1) * V >>> sig3[::2] *Required attributes/properties*: :signal: (quantity array 1D, numpy array 1D, or list) The data itself. :units: (quantity units) Required if the signal is a list or NumPy array, not if it is a :class:`Quantity` :sampling_rate: *or* :sampling_period: (quantity scalar) Number of samples per unit time or interval between two samples. If both are specified, they are checked for consistency. *Recommended attributes/properties*: :name: (str) A label for the dataset. :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. :t_start: (quantity scalar) Time when signal begins. Default: 0.0 seconds :channel_index: (int) You can use this to order :class:`AnalogSignal` objects in an way you want. :class:`AnalogSignalArray` and :class:`Unit` objects can be given indexes as well so related objects can be linked together. *Optional attributes/properties*: :dtype: (numpy dtype or str) Override the dtype of the signal array. :copy: (bool) True by default. Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. *Properties available on this object*: :sampling_rate: (quantity scalar) Number of samples per unit time. (1/:attr:`sampling_period`) :sampling_period: (quantity scalar) Interval between two samples. (1/:attr:`sampling_rate`) :duration: (quantity scalar) Signal duration, read-only. (:attr:`size` * :attr:`sampling_period`) :t_stop: (quantity scalar) Time when signal ends, read-only. (:attr:`t_start` + :attr:`duration`) :times: (quantity 1D) The time points of each sample of the signal, read-only. (:attr:`t_start` + arange(:attr:`shape`)/:attr:`sampling_rate`) *Slicing*: :class:`AnalogSignal` objects can be sliced. When this occurs, a new :class:`AnalogSignal` (actually a view) is returned, with the same metadata, except that :attr:`sampling_period` is changed if the step size is greater than 1, and :attr:`t_start` is changed if the start index is greater than 0. Getting a single item returns a :class:`~quantity.Quantity` scalar. *Operations available on this object*: == != + * / ''' def __new__(cls, signal, units=None, dtype=None, copy=True, t_start=0*pq.s, sampling_rate=None, sampling_period=None, name=None, file_origin=None, description=None, channel_index=None, **annotations): ''' Constructs new :class:`AnalogSignal` from data. This is called whenever a new class:`AnalogSignal` is created from the constructor, but not when slicing. ''' obj = BaseAnalogSignal.__new__(cls, signal, units, dtype, copy, t_start, sampling_rate, sampling_period, name, file_origin, description, channel_index, **annotations) return obj def merge(self, other): ''' Merging is not supported in :class:`AnalogSignal`. ''' raise NotImplementedError('Cannot merge AnalogSignal objects') neo-0.3.3/neo/core/recordingchannelgroup.py0000644000175000017500000001721512273723542022024 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`RecordingChannelGroup`, a container for multiple data channels. :class:`RecordingChannelGroup` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import numpy as np from neo.core.baseneo import BaseNeo class RecordingChannelGroup(BaseNeo): ''' A container for multiple data channels. This container have sereval purpose: * Grouping all :class:`AnalogSignalArray` inside a :class:`Block` across :class:`Segment` * Grouping :class:`RecordingChannel` inside a :class:`Block`. This case is *many to many* relation. It mean that a :class:`RecordingChannel` can belong to several group. A typical use case is tetrode (4 X :class:`RecordingChannel` inside a :class:`RecordingChannelGroup`). * Container of :class:`Unit`. A neuron decharge (:class:`Unit`) can be seen by several electrodes (4 in tetrode case). *Usage 1* multi :class:`Segment` recording with 2 electrode array:: >>> from neo.core import (Block, Segment, RecordingChannelGroup, ... AnalogSignalArray) >>> from quantities import nA, kHz >>> import numpy as np >>> >>> # create a Block with 3 Segment and 2 RecordingChannelGroup 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): ... rcg = RecordingChannelGroup(name='Array probe %d' % ind, ... channel_indexes=np.arange(64)) ... blk.recordingchannelgroups.append(rcg) ... >>> # Populate the Block with AnalogSignalArray objects ... for seg in blk.segments: ... for rcg in blk.recordingchannelgroups: ... a = AnalogSignalArray(np.random.randn(10000, 64)*nA, ... sampling_rate=10*kHz) ... rcg.analogsignalarrays.append(a) ... seg.analogsignalarrays.append(a) *Usage 2* grouping channel:: >>> from neo.core import (Block, RecordingChannelGroup, ... RecordingChannel) >>> import numpy as np >>> >>> # Create a Block ,,, blk = Block() >>> >>> # Create a new RecordingChannelGroup and add it to the Block ... rcg = RecordingChannelGroup(channel_names=np.array(['ch0', ... 'ch1', ... 'ch2'])) >>> rcg.channel_indexes = np.array([0, 1, 2]) >>> blk.recordingchannelgroups.append(rcg) >>> >>> # Create 3 RecordingChannel objects and add them to the Block ... for ind in range(3): ... chan = RecordingChannel(index=ind) ... rcg.recordingchannels.append(chan) # <- many to many ,,, # relationship ... chan.recordingchannelgroups.append(rcg) # <- many to many ... # relationship *Usage 3* dealing with :class:`Unit` objects:: >>> from neo.core import Block, RecordingChannelGroup, Unit >>> >>> # Create a Block ... blk = Block() >>> >>> # Create a new RecordingChannelGroup and add it to the Block ... rcg = RecordingChannelGroup(name='octotrode A') >>> blk.recordingchannelgroups.append(rcg) >>> >>> # create several Unit objects and add them to the >>> # RecordingChannelGroup ... for ind in range(5): ... unit = Unit(name = 'unit %d' % ind, description= ... 'after a long and hard spike sorting') ... rcg.units.append(unit) *Required attributes/properties*: None *Recommended attributes/properties*: :name: (str) A label for the dataset. :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. :channel_names: (numpy.array 1D dtype='S') Names for each :class:`RecordingChannel`. :channel_indexes: (numpy.array 1D dtype='i') Index of each :class:`RecordingChannel`. Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. *Container of*: :class:`RecordingChannel` :class:`AnalogSignalArray` :class:`Unit` ''' def __init__(self, channel_names=None, channel_indexes=None, name=None, description=None, file_origin=None, **annotations): ''' Initialize a new :class:`RecordingChannelGroup` instance. ''' # Inherited initialization # Sets universally recommended attributes, and places all others # in annotations BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) # Defaults if channel_indexes is None: channel_indexes = np.array([], dtype=np.int) if channel_names is None: channel_names = np.array([], dtype='S') # Store recommended attributes self.channel_names = channel_names self.channel_indexes = channel_indexes # Initialize containers for child objects self.analogsignalarrays = [] self.units = [] # Many to many relationship self.recordingchannels = [] self.block = None def merge(self, other): ''' Merge the contents of another RecordingChannelGroup into this one. For each :class:`RecordingChannel` in the other RecordingChannelGroup, if its name matches that of a :class:`RecordingChannel` in this block, the two RecordingChannels will be merged, otherwise it will be added as a new RecordingChannel. The equivalent procedure is then applied to each :class:`Unit`. For each array-type object in the other :class:`RecordingChannelGroup`, if its name matches that of an object of the same type in this segment, the two arrays will be joined by concatenation. ''' for container in ("recordingchannels", "units"): lookup = dict((obj.name, obj) for obj in getattr(self, container)) for obj in getattr(other, container): if obj.name in lookup: lookup[obj.name].merge(obj) else: getattr(self, container).append(obj) for container in ("analogsignalarrays",): objs = getattr(self, container) lookup = dict((obj.name, i) for i, obj in enumerate(objs)) for obj in getattr(other, container): if obj.name in lookup: ind = lookup[obj.name] try: newobj = getattr(self, container)[ind].merge(obj) except AttributeError as e: raise AttributeError("%s. container=%s, obj.name=%s, \ shape=%s" % (e, container, obj.name, obj.shape)) getattr(self, container)[ind] = newobj else: lookup[obj.name] = obj getattr(self, container).append(obj) # TODO: merge annotations neo-0.3.3/neo/core/spiketrain.py0000644000175000017500000004647312273723542017623 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module implements :class:`SpikeTrain`, an array of spike times. :class:`SpikeTrain` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`, and from :class:`quantites.Quantity`, which inherits from :class:`numpy.array`. Inheritance from :class:`numpy.array` is explained here: http://docs.scipy.org/doc/numpy/user/basics.subclassing.html In brief: * Initialization of a new object from constructor happens in :meth:`__new__`. This is where user-specified attributes are set. * :meth:`__array_finalize__` is called for all new objects, including those created by slicing. This is where attributes are copied over from the old object. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import numpy as np import quantities as pq from neo.core.baseneo import BaseNeo def check_has_dimensions_time(*values): ''' Verify that all arguments have a dimensionality that is compatible with time. ''' errmsgs = [] for value in values: dim = value.dimensionality if (len(dim) != 1 or list(dim.values())[0] != 1 or not isinstance(list(dim.keys())[0], pq.UnitTime)): errmsgs.append("value %s has dimensions %s, not [time]" % (value, dim.simplified)) if errmsgs: raise ValueError("\n".join(errmsgs)) def _check_time_in_range(value, t_start, t_stop, view=False): ''' Verify that all times in :attr:`value` are between :attr:`t_start` and :attr:`t_stop` (inclusive. If :attr:`view` is True, vies are used for the test. Using drastically increases the speed, but is only safe if you are certain that the dtype and units are the same ''' if not value.size: return if view: value = value.view(np.ndarray) t_start = t_start.view(np.ndarray) t_stop = t_stop.view(np.ndarray) if value.min() < t_start: raise ValueError("The first spike (%s) is before t_start (%s)" % (value, t_start)) if value.max() > t_stop: raise ValueError("The last spike (%s) is after t_stop (%s)" % (value, t_stop)) def _new_spiketrain(cls, signal, t_stop, units=None, dtype=None, copy=True, sampling_rate=1.0 * pq.Hz, t_start=0.0 * pq.s, waveforms=None, left_sweep=None, name=None, file_origin=None, description=None, annotations=None): ''' A function to map :meth:`BaseAnalogSignal.__new__` to function that does not do the unit checking. This is needed for :module:`pickle` to work. ''' if annotations is None: annotations = {} return SpikeTrain(signal, t_stop, units, dtype, copy, sampling_rate, t_start, waveforms, left_sweep, name, file_origin, description, **annotations) class SpikeTrain(BaseNeo, pq.Quantity): ''' :class:`SpikeTrain` is a :class:`Quantity` array of spike times. It is an ensemble of action potentials (spikes) emitted by the same unit in a period of time. *Usage*:: >>> from neo.core import SpikeTrain >>> from quantities import s >>> >>> train = SpikeTrain([3, 4, 5]*s, t_stop=10.0) >>> train2 = train[1:3] >>> >>> train.t_start array(0.0) * s >>> train.t_stop array(10.0) * s >>> train >>> train2 *Required attributes/properties*: :times: (quantity array 1D, numpy array 1D, or list) The times of each spike. :units: (quantity units) Required if :attr:`times` is a list or :class:`~numpy.ndarray`, not if it is a :class:`~quantites.Quantity`. :t_stop: (quantity scalar, numpy scalar, or float) Time at which :class:`SpikeTrain` ended. This will be converted to the same units as :attr:`times`. This argument is required because it specifies the period of time over which spikes could have occurred. Note that :attr:`t_start` is highly recommended for the same reason. Note: If :attr:`times` contains values outside of the range [t_start, t_stop], an Exception is raised. *Recommended attributes/properties*: :name: (str) A label for the dataset. :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. :t_start: (quantity scalar, numpy scalar, or float) Time at which :class:`SpikeTrain` began. This will be converted to the same units as :attr:`times`. Default: 0.0 seconds. :waveforms: (quantity array 3D (spike, channel_index, time)) The waveforms of each spike. :sampling_rate: (quantity scalar) Number of samples per unit time for the waveforms. :left_sweep: (quantity array 1D) Time from the beginning of the waveform to the trigger time of the spike. :sort: (bool) If True, the spike train will be sorted by time. *Optional attributes/properties*: :dtype: (numpy dtype or str) Override the dtype of the signal array. :copy: (bool) Whether to copy the times array. True by default. Must be True when you request a change of units or dtype. Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. *Properties available on this object*: :sampling_period: (quantity scalar) Interval between two samples. (1/:attr:`sampling_rate`) :duration: (quantity scalar) Duration over which spikes can occur, read-only. (:attr:`t_stop` - :attr:`t_start`) :spike_duration: (quantity scalar) Duration of a waveform, read-only. (:attr:`waveform`.shape[2] * :attr:`sampling_period`) :right_sweep: (quantity scalar) Time from the trigger times of the spikes to the end of the waveforms, read-only. (:attr:`left_sweep` + :attr:`spike_duration`) :times: (:class:`SpikeTrain`) Returns the :class:`SpikeTrain` without modification or copying. *Slicing*: :class:`SpikeTrain` objects can be sliced. When this occurs, a new :class:`SpikeTrain` (actually a view) is returned, with the same metadata, except that :attr:`waveforms` is also sliced in the same way (along dimension 0). Note that t_start and t_stop are not changed automatically, although you can still manually change them. ''' def __new__(cls, times, t_stop, units=None, dtype=None, copy=True, sampling_rate=1.0 * pq.Hz, t_start=0.0 * pq.s, waveforms=None, left_sweep=None, name=None, file_origin=None, description=None, **annotations): ''' Constructs a new :clas:`Spiketrain` instance from data. This is called whenever a new :class:`SpikeTrain` is created from the constructor, but not when slicing. ''' # Make sure units are consistent # also get the dimensionality now since it is much faster to feed # that to Quantity rather than a unit if units is None: # No keyword units, so get from `times` try: units = times.units dim = units.dimensionality except AttributeError: raise ValueError('you must specify units') else: if hasattr(units, 'dimensionality'): dim = units.dimensionality else: dim = pq.quantity.validate_dimensionality(units) if (hasattr(times, 'dimensionality') and times.dimensionality.items() != dim.items()): if not copy: raise ValueError("cannot rescale and return view") else: # this is needed because of a bug in python-quantities # see issue # 65 in python-quantities github # remove this if it is fixed times = times.rescale(dim) if dtype is None: dtype = getattr(times, 'dtype', np.float) elif hasattr(times, 'dtype') and times.dtype != dtype: if not copy: raise ValueError("cannot change dtype and return view") # if t_start.dtype or t_stop.dtype != times.dtype != dtype, # _check_time_in_range can have problems, so we set the t_start # and t_stop dtypes to be the same as times before converting them # to dtype below # see ticket #38 if hasattr(t_start, 'dtype') and t_start.dtype != times.dtype: t_start = t_start.astype(times.dtype) if hasattr(t_stop, 'dtype') and t_stop.dtype != times.dtype: t_stop = t_stop.astype(times.dtype) # check to make sure the units are time # this approach is orders of magnitude faster than comparing the # reference dimensionality if (len(dim) != 1 or list(dim.values())[0] != 1 or not isinstance(list(dim.keys())[0], pq.UnitTime)): ValueError("Unit %s has dimensions %s, not [time]" % (units, dim.simplified)) # Construct Quantity from data obj = pq.Quantity.__new__(cls, times, units=dim, dtype=dtype, copy=copy) # if the dtype and units match, just copy the values here instead # of doing the much more epxensive creation of a new Quantity # using items() is orders of magnitude faster if (hasattr(t_start, 'dtype') and t_start.dtype == obj.dtype and hasattr(t_start, 'dimensionality') and t_start.dimensionality.items() == dim.items()): obj.t_start = t_start.copy() else: obj.t_start = pq.Quantity(t_start, units=dim, dtype=dtype) if (hasattr(t_stop, 'dtype') and t_stop.dtype == obj.dtype and hasattr(t_stop, 'dimensionality') and t_stop.dimensionality.items() == dim.items()): obj.t_stop = t_stop.copy() else: obj.t_stop = pq.Quantity(t_stop, units=dim, dtype=dtype) # Store attributes obj.waveforms = waveforms obj.left_sweep = left_sweep obj.sampling_rate = sampling_rate # parents obj.segment = None obj.unit = None # Error checking (do earlier?) _check_time_in_range(obj, obj.t_start, obj.t_stop, view=True) return obj def __init__(self, times, t_stop, units=None, dtype=np.float, copy=True, sampling_rate=1.0 * pq.Hz, t_start=0.0 * pq.s, waveforms=None, left_sweep=None, name=None, file_origin=None, description=None, **annotations): ''' Initializes a newly constructed :class:`SpikeTrain` instance. ''' # This method is only called when constructing a new SpikeTrain, # not when slicing or viewing. We use the same call signature # as __new__ for documentation purposes. Anything not in the call # signature is stored in annotations. # Calls parent __init__, which grabs universally recommended # attributes and sets up self.annotations BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) def rescale(self, units): ''' Return a copy of the :class:`SpikeTrain` converted to the specified units ''' if self.dimensionality == pq.quantity.validate_dimensionality(units): return self.copy() spikes = self.view(pq.Quantity) return SpikeTrain(times=spikes, t_stop=self.t_stop, units=units, sampling_rate=self.sampling_rate, t_start=self.t_start, waveforms=self.waveforms, left_sweep=self.left_sweep, name=self.name, file_origin=self.file_origin, description=self.description, **self.annotations) def __reduce__(self): ''' Map the __new__ function onto _new_BaseAnalogSignal, so that pickle works ''' import numpy return _new_spiketrain, (self.__class__, numpy.array(self), self.t_stop, self.units, self.dtype, True, self.sampling_rate, self.t_start, self.waveforms, self.left_sweep, self.name, self.file_origin, self.description, self.annotations) def __array_finalize__(self, obj): ''' This is called every time a new :class:`SpikeTrain` is created. It is the appropriate place to set default values for attributes for :class:`SpikeTrain` constructed by slicing or viewing. User-specified values are only relevant for construction from constructor, and these are set in __new__. Then they are just copied over here. Note that the :attr:`waveforms` attibute is not sliced here. Nor is :attr:`t_start` or :attr:`t_stop` modified. ''' # This calls Quantity.__array_finalize__ which deals with # dimensionality super(SpikeTrain, self).__array_finalize__(obj) # Supposedly, during initialization from constructor, obj is supposed # to be None, but this never happens. It must be something to do # with inheritance from Quantity. if obj is None: return # Set all attributes of the new object `self` from the attributes # of `obj`. For instance, when slicing, we want to copy over the # attributes of the original object. self.t_start = getattr(obj, 't_start', None) self.t_stop = getattr(obj, 't_stop', None) self.waveforms = getattr(obj, 'waveforms', None) self.left_sweep = getattr(obj, 'left_sweep', None) self.sampling_rate = getattr(obj, 'sampling_rate', None) self.segment = getattr(obj, 'segment', None) self.unit = getattr(obj, 'unit', None) # The additional arguments self.annotations = getattr(obj, 'annotations', None) # Globally recommended attributes self.name = getattr(obj, 'name', None) self.file_origin = getattr(obj, 'file_origin', None) self.description = getattr(obj, 'description', None) def __repr__(self): ''' Returns a string representing the :class:`SpikeTrain`. ''' return '' % ( super(SpikeTrain, self).__repr__(), self.t_start, self.t_stop) def sort(self): ''' Sorts the :class:`SpikeTrain` and its :attr:`waveforms`, if any, by time. ''' # sort the waveforms by the times sort_indices = np.argsort(self) if self.waveforms is not None and self.waveforms.any(): self.waveforms = self.waveforms[sort_indices] # now sort the times # We have sorted twice, but `self = self[sort_indices]` introduces # a dependency on the slicing functionality of SpikeTrain. super(SpikeTrain, self).sort() def __getslice__(self, i, j): ''' Get a slice from :attr:`i` to :attr:`j`. Doesn't get called in Python 3, :meth:`__getitem__` is called instead ''' # first slice the Quantity array obj = super(SpikeTrain, self).__getslice__(i, j) # somehow this knows to call SpikeTrain.__array_finalize__, though # I'm not sure how. (If you know, please add an explanatory comment # here.) That copies over all of the metadata. # update waveforms if obj.waveforms is not None: obj.waveforms = obj.waveforms[i:j] return obj def __getitem__(self, i): ''' Get the item or slice :attr:`i`. ''' obj = super(SpikeTrain, self).__getitem__(i) if hasattr(obj, 'waveforms') and obj.waveforms is not None: obj.waveforms = obj.waveforms.__getitem__(i) return obj def __setitem__(self, i, value): ''' Set the value the item or slice :attr:`i`. ''' if not hasattr(value, "units"): value = pq.Quantity(value, units=self.units) # or should we be strict: raise ValueError("Setting a value # requires a quantity")? # check for values outside t_start, t_stop _check_time_in_range(value, self.t_start, self.t_stop) super(SpikeTrain, self).__setitem__(i, value) def __setslice__(self, i, j, value): if not hasattr(value, "units"): value = pq.Quantity(value, units=self.units) _check_time_in_range(value, self.t_start, self.t_stop) super(SpikeTrain, self).__setslice__(i, j, value) def time_slice(self, t_start, t_stop): ''' Creates a new :class:`SpikeTrain` corresponding to the time slice of the original :class:`SpikeTrain` between (and including) times :attr:`t_start` and :attr:`t_stop`. Either parameter can also be None to use infinite endpoints for the time interval. ''' _t_start = t_start _t_stop = t_stop if t_start is None: _t_start = -np.inf if t_stop is None: _t_stop = np.inf indices = (self >= _t_start) & (self <= _t_stop) new_st = self[indices] new_st.t_start = max(_t_start, self.t_start) new_st.t_stop = min(_t_stop, self.t_stop) if self.waveforms is not None: new_st.waveforms = self.waveforms[indices] return new_st @property def times(self): ''' Returns the :class:`SpikeTrain` without modification or copying. ''' return self @property def duration(self): ''' Duration over which spikes can occur, (:attr:`t_stop` - :attr:`t_start`) ''' if self.t_stop is None or self.t_start is None: return None return self.t_stop - self.t_start @property def spike_duration(self): ''' Duration of a waveform. (:attr:`waveform`.shape[2] * :attr:`sampling_period`) ''' if self.waveforms is None or self.sampling_rate is None: return None return self.waveforms.shape[2] / self.sampling_rate @property def sampling_period(self): ''' Interval between two samples. (1/:attr:`sampling_rate`) ''' if self.sampling_rate is None: return None return 1.0 / self.sampling_rate @sampling_period.setter def sampling_period(self, period): ''' Setter for :attr:`sampling_period` ''' if period is None: self.sampling_rate = None else: self.sampling_rate = 1.0 / period @property def right_sweep(self): ''' Time from the trigger times of the spikes to the end of the waveforms. (:attr:`left_sweep` + :attr:`spike_duration`) ''' dur = self.spike_duration if self.left_sweep is None or dur is None: return None return self.left_sweep + dur neo-0.3.3/neo/core/segment.py0000644000175000017500000003237312273723542017106 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`Segment`, a container for data sharing a common time basis. :class:`Segment` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function import numpy as np from neo.core.baseneo import BaseNeo class Segment(BaseNeo): ''' A container for data sharing a common time basis. A :class:`Segment` is a heterogeneous container for discrete or continous data sharing a common clock (time basis) but not necessary the same sampling rate, start or end time. *Usage*:: >>> from neo.core import Segment, SpikeTrain, AnalogSignal >>> from quantities import Hz, s >>> >>> seg = Segment(index=5) >>> >>> train0 = SpikeTrain(times=[.01, 3.3, 9.3], units='sec', t_stop=10) >>> seg.spiketrains.append(train0) >>> >>> train1 = SpikeTrain(times=[100.01, 103.3, 109.3], units='sec', ... t_stop=110) >>> seg.spiketrains.append(train1) >>> >>> sig0 = AnalogSignal(signal=[.01, 3.3, 9.3], units='uV', ... sampling_rate=1*Hz) >>> seg.analogsignals.append(sig0) >>> >>> sig1 = AnalogSignal(signal=[100.01, 103.3, 109.3], units='nA', ... sampling_period=.1*s) >>> seg.analogsignals.append(sig1) *Required attributes/properties*: None *Recommended attributes/properties*: :name: (str) A label for the dataset. :description: (str) Text description. :file_origin: (str) Filesystem path or URL of the original data file. :file_datetime: (datetime) The creation date and time of the original data file. :rec_datetime: (datetime) The date and time of the original recording :index: (int) You can use this to define a temporal ordering of your Segment. For instance you could use this for trial numbers. Note: Any other additional arguments are assumed to be user-specific metadata and stored in :attr:`annotations`. *Properties available on this object*: :all_data: (list) A list of all child objects in the :class:`Segment`. *Container of*: :class:`Epoch` :class:`EpochArray` :class:`Event` :class:`EventArray` :class:`AnalogSignal` :class:`AnalogSignalArray` :class:`IrregularlySampledSignal` :class:`Spike` :class:`SpikeTrain` ''' def __init__(self, name=None, description=None, file_origin=None, file_datetime=None, rec_datetime=None, index=None, **annotations): ''' Initialize a new :class:`Segment` instance. ''' BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) self.file_datetime = file_datetime self.rec_datetime = rec_datetime self.index = index self.epochs = [] self.epocharrays = [] self.events = [] self.eventarrays = [] self.analogsignals = [] self.analogsignalarrays = [] self.irregularlysampledsignals = [] self.spikes = [] self.spiketrains = [] self.block = None @property def all_data(self): ''' Returns a list of all child objects in the :class:`Segment`. ''' return sum((self.epochs, self.epocharrays, self.events, self.eventarrays, self.analogsignals, self.analogsignalarrays, self.irregularlysampledsignals, self.spikes, self.spiketrains), []) def filter(self, **kwargs): ''' Return a list of child objects matching *any* of the search terms in either their attributes or annotations. Examples:: >>> segment.filter(name="Vm") ''' results = [] for key, value in kwargs.items(): for obj in self.all_data: if hasattr(obj, key) and getattr(obj, key) == value: results.append(obj) elif key in obj.annotations and obj.annotations[key] == value: results.append(obj) return results def take_spikes_by_unit(self, unit_list=None): ''' Return :class:`Spike` objects in the :class:`Segment` that are also in a :class:`Unit` in the :attr:`unit_list` provided. ''' if unit_list is None: return [] spike_list = [] for spike in self.spikes: if spike.unit in unit_list: spike_list.append(spike) return spike_list def take_spiketrains_by_unit(self, unit_list=None): ''' Return :class:`SpikeTrains` in the :class:`Segment` that are also in a :class:`Unit` in the :attr:`unit_list` provided. ''' if unit_list is None: return [] spiketrain_list = [] for spiketrain in self.spiketrains: if spiketrain.unit in unit_list: spiketrain_list.append(spiketrain) return spiketrain_list def take_analogsignal_by_unit(self, unit_list=None): ''' Return :class:`AnalogSignal` objects in the :class:`Segment` that are have the same :attr:`channel_index` as any of the :class:`Unit: objects in the :attr:`unit_list` provided. ''' if unit_list is None: return [] channel_indexes = [] for unit in unit_list: if unit.channel_indexes is not None: channel_indexes.extend(unit.channel_indexes) return self.take_analogsignal_by_channelindex(channel_indexes) def take_analogsignal_by_channelindex(self, channel_indexes=None): ''' Return :class:`AnalogSignal` objects in the :class:`Segment` that have a :attr:`channel_index` that is in the :attr:`channel_indexes` provided. ''' if channel_indexes is None: return [] anasig_list = [] for anasig in self.analogsignals: if anasig.channel_index in channel_indexes: anasig_list.append(anasig) return anasig_list def take_slice_of_analogsignalarray_by_unit(self, unit_list=None): ''' Return slices of the :class:`AnalogSignalArray` objects in the :class:`Segment` that correspond to a :attr:`channel_index` of any of the :class:`Unit` objects in the :attr:`unit_list` provided. ''' if unit_list is None: return [] indexes = [] for unit in unit_list: if unit.channel_indexes is not None: indexes.extend(unit.channel_indexes) return self.take_slice_of_analogsignalarray_by_channelindex(indexes) def take_slice_of_analogsignalarray_by_channelindex(self, channel_indexes=None): ''' Return slices of the :class:`AnalogSignalArrays` in the :class:`Segment` that correspond to the :attr:`channel_indexes` provided. ''' if channel_indexes is None: return [] sliced_sigarrays = [] for sigarr in self.analogsignalarrays: if sigarr.channel_indexes is not None: ind = np.in1d(sigarr.channel_indexes, channel_indexes) sliced_sigarrays.append(sigarr[:, ind]) return sliced_sigarrays def construct_subsegment_by_unit(self, unit_list=None): ''' Return a new :class:`Segment that contains the :class:`AnalogSignal`, :class:`AnalogSignalArray`, :class:`Spike`:, and :class:`SpikeTrain` objects common to both the current :class:`Segment` and any :class:`Unit` in the :attr:`unit_list` provided. *Example*:: >>> from neo.core import (Segment, Block, Unit, SpikeTrain, ... RecordingChannelGroup) >>> >>> blk = Block() >>> rcg = RecordingChannelGroup(name='group0') >>> blk.recordingchannelgroups = [rcg] >>> >>> for ind in range(5): ... unit = Unit(name='Unit #%s' % ind, channel_index=ind) ... rcg.units.append(unit) ... >>> >>> for ind in range(3): ... seg = Segment(name='Simulation #%s' % ind) ... blk.segments.append(seg) ... for unit in rcg.units: ... train = SpikeTrain([1, 2, 3], units='ms', t_start=0., ... t_stop=10) ... train.unit = unit ... unit.spiketrains.append(train) ... seg.spiketrains.append(train) ... >>> >>> seg0 = blk.segments[-1] >>> seg1 = seg0.construct_subsegment_by_unit(rcg.units[:2]) >>> len(seg0.spiketrains) 5 >>> len(seg1.spiketrains) 2 ''' seg = Segment() seg.analogsignals = self.take_analogsignal_by_unit(unit_list) seg.spikes = self.take_spikes_by_unit(unit_list) seg.spiketrains = self.take_spiketrains_by_unit(unit_list) seg.analogsignalarrays = \ self.take_slice_of_analogsignalarray_by_unit(unit_list) #TODO copy others attributes return seg def merge(self, other): ''' Merge the contents of another :class:`Segment` into this one. For each array-type object in the other :class:`Segment`, if its name matches that of an object of the same type in this :class:`Segment`, the two arrays will be joined by concatenation. Non-array objects will just be added to this segment. ''' for container in ("epochs", "events", "analogsignals", "irregularlysampledsignals", "spikes", "spiketrains"): getattr(self, container).extend(getattr(other, container)) for container in ("epocharrays", "eventarrays", "analogsignalarrays"): objs = getattr(self, container) lookup = dict((obj.name, i) for i, obj in enumerate(objs)) for obj in getattr(other, container): if obj.name in lookup: ind = lookup[obj.name] try: newobj = getattr(self, container)[ind].merge(obj) except AttributeError as e: raise AttributeError("%s. container=%s, obj.name=%s, \ shape=%s" % (e, container, obj.name, obj.shape)) getattr(self, container)[ind] = newobj else: lookup[obj.name] = obj getattr(self, container).append(obj) # TODO: merge annotations def size(self): ''' Get dictionary containing the names of child containers in the current :class:`Segment` as keys and the number of children of that type as values. ''' return dict((name, len(getattr(self, name))) for name in ("epochs", "events", "analogsignals", "irregularlysampledsignals", "spikes", "spiketrains", "epocharrays", "eventarrays", "analogsignalarrays")) def _repr_pretty_(self, pp, cycle): ''' Handle pretty-printing the :class:`Segment`. ''' pp.text(self.__class__.__name__) pp.text(" with ") first = True for (value, readable) in [ (self.analogsignals, "analogs"), (self.analogsignalarrays, "analog arrays"), (self.events, "events"), (self.eventarrays, "event arrays"), (self.epochs, "epochs"), (self.epocharrays, "epoch arrays"), (self.irregularlysampledsignals, "epoch arrays"), (self.spikes, "spikes"), (self.spiketrains, "spike trains"), ]: if value: if first: first = False else: pp.text(", ") pp.text("{0} {1}".format(len(value), readable)) if self._has_repr_pretty_attrs_(): pp.breakable() self._repr_pretty_attrs_(pp, cycle) if self.analogsignals: pp.breakable() pp.text("# Analog signals (N={0})".format(len(self.analogsignals))) for (i, asig) in enumerate(self.analogsignals): pp.breakable() pp.text("{0}: ".format(i)) with pp.indent(3): pp.pretty(asig) if self.analogsignalarrays: pp.breakable() pp.text("# Analog signal arrays (N={0})" .format(len(self.analogsignalarrays))) for i, asarr in enumerate(self.analogsignalarrays): pp.breakable() pp.text("{0}: ".format(i)) with pp.indent(3): pp.pretty(asarr) neo-0.3.3/neo/core/event.py0000644000175000017500000000340612273723542016560 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- ''' This module defines :class:`Event`, an event occuring at a particular point in time. :class:`Event` derives from :class:`BaseNeo`, from :module:`neo.core.baseneo`. ''' # needed for python 3 compatibility from __future__ import absolute_import, division, print_function from neo.core.baseneo import BaseNeo class Event(BaseNeo): ''' An event occuring at a particular point in time. Useful for managing trigger, stimulus, comment, etc. *Usage*:: >>> from neo.core import Event >>> from quantities import s >>> >>> evt = Event(50*s, label='trigger') >>> >>> evt.time array(50.0) * s >>> evt.label 'trigger' *Required attributes/properties*: :time: (quantity scalar) The time of the event. :label: (str) Name or label for the event. *Recommended attributes/properties*: :name: (str) A label for the dataset. :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`. ''' def __init__(self, time, label, name=None, description=None, file_origin=None, **annotations): ''' Initialize a new :class:`Event` instance. ''' BaseNeo.__init__(self, name=name, file_origin=file_origin, description=description, **annotations) self.time = time self.label = label self.segment = None def merge(self, other): ''' Merging is not supported in :class:`Epoch`. ''' raise NotImplementedError('Cannot merge Epoch objects') neo-0.3.3/neo/version.py0000644000175000017500000000005312273723542016167 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- version = '0.3.3' neo-0.3.3/doc/0000755000175000017500000000000012273723667014126 5ustar sgarciasgarcia00000000000000neo-0.3.3/doc/Makefile0000644000175000017500000000606412265516260015562 0ustar sgarciasgarcia00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " changes to make an overview of all changed/added/deprecated items" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" clean: -rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/neo.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/neo.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." neo-0.3.3/doc/make.bat0000644000175000017500000000577512265516260015537 0ustar sgarciasgarcia00000000000000@ECHO OFF REM Command file for Sphinx documentation set SPHINXBUILD=sphinx-build set BUILDDIR=build set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source if NOT "%PAPER%" == "" ( set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% ) if "%1" == "" goto help if "%1" == "help" ( :help echo.Please use `make ^` where ^ is one of echo. html to make standalone HTML files echo. dirhtml to make HTML files named index.html in directories echo. pickle to make pickle files echo. json to make JSON files echo. htmlhelp to make HTML files and a HTML help project echo. qthelp to make HTML files and a qthelp project echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter echo. changes to make an overview over all changed/added/deprecated items echo. linkcheck to check all external links for integrity echo. doctest to run all doctests embedded in the documentation if enabled goto end ) if "%1" == "clean" ( for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i del /q /s %BUILDDIR%\* goto end ) if "%1" == "html" ( %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html echo. echo.Build finished. The HTML pages are in %BUILDDIR%/html. goto end ) if "%1" == "dirhtml" ( %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml echo. echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. goto end ) if "%1" == "pickle" ( %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle echo. echo.Build finished; now you can process the pickle files. goto end ) if "%1" == "json" ( %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json echo. echo.Build finished; now you can process the JSON files. goto end ) if "%1" == "htmlhelp" ( %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp echo. echo.Build finished; now you can run HTML Help Workshop with the ^ .hhp project file in %BUILDDIR%/htmlhelp. goto end ) if "%1" == "qthelp" ( %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp echo. echo.Build finished; now you can run "qcollectiongenerator" with the ^ .qhcp project file in %BUILDDIR%/qthelp, like this: echo.^> qcollectiongenerator %BUILDDIR%\qthelp\neo.qhcp echo.To view the help file: echo.^> assistant -collectionFile %BUILDDIR%\qthelp\neo.ghc goto end ) if "%1" == "latex" ( %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex echo. echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. goto end ) if "%1" == "changes" ( %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes echo. echo.The overview file is in %BUILDDIR%/changes. goto end ) if "%1" == "linkcheck" ( %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck echo. echo.Link check complete; look for any errors in the above output ^ or in %BUILDDIR%/linkcheck/output.txt. goto end ) if "%1" == "doctest" ( %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest echo. echo.Testing of doctests in the sources finished, look at the ^ results in %BUILDDIR%/doctest/output.txt. goto end ) :end neo-0.3.3/doc/source/0000755000175000017500000000000012273723667015426 5ustar sgarciasgarcia00000000000000neo-0.3.3/doc/source/io_developers_guide.rst0000644000175000017500000001343712265516260022172 0ustar sgarciasgarcia00000000000000.. _io_dev_guide: ******************** IO developers' guide ******************** .. _io_guiline: Guidelines for IO implementation ================================ Receipe to develop an IO module for a new data format: 1. Fully understand the object model. See :doc:`core`. If in doubt ask the `mailing list`_. 2. Fully understand :mod:`neo.io.exampleio`, It is a fake IO to explain the API. If in doubt ask the list. 3. Copy/paste ``exampleio.py`` and choose clear file and class names for your IO. 4. Decide which **supported objects** and **readable objects** your IO will deal with. This is the crucial point. 5. Implement all methods :meth:`read_XXX` related to **readable objects**. 6. Optional: If your IO supports reading multiple blocks from one file, implement a :meth:`read_all_blocks` method. 7. Do not forget all lazy and cascade combinations. 8. Optional: Support loading lazy objects by implementing a :meth:`load_lazy_object` method and / or lazy cascading by implementing a :meth:`load_lazy_cascade` method. 9. Write good docstrings. List dependencies, including minimum version numbers. 10. Add your class to :mod:`neo.io.__init__`. Keep the import inside try/except for dependency reasons. 11. Contact the Neo maintainers to put sample files for testing on the G-Node server (write access is not public). 12. Write tests in ``neo/test/io/test_xxxxxio.py``. You must at least pass the standard tests (inherited from :class:`BaseTestIO`). 13. Commit or send a patch only if all tests pass. Miscellaneous ============= Notes: * if your IO supports several version of a format (like ABF1, ABF2), upload to G-node test file repository all file version possible. (for utest coverage). * :py:func:`neo.io.tools.create_many_to_one_relationship` offers a utility to complete the hierachy when all one-to-many relationships have been created. * :py:func:`neo.io.tools.populate_RecordingChannel` offers a utility to create inside a :class:`Block` all :class:`RecordingChannel` objects and links to :class:`AnalogSignal`, :class:`SpikeTrain`, ... * In the docstring, explain where you obtained the file format specification if it is a closed one. * If your IO is based on a database mapper, keep in mind that the returned object MUST be detached, because this object can be written to another url for copying. Advanced lazy loading ===================== If your IO supports a format that might take a long time to load or require lots of memory, consider implementing one or both of the following methods to enable advanced lazy loading: * ``load_lazy_object(self, obj)``: This method takes a lazily loaded object and returns the corresponding fully loaded object. It does not set any links of the newly loaded object (e.g. the segment attribute of a SpikeTrain). The information needed to fully load the lazy object should usually be stored in the IO object (e.g. in a dictionary with lazily loaded objects as keys and the address in the file as values). * ``load_lazy_cascade(self, address, lazy)``: This method takes two parameters: The information required by your IO to load an object and a boolean that indicates if data objects should be lazy loaded (in the same way as with regular :meth:`read_XXX` methods). The method should return a loaded objects, including all the links for one-to-many and many-to-many relationships (lists of links should be replaced by ``LazyList`` objects, see below). To implement lazy cascading, your read methods need to react when a user calls them with the ``cascade`` parameter set to ``lazy``. In this case, you have to replace all the link lists of your loaded objects with instances of :class:`neo.io.tools.LazyList`. Instead of the actual objects that your IO would load at this point, fill the list with items that ``load_lazy_cascade`` needs to load the object. Because the links of objects can point to previously loaded objects, you need to cache all loaded objects in the IO. If :meth:`load_lazy_cascade` is called with the address of a previously loaded object, return the object instead of loading it again. Also, a call to :meth:`load_lazy_cascade` might require you to load additional objects further up in the hierarchy. For example, if a :class:`SpikeTrain` is accessed through a :class:`Segment`, its :class:`Unit` and the :class:`RecordingChannelGroup` of the :class:`Unit` might have to be loaded at that point as well if they have not been accessed before. Note that you are free to restrict lazy cascading to certain objects. For example, you could use the ``LazyList`` only for the ``analogsignals`` property of :class:`Segment` and :class:`RecordingChannel` objects and load the rest of file immediately. Tests ===== :py:class:`neo.test.io.commun_io_test.BaseTestIO` provide standard tests. To use these you need to upload some sample data files at the `G-Node portal`_. They will be publicly accessible for testing Neo. These tests: * check the compliance with the schema: hierachy, attribute types, ... * check if the IO respects the *lazy* and *cascade* keywords. * For IO able to both write and read data, it compares a generated dataset with the same data after a write/read cycle. The test scripts download all files from the `G-Node portal`_ and store them locally in ``neo/test/io/files_for_tests/``. Subsequent test runs use the previously downloaded files, rather than trying to download them each time. Here is an example test script taken from the distribution: ``test_axonio.py``: .. literalinclude:: ../../neo/test/iotest/test_axonio.py ExampleIO ========= .. autoclass:: neo.io.ExampleIO Here is the entire file: .. literalinclude:: ../../neo/io/exampleio.py .. _`mailing list`: http://groups.google.com/group/neuralensemble .. _G-node portal: https://portal.g-node.org/neo/neo-0.3.3/doc/source/io.rst0000644000175000017500000002311412265516260016556 0ustar sgarciasgarcia00000000000000****** Neo IO ****** .. currentmodule:: neo Preamble ======== The Neo :mod:`io` module aims to provide an exhaustive way of loading and saving several widely used data formats in electrophysiology. The more these heterogeneous formats are supported, the easier it will be to manipulate them as Neo objects in a similar way. Therefore the IO set of classes propose a simple and flexible IO API that fits many format specifications. It is not only file-oriented, it can also read/write objects from a database. :mod:`neo.io` can be seen as a *pure-Python* and open-source Neuroshare replacement. At the moment, there are 3 families of IO modules: 1. for reading closed manufacturers' formats (Spike2, Plexon, AlphaOmega, BlackRock, Axon, ...) 2. for reading(/writing) formats from open source tools (KlustaKwik, Elan, WinEdr, WinWcp, PyNN, ...) 3. for reading/writing Neo structure in neutral formats (HDF5, .mat, ...) but with Neo structure inside (NeoHDF5, NeoMatlab, ...) Combining **1** for reading and **3** for writing is a good example of use: converting your datasets to a more standard format when you want to share/collaborate. Introduction ============ There is an intrinsic structure in the different Neo objects, that could be seen as a hierachy with cross-links. See :doc:`core`. The highest level object is the :class:`Block` object, which is the high level container able to encapsulate all the others. A :class:`Block` has therefore a list of :class:`Segment` objects, that can, in some file formats, be accessed individually. Depending on the file format, i.e. if it is streamable or not, the whole :class:`Block` may need to be loaded, but sometimes particular :class:`Segment` objects can be accessed individually. Within a :class:`Segment`, the same hierarchical organisation applies. A :class:`Segment` embeds several objects, such as :class:`SpikeTrain`, :class:`AnalogSignal`, :class:`AnaloSignalArray`, :class:`EpochArray`, :class:`EventArray` (basically, all the different Neo objects). Depending on the file format, these objects can sometimes be loaded separately, without the need to load the whole file. If possible, a file IO therefore provides distinct methods allowing to load only particular objects that may be present in the file. The basic idea of each IO file format is to have, as much as possible, read/write methods for the individual encapsulated objects, and otherwise to provide a read/write method that will return the object at the highest level of hierarchy (by default, a :class:`Block` or a :class:`Segment`). The :mod:`neo.io` API is a balance between full flexibility for the user (all :meth:`read_XXX` methods are enabled) and simple, clean and understandable code for the developer (few :meth:`read_XXX` methods are enabled). This means that not all IOs offer the full flexibility for partial reading of data files. One format = one class ====================== The basic syntax is as follows. If you want to load a file format that is implemented in a generic :class:`MyFormatIO` class:: >>> from neo.io import MyFormatIO >>> reader = MyFormatIO(filename = "myfile.dat") you can replace :class:`MyFormatIO` by any implemented class, see :ref:`list_of_io` Modes ====== IO can be based on file, directory, database or fake This is describe in mode attribute of the IO class. >>> from neo.io import MyFormatIO >>> print MyFormatIO.mode 'file' For *file* mode the *filename* keyword argument is necessary. For *directory* mode the *dirname* keyword argument is necessary. Ex: >>> reader = io.PlexonIO(filename='File_plexon_1.plx') >>> reader = io.TdtIO(dirname='aep_05') Supported objects/readable objects ================================== To know what types of object are supported by a given IO interface:: >>> MyFormatIO.supported_objects [Segment , AnalogSignal , SpikeTrain, Event, Spike] Supported objects does not mean objects that you can read directly. For instance, many formats support :class:`AnalogSignal` but don't allow them to be loaded directly, rather to access the :class:`AnalogSignal` objects, you must read a :class:`Segment`:: >>> seg = reader.read_segment() >>> print(seg.analogsignals) >>> print(seg.analogsignals[0]) To get a list of directly readable objects :: >>> MyFormatIO.readable_objects [Segment] The first element of the previous list is the highest level for reading the file. This mean that the IO has a :meth:`read_segment` method:: >>> seg = reader.read_segment() >>> type(seg) neo.core.Segment All IOs have a read() method that returns a list of :class:`Block` objects (representing the whole content of the file):: >>> bl = reader.read() >>> print bl[0].segments[0] neo.core.Segment Lazy and cascade options ======================== In some cases you may not want to load everything in memory because it could be too big. For this scenario, two options are available: * ``lazy=True/False``. With ``lazy=True`` all arrays will have a size of zero, but all the metadata will be loaded. lazy_shape attribute is added to all object that inheritate Quantitities or numpy.ndarray (AnalogSignal, AnalogSignalArray, SpikeTrain) and to object that have array like attributes (EpochArray, EventArray) In that cases, lazy_shape is a tuple that have the same shape with lazy=False. * ``cascade=True/False``. With ``cascade=False`` only one object is read (and *one_to_many* and *many_to_many* relationship are not read). By default (if they are not specified), ``lazy=False`` and ``cascade=True``, i.e. all data is loaded. Example cascade:: >>> seg = reader.read_segment( cascade=True) >>> print(len(seg.analogsignals)) # this is N >>> seg = reader.read_segment(cascade=False) >>> print(len(seg.analogsignals)) # this is zero Example lazy:: >>> seg = reader.read_segment(lazy=False) >>> print(seg.analogsignals[0].shape) # this is N >>> seg = reader.read_segment(lazy=True) >>> print(seg.analogsignals[0].shape) # this is zero, the AnalogSignal is empty >>> print(seg.analogsignals[0].lazy_shape) # this is N Some IOs support advanced forms of lazy loading, cascading or both (these features are currently limited to the HDF5 IO, which supports both forms). * For lazy loading, these IOs have a :meth:`load_lazy_object` method that takes a single parameter: a data object previously loaded by the same IO in lazy mode. It returns the fully loaded object, without links to container objects (Segment etc.). Continuing the lazy example above:: >>> lazy_sig = seg.analogsignals[0] # Empty signal >>> full_sig = reader.load_lazy_object(lazy_sig) >>> print(lazy_sig.lazy_shape, full_sig.shape) # Identical >>> print(lazy_sig.segment) # Has the link to the object "seg" >>> print(full_sig.segment) # Does not have the link: None * For lazy cascading, IOs have a :meth:`load_lazy_cascade` method. This method is not called directly when interacting with the IO, but its presence can be used to check if an IO supports lazy cascading. To use lazy cascading, the cascade parameter is set to ``'lazy'``:: >>> block = reader.read(cascade='lazy') You do not have to do anything else, lazy cascading is now active for the object you just loaded. You can interact with the object in the same way as if it was loaded with ``cascade=True``. However, only the objects that are actually accessed are loaded as soon as they are needed:: >>> print(block.recordingchannelgroups[0].name) # The first RecordingChannelGroup is loaded >>> print(block.segments[0].analogsignals[1]) # The first Segment and its second AnalogSignal are loaded Once an object has been loaded with lazy cascading, it stays in memory:: >>> print(block.segments[0].analogsignals[0]) # The first Segment is already in memory, its first AnalogSignal is loaded .. _neo_io_API: Details of API ============== The :mod:`neo.io` API is designed to be simple and intuitive: - each file format has an IO class (for example for Spike2 files you have a :class:`Spike2IO` class). - each IO class inherits from the :class:`BaseIO` class. - each IO class can read or write directly one or several Neo objects (for example :class:`Segment`, :class:`Block`, ...): see the :attr:`readable_objects` and :attr:`writable_objects` attributes of the IO class. - each IO class supports part of the :mod:`neo.core` hierachy, though not necessarily all of it (see :attr:`supported_objects`). - each IO class has a :meth:`read()` method that returns a list of :class:`Block` objects. If the IO only supports :class:`Segment` reading, the list will contain one block with all segments from the file. - each IO class that supports writing has a :meth:`write()` method that takes as a parameter a list of blocks, a single block or a single segment, depending on the IO's :attr:`writable_objects`. - each IO is able to do a *lazy* load: all metadata (e.g. :attr:`sampling_rate`) are read, but not the actual numerical data. lazy_shape attribute is added to provide information on real size. - each IO is able to do a *cascade* load: if ``True`` (default) all child objects are loaded, otherwise only the top level object is loaded. - each IO is able to save and load all required attributes (metadata) of the objects it supports. - each IO can freely add user-defined or manufacturer-defined metadata to the :attr:`annotations` attribute of an object. .. _list_of_io: List of implemented formats =========================== .. automodule:: neo.io If you want to develop your own IO ================================== See :doc:`io_developers_guide` for information on how to implement of a new IO. neo-0.3.3/doc/source/gif2011workshop.rst0000644000175000017500000000766212265516260021027 0ustar sgarciasgarcia00000000000000************************************ Gif 2011 workshop decisions ************************************ This have been writtent before neo 2 implementation just after the wokshop. Not every hting is up to date. After a workshop in GIF we are happy to present the following improvements: =========================================================================== 1. We made a few renames of objects - "Neuron" into "Unit" - "RecordingPoint" into "RecordingChannel" to remove electrophysiological (or other) dependencies and keep generality. 2. For every object we specified mandatory attributes and recommended attributes. For every attribute we define a python-based data type. The changes are reflected in the diagram #FIXME with red (mandatory) and blue (recommended) attributes indicated. 3. New objects are required for operational performance (memory allocation) and logical consistency (neo eeg, etc): - AnalogSignalArray - IrregularlySampledAnalogSignal - EventArray - EpochArray - RecordingChannelGroup Attributes and parent objects are available on the diagram #FIXME 4. Due to some logical considerations we remove the link between "RecordingChannel" and "Spiketrain". "SpikeTrain" now depends on "Unit", which in its turn connects to "RecordingChannel". For inconsistency reasons we removed link between "SpikeTrain" and a "Spike" ("SpikeTrain" is an object containing numpy array of spikes, but not a container of "Spike" objects - which is performance-unefficient). The same idea is applied to AnalogSignal / AnalogSignalArray, Event / EventArray etc. All changes are relected in # FIXME 5. In order to implement flexibility and embed user-defined metadata into the NEO objects we decided to assign "annotations" dictionnary to very NEO object. This attribute is optional; user may add key-value pairs to it according to its scientific needs. 6. The decision is made to use "quantities" package for objects, representing data arrays with units. "Quantities" is a stable (at least for python2.6) package, presented in pypi, easy-embeddable into NEO object model. Points of implementation are presented in the diagram # FIXME 7. We postpone the solution of object ID management inside NEO. 8. In AnalogSignal - t_stop become a property (for consistency reasons). 9. In order to provie a support for "advanced" object load we decided to include parameters - lazy (True/False) - cascade (True/False) in the BaseIO class. These parameters are valid for every method, provided by the IO (.read_segment() etc.). If "lazy" is True, the IO does not load data array, and makes array load otherwise. "Cascade" parameter regulates load of object relations. 10. We postpone the question of data analysis storage till the next NEO congress. Analysis objects are free for the moment. 11. We stay with Python 2.6 / 2.7 support. Python 3 to be considered in a later discussions. New object diagram discussed =============================================== .. image:: images/neo_UML_French_workshop.png :height: 500 px :align: center Actions to be performed: =============================================================== promotion: at g-node: philipp, andrey in neuralesemble: andrew within incf network: andrew thomas at posters: all logo: samuel paper: next year in the web: pypi object struture: common: samuel draft: yann andrey tree diagram: philipp florant io: ExampleIO : samuel HDF5 IO: andrey doc: first page: andrew thomas object disription: samuel draft+ andrew io user/ io dev: samuel example/cookbook: andrey script, samuel NeuroConvert, doctest unitest: andrew packaging: samuel account for more licence: BSD-3-Clause copyright: CNRS, GNode, University of Provence hosting test data: Philipp Other questions discussed: =========================== - consistency in names of object attributes and get/set functions neo-0.3.3/doc/source/usecases.rst0000644000175000017500000002112412265516260017761 0ustar sgarciasgarcia00000000000000***************** Typical use cases ***************** Recording multiple trials from multiple channels ================================================ In this example we suppose that we have recorded from an 8-channel probe, and that we have recorded three trials/episodes. We therefore have a total of 8 x 3 = 24 signals, each represented by an :class:`AnalogSignal` object. 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:`RecordingChannelGroup`, composed of 8 :class:`RecordingChannel` objects. .. image:: images/multi_segment_diagram.png :class:`Segment` and :class:`RecordingChannel` objects provide two different ways to access the data, corresponding respectively, in this scenario, to access by **time** and by **space**. .. note:: segments do not always represent trials, they can be used for many purposes: segments could represent parallel recordings for different subjects, or different steps in a current clamp protocol. **Temporal (by segment)** In this case you want to go through your data in order, perhaps because you want to correlate the neural response with the stimulus that was delivered in each segment. In this example, we're averaging over the channels. .. doctest:: import numpy as np from matplotlib import pyplot as plt for seg in block.segments: print("Analyzing segment %d" % seg.index) siglist = seg.analogsignals avg = np.mean(siglist, axis=0) plt.figure() plt.plot(avg) plt.title("Peak response in segment %d: %f" % (seg.index, avg.max())) **Spatial (by channel)** 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 RecordingChannelGroup rcg = block.recordingchannelgroups[0]: for rc in rcg.recordingchannels: print("Analyzing channel %d: %s", (rc.index, rc.name)) siglist = rc.analogsignals avg = np.mean(siglist, axis=0) plt.figure() plt.plot(avg) plt.title("Average response on channel %d: %s' % (rc.index, rc.name) Note that :attr:`Block.list_recordingchannels` is a property that gives direct access to all :class:`RecordingChannels`, so the two first lines:: rcg = block.recordingchannelgroups[0]: for rc in rcg.recordingchannels: could be written as:: for rc in block.list_recordingchannels: **Mixed example** Combining simultaneously the two approaches of descending the hierarchy temporally and spatially can be tricky. Here's an example. Let's say you saw something interesting on channel 5 on even numbered trials during the experiment and you want to follow up. What was the average response? .. doctest:: avg = np.mean([seg.analogsignals[5] for seg in block.segments[::2]], axis=1) plt.plot(avg) Here we have assumed that segment are temporally ordered in a ``block.segments`` and that signals are ordered by channel number in ``seg.analogsignals``. It would be safer, however, to avoid assumptions by explicitly testing the :attr:`index` attribute of the :class:`RecordingChannel` and :class:`Segment` objects. One way to do this is to loop over the recording channels and access the segments through the signals (each :class:`AnalogSignal` contains a reference to the :class:`Segment` it is contained in). .. doctest:: siglist = [] rcg = block.recordingchannelgroups[0]: for rc in rcg.recordingchannels: if rc.index == 5: for anasig in rc.analogsignals: if anasig.segment.index % 2 == 0: siglist.append(anasig) avg = np.mean(siglist) Recording spikes from multiple tetrodes ======================================= 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:`RecordingChannelGroups` (one per tetrode), which contain: * 4 :class:`RecordingChannels` each * 2 :class:`Unit` objects (= 2 neurons) for the first :class:`RecordingChannelGroup` * 5 :class:`Units` for the second :class:`RecordingChannelGroup`. In total we have 3 x 7 = 21 :class:`SpikeTrains` in this :class:`Block`. .. image:: images/multi_segment_diagram_spiketrain.png There are three ways to access the :class:`SpikeTrain` data: * by :class:`Segment` * by :class:`RecordingChannel` * by :class:`Unit` **By Segment** In this example, each :class:`Segment` represents data from one trial, and we want a PSTH for each trial from all units combined: .. doctest:: for seg in block.segments: print("Analyzing segment %d" % seg.index) stlist = [st - st.t_start for st in seg.spiketrains] plt.figure() count, bins = np.histogram(stlist) plt.bar(bins[:-1], count, width=bins[1] - bins[0]) plt.title("PSTH in segment %d" % seg.index) **By Unit** Now we can calculate the PSTH averaged over trials for each unit, using the :attr:`block.list_units` property: .. doctest:: for unit in block.list_units: stlist = [st - st.t_start for st in unit.spiketrains] plt.figure() count, bins = np.histogram(stlist) plt.bar(bins[:-1], count, width=bins[1] - bins[0]) plt.title("PSTH of unit %s" % unit.name) **By RecordingChannelGroup** Here we calculate a PSTH averaged over trials by channel location, blending all units: .. doctest:: for rcg in block.recordingchannelgroups: stlist = [] for unit in rcg.units: stlist.extend([st - st.t_start for st in unit.spiketrains]) plt.figure() count, bins = np.histogram(stlist) plt.bar(bins[:-1], count, width=bins[1] - bins[0]) plt.title("PSTH blend of tetrode %s" % rcg.name) 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 RecordingChannelGroup for 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.) .. doctest:: # generate some fake data rcg = RecordingChannelGroup() for n in range(4): rcg.recordingchannels.append(neo.RecordingChannel()) rcg.recordingchannels[n].analogsignals.append( AnalogSignal([.1, -2.0, .1, -.1, -.1, -3.0, .1, .1], sampling_rate=1000*Hz, units='V')) # extract spike trains from each channel st_list = [] for n in range(len(rcg.recordingchannels[0].analogsignals)): sigarray = np.array( [rcg.recordingchannels[m].analogsignals[n] for m in range(4)]) # use a simple threshhold detector spike_mask = np.where(np.min(sigarray, axis=0) < -1.0 * pq.V)[0] # create a spike train anasig = rcg.recordingchannels[m].analogsignals[n] spike_times = anasig.times[spike_mask] st = neo.SpikeTrain(spike_times, t_start=anasig.t_start, anasig.t_stop) # remember the spike waveforms wf_list = [] for spike_idx in np.nonzero(spike_mask)[0]: wf_list.append(sigarray[:, 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. .. doctest:: u = Unit() u.spiketrains = st_list rcg.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. 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. .. EEG .. Network simulations neo-0.3.3/doc/source/conf.py0000644000175000017500000001472212273723542016723 0ustar sgarciasgarcia00000000000000# -*- coding: utf-8 -*- # # neo documentation build configuration file, created by # sphinx-quickstart on Fri Feb 25 14:18:12 2011. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import os import sys AUTHORS = u'Neo authors and contributors ' # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.append(os.path.abspath('.')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.todo'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8' # The master toctree document. master_doc = 'index' # General information about the project. project = u'Neo' copyright = u'2010-2014, ' + AUTHORS # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '0.3' # The full version, including alpha/beta/rc tags. release = '0.3.3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. #language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of documents that shouldn't be included in the build. #unused_docs = [] # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # The reST default role (used for this markup: `text`) # to use for all documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. #html_theme = 'default' html_theme = 'sphinxdoc' #html_theme = 'haiku' #html_theme = 'scrolls' #html_theme = 'agogo' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. html_logo = 'images/neologo_light.png' # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_use_modindex = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = '' # Output file base name for HTML help builder. htmlhelp_basename = 'neodoc' # -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). #latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). #latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, # documentclass [howto/manual]). latex_documents = [('index', 'neo.tex', u'Neo Documentation', AUTHORS, 'manual')] # The name of an image file (relative to this directory) to place at the # top of the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # Additional stuff for the LaTeX preamble. #latex_preamble = '' # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_use_modindex = True todo_include_todos = True # set to False before releasing documentation neo-0.3.3/doc/source/core.rst0000644000175000017500000003007512265516260017103 0ustar sgarciasgarcia00000000000000******** Neo core ******** .. currentmodule:: neo Introduction ============ Objects in Neo represent neural data and collections of data. Neo objects fall into three categories: data objects, container objects and grouping objects. Data objects ------------ These objects directly represent data as arrays of numerical values with associated metadata (units, sampling frequency, etc.). :py:class:`AnalogSignal`: A regular sampling of a continuous, analog signal. :py:class:`AnalogSignalArray`: A regular sampling of a multichannel continuous analog signal. This representation (as a 2D NumPy array) may be more efficient for subsequent analysis than the equivalent list of individual :py:class:`AnalogSignal` objects. :py:class:`Spike`: One action potential characterized by its time and waveform. :py:class:`SpikeTrain`: A set of action potentials (spikes) emitted by the same unit in a period of time (with optional waveforms). :py:class:`Event` and :py:class:`EventArray`: A time point representng an event in the data, or an array of such time points. :py:class:`Epoch` and :py:class:`EpochArray`: An interval of time representing a period of time in the data, or an array of such intervals. Container objects ----------------- There is a simple hierarchy of containers: :py:class:`Segment`: A container for heterogeneous discrete or continous data sharing a common clock (time basis) but not necessarily the same sampling rate, start time or end time. A :py:class:`Segment` can be considered as equivalent to a "trial", "episode", "run", "recording", etc., depending on the experimental context. 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` and :class:`RecordingChannelGroup` objects. Grouping objects ---------------- These objects express the relationships between data items, such as which signals 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:`RecordingChannel`: Links :py:class:`AnalogSignal`, :py:class:`SpikeTrain` objects that come from the same logical and/or physical channel inside a :py:class:`Block`, possibly across several :py:class:`Segment` objects. :py:class:`RecordingChannelGroup`: A group for associated :py:class:`RecordingChannel` objects. This has several possible uses: * for linking several :py:class:`AnalogSignalArray` objects across several :py:class:`Segment` objects inside a :py:class:`Block`. * for multielectrode arrays, where spikes may be recorded on more than one recording channel, and so the :py:class:`RecordingChannelGroup` can be used to associate each :py:class:`Unit` with the group of recording channels from which it was calculated. * for grouping several :py:class:`RecordingChannel` objects. There are many use cases for this. For instance, for intracellular recording, it is common to record both membrane potentials and currents at the same time, so each :py:class:`RecordingChannelGroup` may correspond to the particular property that is being recorded. For multielectrode arrays, :py:class:`RecordingChannelGroup` is used to gather all :py:class:`RecordingChannel` objects of the same array. :py:class:`Unit`: A Unit gathers all the :class:`SpikeTrain` objects within a common :class:`Block`, possibly across several Segments, that have been emitted by the same cell. A :class:`Unit` is linked to :class:`RecordingChannelGroup` objects from which it was detected. This replaces the :class:`Neuron` class in the previous version of Neo (v0.1). .. image:: images/base_schematic.png :height: 500 px :alt: Neo : Neurotools/OpenElectrophy shared base architecture :align: center Relationships between objects ============================= Container objects like :py:class:`Block` or :py:class:`Segment` are gateways to access other objects. For example, a :class:`Block` can access a :class:`Segment` with:: >>> bl = Block() >>> bl.segments # gives a list of segments A :class:`Segment` can access the :class:`AnalogSignal` objects that it contains with:: >>> seg = Segment() >>> seg.analogsignals # gives a list a AnalogSignals In the :ref:`neo_diagram` below, these *one to many* relationships are represented by cyan arrows. In general, an object can access its children with an attribute *childname+s* in lower case, e.g. * :attr:`Block.segments` * :attr:`Segments.analogsignals` * :attr:`Segments.spiketrains` * :attr:`Block.recordingchannelgroups` These relationships are bi-directional, i.e. a child object can access its parent: * :attr:`Segment.block` * :attr:`AnalogSignal.segment` * :attr:`SpikeTrains.segment` * :attr:`RecordingChannelGroup.block` Here is an example showing these relationships in use:: from neo.io import AxonIO import urllib url = "https://portal.g-node.org/neo/axon/File_axon_3.abf" filename = './test.abf' urllib.urlretrieve(url, filename) r = AxonIO(filename=filename) bl = r.read() # read the entire file > a Block print(bl) print(bl.segments) # child access for seg in bl.segments: print(seg) print(seg.block) # parent access On the :ref:`neo_diagram` you can also see a magenta line reflecting the *many-to-many* relationship between :py:class:`RecordingChannel` and :py:class:`RecordingChannelGroup`. This means that each group can contain multiple channels, and each channel can belong to multiple groups. 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 * bl = Block() # creating individual channel all_rc= [ ] for i in range(16): rc = RecordingChannel( index= i, name ='rc %d' %i) all_rc.append(rc) # the four tetrodes for i in range(4): rcg = RecordingChannelGroup( name = 'Tetrode %d' % i ) for rc in all_rc[i*4:(i+1)*4]: rcg.recordingchannels.append(rc) rc.recordingchannelgroups.append(rcg) bl.recordingchannelgroups.append(rcg) # 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 `Unit` object associated with this spikesorting 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.:: from neo import * bl = Block(name='probe data') # create individual channels all_rc = [] for i in range(4): rc = RecordingChannel(index=i, name='channel %d' % i) all_rc.append(rc) # one group for each neuron rcg0 = RecordingChannelGroup(name='Group 0', index=0) for i in [0, 1, 2]: rcg1.recordingchannels.append(all_rc[i]) rc[i].recordingchannelgroups.append(rcg0) bl.recordingchannelgroups.append(rcg0) rcg1 = RecordingChannelGroup(name='Group 1', index=1) for i in [1, 2, 3]: rcg1.recordingchannels.append(all_rc[i]) rc[i].recordingchannelgroups.append(rcg1) bl.recordingchannelgroups.append(rcg1) # now we add the spiketrain from Unit 0 to rcg0 # and add the spiketrain from Unit 1 to rcg1 # ... Note that because neurons are sorted from groups of channels in this situation, it is natural that the :py:class:`RecordingChannelGroup` contains the :py:class:`Unit` object. That unit then contains its spiketrains. There are some shortcuts for IO writers to automatically create this structure based on 'channel_indexes' entries in the annotations for each spiketrain. See :doc:`usecases` for more examples of how the different objects may be used. .. _neo_diagram: Neo diagram =========== Object: * With a star = inherits from :class:`Quantity` Attributes: * In red = required * In white = recommended Relationship: * In cyan = one to many * In magenta = many to many * In yellow = properties (deduced from other relationships) .. image:: images/simple_generated_diagram.png :width: 750 px :download:`Click here for a better quality SVG diagram <./images/simple_generated_diagram.svg>` For more details, see the :doc:`api_reference`. Inheritance =========== Some Neo objects (:py:class:`AnalogSignal`, :py:class:`SpikeTrain`, :py:class:`AnalogSignalArray`) inherit from :py:class:`Quantity`, which in turn inherits from NumPy :py:class:`ndarray`. This means that a Neo :py:class:`AnalogSignal` actually is also a :py:class:`Quantity` and an array, giving you access to all of the methods available for those objects. For example, you can pass a :py:class:`SpikeTrain` directly to the :py:func:`numpy.histogram` function, or an :py:class:`AnalogSignal` directly to the :py:func:`numpy.std` function. Initialization ============== Neo objects are initialized with "required", "recommended", and "additional" arguments. - Required arguments MUST be provided at the time of initialization. They are used in the construction of the object. - Recommended arguments may be provided at the time of initialization. They are accessible as Python attributes. They can also be set or modified after initialization. - Additional arguments are defined by the user and are not part of the Neo object model. A primary goal of the Neo project is extensibility. These additional arguments are entries in an attribute of the object: a Python dict called :py:attr:`annotations`. Example: SpikeTrain ------------------- :py:class:`SpikeTrain` is a :py:class:`Quantity`, which is a NumPy array containing values with physical dimensions. The spike times are a required attribute, because the dimensionality of the spike times determines the way in which the :py:class:`Quantity` is constructed. Here is how you initialize a :py:class:`SpikeTrain` with required arguments:: >>> import neo >>> st = neo.SpikeTrain([3, 4, 5], units='sec', t_stop=10.0) >>> print(st) [ 3. 4. 5.] s You will see the spike times printed in a nice format including the units. Because `st` "is a" :py:class:`Quantity` array with units of seconds, it absolutely must have this information at the time of initialization. You can specify the spike times with a keyword argument too:: >>> st = neo.SpikeTrain(times=[3, 4, 5], units='sec', t_stop=10.0) The spike times could also be in a NumPy array. If it is not specified, :attr:`t_start` is assumed to be zero, but another value can easily be specified:: >>> st = neo.SpikeTrain(times=[3, 4, 5], units='sec', t_start=1.0, t_stop=10.0) >>> st.t_start array(1.0) * s Recommended attributes must be specified as keyword arguments, not positional arguments. Finally, let's consider "additional arguments". These are the ones you define for your experiment:: >>> st = neo.SpikeTrain(times=[3, 4, 5], units='sec', t_stop=10.0, rat_name='Fred') >>> print(st.annotations) {'rat_name': 'Fred'} Because ``rat_name`` is not part of the Neo object model, it is placed in the dict :py:attr:`annotations`. This dict can be modified as necessary by your code. Annotations ----------- As well as adding annotations as "additional" arguments when an object is constructed, objects may be annotated using the :meth:`annotate` method possessed by all Neo core objects, e.g.:: >>> seg = Segment() >>> seg.annotate(stimulus="step pulse", amplitude=10*nA) >>> print(seg.annotations) {'amplitude': array(10.0) * nA, 'stimulus': 'step pulse'} Since annotations may be written to a file or database, there are some limitations on the data types of annotations: they must be "simple" types or containers (lists, dicts, NumPy arrays) of simple types, where the simple types are ``integer``, ``float``, ``complex``, ``Quantity``, ``string``, ``date``, ``time`` and ``datetime``. See :ref:`specific_annotations` neo-0.3.3/doc/source/developers_guide.rst0000644000175000017500000002425712265516260021505 0ustar sgarciasgarcia00000000000000================= Developers' guide ================= These instructions are for developing on a Unix-like platform, e.g. Linux or Mac OS X, with the bash shell. If you develop on Windows, please get in touch. Mailing lists ------------- General discussion of Neo development takes place in the `NeuralEnsemble Google group`_. Discussion of issues specific to a particular ticket in the issue tracker should take place on the tracker. Using the issue tracker ----------------------- If you find a bug in Neo, please create a new ticket on the `issue tracker`_, setting the type to "defect". Choose a name that is as specific as possible to the problem you've found, and in the description give as much information as you think is necessary to recreate the problem. The best way to do this is to create the shortest possible Python script that demonstrates the problem, and attach the file to the ticket. If you have an idea for an improvement to Neo, create a ticket with type "enhancement". If you already have an implementation of the idea, create a patch (see below) and attach it to the ticket. To keep track of changes to the code and to tickets, you can register for a GitHub account and then set to watch the repository at `GitHub Repository`_ (see https://help.github.com/articles/watching-repositories/). Requirements ------------ * Python_ 2.6, 2.7, or 3.3 * numpy_ >= 1.3.0 * quantities_ >= 0.9.0 * if using Python 2.6, unittest2_ >= 0.5.1 * Setuptools >= 0.7 * nose_ >= 0.11.1 (for running tests) * Sphinx_ >= 0.6.4 (for building documentation) * (optional) tox_ >= 0.9 (makes it easier to test with multiple Python versions) * (optional) coverage_ >= 2.85 (for measuring test coverage) * (optional) scipy >= 0.8 (for Matlab IO) * (optional) pytables >= >= 2.2 (for HDF5 IO) Getting the source code ----------------------- We use the Git version control system. The best way to contribute is through GitHub_. You will first need a GitHub account, and you should then fork the repository at `GitHub Repository`_ (see http://help.github.com/fork-a-repo/). To get a local copy of the repository:: $ cd /some/directory $ git clone git@github.com:/python-neo.git Now you need to make sure that the ``neo`` package is on your PYTHONPATH. You can do this either by installing Neo:: $ cd python-neo $ python setup.py install $ python3 setup.py install (if you do this, you will have to re-run ``setup.py install`` any time you make changes to the code) *or* by creating symbolic links from somewhere on your PYTHONPATH, for example:: $ ln -s python-neo/neo $ export PYTHONPATH=/some/directory:${PYTHONPATH} An alternate solution is to install Neo with the *develop* option, this avoids reinstalling when there are changes in the code:: $ sudo python setup.py develop To update to the latest version from the repository:: $ git pull Running the test suite ---------------------- Before you make any changes, run the test suite to make sure all the tests pass on your system:: $ cd neo/test With Python 2.7 or 3.3:: $ python -m unittest discover $ python3 -m unittest discover If you have nose installed:: $ nosetests At the end, if you see "OK", then all the tests passed (or were skipped because certain dependencies are not installed), otherwise it will report on tests that failed or produced errors. To run tests from an individual file:: $ python test_analogsignal.py $ python3 test_analogsignal.py Writing tests ------------- You should try to write automated tests for any new code that you add. If you have found a bug and want to fix it, first write a test that isolates the bug (and that therefore fails with the existing codebase). Then apply your fix and check that the test now passes. To see how well the tests cover the code base, run:: $ nosetests --with-coverage --cover-package=neo --cover-erase Working on the documentation ---------------------------- All modules, classes, functions, and methods (including private and subclassed builtin methods) should have docstrings. Please see `PEP257`_ for a description of docstring conventions. Module docstrings should explain briefly what functions or classes are present. Detailed descriptions can be left for the docstrings of the respective functions or classes. Private functions do not need to be explained here. Class docstrings should include an explanation of the purpose of the class and, when applicable, how it relates to standard neuroscientific data. They should also include at least one example, which should be written so it can be run as-is from a clean newly-started Python interactive session (that means all imports should be included). Finally, they should include a list of all arguments, attributes, and properties, with explanations. Properties that return data calculated from other data should explain what calculation is done. A list of methods is not needed, since documentation will be generated from the method docstrings. Method and function docstrings should include an explanation for what the method or function does. If this may not be clear, one or more examples may be included. Examples that are only a few lines do not need to include imports or setup, but more complicated examples should have them. Examples can be tested easily using th iPython %doctest_mode magic. This will strip >>> and ... from the beginning of each line of the example, so the example can be copied and pasted as-is. The documentation is written in `reStructuredText`_, using the `Sphinx`_ documentation system. Any mention of another neo module, class, attribute, method, or function should be properly marked up so automatic links can be generated. The same goes for quantities or numpy. To build the documentation:: $ cd python-neo/doc $ make html Then open `some/directory/neo_trunk/doc/build/html/index.html` in your browser. Committing your changes ----------------------- Once you are happy with your changes, **run the test suite again to check that you have not introduced any new bugs**. It is also recommended to check your code with a code checking program, such as `pyflakes`_ or `flake8`_. Then you can commit them to your local repository:: $ git commit -m 'informative commit message' If this is your first commit to the project, please add your name and affiliation/employer to :file:`doc/source/authors.rst` You can then push your changes to your online repository on GitHub:: $ git push Once you think your changes are ready to be included in the main Neo repository, open a pull request on GitHub (see https://help.github.com/articles/using-pull-requests). Python 3 -------- Neo core should work with both recent versions of Python 2 (versions 2.6 and 2.7) and Python 3 (version 3.3). Neo IO modules should ideally work with both Python 2 and 3, but certain modules may only work with one or the other (see :doc:`install`). So far, we have managed to write code that works with both Python 2 and 3. Mainly this involves avoiding the ``print`` statement (use ``logging.info`` instead), and putting ``from __future__ import division`` at the beginning of any file that uses division. If in doubt, `Porting to Python 3`_ by Lennart Regebro is an excellent resource. The most important thing to remember is to run tests with at least one version of Python 2 and at least one version of Python 3. There is generally no problem in having multiple versions of Python installed on your computer at once: e.g., on Ubuntu Python 2 is available as `python` and Python 3 as `python3`, while on Arch Linux Python 2 is `python2` and Python 3 `python`. See `PEP394`_ for more on this. Coding standards and style -------------------------- All code should conform as much as possible to `PEP 8`_, and should run with Python 2.6, 2.7, and 3.3. You can use the `pep8`_ program to check the code for PEP 8 conformity. You can also use `flake8`_, which combines pep8 and pyflakes. However, the pep8 and flake8 programs does not check for all PEP 8 issues. In particular, they does not check that the import statements are in the correct order. Also, please do not use "from __ import *". This is slow, can lead to conflicts, and makes it difficult for code analysis software. Making a release ---------------- .. TODO: discuss branching/tagging policy. Add a section in /doc/src/whatisnew.rst for the release. First check that the version string (in :file:`neo/version.py`, :file:`setup.py`, :file:`doc/conf.py` and :file:`doc/install.rst`) is correct. To build a source package:: $ python setup.py sdist To upload the package to `PyPI`_ (currently Samuel Garcia and Andrew Davison have the necessary permissions to do this):: $ python setup.py sdist upload $ python setup.py upload_docs --upload-dir=doc/build/html .. should we also distribute via software.incf.org Finally, tag the release in the Git repository and push it:: $ git tag $ git push --tags origin .. make a release branch If you want to develop your own IO module ----------------------------------------- See :ref:`io_dev_guide` for implementation of a new IO. .. _Python: http://www.python.org .. _nose: http://somethingaboutorange.com/mrl/projects/nose/ .. _unittest2: http://pypi.python.org/pypi/unittest2 .. _Setuptools: https://pypi.python.org/pypi/setuptools/ .. _tox: http://codespeak.net/tox/ .. _coverage: http://nedbatchelder.com/code/coverage/ .. _`PEP 8`: http://www.python.org/dev/peps/pep-0008/ .. _`issue tracker`: https://github.com/NeuralEnsemble/python-neo/issues .. _`Porting to Python 3`: http://python3porting.com/ .. _`NeuralEnsemble Google group`: http://groups.google.com/group/neuralensemble .. _reStructuredText: http://docutils.sourceforge.net/rst.html .. _Sphinx: http://sphinx.pocoo.org/ .. _numpy: http://numpy.scipy.org/ .. _quantities: http://pypi.python.org/pypi/quantities .. _PEP257: http://www.python.org/dev/peps/pep-0257/ .. _PEP394: http://www.python.org/dev/peps/pep-0394/ .. _PyPI: http://pypi.python.org .. _GitHub: http://github.com .. _`GitHub Repository`: https://github.com/NeuralEnsemble/python-neo/ .. _pep8: https://pypi.python.org/pypi/pep8 .. _flake8: https://pypi.python.org/pypi/flake8/ .. _pyflakes: https://pypi.python.org/pypi/pyflakes/ neo-0.3.3/doc/source/api_reference.rst0000644000175000017500000000020312265516260020730 0ustar sgarciasgarcia00000000000000API Reference ============= .. automodule:: neo.core .. testsetup:: * from neo import SpikeTrain import quantities as pqneo-0.3.3/doc/source/images/0000755000175000017500000000000012273723667016673 5ustar sgarciasgarcia00000000000000neo-0.3.3/doc/source/images/neologo_light.png0000644000175000017500000001505712265516260022230 0ustar sgarciasgarcia00000000000000PNG  IHDR)WsRGB pHYs  tIME  ;0IDATxy՝?UTT˪bI1NNwfrNzLw:ct:=3풰hm"@B|珷(%8!3_ws{ߒm:3Kp΀ j3ﴐ=o"/ ^WWW^^Ғ1hРE +ŏDxNɹ+Z[[c=;vj2do|xsoRMMͫ:o޼e˖%<999gϾ{Ǎ;o߾~ן}w%ƻ;kaa(l={Pϝ;w]]zO۶m{ǏhwAC۽kwZV$tԖ瞻zaÆޜ$s=ww>%%3 ;} ^H W]  02ڡ R!ڠQ_CBC-ԚfH:Hu[qDg! 2P7!AZb- @A+[.70T6C+Ch43-F"Ǥ&S)3+HRYY޽{/\>}6lP\\~XTTԯ_l UUU=x̙SO^7|ֿ` D 2IٱkhmR'FW0\l4'@8aH@.dA0P 9iV4u.Ҡ AЌn;] PdgK8/A( b,`8bfQhD68 U[\Ճ:>8Fe˖ׯ ?333_tEx뭷|cwoL܉->~YYY?ӋEccC/_~vg;w/˓#GUD4j)Ӧ2 geeb,G#Br*J70 ii1hOQ|j3V @i Ud|hsPb n:Ȳst,jq|IfȁL|eCԙBһZ J`;@ 20%%zǷ9ƍ}q+++=cwe'Idʕ=L>=Hk$VTT G#ݾ"|Z#1o~O=[ֳ>۽eŊ3g<7}5.ӑ9k֬Gye޼y?OzT\򇙄mD9r?]_ Fri' 4 J؅ aF\5P1RȮHz[)**0aBoVK/=&=nrɒ%'Mpgm2_QQѽ{-E(_ };^d1(@ ,^dnwB P >xQ,b"n7W* JL16xG'o0xe[M1hD*ц(zۮb Nћ|,gUf5^P.G<1w\wP%3u"2jԨ^ (#2utg[pg qv'饗^:-==SWsGTs[t,@J0YRΣ44蚠CN! y?ᯂEa+w&u@3i>Ku!|%Y STu)GE;;zVrrrz%%jlnn>m=ֵk6442*GwU+Wu(pʴ)?3)ͼTE@ QSDEgd/La"Sx6"Vpuh9 Gt~͐eaXm]!l |-ٲ]-E4⤽qYc+њ(d) WC 8,M/):9YGOS?] #F+Ǝmr8Ǩ6!X!1?َ]VDn2WU(EW!:y!Z!1BH~\4Hȃa',i5$xU{L&jVQ¹OFpevJmP,ERћӿm۶?AiGv F_nԩXVVt"E3cQFSL Q@ǜcD\h4xX+J—C0-piטD8#ԉO Mv'jZ©{*I7tSNNʞIxl{棵u=bn""+r8lYa-O0mb*Z+Jv\*9\f>UhBEDڮxMRMyLE{Yv7;QM +چE/|Ȃ*H>ppU|p̘1_lYWfU&b'+~nkjKGk 3f-M#Brd~#Gy/WB+SS6GawHEQ6+Z%F$(NS73B.`5j'4ؘЂ^WhR9ߞ>p 6GΝ;O= eΊhkp>X}G4X4+z5NQybt( o%Pz#8*/k:Jq*]NmN@ЈڥL#Dx E"_$hcD›hCv?qlx~ڵ_[(:{9}ZM E;M #|6Q"VJ|-S(#i9h3+L1Df]!f+\&41W)r-1wnB^uGaf.@jG(&cVk"Mljc*++?SpyۛK 7?(f(\ 8Ah;F@í)s皉6F=O.C2 ].X 70^A!3ԙ?$sҬȶs(IX1N$4Q~\CĹ~dգ(S|Q&E怈V֑A[T ~ ax7*z ~a D?2E4S[}%K̞={wWM197xJ%W#?p…/>2`,jZ'04]!r3,0Dm%3˷82!(!&]l5t\b&v #d\n>:t (<skJiGRmܸqΜ9>lT;555555|_$Hw~~[o/O_en߽ފI2QY{7o|s.[qYH]dN jI*zhHu z9aA #pA\(x|$ ID¹@x# 82,Cʍ!ŤBLwT$<1j'e.,ZhѢEf4iR׵?j+-+XmOFQZ^^{G!#)_I* \VC[{-=P ofh,ӊr݂K92Y(GE㤳hsPpx}&B::n[~dsQNns*fHP5~B1cƌ}T&Yv믿*//_pO>iӦf̘q5̜9 ++RJҽ{{F^[0H& "CE6쵟n4d֤ܳjP`@RFf)4I Y(,* a -8m 7[m0\ C'd!)XτA8Gr 4VL׬A['PY󢟩ﱜ֭۽{wmmm"((((--=묳|W] ׷׷'$a_4G>tƝ38C-*PIENDB`neo-0.3.3/doc/source/images/multi_segment_diagram_spiketrain.png0000644000175000017500000010242612265516260026165 0ustar sgarciasgarcia00000000000000PNG  IHDRygAMA abKGD pHYs B(xtIME  ;«" IDATxwזּް@1f1j !J!H(/!$BKj%z7L11{&i?ΕwJڑVt&؍^rwlp+?9S%`0'%1y%r(zep+ iwv&Xk1#iw67Ǹl{`YpKDI-}:L>5eKjOu.Y}.=vya. pKVretLtK}]2/?nn+[>#TS !/.1[ ,5 Oj;9nuovǦ. [T핒.KK~l]N\ sd^TED읩ǰݓB)dS> aK} `ngJFӼGS Jx ֋lt(VHJu~o1fʾj?l?6%VUn; S w%V<%}]{sd^TDDCbm uIe.Qk߸ ܛpH-VL?s V6%?`I0d6rM>uO | x+of~=f` ~Prfa5 u.i =%b5$[#2="""RgQ}ӟ쒟_pmv'va"pSPkwKnrTD2ȭ7qڑ[=D*5A 4"%;c`vInk]:gwS%$]܂]ጂ#Xz}鿥+tOwVZ|Kav[} : k,إ`5ScLF3<"""JT;+B]a e撝EV=6֋Kb:u;?%oatV.g?kaO5ހu46p:][إװSt LjT9&3="""J6l X.>O((\d!VwKb%U^7 )1gJ uVS @{L 6tӾnGC/'\,nrWۗ4*lS)BDxBDDХio6.OjQjxDDDD:T-.M(PIosl*8b~k7cc*唺"""҉tNwdV`6cw _ ܍Lp`Gm`fWr {EDDD',lTi%j/.VkkTs-rN3湺vhlN|K%MJlQm'Ewacf Ǧ.]s7leX&5}%V*(Q(~Mnm=[RV~n\\ @ l,pKwv&encG,-ۻ'd) uKZ6WYDDD:M6QR'.ajjvl/:Mi?ovϏKI׸}Ihxk|I &;b52,ľ͠ܮƚ:4>W`nٖ kvꝈZ\]~lG!g=V |Ki=hKqz('|9-I7\r| $`,V[]/MyϠ)\_iX @8+`}ʲ-l{c>HRb~mK 3{])X XsRf=xwՎj2pT:m|9藲lns>N. ,_r8/}Yv&pKVp CI謗q~. {/u?{>z=7֦v*HWy WvvJHgOT:]kk_nE X-.̴G]إnoaC^̱XGs\1lD]2\k/+"""ҩsKg˱KχOe= !lاL\z3G ~ |v9x%ϸ^}-EDDD:gisL%""""JTEDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDx 7Gx x X^+$cq  u[5$"b-["`Wn/J U=~=S&^$≯EpXXWD1ʝ}4o`NSAK_*ܲ w+cy*D;6?RH.v'TEqNvWn=Řb];OT__]ynOtԂ+Q숄n#^xk4kwPV]g lX`-'J*Nu.Dž}(..ſ .؏KWfPc?i}8@11tS|3y.wsLq'@yƽxX/vN󚳰KtdQ`{K͜u'pxX]R=uC><(sbX3(3kk?Q)rTFUUt+"*q'IjWcпה3݉t[)OZN }]I>qϽ(=mZ =754 ^05l{ڋx7c1t_UaC}\D$c_|'LNmVnb{]ݟfp5hsW}SS)DU#?hlKHt'eB$闁i:CԔh~$)QUG*-N\=:4%";/?} x(qzmLMDrBݻGc1Q|Ma 6"kic }>pO\eseӗˈ~ Ga㩾S>U|u <5AnSP*H% F$z{ݝ7W;c(U݄w{SϚ~b֓v 2mX'/wKc1X1s+ ?•߬4 ]g]M4&j5u{,lZb|lƕ*,6;r[ڇpR{,-_ <;:cS_a@i~BŘb,'a|j }sϛ ŗ+0PSSb*q"^ؠ݉Sf^rU`>ҼElk>,wI>$q W3XoHs4mKC|]Al_'~vŘ(DD 6q]턏w(ŗKTcmfp)uZ+ŗ_a7`mΣ,1Q|H{DD*=6|=>: `Ui~*"ٗ<갴{;emc P hv].WUޑ"JT>C⫭?U֝4nR1{T""ũDE """"JTEDDDDU%""""DUDDDDDS]T """D~V1U<|%p6 I_Á-gT$"""D5"nq . <x 虲`(pK}7&bzR~YD*}LK4cil&?a!/7QG%L0QUD5qHjkۥJD^.AS܈?.G|  q,v'a!ɁGu`F*|Vn vI]\a}Q2>Ѯi7)]'ͷQ뙴~xڎGbÍ5j6Ik$ Ta{7j_z-qq<_3kU.""Dy3ֱ*,*4M,><>2LT}*݀7bi{|0$҅w(mX ?NuO)H&*Hn 㫏u?FK^w^VDDD kT%V{ۅx|gjFѳOwח`$pm ζ4 (< ܏[|nM|3o>JYҿ_ԹͼD IqlbJ{>LZ~/:ZY[Cl/v)9IA: cq׎#mxvE+J`#)*{n\ ~@<tӓxTss2=BuY9_!Ea eq9&m"jIG">5eiBv3pJZWz]eeTTVQ[x_Ay3Xo릌.h^"٩|h$I(%5X'txFx7^B !]̚դSOL2 >oŶ:w8?"Ŗw[b;'Շxm{Tv5+ ,HMuM#غDET.]=tϟx!p4܉-1"Y&S|c6M}[C5Yx m-; Vs[ڭ&<a^9nkqVzIq)!YY-o<s 6$ԕ!t`>g>ÛY!l>?;e'04m IDATt<p;n#y!. .Łc5*I:LCpR,&wtS2O:JܩFw؜[ainp[{WV d7y1|<0[Gߏ a2--ӞlE"SZ6%)W?r?8ސ\Gcofz 虲XX[fʥs]›oOcx%|W_% [ؗGzu@H2bѹiYᶺP^ Ҟt>SZVM ;s'\B<"ɃЇel2.4ں>ϣ;`iE%K䰿O#7u}?IvT4+գ/W'E=vҥ,[%_ͼٟYۿA<^ra%9 6_}0q<:c 9O[̤p~o󛈈H&<"e>~&=|3/jUI` "a7l5+ĢVLݨF9׎0 kb5e2l`< G+%"""a V5۱?^gr3[kj?/PN>m55XX{_0.NoZ< ~ FbϥX`F?/`I`lLMn.ڦ\5*Ͷ"OY =pbɏ0jT7#v`Ԫeʁ״e=Iئ}A_W?jU/jY/pGb5_M~rl-Y*ci XǪDGÀ7bp/SttOiY5M+*:7^b 4݊]X;֌3`KP簏) PF/NaHw5pvDl:is#RDڿWW-èQv˴Vx%@eWIss׸@-$oأK UN)Dbw x8Pʥ{Yga@¦{aXDDD|Q}aCrbqS*Q$p-V-JTEDD+-8֬rfŒ3'7oU؜?L#.-+QMVH1$6UmEߍ,[TG,]PMyyX籦o耴 l~Ly\IeoT$"""R jжԬPz'))yxF4DJyuc..@yGݲBulJ?GzCVS`}XupGEͩ^)Ykww5 6n _9QԠ$': XbbKT'QR3}{X>jk*Y,Hi:6?8T[X )Iݐz]\>];TlӁkh *)DI;97= N3X'c7ŪaK˾E>R{UM`DrS9d?iG96QdA^JDDD1Q ' 6VXsvCYy=u5۹mk ZƚCIQ?2)ϯ E``[΍X:LTckq+JIɣG$ ^`[CrKJ1QfSj h *\ejk:H1'ki_81ʥ?W1\9e3EPZћmYWNiY ul? r$:u<]7s2H=<\ Њmg7"PW j ~zN~$n[Z?u${%5~EA3~o 02'du邓wEo{ӳا9~ӰK?:p |O666m.C,H'RtOdi9?Urn^v}]M"k7Z n\lMtڍ)d?{bh.)Du+4f=\: y]>$6ֹr@8C`|ҏ1ADDD i<\CšSw Xq6MYUNwjWKDDD-Q،d@Yy-771ͷLL| _>5S8-kzBgAC(QbLTPu*H%|x-3~-$ |Va0 )JTaݱE*i{jDIIobѥc[nL<֏Xhjjf>Xl~ <2Vwvh=㝱 #)1DDDD-Qݎk#TV 2 JJ%>jF)CIUӔ}swvsSPH%+t}S{Tt_zPV$XʗE'':<B>wkaZGeXGtFa53ٽ>ƆǦ. ,:6M㾦QL_-)DU"Lg}=ƞN{Ɛ>v#Jֺ-Ӎ.yȪܽm TN7e:ePN'o<p\S~.-Ͼu96֝%Xǰ)_ )yZ"""ZawzX11_ZVO,Z͙*J<@<陲 TNۦ<"='\K^pp,k"/~O&yg3\@C FK<{a^$Zw|v:Sqݱ6ģǸAݲ{ݑec Σ-.n*هv G#RgZ"""ZG?Vx VVlޫp;3ubu;D󎤤8- jk(P9Br!X'9e/kBݰAg0P_+ C5}` CUcOc~ƒ?@f:槩 ۡ_k5X3S1>XC 5 MMw?VUݱ)T?s '.Am\n~p˧3^/ꎍf 2ZWv&Z]L{v&~oK_#~gK!z>6 .f ynMrxKZ=͝Nh[aҷ.l{tg\>CZ\u;(Wc폃;DuWB7bx*Ej~͍*xՏ,'a:H.޻c.zzz8rؕSX'+z "Xm~{\I}QR(C-8;1CE31҉cfMnV,=\qv)Xvf}tjϋ](InOYY(`?쒼4x MSTc cmKs*s$7;]J8%e4%-ŏM1ԜnX? H<]L M_*.m%bTt'yN~jy{>ؔmSgT',LۨtGt6ۨ*WlvDҽW^1q>#Am ,v.hj|?*p54U0qCvJ@o%&V5 {s&{S?ANQUpH#貿Du9H^eIϔn} ^;ak\tEDDGI l c6f&񼯀=3^bf}(T{>P$ QZOy0؃ilEDD%]zN|0ln'ѐ9,zy@gSnc"j\"yʇ}Xć}*ek|7k>p=L3q 4>G}Q>tb[JOpye0kz;\v;U> `R߮~>lO?f|0ϲɛmFڻ-HYEdGJ"^cClG>=1%mDeG FجA7c7v×w`M6s3ڨN~Wmclfz߽kVKY~ tD'p_/{Y^(/bmгяK <=66s<6S7Ӿ8R)F5~ x_ d c%Ģ{r3vfa@c] N6_={?u"]ԕ=XLb^vDDD&ÈJ],OU^7+&fkC8U`sa+<-x$~j>!Qk'j Ke,|uIdy]NsVE% /E 2pL&Оm_On554 lGAܷuzp(Qp0v3I%VdMF/+ʮQv4[D*Yf{"5Ģφsi<@g (k8^%k0Ӂk%V#C*yNFbWƩj;`\͡:|w`I<\^(si|iI9ب&\C ֫#?Mji\ZA)i_D&Y莍C%LodN+20x󙔖ŀ9 dF(ilC\O,C ,O>>,7zǿgvO8Sa!\ ]D3[C8a +}z>t52"lU:<"F"Zif8j)$Yel}Dvzlߏ>zV'P$`=՗yEi9 c*|8=Y"""?Q=?O4]zx-yׄ~qT Ӛ.v uGc^8?8Mx0eY:""";Q͗upל lKYzjk6;Z=v :̱*CJ:`^nͭ&Ky|. =kg{wki_RvR"""޻_\zU=O!ÏLKqNI4k;Lw߮i{`MFcc'0~W2tDu;Rz7;p֞5a\%s΀CbÇ)G*Q|*^P+xVno)V[:u6+S؎Ǧ_D>8HgMT? _Ir]Z`! la!Xsb|)H>ۥC#Jy׽S #ٍ:K^\%i};Մgۋ.ƽGO Mըn do7ߏzݶ ߋpp!6"hx'ߏXڏa614t Uը~]|shi[N qsiNWa8 x;+= qWˁbTUS# DDDD:n=aQ>]ƠAh)#v.*]|L <mnH:fpDDDD:zGmͮjd6{T>\?k) nƆMT,"""ґq()i~XH|l|x!(\6X5xMDDD&o׎1LK?%Ox< % $ת^ tUHGMTѫsĢ]D*ɚUQ^9*_p0T!jAX""""(-=ZdUy_SVB"彉mK}}9e5L̗8@2¥ہ`Sǵ%Ee1kWmIAlՎFscw*kV |^}y,¥pWqHGOTVd1@`'%Ef f-RHgJTVc31H'DDDDDUQ*"l\5 FHhmb{p 0e^g3%xjM)2 U6M1\ e!ITE `U|nvPrXG Ms i\:| &y[Q5oˀ2śb1Ld:&r c2C7{ΰIDז?4nte3SUSus=K#_*ow*,Y*c)KEkL,cչXSsd2p^8˵fҟ쁄Qx7n{Q*.`NWr]&]?GeOm+bl' Մ)-}NY^ m:XBˁ햇 c L^_!c"^!B> \N9',<mKY4Mb7Sv:r<_ʍ>G~sB!(2WVSMݕ%l5sdn-Ȕwim%*` #;dGtR1о˰ {&pWDs ڃ'cC)E+Mmg&nYwp)QQ*_% âp꣗BnXUEX*Z=EzL"G`6f]\p_-?:0E7cRвމB!D5|T?B:+mW]uߦ0?Ҷyq_Uҫ:e8iJvlX|~1 R [;;NB!ъOly ,W>WG V!t`b ss>^6#[Or= ˀ*WJv1%#2 !G+_##Fcײu;zXT%,l )B!(%Jio\79[тpuG$cyy7cI!J{0X,"q+;H"ۙX6IڒkW = LdHJg6*ӵVc>%|oк'cVscOc: -~Z91/4X!ykOV7C#\_&do Ln C7JDG)6j轩E)}D:ǩ;ڥpɿHw7IzmolŽ5gBu7HFQRE/x7P&_Lֆ*%WTGa*#`Ňy#olzlpD;KW{0 hBT&rLV,Y {)e`VU)gh$o%D&j}Rג!R>G,@ec˦w4l; yzG,QP]'?m4@2`c [}~h}WG I]GXױPоL63X$o%D&jo`K!a8[Avҡ`~,?bArRT`.W&E4;Уlpҝ}O ~0+tv }Scu1nP-TG`*[:]j1eblG2*Z$FSQ ox-[({s޶,A X0UQl޸ ;(|Z03Yrs [-α1rkvIp@:Qs,us|˹'o5Xl'MqVM褐EPy+7X7$CGio`7O$Uv 0 n1Ga_*%b). Gҹf9_ˀkǜ;oa%L/*<ۼLR-4eH}/vr|K8oאcZ&j끯`Ʀ%^~%C#7A5A UoR*VÝ\jnձTլ|argoU,dXgnea7y 'AσiTY-XoG 0|)eo,eby7X(}+[2T&@_`4uzgp7z>}?Y@ʞǥ/ &X1CBe !O'2y89+V%㼭S )g ;;ǩ~}sscTƾεv3cv_N7BϺsSjqg~s>9>5uIR>19nt0MPXPeLY>\n>}19Ͳ80[>αsw:)c;,sr s:Gs|ܹ߯jQGeK}A~tt-x 7v÷SۑL:lZ-PL^#ǗZ1  n]+X|=ˤspAZ*B*,/{^ݱQؿ/d)ǐ>Jp/XY`32]LZ8Dd~mǍYhN_.l9X-@ۢJ]jB'Bb86M%9+Ɍ {J^Ŕu˧0x+/: HK@9 Ar М7oB!D+a۩\l*ze6,ǟOc fu*6/b)>'1i_3:IR2~(uN&̂s[GBAƢZ{ XZ7z'?dXl=KqnHK=+NM4I\Dc"WKzp^,ŷ8-A@9j)r9 ت ?Oze=j!{) xoRfyUhj p%u@'>"tIU|{)Eu0e+kD`M-9GUPBn,ʳyJ),,طɴEo,K>{Dz֍GˇHN,|;~Ȕb_XbLvF@[\n>93w#\2B=q4"=l(#"/B[1*sìJ`)4RpIY_~I-.s>/k8Gԟs:M},mtɡ̄3&ϲEUSJtNke*tTs#KaQg}mCJR]Hc} vy}/a(ѧ? YTN,_.J]\ &|󕍒&l`sh{Qd~,|ef `sts(ׁq mعVb#^BP5x0,7w v}&|.Gv/CF4RUҙH0jE5싹NM4 b-,{qvvdGCi 9j*x969sqXT+ x9ʣuc/zs#|YTR!(&=ҢZ &%^r#AGz 5x|WJU7(`. g+ɿSXQgrFHQ*EUHQRTKL)l2i>Yq( q3ch뾅UY̚D}sO%B> O^ 9o(>yew1p7pPW&5(N;B!>,Z;&jK_Tn~RTGa96xn߿ZǍ4[Ty{a=%?Ԗ*=8}B݉QQR<:5XSjjdF;:fgrN|))(ScQ/cw3!{Ɔd#.`0#'҂2_ly;61jSŖkԍg2ьNh&7ӧiK2ph-+j;X- }2/#`}1t,|X)2BAPkwvfg.ۭY}k>!$owMXsɛd(pz]rdHme.Gl7ƀC1|䗀2#Jپqs|`ý:9o6X_s{̷gQ_Q>'d)|wlydh`Q-8.ANWR¿IFhKFtuWѬ_OWgcS g'rlc.6}2 [\UY%Z~zK;.KeXUKe$+$RxgbgVF6-~/cSd{&AHV/y+17P{K8dHQQ]Ev.lfLJ 5%|+"pG1Eql:UO4`O,^Qd/"6ulkA?ڷڿ̇r7#8$o%F&joc^&IDG(`AQylaurz2_Cn<6_Gd=Z򖏼M2߹i}M2$JJ˺Uq{8 sc.Oy_OGu:̂|]֭IdtƵ#oy||%\|Tguճ|+?5D dItׄ[(>tŐw\2T=G5V_#kOe0r$ʹt[?d]E~ӄYзj,>fM/-xM0rL/*FC>>CV5'娐c=[جV߮!=HT&j x \(^8WIqN 'idۻaJ d#5nY1Myf "H7H$D"2-W"-9}sh"qH9oTaS{--~l:Obīld& |[:vF0pնwuĵQ y; _#ЬIڡ/ЀG2$ʦ砱M2V|8>Ivf֬v7-ҠUOWc]2#,e[ `siJ~$~[\xk1(8v} tݓR/h{zW&jd@2$:DQ} })T~4֟OV+o fwqn u߻|OUJy0ĶȍzB!D!VV?Z|8GM,D}ݮmDcC5}0~ .6q&6zjB2.| 85hb1651bXX>V6l6 IDATVU1tf"ӦOӦE+aH9\"AcW][N"\Sl]D& L$xMiӸX6mNh:}u%Mi8=HpM NƑ;'Dl*8Dq-I abQ ?c-XEHt7͑M _M cg`|+/Xn݆D~i ڏL/:eJ\Ŷ1 :fd>%C;:6 ;9Wٞ``Uqȁm-*Q ;w4 αs js0R gse1%?C`X0 9;<6=bXbU_UT%j%U xHX$6%AzX)wv=N!g(UY- uk?Nu#l\ڴX IPtʘN {t=ˮ7-cX֨%fq"HL$6j@=7ޠ_"v^ˀɓE!n;$ =|.,v~+/ wG?cf,꿼f [q:V}+0Ǣ#GmM;,ed$Kg0,*hq(,gnYhld,f-0/"wGm-7An9]B :*bgqil̴L=I $jxOhRĶӏƢ?Ѭ-4 +8G 8DQl ZYݗw3b11V$E<˘[N٧]QÆ]9p}V~׳縵7wd}imGY+QUŕXKCs׮䂪*{~cvE"ɤ|8Gas Rae LMvYJVkY\?t!I|x"9P_Yɮ_2,`D}3_ \HpάLu ;xUW\mYT?JݦH:,͝~ꈮ)qf?oOlf*Jԑ[;K5d F1]ttT˱J&?}_XtsXRJjwӁ75C!B.F)˓m-`41'9Kw:o}h-Z7RJK P w2Eu~Ȣ}EU YTE1E|x^Xo,uQ8|ɟx~/XGW+\ߠ<9[;Gzt&O?B!NQ׏ꚅs**u"o;hlxUᜀʼYGY8{[nTt1ˡ6:9~ ƿ.~]!SAo{~ps.e^ OAHY+y% -Kpa~:|cV(2 l;66WSoJVVkg0[n-ogb8FS#'^SU7bi}&S2:i%T46Afp͊gCM#N'5+іοù8>s-G/Ӝ4n@5lNRӹ؟_ڵeZJm$BY1qX$ompSZɛd(p3T}dHDSoX>4z*xj p45: hĹغW;ɞ8$h/Xӱ +a~MsUC%[xiwĖԠ(EK^oy߾fX9Ql<}H$CaZ5,PY2$ڤE "U+PU&ս^j1@,S!լYYIҁ@jc/C?<%%2a@ou 3ub\¦^ؽN~Ykh$o%D&j}%CUX0z3ũە-hl& AJTU7QQ$3!}SI~]w 3}Wd+_􃝯śCplk.5'23{IJM2+~t(6e;#m^g cP^vcj)iv^GۂtV?o.Ta}SRE/_ETd݂)oa#Z0䭙LU*g$C`"r(fܳsj_bݪbvռ|Fu-mC#_{r~*D~t:,! DRـ|re b7"Qq=[I?/]%%QT{St0 jTVP7u[.Ĺ945 :h(S_`-pI/}XzgiB2?[PǪwUzj`UnrncCVgL/*<ۼ`ZRTt2)Ln`B8XkHIz+X1%^~%C$ਮi(=j썘.^Jm_{}M¢ge%ƱCm1i!q"?=(Bԩ Rx lcN򨾇̣z$x+ߤ3^@Y"8=ps]p mp߈x,.Atѽ}T%oRT Q IQPJa{X԰WZ[bdr^֎9{͇ [@ #IbڷrC4KKt>f}Y}brGm9qzw ɐ,[|c+%oaWZAw[[0\:Xl3+L t\b; MYTCXD,`W7Q ?Yg@@p9 XׁBѹ(M}CQ_ u'PME4H.VߞH}}d,ڧ2Odžvn ̷^,q` ?6 9 Ng4]Fn=pG:?npdun.&z%Mp;;\ǍrmJp7̯Qn&>%&u pz?91:\}}n,ǎwJX6F9;RW;ԝ(:Ɔal|0nTQ9.Iz%r#D~tHp,ȪS"^L1KrD1/ $ 7 # 8K=vJBRYT'O&b*n0"{~n1ޅl3ں8N?d7,xk$$cV;ش.r9[SZZJыzdHL:\- \ zE!KQCe՟pɾT|D]#{,+73B[\j,eI&x:s K叛K@k$1kꏱiXZbAk.a;/9\U@6]E7{(EG~}Ha?xv%$\|oJSrt)Jh?2WO)R,+x>w >Z5󟯻Q;Xq>v6PKf8EMEU~ctTCG|oE>+4'\6=HONpc|$9G9\7JL?"8\E}W;w"7_ %D D uKŢT4<KT£1i;4>±X12,jaA^ʱst3)|K_sB]n70I<^ 8`&Q-ZZE[g^/PU@, =*G4Ok?Xҧ}EԿ(&=rD}է5u[%Ze'c;{T2 %;>}?'vET_9o,w`\Kk]V`FڟUVa$wH"ۙ^H Cɛd(̵F,dW%CMJR1捃ٴ.dվ2U#卜 MbD3Dž_I#cbc /bQg61Sr-nrc7P&{M`VMmK$B8f4Pe գKYQx+C"ay\cAFT;rl,Nw/Q,f)e(ɞ*M26/?;ӏGLb Q`LV*S}ȟaWSUߍ4>HO7uIl¦fzĸ?/9a9 rLzE1!y+&7P{ŁHDA,@8**  8 !k f2O̺"3W7@G:;SYRac}fוG%aI?ygSSDJT! $C#-b0lb섟1WP70v$̕޴/u̐"Ru]f=|)~/_eQ Y8\-b[{(1 !)Q)5l7-{<vog궜 VSy+Ѷj)v˃BOYgæSK]3wr< 6%6ܾFS2 [)IZc*Uvڐ G1lC{O)x@AxX1,ŌȯyJKgglZ&g ̕7)X?7b,ƢoB Bq,-Q߮ 7PDx (ˢvu+7׎"j&V.gԸJ6o"Č/427Q,7R,4̵$~@fbY% al(]b~: $omq.$FJY)y C_,U%BBɐ(9c4e_j]lߏjq?c?7IO\W6iY3Wz-q*SIJ*SI|ToEٴh%dS P]x`U4D3\o;:- MHf"Wǀ!Yr0XɦJ` T"EMG DrvH'V$$rb'鸄v i1a՚q"fM#B!Qgnլ]yp5.eղ @ܻEZ;cQRX`~hјH$E5aeHRihRTBnyO8HcbyQImh][N )|T4|M(࿁Tg7Q !OWHfs+py^Ȓu foH85vK07 BHQmϜ1ߩ#PKAuv#F?(H(1mPQ$6%+ +A s؅BO/x^6Xl3VtNDЎ*`shW[u28Xbx%2 Ժ$[ 69J]c!}B!@:E?F}}2)akX1`&Ģb'o:s#[5%裵>4P"ܬϦAT"]jXfͥ!j{7gjsGY je%x&SaM5le|1x ~V{M/c^h^ `CmJL%V{\O\dQ>z (4˃:aþ8b^O0˳mqprKREq>ꟜNcNxF%6J Qm$`e&F? 8I}3`^\BUJRj'd 6am5ZpQ7+^'a9<UE 'MŢS3BhIh=+avªݙeҔByKsa6PX ?Mm`CHD!c>Xe! `6QqpXE ^N  -#蛰(\ Z{8i;z,0ęUPˋ35y_Sb HOciNu0+*U !B1)xGsl3QQZNPmk2H4o%pOXј>3~,>h_ MbNB(l\-ib|$V߿KN!10zT?aB!tFEu#V2SD9bHג%Q`E, C54D}708}O5)[L!b+3|~AaluU进O\`D:ҟ):dkgS> -[h*BEo\ɰij\+$ '=\!Ɗ܀Ml 887Bb B!=DQ]㺞cIDATHalk֯EGCGyEPw`QDc_gfFK5#2=G-s5B!DOQTkذ&NC}_, ?;U5aMŏ<_sZ+`RVHaa`׫06|uZx __K!K)II&Zmf̸JI;Մ3O%fB!B!B!,K@!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,B!B!BȂXO!B!B!,HRO3~?/2jgtyB&aP{\,:%}HB!B!#P GP.!3\ED/Rc:]k[s/KYPk3#s>x?u]4!?M!B!rxXOnRJWDN^~r!dS1B=ڋF|7̃yWi۶uxk|qN!B!Bam|_+֊)%Jٜ?6[_d ! v̹ZͬgByMgnƓC:nL18_LO!B!#LrdVѼtM_Xr:OE_G/Eit}[_tszB6- ]wᔒ?gi ֬|?|a2[>щnB!B!!YOsٲp@_\]DV4]pxYg ?'|gRV#ds]~{>o7޽ W4Of\=:JO|? OxSg=?x !B!rtb}_n.9tΒeO~ovyvt~>,]v|Wrm߿kaA<̧>gyg?Og;t?I*>ڕ|qg@u&>DݻwϞ={򗿼o߾nͭox?y{g> F@d9juc]_]l.?W4.>ZB!ZKͳߤ_kO[̺s7uY9姾{M߳gϮ]\zjeCQ9ܶ?t:vss9'~~_?z5rԫA12i թ\zߛ{r-_>sH|;>O9ukv^<1H!GkYkU\?=ֿE39!B9F_,M'餛#&Vu{o uu_o-ru}׿L&骟`ۗv?YnE/z9<9u)7ʵg+9'pgx_;nwSE|k|8F#N;^}gq_uD 8rV;̃|a#"W@WKd]_B!B6 -^6cf-NTjyz~=x\sΝ;_\|N?mgS=MLS cOzғ?__9O::sIk3a d`MXɜUuB{{キߕ8F󛓍|Bm_?;\V9û"cݺkS,~}ɪtvsB!B6!uH7}3fMk9xm 5ﻬm׾}c]w-Rx.in9({RWKGFUp蕯|K_R7:OMjW7jzj'>񉫯z׮]l7[E*F]!__>_W< O~?3:K~}pa֖LM7Fn'B!dc{7wvlX퟾3SJfEP~_}O|b߾}]ϱE/tNKg㨈q-Vm/}_m۲:96kO (\wO|vVkR>rjky'CB/׾\p*}#4߷Kn;p}ݷgϞoVU yw񭿜|B!l 6u(0;o馟ٟݜ7_RJlRj.g?[}]wu_~}e*i^zaN^~SN9/~_|g2;sU_rU˟ܻw'?|wFOjtkk}7H;?0?~~O}j8CB=_<7t׾ٽ{]{XO`ϸՑ)Agmezģ>g>mo{۫_jg+O~kjs= k|rU\"[D7<^u "/~_W:r֙8p-޽{{ݵk'O&uW}Xpl+"B!i g&3-Usܹ瞻[`[9{uUz+s˜ӂW\q]wݵE=5ǽMoz'>_%D٘>b\]w>^#4)Wo}멧ch4ߗOZOscV{c!]vڵ+_7|wSL'B!e Ql??¢Zso~gqb\c)|lf7x__}nMj| ox%\'?6׹毵@\;VTl}ʫ;zGnNAwyo~/~Q:tUHzܓcg ]^sQFjo~_of/ ~B!caM]EEP6\x;w| ^(dΓ/`_ssW?vgK׿K.yS^~=X"l&I}eU/k;|E#~mC^駟we/{I'T/!L&`PZ$֋m/~_>;sN[sD!Bȱ E"FxWϐ?X'йR;v8s% I!+];F6sY.xUJ)/>3N}f9FX} {?~WzXO!B1q?~SF=3 _9VY|+{W^zwuܵ]8Yj֣5y;ygilg%=L/߿6OxRR['[[o{FS- 9X7:ef]wӟw[f޿#a50v-K!Bȱfئ${AU9O4wXuZkuرO~Ouq{1.&ԃ ުCUce0[j[#"{.|t:e&+++1mW^y^m۹Y]-{rB!Bȱ)o66&3'I՚\^zGPl/| {o}7m?:׽}U K.3??T뺔`0wދ.O?Ao]O䨪Vz::n3|k:>w}?×]v 9{??%/y׾k!;c/xSO!B2 yGb<{gdcU?x;Χ=iW]uĄevqE|/~?Ou]WVFLꗈkB!B6:8Zg};1w\d1K/}:lzORY7$]v~]vٺbmɼQPǟug2S~,۞D`8j;|^ wsJd9qZ\~?]w]zg<+| )E z:{) | _H:e:o_6cu6ԋַh]/aCFhM~R(Du\u;yHSzJ?y׭߉ۭ_Mzl.= q^xt:NS^ gqY !4&/PoǮ .@4OY0E@9ԦGҶbuzJN}(=̏Ȅ`<G0FU]wͲ[2^O*M@թ&Gs=G!W~vc7 /zыo`u}H(T[owS)~0s<wsbi]׵m;LQտ˿#[Gu VHy[\rz0:NC_^qI^ƛaGGgywݷ-J)yjQ+Fz_cF5 vygMo:x𠏜뚮-#oPCj́~7~λ;u|=#^.[A#/B"o߾.褓NcBw1 ܏;rB?g,t6֒폺߉*1tl:GnyC!oɤ6uoG%/#^9i=/!XY _4qu\wJܱY!HSfrf: sWۓɠs_{G=ik=0[~WzWDe,n? W}o#ƘRzSwޣ 6.kƳn/o~$$"}k o9Ǧi !ǁXUZ]zןɟ,//{܀P\U[ֶm?ן[>{W^yiE0, <ԟ>ߵkL[ Jm6*z.cnW` &" H0iD.DG:GGlGY:9-šYvE:eשf|G_.HnOUxD5@a['}maG8lߏwUzK6: "^!v57x ^TKsGq[/|p5b;W= }^'=jg[b򫻖~_ w]E/`ޖL-RuI'+_'/&9jPq>^Xя~o~\[綬u4@ 0`}Wo)%7"M $?У kݨɆ$Z`D*[ZS`[b1+V1{ݪDf,Af񞙾q> "G B>S*[/(df ~_m kwv]Q!@ uCȗH/;w'Ooa Z-k$I;*dZ~),_?пMAf&ҙu #4%56wUUځ!$3(Purr,"Ⱥ;cp?a2 LF)g"mz;( ADL:5ZR_|b}30ٶWRED fd]9]E Cm2A "bs+^֥.H &JalPetf~{nl )MSHJxYPYj6@+k:*`ݷDQC6t lg#Bx;^&Gnc81pvW9D;rddS<ϵa;V_eWH::k4Ua:L$Mㆳs=wm>bE؇Fhl VCuzU7kW]uk^ZFu5!Dz;w|o~W;<Ͼꪫg}{ycэ,f0@$hW^y;y]w-yfwfU,sSr$L| E013?_7Gy@`]rDf^DD)y;a ``@rPRJwVn @ekSCE 5H0?h/I,,8MvA\&= .vI"&߂79[WSE{xn bR'&f~_\=fkE1)ʋXАJn̲J`0FgYtϻX0$o)gwv%۱Jy$ DkBG ј7u/֡yW!U anX6,+;ml.y0 1*"LCjڿjY1(*_ @MA$ ET !B!m8.(,T@TL f*p眲ɭ ^'X@ 3 ͹$zV] uZɬ~7{^AV3T!&=uLf(cぅ*jK!KaHZmM"`J1`Q`XR t $H!H2-Y,䫝R;`OeI%BcRT3`?1ht)0 ?%D@y|`b}OҠ"jJdqixh eCI.4bi&@:hcT8/ږ1emK|+\D$B>`gfCDA0 br|ȇfD$/H!EZX64ҦN,"0BQ_o> !,:Xnu0$Dˁ c("!vDOO,V,юXC iڞ", H$8ϧ|Otj=f+&?)|OI}J֪4(& I.Xf_by YC9Պ W`ЙLf=:%+j3, 6>>f3{A2~@̍%R\O9Wѽu}٦QٸPƇ-[W& ]#)nʿ"Τ(T<>e"b֔_QH: iu{N;p),SJU}m(ih /[ɺcژ 9VẺiXݛIxr-sWU4X{RO;)Y $jlBSK_e 7v B 7s'1xN E.+%(J+E=kG/T"Ek 04) 06 j=r5am ;@#h,FyX ,)Y:R%KpNbވ&B'a Z1 $xnoiUaRm]r}_g]5"T-NM BFF bR4:>Sg"zQig3IdA\HFWu^@ ,bBQ+50NR@R+SکPr\w#TF`.^P !~h Alj6L >F5(2Fk Hq`3@7Q Io d-{4V:F&&H h HiԁBA|iʛ7NIH E",+E)VSZ%ci% AX5iDl0xYRWCE,T;LCΫ9_o(}uSBN~%N`*֙L u*b9Xf^tS > F_%h6TWU8\zDM@b=>VPsxṩa*wMƠYgxöZM!N U/0B!Di`@T* MhI= D,!y:)T\ Ȕ c^y1PrKMsFvB),m,Iܢb7h[͖1n[l{Q'ސLb+c3#VyYZG~vPQē6*,ưNPPbNXiYTJ!܏$k`h, ~eHL$ihf L$C>O)jLgM` tj&b]>@'yĊLaj28Р f8[F!,6Y-]޽~=A:*,[Cڷo??x.U4*x AϢH)_lBX=yW|=SkrǶ0)]R'<=شww^Оt+]!؆djSTא7&`#͹` ) *"d=bGV-E"BȻ%`0ĭHd{1fus%DMRJ|޺Yk/@Fbۀm05 XEA`+<:KY)/(g F b#v`0$65 F9!eCH#6t.r5$$T,iʛqY"!$o6$fIjƨ'ߵ$0Y?j*@Y^1:D 1 VZf^5.6mJD8&7XK֓lkB *\p/U)%Z%3ݞ*T_s&׶wɼ"sq ƒa%e8+P2S[yLyw!?#@ZuaQU@h`KĶ F0MSX>xR<| ~EAbT0P- ؒa )~h 0xꫮ_rV!""AI'< D ZXk@h F"S1p~WLa"0i$4ZIe & D&iH)eEKGFCAͩjH&"L &ɲ@c҈Ēv= &WgC0x2L`TP~9Cڼ4!B#`d[$xd [)I;F&#`@,C0'8[1a0n;!0؊$s4X,#m!tT4زIljn< = Z` 8l[lw'3q%Wfme(15Q1Q7G/=!B)jaKH4x[FGLU-$HrQ]g+1&jX=>"XXŸM!Xƒ`*ȲA;JAQ1aL@#S!+$C CpafX4,Ȥę20D=8`;L%(P}BL &`DL:ΰ$! $DH0lI1Du ⇣0]3IB'kUi!lIldd2,i5d$$h V U/n7{(8;XYZ2{ܓtfYT^J"~hm@ڒJ?&-uz&O*|RFMdjr âՋ"s='rw鶺,%H10f .?jBVTmIe}XJISBr3?Zx I(<$<`4DA^,-#֧< _,5~v)f<޿ܧ? /p>6Mut:Hz_lE헪1;v/ܱc 'Pky+]ˬ_Uו.ꫯpܹsιꪫP HtVr0̊k¶-3ٝpsbh9 HCy9+6fe;l %`"hK cERK0 LBH`D[ 'Ylf:Z>OCXlg7Qks"cbڔXT]XsӉB`\!&$4֤䱶`Rsa^1h]OO!"Bc<9àb3qrj>W"A;Z x J` LLC2l) Zx)g$'o,!И3󱗉e}b!p+5m Е40Q,A}*T7@֘{#?DwbXoZ 'zZخC~p'ڼI0լš+-7JF@=9ZJO3PK&`K̙O6*(;`2 DS SXE/* &LBa$or*CÖ[<2)VcV0!@5|@ɏ:GShEFV{2XBr&,hD-聜/jr/nJ=`6򷊢3VU-kE,8!ra3Qvtfh KW$2UhX 65K%$XSϕѤ @ZX-q(y=И4*6RET8# F@ 週X-6TekD\Dϩu~D^:U A `0L& [Km۶:o<.|sΝ;O<ĪW#r,+54ceըӶm W`Ϟ=s 7<ǧ02Nb 7ooc?8i$.d{b ۀmIpa[u= q+y:-;E1s*ؠH1y;gV&VeP*کMmsVc ԅj.>21t\ΒRG _A4 IDAT&ķ+k*02,ybI?xN4´,r"4+hM`Q- k9Z M r[]fYi6FմN3C/f\Uِbx" Sx\GfXh%vqQoHgw!7L +EU;:-dmx({`HUu(hN+Lu1 BRG,f."ڼsֿ v 7bD-'#['a*٬HC0m$4Fb`L DyE)"Ҙ c?ISI3F3'WX!tqlcU3K՟)jW͞,!w!6Y0 w[z2wJ;뉽`ΰL(FVd l` 2:*L+eK=cѕ)V[X1lZnGez2 +6!n} NrI Zja{N͙MEA mԅH@ Ǖ0X$ "$mqS\C͗n^]""Hb[28(rљE͚{$ Dtjk`~bLtvpMgTbGKaM_oǰS;*2 MFA%eed3hRv6?!kEΪ\2 @żh3wqèM jl>Fkbd]&t "fu)?z`IqZ{=o}{Ϧ5ػy=]ׅDd{;NME;6e8p_|ŵ"d->Ȥ|ȟ Yg77{xQM{?O6! i51d;`V]]G5"^ m,53[ox\Ν͐ t,NF9vΊ=ME,3z!g}4 ۂ-F 45]sإj^ Բ4dlS`๷@܂rEfd8}Z̊|)h 2 is1@QfYZJWUM20;Xzmi:X`b#C *PoQmP֢T3Y"A:5 m4Щ_2f.Ad(jԂLgf.#Á0 ۘh=|rCEqj* ^+:SmJ5Gr9_{}CWYxek_?sy {Fcf }c3zxo_V/6_ܛP=h~}L^]BW0*u@,kn$1l&DR,֠FAaG]hU\Is$G3;{Ѩ4h%fF1q܁ x:U)V|U8j,9x>r'8z3{5z nrM p$e`F k&d{uB )*@ĝ |Ԍ @d%U9LAd.%ROjg!,k ;$v,,tBj82 VOm"{e7e5id$^#!Ӎ<4">E G;\ʼ]LX \BZI )()J->'\\TaF"WO@b"B!' `Whv=(^:P F.Yġ|5;'f8dD|DKĮFj//Rw''iWC{6Q24BaWY -PjQ6Ҳqw巜L 7LNhބ#t ] >I]pw` |F\ !2A";wԝn7b֒D3R bX09QjI{1p$LȰlB!tn=>iwѼ9ƅԭyY *^nfXc0ǀm8օ2*&^#/p7b7Uh7 B+uervt*Q lDKoЅQ5HCfݏU2ˁNFxgZ*I%  ި8ʓ6#5+(KiƅC0/ۨ[gB[_RU&r/rMtǔ×@xC22 .ttd6رj XO`O 2ZT<+l ݾ34EQKI;ah{Frʯ!X3@t:rwπeѪxDv̏;v$G`hIXFQފYmDzN8]_̭ò¦2@f@0j$ipTC`f51_ \Nˆ!Dj  u;A}ZfP>4 9Ę XpGejq_,%/!4g+Ei f!n!RZ D\Ck͟܁)FPh;q0#17Y GR'v cRRGXL:'XROrp1AZw0E͛ifB8@OG 1'cӋYYԂ gKVA`Ow8xi)Gg c}E}vY% h)ć օF_RȄ!F$yɏ-\+T-  Ӣ Bl׸ rVE>My'2eqkB7Ljmrr ,$v@$޺M }C &*F5C,"*s8fS ga^Ph5hP`7s(TNlT`f :Fw, t12 ZˢiF^q Zp KȐL@MNN/nq?}dim6+r=:FE !a]i0#Z*TutgPZp2CϬ*"ehWEE 9<4,KTGWu;='}#`0j15`, Ee"V+2[5gMKȸ\ LN L(`-Ev&J<-0X8A‘Eـ3`z7h4XTu#vBuehw;~m^fB7ۍ[?LZ+  D[.OT'e]h2y4Ɗ`\lf".ۉ^,ZBR tm6UnnwR+-YUNhw&)̵V-dq;1,iqŏ=;%e8VKȌAB΃ $ {n3~Pkja >e˿G?z:0:lq7wxݖ_yu]`]Wa}_u;O͚:o_v,8E<AEL1&Ѭ%q0^f.m/.XX& "p, e?tZ,;=hvfbDۑM3&$im84vSV}(![>+4 R$`)#!.7#-l];#%8yV-I'-hE PZ lI3i@c/l0xD9 [&8=`QIٯm‘XGê[Jt1h+;SbĤ$/L:+ӘtB3ϣyt 1lo3oR67aaJR,0BYd("Zv]t] [/a.m5cJ5ƊvX߯afcpVpba`@v()U[*P9b3g؆NQckk pd1[i-~eh U%>;,2%HS kXF5*ϾyA0lNe3#'EZA0X4@2!u6ex@"Aeh[!xC M*|%1!peW1O.gA4h!W%S`i ZX(<5f뾨sAI_M4| *W UX "-+B P- P6HhB95&XHzgoPyЩ-0ch63^D0r4W%}Sqٌ%gvT|!íb9%إ])>b_G gu' M*̰z F3 ^&UrN8 g8ܢ3)3Ֆv6 z!xg:%ƇQ(-Itud8`VcIݵz˨~':9m.D8-CWaG3-kn,K`I}caZXt<@Pnr^n'\Y+ɠל"v4VCF-aBs񒕏3gYrC_#Uk hǟU@tqZ*&vUju<{G_̏4b* N͹&?:wX1p{j hrr=\\3kk|_l̙'?K➜LRv\H*'0sun^J*Iv)tm:!.¥coN8ճ]c@'p<9u}o' N kU 3_}N7?,Ѩw5#K_-9Cs 82_//,q;<>+K2>|Ē98waQg]-̗5s˫?j~/S,0y TSS6˔Pj]SG`R}섒xb*2-P<4"ZAd6${*CuyJ;@(R "&d(SuYIӓ>% <E)W M >7vrUb[@aܤH\fWS &T @tG8)c3z1Ӌ dhfl.J=ԅNzvR$]rfi.t OpdݰN!T48"f^РN+9Mmc-r}m&gu^=3~c21Z6$sHjgRGaq?ViM=egȉ.0aL#'su;8faD(RyԼ'~ &R]ّJ.2ZY9npO(Ь'm8&D>UV:\# V3UEʀBBCJC]*FO)DiEڛP'lI_}H Qff\_Ibr C,rc9|k&{Е֬sf+R" 5J<(m@RLO4FrrNy?:ЫX 4{ֶ@,MI)H}:| .rQ+QWDczw-(َ؁3B; B;b EW [ mXhvdI Ŝ]%E>g IDAT:0/a \@UVg1J-YܵⶰwВc.ulڄJ![2hOY兀@x ݸY8xO`%#Dh"--o4ڭ"xb!ḵص3Kg Y +yQmy" d^rn΀b.LF3<]B ڮ̐ L z3*x.AJU<%6RmDDHX=?1K1cFt("U' ?I}ޜ`m}G):P8_lbvpF @rbp.o׾/n!e6pL𫯾ݔv v;ʪL}gO|_NeԿîtf.gO}S/>Gg.{ ?ϙg?\ZkIajwg)1tY 8)a.  q؋Jp'i:$ke8ԨNѰJMejOLZ @ `Fhab]+X,D^EFmA"UE1 ,yv=arkI'D~j&`$]!Gnk6"C !NvpN 8UXuJV ewgr-XGdy,%q\AJsQ@@8tv6 0gM Ёp! qhj-v$^0VNXhHwYuPK3y- V=V@Gj"%i fU/]X'PAec/9 *xI)Tퟝڤ\HTzvZ Fw2.=TVv`Ԧ,[ v@yD)B t%i0 g)#G2L. <芤8RӕYJ1ƒ+&]&I,#OI HϬ\a <20sEM KG?,`B<+%y]n!$4 #CڀtI6"U8gb! :HGH,.7FP,Kiz{UvJdM t P:xd#@3p eն*4:JE"ld ` 3d2%0g99pĝA`VTK-M`M4G󐊰B0a l-?#ewmBЀ%Phb7vh,[2Rx*%BbBu<.f̪Ӑlaq XuL+D).O~9'ކYɆ!;}- kICV50 &HlU fO1 N XO: Yt )2{3HQD|3I8[w{-3;S43@/3?3}ah ,>>6zc= Ϛڅ]ɴ0iQu4pVV_|w=ry;b+yh7.%6 Ka;D^h.'qC2WdUR& G0 xNSS詃Yv0(UiZ tawVd kb'➸?XN 'NExV1Nz5C+N]ɎHwOqu#֗ذP2RNOJ{HRaQ[Xfoi D6<|gQبłEBXI#R]jE88g!empmnA6pKrvac{{ e0C)/T/d8K tŝ P(nL -;h)0݅ ՕDŽGCP C7v(-;h3@4:dHMM8w`f!jd3h7O1ʜ{! 3x )ٙ nh6 b*bb5{U{! r$e:g~SjߠSN2,&9. &G{J-$ &xlL { vD#/R>Ɏl׎?=5 tg e!!x(0)&'lK4;̊vjxVEcx @q"JP̵[) #Wz`(h%}le<4f}h'}&L@,92Z5Pn|(e9ëu ԑUo/)v kؘq-g`Fn22k=wU),8t) B!ڭ>iqȀ;c( kX p|=bgHO6q^{8 sxz[ΣRnRM-ˌ;d}?ُ+%$?S?77S)BxE;$M<2H~3iqˇ/$YcPpDmPHߥ/:7Y¤ē^\L-&&D.yNu}H=:vE9_K`Hv@:yKx䔦Ea&?AA({Kmb lA.F38y@EL,' Cy#a Ԣv `4ͱ %{#ρ p;*dsjpM}Ll{DF#z% T€C]h@^M-ux.ƉW6Pl44 ݹ aJٿr!d ڄ xI$QyrX2 0)l1a2K>hV劍xN*\Gi]ܝ`e[ۄB;I[d.&@ (O2 vOuEm'6E.`"FN`R*RMhLQORe-Kͭ0I<K% lQf d}(X$h'v ?FYk/4J:{q/}-m&qxj_~y۶??~? gf7>O(/lawEw3 qBc8&YUu(hfHt+i/~ȿEd`f .Ygd+52R/b3]xn2LCS'DXR.ZŰ2qu&:K֬G[Gf.t!x]Џ㖥YRh#3x6ځQX<:T1jEZʰoS:38˯kpOB ,FNlhPb]ٕJp=q8SG5D({FFy[2l^rO MUS4g 0|0_،Ƴˇt#0@YF06(r #蠨7b ׁؙl Ѵl}uIa(YZFz*%"d{2x)"^V@_ Oݟl׸pdɇK _vng{X6p#$adK8cdg>P#dĢB_G؉t;Z:0 -da:.L2Cj<~`W023K3WRi7`am2 C@IPI*Sw\܉+B@ݚu3 1ct%+6B8 ˖+36,d~F'=(N4"F @nI(* Nٲ{N: D'mܥj+kʸ΃x0!Bgd裚ۅE&q/T$e~'U*0:X#^^P #":oVpW76K0eÓb+[kI_Bor z cs/WJc(!e x9!Ԣ^lqTSTk-8^gtm\51Įts,xQ p lâK*.y@@uL!y1ʎ!!;r0 1i춐Ap&pDU!1dQۺie,o=DϓCPVAU ev\b^d. a [9p@j FGM'H#3J}%߮ĕstjk٪(fq;}N}g9zǷ@r_ Ax*Ur'E{l`ϫܱr2ib spo(e-sd{VAJLQEsI ڞ[ƹ7B)ahkV>,9sI z)kZw+'/dwXᤞ]׏E8o_G>O>lG$믿?oFN&1[gx}mɯ/| ?#?++O|ޝKG>|+Ϟ\ޱ g@BsT;p0-4̍!,u<ä=aAB8P*;7`6ӊDYR7w\\u1 ԼTEb( Պ.ZH& q ߞ܀@QnɣD>;S++ |?^GYBK92M S,P3rCx=)?~V^]C VOnh* ozIxC8ZJ.s~D~6LjWyq%`-Ļxr>myJ@Ms8g^:|:cCY3mh"v$J0y"RD/*jmk8Rg!C37 ԐzY0O?^5>`]L9a S dPPEZà elgvĈ.py~SϹe7@y;6D/Y&fp귌EP֐*݁v`CdTczTOdE ȳ8d5 #qRs LM׎fG҃KU 60SQq| 5)[j%ϭAeL>dX8p!|Z0O%!)'.߀'R/=z. Ȥӄu\\H\}`"СMZ>.H$pr.<})k?\l0w%1 xh=Ƈ; TCUX(xDQhf 2\{=cASW0M\@q3MTfu1ԣ7٥N{ʜ& \Zyq[ gaC\zڠjZPtqtteA2 +]= N{1Ssvֶdӡ j | KA9܆d<*u!$#ẩ}7Mէs_(H]aqe&xRFu˥TƧb>lh:Xc`O]<[u @s,X"c1.ŽCU xV{S2+CG=Ё7Ѿn95H(wX9#C}2𰻸ݴ́h qG\*F>^|;*M!$Y #]FY>J̋.خx.L07:P~B >u8N9g!zE򠹹Q<PeGQd,Ņ.WIN,`,N)!pZvW=h,3-dQfuJEu~aa膛!f ҍ}%өs:C}Hn:!0=ե82Y0ao6N;X1RIKG bޠ@_?*k ] > X|sh_}]iՑg4^z~"A_Wiw38kpԷ S2{No)POZH?s?K/}Y1LS;gjBu!7__|>U3JV3T Ƙ`O]Lqn}^@/;37^g̓_zNUqM+U˿S9<7?n}|9p+=(s˩oNJOeҺ{jbQpn'y**l( |$U"ф;^) '[K}jN+f6iyVha]~-ɮr3^̲vTelѰd\2Tp1 dƸCpmh#  qq94 ,rV~Gm̈ȕYeչU\TVʕ{E3}XΰJTw _C'8'g\>rh;]$%VHFcZ4dXnPm-8dd6QT>s ڸ/] "Oд liں-ƵA샞 `:³G1</FT#qc)q1٣*_'ʙQ$~T/s(β ̊d36$zy39e0N4)N L`ʼK5Tx *y<|9 h A|8\3٣0S|K)pB#B̠6V[7WK" ZxM4F0a3FӽnV+]Ӛq:j5p)EC'~Bi7 J*lTJc,C>-3y$"5t]X:3K>\ez<[!',o/(iB#FA 4䓴tL'/bl_p#ɾyWٶell鞜iCze/'&+D԰3z "eE'-􌓫wMl"eS'BSd,^\KA>NfInjЩ y fF6v:FaR4mEIj1(|*#e+,jhVm!j\؍dM@Xq8ȭNKgPD0r}1 $ +^Ft WcaPYϱC羶|=s UsS [7R6ZQ#jLQU#P 3OU1ad%$9ڭv`0l#n\nclikɍi1hl՘p+ 1l0zÛ\6,V^ R8|Yp4h+,iaPN-Hx!i/TLn%M7A ׉9Բk K2<_rkfYψYGYC 3G5*W X+TB@W!<㯜z~w޽[USb:?稿ԧ>u{k^Be}3yg~g~tu*1M;wO{Lk0^xw/|tgYtyD~DG{r.UU/—'|ew{Q}z}|\eO{Os?}NUjhQ Iz7ʼnc}$O2huYo*b<:>ύ=EW'Yf#E`^cмάQ}9g99nϵe+wjiAz,4;}|l14;N;>MFY33uЖG1zEQ+PGZṼ<*>P5翕^Gdx(iWJYl:&lHəExfDr5ΰ 2q-%`e A(L*\I1|ԥJvz9pcSٌv''W-Jk ֺW#Y>y;mt(Sa<^Y  |4^NL:LjwqdoDMBa63cOAQ#2 om6:\Q" e+gSn  1#:;3>d~2Q*q nԽs#CЫDLaZ`G>/Zd!&㱙br!=WD uạ਑M*zЂq(IE:-Ww<ȸ&>F*Z9`}̐Y!Ќ b=$<l8qݞnr8|eN87QqPO-ɝ(}qcP"4.mpxKTaIV;L\RË''9=B6ĭM2qˣ }\Dֲk#w;8{lRPdkSCBgVJe RGTF%,.!ΰ|d{a",)FK #OoKJF1D 9a22DtʭKzK00v-CMe0PZ9$tCp{l~ZT҃޹q"ld Zәԅ=Ȧ^64<h[³ ,*l "ϸ2JF,` .]a"kJ\{݈)'| #נ82pf:{ipw}g.//.{uE_{mo{c(Vj>t^^J,6CiN)7-oy>0놳^l3NM1/r-۷/}K_׿=~eax8 *_[`}C#f+$7|r_Ͼݗ Wd% ,`PQRR)'1*5B]FVX]8\]CYq';=0«(ޙPX- _>62^K횑7hSXgu.8s)+L"BL*~u9>dY(ڭ6#IR?smTRh`m,du߸*!t`#u"E3W^ O#3UY5f`9·hsn!"X7UrQ "&J ,6:tYv2Evjr U(krgA-m DUTo'X^#**24nkt'x:C] vO";NEPb"O{'XM JIDPey94q!i#Ap2cH|n P23L'H./ll@KKJ*r_̗xhSp=h2W(@G5g %Y5zA*X[q·q ^-ݷ(} -4^'0͗ pc"c]oVlWƍIƍk$QɫBm5eUKa2XC \MY9k"2:`\8'g &TtIqf'=R*d?m?h6$?A;)H3; #F:ٔOQXKAFvLdUaȘ%Z,qî=ܫ8DGh` WF!X3qF>|JdDjD3oE}X:abg 𲷎ĆՕn7AK\ Lt L^i,nh6vʧznck̓,);ۭ ^p.=LNVVS()R\ݑFm c* 6DDɩa{|S$'Ex)>'pBYyK%ج*ԈB~"r-veUB`Q&*sg92n-qsJ2,usqkC^c5RtJ#l_|J j6(Y%4H*RJ\ae L89l [ q (Q41FehF5q~E16l,#zt9 Ǡ1N$tS jEmp""E5zjK^ ~?S?OREO#@~X<Rgy;}{_?//,|ʻe8sb-ͤnnn> _8??p9)7H14. lKLsР5~]xݛ>*+5& #E m|k!q R fT|e{5perjHR6`}<=ѝvB} fS5R._:)q#G3Um -J VgJ,oRp);F-ZLV+qa~<HXcO+W73Ӭ_g+8:l-ZY"f ęl*M9MZbc6hFNm{VB *GA֗,򖃝IPؙt Eů=OQ1Y)e2\98:7!l؊vc#tL#}8Vkq&-*Lj4 55* 2P %. NhCTŒ[ f6jX ^lփͣ=C9KSd׊݉?R%L-vU0A*F+%,a>,d> @>G(SXFQd68=QҐQ,Nmڸܔ%c EB Ƥnl>\y^G,4N {&di̳궐\d cklaCh/z0Lbf&݈TBV%`wPdYv>alEf|iihFQ)[~dEG\Y=x[9 ,,)F+A 2_VnF\eXi컴2ɭC[IhB_X/P]&;GcwTVbinscm$Ebٮр^pgVj^̬\d?c>z,PF}༞Dɘկ~ݻw9I̞7G{{s?r\~~g?񏟝K;ޝ;ONi닸9YUd?ꩧz)I_eSThSye>'|~fqfs K?_9mmd|@ 9ш,d"wS=FQ4:+ΦC~T0%іPilm#6Ccս FrUZePK=Q?ԕG7@ Ԃ>R֧*T;t& t֠qc?; 'up!#UX|h*,[DkkbJ5aUn{FTT:jd S4PIh5Z=X`2CQʘ(&Rq֢2\U"G@K4yty+DWJJ#߈8FcZNӟ~A3.`7z : ]E!(FQX^vO02jUqԃbwE3#ބe,Crӳ`=mr9ۉUa -5x_qiY-34-eUXc^y:UYhgjiWV5VMG+h+`6F=gV6qA q$vBpGƜD7_MK)ؖ. `ar9|Xfր۠:7̪]c|Lj% t5|7娚Z>efV9P=1S);ǘVCcKdE箳i(^r) edpҁ8`=^ lLKT`vt,PEd$֘[,C=!Z]D#*\Ʀ'2K B.sDnljiGvwkl9]-yAԙ¢6mVª!#hdlsk[z2Ǖ',u5 I=sԧf82R*b|>+}WuSJַ>򑏔?͕}$4 ;zgܹ3O$3C*_<6qgln$p~ՒigWmoԧ>?g???\t/ iQd|ꩧ>=D̑z_/մLٙ9+jb:rX6[Hx^Y >8RE&۵eu4aR,"70@fFZIGcOC[wlcQ'%ctbYMR$ y kܒ*k js3\f9A@:>OsRjTxABI W!~xR%"ypk`֫DڬMC;Bp3 |Et2Cy7WNSO<9fgVQdVo`= iW)gb'=)cFٮoȇQd+t1ncdjgbe M\VYcĐLjŒ#IchitXe),%טۜͩX*A66:˕ܓ^+5NJ V%wo2EF\a]ϊ2NKe3.D-jr9aR\xNn3<OD,!$*MySڗWyN\ ,.Knj@-ڹ ֤CG5PUr2`lbge(<BVuԨ7Z؊'`fvaM:ց谞Ӷ+R`r ālC9:e2Vx4D56x}e'e"~@t#c.ngZBKp 6@Cj-/ VeVVU>XG:K,v,cxXȺdMq05pPzV cj jpA<#s0:Uԛ,߄"CBXef)b#o!M}M.'cg]gV b4{PFK*o-ڠ ֡53˻ GSR1GL*̣ kM: [zdedvL6h\Ca-H Zƒbc֡l"k?c#Lbl+Xc+LcRG/MVľcY:6 *ӹ8 mpct/A,Yjs#@ qF h?H/5JԊ]F.ׯ6 m:y6V[R+q҈69*̅!0T9 81RYq˵ b Ā$CHl6G<*RuA 2P&=aK뀖9nhyX kktĊrŤ[+)l5"ΤFTjg~6xɝ*{5Nmuu 1da^boDUF>G´([a=]Gd)e(*Rc!(jV}#t8B*rhi L3Q$9WN\UIiVm $iLd9xT nЁT#D*=!*(U1*й-QJa&\&i!LC5iPuJ2_x#.A#~Š[VEbͰt*~? ѐV2)\‡&b(qoam@+ł8V 4Xº 4$Qyxp4jcH&:OsBI\[<ԍ*!P cޫz}=O^˸X/sO~ݛG gP9I3{$`&SakHo60ܾ}'Mƿ79?̩7?>bh}pʪ~cXO[A~ӟ.0Gcg'bw΋_Y'o~vf.^z3$ !|Lqtu_&`o'P 7hGVEu·jrrCܔeŨȕi81LTe2yeK5y߫UѹlՈ ` 4hYe jiJ5ܟ"lC'~}{߻yt Hť͡M^y{=O?l|+?oۏܧJZh@h-^ɝirzD[Һ6%SpWIŅ~x/pw%tc뜎.gwF+:it Vt3#.uqɭ63ۡeFrɈD QpHUނ  0cQWeC|f܊t#1YWЅ9PA*GCުlՈa2pE ˅"nPcם`cI$EVFzc┧sy.u&ӋCmZ 9c^ڞ%)ZM{R^qWxx Xf#B{뢂x=Pp=A.@tLFF7F*=+̰^D8vЊaC;3-QYDT<]ΜIl1!Ů"Mܦm FEX-NU[j" 8.N $ĵe{dAi+l0"q9^빯(qh)DϞ]k=Ys2.:fbRA]$Wޚ5=ߖa7Fyԋ,IT.LRԧ}qZLgsUA%ϊc{d-} rY Lb*AM:Rɳ e7]7䙙 SZM٣ 0k#F<[j^C,(J]_\X ʺJ'ժkdЧf T®v̧}i>+B:mB=^#| y!#HHNGxiI\J$)w"gF-75Z\A;6m(EbN,Ӊd8\ē1oюzI>8VP%v6YQ\>{1UaV(.I`tڒRvuLKJX%ӝv'aSˮ yj`;(fQ[s:ˬRS]S|goGtf(%b㱺x\M.ڇgc{|̥@Aԋ)ǚicRЅb^0p 0Hvk4Y8rlMo,{%ۏv1UDs;_ZDD}SmZI{SO}oo} 쿱qJwlww~v^o֯گ}'ofҦ7=;;{{W~W>|3o9qNSR/?̓p p2jU]km>O|;RgawZ3΃J\EK0Ufv5A=ae5!fY)Ρę]j[gYUxFPͬ(R,kb9:x,Rώǣ#ʬԻw]?}۷Ep秱)͆?ݧ?|3 ۿۯ[(c"Exf͈(y97g٣f:tBrd*YEݼ@jr$d [T𢪂E%Qa xKam27̉c,'Cq_+g˸n&=9xxƵTUط M6% bFmֽ0.ĕKSdC䉨ӣ=eۦ`^ br1NrF¤08GZ 9񨳆̀yTDe^(pX¹V0c|4DցEf zeg _VzbVR-3YUdy$*k!gdaCBYjw iޜAs䉜L:}e%:;EK,(ϊ]P9zHVHsYV;}tvfQJi|4Bjqvj3 IDATf"B}>*ŵbS1BnJPoueG-ðMtD{*6 3^hNTZ*KTxx.EH*ISԻ;kpC`.Bb ԡKLnnCu9[%ZՅYLK}u%fU=CGCxgֵ'I,LQk\Vp$F{]Vc!4[dV8=aIY*s^+kF)tb8kV7vFxZ#PoPG18tIr餠0g1\"u$; cT:8q]=U:p5,% sTjNI!Sѻ'QZuvK*j.<0 J Q56iBRc1ɤGD\XulBd&S ,"=CWxHva7"M Y2StAg.]qמSU3wx XW~ƠB_ EDG.s#eku­q>#c8R Nkh@=,暘03D3>d+9J ,9k)ڬ'}= 52簞1Ԗ x!bF.U3@GsahM8WY8(ݚ;Rex$;8J_RDcYWGBTǗ}mR,gi8-⬕׫Zx~~uu(?R?cɈ,v퓨Т~rx;ۿ '$d=)...ۿe᳟>O}S/ſxxOņv 8G;/ .ᷴ! /ofoѶmNe|}?a9iTiE7P>~?#?rz:41oUYJ{?Syn8\dU(f+%w+(pgϥ nb|~Oڭй38s^jrLŘecJNJD(wbUN?22ftX7xʈRIT1]p4{'}GztrQ;xwp% u8#/H'F'qmt躮TYhlٶ\;:M6c@FS?F F&P |J8*.zl,]8n Q02: YGӇ;qh , #Ȇ! fKnQc薪*hVCE(wI^gZpm0\Q 55pvgҭ%9ϱ+d,N)[`QlOr@GgcAT8f1M/AG`Z4)GI)R*(Dg3ʆsX3pU"Q9kN%%n\՝5LIKl4b4[|CgeJ)vIHEt&5w8>DS!-#D ^ )jZhNΒH.Fd#SQƖOvh#?5sQQfJ`UM}&CwřWrZOG^+.E ;fBtcɭw uL%)%Y]@ȣ2>2's#s gxF+ I_%-?&Gŋ{6Z"oqΤy835cGD]fTuXDeaFE<6C+P\OGSfpIQ."uD;{<57pF=]&` L< Mx8Acx\[_Z!jdxL+W׉Cɟ__>ӧ'O7'W\w{= I^?wwWuC+wۻ[??Oӧ/M1{MO{/_?_.81=OO~1 Tb91oyiQf^]]~G_>m?O?37Pʯ| { fLO?t-  1s{ZƗ+E+g p<s9ѵhW˜'R^5~RR_a 祜z /ܩ \L(+g[xrKt \/_ce=Qp9 wj=]M!"庬\ _y,E+cEQՑg fJTP;b}/(Pnh ZJ̭Bs {{"" VvBXhtͼL:I#wpQ4s,G,8GTQ%s{pe[_č}x3s ocO]#ʅ0ڦs{آ54EKJ*줍#% 8ϚaNw׏ǁt;#9G} o+13{XFPL?ENHzh"Zo;9 {)huҘEqnL?*TDX r lOvh@*'f}\A_u ;h-Yk33qymPF,>7j㦲$^3襽& K|)?fb+#=mNHr;1.Q$LNغc8Gh k!Sψy42fm{"/bؠ5Uzt!h2%ZsDer(?y,eHUcxoF92g$"iƽ7@ k=77Ce5K5Pe.o5_E|8<NZ0ǛBᬕJ3.OIWpVRG֖i Q;ݠH$K2`)NTbWGwKql9y ].cمı#Ё 0l ҿB3ʣRp:\h&.x =LΉg W!9F)kaI\-GPfcs] s"8N:L~-pkx^nmfւS'=Q3{i/1'+4SVs#[6{{{/ah/Ί/+tkoRy2rV YC!UH$W x-5]'~zچ uS Ϡ[6ZDg;7`Z~OB: VA%sFL @BkZ!U\M36r%Z­~B&Mt9uCGUs Q*oK%bf.aWfYH3&iUOk u;<fe1;z=x;Zϗ18DIa_ ߓ/t}}ohz<_x}N2}ߟO?x]mb)Lv wk/'__~//xPfЇ=z 8__گS|>ɟlF?HuC:uJ)xaD//?;s&NpsAgB!4cW?ѿ%*ٽFQ1 iB܊[VT[q O{BNzó N<k؉&8R9KV$y#nKfS g]k9]J8<83_?;g22b1Ko)OKڿχ̱6]˟oя~7oOg~ӟÿt,-DjFYtaOn&7$tza$Hg\9]]-{{r'/<@@g3(ٲbۂf;MQMy ‘zHd!z-Y**]hE6>jGO.{sKCRϪfm75_Uذ0Bsy8Þ=FeeaJ= >[jP2[CTf7sXZ3j|r0MFGktGvi)o&Wp)>[+cD3X F "aY#lS|Cli/3 E6%PW0L9tHKaMn*^tdai)I/n`=ڐ- !E k(`ڻiۓ^@ ?dbpmh3Y hWzb!RJ1eLݖo#R:[;la %6]@QQ\M!} $CQPida54Ed` kfrls*֑ViWJRj #XNQ{%ދ;jCҍ3"'jqcY'`}[|{簻oEɛoݟklPDIX{E{ܢ-Ij sr*9ۣZgG U]N7[Xޥ vP!8+,"lO$kW%[ǮuNڣ=DN{ 4B ݕMS[SPO|'EQ/% |};g>l!ܦ0ovڦF~w:O[܇~ӯ[.=>O_|vJOIJ{Ӌ/M}F޸+寜wS[57^u]_y{>||M9C6蹧6ۜTvfJ)|;?goyHEk wO%(>ϿٟLV/OǛ1$hܙcIgJqp!b+I0T/%pcW2vŒ0 O(D}@c[FRZ#7 lwf:Vue!g](~oC_zgvd e Ks2:&yTH( -?~3O'Ӯu,+ 3h*̀ ћGNޘ M`\T!!n9\#\ˤ'vg.AXQJΫێ;+[Iޣir x=,G}+̌,8'ʐV^<ӛKE?4^ +G #9bo)KvշpOQ{7=7əuf ڎO6M:[|J& 779G^䐹\i;fA8oьXQ{58ke|MI[ңVk "]z :SbعkѧZܢQ bnF p݃1:ʳ;OQ`C̈l0ͣΝs1͈;Y4!8ʵ!BJCm͝S+{"qPn*wbhv{2 29Cı&sTX+R.*%>cAw{G]).iЊPqz]4;MXyA\bafh]oS!I'fx^cUuԔf=:rjpwQl;X]fӬ[vyStlf)< 0p5ͤ"^f9O[G{ wnʩjyn˞ol'[/fNL?,yٛ٣ʇkl(> :(Je'U&KﰰGj$p@7"C1WkXGAZyܪ,)UJ*<)f%X{19cP|+KPNp7%E֩B"6*QaSr~֢;}&Ў8߬ܲme(G;t+%!]m+>1NEʼnfoovlJ=[48 bnn=&c Oy6IU1f IDATl)VZ^7jaJ!slwf+lBcZ4WvӲt;\^w0^0Xc3JDǁw'V5Lm dk"\w Q•j A JhEuhDؔܧF3dro9==b˘vu:,GNv3:DmRӧO!O/< }}o{-,"iׇ,/Ó5T@7$-~wG~G>Oܜ^f_ah#_kk,H~D_:)ANBY>vS??wEatP_l|`pN{\&Shv_k~?/){7R캶?ǿ뻾a?_ֿ'%ӒƱ7?_L:`'BC+y٬;mPNMrVdUv(XIyG,-cR>7w*[b^8 gS[- bMЉPfsooaV OՂJ:.̺y5JN8J%?C%Y#4oo_GysAfKװIVV̤s]F[|m8!:?Ja UY5b,<6%ϒ$KIFQQ{֦nœR "K5p<O"Fd$lҷf3sĿБ ЙJFD gfSwb{e6/׊!6ٚ[E]FGK2ZQlk` ;bg,Xف:ItþI߲"[OFrFsf:\cRQ jH)m Ol4`V˪xñ8L<›Z̃^=y~ihnv[Sq$OÈ!,MuVFjq)v5q\UCuCr!ٵkZ+;&J˔6sbHq^꒨1Mbcv뤇'GVn'Pa.1ܚjFk6X1ٚ5yRA6ܺ[\cLQھ[>T 1okX!n)QjĦ;фJ19L %7nTkl4IZHvR7vh6V;X€#tkzku6IC0z} R-V}">=d*++I}fѡ-݅ɀj^]pOב)Z&s6%9bN,*kjv] 8+X5TZ9W᣽Lfwah+͈[LP1Z\u EQFUf eHkVqa\2yoDlݘƉM )mX(X ˶@bHS ċ;@!W5y'@wAcSA9]\ksNGzND(ɣ HR ep:R;$1 qt!(\[H,"uBo_s]ugFs>T첩=K%|ꜳZߜs|<8VH!PgGKLe-WJBU0bEwľjN  ;J x;93.p߄OǞyo{Gx M?򑏼o=rHZQdؽ7=3S0}ږdJSLO}S'[/^j+|{uۼZ;mtFZ_DCPra"ݤh{q.m]M}-5pUW?ӧO'@͓f})]wÇA ~-| _`=* Ba0ص]&x $jN 5fk5'* b"VI.PCi`'b '"@,HCm˸"j6*V`4gxn~(+ eqA:XARr+ΐ̻l9^Aܻj[vn\ U @ÿ=o_z'OE ҮGADs7 1J]ܔ5 #4XX%X4, byӜL^b6'V`?8jARIg>oQZ>""0n֓H]sI]dW(@ZI}.̔ Ї"F bL֫ʸ+m%]olU)l9K;k((H03+BQ}y{!3GҶtv1 =`Q!X!2GC, !lG [LD2c֧q'r[LE QȊJ?o@Ϻ=p*pbv+mGXAwD@©Z3l y,1&^sV bȔbU%PQ'!{: G!S P2!Sa*ȑ|`ܘx !RrZ   SM=W}=1G5DK0rD*i !b=WEhSIgV^ÆNP$Da:Oi 11{.fIbA*DWRR,^0"[.0$ B5|Cq3q+fGhY2 v0"jCȁup@ MQ PR> }uㅐpB(`%f**9؅8WmEzT0, BEnG}G>A5bG,MdM7[6b1ᄛX+LZLvܰ`V[*h8 C#s%戅c H= n:4ESČ " ChcRJ<c qZ؀莰)+/0D r&0"RJ*QGR۷`0w"EHPv UAv @I!S\,5ap#ka {Ѥ["u;>B6X5`j*8-afmHe2Ҹ=ՈL*OnyN璑aP1TJ$N@;`"FԆc}8du&L/2ټ w;& nm .m -"A6|yU8~ cY,BUЮ+ tR]#7^V$d(dg)ơ+m.= G8@Ėp a>l"@.@1>*61V YʢYZq?PD&a%5eSDۤcJY}zTㅇ9\\PfRY*_w"_w%#^j뙿2]uUwq{Vmll|g>mhh{b 5|Bf Pxe _4{akF)c7Mo뮻RAt=G}{o|co.KJkww o'p_ ¶/~TeDOd++ ii8`* /0"\U2KU!v [1>ً'& #;ԫ26ȫF֒Qn wvm{F3-6mߎ!>Y=<|?>~.ֶmeX1 !Yv)mxWj6X%U*GdY#5 }-SPCQ/FەۑIpgвa3[F5F.U>B2(N@[Cݯ~v9 < ~tYᲚWpͨg+<~}9H>YD=4 A,*=`x:N#84F' nl1؊ׇ# u瀁!c"|Y0|i43MHW no$) EK#JaWvA41 m#d@6j** H (ʳ^s8gD M3ԫ+d0Ga^\Q51.9*)cMz),ai؎CP|NZu5Mv,>'P#ȘnpȬoFjT䫉KCwBxoKCCq>+B%PWw:0vs0Y`k#톌 u Ҥ+Q%fRs/hα ,[T5*rG晒?Db.0<+K4Ý~ˮ%Ci* \aܡh DNm0+{!` ai۱)ۡЃ#l@ߕ{pv8hܕb8ZhOX K+`W~iĂ<>a:.UP8MY,:G ׽,|$H(KAi e9Z]`fUQ=O]&LAOkA5.G*p/''`Oo@fe(64.6Ti> [7@jX4a˹ش}!؃A }+CŭLjp-@D$03Y^MҮ)+&pSJWjZZr2}a@ҧg3`.m@XatA'D W*Jrm:7]x}L/`s$*ʋm `8o{??#9;oEN:4ixĨݑK!P,d".Dl"n=fӪ#4h=o}~B_׿ ;nt:͆;]z^WƵ7Ӛy2OFL`ʸo|C=4tHJ'uҤW:K>??kג 3_!.m>?up#1;;lOۺ(巿An;zn}s]wǮc8ԈK ,,+8f%ZA̬Q4*@`rØHfQ5 z3 fJ*7+M#GOrpYz2b8`kZr_iX ^4; fycb8WBTG@8 `[CKP PF *$xF2!6:ԍw)R۾Za>tLvY?|v*9..kڎQNCB6ZqXp{ -FwLIKx}ro^VajaCia I9l!B-:$`&Xo"'#B@&h".DžnZ aV8`?6oXw]gR˗ qF 0 Q C繰L R@(j2tq%*+U+<0BO8 }Ɂ┸=`>U528;qd[c6ӡ)["&d "/ Ă>U&bCpHYh6Z1 x糒@33钏 6%x:C" ,XM>Q6SAi MƼ1U2`aOWSpx5BE-c)qD_q$>mX$05B:I/h8gbh9'~P "oO{y|;l7!.*mo}[ǎ\/Z^^W~> BT8i`p]h͸fo( ٴ B>3v ;P 5Kac݀,,)`&};qAx9;̩]lj  KO=.G`>cQ9 ./ܽE$4'S3_s$ِR5GQ8 !#DF//{귏EO5`x4>$\ R%pTģicKa-As7`l]|N 9;GVtP4R&> m*G+ö]F]BCKġc\v;2lNWb4}o1h,~=zH\55*RQ\HsXw'Ƭ1GP;+6b"pXjpzR8/ \Zt[ ])`X φz;&ļgAaD@_9$XO`VwEYp1d),8'TDxx !zRUttjf`!U(E=?d?A`.n.x%|ߛ[)+>g[^Ϙ_r\SKӿv":_0 IƬƒ]si?TtBTTNj[JuZo% їr ɔgN&_p+e|ֿ+!G M^ho|ox|2`]hJ;Ç'g uO_G.߾֕+E˾Ǵ\믿_8 /5N=#_ӧGbMWWWYkz=%,c=ַGAj8%ZrQO&dtb]5d-(rkhѨ=ݵHB2e.oT$tߜ3`,xޏ׼5wy﹡Ў IDAT#xR+DŽՌ0⨰ w{w|o&L;= }p|5fboU∳23Vo;wACeMnM88b(UB̭}ƌ@P#1j ~z X#$,V]h}+p@nC#Ŕܪ`̺ gaL`=:k\(YPvd[A۞y(z %4˳cD֥Rzy!Q\teLf)il45AUR吨}kg.ʗQ/jp9]#Z`0*")Z#-V=5pRt7K!X!w@ԉrCp{zvqPGBHQ#Qjz@Er .:`oѵ& >H`t ;"g6$:$b7Mf`Ϝ)Mt:Qq)S Bv%L I}$f$KsRBl@_s.^CR~Wte@rq<)s42$a~!vZ7QWlpY)U|b1kJn'Pgُ}rp&/Cle0vP^1[A"5ͤOOd?}l~(2BL,=έ9?ADljj%mlYmG? sW5]zW k֢o~굯}mi?Ŋ},S^p ꩧn馯}kU8^WYzk61=/^ҐkkGImd3'>{{)~Z Th" THf~L\EH~ZsGڴc{nnt%~fRtl95&͡!e>ЗBgA[>n+Ko8&V WDhD&Dpycw_iԊ’V٘/0Tj( EG mqUU"Üc+Tsi!DĀOQ05֐5L"0[' XU.#Ra.#6-`J@(?aDD4&U˄GHo犟4ۡ?ianXan! LLgDC Ԝ$XeܦFS*F9}!Bۂ32R]xCj,.{壍)HaE\JH-lbmʽ$I)S$9'4|.='"wޢȘ9 dij?1;G>bS7@|#Lg4^?LJOLdHiq p9plO߇ր]<ЇlzD&VV% R||/Qք2O1@s e#qMxxaPնm ;"&ʁ=KM@-(_絆9gdc<)R w[͒?)B6%<䎓?X9Zkm9V4o&?g9j&Mv2}lV^ &z],QUUEQ,cCoI^/{n?__1>u]'"}[3?[o5AK/砼 #΅-<@Bҳp]_[j׭Oź.ѣ__j1YhٻONDN^J+]wu7n.wpOj8|UZ n'(1`Q,...mݶЪKU:ϞP.3oRnߙ8Gmv_JXV׾9{01eBU Aű&l!Di֥g>[Ys⤌4=kq9<Ӳ@-9edp0 T`s;8Mc%x4; ';57h6_%}j,.?x{6М0iU]/$7-9ViV~g/DTzDYMOj1V`=c `^h~ĭgpNl_Pz RٞygBY:g\VV|6%T^.%U'T6:$ӓ3+2z*]l]!bx>?3 M ?oߛ^!E_//m/'HMM(sb:}kb։nٝS8IW^A<𖷼ebq\4WUi,R*Hk{+[4,Hi)ywQXۿ<)H>}c}ᄜ|gwQ9sJabD:듈u4 ҙgRcwtM'WVVr"R hw/]kfTjcJq|c؃>wP Km&GNLrL@"hȌo(^] Tֆ2A/I|!@sDX#GQ IϢі?&]P u~ɱ]s;e`=tLl{ r-%Hx.XA%YM+ 5 ;䃝uDY&GU;.NCx :[͒FR,PެM< P8WQ'DYr(1fIxi}6-h^.M,lL S>\`X*ryq`40!m͘{^cYcP6b|깦,R~7-y c'Ozn 9 k|viRa.؇K|㫼 nA ٱC\t4nzIO(lK "H' -A{{ykz }~_ \Ls]ѣG?T (`#kƬ;xLߖ r7E̅֓.ަyBj\A{: U#S'hݥ0 C3f3[T-;(xUwRT`/E΃L@϶j*aH'dbiU֬~CuW. zB_40My6]RRHU*)den ͊ LgoMDz*4Qb`2Mw5i c)ɅL5!ﴌh­0{q|PP&|jb$8V GK rߕo8ց]`Ա}ȩw6&!H t԰nZ (B0swR%TJ/~9OYn! FX}m 05p*Qʼn63C*=w;:?FTc9945ElO\֗ ;_u?C'))ԟҗc?cս8~zo{/TRzew?0O 3J _/2Os 7 rI}ԩ|3Gg&U4ˑDObKq]֭P$Xbv?l37rAxI 8+ECsb3Stˮ"i "$$!Ƅϩ)夘::rvVVdmJUaîCQ 4ldE5 e֛w􁳣Q gAC#`oN)렦{9 ɉ rT@v9jj ~`M|I@lɒ%'ł hY$=u߬uΎv:ZMYA<ѳhIzT2Wr)(5x)N&9A?n,Y}0!tօڊIXwfd>9 pK@ : m[fUnX@JԎV>ބΏ9qO(~O # \lTQ#NHEH3.TZU>~u {ϟrYr[] b+W e:,b`rޫ'`f$/ҙMb)Lf(A*/@L}E(@unt'ͅ NH7"J6U;l=z6!ћH!k @4+G8 :> d]jn*~bVgn3ja)]0 4X%gs}C_ jlO6+gXGjԚ*]9-AqVwr">2(j1]ٓA,5HcU&' 2 L1Tb>WBd)[ԥfam#o6to&]e螇MeN[Լݯ}NzpV(vHi'P 4bELf#}ҬET 5b}n;.w}cRboQn[o{]]]M:2%o{۶ocx/3J--){ʄ\|=? ˄6c\sW\|TSjق/l?|7eY6!)%iL_@Z\{{O>{^"BCgkشg&ٺg&'|(y`9l\ HH19+&^lr׿`?s!nmaa ox7xI2Dc-T;+AK2'fW5ۆw'Εn v$'J 9>.{I=lJWޔ e^\xĬ mqj*=}-So[ʄc(%aȲT ͤ9`8*ļ2UKKqÆE4 iq G [JHFgH]m64U"&3T-Y;Yr}#6"4\jvG枚iHgc͊ԉ p0ow!f.ZL6j\Ph uGWNNۙMKlS>[bl7d I.\&qEorgͼ{~ ,8敶Ϸ10^['i쳿˿?ɏl^I )n'\o~_n%bxJzGN<@%B3G};9Ҭ7/|24ag(z(O k!kp򧤍__M_OPʱAzmHv5G`vs,}Ng w3 ᰴ+MG1 +Jiݧ(cmf\sr-sss 'YH=رc7!:Ժ/,Kw $ظ@4Cuv @yQ4qgѦ}'Mp:h c [j,}Sy3dW h: X-m;I}oiY|( Iǹ߼UPKđWE#0xp0[_ 74N -LVϱ7 |}0L-~nL4rE3 Bq ȜE!Ẍ/4-33f`\`ӫ a^5Ѝ@)tD "hzVGΘL"dm@+3JT[nwaKoH,cOU#g14b .?&I{6쾁}4OP=`ɰ$d%ud1&3gtGO *e`qq#M /d*^2_z2B6Zz4U&p lЗ hh^D;8gvyΏY^@=i^JC]T!kIl<]hT'ZX-MaD.A+Uß\OeB ,‘dރm0@w{kk3.nB7p]wu$?QH?w}wS̅) iI8]w~u{gnu~e@ۿ[գ k3 b/4<&0>˧p$Uq ؄AlE=DT5~wOa IDATpUQ&e[no|{/}+^ #½KL$e TNrn;Р>'5 cRt7Qh%_B7G4IX Oe0$Pg1ӣCP5R2d^ '',V%3y\=`^:zSb΢5 >OQ2pooY`Hs!(`seز6}[0s5\"##6=D@% *UD3B'eg w c"2[`Qs 8 tfQOŮKsv=B+Luddb%”d$ ` ͬL#e.>iQ Úx>ToF?E`XH $ŅQ S="PD'Ko;vf#v5 NC))iȈ*rAXAi3g BZQp` +˝u*:6JFBy)BU(r!cɽkuFaOͤd=" )iVb%=V pԩ/%Va\>` w={E%xOsp̰ D97(;Ar*ֶoɻ|G[Gf ڒڳw:$$N  X֑H qpg/--2[f}쩦}ѧzkieŜu̮艕R4ʥD^k*k0'kWx6;da T)ŽmI@zy` \k-=ϻC=2 QrEvb$P&32(a"YXA%C $l`2&s5VW (p]]_}Zs񮽫p&T*}z׳^1\>d> }(Ek_AZ l^{~qi] x//|w|wާ> 9}_QbR[V/Ix._|/>>5N˦6"W_}W^zgսMx񴄧YSOoK{ϳ1Ja:ᐉPoR 6)txj;ڑa+ m)K5IM@4%ZۙVkh{??K x'l>O54aVJo__G?+vQ73 '46G[R.*&EVyF/<צ۾Rt 'mhGGY䞾;u2D7]CYBr#bt pt+q-]X>#kOuQ=QTGC)&.CۢF$)1+zQ#`<,Ʃ2K fA[#0D@@)#ŀЀxQ((N`Zت3z;^T,( hpoE=rr#|l D qW⍅aYh)pr%ܲfhzMؖQ8 #%?j}$UCe Qz̈́6)Gff) =UpFD bg ZqOVd7'9?+IrA( O-P PߥJX`F[vr1 P6X<-ư0w1VnT4\_3@] gωLMpSe1yzU:t)+ 6󻂭q.)6ڍc_|#wI>OTANK{\K59lW$ɬ;;go3L럋ILJ?'OpĀwFIҷ}۷,G}@?_-ǩqX+v_WWWu-պm6(%<ú'?K/=[y+hV`[=q}0Qt"$8+iE+q׶S2`+|D]n*sjn;OxtCQ'7~Q/ZJ)_5_kk??ͺ~?u071M!~G/C YfZ-TY~{r8=iKR*xQ\B5ŨIΨskSJ@ ;C=xѸ-G+ܸO1QU&; n~zۑ80dP 31 zJJysEbq/mpnQ{|*atepZox tÈU~ָ()b)Xz8s:jR`桌LzX%5pmC ǣB&LI,aIj#<.` M 'Jf?:P(&)_7 C0<)H+%)1+pE0ޓPt1u{@jQU7bYf0Qae !8O+0WS 6\24&=N[1 ;-! &|`l"va_?I fUPj^p]q\m¦wM M ueOk[FyLF1tgߔ_NQlRYOj]dwP,"٭̃Mo{:3IQ֚ 8#ס*ove rXH{Lav;~דraEYf_` vn άF{b'n-W~Q;`lfb)iCB7=3]b}}V_}o7m4>xD#w6XbcLI}9S+^/AX6wXK(:yhm9-D BFd 3 +zb&f.9 b>>)·pRJ`:b(Mc8(YԸ֑5YK螋Ϗ?ᡈgN"g~g>O}|~??U%HU'Ms_o/ 1q-!vc-^Țk6GG Flb|es C4zVҊ0243ɽwq;yYBhOCJ!Hx4_+~;{߯>Y^nXXtnZUr)q ؎d_/]hMYDi<&1/Z:}$Wdk# p@s|]Ⓒ\QD@nnYR5y.1 uq:KN(#Q(vXhXe'Dޠe1;3eʈ ^uRB[3Qtq6ňc`ftp'-'vF ]M&6؆#RwR&>Fgr!p'bIat5΋C /"̵܋BP{P.>.* bRj$H6fVdK)`68*GZ,CNjE}YD3obG/g|(ɱfȋ`!Ð43tAO.9-`F@̱>3 4h5D-?,+`V3% 8k: pKwVZuuf;Xj;2Ň. :rWڛxD}G81W #ឆU,5"{7%6*QqX!IFoed ~Ң:pwe42؞ޣ)=~'_oh({46IxxX'r< KњLjg^TzmCs̰]Y IDATt=ǒyBz1x?4;Gk0Ȕ.Bp"Gtf'> F5ĮcnW0RUkfd\p'IHtK37Tf%11_yb3t+IR4̂%MfiQ푙5Hc tžWd{Xld$t0hY^( Ш>Hj(ir lZ#DovKx`:scd=QBfɖQ9` [ukFD3թ+OEҋyan^lHFJ1*! RI>6"^qI%پ(`͜Vt",m҅,|"M3J6]a3-Jb0؉+)Rsu5#ae1jnɋi7&%|e % 8;c4,sc}hE)Iqډ&(Y$yR^ /`#஑,fߚ[sX)͉UИeԇؼ9##xBzu]|0Z k׈keҹ&j0Xy+fnVc3b} gv Z8*VsF|k˳QCXx 'q6"uq ss Wft7 phmqimaR?^U8?|6//I5R)x|n9g>O~^/?zM圛۪}?K{5ڔb?X,NFHޒk '>/| XOǻeX+^z3~ ,Y! *M?>)x!X+-Q sQNJ#OiĘ۲RNV,Lq~034@wh@֍Hns9jo-ˈ)U0uauN"$2wAά5)B*5NŢ֙ki%n Àn'CX+se-38K\̝'ƣ̿<V3B~4(9 Ӧ J%[2^yp3sK P7\l2GG v*\ ;s-(milV꼸)p#`$yEM;Kti_z̃>U`eoRJ פ>[^̶^lnEPQ!)lT= +Tla(7\f̬+|b(V8OD <=}&8̰bf4whq* N0*!LqL'aF&Ɲ8>NϟҾW\Qp7K;/N9li tJ l5@9ɼxPt"HZ$KNhLQvjaIrxo /9a SEMDrde-Q\cE1gybK\ԮA^|H/\p1XCKt#Э5lEêxݢ ^1kFnfDIWV ֬ǜ0Yh;coB H[Is㙻Xh s|aZ4x}MHxCj =qM< 5.ၥmS4:ؙLl ,GoJ~sqؙ1bnˆ!ws-V*0ʏ~s_M4_+|EzǪLw^_5e_}ooH)=?fs PQ暈Tw 9ykf\+;/>O+oƻ}z?C?K/յmQ+qJRc) AGNEz`K8ja!= M;]wȴoӸ)cy<hR(Exk-4wd]3c+SkQsl!u>CgnK1!+(2*T=*G'i$B&pwpЈl҈1,.*i+Pn\9S4f6AU +VA䦜.rWҲ粅E,8V:؈}-<Xz2W p{Ґ;pja5SԬC+i/RXI*F@G]kO܆s?#s̓י[@f 8o mQ`@cD 3{&%fhM9t\¹|CiCa<n&3C6{>@OXk`Hs+T}0|n>Njԧ>u ݲ~N?">O|Ns8r?4wz"z?n'*ǨƳ_~Cлk^m~}iSU2 iҰc۾ۣ 4e=r- b (NkMlMn,NJi ֐mRתP4'v+g.nWjw~-Io& OɁS23T~4m{ڣ|K_l`kdۜG;Mmes!3YF\Q# `,ҹe ɐxWH09e,YE<`kkegf&)n" ZE!=aw1J#Xg[7i;ž$M h=D<1=j0c2aH1Wu1q'u`m̍Y0'zq"}XQ kb)s)[)-;:R ӔH 0т(Yh`%Wi dpWCb s]e'm n if*f TI}!`/\23b> fjWTeB.\cp .Q6öcRj st).KL1L x$IiZk7f:gN]"&K^,}Z,%FPF:PzȈHCK؈5eA\Ěx,L+ P ̖bsp5Of32`&\6"99@\oݱS;j5k$stE@XE\J1Tl:e#;p6Me5:#AnxŔߨiQ|q'"IQs1mjU֢sZ Y6Kf"ͰA ]yo,/Hw+s_!FK=jޘF\Y"txQ\7 b'Ud궝}ss}콏mFQ A [b ! KDۛ lBBKV1"Bk((HVTT*U±}s<Ͽ]I k#-ku;xǘ߿|X*gL+[80 v0F4hsjx^l0p,*@H'$7EXK.]W7w[M)`0͌*4^( XO̞zw (:#`ob/] .1&U[g Sޘߊ ~By2@o.KfpO2NCKRMr̒15IвZ+?O?y aJQ}gfG?NgNoZ1:m|3?/׷Joy{{۷/;Wge-'?ഉ4M?ԧJ)|Yqs8iv{JW+֩\@O$k64 rJa,җlU aCZ6.3xKc\uֻ1kyt޲I{iC'8ो};W,n0NuIZ|&770o~W>寏Ỵrrfx pw-jTK wZg9br[?.LNC3iflHk2[R0rk荥t0|?#ZFc!Nҳ;bd9Os43]h'roL>#Xbٲp%%K3E^U_Xn`8O'1klaÅ Wщb'm[`E\0yG\BʮMКroV|g8MEjcc,捄mJ)N䄍>f9ΦOAѸ.D`QeS!f :TBqhgk /R,m ƨKt=pp}19 ^32;p|T溊Fх]}"µN J8\^SxB\X2Ǟsy9j.O̵O:#S =C ZXMtK 387..w䡶lp.l: .Y*ىi-Pvn AH?/2nn erNR'E̥6we5֣[؀G{Za,MGifԵe'.E#ÅaN.eF] VЀE2UKFfkx„L'b 2b\f\7n]' k9[q9a+T7oRF\KӐH ( EY*胾{redr*i`%vzØL30y;8X ɘ9X. .tb]ZVLcC6VMdJCc #HOqhaR3>a;AswWK 1y'eʃ꺆K0zxx4YW]wq/RڈkvZ',pX-:Z!ʙF6WƠ֝D qnLUY|&N6hŒf OJagyMy=+XYkSEj]lzpp=k@+ ^0ڹ턀N+1`.z_RD^\#'GXYRNMȦ#/*=ԛðya&k)YݦV4Z|67D%Z0 hXj&>A~w}ק>J+9=:>--/_lfZOF)eݯW,>63ЇKh_&p9'>xoM~e[@k5Ÿg?o?|jՇF66_3&\6pgn9C0vbU$4KMLGs6Ğkry~R& D &K3OϺٸ~riY?綒W4_#asX70mڏ}Gz;S=*m--Sѡ.iukovePYUDx2g-ȷ!\ˁג "8 bR!A1EX#L'|nji @ 8V-ٝF%=r!A4^U,+ױKgW c$TucjZk qs{)d2퓗ptܛ?ʲ>, ;a 3… 9gafm/=2q:H=SR^ F;ӷpcR%Q<j 4JIGط1F8$uhF&'>YSx7[~Iغjc,X =diim#6JNs,ʺ0Cf\Q`O`;9Abs `kѹ5aKdZrSc )`\xgdܨQժs8Ҳ֞%hm43KA2ԶڝdH8dl`PkYbS{"{?דږr'l 4bl,>9'r.2dlM< ~̜zV`V5Ңzc'qM(#0V[%q g@S='e\ tKp̚3lLQ5fG舚5PhF 4 bŽbW-3wlV&&#Y(ֿ߾MAo_6ɣҲ.)W&j7MH}cUx }g1W3>Jjc]~noo[f|z_Ί/[=^-#o[j'?J^u}rȘlR.oji }f^^KaqẕI:s1W5]]A %ae>aθI5Z'NYjo},lnA%33"}-(^Iqg6/?w?7oo|: q+{m Ttkl.cZtfGcoATddL="~k:$ Lj._Y%eF½ʮu{1%(TB 3)w!Q8[aB*O[V<+}I'3{؆{p D g%em1(+LVy̬ޒlϝO+}#8kԧߘ-vgy,-|viZ{HgӏFBYtUY< f ,KQM1u"Y0@UKN']J`Fk?`l4e$yȆBfoywҁ })4470D 4OҴB,HzHK͞;"r,RA'l%=L̞62r *|N)2%\ G`CpۑAXwh)Fhupt 1q&h.3 ;=,{i-[stNP;B &Sv|+R2\-'KJvaMxg6:HZd LMp#;]2`VpArtNVҳڸ +&&xA ẅb;CN"g0/d=gK{ ذڪkkc>Csi9ha-ZxΤ77N:6[+VA19Z\b b;sg͊,ɩ'cWV19]2F FrV9Y4uöFBMi!+fYō*X9=odYwovcڊ@69|ɸ5ݤ;T A`Nz6C->"rg x]ڔ NRr9[I.-{`.%ʐ AYOv,˜ɴn֨Vi=S2'iL{*9:cp775=)55'MBi:ƅYڻ\x\ %NT`ZS˺M|/Tq{84WƲָ X$s/ j-Q^x>ϼ?qۂJ[߽~4ZR#Dgxv>=,[k;&eگدxY''G@}V/k(?9_%aj^xxV-->'>ڧ'0̝~g9mUna>=?{cG,đ܋r8찗؛mX_J^E=y 3م3u`Z4&qY[`ž2S|7)j_, 2}3a[f'_KgZ(Sޱ{m{^Ss>L;˚`ֆ YVsUۃ{ɡ61fØ_($%#͞984* ቅG{i ĜX'aAO7F=؇FjY|dtV{ٞf朔7sy1/L9eI۳UVфFؓ5KlO&JzmQX& l.ؽ'ֽ=XY+0<+ tɁ$=ƴ Q zЉ<!g5FJ1w[Ų1TFKTc2{cьB:J7LY`} | >s5 3)I0YP 9֌u#潲shɁ<'$ $rF̱ll~Tl !AgcS-م=9]tQ"`JHKrrx4ȳCE7pQ=.)5 :'Q+Y}XB' *[!:; pJ2Pɀ٦Ȋ cV0"8#9e x7Sֹ{MѤx 15ZvQכ?V̤ރ$X\1ө ͌1Vuɝj\ ֙$͜yyLu V۴+Ja޳:xl1{Q&Fa=qiN7 zK!#7fUpQ2"wb o 0c6VR"FͤnSdqqTI\KbmVa'n| N\\S6B Ke*ؗҿ vZM6)&t)naϬ)Ra<ۍ6)T%Q: 0Thpk`( ɩrLfh-:7oF8-L7'[es7`ȁHN5cRlᄛE/A)1RNaTԨ`"Q"ga)e4e&'Tl.Of`ң'[%A$ʃ|'LbNIDR#y;e霾~[p*NAc#/27C.Mb~v܊T]clQHwF)LaVMDuArc{8ۘ&1MH#rى+L)/*b=(uxI7l҅t }l%Jt~iDQxdT4Ӿ``@y'MxLtު4Q\VW+;_Q)7}7=4+:ss>&>J=Qṇ-iߣRhRCзxAk'7U۳>|#^ܗ_U]/DZ8۝D)5U5iϓ)N*KdNg.ݪ(ߡSž|FRJ7U6 PPCc}oo?}tag w1$_kc'ɵ֋XL 7eӳN K1)R٦'4+<>t<ܻK/vU#]̳^:AF2Ǎd[+/x͸"d-zR# DRGE.c9Tdiv<5H$O>i+4Qfqj>/k.d5]v1SbGY{]jzO?ֵ2Jwy:Q`Fw;n|3Qٓ#d''(UeEW!&Z>MLk넞)hƩ\Cq!{vыam}ow.z4'D q:ҙg T)d]ڎQD1Oͫ3V'`%%EⰏ)E;Ȋ9+ j7F*~I[Od7(Xd:V{].LCwq";5S'6:Swp3 &qnJi'yUCp[hvau`I)%J`F)LhJsXU>m,bUYT;-vVV`IKNlأR_G^AQvE)s/>{Yz6yGq, 2:z2yL{u#r^'~Kyq̵-뚮Ѝ}OjeaB6CƉ}fHdacNJjl`q/cӒhnTnOܠ΍ 32j$Fr̲In={_+S9R`EKaS-~䴇-$JfBY6Gr( {MЎPfG33Y*d.z2:M7 E%Q ԛ:ue*/nO݅Nv7BdZ{9MrgU>Y ~$,AL( JWIԐ-%Ilᥕi 7Ae;uJ>#N24@G*RK ȅ_O6dYΕM_TLtb#ylܐ$2߅y .%\&.' wfҝ3٥L$LC2լWE_8}tgUz`M]x Kc[Ec¬N8vܓ-$+'csPN^l,\T&KLMm)NHo5%, |^y5ۙ\5,0dyJ r.ʶ i qxtNMle2&xIl)!Zvjp!9ʾ_^RRg@)&YhRwG,ܷ Ke&)$Ýz{#80q=&a{|Tރ)Te R8 lvN&~׳EEX߁KGٮpLV;gY|2'Q! Q;W~R:f_^E˨m!`R5a} hvYogd;H9F:U sβjp3FIqRL)%+np.N".X>2;y͹MЎ8I,3,eb"CVf{9!mA [ԫj:7`Y&|2H# ;K?t T3LW˥t0w]E-YًC2LhC#RS. `VrNO&@:zgu$-5)QUɤH1Q|g7Hɱ3IڑhT13aj I=90LJu @=3S }ҝkZifUr0Pdy89GFMN`#BbA0NN"V>'/3=Im`FC=G"zD 526[ؓOԋswA;E/ 1-[XXZy>s«tF7pHa-LQ K 0)#ep1m[so06#j Q+;I'cn*L=  ϖ1볢yh-M E;vd7a!|rJƙ|끦I&B#RGR.od8:m eIVXgV`H8=y FBdYJP 6 x)Z&r6{8H{&M̰;i+ Lbg4%6XYY[1j18'Ya O›p@ZՂ Torf2kV,w}ΩsEElilbzFEt&fbmDF`1:;b4ǟ1Q58!&f*iϵg}NTUu)ޕMk=<}%Zygq vEq^̰ƪ'­4I;±ڇ+f3H{pA~ptP:U{^¥ (@'0WT* Jh3cE#V(y/Gf; ̠!<"c#&H(+X`- [',ChBc:}Q.-A;<@Sl5"RŮG\j[e Z"[l)1GLj@6pPPcdVtYBLim9ɦe639t7gy9VR`g]09n2;5k.  Vh]}qZ!< kKY4)kZJe=W( .YK0'bil:拞H0n$M$YgX:yRtaT-S9A \omlL ʫ`:*EyCަy3*@*m#!U\CsKoɖq ALHXc8l?騪@=չYT椆|k+>:tDw_VނZ2{$L`L3nEl:0ኸH ,IvHYiaգUs=+ة,h"5J wTkz2tF(v ;pĵ'7.vq 7- nje %묥u֊601nY%Zk0[T%PsJ63FX-"+E%v9im\p)̓b,dXuD8iꡑ9Uq&0Xb0GfMC[80B=r˪Hߐ}ap/,bjK /} i-ܴn9)U/|G>z lȠo:V;|_gwww{{[Ο?}kG7Λ ᓟ_QIx@^\x6;\ȅ5ʀ)V9섊bn͖1@ Gu`38yA t `LҢlTdqX\! 217:z劖KXL@ӟbQ TX*~"h!MTLyd#t3Z(fKb!ȲbAU,[w@s7k\Z{pN: KLP ljZ#ctQ7A{ /]9yihd~TnHNu[Ua@^VP Khz^'Xl*423"˓4.Wj,l ŭ@R ~NPSՑP"2`0zo2ǎ%QlfkiHYWDoGϗ0het2XcGgW٨pŠ`G~"̚i0tN+c&4[**-,Ңʵ  .Cq$T)Vh&ZX|s,C/ZV7UA9ؑEQk3uFgZ;% k*d-EK8^Xc3SB*2- pfӡsgmtVQNfFrBnjD% IK,UteERzuD- Jj!Ks28XJ ]qbf$%8.:;F-Mk1M#dXָEBhWWclVJUf7kB{=q 2VX-؇d%XU*~gJ MЮz(ofV$}d,;v(ڊ+&89,$Z߲v)VK"Y'`f\TDR̍5y8UzDh).9GD1td~؇5^"E1ޱqIn~Vp*~W{/֦#q{{F]؏;O7㸺 Ȼ_ -R-==|?.I麖EzVr֣2+XG EP6ˣ< ` J Y ݍ&i!hC+4򵻧Lefv6( khBjak'Ɠ$/ 8Šo;H2 W9Tn &j}ʾ"Eܟ*q :_ S$5 YU.T}I 35)1g(+67V(b qBL%|o>C YӇ n)H $*)0i!޸&p^+NA튱̬%cua-2 ۺ!Э}]Q%Qѱ֬SUӎ*"XQA 8~j$l Y'Tn' ,*&kE[aScXpR*_V$/?NC `n\q:6]ǰ8v25J}ڣ%Ac`D7(GcqdNJR uŋfJs|f4Y<󘉥hkYds1 v!ZXS AO$Uo 2tX<,2D*P JVx\qP3ڵ,FJG $ N"9^ZuZ FSX4S]%V7 }KVMaR5TJV+Yؠ s#+9R[60OۀI;h[sk\n$&Mt/h b% wt`,kNV܊[,cbZGzXf@զ> 4#M#4#Z*ZfDd]ۉClP`-,V|jc&В4}aaѕIvdцǺuphZÁKɭ6ml-QK"ʈËecG>^I 6SǹM?A0FsO۩Q+lLֈ7B?Gm*Nn>WJMmaQK 禤~-a-m#-`,->4BR< -)CLޛD:uc]7l4DID??7a؞ok#'~'~~o9*V{}Ux"mFI???BE57E/zQѵӷ!jvr://+m3p ?8,FKg3)ΗfkXl J XA~ gRSY5B Y09(jiCemW! /!E0Hy |fes|Iz Re@U 8ɪ*~[@25)ʲv4n"e)',uZei-[KE%k%q#21؁bL"ΝZ^f)T Czhәw]еL=bd7հwPx@Yk7 UjoﲚZ s-<Dc-V#=s`Wp;\+^C"e0MDž4μZ@,Yk,h5ͥZE1] xki?e3^Vp@1+UY`klֲ^2[}kh`"?Vc}"%zM*#d=Ř\u]*kWKHgX:-ބ.56a\MѴ,9ce7aPhKVj0_ b.)L8ZasYv0KudL3c1~OPLJdcr,Nq;}k"Z"BӨlzb,ÏCaҢh YR&7$ˣZlnV+lZ±as"bfP:Jk+Ir+ -&Ʈ,(-kaEbCzzjTvڿȅ'XH;Q%}ϱH܋&}{4zB&]BK @YϪ5smM8D1g 7:g U[&jq=F6@zx/W%NN@t*8Uq6Hü#ctzHөn=JJ)!y 'QjҵgR ?|t:46Z*V.)*oucύty{[ߺ֎7e4!h7k8z^wilVVysʞmG?gXMEOE)Dv11kS=mWEJPPN#0JcHT(}1WƝDrZ,!mL&WժyK>ԏۙݽ菾뻾Mi˄87*zD+:Z+>(1VwOdzјSG&֦yptMRUuPT-ޙRU QՔax 뇎qWŜDPUscAc1^7UYeAeb ԪAS3F*`Np9leV}!*,V1n\C34XXFT|"n}r3>4ԭ^e{'& IDAT]^#T1 ɮG.EpvSM8=;ԯ[WF!󄌲S"h +VN*(!p 5XUVl&Ro5E 5dۓp|kuo!u6he$k#aQme͝i8鿛NԽP.X_7:]pôK^Vo춬<٘ k Hh}<כ__DObv^'Ї^Wl; {E ^|37]w]ck)}=~~cT5Ml6/zы>яzȚ E&]yا">.SvЭ׸]h,1E1;l9$,pULyCKw9?K _ ay0iH${;vxc4ήpwe-I1M$3JN` am(U!TX+WiH=pMя;:̶Hf%kw}yj~wAjq-w}M_My-^6 }Wϊ]ץ#RPo/}K… hd[/yK/b1?=%EakU"W r6O?wլVByO^ گjV- JWVTIp,o"UE0R52:^,`Tѕ*tIQW=J(z8"> b `s|q}%O.bpn~ƓMf04ۖq\7| ׼5Vt…ɟrk˿O}uQ{…۶ueha{m37׾[ 6NXO)N|Im ŁGKTB]Jv1O3A:C0c}NsDI3OkWHMVU 7awϰ[M&?0.)q Pbaޙ _lX/ ֧_;J&Oe4Sr&{"z=ajv񃔮-bT*u?%v"9K"٬JZʁ:lK3zQ"S}QQ{sʀUsG9sd@3=q7`Oe?m=0+E2e_[IIfbPfufƁsh`xRvRϊ<G oZu%ۓ]Xik|A:N~lyhm'9@THC"_p; M=5꽍][ku%n@MgZWjgggMv7MO| m_oGmۈL&x;};9v3I8u{mf %OM?Qb;8jQvg>__NF1wygDL6|zBi]s=s(L7<=F(0%^<`5RGt+-CErE-eEYuQ"FluC/ExP:X6:|D leH>3[T?#E& ѧȽրC:~V]Ikd}>?"fte]IYr&"2؁ק_eYȔz:.e?h^k4Gpb|ޥGQ+[%ĚԺ42̠f:1GW ="ĊzJRKIUu~*yL$nzZC" `Yڙ`V;lm.euSM CbKuoຯUnf~GXm_Uz$?#?2 M6&Is;ޮ;NSw߿{Gtf<9ϩH8n=!y{}Dݚݜf'4Muymj哙oﺮm۔Ҧuoq(Q"9^5z!|9ۿ?C?TW)&N͕oFшSn23{DI@PJ:n[l #TozD ɉ, Uk[Khaei̛dǮcΪn}(W$Ϲ)J!Nq`z~~*{E,{uuk[8wdoڵod7j'd [&'֗ģ~ s XcDAkbf}'JaN@:Nt enubU2KЬUԯX+"*{,]M8Ⱦ0`/a_<|.Ci΋S!iVQXɔLdʩIa]XD4Mn y`{SNVyGa7>rHX`Qi Fպw܀|n]XZY(GEQVHfzߐYs%9 >z1}@b$? rݏL33Kf1 6o!"0R6|aIDՖh7ɘ!%3q&VrUGf(?~Jq{ONKGk)`i ֟ʝ4axltB)d#%M$tɤL|ܗ}ه:yZl6/DkR5F25'u}Qk~.ٮ39UO4{Or? xF{C!t*[JMn]IױNVz{]m@mwuw~wloCOO]wݵI '6eͷ/=C ;DymMo1[gU;bX@ޜ7wʕx`)>cظʌY&B ԩ Y2;Evsu^_eNu /Dѵe^wxɏǬ i`b4X$+Ditۼê:\Bs7e_aN=)`yX]]'o QnXl*Dk)N|S'uRa~aSEψ(&B3vRþ'5%!pL0= Z405m$& 2 *蝆F 4ƴ, <=7QH5x㤦~f%sfTD\K'H !kHEtEV '0&"i[/rojCHj%4lwR%JG˿˳UU'Wyi\+ob{ZB0㚋"PXD4כurYDHj$Z#.E/h/,qp1@ Z$~!l](r9\ m"q]鿵<]E$=4R@5PTk? / pD(JK6TXs iFC;zS)+j$Bų Hr0zf8AB\۔-r$7[&ܓU_+}a g6R+ iu9]3SI&yԑ@SDXEg'y6`x\WsH,gv7- 2@vw0G4)TC5I=▴x/bH>M'xm"$g/son2?{B8/ R'#Dx\y[+xᰥk;|"AC|gfDY~}t^\kLdչ4qAJUPiiܴ6CMRH"%z"Q b$Y.&9ijp^E( &ŀ[HxBő'T2!}2`/#Ť*·[\rd*QVoQéxDSbe5YɩD- |Reê2K[FJ0x_TB{BR X$ɲFMb5՘^N#9fVwo (o7H:ڬ~>|plkvaÇ}_җ *bPDJo,7AgO<i+u>—2zr7#G#;9{WLxwS߄4}=̦_}__{}5}//rU l֟Nt +%bldQ(mH5M*T)/;Za@7\O$dϥRAu+ncJ;."Pp-nc!q *%&x s _j(I*YpC GVx+`k/P^2h3QT{:01SFrY|N|2V*4 O]mXcw0?+Ζ刘G`ň9Ϯoz:R0O68^ϥ=]fHB)d-%P@Rā9DL 6ޜ2f6u;8]}8_xV_ Ncj t`q!Mo] [s)!PO5y{] IC}_Aa7\*ރ# IDATxdKNaV5ȉv.nW2FDasA^bRi,r)((xeB4z&Zq.FCKX9sZvp@, \4œhv%M ,4jkKȐ~ŹvcSbO650 )1q![͜$"^]}f<#ѾͬP~x<[*nqɟBƒTs p__vޘJMTr|3v8JfHCS*֔#,[]nAN??X]]-%۳3+^U_T\,DtDGJm_X TqrNCS n/o񍳿lO(yyN!Z/&24:Jf V ѕ ]ktɄ;A_-'3w ;+d'$oet<#VȤN̈́R&3@"C 4”zBJjEYVUDBJq 1UqhGja tJe5e|Ej!VUh"HCDZTYAT:ZҲ9fOW0IUup::ZIzک&B0DD"kUmhf}] ˔ +pWVJ@/"N |ҙ8$lX<=\KXECzRãj{bZ IVI$=],òЊ3Y=!UkWeZKq9& EoZL'DJS$x":Y'~h^YDIi|o,8Vnf"X u= drTY]vK3GRAB*f2)2zIcOʬ j&&t }c+l\N=w$WRw䘩{nxaϢ0xWjXb#Kh^ Hw :PdlR&_ъ֮-B/ړGH񪧆MR정1g图UmE~053R&*/%W(څ3LNTOhqd_!an5ɮJ$c׀oH"}{`tFV0n FD,e/nۭ}{cq9fOե_ްҙJp-r7l?֭ww|Y\dD:LIٔS[o77RDԤv{-3_p|f뾶ʥL%붰'i"G?!O\cr+ף#3Yn$&oJ1s5HzkW)^{2T$%Yg/' ZVŇWFʈ;ZP[ 'ގPօPGJ,E9*IK;Cy8r!Y&WgR{jK<(>| ;AL`z#rYΒ08z*NHd'bTXYK6f 毰sT%pOe&%% 5<汣p?nxn9ZT՚Zg?{3?D|WC&"8%"Ki B"-'6ȞMPLt%9;~bZTTJ=BZ՛ʿ8SW{7KB&h4!GUg4tUynm;.0xMPHw֞5r.Rer8ى R$n8ל#|qA*RگʯT&껵|ݘ#Xǎ;qDD <2[|\pa낽AOUzJG>rر[bn5̶ {ѣ[= nLcH36%?OofveH}H&+m."~7~cmmm6gY`v{]u w]'gߺ2]GJU%2QNU7üdZZU񅄊gWF䪫psH"x_ڿOM̕hEzrwrת5<b/XiW,*Ҋ&UO n;p"1{Dd@$ +l)dXf}Q2|J 5T"AGлShꎨ8Ģ6|,˰HT=l0N!L56*: JEROK%` ނWk Id VBiB&ݍ-tk%Hm٤{%/zhL_5LS\+B &PHA10͝^H[T޾~u׾hn> IHvb$*hX*5-Hj ML-~{NXgG8)fdcIW~M=w)wݚ."b\ (t-]Ӌ,*4iEMʒa-$41(KͿÀM!=_FWՃImȪƝH?nc1PmjH:{z8h:o"iy ՗S&S..5ZVb+o Vpv1/"}} _~fFrOO?#?=ٮjۿ=cֱaWUE|d6[5paV۫SMlQh>ƺ2.X.Ή/\z,"̚mo{'>񉫲D/ڞkG|FIWܦ>32d[l'[ssvv1O_-զS C+y`%^ږsȳ&O+MM87rn]]@bv.ϤBI:srjNH;h<=r`] Iڑ膦N+ϳ^RU4jef,_$ki#\voSt?dz'ng)`CDА*Ce݉^ʓn)C#*CܧCtXBn.q4[,lzV3{%KbR6 =NKjyN/ }ՏP!9HǑɋhp8j'h$2=p r}"ued:6HD:$cviڙD⪳,Vk mܳV|{tWu8ċ-Y#pLʊH֜t{\h3}ulir݁mTo [C-k:( jW^&e\\\B\QHH`Qw}CWo7{Νϟ).]̰?]ѱٍY91~>,wv'VwݫgϞ2>N׿z}UỲ~G9 @?D`!M̠`PP'lrTd̓;`zMGa:&EUio~<ࣣt,| G І蓦7% XL>$cmrU Z  nC+\B6ِY䱖'e.74n񍷬s3iYd}AN H&r1HѾҽ=\(5~Ro3"=K8}l0H= 0((#BHh6ZM"(i#H*.u,"a%}Ȝp^vb{ahR#U`[t{zpo<),Pry]8H_l.A &W.28/9Glm]$%Xӎe8:HsĿ7,㝌n`?O F]$Œ]Œ]1da߷d8l%D![t^@!Ɖ>oL߰dIFLGcw`b'+poJ<ᑒt)rx3x34"ڱqч1w)bQ2lc])'.* ͂t H`E7Lskdk-"7F.9A\4bN3`@%7v[; Fhu9`{6YS nKPڋMq(hBɞ{iq %ÓeSekq4:_knH^heˋ/s$wȋһ ]GUg}^8r)T"",*/sAm34 *%0_F.8xD$ h MrK*A~spx>b5)VBtq'E/7շ5M69{ƞ# ܪ @Z.t@&AOFWRd|)8膸8( ݌5>VxB~Ɩ{.4v޳r>( Od vw2;QMkG)1U-,ѻ@4jx9WOtw G \FXX kQTV5º_k!/4F?N\]$&mR=oX0l/rF߂8j\FJ_JnS37_^͊hӜ-wf_3:WPe[G>ٔ.2){,ϹTN'͋$B U{" K=+lD - FyECN"A;j{Mv-Imʢd6K+gA9Տd.yg7OO,,,lw/҃>~l0ӧO" sמH}?7&^WVV~G4-gwٟ ;cg1T|3_7Z̾sCP!t)g OFh8Z*Xz"@、R /$ T&7LL&ytte=[Էy_ BEヒim66n?> q_.U9>WkUAznB6=fD䦕o Ubkzh$L#jd+ ]Hƿs7^ IDATe]߱g91͸l8R|*{q効J5 )9Ԥjxٝd\bGEtABM\t O5L)q o'=c屏с=:z`cagBCojr籯:՘wYHcصnaK X͆^ Oxt}MȞbJ:cv!ca$qy˰&vЗγ.~e2,.ss5ͩykf8SIB!9g/=mf"E㴮e.}ޑ}ɾ>Irĩ `^9|\4WzEڤG҆D!ԕS#" SA*يCuiryΌAWUn/M:p28iqP99$O ;v<o4eg/НX EXE؀%U9_p:KD[kI5|i,.w{d:"N!.x4yd_JE]R|\OwnY1?̻ ERdRy66Ζy "r8M@$xψy8刲E`:Hd14tKEx6Xk6USiwp*LfV]>68jD$6pׄCKTD/?'N~.?q;_-22XO_Ls|{m\[[sU*+ Hxpi=-8P{cx!8}/44[&hNc9aOsN#gz n1N`!2G>>".o&({9Z(\(Fz|8!dJ-@!ՌB%ȁytr3rY f1}onAX3!)DOp_q; KEϜe] W"g#.,+>?ǐ wy2BڅI" Vх̋FٓdrUC]F'vH|Wa7<EB{8)Ed/68Dd '.{r4IJV%wx!pRkʆE{C&r1۳ bpe`u8o qtXut#ĦӁF 'Ed%낰sLC& s,¹ '.Ej %kD4Cv"I u㢰+|oaʦPXWܟ<CP DvĒ6l@f{;ǐs;IBQns?uAdC]%D[X@z.B@t6- @OA|G֮p\yϔX@:5NI 9vs A.h,~mSƞc (㔞,578^FRNҸ6'3Rx$HXYg;зkbWJcRiC. -Zt\Ǖy=` Anκ3Cƣp- pgMb-K3{</ȌDY*JmkH ZV BzՐfiU-V PR@Q"yȘEy~szqQT[EH2wﺻtR*/i Z:ZWCG E|܂uCr_0TlT 6V U֯ [ċ1q,"(1 }j=bK^~3`Hh,{, dq>*nF$RJʸ6 e6ygAck0 X#|:XO1 {`x+|r3e9-} ʿ NT"YR IKaw:im6Ff~TN{~ %8l٘ v X/yaFBld t7F39lUg=WS9W3?hCZGύ'?7*3h܅BKy%d)7_v.ށN$_y~C%HB zo&v2yUx [> O?`O O? ʯ?ɘ?38"?'O?Hp#~J?a|k_$m>0 ??oƓj]\+i~JiTW͉o&r{/c5~x|&{ xXݮ8)-xƉ/W)qKF~DPtt("c"bH F5*>gL,Y}Šw%oƜ+^z) "y}joJ z*I ik}`F]iE95K̅ 0p| ^0: aSVZ&ub²?{O(gdѬ/Ë"]Q/~}(1Wkz@݈#`D";-l#ǹlzK ##՛jef2Lg/6e=ҊK%:CxD,LJ͜#hLx 7ጘ2="dD<'A8P#GI^l1PC;S:A M pѱHiOtD<,d2ӅS3D4,cȉ%&, mԗѰĂͬ1V` FëM&&F񏃷zfbh$Q#6O>f<ށMhFtjι 8. Ͱ^54Cv,P̭ 睯+ ;Qg( z  ?T6JjQ z8O>ȥ.Ŋ_.x$rn0 }zjy>MnBr#\Smf;p@Ds! 9ܹ/n*M^0"-s ׄhZGn~~J·*>nnG /+ϥ2Р)h~Dtv ks"Gzw#pd (U-#I%W{a/M }Lf}v|GK8P 'w(18!v8O NڇY2@\ ߇$ºnWSY1y]| sfON )JRn 3-3TbƜ5K ֿhElPzHbPrPDx,אW—=FC;Hfb9f"sRR*[W(rY*HK ҅ک' 2j~nwRceDTY|.*M+RƄYGԟkm:3+V&O&ژf adaE k1?# \g$KUp'c:R :OQ)CsΔa0_Pg~k0svׇ~ c(/..nmm񯅞>kä:T'i/bZi@ݿ}{ ۤM6 zZ`sU7Ua;.??7|s^(R9s-GyT)Vʖե_ [tuL>#"ׂ7<&rKn9mET[QXNuV֋\]<=NjڰulP>Dޤ>Vį; yC˱ 2@AP.PBEG+ş3օ棁Pサ3g5?to`A~%H23a7ouaHNBiޓ|6MbJ ׌7]ꜳf1*1@4_ǞD{9Rx]yc֒O1>ވc kpq=m(ΕhBhJ,tpYÙ1L((d)S|X`lGĺ"L 222=|3a0\)d6QU5*i '%JS8szT$LX `4+B6an5$çvd``^Q51CE`x61Kv-6s/A_Gʢd.TS Zx ]$Ky} , ncR TxmSNΦ,)11Q :m9Π&W6r DrDD/!#)؂?r)pI7|*q9يE10K~ٸVβb:Ү?`hROp ! $s)S XwrQL&Y݅mGD^K[`VT;-g5~kp"K>FORhJ5=tg_^NCz䍪(Hjyi%ට;ǴU9OElDG(b~`2,ށt˰LKe⦳!v4X4nC.L,R%?iߝR 0X ^.JJIs{eU\2iBNM|X[YsN-сpUf{_b%JԷG㖩6^J$v[iҰi]NSp.|z-aCZic!k=xg {ONN>p{3.w's=E {Ax㍿Ta3D2\0DSJO$~׳)h9UsR#/[JM2<6 "_ece )h6J=_^r)Qϑ΋Uv#?}IEZ^$-we!b%^Y;{]5SoYy}Cߐz6W(J=\-nUinra`twe#hcъ 0 ČRѦ0I&ud4句|H5|9$>XTv >t/<I>.A+"*/YFza)CCɄ)~޸Hu[8B`+\B'[ q<ȧu\$m$cSePr6/ደ HFfEI{NlH>WqTq| Qv›`G7vX].Kb+I H$B Hf>'F¾!)ϣ9-:M1 æp9n7]+pKW݇ZK;LS@idBe/0zL5CNHC tkx IDATm9p3Ֆ)1DtYڑfX iZm1ƄѠA3X&aIsxpQbf)P8 Q<ڠxE{2!C4YVI,X 'WK9.R;cjN3v=3)ҒEJ c"#Zh$%9cvpG&XHDcmoJW,]x2 .^Gq7y-kmLlZ|qUڗ=%&~h l}9O;BWat` VKѸ}R:˔ʮ68&-JC a\7&<B2,95, /݄qa܈e[N68PǬD@C,V ~Qfź3־^#F[y' E#R*MiQh=؁mXE|!Y]yF(Q`a)h)jč0K80Ӎb./AwuާHY`.p$+ys8s#7~nLPVI%|-+JPʰ9˧y(}©~xnS&:cg8//qDZ"_]N׵M,ac; =Gẇ/Ž31`Wősmy< .VaL'_,̉-F p#(90py5Wa 2>z ]DKNDI{22MLEQM0&C',xFTNue*q|}.<JDO%g"z?cDH cܺ{/gc^_gƫ~Zj.PG˩,5LN|dl#&1yXa=;u}1+y8V4v0O I|"X|]J>n1CՒx2,PJ3qR_4DPG#(qX8 I5͞5aIsQ:"[!g6-i>Ym TfDD,pI;LPU<}Tr\'0qzV{^I16 a:ɡCgWRS95p QM,80 ]IN1O$IK Ah:L-ERYM%<(Rz)(=cu!]+&)睻DDZfB)Jp-tJlם.,򨥨RSd<}Dp.|i#{$}gN-E$?r>[2nc4,-`21&)t4AaNqr,cINk֑!F#a3h젏5Zx381H83Va p&t4z7HĬ8DiET.O@BD'Q#}$ѸxcM]km-tU|pL~[۞.)eH":yKQ؃D3GuAYsܸfh|V-XFS-$IiF(! FEbeoz=tSd0d?3a5hw6J 5vnK1 X&cĴM7ʟmYgwi]TR]W^jrw,}D=$d#&$p!ٹ"Ihcа,OGnj'# mE~,Epo5^ $5GVw#fV¯ s~▰J‡S)/3o{{{7͏YOo|<̓ZpZ-X?|x׆(YW* ߃~闆󜗴8ɑ\H+hzU^Nppr Nl>4g|]6qIYu47 #e|׊S`S2iN[>hVE:<;K1o}}ޘ?w NSv{"eDٟٷ탃#yRZes]oGܨp.%n<q_$^I;Q r+7+-}w~sE7y= GJsdK|c <2?8MbV7?֒4%GeUI8cbcSy MsmѮ<22Hu𲖉햏M%Oc bUt[;=䓪֣PpP7 qxK=,#o9%ie''A[J+ԣ]4{"\`Fiw: &~ҁF)^sӔIP)S5~ivs6w^ζc6z~i%bQrj 46R: |W96aئqF,L4֠iijS8#gدTfQ@QOSx+'N|0i&[ gpiBFcS=8(6>nkc~ ]EX;fD÷FA3ѵʦ6*K~QjʃuXa$޶ks-)(>TO9xԳ([[#'^bXJԖ(m۾Xc^FFHgQwz#y:5SWktW(eduy"F9T/$-/~\~,yjhEN%|82. 9ٵvO9բWZ;{.dP`I'ǣ 6`Y\M8/]c' bq .:5˧t}HM oY%Hx *RUll~&%si4*:.̈́H È:G)vz0RbtȉqJ1oYЖ‰y[10Z>UNnȁҼHz?qqXjZ廖sDX[u*L栉eT-ܫ " us3=!)OFww~dGF"\p>eZj)t; 9Qn}dlH{<4P6'|Ru4ZhE;&xwfdPFi[gbݛj)%\9aÏ%Nsj;q65ohw2ӗ $%I6ڦEd*%|5%>CI*zoYSQd\VctEх2>(£ $yީVص֔enz #HB,4CVula ST3^?:潰}/cW%Z2 ]3Ѳ퍆5B.\l~p?xIܪ88AإWϏh Y{6Nt6=Y`re+gUG=ā+i/M A{dvF 8e2₏~ mVL&Jҕ`Nm=1[XOQ-$q;2;CU'h^h Ҥm3J@`:e3 /l+g_V0\r{vΒ5n`-:~W }xGap&f::0Z40 {Lʄ3X5 m۔A7}%j"hgB)XӺe7sQ U}3Qlx0ocCT>}f~w/ 'WדB7'Si __Vz__^ts\A,Ze7X E.:^Лu*Q ;; w1s5đnGv36i2,v3'F`9(Rg֗PbՐ-gH/i?k~7?ua1?=.>z033ddVGnX"1[ ';f 9hqX>$>΃9 Y k-o[%O4y H7UxD#vS4SӨvhviОUu?t/'>@(VґmOKLt>"=g(olq7mj1KPF&%d4ޫx ^lεڛ{rP|(LPWm .-84?RoeGCgG dlK 7!$hj=/@-Eu'7=o7|GUl xer˸Q*&MfoCIA;ft[YNeIf'5>0L"S H,"o<0}\>'RTc[#rT){`,Q)Rfn/4А=7TK,'Fòs4&<i'BGW#Iº&%wĎv !%l@ϋP=3 XZ{ZyAxg{\Ezu[s߭gٳX5y|SRt~|| 'wm#9"afdLQOU dsmL}t^LA O~-tVKmE85J$t.vYjE#OVDnR4i蔜y_s=J[vt!YLG+5Ce^W1WUQL2(d&=20WQu~,c@>NEJSK,bF&\zQvԎ8g0 3j]d`cF'sK,&p"Yw9bLXcxN%&3ZSN2{okY}ksk"EVFFfVUERHE2iDԀC6  ˶40d)؆iG;!jgoNO\[VOtu ]anFӟʬRˁ9֍[.W1nb:,: ^6epgشp-ivU+D`10WŧDiKL}"R΍Faw8eKXdlg,:^w:T^ڬb>6q+ H߫ "P(f3n^40'ËP`cH&" $J=_ .ܰij4}Q~O]7g3UdSj.OO8y/ߘ-Ջ+| [=MkQWڸr:wg~]@4-P6~?s͢YT)1ma#f%XG`١$whN7?xLuEJmbq;mQ&I"ȵ њvOzh.uz.޸1{|K ,9Fo|I{g lN̸nBex| g 2,JH+ShdzO(Y>,:CyУ_6:u|&VƊ;EL[#ݛ8P 17>U]ES')s=^r g[Klځ)MG5&h.+_FS/Etla}b'-Ost$A$&ZyݚrѹAl:g+'!em\''laY'7b[pumRn)Xrc7VO*Sb$"z`FTID%!l;bϹn~(K7uӊ*e_AC=hRҨFlLp4fMWDU-:~EG7cJq[Ӯ jDk NG,lC0y>![#.Br3284֕[Jy/4 3ݤ{IơfS8W[1jK\,u=RE&;Ҽm~W:RPJ{aeXtۢeԶbm/G\s?ˆÖT?_L`+)F8C[ 8ɲ쒚>8tpHhhޤzH$ebSHd,REL~$ tJ~ ; ={p?ZQ,fJO`TI\l[]D\,qa&N423ġ,4FτœFeTRhWlhkbu=\nʥ&ns*iHlR.4!]ؑs[^%6ѵ-kE@* $ނ+sl4ƲXZv)YӞ5mj؄:6[RSHb~2<%33%GsŹNk8QZ::Q_KFoSuxQ:{"!表 ҕϩ[;X3qC-fuqϖۭ]Ō5آ+=^ ~jF.ۆԵpAvTƆ$H*|W]Xm <-G:TlW`c{ IDATF~Z׬77jF2 Ц7 DAF̯(fӰY -9a6i S Ng[('avJɯNC{ԃb3@DBTb-L5;'`VZ.IYn24+; TFZ0lۖ^hyF'Fպ$+E}݄Ʋ<\itaYy!yWqЭ__(rz~ ~}YtcZp }ŋ]Rk`+\=77O>}h4k|^Fϼ/E~?/UjHO-U~ rg:MÑi햏ܗ#+1@U*`ۯ:G/]< iM{"|[;ÿJ[Z&rE?su{uWG__C,,͉V9iIQ~N6exJ!ؗc$7{9l"o!xLٰYml},Q6JY!LT98V.$9&2""`6 qy$U"E\$.yX"&.UȇGtx ۝[1y(,6ܩOlEOgPeH؃b%3W~(=Ěd5nd̒ i t&U%z2]t(>ܕitbF'NN9uv}x܆[t+r%yC7QT(9i!^,:|7z&S* S6fhii=8LldK]G 5tʀI:] g*6m)&S9~9'3?U8Hl~0~t }3 [<OA=2'Ʌ_/<)Lϕ ;cA#L\G߁yLXIx 3&=۲):QֱU4V֒ `y,a< ïx] N>s`g-'/巨Ӫj0^iּJSy?yrFIVzZZ*:zlNo|sHȩ+fXFWdLgW׌7.|sCOȷ30=H#=#hCk,Ɍ sHsF:N4Ay+~[w1|M_/o|C}/ ^x7ğ Q??gYiwZ/!'s6`J~7p0ɷŽ o#xN3Y 2ƭ֒?jS{/O][UnC}0گگʯmKQd,Br5 ~?"߄0{I%Tî Ě<!W]acr8Oڀ+$dZ|8$sP- g"k4 d6KN5(B] KYm6GGLa64p~UNa[^ڙMk":SGqu <'V2sD+IXK*kA~}Y8ޓ|fڒ&pΡ;{YڶQ267ͩ;:3W_USRb,4y*ZbP-ym [p4m2ڶsY38 yt66#3.) j궴.g{NNJ>3u[5+[TW8Tn\S.WM5^@J#$CoLݒ"6;NBhO4۔~yEifv-ikqMSOӗP,ջ  6NYhi3X W!> V[JSXx,O|iLB֣u݈w2!IJٜ[lcvjfb9NDN_I5Ȓ=+և,e}Fn9Oeo87uWua^Vof9S^8?$bU8)AƲlPf4Ôԓ','+܃c"_ PtF,|or_ә1]؇Mͅz*[vafyUǙOv4 C܌؈xW E yJdTv>e}XT~'`l zx>u,g N˶S߃0=rOux ̤gh[VşSmr?hn8@$5$Oޅ[iՙ6(_Sn-Hl,I:/>=Oϰ}\V8ħ}Yܳ5DPf*.~H7m|uR8kO9~h>xsM{S~>W᎕4S+՞P2[ m3,jd拪pF)G6iqiQm>*_a=EփO~ُz\>`dڴuNG4KuR: ǃ=PRFB܍u<ϢLFi$K]JY^! j#P:e>cH%ihG*LEC02(;QVՎM!wӚʯ'סƪl#(aCE@m!&+<ab3v\M҅'_[)~Ah?_?/{<ÇZ;grWPe'~PKoͿye=˿`kK _ L?Z~|;q_. FOaMɚB)20,)X%eù6Aoj`= Z*88sJjr݂`7vP\kwu=[,YXu&{3h\<5Yr۸''?mYô5bUx<_IGҪ5%'0 B3 |lQ(qŌ@عSl؅X Ȑ:f%hOahmOfӂߎhԾS-:6-3y?3'b:'g#*C=۴-o>XaEY1C)$#t5fꧡ5raT9N[g4pjԧcj~ 3O%nJMpnǮ7= q '~`9KTr [p̐tSespq4}ͩrjc7-/>*yAғGKCfA;d ԃ6lbai 38@<‘v2LsE.&8t;mMl3ǔ #Z+ma]05Ѥ滩k90Nc],^Z);6(u|Tَ각KisյHzOh~S,7i>X|z<9T5QC'Qu~ÛQ>혒yvl8hJ5Ao4Mt5eʚFٷ4i95jTzLY-9 %z8PH ֽZGdΠ+HnS#yCHyY^=w,OQSg3CSxw~gm]%Z<2 BXs9?/Sz{9H#z`!{vHTS!zJ}cQ { ܼjgH-(P5nTB@6rm6.=[J?.*4QHH~|?!.|U\;7;y`H>1buiކ[p` o %/ԶhVΣrOnWR//M꿈˞???gD _A2F/^wR~ww _Iַn޼Ykzפ3o~Ç}w$/-d_xȐo%#rsjȒL6&81Ⰿؾ'%ʎQUt\RN1KƋjW aaA_oƿ5dD.5Q-6`X &8M"`g5PTJ4}g'?Z{X$hmQ3?F+%?C>FL77MoYۅjv:*͍!s[οZ1w~yJm 0go㯕_P4܏. 7Tk^E<+zY*!@7/m,BEY6 3pQElvV>l3YIhUFJ,퍎wcm<$wNpsؕ5x ?|<47tUV,.Ye+}a 帝NuQz֝A+XF r ЛK&:0:g xߣLK]CXv(r3@-Wf?_#W9[ .|z9C/d8[lkmhR3ccJ'f3q=*N_q5 |pG^gX}WfVj>B`3ubC2kO+>븉} "M8sB#|D&ntkwE>rdG:\؇FD0dAqg 0߳s.JtZ(|s!^,.ވbW% Ű3CM&.Sږw%4 [0uFk1ibY,=)Nc 7 %7G~>RɵK7)`tUx&G5[wU(A(a߲}T Ä8C  J,)C!_u7}4Rs鞴|mUJ!B9.8^%0˴B԰)|X!Fk@\*9,(vs$=G*d4*n3CR31|ܓnGԀ}}$6N-H`dE;a6x G#9jRb!z}5)̢4Uk 󸀎b/0bJ|<f[ Q h,z}+Z>Ѯ0y@!(h98'#Sp8Wy֠miϬuBK6 VnTz|Rӆl=m=62N4 CJ.=Ӵ*tj[ĖegdRtoZGQ+QVShb 4b̚NuMŸ`XZV "Y;Ţ.?svH}d8toTF03E]EPnab{,'᳒]A4g2~A[cMzE%wҮah3ϹQY*t0s M9D2Kz뭷?~|Z_7;ܽijle{{~]h~]uKmN?|I'>e 9e,5 }M7Ru0.(q3&w(8"`_ 5pmsObYn#/F.az^6,ǩvso/P@.rij} 4Ʌ6[TsuM7dhʼn -u ubN\MI0Te|8Ap;%(i1܂K,fдE%θQi87qX LaܯGr`Z9R;CpdS Ɖ4p(. 4v&*-%#aF=$n& d?3V.sJ3| I7-ښ{e(Er̨4 \9URA2TAe,, gN"{,ޣQ5a2A/Y2VYM6?G)Wf$(c3Hr(797&%&z ~&P#24IZj6徙0B ^x'=瞱Ewv4u sFD$9> Xٙl-Wܳ@=/IU7G q+9Fuզֵd){4jf]GL2}2U FQDɮoFXAѻcV9/̂JI~踐7Y!0Qco%$wg!' ]eD@<4ƶ97<nEh %'lHXBy(- AX8IXaȂDbS;)q7jZϽUd7{=Յzp\A< g6.ѿ\R.`hxgث1 Y!g!iden,V`=1 љrWnlyآ7}IR&̃.8EwFcϢ(Bjp^pePY2FOo4;4j=yݑ ^&;Qqzp?,CŅڄzq ǃpr"6Dl] 21cYn(%YOZM2IhVcW<S=](.j#uva6x1*FҋGL"M[hcFHb3UègN3sLVW&$K LۢN$EXM=y*6Щ"DqILY-VU{r0!TQP*ޏ|ni7wzb$Z@cj깒+tS+Aq .ú- b$tD]=E#"RPGkb!nNmq2n!)Fw$T*IEI]}!-?_E2Pp$@$\4ͺbCZW`41D$_l)p|gC ej|(m\$(#_0 N@ܒ_ {Oq7t\ sy )-4|}ߊZ03n/;#'?h~8j ѧ߶ *m~{5FGp8\uy|lqI!ۨ< `fwTr8 &FH޻~_ 7:Y ϵe>e;Hq{}^nFص ݙ/ԍK>W^/.OGK7Fv_WVC8]"Sk25񚡦)#^/QCEk v*1++:hmJANwR=5I ;7rWv PG=0ZۧΔgtj] >HJg^T,?,[}P-;ق9ckZ AS6QE#E.!.- ^<.EIGuc]Xu576 u\˜rXf^y ?y3d):fEyZ`rV}yDp\j(1'DT ']*EcBKv7-BC}5 ׈}.E.u{;@/zf! S>-8^#,y9J~J6QvFBoTN̗)خ,DtJ9-^jY-6uY:ǃ b^80L,Sl^fpjSD}.E)yehKck ߘ`FΝO0" dLe4ǺDֱѮ3F\s}Y,ߤZvhۡ̀ePaWa6vGj:f_kƢ!hk- 1uT$ڶR&Ԉ.GI"#idL3|Ul3TnEwjvw7E. ztlYΝ;m~7sUmp4 G}Dھn6wk=~՜3yei+˿ˇkn~a&\x qye-Va+ⵓYmǞK?:- ;DwkGKu$x5.hl] l k4K%Z7dI0b'(df *2z߰CB #ٓ?@%1Mb{0bQVI/SJ)_6*}^z-G߇K1) lW :o-N(3>Q&%^5yuWX&C2n$,sGD'}[ 5._$4ԯE=7ZY$Ӹlv% ǂ?%8Z4p,_SP/]Xka5ufnٱ& qJYɤ1aa5-o^(s^/u[[6kf"'`sT`|V]); 7K61Y,\ӫ cJܯkVq`?]UzZBM<"-9򋰅^H~|R5qQI-^U"KA,lfJf6gCt\"|1|XM]}oU^1fs[]NEW|7pX6FٖbNNrfrRr$vHdM}q5MwfWCѽ^"I~W1#w?}g>~GʨEl>Rr(p(//L2땵v;~~_^ }$! ;'5_z,9w?:@\,恳)"x؊J6EMb' \&a.klf5~f_,ԔAD;y|IsRPQUQKJ@8ekm ዔW =c\%-bl²P'2#vicW^~ 5bx$"Q\Lr(+=S@ѵ/@0LHoK)UJo<uŸR<.$@q7fP6 8 91%nRI83߮cen8z=8# v)8WI5{ ^teԯ.ba<\On^0B6r<"TY˥d3Qo'c3|1LI5AǘQ-[ب{BfƇGRJ+aM-rM{?(2A*ӂIxTnU MA}6t֏40­鸐A=ƢDYoUvP䮟 {C$``$9@%6ft]Cp,C M7䁮뵠2<>p}yE'C2o#6%7ncoub#9W+W؅ Ibnk@CE["_+fjm߶pLq_6@֣I m>Ï?D[l婧jbS|>L`#_ 7jXfG$VcvhQ'NͿ7;YkMt?'xQ*~]%0!Kbn$R\UR,0eMB}BG2xC"9`uDj 71 {ϩ}+{.E>*2zAYؔ$?HCkn  R4Zw`7 XM#_V9{ğQ0;`MiȿEa)._E_s5i58_L13 젥3*#zU|Vsg>f?5;=XD}裏^J_w1UdjOɊT W`+g㺴+k[mVAS˟ R3CH^&[d" $ ŐR4Q*4ڐߜ^zfq+:=+[ !a 0a4SgxFe8c!;Χ/aWqRuE,u}`ymOZTV2i%%.KQCbpy_~ŘrUdVxn,ay$07)|qav{IViP[X8ߒTLy9}=pAtX6h4([eHRRpON%ZFd*qmdzE ]qO6MohL*`SY&Mե4uq2b^qBD;v]C}:-FgC-9G^ȗ5v)jw<\D:4C]م]0p%8JـɕFPZAD.җ*FrHTUTZ̛Y~}lPYH=ǜB5b;0}ÄFV7 j97r5 PvOvԳ ('p[T `UBeDj/ bҿ']Mg f>s^:69ԋ#;Z2!Pl ?o<=o . sǍjRCZ7p]1>@ШݐDΑe ~ d?x#Ȫ0_ʺg{׻2ҴjaDZ#Ϝ96XGG.]j2Gycy@g nuѬێg4?3<Uu>K(̽kN0R?é]ݎF7/e{ !0;O<3|bG]#A>hRf7:o^-MqpW/'Idz-D${z_Ю8/j|G21_)h4CYM}eE#(tzWD%7D ƔM/ 8Sj #oJCBG WwS9#f)2 %a\#:Cwj6 \<2cJϡ-6}plݛIن.}ucҜ+YYHy$]zJtzhuHH.45d(7lɢ Z1%/,n*ُxl0ʜ@`#]3 Jv uF!] x}eYFJITa{J0Ծo8^-,'RSVqf-ШZL1DQd:ƷGMˁdƣۇi􎁃 77_Ns= i:Gu-Jdf Qg?h}s>KMgU5;O{JCmPҙ3g$zU{{ӟ~C?ō.r :)ʶu]#?#>hk[󇍦ovZb+?RmYvrtV&Z7$(dB}m2(HᗎI=8/u)lYⷪ}2_jtD "2}[@(]g֌e[*l] "˪YjRsLȵt6T;YhqV"! vB3O5t2(֩gS\wH&ǒZ6MUW, =޵TZIbWDly1FIal?MNWu|9K7$e)iT"(Is9wV2V"U4\(e T EIKыHL#w'O` }HAL+[m[ ~%>O}. c|xK*mi/O%XHXkᝦCOusmѸw$HZQ!"IAB=,&3ClAb2lI#KI67VF$!t-wtl$݌ dk{񢆶I+WƅYQyq$M@IM"SvX+)w?*~a@uvr[jXPԧϵ# .^ۭwRJm=sK^z-;,^!ㇵ1~Wu\R)[[sw4+O4Z66!"Lc?T4?QF+hHԱ]cJ %pvXK&yBms6Ffcz9Ee3 I ۛd :fN:)+rE|ʭj)NV0jT/jP^Z&0K8aSMDe:_A;_E% jPl3%#Vg҈>Vpp;}y,sqLZ *J&J!ߛHiA2~b %2Z#iU8t#"ӵI̫5jB3 CD%YڛDeJo ȍ>J'םO mIpђdvL3,Eb{]oi̕!یY[JgRȖL8ɂzzz%D]B;@+&-HB`&ψv qՎI8bպ?r2R͑ ݂!S! #Tb5^|Swlmmj vzp ?rwXȬ_uwvf}+ X}1?OAWj 60늜FYԲQIx|&d |#TW#Vx-0dcKK{99.mX;emo T`Ie`2RT7i\QvlZ?A9z{W4=/"Ztn X;lTC;8iB[.YѪ_Z` <׀Ma:HM(7ߤbZFa^7-; )430@e!"# PSs9|+g-!dݲ~ r/0:0]^vv- 6jRݜ,Q@PeeX@$(b z"bdaKs6Yxꙮw$M).`9M+%^뜄璩3*I3(Es !A"%Yg>بzT8;HlkVDg#t:YU"']ΑciIXJ|gaW\9}_́43^TՎ /4-;cs\EMVt[_[[4кCJ)Y۝ {'6LY ;b#PIoܕF{Uջ1Zi;ɁFט:]ɻ_M$U>o #t3ЂF͹> -3X(x$B֖?2q+8Jsr1H DeCӀW*]JybN;cZOT\Qқ[gDn6 &$ gNN2;sc\L#Z1ݺ=Ur8Q):8tUTH_m(!zDa>Ѕ;[$K_68h$I8A~`5@gq"ޕ:m6^t~xD>OZ =];`J*+ppPxH"++0޶R5hU"3?3krmw3\m֟ G &zJ|kke_Zb6D4@Tq68EvzR0r^f2.1(WWz:/ޚ͘7;`I4a̢E'b fxHswt"&I y?Ef.po{O4]vGj#XH"2 e4!$?rZᵨ_*_WOڭ1QH>ќȪؒmzdBJ˥@2+N|&U3 ]|IQNj|I:oeņM|NnE`N7#H_;RJ8mbR@1,8Gbx;R潳3>_ 8Wb0FIlxQ +_q^NDI?̛|ƻSjLB 4=;vU K"d$=$I"Bo5S|~&:pŐ/HwnsMn>N.k^?b+ 5XOf+BRZF*ycwZRh]e[SxÔ~mtr+#W,="Oa} M}W$$"-(r2{ 3[B}ι?>я:uj-;js{p>+p/̮ʽMQ|pOG^. )*s2{"McrםKܽ}kʑD&UI}*U]gx#cVdz!ƷMS]k)_~ʼ1gU|3ET44e{ KTJ!G{' u(CKПɘGp: 8|=~4&}V|TIJi(4}IjZmʷ M 7(ܒXHQ!CbZ1U߉T3hlPPRa{nbirp 2&ڔPДo\yl!ϱ+忮 F2_AI2Uxdz鱌E 0c3nnUq[EMxM T4-Pc9r!u!K=i;TrSy>Z?v9(i.QPa@UB`u|nsEh8کHR2MTSuKixmCxeI Nz;S !"mBAs~:G6d{ @#>du裏]G&F n[/XkcYVb|+ bԩS?c?/^۝gM)OTj?`_Xmy\6 qGЅQ[FY_aָA^&5f3?֤: +lVRL3Y(5aԼjr\ِ!B Td@{!q?*."CC@nRۓK&sΉ]q F]R} ` q5! R(FOtE/peMu?$*6ߓ٫OC{ SX>a!ΚnT[+4zÙ-yXΦcjZٳg]ʩ_gs~8k6kJB2Ȥ뻎f3z!2gӯ1ǥv3ޘq nD@DR7keu[kU}&o2d;IȔ5x$ˣf`<W7RyHjrÑ]ǖ7\ QpҽCZ\oEhxĶ0<+D90[:? 4Ўh?]:7*Ze w>vq+=!u@.Z$"9o_ggѡe`f>"3E 3<S=.$]YK6$660{si{G Gf,KS醴Bl .,>WϢ45,!"tD&hG# 4u6P^-4>侁P20 t r:n8<B-`ѫp-œYMn8;FOny_`M'ώ/3կ~]ٔ''gڟ1H7^HJ~ݸ9ԧ>ի$Nⶤ0~=1/o aGWXGknToU2`\`wF-mqo\:2@dGi>uOd$7 KkE*%DxЦ'P3"D& (kpyг{* ;"ehbML6m[!PSJQ$0d0"؄cO9!+T?Äj}dߩ7^1qF)k5P(snYcS,}iCodH.)_vM4B}P/̔g٦cb+H#uTt8ȋ=RjCIo 9mG>ד";7jw;(+V hf#8\*㽉=#uDw!I~ ] YpL{Eàz'2:QG9idRu.Wj;2dS>ZV>IhHe1D枙SSX 1hܱ}K L)ݗ̭]*!,r4țdLR;1|ݝ@o-r|qd d.qJxTm?*?HOl=l{uk5%OLXd@3SIPk>aDaO$/ֺ繺u)=~9Ug&!;J̞b\#H8-4Xbf!y-%BPQ $|ٛT6Q*,p(^f/$~+`8u"%X%+@ :Y$[K AH1&7~#tM4i|hT ;9%y3ؚtlqhƋ\SEj%LFJImlɧƴwrp #^D{ۜOi'q YV!Nqcy}Y>E'O$):}B{ً/]t?6Α(R`?S?u#y'??p^Rt* MEե7woc$w{KuB;Uas TѪYWz&r=RM]k#vq&RDU"@< ILYxB xx*y^E\aHgt`CwH&2~4Y$X0 6.woX0%UHGH$}Pe!A=E; A$Ck56kZi24"F"8(ĵYuhNP R4*AʿLؓ;sq/cנD^fg휻~{g4e OuդvReW O sZ"ޢUePȾZHT#L"|B Vc|Dl4)o ?C+yE4ZY&itp+T{Vreړ08-zQ(fo7boϣh'nR0,L m)0jR 4XKi;.^vjWGv kc$*69'q DZmfOF2_;h\ZG}3rr'oCEd{:f>DdY㆒#zooqgΜ1N\sN,Zkcww~%dB!Rf^JI$QcyxV#RWfZ#[k#ȽJ*hj]cjrA=ݦOB,@z^< L]"T2)ЏVxRG?aέH$;uKPJMDK 3vhh[K+kePʆ qj;N݆74T`nKƮ"pU H*7)= rD 4h mbѰE@R5a8 +~Ӻ:` guyyӡRi|/fq{w9LE;[ahN#MlN]gx"J k -蹮*1m/HpYӳI4nh~TUajK`}*)q #D&JrZ :1?+,Р[#%r}rn,\+!<^A#Dp\7wP}C|#Hl V.e%eop1䬶aåWP1j! 9LHMI*LMSnҍzHm\Уa)!!הKH8c=yD |dh_ ;8(BX'%tnG4,8RXK}aKWA]HrN^Ya!6V]k=XHbrbmc#Su'^!2sE.)YI뀭A x|c wn?Z$h*-C(lH ˣS5[l7.oОgS`N8mHE6^8#QZM7$WPhC.b" 66(*׈o\cy``a^X%hgm.PR2+8v B)_q{Y!ej{fi+cV\T72!smUIˑ5x}ȭCzlĺ۹hkP~˄9PÙB B9KD̓h1h7pods[Nø6WtXPD\ٖU(V7 g*̖X{U{K ѽluvCzvȒ `80J8lāp8 {ṕh8 C qZd!š.a 믬MI"&19L^!]68ef\q;Z%&v[ PC./羛1O-aYcD^]@uC}i44^@D^m 7 Kh]$jU&ǺCD66c^Y(ֺA DsWsԜՓ ұn;d~'qpa J)X *9ci$Ps9>|w_TDa?}^xR;'qq?OdƋ돛iR%>m?3ـ w̭,,l!rsM23Ѩ ""[)<T>cD`TM=Fh?w{#.qFA$"dSu6[ :Jp/~Jfn7xeSeV}MStsLMݲlǹ_$=׻u,J#< 5Vlu{X* 3Qs.ТPDp.I .sSDp^ŞdlFGtx,VGoVNtC6k*.Uk nLC[u6FLwIآ-IDZ |^Soŀ,]^rpϢJn݁JdiC'lo۽c wD8[SNKrH}%%5;A /T]VxG7_6#x㎢hIJo.ĤVa]AkX_tA&A!aP yk)̑}I\!e\VڏxlG#NV^,mUvuP$\)K*4PB6#D1-<3 Y5m=` Fsb-KKTk):3%[-OSuƇ6ҡ:%Nm|\qC)x?gY {*B =ּ՞93[_x I| 1`זCM*x0VvDLgOXRJ8%-|xH$Y)(=ӂ1v_e`OM#0;t DS7PDpk oA)@kVmg3|hYZx eמ)!4FnvIc7u} uIfdT|zdZ-kGE!j kh9=}w\?S$cE%qkw׌#*g:L2F\H5 @]q ^Fẘy,xeGK.ya}A|9~YZaG|j^u~}F*@TD^ibx`{!r]aQovDC/mL7 jxK87& D|m}Mطuc ZkJ~F/axuw# cDHXdH GHcc;BHNtIx3a=7f{l1@/MbG*Ht7,WC n[U.õdߒ[Gy9uk*u ^؃5$^ ƀIȗH5,)" Mb*W2Ҋ!MT&`x.N` cd0`h)Ɓ6m# \MeG6rU%3E YSDu<2:z_>w@e6H5]JDl i#QU\bR%{͒QsyLnKU|0bgvmr Kq)tLBVXڐ]Eju0"IL5%^9>&Z>U4eǀ$C1aO|BCHdD@foa/%rܻI$ -"jPk4g{$Fb鎩)4asK Hq do]z^Xv,C-K*Ke-*ȇ8AFPj$4"@{) JxB~֟IqҥG8!=Y- 'I*.Nf{Ye@%.^uUEԕG^=Kj|x5I&tܹ'xR M_ݻV=4 ,XDM+Zg"l7m@r"Qz"8ӹMs5nPmaCL1]z󄹇 IDAT#B{p9)ѻH͒go/N~ NrX5ISZp3Y$*P*gu.@ۑx7#x/j|4[AygҎr?"FxkX (vD8 ɝEDH*ݠt' "D$l!iMA vY \sC[iTy8A--")W\iuq2+HKuRizaK[9\p_6%,A6`o%H5U[q$dҊEV+>H^Va{lTN=ۈx*HoQu]4:oGZypގ|h:Tfq?KDYK#hc6PsYÂ8mOV~Z<|N ұNwafˬ*WIσ\>*$~l/Kn|gM=6 q~Tm=Rbu%ZT%p&F-uy֟IIq\@ڥB>ϟԧI=4 ;&\/,V؋F1lBau.1@p )b{>O3']aaG?ʟCBUA*Ǣٿtλ"W*vS÷-f R'׵bޙ"N.w[;Lkmڦ43.4B-U & ѐ b;/亊iZ*F&Cm=Z!N b.#TQq>ûAsN2\DٖW9" 4|clFh;^uԑu!Z3&t\3>d@ 97f$ !-~6W^\ny=xdXy^گV»!Jj᱊",-kwt*tdLQ. +<6# A\#uqF'Átyw~gFX[-HڝT(_sjS2.+ X4F-I$(-5Y3Od:,W(˅u T^uIJQyE7.D*p"7u>J< nEƍ[k-`>DfBЀۛh.`a5&$Q%-2hQ1)HSي$@Hk lzܥ"{g7Wjzqe*gɝjAFMT6Oi {5iEH$DOYVӈYcxI*&W*@2|ވ& -c+Y8̌o i.ňt_cgi#m5IyVLG@%]D UK Gwo/DM 07 ƌPp, ,-#EcSج ڰ-jiM9'Hxk'K^*_^ltq=hJDzK+^δcz'"|!9C+mQU-CiQ O\N}؞-P7|vؘ>kIg@9َ֙5ҾgX/O0סԶp b h#1ĆGY2 3#? AKu,Z6jZ9*IS)C kXlms=O$Nn>w޻ږe]1Ɯksߏzwwi㎉cEQ0H!%DFID@P@H)R+ݶn֭{sk9VCSv>Cý={|cݓ8xPկ~Mѯ7Jׂ% JM I8sF[iuYU?O<\)gbfW襝<`ھr>7rhDͿiu`mI$ xDS9qmmfc71"{\Tc67Mi)H24F$cYyY퉥4[𵊱@i^^E GR| ԨW+[HWԡzj.aJF':X5eh/n{#fTPx^lu}t~ a d23- 'T^dD/0kGJ{v~j?71"u^Ipr>tS=oΙ.! `xy׼70b)SisT%p%__la upߩz"v床Q>|\fMH#';Nu[%ta-OZC1KԹQkW<Ǘ)`O4Zo-Jģgm_XvSdM(jwnIkSÐg·0 r M|g=%˨7d$IRNTd Jq\kPğM+P!BlF;FG6j=oCסL3ekEyn#9Dİs[4 *%ZBP Ѣ!B0{Zk%M^:}>g2Ruoי̌&C.3gIՅO~TOȑШ^N†hQZs<0(oS\Gk&xZqwAhEv/zK!ؙ@(tlGiؘͤB##a 炢R<ox"X73 45?n5WZ*x4i݆7E'*em->l9w2> vAa# BĻhveJfqn{-6Jc] $!|_gbYY@wuW/z8>TJ)ٳ—~75m> @NsdjDm&ljQ>_|0rƬ?W3 Vx2r~[ݸ<_bseހDHc:Z[>P-<}ZK4j!ī鼬b;sDG;ߢ; Epxzc#w4l{NWlt|)qyUXD \L*pD$p,u  H0#K sAP#ŕ-5.Pqc ,%I] |+ުy?`f6(RKo >14ΉLJj #Pj[vx&v3g 0-_V~H9vz|CpUOwUp.tTwM݌%Z7㳾8 \8h˹>ziNo?̽<֖xj'=!Qe1*-d6n`E"ɗE(k3;0[>H{7x(F t&L;Y:'\̒g3vׄd7c3NFՂ`v_8F<]D?I?7r涚FmCcTU*݁u8fS;E龅b>W(L$ݭːުlw_IFG԰^_˹yewR_ m#&wב8]Doaת̚{ه3TBai${c$IUT1WFD τ.8BH"+Dd$U(Vr+j^WYV0v_+@mW d]p½ޓ$ {D8AD"w^݃-~ 6B%5s8:lw?&"&+g_2DTֶݛRV$i)ĮÃ7 IJh?Cp4JHXµAnzaB\W%Zm54pR^⛤,ryQN4:B) OHJysu|bk8dJƯF~e:%צ.u]9'E YY=H ޾ONgbYYg#Z| _X,g,6z^a,a?j7\ݬ\($ޏ5@jH8hq3O~7.8MYsi>Լ*9.5Ro 5Y<|wZm]ikH- 7N͈ƔI% {B!uS= :aNJO^ܚYBPA`L\n9ŁSJ I3Cbl:Nc5f$E̙EYzCu*8MX+#pjwc'%vx 8-Tl  l7 ? <4y@v)`|Ã#xkufBVX ʎ+*xS.Bŋ"a8䐞IS;y|LY)_),GqsQu 4rt~6Xuj/Y442Dc{@Pل2D2`M-=SўӇH̲~򢄸nzB[)-\Q5Occ/Z~r%7>J |e_93IGZg~gQp?hEZEcT `loHTqԀHQ)#۴Mel96?/o埗ў6t(b٫#Qnhfn"k]A퀞xi8 fPR]ipsT.&KvMSkh2e^EAB\]$;Oݴ[gv_K+Cm rRo:צʚb;bPd6܀G:̥gNؒˊp~˲G[ ,(p#4\DTAKLkgjkdD[|F TEPGtY}D4ՀU&g [X*0]<^)/zhS&uݦW~ Jg} QDIG2h{gb ]z^'ಷġ;׳#3U04I誝 ~,cՀ#Էvv/@x9DBz|~D zpĦUjAA-]Dd%Wfb}~ΙXgqg qnfs9~F]Mu_\t|\*p [_&!r_>^9 CvUw_Ag?s~`0lY^MO|AP@M{p$D K穷fdrIVC7Jnl[` 72 oP҇DW`TOu`H/"E{Q'xke,ު02 ""m&Ƣ(ۣZ.&¿'vOe>@-@'FҘ]D呁E ÐMP0ӭꨡD֪Mܛk4C &!!Fd_xmm#qt{/ zJ@9V$@SW:4$IjyV%tڝjٙD*ZnOt ]QbNͳS'M fP0Wɘ]ZvF$ uK3`jI -4QʃKvs*`6GڤuPǕx98u]ݕXRLBѴ9Z#[ovMNisj \i9 љhugqj~ _8Y|N7o~i r& {@~oȓNiǃ?.8zJ f3,&ܽG?-]Ohn0?MUHr$aR׌ xc9F'fum*u@MuK&jFi_ZO¤YoLh# cYLdumvbDMpVZ[YR+krT!P=꣥JaM4am[:9D67 7$<o TC%dn,zęZ kFE1-nְU@|[hhFSpa`ܕ2Gʀ4㩍*ԏ 85MFF?ז4HBd&Ja-L؈i=^EZ.0Q3] _^S Uudц2yAW&!Rcbt&Z.sm1I!9Ej[!9v7v*  cQw7)8ԊtbQR Vl5oZs"ǃC[#nSo K$}&M܅\o'pQ4Ob)@&LASh nk$&dCf-7?@<&Q%rҴ2%A zYX0r !3 y~`N}10Tvf$ȽL\hb_UY1_lġESk CMJ5>Iapx<VmRAuchX0c0#mxsw> f{?ş&WO@2i>SlZڸZRii[.H>QUo 1Qbՙq%E5\ėtHąDh\Z /jawtS4AZ,4$Sb`v{m 1`$THbckA4'Bۥq3rtn4Ȳ~nMQtUz6_0X>7- 8N% ~K:d-d4*SMOC%>#'=8 A9v(wB ?¨n 'T%PntC%\;Hijf=A$~75޽żã T*W4y[9f_,?WM*C <yV^񤩖 @m#9#S儼ƊgN⍴uEjf{= dvq O+Iw[Ԍ;yk ɂ A~zw./lĉقJ #dd "gN)ՊISCBPS7Ae&A /ˆziCfPDq24{^ңpXhP+jxV̋=V?64Ĕ*mq#VwS;b9?~`%51.X"8+cK˝ZܯA,dS@^ؖ:&|HT}غұFZjdB`hhizy-y5S>2p⵺&iHpü\^ uhIl"a*\%Ձנ(iEBRJ{e;6)-Ćklz #KE;{J}̸UFiWHZf*mcRS5uN&ΐL9)bnSB4@h43*4 hlK&O WH}Dͼr{I h4꿲v+t 썒mM넫<%[{5R`/FˮԼy4Ƚ:BtË% pN|'M}oEO(c] b̔`Ƨ3/tLDDC~i_OO۸ iΖkSy}rQ%pͮ!7QT;v&R-M.ҴR 3eU+8+}}}k XJebA;cY0\+;(ZP秨4zZN ;irƫMҾcYi/y>PD?1Š9heq-@mTsG߾O#N0?%7fdxGHD@ nE̹ sJpJr*+)-x)m64<[Ҝ1!#)^]ͼG]. Ýqyi{[NMmDMXx[7-//9kEˋHD7pÖ]SDy#˼MeCuiH/]K(Pc$kLrXięljO/8 e,"_F dQm[q^7sܪ7^e@T rK^^ҸnmZZ1㲶s!WB,K@y梔&3FCSs}6_N]*D,f_P p4,Kkހ}D/'I0T_&VaNDD$ ?Sp;c2ܧU' uo|H"z9}ŞPL3GKp R$TzzR(]P9__ )p*T'͆CetQk"NwD"WZTo9+āqó'Fՠ;6N3J(r9kBUl [>ۓ?WGWA۩)"G:iB$?auHWBBo.,cKE#tG r7ATXp`CSpNlp][*KERI:F;ӡ"po?W!VRd%a X5} y }bR?V?9ݟ&A?jOik7PB$L%T.U!DQhű)ҸC[ )R%חvryL5dGoi"X^FB 5HՃT[=QtY;53QݒҤBY\䇲^Ƚ1N]T48NXC<񘅐"\Wwּ8#C72u?w$"0\9`B.6| Yt?G;$f%ZŭO^.-ShoEDF2Rc$yTk_fޟZP4K"L-Y8-JV̒ZE/$Vt0A'\yͮo鰝aƙXgqg?pgq#qx`0-ۻm)(>4|UJ5Hp~qCBPZXJͧh]~=A3fT}^I1mQgQp qZC$mV '=N,݉E!f΄:Q 8E!?wd㝢c~HUq +yc1ak?%&.x $DR}&r* v~^UjH 6EM1h**8s 1 T· 33!"Lֹ,&'exO^;EƖE՞QpY0ev*ehLӖog^w%T_9 n!]&Ta;maӦv!!As'sIr.ŁLMß^:U21ſr.X-7] uPqQDb_f[ +aN'li:%GjJ[AfWuuUIHetɆ1,1% $ry Γ8$F.FۈXefߪz{9yX46tB*ԩuZk51۰TD3eB2){*V-r+מ7,"M;}^.X,5TnpȌ+ynʦ]}3I299)Q"K٭W[{IPCö|$ rWt?[C]?ϰ8bg1~dR+L'/A?kcvR%PפЗJ`& q;){IpC@=c+w6O#\T߈ tA?G-1݂fЄ&2]T>@%4 >dz0-;)oRSIN ,hQ;w=L4i'/nh:e;U˰NRL26b҅ESUEߙ 9<0Ob2RW*`/Aކwб!>ӽY-kbKpn;s,ynHhCY ɁkBc? 7R.VkOn4~RO a SGKL[LyE&5i K`ű2?3J0kBcHmKB#OflS%z|xor,p|PE6]OS`־4[,Y̒SRPG1&3LJ%dh.J{o|1+PƄk*܆ Wθ[X,;%gE.b˵ hP,} 0/t ]s_oR>^BwU 0?bta62N"U* ˱7_rTT'_Ѕ=kR>YIJgCʮpqGTX{C#Zf72yP|i q`bڕPG_쐼 5\Nwy϶s P5A"u՗l9r=g}1鴙p>rICU-7sVp{vBDИن h?~AQlYw.d,*q'DhMSr4,\u<#Z9E6qiIse\:'H*YBUc˳,!Kpd%l.]%;%؃';"@hKrGjxˢ׵?]d{>+}&HvIvoN6|T#FɔP&oJO$ txywg耕,8靴!Rٿ*\mzF )"Q*%gJ 9NYj 2^P=]>RD,awGL 5ۥ+4 <#1<s[椤~H?tXd`pYJe,UO6p>U b܊\`p.t ]軬ŝ7|s٥x ]BzL\n,({Q]LMFR378bx:2 w;b'ٱ/|^ ~诵c?c)w|x!9? y#̰f沣K~ϴSB""@ߍg.4TYP `rC D,A>(IXu aq^Ϣ}r ٓ B/?bD V1yJU&|(䣩 iݑu[w-?LzrbUVFlH ~ OA!̙^x  tTCRO'V|KO_YW".r8c~=&V!* &-O r)"*axs-O; UoXdWfH,li4TRQ!5-TNN djCHg@I)B *R]uNx~SJxiuHϺkC: ! "H]PQU_z$kRlȞX!NSA+p.B}2R' ~a IRN+Zou}fgQޥ`##E"Rr1\̒UVcK[/WY7Ux6jKB_''.JCv,,GdbA5ѓL)--?{xެ@ HݣS9Z~yqܪ>d<:!zL~*>Dٯxa"3ydA?G}|, /)s~!̹/ p:Z}B#1-̙,D랁tyrF&S:C!*u;mm8T@,jb`PþP8bH>̌tXG_< >&afn;D|ns\"kg ζ֧t2v_=[8r7*CaS H`pKa΀R ]B?9Cl|'.4ߍՆCY9W)K{}w.?s-|f||^n2 vM=Hw &w>߀39*%T<~o&(n6JPr[_r,N{e,Py*teYM+y^L z$0,Dm2ivnDhbCrMRs1BmrLT*h$䐜~{e,bD\);oOrSCvѽvo"Omk Cq:1%2 q"Uȟ/| /J^cg%9KG'd`wmh?:|؛p*ޖ*h#pZ?捥 勵F?lQ"?7ʡ}7ڕm m&Uoi4qv?@;3wB̵m@}eYI3y#bbg儣<ǽhLt@xd=\qS8' Lܬ{eCYMtfix#J uv LSb϶GzSOlH*m&I#-DD{>mE8K ~gOֽAo39( MER1 _iCeHBtk政KNsU.;h ":r ӻg>9*06C  5#qpI:E&tRU݉ fhWK>"r\ d0z ~=% Gp;l72ALsuw"LGQکϪp BvݑNj١k"L g(,`%O# W*(I,E^Rܹ'ؕsW{gPdC1>L 6{)'pҸuŁκ!τBdiV!-:G)lXgY Pn( 7"K8w*s9}AdzG,ANQRDȔ3РFGs4˄ %~.| ]B7Gr̅.tﶾ<89fxu^Bk6ncrG_;cA(DqtLyZ`"bfy]mi Ibu :C'H\R<5۶=k`$C,|ge}}-jra.\ Ujkp '3d-&6+8Faݻ)3Ry37w-{^e#?to*nqhڋ@%Im9,(Yt6Y6͏nG1(#۷)a, Cd쒇 -QT5n(ߝwg>Z:WUDd%Lһ~Yzo+tʩd;Z;>%~Q[PomEI  `ͪG`؀OЁ•omg?g5Id|:-<طyApMõ(.7p5~4?VV ?34n]tQiFPҾ]P\=0­u!zpȧMIcfT 5y>0YHPĕle IV:&!"ڽ9' O DJ)x"FuPp%zOiXRd~BXmSY'3C>8:~c9z~p0:$I2q,|x;voIkޗD$i@9|3j٬GVX,*GvTN/FigֲA0u7wzȒVbD-I)`l$ _<=-K@vmOAQĩ"ZtN{1SE }a5?R3łvek%kƱ[g~tLpʡ,U=-)E|;w-T!NO|s_~w)UFb~WU{?=h !a*%Q~6Ʊߤ_iYbPyI"Kk"^xPk+4܉B[FPЅ4jw"'m~g z}M%MiH8q@ERkdM8IlAkƊ%5F*g$- 2j(wP-6kjFAiJfJe F8}d+qQ$2֋#2,0"3X_;`*(-F{mPi1&ԫVn(O0F^ =yHltW8UihX[ tN$ؙߛ3|Kubk˿:ve58Peށ`u8sJy)L\ͥu@IjJAr{,D0D_4f.t }7if?x ]w,~4?x}3{Z (Vj] PpE.[;ow(ǝ;dG緞G_\]%s8?>> d /f[y}]> }J 6u*\.A=OZ݂oy~K+W|%Fad.9j8~y4W4L~d+_P|ċ;U =U0qx+}6T;Eb:|+5KhhR53_F!>SK2r!oxDG햏"B'` (]MS:?G@gd}^pS((̌%s; 1ҎuZJP u#LUoG$FZf3~'XhS, 3wwT y17:JTzS8g, l[r,IϹVoAP g?]_W ۅE4 NG89d8`P* J=,⧖B4~po% 5 B^Rxl^2tۆhxZ**t!.v9`N4eĆ>TfQ\kX=c!*wIWvڟ5>C 6QD{+!/m72(li -pє\RO҅2C4KlnJRAdicdq0̻jtE':4D :q5Oi;"1owb)9MTqE3a{wGGWݵ_GnQ}_+0cOy؇.]'[- wVM\Gh *j>nVV߂dl̼Dqբh)ieE37T˶}~m!.?/w-;ҙXҊoƪFCWHQrWK'?/5q88*K옐,oz ,F. tmdQ(Sa~o rycvdWQW֬YLՈDgFWsx&vПU`Lvj M ⒔na>K_(% Yҥ{jɈ7Y u㛹A}])v<™'Q.ܮ4qMg M!4\>4IZ# 9$$'W:= "sڜ^6]nttXam%AW3NRW{IԲPޯ zʓ1ADQu<^ @SHF0U"*'UGcs+?SGH/1_8[]ڛpOIaԻ3oT yWQ|Z]{;Њw! 25z:_KGԉ;IuL2`^d[#uQ&,zr}&+ŲRC)(H _^Bwysѯ53 ]BElY~;T椡 ab*ZU,B%!J#RlBX냬U4X+ )D) 8k5 ]ha:u7'ߘk+jX/swU"]K%:l.Hp7@p~y vk#|8{egL]U#Un ;^G&"XI'ARҞ6 %qiP^"%*@m!k578 qq r^C@ft4:HV|,Ms;\:j>6']TZR8x؍*":=މH)̔Lj3 6KYEK{I9xm,*U2zLٔxVo!~ sI<&t~IDZ={C@C#4Ps|6ҾP8ɨ;+x?i}ce2 M)wVS #!qbϯ g _ï|v,~ā t ͍Xdg a;BT_4\;2@")T,iL}(h'QCQ1}FUU3}2i(!۔MZV]<#fIJY@bi>"UaN7!9E^^WbN;F~H3j1쮖(5q0he93~v[C_ !5F?΢A[ "te +{2z ʸuTj"tD ޟ3fYKҌPwRRoK^p$;sa`OsNW|2?nyH'|fߞy bpq-DWE2 U|5荁n4wz\.5sIcfNI<{i/'h-f2QGVr(D~lGH^*I1t"1kWt8V%㌔qy$ DO TμxՉ5s6sG2g>ԥm>83ƮlM/&orNi31r "EeT.U$%\ld.̤ۘ+фĭ/sZ lD' N" 8MU2@p}A2"/)`cY<{!C'^^i[ 1]t}?_&-4 |z(E)|`KP+V!Ѡ͞kTBDY v=! P8zԍQ2Dͼ0IrkwC uMM-* 3 rF8sE KW6,y~ss0{ ]Be-n;, ]w>%EJZWFa*kmgdHGk̥敒coǝu7a=glp)F%u.3 uxHyu5G.Qf#:: 5U +_'Ʀ$r^FZ^1^_%o7~]/Kgz'!$DF?qFJb@M$@y B"8jHh\:DK><Ȩ or<־=x퍬D2sX\w<tA%IN8OaC0އ->c(Oj"CEo/t;Vs Z*} g|h*Sk;H-݁|MÕܑ|YI SAKZm rk'M-O0T_ e6 $47˚/!jta Hd›h} gԹPzI0ҰPGD,,bbJUH3ip.QN i\؅>DE1o1ӽIV/ĕ <^*+5Wߜ;7dHYm;}ju kL<~cfw-UH^?YbL NKAnV|o#WGgu/k(WP?aˈx0!"N>~ئWt*ύMm*$ts{eW +=`M3lIJG$96 W~pԱ0,3"E9cOGڈ1}eM"INrb}z+ajęsӍǁ'#T IDAT%%Mˣ*wFSSj S"nB>_T  (%s!RiީY0u)M8lVe2C1G`ۃ3@Rnv"0 [r`%#_M(v֗cuw27mLD(i;sofb3Ph/\nl$,cKY4KR%o;AO@dMKh8IЈ!VL)W~aе0 gŽ#.ȑnW`,?Md.pg,jz3O~ \g 9#/1lD7q>v*rQj<9UݢD=u(jbzc,3eqYHڌR7W^?wS|)-OY2vU҅%2S+?[Ʊ,1fZEeUD}b%*^˥W:k3_ÔpmïS;x[E Z$k~#c!HX?H=(f5<[Gӌ!J &=i@`$|#~\؃ uv(x&8K[)9 'Hi9#QLH}ˆ}ܬ<ٽ⋎9D'wCuXj=Uzve]>$m{ [؍0/\c980 GK߉~wR$Dޯl?7;3Cӄ 鮡vD~+j*|[-w%2м Aښ_f/3_X&I2cd&wT˻#DiQ]dc @#wb#.qwަ/ ۍЩUeVKUd 4nB0媽 ,Hng9GDGcB+i~yr驉aWꝤ<7AJs Yә3h W,5JĪcz\C$g۵̃L2ld *?J`ptD쇷 h=Ō(Gx}g7[Y1"ca@$]70i= 񼩫7ةÙy D,o/IMA†~epB~B߰ ypiŭ!,Y_2Nb\hk~!wRF5г[Z}u[&>Hv$^C@U`g+cdץ$d/1g M-5)J!ABZhzJ؟j1N9o<Q2?-Ie KRNEwC@0)la{yBQAn0m  YI}MpKR5)]'U*P[K׀!A|a¶v1pYXvL%СDau~gğJOrZw|ۼ<53A5f xYlAe 9 ga,9spAat J&|N"f3g-vL0ꡲއ!麿o#ptJO`>e"}bVL66);xAr7Y5s{ZJB<z[Y[9gQRxJ$<]}I3B`ă@~8VDκpD҂aMe d[dVu+Ev0O"ia}KP?_bc}(s 2-lS"" >&&}yY84η&}oBT3Fgb=<*5Ǭ|mO'2)CgG|617}h=>Ķܔa-we$g N*=oiUph1-tUe%w'[>Y7ߏ> ]_ݱ |A ˢu+1*rY|Rd*2s#A3r|;A)!2:H!>tAIGx6sd\_ꋘ d#Q©'  ܀HL<]V76.D`_܅X73{3Ǒ2u*NpMw_'ƛ+pmϘX+ҟl(;2#eKڋN_8+÷eNRՊCP݌rfTxMɈ^Q Y8[(gK kNz޲",!2o $ktR쩦EZ#am0FZѳQD&dR淚/ΠUz #\_'" 銔|Bsĩ.KTo@xD{ v0#GooFkuo4Kj䁱įH }@(I+%db1֗8qv[yd}/\t>{H³qw]U)~~~o bYL7 +F+iz.* zi7ɧSF""]AItOD;hvf7IvIG&hl9xyͣv>NwUUiN>y(8!6+?Uy+II QUnj_00>* ΒHy!o,j#fϓ>mFB䉷WU`IG̜\+PdY{C$d!%L( RIoޞʵ|KP5`" I"WK%єā{FIdO|.[BX9׳܂fbm:+CS_qLx*Cp53KZUyOZ9IM&PftVPKpFȏڴu:,OKu&-+=ȋ.߂$ Ǩ=@+Ȍh1Rxfh\[Mw$#Nm)̅ND]d kqn)w ? 3蒠mI;d`~9Y79҈f8Jg$eDIuٞ"t)] v WuYsiHcL?x愫%ӫ_8\KhǪ ~b&o\`m\fjnhH'aoLPS3-EEoU.C;^)w`];'mh3Ȍ+s2uJ(A a\z{w$>U1Hd2ف$θi( 7_1vvHPgk4}=CSD%%dzm 5p_u|FiOp͡4U`\{{@P:*H@IH?BAŽK:K!t~~Owx3Re+˹Ӣf )Q$iFvYe!gm|Чgsr#+(W5 &0*tB_E QsOt|*3KYDn}?R7Lٽn m98lRM CS;h_*~&Dcd.psp''pzUSGJ͇5囅7k@cGⶩTʥ;E 3OP!pcgB8g><Rd|x K.:.l.]3eik&xƶޗS\8egNr檰fj#7X8oۧ;.1}-镕MSeݺζ$&< )Cr,m:۶ױ^wAPM .@ B,I&DF9geD"T* ]H:ʋRYLG6Q˶o9?kXJڣ&WĎKf~x(JZht߻*HZ8|t(It\;dG<ўmzRƣ'<"o3y=%0xP~Pxaާ**(+͜@*zYS~e#Z t!Ӱ\ Ji?d !j.HcrGoy1JMQ{$ѷ{qؒ4l)]kKY$ O3gEry+7_Z`#DP?ٮ^E)\;<&YÆ4$ ]dJY-{q:doX"b aLDyFhEXG|ap ?ox7Go/:BfNhF$p0 Wqr&.t }i wƥL U=w=˷/ !zsȵf!\N?vʯn~m+!t݀]%脈Ucqا{pOs\2-UmNQ ~H/æWgz9@܍7Zh슨n Z)8wك.p$QiDeo&JVI"3k*AJijbDb!pfphRY \ m;YCf./ǟa*KO>>zzowym[;YZd%vį"G'kPcZ>"H3dyფ ]|3jZd;~eb]3f޻("T zD*lcS;ݳ606gr2 ISox;:^-N=jwM IDAT)Aј>u凌?}v9:)gX/C$дix&R[pnZ2|rMY!u^I15$ᣙczZHnY|{% %3-69fPR!ѭQO.,3 K+NQ@7t@:&+e_>߁rvN;2cu Uf^S(ޭ>֨O/E7sM~:?19~]UdBNh>*'T;\,i5q+rVE5׳l:"L6P|Sk H'^rhU x>kHԓ4F+X F_5߫}OT'aT%\ 6WXBE;ٕ=5Ñτu T8L$Dd-sCU9Uсrd>y\>~wvu )R:Hd"x e3piCa-$Ge^>0kA: IVf/+PFb^g˴QF:IH Hx~%v֏g}l|k2bkVv!t^[z$u!Ku[o[ty7?3[`hR?=^@)_l z2%y5?t$KlF? N89LەyNkyQ,AגWfe 8J5ţScc)׻^-DG" &|獣 A&EcmbR3;Mĩ}Ŷ la$!.k+bݭ'<>"Quc [!mDHi@1&$׵0uJ2k-k8 V[z2$LCq@gm[MI% h N 2ښgmɹPW!q IXL];ڤ[[MW!D*%2K@pl.9w$--Vg-uqjߝ ιHkhԧuͲ:Ue5k/E0a/"Pn')XFDG:9",eVJW mz0Ɏ/Y-m>C$ͼvj**s8R1ki|M~2N91yzےhFR 2V`m==l/zJ g_~n>*WօWտBG]+3s1\=3Q.j I`{\ ӃJX=GEI%Zbi F3~+A>jAAbovqu2x TՅ~M-:)95uMk9Ncy'/!O+} >ے"ѼEd8kXrUV$if VLJIWưrKME}<@)&i&d33y.шb`,Ч?x(C]A7##N-V.qS?+#6~\t{}o{kۺuZks5m.~9U.B HD/}Pb0) # ODbA@Y\JTSgٷ_m޿CkU= H?̇5Zo}BŇ-W֐ׄMOi!(?;V~`\e!ټ: ?NlBd0}yL3憷T\#"m nYdN,m҆rljnEXn!`n$w]Cp#!NGzIpm]f6C"CO!_XW@;69L$ {Z/,[Ѷs\G&QU3[~sW _DDy4nDHII*[o7G9( X4^{~$==d.z:Wzdאw8{o'2.l$MQƃ.Э[,x@1"=oQ (.,y;`%у(d6 qd1Ȱcz**kYF%X ?9Lö해|J _\hϓEk|1T6jtSE)PJ@#x?YyN]V*]e'H+Ny.,P'mI|#-پJ&Tl:/Dĉ{ƛ5BeoGڑp9fa$tIg9<ߠ$+M=Xq1 1Nؾ? WJ1cI[ AaA |+|gƱW ix{C.1} jR=û0t>VSPp2_M9BZO֔( 7woWM.# 5Eq LaӲkK6TL”2fX)9̋k%_eVy-"$Ɍ%IOǓ?:uL~4;92}kDlF&Íyt|\h%>3@Kf?T?pŇm *}-җ8{(;± .g]+ׯKi*ZKy{ 7a^|ԛ2qjPXlZf(r9¡_O"L',͈А&͗vpqo2CCsHUi, m#t\B4eyEzBYtYI-e[d d m&5QkM\pXbIKDyTרzfuG&ra MN^>>uctLB$ļ8~&28}mKB`ٱ^RJ"RsHB_mJ܂3\F`sU+ % i'x8rUV{/&'Np}P"z _ICe@M50 EdIQ&W̰ 22A7K"F*g 䥾$.dLb TU1@Ծ4<.EyiX,.'^%e@(NHHE0.PyJSYy./U ~ \w-d& 铤'\T70F[lw[t8ߥ, W}8Sny~;J76U.'G7wZ 4?发Y[UR LŊ6i:Υftk0W "ZSٹ9J,sG"JG)(Uo/^& pv%^duƜlEߕ HW/gjBڤe}1RԺ1.)KB\ apd! |0"ӧN,_[V0jg8Lc Wkц WRVňe3In~Ϟ[^ /g-+CD-tޚHIvESeo) M<>+[#.DJ]Q+!=  ,YD z*rY ~ Izz'ueY!YqLae-.'A)ӅUp8s@;4R;jY4;tB W1H'2^ qS݂)@ײY ֝ _9 6 Qw%Rp'(H>۲ /TG7P7 JK C&l)ʦ "\:׹jw9׹~ާK)ea//Zk??1r"GÅʋסgO'1I$^P3X9Q. K=!$‘Iz-z*z;wֲ<[}[uL ;/!IMW“*ٖ1_˸I=%̷Ss؁|1oNT|OlCO>F&xB /Jd߉ⴝI ' %W j\QAR7]tzq&==MNC-R2 !ʛt;CC/!GxƼa1s\'clf sWW>||)gguv(c(';JE#WexNI"IB^e3$otRmv=+7+PŏpDVw,^ Nx| \կ.7xے+tl(F^?}g$uP-9[h&E), K]oҾwU69QK]b]7(^,nO\yr>qن#.}1|D@_a?*dH;+fQtK߿ gC z*bˢ j=KbWhp[2bہ+ʢ7$/N,0P.&+ˈ0b/?L[],Ecé'NI:#7a?ٟ)=hdNrB}I85y|:U=$M̠`2 V9n2[_pM}@t)z+x^,s>sn,KqNch <<`1 IDAT|W}Y}g)l؎\\m, Y. Λ;t2R]<Œ }8]GP/; &QSǘV8e#&w$#zS g%vY8Ə <7ل94^wC/S66͕mo.j7lti/\9}R*6=qt KήQi_2/ذۭ 7]h&9O<̚R*tknri-5dwE^rަ}p!&vΫC@K4PgDDӢ`i-w7.+ "i8)oܡ<(p2|=?wh7y6P2iHj*. NÏ}lKЫL>)*yj ޷sb(8n;]T36nO4UQұ)5ے F?X;B ٰPח P9o߁=-a m?[+ߑ~`,܆Ƭ\CΪcTsi=|䲩xʮqMKHwΎs:gُm쓷Z<9LE,?躊^X> \@.HqTIOvx%3踆.?ϵ$ PMo 9vNI}m8$waJY]~-ʑۙ\Kydn5ʳ1Ctbg(߆S۪km* Co t^.A7C~|=7Nڙ=!%Dž1{-2g|yN`Ȅqx)0ʧGxe].R,@bwR9ʈ3 A+.ET0!BR'qON1gJ(4Dq`vG[>=e[#r6葈;(Ymܬ?׹u| 3 {\D_yaYXV?vkV8{auq3)iM P׭rh$6'.<_fCA(8^ukk,\RH{~st{Q+;5mmd/`_>|]D5?A+5hپ)8g/l=~/N(?#U4'}BdCs|8WI}˦-zpBX knee_NJ)w2:T, ZN ;jG)q$%1uW|h_Df}jc{I?UB|4ff׫,8IR D0:-}FfA$g |ou fdڗ7j˕{ݙ jtvۏ5CZyx70}7_{n\f B&3 Dei5x[.a*A_l^iSͪŎ . ea}HUHJ,^&hTD"*|W UK&n]ٔ1^dž><%_7Q-vֆWc$F㞆^%cbagCf} R$I "B ﷅIh=@kT`9w ˂CьX/PWĒ'@r@[A4F\V08 ?<#'o80[Oux*?pyI9p;/aS%xYx|Cِz##B.WTne1m+on./vpJF-Pc+kӽ=g RHiHB\يDDACo@H}2yD6 -j8_ _i$`r\D \U4S luX(ʒOߪÿ*=_k~jSWȇ,/ A!TzfoxO6QjY70?ps~yգ[G1Ka\Ϸq[9ٕ 4gmNJ/8É&WE݁yxT:=""!oW(woL8ל=doGDnRa˨K$uLFx·yK' ":Zk}uG F#V+ #)i| ݐ^ " R+׉8F,"yYs\{=7I8:ׯB9""`ŷn\r 2ں\vs;X˪,2TUz&iY@Or~Yk`! *CEm8_9#mjK9?@I\( u b) q(NnmM=N6|"h6i*P$, IwvrM<,t>ka؅5f9~XzN}۶TVJeAA Ryz'+! ۳LeY$g,aF0#BV/ +:DӉءU*~4>Z3bn7 9$CE9t]+~ˠYn,m6}G Dɝfڂ+\A,c5^3^YtHHؿ9Vos#yAb;r?q9;Rݿl +K1tאtYnJ7TaȈ̄U4=Q gG dm=}3ؾ18ahvLA⽹6uXaod׳O7`t<2-8`f 3\5?;DvMˑ< u*c<Зr/ NAѷ|e\ӑ.@]D0!?xEu+a\ct6+Qq[R*n6:UBq, \ke$l@OwOJYB  hrL?vg ʒ$RoC_-4rJ!.: f7ڧ-d@Q.δM2}X-^Ka^]N۵mrzh$T|$עF.)M+l: Ry \YKtս؉Xt܈:j:̘!҂vV,,W2[KɟOcͶ2 _;8x0r<9[\p5cO[& -tVR@D}$>w99 :;ܿf43&h:Y݌Q,ݽo< ٤aBof/+INz' ]]2/r;nYNHj* <` )Gư^W‹FYS$ 5e\ݨdVAe_^FčCR]v KJؒ@jWa*lO׬pn-W'lv\q?wI2w4Q :ZyY5W)%ojT[y|>]?PIo(o:!;;^ E>JZa$LR$giۖ3Gg8k) ,'dNlG:/?liq:7us}GX o;?hv2s럘-"~we!lJ G^X/ 4zk` &<0 BɏR$94{v$_G}kaeز,2;N`6@U7CŨ?Ɂނ/mHF}q(jx}Nche 3G?z6鐟s I&.{q@P,νwmgNJQ]TK)ıT:]<$C)cqdʈcUacQ|v ڨcR=[ ]+ SufGGjZe)KiD3πNV"&b-Uv) I!!Yo^?o:xTT+B',G>Ąx6rL',I-[us~kB\ ȍIbIJS^Td-:^v 2f,}D2fʇ2) }m7S=:C:շ"]d[>:dW-_2^[HSM&9ΈLwbZ`l4Њh޼LW&RcpK1Lk8Y{^HΓvfT cIS#V/F{tƓQ/eҸcf@j^ywȑ].R!y0e 6-6$F6qݷ/e[Jl,^|`l"M`$ŕm`"h L i#FI"gCmkWË{mƐYگoEo}i?2C{F8[b`÷_l 0:տ?Fka 3[W+V&"yj~0?oc,JՉYLIxqd=-/Bw:Frxo<D?|{ 4?FP2L-f,+{Ƙsχu#MȲD"GVrr/97Q"E 1'Ȏ8"#p!40 4}j]}O߷֚s]E-.7]jk_x̄OA4* %ݏF_?kwv a :{"qW ) P"`s@zͻis9}<:Eqb !&n$eW[b.'+O\0I<ѐ2 jYOAۛh sibxhKRLcr# !.3KNj<)3ŋ댮 s]E"pF֟ g8b{-,<?wSs33|2~_ֱ K; 1h8(ĩL~ VHaNPshS k B҈/Fe.CqQFG`_6yq\rf~ ?ulC)H'6VHԮl$u+~ 6ËοNG#4QPrZoi )k&ا}WߧI֊ǒ܍\sZtQv }kA-R2!FW%:2i/;8☳# XE,&n$eF{8y'BfH a8v:Hڧ.kbBޤTQ[auF#ᘠGHѴ60eNMTGT6ܣtSG}R]}ƳI^7'::&io!S2E~T:h51Fe016|kx)/Gd@;՝v侸!G/hUp¹Fpb=_B;/LHlGBlWx0v/}kRX ݭa5,8Z肋rJ*|v@(&n`D҉~,-Cr Y%=h8<'w+e,frgnin9hmǔGl^|]Xg3 $B@o>X S)ny, ݟ~~ x:>RVq:"sFzFi!ȒDֹ~3 iH(am 0{`Eo豒2Mt+tz.+S;Fm,ZcXAUIY\O&oC Ih+Dv)i*_@_M\q&rf+ ;.ӟ ?] vasŸ^<@q в_bUEK|ĒS!I")@iPɋ+:J8G=pXj:'NGwdK#+̳g{|~CeF`?Cɗ{A,exuHs9"{R_R!BtQG3am=s{XD؋X$BO|8@$Z,eH7Q %D+7!.VzX&*b@dD&ɱ^1y_};g2cx2C"5M7)uyQ# 6˗bnM$/:O.{p%?p*c]OZE`MB󌱡h?~C"2̱5Vj+R2:SO4]먴%℗:;HwUA&ו_{3 g8ß )+^{y 헒YFC /0M}1fm'@D@xíp\5k H6fۓ6p 2ԛV=*;ng8"K[5ZRC*V&RSJ$28"ҨiS*wnfwx#;ȧS!A&4-R40 62VCa \*S˸:kxSYٲ9ԗV((7zv7jYmQGligs# u2 Oias +v-z]|uF} O2! B#YJ+d TT|Ad]ootV;9V6er(F!E;w 81m@]H0} x)GLDoD5k{ ]ȎЄصrzG{uՔґr xCtD‰r˾!Q9I|H]%Sd|:bTXs 9L&IYdҜ"SV:'Ŕ =Wɚ -cm"[;3^PAB 1"]ٸD"h ֣Zpfd)4u 2 ڸᄴ[\ۤuE"W_iJHN 6Pv@jSQVt+<"t$+kt$h%=~7<ɘSפ? M;OOU;_} }l>S悂zixȃ]W2ȹOVGnм ^k&1fROh>P2T"Yjn-5!ȹwGCC?MiM}.32nG÷ ++Y\ϴxS3f*tᔶ뮭Kk&sO1]mZXF֟ gff=+y"}/g _c,[77} S42üN_ɫ;mlI*.*  X9ˊ!a|Ȧ +2wD,W^u@YGD{Zi$ȵwV_բZHCsE~j?"WB;Ie"E$*f%#4;S~P x"|:eN&HZʋouOpˮCEfM;mjѽD(Aݛ-඼S`D4ɥ qfMj$:))+ݨktf+¶LJ9-;IMY4px%D PMIRUܐznߏOEDbVAxDrǞJff_izB{:I6M$ܪ:V8%L MNsD4vomfW`G:t֋K1,#͖ʁr^r҄:԰:l s$ g=B|!J:p(J:0e(F 4T1uԅHb-+z}}S)P_{ӄtl..5S 2֕95իY.UKYô( !ɼ(|Dxff*~"vZe?MR_$R{Baʅc }iZkD)lޱ\}:5t%BHhFˉ>\FD,p=-ٔyK\%evǰ|v%6c?J;R^cgB2!XB?uqIQw" )M{CJ&R!9}-7s"v'͵O_/IBBJ}5mzZhn!NYBzAlCCM;Vu]c&w6qnKn ®65)t`u8VReNK# քmJK)EO6D|"iV rCy}zS؃֤hc:gw*;RISfӨv de7}CA e$q1>\DrjBҘ arOJ?Ú*8!!ft*P휟p<66p_8?^kcQY>I}Om1Jww IE]y }52"O'NF]_Ax3 T/8,2ZUݟ_Q -3/" :H^׭C)!.h$zɹ܇D~ 䑘\f%jٙP-i!3,H Uj E~-]#ͳ|NyPS]O ۚXI3Sb+N:ZjqtxbߺU* ,= :b%Cx g׭ (I8VMosdzڗblk=RzmlĒ]hD{"ƌh{t%"X] 焒ݩm+QҔ ٜ'"I~^pؑm#ndiTD˜s;yxhj]|4Ȭa{#Z ,m.ɡw9@K(/lIx_j^'Jw#)M,"CtҤ%?:JGLM~::]p_3=2zMH*?n<Њyna2{}lE.Veb.ȻIdugX C}Yy qLZ?{'Z72j|ɇ^omwDZd*a.>frs)$YHY ez݆8iQO۠д9QhHHd,`+bbJZj5d8LnkUFW|gљ{UEy*0/K'Qg!wͫ40m4[x̙[3_`r pPuo!"fu9(-xu='GWe'(K#-%RUJɒ?Z$}O[_ϋV:m-l:YOz?UT]]AbeRf>I~69%ז@'.\ء>b_@ "eif92P=Ff9eD猬?p5;;;| uR9H)=5!yJܟ3bd5X5$ "& \.ES`vK*{2amq::MD0N.9P"5EJIzZG8I HI3pt>Te1H8pl~ /䲓GVg*~]$# Hei߀ҤLO"7MSt⿃KҜ]B7#fQh` {-&Iz j##G(rCph~K\2ݢgf"1N1_%%+e4^kh+: ݂ߙ j&>iANt+7k蕎A]* ??fTG~t~yͿZOh#]WZ@PnMS*ZFi:DPudTds1q }]Pմ$(E9«_e=z\s]7ʣ=+(LⴂM7DKD.Дƛ2:* _Ys ;66:Ğ3`е" ݘ>Wc .M9_hjD0g7 kK<~Z ]HZrroYZHռĂǢW)"FfM5A,%^L1 $Z{=wX 7 -Ӻ=dmEsSGB _Aȓ_@E azt|C=ۀ'_uwyֳMQ4ա=_5πLNԊd9U!IbJVZ.=\q.淉u}JO i_{Գ3 ^t)df8/_re![k#;w|_43 𧄥仾뻖1ɻZI>$L.&7q%=nգ%0rH[zR:E.Bh1ǭD=#(KX(sM6LHhޝ 80K2"]'Wbs~Sl[+6H&IjHi{S:V QMQIeJ{7ό-͜ kv<Ojk:!a:]ҖJm`x.j.]Ks6!:QXcp Ռ,rR ZM xϋ#F#72(5t’΀ΓWkh%E6P.Hކ.….bm9SZ=rbQZ +QIH|ra6DH#S㨽5Z=S@Tu| i,CiUV=%__/i9\_ k(3ÙgA]h 4ICvzA7/5pkw$ Ō>?S><< sj ~F\,6[?vn'',:YFsFF ζ j`DRvo'5bn/ 7[45CbΚtx:jcp#roE]5UnOW:TPTR2Ru]jT-XU.i37( R6[B|1if5 &HD:x:`o9jߚ:gOՆ9#\649]{|Edww7AZ,eúfim"ۍKrb&礭wIΘp;=4i+:pZa2x N+o Ϸ Aζ|gr,fVnyIߘW*&Ɔ::Ćc80`d9$IPyc˹&0=N#5Ş1hr\gA:m9FF"/yBٴsHx萚{*ib2VΎB'x;xqg(B9fSMTP<]N+X> $42qG澖نH)ݻ-FJJi,HKkjm|klZֶҝ#S % *BE9a=P#MT]59PkJ^B,4 wBwXoӔMg]c4ͶZ{yߪ\}lH E Q$+H YB|B"G(F" | Db 0'o'>9st~sfΜ9SZU>ϾZWeEB<Ш׆yut/#-t '[l=IV;Zx5wsj\aZ՝FANOm 7b0\Z ɸڲkCIM\ 86SFJs۪)$X[! yxOPx C- KH)'1|.:Z n g$mdUj׭|LUߊiO[c53?%Z4lB j\irn4WfYt3wajY1<Ԝ5+N1z̄'(԰%J&?͇gn9S7DZ9j.y ؜#7wHoll_smTJvf5r8ɭ)sڈ!qYY[v>^.p |3D,ل/bUwk̊S6z|Rkqr1J<Ԛa]ꈧ1DS {:OG@0o]pdPl&q4fiScŷmZFU䗈PN\^V' m, U՟s& {= 1& clDj ?:Y>^͗'bDM{z5Se!۟OFjyNU۞S{ ^jP%u=fsB="V-Wc3#y17zkq}J[ȵ6\V./{" 헷[u2AnR+*iF eԈj ӆK0zj X RKEe#0U,:t/;;DfnR>?͛땉VMlע K%x %-|iTa-R eI`V1@a*Yg6w/$)ܒJ$*V*ky{"WB}0ڹlbh#4Bۀ܀f壹^t~r#.Wύ\$˵[Z"K46lE1S49"q!}<پS3+0ƫ5Vv3CkHۍALа;0,sR[`;owa_]rK-u֊ڥv7.&y O esÅVk@R)WX)f<k jMkfHk pMHyNZcA=Vbԓ96!${) "sDoZyt,b rf'WU0<,4ܽA{%؊.b=ieʽQZv qMQ3R_6GƓ qﱥHx7\7@(T1B9g> E-T %9݁#&*5ѽy_ \эh/ \hʵ^y 'g4ts;u?o8/=V=0a̰y"ݚ#y3e8F.Ll^~ `%-mH?{ҕf#EUu !k|x{SܠD˺#[El ]!$:a*(<9qpsǧ}+S=\IANӳlYԈ$0f !PbS{x H0+ ɩT\KBsY$D5*θ[ - >Nl"B5馸,CƌГ멌ym5: cO]%m LzVmNȑ%E喛t> GQ#S=62Sp.zu>^CCtH #%O*,4XD{('ce}>ѳ3PvvqjkL MeN*3eHqX|Է2m.!RbMܥ4Ʌ^jfK4SŹώ8uE<g?O$#Kw?OMo᪜W!FCS%]nӵg# A=7^v3'xnA:ޜ-8nA?:0 q-1 y%[ܻ|xLhGUdaó:kMuƏ,M\\V8X@OV5l/VD% 0F,dWWi`VXZgTAp*l\=τ,C7vu" z4$.z k"eBHٱ򲝇IJ;'Q4dZH6vxd5! $]IEdAT!Kf{JwޓXWtrQ3.wOTPܨHkŢ= ƷqJw2z/7L$hU'´vĢF| X3C;/9˩4w)O%DB8Q;f 6!@dޯ$Ӛ~f8 r>+ <"z2̙]@k-#Zqk}ù_>6rCN?z9sdcFER DH] CQaPdK]^\ԽǢ%ؠB4F/Wѿ7r/G"R:ܛ^%x x3m1cn)O <=s7 =)4V#% ;iR3{%ɄBޏ66rXۚ_Y V4! Emr=UIaPցtcp2iВNP!+10+ oqB~'D$Pz%Pkɕ<[t{M|EC }ş^z]>ѝ*%_ 铹 lQL>yH ȯ%o*h3}cµry"q'/3\bNWڮ.%FB8A 0?Ykj (?#rʷWMwH#mhuAu'e jxw/ ͈?";N1hVH"1l5e23d 8eTFbV E3Bu'J\*sg Eޤޝ9A:є?lG*.N O*W\FҲgO, I+%',XjI75&w"/, $i(B}A̬Տ getH hw9 S]BoH(χ p~o][.>$ҡweWE(.>61sLQm&w>?V ep.noq?ÙӼ Gdq'/ $;-xsAfj^sz,͟Px wSBS[g ?S4 " f*೯-f DjjTNFq~\0́`³DK$cPq k PD #!##9[] d>H  ʒ:⤔[2qPڣ}g2  xsظEhM*`GDuhN n2GFO=!ɑ߸HITy5ҿW׉JT>ԡˍt챊Xz[{z3D#8}+S]nڱg,Eػ8Dww'G+d`IU+J[I%(uJ*I=i<>5ى{naDfM"!1iXii7 ڇ|o?ϟFkU#{ý#xMy-r{;>XRc҄3_"$$2uN0\[J#[mL\%Wrz7 iLg1g9n#2WPp%м5J|U07v[k>-c*v~~Ĝ&$s{B‘EQIwMIŨd(b=H##ˊė-;{kf䤝u~Z|r!>t'N8 u!0Ǣ@C@M KXM].p |Ǣ}=׵R|x)8EϾGm!lN:<ŷxstJܯY֋6oM>$ }ZeE{a5"I%k@xL4mFb$JEΗ,>ϢߣpW|(͖̞r:BIdI,=M,sMqt {E({_.!).CVn=hq':$+qo9 loT;wkŝ]݀_JVY_?O@*[$2yTrqHQPBxDEw^#4 J^#uۭr9c֐%P7`>'` Tw-se&$&uh]ihvʹhEoƾMYti+rdQa- t1I I%RA㽷yFAڜPӑּjtY \;g|D< Uu^wONqѳ.Β[?|f?u[z/|%g$:#RB%˽̑ޫFP x C<ͥ"J9dc+H )2y~ }޺uP 5)}r̠5m_{ʢ-s-EB2\'2Fc0/~~b,0ILْwu3&Ep"r)NhΚfdaCzSۏ{1Oq" JH+bOpX8+:*62_ْho"3G,a7=ZRͻy8E3BS"]‹yrǝVQan G*JW't/|( ~n,?'RhnPtKb,! 67L9ȟOx Og֙$sΓ0cx6*ӤrƆt|D< c_/pI)Hݺu;/zNx/yE{I I 1@m_*oEic\27R k[C.$j=JەߏwWê%}DK2FVAavqܮe}"'z֘"H\t0D$%^O34r J(8N 8i` J3"+".)0X"@ mI)ĻN b9PH"qBFJ^_ IuׁE=wƹi=~,ʫH ܉;M3H7B@\i}z6 /p |L[U~3.*_dֿH)uLH1kCCk]kڭ;|F8!M$= gҼ`xQYVR2e 4OCM+1^.ȑ t IDATf>8[~ _euM#[dfܴT[ 䈓fIWFzQ#"4ep;wY=j':dOe**RYf`F$XJ^LS7I!x ,)O,@3h@e ""5J(9Ç*Ӿ:FX4mٶ,i<ߙ"?)㺙u$YyŽDH*~}HuqJ&yD#LR@3>ȵI;b$>⬒8H>(K@  垘Aõ/\1ʣ:|c*ffH !傰zd,[svT*lBVX]O" yv0 )z鎑vQuAJ`pМvgyr5xt 9d9)"ji $ 9i&J]7L/$p U@H Dsj>E50iWg}9A}y>EkVF'bD}e|b)'%rWFQ%%#axy1s>?"] f#$a$!yZקN&>pt&-|4#z5ڍ-޹%F QYO~)d"7YƐȐڬ^l=ŢSPDlMy>*[SfYviuzz*k[1b?Od&%i6op%**nc;LOcUݰB߾YY CЁN=M ne}i]F5H)m f5P̛썋C77kWW*5Oe.`Ð*Ac+Olx(8v=#WI C~(<,󻱐9sC 'b"Iy+BPWqCLd.pyJgg+c_xᅳ=^g 0|XeϤo\yR Z~G-D^vI0;rcu^{>ЬEȕ^ܺs7#4Jń_\sL>^#b\2 NXhiC?c\ryPu U[d/HO:w~qp_*Ai`YSF'aJM 8-T<[vf%6/,@ߺcw)՘XIuTU 4QQox*9Е'Ark^#p=G$e_e NDfPGyOLb3,{X/4FdA@eϝS]qdv pbLD|+FD@4Zi8ɏe}n_s>ުV=xw&d"ug3btck~jR7԰=~^܇$F&BbՓseOF*xxՇŒ㥕5ۚ* /UT%MMLcn-&nH a9PNXTxh,AF~`r1t߀b%*6!\* ^d"-mBvqW~0+؎JB׎Od\#1mnueәGjiYa# *v Yϛ5$l?rtۈ!2R# E!E &`7]KuliSNs!*6]:{k-igWYp0N8͍F`=qKKOG*#qFEZL?#M,?#qG\UbYfmuDafNV[GL~o~7"}8 \lc7x/w<96]mvNH1ˡl Pth{a'ۻZk p_nA [e.&}^V=sJk]c'j}$<*V(*מܰA D g'/{_=\ISQD &fH&<}zkrs`Q2zIm䤦,1}.2]]Ўl_3˺HS^? "%X"x:+S0޼#tDdJ eG(  1Wo$T@+e+i pZLW|N8pW,ec(A+-SKYf[gD9lg^)_ mDjqع+Ј=ڭHDu.cmĠ$ &z+çuxعSQd ^M/P Ȋpne./j/g>[']zlfVx_xeM u &8$kV/ 9yiZ} {x.&! ]{P`E3܍fKT["i&+?;4O,i2h'+޹_1C9WNJI]lamBjppzXx97(*0iLjס_d*.d_ܴW(ݓҦEt/oy4 21}(xrҾ:}UHMg$!!{VUD~|MIYzՖÇIvۏeǕ[kE}NfVVX7,Qƌӭcy1 8 6ala `mlM-`--jQj(R$UYYYy'$ JIDeUyrkG|kӸY4/ S(֖ԻDƝ򪧐t; ;4A ;#t^"ʱņF&Σwث [&LԺ6?Gt|27CQ1t#&潧oTdfOb:Uu?Z+/}"YwnlˏO}SߣM_kbpL(QpȤ@Qyiޕqzsv5ll{6kJ7H)MR[C(\2=Cw׿x ʾ.[09@kPyVTy'Nsu.<򃉛wE Vprd^ ɳKqH[ af|8X9#.0%\, `IB덹W }݋%R<0B5s1RpJ`0ƙzΊ/x?Z:yڤOڨ(PK("҈*AϪ8׹#FBbH<vtc8G hBrȔ4=i=?-*e B'QHnV"99N MAOU.'yƼ ǕyQOJK>qꫩ~2wA(ClU~rn)yL*Y正{X2Y [XɜB^pFEnuv[@FnaHܞڿ*%݀$<CecSWĞ?+[L$bh?7+(K^ u/x(PQTJ ,4E_+<={s6̸o/5ǥ:̌Q. a%nZv bHEWt:T/.BOGKQӕNfګS{eA+7y:1huGI78ŏj7 ]glCPt77a O8&giMela8ֳw6nɦBv ._0"DU9Q-чB>'j}1[lT[ksvR /iCYR8Zs_~֪fTkId<;wcp'Tǔ DJDR|}{9?8P(:C#Oh8Qۂ9oϥMB4$"8kH(*>.<@ #z" U_!o*Kw0qOy8ñ2D5sMx%ǭ[>ƭÑpH{棻:ϐ$CVuTVfX/!&:r]÷6~k ܀ =Iz<i'CFڲ2S$9HMcS L EH߅{IuG U!3cYNE2 iHR0/WlL#&10R< ބ ڿx{&4Nڟ,-j@oxedTkHE8v~ӃYAsg7C .];^DDAS0 ?z** \sb曈(tpB٫"k EOqqG=wj N&LAE5㫇 Jֵ!{u)|fqwnyl"UuK*c#%.&AnɅ))] D { Y*,C.ͻi@R-\ vi5Lb2.pHd+ENUeKMB| Fa$)eYAЉʊ\N:Y`A4mV;e}J'1`-oGڄ2bݐ0Ah^:z3P1Ż}yr=>H#HywӚ8+foX-c;oO<ӌa-2O+[hP2UE,[VbUiY}BC)6++'Y}I,$ve`*ē!T8!.*| ]Ƅ-QÈR^HS )#~|+g柴r .$K| oԦ<oc%ixDY>to Oćoie"q?@th*8$ ipƵ-p 6V0)c{u}X7EKщ5݉24tn/ٜ'%csߖՐJDkꁣc s8U  ~f4s M$6=L~,frlcyOԧ>uzJ?+j-};Fnt {;tg*쵦AUPxrk.LA nx{uɝnz?f86ؠ+1% q`UW9 b]ZV7zไoW3 ,=]|qG'^X?&Rv]=HfUk[R187ޜZ\T! (uqjE0 rlaqr*;0w: ! l9|E4-fep,ee>ZRBZve@B8˃-F5'EJ@XpmqA;DaI< 9ĐP J!qtM4&A& l"D+Gj REdҦ¿i oQTcF3[0 .k?uajzD)3)֚qߢE&z$M< q4}]~ ><<%`0zl_` "s|f",Xh _֦R)1sX1)M ߪ{du}'eEPi4t@^ث~.*w7/~!?FV,Хx(-դ5#O r,4&{<jq:ҽ4ADM lr^kn]9tY8Ev >@D"~^n] Oٶ~-FF9c2O_ijǔ&IUy>%BfPYH k\{Mz [I@PP8 kPC\՛sL;( oAA8~ss!AC+4 3,*HkQҔD϶$gЖR49$љU^N{`(7uO8@T UmNG #6G0{8%uU84FRS9f1ADԵ 'w\7A4 d\ܷjFr|c`** '44zPq aj]5=yFaZErRsN(Z(qja f%'ͦgjj q一}R`W015y TQIcVX87*bAN$‡L&7C|w*4Qu1w qQbH.dQ#h GP@-lTI.\pDqVN_w 6Ls\gUFϽiJ4=q☯$RnB8"hq)ndUhkƻm~̖㭷ުnc~7锎_'_YاF}r%u/뚶tuX'Mt޽Fp?gժ]去0ۀ2w$cd49䜈f iZ~LQ U5A,&B]RZ_A|TWAB'.MY:vG 4x$tG҅ -x4T=QU4$HI)=`"bQVm0!PBT{L|߆9Z1^6|j=b'\sG2l]i+`Ɔ ZIľ?F H;- W,ZK%"!"ݕW̩Dcf$R 'R#'|L .#\^moGp#p߳!1ƛǾWUi=.;V-/:xr; v$.v AAJs]x&w/H4oJzN7#”sdJY< K:pG*=^jTA[U8/kR@"ªrU X.`W rv&kkv1x -oZO<(>^ǥj_k!*&OU=LǛC}๣_z6٧ECp(SDW0{ @]SYvS51nQYmW!F~l?ITKD'ҺD#2?m?q/y~@yHoI͵Oֵոқ"{S/7ZׂM@-Ic%-1CH)hq҈ v#FSAO3ܓA Ү㬸XESE֛aH2hi8{]I\2rsn{m$Ac›}MCoUwI=tAuoqDw,!m 3n n$ 72`ªv1k@;^J,P' HEmƢ,x9BXilUvǓTif(%ԺtֵαDW GOO.\9x(|Fák'OXEP6ß-{=WM+>_1-qӸwڵqka-tQ *Eno2G$t}8mg +_?7T֤*eXպWb|zKO _]K$]7l}-+Ѵ뼼ˇ\49t` B< Zp e˹B|LsT+;o/ugJAwl1@EUn 2oYͤv+bOC" Hrw@$[6 'ts&I^[1@jՊU9]& )ډh m1[XFF\U ~(}AJ 3Vk4(vaG :u0W[xn_xǥKDYDH$#DU}͸Tڲ Xll: 0R9Հ$PHà .L.~LINëHW؊ů|Pf[|6UGsoLfhYxv %7D2`4KXu,_AK<Kxe֬t'_/='oSJ3}KEqr"#$Gz~uqnIWTq1e>SRՆFM0۬Ç] 0hħ_5}\=Iɶf;_5OyEeҠ%DzWhl#` >c _961f.ow[͘n`0f Eyoz^ѭG^_Mܺc)JU 2ThtW.':Zs^xa3"|av}繯j'R@ɰ@'4mQ{a!q bךm[gc;,v%~Ӻ2~Pۖp0`)7 7QYZ5H9ɤl%ڵdxC/hLVD$iI]Z;fD`v9]!p \Cti ]6֢94ZDvU:~f^bWX`|l D!pob'< Tva8mzoX,:bm OԴ=O'**{eQBDrANJ&J-UAzRED/%#nHN!Hn$`H<q[C^ϋR^[hsAG# )i+ĕMюEt 7Ѫq(NAz,aNkG4{;}Atb?x>@ -" ,8R~ȑgv#u?ꎠ\olЂs(BjFշ}>.mߙĉCutHiZ\{I:}d<6pMDucAjz;.PM.ϿB:؅!!ׅ= $bs I!i>F'.#FHh1*L}.ŋ@ h;-زuHp7lZhϐ>]t, piR;;2O#}.RTx?m&-N|V|[hgE]V] whw {!\=$ŌkeK APNQaǑľ;9 hau'Yx9յ~㽱]z9V"k7H!ǃmB7޲wyG|Pѩy33Y+_`v KG^fRųD߿K<ˑ$edLffzЧr^{n4D*n{mD^ü㷵 8ZQ.&[; 5CdísHQhjl2fїk4{{k,8ȉO}pqQMYQvks +S^,45 Ʃoi8.<|~מ۲G8SK ,-L4%ôO=5IGB5۟*l1o$U^NZRGis\$69ɛX:(!MaMhBKI{* ]K6zNS %2sDm.$?BN*xSH nN?Cj1KɶHۄ n{IcΩ.e,Cr䬂4ֵIƎ'4tE䪂 / #v "^p*a BXH q2ljq89 ȵ1v6|%tP#kc lCNPTaM|9'Ա9|&8h5jv"/f?0oj-$KKKAa}?b+^D(8QzQc84%-'9rk8a28jw?|kŽ!GV}h؂ "RJ̰i\@.yv$6]l sK.q4O|wop6hϱcHbU^}N35 (6L2VF ܭftRۼGS0m8d>F^㽒A"'±hkS$V+cx^ v[,DdAO]>ˌ(;fwFØ|&E )sJA ܤKS Cgǧ\巚=0fU; ,l _}|$uڲTS $H6u5CBUu"n[nYnJMބ[ccN@1y:~Ȱ|NC0AicQdD՛׎ڃs5A;mRy^J-Qc Mk d~J98O\FZem ň'pJJxxWi@\K㘗bK6H s6ӂspkD\PգJVLcj^x~>=HuߕOnuj7-/~+.Kڷ)‹i,p@I-E4Ģ0[:mHY5 &2iqz{!IOr$ԡ^o6cC>Ξn [ Vv'VʌKrڲi Fb,cАDJ/(^bȚK$$/:@fY0ЁZL k ~9IA seشZkX48 ],LdT6eBZ@k005`@j(, sf44SYߦ\(-e-6y~sQBw^9ΪmwůR~'2x)Z/& v EP}qh0\m.hMN|PY`lǜ/z|!nX $=6]ad>_CIPu.>;0LK*O\!lhV3'?ifֳկ~No 6GD۳$y3έ@FHH,%+aÏ6~9vX, %Ҵ$H kwϩ[~YM5@ \Nש}[??hqI AЋ ˬ${ m^o<ӧm&';Uh׿u3kxۤMfN67j2Hv2Ԍuz@q2Rͦ)u QKy&M)jEhm$R$QF0YdH>:"-z$UvoF;|d@RJja[ . .'Dj=v D,+̡Ȏ[ ,IطynmO8AoQ^j` m`G#x7 }LX5oƧ-;Xp,$ cdaP?liԠXE)!yύ{)X2q*Zn|Ij* tn[RDWX@&c{ ث#rŤqSeDĩO~} 3r?#|2rwn&m,"pj^4]Ml45o|%nw4뿾z*s:\k>.os DHTIYմM-ϳ}NJFHV+o+( մqjaa>͙2t.a* . ^"ryIVJ[;x:S&L-[J Rd8'WQd'6;͛o8ormvU %JևuHL#,5浚ar'YXF·ѐ5(HKgЗ9"Fp\;I"K#kC8$?Ku*Qjt@i2pJL+"kac;FHR-( 2aêTorIPy;Di<;ћZ,N:RΣH1;<02a}RhnVZ^W!՚0E Vê%qLS:YoꑣyVW3 wNVutov<1S  Qqqj}486 :cQo  w3KEQ!ɲ1HyZ-3d2\zM{aɲr`p nA:MA˺}ٵD +NtJC4YۯPq@9䴽#@ Q2:r?x}ҕMGa]MSWp0+7\ɧ(9taI^D' $3j('5s}$Mq$. 1Ps5\va8Yv)/.Ʀwcwv/|6`O&IFiqs?7;~ m~@u?*" WsfzVvI&! \"VNUөglɥw-G%]! dR38'. (߃ǒu(kg2kUD;Ipp -c0\%cfܡWb/E'Vhkc'g~~ux6ՁcPE;`hj\bx SP:)0t4/<ikf;>Mg#.o$*t#Nu#ϕCIS eXOndRIpmNljM-F DL*`,M9\Ȉ'UnHdEax5R ~!x ƃb^Cpds J[#y@Ђz^\ĎRd,/9izri,r=D=hZѬ0daP4 眇Q-E\L\ a(QrpB6Q#G7x6#YL  pk$d2VuB:uv{PkM)1[ ?au/b߈h?k(g fȓͧtxaFif:U8/U>A(&p}{`;I 41@i#Vc MV IEb&峿CWtnf𞲥LfF`t,WbvfFdl dȩA٦t:J8TGX+X0pEEoTps㰇~dAj1`X_$ZÉW, ۻX oY^|Cͭ`l%RAֺ*rI q<"pE 1jUKW~\I0M=A2-OD%]or4ל9]Y.&N0;iEMA.CRje5AM Ut-^ep**R Pp'&^-Xs3VJbuk$~l0rc"[)ߦQtRrXta⚱ kDmj5f":W-˺Hx:֦A,* cB<}1"S٤ap?契50z{*"LkT1|&3Cﯴ~2e1ɳ;pT_lHXmE]F|˔G>+/ɪ|0@܀$@eZQ}D2q olלBnEw`vNp=[-#<"2f}mGÇ'jǪo{di >M]璉-׾E݊N Hx9e`gp;e舽;0(TP-C)ae,/OjA̞ş3$G^}Zˮ8U>J Lu9xxKTS&^y\e x[  t^3q-ԙ8txrS#d:p?>>N!m9;9|Ks@ D_>˟3ucg>EGƻVܾѕX :LVSFTz6Qr]QR.OL@(Ɣ4Ƒ[;QL:|ګ ֶl+sQ!m&h@.zlHꍈ y7 `V`MߌA" T;nZG)psu9׬+s"UByʼRΧts`LCgf`ٜqN#*9 /AfbNpq`8yy/SMh%,`hLh ((y*bA 4q>|jW>tZ98Mf.Y2Yw,):'%L:_i` V#{6,k5,M130jҔ*F~<'wax$+99\X3_i۬PcDv%~t$Nj-ηTZfuᬠLi n fiwv#~8J)9gWWMIw\JI_w7t>,h8~_wے-%EU26oL5!ݛeRjrG0g{=}&."l>L̾o_|ۥkPS9{i(5n* Kz&yT۾ێn/w~q⧇[Oe~ LzA0(HbSd*_Y(f0K^7QI.uk:@ys fHwznƭu'shW ^F_a)ic6mAP@FKG@䦀vRϚo9ؔMѥ >8~*DNz8^V~l]Y/cU.uFzox#E!SòIE5*& 1B(Npʸ3]$+nAo^PQƍliԴ1$[=AEEZs Fj&BN0=P]VRbD)LHA#9Q(ԭ"n;sKɆYqkwZ5ň+'2x } /pL'a׺Y1B>5Cͫ2SSdTSKu֌O*kc65SQEh&tpHn!+wbIsv#vv᣿>h@ض䜾ɿL~Ż,wI9~\qf@qW,@f|~ڦBN&=\ w+'Es3xCb~Ot+6 ]pR:l~Dn(fXp}ju\M˚E*)܃7:GmX9R6EsG=1K_ .^k^0;l ω\:ֺ&6pֻVaJ"ZB{J Ib6/L>e2جk5_{6ÚaC>-UUpIp(fMuq+פF?f!#6z $R{x&:1u (Agپ6H$͆Z9E_"6Ygi؄&ftO@4#nd!̨%6Ʃx }3̑Y$LqXJ']e!Sl~.1Ϥ\AZ͞ʵ+cOt#q-FMZՋy!ݡqPRe9TҁY8ȥT/5H&@UxMenѢڀ%l5˂-A9ZvfE95.`V6 ~er2_xyOơۅ藔u::5H9馍?9᧰{R:—V&"a+y%n] ƌD5Rp 3daO8ߢȨvs\ND`SY:ɞ| _UVw `o_iv?[k__ﺎBj. '=-|gx?֯|+p`xJPF&nŕy#Sp3Q H)r,Ǭ)>zH|ÑBb2ԭ V0PWp◪>_'3*bx9uwOXQ~Q|*0lAѱ6i˰f%@0VӃ:J%TN#Xq6||R|=qzQQM-{-j@ը?= ݎf%qdO+̲1+8gһFMciG]Z-Z.%A]#J5\A7²E|/ل2&~4%LDIk_O7/~͡%e*cXzwqVnK2ɭKݐ2-4&" IDAT5Rrt(:fpe>dk(31` StSs#Z*4wS`LL#%ǥK>嗉`ʬ7zs3!,| Z?SYKC 9g&=㣞B2)!XZe/m621M,)9хU:nVrTۄU*%W-dvFI9ң['.pILc,Z%drN3#o!mn" u iנP{8爤RыU59hO뤗ltƊ_e:d-q#2┬Ϯ~O,L£po9>Y]^ӤLE}0';5[Lns9n+!-;e;^͖_SnMlof?ƹ-v΃/=m.ꊇPs|cyn!??ϟ?<[;|wQQkUz) V؞w}*VgzIg8aQDZӥr%/q#/H_>VlRD%Hډ D1C= PleX F7;}W^y3}?4#exLA2nĻ*R+.X.TL.p‚˲ 208@XzJt0.*]4 6'% _X2laJJco4RMb-o7BTjK6Yj*o|S X{j 7 6+XFZ+noN:\% /pCe]QdU01A  /MUaСNʒ1oO@VCU ڲg*1LwF$_[&}0cFLX+GrD.y~ľ`Ap:H^LoR¹/Jv^:l_Kp-M-\$ALfX&dɭTԖܠ]CvL@)Lbu|IVP+74 _zZ=3o~Ý<45Sr|JAcC 1{$}+_~ݝᮀ_ك7q4{63ݭ4gH)VT%KX+OfPT7P/^jkd"ߋ%umОkQ 꽓/.wZ3XOy,8k(DU#&7j-C-=OUlfg8[2lSXL_$Qy|C1@/.@z P\fʹy{v%ߢm=|7G0UyPk6_En8>Y4'uwTI$QëqX@қV//[\G:մÆ;<|– "Ed ~d,df?+B-))ED7pB;HFckBZf ms5s%ܗ(4fHAOLs R2lդ0gD *,WE2KUf XY{$V`$TjqKXQ*+0NT%EXAL %HHbjJY3j$ uV pa=y4 7W c'ǝxYu% M37/ {9as2mp282s"#vDXgs znVɔ^ϟmN$򏗜䀘іb-8''t'? /LLg7O?~_pswbmN=O~ZkXIV,jc-6K'/}K.\nAXv19eOFi&@Dn0sG1C]8 a'-UQ094qEbF?3'^n(q?c.qR %4{jԠK9=e!At+f†X&S򿇍rrV;=XDSSFZy@HOLSPp'VF+9K+T#<͜}p~J38 pV&'37$?>"l.{;"KrHvNěcfqmT[l;4=DQ1"> y/~2W\.󜷷|b-~*Cءk#T Bq??:݀y3o52όYwui)&5dόc.1+LdZ, L7v"*]ڄ:j( 5s!''RzXfr=[t \f)Q@Zq q:Gz9WŊ&UU0 •k=3NyOoy_hN yܸ_ d^rW%NA44YtMcs(aӮOzW L7?R$|6"~YVH}?,g{a&I$A2&.!#MjI'SY?Yk$ˌۭ*sJ}w,(-CdS'+U)ICO+©Yu3'+|5? ;ejn@O'V"d9%v?:ޅGKxu,=jV[?~:"D2"K(h_s{-I@WEZkk05*@|>~cEy0קtR;tzhv",2%n&+]?iJE{]wq6~-xw,̪;hwN//ʯʛ?lq~驧\p i?}s"❆!HZf؝<"m\Aƚ=0os#GD+c|.޻(Þ>꓌#&Fˁub-;ZV '%.eWFetmodI.^k}>o;0N3-q֮vSi$Ι⤄457gje( -tܤ?bXdI#GٍCt>vJYZ[z85#{0GLЦCz'%#A!HB| dԑJo>:eXpy:9q)$RX7u!Zza<9!I!KJ{}bQqgjCkFk)a$ʚz#44KD$L]3k0C2RR"nz< - 7J 'V[{"tyIm݆Zon'^MzBLVN@)vhs&8cF8;ո\`=J)tGc&=E ]0__ڽrL>l*RSvwI̋kwrt[qlh2&́L =|$NikGW%eoTD`c1̀*uu KiZd$Ƒut4 ᒙ-vtgm|dvUxc0Gf,hfר`u,1pz뱺ĤPmVDXV9;bF*(^B=.dҏ1tY^2]"TKӒ!RWYxOAS۶ . -_,owP#"dSf5RrO}%4*vDj#3}Ci% %K:JRjƓUꏌ(y.|S^]NE$1JBÈ]R251cPs񡔏 ~^oϚdhTb-ug>sNpJK_?hRʖb Ӭ2Z|ݻ[~w 3 K)"}f̄eu` \}񯶲>i#<(\`xpYDϔ+ʓhɛVۘ)ma?T:Q5Ǟ;p]>dl~**EDEňݤGjx<|<塪{|yi͍U}q|T+AuGnw78x3k~=_lxO!s~d,H5ؒ[lŻg\YֿO/Lٳ[~-+8Z?OGFD`fچۦ-qGpWӄhh -_2f)3rN TBa ܄'D %Hsg_yr1YSǒ \bЂ hr m1A Uc= &*"^Sf\1.#fd '%RLGn?yL4(dcWg)Xxg՟$2-ɻd-39˨ Ȉ!X w4.(}*btA(-$DΥp$im%o:kT8Dp ټfxQL>HM[1ymhE/ 8hZTdxO xd+"Bl,Xv~T9S4hT(+1][|H}%&sQS$>ՃgߏyJBLգ2%cVkX'#|-Nl@ Asza"PZfKH)dMjԖ ru&oƠ \!Lh2f /A4H*)Rӡ*N #P,2MG`eݻ5;I5&S~ew)G9!OF.֛;~ͥ&$_UѹF&3䙥IDrB9 ƾpҎ$0-y ELW!diY%u XUu IDAT$:Rv57ɑhO" ]gm)~^uEbRȄ1"P`{i &c{_AUsR$ oĈ͸Oh*2!,p(F,blez.@2"6MWrF֓޸jJ>Hb d 6exᡔ_yKdؐXB"p^DQRHI~ yW6w(Vj ?o[l,~N6HTj*I\Q$R9 RNՆf &-$t\c#949HgDdϸ =grva:0M<{Quhwܡ`/؋IG9P)͵ -$;(2qBG8D^^Z5va!:)jk3$3*P}ZΌbJ#pUn7VV!>[)g2zqq# ؾ0;KlfBSHj"A,%%QgMᙉz{V|U_<8 ne4bYT -E`6;p*]HѦ.PJ]ǰcylҏ DAk@ik=,_;My8MWzU N}6i|ɐ"TFILDbm7^!)> Wd[lC)3d[ԿH=Μ|-: EL/OpNݿ>rErsXd,C$;-ʞpٸBSs/yښO=&{e'PG ~a y3őWek^ =Zȝ{IWH̡!]J\cuj+L|QS)P (J$R§d6plӊ"sGz~yYL Ԥ9ҡx ^QNaER,Ɏeqs8|^N,PPeVYxW]14&Q͡oKfxy^!_ FރSsrك9cJRHU(qI4$S/NLj;s:*tBq?KWLL%7М voF7mUIM_3ϧx7)!:&3=@a7l+W3O"q8*46D оd)c 8'A'%A _T$@4 Hv^(x+ߵMx!S1Q&b$gAdV^=&M.&*9 ox uLaJnIBxhJn D@äj'LN(  JZ6>MwF '5EN2[f.DAd h|F`Pa"q }DIMOkه9hb+X6ĝ%ђu2D6\@#h2 a.Є*{ì}f keIHM$+.$|%_y]:lB3T)yZOp%O ff(3r@ 0CRHb-uh}hWշwwTuG3c>%?+aO&qmr8?Wj2cr{gʝc6Ff0Cw\ v9dLsώH#G#o 8FFColW9^ViJSFG>1P1!EњRR eLg>y",cهK#]%f`OZ= KTD)q7tU}Ub/1pns06qvaTQIVȡ;=[,YaTAѩ }#AM3'Msa&8,a-F SjSNl~^9 fIDs:/?sA1[?,늈PXs%VI'0tz)MdqptzB"6SeK8yAx9@O=3%lS-' ,$-@.B#]bq@eRˎ e(TD9~Y|A *L/W=x9U@Yu#nL%v<|M]9\"8%>݃%< nrUT#X$\y}]DzsRE$E`XȽCUs đ&>[LՅ4~%,K2f;:H$H1]:- iU㴲Ef:\|D##Wi;Bh0isGBQi7*Px "˔e|~ :ڞ5Oы i#Q>ڨH0o5 @ud:m*%b-~2TԧǔvZDE2ZkĶuo-?8w]wʕ}c?K❵#$٨_r*E1-3#4){pv*Oۦϫ;T-V7&=跔{8xH21~ou+?:ZDLnU< S&t D'#23#eh޶<4p soW2'&;2_6_ jdJ)pf{;BGf@*va?gGKBfQ(d6DZNo  U:C 0w,Xe.p1Ҭe"-{"7EnEئH9e>_Z[`a􈩄X3vɥ-|=n咝'birfp qO4MX92Nɖgӿ `Mue QzL8Mi$QlP`22 #4lzHCEueu` 냏szw.' > XёujpRFbwgv0'u!mlʫpXrҝ( w׌ 5묞InJr^GMf0H֒DL9sڼ#=,­"K.Y%lܵ' b-u//_d 1E/H@U9>-I^Jgu/|fw~K_?}X[c0n°rѐIK!$1dWQI!Je>f/y+y6x3{Si ntw`!7>}Üؾ a,FCM ͼc2DdgG^Ej&dr_̈́gk0$.;7*3['OjJսU! C2%RNf z-j1B=R$BS&eK.t9}5bv,Ć3)^s"^$?lɼ "& 6ÔIO QsE|18bh׍Ud '.wF5'v2 T$LDP31R~Ҵˆe ?]H)ɔD[hWW,b20ݱxj"0$*S J+A-|,<؁Ҏ'3hhIƙOƿ%qr@$ͦOރgaM=2#6#ū'1v zɪI?f;S#)AV@>+%TqK,I` 5e(NN˻PadyhJ ":d|:"qxҢDg&26gz,J8XVҽ+!h'Z!r9)U)] 8~սL1p-9 Qz,{,hTiJ!GDĐȴ4$Lu*>IQn ^?`i>R.{OJ-Kĸ&/4NCwC8&L*/zF՚$w1yQ:lBN3 $JdT\9JNw.^Y^q5.eͮ~Qo-]Bg)ӟ4oS??oܸ6fv-7LZگگoV)6}>2:p, xS@J E oNb@͖_S;N)77?M:l'-(4Eq_T>k-U ;z,P<_&W . ENTjK+:;OlLMXfhJLrHl$2js(YY7*U]O%t^ N}̹m1q pjLW2XaAc6F\i6GgPmq"k;t[2 #ʽn͂H줤`^s6ƌ@&kSel,+u.&.8og~80g_qrEȐa^B_3G!>FMא]Ʌh9Y8;ERw)Gxau26#'NY#%!(W1*DVlĩM*̜p2T@eCƙbA( u'chsSFϸ1*5Y 6EȶlЫXE"2C.s !%$6D@Ri֕$bȶ$0tbz'WpsXI[Soq}DPH-@EG`1q2%.!6>xO/E.94xZT#1X',6eM_i #!JGF\:^6ſ.\<вv^-TN;ޛږ]ysnW}Y H5e0M zHңȂlXq#%L z /4wh)RQEVܪ۟sZs?sso3P(O^k9O}uyb9hA`z{h`7,p謷ݰ *<<1nT[p,s,'Wi@byy|kRg_|_+gN]kkʯ? }qN\:k>Oga>rXw;w`rpT5r~H??7/}ی:8{%m[MD&5̼e͔cݣuУ'dcX'ǩʄ璿bo{c3wfIOZ,|:Zi”F=bHny-Uǫ1ZQ`UXdPh6\)s `yk7( _3WG/;[B,ܿߘ-K)nu3X.N`%&!Rl.sE#Yy2ZX rqak(P l7PLԙWlB-=c܈dJ#{zULG7VMnDeψLyBVPM > CKV&/Cr>3|?h:X3[2tb5qU2Ap#:}z|ej:v2HaBF$Y;L̶}~4'/ '&ya w`$(L70+h.mMаUEF.EUu5[`E{؎TZ2)jLZ$;lX|m"V",Y? [s<<> Z_+r}?4ktk]oz>ybytNʭ[7'dD: (fn]2) ^ {6݉f+l ǖ3j c4n3eMoy4spc%` k5{gwpHY7_v .+%2Uf%\ l$$p^Kbi G \DOaƌߟ2J.I (\p ްxeMŦy F9T 7;p7;)W,]N60 |ف IDATF1 OSM cZK2HKD[ɘ̊鑦E9uA'ƶhw(,թ[Rj"ƥ[SB9l5Ni֭v<<?B~^cW<qXATJJ+b?=O֗O§`߹me / ӟ.H nVT(ͪЉBEy͠8iӃ}0_J[p՞H^LFz#mNiC Q$[p .o:K)ZD$3*G: ,]L[u;Gմ9@. Ab&|mh Gqjʧ7&i i )FT5W|`Z<znK0HJR(I;nuQ:%a*: Tl0sK2v[PMEDb-m*o]E瓙qSfIKbƞ(&x#:dsl/u* 4 ŞMfbt&MSSԯiM}FMh]3L6Pl0lj mFr`&PB y:tm5p.τekvagFU sي.1jYFZanIMz4LiS8-b;/[\;2ߢa۱:&cOeL)̤1Rn~w+mʼn'6jv 11f>3KGг 2' 7aIyzSD8-^LGP`;JBt/ȤE@4/t$" PzV >a9JL[ޞ8iP4`x ^x4 3dKp_~>zpc u ,u:f&.?l,<#7񙙙OǷ[wPZ /;qu:TmbXr Pw}9O&1[-ODF3@reSV!`vOF7o g%.PQaZ X%+2u:CFr4k+"pJNͰ]Y!LɅ&sCa: -ݘ DKSo&P%ke.!\%KUi.ٶX" 97fƢw#ГrКXE)=f{O ӷ#I۳jjbD5_\I Vf mĚ,pn֕1ڣ] h[>m= H6rt̶s[92SAXء!OFHx އUgL|I"uh qd}g_~<=i!rrmF/n!/s΁q;x [a]Kr8PcyU܂l䐸e5)@df8t'oJW[ H-ؑ @p 6F+uhAK<3} w&(`W)G-"%`?hi Fb,X:` cPЄz;qql/~],>Cq3 "?'?S?u^VmN)gN]sϽWm}iÅTL䓍>eeLaw!{OgЋ KKH>նL]͋ |;*y) NkoK%Fi tDžcn[)b" A u$%weeM'}H&JzץMAE i;Gi]'OxU'w(Oոt y\rCDE IEK` %r4$ o epJ0Qء=/> X#}l[)U˩nlCzfbF> 8FҌA ƑetJ#nd1͘n&YX&r7;.މ)A0ѡKLD&Mi1l.ʋA'Q#ߓ앫nNZӷ*Le 9x2e/7aPhh%+h4 [@@Y]BbwH`f2DP \ƞp)*l(>ig_iǿuorYyIoEtCV?~>Qu58 ^4 3eJ^I˚`iS)QtY./#Ifcedp#|j[Iֻs6 d*bfTF{•SO'w8c)0*' dћFc67\C|c3g(: dnj2A#3qd&8CNMJ\I+S)H@1 !@8.t6f1\.M]4zwLTmż Cɶo4FvXweV{سZ[ڽ)F-g>_qk/'akmydv-7vuW&&3Bymwo6K_# ?mI<ͨQ*rl1 7$R3/} k[c1QyAQ Emcw~ r=ӫYO<dxZ[+yͿ׋li[JCR^XD!ۖ@<;쓇lmv t| MUn3ϙQ {*ymF~>5Duld1qO$0|nUxNwpiyp+aX(NdK43q6I|3vq ?]z3|>x0癳,`gq?a>ShE˧˓O7F@F+?P5O.NHܻgɃׯj?3<;1<#~}|) O&Eg'm'~(x~ TTI*J,c g`¢[& :b%8g(mfB j:7ߝQv3mKW!pE2BwC/J"co²umYr5\\ ph<$]iLx6yfҨު%,]+PgBnWUL73]#7[{m*MT8hR?J]9ixX! iuڰs9?5s.ftH{7~fl|`p=TE&Lni\!GOE`S! F%gG7>k%S".caA, o aFrܗ %̐\d"49&^@OB޻V,o)l=XQ_As+jj3O *p%&܊3`Qf]96w s$-H{FGȹXq/L~hi;_/yUJs3̏؏}v2c{F4a]/]ǝ=״ &Q'eu$ݾ}PW<^{m%&TQIGOg  LX[w (-߇T J3]Ws]{yEi`C.eq)ya5,- ޤ\3ҨbcA5(ŏkJlЈm+ NRL,C* Xx  ڍG5ew^xl//v%R\vX3yhkxg .Gyݳ{Eꭋ_Gm2Uk橡\N]Qʙ fXgTHzBDj_U/t@sk%7LodT2@=Uk! U]g;_/}q[k`vj'[ n샢v2Gc~$9CE>{N T^&^5'YERRK+\'ϋgӦK3Fic{F F" `gM]~tst{y=]}6@_yaMZ)̮MGW*Tu",];4m-[ЛYcAMȦwiڽk .&+^ G(Ref!^|7٦)̴36mlmvƣ+< &6ryNfouV&Cw:Õd~bô.qLNAVoc41~)WmxKq[S+M'PE3a1oqJU12BxNIRHWUE 黤?O72ed#y{oxw&b 5`09kL 8@юR ').dV9]q|k)Ly2}nÊzu$vѮ=+nפ2LcLX1?7`6XRYp(,"VReKJN8 3U&vGK>ѸbN %ˈ%adţ9UK)|,9f >lbgSj!:gZ[ؑlJ)rf"Ir.֟y1Π̒կ}'lOt3un<_PP+hQ9h󃙞Ƌ+[|;8D%>e|9vck;+ ea# ,E)I!S}@3Y?/eψa(ؼ[('e,qAXTM)T0˔2$3  (II EG)وIg <SFLhgK=bsdfn1X d>v'nMΠ44Ě|nX@OG Yb} _j0WiA7tI#mXQvl!ȉ8i{+m:\s{r?7Dg5Rq;7W)?u5x$uxyw(BѴ)̄$pb).K3g=Yc-F4%#=p?,ʴTYgHeF~|Ǧp[ꀏ]4ʛXK 0 %&J5}By 9܁KP > h]tYTlDKg!N2b;v[1pn&׌mW?݂)Id _p\E{fA]<=5 t_\fAe݂-*\k}.YYY*TnZnlpG8xC6*D )hhF =6Mx-Qn nURny(kE[[#%Nđ즳Lh;qc`%GT5`Ʋ6%{HwvoQǮ"kwfw}ݽaj3?3x;^rhr^#` ~uR^vjK?Bww9>;s!J>$}c\6 t\Y w .^E#>SA\6SNSKU7l2Shs3͜}iK!7cXNp @Q De-S2! 6)2q=Z9{Q7`zW aYThK)n|)뿎vȆ6H+uι `ɐVj -82{4+Nr^°+cC̪رh.l#,II^<$\~g4qGv-qTx^Sx9IGʞB=%u GU<v̂Sc fZEx6:bC^K/d/hVH # 7a;}U'֊qќXaV.R!F"iYpQڎq 6HQ_;9CP`cߙ6Ӊ&y+^/Qo 1kXÊj\M[! UxIXIFaWC>:2^-g %B;3f `7D]G7 VJF?qуEǍ=s7ֲb:naKR`}Ӽ6ϜV 3yrK[k$ B̢vxvtZ啘o\iD{2$c 33RF@*`w09p2`mkKt{,׆ [@ t ^[dQ BxFmY'-}A;4kݽ\؃@QN9Tx~<fĀmuԀւd[~#ٟK%jsՐDOg e)f{ԕbD*+c0:j*gv;u];/¯߻wqH97h8>Oo;3 }ٙ;0ߔ?|xC`T=|f!Ȍt`{֨)vl^) 4Sԃ;w0zN21=_ ZdqoP'+Z4XB<M킏T&j$jye:\Ǘ"MXv19x \ue5+8RXuTGg0ǒQEAFWþ_sĉw:\#C1< ؐN iwN@9LW;mUEZ)8b%Boۿۥ73-p'#HD%\:9118jhGk?Fj9~ #%EؙQ>F͊F!lOZ߱1oxΩ٦X2av骑p1EF)k&rPO6DJD8~~k,ɭQZafgumC` v}IjZ{x4p1kPPDFqEs ]e[aNJ/2rnd]ljQ+LM] ihFr'k6XKm6]ٖԾQ&_Vyv߂4A7N\(ybҟN [ LXSXT`F U)l3" 9Upk:q4>;G* GWbzx?^|~^>qGRu 1ݬ<8h* 2 „ڗl^TScl03ZIrTԥ*܁s`%RJH㡚$̙-^>Ǐx;j:Ǖ9=QSt,`yԣX݉pzlBrP`j$P%m^{ݮ;,oryňpICY-<|}{O??lHax_80y,ƈ)a/H;⩅xܛ΃3.'3w?nvxgakvme5I f!^gEK] ᩝ<_Cu;&G+L=/R;3&T}^ 2)Md= S&[tboJ,+' 4H4ɜ0mܼ erg?Ngo@x[ҽblt/W^kp%vQL5ڜ獫i0]-6.-6yl̬(/RfıM}'g>zs$ lVΊF!swsvG57;rn@r<0K}(X֝1P=害ZGm_OYdVlf$8%QBUTZ%fUg^2\v4lzYAˑSy8gx@/gwVxʑIutxhBMo%q4zBd o<ӂP)pt.û]qNt"ug>T}BrG6fPG h` Dyo~9O/9/=334Yf1t1ZYxN!h\(XvipUKuxHKg#, :,ysk1ZFbM To^>294r̿k7nI H[O@I"E@(uMwRSMVJFʱS'PgMsodL,H1#׸ WrpΘa g7r"C.x/UנL8XI :as0z#AYʚѼlpMGth/Vs.OO@2Ǔf]6깰.EP1 .AX-gmJyQ9:rvq[)v*k6kcj}ϋ# tc$K9aMj֤֎'l/v46 +؆@ǽK`@-N>bҰCm磇y#wauh;VN?O?ٿhG&٦Qj;}:)=9{nMTg%X.x^K=??:ª#Fd}l{ xO]ĹhfDs3O쫟ϻ4̂=??%P={  J)1}Z8L!B(T#st_f`ПvDK#'m*7KnmN=rCl̬3 ֔D-g):͝f IDATH)b셸c-TmN2ԀJ%jsIPs]3ܫh6矙-ܟ1N)tS^̸Ml {)JjdXըy/u嚕r5c!O~Fvb͆j4 Ya fɎ̰T* WT=zÆE8l*BTF12)!`ڣާ茘Zgdua;1*׀ 5F l ̖tTӘRLT,r)JIüg 8/2oOIsAenڗ6`3ą)Pļa,QO{sy #s]Ia tt ="\/?P1%ۦ#ng|гoL=3>|q=܃TN+H3]n mwU cܞsu +=6StPx,'O;M'd%=Tsii ubg 0F[⤷{_rޑ;>6Az֦3=s;~6Ԩ0O6wz*Ⱦ^BfJE ) 1w߅QS*6LF6؊e^EhVIj1C7\zqs QʭoD{sw:~Me-gmDA1TZp_nġM=Tַ>9ijh0L\t |ɿ?cp&?otoV^fZǸ5Ĩ:V*4:"*~r?3]l4<{^Yo^,܂%Q:Cs`oD)dYG݀VO&n,m|?T&|a?;y@Sn Hnra>jStMBrZtDēp@iDDe,d[(EuC=c3 &Ċ8n1'9ROA c9J=N jtɴ,P[-5L 0H$[u Ti\_x0Y^4yT3eC^D'zҬ dd;51t+0+FzulSCbjXQw#n=]LŝsץDN;FR,Ps=7aRJ޼Dދ/؍;Gq|J\8?=N'ʹ'Fx,Z$VO؃mi܂>2i%gM_;X+<~jM\P4leFϟetF|6҉qB= 45lKשm}n)تC&6ƀj g쌬W!Jf񬉡%}1L;3:)=ޛz'ᘵhzY߶z3E^MΝ;9睲~*$Og`ϩ+Ӆbx g6ɺc-EɞIjz"t[Lg:~ًؑ ̰=,-8%k69sbVq_%&Nj;~h*ꫯ~?c޸8ݬE8ljXl)slO@6N$k$,& 5έfBQ(>zEj[4;<\gbr56yFD 6bVIy渪S;Wzא4BLLb3*83f̲YwS ϑw83&*‘5v>D\tFUXMyo4(T9xAϻ }lD63["Ā-cG:18MހS qN(Q녪 I4MNzݗ%Vܳij*5ikd`)O#V" b5aqTx#VISvNco!Ιmc)GըVjXs{Gs M'>YC`f1qf*!Dδ+pd@#SFd9'R}9J̉ްT_}$XA"9 N3È;-dA1p+if7zʬ*"alQ{5J ju5ݢ1nn$q.9/Wj.8%pٜ0&xw/3^4_XDF1u,\ p#F.6Dt# ^zƨY{JCA5^ h/LxJH)6m׭q<[\c9xޅR}tJ bZÈ`Jh&*>s ^>Z)cFRYSf=~Xxo1DNMkHbD͆+č0S*#r50++RG*ޔ _Y8*`kc 1ccl #V6|jgZSk;~v>~~oիW[DL}bl>_~p7; jM.-f5~˱nҜ(nZjp ulOt*'pk!n_B/.K&Z:b}p2Uw ? ,^95?SCI)(٪[kG>v^립w|ӟ?C8o M49 (օNML(=lD'l&dO/a)ƕ\qlh.4C1ٴKfaB"rPͦ\tƞA&\k f hڧkfAӵf㕘D`DR"Ubq$< h | ܨ0/lЩi(QQgҞ%K_V`f^GkeY|s޻vjݸ11&[.' ~$q?P`)@Rc.( EJH@HdEON$Wnˮڗ{c̵ UTUݻ1|{ަ1*Z5ɮFm+{5~F!c e(F&P=80Fi LMidd7#[pb YK܂Ғ$aτcm¦YQ!=dBp2HVsRvFF!jWkCy'wYN\91|;FKl;5ۭ1iVy xS O-H=1Nx 2uΙ=;kwŢjf8 s07FZM.X=so3eTm>^}Ɖ J"K&ensq`CHmΉ$[q׈chY'05Tc5n38ѩsۃ(nhBkIa [Mq[6刄5]`ZR!l JEH;>*q_fX-Fbnzȭϰx"CY!$3SK&A}Id$W;$!i`Y00yǙMȳ̯$[̂5bCs;}qXL4d/TÑq,aխCil+ok[C~闞|&|7}D9uK:::z{я~t{d{R2}ą ?OOw]R#5ybQ5vY<;g6b9NAuSCy,U ֠e݁'kuu\}OOn][x:88x'[lQ`6qpbP&Ao| y:,9.%-O68}Gsl4qb=|id65k8NWY:Qk5~S3f6% 6 mP.mNSNa-1v6wXԼf̼I HV6­%u 5mn9b=ƓE6V:!4bBz6jRo 1}eڀh7O^<,LU.^2tFY )b`ś{Kuac2vlNL#F9Jxˆ͞\xB"TVE<'vu#ga L$nk|[Hk0M hA4k)  bM~!ٗ& /*+(3u kuV3фFk020(:5vMC*aϘR-R sbm2jQIQqJ 6Gv]}d _d"z˺:laI•(]b،uS:^hy]٤> y:"82Tko淪L.ݬ Jիh<Ʋ1R羈XWn7R^8Hu:SBQW_upƠ3vS֛g(+:oS^xmmKr΍f==U)6BWj\kf=xP80u&> G?c~-5"NxffJktW׼t -^RX1[eᙡ+Kλ_̛ oz'_^JH٫O~zOͼNJ YxpTt$QDFH&7Ejd\u1t}"N}f&;ȩb ӫIj.:ozqκgM,C KX8΍6)Sp(`rǡj(9).1[͠G1K:f/; 2>shṋ%mdOۤg |}'mnؼO;͌fܝ3'%&@%%^%$Mfܢ}0"NO*Ս&Tuξ`|.HH^8i0.1N{.lܣW_^:݂\aa蜌eW[6Y,QW\ {%d $X%sg9ǡkg2U+TM>(h'0;UHN|*"N5. IDAT7r+oCAzڬf OӢ2ֆsa(7YӴ)̦{Tn4@.#exjOQ<'/ېVUDLڛRQ6vzEGt [7.Ӕy.3N,oYFO[gz霧[f:y}9ns}l%RbV`,X" !,c- 8 M ߝ;vbI^\ j U4B:[Z"׮ZS|^|VT,{;7fm(q|.tmБlHT=S\NTH-<#)w|>{_mmo8 0$HOqhT]NwN*8v^,#et\\?9O1FX:HK2p˻ '5V5IԞ'?q Sڸui֛W JR>kw`)[_ :nR)FX 9Sd%3)}iv"Wr9%c˝u*[fg7pd Ⱥܿ 1jP ʾlK";@ȕ2&GZ^ IM/6E?^&pG5RHX{Yk9gjTdVvKȜd;ԽhLb8[cu:X+u頣9$#dnmpOIMN#AtupH2`H-Q !22`,:҉kd̔^O0xae7Ys\;N<aGkIX0Q^Bv0 x&'`<˨ ;uxu?^u2ըdAI%5Gֻ+:V0zU߭HK\MǪ^Huc}tqx.H͸ej*ã<ʻ+QR}Eҗ7̟} ͫbQIbN}պx8̆C'w>H:?¥Djd/8;n`՚`HM%[|D̾ebTӔ`M&:n:)Zltjo7sIzOQjh,Hťr/p5~3 e,6mF^ڸKBO{DŽ UqH׹0H `t W[`RyIÎKo符dV{IjɶjQ.me8 fIӑDTYXK`~,#\I/;'M;c̀/" ԵaA5L^[pՕc|[㻥=xYI?"{YWsc5[3ZbʡaYUhXbdzXOw>"_[=ߋ)6nq$ IknN^ZODqaMm잒ɷmmk[ۺ7-g~gRJ)S}]O>ASꁔ0 [~[ۺg+2~3X識t JV]}':<>ǫӣ.o$ "wb<'5ߌ̱aј s3{i|kOd=̰n] ^Bx hwf =ΙsK{n{ΎH̬ sULSxSW>dNfy, A_X![Ɍ(:E! Ҧ0qx 0*^^Ց배X`DcaXS*acU cx=2^3Na]îsvI{#VKw=KK6oNT DH9$%Xq/ny y198Vݩm2cox%78TCu{fݗqZ^~X _$}cƩ .%o25 n2&7X8 nujUκ!|}oee j@81Qd#Oԥo  wSĦ;_4m]w048N1w:j$c' ؗcڛFNd^NT7!4niChl,bԍ1C=xThj~snaTj )<;FN4L%KnX&[ډ 6 U84|CX}.GMͪSӫqah1+X6+qR |F!-s!b3lf$ ě;y ttI.DI3v;pM) }z{?lacӌ ˕;GAfX:KXre )tG .ժtb"ƺc М80ue /?&^Z4vϓ]![W=Uц=)ֶկʯ4QeSx}mr!h?<pXLuxƪQh/[uПS.nBR?؇kn&}}3-qQ?Ʉ,Rgӛqr[o)v7~7>яR9ıN` {=؅M~~fav޽Q3',op.jUCWs$ *oU# 3>@v-`U,& flfӈk8;{͆FAy$u91~'鶬8pEv22 ֛\Hmc//ɸNgY2 7@RU5 | sY0s#2jm^FZ7||'fF!3XOji ǒ嶽q$+dFM|hҋɩp7^ FHĚ.8OݥC|_:c+&Ep^<_c썶D;{6}?g ]x1ng3Ӆ9Ϲy%ewj zyI;NY$"?v1á5- pA*"ո77B2|U^D41,$ TQG5סZWn߹r3p9Es)߫6W]W61jW?4c"$䖪nG߆ELH`.P1K *Tbrl/\g؀Wd1G4bk'Pguq6BbebX d}a"$>O/)2[uL y'vi`I# 3`lPjSy7Yܕ~0:DXb6Lp,'#N=vSj|]Dxd'w<簙S564;L];eyg]_=0FoF47&t &\J==֛ϦRLěM7#Sx-Oс o7EjP %X@$Jխh2}W@;EA4 wA' 1:@WkWNu$0cmc43Yx kOqv&yE6%0H{.3,u¨Uޓ15X±q¸Qa8'fs-}$47z8Ċ$4.vy zę2豳EpZ icC肋p81MZkiݧkس΁R4#7(G3OJ BbC 30GYfKFߴzVIáq^tok#pH* Sd6t*N *ڄO5Fr - 0XZ7fΈv|Jt\1NBoDF0s#.M_zSKKxhOF$RsәZ!.psED8a$ir ~\.K7ES+0k%lzB>dTcW TŦ 1Z|_(>  9 6t?˪yU$o7m^ޖmmk[V{ҥKw~~.Il^_v~>򑏜;wcb`[YRBsaίOm;p֕#aWYb6"k!Уs`"/ V ZΡ9 N4KቿHs)f֛PM$Oĕ+W%4a=>fCh2lv`w)춁ѾNF>kvX*da4 N[ =| ;ՋkZm+`tbkEW{IxuxD^iP6IV^OJ}/뼓N7zqYt"i\F2E\Mʶ<z}: Fj#-S}=W*+vW ?xӞ/ol~b4¡ZkM^nDlŚmmk[s#L߼{fbzȏܺu`b`[v*!(?S/xs0GH̬0H"+7p'Us2VJXQ"vn{}-ʯώ9`{1mm[}~>m>웫d{d39#4f@ d;M") ~IAm`wŔN>Gl (\ @c([OƷB3؁}"Lsv$ցP1gF^oُ w6QdaSa>aOUϓ/ggv%wM9tin&ֻ&g_Ҭ3vnxg|W 0c-w va+X)伍T)ԧ` νWiN8l4Ρ Tm{:lTg\&\u]NF`مO|&z( lu*M,YτcN{`>l WB_)<vHI +T_x'ЋQqifD-1KXq(5Nj1Mt!D IDATOG<.ܢ1kmZ+XYOz2v·o`p6"K1-hW73yyf0eC &n{ ,4ha'&nhPiO)ǘ 1Z$5PayĀ ffsHNPb-Kf=tԻ-3LJ) R2$#ȤRvO7g amX84˼D3D;׻ۼ;V_3B33$wDde~ ĜhĪC/|z0LJ\}ğOͬ³Y"$"9:4݂_ >_5[ GX7v=M\>ٙ52.Co|"*JʊYլFM C4ou<{j{gm=4n+dN4 4]8dxC<:jT8#brFuG06> eڠEtR K6idMfEN.ǘ% { {kCh<@ƒ̃Nt ](W_EҘ EL5-K6%Z!E`Iĝ'FEե\g0 zuוNU 1r8랅qg :HIUpvː$y1(VWbLt]DLnBqQ 3\RTZoM<8P:,w}jNn7U w =h3[Pf0;؋HkF=K9jG c!C~2&d tE{$2k+Fr |_MF*(W:c&t0̻N-LȔ!J0?39Zkim-mԅ}19Uu\ǧl{]sztݒ$UJiokݐRA??u\Im󁧜ZlC 1q}|nb/6:6^_b€61bdݗ ?g6GncUx5 J⓿~IU4%d65vvMa01\*rpscM*Wնp4*!U4p;oEEJ+0v9mA!9dڸU0{9וvTk7*-bՠրtv̦v@JUG%aj6\+ 8(˞ E L&%.ᗍ*LS!pjGi#4]Vk [K34mJϗ|8!v Q!-0a:F?XCJ&G 9ca=S$@YzgehssVGU8B lW], s{bU?.8<'ЃaH "qCN.K6O&x[-[[ͤa<| ,'80;h80l0P"A{xf?,R/R:H0Vwx8FX9k҉Xj+: }6tpJO01fA=٤9I+ czhWLnrqZA-]fUڌH" lmM@ܮ6Zo}q΋{֙b.f#iiTShg#bU,U?243_=4 Dlg䨓Y׈_9AocK(OOB<뷱mlconJ)wooM Ĭ_jD:r?￿mlcߴJ-'mg簥yI3F#o(Ƅ8% hq"{b?ʛ[Q_̯\AΤuҦ_6QJYYݯ_ mocCr81#sb؉*dɖVAHc{S_j@bL?Re1@ڀ*gI{U Hlv`ňVjpԓ')_\f`ӵ)MGҴk[CoUn}J ۇ /8Ύ̔kyFQfvݯj%D%쮾)mK Eĺ)`b)Q4SP ,XB8Ğn<|,Wݍ[>) BXoIğL֏*Y/7Гg}bjJ_jy4qB s W.\'LY:ml֜~yf VJ+2Zu{V)cGy.;)uN{#8J<< ̋lbp1bxf܀$WcVNV̠V_t;_Q5A Y{~AZHfCiGԱ&nME)Vj3S1M t39A+E2km_ y7֬ZkvŅw4H8% .+@9Vb;'͆>0{q_9SE"r#/?/S_TF3W<,J.0ћ.;KDV(T.IVYx wKo'MuiŸGo}Ӛ@놞ƫFkռDy{z:Ǿ_3 4 -I4(|.kITm G ^BTf:R)d$KaF \$iBcM Zmx^e57<`*LejjDoip8!:;,Jr;VP! To}4ΕnUh60UcqWEB_6/k`F&8 WDf(1KP%Xa fBn9<-UH;&ё#Q&ddt$SA\.j%*shvӗdO2{U~p*b&QLa?1Tpa̭LJ)U>`N#%x#JK' XPa(kë{4`Вr'VV.ʡzn$;ʶk(Rpj~S+ӳ=fz¸he% 2Z`ы (L*U+0Ka]vd;:*[7r4ڂMm o.jLnBV0̊AB\E&b?ȢhdEלP2|H"-jJF?h*S47է & nNCRj!YGOB, ejv0#LI hky(X 7;1`S|b$/V wIj`ҔRU$5ih7ÛSf6Rĥ#ngPk P}=ݮWzV%-m`/,lfES fPL=w e1TA-5 ~<}Au5]ͥ+gUI3hGp>"Ox]^t뷱ml㍒7 <5Ms__Y'M~|0}۶am|S5LJ x+39St|mˆTspZRWVP^R=-\{ S^t`c.'osm!6wZk<)A#lr$`ӊb>hK@Ԯc gP(idkLT*N-dC6aCo<$UYs,slEG9Ki LDnb4^v,pNN{Ҁ)9~kҶf!iU4+̠(YJce]m(U #,θk,~/G jA sU͖jZ8p7*XT< :) @ɩc ԋjQ7'N-<.C I°P d"vp/thl\^:k5؃Z¡5)7gNB d:+{A`3eF9۰V.ycq N-袸yQxU 7ž0-zXRP=dJK1gJ1Tے9o(%B3 fGoNLD4"P6\Gsl/(yۆ X5ޤoc/2C51N#&F-,u!f=-M;sI.4I:chW)pXeDs.% [Q\' ֋"S2&TiVDs3hLQ ))o 5Jb)naKhZ{=a$R&cTrŵ®̓E(AcMAciwWNU-6339q7eK$h `G8<UKjkc*.#<`-*7`1\TF_*1>4 ߷_j'.r6+"P7lw2 G?_>O*RI6C5Q\H?뫤/?~;)s>KuXTPua_6[V~> )p#XJÿ_~ً ׀Uu}//r}lϷݢY&ЪtZՈDf0)4=FRT0R֩R,nuA־pp}U{ni zV|LZ5 [ ZΘGy.lـbTzEK4*UBAZ0#j>+@΀4 y3¡=`cinm LNE ^eP U#%"0[-VMR!rً:">e4G~n]zLLV>b'x!qV7 F Ttqj)vYF-%;]GyƟCca4NHq$4Lמ0|iGӈ*cA-2шc&+PԚf-KYg,PoYx<?+?A+ScN1…W`&D"&f̰= GGkZ%?`ji䨕v\ /D&.baGw;SòSl-52f U{9qGW8vA}CЮ4Ü. x5 !Ugc81ֽ5WX0:Șñ%Z8+U/t^dhan@Cf cDWD'ؑ7=rc;{6 ){]HC6 ]c;* J1!3 pvέ,Izt͹-8ZVgpLO<7n9o7%JPnIH??nKsJvQ!-N CL'^Ȼ H:.fp/"t!~2ip ٓ\9~~C=hZ¶Zx H8%Ֆu#_Uve(US5+ؼB! & I1`E86 . 3h+Y^fpz2z7Nn-O1`M))0}>3L m|ð ºAID^~;9~hSXlx䃚 ,4fkY򀃍N.ۭ^ l3dj;,1-աnC%#58]E 8vCƃ0dbX3k!Gkdb2{H ŚCӳǛ5e/ 'ųE 0߈Ghs 7h/5< _aHg.|]v+| EpTxB= ӯ$! LaB $v;o~mS ]P(+A8Ow?N1K3Y8X:?6; Y^GoC'h _uʙYfP֏]͖U.hvQp&>L@4;"NS@襂E""-l][_ZIb80dKaOMC9SC]Py_T- Cz.wX6 "QvJb(o  9dX%NЂ. K VTEKqIėNwfiHNolSJ@;iC^qіw IDAT7f#a|/*0o>Ҩ`F|j6#MV$ǹɒLV;RoȚYlxJj㾶,p_}C ;; 4A L$v\rK(D^|hhPS9M꒙V꯭TY'r\W}S؇,=s+c׽{ThVzXX!%ᯈO7tQy z:+ы%lw}gK4KL 4'gmt>kq ,2x ʎ"V㒟/]JO2/dVMhZVl dhJ5]6.\N6DP*hWsWW3lymPȾ5` ;^M:iǙ9- J>:sg71K)h+^} CLլ3;Kޘ vs{cceyl Mva``k{+*BGe0vvbs)ѣ%L=ͧ$A1D@c=UΕa$FuQqܝõ-X17ZEN&VDH0qk[KY"5oəNsgc=3S [Cz=v m0waǢ-Tf]QY(##N"-D:SpW?, dŖ%%v@=[G*+!\˷vU%fMG 9o}V~66kDbfxZb j8֜5 ??3[K\TAuUc;r;LJO~򓅞3׶V<=lqJZos^RiPY/:j*/Sx4K -J'1ի{x X:$QͱQ'O|[ ֳꦣ-# eXkN(aV eՌR- iI!y"Jc"[UV5hFr^܀*aᲐ vD#s^LTUh0%<,U<&N e7Xl͕R8fl,U<73r Ud;9ZԘJ\[QJu6;1Ƽ(ZHj&M`kUU-Q,%bZJ&>6!, CBH=R^BVRQb wmX #!ɂsL4hVI+ A*v&=$!OeL|5ɥ@E⅂V y fRŒJ"%{(JWbA6L!⅍\v㔙vձVNR'vv }鯰 /V v3]+%"[ Ji/y?)$²hwV<|Oa&zڧI}S.Xsxo_sZ#V¹R#UK8HϮ!\ dd9G]4\͗{upyexѰ\؇rWѐT:!MH(CBݖUO,jl 0u5nRuo@ml f~뷀mo4OM_G衰grz#n|%ii8fJkEqWRz'XMk~n{1%\cJ^XQrRH;dnRvl`;ؾl_v?_{0z~fDCyas4Y|lf;ƞl5fi)1 q`0]G1\wd</$b,ehB w nJmD;PV}j~ 4pKRC(s0OԋY g@{Y3T-~NU|*X С>eưd ȡ(fE+Q 5xJ DQ kݦz:jbç Xơ&v a'eqݹ|8V VK801XJ Ypz8A?1,yٱ&6`6mlpvE菚Y{VufP,d}//Uي|`C]Sm*s_*e'=㖞5lux\d貱˰JQ(EI?wkm30kwxԹc-I4w7nܨQqgAA`LjaUK^rhU~lHKLDUk}bf(b;9L w =tW2FL*str9&F<@:;O8@ng.M,&fliROX8CM/IUE`ZHk^c /v`^n>?'|`Vvj h%L;Ild oĬ g;`T11݈=q, R 2,C}5)Ghqf8LɔMF&1&)5F<tNˑɶ̕W*QEn FȔ+kih)Pw]{໓e#1,Tƥ'm?_'fF'LŽP̴V͐=`)H)֍8w\xMc^#.q&C5 Bު'P(Kx,suD9ӏt:/ Cg(9%+XZiB3Cwo']1 "a>'FmbEPYuZ6ﷱ+Xۦt6^'Io~wwy^S4 6 'q\gbׄp< C/R=҄yڍ3jR~ɟ>WE-kum$R'~rIvA aC Yeܱ4105&ANT% ?ɨ<` lZb'醳~k^\I6f]}lf0'Uf,Q%7;0`}mYW6}1)xIV $J&潓̠Cbp4L$K>@oa*va7ةrМiWɍ`H;jBΗZv SQCU)MrV b<96N'zT1GFVEf=F7Mr);P(gƛW7X{Ȭ#]<*#OձtD|fSknUteÊr e|7݅=lWbN_"ȦH7D|}V<1INx[;(s̓4i[,y^S%pNjncܐizkk~F ځt7'MFDMk^o>he5ٶCq NP }D5x!OюbN05me%W 2S"ZK;S1fihR?FQCNOMRI  ל9g|G߸WZb ޭe,{I<K٭3:'2Ȭ3:85H‰)kzhL" xe,`FAjb(ܖf\v3%*Ejи^VI[H[adjd\ƾjstùQ +"<=m;76FŬ'l6??}߯UXl>ImVx>Wg-1vՑ?/|V!+˨7a$QT,x"4ݰ5Y8V\=|R>G+~;MJC//nz09O&#"ațL%4@L^~Fk \Tf1dyqPJH%A2@m݂@ÀTS`ؚl?Jp Rh5 [Dآݶ!4jYm,Euω~*NE*D̼'OFر#_k}?4N#Y &1@Z!XTa~(=*'ž۞Bj䆋ZF#ʵWO3jQ48CTNы^Ck Z6a\Jܴg@g3氨Pxɇct ӈ 抂"A:6Q;pMfFUG4d[ & !/[*4ǖN $ֽWpưgcrJW3:ͻ6dTj,.T-\]wk9׎"g'̺S.UVDHa)Yvj)--±:ޖQ.%*>Ӕ?D. Mh,qNjPڄ *%*MԒ)JT0P50]/Mo.c>:G{cί idrir40ZW(n8tfR"G)áib|sy۔83MLtN̪ɨn>V:f(<,T"Ř݃CXYu+XRO;;0E6F'I NTvҠn/8[/ԜD_ԝ|w.+D2\\Ի^=oZe^NSsA%5m 63݃ H}D37TބJ³{24K<'fof IDATq$6Jph.ա8y"b9+>'_ǃ3{gT>N.\^{Ye 񨕏0N$JH[$FnYys~U~+_w݌8Ӳ;̗T-oyZDϖt9mI[TEFTBd)ugr3>cM(d9 d HgA?5qsbdV5R0;7{F[޷ma4tAP'zQ%[!`d#fX&+ldH6T6Fbt4ݕVO$>>Ë5:du!X!VR5X/ _Nn5Bӑu)eCHż5ڄ>X97$t?'@h7޶>!MݖubɲMy|4OD8Xe^K Z2W`9Z>t)U#ٌyƘ3Νs88x=<º45hLD!:z}2a"Ij+Y2érYs<,'ʅz~;*$r:x[bi"G+fsm4X6f00f#LL%@qKGfak~9U]jy\hYp] ]uf4`e8a/F扻)]cFm|Ky?R'}Hywcl`ta=c 3Yl0_gnO3:ΣKg]2 aIk>E=5akV06=-yS8O}Z~uV["-R[W^ pR"[/?23;_d{0s1F(f{_jɂ$0+j!XQJ]cܮ{|8x/g]clwX]b_ZԞ>眧-oFńKP;k׮/k^stto_S:ZDp$ㇻzܨO_;֨/hu T:E*ã_K#^W=T"@|x=C]Wmo{[ hmldηj JlD:|׾.5*2[eu&| f]>B ,DXJ%ECHItl&͂O!RE:U_ՁsT4#DVxx;n碌5r!>*W2[Ȋ3$)a@Add1A̙YwWWB^kaJdgq'-l`tιM9~-?tdh NQ}WݰB]v*ݨOHb WqS!"x\9"u m9ؤT䆅&H$ɒUo~!]{3UIum!/PRpyWj$9AGw& Q+]>z->4|Oɮ@wb X6\9x.YO%qΫ4kc=j +a/KHQ@!01VEy fcsSmNr721/]j,Y"[,w>tOވˑffQ]P IcYthv-%q NoVMTN(S\{fZs#wgǼYÏR˷D}{-{%U[:CsgqU}mm }[aq92**D_!qʪ+PSyi4%st0 b|FGŲFǤ#tB9_I4J.Ĺ뙷MR,GƭPX%KIУ3',s7+$5EP/51Ku!AXJf :/ K CorqCGz?3Ri]|kY3kCA[3B䌧!h!1'3,ҽ)@36}zef )lmG3{jU9(^ū;^ ؉.v_#7ϕNZo?ɉ 3Nw{ u2oVygu6\_J95qCsrrW_ Su&)v[ų}E h{k7^o 䟧_ż1\El\x]PR*&Lj3j~rK 3Q:EBuR$ "\@C4Nu" SD6GXev[rs۪kKA-:N{N pE8F*}{zI$l3~;}ARo E (ZFz/3M46 qak/Y[EXOY-vZ㚏 ,ih_p=SUF*n[@(FQ =eqD|mZ6+M\щK 9*D'#EOb6 MIߞw"kB.@T6eSWgcL cՂ[h1~X2+|`10؇lk]י,"EBtMMM sq/d}Ke:KX<&3t^Oev.홯y]ƿ`9cϘեx-xHp;'#Y2Z){wD\Qt`.$!6 s?n|<~uy)]뼯Ip2"Smp]\?$f>3! U`85$PU^CVV [XؠmwDrs,e݉Sq=Yd9D*N1!7J]7v{"Oe)N8uy?Pmhkm:#g:&֒D"]kRۡڞ]|w]鋛/[Te7o|k_Wp_T]Uݪ-fȳ+l0UVm%;93bXC=Tלg8.??s?sväVui%UXI9扣?yūzo E_ތM2˚xo]e"MR1b[I ?-# 4AgꨢoU K9ms3blְ ΐ 0|Nj IJ -t=t M? v*N l,L t F1iЭB qd333ٸM ͺ&ZVP?3]%=<8'%,.[-8Jw&Μ84ȄՀp¶p+!ELRxJsPv=1ܕHxeLYh݃gRDzXr(r0[ZٗĢ0S(id0QWg{͌bbx/1Sʈ2hg¥eP%1~CBh!Z庿p_<]؁'-BCW+XFUb!0# h66ghvq ;:ԋ~,ňā3Ԋ5vƓ 7ħhN @G>fLHȰfq ¶qwV#5Ckt3q(.^$JQ/Mmrq#" `^l_"͍8guK-Sɫm]V&ا̌8,]{#0j%eӟK:Nl(Ejcfx n *'DZfgx.1|X-pTͰ/3v]b_UMo3:=̪ݿw/}Kgb+*ng#T˴dfPF]~v )R,x\&ДW'''@->w zIh]p9? %"z1&yH}yS 4Wv<Ǔ0Nv4z$WyJW𾔍U[U\*Uy[*:UС1Qu+w8MVh1wtqe+JsC%4f0fDrQIċcq žة1kv?X-yT?I垬j[H:b4oVjrH%W$#|j 93sME&th[ F}_[0!a1Pt],L1_CP:yeh2܍.EŒW|1P3=UcLiy`8-bΌGᣤOgP6JTh>#?76Csi$'[f75TX݇eY98+Y=ng9[-빙ʳ ^@U,%WBwD3n(r\ >wKOwa V~m68|7, ÓwWfB]^t v'iNGnHg[+5/=dM潫 \2_jF?ccwKՐ|Xs1k_?<ԑ|nTGX'g~_OL(/wn[8fN4O'dfRY9L+WJ` "!mXXC-Y(,\"PtXDZrLGzT"GeUs ru9/6qTlw AX.=1O3]zD.GO0‰u69mPGDX'padL:q'}Xάo!%oD] ѡA
=bo[Iͭ%,+;:F75t,):όO)])"ov [p!O|C[܁9Oۯyk 3K)m6i/}oo3ZshFf_oى<8wA ۘ;x>WG 5t]PWQR(@ՂJ*z=" 0ܕYx ~D#Vp_Kaa:8 ; g*iAa|ZFX:uMY `-:zbcv c!2",Jpǐ'Uu{<Ж8e s%o(էΠY76Lx,~ܩFabee{H*ګ]ݮB!Z5Zv@Q̞ӌxh V89Iؓ/-%6c|g ,; (Xt_Oi* FWulUjۓT`.FˇIϵ|y#pW=pvػև#7бhGXYOMqWF/|k5jcP6ssIE \J̰h,y͆j)Vg,kn p@UDp5cXlk \ ]}tj:<~ X?4g}uK2:9>>>WtJTLEikEV CX!ւʘ ]_Η1 E^AYož.-LuEb3VJSͳ/^ ?Lz"E k8!3~k=ȥGIZYtXy!EĽtJ<^p*}rC ֕ȌTH"?ni4sa~a꿊a.v]|ّRes['{SՊ|ɟ?o~w+ O}_򓟜@+s_*!i*@dxfF O)ӊafOŽQ cj% 0TJP8;I6<38зaF`u=QSaS8 n`p=8V- v]wGX+sbYf(S":Lq$Щڨf KV-Qo .TKAZɔVf!Po. ';xCtX 3cjk棗&O$0Zuf89=tm& LhL]o[oFIɽq4@k<(h @g>XU a4Qm lqRcBe:,&[Sq=s,eXv ]6;0j+ռD;FJn'n],lx1R'C,U~=I(]H!pi@􈹘Q,YR"4s͠AI [KDP_؇C*\aI vdg|2#mc!RVtFOlZ$kG>ǃfH$NDQQAݲ Wx}5|k m3zk0$9eK(pj83I,2DB IDAT#1y+aKWR ma6ĦJ^B;p@&VS%0d]C0Kػ >gE.̜o[E./#6g 9*\4vWso]b򵳩,4++/{˾$Z_?Tҧ?o {Ϣ'>7???LL=}n!QM#&U|]wϯ^]1$_*MZ//899ޙecJ@ą;ct:(VXxܔn1h`g93c]0%Q NaBiEZDc٨ykzgvak鶣Nq<i42 5fk43RmGD-:oP_ʘ(ɧZq=st8#7N$u8@(`Q-Yc}2|J2v ѱa&ʶWmQO^ctF0̆P9uc1tFHJ!DgE했ixm{Jm}kXLӯH @v5IMTڊbP9@vK@jjS)!:b^fɬ!pk!zǷ&ObԔz+z?yn@r[5&#|-"8jBbcMƔ{oNڨ#DJ_C trS8f9ء彜ygF_8/ ~K(BEщRHh.cf)6-^XZ;?d8ݍDi νZU oC|^dVTH|g=-u7;>0+F1^͌8Auvϻ nv]iPeU8zh[LL3+w|u{'~'xZ3[㪽dI,:uvUrO(m;J)Ϳ7o}[)o2;|wn d=/{~׿ۧtbooSJ|;_mo;oqFKōU{trӟ5xZH1ps ( U-BDJEj&cL4 SWi:2Յ3%;/P.Kt︢&ˆ$/d^Id1u& #4v\$,1Jzbf"A¼GF?,(M8c}%Bk؀âϪ(^70k1sQ[C|eIiH 3,P J â1DSޅE^Y]+pr UE<4&9P:aFpZW׈Hz!,IIeaE[S =]Xxt ږM|+m^ϧqHɅo9l#좿gN:t7/9pq?LfNs4Ǣ|kWW~gʣM:SZ̍CoDCB:(,7M|{w3>}]S.=ڔԥY_ahns뭗> }5-/p+;wPy?e`9:SXC`/i L,-/R^zTv^UI kBQfCIA+ ؠMlʠ0kTFƘ[ kj s7фd1Tz:덓ͳgPm3-gMЍ|t{E3>C%Ljsϩ1b24VagSP39CQqa k4 /ERyBkl5 mx-/[X_hPJtb"ra.J6*[ڕ{<@S ̶LKP c'w5PF5I.9ūٟ^^ SܺEߎX?cP{DFpMJ)or!lLP;}ҧ[=jEvhsʣ% (Yk:2hhβbwR{.v4Nm۾/*UtG4R?K/}}ߛk׮աhfp=>dsA+4ƭiڬP5ַկ~o|#<—$miOo6vW. 8SWx,8"zi4 De]pGb o/d]"ٟ׿~~*UVw 1sy@$Ie9 %4 {n޼x;տW(e ~ww@mW$ϴn6}W mo{_UJy&j뺺Ї~G~~~TιNi3dIiĩ 2~7 o׿M2,(Š|Y{ 6tF,jW8ϕ̒i_\b%q}tMd hmla,W*U+B.fٔ3}[L^-:ӑ-vâ0Z6p.Q~ST~unsYrcڣZ8 U2cVα]YHZF^`o)nu[VD`z\ Zc#6n 9ZYZbVx B{$@Smbo* =QIU>"xHLUTC3{(SHuΏ{I\5\dwQ4+, ʑ5cZL5Dil޻r]j˹mwNZH!AK@? ʶtvN΃ejnl abZݍօĔDQ%<}[kU9~j9dG1&7׭U|l])8qXװ5 3gDXn tFfR틚"*]wE,NdkW^rmZ3~+\3:զuj +Sˎc,V} ЃB5!{$I.u JKM&W$ec%;1eapGos)+ ˢ3:Lr;gf(m\z"ePHkx+XjWEhvQ+*s:CjRtɼ^,CTb̺5 A c"@w}^w{a7\7T|i_)PHc"|G*we~PqN.c_L }&%㋈`D]/p~hB#,xM)SSy]B$g4pmkߊ۱۱߲1ˣUD̗9q|+ܔ??˗G~G~G[>k73з&UĬ#wq\>c?#?É1.+v}b?m` ^BfC2K\(͏L&EǮ v[X"E?I2ւm!XXYETH ,;, xde}4ore;1N/n^(!&# Zӿbi_ ^k'+hv2). 4YcZlñCbmon" `*h̝ '&uB;)F/^(sۑ9,H{`&OeQq o VL3vϔE\(9&R[kS?٩&MzM.MNY5S lm5FvcHyiMJ:+W W D悱hW/{|N{JCB^Fșz/!:k>u/z N1B )L I#Fa@6EʾWCԟ;5X%)Ն'5Q p v\ P4ˆhe GЈu z;+KN 0E*I4SicZl|#jF c/8 !Y@45=hni) 0>9VIy_>_XY"7ՔZ˭]bH$ 4k|Y| }޿ꀈe/ B4$JNR;/ZX#\1QbW|JD0h,e@I J֚0PO qUE{/<1N5YryЋiۭW؊۱۱ڋ㉴>cs2j>|ӟ'?Y1/w89OuC믛>37z׻.]4kv3cԟ^Osca:~w?O|b3vSnus|dO_k-ۤopDJͳ_;vݘ zG{=C@ݷKs֫Pr7n7OB\tXTBln]]㒪+z ZH pYcX1Um:My3ͼI*xOĀSgyYik:GEEP)Xى'iMUzv‰&ɖ"&= cۉŵ4%:*vGeGNB2b ʰ*^Ʊb>ivقPYB5<% 2 &/m V27IaDEW#kh61 s(ȢPNLN6ublƶMmNe)3fOqlrnq \iQk(ӆ5!^{, yb ȰCqBjf pjXLFY+et?,.C;x)UIj'YyN/|&4}}^C&F7u:ճʿ8+i;718OY&k!s7\CرlcnZ3e'"Ja bbɢ"%o5mx%i)6j-'4h_ aʊ,['ÍZ6ʽ간M-D30TCDa\*& 7C{7ŅH^8/' $zgJ.!W'v;`/:%쁭 H܀#"4{|zD*gӛ=,)89,K6h2DS3s 7&Kf|;bvlvlǷbT8Cu}|$J׽QNt}k??{?C??UU4ea眿կ>Gy'`2n2v3>QNl5KGn+6Ѭ՗96*z׻~Uq~m6gOO/‡?x`{%Rt.~뻲ȶOٛY%OG_~H4lp){=#b22"ehۨYrvY Gs"Ny9J)2* 1p9iFFUKiT +fce!BHZ銝vW%z͊%lX.XTQhy0&:Xˇ`,ec-iiҀ-ј%p  IDAT[`Y_K  $n5]&ґBU5q*gOu' ъ^L`jz~3N;xr֧ 8G?ׄTs&5ӊڔPS%QSUvkX%jՌ\5Yq,rԈM>a:v5({h0 RI\!R`X?rL\ ;%ᚑUW9TsFVYԯEqF 6ǜ*f<|T꼆i is95_3L3+ePZ4SXo5*„aYܣ!;ٲȈҞEns'b ZB6:KX<1-;>h ʭoZ/$A%KkH!k,rJT- TTQێkdf3'h,놘(܋4׆*!e|;?+ox^i0s%4n*%ڵ#dK*f/G€Jyp2HZQ>ZtbRŻ_')^PkO$YVQ̄Va]=gZjg޲!k)Yr,P \* [M3!nbj9Iz,MHߴاD$ 1RFcT'=h0Jh+N)kTP(8QF'+fS~`Sv;m0zEԮsF-lna=Z10&"A/֣ެ# DS!u떈kXlLSS&FƹƸ,^Q0*h.ٺkmsiAQ6X5Eh 59 ,34^Pifbҋ3a3t A$A:>"vF.wGJ\2#tN1q崆ZIjqmQ؎؎?=\sk׮Z,߬2|[}g}}C) .]+导΂|<ַ]^a0S??́ϛs"⟛fuZ|?OzsߔS\ -ts rsoE%pbYf헼gJNBAxBfdz{Tpۡx}PQRcGHت!ҚCB _8Wؚ4SEgQI kɥIxFX8, '&XFSF+Y5Z#E6&*32Ք YRD%曲HƢdn-' Y9@d4|@ WCVv@^lUn_ 4ϻD2b);JX Sda^"Vwt6愯:*ВOYj+ԎJBYBRc~;9A`kh%LZJ-e75n{m1Vؓb0(Q\K[*K-*ixtԫ&NaX RRTR&s!CTZ[[~օG0.,>l';"gP ֆ0NZLHq\RJ}f +yݧ.Q>\|cY=Z}Z--ZBotig(p]w a~~r ‹5݉E`s"^kiA ijHe=tM@ j[Iu?o؎؎W_yU>{#<ׯ_>7۱y˱)!R׫.==/›9ng Tκ{\I`~Ʌ><+^-s^."jz'>{ٖO>}ǔF0og?~y"m s}_~}84 CJiN>Z/YlJ*7~o zћ-`$pP6geB3WU'=Mj.1b _` cL9L]i2n kF77D/"r9J+yD)5{J\T:+9tiFjKh&dJLKXU`*n/\ ^q!rh5e5qu+5 g WgmuX0#KH7D%kBi&)M @XrRuz@I]J9]DR+RYȄ;V,1s_蠘YndAd- Zd [ =^LA$XCGSV#!G%*wh#KI؅VxӼҠuNJ'utPkާV՛6֙:%S2pw< if`3qgd]<#23ӎZHa] @`Zvʹ)]JAb}ZFkK 8MQ{8>:-|), (ĸ ]ace7)z p.u`qJek! iBoptJ;;Nl؊۱۱JI)ُ9緾7<99]mO緼 XƜ+W\/xwY:9ͣ}wu惸^ŬݗR~fvʕ/| 8>#ⳟ+WdQ~~'~杶1'=Ԫ//|c䯼xۭ_җ/x*9ی*~Cv=@'~'v/c!\JD;LE ]-ec/ž" GN( z cS4*/-&GHLDlgdk$NE/F8`v`%N`}PTy;=6?atOh󻱼ۜY$)3%|XYtZ1tJ(/U*%5H\F5iUEJe;ESi:׉‘-X"\X{>C@g掎.W3lrtG-: 3Uwvgr T34w4;.*p-Jfa忉ë(dX0Jd-d (d,g/YCp0.G@'5GTf͊Քu7K. [@gfF*-k?Cߐ̽:a HXWDЌ9)URkvs e 뉅/J";ֱ%/?,h5h:u=Y}q 58X:}$-#qp%{N KkK]ܞ]?ZӟaulWP==!ށ(׭X۱x<*UEmo{G?ѿ7j9ΗY2,SO=u?>=o|}ozӛ{^K.HUw{ޗsիW+yjLN)=|tt09RNjg.7ķԾs. #|qQZԓO>ySquȘ]z9Αۙ̈́qݿw_/>%Lk~]#ͼxg6!HE6 'Up{@K}(`I4%Llѭr0I;'"pRj 3}5ZE+=J5, CXx;>OUtLι;vfC#soW~Wyl:=&Ee6m3*hX۟S624광7)~[xmWh8+%SUU}5JB[IA UOoi IH3wvǦgP]VُОqSekTZ ɍVru#Cp \6̢  <Y\S ^0+2X׀SwY虣eiWT+JwMԹ";!q[vlvlǷfyݪ_?O?˗c[qsf) })myB75͟R_߈7o}s}_ ;Y,s[&zxI{ң>ZE~꼪Vz._OMob2V_kBc=>>hvLww\#Ї>7?ϴIe5%#\}b_'vKUt7QUg*5!UyCh -ĀaלݾjUV,ffZTz` &qb4W=W)uQCc|mx(RFqոGhN9-!npd! >q7k~ oy^*K}Tޱ X*ؤ[n^}ӫB27!7F-وgWrFUh#84ܯYwҧWt~]1a`Mӯ Z#%`V|vh_/_kXNF$7.]Ԉu`j/EVB:؅pvT[@^R گ nJƦ uͷ&jS͙UT fQ8[SiH>uNDL_O5s-[_"1s1xc6bCmvEi0JwX3ژ{1Ȳv:q pahp9[&97!zAR[=no'fvq:c;c;%:,΄ʯ[o[Ju-MY&̜so99?L57})?PY a^M̠;V|G뺪V v|{/*KtG?xքnl~9E6"}'x'䟼~,ݮ9Ͽ DBnqA'zcGر#Q4ɵ$'d? !XB KF\I.3#A N:,1 CYٷxz/&S< Ө(Z!\vv%BnlPcfm MMgIv`rrP`֦Q2wɳW1\*)i*'PnY IDAT)V+eb02Rb Xr% 5FeQnH~IgrbO18u) Q?ksoI^M/$zU rY*8?N [ɀQaU돬WnX>='"\3 lau˔̱K潘B*xx)%TFce~ܕhkEV-E:E E$DRX5녻qPL׿Y0iWYj1 8WGvy+혫&0Ѵė`ia>&Ժ< [zB  ]I J^K*A&21`us^DN5Dzb',YMV3Z̊Uq]iJR-xuaܽkNT)NfZwLu˶؎؎W^P:-O<˗{issY4JɨNZsJ=Qa3A{~bEߤSf:T{?Vn80};comc^fA_GR׼~r{=v|1kR(?~WySX,y.'&{0V{3`fv*Uh'Z FDA t8KDɃLf%A`: HʶV`ծ S|Ui6#ZC@%S;;+ (;cbNi%b<{Sm$MjUvVw*ԣih`KbIͲJҟq9žϠLgg(L/Y|p*&SQlsrŎ=cg{E-8nKЫZ*ޔܽϝU7߹  i:8 ULn1㭪z$ĬFr:~=_Ih$nW WK:'bMUc.}%"3ͩ_:8YK*_U|9x^r-uKKB+XVhecDY,ٝ #18B7#v>8:0ٽ3_ԘZm;0 sKY7AfF!G}DjYdYd+bqwoa% (DY)xX{*3$I-}ܒy&!Gnav¬ۊ۱۱4{M k?`nA aWEioiԧ96`f͹;QY{y7г{{w.;xG?[Fn~/>iSSLjnM==}:ܢ̢v{u{g25$O[8sʕxϏOw??KK|aRnТą L.QWn) 's+YN}Q2<)pQ^QGPؑCottt1z.%sEDȓ21U >NH 0T']M_î 'H&2EANs4PnE"f7n֊Pv<.]"_X;03u:o?WRrFIP93Abru\ ӄ`EC88tMK] #aB +c֖+7?̲; EQSN@QW,2{ҮK nBEi YWH M9V[1˕(4Lb9#.er+` ,Kl|hen{eiG yDtѲKD>"(u*#T];F(اu=Q^JmQ V-Kk',e1J/|L˦O nPCTÙ^cSWWԄwq= .ʢao؎؎̨Zɬ/{챷m礖٠cqG_Mm?8yc\,ݭzlKksǸY&FjQ3DŽL&3R8/ˠ[(*FIRĂX`o+W, \|:“U SkE$snr4K%ByMS}cS·cqum_̹ \~zw'b^dtFgZ&jCkCӼOl&]yg&&r[X`h>2;cX\ެoںʎw` Ss|#ʝgg}믻7A;oLQqG+:^3Ei(N K4ޞv*l)g~q&4zzV{t3ñD}؇ Ү5y6h!J ,D1ê#47*UCMINBHS4Ḽ],^tAPZKu-硶7k3# Vj|k@!ShStq%X_qǔԼzm2EDo ƃR:qC\׈%a%PZY.EDP84a9K1W ,,rĠX3\̎/%¦YX2:lׁ5(^g'Ē%""(Ӣm]xGuNnh^NN`Pju >3e9x[º#K7^ڿ9`u`{+((KKYE-y޵_Uַܤ_a c55wJo;c;W<=Uu3 %vD2)@â([@)&nlؖ9D?|aH2BM)0bF#*X#ssN}rUgd gd-4zzNS?y[(5’~/ſ{n J],q_n[- uc{m-W(ܺuWW[;ahӄ|} ֶDpo#+:gY  Lܿsu?};6znW*C\u(ql8T<߷SFcO-V6lL;4}K_'?/8meۉh ʬ:GtKf U-o[xa;;Me1#"4i5Ȳio" Tyu"r)5t nF{1+NX1L첚*{&͞f6<7*BgVFZ.hkkEZX G#&|]lzflBY-[C1UpXLb%BRTzavP/Q o- wid2hksm!)l:]z9Cs,E4N̹e`d7xv`ھ,G72uBubv`T4{-x-Bmr5%" WIN'%+sZ^%&FT&ԩkȰY8(AdhK45ܪ(b4[&/d\+!W}BDH t,&q[[-\ʍc0=v鎚IRT\@Z^PjԖ&_؈qwy@(,;v z 3w0Ő,`EBYn1!kOHKR!Q旼ٌ,F{ndl 2U]Q.:MLnp;r>iz%tUI,Ť9”|rܹ߸ύsNQ 8W~pWI9ȸfv0^}}ޤ;~]h*9pb[H4J%V 6m͹M iiWT Q*BjA*,_o7 ҶkS]·MJzTur..Z#Z];JU"ԗ6?__zoVml؛`1(3"&\yDKk6looܧ?g}p^ >f΢ay>,J#+ Km[%vϚk6qgGd6Ls LM%te*UE]#+%#/fWc Mm9=„>BN Ns;QjOZx5=hTۖ>7hU4G:}Xh nJ}7~IUwB⍑P2{]ZnHT׊4V@(vKR|Qtv7n[GS]m DNW>hjƑ+29kuh•ᚌSϒ5 ksm vt~ Kcϝ[qo-u mrAT*#\W}UUJn//Aku@*vf'&cVmB)\sWhm'\M <[Cܡ4pmFlZ+ddk"EhЖ+}/I[pִǴ5qKY:|g.F׺$NiG-3#TeR0!h p-8i7Dk'-s. 2G=vNfE-f_R:՝s~O|{{;~{f uݮjb=%P0uyIxHs7n X59?oͿ'>^j\#J,׼HiRTrjxu t=%dWtUj;:aE7":u^Đ"Z&m4KRa1qLO_ $MDnWuHȪM [MWIm(Jq#ߊ#\'C+LtKWij^`+L6wDd"/{fǫI%LCa9B=ښ5+nuSJ, aFiM-2`  ~J}ofn )5I8oX^w\.{$-q˄iA'=̇;GqUpͬo2~\jZOVԪ')%D{'JCU- qʏ.Np/ yZ8ė,+%&ھJ1Gڷ6*lg6sghgOF4͆evy^9ǰO`,B\|S#Bu rmp;ye%-vUJVꨱ`U J11 8#;!؛c zqNÙq!< +OyăʗU.׻z_+C2N!jL~p'^^t2j2ea[x(_7䘷Q_z Bk|N†s+XqVmVtZkSd7}+r<\5!mէ?&ID?~g~jj gL2Kptڱ{rMrm»KwٿZt(xtOp+n &;pXrt!YU"pus]߽? ?廿ϾϿo/VUS0bز"TIE&ZZ#Z/>3<̧>;iǔEZ;c؊$*T\kD*VXDZ)3{*M 8Q/oQLMAK$;<^)"wY^'sd_h^bJ(2ߐnm@ Tf}hV̫7O|yd I$#l)Ufg\_]j9n :ӨD/M/mlFX V:+ވ瞮9ZrN)bue\%AM gpx9˨ \he IDATIlaˢr9W$ڤ6XfchoGDS dDJvD>ZUZ|Q??8~WIJjȻX+6DNK-Hg[֙m|ٷ!~33|&Β |v4&n]zRd5M4MwgôXgp,|;7?[?3?5ŷ|ן/}zæ09ɍR"VDs6~SG#?>ԧ}ٯ|+I\9$֩LfkyuSVܻ;meq;;le1Hg\fn55:WojYL;xI JYL^0 ndlz+(ZbGyW-E͞18Fw6%h:`v5-hьVFt6kui4!'iZEoR,!h1` z@7T- 7j=syF?TE$Vw%o<1 Ql4uK\Bc/kJvfInƖ~ uYeGU.q'*bRL= CD5Tܞq1ZW:fp)1aT,B]$vŻ!cTea 9F8M#=B _fF [v˹{OaڱXPk97$MQ=[v?lDV]Jа6\;,kf eC{wMMڀ5o,:p%`l&J͊*9T4͸c}Ђ/L#eDmQ 5]rlhVo$aOk]1K" KNcj0Iu_~:k|nG*yL~HqVOZbPn*@D88oєL(D;OO^т-8޲qxY~|߿\dn[upj??]]_|6,nv([Zg0]Tꤗ$=8`56x4M\P b |7۾w|w}ϟu~++(ko.tQ6+dej"8j,9d_~gy?|s{^w7_WjuFQqdTe31wxE;ZN@m; ye֩5YU AH ۢ Ӑrh)2sU4Cz?n6'LPx7骹Kf;gwvUwDJu"lRZ;P!0}^Ghs5]#Vsj&Tj)ˬ#͎cd2\]LVFf :6oMTK&HTFY- "\#y@N֟h&gf-vHSwQ5429$JrYj rs̙K!PDTZk3LI][,$𔹮\I5ĕ9 Iv8"jZaébU%"al.ɇTs> 6?;yzkLWFk+{zP0J!n}O=v _U#닉HzGh7fDi;aط 0>?r-GVr+ӊOaorVsXzbR*;̎9&+.^+ڰSo Ԝ&57|V?CrտWG[h'}3<#?#?|Skz۵ڌ:5Z,uw QU ׍N[٨[pבW:|WaBkVկ*aV{#E@W+I3S6nX6Klڬ"gԓ+W/|g?\}0[j ]o}۾l͟SzLgw}[V&gw^)s}_/~q=L,i(o֎Mvb2iŞr55jfR*(J]y -G P#m7ΫٽR*cUTLD0+bi.LL"3kRSք'kߪzہ.YQ%4.L\P?7vCK?9wZ5ɤGhf^ C,X5%3vP`B0:/[SLMB%~[@]G3X8JR o\q juÖe݁T5WdqIo6R&|Dۇ '~2[49Jg0(Vv#!NZⲟ[B=hD7.#9mŕ=YU(OKh-v~R@{U5*>ܚy7o캪N; Lr#4*Lo]1֣@k^63(޻ZC4BJ/EbyN>喌;_gWzmU{K%O4˛`LV-z yng}2ǘ9GI޼AI:RuʶBtPA:e艛 [hW֮,t{NRdxRo{H&G|֥TPfR)('Z88I0j_Woo69h4̞z3,Yk8]2SdK`pE"qPЁZ%axKtJemf[rd!Y/Sҷߒ{f Nkqߦ*MƎ I8AԱ|w6;+:Hae?1=)U pgZ.@ca.\դkܒ$@NʰZS]Uv%Gke+8'OKCߵϤC?Q6o:D0VK.K"Jg5@$kZc-M8X5z>9֦Ce,`sW; .9,PD5<&[X'HanTә~Dܨp#8؊-u<${SF!j&;x~OKzd2jq3kskBv#O `J-;!ɪ񕏔04$l1glZ?M^5LjDdVl`հUɨ56a>;888baM&"{WW_+`+FSJiߴRo3n۔__;?ӪT~;~'~'Xܴ=ڛ<4>|&NMzUYkrECS` 5laReahO25hOm (5u,ܔƲfp.hѵr9:I6<ڞΨpJ#;m`*W57ɹXSk"JaX?FZdML{w|5F2u8'5ӝÒ(шwh+vh U98 ]ܬԶ0N#?02ksF=pdڙL&с?\Y^}IMuvkOae*~.Mfsk ]+*K#a檉ս=f_H/B\KHts:IUX sn㯻pHߨg'Bfd>ˤu%(">2NN;O;շ̙YROypZ 5_ ZQP<'};SC6+3Gh"EbfmdM]]QINf3&[e7 rĺYן}`~g[>]o W ([.]J'&HA=Cyhhq7/|Ylo"VA?G˺C)U5P[+}\nۼ~ R[}+:gHY4[+<5ߚ7r%A$dVp*oN.` a۶e{d*i@TlLm~U'ExudEe^Oge׆Л+Smt\ j7 yzfZx2tXq6SɌ8Xin߾я~ci%5a>q[1[h~x5~-CuŶ~h K)KQpoi''̮1"=ʉ%#)$ˆuQS)h4皎n^0|+ l̹H.\9:SDS@hXrRtΨZ6r39uyֳRDu OӴhl}ko = nP+2."쬝؅F5 h~1x\$'MkHuv$ hpblzhL4pn^y$vk.&$%»WZ]*=\O]gD+/onP죃'bcEx)'.Z榇O78[,eHKb \ϕh_Ę9#):hȈy9ws* "~ndҭjD&5&-asM't*{jJ:o]|OaA2UӅ77Z>12\]|7Oa^֦p=s%fiUL][ɫ#ZkդIVׇL٤UQv\u[9Ŵ7/Z*+"8x9oϕMVAzmUX >O7)aeq:/&__^*2ftJ;4/ݝ;5lE`TrIETFFQvͼ T,3qiM]w]]؍{Nhay:H·[fw PLm$M3t~E6lX'ZkUke[YE1Ʋ*T4"^VzXp]4WlŒ"qjw{#ea3TaKɄ&F{m̗m=yZu߆S4 R{7!, )ܞIGpdwmVmkV2}JIPǺ4'[Ɯ-i_몇jbE:Iqq?MfȢZo߾/?Sz9 =*v5vW}sc8~?owR`WI.}P5vVƬhj=łEѺp1wNpnXd6Aкks%ѽmܶns|&WVʁuO8R,iͰ>Yx7HT;OclJͪ"f.ˇT:,\ +mc 1m'.w;b _x`å}EC 4]%H%gd^6Ԣ fU#ݯo?RI BDC7g=I^\+//p\5d>!%%)-}5x͊"m*^Ap_}hnS8N=LpXZPMum{" W^<PdEVjlLPe7dpAw} VDi4[<kujty&3:p IDATw!51kt ,8 6+ }pZ8҂Fk#t%Е[ f٬ӝ1TOؙޒ5 DFJ悚ǍVgANdھ/4[rڜ$z㡉>ڡںОϙZBGfgT qRֻ\2]VfChWri{\=zc ‡̛8Aޡ*cv .J$c_n"ER6p&΃[p'<-U X'gfih5aO\H`d,Ͳ{%("!Yi֜ЦMq(QC!/C]Clpi [(qqG3WE_ЇZex[Cbbx|<S+Tgy}n{6eOc߃lzߴN^HbЭ!ngjÀxޙn QVc+;Q9úKQ)A 5Ź_=ukrsjɞ kU&{Ё3tlIuuRq _KYwhۭ(fM Hx"7ZZ`SCs8AOYNh"%bC7p.n;C` [Z6E3q6fh+Rk=3ʭXaK9 d&vppQ8/ h-͓擒qqqЧ\_ljSd;㓟w^xᅇ˖x'q|5F%>}GxfRJfݿw/)$Y#3*= !V㋆d[(a9Ti0"1Ŵz>o9`gv=q1W v`$ Úz]h;SF%4Q<%R8!iFT7*}PX)׵ƀr1X:1)܂UӲް9Kwaߒv1HEEmѡ2PI(v톡Xw=X&й$Έ{QȦՁzknVQKX'O1ECW}5R/W4]\S{nz*Uz ց=9%E/:TYA52ZyRY^F䛵͌Fbl@R=IXJ@7܇bj\fR9VPSQ[խ<́->)x!if, Ldw۩Q]m?%{[IgGmR NGxl:׳?Knz y$,t /Ʉv&}$ie3W7&juR݋z^jB:y>ѫ pWJ{Aa ⌇]z/t[T'ckvװ]#Ͷ{bULu @3=,Bx!]_Q)n;s%-k)Ug>C!N ^ WtlI7x%]MZGNIXȽ-zd*|)PAM^E@kkJZwjLl1ۋO/TǺg,XR gO_eR6V4NrpXrM?I;|W $2L'.>I1W8 l\z>?P ǓNJW-u33-go"oة5u4J!DꁨF{ya<0gӧO~c'?__?KHl6I0ƻ1NiOs?s>MS ٿ)x<~ӧO|/~Dž-m=)cOPPEk[ I耎&2\cREjz ;M{*4I?kpTXJ$J5b2N2Y!m8ݢiGylDsx-ݰ{ݥۧK23uOy̆@A>9x֦eYz /]I­  ab>>f,9]-,* !ݧ O݃wALd[6He.Ug{[:h42UD0ʔN^KT~,. m+Gk̵DQ xT`T~6]^4y2[S+^j^p\~jJҒx6xrnq-|2퍒P;1ɊF2ee)SвYD$S:#LI.J H^T}a˚AHS8u97 8B}slƍpؐsqa/P/&9B͛[߂+vNܭ~^LJ)χJx\pY-4Йu`*ϮNkϵYN&mcxQo>(g*ƣ^X!xb -9(Uؚ͛!\AJU8r_Rkz/~t7+J_%zk{Yk1ze.C,bZFJ4:GW͓.%;:¹E^~+/i7 n7p)=$1IhT W ?0n;Z;E~˷|?'~'_˟gxJ/eM{/ܚi'-ga֙I>ݳk>W߿n34tbRYzi-Ȑ. u_4)#3c N_}=}Pε4sc֢#Vcw٥)I^+e r.Xt)Gw^&R&껬OV4gH=$0m' H/PBϪah r!L$rvӥ2}z}ܒ-~d=1󺹅[d׺G pIN njo Uu&:ے;%[dW`Cz,XOXu1S¹a\Zogqt$[| $j)W}HKAr2"l% U)Ɗ⌣9KJ])gfPxhc{q K&j YTzZ+|xE+aQULw$[B^AMp&[`*rO/k''Ҁ+Uy5`I5#Q2*?osu\/ͥg2C#q:^X0xU$O~?O~ӟ*(23NŢpf)_(iٜ&ꃳxqԥW\ooW-7wH_vAuE_[h"Mxq"/ #诚d[57wZȂYD3yը{$;Zֳcim)۱< ܊kFg':|9jM}"OkY,_!:t׀z2\gr!t(ƴ": r=,7 1 SWA8ټښA::sSmݹh}_|߫[+$YL,x9HNwy~ahf0䷝e{Iͭm5wIj͙8oyõ>/h+Ύ>#*V,i5Ǹ2"wQw}/U ? FA)v }@FnCӾJOֲrHt/ i_)V}U/[bk+e}+zrN[-~$=xT?fCc֦5)1~l7+bKL=o*]];;ք 5bA%'0:_G+pqUE';4+xj[pؗs]V_ފ4*i3{Oqx/s}gP8Y\*5^aibU;]h3U |e2)ichlcc*R݋o-yOm$^/Z9CĥR>YJ;7Ğ㌼ ^c#ѧI9ySy%&Fm"9'[fzx}6.RX?AߍZU^wm׭jlr&>B#b'ů(KKm=#⊼v{̊0<*n÷=o5IqԏrlWM@+6G `f䆼4A7{CmQ{B? 6E~gYƱsnV\$ >v◿X{g 5=rf:ߦ`pR`<0'tOY6]/ȅ wmAz_Aj)fU_5R6h?"9q@[k~cL uaХS/^.f]Av N~g‹x#rԏ)~ 5d,VZL[[ңEj~xn~|_] yLɥmW::9Դ mnfWxK`]'X4sDa˸.n55U.>F(uvi ;^U^ʷLJio͂pO }i7M5TdBU*J& ޣG|K)fso,EV[LWr Lо,wnnN2҈KTugX2ka*~;@#ha<14q xm a#{ʊpAFp"Geŗ% N4֜xch ѬhDV΁bG>6ͱ#/`(g:is!.id!(ZSBE$o' (O"sf &iae;?{~ WQ߮y'9fLs p<{a<0"I/54__K~]⓿f77?{K=M[8'h4˲Ĥ6OͿ7_snlrt.ƶ}r)vV9j6fv4h_8Ţy<; [鄼Jo8qO-rA m[ >}YV܎\KTVKWu#"=8/< IDATG1q-Ff*X:nbܖRswkT^c݉Yo:ޜx}}rYIRP!ã@օlU/ҩc(2:ɥ "|i.zC7`yOrd ּ-4G&#3-.~aL"ǓHwf*3g5袻ߩ1#bO{X\EgrxqW>^AB]-Մ>-{)T1XXļ"w!^(\143e+WȡUuoFJ\K;#G;ZOp(6ZdXcwql9;bPYIXɦMXF=ƿծpi{Wd#FZ(>-fВ-XN|jFNؘ T7fd*ZC#&pLIj5JaAdßv,jċIs!Նjxm 1c7 e&W/bpr~aKބpsys [Jp?;ӑKi2riFYrV]CCpv3M_}%Ń7|sv@V:'&ů5ZeJ g}ʱhE }+|ZyW @}M:Ԛ|j=t,"ΔI Y ̵HI Z7&Sn6؍L<=M ܮeųƻ 4<[厼[8%?w[i =NaNXVeRar'ɛ֙Pl9$m\źθ\YF%>J;* !RfĤOh؉`0d[<ݮjs GPݑw5GEF݅i?Tjz~ >!J2k֙X=1hYozH{8}Q+.[=UUoc|B;kE5]υ lɝc)r׃ =(\SwEGRUYȱt???]5lg;bWkv;GNr>v_ÆeRD,qSl<'eλC\SF;SO7l5%<6&iY.ph7U$Ug{6e_3hb\Ymc﷝=qY=]+^ UV&xIYYrwܦhZ{6_X0xQEea|3?#?~klɩi?~a{P?Eaݼu3$7|7|'O5!Z}pVEJae\tr?Əfg\Ot  tW`k4ɒ";4v\n{kg8m虚[/,Mƍ-&^&k:fh˕X]}.A){3ĿiqKuֳ;I!]>GCY XGb/n3О'cSNqc=OʄтC7Zn8;O 64pCY;=8XIZpKoDEgasw'9qF^ܚvGE 9݌4Qql;1**qAQY&:ܧFK>97|J^BvFdi+a}LsfEAM/3q^/z>BB Bn,mlPa&<FKR YOt怆 R?rO+"&XװwɖƗ,kW#N|o2-Sc!+ʲF1 $HSXprh,{M7WG-w=$i(Per*z/A]mS2\' 6C.Dg؃Lzί$;'M\46fk#²7y3^t[E?&Yoq_7I)|ǪUKD3i.Kx P;{fKjl1pb Bu>?QҿO}M'B}'O>~7~?R0>4e hКEl߶,3M;n4e,KpxJ.kūW8`n;8k;ם9 Qn53c73xәc+pP O3 l=\@]xj%܇~樴{dFl@ax{tXQlt:o!tEϺw!\̢|%o2x'S=crHH.}kV=g.ξƦ2V#.ԙLq~5Pi&z۹׌G+=h=U13L7`^9,!)+z-KO"94nlhqzTy╈`cB{O'дCEX|ޞ4iՠ0 b'YmeR+gQ%ܢ7s:< jFݧŲMA7Ԃ&"ʊ Њ95\AL#ɞF8{&*TJ~}u&<.Z@GI^Sr}y?۫*Cջ_Z`* ڊpGbbz|i 5 U 52Nq49&hJ{[v^r;0DU >V]8܇0sb4ve©*Z⓴kZ6Y^ .eądC99ORI i{ |#\j_5ҫғz$q6h)<ClTmh&i1uFӉuڄx͚ NkxԎ'~$KxF|(k'}$M[kCǴ㥮d}cO}SKy }w#0>J2<<7џz)6<}P۾O?я~D??_'6UĘ/[O}ͣ?o"6q=`wCIX]@v6Jg7u|M!OqIϤz[xW6[S#vp[x= $ ,x lwp su'zh=Er Mёt CK'XGnĂʃ| Ɋ=hI-t x#$ V4solB>M]:!_"p/ֳCz!xBۮ|ef\).Iz̀촕.-"8;9M 㧪a6fN$M pȃk61a'n}&SƜUtRd8P7|.7?]?{cbbAOǣuc.t{s ogu&ӧQ񦁾.#kW>%Pt薸V< t OLPj]PTu)Axr6"5U1,9)PZ=-t!&7b=`wOMK ̞i-%^>dƔMu.Mm9fgG-e⯄>pW Wch!.+{;5V}z cfKn=L :l$7S4 q Y>;]oD_OOOɶ&t+kvpi.+|ǚU;íoc`#![13#ݎ^SGwָ0Nu0wp'b.[| R{kՐACGqtq6RW$S=q~/˸_u&p~VqTҳ}|yh]ipMHMUOpa*rJ|j/G#8e;5Nz[/jnK͡L]{н2ZE95VQ 71i{vb?$~5^֐؇F@Mn]G/[&v\,{k/v4JGCT1ϴ-[H+eК]9 G{Gf=ʲ'!r ᫺Ij aLt[zG}eXo_;huL>#a ~;5,66SG99EaCLt FFl!+IHq0G],Jf|0qCmA&>(דgɪǧ<EƖOn:]h#NHG#TsE^\Ra<0>ࣰ$'ɾTۿ__ A ͇y? P940ϣK^MW[6o7a9lO|W{`ҴL8jӼKɎDt= "_e神s/#B@ȶzopw&AJ!D(l;\M`$ ֡ e 0'GO$۟yPV,`YFzOj^2l0+K:a]G$_F4AS@Py˷V@ =„h^fgw"5}evج"=T-d؍giy= Ū=1Lۋuar@*Y#b/Mk'o<Rlж{ j6 <֐R"]6B<6Bh3\s&[I.K?(m-#@Xrd\ڻ+1$-ܢ}Hcp+XU\֓b 8f.n L7#4]ݝZj& fP+g?O*`D3Cֹ1 l[ 짷Q'I\_tl4Bm ߭cY;}?ʴIb5i%k&/`Jfd&䝹Y'FRfF$r"d<}$犋C*.kAk. ʷu)aךʳJ^?ns|k%73]cg~="V&?(APC-@9UN"n{k_ ;tH6YI(sG;`79tww\*.&쐦D;9BeGUڧ̀pMl+>,=*;K ?0|l^- IDAT@74}|~~}}=Z4x'c컿_{~ߩŬ4,:'?Jfк~P1mi{鏦+ wyoo\bvv;zC&OƐ΀VL[ŵlQrqٟ)fX$2s W eWϯ4{4Ө%8'o]HwGH|j}I_+q [RǑWW,%n 7j?P>^LJx`TPݱQ8lY{2^| U"!Y'"0^|+LZ0kF8g~mγ>>UN%i!6ĸ*wv'$H-_ L .H-PDA@ EniP:QnH$PPNJ]u>^k9ާ/ƘssNUry*{ϵ֜c{F' miңVܒV\[ *MʌcTl7pi6mFnѺu=59"=ڪ-rx4[l$9& mŨyt4F)9uп~^xg~Y|}C}{yǮWu#U4%J6br :E!E9c^G/__O*.M5˰8Gt 1ʠB,Z:5>ܜ̸e(20vZVd[Bl?-'1G2-*!_/ֺݝ\d૲|}BF7@w;bOJ b[=]f1(@qMPDU`,Rə4ͨ7KWULrw.=z!{1GVv'7WFWr.39RH4]d|ڀܤiO9na8 wުSHE9FHE kBKQHj*}L=~jN1& 4mkSfּijKp`sC~bkF)s'ƨ=cvʪ) *PGA7HGYg% a˼%$(3,+Dqi*mEꡘi-8Rnr -}2;?PD2w[ >:lR0DBnŅ +qґxS@inEG>;N1{Ȧ@ T3Fv|%zV]|>$<`+V~mlP7Ɏ$0pƒRvhd 3k쪼SKkBZD/gz»[r\gbSF<I[tͭbpfj(SM\$`VDvcC]b'b_r!wKuC.3?4\>J&<ѭɬ֊TkV^FJ@ 6٘NU nbv]*.h(E%MF,RbI}Sf֤ߺWÛVt9+~=~?}x瞜 V3?v^m_'c{G:][nvM3N(>˸o1̆rMVj\7*8-?lǦ5ub3ǍLvEYh6/^ɴ*N@dH2uMLpaoL=ѾimJ =GgmU'vq?I{l6 -JS9~8fGx$t.} -D`-bōRc|_`Qn5k}9ޢdyr i{XI l6T{+ʫj\yI\+Ft?R"ۉNլ*hNE%HTp)34#K!+)V 2g&Hם͘H$ZB"P0C@vX˺K< yP[b_Y7uj3ۊ% %2`GlL*5 e^ADPno8{nIlW/VA6s31Rj; <ٕ{ZP!dɠapj; s29%uTǜre敹[an!k$lF`6EZzɗX:G-6&4 Ц< Cf)|[{x)mox[h$?EJR^{ Ħ=4$hs#֜|w]}}}?? < 9(P(Uzw}7?/~ᓟo, 9MEEg=# `q:c|dgb,yh"iٰ>PT(_I-ÒM~ެ~RvW1PϽDg+#(Z\ _Q<='-AR䧉ftHS=Wn b<^-KD%J ay|2M14l@ [TMWҍc4&ֵkInk 捋9,EFtR@ŹPpޠF Sp :xN 鬸Sa5[s)=Ov\)%]ѷAݟʢDtXT[H7lpalɰciik7ywoLSL(Mefֆ%${ {+#FG4AM[9+]M0 Fõ{IA-U =P[#abݵ-V̌/=xϢW_7p5G8zrU^ZG<1$?"pJ[f6OoUSz3b)QڪUF8G*m)c Vf1_u9/W.P+'X`!瓮GK[u#g4)MU.p2(k^pBځr-d;ilUSՕo]TǾr7O\S6xȷ]['O}`v|5R@7b: 8.7hLtyt}JqIxQ\ ٢99ZO 8^ӎlq RAl驌ɖ5;!˭ 5H-7rv)fG x<[4cnGkjnE] fkShjZGT2D nF{z`/WQj qx Ub(yK`#5aK|io `3/oeW-P͙Ah:P3{tp[>WTʶA-ЕA@ѕ%׸Rݱ= %BBVdv Dxh^N8&_%ë1ppUԒ]i칞vz,v)Jv`HQh݌[\ZZF`ఄ'lx ~faPFߖvKUiy?v:k#v8~̽%t^ޡX '̒VcW&:"7B%D[>krhশ6kA! {iSN+{c_Y:4Wz tnKP?=4/_GM%8*ۼҊ&{GsB #N([4cbqhkEB<[hFrZOwhҢ@z:O 86hNju#qx S JMi^;o//}>6MFY9svֿ$eɯe[p-R޽NPkp%%íd]#bTj׾?~?ADKv&cpf<Ôv0uQWۜyь8K64t g@ BfIi^ ՋRئ!@YtcnXE]噂)=1EYGN-t'NoN^ yy6w#/S98l8Qsھ5 8<5*y|EFbkGO.w͍S9FyԷl\h7\i$۪4"eЮEy==tX&XPZ;L<NX J2!>ăsH}sDcutʒϱΖnQ97H^=|sm+$Ԅ&Û Q\ +P52K`G& 58v )$ɗ8zQklb iGLsmvhOS4kV>~Jr0lbLp)@zCKwfx^bxj>( h>oQOf`%sBf#ߢS@L܇\z VA, Elfj&s4%S IAjD𬎉)}x ꢣ5Ƀ.9#l9[{Ӡ:ҏQ+ Y}8y QU%<`ىޢ-b5G1p0\.-:\Bɱ'&<7S4jt]nJrFB#;a#e)HDaTN~C+.x ~ j.SpBĦЀ١UCT jmVo`s۪}/naqKQ9le ՛ΛV&7ݿc?cMhJh#VLk>XZ(V;>rQ+Jg)Jwrbh2;qܢ@ rD%-싼ԅԽw -~d'Pn0au(}ZNoS;'3t|j^Lf&BBUfv ].+4WMOH[-}$k Tk 5$ s!vTvq,DȂcqp#7\ϗ܂;5e^ e0l+3:AMj{+0Ōc3p(fի2X#|3LHKRٵpv-"&aL  WG'Ư&74m oBR+6|Y :xmurrB|jo?C?|_O|+OjJk=~w|w}?_JqݮfiVyy)"J7*ͯkm< yl!ښEd!CS4Ԅl?sǤ(Dmektn ă8=mp.(vt ep'-]^ż*XҤ|s$8nU7ZD#˜;,+g<^|񑫵g1Ǘ%5|6ENFFvWRdHډthz D-9I?gbn+q*Ni)FSKzDZhE#m)p@} H3@XCCc YQ}\2N/{6^q%V/?4Y8Z޸͚kSL)V*_4Zv# 4 Fpc"ɹ[oB5d"׌ڥs rnlO}g?O}3^%ݻw{?';g}g}إY 륽RnV}{:#~Xq<ހdg\޸Q"|ܱ\Yv(Ǝ|G~oͿO?ܿM,Qiu0G1Uf}}[Mi"ْuM5#;if n[ϩ v68mȷMAt+|r;Cqً*[mpxjȵPrD@A[K)Ԋ qmM pܠ0_&djۮD:s/GpF՟Ǘ^l/׺OH~mz3VJД>pu&Ido|m:5;.!JZ KKXE]ź65YMI+{o9n/݌gQTRQ ؖTA DDF2}ZstJxB^ұVk7wm.P6$lяp"Bx"Ue4AmƝhDY;1(,nߙXC; .˨Ggt/ج%x's@%n"W%K*n9K+_͝TDʱ098aTbWW7Xw'_*@BOf&39$W0U/Vz&Ê[VD3x-?r p|&˓wy%\ΥĵϠ+ceb*2h~Pd's@H'=DdWn[cj]֤2) UgiP"\5\'׭ûIkL%g.d6ywVjYSGj ֢BTz슿Lx T u<|~[igEIi9}{ۿoͿ'?_NiWi*'ak;)Qg5=(;U}bA[(}SQnɟɟɟ>??jTPL*"){yi昛Rt7šI0Ԙv›H˧ܤnf$mۉt,gy@L S <ϖ17Oe7A0ӷn$Ɇ16Sdޚه/+{_&^:sHJ߁Fnm16Js"&H4<=b(Ѓ=6@6]EȢ;M lSf1vi*b4!P$ gg% 4)p:qqu5>#>oo//r g>HɦzƮe8(*ٯ_\ ᱌_ta% 0Mӊowoۿ۾?w?3ϼ]:ͷ8HĿyuM_7x^;Jq;~2D_\mKA:ed9 s߿ ixf ;ݸS0JuDMxHe[j>e<%RoK~@U{sȝ{nJХ@m aBʖ 9v :s*R4k~bCEr20.vpn`TŔ,+W!;Nt$G"X4p#L"۪-ފ‘Oko๙-;cPpxt=L|WpP;ǸJpiiu!zPGk!JbA,W_i%G1c'O=FsfX:epr&Fc2/75=Pk]j&zJ}V%ZBqBHM/VlE/ָ݄^|KWEG$:դj9"&M ڱWzd)W嬨d$ qf+٬O) LIىT(l(f$%Vl4s3q2{g 8ўzf#;g){[1`DJQO5F*Лv ?5ڠs%?jz=KN _??>_'Kaj:m18'){mU I "@m}ң7T z0H0tL ݙ=ڋ}cؖ5FO;ɥ%mSJ]ܚ|$ƾlVo E.T*L!N\.; Q87nԩ8 0gK.9&EX_k3<%ʴ[m&KmڥW::6(098ZٞKtk@ 9D5.hcvbgR;rˣ.͝|m0&Iy'/F̩VZLsTݑؚjB?>itxYE0ȶ7.d!tsPO4khʙv]  K64U]krqW\OWyu+M"#jff3Y3z(%sF~. ȬdC{ `aVY4\B-+IO[pxFqAlWE1!N{S(ʍѢ+ ^T}fLh9_;hNQPhMT-!Gz>6ɸ7f#ƠdD;7; ' 23 9$O-nճ:+L۵ =U bVeMI*L0ŏʅPOJU-=kR8 9U3>q^>5sE8*'4^~q+uc 7! !.B YV AI*nfU\[2O g<<<ƩRHכ}{_)C>}oog>i[[?v{uuO67|7|+8~C-cvy>Art%[j T&#N4BS)Y v=ľ5-\[VrrhvSa==6Y- Ii_`E $qC,h4ṉ?VX ړM4C>7%RO>5[ F]0YsFX@nXZfӞ1vC'0;7<[P(ymE%y=,H1+V Whf y6AyM uY6i}+Nbu9po'8"K}Q1)I{ÂwoKdy=e i2ztyo Iэd 4h6l@qHH#sʌwk#ݘ=볶+U=\WR漞Aм?hQgr1[};{i([10P)dy:@6s۾ӋjD4 Ͱ9 j Eq !cj@&틬^70_)1(g5z\7֧RF]+k?:I4nooof/Zkok߿51Kn{ jn-^oOӛ9C"2=Cɪ?g?||d7qòq1)֠ ɪ&U{|"ccuAj VN2ܰe`4 2$Y^،[BB! azgk1"T+6p#p?L> Po-|+6XC,m_S\w<ߥbl`Ql}.-Ti_WV }t+tD&!|_yڛW֧RF]^ʯm=`so޶wܿh5 I_/fJ9_{L{ymv[SmS ׏7E-umTޑ?X??_G*ek_ 5S7 ¶_ {HCNfJ1Ѳ! TOA #VAϡ[A:E. RЀ:XHm1L>zH`q־k( XaɈ>y)+Nwޏ&,؀ށ=fM\7e'ZQ]:'Y =| [Y>^*z>J^j`#zLA {"Q 4pyݥKp@R"flaleB 7 J1nc4DsO'h'&\%hx$.F6BzW3PIr"t CI}I",'$ 0p' IDATL{DGq9zX,UPHK`W/x%| ;R6 `G]%`fVhmw#n~%L~m:B70 \!'x#iV6 <f p6DdMq፡ tƨ=F 6G:.O~y}& p;bdRodž+kRߛ'niR+!\NŠH!hlT=c>O 1U0AfX@g\'-hXo c,!s3Ϯkܝfn.è `Ԡ>C$>a0MEpe0g ͵c>RJeMF_o诅=VDrm`} ӯ`7O{ ?m15.7ڿ]is{KןI}JHױ8ds$чg}gZ??e[@ u.™ZUm|ŵ$)NHI"qҧSnFl R>M!fxZӱ(5dO_~ k:KDCD-XɝdT^icW(ۛ {4lYY}vO/(79@41Q QrV2B އ.^AMd#莾y&I֧|?<=M k d?ۆZB7[jU)p#dwF6`.JMt!&L?xr6E3>Iv?.cAu[a\XEl#~~vw%NB6ڌ6*lOvG۴Y ` bb߱AS(ނ _YwvDL@1 $8};`"6Z#jSߩپPq ڠυM~MtE ֗ea߆5uɡU8#ba `‹Cm+<(&F4x^Onh{_ٷD*+ E_!5\+6ٳ~$m>MfXRJ<^_Skb۞6m}sO^ۜkf8|^M=-Mk&}ò/JYǺݝS`m4)B1/Vseg}_|?O.3tN$I}ֈxe4s$(3Hлnc lE;Pf6u6'8 { 3PoY }8=\ ' ki(9!(RPL2nN31·M޻z=c\lX [oa- z нJ/_,$4`1ϽgBo` % ]o;9#FӵIj7ܲk"x8Јpj-j(I6 5ro3^:?( ^JIa=䢠6]P!l8O9bݗ!?>$S e3\#17&P' ,\ 4u[@C ౷22x rp}{AUڝpD+  Gqi5I~x.;A; ]{yD>":ƊDM!oy5ܞt]KZ$8E~k=Y=\F\JM{0cEAL&炎zX?ilRP,g" 3K/,K1fMi}N L>Ê6DdL?4q;nhZp3RA_% R>@ 8K*l^!`P@7x~ۤ}wXMd}  E\XBKhm ٵB @+Ұ@H<?3O)_'wyK׌~_^yg.҇iWDy`ez7ޑh7?w?/~6]VJZQ!s톯~1s9V@YWǛb0]< T.BQ?XxN3*r5?E!B 8 `U+0ђͩ6oˆ*"zd`oQIٰRPQQgģD+hR#.dܦF)}{OG?r!琼C x/V`gpj3U|D5]Lm |B`PM7F;X NגA16c BV| <4 +KijZGv'- D8oL@z}#~S:qӫK#%Ek74 JU3dnX(6pgcQ {=48kGFo߃`,B[ {S1G\-FD^Z#%l&;(16أdt`sSD,Pi%1Ɗƾk{7Bʢ/Pm@x"JCg+L|dux ou5 _P Kc|=ԛ^3 L1^${F<֊8{ld vMcG|^buOv)e/*qp SS_th?ڛŰ;?6o~ SJ)wZw/R.o ?Ii?W?ٿ/ſ|zzO|;\`?tM X\'ƌ/T<*8Mf8 g"|S٤ mh mm#JDh#ϒ4h-ʋC%hpIa+swkϔ~ |"fls`}ZDg7\7\{ըAkқCOjOFܰ!D?1W`Y,&,У0 a4^߀8 o ZDL\Bl 8Ju̷s]Ki MprA\P/_V`k 6Pb-yxӻ6ƽ_$xZ`S}45" #7ޣ8A ;# AhxA<:^2pN p6klCY>J*bgr*GCl-Xij:= E8 lwn֊V,J룢Cq߇'eoqqACw"̩ Tr \he~[ʹaY3mP(ո| y0ɚ$e>ڥԷ&3viV U[A/0j6o^۴LXδb<#&`YXT/7`H"3U=!XR s>nTʹNJMЃ?h||+`qlmV;Hհyi~a89ݜќi67ո,LbbQ^[i,VߖTbhH CɃ;FQPq3 n4 \a{;-Q承36?f ٢n9@(iDl'.hcj RL]!U.>((t R5^j]J4hNx<^g~mu2O)RJm 6niq$j >0h!:??Ͻ5$ۑǪ%Z`)+|=}wdǶY;t@Etb:J/l~9c+(Oy~_! lxr{40FZ x@Q6ƭ0Yl3+g_QlBygMn_xo:KzOOiAE-';=iť!*ʌl(j am8p'GO;ztRڊO譞O [O oV-m,A̘8TM_pZp;HN^qǽܰq>aPQg&֕KPe3YS6h݆voxEW^n# tVgL3.o {Wwx~Xpy'opR74,LTql&mnop! v|aByO NĪm 8?GZ@4=&'Qh`Nџ{lHd=]^odȱ^=|e +fWp€uvm =p QmoVjU1Em\/K>#+JO`*( ~ӊ;&6`Eӄ_cX/54`PMf0 X,8G%9aG+ "p벇چmakXA d@0`RsQZ,? NJ)Rn+ PQ+ (w6lOSA^K>qyʹ6ArB. p ~>g44de}v( MD:h~7c6Kd&U?ֆqU/ u Ŵ_: 8w 0T. KLV XX*8L9Qj! lawf #ڸv|w VX -̘VlB0UԉSB(eF5o'g&]/X @\TD;;lZaO,uraFd@#>a04Ee'ep0-9}*{qbU҄rA h|( aϜgLٵπX>b)<x#",S0& ǃW 3+aQgۜ5} 8eUe|4a.4 5;fNMiXP%¸ŵ8_<X/ aٝ ~=h%l$ AH0V`IAL_Pǣ/N;p~8 l^g,Q]z5y *T% i2yPu5"ΑYQ(uvFp7FVR9=/+B 8`P1CRU&R䊛 x PW {챻Q `G$]#/{zNIb[peq\1U|Y('O:r鄧x<^9 cm*LQ$]<-چ-GǣV5Qk{s U3f`Ayg5}w|:s)_7Q@ `\V4P,ǘ E.퇓]adc>з zxuAzB GDhd0@D HPMc"ƛ~V|9-0vR'CBF/pmV&˝iCcm61WՔw sθ+֧RJ)L^R)eAF{1W^+"q$G\'Ea,4W8 RGu\5Vx [xXhjٗ_RB;!#9W>, 3_*E[hG"`Z@T3zu5N4ylx z\\/9fM*w:2C IZa[Q;8L_ xShM.$Qzbhd ݾLkk-Au߾FRMr܁^9|Xec$xS9`Ta({JثKC*o @6 EAh0Q~^~MEx~`gyĻOHE_oUJI! <ڏ_g3*cL#nqs?) wB@JPP5]IDATG~M&tUTv=t_Ky8VH} >؋?\7#/~yv |6N/FcKY&aȰ>RJ)RJiBCGH pNJ)Q9`6RJ)RJѕRJoZ)RJ)RJhއŎl.RJx 68)RJ)RJûm?KRJ) RJ)RJ)7y}f)1֧RJ)RJ)2O)B,RJ)RJ).Rz)YYRJ)RJ)tzuc٧R&ٔRJ)RJ)RJeRJ)RJ)RJeXRJ)RJ)RJ) SJ)RJ)RJ)a}J)RJ)RJ)2O)RJ)RJ)^X)RJ)RJ) ˰>RJ)RJ)Rza֧RJ)RJ)RJ/,RJ)RJ)RJeXRJ)RJ)RJ) SJ)RJ)RJ)a}J)RJ)RJ)2O)RJ)RJ)^X)RJ)RJ) ˰>RJ)RJ)Rza֧RJ)RJ)RJ/,RJ)RJ)RJeXRJ)RJ)RJ) SJ)RJ)RJ)a}J)RJ)RJ)2O)RJ)RJ)^X)RJ)RJ) ˰>RJ)RJ)Rza֧RJ)RJ)RJ/,RJ)RJ)RJeXRJ)RJ)RJ) SJ)RJ)RJ)a}J)RJ)RJ)2O)RJ)RJ)^X)RJ)RJ) ˰>RJ)RJ)Rza֧RJ)RJ)RJ/,RJ)RJ)RJeXRJ)RJ)RJ) SJ)RJ)RJ)L^zIENDB`neo-0.3.3/doc/source/images/simple_generated_diagram.png0000644000175000017500000137730112273723542024400 0ustar sgarciasgarcia00000000000000PNG  IHDRsBIT|d pHYsaa?i IDATxw|U׹7ސ $!@dpUQjUREi[E+8pU!!! 0 !}䚐fH9s2"|իk hϞ=a^nzDDDDDDDDDDDDD3t"""""""""""""|\A\A\A\A\A\A\A\A\A\A\A\A\A\A\ZW_}^zҖ.ED<<aN`@B0Wpq;^DŠ:pf?3?ym<_Z|Fcꚵ^j6Ei"""RCH"?E@ey%Y$0|s pέSv^M~ͺ7s>؃&2sدc?s9ϿTӿ""""""""di)o9B^U%V+6UU Mlb#C1VdptQvލa:Dmғ䤦*QQc>~y<^QδW*+vø3`lY.!|GqR^RPDD<_RfAF;YKZNVjC{6 $bDDkakIk.N;AHbйgƖufƽ|Xp*eEelx H..n.wfM=u=d&,ΙJ^mYK>Fav!eexxڏ2`z%F.y:|pZ8z$zB4zstg:3^ņO6צ;ƈQ@("""""""LF|jjK!){7?J]ّ:-݃]MaN!a`9a>;~Mzc.5̞j#m |dߦ8q Rw3p#7C*__ޒ9(Gpӷ4vI^F+Z: hР{?WwW~nU'~`v5;(W5dcy9r~p_nnvƝS8("""u*|wHƮ]NxC ੎J9SiHەtл:/~Yodbc%%&Vt;6ç8z֝Z3#ѝcj렮صjG%ӇM??? LXXxͧ. HX3o k߯^t򓓉\xMr|mt9""rA("""""""rb 9\VT߽fVǶ^^|ӯ,+XDDDDFv^OOgp\ʰl7m, b՝ヂprv3EDDD.F 99L۳ aKa!ROoϋ;cRptYC̴.CDDD( 9=1 Ƕe综<6vu """"""""3a.D#4&ed@@K,""""rZ EDDDDDDD(Z#1%99OKnLT\]YIW,""""r$vW\}%%ب~hc4{"""""gKHKK2ǒFiӆ32(ٰl}㢏YDDDDҠ\Pv k6`6 fkkvGp˰0ս;.EDDDD.> EDDDDDDDLG*&0+$9b<پ=A( +ޛGpr2m&b21ߟfd8 {ΐUDDDD|) +n穃y@3a6Ãem^f3Keb UU,/"TlED䢤PDDDDDDDHv'e'᠗L+;.A+##yAl@@""M}6T@("rR@("""""""W: fLrT/+ˋevqwwǧ &s#"r6&$VE{K"""H\q^=|gSRm3f3V+V+`ヂwo Z[Mdp Z""coRTU9KB}(;p6j5lۖ׺vl]cjDDDDDDDDˇܿo}eev?/pڵ-]úTھ*Vۙ?5k&%ӬYo礹\8p}͟~ʷf7r+E~.CDD䢥B"|={pW&0Xл7jՌ՝Ӧ۾pum˺u<8p f"k۷' Nd+c 1{ռ~ sJJw0 G$*4K[o7skb`."ߟI=zM w.)@n.olF7࿓&qw~FQ@(""""""""nڽ4xf;+E9вei,MH_{QQ<[UT052'wMd<0p`ub`۶L^t8v׷laC䕕9 FfL.{vvJG%'3kZdem}Ḛƭ8pWf_Nڴ?&50pQT+V.5 ScZTT m_NOZŶ#G++gp0sƌUgLx8OЬUw',XT֥2?>ֱ#oD6Uaa,G#GwA`GFrGDpS`ɭr{>C˖>-:tpZ݊.(c962cG +*x`R'ӳN̓ߛ?neoM7ӯ]j^c;Mܶ*~yUf@_bcNj,L⿓&벝qcIqN'wwO\}H|A1Mi;vrZyyѺWVG~Wn$eg70 7PTQT9=uEzA/_Ç?yrkAD ;S0}{j^Zyy+3EXzVlVX|k#:ur{d^޷qfYU:^.ӱLf6RΈGMO^-z\MڿM==Gnh,IwG@ѠnuZ80TrNV{t=p3ٓ}Vu _H_{yv;V+z#]0˒С͎pkVCxb6y;..ܳ⊊S竭/> I99"""PDDDDDDD.KO8ڼS>vxKLƩ"viչ3.&P,..pv'wP\Q^ۼۺ1nSZUE|FQ<:x0|״k=ٲKJ(oZ>Gn9Yuuˉ;v0,.. 1OִeqR==;B_z*س;nvK^cf^yݺ1Ow袋;vŠ |^|q%,?pɍriKj~) aJ3βv_N'$>-Ll6*m6&';Bѝ; ئ)̀LrJJXnw&3v.egAf&r̜.]]TQ;p0 |m3NUlj ㎈l_GdnӫWM7qs^{zFzAAzj5㝱Dnn3a2,nj> ,LJK$?^nnvfDE94:Z)u d={ /ٷ сgy5'~m**KZyp@zʀs3 2L}vB[__ދgRMIa69OQEtÊ4釈\:⋊7)&`~Ϟt8NIe9B@֤9 /:xm||xkXLٓ7j: àgp0NZB޽Y\o}@JʫX[y}??NzA{pZs023q3[,gTh(˒)rz biڱ#;Ha!  :,U:3?7 pr3_?֤]R`牉Z,\ݶ-P=nf3ǎ9l8|:yqznӧ8WW~r%W{2u"9} o~~g\֧լOK;" ``.()5))@uǻwsFyt22.)fݸ8މ#!#=YYmp}N5*V32ȠR';^\LFQ??Β{G%xEDDBm?HFEV'L&VFFr_Fu(K2odFF6X0K:F%Koуk )K` 7<͍䖖rUh(3xu\?><0ukNnc?Poo~ͬzxyQe`6|z뭍·[Kz5߰DxK IDAT~KC;vm͒v}~*[Jz3gz җGf~B/_ϵ۳z4;yoӇLncizx0g&~r%.&ছe c}իҥϏ]byɯk,ص>[sm@FQO8g=FFv>[Zc޽g׌4w.aˋ hӆ=D^GDDB2{ll,-]9{>%RRvyLlߟ>l%#^^<-]N\m.ܳyO=Uo+nכoofÇanӇ&Lh^(C}ޭZĉ-]%9sW\Lyh(,'+""8-1*""""""""o$05pE^%"""""""rɊ/*/)) P?? iDDDDD.vz\*l6%$v1 ) Kn'f2++3mK--*""""BTlLعMN\N\ȥC KFMv*/1stEy4F[KBve%+*r Dz{3=4y ( ^JY#I)++1Fs&""""r3EDDDDDD䢶Aakd _,M͋4kVKqΦ/Zt"""rE뇼<ő[YI)ف:ujZ܅iLf~BB/%/ӬYKMms6;v0|ڹ׶la_N!魷bqqa~=Ņ)ӯBB} f;q"O=;cԩ i\8 EDDDDDD2?#IIOJ!dWVb=ia0!(^^^ZcK=j%%p|-et b)q<]]84s&!sqLpSa;8G͍pW_m};3=*ƍlfl׮ ;S0}{F Sֆf)}3f UU| 7#=Fc\Vq1OZÃSVe==z}BC۹v}lݝnAAczbcнpq]=a=9ZXHbVc+7gjd$ogxռJ~3`Q䔔NҭYO> ;]N)>55p…u# X(7~n\:vkul!5S|ΦtoT3RSYܠٱmP۶;ƌ0fUwvc t0۵;klo `Ȼ(*55̭{skެ8pq~Ȝѣ'&:B,d]_?/'M5_"""Ҽ\,z.CDQMy^}V3 .G#y'.-t bqZrsy;.ݻLJ99KM巃tNX.]Xp\%&{9岞<= ={ݪnn.i=X=mmbiQQ,޻i'&<*}ޚ0Am:Ə֍7mi[`1=0&<NyUؼOWWNvQzx\]snԶ-BBxtr&A)իFV(`^|<[Ӿ&HӫWj21#* OWWV<ʠmy/>.(7~{o楀PDDDDDDD.9,t-fTL/dn.&Ofjdd؟_|Ani)W2#*1kaΦMDXy;eoNOZEƄۈܿti7>Û۶7 ̙N嗗{Ns.2v`^Bϯ[GNI Kw7 Ņ ݝԼ<ǘg o?N7@O1Ηx=yLˣG3?!ׯY=mc灥Ku Y%"""-챱DGGt-""""" (0`iB~p6Gˡ I&ΝyC,Ʌa`=xKs,*bŠ{~%&[a^q1塡;v ,"""""""5ĸ@j4顡-Y\BY;}:{`MJJKsXmQZhQY {&b6˙L[KuQC|Ϳ}ZnƊZVOeI|ǹW.GDDD.3 EDDDD.Sss`ӳgC\*\\O&ţ۷t9{bhǎoیHSX3}zK """1"""""*q;wÃ':t,&=e@D.]|}2~KBK\FJaUV|w{KK޽d^\;-k篭7FT括,WL9[ EDDDDCJ\MM8b8k'w,Xw{ <n8{3f^vߺ[|+}`|a`K:YT1:DBq1!nnԥKK#""""rQ@(""""rr*+Y'XPRlT/iP? tlm Qjv;QVab."JJãӹ޺HeEeٗ~ɟ?`ƽNZ3?Ix0fZ ç oҷb^>|UZJ^_5ӧ3cǖ.͋ŋ=lK""""MD)vJJXâl6`nןɱu0ߟϏdYL)) 1 WJ;Ҟxzvgy_O.qv' ,w/wʊؿuc,؁orrH.-嫜Tܽg;$ݽelf-"5?MksfbLls^Hxqzv?N^Y[⦞=yk0ҳ᯽ƌ(qlGP}zͷ\^f|eV嘨68yPv N ݱbSʧhŻZ1ܱJXF0̎Ep|]UXo_AVkwKhz<(+.T*W"văж67ͦh,zy;bMxbJ4 #ÃԪU,LJb鸚[晻7lT滩TX"""" ors*;eY;.j a?1 <6[{(*2 ɂxyl_rAjCP ׇc1LnJs:[k;#zԠ7rεT$y{=l'$]hu8v׷laC䕕9 FfL.x3/fgؒΓV;?ڕߗ'c+~̤WVs8m qۻB~pt̛Gޛ41%3xo!5RtLyMJ #gýlJOgXǎ<>knRZc2yt;W Z\nTV?&r\$Yؾdc\5uC:>tr,^ 9a0U+r xQs{]^=j%%p|-et b)q$K 1uknjՊPSn<0\0,xxt*,vR\ܽ8Nd&9ky&Kv ŷ/Y$oIf4£^Ϟ$TRXb.D7==|x₫tJgJ++IaЦ ]V5swwEIIZ,,N `xx8a>>嗔WUaq飥 WF/СF;\M&zϏܓM״otͽz1gFJ++8ÎooP{Pɹ,LJjxշ/ Ç;w.5Q;d픯McʢWV̍czT3ٻ{oNDbT"vT5ZPUJ5gV-A$H$$!KG67~>y|s>=77>=/bmmUyÈY5p~o %빸eV-sۛ,.SnxU`yt,$-pM^;s9~~78#t!!Ҥ w(ˎ-[߫W+0i_^N7EQtu+Wft2?N*U,Mj t)\ȸ8^WOu<;Jd;WM7杍ܡѴѱ9ȸ8V<5k0h84r$ffBY973S'^m/;;&7/Ͻ\/O;{xB!x\D)B!B&&,9vԶWpujD_b0S "gEGӦZ5mْ>>:9w_Uvwޔuի?+N`mk]:u+'psc7+:3qcWёmM>%B B!B񟐘oWSBssPvP(#›/ҽR%P)X\^^"y4*jU-ll17w7j|BQItFKJ*^ ;uV=R:{2537W̕\j*tU [[N=.76ެ9s5j`gn=f --8׻vlZXVq> t:"2+SS{_%NCzCgmgg&oϻ6V_|""xQ#iB?K͚|cI7wG^5lW:]˲ǩo11NI%DŽx{568[YQ6e]o(lϻ6QfNt4 30 +_t_aqv&*>..ۛYlq> ;rBQ1HP!Bk-7nK|<+Q')X^Q3xˋE7I"3Qjӱmc,,|PBRY6G8EA0iVԫjCX@ٓomfT,_Njv6 ͚njiт)w8s&yyhǍҒxfz-^]jJcnnG^nl<$-u:N^xysxx0i-[ƥ4 ⛎ U ?pk5kc>RRy3:j⋐1P  $)3sIy oY渲-#5={Ұ~.8YZ2en۴ YЫ^E=xzsP>`R]Z&fg3|jP>+Ub~V%BA( 00ر!BqW2Zf&$C|<99TnrknA33ޮR܌^-Dϒ4{t,j+lliFSv']9cH!W~EnB.v:;Ҋ@]%`>[5:uxcH5nj1v8wmnL CݧeO8hu:Οg-ast]3Nuc#D)LanVnn\)W!D-B!V |Gze%o% N*tvƤ<ŞDFbD鲰mc7ؼYRJT:7i98an#F޽q>_YO\ZDD0qcI !⑑B!ru:GJ~>V#+V*4}!&ǦAB:6Ҳ&*U؄BU0ҥ2nt =yu +Fwk^L [W=/D=GvpmU*ShΝL޹Ll!?LB!-1$啛TQ8ߔ yz2ãBU(sED/cbQp7o*)k`+ vWQt׷hPM鲰mm &&tѱmƎD!ʥAeʺT^6-=-yae* W22B+SݬB! $A(B!FQ֥'˺j>V^^XUVddDuƞ4,-aksQQP[S|l]BQqP@ݍhJ/j#-+;G'''jYY=`:&%ء!DQyei@B7I !B8ɈS؛qĠoo\_wI!3cjDAA**9!X[C8q x;!(-[؁Y?`ob’uiu`۵'X5 i#&M2v8BQ6v&6=!wGB!ⱺ216o.]1X^bP trr5kmKs\ڂt`kSS'cY)( f!D8@>E[=…R }|{U"}4 C!ʥĠBIP!B<6SSvN)pz|K;GO%#YYQԘ8RPElm`aS!ډ !ēꃪUYތ J̕]@J_@B!QSyB!Z^wLN6n\!:].\Øأ(`*Uꁥe I3w|=1c7p#*֭{ƌ~/3LJ{&phŊ%EubVS'( :~rwo>틽=Gax{{; !B$B!#( w/w*P~;Q+/}|8׬\]QW(dg_ 9y57nl@:VVqv]S4K#GZ q W>;w|:FIW"hz5&*z]={CZ|#J*GY)@-p(+ ;/|0v8EAASL!((GGGUFnعsQڶmj/wٿ~yܹ6 kkkڴiÄ v~Lll,jZBF %!Bܞ$B!#qMv$iZ-2JuwvB挭Z 8I~u_BJ_]ԙRRaVXvv|L;w=SˊvvQ J2\a{|ݰ!s'=1ÇLoӆ >>e!?Ғ>ojcAlٲSSSҋ>t:NNN\pZѣGXh-Z ???ƏϹsǏOKkTVMȠAZ*5k>WݪVfذajkN:E׮]\2<$%%{k;vЭ[7omG!88'''v˗x"={Օ!Cp)'MD6mÃIbQvm7oδixW8}4O Ѷm[ _޴iڵrTZw}Te]XXڵc4j///>C\ Z_.btٲe :@_7d̙=7o48?;̟?>NNNPV-:uĢEO7n7A!LB!ٝ||Rf-9Ѵ)]]+|}GJj_ߌ(6$ci郳sO[`rkǍE?3⯿hۘZX:u**ԉmӦqtjvJUhrsW??Ι5{~M&з/C.Fp0}>a{4R[7DE1&* bs\m[Tj5gΤ@{qXF]Kor*"cǖIeR8e wĉ{e߂1g&.*gƍMع,[?owߥj&|9C,Oգ?0?E Z!Xk4,S6Z`nb"nPKE)[n ::NG||b߾}|8_PCs=AaՠZ͌5yͭ% nMV1@ԅ9Ъb3:?''hkx4hS*[U٢V`%$prF?lW⢢X}MQ9m_}w8xy5P- It4K*׬nioSCQyzc||Dq47Ƥ~7daaA˖-GDDDЮ];_NDDCկxuZ5hРժUԩSԮ]oooZnͼyh۶-Vi4͛iZ $22֭[ =..nJrr2'Nз[.\t*Uc0`@(Gs=&njɓyfdeeammݻ[.={ +5jtZreM(I||<^ۃpssĉt[qȚ5kSNiii̟?OOOp3fh׻r$333Jy 祗^`Ϟ=9rŋ5tܙm۶K^XdݼtLLLɓw5^!G*B!ِJ{ xӦ swpA.\h .Zmqvu'&9`jaA^bɨQ{y+eThݺٿFWʡkg_ըQ @jl,yYYls&կGb-%UpٽzU֯|ycjOBa^|L~СDsl2X~W7m¶wZfTiԈZf髯rtj}€<<FgAÇs~҅MEՔJgTf۴iL }hQT0PJrv"))I^܊S5|*| -_dk~5(~HW:uf̙tRFqWzxxP^=^y~g6oLttܹsԩSAB!mHP!Bܳļs _ԵR%Um9:;c5ޞ> hҤ YYYL>jժ+Ο?֭[߿?őapL[[[zͼyXb:~СCcSrrc۷/{!55U$$$aSTtܙpv޽{,0 :gSKV՜UTݝ\t`l.\{ǠAhҤ p{V;|g[wɨQ ‚}sEiw `hZ92BIP!Bܓo}ؙv۹MT*>fwFԶz!ޕ8RRV0!??k8;6/KcW_qq~_ٿ&.* ߢn~"}11l4=~(PrDQ0_͍'#) !ˑUX7~<]ڠ2zP$ n޶GYY+<';w.RJVw k?hsd IDAT3۶9v-՛7׷%=a?oOFRms;'7mbϯp).lIIi&-8jII䔨BB6fM5SZ`AR*+*FC۶m?>djjJ6m7oA{QoooU?@tt4cǎŪ簰0l”)Sݻ76*x VJ>}ٳر?;vkpp0<쳬Y 2x`7on07߰pB;Ƒ#G駟pqq}AV011!::cycM)BL3֬Mʕ+؊r;((#8Ϗ%4}Լӓ2کSر#,_Uw?իWټyC|^ !Ŀ[ttT !BۛN}t0SXT._רQᒃ)(*uBQHR1v2oϓ=DڵI&ڵܷׯrJ.]W_}epB;!BQ1)ŒxƜ;(ΦiЀF3ĻRj̍t Ri]c4;D!R KK&x{*<~G4GVƍ3vaÆhZ&NHƍB!"I !BR z$+o;N ڲꩧp`l22(ywsswcasyyc m//~rn.`bl,Cp455VxBUllCB!#Rz? !B$`~Cr`+6p8RRVʄdlmqve I !\fz͚L4S嫋{LB!BȞ o6=[fl߾}[oѧOcWjmUd(?hB7I !BWc1LkْIO=/]p.2R}.dر|۬۴a/c#5»w08wdũ͛ii{a.S4a?{$z%[ٓ/aZ˖l ~)V v^&0LJC+V<Ks;;89aRPXu6le˖899ѩS'-ZDnnaaa 0j5;vd̛7Z}d[lHf͈M63F?vڴijׅ$Μ9C=pwwߟiӦq5s۷:L˖-5kOnܸM@8{}\;ۺu+ӧOgɌ7@֭믿μy?7o~|ՙ0a1J̼p} '''BCCY~>sEVs ڷoOʕ 16E 2ZFSR}kZgϞEVYرcjnSi?i$ڴi5 ,QeaZnNNN<\rht邛+ܹ\#GK.qB'$B!`lm)P@]kkvR¨15`~U@i:ajZɨ1>I֎Bf_1-;uTT*éөۦM*tKѢsӻ7~~ 33kk+&M"o_,]J`C}Nϟg,`|0Ԛs x1˗sd*ڍͫ6q{QkW4jĘ(DEQk:ۛUŠdN=PQ\>}ЫW/mFhh(Ǐ/`ƌ|w$&&Ȓ%KhѢׯkѢmϓϐ!CZ*7of޽=:t@DD~|DD+W6XeڷoKhԨUVeҥL: 6зo_ӪU+Zjņ x={6AlW^e̘1̟?Tuvs+ivm67j5+s0w\:JuYYYmۖkIz֭+56,,zj^J.\ILL'svv'VXAbb"ח:n0~̙Ӵi2c߰a&Mb=zիW߾l2tBjX|9v瞣 ǖ-[X~=Pի$$$c !Oc B!4ĸđK呗wK5̬Q{tm^{  pV8jxv$>K.1d֭ @ZCBB0aɄ :u*N">>^ /W5kח/RjU>chԨtܙ_~E*5;;)SOPF z)N<_???{‰'hҤ YXXйsgmvOǬ_>/׫Wݻwb 4ib07`„ 4k֌gKʅ/}u.rrr27j(^xf̘#,XIEeٽ{7u֥gϞx{{ӨQ#cҮ];JIpp0 qFV\Ɉ#<\7xCB$BmmNc7_7Z<ׂOU5RtʯVrh!BR=:B 8:7kL7sٽd7]"R Y׳vۅ:L*}~:<8ޣ[23صxIJ֍,쭰lGu "28rI--PVMFbPhx4kccSJt}L-,hK,5 @jw>XѺu_~u9Ikg_;4|~}jl,yYYls};OV6?4Ջ vǎ4bsi~#F0m[jo> B<9&z{"9`]0)իS㗕ӧ mMLL oի3x`7nLHH=z^'ؚ5kxyyannȑ#裏8s [lݝڵk%:yt:+V୷"::Rcn޼Ɇ I)sss$Sz?,7AؤIg;͇gs9kZ~'"##ٵkiiiqܹRɹݻXJTTE{8991||M,YBAA *wP,X]v^M6?ޠlIiii޽{y&Y\Ν;ǕB!ۤŨB<1JBq#'wd;gZ{qy_KG[%Z:gaՔUe畊S5ޣ]wٳ|1ddL$d<;d{k$]kk&K5ؐ{K8;6@F; uL D}J 8j7nݚgĉ\Vʆ e E7[{~-#׮+ mf\.6հ!oI#+5D;RBT|uyյ\j.^4RTt:1>|3gΔ_СC4oޜ~ )mڴalٲ,,,hٲ%7o&""B_=x+gy Cqic Rj̙3gСSkQn]G|||m999]uj4 PXp!>!!!пR0xk[:Fa̚5 pxۄ7gϞe֬Y(B>}خ3f0uTzALL [&33}zu,B!ēBBqNdg=(Y:crG0.b/| u:{5rdwd2<%t˧Ǜ _`V|'N:Xj6twz<8fn"?7ZM^M1k-BҴgSIQ]EaCj*yyVrA j_CQqpɩ3ffwLc?`]ٿj?ҏnx28NlL,[lꅫTD3Zmh[L]C~n~Fqt eV5*(DEүS]}OrD Z5ӳt9^ӝϷĽ?Oo_ܼq#GL,sߚ*9k$ǹPk4Ґ4 صdĹ  X=e~98,6*-ю46&y)-kk.%Ѣ {Ir\2 cIpX0ʭN"(8ONfUWk o;=g-+7'黊tGQɉ{昙=8h:tՕ牋iQE-/^䯏>sq|:NGDo8]zYbQƖ$؍G#j5ĒhKƮ1QcC`i* EV"X<>ޙ93;j>WTa ]CCZf__V֖&?OΝ1yp:1. Si%Wp5 Y__|D"yRG̡jTQDŽl݊f7Ҳ%A7&&z} V*cbgJbYT3ZΝK=裏ׯNNN\v ]~:t`9:ŌI|i>քp>sM;2~x"""4:v숟JRgԩSٲe ]taȐ!4oޜvŘ1cpwwg4oޜ޽{R^=ضm$ 8q">Ԕ3fPfb{P;wA~]v;I&BNѣ1m4y{g/˗/aÆo$&&j%8۷oϊ+طo2# IDAT,Y2*[o\.׌7 6d۶mԩS,,ԿӥK>3,O? 6DTj*lll4͞=>}0` FժU9q.ժU]vlٲmے122*a_ : BARJ J!l|IGR>݁왿xR~Q#0-)VD"Q#.I>$3-SF %>;!wEq] ֍[GNvVпWM'5%RVGvD"AR?!| Mr+, '4F47A9JMvF"-͑G8Qc(7{EqeVVn+@lX,3ץUVE-JX@CQTZeZ?G5$ qqEqeOFյUXSgED&n %>ENpFu N{JIA@֥KrT٤r*($}cbRT$X^zFF$HOLĮn]<{ZAAl1]CC3uyrW%קlSupF    ֮4`ί_"- OO| u;=yH%9&}}͟Obd$VVToт$>][³W/ 9sh_ zvvfMl9Ҩ(1U_ ҥ odffF ̗W޽ K.DFFr1q133̙3,]ǏӬY3ݻMÆ 7+++UiHPP'Of@4*///ꫯ5j)))899~lll2e aaax{{ybcZZ7o,]aϟOXp!˖-#66ŋ3zh={$>>E3f OHHH`Ĉӯ_?<<<@J?9sEJJiy,YBZۚFɎ;J{̌yqMLMMi߾=fl=z{n;zL&iӦt~tԩ̜9>}`gg/UV%<_ :@̀Ƃ BAq>S`zg:j>L 'ݾFlX,{{H$zMEuxCņ'QW l۾ƥuel^w7%ӈ6}#8A]ѼVBn5ħ ӑxbMOߡVZ csCg OB;sMѼ$ >W'/Nm>ſ @q]ꤾ&.{wm <4}i< ҡK5ztDtuy-궫E$ ~kcMH,,101 -)!Yyo6O=택'aCtt w>]@uٻm䄝 c۷PT87pfC^zQ. -j˪OWq=}OfՃ^zd< qG.W/4og9ϐ -OԐKp7Xy]'g5{l23+$%%=׷o Օժ͔$Nv+$X ][/Ɔ(UV%V¸S'3-kH [RHp5ƒ*gDEE@dd3/u2}t֮]˝;w*-ǏӵkW"""8rNNNKyX|9_~%lI ReP`,TRpۜJNfmZȋ)1VIN>BYYq@ff-15mTZ1MAYVU%EF2NTz͙o>,XѣG"557ow_  A(bĮ% b G2[E$^G1 mߣG%izPKLca˴Dɍ{'b `dIJ~y]5yyc5ռN^`نBihLG ST\9~l)ykX0E[˙mgGfZf;iiO$V{='3R G%3*IV:%>{p(SYz`'oz<9V$ֱM2]D":.;9.Y^(s""?}nP].:+ϴ.RARJB_ߙ{`eff-ErPAxi66֐*Rj*'ebjjZ俔?W+_8qK3f o[fر  ωA(PY(xԞ~9P O:3wBuV2212 'ci[z4006 #5l } Q dfjף5YT-8ΙES6rڥr{*UWa]gBTtzyEh/Iqn W_KD"-x"wRix"Fgs]걫]\LےFU2sԐu?W4\ec%sɱRvrC4EB:KO>?,s Mƴi*;Wš5kXfMe! sAX2^M #J [ w_Ujhe"ӕN4,>@ZR.Oza]=~ϒHZRW"90k>_Ͻtߋ:ۻJTrpAE?+ % nM & TI;/F v#a2{ajeJ*mQr$&z Tf^ ]}uذXLBlX,taٰe(_C',= fIp @(81y/[Z`IQ=C=:}I>`GG~;B\Dيlc5oUn+kcWM[ &AGV$#UTqQMagS (;+%f '7LE5Ύ#XXTƅT JEZuv $cn ErPAxi63 l௸8"22aaak^^z  ³=AMVrB HGFm\<]r Y~.|QT_kgdyţGĆŲm L xc1HW7Avw< ?X3[3ZiOT*ٷpC"`biE\R>}}(Jv+P|-%=zg?z̕W - Z*E|d< /GU3ɸjѺkNlx5 gOL$`WVdk-JfZ&kX5ԐCۗ:ܜ|3E>GFr69aWDkP*3HN %%jdfEO/~UA#HD˗ˢZpgAAAxш B҉Y6&y 7 awR迠OVaG  i4Z$< 16]]r;QM-Z5ru<"3-S+S\?Rť q)G17ǪݼӶNIuKH-m>l壗 9Rڻy1p@:tOCE=_guR0/VTΩ?Oy)xRS167KjgUrettѬM5aϼ /b9>[7Ny8&c`lF u-y@mf}Nqw@fZ&&&ȫqiG+Ymũ&GٚaaOAm3a'KG/alaLvu12a#02gPN5LMESU_<=|6s##PtYINA@իG+cD*IJ[9ReQ]ZSBQ*MNt4N,yAJ YY(u^ݬq10n9(:;c(=   $*00//ʎErel~k"i8.ddR*<7tW#qiT&%[ͽf22d'۽c5_G"0پ< cf\)'))>dիd*U %E 5j0DJ$-*IIIA_ "DiSLM=H^;+++QJGH>DP`inN|||ehF߸Q<3L*I@ GyVCr?( j@H'ӹ>y(%pM)Хs(TO^a/WvϝLڵimnΧ7oR CunS$gfeFj$ܼ&& HK «c{{{Wk X{>]] GAA^/#x ,bV6P>,Z#OA^]_{{N5jyS}FE^@X``FT*s15m$!Cо}cǐJDDDTz#{ݽ{T_boX/XXΟ?ܹscǎ>sX*JAWW\[:㤯5MG"auLL%E{`ccSѨT*^^^lݺR+)F̬AX"A(̨Lb=x5W9WO ZJ )}o1¡&i@, Xr e"*lr2Ry jƍx{{cff M/JŶmhܸ1L2<78p ...rzŶm۴-4N>M׮]R ={رcZ0}tON5_>/&##C݁hժiӦQInݻw3`xٱc|?fСPV-Ν[ٳi&M` Xv-N\͟? `ee9z:mFnݰʊmT8=I^JǎR C !88XT*_e?Ps{agg-Gjѣj Ξ=)ФILLL)qF&tuu.t_9r$իW/6Q3JǎYd +Wdܹw^6l%wgjj СCJdSNe/1 Hu%[⯸81U 2'''r9}^zo aEz_2sӦMhKKKjժӹuR#~/ ARF|nRSa`L#c}fV3nӜ8:o@eP߯r7w\FAƍ9x Gm۶( eҥ̘1_۷3m4癙ԩS۷syڷoϰaôJ>|8m۶ӥKyzJ$,Y½{ذa| &Mb6;vgϞtޝcǎ1x`O_>ݝӠAz衕t7ndԩ,]={{nJYf|FJJ }o߾ </_~?'NШQ#tI&$$0dڴiéS8qfޢ`~ڤ͐!C۷/w۷@TΝ;޽;ӦM?(qs#Gؿ?mۖY<`1/Rn]$ !!!,^)Sp,--K\o;:~W:w}qi 8777"##ҥ 5kߟVZHܹsxK1fzDD&&&/W񁻡vvHmWJ:njJb3c 4l2.K&<\ IDAT^ѣYt)~~~3m4ؽ{7M6eСߟx6n܈JnK$v_c߾}7QyܼyE1c ,X | 8ݻLJ65jԈm۶@lli3C )sMBƍi׮>ӧqqqٳ/W_}E@@-"88]gϞ=:t%Kw߱pB.] @׮]%66 hcuܸq8::jM0#Gr]o]f #G,r[+Yf /_⿒{~O>RړBT*?#L6]v1|b_i7o.SA*Ne "PW,Z @D7ܐ+>8A Tfr $ LfLmNlpɀOH(r9@JŀW NIa: WB`ԩߟ_~E3^zZ풒Xn\r j>eɚ5k͛l߾޽{k-?`3g6l曀%+W ]vw^W4i}ǏAT*M{Y[L>b Q*ڵ3fиq7wKq8z(}LƲebɒ%oPN._̆ h߾=.\ ;;aÆaiiiScccjժl(cj3fЬY3´z|r`ǎׯe~o۶-m۶ռoԨdǎZ7駟z T*1Ze2w\OL&#**}{w3]6W^-]vaggǖ-[hѢZ/TQ?K1.\(u,...FekG7++vőo>ozz:ѤIܴMV^ƌÒ%Kyyyi[gfժUre@3xb8RTsNi߾=23gj]/J~bb"'Oߟz P_Cj׮]um6d2MҩJ*Z^g 1W7oC= K.3^ѠA~:{V(l޼===N<޽{2e zzzbllL&jgddDNX|9;v` ˙3g0a-[\˺u0`fzf4ǎy]zuh޼9ɚym۶ahhXd =A{s){phЫJWigA#94::蘓q G`l\s]=vկ4ggJ:3΋'4XfMx>nܸAzz:z*]F4APl*zj+XVާmllpttܹsi.]hٳgHMMƍZFɝ;w )N(FXe2 6qY߿Iځ:Ҽysĺn:ݻǎ;ؼy&qJNN={ZƆ 4=_ڶmK˖-qqqw%͕+W4g7^^^Z#o|||ؿNjOt[[[LMM|rcؘ7x@ 5jJvC !!!A~ёwym~VJ14wܩI)ղe2%{rpJǏZLͧehh?zI =z(~)r ){ァymee4DB j޼9y!66zI;۷Ls}J{mY3=*sMa7nGvݝ[oi}-FbΝ+{.v‰'2qD7o̙3~ AOO}?~<͛7̌6mڠP(8|pviF$AA(k/+.EQz@~9@Bʹec=0A2C tPT]#9뻒R7rySd'= ӫWԔWT?>Y6Ob"ΟǿA/>|`d'N0|pfϞqqqaʕ9}LV aT\ܞG~~~D+2J%[μ,#$ oߦaÆرȲul´i9sVdbbyx(NqzhJ}L4 [[[>c?~6Meۗ+VеkWV\o]F6m\"j&"":u{H$$''?_&q10n1Qe9$I;F֭[cݺuуzAϞ=oQ7nܠ{MsL~94Z\ Ha۶mܽ{7 B/RTQFŘ1cؘ&MhH$~?%sV^Mn8yd5~yfo̙3OKETPN&Lx{{GYUAA^T"A(k{( 5?&HA::4+ԈJ1J9J%PY*$RFr9RSCHw&33S,,ȨI=OZ[ؤ /^NzzI *3@6֮M*_(XjԨ1[lyN+V7`„ i'Nx.=MMMqwwرcL2ضk֬666LJJJGcv>9+VKݺu5Ccaa/L2kz* v w18tU?6l~广AE" ֲesdރ{: ݭ jciA^4Gs$ž!)_22J"#nFOx2yظ1^ewO~j|tt MOO3f燮.CȈÇ[Z۷g̘1lݺV^͍7J$3w\zG}D~prrڵkkƼ+BRi)Jڵ+| ԪU3g"˵[R*Aٳd?׏'NPZ5Ǝ˼yHIIW^M VZѹsgKn߾M޽INN&88QFѤI9r$ǎc͚5֓ws ‹D$AxE-R'կU"ˋJ!9H*$'ALJerysroRR/_Îz.}ʏ?kʕ̟?;RN͛&wYZZ2k,V\ѣ4hV*0oÆ 0a&MҥKL>Sr>{aРA 裏N:L8Qf˖-|7lڴٳgckkY\.g,]vѨQ#6l@zg_ @ lN;&'?@)V0̼VJJqN] %c'p ϐzJcfCCg'>~/;.!oV74dOx{[x\ʴ BYIR֬YSWՔ)Sغu+7nܨP^x/jeL ï_ךVȈ+(lo^:Vu߸qZjt Eu):uD>}_+;gֲeKԩSbO񲜯AQ`2 1<.6 (49(rR!9 ꓙyXYuog{򹳕AMjhHqgNz:ϟgo1IA^III5~Dvv<ͱz=;o\VVŶҔJ:_Ȭp1nPrrr011Դ'OWHnx6l=zpBWw,-*+rWә8q" 4`޼yٓK.!˹}v_~Bb, 6dڴixzzVv(O֖yxb+;AAx_Ax-=EN@3vJ]YFڅ~Vn?TH$ӧR(?ng޴;cȸ%ryG ݟM>S׫Ǭs(~\)wڵ1= "ɸpBU`4e~x;vC*ȇ;^ l~/*/rt [W^իW999{.b,y^AE  vT*?D<%Sul$@Kk]݊,,*m&&ej/j0 07]{L*3LL{ 99d8~}>!ke +R)yA%FjpAA$J I:DjhjX!Z`r./T'@֭G3gʽ 6 OQ=sFk[؂G;Gȶmpue+gidVzԤ  ";3#G+uu7_,!1*,ĖLT9xbisOA8.F9XQ B2fe5VfTAAx " ?I!t!*n\&4 fv-l>œy1>ռ׹3ϝcs8zywOAgQk=3\]52n9sH~bL%نnތjUZM^j'BJuhLfTadƍZ¹5H͐VTLp Pڒgl] HAA%FAx]O#?p27= qvu=e v 9\[.@.GynGcleE.]W*ծvmy]]1,{ynvD㏩ָ1y747G*gl C"C"_MutV.#"q s~:ҘL+A^">dd|A1AA^ {?ߣvzqT19,<|SyժDi}yC336k__|#G 0a,rk9r$f9x1)?AxI$&;;_>&2Vt/]7XAe/ҫJ 8"Jh  / ᵑEXuTgY_^@2;[kZfjjmMLJQ渾 _0ϏEۣHK+[%0:g^=v8vmCWϻVV5i{ 썏EP8  T.VVZeFu$&QDAA*H ڈZ*|N]Sy.MLM= |YkC4c > } ̹iS|y}R443ɇ2pzƙ+K 7wCC7RKx-59R1 3zBaJ||*    J"|2U^T] dTH$FƺѮoˉxCσKu ]Z$28ثWIKH@MW0,A"zwȹuJ^w{HMeԩ9}G9ucǨѮi-{4qn nD&cKݺLsv.]6E >|X1 30ѡyV(  L$Ax-xB?ӱUR^Zt=Ljded~ܸ= QXRG-4|8s9![Xӯ1.}=~ԨA J\ BEegk۔+wʔ;w 7Œ~e! iK qp^c, &K؟@KH Te϶8U*dCy] IDAT7A*\Np&52*Ս>$ u#c۶m4nGGGLBLLMbb"\N^ضmOz;v Tɓ'ywCȇ~HժUٳ'Z1$''3fqtt/ڵke{333Y '&s8>,^@,X3fh"6mvjj*' ǏӦMΦMr5j(t֭[IOOo߾ 8/j5}ʹWTmۖ+W0{lnJJJ 5a?ׯT*Y|yߟ/ϳn:ׯ~K߾}iѢQQQDEEicU(̛7k׮|r.xqQR'MJ*t?K,B$Ũ\KO&~JNc56C qvv%B\ݝׯ7r-"SSiB;I?^Xbᅅ?k;880auN@@}ѩk=?޽{Ӿ}{SLQFSN֯_ϥKعs'{yyk.x뭷ldd:#GдiS:>NL###*WsFҒիW=!ċKŊ̉о6MYg!Bg%3ڝwt0Ƭ{ 2yƆ FF%B( f~} r)U*:>ͯ% aÆڛDFFrme˖[oꊕ-b۶m9ܹ{OO\e !99ښWq\cvtt$,,ݻ\GɁ]61Nq N:-!ċ̳-c^韟Ng!BdPQnS^r;3ܓTPy=K.@!ֿre6lH#\S`Gdh4%afee s6k 0b4hŋ9y$cǎy֕UmYuT**UĩS ~]p_2Sf͸y&~-111nݚ~QdK]BRI';;]N&%R^韥B!xV2@((bbP%V6X[)6V!]tjbmFga7"#r4obdZji߾=kި-޽Ktt4:_UV-s222wެZ 6~znݺ Ү|`g 8^Eg!B B[Df }>r`ݻd !ljZb !(8'{{)th4$))% M6=z 6pqFͥK,ӸqcF9sx"Go% "##qsscjoҤIlڴ/rq-[իWm۶9sGKZZ@|kmkK8XmJ,B!DndPQ.=J}ǢEW^;v \KOOҥK$滙SNQF ۷oСCy뭷\2k׮y!KffT0/Q8TO?K,BQ @s K;!(2 0]pT.\:M3kSjU4,7eJD.c֗_p6(ܹs)2GG3M.?0P(wscCIWf999#B䪼WΞeKl!-[j !/鯄"3*YEԲ@Aa쎋Ӯ?hTʪ,׏a+ % Zmfy8Pj헴'7𧟘ꫥ%kA bkha.3@Fà~횤BQj|mmu!B!@uwQ=3:hTFî8TdvXѵ+ゃK7g|[.gߟ-GTBfք4jKff?r3 H|C!zbB(uB!GN\f̩C'>|H\Fvkvv% JJ7BҨը~dQ͙Gb267ǼBR FQԔ#>>UԲbbhuii%B-,2q#BB!D0|z!x~$LAȃ;`mU4{GIf95ֶDr,KG=pWO[fq$DFx`.XX<{ɐ!XU @Jb";OFp0xtk4̽kz_}Edh(FF87mJɓזg#wPۛ#G?`C׬iSNYCD<)Uק%pj|Ӯ^-P3\]:s&sa^4Ϟ=k=ٳ=;gPS'^ޝi7{l;[+Wv- ȡ%K{" &&:<Ȫ!CurҶsi~֍؃rp lNp1 ֥ܹXWׯQH/p!_Ǫre DwFp079q#D獥<<|ܼk95p*)'Nӓ%BR ;)ǗvXB!$3Eћ_NV׾McghEė|{<%$߿jQs|*|ǎ_s~NHMR[wlffߵ+׶*fڄ ܽxuァ-#]A˖QLJ?B;@{ ;OAܶVNv:=cc/Z[Nl?_R>Ϗ! L,,ߟ11:޻Ǟ3ߟQ;wRn] QaaX{;v`fk:]_y;ggN[Ɖn۶X98(>-Sqc޸ag^:zB4/˗sdR:v…ܶ ƍsc^:ll҅kh4׹WfغuTtq!phA|DD,^{O;Ν7Eچe)GybEr+y5&<ϯ\AK_)BCoKKkB< !BQdPQ.$'ă; 0q2nM5hlYʙ-[h1r$ z4]VB֮ŹIZnzw5krرٳ'-)[UZ9Q)DYbehf>ob`Xj=3҅B"34N&%rDB!E#)Fw3yx~ {>H4T(TELMqTU۷9AlޜS};NNڰ{׮:C23P5#Fā pjjv9c?&CpY4yzb`d={@Юqԙzj6mJlڔ4FffRnnK^jl]س[Ѹ1&GOO֏Þoy];k5׳=_u>3`t| nfcå}X=lKx wW3#&bb tˮ;ĉTruӴi};ҢkFFuD zww\\ىJI)ӧOťחÇI]CM6ERB%{QE?Gg!B_ @s K;!x&G i'ej|^יM^Ȳ(mѨ-px=>4P:޿WYy:˺kזvB|76//?X͹А9P8"RSFkgf~cܸqiӆYf33+L0fΜY|9&M">>UŋѧON8ȑ#ׯ }:r,X3fP(OeŹ^@ڶmK`` Zc1w\\]]~fիܹ35j`ܹT~gӇM6a^g hhinlYg!BB!s-jeT\'U(ܷζqq(ɼɫZ?W!(:flpwg+S&^ƍ׭QmڴQFigSfՋӧCJxYh9rӢE sIm6VXo޴iSKKKV^ԘXjubŊٓ$r9>?ϏШQ#Yn 脄VX#aaayhaa[o9rvig8;;k`llssss_~)P{BD̫Q%H,B!DaHQ!s+#1ͱ9Q}/{ m{BѰ}m&҆3B!2) f׮ڵQy -s7ΜJʅ۷9q:tޡC!!!lڴ +++WNx!;w>>Yf|ܾ}[{S0663BS>S6רQC{2)Jvؑغu+~~~:;wLPP111m 6|&22RԸqchԨQc;v,K,AL4˗/_@m !^|(B!%wDžϭ1h|VCE'%FBAG; DFǝS!:.j00xz؇NN05sdh49̔;h}$4qIY4OV 6q(5SbԨQߟu_w}|o$dVO2xw=E_Q޽;lذM6ѣzl~d*57|rv[F!/韥B!/ Bc+&9`l{‚ɨ!J>ФY!dP\JH%!(! JvUJⴳ@ ?YXJ ^Qnw~BW5昏 IDATr3%ELBJ'٠MKYo0k,hݺ5s!##CgƷ~Kfӧ Ã˗/qF̙B>w8;;suvء!ܲeKtիWӧy?VRIYƏϽ{?>+c̘1\R[ۛcǎ1i$FERR5jԠcǎV&**Ӿ}{>3mׯk֬okNPŰa/חEi7o3x`/G]~~~̴ixwl?x$''ΣG,رcf͚mۖcjӇPvJDDjժm!JF]33`F?K,BQ @s K;!ȷ㍎ݍ nn{;sL?&:;IUVE)IBQ%nCQE;wJ;JZ͛aal͵L֭_as8::QCr EQsp$1!!})+Ky AYA!DAN98êؼ} ?%"[ T[9G}g/BRN/:n8""3%pE> ̖AG tvLo4@{lbJ"́x*Ud(C :)v}űPe䎓iWLjn+[s[9z(m囮w|{$ѓQ W~z.{{`˙-0gcbh=n?Xwr m{w&oiWe޻w{tiSU;LYf2щL>'i܄!M?φSwkգ+oO?Ѝ~c6oٽ3C ť1=ӳAOzz&X?̝3XXѴfS~sWܝs,;H ՙ^*3e<㷌GPpeB/ċ[JhؐNOwP슋}h(4Pv !Я9'$B!(QH\&CJov0!A;cP-%,*+xll`zcY=d5Xڃ[o`|>1jv+R~].\fk?gŦ혆#WNFkEWoѨz#o73 +Nƃ~ ~+}?43i9Qu !5}|ijJn@pb"K)!/?.CNB!D&2 !+1b2mPnyc2+[X@yKJIbvԨP'i?;F}^7|z3gr><놭q33ugna{ۨPN i<1nsmj6k_<{K{~ սIJKb顥yF̆}{/%0 xsv_ֵɮy;L0ͧ7|#W\_`Xaڲ+TM5z,ARjv (;Lħzވ#d'`jdc+Q*zcFimhc3o\,!!ԶM_vҐZj=S,))>ϘVcO휵+ZTJR Q\ۙ3NL|_*Br2No//\J:L!e\]33g!B!J  !45cprE22} ex"fŚ:`gEK{KWs=>hS٪vpIöSźvpyMgʷα{.eiUS ^՝doxAvSkKNEҾ ÷NZR}ڿ_[,*Qպ*>ֳ[9vWb֨IWTSbRvptEçgn]~PԌTvߩwg/6DӦnܝMXWa=y^152fo3Qx;y셍MBȈ}^^ cSll ؐ9HJӐ{yQVBQv  !B"BF|`'B!8BFXu3kLP}$1Q.TFCSgOX 9;݋ԨPC竚M5ܫ z|D%Fq>v[\r3/߫l ,WkuzPg zUHjF3u]gj^u^=s}OQՃ120338Bo_ oTĩ\`#X?|=Ɗ}&yB  ~[iιQT*چ+.BQU42Ү.B!DIB!sA1&gzQ&$obeUlOu:~`^<.޽șgX5'Ž ??gɡ%\w׏9,k=O6}Ÿgdߥ}-̮KyØ c8qƝ׌,cNEbʶ)ܺK̘VcP`l9q7}a7Gw/W{scsZgձUs~ m:j6(֜Xygs1,w1[lK\㫨hQQaTbg^vn=5o]LWkʪ[DŽtg.귈ZjW|3*W!hIU^aaq}R{~=={2QyOFI:cmlNGGkSk~Xp`NzʶĊ {s3k}Z[yf9c k'>ȄH:1Zz3\̌2לPgp5>v^w؂q7pt#۫7Cі;( 貨 g_RQ8c~} @kȜ> ,otxd޼y4mΝqqqG,-_R$(`}}}>|x1E%ys|*? !Bg4'NۻcB\]tɥ,},itQ LݨWDGUjU4Q,2nJ_'M#6v(BP%nCQE\Ekw\Ϟ%M1>\\XS(JNNNDFFHDDDeݻG*U_޽;֨jRRRX"yzyiPRB 0#'>>h___\\\XlYi"D*H>T66 rs""vEVjQ:t(s'<<ݻSjU<==3g111nŋaoo#$::Z?00Rҥ *Ub֬YzS]p6m`kkKٻw/5kdƌ2 6L۴iƍɉ' X>c4h9jb֬Yt҅ /_R9?Rŋ5j...t֭XBj9̋? !B By׻ZfzHHdUh:.:Bq37'LJNqѣ\g&.#? Ē?رc^9ݺu  IMM~`?O=^RѺukj׮ˉoG:tH:t(?&&&T9*UbժU3~xbccswdP,X3P(|Ʋx\c m۶ҪUkٳgܹsquuڛݫWsԨQsPB'OO>lڴ ss|)(;!韥B!(, BybjJWQo*aj_13òhBbr21᰷7>MHRAB =^ e[[ukkk/HZwww͛VNj׮͛7QF7Zׯ7nݢzڲgĉׯ_שٳ\v;wRN &믿HHH`Ŋ8::?1F#Gh׮mڴYccc_ Ԟlfl\?K,BQXbTQy?jh$ԏ7T(xƦXB!򣢑{yѮBr[iPM:<ݴ HZM@@S_~=͛7יEj?t>F֭[^3u6l Mdd$oƍFFZvcǎeɒ%4hЀI&q|BZ M'B!YJd!ãkHH͹l}/V812Ti4źFA8uawŠ!(8u*ThdFui00ௗ_f ~2* Cˋ%dPtԉ9sؗrn |~8z'RRԦNڵ+CYfܼy?ѣGSvtrbVZ8::Ql!DaIUm_X*F; >;%KԯOo{B>ڵ_۷өSG~0ݺA~H,EꟅ< AYA!Dn=A#=~r4]חuCM6F$@J%7o,vEё^pׯ_GTT.E===1U*ѷ®h{ݹS=V^|2 .dܸqԭ[:vhBeeLg!B#B2K{z g[yZ)/qlْ(J]Q랚ʰaJ///Á,RɊ+ UGր`ր{5y:Ȁ֖ bchw2Sx_"#K2"K/aeekРAE^||<#GA3#GT!!DaYB!xqB2I~=@H̃v01)DN͓+ggddDʕK]Q {j5jCCݷ"JĄ#GrС<ΝK~x׮]# v1|p.\X8_Q*"%G}|h{wRSȥܘpRj>^D+*{!#CYZZy{GfE^B%20@(B!ċCBIN=@0*L1ă[[YNWqPrex RSS5 7n'''&Nȝly뭷Y&ݛ7|Jyau놽=z"00P'5k2}tON:ux嗙?>))):vE˖-cǎYF'x2h [oRJ:t͛u{Æ rʸ1{lѣG111a͚5'OԔ˗Ws̡ATX!C봱qFw+VuDlJ%ϟ]v3tPN>>޽ӺukCCCCY`3f`ѢE0m4TׯO@@ǏM6 >M6hoĈnݚ]vaaaA׮]IKKW(/ܺuUV1f e6oL^ѣ 2ӧ3s"6~!kfΝ4hЀ={ }޽S`/nݪ3X6m̙3y|2IIIۗ~1d>.\رc9p 6k׮A¸8ʫulnEufС׏[r]뗣̔)SP*lٲ=z0m4֮]Ժg]sNhݺ5:mݽ{q;pQ(:u3qDN>Sl߾=kfҥ:m,Z.]PZ]z뭷Uȓ"""ڵ+ue۶mlْaÆ锹v9K1**J͛75WWveAQxUMLۛya|S^BQXB!Q ! jE~߫OD/miiiL:')+ptt ,,Y &h_׭[pӧN] O?`֬YZ ڷodV~2OoޝtL6Y%BH""D%^R-]-jKjQUjTAĒ,Gdn&3Iy^}5=<$f9yL<^x'вeKEa̜9\ @^1c-Z`ɒ%ό=9s&< ZdeW^g4nF… 8< ,`ƍ<r!VZEnؿ?:QFe\$...4mgg;:7&L0>fΜIv8v76Ӹqc/^ @6m8s k׮eȐ!%<{tt4Ǒ믬]1csrrx޽qLQ|PPmsر̝;3fh8 !*Y!+I !*iۻ[Wii&O۸[^VRRRaРA%iLDEEqy.\`Z|9[neǎ\rNپbbb?שSvmLT*gMTT,++>C>c:CBBy&,+F!<<]v1zhvťKI;(Hovb]r%M6޽ۘKLLD3p@t:,[h:vH`` {f 8/ԦMDEE{nasлwo&NN3ھCAR{űk.cƍ& BtbCppIr1b?#?0K.ߟ>}X=`0Pt[^zj[ܢ4ٹ Z`HR^Zs188uBZ$ !Dy+^A_j4P B~x}BJBJG'Xٸ=~<8ɉ`ZQuds 1zhfϞ dٲes/$h4&sNaѴi,VgqR+ԻXq*'N`|kҰaCϩRHHH`L>_|.P0o<>z-&O< &y{{[LVM>===|Ν;[]R3g -BRnZT[j57gxr2_]l5IɅ  , AS{Λo믿NFFF,+ p)?}=O{{KBƛoi0*F橧/[9998:$!(,Uz.@F動5;״_E.۩Ttիiɒ%t҅I&*d27777nLll,SN-q~k׮u_5LKKc׮]DMYcr O>$&LݝzVZ@DDj]vѣG㋈ ""7|@>s^r8;믿[lڴ|AKn,]c?44R2|gر#~):111̚5dlӦMf빹c2v-FNbef8k4,-k0Y3Jx]4hPBII|sP6ߔrœF]@.H!BTI !*= ݕJVQ+ *Y{QfΜɴiӰgȑ8;;eƎK[Jӭ[7ƍw}G`` ˗/'%%LK)6seƍsޕu_CQ>j5111?AѴiSf͚( 6 ???fϞFa۶m 28ׯτ x`РAٱw^.]Ĕ)SXn'OsθDž JL޽aÆӦMr=O?رc5j}?#˗/7.2e {a˖-%N{׮]Yz5|Weg۶miٲ%/111襤$IMM%??DE1& ݼyT8uk׮eŌ3Ƭ5oQ111< < &￳fwƍիx{{KQQ"J&MpQJ9er  ^*DZu_IHRC;LP`/B!D+BJ'}yHD{+q*i_~O?={УGzI\\mJxAc= /ɓ4h*YfmW~JZx~ؼy3'OM6̘1۔Nxٳ'ӧOgرӇۊuΜ9|7١R/8qӦM33k,~WvJϞ=ύyyya|Aڶm?… KLeggsQj;9Wּw}oaj555'N;9om۶GeҤIЯ_?uJ۶4^}zg7$?bŤIVػwz/"nݚ^zT6o… Kܿ?ׯ>^*<9XM^jM>$9(4oٳm Vu%7?z]o<66!Q)<닳FdUz`Oz:wVː$B6iBP Kj':GI !B $ B!D;{v*ZEA(:;WTx lBwZ͐$b1I?3Sa*LQF$'s]5&14u𤏏-C0(+ B!(OiSQ2Oj48wMBf&"_CŨBڵYJeCHo>߯%Yx_7PZܲ%j$ቜ:s./^'YYtɘ|4 tB!@mNQi˱8f}N dǵdn%! -ss:9"LQE :x}ą H'[Xre[kϧFc됄D5jW_套^u(B!$FNs3UI{Q!lGBzYP\ˣc|<""{a+Ǐs4'N5h`Ӹ R̹N`yvv!85%Ix:ԓ'Mq*NNdx횭CB!I !*tif"`Uz/M6xxxPNO^+OVgԩ\,r7͛7:t(3h {Kll,j?j׮5%003f0c iѢ'77dM6ѩS'ݻ7_}I̷͛7(iժs19E:u*AAA3o<ᄆd Uhݺ5 8 6˷nӓvڑpG\#00ӧ ĉKMj.]?O@@:t>3I~iZ&N 6W_5;_'OdРA_LYbjн{wj֬ɷ~klWөS'\]]ٽ{7N*qGEVg<BV{n=o<y7o^bՕ:uРA5k?36m2-YgڷoO͚5ҥ &8}43g4V*9s}Ν;ٹs'/GaÆmۖI&裏SEL$j0MKBG+Eo0XRk H{ww&D5@OOO!Bq/IPQ)Xj/ luYYƹU*ZTܹsyiժ+[n%::$'|̙3Yhk֬ayyyfouƨQ̞oDGGi&\\\ׯ* pYVZ}:ׯgf?>iii| <^z#Fc.\s=رcML~!&Mbȑl߾>}3j9l®]x嗱+rԩSj> 9*sIKƎҥK3gN6m]0rHO9sl2{9 =Mϲڵ+?qqq4oޜ}g:t(<GT*._ĉy9pdff`zŋMh"i۶mҥ ݻw7wP\^^=zՕ~AOoRLMM(HL߹s,>wpp0|d0_T kAk77I:$ +9ul oSrueJ6KuzB!Ľ$=bBN Bf231ܺ](Va~~>o?8~q<,,d4V\III&L>>>L2I&=z5kd_O< PZj={ p^^^,[ vڱ~z~'q&O /ĉhٲ%0}[&L`\aÆо}{q/R hժgݺu\r;;;:vŋٴihZx ̙3<@͹|2Vbȑ?g2rHBCC$4mڔ5kVP'''V^ @HOOgڵL<}lْsŊ+駟ر#?>,k֬daaa&͛7?d͚5id^xQF+BNNݻw7׬Y}><̛7OOOrrryK;wYf|Von:\\\֭޸XXK^zƟDPvm̙;_ڵkG>|8/_^c'w;;~ Wb"{32[Wkt޷QQ4Q&q{t:s$AVoBTAk3 ׀ү?Aڥ4T*NnNxzȇ/㭝_p#ٽ\MLjͅBQIR:b>בIC]$dfRx\B'+rrr4hPEFFP9<.\0-_C-bƍf1\NM2T*gMTTv RRRM*ƌɓ'fiݻ'Ҿ}{<<<ܹ3lٲ$zq˖-ٳI_DD*ѣdff/ӬYضm7n 00CҺuk˒%K.Ϗ$~ 9*^zlӻwoåKʼo(H/\޽{yLyLZz>c}Qqss㧟~wХK3&&///cu~N3+oǤuѡC\ܘлw2?((4K]ƍӧY~=:ub޼y矗9!N* Jȱ|#zOJ2rNF4䟯.j uF2gdv3WiɸSZ meQUjw D!oIR9n~j{|.VV ²sss3y\$(k..5P IDAT.ѣG3{lN`` ˖-w1ۗ{4ٜu%SX4m4 b__2%=c=Fhh(&M"$$<ڴicR*>V tj\}ӦMoO>aҤI/o$e]YOy[V:wa}~嗼꫼7fΜi6? V6kZ}j4~i,Y„ Xx1=%i֬*suu-5!99ؒZ zW^̚5~o3t;K V+ _pGTNNST?\Wv*y KBS`V״Zu:zjgȼQ=?[qHGk~% G.ppAT;=<(BMBJ!b٘S)99&14|+  WO>w%KХK&Mdhܸ1L:uccc9v7oy.]zW,L愄Ž;>|x3m4MFNXxq%K(I$MhӦ uֽڵ6olRdرcƖ8/ciʺϧ~,^?ӤnE#66?:111,_,yڴizOohYO?$DY& {&&o!IS.$ -7nH(yeӦbٳL>q E+nڹ)^~^xyEX]˶xu3[p ||:+AWjŨ%Gvauzj1Mnf.;su#m -S!B{HBJAwü)ȃY-IC+$AV9s&ӦMޞ#G̖-[;v,58Sn7n},_2U3_,̝;SO1d8|07ndŷֺuk\\\xw3f XmoSLɉSfM8@||<.;w~wx{{yуٳgӿ;x ,Cnn.f„ ,]W_}ո|,Xz3g܈>@әЭ[7,Y? , ''*ʺO___ /@TTZ*uǎ#333g fILLDQ4h`OQ222HMM%77ӧOyf>#c2`q=zёGy)Sp!-Zd^9|0ԪU JE]۰a_|C I&c.\H>}$9(H$>KIBb^^A%ad$%IX-0qcʼBTgK8 $ 5*kF5#|74jӈ hвYܺl+2-:˪bФV[ޒwހCJͺ5I1ɺeO~N>Ӵi?_g9 !BK B؜!׀g qu,T*tJE n7X^^~e裏Xj...tԉqֿ{8r'OFѷo_f͚e²1kׯ7o??8yyy4jԈGy6)-6www/_'|B׮]dժUYݦX=2w\>sT*!!! 6 5k_'Iv6m׿V%%%2G?< 2QL*I]FJJJ'Mĵkט?> .d?d>su~i\\\2d!!!ƹK;Ke'1cXv-cƌ)p۶m珌DR|rYR1w\Ν+'((/lټy3&Lhт>֭[q%z*'O$ L`MXX^^^k={8G9s+vvl)!I.VFEh}ZT /=ʕ|crPRѽfM f!DejdZPTk-{%Ӣ81aRӅ<4d4oίwgEaӂMf B7Q*-@uy'q,l{ɺZ&fR M7!'#>JȆ{RBQTw^l>{&e6I0~Z&~v Ps7(????Ν;wϟ_?6lȑ#y7lM,^W_}sI+QVi:=ؗiH|%IXep 2shHn? QE˗yq.IRoAx8t}`-B6u3=zE-beϖ<2MmwbAӣR<|'BT&BwlBA!xI 9 :UUVV _[""puEca8G}wr~>O9BO4`_Fd.JO;;4L.jsB]yۗxjSt~3n܌7%mS/Nxx.-a Ai& qB!*i1*9X9[d$ *^vǏ߾QU}͊2n8֬YC߾}0aQ$왐@JBp:7. l{|Ea#tƛ4@ZxN[&Ds%?'OEԷ>iT* ~~LoؐtַhZ_fZ0N?o^/~ ̶x| |wQv[-7R9udWa9]cPUDFcadC}|~#O#.q.{˃?_3Ӄ!/Y 8m򸩳\ЪDlBrRΎ""螐~+'rrNH .2o{ݢr8˸1X" ^!()9915`z{|wЙg8seΎt~x &cjzku\<]6w_XK7,FE'3)%cy#$oO6\ !U$6w|";rsS)  DB!*LM;;$G޷?"#%IJɠ( KN&b 0NԮm&Nұcvo1v**`r@pXUh?=>9pKideRpN@Xtlaf撰)nPaJPn,LYJT* 'u^$.EQdz}oZ˕: .ځ>"BTE B\9 B/.5V/xB UZPΦkB""$a3yZ]PBTz=o>{gR>A//>lܘ5l:5G&hvn`;~a~a[yz6FvBQ9 B;k^AhmA(;Www myB WΎi₥=Enh-/,lHv673+6SBEQ%ϜA(/-[E I !BJKBeܭ8(RARA(Bl $,z$&rSg.=DR"EzxV-&DA}x"9Z- ZPy'(meRrq%'B%-F6Ov|^QgZqX_*B{ޞ#"薐,Fgf+!""p433Հ#7jd˰nhL>q%/VS7Nm_n"*X]:B!D5 B345,%| bT! %ṲWb"^ٗ'ME37k$n(3EQewbŋ(`NHVJrP!BT) BT*^I\B!v%$ fdw~r 7k0XRɘGך5mUɜϐ$t)h'i&kݚNS!B& B!)Zra̽P1nݺ: !wޞRqii!$8y99*' [AA K*Ck034۽nS1n]o__4]D!BTQ B؜Ag~Pltn.vE7trDWlI!GiIB5>A1)H9{ 8k,߄%]D7O OQSp$ɉY޴)mB!wEBә9[nZNAKV}j NŋܼyC; G߉EV}v|AjժEILL͛<ԫWg7n7ߟW_}ÇW1eڶm3+?+Wm6j5j+WȌ36mM4e˖BQ$lf%I__s))($PÒM詀wwUXBT :SRϑl o5Jjӆ6R!B! B!)z sְt"'] WEILLO>a̙,Z5k0}tc]b_~%o&|70{l}Q:vHjj*XU*1'O>xB!*/{{TŖ{ E&8!I ֙3k-\m(r]Xx  Eߺ5!B!\#a BP*Xr%~~~$%%1o1c#иqcÉ(u;JeL* !Z|Ixh-T v$vv/~(^Q~IuG&D%S?ϔ'LD&M_oFB!$A(= |.2V B777ǚ[ c̞=ÇȲexwJW~,[͎;PFw{hfڷoϙ3gXnk׮%::>}7)G!?h,UVt'MVG0n{_us4>Vdfd$iҥKtW 4Ayfz͜9s_>dggߓ8B[!A}-3a4Ԯ}Nډ&c`YӦʜiBe~?O W|^=iԈvLb0=7nB+s0YBQUIPas*;δ2Pc^kpXj8Nn7n},_awjȐ!|G=~z7oӤI `qӣGfϞM|[&44 /_NXX{,]]vѨQ#qpp(cBa{-]]Nׄ IIIllтy|ՁAQu0"1z9hիIIR~> IAаF /$η9vEqrr-;j;:6knpªl0ȨBTYv8'JX Xtȑ#L<NG߾}5kcǎMIcEj~7fΜɢE:u*jբ]v%jIII!==C4QF x N:E:u޽;+V0.ٳ'#F`С;v+V0l0tBQںK˖dD_uxFl,%&rr„{F!GCq8k-F6( B!#kYfXu:g60|ɅiEM^qD7n% /z?n 숊Vh=J^oLکTqscMRyQV>Gh IDAT_Ś[חYAAW@2flΥf`oTam#Gjp7m\z:23yG.dd0vvfxDk5kѵ+ O^̌m8|3Knڵl;}l|q64&z44 GcJ1>K{Nk;ۛ7n0V'%qL1%,x!.fd򯿢S;w&&$_~!ּ5M>۸6m73lfoo'N#(x+ֹcymVn沢|8r*"skj^VFZn.RSE^hɁ˗s':e9_Rf&?u]]O![,r4 BC䱥VcRA(BT]ԯONӧ͖c99JLd[Dnp*\b6MVBeP^\h YMin+Nvvk4qq)u}ŀةt_%|?_~[ IIl:~ܘe /kĎhモ(L-1AF"geq56_H W~GFFnEK|!G^%VU(|3hϞ姣Gڹ31j\ެQ{{4jzIk/1׹sLؑXq3gؘG1V7<}{ == ҥN@V>ZeB!D-9;"]iv*u*4.!BT遁d9wl؟Ck˖ИPtI}䈱J !3ijFPVtPT dRثwp]s^.vv`Oʵk|_|{qEQHHM%n]Vrm&R]ceb"Ϝ!%tZG$zQux xuk5ײ̢{W%_y3+פ ###4!5pd3$_V"tˉ h6e=_4BQ HPasv/E*5\, w !UJFY|"Jz`gZbmXǏsM5&5@3U]"Du3i9{֘0*Ã!!g|#RTRRja֥ C[>/xMׯDr2zx ^HY g͚I/v{3=Irw !6"Kz89ՖpstNiS9~vhxeKbOjvII;:uqh{qgrȶ'n'y[7Q.˻ y:]XT*ƴjŒxK:uCKYc[סCy}{>/z>lqS8v:P1В<_B!䛤;8dEIADETPp,s+\0\Rjmf[23wBEQdgf~&y=Oͽ{﹗qsA>t)\LLJ#4!BBt+XcbU =RjOһ6J`#MCΝǙ};^:[[Sښ""|7w}Q*DEq=R pqjJ r-%E/Y8qc߾W2ǧdf2?[8:֮ @k'':ש{۷33,׮EI?+,:p=qBQ1HPQLjOi3hk钐kBBT !yxƦ)Z`yRϜys1#CZT 81ť,j$%Q~r@fhȟ KՕAΙ?0oTP.p-Kԭ˶u4]Ҳȸ[%>[1޽4[|Wӧ98d/yUUu߿Obf[T(X޵+\wG0v-,g,?ryyszZfϝ˜ѭ355ww Jxyy- Jno`R&oF{WW|}ucE`x8>/\wȩd[7]o ~auE^/!BT< @{a:!s*=!N-o {rݗ*3WW&99frtt$11.^H "$$']4hϟg׮]O 2!zWBTQQ{Wfnsjbs2ށwiZcml$&! 7tT$;: f*U4UV $4qbYSIHڵ_o;w.p[Rv-PxlAs"%^rvB!*H B=Wf\o۹zP6yڜTt%ƍYpaYb>Zx16lxR ???: !xTl9}?V߯(zeh4 Ц$3q?x?s%>>|&E~j l@s!sgRR:"+W+pB!xJ !#PU ƦbgMZMFVb6WYJkk'OhNO!ēaa`V//^c,Xc0`]ve$%%ע46miӦ vk׮l2EEEtRfΜɷ~Khh(3f(򺆅%/$<< #k֬e˖Gwn-[m;}tJ%6l`%:B74dgFVݨhE\ZZYhZ> n1VL; L',Ca3=ʥ |I&8:rIqB!B<*7Bg w[ 60rH]5_hh(zcON= ;;;:vȷ~1gҤIj ȩ|`Ϟ=lڴ+W[o7o\Zsss֬YИܹիS+W{$''caa9UVͷmN:W_ФIRSS駟6lV\'OdѢEEhff;=O@@ұf͚uVVVbbbR๹dɒO!D46f!U&wݽ倏eW R:uͼB<㍓'IME-E'^!BT !fe^ K.qa:t蠷CzH6l؀W^!%% ɓ'3ydZh'|¥Kt=zCC"^* ^ybKBNRR͛=@ff&;:u[ꫯεkt7nKx{{wy5mڔ'OҤIb0n8-['ӦM#..vرDBgÆ HErm+ٌ%*]\pn3@2|tj*=Tf$9(B!D)x߰ !If 'ogjts( UFcW F!00'mȑO'0{lƌSʂIݿcuSҷ[n$&&/a5j_|C-BdhmͺyqeRSRxqPYqz,u"g1wG<ΦVt4-3LT*խK6}ZWEFU=h1Q! t҅?~[|ӦMRJǎQ2` @pp0Ǐ?KKKRSS,6!\.CcbSn&)VիW{o.]KuTBhZ'%166<_(fUz8Jm`3`~FY"LU! $A((r^}뭲lYWk̝; |}}?>zUt9-ZW^׏ ?> ɓ'ӳgOj֬ɹsؼy.a׺uk:w{ٳgիwȑ#9(*P[geeŀ>}:o棏>]vJ-GCBB֭z zM=z4dÆ ̝;0`ViӦntԉڵks֭[gDEE5'ς!ժq%3iW`dj~pA2$&%9IAȩ *ȄxܹszLGT*133ݝ_~UVv{B!J+B6'Zg ldRSXO!<0R*A$Tkt=v#ɥ[qmy_]#Wre^6Qk4_\Gtioωfhkm]!RuVuFZ9233>̟?___6l_~%mڴ!+׍lJUR) G_/_ܹs̚5H>nJ```CRR111,_\7A!ABr (̺AK~`ΝeBQ(KxyhdA/H@FKQQ?Sx??|ɲ۷߿?XZZҳgO֯_Ov9;\}ѵkWTB= ӍiРϟg̙j .p9J%RDRq]Yj^7nؘƍyש]6;w믿СCzcMLLZ*nݚ{_߂B!4I ! BI厱jŨB!Q#T|jZM@TQ% g.CQAN+\\2,!M4$.-Mo>P?++N6kF 0hEԪU+*U+<22FCbb"ϟ`ǎTZ δiӈ`9r]9p@OZ+W2p@iڴ)˗j*N:/[,\$ƌZח;wœ9s8vzsFQz|m,;vX>^}U{yy,++Bpppɓt֭1y}ƨ߀Ry~+|zrWz>hyD_ƍӬY3]ӧ6lXW 8|Jhd{!IPQشן^xG*;i1*BBGin1$Orx*߹C&l oԑ/JEv9#WcӤk4d!< O?ќ?{@@@;w\z#Gȑ#ٵks=ݘ={hX~=/AI5hЀ(MVh د.] gkrxxx?SӺuk 000Grrݻw3d<== ȑ#7N񀥥cJDsȑZL %I޽.wWh]q)B'OBrŨJ3&4RA(BR%kN rib*mWޫQbMc]Aݼ[޸1bJVVV̚5 kkkiڴ)))),\5k}ШQ#Ο?/ieaaA !44FC޽!z?ZXgϞ߿?GҥKz/틑QjG9gҾ}{ݲf͚7_ջ dٲemۖI&Ѿ}{ؽ{#(biiIjjCYXXCsݻ7ZoP}ҵkWN:Exx^؇eڵ%P!O{B;VgJP!15k X|b)Fcj|}:{)4V___ҥ 6mbڵߟ-Z(qwwgƍ%ڮnݺ|g,Y &pbbb SN 6L7G >=y$sL4ǏUQk׎/p!FӧUiw?vԩS\~}i&Ξ=uMLL8p &MƆ^{'''/siP(HJJ"))8SSSr ݻ9sЪU+իǧ~sB!# B!DKQ!B ggUo>ލcիw%֢:u0Od[;x\r*p +볢^=,2Zk%`YB`大w}Ǽy k׮;vL(9ˋٳgӰaCz-Ο?_d{IRIXXVVV 4'Ihh޸ª Z^eeeeqi޽[丂70N>M޽W׏oFUV|w_]ʬY{7;v,SLgϞ( >|P:t(j{{{ 7qDT*>|8YYYKA>޽{9<^^^T^]_?SZ5wY"h24C{7BoёDX*ŋzj:ļyu!!!oub Fy˹spuu%,,m{;???\]]btBx+!j'N@`/Z[?8ZdJ.Aht\6|j=˂Do +ݩ&_?Pߟm۶}v:DzuV:uDll,eJ!*HV?!D4RbR˄F?ࣸq&L`ɒ%oXZZh3fLYV ''')v7n|]e~ᇲE!SR(Ãw^F˱cj7ǒDj`T(+ɼy$q)*rYz~?.ēRXf K,!<<OO o#--3rHI !i1*(*wX0ڱcj!C`oo fffT\LJ%UVR ۺZ[[caa"+=eB'X䏆 q75wV!*YYOL={VorvS9%j@Ç9T 8ڴ)88TOB`ь3ۘ={67Zj|GeB!J$Rw=Vi!Qq_ o*+WKҭ[7Uڵk>vLL :uJ*888зo_ܟK ,, Rɞ={ܹ3vvv̝;sΡT* ׍=uڵښ-[}v9snzޮ];֯_L:˗/J?OOOLMMUss΄R;?RIpp0#GŅ]>B>+yyykt;FSh=..NoJĄOXBt|||prrbݺư7-[гgbWV˝;w aΜ9;v_=ӪU+O _NHMMeՌ5I&qu;f E;hXt)3go%443feeq|嗄pBq%5khٲ%}!))$Zlv(J6lŋKt\![5##{yaR ݽːZmA?]n˵kzM5@pݺ*({7t˔{7f+*B!3bB<Ӭ[[s㯛*# aPKKKb%O_^vڸq><<ׯKoooQnoԩSuϝ;Ǐ_Es&UR;><ܹʕ+qppɓ,ZmpwwǴw߿ڵk@͚5u묬044Ĥ߃;K,)BTuMMaCIjWPĄLZ-bcQ.AިZ?eLͥKC%{VwubQ !B! ' !ʭ@454GCƥk^H"##ԯ_FChhC_n-[%5jD>,SF ]r_q4nX&11K.MӦM9y$M4ys7n˖-ӓiӦWmBQf{g;\UKD%^ JԪqS;dO*ŵk?I !BwBrU8 .q¿{h4^yϟo]_a E[NBJ3Tvvv/nݺ/† bԨQ|ݶ8@!Dޞt>/pSp26#fVSϞ[f8;S7LI9[ڠ-wB!B<ۤPQn) l\-|*ބQzu\]]~ ٳ'ڵ9rK.ѻwҥK.\@llnYxx8寪Ԕaϟϒ%KrڽqB!4''H˕D)㹧vP 02b9q( \aΦɵښ6B!I !ʵAR {nnYFϘ?RI.]Xf gΜa׮]?X3}}}K.lڴkҿZh7`q4hԩS155ի$jŮZ,qww'""Dۍ=͛7޽{Yn~~~OQ!DP([PYj^fVV{=^ZT,]#|/]aXL Iht-E5]JeB!4ɧS!Djm {ZCvo|8]ѣG~2B %?կ_ʕBIHRf&g5UHbF/9ZrJ%kcI:23aUyݸq &os%O)goK/vvvߗuX(JV\u$%%sP*Ey!B.4BQngkx32HOŤ#+_mFvvv̟B¨Q5j߯BQtc \?)imBz:?JRBz&}NZ{.~m3 O<ZmWرZ͐!C'`,]iӦ1{lٱcƍcӦM_LTUV}qcjdggSI6 !RA((2r?nӥT9䄫k?}B!DutdCVjǎq&-Mo3gPzj0z׮];fDD۷Ύ֭[l2uܺu+͚5ښW k׮],\ٳg퍇cƌ!$$ 6̙3ef||<={[[[_͊+P*DGG@*U4hGэyph`` JJ~:kR+qqq(Jvޭw'NT*9x`>m۶Qzu|M[l_[[[:t˗y^}U^xywػwo\cǎ5֭+rB!BBr+#W!N~̫B! xU 9;@۬v<׮M%OL^b V-9U"77~@=6nH=x c駟h",X@RRIIIϴlْ>}薵lٲdee۷oL0ϳ۳c;vPJe;w$ |||prrbݺư7-[гgOCѦMڴiÖ-[;w&"""AѧO~w^J>}tXp!III$\nvvvd_hh(IIIDDDF Ef }˖-|gL0ǏӪU+_~W_}5k~zz놓L޽ٹs'r%^_ȑ#O!y"V֗aR0ʵP 1cha˗u8!DyTCB© ~_֑HIKst;~-s4  ^e[\|x,;v,.4~!y$ B!D7An St7~Al ,K%FCbbb_!JT*6{zadf {Gr"5Uo;V7RU<_Z-sr,S Z[U ,>QRRR8}4 ,KiZ8z(5z"rvv4iuF~t ͛cjjʎ;pttȈ#F0m4bccٹs'ժUnݺ@Nkɨ|Q4 7HIMMe˖-^~8::믿 lڴ)'O|kJ:'Zfҥ޽;wə3g%vooo<ӵkWlmmYr%.?3ټ[n3p@VZ#:u_~m{qYֳyݹs`v́HMM%##M6 ,ر#\ !& B!DÖHV~?P GcGw߬Yh4ihZ4hZT*cװNaaaY: !*jFFläi4}I@stN +Q([YY 8u?n-SZtz͚pvF6٣su VP aO,]I&_ѢE *UD۶mپ};NNNcllL֭پ};;vU>W^ac{[X`8k(<<1ȩP X=t PมC̾}XhCP(ر#;vdҤIԭ[ݻw/ƺutw}Lj# r= $BsΝ;G^ظqRmСzf&&&L<5ko1`ԩSh{QiNDDD睗.SLa֬YDFFr +1c{֍W #880tPn߾צ]vlٲ͛7gKZZC[ߟM6qY_^ʕ+IHH֭[uիWK.;ooo 2g֮]ˉ'8vK.jժ>soz-vItt4$$$u c˖- SS"I^/!y$ B!DQzF߶+$4YB ܻwCê(Fdd\+ڍ !WT'B+?k H<XZ0U>oojRu%m BK.l۶xKӦM 1v tBժU<[nxyytRON^tcWWW|||Օ5j:::Iʕyiذ!Æ #55 0""4FIF0aUjUfϞͼy߿?666EOjj*+Yntܙ 2vX>Sf̘7G|',^7|ի3j(ѣѣCߦN:@ZN8J7nGq%Nfffsfeeżyhժ;v]V77e߉{iӆ 6`x?f͚ѻwo&MD׮]ҥ^yOz !#=|nBc!(/\[z֫Ǜs/ͷ96'eTn̫;u&;9!F#{7:jb!,P<'RRxqtAPI仺uKlpttiwŲB "$$2޽{tڕ .sN"ŋu8+!"##PQ~ebTecwJ_\x'31%|  d}KBgHV\ΦI4LvՒ8PY=u[t|^qh7 09+ *TD,cPz:gfJք:{{2))pi|Cڹ2;Vщ+Iqq&p!|}{ !oY,8zM@`fx81GVWz WOY,%@EرcСwٖϘ1hX`gΜۇF>`„ wuF߾}/8p ƍ7ޠ[njՊSRRRk5+ͪ~w F˖->|87nѣ 6`Yl۷og|r =z4:uߟ#FrJf3 <p4{sWM7IJe,!Osa~wFAdd$]wK.妛nGԩS@U5[f.[`9s&ۦu~ٳ'b ƏT4-Fx c[o֬Y̘1H2e =j ;;Gy]KYtmyyy9/={ח;2}ts? 3kܹՋ:)B$nK8{nj3/ҢE +˸q8u}7ts999mE/~->3fo駟xY~=vmLܯ=ݻy1k,ymvG͝wIZZh4ңGVZErr2 b񶶘{fʕ$%%ϪU/>|8wy'7n/ 3gW^3c^tMg} qX>j(N<ڵkO͖L>$͛GZZÆ sfڵ|̝;{aÆ1gwޱ*߫O?4aaaL„ 8x wL0s>}:999,^L̙CP#GG1j(RRR/h߾m_"##_|222 !!~z~a2RRRš5keРAhZfϞQroK.eӦM 2~֭[ǟ'vJ/?s=gw׮]ٻw/VbȑhZ[ҩe˖vQM?3<@LL /UԮ];suٻw/n ^ϮcΧzfDD111tؑ={Э[e)&>sz= _5?n`ժU7o̔)S0`PYYdxx~lfwxx8:tR^/rJk_!pRA(p[z.A=B.s./@o4 O*os֢B ʓQ6\ reΛu<۾=5MF##23mm(EӴ~.(]jobb$9(ꕷ7O>$Çg̙3ǡ} xLjߟx'֜ Ee4QTNFSyCGk>( Bfɓl-*{ܙ?S9XP}.5=}>))6m-ד O4S/2[oeŊo2e>3c0jN,X .U͊-Vk6rm}?|@ZZO,fbΝl޼ /׿ضmss8NK!p' Bp BEQko5zBjb*Vӥ[4/Fc.)th*?3[WZ !D3tl&nM_/ۛNʰ;8rJ Z'L&Zŧr뭈>nB___=Ctt4/2ƍ ""{e-ZtAI5g(b7( ~;C ᡇb֬Yx㍼曵nlߵKҥK}Qf̘ɓiӦ AQ8{/?8ӦMcĈ(ٳh4lܸpNn6{8}G>}9s&aaauƑOvv]%88m۶1j(^{5z_ڵkٺuzʻK||7VBE1oG9kU}ѡ[U#L޻aCQ˯{u|I֎n޶ zj4|ѳ'C[pex ȑ#K͆2h OYYYDEE{#4oСC5j .tu8mу>ա T~_ !p.55U*f\AX7ʗΟ/P85ZwBrF8|.9 k p۶|4ɬdHVw:y))lIH䠸b4f+uֱn:ҥK7nw}r|w5`6nHvHOOwu8ĉ,X *@!B4<W ٖʥ.gs`eG/jwMKyyimQ?&Q֢BLRs^-:thc+¼.],)asa]+10\~0εzh^hlJ_|ҭ[7x>&&&X^f]eKllø,[&""矷EB!$nKW& [HQz'tX5! ]sZb)wTU#X1Vxx8B! a[q1KzzPީA:]ĠhW/z''UUw/~~EbZyh;֭[7[o< PE xO϶ }C?!h?}Μ%mk;j^~~|E,1{23Zuhd@j* lsTjOULAB!M$nia=Axw,vmgH2(, h0EU1!͋Lٿm1 LJ[vUXjՊ¨yUb~,(`.ʴĔھj:s-B!B4q B-h~S*fHFQek$ƣxz`4W#BPpZHrm B!h$A(p[ZV4R;7 zMtIA-dOZ !D3fZyF048..kWu|bVEEWsqdڴ4֟>mK+T/>ޝgڷhBƎˠA\B!D! B!rbԤPEr[=WtP=\v#._q6\<=C*[=GBԏ3ͪ˝ݻV˚^j[* ֔e'%X΍ioocbukW'h@F!!BI !ܖFQ~IT& K˻Zz8$8!2jjFqLcӵDQ*0**׷` !hJ-f>>tЁ~ɓ'd~'4 %KЩS'fΜɌ3ڵ+111 B!DS# B![:6*v3Z݊^Bp@:IR`.hܸąSU EEXetX&,B D<<ܳB1Nը*/܃ mт;vtߞ9윜 Z})X*;{{@lH Q]FFfb…Z_|Ѷh4ңGVZErr2 b^a_'N[o嫯QFq}ڵkZ5ʶba߿7|r)))5hO/\/^^z%vN=x5j ??|[0o<8~܅B!D@!Ab7vz}'ǓvMbjvJzݨ++Kiia1)/02! l敜A`DVuUXN\Xg`ŃM.)?k{"XsYlB\gϲdBC+[ܹsږnݚ{}׮]ٻw/Vbȑv3f &M'dĈ 2Ķwٻw/]tadee@\\ׯgժU=i:(/\7oL~馛ر#W]u~~~xyyhժvg}vQB!hPւ=c8yv ꇾ)DRtg7mD],b hh0 Qh@!hͥF`fN.rhA;OOjXP33)/wEhnMUUCqqMV޽mAx9ѣGmc-bt… Ynþnۿccckj3Jii)  dggrZc eΝ~u&L&22'| NQ?_ԱB! BN>9a2 Jaokw~#G&jSTɔ^Ub2cWQ(9UQQ_ iݚn>> tF[c\+Pbr27r={xaek=mh sS4T1?|@ZZO<uj?ƪmX !== ݻY`A=es!^{5N8_wmNmWO !BJr'$pk-z_T AZ m|+RVhLFc.%%ۢ(daK\BC9Im0a7fU%uATjeĎ,w|p럝;3KD͇~u]ǔ)S2dϵ&.FBBǏرctm۶#NLj#Xt)+V`>@@@ r\!BHBap B^C }'Q/;֨TWZ+(,LFUxx3b`0$a8B4e&srsǷmKwӟ/xL6<Ү  /O$Ů$f37edթSv-E5(vP/ !ݠAزe +V 99I&<Ã>o͞={زe Vu#Gŗ_~yQǛ>}:WfϞ=$''h"i߾=flٲ'Ob:y>U!9­9UBw1'ulO12PZ5sDZB!DjN +;vtQDH <$޳ '-'L&KK׳gm b }6m\FQc{/?8ӦMcĈ(ٳ;~h4cС,\8NRRnWQQAVV;E;޼ $&&rwŋmˇ رc=z4Z.<B!T Xۇ3e~ujQZGWړdD5tj˻[qZ_m#Fve2 8qb5jAo}n,K!!!)s!Dsv-[Z`bvu5\ؤ$ fj0OO!U5C NO`yQ 5a``+8r:!B-55U*-X,~VV1$w[[o$#JQQ*ZRQo$ RNCK9|3hq vz]ZJTvAV˦޽%9(B!HPZ8i1 BN^և6ko[ex2ׅb;0wdgSe)*J@Y% 1!i4<ۡ"jOqWHO?pMScߙ3\NbK {lIH+B!B$ B![ vRAxlv&q["kT8)6o&n>MEqs OF&1dmdPyDXzEaqT UQnեZu޶ jVB!mIP֜UV*݋ow_K-k]ZjeݤJ<k~**NR\NE|ߘh4^ 1!׌Hyi4<Ӿ"j\۫:E`_vjV%}LjL,ܘH0wo4B!B\I !Z!&WK=~:,[n!w~.|pXTJQQ fs!:]md/Ov.P!DCK)*˓'1רҡ9(>Մi9raջ={Pv@~# !B!ĥ­4|5R,EQJBRko9T=>).mk-j2?XF&LΦcE~Z-O9]9٪kfo$5zLEUUݿv p{H_u6B!B$A(p{NJauxA}bFoX+(XJ)*Jh>XF2 DZ?(m-,d3ig;t nۑtqHʈ;(4n̪ʃ{Çmӆ/zɃeB!B!ݓ;iՔ*xjx- x{վ8HR$ 6^MPqq:F6FaŅ !h,f͘_ՃU4VEG0F#Ӥ#4YdQ~ò[7<:B!BaGBײFPKӬ ΐh﮾Vw5aٞ2RyO&/&dʧx:]kaDۘB+ORmLujv0n8qO$Q;vI_ 獈I ̙3 wu 8˾ƎˠAe_B!$n/DeQ&[AXSCī #&N|~-[80梦!d2t ۘɔ_ z}G(1FӠhvN׿ߺ5۴qz7)+̒FbZ, ݶOS.]ѱ$]O|嗼;ŋ8i,VkZ[HJJbȐ!pWRTTd[#}myN9s&3f̠k׮V>WbMobcc 殻X֒TѠhl;k1xb0 DGG[o%GYr% ϓwɯy]N<ɓO>ILL >>>DDDop뭷i&>S۹mڴv|'N$<(}{ev-FkkX~=SN壏>bɶuONNN/&339sd[>rH>#FEJJ _|CQ͛ǁOYjmft͛/̜9sؾ};cƌ`ذa̙3|ZV׺u0a\s 6l`ذa̜9?x{̚5 j*^|:_׍7%.ԛoɦMxwصk| aaa|g\uU}ݶsꪫlΘ1Fի7oEW!RI! x3Mh?ӞfX4[JP:>}k}#E))َ^E|Υ 7C"uqB!+UTmcAˎRw`*`WI]rP(ٓBB\neL8Vͷj*ZnmΌ3>|83g !!nf.\7ofʔ) 0/n:,Y<`ׯߪg}vޘϞ=ҥKz-Zp]wQTT`VN^_0tPϟ@bb"|m޼nVرcG۲z=N-**w}'BqP!tlvfoo/ޯpd~o;ڇ1y[a(*JBUxxT~x**^<{B,/ՃmKk}~Pr(‚]EiTh2"3%dڴ4䠇hI QѣGIIIoi^`LII ~-SNeԩߟٳgsQx IDAT:^*Ÿ C T!h4|7=d⫯bСvr 6mĉ޽{ے9rkӧ;w$11cx'a컈7|EK!>HPZ8iVlPZGp݅jJ\6wbm^’@:d3?F&1t|JIwn>BM 粳]SS_VfKjW/nn%q Ԩ5fZ7 {2d&NÇ;v,?65[&~_}`V[x89r< ;w$66S^ж !B'I !ܞ B2(6`OkuuTA5z4CkC8Cqq ZmMeb)j-`HD@B!\OZdziӆ^^ Ic^.*Vn.NjxrJNy9UWw@ML 77j>~Ν;;|ѥ$((Gy'x~8L&k֬>LVV7bZmtU-Ftvm]n|ݺu\lٲ^b>>>3իWob:7=?WB!po B`'P9G͕Aīݏ\Gy3{Cف JQQ ft?0r;… !hL:P!b6ד8۶ed˖);w[LJiYRS9j4Uzj4apPP!DStwꫯo1j(f^{2rHKǷ~C=Dqq1%%%L4 6͏?7|cK]}z<̚5;wyf,X`;FmU:, `̘1]KO2h [_UO?cǎQRRtߣF믿f$%%ϳh"zMnJTTIIIɓoطo+˗/gϵDTjQRS1H* ;23[MӵDQ*[Z, !V !ĕbNn.&,fU2e-Z8pTffTXY,vAVO{xn2!DS[O!B B4 VY tJ(32s-7v޿­ iTU8mh1o?j4RZO?晬8C""„J1/7ՃN\OsӱgOq ޾̹R!#2Ֆ* һ7$9(B!J>]B4 :kPd6$DPh;-nxw=ז>UGu.~3R~q}TU ܘƢ׷l8B^N@W,N#xv~^ZwbSjZxxk|<=dI!B!ht B4 ϧwqV́F_ww_b!pйU/`A6o&4c)KT^~aPq~~ B!GRd#Ճ 穰0n v*#,\ JKsh+stܐB!B\I !ޝU3{&mtߗB tCd)P(~] ""~!( 쨔}GRڦd>di&6I{+Wg9Lff:z(dAɻ%drbT ; 8͝|!;AmKIӂzpIrXEB7tQv;g'jZ5EQx|d72ڲk[{; KKq%%LpP!B!FB!İf4Rp%OOf#W91V/pOzJvZݲ_ &l΍X,$%M8 !>vpQ(J!4Xzp]/75{^/ 7nDžJJB!B$BaAQ "4( šu4k3 XvtkBB~ucֱqF#| H{#w4))P[ !85?ߣzpƲDMqzjj4=`\s'u`zYq#mAH2XYRŒd B!&bk}uJ@xD }?SvWzFj<[t5Nv^т4ڌt;0GƂQXchB g ōiMFJ1qCq1sSR0t~;[;w=[Xq#-P\8hUUޙ9))dB!B!NB!İ1jt 2  EU\ɬg1w\ Z1qc?;w k?]ϖ &c ۿx{, Zio/dBQN#\(Bq iw@BtM⯎ipS`9Wxjk}>ظAߙ3py !B!BajE;J@׫ő4)e~|>;ңgiQUy5i`X_J ֕joJ(Ԇј kHJ:>&!CۊFha5 TScl6ז`7-Tƌzl&, +r ロ=:v>S?CUUV^=-Zķ4+!B1B*X8̯i4B02jVr6__:j!Xu]4DM(FɸȂvv̙9('~#j$9y&24Nj!8t]UTxji2qȑ}&+F䕦&^lj{-u};NMG!Z,JKK8577{w_~Ái?LzUTTD]]_`0YE1zhyDOE!bP$ B Vk~ǘʨGQbZi_jF0ΰ0F - O,0gb6͇"byqc BC D( NP(Fׯ]A´iI qxwD"|;߉ U f&:8 `X= !By-6P gg0i̫;bsE k ΩT;;rp}BKY!8Qܶ򽼼GDeL<=eJ9#Z+3S43G•W^ɥ^ h`0P__ߣhoXl̜9]Xt)seQ__}ժUʚ5k8OS-FwŋIKKc޼y;5[n%͢Eַ/^s=I'DAA7pz455qu1c ;v,ӟ8Yz5==>UUy=z4^xQB!ġP1l=2* | $˗/?9MMMe ZŬ>pӦMw}r-<<|{gp95?zjnv?O xꩧ7o_Wy  !Bm.[1lBLU > ¡EQ8p`IhWpLI~vO$nsM$MJ"L2 TT\"ǃ;**0*J\5DHߏë--z{ɓZ !pp8ն:uj\X5~xƍGEEEEE455cǎZSLaTVVRXXK/n}o߾cmݺr|MƏ@vv6{nt㏓]`s=n3i$=~wk׮3dnKMMl6cz9L4{wP'BqU!İ26a]gTYvF6Oc;3VGGIaCޝ^bәXoj|B W~?nh Wl3.=}?'6@6l`Æ >Nisa|kz3fp7gϞ;S!"QP1$mlÂbTH?33әpj^@h3ݫD\PkB-9L-H#uA*q-B =1u~O58vnY ;9u\rc;9!Āh9]w__EA?Euv}ݗ '6?,˖-g}^x3g|rn~@!"Q$ B + ( t{SZu  m a,xVi{ހM#z@E1*p@OA꟮zLY&0ڧQԡrwr㓐[1 l6z#~pX{p^j*'pX$FP[Yruڍp+w3!ٳgu]{f3omB!2 Jz 'i?6x<[ &(8/}i.u/4Fau&/.5h|gACi-)RP9ySB .?G% |>\/ՃCBD)٪4WgVjbo~Ê+ +9S_f7/\,.nN'8SiӦ1zh.rn&N'IJJ@W-'|W\O<9s5\ҥK?~B}> P͘L(Jt)\M i~3gtHɸ{f^jµ-M0t>z3ͯ7MqE餞Jd)E4$֊B$J6JsPsee\cVN:Wsxz|mzwݞ=,IOgbRұ'ZtmgAA6lWկhjj"??%K ŨZ'\y啘L&-Zĝwyy5sUWqe1i$nV.첸z{N^Cz.kbԨQ,Yk6vW6m\@UUVb!BcM3{DE!U2Oz?t]#9pk \C9h! N_mFѳ oG H2rtCB$ROM%eN )sRO'5B/t]W-v0<''Qny<5nﶶ=FRǭcy !z\xw{0a_=5!BBaŪd4tLYlDӂX,x0XCRxK5/I'}I:wn+m+ۈ# zC|U&aMxyy&:JrI2S6ok#!]hia*RuPUWsCyyܘ8)S0v{ gTN]>n{ d~j1pM8^o yꩧjN>dկsB!Q'b)Zixb_G]^o&$߇׻9?6xPUSG࡟HD$ ~XlкwZq~Dkv_õօ{=7$ϴ2+ږ>ӎ}pxw,ǩ**zy6* ?kq FBEl3b{ w#~˗/gGB!M1LLJbw2q׋VWbhѴO)b0b@ %X,GDbPH99S(EZPõE6Zn΍֏ @ ']!*X -(&싲)c< !gn7vNY&Sb&%x+v3@띙3I=Hw}Q57k5Z={IGmB#(SB!LjB ;Sc^Mch=D|{am $'8UH#4F_#@[$ʰ*fUŢ(U5_1KqE!h$h$d"IU ʗm 9 IDATl%%0AQxz~ Zע:.KYYGeB!B!GB!İ35)N% "FTՆF+Bt]'#5u>fs1n d|11!mu88qFvmIT:ڷ3GBqmkocczqr@D 1h}},hp\,ݲHO)+gΤjqV8oPڱS%b9sxT[[KAAA!}MB& δ^*ferU޾@m|l,ndnEӌd]Eօ-ѾGN5AU׻Y?g}LUqcr0+(~|zM|o2d,i_?>s x|TnvvF3&SrN ]K<~F"޽&0OtPB:00{: (uiF#&Y> ,, V,,A6m"i XTΜ^^{GfV44Įw$kӧKu}HII@4<!_-!ÏBag͆YQv;:<J&v޾ 1E1Bc4&x=&5aWmj>vE6<=6*3V ͠FO4oG _E4hkcG+(Q18A(JwN^5<9F'eʘp {Wŏ((z Ht`-rK``:,w$Be "Z%>j'2(ZmRdRhPh>6>KKD"Ǧ R_>9C{ F}0 #--vuUE 1=wYf!zF#]ws|(9eXFݞ:޸ *Vi]7, /vu 2W|ԧf!&>W, 0w|{w܏W'<qXs=B!B!ֻHǺ̊B(Q6[4oo-FK,_`7:"^֚V&6Ϧ`JDJ47YI9%,"^X836K~wI8dg0󬙽V5|'YŴӘ}lFf;7s4 Gviύm۽}?o-n؏^(7ઇӗ>eg{Q *ΛŬsfaO;S&<-r5s >6wk].*7S_!bP?CEQzQM1t)v;DՊzaܴ@G8(^ see\hD]*FB!"a$ B KӒ!nL'ΡGxΕx^EAu6ҷJɊ­=_>|]C5b+䅓鍢>Ug (}_ TCw{tO\RBKM ;?^\߉{x>.7}=ʇWSDS|+ihb˻[% DZEnܯt5=P%DtG"|vSu@( 㓒=8kbR9$i\e  cvnnߍ MM| d}=KQ!B!H Ԥ^OmpHzwƅ@$'BUb ;1$ f41 i)qnOoF[)„ho0b/YIjy_  s0L8uf7Wo틷 g33q5kxooi L:mO./cO_?yݴյcs?0Il|}#[uHǙ9Kg>x7୿ g;/`7zH8S5y?>d˻[+ǘ0e.JF~d+^7KYJ"C.C;[/]%rϟq#JڽVnKFs1OܭxOp_Du.*p͒m&aVU4kF:¾xۅuO$+ok8L[83DuByuZ@tM 6~:)h;vgB~VXxdLɜaCۖw͕VB!BqI@({4) [:NO)`j bfyrxio߂dzHăG0,8s дv`T[յf-?gD+mG8a_~p68ii%=/ue/| 3‚o,aWTbNն*>*?=42 2ΖX71_ D{u=f/9r8~t>wtm=0{lMn*C4|+>fqY6-Ξu{b!^ZN?1{>كŃwC>vws/3a73qD|'myi9}ǨVI$`6qsy9W렜p -lskw M ]b]H-u \T׵CPXKUIIHNQq{Yw<fNst:\_X++v}EZ !b;|Bfqsut; (׺NŅttG_߿9?6xu{Ѵ >n  Et1im}cII=!kJ-\mZ]C*h22z>픑U͒7.b`'W+yV}y^q0nb<%c# ,`4 GugVz' u֎T% C9vwmovL~w4lwFt W=?mnhѺ:>s13c98aEU69#jAjj>{Ѫ?UQ:}0΁@E[Ш(LI21%<8aQnF#k--[[b5wV^5( uvk@E4Uwj/kU#R]Fvq6*yǏsmۚ:Vi2_4L#bL!j1[!hΎ .7o/@KW5O8FU$'쨪;>xöUXply]iNvO-[ߋ( cO.hZQ:vw- NĆ7R{сHL!8hwxgq-6[zR!Ա+8gMJ>|Csw*–F63׳ᕇ89cYjt]?eĨmmro.O.6q67oYvd]OD0|2~w/Wpu^ӓ{hQшhdRRRNs8Ln]^/^ybU Ă#U}ة%;a21?5S榤prJ #Ҟ¯CEEqD2k/M&B1Y,_&zBѧۖF-0m% B [SZ6K@}3bCUZB$'b;Gtx<[4/Fc:P j%5u!v4 JL^0Q%W{st.\sͿ_VQV~|;imnO\6kG/ lx}Cl;EQ(9}IʼK5h($g$i齒`gm_=i||@j /ꝯi}vwAO\'Sfn bos̱PV8/;P ċ|6;c.pw[--~GL ,HKcqZ&SGɢ<9y2szi5z,IOgB!BN3{DE!xڸ1p<9|qz Zzwr7#ߏi:)(J8܆ZNt! T<~w/-plyh qFZɆ[j\^έ jT!CB }a B _ IW=u8*|rJ,X8`Hnp؍dz躎4P1%$'OEU:N/O TUŞn-ұp%--=A"NOM^kn/;J\<RF^ӱ*\.; DÁ SO9))|1#38%<Z3|uĈ=p!B!"bRII=28bt=L{tڃ30ZBvJ PٜC8"nnNT؊ǚleY3^Ńl$uD*9csws&va]cO\.nʂT0,INFU̪ʓSpgGڵiiW!؊2M&I !qOB!İV¦I)02=99q;c"jՃP.gx;PUfs`K)NTcl_]ivVō33)Zccy 7ˢ"im^njNiD盚x6pȑOd|[h aCGZʎp ?5]/55B̑&H$Mx |EZ !bt]'G"5ݾryB]0O;BaHB!İ6nqIQïp߇RD(>uTU\hlA PZQ5(ob00 `Ƣ̵XUq1y#PhUNzuuU&軻v(-li5*Bwt]'>4HO/c}Dpx:jH jmԎg$ Do'Uv\ gԿϮ(ÀBamgEWH)=`]BqF{VE ŅCU$وdz ]a բ&RS瓜< i !İH]ik Ω Aj^3͛ \2b$(\7w3)oϜI!bk#G jw:Yۭ|~awH Bz:VY_Z!2"+5PxK(DS(DK8;Ƨi)D_V IDATtԉ}~8\Iĉ!Vbkq68 H@(fR ܑ% < |r,ؘuE_1~fsT`8eXB1i_*+{DIRU.94EQ?+,dU[|SjM^/_߱_EEon.Ix]^/˶nk&U3k;6Z,\:bv okv&E!4v\e VUeiFeg43hmI!h :a^k(36 񹳣gt tkt\A8]pgR,YQ*UŦX;uE"lio:|5p[9%,ŲDOc991tȻ*!İ( Svֺ_5v\v4ڃx0XZRp7G5U$'όB18oc}NssI>A(,IOgIz: j>Ty:{r-,|2놦P7m8bT8  Aok~MŦ&kj,LK,""B!D'O$BC0HC(D}0y(DM @]0Hc(35XSY97fE:ABW_R$l0l40H<` `v{LJ#4+ j?ɟ}nhl<:/Nsw`Ic߈lprFu& @8fskNZ[u,f=v޲F?~xUdf1{l.lR^~yG /  nwK[<w[+G9@~L1M a(C#ofscB!/UUo~+lgN'԰qW@[$ia!Vk&Z@Ӹp-0332|uwxs͟DX^VƬd͗$WL !8DtPPGWyM @}0Hs86]d誳*sN}ǦdLdLLddL{fEILLJQ9!T{䘑L.jGq7IL`;[;tIq?hl*WQ.q?mzg nD"^1u(Nji$UBqzy5n,JKcQ<( X=Oq_M {|aeo4 W]ͽ\:b1#9pwvbwrN!Uyy}:d4d.jk㕦&oxpCy9V.1/ee17%*!"Qt])2ڎ--́t>*U h$h$#wܖf4bkvDOA AoRViX%,~ϟMՎ*4Mc˻[|6KgNߎg]u30-?ߣR>)cu B3:/c=F8fG8kQrv %gK(o8H@(X$@>߁Ճ!t=>EZO-P .x;11˱Xrq8c%ſ_c#;!8ͯ]q[5dB-(`U[wTVFKˀڏvnhC ?TTd}}ܘ8+=;ƎM̤n(*bgv׹mϟ++cEY&_l B!Mi#Q3tFEAhywGw4) &9f3f3 #fFL4:+R !G/-[E`73쪡qcv5Aٺo6ovٷq_PQκ,F^\UEݞ:wT&(bhZgqL{5N籟q.Z= ]`0tUBX,XCJ;]zwr%jj-&v V<TĭMO2й!hPе6W8uu=NhX,v8=hoΪ*IοﶶVf''scq1˲0$d3 _׿ `sSgσW=f[,X,q_,L3%b}l>}St]R @\rG;pRGZ^B@줨ߗ bwBi.WBWдCWf0؀ٜ6&3Wt~H PRb!5p3L&:Tw^#}lzBtꅦ;<*}NLzB C -E"e]?ņ}Etłkh$3~d2d5yxHN{L2}f7$1-ͰaKȲLlĉ& HNᒀ~(/hSIEX;${ЁQC` ]}_jk`0lAmtݱZeRٹ&OA+*+6=Tmt93Jp~`N^ Srdzx!'##y(*p:h۹fGOzdd@YvGfdfdjlޝс DSy9?`@V m[   Ꮀ0 B-&gA.h-efLv}oT!i0Av% t3H_lC??E.HVb׊]Z];Cࠗ}8{$wO.yy\$];yZ"7y~qIn8 Kc[SO|'xUSl.wH:A Jf+C EMaz#F^T net! `ZmX1ur6o[ouSBCy$&'ɱ2ݿ&S>=͔'3lx(*}|n eP?ۛ^11̌fcYE?VTsEf`aÔMeeTL eJX}}{ATAg+x,kذ_`JZ--Zйw2&_?kS4htߧ.*,& tB!݉ Irţ*#G6 >(6 @_'} lNX~bV7tpT?C3,Bc6 8]%E{x)jupM89)&cY@t蠩 aUUm Ϗͅ`?Cv]o3j9YfaI -. n>:zUU.]x)(}6)%m<>x{ƺaa~,/糒~t?Mb7yfZXqDAɲLJA׿F#M&Vq'I"V7u:u]1 SG3zvixGOxR䒿\%>[bmuۨ[@A$JR||m4z[dF#/ xgVu>ɲq:cWbWF@0 mM85*//T=Fi3AR( 9sftQQԣ1GB8vR ,lR%Ե+/ǓtNAGH{HR}}kǎh|/:TrKh(Rnui)^Sj ҆sM&bVVcĐ3wAlN'f;a4hɄYߐR}LB?^`b@`FsѼ  @Lj < Vk)&&{^Rspa0lv'J^^qX,yH IkuguJFՒ2~F3甯/gf?7z~T<CQQ|UZʫyy7Z (5h,}W^2 㙸8b'heEe%9ѦjEz:.E[,dzԵ+"#/2:p յe)[7qi@LA8 v;k4k6__Mn+<P-I8d㵡ZMNG VKNGy7  t MVK/,xӈ} `yRSWWl>FF@(ژsާsVWɓ KNƤ׳Xpܾ#W?gC$K/ad-Bwgشih\gBv:3~|v:,_ cc6X|u=oWӧgx&27㣏z/d2alƿ"VVs{`ooDn7Y2s&]4㜮: <+ 4ڹswNg[cs8~i~yyfT'`RJNSKPp{XCCYZ^VULjOYXR½<KFm7}J`*%}}>x+|ҫWnw:a{>t<11쪭>+)fkms:$h<԰0Xh#p1e|ţ,~&68u}G$sJEУQ&`V+A'A:-d :zdY ju&D?1P]v;GKD <9z]y.@x>^~Yjjg|=Jvʨ;~*sr8bEtLUUo\{uуy^Kua!^ Pn|] j4v'N|t!eamd B{' <%݋sdZwXΝ n9,4&$1lj˵(K*>((@s/7'#Iも>7dYn3a x$& Rt6049!|2.(CC=v`ye%ʸ[n*I??$&Vq[mf66#W{q>f(.Pv~ 96mJv;6>`XA޽ e\r]^#$16(AA|УK++e8d Ͻ(PtB/,@.]PsA: ,s vRh[!qIx~>>!-A,BA: IftE$lL-d? :P]T]P1sP( w"7߹˘3ŴiXM&㼚d-(\Jc榧ӣ2v,R'M"0*_bЭ$IȭsK49=˗+X(5Y wK>9nA,)@;~MLʄ g2~uϸGeԩdyBTxGDtPνp//^JHX]Xȋ8ɳ99ϓVÇVSRv鸸z_.!j5 w Tl<ɿ{L:vƮ]X\V’WW3P'Y?##N"fcW0p{M F#{J,8$*h!QEIJ …DATcսJ`0pKL5x{/gQ*uN`d+˵`(=2;3%KK*srvl_~I!nw[֭mYm_|A^.kԔ˒&?]-/vb aL/:iRNm9k2voUO>9ICQłgd Anj5t!}H>J%fzD0;7Y1'efff2'/חzô׭ݺUUA0(L R{D’/(jmV|Gx2;ɡ<E .6쬩ĵO`Ӓ NTW7}}}ـ}|g pBA:a-^!PlUP==ZKԬlZKb6CMdꎢ%8:|jLMe7Zfv'w%,{i,[F`T/"} IDAT; AC b]w1԰0WPϱc2u*[/'pM+e"RSQZ$K.i9g{$_s 9v-=Fvl3vIܰaH ơykl)GD'|JfpoDj^ eˡ7 *`}ׯD;N5>^Ry񭾿ꩧ0o_y'A11.\($Y},~3Nر1O>& 2Λ7;$j.׿X ul?RC7o9gkɎBl0YԖѵGF`Mz=m}jDGaW_H495l[=P/f*OHc0 Pؔ|BnQNNj <=tGx1=}PIׅp]Huu+,Bf%H[ n>pj54{eAνl!``1hV ?5$_|A ۷ogA3"i5k_ʸsoޅn$IZ}L\]Q||R][`(nc;(g4<gNa 55PQCZKq8j`$IX;mcb AEƊ+R'M:@8 g~aGFO<*9,4V||<FEc+)aJ˼=g%%ϑfY _1Aⱘ O E!2<{Fwpb>7>FP0#X j3Vbx[<-p6ON;5Z BA:aVh&,VL*zǧY 9TUd:V$13Q* NƯ)׽qNANYL -</1nF֞¥!;v0K^ŷ'oK竲2 -wP.*⃂Vx>2PahV/pt4DF|?ւY lf}u5  8-~to83ĵo@ADDPNG)I jv P]-0c{4#˜A ~5fs>:]"fs&Zm,AAcQ~ 盦 3{}Aۥڽ{u8U; ) ]0;7yEEȲj$EE|QZʋ"*8[uR92cϳ}4јUTnAUUX2p|N@d$GG* J†j6TW^^ѝׄ@,RqI@C]~~pAmߟ/VWfԎ);;ѣ;+B'' tJ#h0xL(%ف=;?9FLTAHE6/3z=~Vk:]"lBK (h,*U pv8ݍ ,LSw1=<Ix/)Gbbx)7(tP%ѣ_PII;aYZQAu2 C8v^fӨ$BB.$}F#EIGf9~sh۱ Cg4)Zف-=7>[ ϏaRChpk-8GwM6/[LLJ<7z4v~-صiK|w]}ٵktn|\wH$sBA蔆7r2kd bp9dَo_$M͹TU0&"6l ]KA8|Jn <+/hS}}y?)c Fe^Ϟ<ˋ99Yuu\{7tI8uhxG8xrf^ń\;Yr2/F^h vVY惂>,,djh(^ /ʅf^&.oRP$0Ѿ=Q bA4\U7*L&~1$1{XQXSG۷3p<>4{nz.t8ݎZ8"B4߿|"pFb4Ck=XVV{Ʈe2A_iEM`6݋Q*}صAK%IL <:,l6RC߾h[) q^< 99,,)i6ܠa_啕,OOgft4{=>SBCYP\jb1x%/[BCs׈jy7)x󛕥u2 Oq1t㱱 ]{˅4n\*4H@F0" x]ĥDze,ܽs`D6+9z⃂H eZLIM1cr>ܴ2q_ZGኣGy?(+#-2;RS|VBW <0! g`.j徥K<..ެa̧C/ڻw9F9%#18}I[15`ղ-Y1w#y={˰9|oGO;sJ>́oq~8p34^h1'/2 ee|QZĐe`L ߙ,偁 dd@I:('+Qn2o0_ ^Ke#:tmgO4J%MJȝN _h(sr!!@} pՍ7qUXˎQ(kKc,SQQ*y0>)vphܜ @\Xz0 oQ04*Æ "& abqߟS/:o vKBA:Ḳ&wP?Vkfs.ju7w,8P=TWRZabw+8(>(3fP]PE:+'kb<de}ܱhCu͛[yh:"#O<';6gq9gwXvߗٽX\s28@1n yPXP\̬L &Ev0dJbE1IpTsdL !/ù3,x!7F#J-z~ݹ<AAoA89eF#zVUUV@Ash[BU(ߟK]%C_t%A85P5*/J6}oFݼwULe:] kZ/{/{d@pE wf}^KJ;؜N9"!G۷/d7o>}ԶÙr% wfBRw@zîb4J%h>Q^-+I-ަ5*6V" tZ[,U[KNب0lC<^&nףRڽ7 b7(MK{]Ζ~}R&LGIω\F+ "hۙ1A B-ݺثyy,hSI}te唒K׮cǐe{ 6Lj^R7ee<ǃQQh:^Wj;<⿭(%uƮ]YYUKQ] ܏5Y1wseP%&Od 9Y9\W*VVUn5T d+C0u'~N睞$I5Y$|[hk(at==yܜgx.]P)|Ç: <=,6ޥK9ܟFϐ|jϟq$I2 ǹOUD]gO~aim޴4nNI.#֭qx`>NYow׮>!!XvҚflxE2I tZ}}H&_*nˌlUAڤ?ATWh܇^4a?gloË &dD$PT&6 'O兪{Ur"gPhpv$[uO,i՚*hSJ%KEwJT*^NH䛲2dVVN _##ߟ##y=3="axUI28+b00;7%BzoP^'UdձZguU+{y6.z`V[`@Ij;s /@t%7ˋ6;S4J%dj7PWuaahJ`1*ϪbMv6kN岸8~ʢb95-v;^m|>fk.I bҧ[7wжt3x0 V<0t(°:,9xSSnmNG++YyĺJ~dA[Sgr)INK`d@@;*I&+/FuuYըTk;& 5=NF~֠PVwb9@}ߵ}ܾ~cvJ -Vk)ܳ噙|z-үLD9o>?ܑ#ݷ/_w/i?璳y3%$u+_o87;ngj>Y*+Y |x5;]Ə>rd$^ߟƎeܹTzo7dL6??no _`O?1D5c3Eu̟8WcT:>ee|[#FR]Pq̑5k5j|>u*fپh~;SRxm`>2Cîŋyε+u/M#wKH w1%$/Y35bLĮou\Oo #X˔ddy'xw^ݛǍc߷y7kcr`2<>>^h][mU7{!CY i|ݧ pg6u-f3={8^V{!w]g0ߟ@Z~622EI ݷlaѣT0I&AgL;xȍIز⫲2\- U5 k=&VG3佇z}>Yo\y%krrۏ?23C,9x=j.<2OTT.7)϶BVLҕEȘش99|oލ^{[hC!I|{7%[Yh<6>ReGPf4z @^{L4V_5dUU:;GrMy11\Ĭ~㹵k9PV|>ܺh4ٸ=%%e ~ =^ Ȳ|~s:͘LQ>wQ4*=렾+*Xк&6+{FoDeY\S oAPL ;wrxj]Umz$/==?e<@CT;m %>.]wv}ve}@eNWh LZFYrW_//~ md6߹É<1ছ_ګsG<+FvsQTDȎ/dϒ%OΝtBʄ 󄵲9U w jF#fdžŕO<@xJ /ڵa+tӇëWq#/vxICY%=9DD\ lNl6  ԿzN>Pn cǘ][-; 07?3'1;t7wƇl2<23kW;ߞ1Z-&%pt4yI Ju5rBv]輪vָVVrh7yxqAAu!A8|4ӟg?kNmvY3GLG1UnZ:w{j^3nj`c ]{DZ=CBxO}L|e>6Ɗ .{ZE@D;8Jsf;M۶ Se%NE ݛ|iJŀnbǗ_6y2V}?UO=>fi,1Aϱc7T'oVJ22p:8v^ L9#-,9F6g>j4K$9r%#[xGMi)s h[,dK}pJFy⥗z͟|Cch\{-=.ɮëVCT-{6B]kJJ䦧So ̑ի[ >gH;7݄_hCLaȔ)-޶NvL$Cy9;Ѧi4wo{TL\ 籬,w@CII8$aRM7:<ż=;gg_Ndᱬ,~b;dN^'&rM'ޗR09eݵ,diE 8 ߵ pEp0cIeAbxt4kN凃U\LSDՑʌF8nX."@(B$ ۲20 X^QM2V.KK>gI-_A3QUIҠф",lt]R;S j|_7NJŴŋ4 VhV@R*SV~榧ӣ2v,R'M"0*_b ʿAƏ>p e{]q7s`rX'n+{ O=n' IH@16ڃK$4>>]h6~RQ-MN'ݒK"p< IjAұ,_Nnz:W`Ohgٲfn휭ѫI9IIk|;yl1]` _L1MMLʄ g2~uϸGe\xc"V f[̺:n@A};:C-ˋϒgd$'aRGM\ä\{-a{S]P_e5OI$#Z p tzWne\ leVwk^o*juc~ DDYhEу|Uz5uحVw@0[薔DPt4?0>6}1j#ch~~I/ZDhr2;f6V%9lQQZӚl|m|s8vU-{z\~wQǿ%! Q@DxEE*µ]SJSED" )ޓl,dCQ~!SNɜ=pHؼ!CP:9ա^į\ՕDWFdY>댽Z~~MѸ…vb?3fpzlz+]>uh2kz3grr^n5k@ ZM?$Y{16kWG4}:N&GVڻ7.]>C]W]Ͷy?PʲHݹF%fV ~*ܶcil$|$uh\]³pؚy$5jJ:__o $I▀n㽬,Nb '$6i֫w9iL{!eN|:9yeZ=-'@TܨF3+Gp d]w5;fف=o3fm|~$F͙laK3 -5 :KQ[?]w?<~o-)0oFwk0ŵ@{ח<~wO<ӓۛ̌k\]~=YǍcSm-ԥK):q/f$c>n7f?^^ZS/&x7^34[/ 9/yOZuOM^ߟ/gd bcmiBBWQO?GcܹD ƈFMkBK _8{yqSO53t([^~VInn`FÊ6MPLǎ Œ $,7h~[i)1qq|}I϶ߟឞYG 浌 Mf׀Vx#"{QvdtXR–b~.-l lrv??p'Q*T F WW^3 fyu53~AQbTa74[\[K^O;3 /7:Iqv>]PMȲWsNLDϴ& $ /"9x ;~=xgV۹3]ѡYF wؑ7.i{?NhHܺk{d0>^y%zO@J uu-& WVk~^ 0ԗn<ޡdeYߣ 0;YO޸(<ձ#L(D(6/999,օKg6KJX\Ljm-_õdggCPPp1LJK  B[' W{yYLymkI \=gdHuuJ tPZF [f6Q^kpvXoAh ~]xyNT[ XLlHk$>>|ʼnI2w##(!* &\^C\A`Zl\ l--ez]jkS1kQ*ìf= }ۧQQYNф5B{of%%6*q&֘KJlK|feLLw @ rTh3{|IMhl6[,bGG3߿C=/\]y",{YYL7WWGH]AAy/+ E}Ȇl((`ad$SDKL&~ҡ>*ߟQ^^8 3֯'_O?m^޹sX~wo؀~AhDPvCP0ӓm6LEE.T hB$l]ȲZ^Z탗HT*{{`6q-5/pAZ䓞N΀٧}>/+TJ]Z>n7 x__HMeU~> d=Pd001&999 sBNˣ`Ά$OvÕZ^e6{b"rrXMHi'kkP\wv^ps@71~ANX۴%#/7&Ntt( # ]6;qVUQl0V; $Fc Zmu,0kquvҠ&S5eeb0Z"IJFp) ܌4a::A-9ƝHK;PYii͖%=zgլeZP'So$lH'l(*b[i) &m* h6Q yZՕ_ۢ"II!~m{E= -eUUl(*BkjP`>;{^n󣛋KAp4{e/6"ш2-*(BrwX` VZʭ @M1 7$t6meHyot8;G"I&S&S9ޣj;;h?\DE8: A*J)ǎkhoo Ռ :4O* Ǐ*/Ovsz6'{i'rHl$ILgofd0/3h̳23YχQQL %02]Q 7lf u0ˋKVA8ssy~"7rGϞ رٺ?8;v\\̀]=>>Segʌ޽W/[ᾴwf{Z%%3{\q%22ؕC9c;uB+,zT}}z=^ANt]ꊏQJ*IbkI"xSնIOXJɲLEE:xXZ|1y2C¸G!ov8;PHz+^ BA$1ǧYo , K,G(˦(8;wiqۚTTBEezLB6,"^`_>번W"Xwx]GDAvyvTPZK+gԶ!8o rrbq׮bT>Yۇf8q!Wuuz<fGGuɉtvfs^lٓ0O.{GYxIjMRT?aevrR{p,ˣ\'ؐVƞ=)6o{@/6 mDx83?}7z4=YzoÝzqC׮4bx}n\ٱ#DD_bof&9W??\YY\ӹ3WwL'//F3GFc'x ϔt,9pݧN/;Ȧ0FbD-]à II<\672ylȐsg`h( >xa?]٧M3y \l--,L:45:@ťmrva6עZf O+QZ!r\LT T֙pR:Ӻ~qQFqtZאЪfn9v CRx8<=%$L=)1&Dy 6*6 sr1)ŭ e&YR/.fyt> ԉy"5oP`I@^#Ĝ.]sfe~k,>`ùI"-poF̕Wʽ?@U]y$ˌݱc %'Đݥ kqz/Ză2kϺmh})RAq-BՒ, sL`XNMMRPS0Zmg6)+E]]6Zmd2Fc!W,Fz9ڜs.$$]x9{ٜr~SLWH/ ݩɭ%#f0}t+ymkğh2ҧC<>ဥlY|b6Ⱦ}śz_R~a;7,W"xk[&|JШ46jXf#D_C/m~?dPAL4Z2J2 _=ō=o$ saڂ0oOߟ}{33ҨGh׌ t9VP@VE7t ̚cqwGA5.j5zfZ,9pǷnQpR*hs+¥A4]k@vS}BMh,C:pE͘LոtClo"eY"$4HY6ew~$%2n%\[߂}~㱑/wg',h6rۊ؝Ɩ[{\ܵK~սܵ1|j˸mmzr%B=C|NYq Z=:鑚,̵ϰ<:Q'm癍4l1T0o<S>H^*}N,{\ޙ'O0uT @g1m4<=X2u c'q+,fѭ0MܺV*u}<{$$q[<^+VU |u+Ժ΋_DF[?~񑏣U:gZRwos{:{+ ;`i̺b͉#nNGڗ%%ê[fyT|o}{g\('';/$I| >nee9.58]PX@^]>IITgQ;Ґ|4%gC,ͥlKIAhgg^ A5hv$ 026 +%ǩU`:zwϬ?ّ-@׀ĝc<G?mzU*~a?9eOgO0>-y;Sv2D%mclXmIfZXyJ,sĞ1ϱz22Jfi]7;&}2J}w Pk1?̒5tO|:Z˪U($o6j!|L` CB\Fnf#o]ڙ!CP)Th3)/߃BAԃGp*T*w#7&W_Ldzb0yXMmNLG59M}ؔ&a52rͶ?'lM5eY$1:zͲK~q/ڬ3,bYNK`dȳ\pakGsğD ̲=wN17v&z`2p,W_Tw~_F=?&h7A=y;};Y}za֓$Y) Ϛԍ@@6dM I~ޟªBg=z6:ct&c3LMo#1\}5{NdpgP,ܙ@hTw25 MPbx8rct@JM `A.<hχY^E3yItY8wZ~ݛyyPp\H`poRJJ8=6*6^^ I|uI@0 twIAAAἉ [7hV矪E?Zm1 jÛ-OMM2Zmx}^ WvkЅs49#2;- m5ԝªB2s|U䑘h]VRSL6:=i{l:5NJ'rfl~Ų3e'zo?H/Iq/2xtXޱ܃DJu6D_áC6 NfwݦǎϬ?)=8{|&t|}B-Ks%˲ϻWH/5t \ʸ$Q#y_o:d4n ~L_Q3'N}npW1Rг'*{2KJǏWBDOWWcg!={el,J|,XJ 5xuu|E͌ ,[J T}ɹ މdH  H nM#$jjeJ2fY6c࠶pr BAh$]e.36v,omv}@RAGs"n-O4K_JZqeUC'{~{w[]GSXUCk@>![5syzM5t'¦,ʹY!!1il8 ~:q'D[Ō3 a׳Y}`5%ğgğ@T3=?_Sͬճؑ G7wOзC_k9/yo{5X{3w\Ofi&J\FMis;Xg n |Zx07Y6MAv6/Ɂ|&_0GSS9Te$^*wo hD!  ?$ [..D4j(ˬ),| 7JMAo' 40u I*T*Ko001ڋPh[,MΛ?zl:IL3e'|, %_N\?g¢ NG}/-}o+ ~<w}Ȕ>S>xuU*=lg:gMv׺Of~1ҚR޴ڣ16({m@گmΫ',̈́ ,ɉ={8:BaM4h#{?;P]ݪ F??vg{ Lk{`m6K)ɓ^ Ͳβ29~={5!%%\Mk]@Ea,ڕ/  B&ׯcAhuϤ)u>ݷ/Wzzy8@y/8;w.ed||ͭ7`QXZqv/-j6xy]騷.#Lp s^pt8mO'#2ѡ8ܗM?ů7Zx7GF<*ږW~M՛$rss2/Ǐ,O "TqL`\rM S8TUeRPEFrHHVÃ{Aڴ2` V\PAZ+糭"A(B6ݝfQYSP;]:]C!Zm`(cdNNee5deqHER4&8~:X> =xg:vp:h+2/o 1Q͔|m IDATf ) oA.QJ` EKy $eʌeCz}&j_h4qr Fe*+gt,E>Xz:"tAAhuGxI}խ.:U*>T*Q5I4$ede1\x &+Ey*-W8Z}!!ل20)O2t ~cg(:]&]qq! MfdJIS Bґa68:rgftB"6+.,IxCքUW7\6o~6I7W ;t~mTU/񄖹(,bw%&Ra4ZyrrU^ηݻr cQNɵ$,Ab2Ib?ӃۻY \AA`ƌddd/8:A BA`IB!mWHM d ul$;ӝMkiQwAe( 129:A-Zyok/DZ >e͍ "$+œ'~`3t:8X_wuqP/;z]I'gϲMܑ/eeH,ڕ;$oͲ̎2]Q&Y&%X+<=;(TA=h4G!트A2x?7{xxJϪ6՛h@KCYn0BNi@eOh60JӅ}H&3 QAACE`b㩩d6#$1ݝ:tpX\?P0Kr{BuuJfn>vx="Rx80Y*uuų:].Xa[,ٴ4rԚܕȶ/:Ryy,!K !)}!!L "\m7$ \f=C3m4~a(++fݔ0fn6&N~Ν;5jv_'..޽{ҩS'|AvW\3<1TTTϲuVt:wq3g$&&悿_^/믿ofLpVj4dՌ .q$ӱ#{Fcce~>{q^^:y,T=:ErPA8'棏>W^aŬ[^z^[n[sWsww5ٳٸq#[no=z?`6[L&Fʼn'X`7nD1`[9==B+._/W_}D֬YCn;w.r W\qyyyYc$>'Ob ]K Awupc&`uAw&ʌʲ$$&0QUu&I<1ː$ Q(  º:f?zԽIgggG%\$^*_2ӓGRSeN~-+|ߣ}/Pe!!IΚ3yItiGD/.,AlS }4 afPԙ 7rJBC-xZ&%%uq7kڴi<g 5WbbDE%!_l&$ܕH|Eow؜(//vc]$KMe[zkE$'TL`2Í)BCyS'D ApoR@ ]2-wͬY;w.ӧO'<"T2dNbÆ _#F0n8֬Yc]ITAZA(D??L|d'hf)(6ˍrdوPSTȸI( wvKow wJfehBČvɉm{Lǎ{$Av6W:J¨(s~# +.K|*Ut(^^(jdقR TmZ#\hzKg63[Ts\ȯFo6VkNFoFSL JIbҏ?ڷwAgdgDm @ ={w~JVs K/QJ /^{-bcOo7qMn*%̧B6wm@PW?B[O&-AtEe`E?(DMOÇ٨>cXܱ#n1.aFh^vRNߧlfyq1 7핚V˪$ ,E屰0sWna!<,qݻy%&EF7/ASb iIIIXroo8,#4@tt4[^^^ۼ-;::i;-1m4M|OOO/^4=''%%PAà i^NGNGge4av|gYRQ( 0,(7rftZTyG-ti(!?7@V=`;)_^PWI`O¹)͆Jq&ssQ*UWsh{y7IXAOJM<uB :th $I<F7CǛky쮩ޝ~mNB8޿a x2<+*蓜=h_߭@AA.b BA td|XXZ}ff7*CUTGOQѩP[?8YY$kje9|Eڗtw/GSo=o>7\fs+G"1-'q.>>Ӈ|}ƽ 7͂fS͹30|}Q6fefaԗLrQmciLX7n"#Y3l?ge~ LWnS[V>6GQu5U)WU} %hv kDFx^o8Qhz:+@bH6ˊ8ϟ!/t,ѣ ٻͼ߷UC:w3!1sՇrgCf&qVF|I<}tmۖwo6%55d=zU!I}Ml%f\+gqa!TW;T)@xoB $۷*U >?|+v1>us@1=Ĵ3RpbaQa!mƔ~8rc 6|{;Rܷ/w[~bWgo.<ПBXAA.T '''ڱ \PTVnȹjŹӥQQw8$I,ԤPSoxy`2"IJڶ $[.ڵí1xGY3aem-Ɛ[;zmߎXvVJetܱo:S *֍-^NGmۜHu.ukkY\XʒnsxJpis^>{7fe^av^=QzzyMDANaa!aaav8 j﫝;w BA\_32 2HeRx&b;A$AK,ˌMKsL oV$AeGb"#Y[aA o vj׮CU x1;{2kʸ~.om6{U^c ?_##ɹj]qE]%yN(%Y|խ6`oM ;v<\  ¹" PI]UIC\TZ]wS9X,5ՁLŸGv>1 Bk߫TC Ly*|ҥ :t@%I6ZOgeLf&E7#:eϒ`ѣl<ۡ_pjVܽ?'Ӛ&xn,ԉ}y#6 B﮶mّH{ݸ@BݻYZT  i BA&EY]VFrNo0jգTzumDMb 5nnaH͈,N@p; Bk7x&+i ,ؑ %\R$I0~Zr\ܽIǢ ssp o%0A$/FEF#S /"h\/+0zwoƵkYo]]==ٻ7ԷQwM0># \;AA H @///:zxgf3קR!YTjQ Ճ1ǜXAB 2clr#^ !t]˞>}ח$drvl>v^ijQ 쨮ۊskSSèor|*"kXݭ[yn6W+`ft4p%bMj 3fsaܷIJJbܸqge[cƌge[ A'ǵkǿiLqڝ}LeEu@0BGnf@xy@.'e[; Af\ NѣN K;u 'Hx`I6 >lٓf*SŔl{Lp 8 ȲGV^++QI}^OGDpO۶).g$x{3b~ 6VcLj߱ﯸDoSA](U׭[W\ɸq5yPo…XOQBDLL +VhP BAx 8Nm#SV{׵OS(mor IDATڈQ(ܱh4XݣĪAH+hLnߞ d2ң$ӱ#Z?x}GqIaaaϑz=2:$|YeHmX]%&GFrU3bs{@;{f޽dX2;wsgnA Lk7ѦMM衘' A8p77iNUJ`eI bc>-* @ |,((ju[V= R a믷v8 ZTtxҸ`,3!#]UDooo&J\C$ ݝRS1˲s8fo.>ڕ;۶uZCXIO?MlF]4tf3)*b~~>ŦQ(x8$g""mbQeGb"+`e8p]ռѾE]!* ԩSh4 >ebiL:ݻwөS'ƌÈ#Ilz-6mDaa!K< fϞW_}Enn.<̚5 hƌba͚5( Ҝ*ƌáC7ns!??$}/-IYƌn޼~\+WdL/ֹsg/^|JɅu"pR*;0eeNc$g-Ah2`4ф`Tb0BjAB`C팛ۅpifرL>;66˗rdBW^b3)3cCrP tjZa hwwIL}RYTI\vG ;Ů{ba^A/7ך,̗ee%2]%UELJ##pYWϮ޽c^tucT\s'^qI-w᭷BןAΐ/"3vXB[޽Fwl,$1piOBBd-چdv~mxbh[ߊW^ @BBُ>}ڢX{3f ?<ŵh[o' Ah!!|zt,uy9GfB !̈́BNmm:lD h,ӳs,.fn~>E& ?]J \Y_U @y8= ZܔNrz-JJ+p&U7 gjգgVa6d*DC뒂ju&S1WѸ~J&r0uWڴ3q/XN Ix׉K'|=Ӫ16lDGG3dy^n߾9x -s,_ܡ 4VepzSkQ|D--V(S':hLvg铜̆=_6 J{g 1OM%&Y\Xf97֯(<36H8NTi.bJv66&ˌJK^fn`pӶ3 Bk0Pxn"44D~k 6٨tBB7n;0a&L_cҤIc2o9rǝOFFp˖-l6{]C#h4 <z>}^=Z-FbԨQ,]gy^{ F:b$ -$Ƅ07?qh,l=}Ȳ 2 Bpӳ }̟? ۷/W Y3g+W$//::$_F#ӧO端nݺ1sLnf2ь3š5kP(ń رc:uW^q/)){YC{'xٳgSZZʨQ4iڵI&pnf> Jbǎ ~6m?+ӧcƌᮻjqRTw%== Fȑ#i߾yӧ/`03qDڴi×_~ɸqPWS3˗;3gukXnf>swɬ_.{w,[m۶D׮]@♈&Њ Ix128HMRg:D:9A֍)ANa\ʜ9s_~̛7oW_Ͱax޽;YYY|W̛7Ix{"77ўkyfذa;v]v1qDleYv˨Q6m+iCUV1d ]wO<ѣYbs9m6FL>}ZOp͒"$tDA߾%mW/jii)7pQg  ='Ofر,Z#FO#11#FP\\l܃宮yǘ2e &M[n|G,[0&Mr[ ?[n|7L>Gy?Q t:UOӳgO}Q:w̻k7l0nLPP)o_.V".p {zr'{kk)%%s |@ \l6#ju Vp1Lݻwf~af͚Uwzj{_|ix'ٷok֬7PwsˋO?ԾT233IKK `'>޷?55xpp0SNcǎdffvZ R8^`ʔ)<1z,L>3g6Shhh$oj宻r9h48pJ;vӣGHOOSN@kb2Xz ǭ[?p8? n8zxxRtXN2h .]ʀXv-=TMZZ9ۇpq2q3%0&$sXD___v$&20%\fYǓÚFclZ%%L'Ԯjf:rlŠJ2Wbd$Pg;eu:UU\s'J̤k[;TA,P̝;s:Lozڳg>d777f͚eTsco޼͙|W-Zt,_izقp) h\vEpS| CXՀ z -K Ѹ/7fǷ+))?dРAo6֭[je۶m9-3h ֬Yc$Ir-|7DDDؓP%m<{0wBB٧X#G7K,a'nmm-̟?oooaͮ;{l6lpOECU`ڴi$%%G\\fo&{r?sכ8q"| ,YaÆq%Edw W[딼VxQ]AP`[b":\p_OeelVQL u6TJ/5s3tYUŭ))$$'c6萀IH`kBEr,pwnG!Wwr2S٘a   A8ELXX)oS-(>lJO7RV<=!IWL('jYj^X4d 4PַX,<уKk.~ijNҮIiӦbڳgtcvCT*{Ӧ;L&8?(˲qM8'yfv܉r$+(zr9$,,+VovNۋSSSC.*Z8.`h692/.PHڳ'?pq,l?v/29uvVJZ_y%_vΕMgJwW\S,@By} pj >qYk4яF[h,d:$a04AluF :_|r~HH^{Sk3tPT*W^y%.>| wOFF}ږ-[0|@>믿ɓ's7pqҐk<΀7qqqlٲX/ww31F<-[(//w_UW]ԍߠhHNN/+ټy3*{/:tS5q9^$I?>%KЭ[7{q3gÍ7xN#\\tW}}|LE.TU;3# |[^T s0WJ/dg>m̺rg %$ƇrY޹3گrJIb^ס Z,X yb 6ma  aLH??TTPj2ܨ9 MZRG{gw$Iھi4fΜɴiPՌ;Vƍ0a >Sj:t(+W䧟~ᄈogذa̞=_ŋٿCQW]v%..|_~J~m<<<eՊߟ/hVXAFFv>G___Ν]wC=Ĉ#$--׳tf9uToƍO)ֹsuV믿N=())>`ǎAooo:ΝK`` ,\mDEEh"IIIa@kRR UV1d\;0`?>aÇh ;;?lܸ+V8U u|(NSEI$^elZ6Y!n)1R*9V_neTVQnd,Çy57tޞptU1.yXh(EDڂٵ33i] DPAA8OĝfApG@ ͌7Ii)EDh[V `B@( $IFGY_<`><==kyǀV\ СC>nf6^{5 |f͢k׮|tɾLs7ׯ_τ xСo&'NtXi[ڔ4v}Δ)SX,~̚5 &ؗQ(۬Z_kM61x`6l+ߏh}y'<%%%dFe?{a֬Y8pNG6mػwSkż >Xz-Fp\˖-c޼y 0];8,pNvyYl=FbNٓɓ'3enj3x dС|'Aaa! Xvw}$I +$ """ݻ7Ĝt?babF _V\DNq|OA<LwKjOYBco %0Av&&09nXUR¬C3AWPB<NSxM8< 't[9Ae~73!Ϲ0 nS=uN1|UN4a6vʒJ,F !86%02l}N|z%Ϟ7mvȋCxaI?u IDAT´BgS]^MWK`d ѽf5%Ƴ!ww.]\? %rrr 2?>oZ%#(h4݈w~ B 0 Ę3A1|p,Yᜱ}ҵkW>EWfe)9ѐqUx*]5ON={7%~[7ii;V+Kx#/5GJ`RX~b,BeV FiTOA][SO9q;$og ^ž iS=7 -IxחOy}&U7.oʆg XΫYEnJ.$:' BIz @>5=AAL\~UϬ'vgC|70PUR&AXΝ;E ²2zWWse`'b1P*/_j4 gO߾}ٲe ֭cN5/eee|嗤8aWu5 #6ཎErPtjٞ{v=)nl DQSbaQa!sZ]V5?##J\M,Sl2;$' FtHJ6j3۔sBfXm2*,˘ f4OIoB9ML>hOugo'02]2ROj|ti&A SNiݨ,${g6vX~swi&& <[Qo7}͵\:R~I`;n+ ,H Fv~$ JM=P(VTDl^:_eC)XJ=+(f鄺;_י{_Cфą>aSYFPk _ z!۸LaZ!~u[/ Cڟio(+[n$ޑHpl;d'gg:Մv 7ѮCfycTTO\8sJ۴.rbꮫn١2Tw boŨhfҍdh&G7OOpz5m.lVK >9o~4ڣ & ϝ}O p BAӤ$ƶkѴ9EaމsS1(JZ$ihBj;ãA[3 ¹~Qjj+E?P[7ƥqiiI 6$wTCBpS(YZhdqP?I%Mb_N%Iؚ2 @-IX$U*"A;QoxX@ςM6Q]QlqV,R~IAHz畳׽<#ط)o("Î6^s;47scGkٻirGk9RC|* *>j֭nzF9´Bl=@uE5uٸg;zp|VEv;ѣGYj9\q]\rsse˖-\-^/))X/_~\ BA346$jBoUUttjE Xz*AP7s  +*i XکQ%\F$IQQ쪩aѣ'\VIg̞^^Lfp@41hW[jvԐRSCNFjI% eeOVK7OO{VKYMT.[2qWNUvG׮16ٞiozJ Be7-dO5ظ},5 'OF$j~xjֲ~nWU*H-2wfݔ,`/{ ~_뫵lj5`1>>l6&MڡIII ~~6gݺu(/౎aŊ \.O {x*+e"%Bt4`b9$(/m$_ӂ ¥EghF/86-*x!*OJA//f2c]JjVRjjYSj9vt+'lִmpslTI%@vڎp {{:M=^޾4fׯ8ޢHʩ´BxxPu ,s+{~;ܛBa(jTjVEKCumiTaoKjZFEbHH!dq$Wn >9MrysRQF⏡!]EvZ{SېD(Ûy%;k(-,Fr;O;d阥jrhL $FߨzҪ)IK LmL&af]kZz=ث޹7itK=_wb^+9!]VU,-tTTd6ݤk]U2%Ֆ$1qD*֟\2ʊܿzٳJO<ڍo73N(++{{'`N޷bhhDAhĭ F_uqNK@//Ϧ,ju1r7Tju) ##Ay+)j}U1YSm G%j5%'~0.\ [ծ. Y[`RIDN\ct8r'x6>oӉLj@dt75e= ==^(8~{wq''[XmG=1;c(* K1v6wffyVnr3 uC15 Q)9! _&csc M )/)g & }C}m,5.U`9μ3gX* )gS(/&z~9mGhaoM@U*A{TtrIőy%Sg@n݊Կ6ř3w= .-t'PVyE05c&WXͦn"P6r6k{7Jw'rWnp$ #EE$0258]6,<<'\.'== 3'''XbQ8xpqqaҤIX'6""LƁ9r$,[$d2ڹΝ#44KKKݻdѢE9!!!L6MCCCѣꫤ&KFFsΥ{òe9r$[Ls~2իWOѣoK|Af3yVV!=7ڑPZH7>U53 DO6, V-,+Wj%`o g [Z7W()&%[)ϺޖrmrieՁoӀ PZՠLFBAw33m[P?\ u2pƀIk_(~jzcJPd7w`[3CYlco{W2Yi0B[ohl=7weYEհ=u'I]oJ$?9X'CqanKlqI&3gǐjWVNu%p<'w$?3_WW<506 ?aVoQY{R͙38SgN֭C91.D\~XPцX}*w/ח4<;Zfsm-,1VS_\kHhVZEpp0sՊvvvnwzԩSy)--g֭߿U* חu֑;رc9tp>C Qո֖^|E222t~Vĭ.&&O?E!I/^#"" 5/_Ndd$+Wۛ$m2rÆ 1wwwV\ B^clٲ:H 4PKKH,{M<]K ?S 14tԴ[AFìxj^ffW+Ջ_~#AdLF}PS]U>:|kO>=}8!2.gw=#S# ?_;קMf7H5͑-Guvq#j{Qظ~O:$꿐UAwj)2.gɅ!O a^˭ <=nbaBzQko/_¥ȸAQn2% ^@{2vXN>Eʹ}>?ɸѤVGERtg#ϒS'>=G_G}u,v/Jɢ 5{x5vnQ=043&39[w[zEl_k#± M!?3b-?,-tO'<񱥏T'&kqXpj) qʰYXz|7>n昛W}3m;/^?}vCreIFFgϞ&ºtBpp0W\M;wĉ'%%$&&c:T.cgg=\֯_ ZmLMMDÇ F'. 066'|rSH$A$I<.Ԫg8 L*`yju)*U)&&~ B5#99:Bhؐ\.-ں8ӓid80?v/^X OVwXpT[O^YƢ$ht3WO VOdHv*dj!SC3Г7=<h|2j5SWLE&,Sr}.U>|ۦ.yyL=GsꊩZ:Z6z\iS+ZZp@9vП7xN!u<{49:dN %tZhsz. o_Ǩ5+]C5ک':Ԥ6cZ|'gadfs?O܆ƍ̌p?55m"&&B3Vټy3sip7ү_?*@駟x%mۆ69p6i mr 88RSSqY^zk3gرQF1m4|}}ߐƟA[ B3 wtdŋh'I| /QR2$AAhU*MH@nULP !֩4~vXRM (Nxӓnvͽ^DZoM՜((`N99%U$ht[Yrd$̌ ~&&d](o}L&ʔœBm"F$y?uxgSVR$Ixn&_poǷ' M )ʽ3զVpj5Æ cŊOUǪkh >5ry-eBi̘1i&lB@@3gdҥn۔@"AtR[:Avj5L l140m'3fԙRecbcbK.o߾E&xRJKk%GLr%~ Zy-1 &eB.g3]\0׾AL$Vv:+|{ǒ%j5Ge_v6(QJkV z2zPOfjJ_sszTVv11AZrT{}tʀ IDATHwc)*@@ { }p?:n+ə2<3]uiqG>|ȹC J J05.stO U?쌑Mo?n8 h{{ RSSyoj_Fbk###)-knf&&&L2)SzjΝoq 7`K.%=7ng BjF>_|SOݸVєP@.71 ]ImPA}.ҥ:$׿1 Bsh4ldŋi֗4xݝ\]1kc}37H^^VRR(Qcz@\mBh~> r*UX=6=*چjljJjmBb B=̌;lKq[c^Sg}v_Chx 6nȨQo߾\|_3g6:sРA2j(^}UrssYd }Y)v튗'O_'77˗cbbsSFi*G2e[zf͚<@HOOgƍ```@XX+V &&,--k} mwDqqqErВ6ߚArTNyyzzVPdBkWU17k,,YBzz:SLa89UEٳ믿bС<#3F&8""0"##Yx1G% +V̙3ٻw/W^ysٹs'%%%!VRCT=B[r07y.p8/**r@_&yWW^psúqIxˋ"XFZ7~~o1I]^<7;T\<z62b%*tڦ ݠ tuu%**3|222pqq!,,I-Fe2<󄇇OHH|Aq7}vz)&Mo&M҉9u JQQ;9sHNNӓ0ǏOLL F"99xL4Ǐ'88c,,,ȓal+ ݙRZBeHt3y$I\@[ _%((s"I/d7pcffƮ]xWꫯ?~}`kkO?;k׮믿&==SS:NJJۛ5k0u&?7_|K,aJll,(((' 99͛7`nn^^^1l0f͚]MZϫdg3kAhŋlDn¬:9 $f;w Fý'Np8/H8;;6t|S:KdN{9[Tm*fBp!VV ITq}Cڜܖۋ;w2|poΰaZ:AԟWQQQP-hHH eŴcccYjqϟǎga)S0sLΝ˸q6zfI6'' BϞ=5k&L.!B  -@ K^a@f8-) rʊ/x }]N'_5^O]cUVTrZR /֭[4hÆ ΩoITAZƵ2^KL/}~nRVIJ MJ"_s}A@ft Khb챼ŠjU`(QVkRfgjBгBP$AصkyMfff~3gj ~ B Ir@̦ff=Z:$/8p /xc}˿GXtk }}}ƍǸqصk?˖- H ܼW.^XtQl2A MRk4lHOgŋJxWcn17m1 d*5@ZM'ccWKKu~I`U୴BApwwoAhDP:4}­ e̙lڴ OOO֬YfM8+WO0}tFANNcǎ-X={ҥKYf ]v 0+94˹ pkZ`/fʂZi4vfg3/!3EEHPorؘ>>ir783W\67 AAAAk B 25툩iǖCh$IcGy8^yJ%#Gd>x`Ùv$8޷ ܊b_ȦבKz*倾L|wwws*yy/VV3J\rrZ/SSS􋊰QK#PPnbBaaaKsuܙsεt Mٳg}<**JT mW߯3&דcfm-A#>(lŕRc=}?diy&>^+W2-07 jPzEXL hPAAj B}U_LhGG3_q-d&sj) *UDaۛQ2%q\9}ٿβ鿛PXX3X:B.\ Oؘv"q, ;ٛSk\ P&A B5 x.!}994jzzt''o񵛫T5# FÍ6aC yJ^ mn0++vU"<]XlY[h`@EEq  #NdP=[:Ooʥ\M3H&g]cD_JzAlmkۢv-^VV3FgyWCfH-sx W]\̾S3'|V`9n=0An& Bh #6fCĚmrߍO`';I>LtǐC(.d۲m``FaaGʹk@cQ8:{aje g-_'OB;GB]ȷ ж)5j(B,-)Z -zYo$%Ej*j)*e2^tscpHL6=+ FQ}ê %92?Z\ŎjUr`K ȈAA"\H,U`'M3Zlo1W/KogBrq!< :wVHKc#ML$o++Pf=ߌ"wCi}Ÿj+ꪮHJ"l:{mۈHJ"<ҵ+t>I99x\/'3x<ӫ0tKX]qrDx8='!r6 PŅ]Ool0_AA څayi 2%I1Iڏ{썹yeUi+I.\jNqWOO&]R+v&VJŤgSӶfb"N< BkTŋ@#"A4 ge\|<%%N V1k5תR!"'x:*Κjsrg=-,0hyx0Z@xe~M;Ԕ HB]gRM$p;x0EE$屹2SRZWQT^7GOj*sv$9/޹>Rؚ05&ۭ, te{s,L>ܾ66l0+##N_I# o٢E9hM5.b&zԔA|_{O;SS;;SS'%`^ xFR;IG̝9z+ 0ӦLڕy`kbŒ=X}8 Gfq1VXl,ڿZh. w%@hW [W7olT|0H=OK1ǩbv? 14?$bMDNDh$ ejѮjAYz Jeq}zy@DLa!CbbuI $|{ww~SAF\f? a}z:t`tʿ %׮]~=|ީ59fiIؖIlaμ ?wP&Ui4MFyy%>fiMM1j=JaC͍}ogdž'Yyx6ͳ}0;80[78 'O-.cәOOgg|wcdkM&fd` 3c:Ϗr,Xc%IbqX}\]bZP/sjyo`Pёc==B_.kcc2fy2I?AA#FǏ3烈ZM|fm;   ڇw\S+oWbb^Vjh #S#J K~'hDO(Pݻylcyn əv?LbAw7 k##a^E\XB)}[p!֭#11C!$$ooo[Wxx8.]b߾} Cri)_\k=7𢛛BrLLTd3h"ˋobNp]ƆtRt*'jgedGGؠ5 J$^d1=˗Y׹~lޝcb(h-X/3$:AA: ֩ ptPO33U 8ɇѣ9bW ttdn߾O?Օѝ:15 F՞9_GcI?jzٳlOH$R)U*.YptNe287y91ϏQQ Ёv]ɓoy QWA% BhwFuŷ`O!BH8k\>}g}3˞i@E5yk 8?w\Z B4`jeʔSXrsX$[;ikWzGER׾2˙]6|ߺu+:nڵkyQ裏PJ!!!xyyf͚En/ֹ/ձ 4rOSSy=1Q.S]kRN'W;[X]t.)X_XO){_kk*ds_Zoȼ`P|}"oL'Ah^$9zgO|-W 2* _kkBos5ѓ`mdAA%bTh*? UPot/SWLYO$OPL|:M?+qsOgp#ePY CCC;95y,s;s,F$IDnS# ڝ5]k\PBӔvǷSiKKKnBҲYڕָP%m|MOm/;R-%ݶgfQ&$Rpš IDATZ{1={iǎ&ye˱c,t%%Q*`l"//.="AHX,Ua+uncÏ]$Tyyo|I8))uˑ'`a!ڵ˙?>?<8v'::N:ĉɻ/tRKJJ ,X{ ,Y?LRRC.\?8s^f֬Y,YtLٳqrrjGFF/fϞ=$$$O?ͼy9r$DFFn:"""8p 2?'Nc:vΝ;=>A&$hѭ"<~Jla!sؕ[ J!+}}kk`}ji)_tGƍE $LM[M5?#z")ee@9|ku&Hc]L9{V|?xYץ Vt~0OOHr2>֘b ׺LSY%zC?~[Xp.#Y=j YY|qV($%ݻ}1{ff#11#e Slazp0N 1W2wChЁ~vbRnv<{jHV?N~8}.5)i'+&U\ͱ42i骝ž$ed`kbgȌ=xq.dDx``1\-(jAgfbnhT"ZMr^Qii{ |1rdWAI$AZ2"sr8STTe(Tx9޽U]e˖_3oƏǙ1c&L 77'Nh$>aÆn:,*/>TUqWG}oMvv6oSLaǎ=?9spUq3f0}t+6o… 133'΋O?eѢEH /@FFWy ,,Lk˗ʕ+&))46l#pwwgʕXYYi}?~<[lĤ֠Dfnnq5Ϛ ܌r%%IJ2ue2^y77y=lTdg7oΙ3gNbϞ=m[._ >VJ;ԩSY~=={w^MՋKx磊L&cDFF6XTҳCO:I9ܜCAA" 4r*,(XQ{{ |42uk[[up`5m$^RdU\.'XWpŕ+<B\]yG|!NNN^J) kpZvc:aC||Z:[r.#O?ex8[:AheX[XHfIA툊k LL2R9 $;m/55Ǐ3tPC괐b˖-( aÆQXXȎ;x饗x饗۷//&55U}tt4=X$IbذaM] A&4i{2m<3>b"##~v,((H&%%Ekի7_~IY` Mv7u,AhMM +HlGVǎ\By$^q8w]Jl~!119{/QƾXZϏOm3Acynn:U*l!\775<9. iK #JKZR-)S;w@"9( v> syV<JoVڣ˨jMFLLΟx O?͕+WgGuOK$jj** mX4fRRR7oK/5iۦ>IcmmWm#Aq$OB7F}}O^,xΜa_v6jW V 63C__gw` S1oíqqZRSw2e tXnn_xݔ#@^0Ͷ)>x#  Bs BAVp323I;Bу?S;hصkNR/88ݻw]동vO='saŊRVV/,q_r3@Vk+(--w5~MggРA5K SLa˖-XO>2܊Z,6AS4 y{tB;PV()G++ {A=*/qO::jlj`Շ` nnݛzYWW n9Izz<2 n'Ix{3F[{ ѼVr၁:{NŻںՂ  H " ==67Xzb"1w0*5xyw8t?0JRw%))dzuVرcӧOBfΜɾ}x"{?&F/̢E|gcWhj=faa)S;w.J֭#==:}vf͚űcxWYf &Lѣqرn֬Y$$$pA6nHHH8|0111dddܖ*FAhirigg:t(B=3NG()r~bV}pnc޽yEePS*_R(sgsWWSJKP7R/I:t`y*'Or,  # Lj F#ڗ_|iӦ3qD<==5kV cǎQ\\O?M`` ?/o2}PU_9gx'cժUƏOXXFޞC3xӳڰK%%9uN"Vٽ;v놯Zpptt4ɘDLϞу0jC LwrVj %%2Ibm17nrT*k۵ʁ33ѣIkgq.hg9pwﮭWnjyZ:Az-\::T* QQQP5Iv~?5¤$.'AA`\WזEhvggQ$&RZOrbm#)ggef6Z--r%G9ĩ^|W&<@٫"rr>Ӿ'Uy6>   ~S%j"Dt$ RA KԽ/ɸ8v4pAk)BۧD.ot TI)-eNB_G&&|٩.|S-XڂrIBed,::b;OMw˩X0ʪI[7EGW9LT:Y@AAڤv h4 3E$X AK)iw[b+&'+*5k< -XfMK mؘ-FլJIDu+%$%x0199ȸ"Q5Ɔ]\D&nN)ml؞RA!*?`Iga]0 x1ٻ{oH $bFkڊhhUUUW*vĊ ADGn/7 IH|<<8s>snsޟS=AA*B===A; u(SГ0,v'%iRH}nX `aݺDF(Fq++R ]-,hljʒX-^WGOѶV_N%>|Ybw`m|77&߹P4nLrAAAx>B]]]P37]AǺp"?H&c'm.]* H˙|xxT^GAR)--s.!/Il_+@=brfJcQ&:"P]nhknΩTgdT|joOxV5FJ]ŦMtLֿ&C||+qAAn/EPe9cYll\ `mBCԡM=Aa.EǒT勻wQ-胂PedDhV{Jݮ0#mlekae3m-H͚} DDp#Ir9ݮ^匟&b~R\'Np9!Č [Y͍i[cXVڭ_Z0ifEK0ח۵{6W_׹l J[ggmFgff8ÃM# NZ^Lw`f9Yf[۷8S#)[*KGA*T>;F NRYj0l 0Ǔ͞$j\77|FjԠ!\b}Bq:Dvoo<5B"43A(Tk[*[}_zҨQ|kl~F?DrvvuwZ U|:0s' &qgx6GΜYiA$_bŃxUɡI&afcS/Y֭Wr),5Z7%p';QQuAAx%͛s gz.32 rӦcp"0]I$ _99iUPT,Lut8ШVk `Ç|VlBj|##/=J+0;ڬ[yTFL:s&а={haֺἶv-sƦMvEZϰ={p\ ~>oeS~qrҒ//μ>))|?O<Hg$*5Un޽O͛x #RRΜ[xw.,X@_e͛mznB`d$._F:s&ҙ3 s+LIaHgD7{%KuV_J% 2^l[NW&14f͸G'+ ‹Oi Bq.UM+[̊$,+  J8ΞeCBPz9,J]HI$|dkKxlĤJceE=CCM616GEeFЗJ5Sbb).z,<//$ ;BCor~`=6z47kƊ vSY4m͛߶6>>8qNl3f~TҨQl2J}Jgdןn%^663[]CQJYz* kcc~ۗ+}[^^|y(?vԵҨvmmJlZ4 a$&M޾1̌IZ0i~Sj҄_.]BU$3[xp_r^44d| C.* K,(:֭޿7uF[nz:G'<0yn.w4Ljxf]lyctVA%o@|BE*|PZ ƹ빴u+ |[oi׮uxKVO푪sX?p 6j_EKߦٻb`fFg,B߀̛W?g0x0N+V~׮ojs_3Wsb20?u*Օ?- QaasD P%B33yMΥVZ$Lx{{QKT"K''pU*YT'  4իZ0\ xҲz-XOlra@QdJ z=vڴ+fxÌoтIZШvmT*_?vJb"ڴ-nyoequ#) =Ã#wr;9+RD·:h]-,HeL~5m;2C=o`ZX= Cj=, IimAμr% CBw‚._~-Wv`uL:=##sru4a cĉ@ɺA7ҠgO#7Yć ɡć/s]?Dcqnْ`:'xM;aGFo!}=T*ZCN:}:In:s|4sɓO}9r%}.'))Dl2ٷS%c |krxJ =|H/ͯشi.\`ѢE>tWSSq07gÕ+8idٳP$NR-s9!:ueKoۆ=ge%zc̘3.Xo`x8IKCTPe]2ڵ{'8 IDAT9? AIML`իGDJ p pUP?ŠSAAxrwb99$޸Acm"#%C{~=Fmu ƁǽӧQ弽j8lTG}Ӧhʊ|MÆmC|"R4s'0-PslbLJANfRU*|~zm >~c C_]ǸH0]Kgg1X׭G.َ k`RTo _5֖JdJS=O>dĉ,_?333J%ƍё,,,*ߞ={deoXMڵk ֭ N2Mdg4R Qn|Zj+#5|?x;k?1Gp7;aWpiSj*Gu`pZ6lr!>>ssm==}{YZ!}n-1waQRD'@z Gڴa`%55`v|ܬ#4iGo_yz>yeeEB_aa V?w(jԵ(/€ۙԪtҒw2v~2fZvM$dd&8/cngutR,`AAxrB]Z~FϏ:ӿ?/!!ggX/O+H"rrXDŽR^LGAP0bE?ӣVZ޷FUУ^nn. Rr>sU(;C8kD*2R:u=23GJ U3fp';}OϧիlxЋJ"^FlrNN}HSn`{h(r9˻wG_ bke޸19r9ܹC3uPS}}ZZr<"eLUbkjD&ή]TSȈmׯ>$02R+;CCQTϣ?dt sstY͉OCfdcmٙz5k_Ė-ՏruD R{%wSƟ8{7[ƁJ,,o߯3az̞]pyM:T*E\TR}cVwojw&+P"h9LiϿX880qz}=*m/Uj3X^rB MwÆ c HLLdƌ1z|||Xx1<(w۷nݢGX[[cggILLԼ~qR)NgϞXYY`"""Jjy&۷Fsa9sfv1|pso߾=;wI&3uT}'|BF022͍ гgOٰaRTR)WfѸлw* iOR3;?da __xJ::|dkKaN \tZSS*ӓff*T*.ed3r !<9 #E7ur"W`\KL@y ~v ޹}Lk.]8ɡ;w7o2r^“aNGEq#IuضǏ?HidĊ9ΐݻ9Ixr2;CCymZիΝ/-7"8>oNdDE$$0aNDF?*JZupvlL WH*ŅcLJ")+K&M)Bo?|SSU(H 6-s11<ƫVa˪=u  ^!1o~Nmde>|HfRZLՙ;s2yYY9u:gꊞ.hiF]Je{t9TAyb._&RE_ĄJoS^5k֤EY 3 /e˖d/Wv^tt4M4ё۷h"eӬc5ˣGo\]UVY9{{vlf8&zz''3pN\@2Ba0;a7F-3?GlNc8"՗6`Wi7Xڵui¬!(n7K\ҮD"ѺVB~yp^">>$yzRȨӅ ZE-[H$|w/~VZ7: q[wvb{h( ;r# ‹+1Ioީk]Οa4ݛs^䤥tqkW\ZB˶ѣyHsRJ{J!RJd%ӣ7}}{̌nB/(q[D"re.wꄞII%?2(HsG#UlT+ZӫWcV6==Q)\޾55|öQ#50xU%HϠ `E\ 3Öo VիWu $))7nhϏh48Sj#""½{8pښnݺylܸ;;;BCCYl166 >8{,;v}8=== K}<<90ښ d~.ErMghpP"մ)6Z>]kYYl!ڴaZWkL;gR_cbηz)ޗfbsnnZxy奵uM,v n\F} lkj>}JlW\ҿiF|F 3];f4a])AeY#PH ټe wg]J P]ݸT{Zs=n$8+С`q׮8nx5jϸu+{2 ''305_~am@'#/]'yXЬgV.=AJfh~KO. ω`155FTk׮2߾}; /l۶Mk۲}{Ahݺukܸ&8Gll,qqqݧYfҴQ M05kШQ#K+zăQ1OA"^ BkkZyT:Ʉ(Ϗ{h/les;y~CK43~u^ݺ7 be^9FZW֟rnْwh DAxNSiʔ2lSY@\n֜ GHNeuuO&J]x =D")54ExO:ޓTkY.-=}!66;v{n|||3f +s\AMRɼhfFD3Pm]]>stCLu^[˗Bs33Z奔9[pfRoHW6źHGA*C1_l˵kܻYq- B%{2+˅-[p;w8f Wbe+A 2գnF^~‹ϏǏckk֟8{,ɚu.]"..*X2W^DEEqmͺ@rK'1dvŋY|9yyy@A׬"s ΦsCR5X8!k'ߟ"82Kd]BB;ښe% /CCQ廬Pa}GŵAA*BRZ|<&L`UϞ;}~ʠ_~n U&,-K+lV?r>}:R^zyfܹñcOU:m۶XYYѫW/ǖ-[xhٲѠA\\\x4ǚ:u*FFFZ*, ÃWhcӧپ};ڵCOСCΞ=˕+WHJJ,FA.3mZs;+ ۛ[͛3=|¡Ho0:xF^VSRq^_AA#RO?e©S|y_O7D%+L"Cz-ݻ` O*iooOpp05k/aÆ9reJR?9Æ cҤI4jԨ+~2x`.]ʬYKs*f,n߾MvvvY܄ aȑxxxl2k CՋZjq̙ _:{AA,Ek^_\oޜ~HY"Zx1$>upК+0*7>K$yxfhȦfGEAA$ŋUw_ *=z0髯; `,2DB|||1#"o""JdHo\\ɩJ*ޞX숉}8{ 3IӶŋry3{UؘukBV"?ut9e1Ugm(Jbcc7 e3fϕyys=*s[0ёY..H|y }}ss9*GE~:3NΦ:.%QNN(trԶCRKhf&CXVaYY)* PRQZ/A@ H*mذaDFFrر   A_0fФ 0,Ǘ,Ν*Pqvfsb|S!JŜHԫWM=f͚W_}EzzzuwE*ڵH$dUbپ<'OYJDrԔYfUj߄#WXUPN735 _}^|2L GEgحl¢[Z.ӹɮ=ҹ-PChf&YYffr%#lrԟR EiA; (}} p10G}}Lg'%77}y(  |::6V<ƢP@tu+ ,*sV1E@@@@uwC*v 9nWֆ Vf}*5֗.X/>wt;W*k[=7-G9QUW_:uv.` X]%B= yRrdu:W5੏Tr@lMpؾJ 2 +xiQ)࠯gdD]CCd`P0a13scǎ#11!C0n8l%aSRR7n'O$99.]0h 郎N#ǏӡCo LJŋĘ1c8z(Zbʔ)4kLӇ4 l5]\4xȠ thԯ=|fŋ5wbӧGRR}H$9Bݻ3{6W ũE ޘ6 kkdI'oC(iл7> &-uέ[ǽ%'- :n[?RZѓ+V~׮ojs_3Wsb2MM@㟺}Lqt䧸8ڏ>,ݫg /M)$@+s*mHdEKLrpҶذ02pi:֨OO1Hx*f::|dk˒$'s++FFޞ>4@Ap.]_H/#hTr;;[Ռ h>{u$T*U@`岔/, 32I_ ƕ+WXb3gD"駟ի7I IDAT| &&&:thk|G|W|[Ѻuk&Nٳy뭷w :`ee… qttd4mڔDKsDD[g)~zlի ..P̙CRR111ڵ 333$ ?]veÆ W^AA+ H %hFZ1֨qxv&Pݝ}2q,Pl4l GΤ'&RdިGDGS71Ԋ4OΜ_ӰOlڄi6b]o{4L\jߋG"p_ߩ}/&!45hاWvԊ:nݸu+-:NJL {ۛS`߸1;e nή]o#FYp&RRy,g֬!Y][>d&'sx\iZ;ӧޯ>}pl֌sobBІ 8%1sru4a cĉ^נ ۶-o.X)Jo3o.X@NZVdGΝ-hF\Zҥ=}׬B?}:5y{J ͹nѳ{&M"ܹg*ȩ+p!==NI!:8fCDȾ}ta߰eS՚'Eg 5uu{ /c))%^FFT]J0I|lk!d*̍2ˉJ).[66"kPx&YYKcbXQE֨77&޹|OpTTrF!@`DN&`#,+T T/K)b/, l`PA'IMMeƍةL=v|ewwwn߾ͮ]J ˜1cOSN+6m۷W۷o',,`Tٵk^}Ã5jT\Ϟ=K-x7prr եVPĄ͛7W=AA+ IO…X8:saG2￱VU~}gmǍ#d^n`֭8KjX:;S8uh]} },h֌qj\Ӿs˖ts_ښ˗G^z:g~R047ܹv0n>oi||HyG4 GHxwFM_)a>CL={ h9|f[ mmo_r5s't:&M1'Tʛ  S?- Һޅ>R.U54ĹeK::6u36ߒ%cP=к^=VIj\涶OzJ_G_}x}hsrHq6cj2X-aZ&3ic].]mÆkdT*eI_XCb^V0_be\ӊ .[B)J$R}E͍BJ *Mك=J3!<ڧ9=_ eIxkj`z|ƍk~~~uݺu=zӧOr9ݻwΟ?Oz&++ "U~ kqΝ i2*bԨQۗuҫW/oI$MPQGˋ551FF406ƺ)  tv ŇJ}jm'CڎG֪ ?fuhFoVnvZ:tHd+֦MBD{NZl 믂q)Nk.ڵ+sE2Ɔ+W4}v{:;wP)({4]υ,,4A Pp:MBkXk?yn.7(5@v[䡉kN"(?~~ @//Lk}iyH$R0mhߩ>k1ӿ?Wwf?ӰOZx 'CNN}[kX$  /s>Wx KE%2ht$ժ(;\ NOgVR ynnC*J4hJy)IER~>32LXHJJ}+,>ϠTJ}##|66.xml yk[H&+8[`xɓ'1bsaС8;;/0W=`q*kͪFUZh#;;"P&CCC>s>'nb~"W:uJ%R߱be 焓PKE}j.o'@/ob}( D*J~R)Jf*ujH\]屦O*fp;RI-wwZ~C%n HԣKDT*qs7%q~Jw%d^B wBM2X880qOlc,@=gnc<(,)1663AS`J 2J* (\b|ncݻP% d^8AhafFsSS.k20jT*"rrs!=DwS)?UW"_md1L0}͚5iӆE^y@@@7`888`nnNVg 7dN9ʙ4A6zzP411ȨZ5ǼyHLMAnii̝;W/-[uv[\Uۛ3?^ݺqxnܠ:0+9+Wʉ~NT,t'0"gT*n[s)|4:uCPӓ{料E8gϒ{7Rx ?tu1+gPýcG.A^V&yԩr٭l!;%C\ ׯwة˛_) zСRT=}Zkݝ@l5ZδvmGvM@ƍ gv׮Fǎ5?&&Tj{B2JёwhcPP:u^% ))%,yy/]tR &Odyl,#"RQn39:#~w Uh#_,'3)y+._$4+\bz:32PH$Z&@PeX0ZG"D +}3;v̺u +sky 8K2b>CwNJJ ǎݝ~_ll,;vdΜ9n/iӦxyyκuhР/:tܹsaff^ gCZlM00{xw|^ yHD1MMilj:h(.}#QCWnV.PW7@XC&xGQT] #&8M »gOάYßN߱#O~vOGol+0.5XTN{ vKù8x~狮{ǎ6#ѹ3wQt6mƎ%䯿4dMחna`fs˖j߶aC8z5M "9.T}.Æl=o?H6mJ-Ҫ2]]k}Dҝ;[ܢF6b1\ƍ5Zb%Kh=v,.R(ynu+};:aZ6%2(Æi?2oW2dӦ|^ڵJ׬}gl*ekˬRkhgTDRj&`u ֭[L2\NϞ=oH}Y>OZWT*ȑ#̜9UV1uTj֬I-8_~~>aaarQL>jժEX~N:1l0{=Y~=C y ‹T&cP~7;Ь,gfr=3 ZBJ B23QǣMMS }ML0FLY>Z $s!7%\۾BDRj9˖qfBS+WobBoorR[pt.lıE0kU3{6̞Rqc\dŘ0pΟñܹl|睲ϥJ׏X.m;poߞfCpnZ6vv|{ΞM~v6Vu/Vu76~ lx1+%KWؘ{ptNf ͵ʃGw7n7߰u(xzz J^V;ƏGTh5rܒ##9~<))7 CjJ^'\/$GDgl?.XӓFyܿuv&v2o&2:80#"Bk,B A(Kȫpte11%@#VaYSbngepNN.lq2 ::2W0"TO홤@MKǘ75tҋdw@hڀl<Y i[-^֭+]v( 400`Μ9̙3Gk9Jٹ:((ϟogg)iӦ1wQ,/V ޽{nK#ndoZYiU*dg,gdp93&Q7fSbwMMibjJcb C.^_uؐRaܣB_?T*#vT [o4:֭0@^A f"s>H$W[?cdrcG33j e~TS-d^hfxWz5ŀ2J U/M.gvd$cbPT*(\ E ;Z(J+Z^:d6(x0fhH uHcu.^@\n. ?#Y3,^bggGLLLuwG**_FVfn (G _g m*5ܜ4i"Pۜ1sȍLiFjjc ~53_5II\߷grRS /b\OFL.lB-wcÛ*tнw Z(C҂ DW!T  Pwti6-mS\ENθs6'~Ύszzt \UA.;ccH=FQQlmP <2Bt.) &+|Yhhj uOR;wHN 7==˫^#e'%}))ȋʊEINVVx AD1 Jv˗#Jiݿ?nC\5 28dФsggϦU>uꃙ,ҚJlOJ"<7V B)T*IIr-)!Ģ'ݨ2qa0'32xMge齍p75ecVPkR 9Ɍ 2*hۙH$SdUl`iɪ-x)5)RAK"ef.% geq:3(Qܺ/s05iiFFt5ѓ u2&s{{è7zΞMٳ ÛǏdbwwVh_FGP <.ggkZ#T-${*DD(cK,ԝ|޺sM[WfͰ2_لR˱4s,-|@w 7}FhnfƓ66t5Ѝ MNfoJu͛tq5r Ar21{{,V(Nffr&3}0Y.g_J ,iҠݬT{޿֭c <_7A؜FX %.qq|ڴ)梕 BKKәSmljxWf)1J7̽s~uk##6jEZQx)T*.egs,-iiq_N啮7(WW9;ںVoJ$~S$ F]Fh@Ax Yd$@ /h=NKޅ{4 hR srHK 4W:Ar$%AAZ|ikѴ4#%p:3KYYJAG]BAEzd,еڋ~հaO(lf&c]jNNRn*ZXȉts$5 YY((t%.%mlbc4kРͶ"S"*T 8p>WQ q*a:`&Z* z0HhciIKKƻP`wΩҾ%w.y #Ol0V@a^!W^լW\v LJB^(شA%# 455[ss6A(ȑ#UpZ2#ձh@Q <-dJEԄJM-DS3=-W`{,9dyםΒfDBC).?E ?SSQ.=>ՁJTJwkkncCU23=ܿhbbӰaUHliݚӧQT(;}._y{I £Pj3ĄY;;R@^0 |4 lƨ/FiKOLgQTpg9$׏_~}T*O`@$s\-/2;Hh:Ҿ{ZĈ /ݚT*f[Q4ǭѫ}VR~{違N IDAT~;GDO&,i9M<>#k+> ~{0gʑ+$G%۟N4kDcaOj\*+F@R_!;5ÖT*qmՓ- HJ438Aݨp{OSSJź>k sqRAo'32/֩}Xrm1W:Ƹ׉ӫb 22bk+p=<}|HMOE._y{3M@ZWDϟOHH 6mʚ5kx_cǎ%""#G@d b6r.\ʓkф'3%+wK.iS 9*/j4WM^ZRde)+@"y9ˑdϢۋt;-D"?RapUzsddj:hbx"gG!W]o~!\+RSu^[vnvJbs=_VOtAglBH$A#HxÃwպəP5)1b.BA15XA~'7mIIeˀn66tsqՊ Ỹ8dα [[6j[=& Խ2289z$ULF-AE KKd5\ޞD ."!D77[J]k׸֩c}ceui׮]Ȫ8pݺu?^sñ؊+P(bڵErKb̅p@{޾ ~{0'Z4JK^\3s2rh،/wǽ&qHGz-&&dԮSfrziuDWu;49mn%CXΝ;#X|9KuG*~Z?NEJYhh(RH  T77JL& ABtffRu޴]b^#kޔZ/?vuH"aIf/)Jť,FE', 'pE)J~2nkd GGlޜ :ֽ;iì YTXW$ k||76F:IZX74#Cı-V54X z4{m1 P>D"ɯ7bIHs%vlNt́x% 99Ds]}m5Qx< ”f͚ń eҤI̝;3g:44i҄ Tb)%^ݻT*رcU޶]X|yM'uؘQ..ZIB%of&  Cjv6ʲ5e 1>LJ^]W< y /]"P /33N2h|ի8mϜa;LM%o^ ?7FF urb7;v$[7v'u36M+.)(y^W`UY_ԵB̙^^^e*N>O?#ݺu'`w2eZhZbƍhݺ54nܘyioҤ ߧEꁳƍӬ7vXٰa3tPضmfT*E*jo[Һu ???.]-f{G\-&''3sLڶm9͚5c 0cǎym R)Wfԩxyy1hРZOQ'@`ljlK O$NzP"hڋ* 2at±_j\ٹmqr<6i'PRa;y9;$<[yjWRw_vl䈭 ֽ~} KR^(޳\:H$ u cb?DPꫯjU ZXX0*=ң -,,^jr2QxLdM)F,CAP?T (u\(w57v*͉Ly̢v^1J֝|VbmCiK[e14-C[ՋqGccF:;91J6&7Ƥ~Ύٞ,֜in֖Es?/^̏?Ȝ9sfńj}=s ݻww>#::O>'NyHKKcݺus rfÆ ٳL<#F5K$VXA>} F Xb}c߿A_2c ⋾xJk߾}L<'?cϟ%&MҬ_͂ H$̞=dV^]y gϞSO~,Ycǎ|r6mʽ{4M6ѯ_?5jLlgggذaܹsss) T:_ 7o7dF2lm4-@0E+Wҕsk"/Er?w]"Ac,oIzxܻЁ?صhWƩIIs'.M]*ӧy'婧ܹseꫯh߾=xbm~~~DDD`ͨxFEF3gгgOJWt=zޞMdd$C Ņqq +6]6l/ΤIhѢVVVۗד G5m`R1TRSGT6SjR%]>xr9?AɌ2횋u6kr9+uV0Y/BŢ/_FBQA#]~~|۲H> JAx8Ξ \̷ 6R&c#ߴhΝڕ|}NKsz,6C뵥JN6H,7m<*t^asNNʼyڵ+;v܋>?<'00tRvI^ɓ'8p ]vI&ݛp ·~;wfʔ)cT*,--ٴi]t׷ܘٸq#d|9rLLLL.L쌳VҒzʕ+С| Ǐg˖-e~z @0ak1 |||;y$z"88ƍӣG'LLL033ӼV" -ZT鸂 J>]SN~@{wjޝJo×Y?{fpmpϴ +˴L-A[gdb)Qk_nu0=11{uxy*p%G999U]TT3s璟_|]8zhӫW/:uΝ;tF*J?Yf\tŋsc`ȑ̙3GGG233ر#O>$?#VVVX=zNvؾ};? سg_|~j*4yڵkfCll5ֱcG>3f0vX߯9͛7k۾}Vx \8pAgg`㢮hHX )mzZ٩q1d".Fa% t,璯t:.\:^ZW3)e*ONl;AkGk23%2uitxuኹ9Z?p,a=v Bkkkiiݺ5+V,yDFFҨQ ?gXXXL&cZ曚{yyѨQ#x 222LVVB&ֶm[<<<8~8O>}^}֬Y 6 3g>3M̙3yBt輝;wҹsgMk;5H$>cMΖ-[;w.Nk#-qtto߾|aaaL&|fddKQe۶ml۶;p!8I Դq66&PLlJH BA .RPDBvk*O䋨(- -d2&qW7srw:*"2מ6m,OO1#PDz:gwRNʗ2%ۆ6iЀ~E [@P$ ozz2@`V|ԛ70ӧ]Z2 WjZN.B <mΝ;gڴie{Aa? _O? r ԏ01.113aS8׎_#56|,-qh@Nj}P"hbZ#=ZͭG"]g8 $ W]}>=/Iy9׎_΂VOqƄ ѹHF?i5*<o!p9\\*Jv3*~tEk?]#Ξ=͛9q׮]CTRPPC:thboo'O$}7 X*e;FDhnv)bWm v:3SRJE1>>8P~H &WXWu֭i_CdJ((ٛV*T_eiqeLƳvvҴO1Xl 1A:+ }d2 B]*]\>fM-Ly'y>ݶc{ؗ9^@zm1smJ7KQOeljL:U ITҧO-[V湒KG"TzAT/ܹsiٲ%tؑ U(m۶2YWT/ţJ&J*9wÆ cΜ9,]f͚q!^u*L3Ҳ*ۏdZxZPؑQ"Ɍ2mADJŧH(H$!|f&c_rvv[Xּ#|x(U*ee/%\BZUi*:[[ӧDP#ZUj._͍*iӦH$iӧ_Xc|ײeS ŋG,[ \=yѢE< 6_~???n߾Ylz^xƍs=~wM®[n 0~w2l02228|8C_g̘1ܹkײx>NblذAk@oe^u7 lݺ .={dٲeၭ-Fb` X?k=qV_46[W[G%q%۳eiРA4hk׮մ8p:ܾ}Ҹh~̾t: d׮]3$]W5AjҠA8x ,,+}N׬YCÆ Yhf… )"R3ںΧ1BmhanNG++Δn3* DBh#){yyeI$"滫n;v,O<L6҉{ꅩ)=.W\ѺС|L>ӧOwN@@l޼I&aaaM ͇.7d.]ʔ)S?~<۶mZ'88>իWӹsg6oѣGqppߟ۷닥%vvvW^̙3~;biiΑաO P[ƻq&3SX _PI {{c>cχ\~ˋ[pgܹ̝;Wkڵkˬ7~xƏ_~tmյG}eʕ>Κ5k,W*I$ uo:<؝X/)F|ɹspppwߥM6L4ZrA2222d?3!!!ZX[[vZ]FPPwfƍe5qD5kF=pqq!** KKKN:?}mڴaԨQDDDh&J|s=kw>1Ry]ve9r}rmV,| IDAT\Spp0Xb}%**UVy- .$22FZ2uT5k;w....|RΩ>1Bm䤳ֶ$D#P#]Dcn50 Pj*$$Ӫ**11=j$?gzzw"9W*9x>3nߦ?)ܹѴ4MR֡ijkѶ-iݻϏurtOOMU\tfiӦ#HPr9_  gҾ}{CRmAt& ˗ٕ#:YYq(. mJH`Եk:ت/<ΟDzѽB< s:߿_[nhՊ5PR ٛŽ$TjU ꣸ը\C@+ssUwrFC\AyٙV lڝ9CA-n}_qÇ  5Q{  LMyg;G\-"?6k5w9bTA(WW''k33U %X"PGS|u:#:.@݊|ھ6??Q)VO;%-jsYr>A}ID ~ rt=Ƶ#H"u/<%ssb"˚7ހ‚e͛3-Ͳ7oݢ-q  `8mњֲeKt{  BH{,ؒhA:V2hߟFFbbI ۨf@R۷}" _3o/\DEٳxݼ*OdƦLORw֭"U4 iI* Tww#+zPph5*  m,-ֺ5MEK:P[J ۓؓBRԑ /zܜμ䄯E.T޾{W)T/s75eYLqPWOq:<) _JUz aT% PZmfƭmFTF:;.0A'rƦݒHlzPLvss+W (n)zQKQSQYJ~OIasb"&'Ri,)(),-xy';8;%JX[/\]U* WކMA4hUNFtm%ZS P *]W$A srHM5Dd oF*x9xӳ}TT*'$0M U*'Alm ҪϋJRgj*ؖDRd*m{G8;3ɉ&z| Uww''Tf&sr\DZ:֕11@ aŊlܸ3gΰtRRSS !<<u1~xuk<wM6%44zJ킂hڴ)k֬A*fRԔ 0p4 q:嚿[ BAA/99Id\boJ GԿZ-)Nռ|:# YYeˀ^vvx=-0r&ݸ֤jC YXϏf-J91͉US8A.0ٙ05ȅn&&UYˀqq|֬a+̌6eΝ;gnuCRM¬YXj+(JOntjԨUi]v!j)k׮5t(  Up\>ƎKppAcwRcǎ4ҤI,X`֭[!R) ĵȓtST <N+M >H G8NSQ`Yο"9XT*g33sS.:.T:XYR ֖Z kWk"9I$y_qqփJbozzDQQKo' W_}̰СQPPT**ɶʪ";AZG: .X0C!'27˫Zҕ<+ץKjH$U>AAA7*/:rH㫼?A$ /89iݴ6H؛bA 8Ks33̫Qpt&R\_oJp:X9`.ukVh!Aײ 44Tʉ'0`,^Xׯ-]tСCe[^Tի:u*^^^ 4Vk!!e89] w%1a[b֟~Jg4u{ws0sI6K1W=Kqs p޽2ݵ H,^!!_#*Zغg'{ؠAżl#9GG}rEs >tƾmꊽ^23#V?RZZnFuRG/xE`` 5b֭,] /u\BA=HOO'$$/K.sϕYwtڕ'O+_aƍL6s璜uB ర0k,Xw}ǎ;+8qYIK,رc,_k׮f<޴i]taĈǗ|#Jٹs'+VqAx8c^lrWc*AoZ\Ibvv#y,ţ~g:t耥%qqqン3ӦM̙3Zۦ3}tZh9z_eee1n8aɒ%e9w^{5//f…DDDhF-\Rɲeh۶-3#Gh#"" h9xɓߟ-[lْN:/h_Y 5<ҹsg8Pf͛7ӥKa)g*񗑑ih޼9\~P"lڴ)HR-FK۷/ٓ0xqssZW Ե'ml.Us%'h.G9kWRbeL nI+WܪχNlVQ..۾h) *:Ngu$JQ>UWǝXM|׮WWKܠ-4? `_J´xI!C=?t0Xu쌳^77?cZn͊+޽;zb?Do<ǎ#99={Я_?^z%6lɓ'*՞uȑ{4oޜ ˗ gƍ 0QFh"rLOOg 03a~ ssJ_ɓ'ի4nܘ=zh7mll011L>|êUEU: aawOS!VʲtZquۗ'Oj*.[ƼÇe:}=^˗T}nk`g޸͗.iUc]4Z O?%oК}ۼ9ƍifo =~<ii+CUdzf٤={hbV~JMXu!]=7n0j\/ U速~XD!.hދcvQ&"-G"]…/_r%K| }IzVRo,la<|WWW:wLxx8r 8ɓx"?lϞ=?>{gLÇٿ?VVVCӮcڴi̞=OOO9s0k,[f̘i׮$;'sl[|8/~~~ݻR5W 5ce*X~mFAx ,wj?gj$h|7nҵk(V"E>koo`VC\A̸}3Eu,>-ˋ;s20<,\r7=2 KKKZoߞO>Ef¸Rʕ+9p&M+4owww}NիWuׯ_׌spp`ZL<6k֌mҸqcnܸA˖-ˌ+?sM߯y8p;vW^,뜎=iӦ0sL^x~i}]@]١Cnݺ7[n4AkCP⦎ؓ¤* ,[s ~H$z_-h=.UJ}YQZ^ 6 <mΝ;;z}$i^@B{:@T \(^QD"^W?u\BEAEu/2eS66i#iL۴M<+6'''ߌ<3g䥗^BOKsdǢ|r 4F^J CN}ipw؃R1ϰd$Ԇ <3v,e/f8[HHcFcvv 'V5dIߐϩ#~n"q])0Y<|UֽkW;~wGrzyj|/Bj4UˠA|e˖믿{nf3|ٳPZZZhdʕ 9~ uYrr2-2!j|p0~b lǢhI!hiEE.n#=<k`oS|r{z=r〾#ؘ QhN RRۗ(Pk=,5Xb0g6Vn?S jsDSF hLOw\?RV֢"FKd/tl &|5=f3x`/_NLL ^^^ IXp!yyy7ܹ*p1a1Alm}SNeԩ,Z|{^O@@[eQ•˺vEԦh@T:#8X?Pb2q87׷lM:UU)3e000*G?gT\{{RCcԷt|Q~MKta!B^Y!Ncӹ]FÀHs |5Eq}RRR21MM>kokKDuX,t:6oތo2S]շ7|,X />Jdd$ӧO7XjU`RcW}Fqk1/ fΜIϞ=eСnaaaXލ_Upt5m1T>Z-cYb}%%ʘB(d"ZiD23P3{P'`doI#kSp}XS"#<8~~ eOI *6 j )+l: }2|IVX 6m#G$--3g[%i̘11a{1 x9r>u۷/ ~<+SWUf9m6NG}İaܾ߬Y꣡F{ IDATILL$+++V0vXI(ƍ^c|^޻(WЦU\,,7Yvbaʁ$Oz" wo ࠨսNͪ'YYX\}+BB-2-3&?êu팋#%%PΝK~>}:nh4lذ@MƜ9s߿M譾|͚5x{{s뭷oϢ验srzJ9rQ< `$%%1|m7tƍc„ DDDyo_ѱzyQb2as{HNkpp>ju&%T{$ǧJJ%%r$9˞,G`CUxNy˸21l>uʭT_{Qzct!14۷d.7Cfc.啕mLWs@4ٍ=믿s璚u]h~C3}t&O̼y뮻1c&L 33zK=j4&N?Of3III<T V;+V0fF#>>>UֹK3gk֬aذaϫJQQ&MBӱc=ƍ9x aaa6T{3+^z)#Fp$U2tBΝy뭷gݼTٖr ow}7sW]u_=zp 7ʀXr%{ϭ*w3!ZeUNjٳBtD)EE.U ̦^CQ014jb;Բ2&ˁZ&UP{P xzu~^UUi/!,+| LR `jT\JWGb?Zfjz|Kٌx3=[##2MƴiӪ,{ꩧxꩧ\'!!?~ܤ$֬YScǎuTr֥K{ d2: ]ǕzcbbXxqf`B/t>M<=k5έvgg _[S` srMѼ>,*nK~% L? 3W&&;ֿ*1|?֮~a~6.]x_Xc#bcl>6{/ef3w^M[oqӊ f-T25. oYY\+\\›۶q'psKoӇq L3"^~L{[p0c,!rf}~_tʔ)S,/RgddG$-j65Ĵ΀UKyyEaH- .{iim/WS,-e޽lQ}|կ_>Adfr `V1z=wGGsGT]`&8Eazt4?g iڽ;> w:Ɓ;lVm'9)Fӽ{\ةOEH̚5)Sz{K̴BۛXt Yw,v&BR\(BUӬ,\) wޭazVuuŻ><;3۬OgnF_\O_]\UEuGTwzVEkϞ劐VU棬,`6`/ѭ=4!Bi29Ipp0ղi"$YYY(/'AXJQR j1*3:H6EUyqOK33.^֍ŝדP &ffNFFtRoRe ~~a'xzreH?a6)6 ( {or2yBF,$ *6HPѡm-,t}}o/-\y{sq``YquNay:)قAP;3*Ϩ-鞘\r**h呹ó < Z,<~8zlǪlMbZIOOo !DsJB~IP!D r](lp}RBjyzѸf͊mYr{w/FcXշ/^^=6)ق P!!x\:$P\[Т|pYf0p;136;}Yx uByg[{B!IB!-BY0fUeKaa+J!Zb-0&gg z( S##4YVw:DE ͛xGd V{pOt4S"#[MpWt4:wŒ f :EaAٵ e>r_l1O4I&5B!B4Ł++s> hϹBl悂:K֕Ӧ2dHL&u$]lO{:7 WV7h}*Oz⽼xKlF#:eB !B^HP!D2cZ(∄eVGbzҼf[Yj0pΝdLM7xeh(&ۻ9fXUq>6o9e[Hn=zbt``Qݤ* ?=ۊ#rϜx<=%;y ?BZ%K6>Oo@o4څgD3,۽Ѯ4Q}TiӦqI֯_mi4,YԩSadoرtڕŋPVQ(+gVU+)qӀ[e}^@FmM9*O8'O6y[s;u⹄40U`6OsRkCC/6K:k$o~Z-7yv6fUE(|l0pMhhkN^ ~>LV+sSSYګW+L!D[ј6r$  xj;:Kvի[?h:rA|<EnS֧jvް'O;>ٳ)6 xM|'xNMz|Qy |7X=4oVZV[2~|?ϱK̰NNlAo0?2vL߾YY gW0c7l`,:'=}It<`N##y+jlk֭,ٵyy$}үN1.δ1[|hfy޽v |(=BCm_|Afq1?WBxӊL|7e}eb"W&&O?G?~~>}l2Z3]ޯba?پ}ZnOG|Y|u/o-}Qe\m =M%\OM=z4̞=#GT_rr2#G$((/6<$%%̙3پ}￟8rss~a9c5ٜƎ˝w^W\ɐ!C#33۷3n8h4BQ.NH$VXOO~;S iN3bWPڂ/;0|WO-Lܷ@CPG;wȑߟkBC%8(+:E(G `hA/ccۊBoEF#7ú;)Sd̒%dWYLI s~'k>q,ZD@Vtǎ1iŊZ{ɮ]|w/^r fdI sVjT9EQa֦UW%06'8jɓ0S/G~Ç|$әfv̥˖Lݛ۾vTU/g<5f _:_]㹼mYzu|1y׭C(|9y2o^y[al8qyyeY|u3 mgƎta!鵮3of>ع'7\ڶq|7gOG0g9sx~M׿oPnH>;zdU9/3*;;Gyٳgͣ>̙3[K/e|ݻnv'O`駟o߾|嗌1GyԩSoYf :=wڴilܸǏ7yVwf<3(C=DNN-bРA\od2@!*{x]QٝU!΢-)qyVK|=e <} fcKQT{pZ+YwKEKJxideU-#Z*3 / dfl,׆ל6p-2Olߓ1LDcѧXYx1 ÷| !hctaAxޡC**7~WZt,OSXs]tQugt{`lpt4a/̺ǹ>NWJh5ۿ =!:ٟU{ 耚y)VCO`??{yqܱl3&Ptܭw: 1x)-gh!^Ay9v`SZ[)h6#U:c:wv\A\@aadf;+ SҽCpt? ={rǀ > IDAT ]:XcG*^^cCuC}|AM ; Fdz!F4O.##״4`6JM^y?9ٕ}C 0nEὔ2x0<>ڏak;(*bGf&/]~yu״zMw7JFQ_:ďǎ1nRDD|=d"e;oՅ:d2sNFի5jNQ?OU1rę3gx'֭owqFg{4h#8皞NFF1118Bˈq*UU`!AEa@=EIOG(UzmG|ZT~m8tf7;hW/^mVTOsht/8X-ˋ㹽BEQ͓Ǐ;2 "@pgTo bZߒB[Y};/ǣ^H/ӿbA۱*WtkI?gd\iiRK]E-hX]5(JUU;{T<2۷_WJVU+W;|i !h ߹Cp=Td4۩S6h6IGvgpCYsVˤ޽Ի7kO̼/'EIsc%F ٖ5gO>ܵɑEѣU֩l?~JJm{zr[,ڱ@//LLlB|` Ü3^vl.-865_xFeh6khQw$ӱ`9 B7M8?G~`޽z뭵nO>O?e̘1/\EYeee<!D{ɛee Bt{KJY@jÜ}dOW&gV+ʜFoC `U߾Db̪99v4 \k wFE8A`C"ۛQl-,Ċm'gdU[7*0"#,+ 3yv6 O !hvŠӥ F׶lãFX==yfFzdzrVτ>cڀ:|Æ14:L|k}#" Lgi=OiB]}NNldN幓zÇ3sp\BZ-/_ /lV+o:4&_6o!CpKvr\5m:ヒNɓvđ<,.f^w -YŨssyudeS-yuIIۼ1]ږ-z3Q͵={7 ˗32. goV磻`QUkތ tӸqgܹ,\:\pT:F#zӧsf̘/ȅ^y9sf?w\駟4nUU0p@BBBϘ>}:AQz=Z-E?*2fH !DKU\Lmqz TUOvvͿ.v~X|nB:1獭[//g`Tuc](~(cci8ӉeNN<ɺuxgXt)uN8AnX~=ӛnρׯgС,Y̛7O?T4hs /dԨQ_;k̚5-[пZǼq*ct%\BBB/v6l^ԩ~!K.~ .}ތرmqq\KSg!h/V+ⲬdNGޅ|2s_%khP?ee߽zKhRy`Z-6ݜO-+Sx`dNL쨀ڰ0t9 VUTysQO[RZ:&,ٓ;[uLB!.::Oi<=gNkZkw^Z{(b!7{&T6OϛǒQQdffֺ^JJ Bѿ9t%f{s ׯ%M۷@~MΪlwybcyݍo\w/fsC xkO䢴Q["' o<jMs޽|EU) 0 }_hk6N!:#GЀ^QB! 65@*Y%%mY}hn BfѣK+)qCQWKyQė.Nޘ`1sδ@'//ߟmUU67Bt_Tt̎cFL Qr@"TUjblT^tl&llbjO\(Vuz,jcq^^6zhjjj j _jѸ^nѴ ܖpGT_dej&CSD޻f3/<ɋRUC!V8YP@7ޠoDF͐BfǧXTy'8(TTEgVU 4+eA j߹*>͜cl 8(}{V+ge4_i2'>xɪdWTa4i2a2i4b0(f b27ʼjM?VcBU)Z349lt'SUdhA@X(OOz"=<H]UUZt,Wd=+)?C7oBѪ ȴ[{\.kR BlzW;]2,[!ڛ%%UaURWCe ̌m5EUhv=h S5} hWx0. ˙ή82L&[L79e4a4SQQo@(((gk8UlAGkX{ z1J'ãMOh%"euC HdeB!mB4XRJPѮ*.5JJj-Kr}xx3J,nٿo%년ԩMΎӼ)#Ph4̈evl,ϣ_{/Z/3L_,\ZϨ;TWeDk+3ZI|>tv\"<ܤm<]j Բ2^LKCRhO@|> ڳzyjW`6SP~)+t^VfPjRbYjPnUѽ&JqYti Uϯ2p"zr=BܳmیF#m4m]%@!}оGF30Zgθ<9~&I#|TU=<r[ y STTflΝ6, 9L7^-[W0A{RyPJY^`^/eA[P]frY,X,u4oR*i+϶BapQPZu:u:<< ~=~=@QF.!<޹3sSS1*9?:ub`+| !m{;/f͚ /@VVSNeDGGٳٴiyyy??\{e6lq_xٶm ^s̜9u1zh}Q Caa!s~)Spwh4O?o>¸馛xW5k˖-@cadNJ.]6mf?EQ8tPOtCQPU4J !D{̙UU]ՇCۙ"sfUUf9»sUUecAϜ8|I}4=pvyp0s;ws$K+/gC~>YN558ot `D@O^\OfZɶ68{ f}yNEE=8>{%82CrkJLq2 8&whAz=z=\Hǎ݀꫼K9MBۛGy|Ҧȹg9,D{q^v͂ xgPz-Zؾz̓>k׮宻禛n3fO#p7ӭ[7."sq7s0b0n8xWԩK,aСdeer'Nk׮|qn?%K駟h"`pi ہoW\ҥK tј^!\8U BKkv2ڡRv׺-1<r|ը+>SUx 9՗1X>,;wnLGڳg9t44|/~~ txyIV*~gʋD\JJA@$aUYtGJɄiW-3YUY%'LJHwU_ޛ{oN$a@’0,{8pV۪j-jKJm2ԪUF7 ޹&wp ! |${|>_=˟yʊ0KBvUVZs='adY枅 .ـUV}xw0 IDATGFFuRRfNҳgࡇgeС>|޽{r!>sHMM/`vm ˋ}Ԣ׺uV΄ ޽;#Gшu&TUϏ7|E'F{MUUH@(bTUe9Bo[]ziAAtkʯj[cuAAqot4/ݬxˮ-(`qf&*+`SUW:67dB}Tp,W X\%%mjqTqL@s?^cU8UUucu5',NX,ԴIג(8^W6vgS8VQ_>:o) ^Ώ5:6UŮXc9mG[+^_C#vͫ SY^Wպ#mքW JFܳ= \A'hӧ`[|V+^^^5yd) ݶ}vz͎;0L$&&OUU~'阘-q=p5ЫW/MƜ9sHMMmqOBV#%U99 \vvl/jٸn^(*jk|IϞ<jVU坼<~!AWnx<>m fZ,lp_k-9J0V~~x_*άnLuu1bdUy55:s,z n?QMGyFokܨ tu@Mny3/c;+<=?mE55X8]]Mnu5eMmgHۜÖRVպc/="mDi=ܳ]%ZCjuߴiwy'>,s!!!^{{r\lw՛>ߗVψ#8q|k׮eL8wyǽMcBȄ⍵{"5]̞FFq^µ+vZ]TZWE[qmxx{ ϭngUn.$ 65f-u7${L -[z>.Lcd`{70s*8f6s#;UUEBvUEъ\buÓ J\?FֱΏA:ut:(V:EOevTG7kUYeM8p!^AqN.(Z j7*"s2,DWpMyWꪫXpM6mC aҥ2jԨ_sxyyqsW_1a,YB\\L 2!ĥ!ZG i1*bTV:uͭ.MPtmvjeBF?8) Z-lZNLub=QQ<_PUnp!xށ gD !! bd` 1z%Jqcf3,19l6sVS]Ȋ?WHXM'Z' )*r@е Dzy9`wegg 2ȴXz]ncǴ%59* H#ˋh(H//"z"z"GJSn絜NQx/?Ua]hswƏl)-R6[OV}bdn.srͤ#p|TTtkI !ZdY枅 .ɀPi}m7|3j2uT~iFsrCt}""x3Vn.ѣ]oj btPslfKY[JKXZA Ċgӧy1;-py@BB:8axi4Y}][!DǓg{P~b=V%;;N:BF]J竴;PZ:s_BnhwrHb f':0'2fcʲXs'ol:8:'i+Uv;9LkjZ 4Pl,-ʎ >*(`MA++Qlͺ^Z WruHもHkfn񯲒&M&U:EUmtݵXQ_Dx88GX6Y]UU;wQC0:OJʶ28ے~WZJ6\0y#GtTm౗k!Ru{_.!DՎ;. B!/q2Y_EE45bz kj@ʪm^t[i65v;+ssuCh<XBYh Y[P@~M{k{l\p:Er~¸`.ȩJ~M ]!~=dWU>m@58֭<4(  v3l;š^Qݼ u: qֲ2eq1ʰQͱnB>),1 !B"y !h!^^AF&ۅ9d67z_ш(_//r;_{*+I۹U¸8ѣ*ᬪss㜨{h .cc jFlA#C[*@__w xe``WTc !VUgm4jՒh4LJ Ct:i'%K6yydg;_39it + kWߚO`dyނl~s3', ooBcC;Ǻ]Z2`搐pA\%9%p $$0gZgg !]B!EE*]!5]psr.Hm0QSy9өZgO;q;yyqZ,{S Get9+TWB糫֡͝PXG0hd3L jvbG[Y>g=dZ,űLCǣp|x) :a^^&LJF#'homFOTU^Q"<?!!L ={RTSÆ))ݼZ} Rþ jݶ]< t'/2?3W{s۬6ՔprZj^<~GJ]Rp-ooa= A !B!EvIJ BtBoP^c۪R&fd`[]&㰫*k x1͸p6Aiǻw羘s:4C5mU`?ׇ3#,>wЮdZ,쪬dWE쨨 bq_ZtQpҵG c0LJ_O]R\|wʀ// /eg:iX傯z)ٿi?U*zJb23/h_G͠I ${Npõ f<:わ|ج6 Xew\$_?xqCUeB! B֤T !&SYy37քF#..fTC xi4MNfs=RU y1L`hoG`*>hZ֡8 AA\δP f)Z wUV=#sV=mY Xw B/EHzy{ת7tJ!ښgQV^O'&v*'*G@5Ϗ g|} h(]?Ow>tP/2m@}^?E0h Mtl.mG):]܂KK@x=dMm^~NBfBbB٣k=8h{my %ub*^F/>@Y#MnFX|'Zmv6=`*5{Y,g>6nqwcݿבs4bsLf-E+6 ! B 6nZ>BtfkD5 nkۻs:_ȰV?|Q\ǎ+ztӇNs"!cч{{,UUyWIDxt'"11y.ׇ&kw5>(hȶ#v{=vGeq%E{Ss\c=s _5YY$$p<8 ]^ї'8* !BNdFQŨK*-F/l~pyN-<=i(oRRHjeKMUUYWRǎCyyAEQet4ޝnfOx#'Ohs ý.,a w9`2Q휨) :AJUm?^}}LUXѮE;LI۸jK56#uBjf3֫/||236WsGB@x^/ _~N<z3 D'Ea2̠Tgr?~=& ۮ"+#]9WWT~Xvjs5{Þo;2`܀zlzj)w8h10$&MdGTX0ncnCޱ<rsOvy[ly{ gFox:SʌGfP]L4-ly{KBqP!DS="-F] ǨyQQv. Vq[_q޽ uJ QlǏgsi) tmvS n]q1o~~>&V;憂^ gfXCtPЕ[]͏X^ζ2SPS l0ˋd__A_^&=ť^\~ hTv?Hӟ}9<рk.֓|VMeq{?s趣']ʙCgTXPpұ?VÄ'`1-#ۏgݞzH7?ӝJrKX5#[Pc<55 \gPUAXw߱ζ|}LJ>>nѣ L&6Hq):EG5N#Hh6;Y())BDz 2,厮9GrXJVc\H]卿(>]}S5F3ϫLU8wd ]_]pڂiOHz ѕܳ3:=tV$k׮E6+VpcמZl.^Fbb"˗/!ZѸ'UrBqh^EVQ(Zkt߇[ [<(/ ft':SǏ*7׽ƠzL{yv^7>hNx _Œ0gbH+ZYQ ,/gkY{=Ɇ*(tt c# 󣏏Nw]M6o(!C$:K 6s܏BmחYA-uzo=Gl>zɶ5سn1b76ψHxp|׌iYYZvzzɷLL!G$*dm"zԿxyVR(M\rTU5UAqѐIťLBj]ZUUU Rmug> !Ds쯬lp U`hdfJƥcZ.NHݻ7{gx!;󢍦wyM|<'ÇVes\"0ݺq]xxVșl6ҝa|_VQ9=5@PR]a/N31$Šnуf3++Lۋ/]~UUy)+(L1$&Ժ))RX:jjػ~;t֫E羓F$1h v~2]nm >f>6_N࿿/vng v^*=ծrxao;kd59Ƙbx@v} KZ 8*(MCOx6{BtV2ܵ<6 IDATܳE Cbb">ho +W_}cǸ{IJJח.7x}bᩧҽ{wx ,Z'|$RRRU so7w\ƌ믿NJJ !!!\{9Z?{~h44kc=XTINNo[_PǼ 2XqΜ9C{(((ࡇbгgO,YԩSٸq#+Wt7_+}Gbb"ӧOo !ZO1 ڵ'B\*LyKQ&,qd"-=rF$JHhVdx oNFU $h\Z-3ÇqEsMWhdA=*MAUU9f6Fn.>Df) nm=vؾ?2YEBjaz"{D2l0X֓ۖF|r<QDF$ q>15~Feg$"1Q7⎗U g>:sF7b_gtza~/ub*SO!~@<>>7^GpT9#Dg#s2,DWqIV.Y^{ 0f,Yk]=?rW{SN?͛7rJx JJJXbɬYXf >>>~N!ąрDv}B٘m68ןkHCk顡ni0:=⚚Uiw&*+srx1jj!`2vUVj!ڜ-2ݺ1ϯM'vvJKTRB=g8_smEo `?a4I!:L6yyVڌ -m1wuۜs6mNZmHt{JzNJ6o~ot&F jv;sA`rrIc0NKlX5kƣ3zecP^XOqU|r~P ~w Wom:Ӈ2tZ7tεo!:=ܳ]%Y}Eի͓O>u]ǢE2daaaL4_~֭[Yp!FWel޼O>UV_}g5ofc.--7 )q%Xhh(^{-@DD~Whcʔ)`СL&yZ'RVZELL _<}}}۷oO[neܸqMw?:00^wo߾K-z>!ącx؜#X5T9g~UU޹fZ[V@aa! #PCUUNUUS]kڡ98G(L enTj/+cKi)KJQQA6*-}V'2*0ߟ>>>-ZQg2qd"I&ETjh ҽ(LAڮˁذjzV7ȗi s ܳ= U\rӧ駟\l޼;'@O&?3g#pBVZŴiӘ7oр ^cEQ0aB>A\gq 74k|G/uɓy饗'yUA'hӧݯaÆo߾;3w\>sqիW;iҤ?{LB9ڋ6_eRHHUWNvUUA7)) ;綛KKYp?ۄ6uU8[qWf^('<[T} %%rPu0&\HF!ڌUUmF^Krj)**uFҭg7F0vy޸8z A~f>23~~Fkx/F\7]WBeY SwXμyX`Am]Ww}tM;^~{|m.,D뼭UY]7fQQq5jr+-esI ߗQihVougp//F0?pSqj3*\k.ړC{^g{3.fȐ!|TU嫯ubM}I:zvU 2* L6?| Gv_|||={6k֬aҥKT; Ԋ+ϳŨT !:&Sʳ*iTW L- {z{oo"`<ͩsZ-DEu`Γݻ8s4 Nʣ>ʱcǘ5keeeܹh UUٳy')))=cƌq_m*^r%3f__zky3gk֬a,Y伏mۘ={6:Æ kxLB޽w%-- ^رcYt)N&qbTх쫬lQ(uu+m6&Ů|}*50/z<6g_sܒеnݘRutsXX_R*.d#BϷܳ:7cLF# qQfT=4! {g!K`…̛7tM$$$Իcf>RSSy`0()); 99Gyǻxw7ooC ᦛn⌳ ꊢԻ/55_WjΟ?n[kQYf1vXMFDD[li3 K@(=NQ[7?Lݽmee46 T/4l oʒ'Vvi쪼ǿ5֬pTUort{neup~B5#KQ8>0+3)9ݺGA!."" qB!:=ܳ]?豴Zll,pԩN;w.YYY|=!yWuMڵn; !DÊjj=!C\g j{EQQZx#̚d|Z6UeUN?Nnuu[B#%2bbjHvUBr}K(jrUPW2߿5 w{]tlZ'#R)m౗!DYNW`.{Վ;.B!. YPUnU6UUy߾ 0#,s]YQ9A-iq7x06yݺ@[SOPR·Ζ P?7%%Vmjqƒ11L i4t;SUņRuVo@е/ K3"0hB\z egk5X+B${%a|By)͵*\V6UU!ע0h^_]o3g`[]\X#PhGmr-CKJcm*αpS;ߟ@pD@:DzhQJK)!h!R&sB\/p!\3 Bt6MfB7ZUU:rWU+x|<LLZUYvԶhOs{)~~*68[v6EQYI٦Dž"WruHc燗(hCzDžqKddGv;Zj%upQR+Ԁ]~ABUI@(XgPPщlVNQQϓǏBvv=x4. !DW%BvS1-BNʮVMjOge'Z|/ȀJOgsi)fA-8cbE55Ǘ*ܚz-L"TT??~  CܤPЪ|RXHݎ5LLuYB4jKF٭=!$BvS ":]]޼+Cu:F:<ɓǏ7qz{b{y9>f j̃ b[y9KOӢ"2**P6|s?}||`F$k !:XNUAAl()Tl*-e\ppGYz=7OBsm-?ocǨiuUZ-wEE1)4&f޽v.Ow !! &-(HBtB׆p?,(2B!s!h7ծ0JjQ{p>,(G_NyyX ]Z\yy波"jHki9\^^Ɋ}1̑#yOnpPiM un*QϣrWYU33,^̉Z`ثh_s׮eʕfEz:ŋ[<ζruCݻr%?#i+VX+:zB!D"B!MA(x)JsVC|5{6k@Y YΠk=F#zEaVO֯eڏFσyHゃ퍢(M@!:F#ɾ쩬Tu5{*+#r, `aU?mSGM&)9ɽ{n`X|wkrۺ:癗tLPXvϹ0޼Z Eoaɓkw)2yo|G `XL 3v{ަ,Zկjݾzv-Y|;gy?Ƭ,dpߍ]{y( QrE\9K$sdgk-[j ,ZD+V@I&333h4lܸEKKKooQ !Dj< B!DgulD8 b=M.6s95 ,fsʘl˂86Rr啬0bbHpPe]>i ;r8"c'Z{R^]͍:m{oOdbvJ xyqcpaVT0;%/(J,36eeQd6=99};{#76+y~V=0 #j,*zFK%$Sݡ`C~ YWl&ח))b@=ֵ31TTٵ:w[omxLݛI G3HfUv-?i~QA枅h%~7l6ZWjvfFÈ.* 2q%DVUUB!Dulnv&%V6t U~~L cbHkBQQz=gj]D)CCY}˫z5g**X6iOoġBzԙ?4_~I+ӦPe/,FO竣G2Ձ{%9m:=]Y?֯O!Cۥl68f  ظk/~s,-e+0'5FQe-[X{ `[v6^{ɽ{i g[v66}ĉsr,՘#_d@_937kHlfɓǭ=p_^͢49e {Xa YѮ`QDFr".0ooBCؕ{΀0m EiQcl!CᠧYo'ʟcɖ-|'FQ^ھ}5אïoon0 #wǻpngǏdTYoޡlr+$܊ >|3fEJJ K.%??}ALBxx8111r-_~=͛73uTXdIe`̘11rHkX(wZZͫnjϐ!C̙3~ -QPPC=gϞ,YSqFV\F4 wL>]'hЎBtN&BY^pϥpuhN-կW\C(! !. pmX:9O>ڌkjMϐf˕ n_b ^8[dZ>9޶UU9IILIJA~~g?cpTIKMe7bd́ IDAT1wo>y2CӸq>h[{L o\{-avJ OWU5|OoDMĕуWM'9QZ |Cbp0|3WH @JnZD6=e7Z?XS gf߾$3$:w?gQ xꪫxs׮W';U&欺ѽwھ}.slWPТjAbp0I1<Ǫkӧ3<6{etB> ^^h5o(41txi:H;.OO'LJi}eh1=ܳ]%WA/2x`ϟONN_rI œ9sXp!UUU/aڵlp/8ѣի+Wgyk-[ogܹ< l׍O20x JJJXp!Zp(RyFF?Xx1_^yFǾ~zƎꪫ|._ٸq#/=z 33 7d /*c'5kk֬ǧ)\J_xMj~LJ !:;뽖^xx[sU jL ebH)~~u R5#,NWGPPQQ;6C#FpXÜw`Tghʣj0uVlpTee./'qΝ'7x@W(j/W_]kɽ{WVہ( 9 T-8s\yvUe1<19))kSuC#}_i\WkRXXȞ={|F׏GҩS'߲7t?y޽Çb {8`„ -nw%66L9=zrӦM3QF` M=z`޼ymڞS{PU&"̪Jx: Tm009, 6CC ngBqE |U\|ϗ=_ wIf&O|5N SZCUOYo\)W?gS7~t)Ϗ>} ;wNwܲA_kz2[7^SS(ttP?+ӽ{Gf۟=1b75m[VgƒA={[S_gct\/Hh\QUn^:p].&aY?l[{ѯӽ{ 6)-g}[*gxyݎ0YH/B+ر })x<,Zz㏹ꪫR!-->#~ovҥt 0|Vܷo_4@~رc4{衇;XbSL;[_Қ?8BZS& f]qvTC)ШbnB*Li6/ͅ4  XXPVUV02ꄏ W]ů[--[ 'tamNcv`_a!eem;v0".G ݷ.':X,/*bw=V˔$>߿U-ۿAS0.ƣLh_t41AAMZT 3354x}UralpF#SW ݻ3{ WY8l۴8jk9V^ ΥPFdzx>M`BUU$3Psj{~>Nk5(.o-F#UMƂM&M d4r[޼};&wo\ڵt f_H߳= $ l%yOy,фMQ_5%_nmP?2Z9W+iiӦ'|§~J>}3g/Bm1B\j~i4߶ck<եMc5i4L٘İ08\יPUCCMMMCjG=e[xFQ0( wmT FF޻oghhj  +p]x8ystҢ" 5r$gf2eӇ!;rngIVsƳG3`?׏l#CDE1}'#iII X V foa!f3aq#V+}"#*oEƍcG%K)%-,d1eJ[/{{EFvY~(mjɓ {-NO'),R^[nx_l2w> N `1xaFߟ5ߵ_mLB%g`L z=6n0:>\lXF >{ Fâolq55/. v?A=%=QQ**⯛6݉,&KW{n;{I@J?$&&Ϝ9_bl6;wرcp mZה)S~}Xv-Lݞf3fb֬YW#Պh& !Cec !h^}`L gTh(ftRUUxa^>].nʽJ**3KwTEir.fhi>_¤ j4Xu:Zu:,)bkǭ::&ĕhBXե|ZX%&^`@q17-\HqU}3-n9>{-˗̣Æǵk hοc^WToW091gGe˚}=//cǢ~VMJL䟓&͛kq!!9U*Bߨ(HK+WW^Nz|QQs$n(?ÿw˖1Oޚ6my~`:zM&/.&``TB'PhJR_~;nYj6{hp,>MuڿL޽M9ZVݒaa3֬UWXHMWw}0̜7ofʂiH}5VM㵭[I?wƌ4aʕ>yyjS5]pGZ)u rI~Tzy׷mmۈ Ѓ Kh@:3sg>F"[܎I󙑾g!ΎSO3e ‘#GXd si|yȑ3eqv;=C i0tkΓO>n祗^h)UU[ˑٲe f?l~_2i$wΉ'IOOѣy 66t2wZ6ndl+~&Wt`LHV"z.TVFEEqm-RrQvjw=- ;UUMìBsb ;Oxpz<43}L Z-z=z=:6P7D u:uK>0)!:#BBPX[˶r\YBg b_eWVQ{ y/T|,=>SOmxkxМ?߿\5kW/w]ͿMNcUUa |¼ƒbcY9kV?2lXTX'GDO~ྻncԔx}qv;Yz-{-.ӜЀ7Dߧ{p…}f1bbxw5>f3hm榧37=}Smy'6gPvsoUTz|)mmlvZZ\rI߳= 32ӕNwؑ;vccQXXHll,GnUFa͚5׿;@ד_Ӷ/_ν-B=-Ҡ-}<69OUUUC=Dnn.=x_=L2\֬YÈF_"0e!VB4M(ut*WUA~qw˜i].Jr[}xo%7hTw!)@VKNGNW. >``0e0`EqIΚRdIQ%eYY\mn 'EQxye,(`iVon+ݻdh5gO6dg]ZJw.K-[xՐ!$q 疷YOh? n߾~~7\j:vH^^ws_| X|9Ǐo!q|^:y2+1BsX5VUZQMiQ:H@@H@R$FzDT(vtif3?43W_{x8wX,H2}ճB q& ++t8v:?9.eM x9xnLć8TW { 9ıf:N{wNWZZO?M^^ %7x#Gqj/0!80{w>8qy)sygnz`h0j44L^A.h`_Q|O㷼[4UUĥPwuĿJϿTU 8_h U;fTe47HH'&2hDD7u%K cnB*,8UW_}wupsaΜ9|Bq.(hzwq++(c q ;` 2a5kAB?p}Oo&sHQ>LIawߵwSL_hwΟYbj tjԇx)J5+fIRxpg|]vSὮxtxxxf.sMic55asYE=lhh4`2P F#~Lv6P9B!.!,8wܹs{7A!.*mʀnh ޼M*J_jk)+(#;#FJ@H]@ͻV$ )C%^@(NGNM^OH'(NݶhT UZZZrRKEQm-%\8<ފ UUEVURaզta'u^`YqyH@~M `ݎjT!{BMX!y(  e&n'gMCFX)+(ؾcz7F:Sk6K\ԋڪ%*Ƭdm-nAcPb+L&:L CvAFMv!KU)k"Hl.r|{>_tlj8W(h唀3@DOnf3|hABˌ(|߰_sC4!B!eQB\ۻ BVANv=ڰH`yg 6ppAtwgA$Lnlmu-+XI,jkǸoc?i`sޕ;~kH* lH!1I1}i=߯NK5^%?+u]lj'(+(n%g,w`5LQUSQ\ATMGNWnzfooީ{#I~f/IFWv:9tuxhPNF#]~ BH M.K:E׷9`*%.'aSy՜uMyϕΉ*8h`TM&ztov4ypI_|̵|{~Sn;HƗy\%m*Z͑#|ݾt;yP|o2,::uoawֽb.~V(կ.vwΛ*JcWVkJ(D ěL$LtV#4i6q4i00X/@ U}C7خ^Ql46ƙLW!!X4*=\EETU^3!-g!ĥ5ϗE@?'|nB(((? !ެzsյj]۸ZB,\} e h>2!TWo9 ۖl#yd2 }apne㉈`՛ػaiT^TΨFglR[]KַY\kIĂ- r`$V!=vaۓO|@E.:eZ&>8VW+..y19{9{XkVUjTjCRPPֳ͟Bݜt uza>֪1rl4 HXH74LX s hǪr.NrN]a:jUN'N%% :$$$ͤyp0h4L caAnfΈnB? !.%,™3g2snBq ٺx+oUTVż/ENqgwf7;KJu&}M5ʈF84uae]()U$ K"{g:dI޸Sm?i8PucഁQUХ.ű'E<׵kUe.vR wץ.En7է mnݨE.kv-+kXZ-zY,$[,(hQ!! fx8VS :N'G)=!b TWS] j}z}hl6e0ypM 磂!u'OY;~A];[&.;>ճgwSH+pgZsۻ)"!Be !ܱXH[OַYZ_Jf|5ŠcSs%%@pbc|:a4vT7۞N)| &vlXmSwYlXz0o>!*mi`hz]51 IDATAb 2֞0ך*sqhRvm *' =fz fM&PȠ955 VW]Už*N~tE8XH) BC}Ì>ۥK{7)5ݻ[[ĚlF=Dvmg׭㫃[XH /ltLHtJC' Fx0 tr55\jVU~p8pCdbPPVY <'ʤ( Ct9trUUt:9pUU]>En7[YQqJ/8Xm7(p8#~49,**|-_pkddLo7Ô:t 共c"泽{.-_t47rSj*~Cjy<|EW_͍BK_nQ^7qqeOlc!3|=߲}jěRSwo~Yuü[q\o_8O23kqݜܚg4ޚ6/nrsGݻ9 bYVs4y<† |}0ytwÇs[8].[{]ZJm{}fŁbqsj*EtPPA[Wߨ(ܱEE8xWW!h? !#G|h62L}d*o&%Y=4Xֿ!eT [o%',=Sѡx UUY V2xQݢFX)+(c4f99@]ȊK2 |9Xt㍄L>y_PlItZٜdzֱy'6X5k3-7?mŋYo!&͘}OKwFEQXoGvo$?y?_o%QQ,~Glkg{rۢE|8SxexZ? @_@Z9rwfwh0Pp:t1ޡ~knÇ7/kJN_w1AA+,7\cǸnLMtq㪪رt 'yqFVgg[fk}8QQ_Ǎ#*0 ! B3d#9'85J io>k_lp#4W@:$tD8m/W}]tJЛo2 41oҶOOlXֿORzI5J^1Gn-u[PYZy qQ^Mi+%;$cewe%N_ rfT]PYkǎ`hhr74D u&1/Eq8SY*+]Yɱ2:оg7Oֲ/JJ|i2bhRvQKUd8#F0 n~gJ&'FN Ť1gS~<7f Ϗ[;wq/ͫ[ݿ?PT԰ JH@(>%) V˞6Ncfwd$q¾B~D*5n7Μ G=y|p Z-VMS*@Ǣףh,gfRb"o CeMiM<2t(C;U KVN(ܱCn'98!eվ9un @B\$ B!Otb4щѭ^h62Qц3<%Jذ`w]h|x߲iz)H6kO,HN&-Ol}ͭ$M oBr3Xg~rd_x JN;ğX񰦴v/(348p@PVHGaz=WsuK+n9uJ{ό9AUn7v6b00(@;0Kc6 p.=_ O>:vdjRi0|ÇYw/rr8TRGUqֲ#?~?~ߛحvLlPwݧ( :t`c`Hǎ [7ؒǠVԤ$ VqM.o~ #|$ө![2o?뽽s'_gg  ["^ٴl_UU\.v?NZTTϭbϥ-m x7#Gq.Ze7v EE}pPpf6\BiF9xtP+`Jb"wKu8ǎ`n9žB< tCٳx pP!%CG,B bd|lD;|u cc۱uBsM($Fn7YUUdyWUP׉6UAZw]74i/g(3VK?Pj=9 dAEi$ɟׯ?z̤bĉ{N)7ʚ_ئLڜ -+;uc#xd0}rr|ׇcU~AF#l6dgi;W Z-{}Gq<gfb5=iQQZqѣ9~qޡV>ٳC(nǨך[8S{;8^QAJCO'4 { x|*^޴&-*{r{>y?_ϴ Z1ǃ+V8K!\|B!W4FCB­@{-˗̣Æǵk moS۹W/ە+qyyyjHfуϳD՟?FCO,xE\ӥ [kײ`n߰Hŷ^Ӧ֭ϟOh;c>B!.V n߾~w[B\f)Ӕa1ym!D<Ǫzn8 rWT] 4xѡ CUB7UUɩUG)[ֹ z= ՞]9֔2[9Sf^bb)::ak[Ng;[Ӧ]mgWym$mbN+ƣGܐ'ws5uj{7E/Jo!h;vHB!UzZ,X{[Upml)/??D9Z]Ǐc̵6cBCP\E!d"db_\w~Նɬo˹^ֲ%x#1lfpJOM;M:jŬxp*K׮-t\ȡee#$ >nBqE !B!.kZE!b!bvo[U *+(4F8쯪\4@ l `L9Xu:`}.Ue4R^ֲ2*xnxݮ?7V+CV fJh7FP>/*M]n 9b5k.-%),z^*)?|;2EK4txu:6*U!B!B+VQHXHX\u5 -+cgy9nؒ~(!!IgƙLt @URj< 2B"d"$ Bq~y5!hi]jgum v,8y8QQg1#"aa_ل8!:ׄr_EW^uohovgؚ55|TP'QPQ j<vTTv;kKK)sQm XYo `hH ZkBCI1QFèP_E!Za0p_\;Fam/,*ɓ<ر#DFb9!B!B\ VaaLvo`%쯪BC@)ϭyyKNp glh('.Q!V+CVy:oXc558  IDAT٨0BgPY|Y\{ޗV8 !hx&!'nGǏBg:ܝogCw]-Z_!do3]`˻'<ӥݦ~wn,.]ټnG!.W:xkWԭÃ}Yk)|+uωL۽ۆ ؽ?Nqmy!7lpOL %'7t(G =Yt4I~̺h=XeXP[˂'w/OfgK Ǯ]:NH lv !ĕBB!ֱ8d"u$zONdϞe>{ayy 5FEa!}f`u=49[PeupRLرhOxC$/#^O\FDW_)ϧS~ |3w,X@l_ݹDtr27&:]|GeCmZ3]08g{WI[oόJaso)?~ϙCSFh1Zŏ>G3 g}!\5F2:4v%EE,*(`[y9*60ͨd$C>xN]l^x#ỿot4K%%12>w;>ճg#?'Oz^ֹ9OY;{u+9XR ))<0h(t85wŋ̝˶c}<|z|<>|veeq݇Cr<].V:' 7>%ƍ-wdajw5?콇L1bwKZVʏ-J[5:PZfѢFR$F(=nr$H"B}߯W^r}sqsr>.l}21.`Czܠ |*3^?Ç !tq) 47gav6?FEmXg¦fM|G@Ȉc>y2<^z}沪_?5o67GK5Ǐ}ʔbw{ z̞KNvz:H͟O^t)AǏm$qX{}ShȢE7Uޘ<׹3]McϧjKY(X1S\]fg\ccٓ@f^^}:oE++985z2CTt%=p5Ԕ)0n. F98x_95j.TXPښ|֡3g^vAu.{wfܾqUf;}yxW-3AOzBbmZ,ޝ̫Uwb5";-cKh ښz=z*P Yӓ?z8i׏&+:~E7}X8;{ 5ʌ"5& ,B?;==;:2ё̼<'$=6ccɩBFxaR1ޞ!435ϖQ6K]]zj"<+ |Htu-M$4.ip0V:: ],-1+2@G] ;c mhmKjae}ݶsb}ڛa6ǵϖ+WJW-L:Y3N ='y?7E ;99bS+UJ%@{P8vP)'' v Pq /Ԭ - eڑ#^>KwnS Pl&4vtdk YRE+#'tce/*B[:0#%, -7Wkט8''99QBI !x"DGy"99dfrСb .//mraQQDGcZP&߸u!OOZiv¥qcmrn]Lk-FxjUlZKڕ+W2ysjoWС s;{XUzj9z4g-ԉFCfClȐRʎBcTښn|_6((EOZ gɔ-˲H#doaN qpvN"SS.;e-LǫլaUt4: imKԮS+J ggz>C6&S@nn+5׭RE, ݧ!?ZI,;s!!CZ׋%lHvn.QQt0oo>>t?]eOOV=]>>޸|Cػkqqdзn2HIǀ Ld$ٹd2/c==ںh|UYPnefjorJː憆2ΎeB!x(nB' c aU޹+77Ӌ/q筲NҘ2;OOz̚?wܷB(yJ)71a[#KKvN:"ܳOm%J)b@+:~E54ǻo_}{ ?yX*=&B!BAcSSsiSB[ڵdiΌ 䓛7q;yv"!kZYi MMMIlƍÃvsS!FßL 9<{7np0!켲&V\eocGyk%\.dns48;vΎe=z0z4}}IξY%#zpulmu:ٹwϕMJtDXS*bc N'ǹѣiv1L< Xү^==Fae@7.w_ggBƏgNNKݪU۴ 'E4lȞ97z45J} Utl,S*J[NN6ozh]pBA.룣it4,䷲B*Ʌmh9z4 zƩaChy}ݰfhxn;N;cNn}.I֖%ݩAǏ.رRsk֌>={JmW>S+148?\ƤĨ:+W!V} ٲ%_׬IV)},)W` t}YBW52¦DFþT M̘aoo۴`ÆLrq6RV& wp:?cx"#.'RsҋCjߢFeOO-f>%\~,mܘԪ=>b^Pֹ yo^z>(C4qt-)٬:wzvvĚپ=SBWd7F bL&ɉΝƆdKY7|83SkK)RgE^߳'qAA;<ڝ:qDv~q(Jm?gۤI(utp兩Sm̝t)g87jD ڵ -,8|9ysz/XcHe)9?l<۷U Vh9z}zbbfq{{>'4Ӈν>B<vV..:T>>ݹ>> C9­D쌍X:zҾ~kc==Z0`\s׭Ғ)ZSŎUP{ %DE1ݝɮl᫐.[pNp0_z: : ,`ܹddd<>Q144dɼ{S3g5̅Brtt$J'UCwp\/_ڬGѧ}i;wPYϪ<$$st4ƒWB_b=ml0SfF~~@W#XsWdV{ f'-/;[|<Ì1q3/:5.?Ƣ`6ꣃt~CBkƼyJK#JΟ5 Gʮx9O)mgn$WWYYD888 OljY DEEٳ2P!+V֬:\޻7ҰOJ=>BTښn֤56؟mSZzCS/>T c]BBDb=fcL HMg\VFE42L/t.~!XZVf&YY8=VJ%-,hoa5dw|<r01(Ysxk2_?xS*АmVu ÈFSNU#k3VDF04lLA53㜜ս}3)/x!x挛CVbVJP!_~T)B<*zz;\KOg]L "# *{‚N$%q,) ==rrb#vVxBu"%'&21ժ1Z5231>PO:qs+u3[T#ÆUuBM+1FCZѣ|R !ģ%%FB!B<J%/Z[5)j5k&<+饖 -||.5/urbtj8?zoB<XIFCXV=Rt ))e_Bq?RK)+$s30`aZLsw燈懆V[pؓ@SSvr"%7)AAZUlB!BTG991Z5NNp6ݾMFCyjf3+866Lss,QhiLJL~&ʰ$K]]>rs}~bNHA;Ϧ0ma/SȨjBGLB!B!BAkssZ(;|Ndv6*MXX6W- IDATRȈ HPQyPR1zG/!xK%)=wRɨjxёqq?%j.TIBIP!B!caGnn|ʮ8s !DyyZ5V6_A˥ݻɻGؠ ֍W>>,ezv6Wnۛu#FLmά_σ3^=j҄Cʿϰsg7l`,lՊ}pnb0*2~%E^/Ç3iS5mO>!bm r>>8;;G}=11}eͨ;W8fMbXGƶfM.>>l21{3zN3:u*sСQRFLJ}q!ڵkpy{fΜҥKٲeKRYYYԭ[-[pi:t#ȑ#i׮{ؘ=z}]Pwڵky뭷2e [lѶٺu+ {ȑ# :3f0k֬:65kdϞ=4hЀ>}^d &o>M;+͛3k,ƌÍ7HII^_~ -}abﯿz(Z_ĉ"** g/( !(B(E)lh?=ff4})NNr!7[ls]g:;Or1Rl>;u7^eL\LͼRbT![^^\;tǩٶz͟o*&;N_ߺŕ{zROzBZXڽ;IWƵ187jDvZǖ,5z@ C:esE,d czߴGS+ʽysh\ˋG?M^]|Nf&]>-[jh4d{|,]]+էXvǣTHƟ2` \!OkahaEYcjo_VHX@[zXXb`fJWҚ6B!>^O˂PRR*?ZXYY- ~ QQ-Q(Pk4(_pLjUSvv6ӦM_gIszkĚ5kprʿHy%|ǵklٲW_}X_d„ ̛7k_ѹsg ʊ+W ۷k.^/82e 'NAh4O̙3t҅3fP~}/_ݻ9r$yyyl߾3gos:u„ >|~QfMT*?CC*}_beeJ䡖bBgwWo^{V]C  )& X{眴)uPI 627®{== wtVZ&/U-$A(≐MOŋ䐓C%AO@%GEMN iSQ$Jsi.\7&ޞKvbH(.]xIZY=C!‚= Ф mlJ]pvᱤ$:?ٳ슋+@XZjj.s8T49iZRUp\ѣG9r$ 4`ٲe0~xRK)kffvW_kSXtԩ?^?pu<==+tLQ2RInnn,Lϟ' iЎ*!x}jW>sq#>Ҩ|TVj*gܨQv\Պd@V_~!m]}9ԭCݺtx}ioW, 54a>4Ӈ3׳8a*==MLxBhy722OQQh4bG ?K.PؘA)3 E:ZZ2-:^%n\{jժ1]v~/_N۶mTdmG>ّԬY#GGah͛7Ύ}iK&%%G6m*۷4h.fffZFU233#B4;w׽[챹9GV)+Y퇷 bnr1o,֮w˜{qf|9s&SNEWWÇcddā3f C7M6O?ĵk*t!dl3|oAquuʕ+ܹe˖Uh4>J%={O?EV_|Y+VFÐ!CprrbΜ9T*OѣG5KgȐ!4mgձcG>̕+WZ+ !Ľ|LyYꍫӠs\'сјٙFr.\9zx gOZmJbaѵGIbT" kk\Op c}Q%'ҥ>KWWZé5k%Ov>)zSgO₂80w.yj5;uĉ2Q( ۰ζIP Sjۘ;9oR͟ϺqnԈ&kZXpbrϙG^3GR[>Rr,gھGz [[:vv$Qc^곀π| (9VqiS^s6ԶI ~)QQX8;SE }|#Gɰqk޼} !xT竚5͍ÙJrnn 722|21͍!+eUOO9%&Tk֮]1Zbܸq@Pi>7`^ʔ)SPtޝ/1cƔMYϕզ=z`|駼deeQF zT(/O , ;;ӧciiɨQpuu%88±Λ7'NNMd L:eOOOdGkQF"::v͛7B$j?=퇶gȄb7NFGOZz@ bbM{f>?t C<̙34nܸcBHFK/1O:ܹ_ϣgd:W\ڽP`gs' UIeEd$sBBDuu͍1H<[憄aP6Ф &&u΄DC^Y!gܜde2ݔG}((|e&'rW:!cw ƅ ߸|BZO8P^s!B<#xZIQ!n_Ή+JMfv=Z:6&1׮WӵkU#T2ё!l[8;kB~0A棠 f 9;c*k>`RSr0!\\_y*B8{YSU#BTP/|*| ۰CB!P)bkK$$yp0%%ݕ(Rrsy_yttDW(| :ZXG\yIIj4d#R|kv !BIP!B!3MPʊ.VV%'Ep0;Q(PkRīՌ~y̭Q>66RVҒqqr!-C(B!vG!B!@s33ׯŦMogPff&}gr41q)Hss: 襤 G!B& B!B!/cc~S@__ެV K&Φ9z\崴*TSy YY=Nx(43CA{B!O)I !B!PА ^^񡃥%PZAI޼IZ]%GbJDv6B!O!I !B!Ԕ} rۛF&&@?s,<7aadUIhmn9 B!x:ܻB!B!joa[cc ((6aV3 懆2F ^EPTaahffXG$^간B`zPm2lzܚ7U['O(NNOea/Q ϋB< mmacÚ(>ylmYYt9&&,QC[T|CB`πݫ8!(+W@~nGyB!cuDV}|d"nӆ'Nc߾"܃ 1 IDAT9m;vgOf׫GjL ,͟Ϸ:U&6)@V X٧Aǎuk|MEc)Kfr2>Zfv:v,v&Oֶu$3=<9uuoƍ9|]}l<痿2;N =>}ݺE:|ӮǗ-Ӷ};+Kooԉ#\Um$N#u–-94ow`y^|ݦ Ǘ/''3^6Xi#ݕЬΝ:mX3#qڵҼ9 f dk4|, %KJ?p/aС7oggg>#"##m-[i!3%c @X@wc[v=G?lJ&UBCjի`dm/Æ=p&;VÃ[w/^/Tmi3nqYruveg֭443԰!uhz˖To gb}4zUVnn{zukb)g"::4n g6?q[[.E_+r23yQ0Օv%605S?B׼ytud/5k0qcSR8bף1)͵187jDvZǖ,'1q~&9?mk 8zyq!f۶?, oddeUxUƽƤt%BUА_8LLDŝy@Rn.Yڵyʪ #‚Qk4OJ"WAޟ?3gP(0a,+.Eݺuy011a1+VjsF3O>ɓ'kQF ڴik͛7ͥcǎ0|\]]YjM4!::ccRcuO :Ǻj*֭[Dze$""K.0gbcc c˖-7.^]zj+Bܯa[qG°ﶧMzNٻs4"!"b ڻtQm.-ZZjZc' AYddu]sϺ?Is?0i>?ϙd*qHéݮ>{cݞraa\Fl,|Tyyڥ tkRgMǪjm1RKlN^cbHy6i&v qkRg{VtE{@:wƽMR4۹-t\[(1AX;0P罃WOоOqkp)"11{>qv llXT$(&AA%[^'B!*V# vF? >>=NPhB}y3ˢPN@Hy{2c||8 U012rrfR۶CG~~|3p[|<߆{~ 3I篿?^Zrv&='7m vIHҥ$|5 ,z+ObVDg5yyѿAjP?~ժKd$Ұ!66f ޸Q[7YjK7ukrU*V':;jv6;B՟ p?8N ++ƾ hJX>6TMMyA&m #G9q۷qM//ޮ_"% {jX* KIqtſ$B|$}yyͳg߭[ dWp0)/jg^ /'Gg?Goo贝ݱdbUpn҄￯mسlWfV[X{@M{ĻgO\ɶᆪCZRbʕpa~5 T^N""%Fo;Gfp_g`Vba٧)/Z|xx:7is&Apn݊wϞh׎s]ؿWTڸN&i׍@ŋZE>s++6~SrxѢ ,PZ$`їBBQ(Tn$%Յ &{*ΟgGq%-IC {auσEzzz5ǐ!C2e  ŅE?yVp|9p@&&&Ozk4mڔ˗/qF6l@֭yYzvҾ;>E5%0,KͿJ*ٳd沸kW^g۶ƍ &kǵt>ڲ{SShEl8swBCNy {r&%eT-a{/]z298u`aoԭgsJR1W;ӧ- ?ϝ#}~ؑ|_1Bڵ|u+I#G`R(AZp~~duL G5m IbpvjK988󎒌ݵ ޝjMN&}֮صk|K_//޿qMx0W_ n$Av'$魷Jfy gOF[cc9v-5kv/ҲxjdfkGGR߳BXXcGc)a}Ҵ) Pjbhf&3 P07*UbbB3Q,Z8AP*LYODx2 VVB>75C*fjՊQFiTʑ2}tnܸABk?Kݛ޽{}vڷoOpp0իWʊ2;!`nh>zzT.e-jV쉾RIYŊ'T$x֞>Ͷ /wFj~6RZ={LV#qBJE:uJޥvm MN~*IM[[TMNlZ&'?ս{c61W0&_K`f`R<<MƒSfpb"7ye6;Dz=3 ?mTƆVV4]l͍6O?Rgdt)9YYx_DC~׮\DGv9Mcǖz=ڶkGG*׮]lo%-) k''\5R*^eoM%wwڏhľ-Ύ/dԩܾtj {Ř!6+8։1ǹImXK-XSY1ht<&ZSߟ?3+u;oM԰!m}1&Jh_k̛ ¦F Z Je:>F4~ 2nݢ;n-Zgrbq'&ִ=Wٓn?k !x,VvE[7nnn_zl{ښ?RRõl@۴iYn...,Ys=@c̙ 2{;ݻg ^Jv2e ݻw/ƎKƍ[.,Y///kжmۖ rj֬%O^Y tsCPqU1& Vi*ep.%3;"BZ^^'),Kk/k s)),fD߸AJEJŦ8׫nn @U ָ1W"%+ ;SS;Fz.Rũ͛3jvEGÃA RM3DRzzt-9Nc׮2&/Jq===K<$#BBSu|)%(_=^C^)R^0{77\}NvF..6ccDAt3k޽[^Ȉ/ /tOڥiS9XLNN%OK#;#ƅd:jjƵys\t>@I}[PU+jjUKW3;;L\fm⡩_y<۷dzc~.tiҧ;֚I/\(v}U, ZCR72 maE딿Bbgf^Vߺŧqq$);χqq,~kӸjij U]svI 曜={/<:w̤I:thǔVRdΝL8y1f/sͿ\Ν;GZZnQ | T\m߿?񄄄TfK !ģ*oBEz %TirZWsV{ W*Yǹskg ^/TrSVQdya6p??jcf`@ tS(f):~RkҒ%'NЭvm&$_34&46IC` ?sWռ~=u+UbTԶ';/"^/G^($BkܰʕV |9vڵ^<2nb֨AGU-;p!ModDmD^wϞRfS! MPЯre:ݥKt 3 Nfd1>Vb#+DCss j ܽK,)@~3eL{l=ťXCYsssNRťycܸq7fffRdMk O1YXr,ek˞UbDZq+*}v_hҳ9bv^F:,[]P/ HȠ^ҡe11ah mܘ1;w2a>ǧjUrx '!۷޿?ΚҤ ʼ/!R>|!~Z81$, sxQflɭ8:Ox.V\EzqYLL￯谄Brǚ59+HyP~tkP pqAP4:d2 +733ǭLd |[2sry3/^;x-tpwEt;gopb"s##h\fx 'o`֑#l+zϢ(Jf !hDL"}Q,拤?TtB!43c7nxnMc3̻vykefV봶PZyj5'32ǴzIB!]C0o&{wK\_-YڥvmϷ{Vh(yyԴG:eƕ;w~lML89t(cʁ޺EVn.Ɯ6 G@tȬX7~|UlؠݮT(XԵ+ݲeԭTڷ٧(h/vXt8nD7u+vwժjт/w M&M؛ҽN;IԭT@77FZem>|ʘ8@33y-Xҭs"#  ל9eQcǎѨQE!?Ij5f:1+:!(UwߑiU _^!D2󙔐@pb"jZf~xWW%I\l}'Oj!@3Q999qUILL IDAT|Z! !So}QR̘0rdES^B7c^khu+Uba׮OlBp0!dWZ󍨨()1*B!B,\&1~~$TᲣ*Dj>̺[sRޭpBgy0jHJpۭLFF}*:!;)1*B!Bdjۛd>#PQp37>M sq75pѬ45tV*`_j*8;WtXB!3]*UVtOJp05mmCJ@B!B!KHPгR%^志.Õ+ܓJHԨ5j`"eG666ݻGZ4Tj5J$BTWtBT()1*B!BL͍~~+Rv4O&OK۷+.VVjʹs:+#B!l B!B!MMҠp42H'r iyy?RK++kB!xIP!B!P(t\&sv@hK`ם;? S($A(B!^x B!B!aLJl&nk QCh;Gf~~O`mʺIM&Lk\T5p@ڴiT%B!Bhْ=3gj߇ѣ+0"!BS41 X_6fzu#"WA VV~͜ CP<|`Æ ,ЄBճR%ӧre@w6abv6Oxl uZ~NPvv6XXX| gggƎ„ 7nx{{g ]|9ҳgOn޼ u%IJ%JR!!!`aa?wc֯_/NNN3ׯ?v%99#FРALMMY&tܙp.]pΟ?aÆJ׮]I|B!EIP! dXvoo ;PiF1-[;h{ƭ[-ݦ {gի 3͍+QQ26ÇiߞԫW;s&ܘ=Ǿ9sXҷ/[i'~8jfȑLoтkǎ߷֫QƷ5k>Bޞs+a6a4.!$JgTV?Dkpp0-?dŊ? t;z(-[e˖l۶ѣGpB>#>cǎҥK3c lll… y78vk֬z ٳgsE.]JEѪ'N`|̘1SN@׮]1cIII$%%(-|M6вeKvM׮]0a ,й^tt4saĉ̛7PƏ_fٳG'W^ӦM#<<3gŋqrr`Ŋ4k֌}jYfcǍR$,,ٳg?u/ {=T'~RE+:Ǧ8ex+:!W^#\6JTCQhا޽z>{6 Ҵ)`]th(={nݨG)S027'bRBSM_~O>|/f!BАAWkNxuL֖pϝKiӨɽTDEiϙV6u*>z zosqܹ[Yq9 4<`??^?NBgk`ouFʼ{,wrs p aɬ[ߧT&ߠQjjU cذa|TREgqѫW/&L/tЁyallÇ5j͛7,~6mIJexwjsssVXИ޽ovvvٓt,,,&y]Xفk֬SN4nܘ,V^￯se˖ӧ5kV1QNLMMz?>|vig::;;kYYYahhIVN~GWu1A>>жհ"iHl4^dݻ2ܡ%~|ߦ խ*, {4:~Ӿ_? ae;D!Mf !p>#hDe|/(S6`S:5^^{RQ(t+԰!]&O~Z""tY[7^^6f 9YY}LQ*afo= Ĩ(*ժE^æzuկ/zJ}}kX`q޿ύXuL uvΫR3;;-, !Br~,{(2UO@<<֭W^eݺuzlyOr竝;YoԫQ\IRSq9=J3 Y9q"v$agǶwa֑#8A۸oׯל9\JMe޽Lܻ? Z]|6]Ln~>NNLoߞZ:qm׏͎ hP ѽNg҇'&3llhϯ2?:C==BN`ƍVNHHi}XY}|Xq|ːF0'ArD&MjcÒnhfm΂t NMy}֮%#'-o]b~GERƆ_}4ܽqw->Ĵ4--I><kΜ!!5FE?//lLLJ T*7 ʘ$BTu;~-ΚriDЁ-&e -[x믵U**{xƜ9JRGilW={߻3۶uv]:+OKw!?8e MǴixsdB!lg&M7P`̤ѱcLtqat?/;7cc,HG_ 2-ޕ*=TV ___/4j5۷oL_FرcsJ~\ CeС3ӧCNN7nO+W8wv={PTt%F1*e!]t?wնoڴ֭[SwyDPPgĈ|biiIVVV&x~dl֌6<Ȟ )_qvѧ^=x;W_ n$Av'$魷4_#5%MM|nw[<_}K##?!!\1Bg'[2ۛ͛ɓ\+#FXF #ټp{x׷ę}ݻ9t ~Bѣݻ7$$$em®Etܙ/ .ЧO8~8Æ JVm"((qƑʷ~K6m3 J.]nݺann,oгgO># @XXK,!885"" /__ KGѩS'ݹqk׮% C̓m2}tqtt}}y,'?Qؙ3 kܘZڷ/UJT9u%r>mTƆVV4]l,51AOАexǮ]С4ДoD`_+S}͍ A)+WfAT[ҨQw~c5{2;'_Mlr#%go 496V`f`RYfb~ͫWKw?h*[-9qJt]s% ƵjEjI.!:)AhU }v4?n͛L Yѳg/6&"v(v#FFcKKn=^+>{2SRlB_$j* Fþ}qm֌[VVuJRk$:D`bc R*U*N] -)eoM_Pz0e]KVvBQVE*\ 5kFܮ]l#ssL }1B!x:qߟ$%Tnӧt҅DCBl 9v:?Nk5k1PVc׮2&/J&'?.ӳ^{4 )NN9}Z'AصPKOĻJ\ZfpiQifЕWyLPХZXָ1mۢT(XŻ]Mk99ѵvmx{k˃HJB, =s}.qTj5YD]NRؗkʜ$B re-\ɰ0P(wsAQH.͚aͳgi?n6C33l`+['N&MZU_y4׏dB#m>ٻc -Z pm֌^3gbR#E32ʼעR(i343KR^=|zƿP9fǡ ׹39|s|B!x,S7*UbЙ3$kfՌ<uلXߦ%y_*ݣSӦMcڴi:5kOZN%YbE122bҤIL*Vxa/^,}%,ͨQ5jNے%K7x`\yJ:& ,+P}J?Zj,^XJzk !^>E )?=;cTռ~=u+UbTԶ';/ y&Yc%PWS*z}l,KHvZ>o)ڗgESO 1ﳤ3zvΜȈK iذ|߶-*&uO3n.AOOO Ekggszyzm6r.%W(Xd_rBF)gTBgΎ.'ݥijۧ󾒻;fi}M||s[;91…Rv+Wj_Wsy|ƍѸC#B4t&<>ŅfDQ?25kajdƯȃȴg Bfa?OZf :3w/23r]cOBoXUl?K##rs/6I !B!\rIi֫)Pf6)) z@Z͡煰dvUa!37ys5lϑ[kk>jDܤ>#=;΋ߥKRX[7bo" $g[YkԈ66 Jp0W4^cC߾xVŠ35eweNJ:a^IKݻ>''ڹzs߾͑!CpUؙeJV:nt[JOtKzWʨ-r3A tsc¨{-Z֖9;9.˖qUtVړx(chTbB!ppp IƬS'F~uE# ;27mB+:!x)u6ϜFNP;91 Cs\r+|q<*P W0(G_899qUILL| !crpp()ffL9W תZpHn~>g3Q^ċgBp0!dWZ󍨨(A(B!BGf6ỚلA鉉"_Zr6iVwVV#BzxzwWHHMpZ&)#{R1iӊIIP!B!R_kZzzڵ LDF!9"C,,\@B!Xkbm]ѡEg 86mڰ~z|}}qrrb̘1\~(1b 4Ԕ5k @Ν gҥ(JS*̟?aÆJ׮]I|B!KB!^/f͠kٺ5Cth(޽zvN"OVwϞdgd[7*]LO?r{+8W}T>/ VV̘?ϟg˄ 4у6m帷k@@^mB!(P(@TxjJ,;Ҥ jbiSZp֬Y̘1$_Nʕzܕ+WF]~m۶[aiݺ5weҥL:SNѣGb<͛sa_:uDVVÇgԨQ$''TP(Ū{DGG3g&Nȼy ee^|5MFxx83g$66ŋ+h֬}%))X qơT* cvB!MB!ss=}:oPÃdNm@>}^~~D_KӦ\ 51Q$\RI`mY*uSӦ\I5wF1c8a""pkkkJ%ffvHk5NO?Ҵ)_|}5:w xfw/|u^{ 3M iTZТ-5k)<\եzzji)gB!E53㘯/?ϯ׮A|gΰxx`S܌#-?= 2=66O|^KKK,5$ L4z$ݩU/_FeNrr2hnݺ4jԈ+WPzuc̘1 :犉ŋlݺwww*UDzweٲe8::pif͚U1fffԩSSSӇÇӮ];ڴiv9ԩS_~呮'x ܰKw{r3aFGsO!'N0xFT4! B!/ dfk$m*?\ݽ޽zowsbzjayq=&7aCeguNv`=\ƭ2nF\>z1p/ػ|%:HrmTٜmFƘ/3_3f|avp>laL #CD("Tt>ܿ?{Pwx܏Gu}}ݶ}}ޟ yeq}׭KΝ _&>>\#GxsjuGf̙DnڄSNIzmСiֵ+?\ooТgOVzB!x~hi ڵ~<ULXı46bdH9B)ܽ u++<<+((`ӦM> 6Ю];ur kkk~W?.ѷm64hNK=:AqӺukwY{nzqttйIv !_5/kiT6zt(B B+^gK֭8^<3yn:svѬkWv맟۩ trbEaJRä\Άcitq䤧JHΕTvE2Uo[_: ҨDCҫgo^>»O,>1E\Ha';;w['B![/=K{(,9z93ϰ099V5HI!O☆tڕy=pvf6*RedQ=ja&8fU%ݛx6n͛quueر̞=V=B;UϩӖՁ(B,54ZXn}>1ͺtQK*(EV..߻-L(&=ccr33K;Բ*U:'\K}{{cnoOڵDlH#hcP^oo *Mkئ ۴gxxyqnn\Q']ѮB!sݝ̼z*# {1%GۘgF&p3'K]]/5>_~|ܹs:upInܸ*W^2e /^T" " xY[gO>o.޾̈́}ȈWWjEg0>9#ebĄ1^^Lhߞkr(.Cqq`@6D9mKzdb"cbp27gϐ!O=^!)D!xtu8ӿήS 8mf,֭_?=~Y.@-kk~3q'6͝GPIF{ɗ.q* vHĺu$;_}EOt9h))ur22g\9vWrQbTWb KU'I~-5ϥKU Νs6lT<ccQNơJ711kW΍ϯmzk] tq!q'LOP*l8R~YjCB!axun>ͺ#X6`Ǘ-ì̍v0ftt$#l^fd vecR]y4 | nMڍ4j> d @¥_>\ATҬkW KRji&K͚ /б))ܾrܸcm33"6nd,43gxS403Ӈr__v: !B_6g۴|bFaaLI\p@=]]I U ,ۣ9'Oe˖5 U*jb\xd ՕQFѴiS[G^ѣ_ޞ77׭_Ӳ^=?_]fn.s:wfHV̪^D''0aZXZ[7^K{صk\MMp|II)]X lۖu+^l=[i`jJ/gg*bwR)Mc_zcBzE1jvT|cջiS7fcTϟuƶn_~6EHK-Bt8B!ϔ_ڄ{ydhX&҄""UflL^:zW쌉I7xKIIaѴjՊk2n8QրBTԏ'O'iߠFFvvyyYb]7or=-W`l,&&8Ԯ]U\C]]

PPP@gaC>GϚBؘ#h+D޿鐄B<%z.ݸN~.}|.mרvm?Pnnޞ|wݻ?򸵉IYKlHP!B!5J7ss==q433)^oZ\j 㢒gՅ Uwm}F !D%$$h:!OHB!B!qαmmx8V/QrM܌U8u&=ꁙ&&&@gBTVBGB!B!Lccu 32p c 4&P ,UPLj3}t>S=fT|9.JQ*1F(LLL>}BIP!B!5R`jFzT9iyyt;''ޱt434D[ ()@_~{RB'11l}%PPaiƍ\B!3 B!B!BxZXIӧ^p̅ D޿Ϸ(OWh+D޿Ou5+G~oْ}w0E.df?3߼oL'= ~nJEBTc̐I !el0x[ǚ@ﯿƭo_nB!6-dPTޥ7np6=M..Xh,&&DgfR!7Шl6,q)/O>&_̒7ё(s/XV!]~^T$B\9zk{8997jD/Ĵ~}w0zB!MYZАNC>PPP@gfo' |#\\<3_h-9Ɩd/&٨{}yׯ<17fXC!jʮBj!7s&n1|w?&r&uBAs=f̠øq<3m.}Zfڵ8sp|ׯW9l<+\ɰ5kh퍪"z5>1swsp<~ݷ~ `>4P(ԉ\~!ys*FnLۑ#QjiaXoo%K[Vj,!B灛1'h_ yl,Ϟ%=?QW)3mmltu2RfTjGPZݺ\fFF+(nVt4OxZ&CB! Bq7gl8K''ғ9e R`ҥҨ];J%.gQ0j}4["73۷9x0997{tv.S{:w.p+&OǢY|f4P10qct *$p%%znSI V-,,8B!vuuuK޿%9m JO oT($A(D5T2aCկG/*) -My>m^>7B !adBKNz:{.. =~Tu[[wRD;t[GDC4-__BBH}WEnS-A-HKL^RPa$FE:ՕOa IDATaݪ?~_lmq}5 kGffB!g:J%4i²MQ(Тp6йtڄLp71APRq޽g2⟳cefL+i dx! }!D B!WPT֩;;w_{AA\.Z5"JmX&* ?$qQlܹs'nnZ\ooψh^sl7oΦM חz͛7?ט1cٳ'۶m#77)((`q%Ν˶mˋGpETbŊJ]Yv-ӧOܹs/4oYf1`ڷoObb"X  ,ʕ+XkB<]ښ@!6 KK#6o ZnP(p~e,7fdG)Sm1}:Ǘ/'bFllx nnH eڲ]CCxz7WTQb#uiԮ~xnVwbcɫDұğ|>n&Ϗ<gǎ#B!ʧTY3uzg\fZU$41XZ*'e5BinΔ+WXrJ g}w=(] >>7n`]Tyhٲe_q-yݻvuu}P4iBxx84jԨT?oMUk=z4ՋaÆ :( $PTPB!UAP0qc6iR?$$2x-17ccrמwJBhFm99q򀬂&]L0! &Egi.^cÌ9VZtRN>Me˖J7e6oLtt4aaa,[ 4hO$''SPLU\HP!B!xx`67/ge NWA2OW!ZHPzj֌nn460P/Ry+:OLz&ULP;Gѯ_? 3fxSRѣ=nnnۗ%,)77 .K,> ///˗/W' 7|KKK֯__B<} @u <<<4B+++U*z`§j:!x9ӧc 4BjfNOP3b{˖t43{:UIIT*YZy*YQ=|KJE^iЀ1zx'BclmmƆׯk:!x~^ B!B!BX;AAᬟ|##pflLJE>z^U+tJ>|6tSh>05dM)9" B!B!B0bK˖ $Sł'EflLA1d<ߡ>[[d[˖Xo 99>sWN5OBB!B!#h+,vrbz x?&ɗ.**X UinNt6|lgB6+nߦiH )/B!DEHP!B!1 ٱY3 Y׮1yr+96uugӫ0Z!DM`tδn͋E*< q/ɓDB!I !B!BTАzݪJ%ZEV'%iW/W##t I s64$Օu͛clw|WBGBQƖ X1xШVVlp0H`TB!Cڵ9ၹ * ]^ ')'26FG O9B KK.z{3hS%RRɕ+~fB! B!< #Vxz2!$Ӣ?T70rTB!jlL'3x8 bdDJ i+!̴ٙnn4W߼UQVi'%2+9cY!(KB!3_*\ DSqZ::YXPW!Bb }Z()\7zv6mN |Z-7j>ffmݚIvv(A Bk PJfBQ]>!Ʃ͛Y+|ʯcI| NѥKϏY=bFtLu±DRk~ݛYZG"/;*QvW/׮{7ogٲ%.QvQnS\jh(kG`-].ߙt<+^\]ڹ}feDnTZ2 gg@ٳl0y/̖-Yҽ;~J^eeҥ3iO4B!Due>WW׭ Oˣɓ )[Q<;H!hi'p)@AELjމ&ߡB!DEIP!D9~a;vdJ) C1cƍ#AΘns!(S`׺5֮ߟ~~`X0dJYCCoRIs[@zΘݻ98ox^v6 \Q[b߶-OĹ={&Ã6k?]0zvo>MMi+ 75Y.3Px1ogxtL?cŴ11{0С9S++&0!$v#G>xB!ՙRA0I]P@Hܹ SonƄyyu)h+1!vܾţM:Fi: |||x뭷|}}/!ijB\̛G?s`,HON-`ҥҨ];J%.gQ0j}4["73۷9x0997{tv.S{:w.p+&ǍwUo;8p'6{ТGR}Kc>pPj2zq+&_[~("9:uViذ&Qx*zx8uiڹ34eKt14DTbdaD!BJ7FOQy4s5;m6h!DP0A^`dt4f(I99<}A|H]]]+ʥP(زe ZZZ:g[ڿ`kxyl5jIJe4Ϝ$BhܝXrf瓟Kfj*j`c:7oNyֲ̬411OI !иu4lzo//#Iʢ aGi$\֖ʑ%K˴A&+ڧא!1I_FSN}?!ٶ];ΥY׮ݠV{u-_oK.ѰaCz-ƏAY|θq/IJJbС{XY1c'&&+++ƌÄ ٳ'СCXҡCJ%K,ɓ޽'''5$Bh\ íoGANFza̡C[X`sN\ c@9}t ڹǎqӰM^5J/.4JT=z/[swZԫG#i7r$ YJPXM||h?juD\H ۴Aؘܬ*O!&[.Zӧ-J&=?lm\Jƒ5k޽;vvv̟?ڵkO߿?7oưD5+!-$A(Btu8gG=75ft4 gΔ*kh/Gر܌8x=uv~9}p~^NnH?VHܹ(q}}.^o~׶o׎}FΝq#\y!SG_׾}7s&ffunNԫ˗ M@ٳ?uWXZz׬"6l\a^`_r[BȨB!ĿE:uJS*( x?&&Up20@uĊ4BTȬNt͛3f SL`ӦM+SYO>o߾L:OOO,,,֭=3qDڷo,vvʕ+3 K,R066f͚5955իW9}޽{`jj eJH_ѣ}^^^ddd/JrJlllo}dFFF4mڴ҉`:uꄯ/ 6TU{mM6e…ODB!a0؏?rjfP(ppU>6 _ƢqcMLVZN~~t. G9|97bfcCc uv#GbXǗ-Ԗ-ӓ6oR%k+ܾ|?fϦ /N0%b){WO_'q_] wLI~a6oիKn뛘p짟ڵ=EXY3^5?fft4>t|ǎ)Bo|._&=?i*sI/!Dw N8ٳKܹ3GQoo%edd{n^}U>C&Nʕ+իÇ(񧫫+X ]tPvvv J]v1` ömK޽; .֭[ԭ[wwwurÃxnܸƲZnMTTTb))00vޭ~+tnn*=5$BTͻuy|0z4//}Z"XR2_f۶KzzOIyHD=[~Y,5>V'qݺ X*V=.Ʋ9x0?>#y]!Ą<,ZUzM||<7nd͸2v'DMtB!DET*x_Zܬ,/]bxf%֯B!Ws##=Jž}J%<<L9 'B!DaHP!&N4h7dff2f066FOOmmmRRR1b...|3uTu6l`[OOO DBBʐ*G{ѢE V\>^NfΜO? ^}уŋs|}}ٺu+9o#T222x"lW@\]]5jM6oU߿?~~~ KKK=Zĉxxxh:!2VVV$TO?t8BPsO'}+B!ZRNvgOIGZZBkYdƴTM ׯ_t8U* 88PUWh?øB!B!k::cm͒7V({9z㰎 y1,\Nܩ8R^g2nd9µkLCKG mͱvoY[ XQ))}> 76Vn]=3FIJ!*IQ!B!BgDK`:eK֫G-hnV.QFwjgGqyǕJbotR"Er'ydz3K'.q|b,^RQF zr7dJSST$_Mٿgj⩐B!B!ϘRIsszٙsQ*J5߹ oTT*&%6"ΐSi`&UЎwsǨ))\=s.j{R9ۮWsuNVQpV>rPj)Qj?AU"/7=*X~^>q,-}vD\32qx-[" B!B!B R(p12BGx.#?{EPpYa2[Cv w-L;B.qei]S{5T(s%oy/:6ukiGǡK'~ 敛ۚ } .z+P,ssQ~Xт:ę3df`̖Nowbׂ]M-[۝1K4/8pnc}$^Jdg&j[ƣ^nd]Y@=K!x"5[=5cBpn^Izō vs-IdB@Ӻ4})mEX=lj / |+n eׂ]ޑڟ/4{ BT B!B!z3؈XL-L]'#( ]r]9a%hO\dozCܟHV-n± ;iVv}DBA$.'qj)F,A݆u aGkճo\ 7cSKtp r*S['Zе!,Sْ[ʩ?.z1"q&^Jd- 0bBbi?Z n:N#n:kܾ~N#;==ҥduDžcHJܩ8RS0oV[ʽʽv᥏Ǒ{#ճSgy/I\ BZ$ n3>h2-~O xp1?A\d\=n MOmڏ}/B@B!B!B#"Di}kS['E=NzY5qnKNf:11#u`Z=Ғطx玜΍;Yz%]NelsӖY[x{nzܻEƩg͍㛎SPP@Nfǵ33o-;|༒PG]W' :+n^s)L߷|y&?]@hu„`la-C y&리#^&GЎÔ,%}c}ZGQTDPϬ,+'3Q?{WUps/VQDYNzT̽5G,-ڰq4Ԝe*̽ʍ#DQP q{?[}߯s~s.{t!Vf%xJEhֻzzg`fZ5H`W~س\9y_iήvu+=at!v6G J:y9aY˲`V%=>AZ\WN]o+Lf !O#I !xn0sGGz̜ ȈcH``5GVV׏6Dǽq[fh` u7` =gƫw'B!P Z  R 6$@LJOi֫n-۴.cǕWpw܈? Bwh+Ri|/i~nb#Mv̮3j.&};wIZПRKI5֌(=[kgkm?'Kvt 7Pa g\9qEsݦuXp:Ձ;N_J-%wFHs;s ȾMTpwuǧsƝ JZMZ\Z3dyѢcö B3;T lЪ/ xAOif6ftzS\IN]^Ĥ_W?:@Nf!Cùs~׫HKdcL )IB!.E,Etjŀ׮MYFmL =GØq]>z:8<ȄB!.6,4 MbT"Ɩ(UT(88ku}+ĨDV<9EHdݪ|tvv17'-.MӮhmz&lVڍhG8.]gȺuϙu7Sn fU=#d&vfޟrCQVuXRKIMQj)105 3#tOEYwk5w+/X$5qƭk_7?~ĨDJ0 ܠU2;y;1yJB!f B31)؇)/'m]{7 ̟5) kּsO !B[e73~eT +^Q,`5At|#ff_AU  x Kœ%f'ť(uw!ogkt5tk턓wA9 |?1^LCJl3$K,>چ&ta@Ct tY˴/OR|/]y B#sJ747,잳1fH-t`M33-qaҢ1ɫ?zC%F3B^=zϟdV֭pjт!P !Bw VS(SYk~FG_K!Z>6K`=vv&>zvgU+\GbO%;WG/l\l06%Z*iX-Y~Iy)aèYM5u t}kV99-{{?;SCFw=8ti薮#& B!O'cD%K35%2e8͙Ct)n%h\ (XcvʥK:-6cJ--.[}Gqpjт'Nu$MO~;_ޞ Jtoa{Mjy:$GDp>zA˖ IQCB޽d߼I>Vr2;jZ $&8'c^67⸝ϓħ֮eӇ>TϬ74q"my>JŵO6l+K7ivvhܣG!B!wyz{vnvojyq3f'R«{ݜ :Iٺr;vaGe+wɿNr3&LD[]]z~SɯT*B69s;sV $28k06ysfd~z`Fo^ߤ{%VMMHθg_KޚiO\ Fnv.+V0iҽ #Y{!Hgk.'{N'Yi]LmLm+1UaC< 7Zp! ̙3tV\ɕ+kXÇR=|aEGGBPP \\\XV!ijEB!lΟq7n E:eYԩC3p"#> ;wjR 1ch:x0mw7w./OX׫ǝ6nħ_?LLЪQm=rKq֬[Ο}Vb_a4?׮=?Eέ[Vɿ{ jnWCC $LLY.5xPI.W7+Xퟵ< TT!B"E3 ޝW(xu( N,(=Y]Ye 7կ^'<>|fכ 0,9fy}w;] Bڵ4ogcno#_o?/6k_c߲}$]N^ǥslCABӾ\\ VZ8w_k _@Mǚ bwXZt%IglkSWB f|6ޤE jBf&>uuM{(hra_/5U߽KVFFKsmӦ̾0nBlH).Vȿ{M+?)1#϶aCjΦ :p_Ļo_,42_~xNc^VnMiб#ZtB!<~mڍhGJ2wHm[Wx} { 6rrQT ;<ʾe֢?9XU=>Azٟt9F/6܃[888z:j6ffL;/O[G&ݛIwtؽo*"}sw߽ľTX6/7 ʩZձ­[?jFF=5c/:g9r$beW ٕ`m]>XAWWZHah!Ov|;S];믇SP`bg}Ɯݼ74TKw3l߮֎w/FFuabk[b[RnxҢ=$_nffqO~(Fq=2:wfWlGEP w, ;ܸv푎#B!*3#'̗dN92U"'3BA0}__8to_gMoo!&VRuEpM}[(JOWUZZ WZ ّV}UC2`"))S\鹑;;;;w.ׯ_t+++4hII BTr!vJ͚5={6(J8i{ڶm-[d8991m4M4tPڶm˺uVZL4*_HIIޣqPn]fϞ @׮]9p+WDT>RҥKy7qvv{`y& B!O cZ?~ZUliSAZWDĄr'%..p54sG2e2tU%-:'ѩ$;wB5ݛO&?7_Ãv&0vntt8a&y7'瑏)B!@HcfgFNf:XձQFZ2=>qk{]C]ogcdn)3:n8LeʨU^KEPL)*A~HK m| =/g^d$O:{Oʂ 7o$$$TivիWё?9sc[qiӦ \YfF^ʴ>|8=z^+6w.]_~aرL8IJ>}ŋ3m4{֯_ϔ)X.R,o0|Ο?ϲe˨Uիiٲ%'11DZ.RɆ Xp}+DJ !vi11 K/alcClccGrqz̚9Vj55lnD̜. vEĄΞ'џ~"yLll310!i߸1:^>vSkזi.cq}ڶɉ))oߎSw`̙k{vի86m!!JJµpr[8y3~RC_=K!B瑞>)VI]6}^TVvUT+ִ419AQJpA\;YӰ!  &l_~IÆ K$puu%66GGJ?p)))?^\zڵkk0I&iKuY\_[A]+++:ut`ժU888΂ *=Аc``p;z(/"m۶NI~CYh}'Hf !v:İwXҡ}i>m\G"?i;xvTBROmk2`RX޿?qq42gF,-_Y7 ;wߟ#դ W:t1s&ףX1p v{2J?aW:3ܙ&QŅŞ˹uhTyyW^雚rqVƏ={raN|^^*e~>fLc !B!G73]\V(T@Df&|WYQĉ8qcccͿ RX~=?hٲ&9퍽=k֬)^-[PvmMrUVUZG%.. iڴ)4iҤ6ywhܸ1~)QQQU>*N! dBjgde+KܳnWt[5w%ytG%o ml1sfS._.Gxt\iJo_\yJ/YZLt4kWWڇO~Wi!B!NJdnNp.eeռɶTV֯ua5Q@RѱcGq_E ExWvEKlk.ߒwD=cڵlذ///Ǝr)*@A(B!B!16T&zWZ 35{ =...%U%Aطo_=JZɹ IDATZfɓ'W^XuFll,} ''y xٰaseѢE^3339B!n B!B!BHhi^=5j6Z@>p#/aaIJun&OR[n^K.o>{*lӦ 5k֤[nl۶_^{-ZX*<==qvv^5i$ J$TՏdlpp0'$$7n'QQQS8;]v=zӧOXf1 Bg”˗ݻB!B!}lif`aK(.Pn߮JtVVVZ8qKKK>5jѣ̬ BRIPP : &иq2VT*۷ϠA?>9%b)}M]cec$22{^gi.^^^=`ͱ~Ѯ];u놵5x)XBUffk֌7/^dԅgOz:<=ihh?;!sHB!B!Ou,uuصk5G#p46t䍈U*򁨬,BCY\lmQ(B I !B!B!V)sM|MO;wwhBBjժ2 Un+ B!ύ`H38aqq ָu?xSkײrr%B!_iXѳ'{y=񱃢ir%ヒfӧYLȨQUkƍddoȐ ۬8u62~PO^w/Xz2Y3--Y}WU{R_1iSV. I{pUk~Ib}Rz:MLhmRXRIӧ2~| @g$jɓ| gҢ}4MRGSS^]ZĦ^O3-NN63sH{| :;&Ǐ#h?ܺu뉍)ؘ?$A(⹡gbRvZ]v_u(.Oї5B!cmhX!9nҿaG޷6zW{8?݄Ǔ^^%0ח/d۠A%ZGSSl׎VV\IOgx.YAx[U>۷G[䗰01n.%&NZ&"RR}BB2p 휝[Ϫ335mkϳ#=]Srtŋqa۷/}}"c !ē" B!Os8|9W!Mkצ'PU+/?s}o 3f`^6=f*wׯZXo"t 9aG/'=&gm[\9|JcbLӱxk-]b4/'sa#"0aμl2k@ٳB!Ͼ ?vWp#;ssfoue^ߺŘ[9qY^XvhVոM~Ioo7k]aFv6o'cbHuI.]ͣ%Ұ!Zڬ8uyGr)=: -J$ϧO3fdw]jsLMe®]amhP//7nUd^H\$hdmw]r!%^Ӧo SמsgR3Ϩ('$`K''#*4up`4@XL ],ȑ4sp`'~Kw&=DغÇSs9/wܲcƠULҿ cũSDfi@OO^mH{33z{:, =mmc/z>|XsO^Lnӆ+4og%-у]/p6{^D۹ޏ{*ek+Wu\X﬍mlװ!/ZЍ ;#{+7n7WIg|dݼzOOeB<I !x*l<K''/Y)/RS̡~ Xs[4w.fxv>[iKX=t(N-[m ZZ]VԢWO`I8~ȿ{M~HΝy!!<\̴4n&&tPC\!B?p7ljF~K2ۛ1a6FDpm8nnЭVVőRݝy;_8aUt`0L;qzƆ,_9o ^,҅Lݿ L+VhDllf !FQ }}YZp&)a(*y9=ь$իqrrjSܜdf>̾  BB/zq*1vB_A1iSg =@pS(W/DJf&nd}fiIˉJK۷9RQUʹZ-_NH\mea iefF߳gI%Φwue-9*I !vwI:a߸1uigQfˋx.ܩIK&&8nj}}eܹ}B!xר{-ZgZ֪EwwwxyiFRIb׬IR|lmK:#MoҬsIFGwl,33SQIy، j77?-j߾Dnn, ;r @J?x䉄N'%a\paZyE N%&2'a?9C9sR6jy=r15yGX2VVǩDmmkZ&&/ +Z=ˡXΧRgn(ح^ٳ,74ajP۵CPÉEYŇB7];wkWWϢ2jk`'y7* ZM>uB KJ!I !x*-[O~ ;>p ;;j8;svf|C (Ht34mZ\5 z~ ƍ֭3OgʕyJûMĮ]\صں5}<B!ijvxiS~;{w^VITgRp0&[Ջ/2 '33~:yp:ݼY"WոV*С̱2J}D6EEYGF$ۺ5Ja4qM== g;e; +h~Q՚حj[qY<ssK5)Ph~7Wy^mԈwbÅ s#ﷳgiwkk+VEѬR1 c71Ϲsdg\ɡuueB{.! 3ȑ oOU%_:x̶k6T1xJLLX1p wRR₎WCC1wt,:δ~mض z8 VDF <:wܹ[s۷1wB!fol-[rhpzԯϷf͍|11}))\y Zש^%\-,8SֆX٧E7ww]d[d$m4`M FVө* s 9e]xx1%r*yn Nnn,֍Çs5`Aeu17G>K&f޽K[dޣlnN''6EDp"jԯYSlMM% &*-;OkkN&&>P|&dsK3+52ep,=~'Nͭܙp86mbUϞ~A)M :ccN7ikk<##s7_!7Bj"64Wڿ`\KO?%)v͘={˚jJ?VSC_A˖abk&N$lf:P/^-*,Etq/\HܩSrHp6m4ox}6~C]ٳ9c)/ ֮5IIکSd_)Z!Br)-vXWTbb*LѪNaalxa6aw3l:9q5ֆ۹ZlpXGF2nvB☴gOzza[/^3gxo:9 6mP*t d3\JKcߕ+cQii|ٮ1t dsD>sӅ2+O&m{v싎BJ )jf7q.9$bmhY_f_t4#6ofץKDFoRq%ST q}kW ˖=2Kii|οJN~> дuYssqU~ٳO!%39;kΤ={iy*^,)|oό_䧓'yk6^bYY(__nߦ͊dfrw0cΑ#DaC[gg~Gή^~=9yyԵW%Eקaatrs+cy޼Y}/n;AA|g))S‚G)[߾,8vn4aYx.^9n=XBK%Tt J e /ݜMNfj@۴)s^rqa7m@TZ+klcC3oݢ=ǂ`Tj5cne֭Nff\?^'7ߠT(52)ؿ?6K\P(fgG’33sr?yu򎃃BR>B;ّVcإ k$Ƅ?ζm*$+%Bwq[U3ŏbniɊ1B'ĨB!B!nŋlxKiir 6mB_A8YWByǎ=~ï_gŋ[Zu4mlhrW<|7؏Z&m]Z0ww=<0P*.5-5F!!B!%FB!B!sYyyL " --ܸ15*Sqzˋ 8>*~ 0eGjL&OdFhy89ɩZ~b22p?Okkv튡Nu$kkY!H'U.֪%%Gz BLrru B!3}=<;GIxN82>%š.q(#c]̙̙3z1 !ģG}{(. B!B!B!]ynn57繣RV)%P65j=9s&IIBt3&_$B!B!B{Y3MYN߾M>MݝWml*=?++ 9hO4BܯAf| _b\WWtJ5E?|5+rnT|"˃7 b׹sBT(u%̬CB!B<&J%SSFDDpW&_xod'zRGT B!.TO?Uw8BQ]wK!B\dc=ΞRVqdX IDAT*;x[ÆtH2޾BBB!eai}}}ڷo_!Dn !B!o 9Ѥ #"LN&O&#?Ng0ɉqq57Ho@B!B!BiiAPhMWTWxBXIP!Sc…4o---ϟԩSqvv_bݣT*Yjc2YR$66Z`B!ĿԠ l8u ixnHە+;4oӂ; ( tp/::h/dBhhh\T< 33G֗B!Ӯj5y*5ݸ9)53?B`Ƌ/mkK[,=~޽Q1f[!Tۿ74߽{q]Nܵ E*u+.pt,Z/ghg1y>.^ԙ7O_pɌ-]ÇI{_ ђ%~U}O jܘ1xPDu)NϸNuvzB:ѧ]mfL(,TfMnݺEӦMiժ?,\6mp|||Xn}!$$ڵkSịG歷ޞӿttt۷/}~!cƌa߾}"AAAk׎ Zn}{Yȑ#9#F0j(~7Ǝ[bZʊ+++ϧ~JRR'Nf͚ӬY3֯_?ZOxO>k׮1}t:ʕ+;v,IIIFFF@eN>ʕ+3g׮]&==${=J%oZZZL2J1!B< coҒcd jfI.21X\_hLM2_`KD[/^dw~/ǧZ;,{wwiRD"(b/)FKb4'&15K]`  PX^? K s>v枙3evnia۷Yt89k~wYܯ3m.u#Ņ}7wT+3bh==I}grb I䳃s =J-(`S֭s޽fA462͛.HJb#۴nv6'KgB^p25LZ&,./P1k_]HjF zƺ{xr( BNgdp5;J%;3èsyyD'KGGDan_}Vmrr֫xIP!D366KKo'кuk[2ggg7oNBBvvvngrrr ???T*SLhF/ѹsgݻ1fffT* 5_x1*5k֠Ur]vruiɍFܧM7nӧO?g[<Hҥ$A¼9ޣiսb;)w.$`Ĝu_khkh8ONfms$DԾIS;OEm1>Iڕ4n,|xW{7{*\qe;sܼvrFص֌ɷo)**ՖW,B[3qڿaHP!+22(4e|j K.sӧB~W<),,$77P Rm|III ͛LU_:nnn.#** oo'ѵ/((^cY>}4PH $&&RPP@NNnݪkYܿ{IHH U۷W'9N.Y B![;3t4iB c -Wrr6EEdLu~͛6Ȉ~e OJR':7iA}7gpu:\\\?[ؘcׯё?bbP)toL‚ x9RNdo\Y<7xr)=cˋTJ ֕n[K#K jڨ(&$J~a!y숉HxrtT':X[<"n^ؑA֑>""޺5&Unoofڨ(hA`4.6?J{U^DR=߼IaI"4lY65n͚IrPK =KԮMHFȒߧ^]MPN?@{$﫷Ukv5o @k1<=1t.k^ƔV>8~sul]֟{V:=I !xbW_UXgZCeK5TwVZ1c \\\ӓj-((M6lڴºU6YZvltY~'"ԤO>|8ӧOg899믿NFFF Bե\˪RT*>.]VPPbB!;>5OO֝=˦hfҲ%sraC[C??̸(7OQS*reUJ%eJ8B>T[*R\devYrwGƸaobʓ'jSXrYݻ3M ;?lnN3c/]"@=OUrUTTT}u 鉋ZZt{v ? [ccV:@qpܸjyӓQmڰ9?'yϞLz4_EE> |Ghƭ |^sx";#q@GL*\lں)_ @'z%Ua؋}vuRCמFquk K5Yyk y\<|Q#Ax7zA=c=;8R_@gΨ*iP': 鄮.u/#^&NN<34viN&zeLM&Vضcd{7‹$>8'9zC3?PMiҳ|tЁP,YRe Ə?kB!ĿӺtaZ.y3cH˖l&'?S2'g鱏$& 7W=0$6+CZ|?>^]b͛$޻}d$ݛ5cF׿nAQS/3ѡqq7*ԖJŋ̬0hut*wwkkU*"q(yhеkrm®^%l|r99rտkU+ۏ =<>2 Z[ZKVTOI;2cG۳atK*G[=Z=jU%[e?Tlc߱xLFaA! g0mlJXug^|&t}+_5 g%@-vuUkR/,*hfAJl _';#]{ZQ!Q`ZuoB ]C]3?ϝ;XT8 }Xv'F ּ'Qd 7Ka %4ݞSq ?Gzu?Юg;te B!cl(J ?˗ 㭷"66LXX-bҥm:v숁_|Oo41OXX.\͛1uT:t(˗/'662k,<+IIIܽ{n:DMḇcpuu:ޠA8v&Mرc?mgDEE1sLۧ7776o̵k׸}6}zmۈeΝ?^=G̍7¢yy5A!Q|r(!o36 2>͚SPq&5Ogc WvTTTTaĠ6ׯ'<.Eǎ cM=.ݚ5㍝;~t Ltu+%?{{&&):IILRzݭ1׳gIs3ݛ8}2o$&l^1ݳk:{+MLz:yyVN$%KJJ8֟=P˔71 '29U'OVhۣT(XEjFƃqcb2y`®^z?bc[R[P憇}G{Mƍ1Ç97GsڶmK/D||<%O*JKqpp`5:.I $&&#񼽽Y|9aaaۗX-Z}~-}ڵk,^¹|$$$Gɍ:pqxWqwwgڴiS3f`̙XYY_1iMbB!x uQ7x~ݙK#8&k9z:M̪^tq#og}vrb~@s}8Zp0N64ĴD*;m2ˋ ۰F[Bz… ^_mDk:jU,MeRVL0"oϊ=x(+i-c cݼGxuٖ+r?>m]e{xB III #B!oRpܜڵㅶm1۴*K~٫S='41XjR/bliL~<㗎#\8p[׋K}͕ٚ.úR۷i~:@rL2wRP*0ojN6M+S.K{yr2sP(8TZ} ڷFQzJ>O?pSZvoIvX3mM۩4{[7O)1*⑩} WKKJ !BONE~a! ՔβX⑐qVĨ Ͽ_i}v"#UabmR!2ĨBwB!BGRi}!BOX;m-1}Jr ' B!B!B!BB!~>u e=_cmo͚C!%B!*P(j~Μ9888cU<#%%.]}>n EחZQHIIB!G"I;QQu;wPΝ:gU~8yիi4?^?o/>Vզ )ӧ_š;w] !qԠB!*G jT+EEEڱ*;GRӨ{n=E#Bd܂:gU_O+|ϒ~-(Kq#==YЧ#=T.moAiaQEE4 B!DBرcgĉ̛73gpU裏شinbұcGwe֬Y-SNeʔ)=nFFSLaǎ1~ m"##5k1tP^|Ev y%,̙ٳ),,d…Zׯӿmڴ!>>sGQXX#t]K#GgÆ 4lؐӧ3bu2i$N8o_ox"5ѣG@ٿ?gMɓ}q=}]BBB_$00WWWn޼#r WfܸqO8vnnn|W4k֌ɓ'w^9s&A!Q{=8X=үTAxͭN&'Q^ʝlMMW/z;9aB`lnge1uNs++NN<ߦ ]]ɝ8׬a` <ȑDܙ#MLuObۅ l9Ae`ۻv1{{u﫱۶.ac̬{LNڲ%/mKW;;6sYAx'4˷o3ukvꄳ9733q\_}も(N$%_ޡ=֍^?7ޠ;.]bĿ&*GN~>scϕ+MKB_[3wouOGJwӦ6  }2<ٴ44`d6n׎vVVUdB_~M}x\BgBT)n>Am%A(q)-Z{G֭133cȑ0gڴi֭["66L8DBBBok.fϞMV裏8}4nenܿ#F0o>>\~+++&OLjj*ׯĉ0m4y:w̮]0`۷oϏΨQ^Rf¢?پ};}abb?LPPFˋf͚(ǪUjԗ{9}6ӧO'--MMvv6iٲ%W\aѢElܸs?ӯ_?XXrԔ`ٱzj:vHjj*;vN:eQTꫯ2i$}yFݺucڴi|nj1W<4ZB!Dm}ֳ'733Iw-#G`&lnΖ#1lZZZ\} c2M6Ν1fˌ7V1uk b;_?,*}7߳eH6EUMuҤYܛobװa⇓'1ѡsu[fݬR'R699hݚy{ϺsYӦaUrp eJN134= }}oom#aǏiÆhTlnΊ'5hQir3gX޿?$ݿO0^vscQ߾%jǎ[[Ɋҭ7f̨r_M+͔Nfg~BfiV;!RnB$B>>۷g׮]l۶'*J,Yx1;vwZܹsOaffJаF%E;t%OwgDEEk,Z&LPҼys7n\T&&& .ТE 0`Fs :99Ѯ];5kŋqqqaÆhkk/֭ҥKܹS>–-[=zzY_f[ 6={ӱcGbbbpvvfƍA!Q2FAT*,kpRV^Qޝ%xNffJPWWcVۭu ssbnb˅ ڴ2mKNfzzDWW jYr͛svFšٸ1$ȪRd=>e^alpe*d1wo(ta֭d塧EIW)Av<%߉Х$܂fB۶,_M2eGabnNv\Ğ+W,Ǘz7n>bIe[~BQOGE05fGB! ..774}N::9XG%55U8hذ!;whw]/_΁8z(c&NX(((`Сaʕ_~ͱXP(h۶-'N`„ (ZDӦMA___t=~%֮]"??СCW[)H+[VO:!BfW^ߟ<=Yw,w/kfH˖Uns >Wo=憽 +O+56eD\F!(kwr")IDRP\Q8| djlW>*I>WM1M$Ņ'O2L­24!7 xm.z56oaj~XGyg3== ^VMB_2B!_e!98s <۾}{RRR8p@入%vR/{.Ghb &MD`` XZZj166&++Kc;JGVHIccc233kU&jC] ڵk\tI,<<+WG}ȑ#qww'>>k쫲~ -- bcc+~ԕ B5%3/562bZ.7,:vLHGr>2͚1kWz::̌5z0̨$V~ui;TX)xe{;~Ϯ˗5[q$<<lM40kj[MϿtڽڱ#;ccYz)_=JŰVi6sv.hk1ӲKikK#SW%aB>ܷr\FEwB;A(ywr  "''?mmm&Lȑ#7oƍcҤI 0dZQT~H~~>|'k$+}}}ٸq#>>>W_ѦGL>`<==144iӦ,X3l04h@DD9 … XXX`nn^>ldQQzyMcL=RyXZyK̞=;w_jYf,Z###Xx1%s端"** [[[LLL5j .W^a׏;wF-2d渹yfZj!5>I B!=+"#9::W[,$E q1=L}:8#&^NN`= NRzzxfmiըU4z<1ch\BFuƸt"cmc??|/,dGL :IJTRI?gg?NצMild£GQ4 gct4>Ց#kiQۺ%5=wkkY&xx`EÒy:ʊBBpqаcw^:ЪQ#)XZҴaC2k^J"&u숞lmYu\wDWsi1v6%l'ƏG\ZsTJ XXɉYݺ9^)X2} &ݝ|}1GD9Np;+1ch3v6uRAؘL@mĿCF !x,(JB^n&Mbxyyѿi^2J",,޽{d?~CKUZ`={>`ҤI<h2{l:uĈ#1c 0@3fP /7o| vח={??~s,--J%+W$;;=zb ,X@F43|p0`>|Rɞ={xXl :ǏkPIHH*NMcB!xz::2ݝ[b嗬;{޺Ũ͛iw|?L)3.]P)tX yxm[zy134a6>u$ܽx,_^e|wsruo2r$_æ_˙ƺCyC==;򛳻w-#6ndTh/NPe^p!ׯr X溶*z 0;<M0fAӺtaiOJڵG$$,>}64/CL7kksW(ꬿ_bS x/GGbe@''N䝮]p.#OX[& WRj"+/|٫Wnڔӹ̒54*R+feŲ2rsy͍OYw, k#Bbcp~߸-cҲzd=ޫW)_Eo':헛.Z3vvl5Jcݍ]۵Sltex\kؕv};qq$gdϷiV}w 4jϝ#YY'\]/OKfPΝ~_,YB6 )S]Wڷ}B7;w?]V71Fʢx7m֑#ݿUsddB!B!O+CC S,~LZ;ƚ2zff&$SZʲI/^$3/8ě!!$޻GjFG?B_1lpla/=gK{^HOgnIqrp0l9S]]Φ=tr\>U.^VV#[>r + @,.fzEEE420id`8w/(s'cܘOO3dz#*'%%aTMB!x\9uGh]TOYݻwu8ƸҲe}#Cmmt4@K y(tiڔ##iOstct4!/3CCNfÙKV^QޝdfX\,,hT:&*[tWW\B̭[ZXRz:_9·ǎϩܭysgn@&Mpqazzr~}KX?Opl,HwBr YpqQR*q; H:;w\!tf]΋(DU$A(ejji}!B!#gQI;!&B2Ba% GwgT6ٸJɺgͬ{Y;xpFneaAnA_ mVXyѴdnAR>Rsr4^ݹ͂޽q23#^W#Ah\RW;v׏?473_7n@Ĺ>B!B!BQ꒙HmCs33p45O_1K@WWqXW\\0gùsxttT/tj: tv?+**zhiݛ!6ƨ֭kؐ 7o#&{GFТxvT~]-bJNL~HYүn[ߡ6FFD=uv^9;3ݼж-_ĞW5go<"/[[~={}5|Ց#D`klFIWu;p&驓=<{7J%@V'%# JǤcz$%@NAx|vzZZ,߿F+?% B!B!B0=CB*AUoXwwFoJ[4*nS(FU f\\=z4 []]0&-Fm̭,[[/j㾔NzVVwxִps¢"na4616f,`mT=Y ^^1df~4@s[ʀ_%=ǎ{%ЁԌ |Vff&Wxd77f2eKKO}߾0pz׮a3>*Mkؐ]0PA:+B4i҄ׯckkKbbb}#U+!ҿGDFFw8BQ:?slccNJ c 3}z},>~m#GɩG.ܼI%K7v,w8B<ԜyX9$''W.22RF !B!BK.eĉ888T]QGGGzŋYr%˗/'((  333}Y4֗~ DTR_MIQ!xIP!c|/}+++LBLLvǏsΘн{w"##+;''Yfꊥ%'OĉoM4!==]lƌؐVK``uWuĉRTB!ϟ`׮]ݻ mXd seٲelٲ>@>''VZeN8ƍ+!!!0`rss /ڵkO̜9-[l۶C2x`3f sO?Ӿ:u*͛7gΝk׎!Ch$~mvٳYd ;۷oXO4iܿ#F0rHƌiXt)o&} 0@$ucǎ[n>|JiR&mƎȑ#پ};iii9BRIPP>`ݺuw]{i?ݻ;wbddJKKc?3gкuk NbѢE{>}33gϞ4oޜ+Vhcٲeߟƍתkj899i?R^bb" Eر]j|z*( bddTrB!BQ7n;0elll9s&'Of׮]@ =zЩS'nʙ3gx饗*wȑ0gڴi֭["66͛ǡCxٱc|ܹKK*;v,S!y?ײ7o o67odoߞ͛73tP?NӦM֮1B!#77ٳg /tR6mh{.k׮hz+++}]-ZÖ-[>|ƾ^|E~m͛O?ٳ'P}:ڵw5]D2" ESc$1Hƶ4R5p6K6ZUU5cCAc&hV D !y fΜɊ+xgY& | ۷tyeboovvvtmj}L0Zj2ժUcΜ94hЀӧOtRzg݅q[hA-̯ׯϚ5kXt)7o~:~Ń&7_j9`L㱲"!!+W|_SOq\<,\&Mpeٗ˗7K6777ۗX<<<ȳLDDB)6LɒBhh(M6n~իׯ_TR_T-[=XYY1dsQQQ۷evlٲoX[[`|||1bwޡe˖y[bE/#7VVV-[/AK;rׯ_k׮y_ CBBgϞ5ϊ cÆ l۶ .c:u2₻;;w4 :vhqyٵk8r13f01L\~{]+ZYYQ^=vA߾}ٱcϟ7́ƍ_$yŁعsy.66 ^|Eӱ/E4mڴiË/ȋ/o 4 ..P;w^iӆ#FNJ7͙3-[cRRRHMMeŊ4o<[ իWoAAA=Ν;_N۶msmoas e˖Ѻukmmڴ5-nVVVVٮHVJ1*""Vr̃p lZZye˖ѤIeڴicQGLL &FYQٴi/6{'O裏^:׿ߤIXv6捈HVMxgk^cp˖-ۗu2gðaøzjՕu»=h̘1ƚ۷G啯6ǝqƻΌbAb?d2q [tEyYټy35j`ܸqTT)syZ{OG}?ϒ%Kػw/͚57`eTlٲу?ė_~oq6߯CSO`0:I1zLDD-A(""V/ l~,eddPD on1d{2wƍXYYqY._L2e v7"""""U^{{{.\x_3>s7oȑ#۶l@!sttZjDFF2jԨ<רQ#\\\Xf9err2;vYf… :'''^u|}}\2Fvq$x{{{_?BhYf9U(իi׮]sR_0`:{W\6079}?oiӦ̚5D^N8ԩSs-өS'&Nhm9::rumM1*Hrr"#%NFdPͫT)X%R\iPDDZ:u",,k׮Í7ؿy̟?o72`^uEw~G &L`̘1X[[ӧOXn TR' Ah"<<< ȑ#tg3e^x^uzIʕ9t+V0yߺ d24tԉ_ĉqrr8b5L777&O6mgϞlٲJ*1tP>#\B׮])Qw󄄄l2NN:QBGZZd2v%v,]9sпlyԩC G :7\`` +Vu899P?z7?7*.M-Y/m;}noq?Y<{yf&X呺Au7N@x.W9qfV?Nlb"W҈6ʥK[gSUʔ:uxn] )bTDDZϳqF>f͚eQ駟'$$8pݻw'oB=z0ydy{V^̙3<HHy͕E9y8{,>}̚5hZlIVزe9`q&^m/?xwڵ+'f;nU&;vZN|8]>,8t=|b1bVVVk`3lGYa}:~~~ӻwonܸAhh(~-'Nɉ3by4iBŊY|\_}&**uM}9<==ꫯrl;@dd$-[ɓT\0Ν?O?mQ^DOAޯDDD}IQ3ߥ5FbѢE9rC)+WWWl gyA|w6VVtU [++޷Ϝb494NؓȪcǨQ_};vw/~RڼR/]`WadLq\-Os##FLmӆj=fײ=Y/;q* %˚NdDǓArݡޏ?~Wҡ$gb1zϹsΙø-3,[Ν%x22Ǎҟ2dJ:ׯORtĭوΝ˶`ߺU0aC=<_wIHq\`0;Pq>kߞ^Yf^LItU+ks;Ɯf̝g2|:xzfϏmY<Ƈ~?ιsr-""RVܧ޳fѳvmF4m sF%5=֭qe֭sfppXcDӦ|o/,XqsrFF}-{Z %syp-':w5o|^ v~22`Y}WWx1cc:5= o?N?D;wrQQt;.66VV'Bh(ڶG8r:̉ K%yᩧ} zrR.jB)V222(]t)G%W;w /u8[r}+QDT V.%sۆ6nlٳlY*.M/rj*N,_.k/glws0) WGGTz۹~n MB53QGC Ro }YٷͧNѪj|_.YҢ@??| hbO<ӧv-jٳ|w[O`R&i;q@3d(ggG#ww 柭FUQ o==䓼RnMnNN X}%KրZ~ ۳ l;} ))gfbà\bo;hUjZ4tscNL S۴aS|sDsߙֶ-/֬Im$&f+dkKʍyյfM._&6I))LH{͚yS 1ysO-ޝ6˵kc[K}{ΝXkF̹s>l114RO?MUcl9u*4yupzת kCII8QTle-oŊZg֖I[𸃃EzќL&s9'hSvvMLd k(._yLnՊ.^^j;μ߲%#׮h0УV-ʔ,Iԯ~=|}yIsWg֭|m_8o$<<O,ãLŋJOzg\V=J'g;;JY[Zz\rJڜNN9o~: IDAT:Tpp0Ǚr^%-#l?so)gg&<BB0iS.^'|k=jfpÆ|k.c[zdft4mؑfaaw֖Ν4:p껺 /PO-ʽWiNRJ '[ڣoYÛk֐IcwwvbQ&tY׬+S$Q |kWhƘfVVɑKjռ kܘ+֭t_3մ)ޱcJS';vˋbKupE]L:Ԩ@2y]{G4iB?3{6W;ּ/GEѧ~}Zyq#n} -ۢyB)kkܝ(S}Wx%gLwǧcwwwpssw^DJDD}Q{ruu61 {{ƏQ223d kO`]^{龬9~~!C(SÑP¯]#9wGzݬbbbQYg{B)6ׯoCBBgϞbŊamƅ HOO:[]=z-::իCJJ x\opWߟjժѱcG^{5z`0G?+bⵕy-[зo_֭˜9sسgÆ իyuݮ;##gggKllߡC 75nܘӧO3yd.\@-ѣEYoȣKgE ZQi}{1Np'L`^llQP)G)4PDD9͛7gȑm[lF@A2uTΟ?OӦM﻾k׮tڕkҦMBCCTK&%%/CDDDD).{w)3NNf\(w/˖`4putr<]o5iB92s|/GF)6y}X}8OjZNN<]s:v %NFaT.]/i<4رc-b׮] 4#G`2={ҠAԩS9|0;v`DDDz\BB^^^,]@=z4K,ڵ0j׮MJ dر$ """"hPYg)Σw+n/,Ҩ[C$A›o֨Nl8yx??Nвeq~ٓ]o@2 B) C^z%>̻Kzz::t`ĉ 0 cږhdL0ٳg3j(ʕ+GF\ƍ9r˗/߭J*رcŅ@[jEPPzرcӻwBy[DDDD~?,"sa1a^|)^S+WVvٰ#/W":tcxkײ3!{{պu)ooSiOolgGZ쳅Np<˖AŊ̌t֯ (S$hVy*_m t##˼.]xg:k R\9s\ɡ$Vmfk˱zrT6ԩkL7陙|/  qfuDV;FrXwՈ]޽-bp!WX+9]괫^w֭@ V#hR &ndd~=8kVZqEqqx+}m7_ug<]MU/W^^]EDX ˶ߟ %K2yd&OlQ7l233-^;88;nlǘ1c3fL9sf'_""""Qْ"R\]IM{Z>,_hN›oZ5FYO?-##<3Ιkތlڔt>ܾ)((s˷3C9{ q.oҥl:uCҟ28w oY լEKח͛i6r$6Yt5Y! Ȼ1觟Xӫps0y4tscI7^aFd֭ylY۷1>/lі;wҶZ5vB[c6l[Z,;k| ,,cn=*?>]:@geBo={Ѥ nNd|yPӓOq۶ [#(UҤ-[x,>O-K>pRO:;3qj0];1˕NNq}ѦZ5)C[sSbb~L&22Xеyo3th! WW? 'OV9Iu}ފ&Md oܠ5O$5#e={bocC'VCVgh=˖r4 .dkkn _x![^|ٴR%jyL i {)ogG'އʗPRRKb7 G*XY3Uѳvd]Q cޥr4@(""""""""""$'svfHH RYqmR NN,;|9wq|}3M&"dXAT>O>ky;e{nkU-f<8svV (@%iβÇ ,gggiILiJ,;|&gI&v=wi&%yk`s݉SkmĘU{s##`^g^/xg\as|кgxOߵJQT{R˜rfΝK-xF=-B`|kyg`oOjZZ0h[SsXlY5vwa,;|"<ժ4 F2Vd2qa;г5x~~`K&/-^Lٴ)O:;N?p˲&v'{:ux{Z:Di[[N]D9x+S:dە)FsSё~~ #:!F_|NB={K[AWRS[KMOgөSu^L[)(}]]Y/Tttdַk͚tYǏomݚJ9$9~ܜb4?ٙ[).U'$l^g>v̢Ld|<~zQLNG[[^[9wSdIUڼJK-98 oKmϲe)Y{5@¸8ZxxԨ(쬭v+emkr5mmi5ʕ쯱-Z0.}GPz7g&'Ñ# j uGo؀+5˗JZa{R%ρNH`]jd fFG׭ieaHÆ j0ʊ, gfߑԯbEml`vO޽9%N~~xϚE =Z4;pspիMLʊիnE>bxe ݩ۷hcC FEih0076O> 66b hӕ*<& .pmF `.slLܲ putԥK;`oZbԆ wTh ɉ~YIJIɉ@OOYכ+Uc##t {{== %^wsΝr%~˳W/]xE.^:n˫%JW/ZE .]3\֖Ν4:p껺 /POsnY'@ h+Wh_]>{s1޵ٻvQ 'VY)S0 <@ҥiV2K{Cٴ)_h>۵k3aC5yT)oْ/Vwz|չs>PWqqL۱KӬreF5kF,MʃcLwǧcwwwpss_-pDDr+)y$"GD?bDQ ߻e7C)722>c}}|;HnƇ~?ιsr-""""""""""""dk|s'723޸qQ$(Ũ Tr2UM s:tƦCXfxfY(\(uP(uܦgAd[a<ʔ!s8 H5:3 bfL4ӻwo k>8q"ׯرc2p@FAؼy37ofܹDFFҼysF#fbϞ=Z5jzBODDDQ"׻j2qʕCD$WW EDX /dĈJddœǻvg!$$?L4[h=K.NŊ9|0:uݻ߿?=z 99={ f̘A۶m;w.K6o3]޽{1c?xݻ7VSN| 6DJ*eqVXAy7/`888Я_?sX>S&L`୷"))9sz]### 4߀ȯ)SyfMFժUܹs̟?{ʕ+3m4ʖ-k>v̘1t֍%K`ggsH.+:\UD""RL,Y2zh"""PE1c/2~x|}}qvv]v̞=%Kȑ#iڴ)p۶nʊ+7ohF6L8880Ɯ7|C[ L+W^x+W舓...َ͚￧}|'‚ ,np$''3o<cyhooWo4DEEѲeKRy_ҥTR9ˋ3g|""""R0?,"+++RNNGD$WW&9%|B)rgϞe|۟}Ynj~cyUJJ VK.ی9yѱcGCŊO,ЩS\c1 i&_qW\|sʕ+޽{HKKc|۟{9fΜɅ (_<7!!!gϚx X6lAAAZ|Uc۵kW󉈈H ++XDXc`6CDD侘șI>}wQZj9s AAAlڴjժ1c l)r&BA+lw獝YS@Ν;#^z:6ODDDD ?EDUX___֬Ycf2XvM ֭[GժUspp0+[, `͚5 6SMZZ˖-+Ϝ9Ñ#G̯###4?|;ERjjjuбcG~G+VE槟{fɒ%L:3gl[JJJ&"""(SYgB)t¬Yxپ};ݻw'==)ɓ'OnXt)ǎcժU\zk׮1h 6nȉ'ذa+W4pxС&L ..(>39r{d2eWtiz͏?7| 0?|;eܹs9<׮]˱ݻO?1x`5jaaa㾮)Ν;"::@ <+Wr1m… @%))<-""""SYgB)FI>}Oٳ' <8\~͛o(QK.Svm~mZju,\>}wKϞ=9wyni C}.C VZ̛7ϼ/͍!CXWغu+,[qoy-%%Gr>޼lA{qA,X@͚5kvޝMHbb9V38y$sͱ"`(DDDFaaaECCgy$''3o<XB _רQGAn,ݻ7 `tڕVZ?G/IDATz,\#Gj*k^zcxyyQL5**FѦMTB&MpppdɒX[[bqd|"Rx4@(""""""""""RHׯo!!!gRbE6l`۶m\ptsϙW^ۢ^:111iQOjj*/u͍ͲeXt)-Zm۶,X\&YQE䯧BN9ryۖ-[ eV___Niڴ}חtڕ]vZڴiChh(*UtҤ%qH:GE@@;v`ѢEڵAqL&}ݳgO4h@߾}:u*fǎL<\KHHˋK|Gfɒ%>|]vFڵT߿;vDZZ@UDEDDDDDDDDDD `q&`m/?xwڵ+'f;nh4~zڷoٳ_$:::ۺYݸq#Gp5BR;v,~~~{{{[jEPPzŅj;_ "7}t>c9w\.>s T\ Gzjv횯fddТE ;w.~!V688ME^r}70h FIRR\`0XO?e„ ̞=ƍgot )SyfMꫯpww`4i҄=zHbb"M41;fF#K,aƌ:_J1*""E '''8q"jղ]zuUӧ\ro޼$}z兝]***-[@*UJ. J̙3 t>)|?,"""r4PDDZ111hWV-233 .I&TXޢnT,_J*on4k,_ԯ_|sLJΞ=1 4 ..??֟հaϩ[.Gرc>6?7kDDDDRYg4PDDZmۖSfۗEn &)_z"zGGGVVVܹ3 ,Z%KP^= |pcs DDDDR+ TXUZ ]n޶gΞ=K Kǎ9}4G5oۼy3`ggG޽Yd SNe̙7Uq"""" |o5~x<==: .)EByh;Hǎ?>Ǐgƍ ><_Zh3;vdŊ|Ջƍ[kӓ^z5jvvvOBL|?u;wEttty&O.DuA&IOV||uaÆٻ4@c;vh޼y PLL*tzWpBYFOuY%$$H/^"޽[|rmCCCٳgkĉ*..օ  6mڤ#F(!!A駟i&}'0j̘1ڰaTPP Ij֬Yu8zfϞ3g/Prr/_.WWW͚5z^vvl٢+V`0>Paao^@kg+==]qqqܹrrr/鿿F%dv.%K(44T)))rqqyMYA@hw﮼*3F^QQ/_.Ţ~)22R'Nr~_JJõxbIRrrڷo_%K7%I}U6m4rHm۶M=2224| ]svv֘1c`0h6m2$h4㕶Nii9uUx}ԨQ͛7նm[IR>}$fY,YQŋmaQQQ6mRSSc.]l;r:]ͫtNpM4IPrrVZ5khΜ9!\]oh0$I;v,:)""R[[?YŞ <&W'OV^^SNUfN>Ν;Wz<ܱݪU+:y򤢢+IݻJKK_Kׯ_of}2k݃-F޽[ ַ~[GjȐ!A{pqqQXXRRRxJJJJV`Ot|yyyٻ փ/!@CsƍiȐ!ս{*tѭ^ZThhL^zի:|bcce0`SNǭ/ѣG?Pp%UX^^^阻´diʕ v=r4!!Acǎ7o^i &h񊌌[oڵK111;wNaaaڻwoHk׮q㆒/gggIR``bcc-OOOlRM1'<,++bs5PGiϟ[niںu&NHݻzlxb2L>|6m2VAAz_] ,Α+W/իվ}{YW `twޚ>}>#Y,k۶m?iǎz;wV+((H[nƍ{nuI˖-̙3k\z())ѕ+WtΝϫJTTrss /(00PQQQcVpprss^{k"Lf{hd:K.jhػđdX\{SMKgΜw)ꁭWYYYtPbwnO8]vٻvB!@B@8Bp ! lڴI |ry{{[޽[FcFif̘44w4n￯x}7jѢ4g{V%ɤjժN+999=k.{4XԃN;SKyvjrvvVv}K-R~~P;w|}}_|Q111ѣG+==] 2h4j ƌD<7jÆ ԝwuW&IIIIt ش5d+!!A֭?7xҹ3fSV9_PPJJJo>EDDh*,,`c0*}XAI2ͲX,˫vLuEׯ?\Zx^jX[NAiĈtЯ:A6pW|5=[NNN{4.Ǝ+ŢC)%%E~~~ڵkkk{8:٬4yxxs!!!m}… ӄ TKpp]+WX_KOOݻw4⢰0(66V*--v%%%vh?XFQJLL3ghܹ6m9diF:z߯SjzNjkѢErqqIX^^nsbMΝ;'?N"##uq]zU/gggIR``222'<'UmYӶ^^^R֭pBK5kJJJl 4JKKMy׷VU裯;vL͚5ɓh5iҤB-^SUXӚʕ+sN(i֬YƍBCC`kN?C;Lf{@%"OOOڻĉ9r;#Fػ5*++KMb]KLLTd2tiǫ[n6lKPb{rss1eʔz_Hgϖ߯Heddh$N:hN:{Uyյ׋PDDD a! 2L.@#DO0@ wP|% nnn2Y,;W{UBK.jZ3H*̔l~U,]p ! !@B@8Bp ! !@B@8B4K.ٻOإKJIENDB`neo-0.3.3/doc/source/images/base_schematic.png0000644000175000017500000030731012265516260022325 0ustar sgarciasgarcia00000000000000PNG  IHDRsBIT|d pHYsaa?itEXtSoftwarewww.inkscape.org< IDATxyXT o憚ffnXfYZdiYk*  sl<8s=w@}̹EQB!B@!BKt!B!JI҅B!(a$IB!$]!BFt!B!JI҅B!(a$IB!$]!BFt!B!JI҅B!(a$IB!$]!BFgӧO'-- ///wpXt)QQQ[o;!BQ@*EQ}Qz-rrr=fii ...+X[[?-;;;9r$ ,(pI6mYf>|X!B*u#˖-{h_֌=ԽMB!BJm@׮]up$$$0uTn߾B!(Jm^zu.]ﱀ~7ޠEB!(du|nݚӧk_9rD!BҦԎ?矩ӧO~zBCC9uʕ^zxyy1dȐyfժU8qp.^...mۖ޽{SR'-993g>*TxB!D􇈍>wvv~v͛'|BzzܹsVOVZEժUFpp0Ç'$$DҥKi&&NHJJKn8q,]Tt!B@|?k_?xiAM8iӦȑ#iҤ رs15jĩSpqq޽{ҥ 8-Z[8w6lܹsOۙ3gڵ+QQQذqFڶmT)B! WM9zNY||</^dѢE0vX4i_tYfPV-vء>_ѣG_|u> ;;+++VZI&M"((8p^z ۷ovO|B!h$=44f͚=xӦMٳS_h7Jevʕ+OW?̙3ggZn&##?9voA} Ǝ{@ !BЌ׫W_Pq`ܸqOv}?o扮Orrrt^[YYok.@3}ᲂB!KvNk״ 4jԈ~;>ʢE044???PT(=Ɔ?^z)6Jbܸq$$$0zh<C #GBJ//̇AR/U!D#]%׶monZ ~~zÍ) kנwoPX ^{MS-5 u[5mh^{z€p2n k{{e ! !J:u'Tw~ 7klڤݮWOaȹE (fad` Kj^]p9$_Ξ=6l;z>50tcYs 2Lp{s{WnϗdI~N_q)q:OB $BAMy5mW/8zT3]=__XJ<qW7hNN_kYOa͍&& J֕p燌~i4iWd=WqyG:eHbcYxb!GaP~jULզ k8 K~|sp8+ع1сt 3#3щiIz썵5o{_¬γ0Uj9x ד3 CWXX+YNU;Yp0w 69:conϡ+X`v @r5xo{{q/+z(x)PƎ[M !0Ua2 <+EQ ʗ~_ԭ52d놿7TV_K Q۪(U}uw-h_^z Q&ܡ(}^a2ʚ5:[Na2ؓ:7 ԩ(V+LF -k e䖑׹JR*ʨ-۞(>111 L6Mߡ(2.B]0T5@:@V\7A蓮fv+|#^;mKcKV^}j髭[[6\X/ҙU:nNs,^̀:tέhUzafdF[y3Y w.@VNw"Pwa$f< c v_N"B4G8:~9 ֭-KCRŶ ][u7 >l;M3JNGY66ddg<6t֜ZC-Z̞-p% +i}xP 014);{,-ԚPlu HHO^Ľ_m-I&4(@{Npl0'bN`eb4rj\ݜ+ ԅAPBɓ:uXvr\&l 㚎.b:Yln]'^燃?3P2&-{XR3@RqX[py66 3?[nl>syV"1#ČDnИje%R@!D1Y3ͲB6P|N&l w21GsGjɲeҗ;1I/ ^B}DWKC77yփ bž 1js 踼#VwcXaؕJ-ӪRЩ!f>=5[[^zYsjvJΓTo]WuK/iQO.'F>R^^x9{sڇ|#6PСr:T(QZҊ^5zQն)? !D~j3GÇjBZƵ ǯs,%+E絣#4`Ƽ4&*"xT0_}g{>#>- VTv9m\`mj'f.ʹUN۵0Ԋ?sμ\eN8I[4( O?5դYEywkLbر̙3B;u U5;"!sddgo' (8991m4&LXdHH!N l\ ;"!s&$6Ğ{[YTȑJt!(++ظ>a`wTBD-ğTK;/ҳӉIy IB<)''X֭YyswTBZ&3;Sab"IB<!4TU3ڷ$B9l@XCIzN:sc?}ݖoA_Tj} =OZjOÇ? TV ///+󭓖ԩSٷo4hЀm2~x/^Æ s:۷ogٲemۖ#G2sL qу~JEnn./f׮]|+VJb˖-lٲD>cF󱷷ؘ 8s ˗/s-gϞe`llLja֭l޼-[rJ}^ڵk}6|WXYYQ\9"""g۶m@\\0fnݞgPu⧡( ϟz]<׬Y1!B._$69bcK#;7POJ%>=wO%ku_ZО6L2jU:T=fqN&N$)Y)d粫Piv3#3̍<5 WWVscRhI.J.]~zΟ?OVXd ӦM++'1b#׏Slu[>oS'JۘyؚGfPA쿼tf,WJ6aOJ%%33 zHϑX< ( رc IQ*SCV+ 4P>ce߾}Jvvvz񊃃V|ݻ(?,<<\100Pjr[ IDATҥBnn9rDYf2m4eذaJf[[[ER)*UR:w7Ne˖);wTN<*999Jbb_ʷ~曊ҪU+% 1lڴIquu- ~孷 Y.\weIk֑YlWvmeONʕ+GvnJy W=ʴi"#PT[7`$$$fΜ˖-G-̷_7rqm?+ӱcGUW4jԈM6Qn]4ix&MXr%W^)_f ~ժUzq .\@ڵu;99ѣG<7ohFsss:iLJJ :$>._Lzz:ʕ WWWUF۶mU5jZZZҨQ#5j-{.f͢{4k֌S啧ɓ'|2[Gtw߅ !黮/?w! BVb$ً];+[=~##(X Uތ>拚Nve-DtQݛ޽{ٿ? .$::@ڶm˙3gpY7n\mޛrmŋ9'qIz*U-/[,+W~7o[fMHH 222Ox,--133#55wj߻5f|||ynn.jsss,,,077>ʗ/=zM0778ccc7|øq駟h߾=o&·Gzj&*gرpRPe$[MdhWd摙vU}WRvK;uXg%ʀ__ f72.M$I%۷}L4ŋ3j(n޼̙37^t 0۷U%.yϽdqEіݹsD/^~AZhxϧ0dȐ< 3]X;v,]veX###BBBXr%fҾ?zǰh^HQl6ԛ}i^=9vNbBj]x?"ߑ;n#xmӏ4ZЈ-SB]⹠V9rv80VywHKK{6qqq 66>XaЌ?Z Rl=zxzzRF *VmJAKpppϏ+WMZZڟmRRGoOT"D)#r Tmݡ_e&|+~xc/2yJbeb꾫PA*ƨ5jImv. $Iϕ{v'%%iO&Wvv6Ÿጁ5j4 ;/[edFC'.'\.v_͉ [Sۧn̞۳B`oHp$EAtQb 0۳i&RRRt0b=JW_P'O`lٲ%O7odƌرC[]kgϞȭ[Xp!.w#ITǎ7sc)=zt 8sGa9L<###RSS~m]gdNDFj쩹y4d<%xcnV) WkWmcM苂%?W^(ۛۓPQ=N )~KIE(  *T@ӦMqwwRJ,ZSٮ?&66={F.]xB |DGGԛ!^=3'swqyҳ1Pٔ&o$\Ŋ M6>|tLMEoב{8BZ|nW͢[~cաrmrs،>5-*zzdeJҳӹt9(((9Ԣkk4f ,E37ȌNU:I$Bgr-F2e >>>L:-[%ժaR9>ΟH# :ϓ|78Oy==MhWgE޷ h:J͎;?T7Ԩ!U30 딧d\RqQj֬g$BgI<ʎW^lڴIa8_|;ww/@7ߧq1=k!ϷJ ^ǘ={6ƍ[It!D\S7Rb֋fQs>5k]EQDȊBָqc-ψoKr}j5>?aQ} fL Mm > :eƆl w+gQ*zԈE|$]Q(*WX $’^ 0$֬Ą]PYٽgE̞Ъ{.'cO`alAjJ5Nd3p<>C NJfĊMJ22a$Nί/.C߆}w.BQҌYv 5Un =C͜Z%?*gRe=014AALt!xB\ .ڎ^s2RR9xݺrӦk \M(DAU~PwPvTV37\SNlP!IB<-[8(N(v|vnāD=?s璕ӧĤ-xn攟=zNBBPucΝæ|yʁUzlQo"'#5D"(JHM 62ŋGtQ(BvU>̌4l_ˑ܊˥]ŕayS!Gt6stqW/{Nfzq|Fn ';!jӻqע:eWs%w{?.ѶR`SyR҉j6'q^#SS~m\jצGq) X\]Ll aC=<ܰ!/[ÆCO˃lAmlgw͎U5"3=kO?9s٪ t֖=uEu9G2;CC5i) 0oZq9$\{Ԥ: xd 6I) t#MJՋvwGF`_э;LL׫R[9RHMJxdiɉ|ڣ!.ūf刓0cb_э;7kV09\Ń4>ѐn)$'cjhq|Ц2&e̱/ gPѦ'Bp!0gL̴ee]\u w] \=u&ݾ?ʊAqz~M^RS8sc׬ڵ9CTHii݋g*GԶV>e+V n^ϔ)޿#G2ЌQY3"p];OTRsgEo9p 'no8QNlFzJ v}| Ps =P];Y̜I֏ߴ'%>͛`WF&&D;gmR%>:3R˫- uڷ'dҒ鉱)Lld$5ZSbx|-&:,_NlkTjXHUrsjEHI36}b/]؎ TՐ?Dh4-{9_xW{΀mf8Ţ[|r'v<|&&:<ޭghZ݋DtnYCnN-{q^YZo4K':B#pmپ{o2/u鋡xu噚YhGճ2HF㿶̜ɪ l9ݿ9a۷@GDpen"#5CXPǏS$ c|{ -f8T3}Q9G$߹i(ceHer:[Š "};ťvm0{&4ʕ)ceEK'y ̬b>ThvPNݫ]}ҥnL|t4}\^;2:U5"62!^|޸1'mcpIWObet=#Gٽ;oϛG___.?;:kwf 4B .R4"vTDW bҥ)@*!$RHIƄTIpbAܹs&gg7zb78w :?Pf\+^xh+W!=8 UGVZ.ԁwՄsrhD޽ImK#GƲdҒسbE6-Dӑə]ژw-]Mk#)wmhѭ?vn[ihv=cy{115~h|!3Y}={Nk<P.ǝ7ORǛumv]:eY1k;N{!S#>4_ }y CpҢ[R_{6'7+=B6.L X* ғ1&?'KxKRafeAA4 =0sPP_p gať ?&;=kf~ IҬwo rs1,ڗ"" aP;Ǿdž *C5K3uZ\FxD.^QQZ|/[Vf'OOFK~wu:,-i.[gg6$)+/\;'zbW(gͻ' I"%ݫ guϔgfx6kFbD˟{OǏرZ ^ϦO?ٙNG9˗oF8aoiϞ$9Crt4iidb_4yt~v6ׯ\aѻ7q[zL|5N4 013ёAO?͆ .8yzbHի|䓘YZѤ Dߏ2k|g[{$ihڳovz:Ϟe…tg6 $yc[Y3?咛Imtgb3!E*Oh69>F0O ΁^_q/^^i zŰьRTx]Zwp!{ߛVZ]Ư[X+c1i޹tZtv_VThHbD""8f \ܳj܁+11јܛK |qJ]YZcҤR_ϴiֽ;z%K^;1"ݢ;}^0=qK[[-[Udž =~lz,-iѿ | |% | 8u+u*7m˂Axr VҎ'*gOذ!ZߧԸ8Ɔ#t6Bm+g"5gfSť ''=G>F[ͬY|.5#b>lИBAH:w =>\u>v˗qeCo(G֭þM{䋘pE<篥.i&ffamo@}Q꜂\H 1";i ,1/t:7ǿ];|۶ſ];<6]IQQ޾EѨU+ ߿Fg㧟2tΜrw"A]|30890]LH?n]zESS_ox=Fm7<?bKz;%ߢr22X2cgv 7/PMjl,Nff2歷ȺzGR}{Bwޡرݳg~sEpno v`{``hѯo{` N%S'|BBUv_۶8s-190Aݺ0(d~3ÇSߟdq ŋx, w?p $ԩ*C?,sw /;gPKe>$VMH8sRw̭xq2wa~m>Wy}.\52 9: 27o΍"y߾j.ZĴŋ|<+xWnE[PPNBe:m!{@,ʥx|.Yװw5Cڣ( N]a\`էoVL{z>aoaam_~Crx[g=0ݫ/.eߺ8iUk?ui:kd^Keէoo߄yg85UAN7Csx]qgt;Lmon?ǎq˟{]a!>-[׮~m׮wl~vZ?ΝKG+3*:e>Kss>buα HN>9ddЩ*7_лՠAƶUq1.VJľ}?svlɫݺk{rM~v6G׭..nٱȼJoe8~554 ]4jښKёLlܔ)?ar`/XqEzLQϛ,~j, ;myuV><݆g·aӯ^ň6JkF6jD op(a|=s&j6mmqk Gd~׮m˘ޢܬCނ^cϊw$'EE-,$1# cQ\MH +Ϟ(w%EQ% ⹳j ;̴+_<liqt-,P”ǯ]NTSYl+C+51NI8wFگEɼϸ (;V~cU5q\REQ2.}tJJzRIܩz1NCٱ;1Rc^h~[y* @R̘(3AAʡիG]]833eNcii $]O̢#K{9LW V7h^\ٺh-Ny5eCʌ ʑ?5ӓEQK_8z򰵵AcyYY'*3==|R`H!sse,wfʂۼΝ 3▹yW9ŭc[’4vYxC l\z7wU5iWИnx,{֬q)ܮ=-% +[;Îqp*8R}vN9Riڱ%}P;<Mݢ-Zml =@Lr H8sϦM*:;$-ʝᇄnJrt4"#bd?_&T:c\,ik8ԬO%&aBuڵ4ٓs1{ez&JEQ8jUw>lإ P?5f9w/,~Q򲲘7zz..wu6~}OpKr2}nU ߻g//\ot@:ť(˟{KiԨY\mxۗ_|a,#Qbxx!kWYîePIPiV|ҧf? -֭[n]ڵ5nj\.>>w<?gyضx16NN\4U$;=M}Vf &/?oܡ7.4157\ Μi3uGa#p)* d]ƞ~`{U go/^֖QN1i 0bYii|1i=[eY]؎շ/W/^؆ {6|!!/Ӗ]15ap0:ą#Gp d'p5v+$f]06POAanYV.CшK1 q IzËQk4Hѣs22 %>4P6~ aaZ<4QD;fGO//3逸ʝEQɓwp[A4&MpJ|< I#lFm]Vi3#FHg2gXtY031٭+L)Z#0 !K/a̧QQUv?oߞggXjll kZI~]l5Co[יW0/ 8{yjw7S[r-1^zz1h'0%qmԈۦ ^Ϛ n+cjn(\NF$)*nd1ǏӨU+FbuJ֡| ~{U#"J{Q8:r-1=Ī7$i.^{Ewx*3mHߡQ[s5BTF1!U'y&>IHw,ӤC΀ Ӽs wݺ1ǙWs ˯_ð0F κ䐛weZ0g΅GIm څ+^^7g//Zm~5Y0ܪӪE{[լVvvoիw2,|ZJ5&&L(3wH\\X֫W5=0[TxNj\\ <zVy~ҹs4ZS <=;oƟ>m\`۬woƽNܼLKF^X_.4Ёӧ 1[2RR5慐2SSu|5ԾϝccQ'bN֦ewJ£IR;Ct5YK2飏o׎KLjIHBy* ;77*.ˣ0/|twwuH ; f]ƺRdMuM[''4&&8{yanm_:Vqzv&$a]D޽\S y9&-\ի mx='OcUGU>k5FCˁLMBm/?4n̥HE!IBZǤI+,,h!Chڳˋ'NNY}WFT0tV?E/kA^.>}8q#>5<6%(ظ=DžG?}xN޽vBIsueQV5‚CrlJ;JHo3dۗ,m[ J}HN'OVzaT݃ڵ4m44%FDT h޷/m+0`,,1 ^#.4={355lap)2K̙K>}jlt!D~]Vz3KK zz_g//q 'OO<=qhР"\fӬwo,#;=w)7ѣg59s1l\v뀡EӦU\)-tO01ẟ_Ӳ% '|<؁^̌ RitC8m?ғn:5yFM7.l;thб#,mx IDATyy;OYRf.EFu"u.W7s&ğ>#&X6x:Ը-h҄e?̙CiTE*py,/v6}xuvTj5A]Vq87͕[=u*SSvX& Bqltt/k?bVaй3skk&}zvI:HI1֏GGSGۡCkx,=koߞ{hbnj!9']!D)<0gNMCԐAA:VGU_`.$EEѨukqskknՋonj;dy].^Rf* B!ʸgD5F/ȻE ,mm ܹQո*Ryy&f-]J`.5=QKHB! jY˖وС䤧tkĄ'0D-"!]!#F9ܭݺhuR"B!D-#!]!BZFBB!t!B!j B!B2҅B!e$ !BQHHB!.B!D-#!]!BZFBB!t!B!j B!B2҅B!e$ !BQHHB!.B!D-#!]!BZFBB!t!B!j B!B2҅B!e$ !BQHHB!.B!D-#!]!BZFBB!t!B!j B!B2҅B!e$ !BQHHB!.B!D-#!]!BZFBB!t!B!j B!B2҅B!e$ !BQHHB!.B!D-#!]!BZƤ BE1BBUC6JJUWF[3[^NBB EQaԕLWg(umyqh." B!yJVUEE)=^rZFQW5=A'JѝԌ"A]A҅BӊRz3JQ򫍓j S J-Q>P0sJU*O.➦RPⷂWE]^W`0gfnBAaAMV0n¥IRVPjj5 (%f݅$ !g)YTCi(zt::zqƵ~[ZZMvnnMVPTpT*4j5FFQРBv! B!YEףj)(jzҋRzm 閶6X]CTh]`1D3Ee.jt~ut!,E)Qf uZ )(,$ uEuzhZ2jz(FqZDSS,̊>d¸(ԅ$ !'gPwףit_@n~fuzz]Ժ0gmWNGFL4*5f&k07a/jtz=NBBB{V~zrB 7uVָtK[ErbJFQc1@AӡkM5&5zL:|prQHHBQF˜s_~UMzb1] +'B]!Z?o`ni(p@2quUj4 f&;#m6%DBB2.[G;`5=bMXn\@QRՒ_X@Eնnfn*LL(z5P5TC-(KPut!ed;W͌EA_(t:"sUa[{CbgZ-_ (0W{kQPsP;C[co q$ !(#'>3GGcbjz(wD]E)eUYQ)xٲo+9 ҅Bs׮dPjKΰ[jTZ]bR[Q #+W9 8wvυbuqB.r)̬aܒ0ֹO5=jQZpܙW?GBFlo׎S=Wsuyyli҄9skiSzT[[௎fWg;716mP{1D!%HHBΝ#q,ASgRo'#,]2#"tw';:_ycl7FGEͱtw7IEaF]!+$ !ugb3W3BC !3<Ko`뇢K}~m+W4<+=?'>+//<=Lm[Y3Tun_!& B) ۷SV#//.zsgg>K^Xp2o>]_X_ӪUh,, wBNB.=z\;%4}'o݊=q֍+Uz~NBXzxsCz-(Z-fԓw!$ ߤRa@jE;~m]2/<=V/mVu3BCk v8lmޜjܳg{1ss]5iBv\\Fcb(LOǮY3܇ aDN]^oaӧk ~V}'zξ& rSM?qc3fp1wu}`afRM^ԩ9wR&_=xt̳g״)`I.Y.ʁhphmοĉ2}t/_NXXK.eذa<÷tͼ2?%% YL+t!Zh@Bfgt\=t֢86cIYnI^cǒZ}}1wuё{tT|L1XX216 둑>GNJKgJ2֋|->#STeˈ^^xN|<Rfo%=;~^1E'evf*uܩsg׬؇dGVd_p[1i}{vt\:7mAPv0aiii9rdK\\L2單9o<_QIM@qw(z=&&Dˆ0R VPTxKߗ \SRb.E{{`ٴ)mPFֹs4n\r/^Dm-[Cefes'W.uF䔳.' F+=<{<ᄽ f̠ 8PSRMJwڴrOh,4 u.|&O6/)Օrfғm^&X6lXxzNafd9m``njׯˋW^!3<<É'sZ@R7#zdd\޼yn5!--3g0vXڶm[걀_gffzjv튙7n$>>N:1dȐR=u'N`r?;d͚5r}S7mıc(,,C <μa!Dt!1۷sx$ֻ{wB_x&3"4 ]5\!?5dC@VW:(z=^Dɓ裣1̙֭˔R ަ { F [v +7p X1-Nǧ-=6+ի c-yE׷Tj5=>R@[" μ:g͢5YϓrCYOvL &kW%Czy5/v>?  Ej!8g3éפKww7IݽxC۶tw:\).ݺqJ7J;~?_޼ϱcqԩԟ ]n.OءMbaaZٳ(U]HNNfʔ)|tЁm۶1tPFN3v*g֭.Eٳ'o姟~g23/D'!]{D~J nj1Xyzez3Yz~6㫯 ?,!`loGՖ)GPi4hPi48kGwޡɓˣ˺utFɹwNz`%+9$m`XY>$Kkrd }'N+^`ZFȎ)sڡC4d?e˸~=* 72. Xu:|y4?_.Օ zμj9qqYk+CmfVyna[KOr _( 7o[oqq өd;a>8CVoyWg'''&NHj9O> =zS5m47oŋz;wk׮mۖ '''Ο?ϪUOЦ KV{pp.[GaCq4v˖Gk#طlH<s%>xY;w^_9BC qaAst@իKKMN+A =:p"}{D2ϜxOXaI1s1ص%;& f-=<(,kjnb"y/Цc߬Y\=pj/~4w.-[!.,ZDwaS.Y9nIIŋEդ( WT8'0%u.Q_*\L[R#DՃ1!yv2° @mfm`qzT6E)m7 >y-+}LRѢE fϞMDD 켡ÑUQgAúիW bccɿakE$\]]/O>̜9 !j/IOMzd$.ݻW9^Ofx8xQh>#Zcn}z5A/Dvll3fhȽx0{5m5]`O`ҒG=JE^3~i׬C8ouF\) ƾE XmRcuth,-CS ']\(v ECѐu+5E#'!co_.49qqXWR9f ?Tzc_Jsa'L Ɏ%#,ضߟE/]4vQ؅v~~d>}HH[WT*Ζ-[tRKJp>>>UrjՊ/0h ֯_ M>H>#Fܡw"-d&]:.unCB0sTj5 +F7A._ f,6,fm =U*\zĴ^[Cp0򒓱 ۛY·^ѫ춙3*lzN|$MFXBm@?!=* m ҳ@/`9 c x wk~GrZ!oݺT5۵kΝ;9s 3nzԲeKyjeWX^BABu\Kr G dSQiH(͛q۷s,ZD]*\Yﻏٳi4e Vކ.RaHWabkkZVc\jhy.7XZbbcC~j*iGp(($$`KaZ^+oc\;r:1, vpЁkJWfm76p +*TwЁ;v0mTYUЩS'ڵkܹs믙;w.۷g޽ >7qqqW^,_%KгgOlmmy嗫!!!ڵXz͕+W`Ŝ8q6mg IDATf>sFu˻ !j BqRh,-kޜ~ոhy,=<[_=xWqիs]]N" B>k__,=$İVŅDmkrbcKͤk?p ?Dѣ]_Gwƌ*5kFŋUw/fȐ!5=EǎILL$$$Dff&w_y,\///^u.q ! BqLJ&`*KA5kĵ.w7lcoՊ3gfgs=<-!]\o&VVXzx`Cv\\ ϓGsyyO>$|˚5k%"!D@0̑*#NBĉ4WU,8t.FI$UNY}}kwW{{SDcqQJgk1cv8ȅv9g;/d|gn0{lnJBBvuU QE Hgw}2.Ng3X_MZ_gB" nT}P3{{};B+,>3}Ϗ[rr]w /`v{2". s.L:ggs{]k+2TTBMP XxT7W;I񡭩͞vZ/_UW]u}YǏ~"""K!aΥ.KڛoˋVvHP:8e5EJܯn0I4xϻ/8%K7@7J 0R$_<8 }aĉ(_r9< QQQ,X[ "] Lz]f&'uioѭܡ.#c@J$REl?FCmzP">{2e >c#!aH,hHSqXi"H{.S9a LP믴R"}\۷st_Qլ_tRQW] !aHȐXEE}>7Oth @#S<ԡz>[nAR 8]w"A. sFZ&=?rhrXdVݔ)XxzRڧ>rs=2곳KKiknnd7P{4NgS5ԡL¶m|Aspp૯>ࡇ⪫dl`8!D@0i':u t:;M@}QfhǍ<:Z\[P\ߟGkC9Mjt--h p>:ɒ"::Z~;,[1c/ѣ4bFlf$ sFZumad> mh>'O}b၅7O[S^7HCA*Th/bv-*:RoOu Hd2""HzARTTs=GKK賩c:?/,X`Hvpp_*sa̙\.GPP(T*?@~~>ٳ,Yl? sN >!NNN<裼,Xwww-ZW\̙3Q*C@p"] H t<<7xc? //JS8Ξݾഺ~!d\]1wuƲ2}곭W_7m:땊dhhtsb%Gtr&%" R<4C B>///_|pV\O?;vy]z_|#**;vfJKK={6-bbz }=6h*+ki$O2s,,>""NJB[XR3[>[^*O_-[DU|<6ᘻR8hֆKJp9S_c` RÃwyD5kdݻw3n8/x}Ғ%KMtt4sa֭T*ٗ-[=O?ͻ; 0s3]kGr4V +VPsGS_Z73DEEPIWűg4*#믱4ÇEEjr\Mj*@ԾC"u?7!S?"!7WKooo>S{.Ș;w2?z ;w說"owʹiӰ"''_EM~At`#mckk@z@&9Y_V2PX[#37g{(1M` Yh*+s3ڴ40殮}q#^V1Q4AA؄Qu|gѐ;:)z6- M@9mi*/?}l@}^퓤sͤLzشi{PK)bbb6mڠ3HR7ncժU<ì[7_u D@0̑tt)[jk)ؾAʘ_1ci@ \rh99JG.\MssQ STJűc\]\A.\l^~}m+OqggLJsQz`wÃݻLݖa<#|ns͛76h477HXXؠ!!a3rKKZٙS+TF<@?Pwg~G;{'^kdug UEQ1/"Xg;9U'&bܹGE]_î9#=cH]v6ޘSwQܯΨ>|_h&iW1@[XHoq#۷w>7WI7,~%g~98;;3}Ag٬Z~HJJ!y]ge@<_UHH5ٴHtx 8z3;;n}݈plm۳R~FY\UEÙ3ڨNNj""8qSu$td{J,3gtma!I/@kCCpZ*4&ĺw,v"ocӑIW9;\YIAv26M@xw1공2;2隀jRRcb8[î~zxx gCnvlB}}ICt`s G=/gW_TR?#VVeNH0ه;vX;̏>2)=p( S:>,t--X&(6hg͚l'M&%Hܚ>' kj(goMڵ ԦbMx8 Fs90ʕhoNsbBcq1M4c鉙mQS Oh.Z d}n.rKKlmM]fM}8ΜIMJ M>K &A?66VX]!aιz X_|J$<4;Œ>kPg0T!?*23q:Μ1ЖմHw2*̏> ,Vcbh*/7Abfo{x=I/5k{kvq.}YwX^=\T@dT&5s[ZRl:NcK mB"& e IIA3z4Fkm.#h&!=YF3?Hur26lrKKTԤ=H$DzMJ VAAXtv6 Sc=\\0cWXS~HRe|駃ҷō0LruH׿?O?! hmhgٳ6m׳ښ^^4~N&5UM6Nw~~X">4-ٓn7e Wee۔;+LښTVu7;Vڎ,{ɾ}=@H8q?:;eҵz{ݔ)FNΥ(*󱋈*8X?AKK OOOM_|1P[[˘1c_@0p. sbwܰsz,d~l,_/>(e3HHRBBv-sbYc#ֱ0/VlkjA&}q //rsi,-.3 2- 7;v`;iS}; gmy/ٛ7cˢEr }URۉ2 V!!C5vhV^͗_~9}B~!D@0R?3o'g*g̙44žTDjf ˙~=YO?ݔ)FU`^פ U*gw-ND& @/ښilgBRQqLKKQh4=.>5nT2?9sOL{ܗ,:磰5遁 &ii(0~4x\=. Rs'oֆu3,Y`mnNUMKC3z4r OOJ)Z-99cRFcI uMokFe\G` &9 3t\;/0Zjk]UPQ޸\q)/LPGmu:uvcYp!G{CTv.~,S"=.^WL͘1x,_Wb;iR>1 ƍT'$`6)e،\Q\mq))9ǿ:O=ߴHfn՘zEڸK3>Z[?55h II#m?u*3va4ss}!̍f?8kVL9&5USg9C3O#S*8$J*8؄QEsEݐΝ^ǎS9;--ZZo>d*NsPShd$ohǣ ,jO`MeeT%$`e //*ccij EacCIG9ƢRviB`fgEOZ_1{^=!!+I ܼ=˝~Up0!~we<0c |AZZZw?免E'!L R IDATn;zAi=F! (޳!f$L̶)>>ّy3Zx.X@OӪ3j__B׭jX'}U9;xq㚲ٙZ;r%Uqq>~{ ‚'N0 !D*֓n$/6 J'ňBj t*8u{ɰy2+hS?CO\\\sE% !@. //%'뷑 &`Emj #d[wb'o]Sv볳ᆡbƌ[IMM%/~ !Et`p]}-0mVZ*R1-.~NNM}vvUGxx8UUUdggmP?RWWǨQ8:@0Ј@"uI~ZZ.tHBS )?rmq\θq8~9'*j#H%@޷^vD oPs1a;vן>}K \xHF'};Z3?8ԡ\8͝ĉ;mQр'?qn . HF. Dz_i.3 u8M3dѵ3,挀bJŘ1cٗ.<At``Hr!@}v6 ++Ǝ")HNJ*H$R0 } ΍sЀW "}(i_T sm“+ܫG.3IIN~.ql>ƹ;/*AJ  4B0gZܖJN7ԡ.%L`aw+ڢ"TΨikjlC:/22pJH<ôiGFRҧ8L  !F4Jd6ނOtawtFmaP+ii4u{.#Eh*+\kC,`۶>ՙIL!%,,|uB& Yڶ57Sw/U;{6qq6(}wߥ`۶.C_8[sՔKMJѓ. z F Pw1XTCOORXYc7 o,.QvR{7EQQ$>|biC[P]Dv4P (ѣG;.  !FXWm{3hԸZZJ~^?W22eX͟OSOq8?#U*v3g(ٷs9s8x<.R Aw+{h Q:;`fo?vD|m:uTjҌfdrqAfn& ˤ(* UIJ矍;h6:T؄!37GVb'fĹX^FFB?"=slhjAlp9f 99?~,LH`Ie%|Ano1᭷0ҞڸɓIMdw^K+`w}mtoق;i}o$F=vhϤ|8~o~;)޳M@&Vǣy< v0:WQh=kb\ҥ2V /O~prQ_zxnfԷ {uVV_(;rd'ޗ"&LWFN'20C>4]k )ܱIדdn܈ϟlԦQʏo?ÇQ991{P5QOT?NKu5Q^ O^NJ"겲3o Nܰdl^[Q0}:eGb6 ]gWߣtt -,$㏩vdo 2E_޷"W 'ϩNJ&4FPIM3 ֡z+2gQԤ0~;Y60mQ]h*+e"rc>ZNNkJr>Vq,ʏ(* GFb҃:uQw%w&<cj f J^|697s;kf&ۧ?-^L޽mbMv3{{0'd)vSス8~exҰ8kcǰ +(ؾ߯?W{"= ÇsrINqlmT*7@?b9jGV!/3;;s3{{.۲Ÿ8* M@=>93i$3gj@0D۪#׶ٟ|Tcro.ڵS=n 'i,)!嗉<:}uH}5ZjjеBE/B ?;CC)%%쎈#\Y}S};)S(;|coϵDMHOR[KrhrN>([oK:c_~IӜ~oɨ#+WROHd2sr{!&MǸ27nr(Mk $>,, hji̶m?,!'hVΥ1dj5\i{}ɾ}^"sT'&r)Z{̃MOd_~!Ǚy3S>_~1ٶ>'4WWTQѯxM7 }j-,DG_QQudצaUOwۦqlqjJ5$% R̞H<3[[<'DFub"ysWW&_ϴ~`;M 4 '{:~.%&MD~~>E},7ff}[3%!mH2X37n}R n\-Y6ui\]'Weeqw1a }1OK)\SPe[0|9_~Ӧa=n|`4N{aԩ>GSB\[XH]f&]Yc?u*ﭭ=e /Dѣ&OȬ*>dʸ8lqj\T 4s>E AacC}a͟O]VG︃}(ٻRi44ƒ?./!!wd{ʸ8}>P&0+nTvt r>\:1͘1؄RL[c#ٟ~iD|\&,҃@0}:?;4]D11ݞ+;tcwEZoKZZ2?F3`^ h41&99AJ  $B :0,;td|nRDw+LMXXj kkښ(&kCC{Rz%%x^ +KP/vf6O׿:Hzk XZJޖ-M&-\ҥXt{۵2C"PMϲg4Wԙݡ.3S9gH׿5))X-~~T8m_EQQ˫,kֆ {g$/8qt݋ˢEegw{Mytt{]][J'zd Gw]t eG`˂}ʤjhPܜnS2'}[i^N7r++)رke>LuRnMaAmjjW_KC^;Ǐ紁M#Pw]8q0y>9"@0>4PI{Y7bfk;5$'cIC~>EOhVgji$|v'__DF{TkN4oqٳw,f0jc312# x|3[SXZJKm- OG"\l_vY$"<<ط`I?O{TV6`=n\Ng͢d>jϲXmcjtL‘UáeH}52֯GhP{j$R)ٟ}@֦MHrβ󘢹 %о%K{^y%S6oZ+`ƎHJZikjۉvy~ۣIxH}5}F[Z 26X9yFVK8ϝ zb ~3;;"D!aX8:4Pz΢w*f ,Z,Mt^ɓidzqRzљ=o7 Fq޾Hm*+VLް(v>kk$r95ݔvc/ϢQxu]ESedd"`H֦M5'-WO3knΚ5V"sz2>ϕ+ߓ)ʣI}ul3~;WPܹڵ} ҪBǎ|ma!L_R_Q8͝>2XixצkiAacCSY0w&Z=AT..;}|lMڨNH`g15(Wϧjg@XSv0Vc-ɓ^'ShCZZyeː[ZbFMjJH27.5F[{m +VBM71vv'cfg%VICq&O=n;'NVq>FEL ao9s{fϦ!~2L$R%""~OmoE"=hfzIG+4wiw4UVTVM7:k)lYΝQQHJv$v2pήǶ&ZZLZBMhh{YJd.#'p emڄLdF ~Z>g}d"&ѢۅY2ڴ4xΜcQJ}NVt5GbU` Dtm--:~:6@?][^מ>Md>c,[* jhˉI\ qVoۛ#GL>c@/rByyڂ#^?nnxXAel, $ƌAښ* ug_}gmj*Nst)ǎqeGm},ffmBϮd>}e%CV}ɒ.Ǟ7@/{ޓ& Rə ,QRy6&htQ[p?_ѣQzKPjjjdr E&] f>$ֽѓq#6w[)o^x?4-ӧGO_gMa;i(llp_w%oOo^=Ӧ'2oK֦M¨32.O?dO?ke~A!Va_q^RIꫯT?N?566m꫱&slLѮ]tco-^Lo/YBGsu{6cIڵNIs2 M` Jݑ4WVbfoj$r9i^ښ'ɐ*蚛q9I:&AvxlƏa }]k+?lԧe1+* 22s?x3?ngo۩NT:UpI;ڂ}h(^u6srh#ޤݥS#Q(h2n*/ĉ.M@z*>~wމ@XP'f66 PRg tq㸮x! vEE 8s&%^Mel,]m[+I{ Q܃?~<111=Dܰp FI)6tv.3{ 5b}d$6YC{0G\˂{%zlL-p{8~=HRa7e ӷms؄3ef.Yĉ$>,%%cfvv<,)V)ڵ{O&ƽ+VXZ&;L iӌqlJl'N0{matKJpZ>H֭tfLѹxt g#.DLTeM-8*>Z/+zy{.A?Nwi3Ns2q겲P7n1DÊl JGGf͕ame"#Tw/MeezK ^MC[X*7.Ϫuxxs_܌ڠlfrO;?ܾ_ PK/҇!k 'omq/ȸ6Hᇄ yyL"cٲniGaee<*~9NvK 2 ZZ,}}ӟp[>k77Qߝwb^@t[ K/Xx8 ['̸UHUUO]f&S͛ʎmb}"=y:ԾYYڐd IDATC?sDSYY^uS+$2>IFf@SαcQ1Ϩ:uwiv%T;q>CM=t&kpR^~j32z<HF&6vWhT >HG"mk"}Q'66Xؘ͕?4JZHy곳+&Ӌtho}IJTUp0Gv wZ^Ծc%%4th\{jawX4efxp;ĕĉ4XR N)ԾLtivD)ṃ4w}"3LXf"1 .^D cqV)j-/$O>Vh}RZu}/m>d Gn |sIev1cdֆbn`"%9p;'މϏ~BYHΗ=GksMց?.OԄ ]:={hSy9Ev=Z?2̤W'&bBsMA65 CVGX T;FC~ c"Wq[DVVVrI!B0#QC|K$Xx{Wvcvb;q"~[1PiwwL%v^vmMM,..FoәIMOorfj1Iٺjff@[IMca…[<ӱV$p4ÃKOpv?x\=|bDSכ0spЋ(\-2')4AAeѲ<}P I7yydb$1|=JY7E >L@Wj5ޫWȚ@LcI uZ\mX+ԩ.1Aݥ~;!6aaz@;v,xرcLee@pq"D#D@pqrvFFyttD#ڢ"}QU7Hf},2ss">RA/,\ݻw3`^@0|"}qR(nl0A2ss Z6=&jODٳi,*i޼q1D,X_~68rssb̙C@ 8WH`rJ'] Hk,GijRR&NO#a%g 7ŬYP*|7cw&22 @ XH`2JD&] ?S=󣹲_҉Zȇ \Y*8kw\Σ>K/?3oALN]KV[  ^uᅬE8v;>0 [*!;2dR)R'n۷gw1a]47("D%`)R$,/ły\Ν3\RymCH$ LL*C.G& k!RxǸ;Yf O=.gmu)Ӊ`x!632J7W u(<~T..xxPE[DZ @[vH$б3s.JKe(r Z[[Af -K9<TTTpu8y&4$ETPBK;V frKFc^t~ƪ lGHs]Psѣڳ?RLB!GfFkdH_ K7HB H_nEv}3+"@\bIW$T .*TD> >,JiE*vX\ 2)M !&@ +.Ih{t:x8Zx2k\-YfuNY6*L=/g\[ơ碦Sf2Mvxr9mXBSe#MC`0hm>.;<2m3}&36=!;*u-Z>˺]u]" {BKd2|I&3)As_%9?#4\ԃ(GfR "Tu}C~pQFD!Siiһ L/GIy1E8hifB`8#WQdW@ڇSjDkpަV)>;dCH}yM+*-|ṗSNu<#,\d6G!Wiq!SBǨ }22>χ4 ou\&f15wn ɻ{K 6YL?=v._^'J6}Q>*Փt?peF$k[YZB8{s{(yP32A(6$(K]>huVvF:tǨ#㐫̞#A r*3?erA&P~?MP!J^\'NJ}k ~~_L@7~~(Y.)SasHzF;lh.y ]e䂀Bgd \b`Xd6o V[vS%v?AWԇ>رÇa]vD}.'yTHdmuPx? 6^HHI};,ډبE 7RGn.tև_ Z;0dWKdAeNԌra3#:`s\B'Q`nIzN@# %X<%YA&҇> vu]w[c%q#g*p,(#r\ѧK&‘4?L'f`?dw0`}>>rY_Ž@MtxqgwMéeQ#\v?#oػ݊$K?dfA] <6$snA۶VR&T#3{FE&lm3&Z{}81رcÆ ;m>XGV{AւBwjH^h+$ #%fE+Z<&'Rur^6| Eg9XTR)߃\1}PXjiNЎ;~q$^,j`I]1M1bPG>.5sr|@V٭hyw jJm>އS 477ӯ_~ngs ?׃iG&oIɭ R3x_O8zH?*R#4TPC2>gP<`ٜQҥ݇iGdx-aqYD^r /;}~^(l}Ұ.IW-,,3IG-?^Կ[ۇ_!EvDvS 2B$\ }j4n>HW:.v@?Ma/ʸŰJ8;5ܫ>iGMbF-{6},IfQq _E |2!Ӻ&3,b9AQjY>7n}&h_.aS3V.җ}F}jZ,b8Nꘜzl#o ˒:YWtra-/lBZb,>Յ"8͛]>ǜM5 1$:;l$mkck!vdZ=10S/laL5Eqz6g'GqN/&uIé ݎNwnbߣ}wxati F}jgBg2|4q4TqHU ΟdPh=#:yh'*~bCpYN^':-R]kqN0N\EՆ\Xm!M5hR1 'L$c"Wȝ{'ѹ97~ۍ9&G$fۇύ{lڣ}ZK&߼y;eYH? oV~r d R xrkIB7^GKy!?; àcamc7Tv{<ˎzlue֔p`֔ݦp3k~6K:Gc5ژ$Z$ÊBB'~t}rށ76̾z~:3y)#!J%+V)C}^VWFpE^Okd4)ADqѰgZHʖVj au' [ >MtB r6^4bj~Glx4 >Nq9_wiq tV[ORG4n|0[468fO[` 5[6 -{=l|A9{Cd1 .nkA#>~򱨲?lft{E)R~Uxx];޼ú^P0_ ܭfvx|7o,M4nErvHF~M߆]6!S(њRB^P+0>XB1d1?w5[+)Y.zq9l$ٰemDerhQemRgjNKY.*~ @]*Z߻I<6s"u;<8X+c +J|㾐LK3I=R TgS{V^W6& ![!JnEV)G [\\ƥ2t;uFӃ٥8~6c)K \OXlz@8\&c+{x6&j^36E?o=!ipZI6t,aU_R2" W^WeǏRx Cx|^b}gkqX|E[_5zwIdgϣOA~?QÉHV[/~//z1Rc~1-(4Ҥs@Bݾ݊\ng&fjiwml2LfZkVcHeƒ 0^b@!c JC4;^cP}'OK dmU\ܔ0d^V~JȳiÐ>RKn_7γ0nmr-ז5O6㱵`.)|0{}8sb@M5hbӯj,Z}PoT$@3ӂތSBcOLAѠ%/އSQQQ@ss4}> u g[Qaŋ^|gʂODv64u%ڍ..6؉#m1_exv~B"4Ph*Bw^݊dΦZLCOnXa +IwΰXgKʈ(.? wgǫwKd La鼶f{Btk)ZL'bPr!u;d{YyTz*yRTe_֝^BV3{>EeC[OI4*}4*C Pbzh޿ıWGǁ_'++QnoCÞu;v%]ja2NsZS iSEpqGWWIip6y:TB.>!۹M )۟BލZIz 䜠Z^YތsbyA<ŷՖJ2CZH8$=v$'aۿ 7߆fML8RO)Pq0^C֔`t$bsE q[(𹝇A5g2o1f |Um\%ooέT\ZhEI= pcGc5.K__AGgᓬsb/ sߏƊDA҈k~W}J.1S@:jD'H51IjE8ZɽNq]l9܎^v;?er.O*dyg+y\ ڸTdG>Z #,{uT<ؤ{]c.Y^iT a ]*]o|cKl]~L \{2Gii$O|:j$2즿c+i&i]Ky!Q9#>14i.EPsXGdUrj:mn_Aw4e3߼ޅOHPsdw롵)s?yEP&t$_Ѱk-ŋ_m_//\R@K^I*CPbAHEݪlEeA@i {6 `+*~tFAG+;߼evTjI?[ B,6}֐ g NIbj "MhM)h҈ڌ`>Li$XqOq|+~?ϋ&=0!F: yZ- $OÅZȜqMiLCV3\ADBq{l-292 9D")Ke=:b iy4JɤLؕ3`$PFC! 75HYa ݅PȲ!߷iG'R 9kEL.#M(:/wŚeE\nF͂Y:G/Xt̊KWE IDAT]&)(淋ϭBVhEm4KC4n*&2kh0Iosp,$9a7> mv ҉өܰ86D}Z!?;MLcyWfVM5l~z EeuU3+OZY3PG'P;u#v@POs;iڿ _S jK{g(m송tQVE]-D..+MD R1*|$8k'eY(8 V?83lMIܭfFe n&?DVlcl!(ZVeilo2 &)9&"9gs1fCsn*NqæIsCs}'YCd:ShvpUFC.~AgժUٳ+<̘1#hټyӟtBp8ضm>*+VlN}~L0^} ?yվGIWc>)Ӑ)A$mm{iEf ;ɘwl)ww> 1מcd4a$bR^HȳiGm474n :\GOqS~1K%q, cg2*:#WgӿeeDV&s$,i8?|,di} 66߼I[tئiH/؄3ZAбft2&[ͿH:J&rW9q,hz;ܿߋ,@0lʈH)Bu9pZDi}3Q #< Txh9O]4n'N"3EhlD4;k?(bp^ZXE>S i.H+p4\>J/z+i*–nbK[s5kɁWYl).K*C 2>_ķ hT(izZuvȬa29E[{[VQhP]" .EFMCOE-$O<K ]9$SlŘ>J$Ζ{` vQGM4a6`z]\2M&}ASOuT< nk3_q*.,pIHe-Pb693BjnyJe={?8:m۶RN nfjkkdjv;zoz \Nuu56le7m[T瑐ݏzm- 1bt #Sr!#DP(D$fY~&:Ux]*|Nd@$u6z bs<౷ @9 OR 'vDf 4d2Ns=- :Vul|L:i4'&eʅRRإ߸RJy7SC4j|Gd z8$o^ʠ'Ȝq ~s.,{h)ۋ!m.KK8C4.ks+IrQ+;g lC.GL\I ZqXA%d"m:uU`QjG6Su9]BFЄڌ!-)g%LVDt/j}!/[> ɤβNC@W/qFMLyA C(]1/ԩ?CZ 2ϽT)I =#p._|1{a׮]曌=^xG}8aɒ%:lMT<2E(Sbp4Vӿfv+?=r>Utymu;dJiNM}!)/dC 1f "2k(?Jkj|$Dť1WKDK[tckML";^#HׂHun_\&?|7Nܹw Wa&v7ıV'+}}fVBk{Z-(QLz̾˱;yܴaOɕ" b)AIO0/t߾M^6#k-~2AҲ,{{C%$)L}j FhdϾI4d nk4)qf{.ci= :F~eDVn)ڶ: 郮|Xngz*0lbO qVv~o$ٍRgD%&!0nk3_N(* ͵N6 F 0tBo1}PlMhJ$Ը׸3^rg"##0` w} 駟?W]˗/g͚5X,]`].vbѢEZPK;Gaa!/f8>ύJܸq#}]6ok[<шNYnJJJhmӌ믿f}Nf3+V`ƍn|>%%%AaObU{]9:~Y٬#s1=5L8mh޿oߑ\h[Cȑ..M!Xj-4x2 ]&ѫ_qr.XdBBu i,xA6.!n4tIV3ϟ1Sx*},$-߈d )nJJ碥;wݑ]rF`-/rh7A!U͵K9KU&@fgw~ ɧRk,~p&՛EmDr.KƶaME[CD$ek.gGy(:Rr: N0 iwBQh ۮ q9yEڌf ~+/4l^miii[s; .ǝ9G!`3!_"z=I_`'Y U% N/&ձQeffSm6?>}ٜvi$&&/o!''!C0w\MF||dL2L;Jbo$n{V@ Wqٞ~Ѱ{mX7Msv|w֤w..Ur\%"!m\*6I97b{G}1T[&|kZQ IKC`7=-e{:AQr]R^,xe4(tR~Mbq4TojF@ib_WvK%qE(Kf.^.8zW?G= O^HK4Q .CRNH" 4H}F c~:rM;I?5j,]\Dl!hc)?=a=oc<9DTKȠb8jp6y:i;h"՛ҸoeaP}%?C!m4 iycO݇q4Tl4l%A-ٳn߁赻L9JoC< Oʤ < ML"gLVAku   Tv(:c[ R+ˡ]$1cp \E9߂L.txڲ ڸTz<*M@tՖ6DzE!4dJP =BS^O74rA [J<̠]v_~%O<d˖-\pvmAM61{lRSSٺu+.x F1}z3gEEE,]AAA̜9kwߥN"wq˖-wjR^^NYY M{ 6l֭[={6/2+Vq3ap8{\{!+((@2ynꫯ淿-|WdeeqMt:/\.6lUW]C=IJeB/RWWDze(,,믿w d2vZv^[oŒ%҅VMFMM :uuu,Y,Ν#ᐘأHY uD1 BkՁ=XQi"pB)22`)_>u:@1d@o-z,aS݁&&Iu%dO̦xaػI~a::8AekQyZ.|5*7/%qhR) MK>"HME[ijسPédϾ ΀BeVWƎiw>W[\Ǡ$$b>/_Iƶrdq dϺA".k1j%c'\]|:i.%3>gsU4#-j4&AslmmaYJvIRR yZՕq`Tv+j"HfGH̢p >[*\w8iGOd g&\:ͽW<L';kq4T2e6) 8IfSs\-a1هãדj)QߊZ.I߻w/*/2W_}5wiii}weXt)r <5{ }݇``Ō9BA||CRSS̞=+x###A~ #;;ٳgs뭷ƍn~:\s )))( ƍNj/h ('O9GTzL&}&NȰaxgBs=GEE>,7p&3gkK/ԣk & ͆}J8*4Iw(ut!A&;PGjieir">?wYR'l9 6mfs}xC@Ȅ$ɒ ml2L^TLcB&.Kyc'PpIĘ9~#RՆ|Ff#o{Qz3{QFDuˍ8DjAm-}X"\.w1 9-u ԚR$CjlTBNͫxnG)* &X{}9#ny Epx{>~ MpW@T(J+EAЀ2;(x!sGG 1A$=yV):t|O_ L"H41IyubҒJz{]|:|y "{M4!ȶ|T Q&m\*羽Gle)qF.q2VCݒw[š~n{ k:-Դ~OD$fGfUGIwGC%]}~ɉei47kya=J8"M4.5ƌ$>uw뎵H1b-T+}paxԩaL ؽ'~z6oތT*Yt)cǶ׮] Yb~χgqF.bccÞ`vыE8p [;/Xc^/gZͤI#.䒠 Bllw$%{˞{5C&8N?tq()--ߏN 2g4^g̘1! w~BYY{1ڵk`0|jޏ2x Q _io2}J.`w dr4Q n_)6Қuu4YGKy!*R&_\OS oCd<~u?oigbdyo%:w!HzcDD:Ci ~?#XEg {ܔ,{]_Ny7;Z"lA+PQFD~pP;h|F6wp1hwN76H;Ql|J$`ZSI7{LN{1ۜr8d~ƌnHSp&UDdUc~'/H4m\*o?$y"郮|]\Y3V%9n,%v @m%;s h. *c,ڸT:#'0QcQ w5!-ؙ&U1^-2n|ĤG.{qS1i:_H;eD2g\sB)TDevx6)QJ"[JhM) {@i¯BfmXڌ.>#ߑl 1Lt:Ĩ^aUb Y9û ~)e:Z+ѹ}Ǘ0Η1ISۃNĠ&D}Eegҟ?jƘ1(䳘C i_gZaHؽЇ.IK71)@d]w_?ѣ|ׇW.3l0/_~X=q|uR,//gϞ= v R7Ά O0n8v!w}(Jz.q"Ayy9s r;f2#aў砪[o /!y@Z YxXE$f1d(]1ƌAh)=K.g<|@ٿECWBr$]ӆ#Mұ1ԭ'Fd~=ť,EMp=o3ڿ=&:Hݎ:FOj]|wƚ;(#"I}~Ef@2[/խ@S㒫H^ { ~w,jb)T׍Hz TA&ZS #3zILbFƯe{ثy6:GleHW3Η28 dr+qO?O9tKsD""1WK{RkcD qL* ZS ^]kٻ JML"u:#A+0EݟgLSO3|pt$(–c?y."Ǝ曠m׭[TSYYillݼyA~UW}GQhf6z<srr8y饗 cݺuTUU/uI0`<+V`ڴilڴ Á磨~aÆ[\ѐ_|!I}? Vz}ͥ^41x<|!iӦȑ#y1TIGh)Ռk!/gyRⱃ&2%;3Zϯ&mڥL}f92Hr4щZROgK~ME[h).T\-Cv+Jڢ~_xD [Kط.|\f)"GNrI%QG85'qZDCYS|>yyydeeǺux뭷4hPWC ¿o2338q"R\\̕W^yLCRI& /~裏{nƍ^Gӿ{1n֠Js=GMM 员Hff&3f8&y$dzxbn7C%))lZ- *2)4 fbԨQ,[M:|H>ow0 ;PGy#/ nbcWLBe &'Ǿ4d ).>z-eBtJ}~}\OgDP;(_) sn&m@t1G}]p#oQ:#pmZ ^#!u)ť1iUXN{ޱ1m>Cj݁^)q9Thj>gNz 8kž{KH&MeA3bHCm4Qx]v,y sz2q6Ҹw#e+rn3udgml=B1-.CЫoCJN6tGFFrǒ~af͚EjKOOgҥlٲ;wRWWGrr2&L_BosvZOLL SLaԸqg޽ LBbb뮻w8KMMeڵ|߿#G2qDJJJ]p!C{j,''+W,X h[SR^^Naa!ZtIjo~Ü9s'??N?~|Py:t(&Sp5z||<+W *K5j} 0vXFEAAAHRrʰ(7 kp6ײ?j9߿<Ӈ^M^S0Р擲< dF1ѣG3z#?111b3z=3gd̙]n|Z6Ć!n%Ǐ;""-fCӑ'Fg<>,F1Ȯ#z='Ndĉ]?%%%P@V_NN99u}~2kj]nu)JNԩ]Ww7 pwFkk+Gy$#@ҳϻ=Y>̈́?9 ^_bv$8y%ޥH03wo-HDǯT/dJpW+p4PGPhMg۪5-c;q&{0BLaY( ʏJ!PvK2 ZJ3 $$CbEv,Oy?=jGg+ ^w(6ƛllv_wZCaaWBEa1T؄aCM-*gzN=B:IW+uo`i] .dJ}}=+V`ݼk6ʩ"&&:ԗN֢TkZL3@j74'q "up4&52"q )sGȯTkqv/̠v!Q)<6m ɯȘN}V>Oa 3ʘ2BMnN4U] “;ר=ca8Ѳ4ijeNBGN;WvMqz4Q N~g6g*JBCq$>gM&qB!ݒnT{!."!}?Q 6ꪫV]nSYY٣s^ZݦiӮ!o :nG=p,V]RGoM&ȱaF:+ 7'vΎԜVci~0QOoFM#{GsmW4|~:ۅB B[H='IdQTTDll,juHLPT7j\D vÀOAOPȺCUrg 2pQAs~צzqx K.)Ю"tj0KdPTT7NE#lja*%;;>l(9t#03n:ByڨVR'Iݮ`ѭj-ɓ.`M!MRUqڥW+;n4\GPDtK:4$m^J9JKKOt dqVr4kf'^ ۝kT+`)Ta-;%]ѬzEBtz * 0aP/!sSSIOh!@aל|X L*(5H")C45@O2n{[;$P_xVY7R& kng%!GkUTCSN~~~%n#K B! -r.E 5FpqvJ2yC̒c6u02'ZoBNϒOS̒ INycL|(b!!ר5x]z@&lvBr"εyI[yb!S5R),_ y ѝ\. q Шɭ`? ![OpTZ҅V%%%Fz}T0 SwVWOBiFPgbɷMϑ Fvcw!ͬ ]W D42 6 8 zBhŕ}O$Fɜ%!{ZҥOݩ˻Sww[[DPg}.3p;W*aavU,Ks;ԣUeZ6͙qK/♽#^er =왡ok~҅A'z5&Z M[  qK6 TZ=cuVzk.MFDIwNJI|&D 7J"BBt#R% q* S)I2h)ؙ&ٛmu?%ve!tyz}iXxq曙?ٳg+m?>s۾j* xc~+hҥz-III_wZNj3wfBdjyoNRR: L`Xl8.wr>Sy-_~=ٝ:],Yѣ*(--{bƌ,^ɓ'F5{lRRv=yf7o>hѣYdISTTW_}Pl EEIe`0`6jdl̤hjNFkk ۞[S[aTS\I2 HK.2jTc0vtY\Ee 74GOqRhw!gBH&pq}<;?s:'1j=GdZz{'޽{SVV&B?z)<99@"ᛷ:|B $B׏={0~6u1lذqӫW/s̡_~~O?t9==7=RRR{1ׯ_^˽KJJJߒE׹Z|nj5묳mvwd4[n%X}3f aaa 6noTTf{>m7tSu]r- 41 jBQQQ<#Tp;0\)-6?YY]n] XnJ/lv7 :rRZFwsrҥK9s&ج#lƘA9_x'…0jT#6ӦM#::Sf8̘1rv澵Ȫ6Ş%>uodGQQ<]:N %}’jzEE $pUaOhR*קGuK .7|[X6Vj%ZLHRnsWw][Fl,/cO<4AF-WE+LLjɨQعsgB0BS`jXhOE. hEc22L!znMҟ[JFEJB_ o%ȻJYZڸyS>kIԩ9-JGnVKxhpG&vK,v׃< ZVaQģCyzw I,X0=@t7IQ|r(Ą}e聺)\;֕E,̬djB8vSsWiv,RewrY\n99iC͛+X^\-򧽥\c|Zk.M20%'N+ vBB^]!h+pfs庣)KҏY^\iz5&|טE0Y(x3LO22n4+̬)`HK{Gp\w/fbeŵl03.F)쭶.ΊpXLv'Kk8;@p};,lpC(TYYQ\x3~8|WŞ*+#H2bqY[V)Dj<tSJWgqyCT  㖾1>fˋkY[VNt}qL*lN Θhr9v~,cVL6'Kjɭ36FiQ4>aOMfMPŵYofaSY=vGhA/5V^_ƙ>ߨVDLq!a ciQ-+͜}wVYvFqqTʫ&IfJoІ 3km>]>;ZE^èhְd_)A]wBSU;ƅyD=k&VH(j(vژdw,,:nCyjԩ"wj7ATT%|~zO2ESpLFZ+R"0oQ1"*7WpsF5;|MJ&)%X]nZHACΟylhqE59l0.FǪ:SѼx3 Djw2+无7eTwHZxzo)_%M6䱬F-:7[xD-̯7$HjZ/i޲KjyhGfWShv2~7(F<~V(Ƃq5 IDATT& 5( SNܕ%9yo\Q쩶rFAAp^9Tw_wL<';,ܾ+R"xqT} ,^} 1ܛ޿ե|qzsR%?{eE<;(O[ 80c :ձ%5 NIGP,~i |}fPhq|ͯ*iL7Plqp,8PƼVg)}-ˋkhMϹ1;]i}}SM @]UYw$ 磉D4$&;خbHo?+埓Sd[ 4͕f_>LKhv{?İ #Fb_7q˦@X x$lOPYZiEIIh ʬN)6sd@b[㋼jǂC6l8A!B}1ʘ>K~ʯὸI?+<-} I:5O Kr( :$J襃hU,8-ћ4<;vn2 @/F%ySIN F%bh 6Bdim{w{o kN͵iQ\nvWyJά4&k<\k>FrOY_FF8?Ȯ*+&3ro&iuÐxl.7?F9'%K#P)DjfKů-}chn7;}fo)i MFqπX2l*iZz0ySGw\&y[_j3>L_G%QisQXZ^GZ cc g^!8%u{KzÅo9od!Iln<緛pJژ^ߧ5VF? dyoJt~-5y\z[s|q[㏚;]Lo㳣U޿8\n06:&h) }+mN^?\Z Fv^zy_c}V]ۙO{K}^&Z[( ˅JYgcq1,9u2b2(Bx _Cm1.FM C5V&5ZB!ɭۓ'ޞ-lgL)3x[m,*;L- gHÀ҆yYo-=^f"D5G.k|6g OkskUЂ]0~ jDYg bcc.7pUQ\- X7Zߗ楹[zoᦥjupYOвz;MZ4̀sUwl\3s_xZJI/P1_k݀ !>)z NLaO9<;hp`͒3[^&_u6ΊoyՌp ʹ=`p"͵ װem7ѓOs|!Bdhd+K(::u6f6UyvD/oh±yBFBxCП*4'%~@}1Ǡi91ٝ.T mtϷ'~4}ۺ2 !}ҏwayd圄pvVYZzth3[Ck`K5I֢\O?3N7:#΀5>ۿ̫n1o*tcu~/J>.>ͭޚgg;Z3 ,)5(/(cgUq[L3Sor߾#Έ;%%EfmY=&}s+we~5q12Bq* zİ^X]nk0z4\s.eUrFvsKK=rFų#zsY=Wru-mO L䲟sYYRGvW33'ruZ$"ܵo k(:*'Wuٜ2w*y'3VfљIF9&N!*Y_nfEq-/,g#4g|R<\$*lNY j8Zo@◣'V(TJ&|;Y_vUuxeEf[ΏqiU]wyU#;gvl0 fCR sGk\s.*,8P{Y&nCZ !y&ꙞdݬJOA"RB<.F8b(ypG,-MF49'6sl }x}Lr@1 ?;+~9ʬT|>97u`` ]C;`r{n7*~}_̅FU[ ʵJפE6Ǡaܷ c> ^zQ:6׏rϹޛ.RCc&;c(J^r\=Plqov04Rl xŃZ1͠a8ZobpD ``r#p_5g MhxYjZ϶_oǨVc0wzG`v_c#Bnj},w/#k"_m#Elr|WF6{ o|Bjߨmu|n_j.X W)I7hfꩻbhUgnдتm3Ix^?mNl kv^_QdqPhq0<2;0LJ&xV[-$BSKHVx>Ms'ԭ.)ߚPtqƨ& ,+哆9ODR2 ĠRM j߂Hm[.qZqcIҩ[hVhKB!8|~*Zz=29N6SB!y$IAI:5Ǻzb*FqqrM=o0a!BF :'J'?BBb F!B!1 !Bb$IB!"H.B!D$]!B#IB!!Ft!B!B̓.4{J(jױZg #mR:7j̡PWPFi/; ! !:(cz,b\ ÅYQ@T=Ht!hBt!Ds+@[6TAz$dO>*ؑt#o`!!Et!NaW-JEI!8v@CB!2pTSZidpdV ۃ)l-t};ًyu̘c]623+>M,u}e|EvzC%3կx,\3q-0a;-60~=V1v۬[KS;w{nw1c"j'گKI^^wog-k|X|XWbE&;^{>;l)2mTVZxռ6sr?hI++-̘߆ 8N:.m:Qe_/y}N*/?oG>i|싋HI+-mZx<_ooؗ^Z|on_:XF~~ 's.8Wzٙ1cuusp= qӪbgPk?DVd>g89$GĨ9ssؽhOeˎ`6𮮶rP&V&qP9?|س\[e|ƎMn%9Ț5c͚p썶o/",LŁ>w(ē|dg|bLjkm5,\_Sc̖-NolU`2†l-;h1eljjTW[X^-o_)wc7[[{PߵC(. :N˖alZ"N|#ڼ(bv~9RѣUeҤlܘa z׮bZϠSS3k :=ߚ?juuk!6OsZ9[дi>~׮bv*&2RKYYw{aa Pf hN 48p ɷg 9v'˖nwgO |Jt!ZU,gPg , fv0u/R".#9r P) AZ!8yI.)vr[q-. .(P2)ϻz{..1a8 ɬ|p&1B!NJ уٜXQSc݇UI}>˄BnpX'o+J@LoBBS$B ndUOYOX%4iD %"lQERP*P)u(:T0Jݱ Jeo{~ʚ(AJEBГ>aB6v#!B$Ip>O =aP+[9:0.59SV & BDt֏"%J#f (Xw)5>?7\3 zSH̙3ɓSy3HMszOP4~|ocoy4#Fj:s<y瞉-::+dʔ>>ۯvwɓSIJ:G">^ZCݟԩ}/b瞉2gʴizt:5O>9 y&S^^ORżyefLө30aBJKs >(*Eny;πDG>?qqpb:<8cgBl#{p#zqmcsd#:]il >>7pZ:K.…(nwmd ?JgRS^yks/&01~! Spmͽq(:ZN"R"/#14hc.]̙3ٷo􏹳 c̲QɓaBuj,f4kpӦM#::%K~)fƌX,VZPNg}zכb" ,$ɘ-^̪Wu<6zjS/+w{M) ^~ekA HNNGNZ҅aEXjF%1vI.s/v ǒy%IƙDF.W!-$]U^ ^ĔIKpml+&Zաs-}m6Mrp.Ãqɍӝ6m4W JE`s+ !ĩBt!B20.Kw:|x؜:V(nIP*u52B$]EAͿ׳]ZOә]:CZ5?ؖ G_'R"/ؓOB-BQ \/ء!"$I"iUqL[B3H.DR*4D vB!I҅B!1 !Bb$IB!"H.N9M೭ںڥܶ E9Lm. vB! ']z{SaތٞJGJ$pQWs*~Jfn;\:G*bBt/Y6_\wL.{K!f W ww\B!IEȩ0odUTn Mz/cvYϯפawtVͲ9+gl3qj dTs9˾5)D s !"x$I!gSq-LJ|ٲ'|ء1$Q?헣W_5gEnL;&.9B!$"؝&{"=Zھkl0o᪦Wyٳ) X$ SE[v;([7 Q)Vʲ7.btcHWWEqrtdu,&<mn55]Uk23Q;|&6,b3jleo) y;Fm?#.A4)f!Bt=IEHqKmqtYI41!^Jvu zs7eѫ)[ΘI2Nʺ ySeGr~R?AN z]nkrf5pM(P)fG;4ˌBFۂRaxgZ듞]^`J.l(PƅQ;c 8f!Bt=E*XNkrgcZUqw[ ~i_2{C*{G>com(fU4JWqFW-i_}v?p03ebG̖ߴ.šo)]ƹ1g@ง1`sZlh%Sxv?FysulȻAqrj.fOSY!]Gtr&~Hvu٘#KGhvZۑfq #.E2o6Cau\+3'SoajJzG\ݷOX%LJ^@RO Sikv5q(o:὞!"l BGq%2p}c~̀ػQ+4霖29^!Bt-"BNT.šPT,;0Yvp%%EKo[z*UA/_˖;Ш9/}ԭDN&*l8vg_~זK&Z7pMFk0oRꩵd%>+ gQTۊR‘B!nRHȫpśdV͖;?'U*45cФYaoysQ~v%cZ<~LkvmcC UQ*44(j\n;VGIu47g˂R%IB*$I=B4^ɲ ~"<:ûvr;P*4>5 TgdUgJhUMqJ/z9}&nE2 ^kf|B!D IQbc0oLE0aC}X'}o `c >3V: G ҼXdT&Q\=je#{=Fnu`s/BMbwUSV}N9EDFϬ|o,{սG<-jNOj>VeM(W+$gQFm3$Qjm1-v)?-c5[F!=$"F$YA=&:嗬Q۟`cv]+M;c=!|٦R8QglGAӧ6B: -S> }Upl:ҿ62q>#3!&|bB!IEҪbѪb|^^RĨTONDNzBdB!!FtiUI+B!N"Ex}B!: !Bb$IB!"H.B!D$]!B#IB!!Ft!B!B$B!BI҅B!1 !Bb$IB!"H.B!D$]!B#I"$m>~ .ݾx S]5Y7E9۶Kטx/|{'Cvi_o\~2rlO>'k<޸#Ź~=PFGszrju6rR~9ջ>Zuv~q9٥y^$IBoU_r '?eX7ERy ݇;-at<>{'}a/}'>-~̒YȦ;*î_*h᝼Bk+;lӱ^f!ozz?iz_f!۳uzݢg$]!B#IB!!Ft!B!B$B!BI҅!)I|d,:VFe@'>"qu`%j-}%Eǟ 4& u&Di0TQNOrtj> {'G'E1ڻ޶&G'`:n3B4gſ'삛cMp4 B: ٶxkAt)\8j rUrU>?}p<_˾~˓m4kywo68|R㒼v5o6& !Bb$IB!"H.B!D$]PRrޙ {>ؑ!"$I%w?Sp+͟վM[wyB!Dȓ$]8sBkQw{~䷰!B'S0 JRF; !B$Bts>Z AG@X1.'Xj@9V:Ͽ67+zZ߅BqR$]`'~޻}I.02 -l{W Ͽw9V~㋮!Bt+I҅Jn2(̈́K$8lXo_O'>Ǥ kOؾ]B!Bt!cWxeg{Ȭ}ÌG>n!B"DWg]B!Dȓ$]>[Lwe^"BAt!Z箆 >Y\aN[bB!IIt!Z]UrJp9| !Bt$B$awYve*z~G$. ihw!B I҅hIh o;uT]zkB!DH$]LS<3|v?87~ tm,qPBn8]ZԅB̓.DKwO qg0h!?v٠4OAS]ٞr17 Sg^>!B$I҅hMd"\-Uۓ6@7y~BqR>B!BI҅5> G~ vB!"I҅5jX-ABS$Bn_B)ؑ!"H$I"$1)BqJ$]Ptm`G!B $]P4j(=9B!@t!BQX8u;|x#B!DH.Dg- v$B!f tp` v4B!F _?O;!Bt#I҅u׾ ?-CkB!$BA0uxsdv4B!` τCƄI`oqx~??b:pot 89^l0̋v$g8Ǧ3鬠v1U!xb0?_ \J =I҅)ξ\xu:ܽכ{xvpSѻy5@ӁD?SIY D;гy'7`: zÊ[ 4C_@mnk=\;wZ:@t!ziw =ξ.[. a95tgپʣ0"2eî`Uu$ʓOϨQD0an# =Q"Mgf<'I=I?5s-uLk!}YhpB.DOs=[6xr(lic`a^ 8~xt_B!hI҅艒`soaٰm+-Pxf|^ _=O^tlRѣyWh+ݏy'>Kcx?f˯r83zseͷp83Gz_~g^\Ͽƭ[y|̺Ft9o}a̋ .)+_ʶNx앿k_XϿj}]]**+x7ظu+o^yk!kpx={xm;>{_E0y/xJ‚7+o-ucOkljhvOVnJ+>.,M(lvo#Gj-ݵwcN;#nl1SHV6G1۬xUT|7lr7=Br).)a޽~mފj<dRo>xrؽwƈܰe+2[TYxb;x0QQTTVi6Z-.ܼ|6mFRBl9W_HUU5[wdЀ=( 6mƹSjvjБLBt!z>}, 6޿ɓ)#YW .V#?{|0͡W(9l(*5nKK aCacm%?#>+KKU*=ꎛx{ϧ)Z^#gaaktj\Km۷Gxԫ[<=x nǣ]@6۴B!N'^ã~Tܨ!'7~h hӽ['VPg !}zu̠Aˏ:u_:na( 8թECw#<3hlmѾM+8;c@>jPŪ sɿVVѥ3ǙL*+4dx뭷0wܪꒈ* OJi!1+=ļyPv)yŌv܉\z>*3*u1=z7n4v(&Ác#_#V(6߷t8|〄i pv7 &|piցYUW߸d2x Oy[ћ;#11W_}3fTSÙtYA.Pq1hlyΏ<[NDDD5I:,:0ϊϖI`N8{ j[88ZFxxrW]wx7Q*2tЂMU*=p @]4U{0J (` {zLE|'J`NDZH:v90ӽ$r@'h6V99&1v$ @jo3 ? 7G* +h;.a@sux*I:ULfm|z^88\8*R| 4D-\ξe`{S]qӼtqC+@,6Hjג~ toaqTR.=HDDDDD$0I'""""21I'ZL pFZJLFh׮'"""cNO +ܴiN@ZZ͏MDDDI:=񬬀wWDDDd$LC~=jc[[ql1!.]ls1cŤӧQo.^./bpwZO&g̭Wׇ{n~aҞ,LHV&&p/r"""8&BB8غ7k}|R ZD\LGc祗}DV=IzeA۬]/F=zݻVXXLOOeK6L,n;vׯ tz}xXN,{W\4 __qV<) HMRH&Lgcc OM*LMC=: >>.1Y /^{M\|;|!nY|? ?0=q Fdo_r^,>%b-XRзm?nn@Ϟܹ +3kX&~9zv<""$jDb|옘?X.ԬXѧXY9#Gzzj8|XՊ #=?axڳ' \LoQ܅6oR2۷v(o;Ad1n[LśS.ýqa. -_..tl,EkqtnFsgkWrZ10I'Pղ.vZ"+Ⅲ :Njkm%qw„h^xX`qwB:o*n/3xC %2yy%,fF.i&Ddrń9 6Z8 !&?d%' δ"װ0/]y^{ HIaϞ,XX\Zt=wChnYIDDUI:JJx%Gۜ= !΄[X*@^bB muj~OG~rZ*{*t"z,;&֛\׋,{ZRdR)f`pkk9P cz40I'ο A { zi Ngal7`#^^|1I'֭03]Ihޢj4|P%"G$99bG|<`ht"zlXZe.DDD&DDDDD&I:aNDDDDdb&DDDDD&I:aNDDDDdb&DDDDD&FiudxV5zKgke;72Ѱy]:YTsDDD$`NFtAS7 A[pD<\:xy ?q~,CË興I$qW|}dLJZҕ$'9MZ={ Z9N؎lZt:T6z_F1~hfAnسbGPo<XU(wZ+"7K|-NmoEZBJ\y//K}^t\ IDAT&lN7kXqdm<Qmƙt24|;js/~ s{pVüvH_Xٛ?Q8 &_o 8;3aN;z?O{W+|o,lϜkr&daנP"]_0>R]H^c"=),qhq}8ڄxKZBg|حX"""2mLҩVHoPzR6tZ4#nikkiŞz; f+;gNf~Xl+@a˼32L'д}=鱹X0R;k{)o¦9񛂯A߮Fptƞ?Σ?\>tozZ:j 6p JB璡BV@e&KZj3JXձ}] Y2S@m\.f qje0PR3K̓ w|LҩV㳻 Y~8#qpb. a;cK,=y{\D^ {r]| IfRGEk'WoRvvYipAہjs%6k o"9L :=tZ=ZYt  ]*>}R_X$L.B%R%/WBms+PoRBU䃅Js"KxlȔ1I'ȗQr{:[X {.컁_IzxY̝ K[5V< Mns[cWpnhaMQZJɘl􁠨.#|Kx^j+8>o'+mK-cؾ,F} r#?D HMƝpgjF}-q\<=E~Ns@C~N4Zh򴸗{HBZb6ғ\օ.6j %t:h -?\h t.@yZqhűrҸ6Tf ^]$0W[(aigW+VVwʬvODD t2T Y[DŽ?U7Xq8A/qWX7D:aۇqAksL_=MݟўMwXX0.8~3'[p{\JUAFbOgplC$z'xmvǕ=1m s|,hVt򡛸:V{)= c[K,rա-*]OTU>u]rV/4BfAn-hѶx ۺbޑjhrIj /DK-qz{ }u_[c[m`Wբ8ZX!8ǟ *V|z/7۠n^`KH;_T1I'jѨ O^'`:^#PL}'İwۗи6t#ml$^n4Ɛӑ`8'9|MUȸ$j/j3gw6x V\1xm琞)Mᗗw!NN_]|:aƺXY5u%b%;4"$SȠ03DeaNDʳe]).DHqު0gaJE6? zPM&퍿nNi94Z x1*I:՘)􅅍?> B{y5VJgaAKdxtZ}[zǂe7[.O;$""&DTc*9>X?qR+[#q8[cڊA}8,D:Yٛa/}1g(l{^Fg찈0I'ehۇcKYL\1.|v?~ڸ%MMwqq9]{)t"qqjk4RTˡh,*3^._//*V3_9ԳWG-G|DC""$Y.lAtX2\>|/ީx??#Ne~nPJL_=]GཎqaucDT)kLՂtro; 嘤Q+aN1[X9;t ߞp X5QttƜGh_L;~;g8L9Cy,'׭58l6ojtOJc@D?OO]㏷E~* ZNaEW|= mk_ ـhT#Z>߆MscDPȽU)Uҟ}7|2J =LD$j5>5W^SvC[GAB͂|.HvΖh]O>kڝ>7+0{( cDTal= 2H :)G);kɴ1I'">}=8-Nr 9]FY;wcn+ٮfmԘ*C&Z{I;ҁ¬հ3C**wG EUiEDTյ5C`SG &WN#/KcE*3fNynG;JQ+a#kI:]cՁ*9d2@].72{?xwY谤ۅ7Zb=}4!pe\ Gj9&DdUx՘m]T)PêϏBW7vxsbćiHV|F8'{+>=lP*Y @_܅T9L҉Xplл>~6ZB&!;=~ {qĦʄZoA1{dq.zƮj]@ْ ""`ND5f7'pFf̭TK_|sx ,XqQAr;* oެŠ敻8-Ruެ ^cP&Dd|:<)gw;20m@ܽuK'_ +Fa=aG$\K%G"' KÂWV]C;x#5j_4(X<}Zqr25몿ZO6?iFuaHcan% h]1T_ĦЧ!""0I'dfBaMJ$*oRKJs;* [?\{Ez7j OgO;X?\VZFtĞOWsQ G|d*ܛ1v8D& Cso%"7U2cS[k3y{cBI:}=,ȭIp 헪_)ːz .^vl^uk?MRF\]cUGufÛa˼3K툙t▦on@/LJڙ\huLk)֤QsPaeWnFi "^"!/F92$fY*J$ V*k0|;{Tpʮ;`p8tc}ZEj>6o\\uJ$\KcNdtz@c̺=d Y$1DDF$`l Gwk8Z6v0V*-PP(0A7[' ; Ju? zh֥$racQW#Wt"2t VÀo'wLۻZBe OxnHDTcXBDTise&eDA ~NID.?I U-Zg#pe-Z0xD0T> 4o\t ]%߿X?J} TL&&N(olls]&[ftťRK > =ҡ›0m4AKmo޼)W*©S{{{ح[Ac¬Y,ZHٳAۀ!))Ij_Rˮ]J- L//غut^z?#G,7X\.7ImҤt?)) Wt$$$HW ///>,7oB||Xpat۷ 6$''[n{ NNNB@AAAؿ1gΜ >}!!!cǎ8̙3RC @&hYˇ~h~zWߢfff /[ #FȌ)H gϜ9# ///Aє:>c xnV+ \r`err 0{l鸛s˖-Rέ^x,Y"upp08, j€> R?~رNs3ggg/bccÇ YYY¸q شih׺o>3vvvŠ+Vk0+/khŴ/.ڶm+OϗIǻu&o֬w^=xpIĉs)6vegcƌ;&DFF /^:dmR|| /H֬YS,"z&LL&×_~ 66ԙ_8xt-"119998{,q!A}bգG9kYг"- .9r$oիV^]laeipB={wAvv6׮]+3fԩS@/5`gu 1cÇfo޼YQlYr o`ZO:F\n0>yd[zܼyJ0b c_|t7oׯ_#Gg@hI A׮]qa̙3 ,[v6h_ҥ@Æ `cQEKaʓAԩS}7mdP2RΝ;y DڵkPT>(σ* TXͳgW^)!FL҉d}Wҥ KMwssCZZg-u3ܤܨ(6ʽxPcN8i lll!!! 6 +VE&m۶ŬY .HFTqQN YTT:BӧO/}rpp5d2؇xXEw͕߽{UI|}}sssbȐ!%>av "ڋI:Ν;cضmA CQ-[˗3EE,t'Nك?* ؿ-Z[FСCw[NjWXѣʕ+Rzq <)))z ?C'.\OֳgO>KioE4ãXKmJ7~ԨQB&M)IAzЪU2lR'"z|܅L̙32덿 @#F@pp0-[W"..vvvpssCǎѧOT*3<={bҥr 鉀?j_ut۷oǹs`aa֭[c0hkgg3gJ5O0$@vpy̟?qqqŐ!C+]SOSYe/" ላ8:zhĉ Q666K{6ll۶ 7n@~~~~Xn"##ggg4hzB.] 0vX\utH-[ ..N;o߾f駟pqؠo߾3f ,X$ǎ:(;\Oڵkq)DEEA&^zhѢP #QGL t"""""$0I'""""21L҉L t"""""'kBCIENDB`neo-0.3.3/doc/source/images/multi_segment_diagram.png0000644000175000017500000025470312265516260023742 0ustar sgarciasgarcia00000000000000PNG  IHDRJGʍsRGBbKGD pHYs B(xtIME & IDATxyՇꞁaesAQ 5*:{FM-ML&.17"" l3}?)zaz83Uշn{.aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaQ\ kp=we`1pн^oC8'ߴ@zI54IYGν3a m7˵ RܹoKq@O o ުπ[/.? /*9W +{:Gɽk^ ~Gu~o0 0L(u:_&5/ @,^90L "1Nw'衟<.V!tIyvnC&Z/&T ܬt 9 xX*^9u@ =U8Sd-laF:< }>7{Πrvױ9^T,K^{S`kH%f'(>.k8 }QrAI"ME^?$# 0nwd_Ԇ 0+m diJ$!Y90 b>9P2!Q4>@fMma #BRcEo9VҵO\W{F3C_ۑ*;h{Zʫ;/{jap$2mW߲n)!3nSRે6 CO'S^:wl' t[YxߏiX |5w&-XU@NNF<@תHjYߑ~S0 HMO'%̳R{L|:YS0 ès'g(]*>xDcat2}{ 22m6Ñ'ːl Td '=P$0Y=.T܉:Kx$25 q7' ޻09^g@:S0 HK$X䨹 E=k#SPʿ9z?Rϵ6LoS 5H~R4:?D?J7 psu%Iݨe:A_  d{O 0 0B\XO+ 0 `326"xXa- ^ ?EZYaaaaaaaaaaF~1Ixb}#,)[\Q+gSu}  0 ΩIȚg$ad_^|`"zz\om.E֏3 0 j6dOX!5iכKғҐukaa 5(i_)pr>8$TϿ1Yd8& T~퇬YP^d1[/5]xznjӑ5V rkHu]aѨ/5!|8k

}&s*yl5ULr0 0 #yz~0 { )H )@)2í0 0 h\@! aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa8z`>ppOSk}n:f^\LVl ÁkT+#_F *Ը|YU^E3z5j`VǬզ%_ ~*\ lhk԰8Cp_+zmSscVj[Ǯ՞y.~5<4_<+b#V+KV D'q`guث98מy.~5,g>͉"vBg}ZcAơ?985h́3.+.{p??ϑaL8GBzmd(-Ĥ/Sp(6nʢLS#5:鳯ìYa3f.454qKw `@1".kz+ij@q^,п95<(oE^ޚ_X:V:d32wYS`5bV922f1JV~JYXZѐx s@\"Z/ߛa=pMp}߇7ج?F$r@tMg-bҶM[wFVǬYH1eF ^ Zob|d|3{2G^ibĝ;y:ȏ* NF jr>oJfaԆ>:fṵՐE JFC352]r02~v_0>͡L?%jsݛܗ& $5:f6W}+D8 5&|)52AldZkK`BR{CA咚\w}C}1cկP_g$8bBN |?|CC>@5Vk+?5xOcVl~5x6&­T%#s&YYR3;zH;"[݅8?{.Z k$P. n G7#H cVjX̆YbCw$ElGbܑȦd?$ЩYq$Ponot̅t׍,'iϓXsa|C.kyݙ@@L=iF!^!P9/`uX6xue@HS5kz|-p tl> G92u`B>^rO45 uld{552Ԁ0^Y^ $}>OV^9^=~YAE`MaT kV _al3jp9$߉aX3~Mr"~G;̳W a0FHEH dr\0V 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 h "3: zpߪ(ZB?zB>53U:^u(,Fmg w Ԇs}`jLFo?m݁ICbnB[=_<I[1! $vg!m9>m}A)I+THw0dlK' N~1J~ɏ3&iUHHٹ*!C½;h} 'hv߭_k}ķ % N N.j{}鐴R7@L$WċX@6Iuķw.7mB1+_ ca' ^%3 $0;C.Sg1-i'|ķ Ow bw ca'8[s\RTJuC?aIۣS9>6wD;"V|? bD0 Gf_m )ҼAdpr1AHARUZ }[6~WExEBx@;'"-ӬdI7Q$VJᄧdO%Q]GWKC^nF G`\ 3Qz=uTnBƌH VI/<0`n˴AZwٖ@ ^y ZoDtoNbJQn ?m)mU1`rCJ6V 6e/1yJ>FOE'!RO2d(>iȬ*~&lE֒K{, no֤U+SV z`S?Vmv b,lÛXQ,|8ܓ${2>΃KZ~ w2$4?}3к X s΃ɟOсj_7o"Q$6^.b'+͗…ܻi^7Wc0g,s{ FaHH6u18s­} ux] 4Dw?*Fv?\H򓊾K%A U{]¸QJe v_\?{;x"dHX = )o.qТtrgO1y>7oPO8YT1-==ݔQO)WʊRQ ^j <{blȳԵΈ!C4zaH\z"Dtgƪ;0n/: AEVx.ӏOc\i߂\Ef[K龱ϼ:Q֐M2NzM;gq MbpU۰LށbspB&zD:{`mL+M( lMfTŎfjpu `g#޾xwA)Cvz8G"} - %/GB1q?wRb}F]Ϧ&OlV&3!8x]~9 '7!Ge)޳Ԉ% Ϣ}: tx8miqDD"`]䋩o<8 |,Ub"e9nw~8 {x@g,qO7M>wO`EV=GH^&RL\K`YHS?Hts!oM+v>j9K/IhD=Hh #.8lN+X"AdmKu'C9yx5 1>+ =7Bq##q&29}VVƾ+۵h`H=*0k NGcBk|f~KQ^b4YtzN\44=Qw7 8&Veq д$RX'B-.Y<6+ CKH!xJ~ٞ8b>LFNތT4>)Y?VSep@GO!!.&]s<P6U|>$7" S[Kc{6Dv=D3L(}޳ _+%p;=9lÑD'̏I(71]*x `s'g>{g^ ] 2^'yk IDATrMܻOTL-tY<Jῥ%ԋ7x*2(=ͮbxN+O" .ahr6Ux=7m[t,Ng y{,(lE]M7\v+^ Ug!كs_dzLpI0 *Z8x okh7$,`ϱIhOA k\* ?"An/K #P[%)0I_w0*cxqvv(i'}&<&itQ1@r_q;`V\z7Gc`>9ńRݱU+Ho&*z#2{9Ug}U B r2rj-ŰEz"MDF8Dc0!sIC[pDHhW9HdL\Ub:n;z=T(y]bUs DCDp+C\`d\ޜ)=n4íFrï<8˓^XVy>S)@zm.{ &Q!|]T@mTi/!'oH=|(d6^<岎!Cz=M/}bb:l 0I\{T[08XX."Կ^yAkJ  .u۫p xnGIJ6/bpboԺ1.8_?bWufX?fHyo}wf3vt@OCEzP[6sY >]&^:Ii6V $U-6vg` ɛ4lspD  ԓT1_x suۄR撝YuמۗIb%r'UsWoOZŝbpxLByҋ g?q56 3.@N|سxu!&^w-?DqТqhkUk"0T,*l)X{2t8h_":[ 8RJ~S)HpB2^K5RoBa<%MI)ݘT"ʯ{(Zn#zPت8X.Ä[b 5lwzp'+" -ʟsv2BUTAm\O/`zbyS%%IˉN75$"@ٍN&0Q~/IĄRޱgȶY<Ƿ-zM=GY߄ ' b'=7w'%p^ IS@~ d62#^Z ɲ)U$VPNM[`׵; ݧGO.р^t?7RQٓ4>X wq#N Bkz\c`\bM)0PY]<[}‡u i|/@w[A"X.ģT\W"4' Ώu- d&bRF_Ȑ$rtAV\sB8tH jUHNF]ddt,ܻDE(W:2ahYEx^^^)dɗ }FAqYdzph[(Xj"p%٪ҏw}ҙ *6B4^=rP_LYt۝=<P,som=}ܯվk{J$DbB;ɶ**pt 4}7y\v z,D뉗ut !BOyHvRa'1Z<=s|K @|1D;8wP.Ƨ÷pSB/T!go8\S?/72+drmPhopXkC_^Z ] ( }l X޳JM)>amAk tj/~Єb|6toi"<JNP spu Ov~:OEL\eeDTX;YF-sIz~'Bjy"+`T`etۜ"I=tGd5>%V/܈lwՃ_:W _ Dٿ+"0㯶fLsi:Z/gieFpN^q"Ɯ].bfa`Q|㉉ F;_<o"8(0M i;.3H=^+kOQJom0W,C' ? | */EQၓS`͍p䁈[dzڲ/ԛ'xcުk aIsڝL׿Yu%{:A.HO\Œco>c%Kd`P8u9gxAT^j\z‚b4fC٫Oh遍 $d 5;׺֓@MᓵR/p@X/^oX`~_,JQ8@P(7uzvi-O$Iq:ClMU]= 6 4`LD'"^o{q[Am=] kb8RZd`4\^# Bi^ʝ $QQb@n OK5He98HVOU7:]vW}Od7 տ"3D> uv*zyH>ybPr:Wo@uO=JN ĐJ8=Y /+kX4wZf@E 9 .V%wDwAb_D |QM A{/? !(|NdbW^w=tPf\/B&64vUت#5UK, %XCgsNzUnl\zfBnˈ[ ^*%W}1q.ZuƗ9ؼ:Aց2xqeN__`f ̋I@5@Sȓdeк%LFt˽9ф0Q#|oǓ03NDNq2{gn-;Y*ZH\= &NP'Cu|  SQAgu$RD݁%af3\4,y"uK ä+R2On)qhuԩe@FzOi[UEd“$4iI518Kaãv\R V,56@yIҋ 3Sj)f{@($ZJiP --k2qGOjb^2$ur?ZyxPʤe.hu=`uj ~:ցS%4Mɑk]Ib 8 Tt,Ȭrr$".mUڜ}-uD5iGg-I4#2Q M[N#Mc ~m}WJuS;P+4:jz oFAWQ x2i4MLWTֻXkKďj \+I6+v v5NS8 $mSؔ`\*ӦZ'a]6Ɍe8EcVg?49˶Utx'2J{'Ǘ76"R*N<ɿ/4ٵPth]exj &Cuu`P,S`cmCnN(4^0od&~{,IOhZ ػ]QWj{1:dޖ:>4cuv=Ȅ^C6Tmˁuc`Fnm!y4ڨyOF{rI̞ͬ'nXa'/7|zਊY}&-P2+ -ҌƇs<IW7L(ٰ}ȶXt60e؆F%=C}cba&p܊0 0L(I+›P2A;$NM_d81T&<$7D$!C.a KGialUNGd2[kCHvc%2E,R%`rqt#i  KLA"o#X0k\#)w zɎec Uؤ}$ H!K!}ϫa9RB1[17 H:Rg(# koV3N?ŵܭu Ql&e+ŏlC6k*0Ț߆ņي%P/6q=Ǖ=:F۲GޯKUā?1ו ST6kc#XlBBT]09[]|PjN'|~}$D$9tm#P/Y]d1T1zK4:O$B$YrZ~ 5:T6,JJ[S|CinB)clA"yHOEWl Fl&Z[_߫/rt s}`jfy_m ' H2}!@7D6?y|=vŨւ1!? iSˡi v-OU+9^v1t\s"2omΪY2:=9)^AaObmb=`q \Tv餕xσ~Vo@ ~ 9*InP"\'iڌukYXa"`}[<7mѹM 20 oU8 EPBiXPs*φ&(ncpELw@bKDJcE&<0G QP: IZz'jqg 8G#4P6=(=e>ږ|mgi6S"Mn;6J1}h]R~Xjqp"qs#>tt^hؼj3*up(x) Lx} {KCoWjMaұ.1$IOi@(ի1IDz& &mQz-]%kQqH2ӁI\d!:ɫq6e*< _tB p IDATY@4"H|t[Wtm2r%қp(ct2?꫽8_%dh:v'qТ.+X62ϴq#د_$e#CwDf0Mz2d%Ynhm):t A߃)LƊ,eT;XÁG< xTlQk9Q &9A7jT7 \gz"˙AO/57"2/7oيYgTBhgm/D\ /U$faI??@ͻtw2%Fwdݶ~+1q]wcZуߴ:{{]Wf2v3*} 9IKn6cEq.Tx5~h vLѫ 7bGYZq"_ؒ51oq|6r'>g{zdq}=4TO9Y(m@Hʹl&k}){}SuJ~K4ҤwX!=H F݁E‚̆hdFt''<3&3 Mc00E82 /4ƫlHKj{{;7D**U apȣl`R;8'E(|:DՋi#I4 w)X̌;SRnYҨQF N da9.aƎ%G]\$¸Y}ug0w%Lvsz^AfMi,-4E̎ KD[r RY $zi |-}bi^Nq#G8;"3~7U/v~kɐYUy f^Gj< cL &ũ OþF08?$= xp9Wm.2=;wPNoOG9GoOf1u wЪKLJA*S/D+ϓzrjOHKɫ#8:x/yhZ㱯x`s!92kT<^r֪7OBǩ:TdID&K%N"XDĶL|w`cKLYp%tvQ>pQ3D߃{pi\,8M3'!05CcrO='0߭TFmmT4gAW&3/pt(BYy"J,8Nyk=FdP*B'˝L{D 6Ce骼oA*A:g>礼jUBRĔT2#.[ &d O\ԉQ08Βtm)j|eq(0T1D4gUD2n|<.Ϫ8u|>Zdh$=ep[ uo..%VoI2=mU(5{PyAiN\85fhExpg<<-6D-"+qcJ9zY` ,/x긙vq<^`P:?lq.92%% qp} Ir@y^cxK,ں=XP=@KX1M\<2v,q/@@sA'/6K鯥.qN)6:JdqgM{㉵^dH{n4>Zfu[ F`VLgN4':uQ d^`t%JU!'αZA7Yv:"< 1\ވA{M{pc4v3 :*NI(zR+P}P.*6:&7Xռ vI3+]O!r$$+UݩQd=nyxT?MD\pD JZ%ڪVmjHnwQAqP  I澿?fr <&w&~lyp+!v(CpM`il*h_ *Pݾ{1VG%p & 5R-$ODC# 10(NHܡJ'ޢxìF}Rwbb~\>~mbo~u4Xhaz鯆U/!jq`uW3X̣\/oݮ1_}1t1!s,b 1ZR5dV M)MSq?s,[io`v nMOFcaxx:wb|E!)2Mcg4<|lQѓC쎥);_Fa䑜CzO|@FhkJG 'XMX~yLIbOQD'f`5<spOxT^0z e' #STgHuzA%Y@"F3#7 GY4kJh^,PҬz_qm08t-+J4=OT|~ ozļ֎=*`'ύKeAX a).>IKd&2ZVLs^,~6b,[>{p.Am35E + tkwܡz6]mx [#rsq> >wݑl%)7°f|ea55>s5v~B74]'Ƨ ގ[GM8]ωe|F~4,ˆp,T9$a:(b<'0xW )nDhmSE$u@SeR^͗Qx.eIHGs ̗sw #t6=(Ҫm)ƄPr4_h-%q8(E /u>+OsPBb8ˉ6qx}@_)5vH(.Դ0PPZBO_ΊZ<>_a[ |bMEBKnvgP|FMP,`)XΣcJky7m$ߤLDN@ԢGaҪRGZ*d9>m 50t_@}xHO)Xc9Z;٬s&ʈ%͜;Ag 9b@xg&Gq~m_Q6,_gWh8#ݫȳUu0 Kr+0GBw;4=TݾlAK@iHeU@) ̭k(%P/ hzդR}@{KzKP65.XIT%P/i)w0ӭew̃Prŕ|eދ.nL$̼jK3XNFܣk8n/!' ډDJ W6`ǁ͓4ʤyhBąȄ7۩.ʭƽ [NB)%-Yo{n0X@Lиxt5?sf`pПUwFȟ=@Bs_ K$_M3#rySHeK~]K$:]A5ڨΫ߾~Bf\kD(E߫oٗ_^Vwk$ e 1YX&սpFWv=.Jtj/ pR9MlG5RCirPt9dku , t4/II] ۿ/fVzt#S,ZptMRKl%h |Vrzǩn ^#ɵiȦFIg۪ي?aWO@iH#عOՁѫQ| h2N G(y=#{g;OU(ZB)ac/I>b(?8ϭ>eY{}(a6~W?Yk2e> }KgHMEyᾟ?\у^nȑFQ9 ¿"נR@QOqKXv$S,|b`PVp׫|]oa<fNgLΊt -t5g( j|w:rWM(doїmߎP}>R@)q+(HL0gm>Pg (c r 94+Љzig2E>50}SEKJ=N0eb^m觻\ѯ/kC3s~ca.rEZ 7!Xѥ$C%^Jpx@u|y@_+vU\ma^ 0DOZx2n`+E? &3'ӶKx^3vn;߲(Xy,H13\cPDo)>IT7v)+/4g9[ BB5uuQk.q%5`pGF/W֦"Jh\/oҬ5n]c(9GhwK$sk)IpDPZ0]~wurMP"죐̗d~;Lׇ9%.<<8uHua,Vw؎p$8~je-fbcD .RʇK\xb8 ydfHXWe3l>NyJЕ~guV+U{#˃K]V2WI/o8YB=YS5_HEtpP.L(^+] R`'3BF Ǹp[GKvYIԛ/O=egA Tf= ΣL@)cQ`l><6s@۳ |xވ?fd)T-v%Bdc*ԿUVZ\1cM#Q`a' }hũEpIHXycѰҀ́g5l$&pkRS'j⊩e7}23OјʤCWa Daa vY RV׎F*ھps0k/̕雑ݙG YS3p~4Qmf/CbS|OXT+wg7Jcgx|a</R[ha>{Yjm|# mva/}$a`hEωšeC<5ȈߕAh@ݩ5s\>4,9gAr^R4 DCKQFiH+.^ӕB zuOV\0ba]́;"p]90B{OZqB8=7r&+WV@X>=z5Z6y;In:wWG=,`:SQJӕI\G D˓|yH60aα*\I!,>WzZ7=66}b;S؀R jֻeO]2"5gO*Os`ʟ[Ź.ǦEMAhF6OVeO{] b}x}@d$wzέ{H.SPR: 6Etg8څa"` !HW/xca0rͯ|P5y{>en @N_(T¨Zx/B x*tXVc>t#02_(}C, \"}U.۸pvQu|58]): ƀx*_@ #"z,rU>ӯU6+4a>ۋ]y^7PPKnTˍHHfb & J`bu1Or[u?K}ZE5hSY,섯33Z|n>]WHWr~y.ųX ?~C\H-N"yFlP[|2F'qΟUgCOFFڲyksl%* $EmQ3<ՁX?`O:\G8{댄$Pvᮮll@t {"#;]RG6U#dSg94_OZ1ՃI܉< -H;K pyt0-"l9N Dq@I7FׁQ. 9Ko;#FBï^wc%lv!0q0m..,PJ?&}+HXm~s T,4Eҿp L%'γr.')#Pn%Faw2lk` ? i->O]hWA ƒ0p'pUйJ4z( yQ%J`"eB_Eb! n!Ͻ@PG [߀ABF}rYVk]<{B7 8/r NRV[_W_Ljp|/eV-CέRB+axLa\'3҇ƖGbu_6Z1P@gy,3MuETbv}hQN.å3,ԇSjJ\xC1 Yli0al"}oI0M"ѮZ(0Ŏxy e 8+hGi>wl)+}wW=ְۛ麱l |T\*",3Y K4w$?k)F¯L/g%d8 ua$ٔPd!ap2-5}z肛QJO66GD+}0.( []FSE _o>ea4+ðJnfGV(}h`~* }g*n£.Ho-}! Ƥ5(Y%;jGmLZ 3EB>bw5:F7ߗWpH۪TaBgtXcsR'̗}#OskJ9'#ю cG-dïKsCC`EDqց|IfذkZOU-,Ql*k_}ꊺD,;b|z ƨ%Y|ዩO }C0(uaOoq_̓'!`hبr R#7.$#Y Sn,z0'Ö́:^)E)M$_LNGNJ!;5n-9";Z@܋l҅;)aa Z$oH|LWBu[[`~2Ņt'wDJ{GG#g \.2gNaM84|0e[9i-1-} >u~a+VLnWk |TxA[;z 8HH|T*B{#cCϓF~-Ҿ9"CxS,\?Y%V3U׹Q=gy D ?i(Sؒǀ `@q(J2݌ gU E3E:1FGj0MS`dI>ٹwRg, u]7GZM̦. oi#Ƚ"O.uř+q\wv+fCqgql>ki# aW5UT^+$״vys'"Hw02/YO0i6*ia)<-ӴD5Fɒ hh3|QؓfJ ooYZ໾E ۔ܢLs+ݦsa[XcK S1^,/$T 2$3L0Hygq0𵕄ڇ(ڣ^>C#q6+nYA$8ӁOa|}k `[&.mv9r?xܓ$%Xu\aFx0;rEoQ{RZDGMA&W?Ur! pv^!hy'&BmEQOFRd6 2^,v'5߉kњD .ńecޯə>/|TW؁R3Ⱥi' j!;5SCP1Ո?0&}e`F 9[+jEZFP[t <  DMK`cXv4YJ~ҷf5|ГSM[c:ttċQHdcG[h́r fx/2ޯu~mD;>?\D`nN71$KlXA}.2+V'(NY"?A$XJHhj#ЕUw"kO>c] w.8aHi1›5GtTAoOm~Fڈ$$~hÀ|J6+$)4gl?0R _5jdD#Y4KQ(Ű:M{CY<)Łϻ #'!S~d4#=aƘeɯK)x+3S0~u3pF׺˕PaJ;p]=q65ߌ2s⫦G/GbchuVѱ&GBFv܆c7#>Fr'ncBTV$=e;l}1 |-JK\U)Lʆ\A6`nf>ca$HW}zVP,Ƚji3+7\L YDly9_Ѻt諆f1p#IٸuBX4P,`a΍FVX [ q`G6WKPg0=lhNO:Yo”fe+Q*g"ɵBj1{ 4-~^eWX*1p'A+|[x[sK,w%Tm=lDѧ"JG[k{_E\N]3$bQtȰXbˆ7եIv =ZECYIK\ /mZ%6󥲶OZ3 О1V-E6)eW 0(i5.(E?|0yA)#{@iZK_QOuAP-Qe:g #rZ;ldmsmXɵ IDAT,#߅`Nxh,L'N?$Nh%2k Pи@K.x$g.M6\-Wp*6Ur#y H/(Y%O\ɕ;2KOv@C+ 󌅴4h4h$_K ܬN[4$h3f'>r X뺒-o}-BE:u}ܗ/$t$ozz  | )")(V|c"!-`K%gg0" %j{ "57y,zRJwֆ5d +Ew80s6Pg:Zѻ%mZm)}t?i޽4YǺ1!?R6r{2–@vȗ.Xas/RqFt]"cC%Za =nk-|C? SWS1-ڄn;scNaԞl?(| ev?r9m VvRPZRvjia7 ;WH_Av-zh}\a]8 @I6l`Z Q8}=OYՙ_k25h׼OP0D |?/Yw_ pW@xm:j'nW :2oqF\Z}>llog]?_h/-\2YV,*!xQR@R ѹdaο)cZ0 %{TssRdRMgKOHyOg|4\,A-[Npܴ݅6 d,tpʰtkI`_HBY۱I솂 B&6dÊ|K1Ւ1B.P /etG&MkLo."޷oҖ3IJOVI+%# HLpECl%jF$X6zO>6:2/)T4?n-={q/Q]902}W4 R®?D(!X"^R1֝qv{_6'*[ò] lϡʖ\%/gR@I ^f`ߝm\!|_|A_̲Mdci%!Ý9DMb+x1|d$:t&eK#Z}T6mɎ 9TRr>5|z%RUYҗI»$،3K)գ̔j> iI9h$ kSqt{$GcfmHMMi,KlA'=tGIvG89Ȝ^= HV$VdV$aA t]ɉWWH'3݉ 8ߪVTZKa ˻EA[4<;aI:I׈9qSO%mp'>CO`13JlZyX{"7lh >뙡?{{ջ:L3bhcؙ5JJ26d⎋nFAU Ik)Ѽ[ڤtjZ2jXt*?$21%Xٝ !sY^MU(՗gZЧߨ\̢2,YݙH00Dzb9qݤ7օaj(, B~yۉ(5NEe~B^>n,JpIQrôVmpb3RאL.=W߻*ฺ.C=L6CF\޾ PdkIJL/1d;a)FS}HB7J&SY[:өd[!yIcJh&N]d*\y YFE aj_Y!56u `nSOr"M);v Rhq?{c0kz^lخGZ[|l&شuw=nCC蔗B&X$RJKꉵЮc&HdRQϬdX6Ӑ޾g,;0ITG@|2^0}~P aG.Cʨ$5l û8ߓttʨ #i5N"((:؝Ɯ̽IuMdA3%MٛٯJ@g!^٣h1@G73+uSszf]_>0(沨c.Ebd 2Tl+9rLQ@>:-SuS>b?}_ԩv i("87%hKR==H۹HBc3; e6xfQ/$xzfĊ8rOdUowSnS{R;@ѱ{.2* j/;XlM֩'hFGp>ŕ|WMtAr8nfGZ<Ña/>X:sCbT" J4v$U38Ύ3NDyc1 [Ph$ ayT7v$|039^)3=xZǐ`_:W FRh%.pW~˅FS_o'mE: !)m_A ی1ƪIhrQC3wa :s$P*@_n+YjCM)E}J!bVջo=ZBÁJO%P(6ξMF"kOTnsfe&?P d" tp^RH!/Xu'vlmٽ V',=n-һъx#:uzJ'V+)YIHuE:/|*>Щl eTFph~Jdۖ4[Bu7sۙ8]ҧZyk ף]3{I"}r5w5Z[a޾pm4|Xhj%|ϸ#ק/ҭp{[nh793?AMR;wܟO}@)E|Nu4ڵH/ӈCoL85FV<{#ҩlߞRNFF#HO^t*^lϟ0(էNa{zyzP0dUT@)Ne!X̽#׻,(|zS}EOqdrgy|2)mûJXS lzcg5UH.woŲ}ܛviD_;NcAcV܁$$_q£F! e k9}46 k~r;3i88;|[~_ `Sxlleˁ5f$ewC9gPɎFޠ4M1rһCa]!FTA ]hU~)8$9 de\nBWw2nQ*?=Fxb|MS?Ê3r;a[TcoA2Luպ<7S.K[TƉ?kh GYl KALne*w-m22*O1\~N@"(U";iնZqKXI:z6(?Na'3ʏx~(mSy U6g @|()a)ZΠܴXhH=Hȥhv4m xH'ђm[8|v}.WCvHa~;eLPR -wR7XcPApH:^xn8)iqR|ghAJuhSlYlJy{pYPh b3c_.aaPu1ჴH01#q>Pja5%\ m8 JD'd&^lAӳNf6_/)gPl쾹Q# Mg< L∦v"Ƀ&[_(zyb狭C(zC݋TmG:Ng弰0_jT73Yd|tǖZˑs ,Kk2?HJ 0xssbF,V1x˱AZ:[#0M&`uRG#8N'*J U6IWT'1m .mC*TnF}2 JU, ƃ9|f(M ( Kڊ%$a|\( |aXWXxp0zI>ʭn[abdx6s]cWtaP ;B+U22U/+ 8k5.wlBHAm KBhr |bSwXFgT_}[jep1؅` ²prfn6ۙa-m 4>I,,w"L;[&6{ @=m <.gΑ=I%ѧmV!s7MChۡ\;{J"GJPu8u܎xU|# T]4;F?_碕 za_X˝PZCZ7 Әff^,tWGLq F*5_cxor3q,}Na h{Ö1V1|9rMf^2m .;;I;aE(XWpZZLK֥uj[ZnZrP((;Ȓ9L&! y<̝{{sg4~{@xjs& 'C}  BwA Ghie mnֿ]m1KF { zVF&wDX,)j\eF+M_ ̍mcdu]hBp%VlEL ==9ԨucaR8D8!ZSAeap:,w:rO <8XQZDRQ#rݸ~_ e1lynHǤat2, |sߜ.)(DXn `K khFamE VKC0Wo"v@y;l@)ǶM=[!y9RolU'IV3~ߦz2h7S{$JFG 4-K7ꔼ]4P*JBw<d1,P`\t`~ft;\Gc9Z9<!•6YՎc9LMO1]Dz6f{nچ}ߛš^CvT<1D6]k"\ T(,9VT;;qn/VtZ6Ղc+?kQkv*vg$8LG0l a>5 ^;@_+U+gsL1eώ~ id 0pG==\8 |Q)c<+(PBc? t`v6v>N*dd 2U<,wU`5-@F5_n %;!䈯7bxh!J#&}q21cpxZXXF's)9\B FψusZX.ϞMA&;VJ9t, RUMg3Dkk$|L*H>bwMcRuQUywXb?:dDN|ԞUv40W"o>**x268E6WJ4i,E2'[pM ? I;sG9Z?o9I¼յ+D"CGN32yX4[Sy[&ґUY6˄9-1 ?XGo`G-<z:WEngƜ9 yiRC:үt -ҋV@)…?(v(S9z+u(EZ 0*Xi<'s<*s@X<آhlLm~:)6}L (a9u1=@iJ<[8+Oya}XY/\,+i{eL54r$EEzӀCX:@E廐e}ps3٪>*M QGVj|7qwm&9 =~n!\lEr P<7ӸKaDӘIcaY;t &h,G9?g3+Sv9(#v0f 8D>*vHml PjHD% f IDAT H ڡIn\o [:9ǰ9Lãd|!!M.sS2 ښKFbt r;pjX[*dsxٞtLv jt6!l0[C?P!Y8Ա 'g׸-3@y #ΡBanwXJLw1:Kc}hlaE̥uN%݉ [ʝ*ڸ8q ~gV :ph@ [7Wm j`zxVXo+Ws!'@g |em 0εSk\=kDFinztm L7 s,52U_ACP)-̊S^bŰͅMj-7`|"=?Sd/6 -C[d N?%^<5xz J xt@i*Cus0]0\ H~,Xg$ai8L_`w[I7vtDL=7q&(S<|^I-a˵Y!)9Xt{R`[s!cZ0gkƌ3oҩ-e;oQnyP=Q*kX= q#/ÎysEn'#!]9Brt'Z8;R['+&nK?)Hx\ZM9@$b( (&nsga ]M 8tưL0H7.7e|cjq t`AFh4}%1 qr *)oq4-Mcu@nVG|BH}08|Lto ZK¸ql ? Rթo+"Txi* J2`ژ<8։uj%e`r @cd0EW(9kt  ApV:3 a>~Q$c?j2N6,H]ҚfaXC~Xbck t W%M ,0fz*;8ښv>`Rkyˆ"/0"s8CVb98R'a%{:%3 WSM߲ʘ_uy}?K)&8Rk])"\H'zNH%頁ikDuq?`05|UUe@G,Xw [r +X)#~y>@eNl@魗C^dljJo-d͊xv 7WIAӎGRpQ{_TFu;Y?!@XoGؗT^{T!ӑ;d߷X|ۄc3(@R5ᙌS]Ԋ񳑯ҁoq-e}˔aB;2ٳ)B ;he(qj>Q̫Jo9%Mv6L<;x`$,ve4ueae=W"mVY<-:7/OSå'ff8d8 xU͆MS@g A((q ypR5Lʭ`h K.XN¡a,.(G,q`@$DbySΤJʇE Og^4njhbYŔSmvBԮEWp it*k ˟ >4C;5\Ҥx[H DOC:ݨ#H] Haҩӳn(_HPɤw9RrucZ,hQ<#supH t1TaK\BZjܗ=k.õυ:H@G,3˙ ~; V퐲\Ee\5:ph|TX*Ӆl4lpx [; rVqC؞ 8qL6ܺy|#l M5u}o̢6F ZSãhf#=.GɈ@i_jwފHxc9WXh'"] StN55ox)Ntkfm~ +ӟ)oE4D/@Txw G/idFl"yFfh~6"] ;y2*`& 2ߎD%2}YOqoM k,t^ƽvMBmưfUϮUa'I-N aw#]5VbuBg2pME*}+k*E֣F/ØIĈՑwn^!ӰAD@H=/sGGV#ˁ}wP&Jva*]SY7<@6REǟ7Z{œsmZ"+#G=%xn>rOƀDhKE;2?qA};-ɺi o#]tYY@iw)cQ5@(u&6x EӉbdz(wkЇH)).d\F#CyZ zq} Q)nHdS'*_"[<@&Di\??v7F;?zg:BOpI_&35U$ה m27Xjlm\|`1e+{5 ,t\'jzoBf]AB.:H(Iw!va>Fc0"Y`uj}0-?Kj!?B:.{] ."2HJqt> 7e]HYS>? _S~vҟטkD\`](mc׭XZqb۶X՘C"Gc$pZ iLY?[#HڮugB QsfvΉYH.|$)Y+2K$w|"1TEXӫ`gbۅeQ}Q盅qpswvM)Pt>'\S ~I_=>v(5B258gR#vJBln(K\R5RY4M׺+ұV bFf[X{l]ivc1"3D=VIdh[! 0$o0Sr=)k>gZ5t B7em9%򟥛cI pK\> ~->c!w?O4Ú\ackåc5 _s^SA\U)0|czA[5oQ@߷VJu) ۅpl-t @9%26>_cTiGOpW8Y(0Ad\]kJ4{u8,.͂iPKj KizWHKoOX[x(k;.ѩYׯ.]5VD߃?67͟L4Ffy]Jv#;qxrczߥqkNi0rᘠ&5Y/,k:^G 5 i%d5?B؏:הG4T}͂ }4G0Hz Fܫ*S#l$v|k׾^7SMo#Pu)hxh o"FoL1w.\xsF@W0S([y-qADgD`7\k]f`9PLJ :IZhf^A:vs[h{@F'dY4dKuҢ=0Rh6Djo {! =s3 6inp.pnھM4Xp1Al쯇=Q'ܕ1]PtSǘzbщsNl-vI]Ȇ:xyi$:C4o?Qhk /¸%r q7-«FWlu lfst58χCN;4ٙ"IqMy`dSN@zllp^ #5@R@Q -u=WkfM8?aZ\/@=GY*\o˓=8R^U{^wWk&)kJX:r;5\w*xHd>ɗ_7Ȧ8Y)Ƣ瑄(mn1\s: ? l Ÿ\q%F1K|ŢwHݾi ¬w|-V_4Rj:6.s@ujj BPƃ o T6I&X!  (:W91P( ~KS?y)VOU׺,Wz:R9rAI^+Q1$&< Gb,$k[]MqWíGd O%JjΑ 6R9'=l5ќf[m e7oJnKj[7CNn-[o pp bv!c6 I~3]8WAօʬ{-HD~e~IƶD-H<lp! jɝO\zӅ%p8llV+V7J/n)V$ :c}/n6k{s&~)F#-\xu3”,'Q <`WL찣4w|J5#"tT~iFsKyʈQi FD Su9^F?Pz3a$U'P-,. 4psa#܁~>lr D0޵+/-j3q6iy"EA妼4PH+ <%AЅI lLeg ob`6xܷbyksJٱ=Ω i#ﱭ?xlwfo+SG=G Qʂ vXןy:XQD 5|fcuIG`xXjO/mNl8a<W(k? *:B.`sԮ*?< G#Muh:ֲylxӅ%F6siLWHbDpPZ#LqG JHx¦_$>7;| 1`eS0dM(w2wN}SCnփ4Bfst'A7Ο,!_Ӌmst}yRlJcab.ZM e*-N}l~Ъ^>,a{zw+6] w‚N@o᳈rMMiͺE`8YP| gU[1de jEoUo86jz(36O !^~^F| 4J &># sNDʽ<185YB_R NA*sM4g\cZ㣧ŕ@at_oj+K覜tovՉ1p2-OVwz/}_7 v$ Ybdl VRso"㘄lZORtX@Wnc="d"|@?Pa'ή0."3Uq@i"ra+,7ǧE6%"ښ6z&BɺHNEc 6g*;7~&;!< %JQMFI7,`D tE\0mnBZ' l aM27i`+!'VAkPz'IU_W];Yq `4@WHu-0ޅ!aiJ=qV /{/auAҍ;}?\E~ɖW&Aiu @ZM|V+yvp̸ta /^ burvoɓh0 (uZki @Ix#U _EunJa-|s*G{g^ xl~=*VoD#ېqz`41&r4M7P]r[8Gѭ׃o4H$ Ldhx'xhّ6.ɱ@N; [WH,hdO#p~;[dbL~^P"Qt$J/kDHڭLLdZE9hO1Rt*}KnB7/T)N\e31*Q3L}7l_QXN$ ÙFMk6[=qD/;Ɇ]A ໢;9< Vr砳 z_F$%e1JZ0Da7VSu[} 2R#TGxT H ˠ>9 2< V&ھoeRG}½ɯI2iɿr 08/^lh 獊H.m#ʠC M1/X"ƒe8< 2?%8_``5rV E!aypvo _UB2tokGsوRH.w?+`_A)d'.X-qW2/ΧD\XeFjO! -.(퀷[ nVHw]Ft8]<(0Hͯ:p4zzɍvO>dY /ݧ 'jk$JgA jNJ#7ag!K8iX%UT9hD(XxF4|&;v0+Ti|O`M27n(Np\D/$.掂*rW^`vܣ֭Csu_#@q+KVvE5Lb Wuv6ݨ[xF9+\g |ef$VPHyl`%]aZw>b2VH3O@ YLD-Rd ^/0Su]R\f&֚CGщ?rZ@qD~ 5ۦ`qT${r̬Tߩz*V3xɁ1h$ʦF܁R> f&JGoK,$?]ӽ R:~vG 64[& aJ5 #9q,HaFũPkX*IFQ47s N~F".4PH7wԣ p awiwR/eM3ZWXJS 8riQ)Zb$FVA)<i긑} Hjg _*8 ƒm`1Viɓ xoqF XTx V6}A8B*`,_6\uoɢ7 ZWR@RK܃+%GRk14zL=  рT1%:Ҋl:#> \+cd[mIO9 3졠8=[Ea/Rt.#+I'^.GݑNJb5}Pe6jdF P h:a<=Oˤm% %Tc~V).Q+[EM^\{ma|W;4sx7ZP,]z7*3!H o"=6`k}u~gfRY7$p`Rd (Q׬4 7HEl?_MMI0kz0A=0*e g#Ul# R7f :B8Iob#rw߀27xt[yQur]Wi ,C{t~i'ݷUkY?^<}tLzػ~z$v%I %9y_)ˠMċjhPZ0%"*6*4 tY Խ!6RZp/$HA7х\jP|A]ߨ&&u:#' ̴u`x?$u)&iaNk y2F"4GR- E9eS u\OÍH*rZ&~+:S]1R?ttQ&(;x@0“>,];^ˊHMfc;|kk;W j79P ( ^h c)5@a^́Zm 6!f= Óƃ6>'6gԊu=J= t+MX䳁~f|l:f6N+JN˅w#MiDp@X|>_[́kdχÏu|LV)\^=2.Qݖe .!)]THN /8+w ?X́5E^föOd*4RLO|5ǎd5ND$Sj9ua|`}iHVE;`Ҋ)j @)f- fr̛ VO60MTz<-MAX_a)^*ypR7{ݔaܜ׀i8HH O <-T{soAE)/˜ur9$Un0H4J=N[}|H㸡Mt]J-]yM3PH6aʷ ];7cޞe&maW"-DuI6)Z4=:h%[\6OvZ䑺0x1R|F4QvU|@n(K2#,S̢5Sakȿ4ysJ~1=ir&ㅷ&`pΠcN#kZEfg*0geHI%6TG(@5^N&:·WvKqsJo)҉uܞ5`e%rLʞ5C2gET9f) ;fMbX~ w(e-kY#L,Híx7eRY\ <9{e-kYZֲJaMRO2Fs6daֲe-kYV@3F) e-kY&X]Rz]Y k5?u3 #cV#ed?}1^8$WTde- [.ne5] i2.P xyIP AKQ;qs-JPN(s*Ye{}3u-nC$^B*; Y=^̩Ov+e/ߍ'j#t h ۲gQɈL_j00B&}nlat]7#A?,Pj&fd\df_04<8|gRfYG(;ʡ|+t`F)LJKp:A!!կCIn:=ΎGfQ@2q믿M^m,_QБs\l`L6Pk{25)]; 2j(巺܎[]A^9<~n#\5/wd.u8?_ie*\o(Rd(_%?8K_Pp;;!)b-:`[}d-s+I!`0ph=RfvaF ؾRUq6Φ |j>f*ֶ&ƃ%Xp% ,h@n@){ 3ڢ|5p){S BERQ.>f 4OxI97 ʫ:68%{|q%?!dhsвz6#;}J~ʎ,[5UA"a.౥%K#=Ru8. '!!'Be V i~7G(;pS`e1d5|;ف|n5;yw,PlH^=03c @i;{]ǽ&p A0Kns?znbNi83 f G{Yto6y-Ѿst }@T8ln+0w( IDAT Gy8=<8 @}V,vCH= nm_#|a^MyYK&,*Zi(]K-</Rݯ9Ebogvio$ 11@m,h?knrn? T.p"i~˓w9[}N6Z('%S0O\| y ,)x$D;B{~_HQ}}Tpo]$<W4|WLtakdVڭwP[;p:Dvq]hc! ۣ<~$޻@: _$#~^4sZy$1Qoo<>{]wcwGc~31"( lNakOiIњ3N4n]p[FI+>=VŒa}߷uxҽQ{WZ,W7JK]L<pv=p4.U]ERX렋Z@khptt8~9 ߁v "#P iiyut4&{P6pѻ`IMӤd a)Oٷ(Pauzћ L߻ˌGD ,P;NzGHT@P[J7pgX0F1ĒMLw;*E b&,9wm,[`뵯)w}y>s>&8Ͱ^}?CCת#}ߏ)/' k:}4N.NE9,~1 (] =2%iwbڙG5ʩ9гkk2LOu~y>{n(JzkzNCX}zT9a%AeCF%c辏act9yszt]"Khc R􍂦&}WHSƇ5d͋ؾ=c BS*ڇA?ALJ$T7x5'KbڌDHڮ2Wpv>dXc9Z2_AݛM2qt:s}CbOҞf%ӘWgTc<h ܛ+T仌=sQx+,.@a[K)\Ik`o_0mb -O! _߀zKoAΠg5",-2D@GߣVwMKЪE+h5Ziѐ-kdX}Lf!UɱExo)]3?9ޘIxW]BxE up]-Э= 1YǨù?zԂqqKtIk6v-m X`~;,pը`ki (7Ԫ 2I7Z.2Bj;ˬKld#Rntp qkkn1xx`ĕ|W +jseHM3Ë [3@I!gjDŲCH"UB4\sM׿u J;#D,1p'~ka\8.thΧcNGxn7=Os& B,1mTT mUқ:2WSO"x0B*J6;OD9n]-jZ_axB*n,yzXT}vD|{\7Pj(Exu(=(E0a&Iƍ9uN 'U֭8AM}ZNkqãOٍzD"L#–ձ!əZJFRQ62wP͡Jn 5X cr4pRj4]^yHO0 <_5 : CsPܬ)C kܬg`M{. Zp۳ !TI4҅ ?C=4i4j sUgDS Ch3OFAOy7U7+ra DV;-c[d[ʑ[M?Bg-9s"-*z.=q> @ 秅FQ1':-Qe`M*8ͬ0 d=p &LG0tNWz駑T2o֘ut"lV£# zvt, ݡeH?iPٓ qV ;,"|Lkʖ!;(ϭCّ5n. DsPd4A.#ICA8LǑ^>ӶXx:"j{OP%ߡ"`#n#ps> VU^f=KR [ca ~ Z!w45#}TnϑJu7i;h?J[0VIEcF:hB s-i2E-SnZ9[lg&lp57_CizpXaMÛ9Sy.:κL?NH, :YYNN gJoؔ(wOdư~rX$vgCк qy&i@ψ3 _\¬XQ, yW {%|[o ا>5HVN*ahѠB @w{ YA}_VcLTچ <&!3#ZG.!9B.Ju egӑ=$( ?Qv@&tjag5yB\1oI8en^ ;EiQX8y(rӬLU`3tw,Z2.$} m 0YL*q0 Xn2FA^1;ؼ[[}V- p^/B'- tl B9Ɔk{=_`0U9H5 3 EEC A٣s5՟xZc48 40]Qlڮ$ZC A88ZzB~t @Q@'# EWIp\dh1NN߾`sXyC.kK'Mi}Sm:= S*}P#QAFGo\_;`$j[wJJH-4#`w4Ky@d3fa9Y':`%psҨG$V tҊ2S]uq諻MDZNFM@Tw[\U] |Oю8U@dI.@k2J-~LQ Gpab޳!n_YafTT[6~(GciܤѴ)`Էk &`鳣{|j#|;ʐ9-1.MWX~paԬuh|2 +baess7:DhKG ޸1&?/wܧko=2w|,9K%6K[%hB5<(Lc%`౜{z;+^8l:~o6{܃ͅ_Dҋ}rqD(~)`9$F sR4R1ՊoE≯l<ʔ=۾dx|)&Έ^Jma*~4Y&~'N`Nz˄_S/9PM(8aH'c(7PY[Ңxe bnJҨc.'fuFSS^['cl]Z-GiM+zcRO70t9OsS&*))QhC^z,Z˨raX7Js)KG&)m$R[_7Jm#4|4J<+X_JTr>[/6zS'ihJfc.V-"<}%ZԕM:B{Bk/g +z)qlr\>ɼgyGR&3DwuʄtEr=U!AfYX)[oiau_eux1w88G1ҹr1|jG@Jޞ.ItNH7mXb(23Op oX03W`B Ymr Z ;[Co#b.+DRu9zF !n7Rj <fFaVTH/#G1"h-,F$W8ٳπFi_?O+zXݾ_RM)>xJ:YBΖb:n8 Ņp|G)g&Xmƫ3\`LJ@!ʏ [QˇGBoGEJGRI0pߕ<"W+_ApM\Ȥ[o?&=h8 &&|_[[z,n>Di3z-V5*faS+y+]I#x$'[hH 5R)I+ts׶#Nw[nΝ&HelhXCkl8341/k,0p h{Z%SXYxK IDATA"S _5Vy= \X0Z*C喒Nfaܸo*X=GV*Ӣ50[I7  cF^|>kKmG lY(:%; qdj~D'-FARXw4 d ž RJ psqU*8ؤMAr_UXJ#ho$E+%_uok6J+kt "0=Sv .#0Xt&X4DpA5h;(0R{Wix}3dCXٳeK̶w% 0%VZ&X'6Y7?{ǗLc^líXD`ne+ ݉$5,mj[ʟ'&X.2Ix?UeCte=r0V_ \meXIw$'idt ڠy7 )9V+3aӁ1# ߕ>!{WA]xp;u/[Á9IaFAANT~`+iJY,_OkL? .R4PJBPtlDW5KٻF6Ye \/Vpg\F"4 cҥFࡸ FsK:6 债|uG!t=ڨa'#2bѐiv9h[!U1^πyT7#'Ks ~,Wf$rX"<2u`?aá^YNrs, ,GҬbJ"3(j=6rc˦/57m|Ghg'}-FdS?ok%) w |귨P_pN r!4ʒa?^V9k4wɷi8՘ @5ElzJY3?b1 ل6o"ˆhvf%9,rUXuɺ&!$)Ni*O(P)ڏCXJ5. _A(JLDgY a)Q#6rjIç1o0=ʤOَHݍ>"Qyӳ11UT'X֓4+d;1F6ѩ姰,wjI#Lu;QB?ޫo"?Lo $HiŚËaK\Xp[~Oۣbb곢‚p]ܶj՛T&r\\! 9l@i-M5RQ_2 cں%=([ 4G+ N1"*~L_|1ajҢ9hW1N =򒟜#L15|(9VQV[,NMp&!d[7ݘ35I]5ݍYj\˴-, dUHfX~o5j9S+{v@pRnWkO1=_v78Ā뗆PPJ2JO*w qӬ4j&C=M]^B=wԆ'V}Jd02T\Da+ЪEŹfWVW"AHo(FFHuOJHT}Fؿ)U*n=׃FP}rlqi"e#3.ɑ ?G#^t C؀aÓMVfCl(]{aAVrR V~{Oؼ>Zk"D-ػrvDRO!U=rIۑq6a@"Tm,GT eJ9S%@oBR#AT/rztFruAd}FEYlK:G º';𝴗\Tk5A %{2kF]PeHdfV' ˰PyZ{/YǾK:u8ƍ BtQwA-4J5v-"|E[wRw!C Ö˳b{}k/D#roH1pJ6:>9 |wZg mU* |מ 8ðgk8m ^Fls(JA qݐjbF *y 7K`lMhIX3 ge` {PKj@V")qI`B#ճ%MLۼ!\_k'xUr[HƠ 6A]SC/w **3lYdk?έw>bHaF {>s~ Bnr'2.m^P_T@N $ 6,9m-iOI@*/d[uI͍i)*iq0`4@ J(%yKR{Ji4jI㦅H4i2PJeRm:{$B&NCJʜ`e)6"թO*>z 37 j(%mx sw0(#|t(՝yJ#L?6A8p)o lwJu7[)Xkm6Y߁R]o"Dkt@|RꅒWR@gu%\=>(-%m@$}}Ӑʱ~{Ѐ4zAzy׶:]^{6ӷ[Ju~|1ProK<`gFa=oem8Jo?&=wR:n+p"ªYGXf-7T{q'r^ )8ѝI%M*3I0mo"E 0Z I Ѿ4aԫ {+mc X VPCDIKCI8iApFU_ѱm˭F$m(gktc^YET|_)pܟ:U|;^I1 Vu*-xD"1ie:Y_GGWh~`Ro=ϖS*OzWpRj(5=bG}Zԛy}z'Ql_=[ FʿuH {f 5/ OVSo:|$,$9Ñ V`cv*k#RٷF4i6'"bnDZ8_:quI!c6_5FYǤ#\ipLJ8…]8utc2Y/EqvWGMv V-FQa.KE]YPZ#bGBUq>5RMפ w¶dt p-U zC!C6y42{_pۀcH A=b0ʅ9a@:ν#@ifkeE߭0Tf] Dڏ^V8)t8 XmpCRVd+뛅I/B\o:к&ObRGr؅sc)@J,~ŭؚɌ9"1.+'~Z !ƪC]:"+w ,Sb0ld̅o\(lD,\_+@i,̀HqCu 3\x].yNc GJuW(mt,*cvNH8rd]Ł] ʶ%[3֍n>|[m鈰w909*ۀpu^mv{khudo湩ۘ׉n&;~^]|#mPK&%#7XluzBKWY O2D}p0y'0‡Cq[ 6Qłu Nvkll;K l c( 6\yr$k$2Tܨ* FQvR1*8>~fOHYcH avȧ:[}spxi1$!n\=dWRhȈsa? ˕ʽ/-ܱM޳8@ /sLOU >PTG6f|R6d%F_RSo( tcpfO@3'yrAlL|0h9Qoo]8#^d-|b 0 4k̈n/nGg48.Wcrz*M 9d>IuM"`b>}1XOx!k*T Az]ANRpɳ$ 1 )2N>"_Td(,\`=7uB(@  m^J,^降P($|)5{o"בf*xzF>BHiE_2VY+܄_hM8Xz"q&Gf-Hr'[ZđH2%It#|"F"T`~Rv Ga|^cu< &++]΋鐱4Q].N2,F['`Ľ_U_ ]xt8=}2?eۉp@ݒZH51 WNFJ`L@\)SEOHq32#};»l c0XעbtZq.r{XkϹΈ%mwgWIqw)ԴɺlxVXvB4)_Gg0>" lT9;I~ÑUB]OAAzek@ɓO_ ~^zH2%Cj[U0.&tTV:#O/(f/<3i ;D&ށFp'kj5kyvHTp >ڴP gJES3)# $?*x[/$Nη2ǚ+Q'#::̈́ &uPC$)g5:ȏ =Qm4<(UZFH}S0z̻Ѻu'٭5vGQq48ogq; ^pC&]X\h ``3u6=XSﻒ>:Jԁ7YCA?e3Z HݒEV")p@ۥ6T#]Xv!{HEB]c$=.00'G\bU |8l6.50)"-|`8@pSMN6=[jdcx-"iFzյ5:̓q=x9}) 6y{_l$l-$M YW 7 mSV+5p ࿺q >$Y h"؟V+Y~.SͯLX|E4B9,d3_TIhj?, 83+ ?Ba. ~؜'2KH6bU|.nMM|r-M75FgFz  9o5!@o:ua$_ 4X2p^FMz Ҕe`#`a`O`C^& j]d ~>P% WXxSStxe(g#:J;&h 0Hw2\wX97͍HԵ]of-Q̵|dmِʳ <^[+R/,I?*6zkj{Ɂw5HN5rO8Os6Hw6"hWo_/8?_"y=CTI?wdIފBvT fZhh ':kjnT$6.ۃ1@٧u9&1*|"zh,/)eőad3šO4mt=p+`v?:zOA*ٲΧKf(3C ́a1cZmdMfjd$ `i`q$8!@es>pšUpN_x5h﨣L7k,]fs7%P '/VF+@iZR{oKR*}ڑu0t8+$Cq. }m_J*\^YFi <>ϕ ,mφ9s{/=Qz_ xet u`{~1lq #pIxc&#;߾V6Mkl52h DO'FIS@XY ? M #N4#\?_^,]ʃg٧V IDAT>0;&OYǟ7eE>rJ =oHJ M^{h$(4TsT9(M'ZG%26w"htWp|I,Š$غM"&P>FBXe@,e,4ZqՎ:8ܧa1q,Z+X/kXMs-t5;ZFx897MTv·`Ίuon{nM<4FT(Sa/K8,Ca#WηuM %W KeZ잣2}XoF 1[`!%2>RuV=[0{^n)tg_G}V+cJ\+` Bm\% )h*SILϩiL{^!H⁆ޠJ47_8&"㬤?~ΙmaVGG4kJZ/"< ws|{v=zp+)zMn+ɂc`F5`bɽj@RNEJ;H1go:&G4JQhezVHjs2dS4P2 d CS${x/, u[g츔+}F_ɢ|+4͖e@ii&W=a#O6/@ xӓX4‘1x̊cx#;#B|8=9"CU^S EdqݪQ'IB0jHw׷o%#ky+Lˀ-f",R< . 3s0;^ɷu6ʯh\qΧez4%F69cgF /xBaIY ɂ e4TFVt{F!= 9oQZp^;աG+{^"LI_V r 9gZ?+s}!IXI vpZ8BC|~ Jy@a/Cֲ_zuսL쉛ost $tOyyfo,4PY&~O5ҿS0=`9)-Edw&!=5+7핀V_MiBO/O\aB/E(ô0I(Q=:؃:R:N#0g\XWh:իYICzjϥ҅5FM7KlDS^?GGUtƖՎkz'&: 8Vؠ  `g v`+= /-`q|C<(4򚋀uY? U#Ֆ?6b#~ 483D80/h:=rNN[ZlR}jVǒ~(~< ^1|>8Zª,R}P_RA\`+҇cM5Z{X^X6i?ۖJ5ka]N YMtD|6"X(5|BI ( 2H8ԞȯZbwjeܓ.3pM5ɛFE]x–Dth8FDwm> h%Qʈjx2h^ wZxT0d/'rWHE̚x 9|fGE tMLѧQg*SzC؝'d'ha>J6ց>9$mGESN+o=?O$R*3 30cuz_oIUnn h\W]${_]]eFZ/J ך#%÷+`[,z:[Z=_#m3O4Χ Zg h7OiG+PˑPSREZ+ɩ}6)Muh <܃"5%$J1MEX2e;R .됛C) ;3&D3 U2;ެVc3eGm|'7&F}MF9i &GFR~nDKKIjasZoFhoT6]Dp5" ^ ߟ C|Q$U郿DAu+m7nBWQ;`~w\M ǁh΍RHi0*)`|Vo02D Ae6h}^ceh>YnԺPl/J\e<}=".mEL,eSE^ 6P[(oFA 'hS6|m(-r:oB o_‚𤑹XҷʾazmI%zti]W4*3Eq4lSN$=[zZ0g[or\IZ~BZY>}5@ioYm1JiPo#Q,>9/ybJuS PZ@)ެP=OژL,ʜ|1-}5[6Zk`IdL[(ժՖF)mU4ulZXk6 (Fi2[mt68kh HYer;K@n6kJ;aQS kAF-mRՐVjޯ})Hek)9R?* :< ܲ' =gYt{ {DAg}\a8̿\ݪNjnt zlDU, 6aV~i{i[$[]]@0w+a=RǬ9齻F-#E$5!ϫhJdjn?rubFe@tH/-?z IzsK0"#Qڮul < @'~߾˪H‰8Z t!^*'- jnF8{z5zIU禯)X E}Ԓ %'"SͯMz&|T@Ru9ӈut{{iI58xD@)miۗ?k]'=n}ba@{OdE@u iKoq(2s]moϡ12Z)VHUuRoN[= kƢ}4aX) z !iŁ(\Q7/ ϐ.W29m6$Fغ ;;]VV>{p| :!vbw8Ty]Rh?-xQZ-5\;DRUNAZQq'W[x."ƯOېyo㣬ξ= K6qCB)".@& j[k}kv.UjmcǷ.@V٬q d?Ι̒$@O`r=g:skk*!g`w%gwoX[qk %ePR68Jh&o1}*݇ic^De˙0c7ۅNv_9I4lw\u3Y4ckaߝ5S6`i@lK*b=6]h x._Acf8D(jq#|3E~mtm"Ba#mls %J2d'r;> yeN/r_Ԫ[HnprݻxoP)z˃L5w_ Hqbr4.c|[ѾĸwbEA3qCsg^`J3g>q/G6J&|cVclֈP&e] nL(Ԏ̜Ң&KDbL>;'v5e[ⶻT(P~r(h\q8-$1 eQwV38iRVD9oD 2'wP:+O q rc<{~/1V(mõ1Pn\ {f8Biآ /?% _8BG}g;X ϰb$-B}k?Fи$afq܄#DNjj E^_1grҳD:ݸTI^)3#&ۤ ZFVaG(/+Lo, ['@YSUQbJn6:4F mB)Q 13 6lm <z[Kwi=>>sV'/b$6M BnR˵Jۚv-#8 BiDzbA<ܢ0 .y GXϺv~J&G)||;nYoa[n@$Gɷ޼53=x+\6:d?| -J-n2dÇm+V0G5wn,?\D.o];eLRϘHaX;Ǵ|fyw'GCmtz~nF#|e~!Ej*n^C'19ԺN# 0: q%6bˎ 3.^=B'{MU)`xRE|dlcʢmݣ@ ꃺ=^ٌ" ipGIWBl#BFfF3wPl!?YdanNt'akOX(6r~O䈣qO(e@.HGɇWNp (/ |M#ީCmy LVf55ٜԉ-Ut56 8LivMmrEwwO6}IsǴ؍Db\"X ˃cl>礿gw8`D8<_$i&~Xc5Cu){Ɛ">&Sx*%?al-x l-" !RĈ]|*N(7!hӍKOͰ;f"U.s?f]T`6M obcg)©Zb'.'8ku0Tkجdɰ`36#]aB{tIB_Js1\M%VˈRSubp1c2pĹ0FX?wXbmU1,.x=V'3_ijqob~Gxox#ZU ?ҰBf8L&eR}xɭŦټpk$(UX}Ybx;YiR6VXf, n^M60^lzA1|` Á:~%挦;V#T!϶au BcᠡԮm#|]mGB֢{/Ą$ sbgs'&k1!RH2 }Q3p|r~ Q210V( [IrKͫs r5>ry03WH~1Iobqc4 NRl|W}Ag[焒\?vo LO}eN!$u y{X]ĬM[ظK:0`Jk%''\NՒ( Ky` OqyV 7jw"E\rcjxÕ 뻅y=b zm[=%+׳[ykng4CXAHn'и=klIX/<&ϸ 2j%q:>?'g"Ns{Xq*8 d|{ߵc@Id` XqW gp^߾j'߉+i\I Az*1% >gg?cOծW& xGk1 x{f ݚUYB)& ܃08vq=eNAxU!joIDAT[#{ 3MgS?o=599F-cK&k,Y |L=YIQVӃB(/;i\,rp,cm-)8^0DCCjĶu ,d] OgD<`Gh I7atlfnEq7Eҋlv1 )*L2{r|nc:`gN.Hy#:c!Sӵa"y3O;s5\b=.;`ń2+?2.-:BUalhk|'&\hz1`j%++zz%DX 6Yi!zcj~}y9&I8A_?0?K@00ABC87L{7lo >4A}{Cpylxpn^b[DcCpbc-%WpD ͣ/߯ŽwJ_p +NjRZ}no{û2e?qh) g^kvڬ08ܶ{8xsզ;6EӬhl߹>AX]b7ǃua9m{-ŰE{!c}"w76f{6%`ಝ1n6W0|+t&i 0qa#f gcm]˘F?@,rQXwB7gI1"4 [%^R(=q!!t[6a0Ϗp-X*Փ<q*{?>_]H  # ١I驺k7'Bs5~ãnq猵RtjRWkW83UL3kZ3$|i2K1>Sv8tRΌD[vcr؂o6;?f3 j57t9 2z5k5k@(6I9ړY047aY7b#LJX0 3EG6IVWH9ȟ9aUr>c6Er5UaA@Gu mX+Zwg3Ȇk.$e4Z<'dNl/QlQn;v?EaT.HU"V  ~G^+0pJT`^ iDVp 2dڎm${jˍ! %9]}d^1.Z &ԸkiGoGIڪv_9OW}N`cx:r_p[iMMS+z.mhQAPBcE-0[<8.aa?VPMJ\z %깥&SS}K?>1~1! qcNdͩJ00֣.`9v-g+UdJpq5=d\y{c(0BmPG.kE}rqDT(yBQ!MnsyXqtW9\Vd S=J 0M`Xn~iM'41<7Mb|x t"AJPһwXg Ы'7g>!('hKS fI F' kޠe\,A(H#EmݼflR.dB0)m*-#c`Z6Ҳ7,I<lkFvjKq}f:l};_^Zr,ĢֻhL\I.L.}t{nbxp{zy^<6 *백Iiu~^=ۊ3X O7Ď3y`/3S+xuy,Cx%ĿJ:~rYk;'3&irp,8aBri ]~bB&ϑr/ 1D 7x{CFXE@ b)pH bh Oa;6Q-/98Py`;w޷UJϚ,M3kk9؀״퐵veҍgEz#܁xKh<aźg #nA'Ur(y\ک{⿻:EexecJhMyۖWY. j!-)t&̆^B(J5JG$26 $Ki"k?;SNn22tΓ"^Exccjua1 ХW>k'`pw|U80-Ձ01H Zq._:ݵGl3}R`lr%7||!M)YcgPsB*Zc@uȤ_zJC=S_boW/H+?ZAQГ^d;ˁ ֧+hֽx ɖ:/4ٶs56vs$Aؒ-)Z-/G!\ BOU`X`wg604~3V?Ob㋎-+ʞv[8֕+^ɆhyM~Hz\/rqDXۙF7`7!gY7\O6{x xɚ.trϣX ӣ|}{J9wMK7k54>Ŀ_u1;ݱNE `i˒z'#C@']1ˌIj <+:y6sgt/\Ó/ d9e Zsxu԰h^j |D/w@?DlVY8D>rdB{YW9/o;%~Svp?%g0ß/Ǻiϳ&Z+y|*£ܥۛ Ð:+-G+)#~Zoߗa5mШhĶ/'ĺMzڝlQDUXgj`Ί>[8QK`+]{~u&cL+*@YmbHusq!i˘][ iD+YGx0q1,t)ːnX: %%\CD%p҅Rq95{~5'#퀷 #.k"bc)x !h`[ph)6 s?F~`61AެOO\kQPZ\ǦWp1Vܦj(XO՞ap3>yVy,p 1wOpWvLN܋׹ Nck#{.hd۾gGYB1`ۖ"Ûkg|gMi9fFAZ\VllIz wxB`4Y̳ETR[5{h<۟x+aRްf=6*-r 0ԓixLU#̍Œ[l`kd..$G0@mTs>;:7ÓUlY%)HrOa90n'rܵr xOlonZ m*lWdlZp73Ж|ZCu}KޕCsBqEuXȴ!g6g–o-l>,|xF=۞Q'v9bīkNGmȖ qe{y+pgSAYo$o:.h~\<ljZD^g\Sgχ_~bґ;}l"|78he˷c=P ǘ_[JmÊ, Vީs ǹхM*L] Z54- @_NNƤ2=g(%.า _[Z{$|+SD[?e~t/*چ4p]gU3q{kET0o [gFQ{VT(7V`2TRF`fRyHP:)sT؆(֖ds(PR=y: connectionstyle="arc3,rad=0.7" else: connectionstyle="arc3,rad=-0.7" a = ax.annotate('', (x, y), (x2,y2), #xycoords="figure fraction", textcoords="figure fraction", ha="right", va="center", size=fontsize, arrowprops=dict(arrowstyle='fancy', #~ patchB=p, shrinkA=.3, shrinkB=.3, fc=color, ec=color, connectionstyle=connectionstyle, alpha = alpha, ), bbox=dict(boxstyle="square", fc="w")) a.set_zorder(-4) # draw boxes for name in rect_pos.keys(): pos = rect_pos[name] htotal = all_h[name] attributes = classes_necessary_attributes[name]+classes_recommended_attributes[name] allrelationship = [ ] if name in one_to_many_relationship: allrelationship += one_to_many_relationship[name] if name in many_to_many_relationship: allrelationship += many_to_many_relationship[name] rect = Rectangle(pos,rect_width ,htotal, facecolor = 'w', edgecolor = 'k', linewidth = 2., ) ax.add_patch(rect) # title green pos2 = pos[0] , pos[1]+htotal - line_heigth*1.5 rect = Rectangle(pos2,rect_width ,line_heigth*1.5, facecolor = 'g', edgecolor = 'k', alpha = .5, linewidth = 2., ) ax.add_patch(rect) #relationship for r in range(2): relationship = [ ] if r==0: if name in one_to_many_relationship: relationship = one_to_many_relationship[name] color = 'c' pos2 = pos[0] , pos[1]+htotal - line_heigth*(1.5+len(relationship)) n = len(relationship) elif r==1: if name in many_to_many_relationship: relationship = many_to_many_relationship[name] color = 'm' pos2 = pos[0] , pos[1]+htotal - line_heigth*(1.5+len(relationship)+n) n = len(relationship) rect = Rectangle(pos2,rect_width ,line_heigth*n, facecolor = color, edgecolor = 'k', alpha = .5, ) ax.add_patch(rect) # necessary attr pos2 = pos[0] , pos[1]+htotal - line_heigth*(1.5+len(allrelationship)+len(classes_necessary_attributes[name])) rect = Rectangle(pos2,rect_width ,line_heigth*len(classes_necessary_attributes[name]), facecolor = 'r', edgecolor = 'k', alpha = .5, ) ax.add_patch(rect) # name if name in classes_inheriting_quantities: post= '* ' else: post = '' ax.text( pos[0]+rect_width/2. , pos[1]+htotal - line_heigth*1.5/2. , name+post, horizontalalignment = 'center', verticalalignment = 'center', fontsize = fontsize+2, fontproperties = FontProperties(weight = 'bold', ), ) #relationship for i,relat in enumerate(allrelationship): ax.text( pos[0]+left_text_shift , pos[1]+htotal - line_heigth*(i+2), relat.lower()+'s: list', horizontalalignment = 'left', verticalalignment = 'center', fontsize = fontsize, ) # attributes for i,attr in enumerate(attributes): attrname, attrtype = attr[0], attr[1] t1 = attrname if name in classes_inheriting_quantities and classes_inheriting_quantities[name] == attrname: t1 = attrname+'(object itself)' else: t1 = attrname if attrtype == pq.Quantity: if attr[2] == 0: t2 = 'Quantity scalar' else: t2 = 'Quantity %dD'%attr[2] elif attrtype == np.ndarray: t2 = "np.ndarray %dD dt='%s'"%(attr[2], attr[3].kind) elif attrtype == datetime: t2 = 'datetime' else:t2 = attrtype.__name__ t = t1+' : '+t2 ax.text( pos[0]+left_text_shift , pos[1]+htotal - line_heigth*(i+len(allrelationship)+2), t, horizontalalignment = 'left', verticalalignment = 'center', fontsize = fontsize, ) xlim, ylim = figsize ax.set_xlim(0,xlim) ax.set_ylim(0,ylim) ax.set_xticks([ ]) ax.set_yticks([ ]) fig.savefig(filename, dpi =dpi) def generate_diagram_simple(): figsize = (18, 12) rw = rect_width = 3. bf = blank_fact = 1.2 rect_pos = { 'Block' : (.5+rw*bf*0,4), 'Segment' : ( .5+rw*bf*1, .5), 'Event': ( .5+rw*bf*4, 6), 'EventArray': ( .5+rw*bf*4, 4), 'Epoch': ( .5+rw*bf*4, 2), 'EpochArray': ( .5+rw*bf*4, .2), 'RecordingChannelGroup': ( .5+rw*bf*.8, 8.5 ), 'RecordingChannel': ( .5+rw*bf*1.2, 5.5 ), 'Unit': ( .5+rw*bf*2., 9.5), 'SpikeTrain': ( .5+rw*bf*3, 9.5), 'Spike': ( .5+rw*bf*3, 7.5), 'IrregularlySampledSignal': ( .5+rw*bf*3, 4.9), 'AnalogSignal': ( .5+rw*bf*3, 2.7), 'AnalogSignalArray': ( .5+rw*bf*3, .5), } generate_diagram('simple_generated_diagram.svg', rect_pos, rect_width, figsize) generate_diagram('simple_generated_diagram.png', rect_pos, rect_width, figsize) if __name__ == '__main__': generate_diagram_simple() pyplot.show() neo-0.3.3/doc/source/images/neo_UML_French_workshop.png0000644000175000017500000052743712265516260024130 0ustar sgarciasgarcia00000000000000PNG  IHDRq`jK+1bKGD IDATxyXT?0,.0(hh`J)JFOZ4oY}]wC321MٔQy800303}^55{sf{0SWuExxxkA!B!B!B!BifkTJi7=!B!BQO\B!B!BièB!B!B0j%B!B!6q !B!B! F\B!B!BièpttlQX|""B!B!???hkkvB!B! *//ɓ'1~x|rTVVr!H0qDDDD48 &&&Wxe3k׮'akkx޽q-8!B!B!qې8|ᇰŶm0uTu_/^͛Ι3۷q)رWll,"##LzT޽HMMEjj*ϟ544ɓ'KԩSF^???ǧo~Eaa!RRR,]A{ŗ_~$lll''' 77#8!B!B!qۀWbРAnܸ?s΅gcر bDEE?b1Ν~ǴiӠmmmr/z Ř8q"՟H"AAA8u2j066f\(Bs8w6n܈hjjӦM;SgffݹuK"4YGϞ=۷o#..eee 'B!B!Dݨ'n"=='O͛!tRuiMIIZ>}BǎS 1`~~>b !55R65⸩Ґ&5///t7n@HHЩS' !B!BiMԈFt3f@TT_x{{sS :>w\8p޷.\`ԨQشiʐ{bҤI haa }-^\pwwGPPɈ+֭[i&xxxػw/\]]>N!B!BHkF6H"`ݺuHII\~mڵ K.H$B>}x=Uߏ\tΘ4i/^P\ ɓXP aĈ Ĉ#xy Dcǎ!,, ƍ#G ݺuH$„ .YW^ży >N!B!BHk0eyÇ___ZB{B!B!D'.!B!B!aԈK!B!B!m5B!B!BHFB!B!҆Q#.!B!B!aԈK!B!B!m5K\с-M;wO>KQ!B!B! 5Nyy9q ѣG!B!B!zԈKgO<%%%8rH+GF!B!BF\mmmD"byo޼ oooXZZBKK ͛7ìY`kk mmmbȐ!n;ww^NB!B!vJ /^@aa!Μ90`@~Gx{{ۗ={?<== &N2.mEE_{aĈ ʿwƎX[[#** ={lB!B!ҮPO\t`1cFy*++BNBYYN: *̛7kϑ/_F^1c //;wƥKB!B!+Q#.ٳuVo߾, ///5 wHOO <k׮ 0~x.}]3f@NNvhtMgI!B!BHGׯc سg ++ fj4ONN\,pϳmCJJJoF !B!B7F\СΝ ---P\\,5m ^ڄbqssP#xܹ`wB!B!fp b˖-AOOOjgggX[[~w?~%%%8q.^F߾}ѯ_?7_5k 77ňDTTTwxb7!B!B!5na3sssq! Į]j@__ӦMCuu5P(Ğ={gnnCCC?^j\MMM8qCPӨj*=!B!B!m5@ m۶!88z*`nnMMMcҤIr <==ƍCLL fΜ X MMMcݻuuuO? qF|;aB!B!BSd|}}j !JBaB!B!B!B!҆Q#.!B!B!aԈK!B!B!m5B!B!BHFB!B!҆Q#.!B!B!aԈK!B!B!m1Uŋ}vuUGQDB!B!_zB!B!BHFܡC|}}=2!B!BQ=K!B!B!m5B!B!BHFB!B!҆FСC-.g۶mDǎp|b1 Z6nܨ{Ľx"._:`Xn]_e>|^z5‰';???4+6___hii!%%ڵkGիWcbqCCC+W"!!A/TTT >>Æ Cii)`ooBOc_^_-((7o涏9ӧ+\η~?^~}SϤK. wIII͚^W\iڵk|?3ۇI&$ɓ'Duu\ݻ  44ѣJ"kƍ!Jegg'N ::ZVoݱc֬Y(..ƅ ş_nݺ׀ QRdffݹ}UUUrͅ@ sqF@SS6mZⓇP(P%OPJO׏B 7߿_5BQl߾vttDXX }WU'O`Ν Ry]68fcc.]x RRRxi~'gΜit>[ųgp%xaa!|||/୷Rz<׮]ڀkbb+++ٳgx1[0rHDGG{JB!q1qD@`` ƌSaٳglXXX5ڡ10RkkkA$)\9c(**;///8qXd JKKW3e 5%c->#CII ĉغu+[]]T,B^'=zW_}mS#iOΝ;` :U/NNN iV9777dggϞ=-䉏GXXmۆJ5󺻻ŋ߿b .wEhh(UQQ>c޽P^^///<|fff* Fi֬YpssXzz:BBByfӧOヘhhh,.BQ9s M$A,C,CWW&&&v9mۇl<;wW맲R:u---DGGAAAAɈ\{{{899!<<@|Mu(--EEE @Orseaa 5+"ק9!ѣ)]mŋ<==U#!K.Xr%k۝;w:th0=/'' 222˜1cj܊ ̘1׀kll~ ڀ zBHHn޼ X/.. |;v@rr2SNpsry?\)P("00A.P3M_9s:s>!DU̙={@(bʕMNꍸu͞=#G666H$ u%''zkȐ!H$8v H0n8^CCCŤI eؑ#GPUUnݺA$a„ ?8v8" K aĈ :uРAq"""آExe_vM8 ֬soΟ?;_C"u!=^vvv*Gy/_.4^oooUɳgϘ;v4ӧOJ_~ip/b ^Nq5Ǔ'Ox1Y[[vHDIj)!;v 6ؿo>& q[}N\B!}Ç=ynWZdǏsvRGj7o")) 022Bn뱤lţGPPPW^AOOb-w hkkC$pppeDVVf>}W^j.UZ?ESmllЧO{)*##׮]ǏQRR333L>Rӫԕ7n ==BNMINNƵkא@kkkZRQQ?Auu5LLLAA___-$.Gpp0Ԏ;PVVm~zwsY+#$$;wTJp3M6qۧOMlmmaddM?W>bBi/dΑfοڵk}vx(&1n:n_=dʥ_t)c6=qϜ9ܘ/ƺ@6mĊhOܻwUV7| Fuƶl***>n:&,322b쯿R{-wc?3cLm^֊a#Fh+|;w䥽>c7oѣGK]נlڴiٳg ʻz*oZԩSYff\Dvv6 bƍ^+]]]6k,h9YYY2ߏƍkq}kq#={f\zUm@Z|bZZZ2H$,##+k?RM=v?_תCuokUUU7kjj6q>|+_WWxEeVUUN:ʽ|R⭬M H?rH^\u/ݻwCՈ˘\NB!rH$x뭷YlРAR8p{޻woO!TVVFD~kkk >Bp-n: Uҥ x H$999so7n`„ z*UYY'ѣG>===9z =;w&SE-TUUq{{{!...\@YY֭[y/k}wXd o%&L.] ˗/~odd$>#nX `llܸq1^.^Kpqq4557np>EB!ݻCKK ׹JNNܹsq,+ysttt0f HLLą sW0o}~^444aL<@ߏ5k <<\eu*ӟvvvVxJ4440dz\]][T.~S(Νy :vwwoH!DyM@=q !r0!זz2Vp~C;.88;^{▗ⱱa'N`UUUR󔔔~(iٷo_}v&MHHhc믿n4ѣGyi'Ollj+={֠%KXIIIiiilԨ[F IDATQ @^쉫{XMOC>5Hͼ>=]uuu~_|ɛVDנ~|,zxxoܸxyBCCH$b'KMMmﯿb|/^l26E=}:}CQQ[`/&}vuxxxpսY] K-]t))Z/lz5ŋyΜ9SW߿Rjܹj媽nv?%Q;!B4ewQAA~i.^̙3*lݺ7oqML2ҿNbD"ܹ>A/zsm޼ ~{njjF 6>P[͵n:qK.Ŗ-[ X,ƹs0tP^GeR5k˗ _HjnnǏcĉ-:RW\zӃ!kP_II Nü}>^x3fR{+F {9s:uj^ؕ|sV^z\@cÆ R˫Pgkc7\żMyOŋ\-]۷!y̝;HJJHKK]40j0f̙Osi8::6Ny`ͮC]BHD7^5,7n,--TTTV 8z4ЫJB_}ioHmT*l/^q&t{쁦fۘyL{yInw7صkZCn R|y4&&& k'''n1;"44|ƍCݹ+W,EEE P(l2_]rۗ.]TeԨQ=z4O?ڵk|6Rʭ_Nz<\?}k5nݺG߶J,(I#.Q???n*8~… x }uӶW;>}mO< hňoBKKۮۛ >͕\(9x,X WEϞ=Լ_~ףv…2SNh5~`.y4r5 6={l=zJckJdd$JKK3gn9#"" VjHWgH$RJ˩︥꾦(44PTypy^s@JBF)Szu888R qqqiC~̘1Xv-<==aoo[[[333 ^?z( `gg{}^ 4hD"ܐ;k.˹/~H$X` H Ho*~y򇇇C"`ĉ ܝ>}={Dn+VK.?td_?Bi+F 䍖9q2]xJݹs7oF`` <==ÇՕ{ >[ YIg0u/22~/>E^ÑJm+ݒFq5;/k ȑ#JW{FQQ5WSLm_zU)1 >>Xv-W~رc۷oFW"::cǎ144ɓ'ѻwoc᰷… ѫW/Xr%T󃟟_QQx 6 HHH=BBB  e^Z]!X~=)>#OޠN{To!CR$5=uILYfaݺu={Ν+ƍWWW 0@(9߿=AϞ=ΫyUq/!G8jݻYTq +]Ƹ{ >mOb&j6l؀3gpW;R722^.,,TJˑ5^@R筲zۘ0aS @i{UM7o,--add j*,,l'/L6ill 333!22044H$BPPN:۳g1vXoa$&&޽{! 憡CrqbL8qqqr/O-եKhkk@ @aaB˺?#iXܹsɸ|4Yݩ455[mj̙33fhVFx̙3ر#h^o0p@1[e6G9$MMMVUZ{njjPWKkj*4 2Rz57cLdGu^;Ou[׫W/ޢnjߏ'O(ܴ4޶2~Z>MCjč;>k׮UKRLL0+F*cdxb=;wy?,|YeihyF>˺Mک}GjFyxx:tyꍸ;v@ǎѱcG={K,tuD[XXKJjސsdff*ԋ\<6664Ν!!H~KΣ# QYYBQ6<lG{{{r={ӷgu۠ ׯ-p ĉC^^=TҐ!C%KظJ૯B*sNRU_ߪu_-R֥w6֭㽮>37m+^Sꗣb]XѣIw^|HJJBll,׆B -- YYYڑ!!!xn޼ kkk9s`jj\ܾ}NŽ;#o w^:.+>Ddd$p "YQQQʗ_ڑ/_DLL N:5r֎l޽{7푚T̟?_u5v FcٟT[Ns'>YwƍDvv6rss,u"++KfL>?&N Ę1c0uT5]4%;; %'5SNHIIQ>077c EEEܗκ=$'NK,i0DE H*OMo Ƙ7]?Bȿ]ȫ~>E{I3{lÇ^^[Y**//7N.]%׏̊lee-[`ӦM˗qU_ z \rÆ É';Ve*竢O[ E^(k$Yz~mXoι}m$ ϟ۷-vA{YCujx.))it1 D_Osu\E4VdxFM>gl۶MkF>x ,\jjuqq1p]@,cܹ#y+xIIjϭvaOdggعs'կ_?|' okk wwwq2ɥ;w.8~ @Oy'׷KIjj*㥥@߾}!S]-,,QR4#՟|;PΐiӦAWW@͐544x`ۻÑ+++y?,O?rr5x)4伖Ǝ/QQQ(,,_իWzTTT_GU)|*eaݹ 5)ڐ\~=g ;Ls:O?矷ϊވ[ٳ`9r$\]]accD5k4Hܬ/GAUUuH &~]|k.,]"}Qhɱcp 8߿?w| 0bbĈ 4h&O bC D"cD 4#7RWJ;v_۲7x+߻w{!ʚ@(6l@bb"<<{}f++>Y7222db|X%_ L5j``m޽ܿRbKݺu8͝ה#G 66V:Z۲~:ě1^֭[yMG?涳T`Ŋ ^$%%a}gVzO#GzFDDs*:::(ʒQF!99'Or"޿?rssѱcG8;;cҤIXxⓥ/+S12ZMgdtS#WZ899ddwll,CCC>>>RW{z J:[vmzÄ׮]Dhyy544XbbbySJ^'.c}XYffݻy邃 :A/z.޺uKsa_W5kRfs}.]dƮ2z^2؎;xi{ɞ>}h7n0###η%=qq ^1pB^ٓ'OfM 9rdrO\{%ijj1VVVƜyu?C7۪MqqJ o7yX~:&ModlֳgO^=nnnD#_kφ[94!By <(//'$uA|7 ~O`ooQQQ 344K^h9 @MOb:OWYY"##uVZ Gnv&L7gիakkiӦ5HC̙3׮]c26m//&4vZSE͵f;v[g˖-7z9@zz:̙WVoWUK ÇUܻwo|ܽƒ#$$pqqQ˼?ǎ57v['`Ν>MMM5feի[h P3c8z( -|TPP777̙3--!!aaaزe p1Df*ˢEm6nQHx{{#447gϱb |w>ZJfYYYx1(R;7w%-[& [ B^ԈK!۷oG\\OEE6mڄM6;w?d$%%5(aر ߿?DDD]ی L26661b젡$$$ &&jVm ,\k|,++6n܈QFϟ?Ǎ7pETUU֯_J" ݻwK.ŲeУG;w1ׯ֭[|~ 婢水DXXLj5 xw`oor… x 88K.UZz?QF!!!e˖aٲeDUUokkk|!۵h+:G'BDDΟ?cǢW^ HJJBddduV۷5BW /<5z;ynH-c þ} ѵkWtx1oڀZ8sBCաC9sÆ Ë/L#Gٸto13---|W `nLEM<{Á0k,7깗:uOc{12ǎkh[wZeSugbƌ\W^ٳ8{<:::ؾ};͛PUF[[֭ٳ[;z[naҥ >ӑd & 44\ЧOӧOԼ"##722w}QF<6B!|!NСCv&M==&`ƌ{.l٢^u&1d(Am6 2ukkk#22ׯI틳g6XЮ){Ň~>}@C鯇ZZZ˗Je޽c6zq9[N%1uYZZٳܹsѽ{wD"D"888`ԩ8}4akkM7QظYC]ucٲeMޛ:`̙{mpk͜9oFk IDAT!;ÈEtuu1e\t ΝSKn-777bƩӧVB'`uQÇ__2`a̙-? nQ93g΄D" ZTN>+WC{5=LQ ܺu =B~~>^xVŞ>}7n %%҂1 333+\v (,,b1ݻbq䖔@[[ܔFFF^fKeff"::jx7ѫW/ơ{z9Ĩt(wt ڋrܼyrrr zlU|q}<}сzC[[Dii)._'O ''ꄨSjl"MÓ'O])FUyϏBv(ZVVV8qС 0dVe-emmӧv*:}4\GG}QKm6l ڡ9ijjja%tuuaBMM0e߿_+W2!H0qDDDD48>x`,^GFΝ1~xr0h D"q+*((?`bbnAbt=r/KK/>> ,@RR$ $ B!O7oę3gqAKK#"B!uFܦ\x/_Fzz:RSSSCdee5T4Z~ll,"##LPq 6 EEEXf xy}}}QXX!++ k׮P$&&b׮]x;__^ݻwHMMB! {\icccjnŋU!Bivӈ;o\( @+++ <33@jS̝;eeeg͚ :J]!By}]|˖-3ttt```H===1۷o:`VBȿJS# qI|1118us000@ii){nTUUhz6!jA1fL:x Ϟ=gggB-񙛛1"n䂂5\600Ǐqa뾮 ]To]'ONNڌ#GJBII ={Zŵo>kعs'1d8,[ NNN\.DzeΝq+Vipi?_011[o&BDDDDm ͛y:""'ԩSO[n'$ /^ QCAz6 gϞ=Uz}2 ;w7m ˗/m(~M={4}"""""""IEE*++1p@H$ܸqIII4iVkann\xQ:jv DDDDDDDDDm=֯_www;;vlꈋCuu5 J1e=iצfM+#""QoYooox{{K~CDTKDDDDDDDDDԆq .QA\"zqF;K,Azz:AuXDDDDDDDDqu`֬YXjU n$1vjv%accDP$m|%''W^Zq>= 8vX{K!"""""""z:?022umεkא޽{:6IJJJx=zSSSxzzu8Wbƍظq#z̜9}}bWmj&_gzxx ** ptt d2q=pqqT*En#`ʔ)033"##*mΆ+R)Q\\V666 ^^^HJJӷ#F`ѢE?~Mڼhޏי ,@xx}ȑ#ѧO̚5Kx饗`nnΝ; wܩӏÇcʕZ[[~/++ Ǖ+Wd˗/0e"44G|||"aʕذarss0ҥKxtR?""""""4rZמZ] iS+&&k֬+Wkkk@@@ʐ|T*DEE{`ӦMsΜ9;;;1W_EΝQ\\_زeO>FByy9~m RFRSSqe 66VL9s&F2^}ZYMo_5rssq;v숄8{,}vӔh5z?ھرcĭ[ik׮P\\7B__N/^JjRX|}RĶm͛7 kkk̙3)))ɓ'c(,,1c ݱuVT*:tAAA077ת߉hT* ((_ BD‹/^xŋ/^MjGzzhZ9i6+o5nl尦 ZBB6)OSڬ\nl5,nٲ_ŋklkKPԹWPP0a;w\ܿ_-?_o%%%666=[[: _\\ DV^.LMMA"J}_N: Gtnj\B]vZŧ)]m_ӔۘΝ;e2+VAΝ;#44ӥѓi崦ÚVj2hx6+ZוJJ!!!bYMkrYlj9:?,((^^^Pxxx`ڴi6 DR瞝$ 233!J-gkkz,,, Hp XYYT*,--jK.RIMEEEb|ptt4?Mnݺ___8pH$Xxqˊ6g+s֦6mޯڠtyy9ꩽ JB.]O|ޕ5+/\;ñpB[Vgg:+Y ]95+Gׯ?&QΝryj@+|&T*\.\. d2q888`„ Gyy9GKSSS,Z3#Cu>GERR`jj OOOn+ѣ1sL۷Y'"""""gөSUNimVkZ٪覮5u z2ZЊbccB@@@k6y饗>HaLkB\\\KT*a¨QD"Pv*DDDgϞmȉY+V:j|\\\_ Bnn`bb"'N(̛7O(++A\"驩RA \.$$$)))T*Ab!55U- ~iqΛ7Ox0`лwoP(--*ZJ-ω'SSS?^5kp=!??_޽鮮Ν;’%K 5]׮]ηS/s;vLoY-AXX?R 6`̘1uuV:QkٷovލaÆ!88šNNNJ2e 3;v1}tWFKRZ7rH( ۷۷oBĉԬ vpp@Ϟ=Vr[nպ]v4h2ZVN>Ni-z8T{ ///TVVB*bpqquXY[׵kWDDD ""BסӧO7.ɰsӵ@6`ɓ'ƌ_ aϞ=ͪ_.#%%neh}x .=???]3st6=Qg: ~p&.=Qyyyظq#Ǝ%K == :,"""""""vmDrr2z꥓r9lll Hét?j}7n'|ѣGCP 22ǎs_~uDDDDDDDDmS \v ݻwaddJQQ{SRRDѣǭ[p\z7nƍѣGc̙۷{ADDDDDDD~~~صkW⥗^9:w 777ܹsGL/--EPP,,, {.\+R)<<<0uTZ {zH!,,LGEaڵ+&O @VV ϟ+W@P@P`Zñr&??MbccP(兤:ŧM*//_|)S8r ヸ8!..+WĆ t.]ǥKt5""""""zF|g%.\X'ڮlֵYYe<3q׮] NCCCBOOӧOGTT֭[9s&ƌt>}8pXYYY5j*** GGGlڴ  A 6 F>}T*+V ;;qEӧ%-G3p\ozci?j^ ___:tbɘ1c^~e[V" nnn裏pQ۷ UVaժU2df̘iӦ[n=""""""z_;v쀧g Lve=jnL266ƭ[T*"@~~>RSSqFtRHLL~Ûo 5 GV{022#z{{{H$i? addDž -ҿʚ5qi_Grr2>3@zz:p}c֭PT8t+}}}#::* ))) 1~DFFBP@"ŋ/^x5f̘'g!>>>P(~:ϟBKV&ki&2e `eeHq',ole6N-O烸[l%,--_c˗/kUNJ+0h xyys Ń< &@/;w8{b}?g@WUUjUVVj_[#"""""""$(JtRĦM@(JL>Y χJBTT_}Ut_-[5kʕ+ǵ4?sL1eeeXz5;tmT\|պ4:N!((^^^Pxxx`ڴi077'||2 / vvvH$̄T*SK.EEEhWGGG ~mI$- M|ھ:wɓ'1j(XYYСC8r9ECv UWWmPRR"q;"""jMǏ,\""j15+/\;ñpB7rmܹsСr9Νbjyѯ_?@NfezJJJ+ӵQ@WS|&T*\.\. d2f֫&իWߌ:0aT#  6Ç8}4ӵ_Sڲ_moѤvkGmrYmkt Daa!vލɓ'(C IDATRRR0gX[[{ZYAp ,Y7nn݊.^g"""DDDDDDԠ#F[{G톔@ (..WP49>mVkqVS|&nms?r AYYLLL0{lqqqXlPQQ\e˖A&'NllTMk>>>pvv z-jտ999(,,lR5:v?ިĚ5k9r$T*JJJ:G헹9$&&">>GERR`jj OOOnAѣ1sLW!"""""ԩS:m͏r7nWT*XZZ6CSuTt ooo_~)s?(uaϞ=Կeee4fuɓ'-M|?>ϟ7n`ؿ?N8xNjyv ̘1C aDDDDDDDW{e / ޯr?9rrrpEL4Icfff7n6l؀;v͛%KZ$+׭[3g ==nnn-R?65+ :wݻ… 8v֮]밈-!,, yyyؿ?+ 4psskֿM+W6?]v!$$066ٳhѢ_tmO; 9;;^Bee%R)6o Ǯͺv69QkqF5LfM+5HIIi4OZZZiole6YyN-]???=vLo_ .Ӥ5Vk•K8qw^ݻ5&TLDDDDDDDM+5EOQZu AxήXe""""""""z8 .Q֪ikݺuHpڨ+V:6DDDDDDDDԦM ޽{w^]4MDDDDDDDDmڵkuQƒ͈ڰVl]L\"""""""""6DDDDDDDDDDmq0a%"""""""""j8KDDDDDDDDDԆq .QA\"""""""""6DDDDDDDDDDmq0a%"""""""""j8KDDDDDDDDDԆ:""""޽{0H ǏD"uԈ]իUq&.QƙDDDDD$ ]CntNp&.QA\"""""""""6DDDDDDDDDDmmVK 9<0)™DDDDDDDDDDmq xAξ%"""j㒓ѫWǪ#88-add0ęDDDDDb>...J֭bbbĴL2fffBdd$#F`ѢE?~::={)lll*1=66 ^^^HJJӧR2 wZ',l۶ }ޚ҉A\"""""zlXt)gaڴi֭V=BBBi&ܹsgΜXW_EΝQ\\_زeZHMM˗QPPX@>}T*m68::BTBT"$$D,۱cG$$$?ٳg6Rӧ7!77PTSGLL ֬Y+W ##֘:u*lڴ bMDDDt .5ۉ'W^yO?aܹ022Ҙ;v@PP^|E&M}6Ұ|rtrs!0|p\pA>grxyy!33Iϡ󑚚7cǎJGbbbuѩS'XXXo6.][";;}nݺ1͛r ƍ޾I^o%%%666=[[:MMMMԺ~-֮]\Zo [AԦP(mw4h~WdffMJ'""3qb+VD"iZ`C̘1C˺_Ƶk͛7C.#,, gϞ*x4([oH$qxORҲIqJ$P[HCT"((޼aggDLq+|_=xIIIE=?cӦMΆt"""z:p1fΜ4:u 2 򊸥sb<}{aƍ 6vbbbݤpuݯ@ee%(wxYs988`„ Gyy9 ''ZOaKz*bbb0zhӉAAA9sڽ](""""zZ) ^QQQxV&MBtt4pUcb]v!$$066ٳhѢ& 8;;oBCCaood;vZّ#GBRzzzHHH@Ϟ=ŁfMl2899r˖-Ӫŋ7;}FC<>3f}.΄:u*6oތ.]Ϝ9kud2F+Wbذajugffbݺu駟PXXccc֭rYYBBBc„ ضmZDDDDAOM D```yr9RRRԩSjߣmoϞ=@DDD}ZtG?Z;v/wŻ+O9KDDD :}4֬Y'N&&&ڵ+\\\0gL2E!ԯ 0uT :TqO\#!!f͂9R)f̘FFFOOO8qҥ ХKx{{#==b]'Nٳg1k,rÆ DDDDϠ'N`رꫯP\\jܹsYYYؿ8x,_zj^gϞu8DDDp&SlڵXvmYXX`Ϟ=6lť_~BBVQ~zŋT%""""Vs&X9r$̙={`ƍuʜ;wӧO-`cc@\tN]v0`L:D>>y&`Ĉ -\P,3.DDDDDjr9233QRRwwwx{{cС:t(OII7|`D駟PUULƎ[lAVVV1^=.] v.0m4OhRloB*B*b(((@uu5 P(PA\"""""jU _~ AQQQ(//Gnn.= &H,R-jByy9{jyĭYsωw=G klД,,,СC)Q9 x4cwXd fΜ)?VVV⽀q[^^V#//јjkk??|NzSb єNDDDQ7o>=Μ9={b={ ${Ay駟pQ߿6oތ-ޏTggeeq.q;!tR'o՛%=e``m۶Ë/.]n޼ xWļkܹ֬sCL>666f-7|SbkÇC"@|Oo߆T*m}L\"""""j56l@dd$F Àc߾}j_z%>}3f̀JKKO>Vk!&&NNN022B,y+H Z/M)CPԻ={81߿?222УG]BDDDԦ 6 Æ kRA!..Nc7o/^g~W8y$Mܪ !!Nyccz_`,XX#>>Pj %""""իW1|pK.(//Gii) 7oqDDDDõ9DDDDDT/N:ڵk{.`2dC$"""j%""""uQLv$99zj4Opp0"""Z)""""""""""z8)###]ADDDDDDDDD-3q[vUo GGG888 ++K-ñrJ{YYYP(?>\BB۷ybccP(兤$ݻ7ЫW/DFF aaabR2 w^>6 /@* ~~~j'j>ѳ,&&k֬+Wkkk/BRӧJ%mGGG(J(JyT*1}zۭDVVPQQll۶ !77PTSO}ic޼yxQZZ-[ 55U-]5N_~N:I/++{"vFFFpttD^`ooD2ܽ{p:v… nݺǎĔ)S|'"""jN8ػw/ݫhڷ]@DD8lق~Ç:u NNNb^B000xJ#h„ b- :Fee%<3ⶀ xyyBCCiӦlllJ$f#H B D"Aff&RiH$(,,;w'>_pڥBee%~w:tY=k8̙aÆ5|NN Msuu!˱uV1mȑP(طooB'jn\\T)S //I57._ L bjYZgiiii7vpSoɓ', ooo_~)޿sY&aΝ_c ;&~ǽ{>ѳ3qڨC#|s}g%.\X'=88X<6\:ס)f͚UV5Lrr2z6)ik^KDDDD[~=v઱@չ5dggwO:6C#""z|K:#::Za ~~~صkWipvvp:y+WֹBׯcP(Xt B///$%%59R2 j[fi WWWHRX-=::={)lll*@VV ϟ+W@P@P`ZŧMƞOrr2z ''' 033CXXXOCO˗7=-8KDDDDԆ9rǎõkנT*z:y.^JU~RRJ%t@TbӦMbz`` J%OެPVV\CR!**JӧOǨQP^^~;vDBBO={ a>}T*m68::BTBT"$$D)TVV"++ fff@vv6mۆ&=zEEEM~FDDDO n@DDDDԆ͛7 '֭[>|… ر#G.\N||2Ο?ÇC__pssS ~Bfffħݻի!HPVVwj~s߳͛#GHDDD qt`˖-xoÇßN:'''p 7ol`QPP0axf6!H`cc#޳WbڵͅJKK*iC__```^UUoY|~>>͓J011L&J;w>s֩kx7H044ѣG;88`„ Gyy9 ''Z &c IDATJZ!Hpƍz_QRRҬ*|痗עm'ma,ၨ(xzzRɴN1bZ ,@xx?ɵN6.(()S`ff+++DFF-1b-Zǣk׮+Vu(DD-{2ЩS:Lɳ\۷sΡC;w.<ŋfhhь .ԩ9'޾}?~wb2e'5q[' Bhrڜ<k5))) jTmbq.ѳwi0Ym5-@ۓ-M'ڠgyy9U_{r&a 5)T*XZZ6iPl;w'+Q] QpHRq1x`jk<]`=zh<#""}|={ԛvFr4~)kf;&~ǽ{2 ;wlVDDDDD"t3 .Q5tP@wf͂B{ァudXjSԩ6nج6Z _ڵ+߯01cpq]As%""""gZ`` tFGDD:"]ܲa׮]fffݻw>|8V\YoyDEEppp@VVAAAL&CppږSHRt 111Z+;;JpwwGqqZztt4z SSS ""UUU,( ̟?W\BBůMX( xyy!))I-dNNNի"##aff0گqڴ#GرcvJ%V^]'ŋR#&&k֬+Wkkk@@@ʐ|T*DEE{yTǾ ( *bP\w5 AыcQhDIQ#J@Q}IJEYAe 0lsΜU]릻O?/_ܱ֭{xxX|y9444W^ƍ8x Hطo_`۶m033CJJ RRRm_Wl퍔xxxȌkܿ۶mCyy\ǯJ}:~u?B!p B!js]͛ok׮StX$&&BCC@߱c|||0n8.;)) oɓ'JܲH$sV'޽;aff޽{<(**LKǏB!Q#.!B! i&,_PXX'O"00p\5;;uXSuU=$6lXx?wwwʗ'RRR||n]YYYOǏB!Q#;ԙE"30\͡3̹Bi||| ;w.Ǝ>={-K$6.Wc]np]B 99c(((&1P )S`~F>]7cL!uCBH{Cc8w\x{{UaB!@(B$A$SNWZ_~/^͛Vcҥ ǏǢEPPPx1bcc<سgΜ9ɓ'* Chh( %%qqq\ׯQZZ!++X@e'O 77׵}S[?и?BZ=Bjmf-9p]ꚙVkf3L4 ҥ lmmK͌ =r]3 ϟ1cNNNRǎɓ'#88I!"ˬY`ee%3?= ajj PǏz+,, ѣB!&N4.[lA@@B!tˏ={`ee??? 2K3449sȑ#kamm WWWC$a֭r_Ç)"""}vb„ r<#VRR(899Ç ~A(vҞ>}'B]]ϥӨL|(smϧ۶mܹsq5dztBH;H`iӦ_~!C^z,φξ455٬Yd}޽Z eof1LMM٦MёM2B6qUCyk{>} [r%ѣ`?#涫/=gX||C!D';T5&͛Cȓ_kO޺bnfiׯ_ B,ZlpcǎCJJ x<lmmѩS]vmж6lT믿l2|޾B!BHkv%X[[cڴiڵ+]srٱc|||0n8r8::/H__ǵ=CVCe=v ˗/ÇuVܿ>}:M'_4Y3x3 Fik5  ΐH$pwwƍ5Ըe%%%@I˄B!_]k9b ,#= a<}Æ YGnn.cR8 ''G*_mg>ߐOA?ݻwQ\\ܠtBHB],\50cLjaygL+栩 6QFI}gf Տٳg`ffuB!u5BڦSb„ 8|0BBBwww`Сr100@rr:vMW| mmZǍ%sUo+))믿:u*~'מB/N f䟙卿.'ODjj*~㩢"fƭ u֡W^ŋi B!.t^^^8}4\---L6Mj8bϞ=8s ޷'OcXn]vťAq|(qc\7bݺu4iRSSk. !56YXCC?\\\  L[W [[_;w`ĈWWW пܹso'B~c߿ Ut8Iiii0111+X!aF . >>z%vz"7orc:%&&">>iB! :kr9~~~ĻM1c 7֔F޽9"B!E"d8;;B!!!ׯ"B7FyB!z"ӧɓ'H$x1fϞ!>}:v-3ŋ077:b1j6lX SSS8;;˜ǘ1c`ll '''߿kkkB888 ''Gjϟ]v@aa!z_?m4}LMMѣG055)o8!BZ5B!Ҋ={@JJ V\Y#σYc7RRRQkwAll,S1{=<<`gg,_OV,#??HOOGff&VXrH,^IIIزe |HB!Y͙3zzzyoڵk7|www=bIII}6N< %%%888֖.==HLL`ѢE7oߠArJ@" ..:uCSoB!%B!شiGb$._U.";;YPSS㖕PZZ ǃ>nhh-?}0~xn(___K?sL333׏C6%%qqq\ƏEq ?Fll,g֭y&oߎ0̟?_p-tuu*~())AZZÍ%M&""_?x%JKK_~QЧO| !77/^ĪUyfEIi&ԈK!B͚5 VVV2>CX|y<?[e055EDDoSSSL0A"""gXYYC J Cyy9zP'"-- p-| >_+4u077H$֭[i㯿Ç򙙙8|#ס<.]Gѣ֭[ D_W\38NGڵk8x fϞ ee5r b1^z7n8k׮ٳ رcѡC>9! H`tHu }C!B$XPE"BCC& !mVjj*^ҥKUVq+W毿bL__ b1{Aܹf̘D"L]]ذsy]ܘ.LGGk׮Ity6m4zٱcٳ'SQQagGe&MKOOQÇk;s swwgLUU0D"/"W]G͚5K!??_~^~1---qDDD0[[[|>SVVf&&&ח_V_C焬>fn̙3e{rj;=FŕUP}}aaazO<\\\:SWWg^^^5_g>Bw!B!BP(]vcbbₒ())AOOϞ=q1\pgԩSpvv$W\۷1rH9rӦMCYY';;ѣGq!L2F|\wsyn߾ 4x T[o]8<~Ŋ2ԔMj|v.] D"!55v™3g?@UUא 9'VXXsqW\)3\/\hyjm ԩ$ p=\z]&D4!B!H/_DVV\M' (++?JJJ`hh$AII Dqq1<:HMYYgϞD"}PVVƍ+| ++ 999[8v^~2?˗2JOOGyy9ʆݻ7(UUU\rϟz*X 99ϟ?HMMšC =T?>RgرR Be:{^3dCI&ӧx)T'$MАo!B!֮]kr{;vp wO֭[:tT}>CnݺDX,P9?7X,[ BBӧOQ^^gϞ!??9rH6mڄAuテ5111\DZsNdffD!Yퟩy>'nNo M9b…PRR|駈9sgnP#.!B!Lrrrs}U#)P9YUCYueee(((۫W/gggs"H*PfU\#*&&&Ri nĭސYgeeeRuImgdd$Ոkdd%%%seo߾rN`Ǐu&k!Da9T?>/^@JJ W_Uu=bY>r^UWQQQg!D6NcWmcB!BOVBqq1BCCsxzz")) ŵ>KtU*om Tڕz*@}Fz^U?o~oǏsr"5B!"4_#G… qE0!PVVX,o޼ҥK V8tN>m'HqF|g*{GVz*/_"66O*?@dd$p={Э[7 8޸5ʝ={k8z*n޼v]8DFFŋ {j_j7={Ǐ8|0fϞ3g68Ճ 1 ""1( 9'jSڵ ...8rn޼'N˗Izܓ'OrqYϊ+Go֏;AQ#.!B!† `ooSSS|爏; ޽[aD";#::{nԶ &:t_>m۶A ͛77ntuu === ==@e;vp?_jttt'''ܿ|>[lGEE<<<wwwTTTHׇc*8p {{{!Ƿ~ \ܚXd ׸ -v*ɓ1l0aϞ=())ip666ܹ'O\=qKCΉڼ}|~7L:CŤI^{SC+OOOn3f@[[s̑BL=z\qҞQ#.!B!b۶m=z4D"VUUowСCann`$$$(zڍ ;w|puumH׵kW.?L4 W^'uϟիW۷/z]N07n3 󡦦+++n)SҥKpss|>ttt₋/bʔ)r۷ `a֭=zt3׿]vGPVVF=zlj`*ݻw" }􁪪*w_\tChjj"66666PRR˗ѧOlٲS 9'j#|~m'5会8q"BBB   p 6:0c hjjB(Nqn1B!BL,3,44T!={0'''&0WWW^z%MEEx" `6[z5{cPf;//͜9u҅uܙ͙3~K0`b666O>ۛqO|8+,,VVV,((Q޽{ǚ}1& ْ%Kj_BB311a:::3fbb¶m֨8Iӥ2ظMўI$v5VQQߘi)LJ6x{{{{Ƿ@tB!KEEE0k,g<{ 'TUUxEHH`ܹڵ+ sss :YЛB,#??HOOGff&VX!'-- {.wqigF.]?ĦM駟K\~3qQ$''#11\ښ5k!HidTHII۷/RRRm6!%%)))o8 i/RSS1l0{ҥ Nr#$$D!BHEB!{'/555xyy(..n݊L8q>>>ؖQRRlقL;v͛7~6l@VVVUzz:bcc~zhhh@(bѢE8xT>OOO|tbB>}_|TTT Cqر>>>7nYʟ={6 Э[7888 11Kر#򐒒5z/1??_!c!yt SNEΝ"| o 2D!BH_vc (1h EA!K666z*Iӧ*W)//K.ܲ$ ʱcR  ''G7ܹ3,PZZʽ W_}gggH$cƍf!nݺq_$#<<6! AB*44TgBQQ9H8qgϞٳg1|;:u\qDEE!77K2dzcܸqʕ+[nx{.BaS 0xPO|B!홪*<==鉂oӧ`ĉpwwԸ1\|8pTc={bܐ ǏǢE?@SS?ƃ<عs'fΜ eeelݺ...uuu=֭Î;]va…ܶ裏1c 777n ɓݻ7LLLPQQo7n\>@  ..Fjжħ'O 77]vmp|B!́k]f" (/yfEA!(&|||\kkk:uW_}s6*VB!`'BC~)6oތM6a޼yBiuH\tgfcccӳ yooow:lOOwV'!-%-- &&&0669%&&w>E 4O?8v&MhQ~Y!Bi!22GA[[Ji?-W5BZ򆐶q !B1NkM377o!:v-G R DϨB!VF\yݺuҎt@6 #F `bbPQB!B!BH j}}'Ŕ$vUcUݻׂQB!B!4 (: B 5=z˗/K_ !B⥥a9r$.\/1!c|2-ZHmmm899a޽ԠK>>z(:F۷o_uAMJ'Bi a9kyCU޽[aZ"xukyݻw%yϵ֔s)ZV^bҥ޽;lmmL!66f͂\]]W^),VBqIϞ=(۷/^}HSNЀX,FvvTyׯ_ǴiӠ@]]]5{.fΜ ###(++CCCÇG\\\G.]WWu@hh(SN(~aJ!SSS̙3\=y!_~xAIII$B!9b۶m=z4D"VUUowСCann`$$$(zH+s՛-yo{4E˭e[^˗W^2d{K,בm۶eeetuuÇ͛7B0pK.eULJH\^UUUtuuudŌ1~7/>ώ9•{I"3ƍkmddT#ԩS\+I^ܫW/җ,Y0 fjj444<&&&իW1c?W۷}Ysiۍy1lӦMBQ|gBEE0sI vE >_YXXիW0 eX,nָ̙3Y.]XΝٜ9sׯ0///fccüYQQĄB!aL0faaԘ1۹s'fmm͖-[Ɔ ”T7ofLUU%KRnjÖ/_&O>&XBB\u=&XNNW͛7*+((`1&N444>|8+,,qXPPjݻW־}  lɒ%5/!!0󙉉 311a۶mkTmAjj*\] 7=z4322jԵT׹Vߵ~}6l0ƌ&MľK.ks[z^ԶE g>dWfRwم XEEr233ن ׾ijj2vqVRRlqL݋{Jc&MBXX@,#22n1c |'(++"##1yd;v }' <sAqq1믿F@@TTTpyԨ[UUW\Aqq1QPPcǎԩO1cƠsGTT~ge+++ٳ@jj\Θ3gr [8tf̘sss899ĉ ?uuusG":u^x0! h"cB|pDFFĉg*@'''xzzbԩԔ=ǃ-lmm?"..BBB!CW^-bС<<'K/zы^֭[zK5FII a>>>cǎRu谐iii KLL֝:uq :1~Wfiicŋ㱿K_jO?5kkkO?q}}}Y```j7fxZ/l}N.++c\zPPg߯VSʜ9sd~'Çsr{ߵ垸o{\f?3ϯRmZ}Gť;88HZu.S~Kӽ݋fggW;vX}!??4U'n3Yjʫ-s[*"Hj;CCCnY"HM2&oU2@ll,rrrFZZ`_T~k8}eWs[cpvvkݦhƍuV 2=@pj4h ȩz;BHgccWhgʧOT{@.]e---H$c2Ơϥp6lXqtܙ[(--?~k֬Arr2|>?wweYGm> IIIx1Fͥ᫯3$ ݱqF(++ϻko{] jjjܲR%Y% x<@EyZn=݋dST|ضmέ͛xxx`С*?//Dxx8Ο?/&`ll }}z{*ڝ;w5*ǏFdn6tuu'zT#l)peXZZ<C{]Nq׀kee(ȑ#:u2.\?nfTWWGuS$BYF!^x{8gٳ?>Ǝ[p ՕK ˥U ~7R^rQw <wޅP(5_ffԲʟxl777aZWv؁SVªU7nرcއ(Bww;s}5禍 sܹO 555L2G^^8 5kϞ=///XXX°xb_H$ŋ888YYY:u*K.m6:v숙3gb\#lقBSS+W+6CCC|ppp!0r?vX_~ѣTڝ;w|t 3g΄k2?~gϞ5*N pqqAii)[7Ñ\tQQQիP.O|puu9:uꄯ smTe4Zwε0@KK ֘0aToo{Q^0|p >?"!!˗/aiilիW:6mԯOi Hob3;;;.o \f̘455 IDATP(dL"H}5tttg:::Ņ]zFwa3f`"|ƬعsjCniӦIwpp;vF=АѣZ˖߲ү^lll2ҥ 5k_md |> &6۴iC!BZLǫyؘ7nYNhh(b;ҀXXX;5;ibnҤIlVt/jWZZbbbٳ&1[[[ž>}~wn߾M#m5k4j\®]r ~Y^xx8kzYOj[[6 /_~ƌnM3!BHS# HKKCdd$9A [[ΝÝ;wPin޼.]{HLLD|||t/jy|>ܯHbcc WWW(:ӧOovt/"4c 1\t WtHB!B!8j%B!B! (: B 5B!"4_#G… qE0!Bc /_ƢE  '''ݻtIGB!"++ 6l=LMM#>>ƺ7o*:TF޽RH$>x<^=!"E1ckoʵ666ؽ{;W^ދ]|/?tRt Aff&YfAOOǫW2! Ml6qDEAQ@zz#!BZ\} eew!/d^߷_BB"""p덌OOOtBXXhprrBǎ'ȏы^j/OOOF!glϞ=ɉ  suueaaaիWRTTT/f`` ‚^=x1Xhh(bqƝfΜɺt:w̙^~cƍL]]qO8 Yyyy3Ƙ5F͌#W^BB311a:::3fbb¶mm&N444>|8+,,VVV,((Q޽{ ̴}1& ْ%Kj&XBBck1˜SSSclΝ\ړ'O LGG2w6lSSSc~!0a/m뻖W0nj&M$U~}gmm͖-[Ɔ ”kyffnnTUU[d +--;u/{a3,>>=|^YZZJgg.\`2d6l`vvvqjjj2vqVRRܾ}`0'ҥKAi_֮] JB!)**‘#G'N OOOL:2x-~G!""QQQHHH@pp01dիEA,C  99:tVXkbȐ!֭?ӧ:Ի};w` C߾}h5[f |>$ \%%?055/c l؈Aaa!;###<cWQRRBii1aРApvvF.]0w\Ƚ}Kk֥ju5s->}|2sssb999r՟'ĩ^!H񠧧ǥW_޿*U@/&ߋ?#F@$wq-w/-]{e]Zm۶G0o<,[ 7nhtyyyؾ};Ν͛7KD" }QWk䍼g!JRrB!m)={gϞ1vzS\j8\.ػ󰦎@P$DDҲl,h`EaSP*EoVk7ὊzE\E+HEk(""PYf%d; y'Kuu5qQbaŨWHL+}1o>>>HsI/?~qqq8{,Y)SBdggVa$&H\  ԳgOرc(//ǁHII3oooŋBSL|W044ĸq{nTUU ?3p L} \]]E@HH֭[שAAtHNNFii)v >F$%%|r|G5jQZZ sss|{.޽owLL ޼y333hhh`ҤIx>>>>СCEnfr,򂅅x<vMegg2d|||%GAA*&6o OOOhhhУ`ȑ011A\\ |r'~(>'NDDDBBB[[[QQQ OOO,Zqqqq8pÇ ,1߅ rzj|BEeOlܸ|>cƌApp0ƌ86]D baȑؾ};JJJ`޽{`aa[[[CWWHJJׯOmuC]և3Y0===zQbdSNa~e6mBYYsss<|{FLL V\, <ޯVVVG>}Zk…صkv)rk[-  MYYLFFFhȑ#?=ڍ >>/:e:Yw\RGN:QQQb\\\f+Wږ̛7='߻w/Ott4LLL!r1h aX|9z-4XMM  . |N///ɓpЧO899"u?O<رc'''L:ϧ#""0` òe(w9۷oǜ9sD5 >>>R'gChh(&L[ rEAׯBBBp%aӦMpvv& CwMzŋpuu⨈ ĉ~zz[/__ wU6Drr2lllP[[7oRh% @@@ĖЀ5 ͅ)n eeeCEEPRR֬Y 6f***ʂH9gbΜ9㏑{aј6mؠ055:gϞ… طoeggX8~8҂  ͣn-[~},,,1MϪl@CC444}vX[[wuX{LUU}YWA աN\@@%^[[ۑ%裏УGb000Bmm-^x3gbt'QTT 899-=ܹsߡ!C`ҤIB#ၻwU۷cccclwbjkkUVu8  B~:>gUSbԩ]A;MwΝ4wHң `ffFvae,ͦ566ӧ &!b ^@ ;УGVUUGoCCCذa fQSS@ w9xNl^ZZZx9^~!  4Ot. tsćJ700`/<'XkWRbjE`X{.444p1vX梭 rheqjx{{ѣpwwŋ Lq8u8^z*DAMMtAAA]7 E$>t"t秺:\.x<^26l\]]i Z*hkk+q 1abp8(((@^^&NHMMall^zh/E%ݻ7|>lق={ ''hhh!CbPVV$:6lllPWW2&~K()) u666ٳg%R1x`" x_=y8y$ @'' l {QSS f"k=zv72AΉ;c |μb c90`2*7&&K.nܹsQ[[ uuuL>^^^r/;}0}tp\͍N300ƍa``.1c].푞???'O G]]VVVBCSSvvvHKKÐ!CD8ܝAA2#>>d/^Dxx8 Çwq]Cڂ:FK 1((ZZZ r8 .]RȚ@.ډ.1---MfSRR¡CĦ]rEq>|s9%tU.fbb/үCCCK˗eѢE8pH'fffEjj*6o8  %UUUHLLDll,222zwwwڢGǏ0C VVV]\ ;())Ann. $u2AAR)uuD 7nPx[l9s=AAĻٳg8x &M}}}#==l6^^^AEEbbbrJlڴ |2BBBbڵ5֮]vj555 6\.7oބ )))xhjjy<8::bѢE?~}IhpH {{{\gHhIII"cAA?H';LII .\^`֯_|  /^ 66Ì3pssÁP^^cǎ={:b ۷oGqq1Ο?`hkk޽{Xj,,,0b>}SZXf `߿P111󃒒[dgg̙3ӧO DQQSSSsΥDBBoܸqb;###n:@|>^xWYY|>|>;vsxܼy7o={"ʫgΜNP,X^7 111:u*^|Ǐ### yDO?xZu666.+Yf!** .ě7o+4QTT 899G{Y(A#AAхqUO?UV1:ӧ &Zmף044er<<p eee4440N> 6l6555"HGR}}}d磠***7nb ^@ ;=ٓ'OpѮ .][ AtKBk B^x'O">>HOOGzz:-Z>Ñכ7o8$$$:|p 0G+BBBjWݻw!vSSS <III8q: %vjx{{ѣpwwŋm2ړ?Á'> pرc|{FLL \\\+Wϟ>}tj=  ZƼy0o>Dll,bcc} ÇX\./u}JVmzGIIIh֖-[e˖I/--LWRRu</^ŋlrORAP8uTDEEMsqq5kSSS"''Gh{{{\R\888@CC|>B鎎BΟ?ˏЫW/˖-Ccc# ''&&&7o=z`޽t011D)&M޽{CWW˗/Z-add7771ydp8NNNxH9O'O`رЀNG۷oǜ9sD5 >>>000x,͆BCC1alݺU(]__NNN;.  EׯBBBp%aӦMpvv3.\@vv6:  vHH$''"R8?Ȁ+F555X`,--QTT$u: ((Hl3g΄.*++b̘10224P;;;DGGmذl6PQQAVVEʑ~̙31p==ӦMӥSϞ=Å o>kNhDK WWW?~  fddx"|,,, ١p  ڭC666 GێqBYY|>NNN -Zܽ{]eUWW4ܾ}x={6;&ԉ+yD4555TWWߞ۶p9PQQ!C0i$}>o߆:厱޽{-VZ  E:q f_!AA(t ;wtttp),^~/oۅB~mnFRO3x<LLLׯ_@QP"SB BXC`ǧGokPLTUU($溺:yiii k    xt۸}6\\\?үvX, ۷/(³gm555BlN*X|9QTT@P%rlmLhkkbVZZ yp8~: w,kznLۇIիWagg'jjjdq9    &҉uuup\5- Æ 7|#ֈ!##Ch \|_~a\f}}=0dUd?PUU%WzqaӦMxJJJ OOOy7bWUUUd?q$>>-[;t# :[455˗tG˗/EF666[lٳgEcrFAp\;88 PWWի 9r$JKKQUU%%%$$$`HMMDEEaܹсOE11;;sEmm-1}txyy'dٷoO. [[[iLG]]]#==]dߓ'O G]]VVVBCSSvvvHKKÐ!CD8<&N(wlAARVVx#33x"add@___ߴFwiuPPV.ﮘOGΟ C"]bZZZ%-eiiWJ<\B[gj:$6ʕ+RxHII%:""B-l4|Euhh(^|)w,Z#̌Zb ]}uuAm۶A]]2UUUHLLDll,222wP8z(?~0~~~Z(FII rss1hР.# K.ﮘOW? xP(W_Q ͛7ׯ+<իWSK.Ux P;wJߟ@EGGwzLQVtt4]GAA ~WR(-TUU)///*&&ᅤijj._LPBmheeET^^EQ[иwEYXXP={jhh/7nehhHM8z)߿O_?(777JCC۷/l2Nͥ^zQǏ&OL}tzYY5i$JSSrȑ#::Ô1A}"ʿS}5e?x<u=F>BUVVyݼyٳ'3FΎZb:"}d?iݻw266Klؘ200PFFF튕 BQܹClll:n ѹ ZQRR… ڵ, ,%ݻwwuAVR/^ɓGrr2^zPQQ|}}g=b NNNزe 222ܻwVªU0|p 0S꠩ؠ055ł }`ggh1>^3gBWW?1fa???8;;˸z*|4Z6lFEETTTeee:= tmʿH$''nY8p 8,\pax{{CSSQ˃%:C%Sx 'AA H'`۶mV䫯)6̛7 Ϟ=#Gň.>___|r_LVVVǎ;p9 >>7o͛7gϞExx8<"Ӓ iii}6TUU0{l;v /FAA~7fcԨQ=zPjjjFQQ,,,ĸLs@ %֬Y…  MǴ};[GuAćF   #X,X,~g=}4`bb(~Zh^zϕ TUU(h}}}TVV***bG~+VСC>} 88qL:obb"5I|;w***7nޑ}{|AѝN\  B.Ѡ(߈TUU`cҤI8xԑɭy?>憃˗>|8=!!!BYYYtLVb|˗EEE d<ǷLv6X,mEN///ʃ <<<בF3__5t&s8xzz8|0H2%}`jWA!AAgϞűcP^^ 3f@OOވŋ/( ꫯ`hhqaݨ~gƍ4_QFkرXj\]]%'Z/]M2mGAć 00Bbb"bcc$$%%W^pww-qQ !++N7nχ\.ƌ555yfxzz֭CHH ** s΅0}t,Z>6&&rppp~*4R4;;sEmm-1}txyy#GDii) 0w `mm 3334Yۢ@Q̭I{:Lspp,,,УGv6]zO<0p0a„ )JÇ:$1mݿ_rKo[@@vu s%*66 >|H>|HQ[nԨ۷osIjȑ2~IIIԀ:VN6ydj޽rӧO).K͞=@ܹSj^*::0eEGGS(nj9j~@(,Y|||r>|eeeQLihhP@ N>ݥ1%+},,,(uuufSԜ9sbP\.f͚E]tI 6P>zܼyZf f )Gy>kAOOO]9Լ86t۩QFQ,KgddD-[q|}4ydj˖-lݕsǏӟ{<ȃ})>p}y\"СCXn]Ey233y9rrr.I&uaO'''X,p8.ѣG\޾sN>}Zhb۷Oo&񮀆Ԡwdlٲg…Xn k.,YSq-~aĈ*GѤp޽.[}]ׯ_? $$O裏\x6lxg\pP6⋮ n}W,AtWNqE ? 퓗GwCSS AII ajj 0****(,,|||f$i鰬 ,_{4i(!::ǏGff&222*#H$''x ti8Æ C\\.\8|0uඇHgp۷Odigii")0ߓ'OpE())aʔ)X~=~G vOIǷpuuƍCqamGڵk߻7nHw[ƍCSS򐖖&rqo߿[=:z>4IlΝSE Ccc#/4'1k,XYY!77ӦMCqq1ʰsN@غu+x<.] y>N???޽ŋ.h*ݝv8q`ĉDaٲeXl[)ᅲfaaA/֝ amm١pV߉(AtlvuN |E>ڸq#\C $$Zp!S!)t2Atܻ4$އSS,DRjۣsN@GGNŋ~zzzs]]]Rsi8;;QQQBJ+ӧ &&&&={6^zŸ+V`С@>},R~G hhhTVVb ^j`` t, 48e8wTTT0n8<bb: []]mUUUbof~0_>}\.dqmY竅ޟZZZ,<|@ȵ֫<'N˗/ ?~sΥӯ]L׃AMM ?Fdd$F-y['O; 466?DRRp)zc֬Yx!^z;w7oɗhL|l۶ .]˗/56>_]S\:xCBBwƽ{ӧOo-7okCm Ҹ\-G}ĨN/!;1Z>zh2OiP_~#FÇQRR!++ wPSS#(  D:q[w2zҊ Up?m97/_b!00Pd?I, wEQQP\\bF+$~: Wګo߾( Ϟ=ϙ Kg?Á'>Ç#00P$,Pj*իpPWW'kbĶYK޲WZZ*o߾d`mm-QXXȨ=; ÇuԩS1~xĄPYYzQcZ IDATagϞ555X|98vĘ1|466BYYx$Hbϟc(++o; W,Yr<彭6k٣GBlgtZXX @-ei^G^ JJJ8z(oڵKPD_~~ncc#uǏc۶mN$J.**ѫE^ꑕ{үwE}8qys̡{=#99Y4M={DVV~Wf v   {8NCCQ]]\.1c a׮]gذapuu;>TTTcЀ!Cb III"(I*&L@hh(6o  !|HMMallLUUU~ejj kkkDGGc…(**BFF=+i/Ϝ9Gee%nݺ%WMMMx5= ˗PRRuHw={YYYByؠeeeBne_ ]]]0~G޽d1et0,߇wMpp0ZE MCC4)Swww:t8uN*r?sذax)޼yr`8}4ۇR~ZSu}w7nSܾ}l6>cƌb/sN'ڵkشiΜ9*͟#Gʕ+ɑ8ݻw*mб~z|'@_.OHNNӱ9߽{3eرtΝpܽ{Wh1~I KN8PSSCbbHRb>|8d\[n@]+k֬Z:Z~=4/d"YiwvhP9eda3  HfƌŦ}'=z4JKKNi@b&6o OOO444`ݺu 6n>p\3FcbbtRѷ,"|ֹs碶>}:_Ga֬Yؿ?8N1mb `mmMaɓB+++ssshjjiii2dPGzzJگ`aauuu^ZCPV|>F2|go4i?MM/)ahhNwٳG9s/_-$x">3Ԁĉ"PijjoFn; CKcaXr-Ƥ2 01;[/:f@|;WTTTx_!>>̤/(\x022@ :FKقZnWןiiI AѽHuwwƍ2xb,^XdUer~'IQB---qU3/--LWRRӥ'fff /Zĕ~@s-8ҥŦ1}"VŮEjj*<<<s5۷Of>or[SRRP]]-4/2 }7nevvvHHHN<>Lf\i'Јötb#u4h \mU^kf@gܢMoO_}͛۷oӟ޽{Gv$h}>ڎlZm T1m4|<)))rcpD..VWWBQ=e͖9WZ=d,))Ke]lyfaҺSYZ,ڡm&aܖ}ΪXddd͛74pww-qQ<~aaa 9`eeŵPbРA]G@@@ ?};PąPD!9q . ;;[mGYYYtBvu-[`Μ9)yI}]NKhq0I^~MTK :FFF8Csƞ?x=t12_۷"yϙ3Æ qY\~ UhAycm֙S;m4z:|}}E:AammMw^xGAuu5vI/֑%٢ {@ ˗/rm6<YYYʒ8RÇHII_ 44N[f zI(^֖~MC]]P=Z#2… 򂗗?.aÆ#^Ù3gd^DR;̙3~{λٳg8x &M}}}#==l6^^^AEEbbbrJlڴ |2BBBbڵ|k׮KgЫW/˖-z:::bѢE?~<&4^<}&MB޽˗ӝ@pqq)SK(//ɓpЧO899tztt4LLLၤ$e {{{zyE>Ɂ ͛Gы > m"#""^ +())… 077Whݥ~iX~}Wѭ$$$ zᢖ/B+n+ir4ϧ[&L=[=<<.V՞huTT7oжL4@6ј7n}~333yɩSbȑc…blH1cK,%Ks8\3=cbEYY)D~'pssk("EÇŋ70c h(//DZc+ԡ4999a(.. mmmܻwVF!u꒎DBBoܸq"p8s SDGGu43gD>}usN:? 6lFEE*++&4< EEERg_V-UY#->KKKaϞ=055;w$_FBB^ kkkhhht A.0!B !. 00r ˗/pqq5kSSSzʱ={ 88׮]޲ N\Bnׯ_GUUMա@KK^[QdUho>宍y"ǏDAA9Μ9GGG(++ʕ+4hT*Ҹ#33۷/l6 OOO\|YhYf!22fffѣlmmqqzŢ;6mڄ˗cȑֆ2zcڵFeSSm9rjk׮@͛7ٵkݻXbccyߟÇ{=ضmQQQdee)e=zClݺAjt_7U+;;???oߎ7ӧVE 8h4ӕҔM6m` 33S)P6ԄY˗/ 11OOO `4R5Wg+Z7GM|ڵk\pSO)YLjr ee{Tԃs*TW^?P/R|)t:6|Iwɓ'IHHP6gU[.x<(oFM!EO[lYÑԼysǹ|2m///̙C֭?+++ǪUJiʆ j$-ה͛7s)eyu޽ggge*o?K}UjР_"""8x c߾}ԫWaÆ)?mؾ}њ;wfرzڵk֭[%ʭ[(((MLQfU...XYYǀvvv 8%KvZ]ƺuxׁKRǁر#ZRJ֌6GMO3hР0վ9jsrrʕ+dee)n 0|p BLL ˖-cʔ)zٳr-kˆAj i޼9Fݻٳ?ÇSY\RF!? =b7oΝ;-wPݾj_V@Znnn/P&QYqGDVVDDDGLL 111Mnf\tI]vzƎKΝ=-ZGI-pppg6l̈#(((>Pfoذ)SФIׯyהk >}0dxLBnn.֌?h~FVVu!**:pUWw_ŋo}sGM|}חcmm[oU1S~}ƎرcINNf9/:-4iGח,?ΐ!C>R}eN˖-} ׯW^^7иqcC- VRʖ,Ybx' رc 'OVkhժаaCCV V~vvaĉ&Mlll :t0^(ްbŊΝ;eC5GCmCmET(B!CٙPBCCIII!22]vѽ{wz=^qr 7nL֭9{, _QR:uS@K=W՚ܟkZ,t֬Y̚5K<;88~zuѫ eB!_ B!*JNe۷ͳKRR>>>`kk˲eҥCSB!Dm!B!BBqFbԨQmB!2[R?5qn?\!D핗۷hB!EAܰC!Mݠ`$Bp{nM BG\^^۷o0*$>>C!7xcB԰ݻtժM6WHv ~5I$ !U'%%^_aа[f͚ԩSk8!̙C^^]Lzz:DFFr b帺G5gpp05bҥ5CMjljBM+svIXXgΜLBZޞtZ!Ljll&B!D1YYYDGGA\\ooouFvv6۷oҥK,]KҮ];z=cǎs4r˜9s'|F㨩_Wjaaa%[.xzz`Zn]CU^z={Vߟm۶pyڶm[q$%%|rKRRRsZ{W_}={Hlʃ蛫+U^:5B!5-//M61l0\\\ !66KKK|}} '33pΝ˒%KHJJÄYp!]tK.,\Wk[lACLLLxyy`iӦ -[u_|Eiܸ1ܸqCvժUt5k3w}߾}yxquueСܺuڿz*Æ '''fϞ ={>}`kk/Ws\sݻ7sU]ZN>Ncԩ\pN{{.)))lܸ+T۶m;w_Oƍ~AA\!xDiZ̬ͮYPTUkΝtرBƍ`ǎӠA5 ,[TBBBptt~coߞ={*3˪Z`` Ɍ3y֭>… +K -ZKKK233~:K.BaÆDEEpqYf\ILLիlٲڟ8q"7?iiie>̩-?P7`޽͛WCTѣ7Ns޽~h&N(ʅ5Fque[[ !m4Maرp<==Yz5iiiݻ UOOOVZ2 IDATEZZ#((s n W_}IOO\^k׮4jGGG&v$''+uuZTz^u֥wޜ={J̙3zj4i;vŋϼXZZҿy:S_/OlذAuU2q쌛#F ((H9btީS3f ... f $$$0~xZlIݺuiذ!#..N9رc9f͚aee~~~;v̨.ڸ8F=O?rg}F_>O=_|E}]x1x{{s73Facc/׮]3cÆ %z饗:._ y&L`ΝK ???֯_^jL2ƍGdd$+++իNcJ?s+({BQ 7jԨ2p1Zʤܹ'|mұcGfϞnnnЯ_?:ud4k@ s)R<ͥa?Xt:I999舃ܾ}D M!MIIaΨQxWݾBW߯6t:]޽;>>>4nܘ޽ٳZ-: 6z{ , 1_@ff&F })jej{| K }ӧ\vMrunJϞ=W/ٳ'7oѣG_صkرL v111{RDGG+Y`Ο?ϝ;w_1b'N(w}||۷y&;wdʔ)JyͶw}^`ApϏkגSj?G֭[hZׯϥKXn< 7oެPsnyBGqŒRPXՂN>n̙3|Fkp!?T/5)R<դQ5uJKLNN6@nn.IIIV񊦐N<'xV\ն/*,, P_R^=bcc ŅaÆi&K޻wX^y\\\:t(6mу DhhѣGkŲֱg={cǎw^ϏٳgJrr2AAA 9#hsZZM4iӦ 233 5RW6~Lo4M~ݸqt=U 2e wޥE$&&r~4h̙3~M<;w;޽{ԩ+baaAtt4w!:::uUݺu%33͛7SXX[hزe ,[BY z\|YYn/֭[*ںqFm*˽I\ 8z(}2[`=}4yyy$%%ٳt钒%P^}O!j'B3JA-RV beST[nMݺuiӦ ;vEh4rsssN:sΪz)jSDնQL ijj*gҥ4l[[[f̘Attts+B_̙3͍aÆU}!DiРر 6nСC&L@f#"",)#Gxiٲ%ddeeѹsg}Ν;ykf8pK.7#^z+ pssSC˳惺+++o;;;Ȓ%Ks/_fݺu16mSOdO~G>yy7TǦ}sĕ+WʪP|ŋB~HII]vٳG矹r 'xyy+~-UO)juSK}2dѬeSsWw_ŋ%}sGM|}חˆR&\~uτiiifgtnJZ֨E[:EiժQN̙3P%?(,,4je˖Fu-[baa{ٲ噍[|PčB >>>&7,kY4S+ϹyOuҊvA\!c)((BBBbF)Pv 9E)xzz2`۷Faִ-RXWZZM6-5vP~xhюiӦuSVeh4$$$`kk[)J0Ѡzu/^L:SNdd$9r"""s]]]ӣGkY\e9x`efRbVs=zU*ә>}zZ}yݓO>i!K/d\j/SeoN:FR)))%Ws!** &?mV|/W-k9݄2z] W\Ef\[M0`| nZyyy[|ٴtqeW^DEEʮ]>|xVWE-{BQ;r Bǒ-ZV5CQV bu$77իW+)E*hJeSDQ{Jlٲ%fƌz/^,n-T,OOO> _ػwoBn΄ߓ̒%K(q&pE8q$~*C4hCSxխ[ev۷4ݻwWVرcÜLVXwgٵ?#o6ׯ_'??sA"##y&۷o'66l]tQcccBkhF\\,[%6n܈/{رc0qDƏ_8VwWWWmVnUHyBIf !{&L'e VgjOOOOzz:Çg޼yFIq42)樽?S,y뭷 Q9s&m۶Uv.m~g?u리YDmBkg;SfYjJmO$|||(((֖e˖ѥK~!ϟcǎ;v^z駟۷4hU4ȑ#3H׮]ùsR+bժUQXXXbc\KKKVZj2%>SN֭[Opyի*3M-KKK>&MğmaaYgXwww6l@pp0ofΝ%2dH۷/gq]oVgii=!dW.T jR+:beV͛7n0:Ã&hOs)jT&S,כ):C)g̘۷ݾB imPScԨQ5ꡭ_oo ܹs9x /"?#}qqqdffRn]:uꄻ;'NT2dǏg|S^=:ul4͑#GXh"''ܹsݻ꘧LFaѢE`0Xz5;v0nlE|бcG.\ŋIOOGࠜH߾}Y|9_}.]Vs=ǫZٿ?:'O~ۛӧWhfoE=!}dW!D8qƍӺuk%EtѢE5B!#iѢEe~֚;w.s-q{K.e>/ҫW/KLppѱI&Jv]tܹsʌ]v)iР5mڴaٲe&/o{.uŸq*Uoy 5?C!ă%B!*T裐b+B!~.]wдiSsfV!jBԩSj}S)Bmy_,%%HvEWzB!^>|8ǏXXXжm[3g[BB! DFFɑ#G0 :t˗^ߟ=zpLFXt0kmJV gSΝ; ̙3 UQC7o^bs2!x B!deeMDDqqqܻw֭l߾K.tR.]Jv;Ν;p/'00utveΜ9ÓO>i򼇵B!EtB!BԴ<6mİapqq!$$X,--%<۷gϞٳZh",--,] sRRR8t dgg*e$''3fdee1p@<==O撔Djj*iii,X\;wކ2럩:uDrr2~)mڴ!99dLRq !B#)!BZkѢE2C43rHuxzzb kÉĉ8q˗qvvt';;dڷo{s? >Ã_99{, 6`ƌL6ŋ 77Wuݻw9p:uRݻ7?̦ӧO3l0n޼INN:y橞Iﱰcǎ|ڪժU1n8eƑ/j0~xN8Anh޼9M4aժU_={2rHԯ_\!Dm׷o_~G{=ϟڰ0z-|||D׳b _7n|@fff/`…888pU[~pyI=>9{,ǎرc5[ud`(A\!0cݺuݻ][s `VfG)VVV=zD6lHTT]vxxxЦMMf/jL<'xo30n8< +++Scƌa.!%,,̹y&v"22{Kll,^^^3|pu=ضmQQQl<=zСCnʠA 5ѣ=˗/ 11OOO @@@rN?hڴi%** ___&O̎;;k4p|h0 jO`0,^h+ 0z FZ*>>3gΔt2+fzv @FJWW heS<j!!!ygNE_/nnn 6L)W+ՠA'//DDDpAǾ}WÆ Cc.`~`۶ml߾hsΌ;^Ovغu+[n>8p;ҪU+lllW9}Ǐnݺ^#F kSOrJ^}UZl1c1\xs /駟fРA7+++0`@UW\!++lO8q[l҇Bjk򂂂 $$///F `4p ȬMb0n͛hHHHֶzBBB:t(Ge֭ni0F`0Rd&B!xi&  !!!bii/dffܹsYd III>|P\\\8< .K.t҅ rj;##_|{{{7n;7nP_~tԉqmhdnà߿?3gTHB6tZj:tgggf͚EaamdL昋T|aVV嵥eT=ӼU_6mҥ [l 998e˖ <3fŋٿrի9qk֬!<<^{ #~r YYY5ҾBTB2ٓ͛7W[}-s@m Pgܸq̟?è2ֶLD4 F1h >\f] ૯2^iaH!~饗Xvm\|Pܼyh֬&L`߾} :7;AFk4Yl|7oi߾=={dϞ=҇EaiiIff&ׯ_gҥXXX¡CHHH ;;?P) $991cƘl'++'| 77$RSSIKKcʆB~)mڴ!99dLRupi֬ 6$**?Ǐm4H&xObb"W^U?!SZZ{T7_y&L@^*|ŋ(q_~t:mƚ5kt 2Du۶mcƍՋ`zaTν{h۶- 6N:Ŝ9sƆ>}ѣߜO>Ҿ}{Z-W.wBQd9!R*OL6˗/dɒR9$''Ӿ}{ +3裏?L{9<<}ӓׯ]o S͕̙3ԭ[(ɓ→3f'eiiIf͘1cgs-[ɓK7u̥韹P4wwO߾}I {иqcBBB{9,W}:t/0Z+SGjӞ={@բذaCcj#5&B BˡC|2ɼ%)+򫯾bʔ)ƍ;v͛gvMRRgϞ5J4B SD͕J*7׿N>n̙3|C)7fO^^ov6j~PvrSwؑ~m۶)l޼???e6]UիǏ7:Ƿ~WM?s)jg*ż~?BCC(q\ 4V]v`0?חzKHH... 6M6aV{+ CeӦMܾ}=z(3J Dhh,ܣG*1w{{{/_ٳg9v{%**ƥѴirKTT=z0zR|ãTRSSWPiKdggٳIMM%99*ݜHU$n5ɆMB!Aq„ɓ'ӬY36m;v(qNnnn3y׮]KPP& /`tĉqqqyxzzx(e֭[DإKҰaClmm1cI&\ZnMݺuiӦ ;vEh4rssU_<hJkk1L_3gbX5?ߢ65ŻrsJZ{`ĉjٕH#33\V^͈#FR k׮姟~bʕ  ݺuh4|?܆L...XYYmUɆMB!AB`֬YQm۶0ĵkTqUzmYYYxٳE%999zM*/[hkU2JT,,,(,,TXXo>>>dffYbu֭]~Fc4B)wx"VVV 8Ш}*k{ژ-??{{{cYYY%gbnO]㴪ޟ5ƍܽ{Jۏ#{{{ "++h"""#&&lll[ndgg}ve ]vzƎKΝHL2\?~|8SGZZYYYԩS(:tJȦMg[n3sLڶm˭[j̜9 Y[[[obo-Z࣏>ӓ-Z>ktNyo3#>cFAAA|}z]~+x mO--յ*?*T?ӓmS]6m` //O,dyoJA6U=#F`\xuuKmooO~~~sݿ"Rss 69}1ڵkWj_㫊g~~>ח*ԩS:u*DFFɑ#G ""B9^Yf)!Rmh"?P^𨈹 LmdHol6}tO^⸹'6 !YNAXEբjAy]4`dff??ub9i$6n_ ܟuvfPuML )ퟚDlYڴiC.]5rau|?qD6mΝ;0aB?}2%6),,$33O>3fwڕ|ӍE̥_Y)_AAA:u/s3}z '99%KQXM !BLJB<&LOj 4̛7oWK111:TKݵŮ] 0*իCVVV?a#!)!o`9~x]W>&OVPB0j(6lPjÇi߾=vvvpݛsz}DD]t֖VZn:ի 6 ;;;={ц~v˂ M6lْӧOp郭-\~-[)uy}kСC_j:tgggf͚Eaa!OF1uT.\NCӱfptt`n߾]kJJJ  wwwF+*Xl'O6:Ov9r$踝 <~qAΝ;O?ގ;Om6^}U6oތ_zxy{{K7h vɐ!CT-?2+L ⊇ñcj:Eu ߩSťCBQE&OLf̀d-ڵk䤬9uC2i$v͖ڵ+w'&&믿r,,,ݽXYYgĞ={V)+>9@c)߿g*3f`ڴi)|/XYYưaÌΩLEN:5Z*Qֽ{w}]FAff&qqqX[[_fÆ ܻw呖ѣOK֭ϯPBB!fE /ץKh޼yM:ʕ+iҤ M4aL>]yW4 a]WIoVVggg嘋䁹t:]cׯ_G]ߢE ձQ@yg<<33'''mTj#tδ44iҖ>jڴ)<ձ۷oFӍ-`0yh4oܿ 7n\ͱ'??ѣGٺuk6gĈl޼/Tef̘- .,|ׯ̺B >nݺEHHHM!j^xAq?yXUaPHQLq+ S㚙vM!TBpf9cZ(! 7T}91AZܮsY#F"BQ6+++[[2 R[ѣGcaaʕ+2dHs:uꄟ_4ƍcСҧO9~8ښ_~E駟rMyLaɤ~QPP@aa!b-[tv:[lI߾}dɒ%jRSSpB>ʻLJKfRRRرcqss#??7n,^'N?ri /H۶mu{wiڴi4PyĉdeeSعs'ǏĤϞ=!SBAqCuĚ5kBzj#F"u[FD!C|||ppp֖^z:k׮ҦM,--ywbcc>}:[FKQgڵ9[[[:vȀ*>vvvxzzw^?u3gd޽4hЀ]2k,ʑ#Gtr~縻Kaa!:tuQ^97nܠM6ʱ3g2sLJ/` !GqjZb׮]\ra)yBCC !̙3ˣ^zE!8e?~AWTx+,,24 ;w,Z}III嶹rȑr >|UV|1c3f̨rqڲnݺ 6\P>GFFr!bʔ)|g:J*be-fff믿LLLh49+꣢rssILLdɒ%5rC6B!B!BCCq%%%%ѻwoWf}HNNY l,]2Ӊ! #;qB!xD6l Bnj Rѱru҅TVZM0ĤIt !*Ov !Dn݊c޽{7ߔ:>m4Z_!BeM[P]666رcdgg+jkd'BϯRxsuu%--5kLZZiii?>033#33,/^3Fŏ?ȦMXbW\>}[npB<ۇoq}r/\@zz.NVaŊڵK=44\._oFzz:ͫ8B!B'׭[Xv-~~~4k {{{ILL"ēNq(Ï?Ȃ X`,*֭[RҠkIJJb̙ԯ_Føqؼyy!!!abbBhh([ni?u<ձc کlH{a阛~]vxb6llڴR!Bh44o޼JjRDD3fx{_pԤ|>s'""$>|HNN{{{&Ow}ÇJr IDAT5jÇ9hhhŊ@BbbPtaZn]1f͚;C@@ӠhZ7o'++K缒ylmmm,Տ666ܾ}{X5kk֬@Q}*-B!ģwUΝ;GvGXXQ _{o(}vbccٹs*̌~1l0 ..xRRRXr%+WCѿN\a?< _W%Çuڵ+* +++󁢝Æ S]6oޜ0.^smHH~˰aðArMt^i4irݟ_QFq)N:/I9ORj4Z?;v;vsNy7nJō7c4iD缒iiڴi'?? }(,?##C9V-ZR8}J~~B!Vm۶4hЀ͛3c ߿{yy1e郣# Щz}i8<]v ___ ܹsDD@@[l)5k{/V:?խalwe۶maggСCټy3w套^bŊ\~]v1vXlllh۶-̙3:u~VZq5-[Zbٜ:uSN\a/^, ڹsg4iرcYlxyy/pQ5;wdܻwSSS5kFFF6l믿? @ݻlݺV[Ut䲲 KKKlmmh4γڵkdggӸqJ >, 4~omm/̢EOy&Ѽ:ׯ]#GR^=V^uƍ:zK:{,ΣSNSe7ʕ+T5CmذAU$=z4Z*k׮ҦM,--yw0aA&''3~xrssdȑ?~2qsssILLdɒ%^_R'''8|Ν;g[[[֭[WB!'^^^9ro:پ};QQQ\|333rrr9/SSSחG_LT*U:%UN!CͿ? u=իܹs/1f՜W:5ԔnݺСC.\H||<[nѣ=z7|oooΫZ}IN"..7oժ!!!W>INNcǎCA+$L\\7oFVE=z48-Rܿ* .a<ϟ_n[6mjLLLطo_'MT\B!(((e1u˖-0z{{{ٿ?{V쌻;->رc:tH~CDǐks*u$ _06JŠA4hܹ86m>>> LII!>>X.\hݼƚBqQU֖]*ǍG\\yyy__~h^x-Zpu6oLRR@.ݍ7G}Ty甔2S2[EJe*ؔy޾ѫ`B!0~!>>>888`kkK^j dLaa!>ӦMieԨQʿ맳y@_nݺNvv6&&&$$$жm[ 꿶oH z }?խQWXZZ2d ۷ٶm޽$4i< M6ҥKʵ͚5#((1111LEZZZmP [oiru]huIHH9믿֚+M66mTꫯ*6L9~EZK*TxjF=_JjmLLs5B4ikf00YB:Jp}222*& /~=:55#FRm{fܹ6m͛7߮B( :!ܗ_~i;uBu/ YB:ի;wvUx^XX{DQ=z=m۶QXXXj`ٴoߞ'*p..ذa B!jBQ IIIښΝ;ǚ5kt\8}4nb…FZZÆ +dvťK~.ÇEnn. ,`mHre~7ә7o^>y_HNNYfo̘14jԈ,~G6mĊ+aÆѽ{wxwIJJ*G^^׷9}r/\@zzz UVb vڥnB!Iv-֮]͚5ޞpum%ēNqܺu4T*XZZ*Ek8}sQ^=qqqJEnnrNHHfff֭[ktS^=<==9<lcuؑǏ۞[Nn}ٳӧcnnT+sBt`ƌi47oJ2jƺƞeƯ=ںu+...?DDDÇ!::cooɓxabq!9qO+VEÇiݺ5fw! L?gݾ};QQQ\|333rrrV(޵kffr8g֖g t),, 33Jzt~6Ty_~}*}KTqj4o\9fooLBR;88ُZp,n߾ͽ{j,7oq}*sBtxR Z{QAA۷o'66;w*ׯÆ #00 ∏'%%+WrJ:t(!!!xzz!j,>ݻ5ZFF/^B!ē-&&9s搝̈́ ޽{s|}}޽;?#)))ܻwD\]]`ܸq#^5#FPҊEΝ9{,/^}l޼YyM\~8%G&**J0qyFř3g蝿ڞyzzҧOJg3{TQ))) 8? weW_)5HLLLx饗&((HI666̛7yO?O\\,[e˖DHHÆ ã n #F"ꢼڢhtILLŅg}VZ~}[B<<<-[пJűvZFIzXz5iܜӻwNgggYh|ǎСCeJ-[o߾DFFdj5\p,ּ,ZO?7o믿̯C0ydؿ?;w͍|nܸkg^n,:uϏ>RtR֬YCJJ ;v`Ĉ@?!x9ONN8::ҡCFF2BBBظq#/^ue&MSژxFDDDEGGcU)YܜÇY͍WҳgO4iAPT!)) VK.]Q53;;;y&zёSEuzɡC8r>>>:v뿶/b.\ՠ>+K=(FWWWغu+fܹsOxxxpe*]2|p|qwwGeƍǓFTTQQQB@@A}fN"c䮰^{͈d͚5<3F1zhZjUxrr2Ǐ'77KKKFI`` Pj~ҫWJCݹq_3gN{Æ Yd iӦЭ[7ĄڶmKbbAc2j(lmmڵ+؊⏍enݚ4 ӧO7o}[~=ǏI&XXX0rHL\qFƎ˺uPռ⋥ưӓ{2|2o+\8MMM%##y+^cǎ 0@O!Gys ۤmqesb%0(ɓ'ӧ9997бcGN:… ٿ?4jԈ>}0|y^~e_š輡ohѢgϞ%>>˗/(oӧO,XwLYs+yoիILL}f͚R9E/kCyoTUCu@T@Z:_֑[ÐU*ZSSSu@Xp!lݺGrQ|M>|8̧N"..7oժ!!!Wآ={c 8._L=c,_:0rmذȑ,ۧI&+MBQ l2h49r_~uzWʐBa$N2AAA6|;T^{{GòeXB<ɒ Ic o~-ҁ 2(ZlY浯 @:u 60fcAvv6ٳG٭{}ƏϽ{ppp899q<==ߙ>}:|M]^=Ki׮>d֬Y,Z_͛7+)馃& IDATҥ ۶mΝ;su*nMW\1þ}V6~MnynC:sڂaÆ)mT۷O9>bĈR^xԱgyF h[jMKKӉѣy>}(4hfffBׯVr/}xҾPmPPG5s-{nA)/_\4ؿ~g݃<ˊ+h.]4CNN6::Zۯ_?QFpmRR g>_UyLt4Suq0vXƎKVV7oF燹⑒E'XPP *3___wΏ?HJJ #11WWWOOO飓2%%#Fɉl̊ƆŋZ{fܹ6m͛7߮B<~:t(;wƆsαyf?7o?<*{O9k+V`ƌl߾?XYY^x-Zpu6oLRRoff&7n$55>SQ /#7nϐ:byxBC"S,88777>2s=GzpvvT*ڵӰaC"##4iREԪėϞ={駟077Ã* QWvm۶QXXXj`ٴoߞ'ҨQZA!ɓ>>ryyy\7qD"##ue޼yL˖-III`ժUmۖ мysf̘KIIɉ^{_~E'(}D@@[l)8p obʔ)GGG Pf^˗^潳ۛ2ۡ(ٳm/ϕ+Wݻ7VVVx{{ĉFEƍ%"";wTz!5oѢEt֍ƍcjj3<;?*܉#PXYYݻ+%_sTT> G!$$-Z_\]]k&_Zf׮]xyyajj?@v픔B!*'++K}.Z 3j(0aJ͛[Z* V[@ˠjiѢ*ӧOceeU4nJEFF-##CAX˰aӧ=ϏΝ;WR4lܸؐz*={ٙI&H1c`ggGVV7oޤW^8::2uTdjt҅طok׮-w~~~~lݺ3f.OӥKtRk7n^n{\\qqqzږ^ 66}-k<==J/wZE}BBp-lB\\~-j@U6 񤓝O+++4 KKKlmmŻ^ թS'z2v5e˖ۗHvT|屶LJKRXXO?Ď;4x]t3gbjjUϩK]*5 >}F'))3gR~}4 ƍEX9ӓ봟:u KKK}rر#Ǐ/=77̝߳gӧO0ŋiذ!VVVDFFiӦJ#B񴈈(K[#*hh޼MFů1'&???퉈 ))Ctt4ޞɓ'wCc-DE'ѣ/322lڵ+iFիnݺƍOprr_~˃hݺ5VVV 8+WT*]K.akkˤI0`@7}GPT:;4VyKoNϞ=h4899~zݻW18uHKhР򳩩)Qecc۷k,q%B!D #00arUۧ9$$$ꫯҬY3F_Çׯp9ϟ+7odʕK8::׿#GBN _n[RR+*fbbRkne.Vl </B9~mg[[[֭[W*āϑ:y4 _<)N,baff虗EUp- —_~?*SI[%ũCnܸIOOI&GV_9XXXhq5I}"O}) xyyy1qDƌa`ٲeJ{Ν9{,/^}l޼KKKœ9sf„ t];%%l>3gǏƍ155ŅݻwzO>,\F_Q3d~¸޽ݻ㫯RLLLx饗&((H)B EH͛Ǽy駟'..T-[Ʋeprr"$$aÆa QdWTRSn޼Itt4}ύ7J.vYSNU*Wm'k֬!%%;v(K>Yd jT.\P !B<*sPQC0DEEaffFff&>|U:gģW+eT*]ve&R4ѣlܸx҈"** {{{\\\ەj}V:!j, !zGUVoܸcDzn:j5/Nٳ Ciӆ^z<~!>>>888`kk[%SXZZ;0a(uIzz:٘@۶mILL`?&M`aaȑ#2eJen vvvxzzw^^9~m I}2rHlmmرcOON֭)((@0}J#B+sY:,,,uiiiiӦ:U}S O߽{Xp!˗/tȝ0af*7!<==dѢEyժUe7e>2qsssILLdɒ%#OBR+1w__5kdff?MUu=>?cܹ+S^'O2l0uޒ4Vѣƒkt۷oO˖-6dٽ-YBQP/^uI,]*=d !l푒 0})_Wu: j?K.C޽ hz1$֙5ԔnݺСC.\H||<[nѣ=z7|ooo9ȩScƍ)[jEHH+Orr2;vuBYBQ!WI*W!D*~e|GFF"5*^CUT!11}YeoKQ:177gݻR_ULڥR4h ;w_9x ӦMLJB)))˅ >5XSV"Blll:wLddR彲4 '##J{PM#""a5B!&m۶R9۰aaaaKQ3aW@_!uͦNԩSKuLʊÇ3|prssٲe 7ndϞ=呗GF dK;l!j,Jz*Ν]vO5%,,L?b6la!DgB!666;cǒ͛h4annnxd ĠA3fLC{?~)<9Ӈ VjUVGqU1bQQQ[E믳}v>|ȫʿJ'j7bjj wʊANNNNN̙3+}ҽ{w~GRRRwÜ9sf„ t]g|///:wٳgx"۷gXZZE9Fř3gWjO5k;!B!?~mmڴaB<4ikf00Y}Jݻ`aaX`A.\յ}7lܸؐz*={ٙI&4~hh(\| Ƽy* /8gffbnnÇW3\]]IKK3(Btt4;v͍\#TTVcǎq1#G#Dݖnx"fԨQ0a|}}:t(͛7WP~ήRT*Z3[2d_~%T*NJAAߢE T*OUyZ w:;U*Z~j:.MjuYFF5>B!H(j R#[nebff?Vbر3 0(5\ȑ#9q;vE4i҄UVՀH+3335kFdd${壏>bx7nLZZ0!dΝ;'E$Y}XYY)#FSu1zh,,,Xr%C )uNN+3=߿޽{+ (,,Jō7زe 7h-[ҷo_"##Yd jT.\GyEqqqgUv֯__;;;]Fvv67T쌻;->رc:t)Bt+/j R#@_ BRRR޽;;wggg>#LMMH NVHJJ̙3у#F(ԀI]tpWm۶ĉHQ2!ʑLǎ7v(BׯƲo>lmmywׯ_B!ē9tnnn|WθJ"77Wxb6llڴId Jw~~>{a阛u U[y5 ju=! ;q`e74ũS2uRg̘[No|_011?/=))ܶ~¾>bڵɯ5h 5jTaB!xzZ#ÿ:w͚))LMMA5jAYjj(j@Ԥ|ju/BOqx̜8qFsqy8@TTB!Dah^CjTV5F!5 qƨT*222tjinh tȑr߰B!A)|2=zΎ|r:t`찄BQGXYYhh4:54uԉz̶5jR yyy@Q]v)$&&믿[@_屶LJ;)-*ڮB*,-Sa"VR3Kf0J254Ț eRr(mxx<=|}?ټqY]QYYRvuoii)ʄTTT //7oƥKP9!q0&L i$w&`ll~; 2e >:/m{ᅦ=rrrPPPDFF{pL:t , JJJ`hh 퉉1c|>%AMK{nxyyw9r$V}p\{@Hs)xЫW/]v&,-- Z_cǎAYYYa,%&&;BZ$J`o닋%hHs!s$._OOOyAigƎ;I#^_@ \\\`y}] a}k׮PZZZ%qqqXĤT(yA!#BI >~HZ.ABBرcBH#"C E؃ם IDAT$n V}ݙ3g1ҜZn-HH}xxx ""BaBAHH̙#0!4sbܹBG-*yw)hHsqydffB[[[ޡB!|l"!SwB!B!B$$.!B!B!4c%B!B!fB!|b ;z۾};:t]]]̞=[>>>ׇh"TTTppaÆsĄB> w̔w͛dѣI4i9HMMI5kƍ(**`ff)S`ԨQrMVkԩ߄]v 4ؾ};lll^|qqq{.޽{ػw/N*t~`` ֭[ǾWVV: `ii  n:K.HNNf7lHHHScƌdCOO{---zIn_$PZooo\\.>Ę1c`ll ???Ojj*444p!8::"66}cԄBȧ!Ǐ %p`Xf"j<7n܀co޼Arr2r[[[p8hii-Ǐ#44dgϰu:Y^^"!)) {yfϜ9sf#$$oHHHUT%e?LJ>%oI~@NN믿uB}0tP]=fddcB}.\obǎ Epp0ɓ'#((JJJrss8wf̘L(((Ctt4*++1~xl߾wg0sLϟ׮]L (**::: HNNFYY.\,YPRRBCC1m4g]nn.OׯCQQx"k̄ѻwoCWW%ʢߝ!ڴi#:t耀ƾ}juҒ  !U(ۂj 3gΔwq(O?!Ƴ~z6f 2HIIALL/ɓMoC|r@7=z4{\[[Fll,~gƤI3g`b |Wعs'6o H1.\sl4?׮]*FUV S蘽=DqqqlIMMMDFF8p 1{lƄ W_}joPVVFzz:+V1&L򠬬X' |gaooVO?-[cʕuBHKUVV@Ղ]vaƌr|BiF<<<LDDD}0ۏ!S]2555fҤI=믿m/_f---u֌ =rckktЁQRRbZjtܙ>}:%qT[q1۷gv1̭[Da޽L׮]wӧѣG 433c0:::LeeeFRlL)S}KJJDUVR6IRRRhjj2ZbkײݻW}lիڶl¶iii1o޼a.]Ķ={=?}|||89w\q|lر̞=x{1sss7(?5wCጉL/̸qa3֭c555ӧbϙ;w.3g?QRRhhh0waa233̣G/^dEСs5c^b8V^|B,X@d̓'O2 T޶m0 3 0=CM AcM8x·W_fFWWeaRSSLzz:w˖-AcdžbnnwR[޽c>r8&,,QNm?_ihc3Bibcc jߔ)S6i8;;Gii)޾}(ooݺ7nPUUӧOg۷R:ulmmq 塢/_ɓ'akkӧO}݋iӦ!55޽ý{xq ppp-[puvj9_S޳ ̙32]z^8884Uuj;uիW6{{{(++Μ9S9{ !MƆݠiС}xL V|}.]Bbb"tuuaddahh} sk9s:uR0|px]0 ===Xǎ/җCVII]qn}m۶Շ ?_5_Y{xx ۷or-C!M 'NĩS;w 88 vwF!t38VON0C6|>"##}vgiiӧO}q@MM a7GD`` 6l؀OĉBtVWQQYf8z(ƌ3g`ĉlȑ#=R`رطo/^?eeer \PUUŸq&%VEyy9/_PPP#G0j(DDD-?kssзo_dee2226UUUBԖ눍ů&CBB0k,*6c5%&&;"Ell}2kffpssùspeAaa!ƍcǎ ())a066F>}pIdpTP:::p8x{M999[C ڼPP7-- !&p/] KQi"%zغu+<󖗗ȑ#3prrqi`uK.:ѯ_?<߿Gnn.x<ݻw#''eeex{G$ƕUǍ7n <wޅ@4h_;v 99Yh/n† pyJKKqAܼy&%%jguĮAb궆&Hҿ6q!K܆qqŋqzAH}ݻSL*BBB؟Æ ?ׯŋQQQ-[`ҥ‚Mjjj򂫫H?I&w\.z>~%QVVƙ3g?Xp]v&,--# IM \Jy$.!4yBJR͛7>~9_ H{tl022©S0v(nvvP[۷Z5a_q%*?~ cccz=zÇ VVk{VկՠoڵkǾ.,,dW5m۶7ZZZ"IBXadV])jqqñcǎzAH]ݾ}[~|>_K+oEV/\ .:vvP^^.b޽5W q9Ĉ=޾y}-_Mx<]ƾ/H}*B>&&p(K66#իWBIRVVƖ\ 9}eW:N߾}?pQ}ǎß ֡ٯ^GիW}||Я_?].]?ĉlݻKɪ.؄kpAbǎ~gffƾBee%󑟟/Iڛ7o⤦ܹsĉb nݚ}0 _޽PSS%'']i+)B!Ҳ6+@DFF$.]Ė8pfϞ-666r(**ݻ3|R$1(BCC1n8TTT]l`5k0}tTVV}Ǯ:Ν;BW,u-%%Z'޳PRR?3g];u2!Caܸql[ffH? =Gxxyݻw_ 6VzVVVl۶m۰m6U 6Wj8!|\d-E2|phiim&phE.JBH SNoooCϞ=Í71o<$$$͛prr?d 7n@PP]"p\aɒ%bN6 @U2733駟n:x\.a;v ׯ_GJJ |>TTTеkWT+65=3f ((YYY055Ś5k"Tر#q%bkgօ"455a``KKKVߩSb7_066FHHFBjc˖-2"Tw \J䒦FI\Bia "5_>Ç􍌌9fee7o-˸8~8̐®=u;-%,--kSZlYMl۶ ___~!!!Oo.]BBBݻǖx2O BPPP歬o:uP}(:Ϗ^8ƍBܻwVB4f|;&*+@\Ҕ(K!Dn>} +++iڵCqq1[| IMqqb/1x`\r6mbǏѺuk,[:qƍ:@O!၈yAHջwo$&&eN P"4JB}}};oFvv6abbGGG,ZHlՖ4_}۷wB8wuB.]piаAƬuHCPSB!4?D.i %"7h瓇3g4 HE~v!D:___hkkcƍBQ4 1mM<<?cEF!DVD.il%B!4Khժ';?!i%rIcRwB!#,, ~~~uVѣmo޼a-,,?={ɓQRR¶GDD'O:Wqq1`XQQ. ___MoСCadd#G CX[[C]]:_Z|_F׮]~E6$"]S'p a]v5@I\B!Bk̘1000',IPP|lܸB}233q5$%%k׮e<==www`Ȑ!ppp͛HOOGVVrrrb eD?iiixfVCqq1/_sŧGbHKKChh(RRRm6'O%)BH3RPP8r:t h!ŋ/_9B˱|rG^0p@m۶5Bddd[n3i$vsA_?q/jfeex455T2={l[Nkeee=zHKKý{p(**Aeo߾Xj\\\WBMMb'c@<|c4TkҼ;+@HC$.!4#_߿`9GCiL^|AlDĀaXZZ"""ٳ'222SDdd$ؘ͑=R7 4?PY]YYPPP;VX!ReϞ=8{,{3/^'TTT///@DgԩSJ49s0|U_8L8r4r)пyLK#ZO! o5Fbb"f̘>555xyyU ;˖-c>s䠠 Dqttt'''ۣw8t,X ,9/Α#G0m4ݻZZZ"HݻXhOi ĉGqIqϟ>///ܺuK(y[S;P劷7 h߾=%ׯwBEE>}:N8Q IDAT!ĕTwZWIIIMm777PNZKrqqApp0ϟXXX4@տY/^.$qo߾-i !r4g; B!Ԡ!C.\ .g_ĶݼySAAAB }rw^ z߳gOް8⳰@qqбEaѢERc"|?Ɛ!CЧOβU$(((0Bu;v|~N"::AAAHOO&MmAIj;^׺䟲Vx - B!BH0vXdgg?3 1w\v1VM@UR6==]:::p8x{,''89^XXqooo}z]댌 ddd ++ YYYbcPYYN}:_EEEŋ䡡Fvv64440ydAII QQQXd ˡ'''bڴiغu+~={Dzz:ѯ_?ڵKY"""l2vvvB6660`DXX&vPXXXD'222.Obb"Ο?4<lW_}|Z /^y$_igϞ@XXi?'r* [[[U츆pvvFRRޥKj 0558|>gҤIPRR<<<Ր777(++UVbk=ywҥK;;:tk?deeظq#455?~\?i^~˗/cPVVFU !Bi;K \(KEBBVZUVIM>~XQIڱctuuӧOߟ}&W~mo߾pvvF۶m燲2=::!x<'.X%%%XEEۧm۶k.:*I6m)//Jp$ߚ4ϟ? ><<ӧOǻw 0o !DZ)Քdp& !ɓw{B5V"PM_}1###9ESw9vAO2}h.))ZSvBZooo8;;舉'M---~mZZ0x`xxxƍñc8.0errrЮ];3 S_k׮A^^ڷoq}}}p8$%%58rss_To !%u͚ІҼ4t\JD+q[]bĈB͟?-MaAvkkkp8vvwwGǎѪU+BN4?;ѦM˗VVT!8{l0 jVaҤIOիz*&L---OGzzzPVV x<|||7߰ `ffvɼŽCCCBMM \.}/X*~a"/\O*** )//8^x'O:vލ<|ܹ...B;vell>}`Æ D\\_Ӿ}{<{ B;uÇ# J@|'yfٳg4?!???ܺu^>|fffPWWGΝgT 0***9rЗ_޽;ڴi===,\P|7:t(Dί˅/JKKuEDDY 5CX[[C]]:_Z_F׮]n?~<|}}!%k%M-Tjj*Ο?/GnڴilC?~~0a444p9X[[ѣx%ڵk|y666^ZY=z@߾}$7=|9f_+++RUt%%%M|IZ^}CGt] IR|p8Bu ײe˖a055m  60`#"5ri.'JP>h xxxoEii)v܉@.r`ʕ֭<<<(JBiZ PVVh߿W\رc {{{ر#W}6Ԕ:6ֆ5lY\\ ---U5pd#GDll, }H{W@P7B!?nܸ:%pMkCCC1w\wʒ܆PSY6tfM!#Go߿߽{-¡CЦMX[[#00'Nl̄ 7H-Txx8ÅClU%%%6l˗/ǏS|VVVp8`۶mcw}5"##nnn"K.58PU666r(**ݻ3|ߗxfΜkײǨ!B>Zjq<==)RB.\> Ukjrػw_cݼySj{MSdFuⷰ@qqбEaѢERc"cciiӧO}q@MMMll,~W6Yf5uD>\[VV/R4 #Suuu,gшäI"ٳ'|}}!!!xPPϬz)ooosnp !"mIHPUUe_ ŭ7?TTT(KJH,]`:u*CA!B>]↘CNBm CLL ߿{{{r,PVVFzz:+V`ݺuB!T]lVZߋp~* A 6!%q !#sssH;oyUUUQXX t :z@CCC8;;#))I_MW\ׯqePVVFŶgeex455TК={6%q !BaʕիBCCqz ׯ̙q5| B)BH3cBWWO?>--Mo R`` gggm~~~BN$::!x<'|y\_AAAc_?@#RӧOǻw a0 LLL㑑"%Czz:޿϶*\BHs@+q !38q"@OOOo}6c<:VZZZغu+ -- ɓ'fmm WWWt jjj'Ӹ1c|>WWW700p\ 4&KҮ_ݻw \.{ȑ#: %%%044Ă j=!4cƌ/  2/ˆlyy9agg<|"m(I!O\Ǵ9PԝĶϗ_.\XcMCir~M'Ku؍ڵk\.[ !˱|rG^0p@dS.]ЪU+p8x-m(I!O\oߖǴ93gBBBзo_yB!666^}H=zo߾HHH@RRRmڨPRR{BhCI7I\B!BBjʕۺu&y!#66V2DGGcr Ǝ`jpaPB!$JBJZSey@!f۶m8~8|||^Di}6̔6$QAAȑ#С!JLL@^R%B!{iN~AAAݻ… 2MJѣGJ{9By@HI$q+ mmmlܸ #kYÆ c]pk׮۷/^miiiڵ+@KK Xr%Ɲ;w.tҦB!"QVdiN޼ySy...pqq8p=^}m(I'N(Hiގ;T:$lǚ IDATӧO\( T={6a禦BCC#bccѧO}ɒ%իf͚$B!BHC޽;rwHLLDǎ !-0a'+VѩS'$'' ’%K%''a̙x1x<x<vǃ3N<)t~TTz4440w\OQQ. ___=z4жm[ڊ".~YdffbPWW-&LYf޽;ڴi===,\q;;;@JJJС0|pNj:v[[[ٳqB 0@hRC390448Ci`jjZ1|}}pB!BM&{5kc$&&OIIҳgOddd ,, @FFf̘DFF[^^dhhh>DXX޿V>tdee!''+V` 򐟟7BQQQdq}c?^]SSpm?~\(-b_pttu|۷E6 i_%0>zpuuwBHz*++!"$q`nn7*.]UV066) p󑕕cƍԄ:pq|UUU"##PSS.~/_Ƃ 5J{ 쌤Zs]sε:O |{%nݺACCxHi> 333s"SSS1`ػ& Nb(&\DTHɸ*R}!ַ24@[/Zr1vXcڴixwtn'!Q***a;vvv0`qY0 B!]ۉkkkX[[HJJb?(+$F٬Y###=z[n&O>ϝ;7{|JJ F x^b0 9CСCP(H$Ν;uzI,,,p]BE'N@AA*** HrJ}?v͛>wsЯ_?}: q_XX#GnRZ/NSAS޷6dV>"moh~QQQ@CCV\GFB!Dߪcx7?n޼7 ҥKq.-v[K6i]t⺤W{Ky2tibf򴵵Uط=x<2 ~j3+O> <հz#"KVWW0dgg#88<III 077GSSSgMi{SSLLL(!v%&&bժUQ5mۆ6y Nٳ~~~(..fccc3NT.{롸fffd$&&*=4ŧ}ڸw}cСly֋Ǫ~_w^^^nS|BH{#77YYYW>iӦ!22(,,Ğ={DkbڵpttDdd$ŭIO]㯨իWkܯOytL\@PP>KKKl+WG[o7oDmm>f'OFrr2>ʯKׯzٳNcjj ???lذ͸t>̖߿puuCUU`qvvFSS?~ٴ=T*ņ p1$''+ʕ+9rα,ւL|ܾ}sn A4#ߩgllZh?^S9AS|޷6K_o}uO*i|k::!fGo]fOSSk8v HRٳaaapwwGjj*_۷/rJ >Xz5:,vm]]]!1n8899a֬Y v-$^^^Xx1MӒPXIے\]ҩk-~},EyvtL\yZ;4)--Euuʲcb:t(|>/_qPYYZ ''Toff&/^  1o<444#::ZeRMkhXZZl=֭[???Ǐ׹ĉR(o1|pjC ܐWWW:{YV\Z"J\ٵD_76iԞq}k}^7>}aj888.BȳfMoܹs8tM###L2 扁퍍7ĉoo>\t .]²eпP=L266ƙ3ge6m֬YUVh?0sLk'OF`` }vX,1```X|dW=qa8;;-[񁃃9_XX|0 777ddds~@V1Z[B &Ypp0,?? ,x`ꫯP[[ ~'H$455 eg~ט?>Ν;llj%v؁۷oΝ;z*^}UK,Ayy9/TWW#55ʗD" p㏰W(_d O:u ݻwc۶m:׳`ڵKi{hh(Q'ݩ1,먄f7!مtm}e]G',///<==1c YHʿo̚5 7nDII ہb ܺu Gܹsu^:ضmÞƍ/~WKHbשT &`ر \ys.Q>q˖tѣҒXh\G!@7%ݛX,FUUΟ?soذqqq z?7!OXJ  {{{D"J?uTl޼ .@ 0#!!M5DKK  @ +cff#44^hQXXJo}ȿ" _p>q [B$aʔ)ɓ'aii˗cʔ)2$ԔPhBH /_:YsۺKBFu #5]. );:af\\ z) Rd>qkZ#CwҽtHf``'OvCwbb";K,88Xv,g^R̙3 7oެyɒ%vr[$%%!))I~Ƨ} %gqʹ]o짟~xnmǹsӦMSx$t$ԖP= !eم[}E\:#!\K϶]ijjq%AAAJܹ 33GE~~>󑘘.K/mQ(((@VVrrr"22&&&HNNۑ; .K&wWVVO>:1}t`鈋Cnn.ݖj˒M$бeڳ"<h&S?Vzx vvvV!<~vb " 1kOBH.Ԧ32vÎNY[[ aԶ@ @TT[TUUa2e [[[سgہ0 Ξ=d 0&LMpm899Cqq1~W[:wreɤtHR444/`ܶm~g$T[l%M4.Ӟ%XfBy{jˆיD27nĈ#:,BZS&6Z3!c{f} 3P]]~Ywޭxs9sPSSlٳNBnn.rss!0 DDDDťb(^^^K/e˖e$򶲲—_~`x{{Ek"o@s"qM$no"N4(.܌իWk|kK|muByi%cƌ1cFWA3C k 1iǎƄJaذa8p1uNS;=z](?Wsk">spi%#ڊ}'v9}41556l؀-[ƬY$Fbb"q-dgg#++ gΜ0{.pݽKd>seږDZv'ݻg.KBg&mkN\%d--~},EyvP'.!t#פ&!{%HuB*66 RY&KYYY`bj=evfMff&bbb`iic5!#iّ@g:---@ruwЯ_?,Z-D"0j(xzzV $=tN1c 99ՕP(ģGP]]ݦ6k,D"vWyyyXf Ο?@*C J{=xyy|xb!D63fi0{lӧOcժU^۶mOawwwL4 k֬ѩ͛7㣏>BEELMM1k,]Щz,ZǏ/> 1w\! G*yxx`̘1r  >W"66;гgO,\?pÇ˃rMz*\\\PYYvl]p>>>f~7j122B߾}'N࣏>Ν;ٸq#┎t{ݾl2̛7S\]]䄲2466bغu+kϟ6\>5'8 B:¦Mk/!ݘ4{B!]tjr Xr׮]c; uaff8;;>>>ppp@bb"b1QVVDDD`Ŋ:,R)q*ka!##}=""&M?SN! cƌqa8;;---5ذa=z4kh}.,,Sn[)u666ɓHOOW_srrD"Q6m֬YUVhϟ… 1p@\>/u!B]]=PSBg1a!t*zB'R\\ sss$&&"77Wiٓ\b8;;h]6$$/_T7p ػw/MLLPWWDOOOv>066F=ήYRR~ o& 秔u!BpM?gv-|7*gk닩a/^WrҾHb/-- *(1ydvB!۪ 0vXaHNNٳg&B~]ډ+t$%%dnccΠCB";wÇQW[v`D"D"̝;7SRR0j(wHHHPz~mhhf@MM x$Jŋ*i~‚#B!;FZZƏ7? y&6n 4K.2^JJ "## "nB!lmm4}\߿Æ kӱҙt9&fJѩ/|a~!,, CRR߿p˗/k\#T]`nnO>@N???vZZ}0hlld;)r?ʹQZZs3p{={nnn Ԥ'Ԏsi_eeײD8~v܉wyGeΝÐ!CT3>}|655Pi _xR/wyG)… akk~C'Bӫޟ|L6  Daa!ك,H$]k׮#"##̙3ѣGCg\?#]tL\@PP>KKK|;v@*⯿¦MtѣG㭷RYq)Gss3\]]PUU})cHNNFcc#GT?7cgϞ:0b6yD"Qi+={6K߿:cݑׯ:b\Ç+V^/"ѻwoxzzΝ;l+b1ƍ'''̚5KaRLFFD"BBB>566 /f#&&VVVD||<www,]=B%m_QQD"^}Uۍ[n{fMX2&L7!*SZZ0cV֯_Pvf=֭[???? ?~Ngff@ @PP9&񁙙\]]ӧԾ®]x lV\f1֩o|>111ѣ2d Ç#?? 666pwwlj'ίɌ;ӧOСC! _>???xyypttIJe2M?fff?s~\>ѿ#f̘>>n@{w IDAT{-666׿0Zƍ2F;;;xzzbjrB 3frm0򬩨ɓ'}ua۶mMy566";;@rr2 kkkDGGx1LtTUUoX,Vfg͛8vлwo\t ˖-àAw^$hJe/_:Y-9s&$ """4S[['6l`b444 7n@ee%VXS|@kn7 E[4'KDe888@"@"`޼yzBDN<%<<<.bLaW׾#G2Vogg͙h^wX,f0z9_FF*_~efǎ*8::d&$$yœ>}w03gd-9r$4773---L`` |rs4440D"QL0Am9ˡŊٳg3>d~WW^LBB[>uT&88illd 7ԹBaLbbbW%֬Y`RRR:dgM[n !C0b޽khwތܿ- e0dagӇ122b 8ٲeb333Ғ7n0 ߟ9y1oR[GDEE1?̜9S*h\~?~<ӫW/fܸq/wK[|EcGCUVXHRC&''P1Ǐg.\zRRRoooիǏG|篾qvvV/..y7[3bK vQs|wq=Dw ~PNQJyk'zf&.!m=m޾} HЧOdggC"࣏>qfy{{ٳJǜ={ޜVh)AiiiJ˹qi驔K߇X,FzzzgjiiA^^ϟy桡A|ؽ{7FnkO"f.%B~#Bun /26o .'O Lݥ})11QC .-- ְƁ~.))K0 )PSSZ6I:s^oeۗ&5Byšo߾077Gbbж0ipRw„ Xn=z_~Z>m >vggg>}hjjݻ ####>>W_}K?~~0uTl߾puuڵkaoop3g0 QxXYb;wFNN>!s5}t^PXJ>l0ƍqN|p]qOS"jBNBH9w\WNVmxlvvv]/&&!!!W^܀np~~~X,z~+++69lKeeR2;mhjjgSSLLL}Xuu5uu5,C3 !O46CSSdI5lllpm/?%#NzgHIITp|ѣzj444޽{8v`mmھΎTG,ow݉@ @TTЀ}!++ ǏqA|"""`?32337otrrBxx80tPݻ;$< 6 Th8==ѣB"ddIŷmۆ#G"-- B"vN9>ur@@>|p___|"j+++#%ɄKy2:u Tf% BPi܀'Nć~m۶ؾ};-Zp ggg455JsZޕ+W0rHh)c˖-(**Ç1k,C! ?RTsmgɾ7Q#Fݻw_"<<}?.-~kHuh,,,0g̙3555ƞ={p)"77 àO>H$@DD\\\:-Bv>hDòDUUUx饗!7rrr舼<_"88pqqAff&/^ XxNK-(W… 9K||>/_6J!K:qZBH7p…A,k h{opw܉y&&&Ƃ vĉj׈5vAlii *k{!ߎ; lڴ aaaJhS7;N__58i``qaݺu8z(=z?o6H|:HLLDbb"nݺldeeKܽ{Gxx8%q.YI>se?c׮]yĉ{.;vhW|URRkk W_+>B ]҉ /tE./h} m=7 4CfصkN܆ajoH$BAA999<`?k{!J߄ JK048iv>:rpEEEprrBKK .]?ihׯ-ZEA"5j<==a`@id!t]҉)8!kaӦM z 'XT?cƌQ(۰aT&#?͎:UUU#'~mv歳Ҳ >.4D"?S[6tPB%6#ܓ```'O*(H&!tӐB7ԉK!O9}< .*+>NBnIB!}N\B!B; Njٳ#ikk.CB!B!nyyy틀#uL\YzBȳB!t5kD"}񰰰@jjjcٝZ/WB=BuuRT{GJJ ^iǏYYY۷󑟟^{ 'OFxx8BCCajjsl>>ppp@bb"kh#_J066ƙ3g:e]'''Nԅ HӦMCyy9{.PpDDD]kl}Abb"qM 33?3vލݻwW^xK/*,,iB:%6# TMM RSSnd7ww^lݺ-onnFQQLMMq}\z[laoЁ֎\|uuu:uΜ9j),,đ#GPRR[n!##-rJ=zs\hhh@YYnܸJXB۷oի}Ծٳgwި w^Bcc#}]+'O6СCj˯]JaРAGZZ9PB:ߌ3sNeOСCajj X{)K*l+**H$«?""H6xann޽{w|͛7z-,Y0 h|[bHUrB 3fz ǃt<; nܸ#G 55fffHNN޽{mۆ-L:Ԅ|ٳ'B!Ν\N~Ûo CCCӳMm366F=b@ii)~W022>ٳg{~F.P\|S|ʹ|111A]]$ x<<==UNhhhP#q> WWW\v Ç~ױ~zNdooE̙3(--EBBݻ|23{uW%kB/C*@bkU7=z 6 xhhh 22}L,cݺuXj S{υe7033$''#11Qi?`= ]xQ)))}tE| 8Pm]...xwԖ744p?~.]1\]]Ėr!ˉ'PPPbʕJoUR7Jm%9@A+++uvl+??;hP!]#..UV)u"e mhi<"p̙vCUS~pe5RePVV <UUUTVVښS}0hlldQ9IR)_uu5}4O:!;;xHJJ?k0u677g7+))|}}~c;::Ưoظq#6nHDFF*,Juu5݋Lx1ɓM7!E)BH7/ŋǪUϲNE Tqq1Ν;Ç#''nPƍH$ao4G5J$u7$/#?zϕo% $ nܸ7n(-#HdT=Ic]MMM011덪,~k..׏9`mm kkk8pIII璒v?ܾ}bLIIQ޽{#!!>|CPH;w<-onKF~KܹswzֿL<}Dii²GsŮ]pq?yyyZ;&N?7***}vr#F`H$8u#G?ǏqY>}ZaMhnn+{o>vDvyyy~:MxPkx뭷 n޼Z펎?gϞoPD>F#{=!==믿ٳ'^yݻ/1m4%O%nN @(B(Ғ,5J_7Jhhh_|tߞ@Ms.~7m3:ʕ+J|~~~ذaq%>|-G?@q@F~WtiQu4֞5/ҙ233҂C  ((H!YԩSyf,\... ?k;wDMM 1j(b®]x ƍH$BVVn H)SԾ'O˗/ǔ)S_kk&Xn0~x$$$`cк< @lZsF ӕ)--m$mkqoر>}: P:ݑׯ:k׮aʕ>|8  ">>lRݻwC*bϞ= |' -@!OX 4Hi{aa!͛|DGG7P7l PUU^z ˖-S(lnnիpB7rrryDff&bbb`iic*7L,^! xb־;wb޼y 0gرJ@@,www8qQQQ*yk ~zz:aii *BK ӎ; lڴ aaaJ=*_]%[?< 6 4{K'5SKگז-[PTTÇk~_(--ŵktny6Jx*cii;vh<~̙9s2P;99O?isP7h mHfصkN܆ajo H$BAA999Y!:BHS7&L7*++̮.O*>˗4? ~ҵ~=QPPkvuXbҤI4yɦ IDATi6mڄcǎaaa*m&Y@B=o%c̘1 e6l@\\vgs:^?gW-t=^HڲC5yՓ !!!hnn@ ƍ1bĈt2ccc<BHYz200ɓ'U.%̄B:aH3f`ƌ]!t ԉK!D/vin<uun/;;N?!OgipZ۷!*,,t!D;%B! .TF_Ob@KK >.C CB!BH;/i+ta$to@őN\B!B!&!h\\\PXXةK0!߬Y;tjǰa45x{رcJ.\իWwh݄B!BHGN\B!f̙>}zhnnF@@RҥKBcƌ_ݮsp'ByP'.!><йB!B!&%@DD؈w}m:۱zj(,,D߾}fffݻwqyݻurrD"-[DDy9s&$ """T;{l555p݋4} q֭[J566ɓW۾:tHmkڔ)7.. B}}=prXƍĊ+tBtnN>CbSG;m/{'ܹ\]]!1n8899a֬Y*hB!O JlF!Oo˃!٦s`aanB!!!|r555!??/^DϞ=! 1w\"))I!6ccc x"|>.k744)ǏҥK066+7nȑ#(.. 99tBĉ((( rJK׮]6W\ڵkaddT ccc9s?{lؠo1`(((M5k`ժUZDžl.??;1k,\,eee000@DDVXA 0 :BXԉK!\ZZ}]yyylr3gΠ<1mK$~!]eee022B}}=T?ւa씖ի!cnn. ܹs>D=Z}ݺu 0yd\~!t8wWbb"VZؖ-.LLLPWWDC4u/22FF;b֭Sĥ;ozG"BQ'.!ts111 $$$ < àĬW8Bgcc#LLLxJl!))ITYCUUlll'Z󃯯=uO~JӧM˗/GBB 1o<444#::ӧO|;wb޼y 兪*KXlN$==Ѱŋc>B!/^s=B!2ԉK!!+,'6l@\\RS7wyNk4xM{ >s!BԉK!waM_ajX,ASAtH DbGJ2ԋStj]:Sꈴ.-N+Q,"Al?B~=ĪU c\Q$.Q+2b1i K@2bx(Q$. gϞ\-[9jn݊(K"""j>&N$.Q+I\"""""""""VDDDDDԠ3g"!!Fˤ˫8qAqqqHLL4BFDDD"fDDDDDԠ(XXX;Vzz:T*4b-B>}0g!;"""zp%.QuEl`` /^xxx-[@&!44{x7//C X,F`` Ə>@իW1h tcƌAUU 772 o6]LLݯ7#8p@LFFq a˖-޽{CP`ݺuBBY{ҥ033Cii)޽+V0b9sF3g`Ĉ\R7nEEEXxPVWx#""օDDDDDTdd$w_߇.L>魷ނ \]]f݃BH$O5 ˖-Cmm-~G1Ċ+`ccXx޽tM2fff011\.GZZZ<;;VVVڵ~ΝW*:~Zee%>>>>;v׷DDDԺL\""""6$)) ~!z.';}4<==2KKK!U>׳>CRܖ6 ._W? 1P*pA]t&x>|(|.,,l4^sl% JKKeeemvvvxjjjᲲ2i|_QI\""""6$::ٳg#00&M8;;DߩS'աNNNQaP="uuu?r9__}"##o>>|XX- Hbu=yAQQ:uꤑceeee^HO#""օ)!bRRVVVH$w3g_ooo,_=™3gp qrr/2ݿ?n޼ ر#C 6 ˖-رcիW F||<***<>"X]z6l@ii)J% JkǥK5>p@{z X[[# +WJ…  q}GDDD 'qڨӧ L۷cdxׅxjj* H׿z 'GJ/~ŋ9r$lllɓ'cĉB|Ĉ033C޽1n8/jÇX,رcq-Ç={_GjǑ#G0zhׯDqaC" 66cƌQ?"""j]xQ5v!SN5Zo^?ǏvFӧʯ]VlڴvIHHmᅬO HqFȑ#7Zfܹ/1uTR駟j}_T6Z62 ǏǣZOuJ\"""""2ȏ?7npq9G.Νӈ\111JFȌ^4\KDDDDDqBCCR f۷Y5֣Gfiy011AVVVGMYыDDmDuu5scaa!\hDDWDD"""Z&q[l5ťiϭ-"""j%"j#V^E; F.c˖-Nq H$NŔ;"""""犓DDm… dcAbn݊(cADDDDDܘ;"""""2iӦ>xm˫23gDBBBj}0g۷XDDDDJ\""""6*"")))/_!C@,# wU:T9s >>^L`` /^xxxkעgϞر#Z@nn.d2~m\v 2 2 ׯ޲e d2BCCw^ 1vXX[[ ,Ç;w.^{5tcƌAUUF=k֬ALLLc???$''77R[nb~~~9sxyy9D3gv$.Q;3yd >qС&ՓD\v /^DΝ666صk~W;ww&i{ Bu  fꍊBɓlނ=޽~ wFRRZ/"33(,,Ė-[B``!##Ckʕ+(**j|nݺIIITr(Jܸq(**ŋ n@DDDDԎajj5H`gg'>O n%"5 "HW6vZzJpvvH$z!tK/A,cn3ƥIDURRt̛7OwB$YxͭImdgdd`ȑJdHIIAMMMxZYYwqq8cǎgSSST*zlmmm|W_}GƟgti۸|2z#7رcqQC988@$NNN"8::T-*++-SYY KKf\>!4WWWD"@,7[DDDbJ\"vv˜1cpUVVV011b-^c뺴%//Ot'00ǏW[ɩϥ;i}˗aaa2O?;:/..ƍ7DX 8PЫW/xzz ,5}|| 1l0ӦMkmt4iݺu={6Ξ=`ݺD8X T T +++H$ộ<<<зo_XBcǎѣG8qp8p@R#L6t9~?+ammW_}˗/۷o#99aaaӯ_?TVVXkK.[k|x jXr%T*.\oV#88񨨨\~]\"""j_8KDԆw߅;bҤIjgCۥ6.m:u* R>Ƞ }4v!k۷ 6oތp|t2 qqqofp* FUU._u֩~-?~999w,Y3z~~]4~x!** }ŪU&yuʼnHӧO۷//fΜ?jEܹs۷/fϞW^yE6ܰl2W^!C0qDR_|6ld2oߎC&_))){.1`aܹz|8FѤ]#RRR;Çضm6mڤ׻8v1}t#,, ]XW/2,,,///A$AT0e -[ tt]?~!N8 }ȑ#n:q"_HHH{3gh}{:/"{3OKHH@BBBhsԩS+Jo>ӧO}_vmΝ/SNՈ)J߿~vr,Ǐ v3H$lܸIuQĕDDmН;wp5x{{[]qChԦK[JKK!^tGW2e .\|>|xW~׿pM\t jtojj jGFԟ<0(--+7]??C.Mի .] ''G]q"g!Q\\siV\HR#dFDDDDDmЄ  IDATpmL8k֬T*E\\T]qC4t( ( ЩS'թM*>]Kw9,XP(n~9aaaؼy36oތhcMn[o68=YHsNꟾ?'&O^TSS{"<<ݻwٳgj*\|...:DDYYY޽F,66֠cfZ'q(KKKL:ӧ!Ho //OxMM QWWZTWW,t]ooo,_=™3g vs鎋 5.]_ﭷW_}4L>]z>|\t * HII5 6J_|ƥ@ϯK`7nn޼d2tʼnlmm5zjG><]d裏xb3h0|pc„ _wtoߎ?ظq#lmm.M7o[k^DiΝ!zvi,N#&&&01Ѿ xVVFW\ץ-z? |?~\<7os}.տT*śo'D"5XX0yfʍ9yh럮_c&ZV%* NZ'qUaoo_~yyy8~8.]j /6x9Qsҥ &Md4 2)9%"Vƍ JX,ƚ5kзo_c7___\~k׮!6jĈ<3$.J0vMQQvvS """"""=qHQUUl5g]D/; 2fDDDDD$.^-2vD/]va׮]NqH$\Gs%j,,,бcGcAMdeee^%"2 bɒ%\ҥKue… '|9s8y$aee.]o߾>}:ƎԩS\ǔ)S}vիW٢2;7}D^زe3CDDDDDqɓ'ك\H$gĽv.lڴ Tok<ƓZ7NQ-[&L8&&&W_ţGp:tbڼyƳ[;D"3ߚ:Mpuu}Q31vDDb2e D"D";b,\C a0}tlڴ +VPoǎ>|8ann:@&!&&on0-VVV2e ͛8v\9ɓ';vĉqΝgi?x tprrBxx8~)))ѣ,--oǏoO3""""""ҍ+q"##QRRpqqHR䠬  àA0h XZZjqY|x饗OիWGӱm6W7khh(jkk;PWW&񴯿oܹs{Ezz:كƍ1c ?0tI^Cdz!3"j>;w4v D&Md NQYXXѣӧn޼ v#G};%ñtR u"&&FѣGXp!/_7obϞ=Q x`ggR]v':t蠑/Q]] oooo04'bΜ9)v؁oI1c@N$a͘0aRRR0w\ φr35fDԼ"##:c@DDfplɒ%5j:GgbDYY[nũS+"1b222aByyygJ۷:t!!!شiJ%X X>DII d2Ay<~BQQ p8t 333a+\/$%%!77WnCi999ƌZW>RkDDD͏DDd>7oF]]._`Ũ7p1FUU933wھuަM<^$L&>?R<9RT-&|.-- N; /CګגcFD-cǎN]2JDDb3""j2sssgq׫W/OԩS;wN͛7QWWm'R8u~!!!Axo>ܻwOBihOsrr>?}דߝ訵l φޓZr̈H\KDD*&&ǤI0h سgPgϞ6:`t#t}l۷oGll9GC WWWȑ#™8z(>>>gggرcرcF7ҥKu2 ۷AYnjI\""jvϟ_~CB"6l/;w?{vGFFjLyBttF^Ο=y$_wΆMHHN!!! AaaΟ?{{{D"t?ɓ 8;;#** W^m ""DDDDDJ2`Q]]݉'ɓBee%RSSv4.?3P^^WWWdee}aȐ!رcܹN:ݻغu+ /6DDD8""""jv܉; j{=9r555w,--KݽwǍT\.ǎ;[⭷vnn.fΜ2t'fBMM C&?c8x` `Νϵ=NQ[I\"6P\\7n9/}v"##Fٳg|rdff P]][ԩSͅƻӟ`jj xwcÇĝ:u*~7t GA׮]?=z3g 1HBmm-_,Qi)))HII1n2Dٳg=^^^4ihGc͛QWW˗/Xx1***p ;v 9::6ݻzo#PT\TT"kkkQQQ{.]]""I\"6\,H Q*//v CXXhWz׮]իz˗/cڵ7ѳgOP۵ncpQcƌHIII('˱e;ԯt&""gǿÇ7| bɒ%NlݺQQQD"D"'Nh'2#"""""珓DDB}6ѥCR!((HX,Fll,>#^zyyy7nqEb ܻwEr'"""""q@JJJ@,^!!!;rss <-xƍ-kעgϞر#Z@ZZzOOOxyyaF\\𾏏r9 ޽{cڴiһ刎$ fΜj]nݺb!""sk| fh<>|8&O 777sΈGpp...Crry۶mPWW:xzz;"""&$.k׮ŋܹZʕ+(**xoҥ033Cii)޽+VT`׮]_q9޽ׯ* FUU._uÇB[n{d%ˡT*q /;]%&&ݺuCyy95>@VV i8w ddd 33yyy#66|^c*++qa\pرc}dgg ]v58ǧY[[7>xp\KDԆ%%%HOOǼyjee2YXp! Pc٨9r$R)d2RRR^̄ZSSS#zVXXL&L&Ì3(++C]]ڪW8}۱m+++ C.޽{Ν;Hyum]]c ЩSsS뇫+D"rrr  Y988@$NNN[?),t*++9_8s |}}5WVV;""""j%"jb0yieeDTڤ zv튎;:tJD"w^=ڠ7l؀7|/wqq9;ṻ;O?~:\"X1ʕ+n:oŴiӚu|JkL?z555PTjM֢7oqi6.]ooo" m$.Q;1}tt֭_~%%%/^YfAT o&&Npssòe777H$+Ç&L_ >SAR!11qqqT̟?T*U[X_ hDc̘1B ѣG1uT_+++Gݻ8t|||48rD 2;w;""6DDDHHءCtb$$$h}xXXv9r$>F7o͛\"`ƍMIM x|Dž񨮮6}]Ν/Rc7,,Lh'yzzj=ITbO9O"jY;v0v DM>fDDDm\.Gqq1Ν;u\111M> W&&&jOppphzH?%"V-;;)4իWXvvv-RKKDDDDDDq DDDDD$iiij̙3?<\!11%S#""WQ0tT*EЧO̙3-( @JJJ@,^!!!;rss <-R{ L~׮]L&L&2[lL&Chh(ݫ~ZZzOOOxyyaF\\PpppD"̙3Q]]чǭ[X ???DDD`Μ9B|ڵٳ':vggg$$$v֬Yg8???$''.-%"gͣǨQ χH$H$p zD\v /^DΝW\AQQڳ޽{CP`ݺuBBY2QQQP(bbbЭ[7#)) jqڵ +Ν;ݻwkLRQQ,=g!##QhiuѶV,#66o|իFjj*qix{{ qn%DFF_~ QTH/2,,,///A$AT~Cff&`ccGll,>gίƅ `nn;V\.>KR"''Ǡvaee]c>hDDD6p%.Q;VӖ`ܸq=>m`>XjZqڟ$8::7o=??_L&3Jfff&ݶ)|}}q9܎KԾDGG#;; ,|zV$5HgV]]]\]]!#҆D"JJJgO~wñ`@P ::ڢRk\񫬬%ws%""iJRJgvڂdffbŊX,F||w%#r}$.Q.\ @hh(1{fr"mWYY`]܎K~M>M~jT4dL8=zT*_|!Ć L۷cdxn755>'b1Ǝ[n_c6l؀|H$b̘1B ˖-C@@^y̞=m899a8zhƯޑ#G0zh&""a ̞=4iYlkN+,gkk> ]HTOnurrxm`mw܉D"̛7UUUk!Μ9d _!!!Zc~c`ӦM N:06o,<LsD7>S~d8~=>>^$$$4'͝;_~%Nkl}ۿ?>g΃^l\KDԆ͛7;vti m=zjT*@uujZbʕ8x 5v\""(..nґ6+WDLL Ri dFDDD/%"j'Onݺ5}}ZYY+fϞ vڢ"vBϞ=~MMM鉪*HR̟?__xf͂R|ML8Ѡa曐H$߿nnnH$ϼZ_ZЧOswwСChqYYYMqF=O%"j'vZ}m´U1܎KDի[n;;&̙ы)ϲVn%"""""zq%.Q+,muv\"""""'q DDDDDD/6@DDDD 4H<?Ot5v:? ooo!h"śsQ{ĕDDDDDmXDDRRR8q=z􀵵5r9~72ƢEԞu}Fل;kעgϞر#Z?TUU ۷R=#::H$9s&'NL&/~2 /,,رcamm ''',X>T700/FHH<<<\W^􄗗,Xkkk5>u!燈̙3G76~Xf bbb4\ -&L?ʘsΈGpp0VZwqq ΋ I\"v:TAk J"iiiz:fΜfʈѣGqqܾ} }F+WHو#pgΜ#666صk~W;wwꊌ TL:&&"ˡT*q /ݻ :uΝ;P(&z-ݻ駟{n$%%i䜜D\v /^DΝ* FUU._uiL74>AnP^^$dff?}UTT ++ D^^ /Ν;<((HHDDD-DDԮݾ}YYYN͋ĉ=%&&;w-bccg2JRqON5 ˖-Cmm-~GaW._~,44999BQQQHMMTWW#-- QQQdffbŊX,F||>>>;vZ]㧏lXYYk׮Za֬Yq㴖;߿]"""j^%"j'Zb;mm۶o߾ڵƶʫWbРAСƌ}VvѡCbܹxХKuum}Vol7n-))q`kk {{{\d2۸vd2d2J0mۑ`˖-d ޽{ < 2b?~E/^L磰P}]q]qCc꟮K.JKKq]XBݻ7 ֭[( ( ̚5KmۑP(0y&ԩS1tP(J|G8p}$jl??9iYRR"|.--^++DFFѣG8| ޽{ǂ PPPBh uxxx{Ejj \]]!#SPPsppH$Bqq𬨨e =N>4U}~O?}ڢRk<..KKF:s |}}5WVVFDDD$.5y;-Fttp뵓FV歷ނ \]]>^K.܈0%"jZz;-xkn-cgg'|677J]cǎgSSS?v\}55]ypB 0ٳ%Vvd}hRD޽GEyyp@t*H EQ$ !VP+f6hoIJE"j-a1zBH6bFk Ƒ6a`@眞3y5X>v1umѢEg燑#GB.cڵmvX,fϞ*;rHlٲ 3f@BBf̘a0FLL Ο?I&F[[ H ׽~444&MBDDVXarS?]ٻw/JKK!J!f늓qwᅬ//_nر;v,N8|ḋ 񖈈z&DDZp!  ܹs@>N <S'EEE H6SNEdd$!ɐn>>>8t222 1sLgggRn 0v^;117nqȐHW%"?}⣏>BVVشiRRRjժ^ PoG͙3R_T*) DvHDd=&L'.w==K. g.}ѓ~z1wwnᆭ+//Gxx8Z-$ v NbȈΊdDEE!**JG}G qjkkω*=ODOiӦaXxq_!LvRSS)C.#CDDDO'qoc} } `o?8<DDY*a&DWss3@,!z\كK.!..=???jXn,Y"Ŀ(**/O>VVVݻwcǎ}61|lڴIoڴixq5|7xwonnoxҲ,YHHH]wu,Z_5LC. ;˿'Oڵk`kT?WGDDDOCD4@L>_UVaƍ}cG!66 KDԹ9s栶cxzz+EEEχNÔ)SwW*077Gyy9ua͏r:/&& ٳ ‹/(,kkkEpp0|||7n ""Y_'@DDDDDˈ#vZ|7HOO / ** MMM&b… 899a֬Yzm/^ ggg1 %%%BLTiQ\\?::氰^╕Ejj*lll HRpȑݨGd,R|Wx뭷0h ( LҤI~]vӇE\""""gԴi  0ǤI0j(~0ij36vvvgssshZ{NN! ˱~ܿ_!Cσ Y \\%Kb,D" >\9Rlj ,@UU|}}1nܸǹ"""zx3^>rrrpA|կ~;v䱝Q^^ޣꫯ H$BRR޽ۣ~nĈD(..D"1N$t:I:t(t:ZZZ`kk 矐?}Jcϝ%"";v/ԩSW\ 6<ԈJ>n݊ٳg֭[V,YFݻjH$Bmm-=u`T*ʐ8{lm WWW?YYYZ)ҥK@vv6VXap==X%"gǡj*%%&HD4%%%;:ٙYfaXr%$ &LJ9[lB33f(c777H$B mۆH${&\C!##r3g4CpL2/ir/_[ol 2SNŪU0w^LDDDOS "zFDEEaΜ9Xxpի fr5ktRƭÒ%K  ¸q駟B"4ݻcܾ}֘?>6mDb/qUܿ_ejvB\\\kpvv/233aooo+ظqI]EE,XK.a„ 1b{n}7>XZZvk"daa+"66Ϗrh.99dSwJطo%%%!))v?_t*?\phv{뭷[ou_܉KD Zƞ={ Z ZmrPpGCCRSS1h Ƿ/^đ#Ga0Off&6l؀7oÆ 39 00:c4~ Ԙ|_a̘1hnnFZZJ娬DMM ֭[yhN\""z$hjjZ;|}}/YdGqqAhxzz)._ +++=h &w1h4ݚZ[[qi\rBhhDnn.JJJ`ccPTHLL͛==ׯ7sww ĈױKD4aڵGSϟ?77GcժUxwzDGG7Qܜlڴ hnnFttAzQx1vvvs߿k766BaذaµaÆABW&"ǧ".".pBs.Dt=VxKii) v! *>0D"$%%uHd4򷵵Ekkkhmmep"uuuprr a{{{# P\\3q0DLL+++HRO_䄪*466v{<ܺu 0dݻjH$Bmm-=5:Ƶk0qDɓ'V^P(ؾ};Z-\'NqCR /)++387m,=#-Z)St:u*"##Lt-**?lll兘DFF4ȑ#e( ̘1 1cFUNNNƙ3gga֬YFeeevn{Eii)R)F[[ HnCDDDDDDS "zF=799Y8g'???[b8y4 m6{b3YwJ{ ߥR)ףcǎaժU~6񰳳CjǰDDLP*ŋ b۷oG\\d2YdFDl5xq'.۷^^ߙ%&&""كK.!..UGss3r9`͚5Xt) ++ k֬Acc#0}tcǐV X0޽*Lorrr?^|KKGDDDD DDdTgEܧ]םX;w|bsԜ9sP[[X <qqqX`MZq E|||chZ\zӧOݻwqubǎ4hJ%Q^^333`ݺuؼy#?q DDDDDgĈXv-x'/~ XXXƍȑ#!hPYY\* Gy"=i,=MHH?y1i$5 Hn bbڃP]] \.\.ǒ%KXnDDDDOS """"zF?GNNsvIIIG:uTDFF2 BrBFFr9fΜihkk$ BCCQQQ\3qQQQؿ޵WB.cٲeyp`FFaٰ=|}}qǯFhh(hkk^^^P*񁇇ϟwk.u3|}}itHII1y***D___DEEaB .R)q޽nCD$YXX8fff8x qm$$$/jhiiZF^^ ""BUEz > Jo>|s_e˖JDDDD DD0jٳPPXtclڴ bhhh@jj2S_x1Ѐ/GAZZ(,,Dqq1qFxKK h4Ϡ ߸q555&]\\ƌf<ʫT*hP^^J`ݺuݞ],#DSSj5D"|}}aeeeRV㭷!ɰd|'z^ubATرcz˗/ G6:ׄ pEqFcSٔO>7|BhhDnn.RSSaccDJ#Gtk""""""zKD48~8便2ǪU0i$ I}GcG444赳>KRckk\vvvsɹ=aÆ ~,%d |N "<<@̝; ":GsBP J P[[ '''@MM  : Nlmme3:!:ìYPWWgݻJHLLDHH^<;;mmmpssD"Ahh(***==x3",,h ѸHNNMgd2裾NO 0vXXbb"6nY(߿عskkk(J|mbpgφ-;wݻwÐ!C0|p$''0}q᷿-rJJ%|||ݻ&;++ r8zA|ڴiXb^y5 !!!z㗔`ԩH$ Ĝ9s;t:L,ys… P(2iĉ' F.ju)(JfffuLJ\\ƌ||;zwp'.3*..Æ -'h4䵴DSSj5D"|}}aee%ĕJ%<==<<6<<B,,,qaȑDzg033Rıczq@tt4aaaoooxXz5b1O??~әJ"55666H$PT8rA[c~immӧo^^^ DDD;X%""""`GGG?~IIIRݰaÄNNNoMcժU4iaooܿ_2 r׋µ#^,JQ__ߍе!CjDz租C.wxәj@ppp’%K?(ǟkllN3=?q DDDDD… HHH@`` Ν >|ЮN\__'''簵{(--B@@@J%ꫯ H$BRRIgt:sMMCDBϽaСtzL?"H~1bD"!HSW PWW㞟 q'.?~_;qu&>>ɏ5:uʕ+aÆ:7QoH$ddT*z}?OxW ƚ>>8t222 1sLFAAR)}]̜9vzNHC&!==]/6A" 44c޽{QZZ TDMz)@?ƔJ.++}7nܨ[jUxn߾}ׯ] 2DK/fΜ[f:u^o7+[vnΜ91cd2믿t:ݟ'__tÆ 7iZN}׺ѣGźѣGF۳g0_ѣuD *]HHN"KNN=x@/ӽ:ݬYt8?t:ٳu[n52eJ~-oݺ1cnȐ!:kKHHMMM uvvv8ݻw=@S*} QLm'Negguٳgoi7xCϧ ߏDDW_<==MN\"g\LL O]='336l͛7QTT$1;\xGAFFj{쁫+j5j5.]* Z]x1Ѐ/GAZZ^"梴ҋF7nh83HKK3xUTBѠu=uϥKP^^())AaaaOk= |3_}0h ( hhxzzJR,GK"??/_!ɰd|'HJJ퍒q._ +++=\&L`FӣO>+W^^^ ╕EII lll* ؼys#""ӕ#<<Zvׯ7]>"""".&oڊ<`ϟGCCD"ޛG٣rysrri&C,=wvvFCC^Aj Ʊt.;;;ܹs߇E/dw-aÄBW&"._ܧGEE!**XgE܁jΝ}3E\"l…$$$ 00s >":---BYo XWlii\쥩 *>0D"$%%ݻ}u:] PSSGGnckkN۴Ҳ ?󯫫򯫫=`ĈD(..D"y3q0DLL+++HRX,+Ə/VqY1q9~O?޽ V ///zQvNNNBcccgmm_~[n?۷o#335ڵk8qɓowk^kkk( l߾ZW\' R[~n. l,=#-Z)S\?t8)S >>/)))x"Ə̘19G-[@P`ƌFO:pwwL&Czz\.ǡC\3g 4i"""b sqm>3̚5h uuuݚ݋RHR$&&"$$D/6A" 44ݞ^Ȍ}6 y-[?DCC^zDDDxcǎ5%&&bƍ}ѣݯ .R)\.GUU-[\\zr˖-͛7!!ˑauuu={6lmmaoo___ܹs0j(|}_ڤ{yyAT?9)))&]EE H단(,_".ugggXXX<vi"'ITBѠuѣGV1tP>|j;vZ={jjK.5yM6A, HMMŠA~~~tWqahjjnܸ...cƌAss3 ^ZiJ~DDDDDLss3: džJ梤666ٝ͛7?,--Z www 1???>}K/!44o&.]??.7u}:eR*زe ~ٓ3[[[qi\rBhhqtuDDԇҰvZ yyyK&ϟ?77NWWWkmmm)ێZ .Q__h6lFNBPP1j(aM]Y*WNaÆ ׆ Vۭ".aii Ti=1C  "<<@̝;0|pH$t:31"ŐH$=C$A-{=@ii) T*1~x|wh}:\\\sa͚5y;qii)R)ODDDw `aaaƓdp]*b߾}]_[[k4fffvds|;zja筧 ]o?6=yr U*ݻ׭DDDDDDDDDDw~z1www(' SE\"""""z:+/_~ܹGDDDOq7|S """ǀE\""""bǎ}=, spNhah_)A$u DDDY_'@DDDDDDDDDDƱKDDDDDDDDDԏKDDDDDDDDDԏKDDDDDF#99Mwرc7n\3{zԩSW\ 6AFDDD͈ȨXXXXuOǏC"(( ^x˗/}dGDDDO %"""" ĺuWWWի,rѣ}KJJ0uTH$bΜ9xw|7x1x`ݻWB.cٲey&r9r9222L^W]]fϞ [[[w5 Ad׿477c…pppT*E||<ݻ'*R xxx`~j׮]p Effuz{{#%%ŴD"/|rGDDD DDDDDϰLlذ7oDQQ \Z͛7ӦMF~;|mrrrpqYYYjٳPPXtyoڴ bhhh@jj* Å  \p~~~R FrTVV֭vJ Q\\&lܸQ/҂]GPPrrroܸoa̘1hnnFZZrss⦬qa>eee?իWC,cBq/^ ggg1 %%%%V! +++Eܗ^z [lp%HMM $ T*9]۽033Rıc/_Fmt&Lŋ5Mg \kk+N>7|Bhh7u}DDDL\""""&-- k׮𰠗'pssn___H$Zsj*G}}=~~~ذa4 {:u AAApttĨQopp0^[[𹺺xc+JQ__ollm밳Ý;wp^;{:ק>"""?X%""""`.\p@BB1w\ڊDn?tPt: 3j]]]5H$N`kkL IDAT{PZZ B(J?}?#::'Oӧ#FH$Bqq1$] : N KK^}yD"~}ڋΦ@DDDD4H$d2d2XYYA* Gꊉ'b֭q;w899 [n >>زe BCC;w E\CR#"ύ*n޽FAzz:"""➞hmmEmmu\v 'N4g3bϟ?~ccLrrpoGV^իWx#6HR۷h?񏝶Yb8y4 m65M#QXX(|WTwݔQDDDDDm.]Byy9 RZ\x }vA&AfDDD4N\"""""rCB"`׮]?~|~z1www(^I033CAAAGM$&&""" DDDDDmQQQz,cwV/._lr[;;;;;V:]ڹszS """""""""X%"""""""""X%"""""""""X%"""""̟?رc7n\m㑜XS \6lxs/6#""""~+66mǏC"(( ^x˗/cˁ+܉KDDDD4EEEaׯ_SB"@PA/>m4~˗/Jku!,, pqqիWws=!C`HNNƃW^\.DzepMrrdddcgeeA.#<<G5ȿ~ߢM/+VW^Qwk.ux퍔qc***D___DEEaB .R)q޽nCDDDDDDDDϠL>---Xv-{4Nff&6l؀7oÆ ?w}/ȑ#Bj{쁫+j5j5.]* Z]x1Ѐ/GAZZ^"梴ҋF7nԁ83HKKCnn^\TBѠu= $%%fnnْq._ +++=\&L`FӣO>+W^^^ ╕EII lll* ؼys#""DDDDDLZZ֮] a0//Ox9HÇ }F٣rysrri&C,=wvv68bȐ!AAckk\vvvs߿kg߾k &W]] ?=*=, 0 .Dxx8 !!;w.`DthiizcbgKK ,-- Dךꫯ H$BRRytD"jkkcƱEkkkmZ[[aii٫/WkϿNȿNxyڈ# P\\ DkӋg 02 2 VVVJwX WWW?^8#VVٳzcܹso~߽{Z^^^B999 Z5^~elݺ?n߾LDDDtkOOOhk׮aĉF'OoݭyP(}vhZ\r'N...JBKK \"""zvKDDDD4-ZSL1~!8pSLA||<~_SRRpE? 1cs9[lB3:u*"##Lt!\C!##r3gGCC1i$DDD`Ŋ&<,"{{{̙3F|g5kxYY5/ݻJHLLDHH^<;;mmmpssD"Ahh(***= ^=q8wD\"۶MۭzZk'i9<6&qD\`".@0 LvpNW%{D\7<~WDkˆoyzpbqD\`".@0 L&qD\`Zz|z"{g5-aIENDB`neo-0.3.3/doc/source/index.rst0000644000175000017500000000475412265516260017267 0ustar sgarciasgarcia00000000000000.. module:: neo .. image:: images/neologo.png :width: 600 px Neo is a package for representing electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats, including Spike2, NeuroExplorer, AlphaOmega, Axon, Blackrock, Plexon, Tdt, and support for writing to a subset of these formats plus non-proprietary formats including HDF5. The goal of Neo is to improve interoperability between Python tools for analyzing, visualizing and generating electrophysiology data (such as OpenElectrophy_, NeuroTools_, G-node_, Helmholtz_, PyNN_) by providing a common, shared object model. In order to be as lightweight a dependency as possible, Neo is deliberately limited to represention of data, with no functions for data analysis or visualization. Neo implements a hierarchical data model well adapted to intracellular and extracellular electrophysiology and EEG data with support for multi-electrodes (for example tetrodes). Neo's data objects build on the quantities_ package, which in turn builds on NumPy by adding support for physical dimensions. Thus Neo objects behave just like normal NumPy arrays, but with additional metadata, checks for dimensional consistency and automatic unit conversion. A project with similar aims but for neuroimaging file formats is `NiBabel`_. Documentation ------------- .. toctree:: :maxdepth: 1 install core usecases io examples api_reference whatisnew developers_guide io_developers_guide authors License ------- Neo is distributed under a 3-clause Revised BSD licence (BSD-3-Clause). Contributing ------------ The people behind the project (see :doc:`authors`) are very open to discussion. Any feedback is gladly received and highly appreciated! Discussion of Neo takes place on the `NeuralEnsemble mailing list `_. `Source code `_ is on GitHub. The `bug tracker `_ is at:: https://github.com/NeuralEnsemble/python-neo/issues .. _OpenElectrophy: https://github.com/OpenElectrophy/OpenElectrophy .. _NeuroTools: http://neuralensemble.org/NeuroTools .. _G-node: http://www.g-node.org/ .. _Neuroshare: http://neuroshare.org/ .. _Helmholtz: https://www.dbunic.cnrs-gif.fr/documentation/helmholtz/ .. _NiBabel: http://nipy.sourceforge.net/nibabel/ .. _PyNN: http://neuralensemble.org/PyNN .. _quantities: http://pypi.python.org/pypi/quantities neo-0.3.3/doc/source/examples.rst0000644000175000017500000000043712265516260017770 0ustar sgarciasgarcia00000000000000**************** Examples **************** .. currentmodule:: neo Introduction ============= A set of examples in neo/examples/ illustrates the use of neo classes. .. literalinclude:: ../../examples/read_files.py .. literalinclude:: ../../examples/simple_plot_with_matplotlib.py neo-0.3.3/doc/source/install.rst0000644000175000017500000000611312273723542017617 0ustar sgarciasgarcia00000000000000************************* Download and Installation ************************* Neo is a pure Python package, so it should be easy to get it running on any system. Dependencies ============ * Python_ >= 2.6 * numpy_ >= 1.3.0 (1.5.0 for Python 3) * quantities_ >= 0.9.0 For Debian/Ubuntu, you can install these using:: $ apt-get install python-numpy python-pip $ pip install quantities You may need to run these as root. For other operating systems, you can download installers from the links above. Certain IO modules have additional dependencies. If these are not satisfied, Neo will still install but the IO module that uses them will fail on loading: * scipy >= 0.8 for NeoMatlabIO * pytables >= 2.2 for Hdf5IO For SciPy on Debian testing/Ubuntu, you can install these using:: $ apt-get install python-scipy For PyTables version 2.2:: $ apt-get install libhdf5-serial-dev python-numexpr cython $ pip install tables Installing from the Python Package Index ======================================== If you have pip_ installed:: $ pip install neo Alternatively, if you have setuptools_:: $ easy_install neo Both of these will automatically download and install the latest release (again you may need to have administrator privileges on the machine you are installing on). To download and install manually, download: http://pypi.python.org/packages/source/n/neo/neo-0.3.3.tar.gz Then:: $ tar xzf neo-0.3.3.tar.gz $ cd neo-0.3.3 $ python setup.py install or:: $ python3 setup.py install depending on which version of Python you are using. Installing from source ====================== To install the latest version of Neo from the Git repository:: $ git clone git://github.com/NeuralEnsemble/python-neo.git $ cd python-neo $ python setup.py install Python 3 support ================ :mod:`neo.core` is fully compatible with Python 3, but only some of the IO modules support it, as shown in the table below: ================== ======== ======== Module Python 2 Python 3 ================== ======== ======== AlphaOmegaIO Yes No AsciiSignalIO Yes Yes AsciiSpikeTrainIO Yes Yes AxonIO Yes No BlackrockIO Yes No BrainwareDamIO Yes Yes BrainwareF32IO Yes Yes BrainwareSrcIO Yes Yes ElanIO Yes No HDF5IO Yes No KlustakwikIO Yes No MicromedIO Yes No NeoMatlabIO Yes Yes NeuroExplorerIO Yes No NeuroscopeIO Yes Yes PickleIO Yes Yes PlexonIO Yes No PyNNIO Yes Yes RawBinarySignalIO Yes Yes Spike2IO Yes Yes TdtIO Yes No WinEdrIO Yes Yes WinWcpIO Yes Yes ================== ======== ======== .. _`Python`: http://python.org/ .. _`numpy`: http://numpy.scipy.org/ .. _`quantities`: http://pypi.python.org/pypi/quantities .. _`pip`: http://pypi.python.org/pypi/pip .. _`setuptools`: http://pypi.python.org/pypi/setuptools neo-0.3.3/doc/source/specific_annotations.rst0000644000175000017500000000161612265516260022354 0ustar sgarciasgarcia00000000000000.. _specific_annotations: ******************** Specific annotations ******************** Introduction ------------ Neo imposes and recommends some attributes for all objects, and also provides the *annotations* dict for all objects to deal with any kind of extensions. This flexible feature allow Neo objects to be customized for many use cases. While any names can be used for annotations, interoperability will be improved if there is some consistency in naming. Here we suggest some conventions for annotation names. Patch clamp ----------- .. todo: TODO Network simultaion ------------------ Spike sorting ------------- **SpikeTrain.annotations['waveform_features']** : when spike sorting the waveform is reduced to a smaller dimensional space with PCA or wavelets. This attribute is the projected matrice. NxM (N spike number, M features number. KlustakwikIO supports this feature. neo-0.3.3/doc/source/authors.rst0000644000175000017500000000254412265516260017640 0ustar sgarciasgarcia00000000000000======================== Authors and contributors ======================== The following people have contributed code and/or ideas to the current version of Neo. The institutional affiliations are those at the time of the contribution, and may not be the current affiliation of a contributor. * Samuel Garcia [1] * Andrew Davison [2] * Chris Rodgers [3] * Pierre Yger [2] * Yann Mahnoun [4] * Luc Estabanez [2] * Andrey Sobolev [5] * Thierry Brizzi [2] * Florent Jaillet [6] * Philipp Rautenberg [5] * Thomas Wachtler [5] * Cyril Dejean [7] * Robert Pröpper [8] * Domenico Guarino [2] 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 3. University of California, Berkeley 4. Laboratoire de Neurosciences Intégratives et Adaptatives, CNRS UMR 6149 - Université de Provence, Marseille, France 5. G-Node, Ludwig-Maximilians-Universität, Munich, Germany 6. Institut de Neurosciences de la Timone, CNRS UMR 7289 - Université d'Aix-Marseille, Marseille, France 7. Centre de Neurosciences Integratives et Cignitives, UMR 5228 - CNRS - Université Bordeaux I - Université Bordeaux II 8. Neural Information Processing Group, TU Berlin, Germany If I've somehow missed you off the list I'm very sorry - please let us know. neo-0.3.3/doc/source/whatisnew.rst0000644000175000017500000000415012273723542020161 0ustar sgarciasgarcia00000000000000************* Release notes ************* What's new in version 0.3.3? ---------------------------- * fix a bug in PlexonIO where some EventArrays only load 1 element. * fix a bug in BrainwareSrcIo for segments with no spikes. What's new in version 0.3.2? ---------------------------- * cleanup ot io test code, with additional helper functions and methods * added BrainwareDamIo * added BrainwareF32Io * added BrainwareSrcIo What's new in version 0.3.1? ---------------------------- * lazy/cascading improvement * load_lazy_olbject() in neo.io added * added NeuroscopeIO What's new in version 0.3.0? ---------------------------- * various bug fixes in neo.io * added ElphyIO * SpikeTrain performence improved * An IO class now can return a list of Block (see read_all_blocks in IOs) * python3 compatibility improved What's new in version 0.2.1? ---------------------------- * assorted bug fixes * added :func:`time_slice()` method to the :class:`SpikeTrain` and :class:`AnalogSignalArray` classes. * improvements to annotation data type handling * added PickleIO, allowing saving Neo objects in the Python pickle format. * added ElphyIO (see http://www.unic.cnrs-gif.fr/software.html) * added BrainVisionIO (see http://www.brainvision.com/) * improvements to PlexonIO * added :func:`merge()` method to the :class:`Block` and :class:`Segment` classes * development was mostly moved to GitHub, although the issue tracker is still at neuralensemble.org/neo What's new in version 0.2? -------------------------- New features compared to neo 0.1: * new schema more consistent. * new objects: RecordingChannelGroup, EventArray, AnalogSignalArray, EpochArray * Neuron is now Unit * use the quantities_ module for everything that can have units. * Some objects directly inherit from Quantity: SpikeTrain, AnalogSignal, AnalogSignalArray, instead of having an attribute for data. * Attributes are classifyed in 3 categories: necessary, recommended, free. * lazy and cascade keywords are added to all IOs * Python 3 support * better tests .. _quantities: http://pypi.python.org/pypi/quantities neo-0.3.3/PKG-INFO0000644000175000017500000000577012273723667014467 0ustar sgarciasgarcia00000000000000Metadata-Version: 1.1 Name: neo Version: 0.3.3 Summary: Neo is a package for representing electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats Home-page: http://neuralensemble.org/neo Author: Neo authors and contributors Author-email: sgarcia at olfac.univ-lyon1.fr License: BSD-3-Clause Description: === Neo === Neo is a package for representing electrophysiology data in Python, together with support for reading a wide range of neurophysiology file formats, including Spike2, NeuroExplorer, AlphaOmega, Axon, Blackrock, Plexon, Tdt, and support for writing to a subset of these formats plus non-proprietary formats including HDF5. The goal of Neo is to improve interoperability between Python tools for analyzing, visualizing and generating electrophysiology data (such as OpenElectrophy, NeuroTools, G-node, Helmholtz, PyNN) by providing a common, shared object model. In order to be as lightweight a dependency as possible, Neo is deliberately limited to represention of data, with no functions for data analysis or visualization. Neo implements a hierarchical data model well adapted to intracellular and extracellular electrophysiology and EEG data with support for multi-electrodes (for example tetrodes). Neo's data objects build on the quantities_ package, which in turn builds on NumPy by adding support for physical dimensions. Thus neo objects behave just like normal NumPy arrays, but with additional metadata, checks for dimensional consistency and automatic unit conversion. Code status ----------- .. image:: https://secure.travis-ci.org/NeuralEnsemble/python-neo.png?branch=master :target: https://travis-ci.org/NeuralEnsemble/python-neo.png More information ---------------- - Home page: http://neuralensemble.org/neo - Mailing list: https://groups.google.com/forum/?fromgroups#!forum/neuralensemble - Documentation: http://packages.python.org/neo/ - Bug reports: https://github.com/NeuralEnsemble/python-neo/issues For installation instructions, see doc/source/install.rst :copyright: Copyright 2010-2014 by the Neo team, see AUTHORS. :license: 3-Clause Revised BSD License, see LICENSE.txt for details. Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Intended Audience :: Science/Research Classifier: License :: OSI Approved :: BSD License Classifier: Natural Language :: English Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.6 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.3 Classifier: Topic :: Scientific/Engineering neo-0.3.3/MANIFEST.in0000644000175000017500000000023512265516260015105 0ustar sgarciasgarcia00000000000000include README.rst prune drafts include examples/*.py recursive-include doc * prune doc/build exclude doc/source/images/*.svg exclude doc/source/images/*.dianeo-0.3.3/setup.cfg0000644000175000017500000000007312273723667015202 0ustar sgarciasgarcia00000000000000[egg_info] tag_build = tag_date = 0 tag_svn_revision = 0