labdata 0.1.22__tar.gz → 0.1.23__tar.gz

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.
Files changed (74) hide show
  1. {labdata-0.1.22 → labdata-0.1.23}/PKG-INFO +1 -1
  2. {labdata-0.1.22 → labdata-0.1.23}/labdata/__init__.py +1 -1
  3. {labdata-0.1.22 → labdata-0.1.23}/labdata/cli.py +61 -3
  4. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/ephys.py +48 -10
  5. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/utils.py +15 -10
  6. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/compute.py +1 -1
  7. {labdata-0.1.22 → labdata-0.1.23}/labdata/rules/ephys.py +18 -6
  8. {labdata-0.1.22 → labdata-0.1.23}/labdata/rules/utils.py +18 -12
  9. {labdata-0.1.22 → labdata-0.1.23}/labdata/s3.py +211 -128
  10. {labdata-0.1.22 → labdata-0.1.23}/labdata/schema/ephys.py +102 -31
  11. {labdata-0.1.22 → labdata-0.1.23}/labdata.egg-info/PKG-INFO +1 -1
  12. {labdata-0.1.22 → labdata-0.1.23}/LICENSE +0 -0
  13. {labdata-0.1.22 → labdata-0.1.23}/README.md +0 -0
  14. {labdata-0.1.22 → labdata-0.1.23}/labdata/accounts.py +0 -0
  15. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/__init__.py +0 -0
  16. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/caiman.py +0 -0
  17. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/ec2.py +0 -0
  18. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/pcluster.py +0 -0
  19. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/pose.py +0 -0
  20. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/schedulers.py +0 -0
  21. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/singularity.py +0 -0
  22. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/submission.py +0 -0
  23. {labdata-0.1.22 → labdata-0.1.23}/labdata/compute/suite2p.py +0 -0
  24. {labdata-0.1.22 → labdata-0.1.23}/labdata/copy.py +0 -0
  25. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/__init__.py +0 -0
  26. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/activity.py +0 -0
  27. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/admin.py +0 -0
  28. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/ai_utils.py +0 -0
  29. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/console.py +0 -0
  30. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/decisiontask.py +0 -0
  31. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/docstrings.py +0 -0
  32. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/drift.py +0 -0
  33. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/ephys.py +0 -0
  34. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/events.py +0 -0
  35. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/imaging.py +0 -0
  36. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/index.py +0 -0
  37. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/launch.py +0 -0
  38. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/local_files.py +0 -0
  39. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/notes.py +0 -0
  40. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/procedures.py +0 -0
  41. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/raster.py +0 -0
  42. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/reads.py +0 -0
  43. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/schema_graph.py +0 -0
  44. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/segmentation.py +0 -0
  45. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/sorting.py +0 -0
  46. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/sync.py +0 -0
  47. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/upload.py +0 -0
  48. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/upload_worker.py +0 -0
  49. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/video.py +0 -0
  50. {labdata-0.1.22 → labdata-0.1.23}/labdata/dashboard/writes.py +0 -0
  51. {labdata-0.1.22 → labdata-0.1.23}/labdata/ephys_io.py +0 -0
  52. {labdata-0.1.22 → labdata-0.1.23}/labdata/ephys_signal.py +0 -0
  53. {labdata-0.1.22 → labdata-0.1.23}/labdata/export.py +0 -0
  54. {labdata-0.1.22 → labdata-0.1.23}/labdata/rules/__init__.py +0 -0
  55. {labdata-0.1.22 → labdata-0.1.23}/labdata/rules/imaging.py +0 -0
  56. {labdata-0.1.22 → labdata-0.1.23}/labdata/schema/__init__.py +0 -0
  57. {labdata-0.1.22 → labdata-0.1.23}/labdata/schema/general.py +0 -0
  58. {labdata-0.1.22 → labdata-0.1.23}/labdata/schema/histology.py +0 -0
  59. {labdata-0.1.22 → labdata-0.1.23}/labdata/schema/onephoton.py +0 -0
  60. {labdata-0.1.22 → labdata-0.1.23}/labdata/schema/procedures.py +0 -0
  61. {labdata-0.1.22 → labdata-0.1.23}/labdata/schema/tasks.py +0 -0
  62. {labdata-0.1.22 → labdata-0.1.23}/labdata/schema/twophoton.py +0 -0
  63. {labdata-0.1.22 → labdata-0.1.23}/labdata/schema/utils.py +0 -0
  64. {labdata-0.1.22 → labdata-0.1.23}/labdata/schema/video.py +0 -0
  65. {labdata-0.1.22 → labdata-0.1.23}/labdata/stacks.py +0 -0
  66. {labdata-0.1.22 → labdata-0.1.23}/labdata/utils.py +0 -0
  67. {labdata-0.1.22 → labdata-0.1.23}/labdata/widgets.py +0 -0
  68. {labdata-0.1.22 → labdata-0.1.23}/labdata.egg-info/SOURCES.txt +0 -0
  69. {labdata-0.1.22 → labdata-0.1.23}/labdata.egg-info/dependency_links.txt +0 -0
  70. {labdata-0.1.22 → labdata-0.1.23}/labdata.egg-info/entry_points.txt +0 -0
  71. {labdata-0.1.22 → labdata-0.1.23}/labdata.egg-info/requires.txt +0 -0
  72. {labdata-0.1.22 → labdata-0.1.23}/labdata.egg-info/top_level.txt +0 -0
  73. {labdata-0.1.22 → labdata-0.1.23}/pyproject.toml +0 -0
  74. {labdata-0.1.22 → labdata-0.1.23}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labdata
