labdata 0.0.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,547 @@
1
+ from .general import *
2
+ from .procedures import *
3
+
4
+ @dataschema
5
+ class Probe(dj.Manual):
6
+ definition = '''
7
+ probe_id : varchar(32) # probe id to keep track or re-uses
8
+ ---
9
+ probe_type : varchar(12) # probe type
10
+ probe_n_shanks : tinyint # number of shanks
11
+ probe_recording_channels : int # number of recording channels
12
+ '''
13
+
14
+ @dataschema
15
+ class ProbeInsertion(dj.Manual):
16
+ definition = '''
17
+ -> Procedure
18
+ -> Probe
19
+ ---
20
+ insertion_ap : float # anterior posterior distance from bregma
21
+ insertion_ml : float # median lateral distance from bregma
22
+ insertion_depth : float # insertion depth (how much shank is inserted from dura)
23
+ insertion_el : float # elevation of the probe (angle)
24
+ insertion_az : float # azimuth of the probe (angle)
25
+ insertion_spin = 0 : float # spin on the probe shanks
26
+ '''
27
+
28
+ @dataschema
29
+ class ProbeExtraction(dj.Manual):
30
+ definition = '''
31
+ -> Procedure
32
+ -> Probe
33
+ ---
34
+ extraction_successful : tinyint # boolean for successfull or not
35
+ '''
36
+
37
+ @dataschema
38
+ class ProbeConfiguration(dj.Manual):
39
+ definition = '''
40
+ -> Probe
41
+ configuration_id : smallint
42
+ ---
43
+ probe_n_channels : int # number of connected channels
44
+ probe_gain : float # gain of the probe (multiplication factor)
45
+ channel_idx : blob # index of the channels
46
+ channel_shank : blob # shank of each channel
47
+ channel_coords : blob # channel x,y position
48
+ '''
49
+ def add_from_spikeglx_metadata(self,metadata):
50
+ '''
51
+ Metadata can be a dictionary (with the metadata) or the path to an ap.meta file.
52
+ '''
53
+ from ..rules.ephys import get_probe_configuration
54
+ if not hasattr(metadata,'keys'):
55
+ conf = get_probe_configuration(metadata)
56
+ else:
57
+ conf = metadata
58
+
59
+ probeid = conf['probe_id']
60
+ if not len(Probe() & f'probe_id = "{probeid}"'):
61
+ Probe.insert1(dict(probe_id = conf['probe_id'],
62
+ probe_type = conf['probe_type'],
63
+ probe_n_shanks = conf['probe_n_shanks'],
64
+ probe_recording_channels = conf['probe_recording_channels']))
65
+ configs = (ProbeConfiguration() & f'probe_id = "{probeid}"').fetch(as_dict = True)
66
+ for c in configs:
67
+ if ((c['channel_coords'] == conf['channel_coords']).all() and
68
+ (c['channel_idx'] == conf['channel_idx']).all()):
69
+ print("The coords are the same, probe is already there. ")
70
+
71
+ return dict(probe_id = probeid,
72
+ configuration_id = c['configuration_id'],
73
+ sampling_rate = conf['sampling_rate'],
74
+ recording_software = conf['recording_software'],
75
+ recording_duration = conf['recording_duration'])
76
+ # add to configuration
77
+ confid = len(configs)+1
78
+ ProbeConfiguration.insert1(dict(probe_id = probeid,
79
+ configuration_id = confid,
80
+ probe_n_channels = conf['probe_n_channels'],
81
+ probe_gain = conf['probe_gain'],
82
+ channel_idx = conf['channel_idx'],
83
+ channel_shank = conf['channel_shank'],
84
+ channel_coords = conf['channel_coords']))
85
+ return dict(probe_id = probeid,
86
+ configuration_id = confid,
87
+ sampling_rate = conf['sampling_rate'],
88
+ recording_software = conf['recording_software'],
89
+ recording_duration = conf['recording_duration'])
90
+
91
+ @dataschema
92
+ class EphysRecording(dj.Imported):
93
+ definition = '''
94
+ -> Dataset
95
+ ---
96
+ n_probes : smallint # number of probes
97
+ recording_duration : float # duration of the recording
98
+ recording_software : varchar(56) # software_version
99
+ '''
100
+
101
+ class ProbeSetting(dj.Part):
102
+ definition = '''
103
+ -> master
104
+ probe_num : smallint # probe number
105
+ ---
106
+ -> ProbeConfiguration
107
+ sampling_rate : decimal(22,14) # sampling rate
108
+ '''
109
+ class ProbeFile(dj.Part):
110
+ definition = '''
111
+ -> EphysRecording.ProbeSetting
112
+ -> File # binary file that contains the data
113
+ '''
114
+
115
+ def add_spikeglx_recording(self,key):
116
+ '''
117
+ Adds a recording from Dataset ap.meta files.
118
+ '''
119
+ allpaths = pd.DataFrame((Dataset.DataFiles() & key).fetch()).file_path.values
120
+ paths = natsorted(list(filter( lambda x: x.endswith('.ap.meta'),
121
+ allpaths)))
122
+ keys = []
123
+ local_path = Path(prefs['local_paths'][0])
124
+ for iprobe, p in enumerate(paths):
125
+ # add each configuration
126
+ tmp = ProbeConfiguration().add_from_spikeglx_metadata(local_path/p)
127
+ tt = dict(key,n_probes = len(paths),probe_num = iprobe,**tmp)
128
+ EphysRecording.insert1(tt,
129
+ ignore_extra_fields = True,
130
+ skip_duplicates = True,
131
+ allow_direct_insert = True)
132
+ EphysRecording.ProbeSetting.insert1(tt,
133
+ ignore_extra_fields = True,
134
+ skip_duplicates = True,
135
+ allow_direct_insert = True)
136
+ # only working for spikeglx files for the moment.
137
+ pfiles = list(filter(lambda x: f'imec{iprobe}.ap.' in x,allpaths))
138
+ EphysRecording.ProbeFile().insert([
139
+ dict(tt,
140
+ **(File() & f'file_path = "{fi}"').proj().fetch(as_dict = True)[0])
141
+ for fi in pfiles],
142
+ skip_duplicates = True,
143
+ ignore_extra_fields = True,
144
+ allow_direct_insert = True)
145
+ EphysRecordingNoiseStats().populate(tt) # try to populate the NoiseStats table (this will take a couple of minutes)
146
+
147
+ def add_nidq_events(self,key = None):
148
+ if key is None:
149
+ key = [k for k in self] # create a list
150
+ if type(key) is dict:
151
+ key = [key]
152
+ for k in key:
153
+ dkey = (Dataset() & key).proj().fetch1()
154
+ dkey = dict(dkey,
155
+ stream_name = 'nidq')
156
+
157
+ if len(DatasetEvents() & dkey):
158
+ print(f' DatasetEvents for nidq are already there for {dkey}')
159
+ continue
160
+
161
+ allpaths = pd.DataFrame((Dataset.DataFiles() & k).fetch()).file_path.values
162
+ paths = list(filter( lambda x: '.nidq.' in x, allpaths))
163
+ file_paths = (File() & [dict(file_path = p) for p in paths])
164
+ try:
165
+ file_paths = file_paths.get() # download or get the path
166
+ except ValueError:
167
+ raise(ValueError(f'Error getting files for dataset {k}.'))
168
+ from ..rules.ephys import extract_events_from_nidq
169
+ events,daq = extract_events_from_nidq(file_paths)
170
+
171
+ if not len(events):
172
+ print(f'No events for key: {k}')
173
+ continue
174
+
175
+ DatasetEvents().insert1(dkey, allow_direct_insert = True)
176
+ DatasetEvents.Digital().insert([dict(dkey,**ev) for ev in events], allow_direct_insert = True)
177
+ return
178
+
179
+ @dataschema
180
+ class EphysRecordingNoiseStats(dj.Computed):
181
+ # Statistics to access recording noise on multisite
182
+ definition = '''
183
+ -> EphysRecording.ProbeSetting
184
+ ---
185
+ channel_median = NULL : longblob # nchannels*2 array, the 1st column is the start, 2nd at the end of the file
186
+ channel_max = NULL : longblob
187
+ channel_min = NULL : longblob
188
+ channel_peak_to_peak = NULL : longblob
189
+ channel_mad = NULL : longblob # median absolute deviation
190
+ '''
191
+ duration = 30 # duration of the stretch to sample (takes it from the start and the end of the file)
192
+
193
+ def make(self,key):
194
+ files = pd.DataFrame((EphysRecording.ProbeFile() & key).fetch())
195
+ assert len(files), ValueError(f'No files for dataset {key}')
196
+ # search for the recording files (this is set for compressed files now)
197
+ recording_file = list(filter(lambda x : 'ap.cbin' in x,files.file_path.values))
198
+ assert len(recording_file),ValueError(f'Could not find ap.cbin for {key}. Check Dataset.DataFiles?')
199
+ recording_file = recording_file[0]
200
+ filepath = find_local_filepath(recording_file, allowed_extensions = ['.ap.bin'])
201
+ assert not filepath is None, ValueError(f'File [{recording_file}] not found in local paths.')
202
+ # to get the gain, the channel_indices, and the sampling rate
203
+ config = pd.DataFrame((ProbeConfiguration()*EphysRecording.ProbeSetting() & key).fetch()).iloc[0]
204
+ # compute
205
+ from ..rules.ephys import ephys_noise_statistics_from_file
206
+ noisestats = ephys_noise_statistics_from_file(filepath,
207
+ duration = self.duration,
208
+ channel_indices = config.channel_idx,
209
+ sampling_rate = float(config.sampling_rate),
210
+ gain = config.probe_gain)
211
+ self.insert1(dict(key,**noisestats),ignore_extra_fields = True)
212
+
213
+
214
+ @dataschema
215
+ class SpikeSortingParams(dj.Manual):
216
+ definition = '''
217
+ parameter_set_num : int # number of the parameters set
218
+ ---
219
+ algorithm_name : varchar(64) # preprocessing and spike sorting algorithm
220
+ parameters_dict : varchar(2000) # parameters json formatted dictionary
221
+ code_link = NULL : varchar(300) # the software that preprocesses and sorts
222
+ '''
223
+
224
+ @dataschema
225
+ class SpikeSorting(dj.Manual):
226
+ definition = '''
227
+ -> EphysRecording.ProbeSetting
228
+ -> SpikeSortingParams
229
+ ---
230
+ n_pre_samples : smallint # to compute the waveform time
231
+ n_sorted_units = NULL : int # number of sorted units
232
+ n_detected_spikes = NULL : int # number of detected spikes
233
+ sorting_datetime = NULL : datetime # date of the spike sorting analysis
234
+ channel_indices = NULL : longblob # channel_map
235
+ channel_coords = NULL : longblob # channel_positions
236
+ -> [nullable] AnalysisFile.proj(features_file='file_path',features_storage='storage')
237
+ -> [nullable] AnalysisFile.proj(waveforms_file='file_path',waveforms_storage='storage')
238
+ '''
239
+ # For each sorting, create a "features.hdf5" file that has the: (this file can be > 4Gb)
240
+ # - template features
241
+ # - cluster indices
242
+ # - whitening_matrix
243
+ # - templates
244
+ # For each sorting create a "waveforms.hdf5" file that has the: (this file can be > 10Gb)
245
+ # - filtered waveform samples for each unit (1000 per unit)
246
+ # - indices of the extracted waveforms
247
+
248
+ class Segment(dj.Part):
249
+ definition = '''
250
+ -> master
251
+ segment_num : int # number of the segment
252
+ ---
253
+ offset_samples : int # offset where the traces comes from
254
+ segment : longblob # 2 second segment of data in the AP band
255
+ '''
256
+
257
+ class Unit(dj.Part):
258
+ definition = '''
259
+ -> master
260
+ unit_id : int # cluster id
261
+ ---
262
+ spike_times : longblob # in samples (uint64)
263
+ spike_positions = NULL : longblob # spike position in the electrode (float32)
264
+ spike_amplitudes = NULL : longblob # spike template amplitudes (float32)
265
+ '''
266
+ def get_sampling_rates(self):
267
+ sampling_rates = (self*EphysRecording.ProbeSetting()).fetch('sampling_rate')
268
+ return [float(s) for s in sampling_rates] # cast to float if decimal
269
+
270
+ def get_units_with_waveforms(self, return_seconds = True):
271
+ units = []
272
+ sampling_rates = self.get_sampling_rates()
273
+ # get the interpolation functions for all experiments if return_seconds = True
274
+ if return_seconds:
275
+ exps = (EphysRecording.ProbeSetting() & self.proj()).proj().fetch(as_dict = True)
276
+ interpolations = dict()
277
+ for e in exps:
278
+ k = '{subject_name}_{session_name}_{dataset_name}_{probe_num}'.format(**e)
279
+ try:
280
+ interpolations[k] = (StreamSync() & e).apply(None)
281
+ except:
282
+ interpolations[k] = None
283
+ for p,u,r in zip(self.proj(),self,sampling_rates):
284
+ w = (SpikeSorting.Waveforms() & p)
285
+ if len(w):
286
+ w = w.fetch('waveform_median')[0]
287
+ else:
288
+ w = None
289
+ units.append(dict(u, waveform_median = w))
290
+ if return_seconds:
291
+ k = '{subject_name}_{session_name}_{dataset_name}_{probe_num}'.format(**p)
292
+ if interpolations[k] is None:
293
+ units[-1]['spike_times'] = units[-1]['spike_times'].astype(np.float32)/np.float32(r)
294
+ else:
295
+ units[-1]['spike_times'] = interpolations[k](units[-1]['spike_times'].astype(np.float32))
296
+ return units
297
+
298
+ def get_spike_times(self, as_dict = True, return_seconds = True, extra_keys = [], warn = False, include_metrics = False):
299
+ '''
300
+ spike_times = get_spike_times()
301
+
302
+ Gets spike times corrected if the sync is applied.
303
+
304
+ as_dict = True
305
+ return_seconds = True
306
+ extra_keys = []
307
+ warn = False
308
+ include_metrics = False
309
+ '''
310
+ if return_seconds:
311
+ exps = (EphysRecording.ProbeSetting() & self.proj()).proj().fetch(as_dict = True)
312
+ interpolations = dict()
313
+ for e in exps:
314
+ k = '{subject_name}_{session_name}_{dataset_name}_{probe_num}'.format(**e)
315
+ try:
316
+ probe_num = e['probe_num']
317
+ interpolations[k] = (StreamSync() & e & f'stream_name = "imec{probe_num}"').apply(None, warn = False)
318
+ except AssertionError as err:
319
+ import warnings
320
+ warnings.warn(f"Using the sampling rate for spike times", RuntimeWarning)
321
+ interpolations[k] = lambda x: x/float((EphysRecording.ProbeSetting() & e).fetch1('sampling_rate'))
322
+ keys = ['subject_name','session_name','dataset_name','probe_num','parameter_set_num','unit_id','spike_times'] + extra_keys
323
+
324
+ if include_metrics:
325
+ units = (self*UnitMetrics).fetch(*keys,as_dict=True)
326
+ else:
327
+ units = self.fetch(*keys,as_dict=True)
328
+ if return_seconds:
329
+ for u in units:
330
+ k = '{subject_name}_{session_name}_{dataset_name}_{probe_num}'.format(**u)
331
+ u['spike_times'] = interpolations[k](u['spike_times'])
332
+ if as_dict:
333
+ return units
334
+ return [u['spike_times'] for u in units]
335
+
336
+
337
+ class Waveforms(dj.Part):
338
+ definition = '''
339
+ -> SpikeSorting.Unit
340
+ ---
341
+ waveform_median : longblob # average waveform (gain corrected in microvolt - float32)
342
+ '''
343
+
344
+ def delete(
345
+ self,
346
+ transaction = True,
347
+ safemode = None,
348
+ force_parts = False):
349
+
350
+ files = [f['waveforms_file'] for f in self]
351
+ files += [f['features_file'] for f in self]
352
+ super().delete(transaction = transaction,
353
+ safemode = safemode,
354
+ force_parts = force_parts)
355
+ if not len(self) == 0:
356
+ # delete analysis files
357
+ for t in files:
358
+ (AnalysisFile() & f'file_path = "{t}"').delete(force_parts=force_parts, safemode = safemode)
359
+
360
+ def delete_sorting(
361
+ self,
362
+ transaction = True,
363
+ safemode = None,
364
+ force_parts = False):
365
+ files = [f['waveforms_file'] for f in self]
366
+ files += [f['features_file'] for f in self]
367
+ if not len(files) == 0:
368
+ # delete analysis files
369
+ for t in files:
370
+ (AnalysisFile() & f'file_path = "{t}"').delete(transaction = transaction,
371
+ force_parts = force_parts,
372
+ safemode = safemode)
373
+
374
+ @analysisschema
375
+ class UnitMetrics(dj.Computed):
376
+ default_container = 'labdata-spks'
377
+ # Compute the metrics from the each unit,
378
+ # so we can recompute and add new ones if needed and not depend on the clustering
379
+ definition = '''
380
+ -> SpikeSorting.Unit
381
+ ---
382
+ num_spikes : int
383
+ depth = NULL : double
384
+ position = NULL : blob
385
+ shank = NULL : int
386
+ channel_index = NULL : int
387
+ n_electrodes_spanned = NULL : int
388
+ firing_rate = NULL : float
389
+ isi_contamination = NULL : float
390
+ isi_contamination_hill = NULL : float
391
+ amplitude_cutoff = NULL : float
392
+ presence_ratio = NULL : float
393
+ depth_drift_range = NULL : float
394
+ depth_drift_fluctuation = NULL : float
395
+ spike_amplitude = NULL : float
396
+ spike_duration = NULL : float
397
+ trough_time = NULL : float
398
+ trough_amplitude = NULL : float
399
+ fw3m = NULL : float
400
+ trough_gradient = NULL : float
401
+ peak_gradient = NULL : float
402
+ peak_time = NULL : float
403
+ peak_amplitude = NULL : float
404
+ polarity = NULL : tinyint
405
+ active_electrodes = NULL : blob
406
+ '''
407
+ def make(self, key):
408
+ dat = (SpikeSorting.Unit & key).get_units_with_waveforms()
409
+ assert len(dat) == 1, ValueError('Need to select only one unit')
410
+ dat = dat[0]
411
+
412
+ from spks.metrics import (isi_contamination,
413
+ isi_contamination_hill,
414
+ amplitude_cutoff,
415
+ presence_ratio,
416
+ firing_rate,
417
+ depth_stability)
418
+ from spks.waveforms import waveforms_position, compute_waveform_metrics
419
+
420
+ kk = {k:dat[k] for k in ['subject_name','session_name','dataset_name','probe_num']}
421
+ channel_coords,srate,wpre = (EphysRecording.ProbeSetting()*SpikeSorting() & kk
422
+ & f'parameter_set_num = {key["parameter_set_num"]}').fetch1(
423
+ 'channel_coords','sampling_rate','n_pre_samples')
424
+ channel_shanks,duration = (EphysRecording()*EphysRecording.ProbeSetting()*
425
+ ProbeConfiguration() & kk).fetch1('channel_shank','recording_duration')
426
+
427
+ metrics = dict(key)
428
+
429
+ metrics['num_spikes'] = len(dat['spike_times'])
430
+ if metrics['num_spikes'] > 5: # skip if less than 5 spikes
431
+ metrics['firing_rate'] = firing_rate(dat['spike_times'],0, t_max = duration)
432
+ if metrics['num_spikes'] > 50: # skip if less than 50 spikes
433
+ metrics['isi_contamination'] = isi_contamination(dat['spike_times'], T = duration)
434
+ metrics['isi_contamination_hill'] = isi_contamination_hill(dat['spike_times'],
435
+ T = duration)
436
+ metrics['amplitude_cutoff'] = amplitude_cutoff(dat['spike_amplitudes'])
437
+ metrics['presence_ratio'] = presence_ratio(dat['spike_times'],
438
+ t_min = 0, t_max = duration)
439
+ metrics['depth_drift_range'],metrics['depth_drift_fluctuation'] = depth_stability(
440
+ dat['spike_times'],
441
+ dat['spike_positions'][:,1],tmax = duration)
442
+ if not dat['waveform_median'] is None:
443
+ waves = dat['waveform_median']
444
+
445
+ pos,channel,active_idx = waveforms_position(np.expand_dims(dat['waveform_median'],
446
+ axis = 0),
447
+ channel_positions = channel_coords,
448
+ active_electrode_threshold = 3)
449
+ if not np.all(np.isfinite(pos)):
450
+ # this can happen when there is noise in the waveform estimate, choose the position of the peak channel then
451
+ pos = channel_coords[channel]
452
+ active_idx = [channel]
453
+ metrics['n_electrodes_spanned'] = len(active_idx[0])
454
+ if len(active_idx):
455
+ metrics['active_electrodes'] = np.array(active_idx).astype(int)
456
+ metrics['depth'] = pos[0][1] # TODO: estimate position from 6 channels around the peak channel
457
+ metrics['position'] = pos[0]
458
+ metrics['shank'] = channel_shanks[channel[0]]
459
+ metrics['channel_index'] = channel[0]
460
+ # the com only works if it is done only for the values that have spikes
461
+ wavemetrics = compute_waveform_metrics(waves[:,channel[0]],
462
+ wpre, float(srate))
463
+ metrics = dict(metrics,**wavemetrics)
464
+ metrics['spike_amplitude'] = np.abs(metrics['trough_amplitude']-metrics['peak_amplitude'])
465
+ self.insert1(metrics,skip_duplicates = True)
466
+
467
+ @analysisschema
468
+ class UnitCountCriteria(dj.Manual):
469
+ definition = '''
470
+ unit_criteria_id : int
471
+ ---
472
+ sua_criteria : varchar(2000)
473
+ mua_criteria = NULL : varchar(2000) # if NULL, subtracts the SUA labels.
474
+ '''
475
+
476
+ @analysisschema
477
+ class UnitCount(dj.Computed):
478
+ definition = '''
479
+ -> SpikeSorting
480
+ -> UnitCountCriteria
481
+ ---
482
+ all : int
483
+ sua : int
484
+ mua : int
485
+ '''
486
+ class Unit(dj.Part):
487
+ definition = '''
488
+ -> master
489
+ -> UnitMetrics
490
+ ---
491
+ passes : tinyint
492
+ '''
493
+
494
+ def make(self,key):
495
+ allu = pd.DataFrame((UnitMetrics() & key).fetch())
496
+ criteria = (UnitCountCriteria() &
497
+ f"unit_criteria_id = {key['unit_criteria_id']}").fetch('sua_criteria')[0]
498
+ suaidx = _apply_unit_criteria(allu, criteria)
499
+ sua = np.sum(suaidx)
500
+ muacriteria = (UnitCountCriteria() &
501
+ f"unit_criteria_id = {key['unit_criteria_id']}").fetch('mua_criteria')[0]
502
+ if muacriteria is None:
503
+ mua = len(allu) - np.sum(suaidx)
504
+ else:
505
+ mua = np.sum(_apply_unit_criteria(allu, muacriteria))
506
+ # Code for parsing the unit count criteria missing here.
507
+ unitcounts = dict(key,
508
+ all = len(allu),
509
+ sua = sua,
510
+ mua = mua)
511
+ self.insert1(unitcounts)
512
+ # select only a projection later, for now we ignore the extra fields
513
+ allu['passes'] = suaidx.astype(int)
514
+ keys = [dict(a,unit_criteria_id = key['unit_criteria_id']) for i,a in allu.iterrows()]
515
+ self.Unit.insert(keys, ignore_extra_fields = True) # add to the Unit part table
516
+
517
+ def _apply_unit_criteria(unitmetrics,criteria):
518
+ parameters = [p.strip(' ') for p in criteria.split('&')]
519
+ operators = {'<': np.less, '>': np.greater, '<=':np.less_equal, '>=':np.greater_equal, '==': np.equal, '!=':np.not_equal}
520
+ idx = []
521
+ for p in parameters:
522
+ for o in operators.keys():
523
+ if o in p:
524
+ n = [t.strip(' ') for t in p.split(o)]
525
+ try:
526
+ n[1] = float(n[1])
527
+ except:
528
+ pass
529
+ idx.append(operators[o](unitmetrics[n[0]].values,n[1]))
530
+ for i in idx:
531
+ idx[0] = np.logical_and(idx[0],i)
532
+ return idx[0]
533
+
534
+ from .histology import Atlas
535
+
536
+ @analysisschema
537
+ class ProbeBrainRegion(dj.Manual):
538
+ definition = '''
539
+ -> ProbeInsertion
540
+ -> ProbeConfiguration
541
+ -> Atlas.Region
542
+ ---
543
+ min_depth = NULL : double
544
+ max_depth = NULL : double
545
+ shanks = NULL : blob
546
+ electrodes = NULL : blob
547
+ '''