3
- Version: 0.1.22
3
+ Version: 0.1.23
4
4
  Summary: Package to manage data in experimental neuroscience labs
5
5
  Author-email: Joao Couto <jpcouto@gmail.com>
6
6
  Project-URL: Homepage, https://github.com/jcouto/labdata
@@ -1,4 +1,4 @@
1
- VERSION = "0.1.22"
1
+ VERSION = "0.1.23"
2
2
 
3
3
  from .utils import *
4
4
  from .copy import copy_to_upload_server
@@ -615,7 +615,9 @@ Administration commands (don't run on experimental computers):
615
615
  whose output goes to ``task_log`` and ``~/.labdata/logs``,
616
616
  instead of in the terminal.
617
617
  --queue
618
- Print the current job queue for all targets and exit.
618
+ Print the scheduler queue of each target, then the ``ComputeTask``
619
+ table by status with the last line of each task log, and exit.
620
+ With an analysis name, only the tasks of that analysis.
619
621
  --delete-computes
620
622
  Delete all ``ComputeTask`` entries for this analysis (careful).
621
623
  --
@@ -666,6 +668,8 @@ Available analysis are
666
668
  targets = np.unique([prefs['compute']['default_target']]+
667
669
  list(prefs['compute']['remotes'].keys()))
668
670
  for q in targets:
671
+ if q == 'local': # no scheduler queue, the tasks are in the table below
672
+ continue
669
673
  queue = check_queue(target = q)
670
674
  if queue is None:
671
675
  continue
@@ -678,7 +682,14 @@ Available analysis are
678
682
  cc = tcolor['g']
679
683
  else:
680
684
  cc = tcolor['r']
681
- print(f"\t {j['jobid']} {j['name']} {cc('running')} time: {tcolor['y'](j['time'])}\n\t\tpartition: {j['partition']} node: {j['nodelist(reason)']} ")
685
+ print(f"\t {j['jobid']} {j['name']} {cc(j['state'].lower())} time: {tcolor['y'](j['time'])}\n\t\tpartition: {j['partition']} node: {j['nodelist(reason)']} ")
686
+ # the ComputeTask table, optionally for one analysis: labdata run <analysis> --queue
687
+ qparser = argparse.ArgumentParser(add_help = False)
688
+ qparser.add_argument('analysis', nargs = '?', default = None)
689
+ qparser.add_argument('-p','--project', default = _get_project())
690
+ qargs, _ = qparser.parse_known_args(argum)
691
+ from .utils import load_project_schema
692
+ self._print_compute_tasks(load_project_schema(qargs.project), analysis = qargs.analysis)
682
693
  return
683
694
  args = parser.parse_args(argum)
684
695
  if '--delete-computes' in argum:
@@ -2051,7 +2062,54 @@ examples:
2051
2062
  type = str,
2052
2063
  help= f'Select project: {project}')
2053
2064
  return parser
2054
-
2065
+
2066
+ def _print_compute_tasks(self, schema, analysis = None, limit = 10):
2067
+ '''Print the ComputeTask table by status, with the last line of each task log.
2068
+
2069
+ Shows the most recent ``limit`` tasks of each status and the count of the rest.
2070
+ '''
2071
+ tasks = schema.ComputeTask()
2072
+ if not analysis is None:
2073
+ tasks = tasks & dict(task_name = analysis)
2074
+ title = 'ComputeTask table' + (f' [{analysis}]' if not analysis is None else '')
2075
+ print(tcolor['m'](f"++++++++++++++++++++ {title} ++++++++++++++++++++"))
2076
+ if not len(tasks):
2077
+ print('\t no compute tasks.')
2078
+ return
2079
+ statuses = [('WORKING', tcolor['c']), ('WAITING', tcolor['y']), ('ON SERVER', tcolor['b']),
2080
+ ('FAILED', tcolor['r']), ('CANCELED', tcolor['o']), ('COMPLETED', tcolor['g'])]
2081
+ def _brief(log, width = 100):
2082
+ if log is None:
2083
+ return ''
2084
+ if isinstance(log, bytes):
2085
+ log = log.decode('utf-8', errors = 'replace')
2086
+ lines = [l.strip() for l in str(log).splitlines() if len(l.strip())]
2087
+ line = ' '.join(lines[-1].split()) if len(lines) else ''
2088
+ return line if len(line) <= width else line[:width-3] + '...'
2089
+ def _time(t):
2090
+ return t.strftime('%Y-%m-%d %H:%M') if not t is None else ''
2091
+ for status, cc in statuses:
2092
+ sel = tasks & f'task_status LIKE "%{status}%"'
2093
+ n = len(sel)
2094
+ if not n:
2095
+ continue
2096
+ print(cc(f"{status} ({n})"))
2097
+ rows = sel.fetch('job_id', 'task_name', 'task_target', 'task_host', 'subject_name',
2098
+ 'session_name', 'task_starttime', 'task_endtime', 'task_log',
2099
+ as_dict = True, order_by = 'job_id DESC', limit = limit)
2100
+ for r in rows:
2101
+ where = '/'.join([str(r[k]) for k in ('subject_name', 'session_name') if not r[k] is None])
2102
+ when = _time(r['task_endtime'] if status in ('COMPLETED', 'FAILED', 'CANCELED') and
2103
+ not r['task_endtime'] is None else r['task_starttime'])
2104
+ print(f"\t {r['job_id']} {r['task_name']} {where} "
2105
+ f"target: {r['task_target']} host: {r['task_host']}" +
2106
+ (f" {tcolor['y'](when)}" if len(when) else ''))
2107
+ log = _brief(r['task_log'])
2108
+ if len(log):
2109
+ print(f"\t\t{log}")
2110
+ if n > limit:
2111
+ print(f"\t ... and {n - limit} more")
2112
+
2055
2113
  def _get_default_arg(self,argument,cli_arg = 'submit', default = None):
2056
2114
  # checks if there is a default in the options
2057
2115
  if not f'{cli_arg}_defaults' in labdata_preferences.keys():
@@ -35,7 +35,7 @@ class SpksCompute(BaseCompute):
35
35
  low_pass = 300.,
36
36
  high_pass = 13000.)
37
37
  self.parameter_keys = ['motion_correction','low_pass','high_pass','thresholds','remove_cross_duplicates',
38
- 'waveforms_from_input','dredge']
38
+ 'waveforms_from_input','dredge','remove_channels_without_signal']
39
39
  # the parameters that go on the SpikeSortingParams
40
40
  self.use_hdf5 = True # flag to use h5py or zarr format for the waveforms.
41
41
  self.parameter_set_num = None # identifier in SpikeSortingParams
@@ -131,9 +131,15 @@ class SpksCompute(BaseCompute):
131
131
  action='store', default = None, nargs=2, type = float,
132
132
  help = "Interval to sort in seconds: start end")
133
133
  parser.add_argument('--dredge',
134
- action='store_true', default = False,
134
+ action='store_true', default = False,
135
135
  help = "Motion correction using dredge")
136
-
136
+ parser.add_argument('--remove-channels-without-signal',
137
+ action='store', default = None, nargs = '*', type = str,
138
+ choices = ['amplitude','correlation','coherence'],
139
+ help = "Leave out the channels without signal before sorting and the common average reference. "
140
+ "Methods: amplitude (low MAD in the sorting band), correlation and coherence (low correlation or phase coherence "
141
+ "with the channels within 300 um on the same shank, LFP band). Without methods: amplitude coherence.")
142
+
137
143
  args = parser.parse_args(arguments[1:])
138
144
  if 'ks2.5' in args.method: # defaults for ks2.5
139
145
  self.parameters = dict(algorithm_name = 'spks_kilosort2.5',
@@ -163,7 +169,12 @@ class SpksCompute(BaseCompute):
163
169
  self.parameters['waveforms_from_input'] = False
164
170
  if args.dredge:
165
171
  # default is to extract from the original input file
166
- self.parameters['dredge'] = True
172
+ self.parameters['dredge'] = True
173
+ if not args.remove_channels_without_signal is None:
174
+ methods = args.remove_channels_without_signal
175
+ if not len(methods):
176
+ methods = ['amplitude','coherence']
177
+ self.parameters['remove_channels_without_signal'] = sorted(set(methods))
167
178
  print(args)
168
179
  if not args.thresholds is None:
169
180
  self.parameters['thresholds'] = args.thresholds
@@ -294,6 +305,13 @@ class SpksCompute(BaseCompute):
294
305
  dredge_motion_correction = False
295
306
  if 'dredge' in self.parameters.keys():
296
307
  dredge_motion_correction = self.parameters['dredge']
308
+ preprocessing = dict()
309
+ if self.parameters.get('remove_channels_without_signal', None):
310
+ # channels without signal are left out before the filters and the common average reference
311
+ from spks.raw import default_filter_pipeline_par
312
+ preprocessing['filter_pipeline_par'] = [dict(function = 'remove_channels_without_signal',
313
+ methods = self.parameters['remove_channels_without_signal'])] + [
314
+ dict(d) for d in default_filter_pipeline_par]
297
315
  # run kilosort using spks for preprocessing.
298
316
  if self.parameters['algorithm_name'] == 'spks_kilosort2.5':
299
317
  results_folder = run_kilosort(version = '2.5',sessionfiles = probepath,
@@ -303,7 +321,8 @@ class SpksCompute(BaseCompute):
303
321
  thresholds = self.parameters['thresholds'],
304
322
  lowpass = self.parameters['low_pass'],
305
323
  highpass = self.parameters['high_pass'],
306
- dredge_motion_correction = dredge_motion_correction)
324
+ dredge_motion_correction = dredge_motion_correction,
325
+ **preprocessing)
307
326
  elif self.parameters['algorithm_name'] == 'spks_kilosort3.0':
308
327
  results_folder = run_kilosort(version = '3.0',
309
328
  sessionfiles = probepath,
@@ -313,7 +332,8 @@ class SpksCompute(BaseCompute):
313
332
  thresholds = self.parameters['thresholds'],
314
333
  lowpass = self.parameters['low_pass'],
315
334
  highpass = self.parameters['high_pass'],
316
- dredge_motion_correction = dredge_motion_correction)
335
+ dredge_motion_correction = dredge_motion_correction,
336
+ **preprocessing)
317
337
  elif self.parameters['algorithm_name'] == 'spks_kilosort4.0':
318
338
  results_folder = run_kilosort(version = '4.0',
319
339
  sessionfiles = probepath,
@@ -323,7 +343,8 @@ class SpksCompute(BaseCompute):
323
343
  thresholds = self.parameters['thresholds'],
324
344
  lowpass = self.parameters['low_pass'],
325
345
  highpass = self.parameters['high_pass'],
326
- dredge_motion_correction = dredge_motion_correction)
346
+ dredge_motion_correction = dredge_motion_correction,
347
+ **preprocessing)
327
348
  elif self.parameters['algorithm_name'] == 'spks_mountainsort5':
328
349
  raise(NotImplemented(f"[{self.name} job] - Algorithm {self.parameters['algorithm_name']} not implemented."))
329
350
  else:
@@ -416,6 +437,10 @@ class SpksCompute(BaseCompute):
416
437
  del clu
417
438
  clu = Clusters(results_folder, get_waveforms = False, get_metrics = False)
418
439
  clu.compute_template_amplitudes_and_depths()
440
+ # channels of the sorting in the numbering of the recorded file (channels can be left out before sorting)
441
+ sorting_channel_indices = clu.channel_map.flatten()
442
+ if not clu.metadata is None and 'original_channels' in clu.metadata.keys():
443
+ sorting_channel_indices = np.asarray(clu.metadata['original_channels']).flatten()[sorting_channel_indices]
419
444
  base_keys = []
420
445
  ssdict = [] # sortings
421
446
  udict = [] # unit
@@ -439,7 +464,7 @@ class SpksCompute(BaseCompute):
439
464
  n_detected_spikes = len(spikes),
440
465
  sorting_datetime = datetime.fromtimestamp(
441
466
  Path(results_folder).stat().st_ctime),
442
- sorting_channel_indices = clu.channel_map.flatten(),
467
+ sorting_channel_indices = sorting_channel_indices,
443
468
  sorting_channel_coords = clu.channel_positions))
444
469
  udict.append([]) # list of lists
445
470
  for iclu in np.unique(clusters):
@@ -569,10 +594,14 @@ class SpksCompute(BaseCompute):
569
594
  dat = map_binary(binaryfile, nchannels = nchannels)
570
595
  nsamples = int(clu.sampling_rate*2)
571
596
  offset_samples = int(np.random.uniform(offsets[0]+nsamples, offsets[1]-nsamples-1))
597
+ segment = np.array(dat[offset_samples : offset_samples + nsamples])
598
+ # statistics in microvolt with the gains of the waveforms, UnitMetrics reads the noise from here
599
+ from ..rules.ephys import channel_statistics
572
600
  self.schema.SpikeSorting.Segment.insert1(dict(base_key,
573
601
  segment_num = 1,
574
602
  offset_samples = offset_samples - offsets[0],
575
- segment = np.array(dat[offset_samples : offset_samples + nsamples])))
603
+ segment = segment,
604
+ **channel_statistics(segment.astype(np.float32)*clu.channel_gains)))
576
605
  del dat
577
606
  self.set_job_status(job_log = f'Completed {base_key}')
578
607
  from labdata.schema import UnitMetrics
@@ -632,7 +661,9 @@ def select_random_waveforms(unit_dict, wpre=45, wpost=45, duration=None, nmax_wa
632
661
 
633
662
  Adds a ``waveform_indices`` array to each unit dict, drawing up to
634
663
  ``nmax_waves`` spikes from the first quarter and last quarter of the
635
- recording. Spikes too close to the file boundaries (within ``wpre`` or
664
+ recording. A unit with no spikes in either quarter gets up to
665
+ ``2*nmax_waves`` spikes from the whole recording. Spikes too close to
666
+ the file boundaries (within ``wpre`` or
636
667
  ``wpost`` samples) are excluded.
637
668
 
638
669
  Parameters
@@ -668,6 +699,13 @@ def select_random_waveforms(unit_dict, wpre=45, wpost=45, duration=None, nmax_wa
668
699
  sel += [t for t in np.random.choice(s_end, nmax_waves, replace=True)]
669
700
  else:
670
701
  sel += [t for t in s_end]
702
+ if not len(sel):
703
+ # the unit fires only in the middle of the recording, take the spikes from anywhere
704
+ s_all = s[(s>(wpre+2)) & (s<(duration-2*wpost))]
705
+ if len(s_all)>2*nmax_waves:
706
+ sel = [t for t in np.random.choice(s_all, 2*nmax_waves, replace=True)]
707
+ else:
708
+ sel = [t for t in s_all]
671
709
  u['waveform_indices'] = np.sort(np.array(sel).flatten()) # add this to the
672
710
  return unit_dict
673
711
 
@@ -598,16 +598,21 @@ class BaseCompute():
598
598
  job_waiting = 0)
599
599
 
600
600
  def cleanup_function(job_id=self.job_id):
601
- '''Put the task back in the queue (CANCELLED, waiting) if the process stops while it works.'''
602
- status = (self.schema.ComputeTask() & dict(job_id = job_id)).fetch(as_dict = True)[0]
603
- if status['task_status'] in ['WORKING']:
604
- # printed, so labdata logpipe keeps the reason in task_log
605
- print(f'[task {job_id}] INTERRUPTED on {prefs["hostname"]} (the process was stopped); '
606
- 'the task is back in the queue.', flush = True)
607
- self.schema.ComputeTask.update1(dict(job_id = job_id,
608
- task_status = 'CANCELLED',
609
- task_waiting = 1,
610
- task_endtime = datetime.now()))
601
+ '''Put the task back in the queue (CANCELLED, waiting) if the process stops while it works.
602
+
603
+ Runs from a signal handler, maybe while the task waits on a query: it writes on a
604
+ connection of its own, and to the output directly (labdata logpipe keeps the line).'''
605
+ from ..rules.utils import _new_connection
606
+ try:
607
+ conn = _new_connection()
608
+ conn.query(f'''UPDATE {self.schema.ComputeTask.full_table_name}
609
+ SET task_status = "CANCELLED", task_waiting = 1, task_endtime = NOW()
610
+ WHERE job_id = %s AND task_status = "WORKING"''', args = (job_id,))
611
+ conn.close()
612
+ os.write(1, (f'[task {job_id}] INTERRUPTED on {prefs["hostname"]} (the process was stopped); '
613
+ 'the task is back in the queue.\n').encode())
614
+ except Exception as err:
615
+ os.write(2, f'[task {job_id}] Could not put the task back in the queue: {err}\n'.encode())
611
616
 
612
617
  self.cleanup_function = cleanup_function
613
618
  self.register_safe_exit()
@@ -382,7 +382,7 @@ def _queue_view(schema):
382
382
  status_field='task_status',
383
383
  target_field='task_target',
384
384
  cols=['job_id', 'task_name', 'task_status', 'task_host', 'task_target',
385
- 'task_waiting', 'task_log', 'subject_name', 'session_name',
385
+ 'task_waiting', 'task_cmd', 'task_log', 'subject_name', 'session_name',
386
386
  'task_starttime', 'task_endtime'],
387
387
  delete_key='compute',
388
388
  resubmit_fn=lambda job_ids, target_override=None: _resubmit_jobs(schema, job_ids, project, target_override=target_override),
@@ -278,13 +278,25 @@ def ephys_noise_statistics_from_file(filepath, channel_indices, gain,
278
278
  channel_mad = np.zeros((len(channel_indices),len(dd))),
279
279
  channel_max = np.zeros((len(channel_indices),len(dd))),
280
280
  channel_min = np.zeros((len(channel_indices),len(dd))))
281
- from scipy.stats import median_abs_deviation
282
281
  for i,d in enumerate(dd):
283
- res['channel_mad'][:,i] = median_abs_deviation(d[:,channel_indices],axis = 0)
284
- res['channel_max'][:,i] = np.max(d[:,channel_indices],axis = 0)
285
- res['channel_min'][:,i] = np.min(d[:,channel_indices],axis = 0)
286
- res['channel_median'][:,i] = np.median(d[:,channel_indices],axis = 0)
287
- res['channel_peak_to_peak'][:,i] = res['channel_max'][:,i]-res['channel_min'][:,i]
282
+ for k,v in channel_statistics(d[:,channel_indices]).items():
283
+ res[k][:,i] = v
284
+ return res
285
+
286
+ def channel_statistics(data):
287
+ '''Return the peak-to-peak, median, MAD, max and min of each channel.
288
+
289
+ ``data`` is samples by channels, usually in microvolt. The MAD is the
290
+ unscaled median absolute deviation; divide it by 0.6745 for the standard
291
+ deviation of Gaussian noise. Each value of the returned dict has one entry
292
+ per channel.
293
+ '''
294
+ from scipy.stats import median_abs_deviation
295
+ res = dict(channel_mad = median_abs_deviation(data,axis = 0),
296
+ channel_max = np.max(data,axis = 0),
297
+ channel_min = np.min(data,axis = 0),
298
+ channel_median = np.median(data,axis = 0))
299
+ res['channel_peak_to_peak'] = res['channel_max']-res['channel_min']
288
300
  return res
289
301
 
290
302
  def extract_events_from_nidq(paths):
@@ -116,21 +116,27 @@ class UploadRule():
116
116
  self._release_interrupt_handler()
117
117
 
118
118
  def _interrupted(self):
119
- '''Mark the job FAILED when the process stops while it is WORKING.'''
119
+ '''Mark the job FAILED when the process stops while it is WORKING.
120
+
121
+ This runs from a signal handler, maybe while the job waits on a query:
122
+ it writes on a connection of its own and to the output file directly,
123
+ since the job's connection and ``print`` may be in use.
124
+ '''
120
125
  from ..schema import UploadJob
126
+ if self._stage_stop is not None:
127
+ self._stage_stop.set() # no more progress lines
128
+ host = self.prefs['hostname']
121
129
  try:
122
- job = (UploadJob() & dict(job_id = self.job_id)).fetch(as_dict = True)
123
- if len(job) and job[0]['job_status'] == 'WORKING':
124
- last = (job[0]['job_log'] or '')[:300]
125
- if self._stage_stop is not None:
126
- self._stage_stop.set() # no more progress lines
127
- self._stage = None
128
- self.set_job_status(job_status = 'FAILED', job_endtime = datetime.now(),
129
- job_log = f'INTERRUPTED on {self.prefs["hostname"]} '
130
- f'(the process was stopped) during: {last}')
131
- print(f'Job {self.job_id} was interrupted; marked FAILED.', flush = True)
130
+ conn = _new_connection()
131
+ conn.query(f'''UPDATE {UploadJob.full_table_name}
132
+ SET job_status = "FAILED", job_waiting = 0, job_endtime = NOW(), job_host = %s,
133
+ job_log = LEFT(CONCAT("INTERRUPTED on ", %s, " (the process was stopped) during: ",
134
+ COALESCE(job_log, "")), 500)
135
+ WHERE job_id = %s AND job_status = "WORKING"''', args = (host, host, self.job_id))
136
+ conn.close()
137
+ os.write(1, f'Job {self.job_id}: interrupted; marked FAILED if it was working.\n'.encode())
132
138
  except Exception as err:
133
- print(f'Could not mark job {self.job_id} as interrupted: {err}', flush = True)
139
+ os.write(2, f'Could not mark job {self.job_id} as interrupted: {err}\n'.encode())
134
140
 
135
141
  def _hold_interrupt_handler(self):
136
142
  import safe_exit