labdata 0.1.24__tar.gz → 0.1.25__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 (86) hide show
  1. {labdata-0.1.24 → labdata-0.1.25}/PKG-INFO +1 -1
  2. {labdata-0.1.24 → labdata-0.1.25}/labdata/__init__.py +1 -1
  3. {labdata-0.1.24 → labdata-0.1.25}/labdata/cli.py +244 -18
  4. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/ephys.py +4 -3
  5. labdata-0.1.25/labdata/compute/schedulers.py +1029 -0
  6. {labdata-0.1.24 → labdata-0.1.25}/labdata/copy.py +29 -0
  7. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/activity.py +1 -1
  8. labdata-0.1.25/labdata/dashboard/ephys/__init__.py +1 -0
  9. {labdata-0.1.24/labdata/dashboard → labdata-0.1.25/labdata/dashboard/ephys}/sorting.py +94 -38
  10. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/index.py +2 -2
  11. {labdata-0.1.24 → labdata-0.1.25}/labdata/rules/ephys.py +48 -16
  12. {labdata-0.1.24 → labdata-0.1.25}/labdata/schema/ephys.py +82 -83
  13. {labdata-0.1.24 → labdata-0.1.25}/labdata/widgets.py +3 -1
  14. {labdata-0.1.24 → labdata-0.1.25}/labdata.egg-info/PKG-INFO +1 -1
  15. {labdata-0.1.24 → labdata-0.1.25}/labdata.egg-info/SOURCES.txt +5 -4
  16. {labdata-0.1.24 → labdata-0.1.25}/pyproject.toml +1 -1
  17. labdata-0.1.24/labdata/compute/schedulers.py +0 -547
  18. {labdata-0.1.24 → labdata-0.1.25}/LICENSE +0 -0
  19. {labdata-0.1.24 → labdata-0.1.25}/README.md +0 -0
  20. {labdata-0.1.24 → labdata-0.1.25}/labdata/accounts.py +0 -0
  21. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/__init__.py +0 -0
  22. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/caiman.py +0 -0
  23. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/ec2.py +0 -0
  24. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/pcluster.py +0 -0
  25. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/pose.py +0 -0
  26. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/singularity.py +0 -0
  27. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/spikeinterface.py +0 -0
  28. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/submission.py +0 -0
  29. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/suite2p.py +0 -0
  30. {labdata-0.1.24 → labdata-0.1.25}/labdata/compute/utils.py +0 -0
  31. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/__init__.py +0 -0
  32. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/admin.py +0 -0
  33. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/ai_utils.py +0 -0
  34. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/compute.py +0 -0
  35. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/console.py +0 -0
  36. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/decisiontask.py +0 -0
  37. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/docstrings.py +0 -0
  38. {labdata-0.1.24/labdata/dashboard → labdata-0.1.25/labdata/dashboard/ephys}/drift.py +0 -0
  39. {labdata-0.1.24/labdata/dashboard → labdata-0.1.25/labdata/dashboard/ephys}/raster.py +0 -0
  40. /labdata-0.1.24/labdata/dashboard/ephys.py → /labdata-0.1.25/labdata/dashboard/ephys/recordings.py +0 -0
  41. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/events.py +0 -0
  42. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/imaging.py +0 -0
  43. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/launch.py +0 -0
  44. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/local_files.py +0 -0
  45. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/notes.py +0 -0
  46. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/procedures.py +0 -0
  47. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/reads.py +0 -0
  48. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/schema_graph.py +0 -0
  49. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/segmentation.py +0 -0
  50. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/sync.py +0 -0
  51. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/upload.py +0 -0
  52. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/upload_worker.py +0 -0
  53. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/video.py +0 -0
  54. {labdata-0.1.24 → labdata-0.1.25}/labdata/dashboard/writes.py +0 -0
  55. {labdata-0.1.24 → labdata-0.1.25}/labdata/ephys_io.py +0 -0
  56. {labdata-0.1.24 → labdata-0.1.25}/labdata/ephys_signal.py +0 -0
  57. {labdata-0.1.24 → labdata-0.1.25}/labdata/export/__init__.py +0 -0
  58. {labdata-0.1.24 → labdata-0.1.25}/labdata/export/database.py +0 -0
  59. {labdata-0.1.24 → labdata-0.1.25}/labdata/export/docker/Dockerfile.in +0 -0
  60. {labdata-0.1.24 → labdata-0.1.25}/labdata/export/docker/entrypoint.sh +0 -0
  61. {labdata-0.1.24 → labdata-0.1.25}/labdata/export/docker/load_database.sh +0 -0
  62. {labdata-0.1.24 → labdata-0.1.25}/labdata/export/docker/mysql.cnf +0 -0
  63. {labdata-0.1.24 → labdata-0.1.25}/labdata/export/docker.py +0 -0
  64. {labdata-0.1.24 → labdata-0.1.25}/labdata/export/selection.py +0 -0
  65. {labdata-0.1.24 → labdata-0.1.25}/labdata/export/share.py +0 -0
  66. {labdata-0.1.24 → labdata-0.1.25}/labdata/export/users.py +0 -0
  67. {labdata-0.1.24 → labdata-0.1.25}/labdata/rules/__init__.py +0 -0
  68. {labdata-0.1.24 → labdata-0.1.25}/labdata/rules/imaging.py +0 -0
  69. {labdata-0.1.24 → labdata-0.1.25}/labdata/rules/utils.py +0 -0
  70. {labdata-0.1.24 → labdata-0.1.25}/labdata/s3.py +0 -0
  71. {labdata-0.1.24 → labdata-0.1.25}/labdata/schema/__init__.py +0 -0
  72. {labdata-0.1.24 → labdata-0.1.25}/labdata/schema/general.py +0 -0
  73. {labdata-0.1.24 → labdata-0.1.25}/labdata/schema/histology.py +0 -0
  74. {labdata-0.1.24 → labdata-0.1.25}/labdata/schema/onephoton.py +0 -0
  75. {labdata-0.1.24 → labdata-0.1.25}/labdata/schema/procedures.py +0 -0
  76. {labdata-0.1.24 → labdata-0.1.25}/labdata/schema/tasks.py +0 -0
  77. {labdata-0.1.24 → labdata-0.1.25}/labdata/schema/twophoton.py +0 -0
  78. {labdata-0.1.24 → labdata-0.1.25}/labdata/schema/utils.py +0 -0
  79. {labdata-0.1.24 → labdata-0.1.25}/labdata/schema/video.py +0 -0
  80. {labdata-0.1.24 → labdata-0.1.25}/labdata/stacks.py +0 -0
  81. {labdata-0.1.24 → labdata-0.1.25}/labdata/utils.py +0 -0
  82. {labdata-0.1.24 → labdata-0.1.25}/labdata.egg-info/dependency_links.txt +0 -0
  83. {labdata-0.1.24 → labdata-0.1.25}/labdata.egg-info/entry_points.txt +0 -0
  84. {labdata-0.1.24 → labdata-0.1.25}/labdata.egg-info/requires.txt +0 -0
  85. {labdata-0.1.24 → labdata-0.1.25}/labdata.egg-info/top_level.txt +0 -0
  86. {labdata-0.1.24 → labdata-0.1.25}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: labdata
3
- Version: 0.1.24
3
+ Version: 0.1.25
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.24"
1
+ VERSION = "0.1.25"
2
2
  __version__ = VERSION
3
3
 
4
4
  from .utils import *
@@ -32,7 +32,7 @@ class CLI_parser():
32
32
  logpipe
33
33
  Pipe stdout into a ``ComputeTask.task_log`` entry.
34
34
  run_container
35
- Launch a local Apptainer container interactively.
35
+ Open a shell or Jupyter Lab in a container, here or on a node of a remote cluster.
36
36
  build_container
37
37
  Build Apptainer ``.sif`` images from definition files.
38
38
  cluster
@@ -56,7 +56,7 @@ Command to start the dashboard webpage:
56
56
 
57
57
  Data manipulation commands are:
58
58
 
59
- {tcolor["y"]("subjects")} List subjects
59
+ {tcolor["y"]("subjects")} List subjects (--add <subject> adds one)
60
60
  {tcolor["y"]("sessions")} -a <subject> List sessions
61
61
  {tcolor["y"]("get")} -a <subject> -s <session> Download data from one session if not already there
62
62
  {tcolor["y"]("put")} -a <subject> -s <session> Copies a dataset to the server to be used
@@ -71,7 +71,7 @@ Data analysis commands:
71
71
  Other
72
72
  logpipe <compute_task_number> Appends stdout log to a ComputeTask
73
73
  build-container Builds and uploads singularity/apptainer containers to S3
74
- run-container Launches a container in execution mode
74
+ run-container <container> [-t <remote>] Opens a shell or Jupyter Lab in a container, here or on a cluster node
75
75
 
76
76
  Administration commands (don't run on experimental computers):
77
77
  {tcolor["r"]("upload")} Sends pending data to S3 (applies upload rules)
@@ -207,6 +207,8 @@ Administration commands (don't run on experimental computers):
207
207
  labdata subjects -f <name_substring>
208
208
  labdata subjects -s M
209
209
  labdata subjects --include-size
210
+ labdata subjects --add <subject> --dob <YYYY-MM-DD> --sex <M|F|U> -u <user>
211
+ labdata subjects --add <subject> --dob <YYYY-MM-DD> --sex <M|F|U> --strain <strain> -u <user>
210
212
 
211
213
  Options
212
214
  -------
@@ -220,6 +222,12 @@ Administration commands (don't run on experimental computers):
220
222
  Project name.
221
223
  --include-size
222
224
  Show total raw data size (TB) and file count per subject.
225
+ --add
226
+ Add a subject instead of listing, with ``--dob``, ``--sex``, ``--strain``
227
+ and ``-u`` (the lab member). At a terminal, without ``--strain`` or with
228
+ one that is not on record, the strain is picked from a numbered list of
229
+ the Strain table. A missing option prints which are missing, with the
230
+ strains and users on record.
223
231
  '''
224
232
  parser = argparse.ArgumentParser(
225
233
  description='List sessions and datatypes',
@@ -246,7 +254,15 @@ Administration commands (don't run on experimental computers):
246
254
  parser.add_argument('--include-size', action = 'store_true',
247
255
  default = False,
248
256
  help= 'Display the size of the raw data for each subject.')
249
-
257
+ parser.add_argument('--add', default = None, type = str, metavar = 'SUBJECT',
258
+ help = 'Add a subject; needs --dob, --sex and -u, and --strain or a pick from the list.')
259
+ parser.add_argument('--dob', default = None, type = str,
260
+ help = 'Date of birth of the subject to add (YYYY-MM-DD).')
261
+ parser.add_argument('--sex', default = None, type = str,
262
+ help = 'Sex of the subject to add (M, F or U).')
263
+ parser.add_argument('--strain', default = None, type = str,
264
+ help = 'Strain of the subject to add, from the Strain table; left out, pick it from a list.')
265
+
250
266
  args = parser.parse_args(sys.argv[2:])
251
267
  from .utils import load_project_schema
252
268
  from datajoint.errors import AccessError
@@ -256,6 +272,9 @@ Administration commands (don't run on experimental computers):
256
272
  txt = f'No permission to user {prefs["database"]["database.user"]} on schema {args.project}.'
257
273
  print(f'{tcolor["r"](txt)}')
258
274
  sys.exit(-1)
275
+ if not args.add is None:
276
+ self._add_subject(schema, args)
277
+ return
259
278
  query = schema.Subject()
260
279
  if not args.user in [None,'none']:
261
280
  query = query & f'user_name = "{args.user}"'
@@ -296,6 +315,74 @@ Administration commands (don't run on experimental computers):
296
315
  print(tt,flush = True)
297
316
  print('')
298
317
 
318
+ def _add_subject(self, schema, args):
319
+ '''Insert the subject of ``labdata subjects --add`` after checking each field.'''
320
+ def stop(txt):
321
+ print(tcolor['r'](txt))
322
+ sys.exit(1)
323
+ name = args.add
324
+ if len(schema.Subject() & dict(subject_name = name)):
325
+ stop(f'Subject {name} is already in the database.')
326
+ # at a terminal the strain is chosen from a list; elsewhere --strain is required
327
+ interactive = can_prompt()
328
+ options = dict(dob = args.dob, sex = args.sex, u = args.user)
329
+ if not interactive:
330
+ options['strain'] = args.strain
331
+ missing = [('-' if k == 'u' else '--') + k for k, v in options.items() if v is None]
332
+ strains = list(schema.Strain().fetch('strain_name'))
333
+ users = schema.LabMember().fetch('user_name')
334
+ if len(missing):
335
+ stop(f'Adding {name} needs {", ".join(missing)}.\n'
336
+ f'Strains: {", ".join(strains)}\nUsers: {", ".join(users)}')
337
+ try:
338
+ dob = datetime.strptime(args.dob, '%Y-%m-%d').date()
339
+ except ValueError:
340
+ stop(f'Date of birth {args.dob} is expected as YYYY-MM-DD.')
341
+ sex = args.sex.upper()
342
+ if not sex in ['M', 'F', 'U']:
343
+ stop(f'Sex {args.sex} is expected as M, F or U.')
344
+ if not args.user in users:
345
+ stop(f'User {args.user} is not in the LabMember table. Users: {", ".join(users)}')
346
+ strain = args.strain
347
+ if not strain in strains:
348
+ if not interactive:
349
+ stop(f'Strain {strain} is not in the Strain table. Strains: {", ".join(strains)}')
350
+ if not strain is None:
351
+ print(f'Strain {strain} is not in the Strain table.')
352
+ strain = self._choose_strain(schema)
353
+ if strain is None:
354
+ stop(f'Canceled adding subject {name}.')
355
+ subject = dict(subject_name = name, subject_dob = dob, subject_sex = sex,
356
+ strain_name = strain, user_name = args.user)
357
+ try:
358
+ schema.Subject().insert1(subject)
359
+ except Exception as err:
360
+ stop(f'Could not add subject {name}: {err}')
361
+ print(f"{tcolor['g']('Added')} subject {name}\t{sex}\t{dob}\t{strain}\t{args.user}")
362
+
363
+ def _choose_strain(self, schema):
364
+ '''Return the strain picked at the prompt from a numbered list of the Strain table, or None.
365
+
366
+ Each strain shows the number of subjects of the project that have it.
367
+ '''
368
+ counts = schema.Strain.aggr(schema.Subject, n = 'count(subject_name)', keep_all_rows = True)
369
+ strains, n = counts.fetch('strain_name', 'n', order_by = 'strain_name')
370
+ print('Strains:')
371
+ for i, (strain, count) in enumerate(zip(strains, n)):
372
+ print(f'\t{tcolor["c"](str(i+1) + ".")} {strain} ({count} subjects)')
373
+ while True:
374
+ try:
375
+ answer = input(f'Strain number, 1-{len(strains)} (enter to cancel): ').strip()
376
+ except EOFError:
377
+ return None
378
+ if answer == '':
379
+ return None
380
+ if answer.isdigit() and 1 <= int(answer) <= len(strains):
381
+ return strains[int(answer) - 1]
382
+ if answer in strains:
383
+ return answer
384
+ print(f'{answer} is not on the list; type a number from 1 to {len(strains)}.')
385
+
299
386
  def sessions(self):
300
387
  '''List sessions and datasets for one or more subjects.
301
388
 
@@ -444,6 +531,9 @@ Administration commands (don't run on experimental computers):
444
531
  database for the requested upload rule. **This does not upload to the
445
532
  cloud** — that happens when ``labdata upload`` is later run on the server.
446
533
 
534
+ When Qt cannot be imported, or on Linux without a display, the files are
535
+ copied from the terminal with ``labdata.copy.copy_to_upload_server``.
536
+
447
537
  Usage
448
538
  -----
449
539
  ::
@@ -509,13 +599,26 @@ Administration commands (don't run on experimental computers):
509
599
  w = LABDATA_PUT()
510
600
  sys.exit(app.exec_())
511
601
  else:
512
- from .widgets import QApplication, ServerCopyWidget,QMessageBox
513
- app = QApplication(sys.argv)
514
602
  filepaths = args.filepaths
515
603
  if len(filepaths) == 1:
516
604
  if Path(filepaths[0]).is_dir(): # then select all files inside the folder
517
605
  filepaths = list(Path(filepaths[0]).rglob('**/*'))
518
606
  filepaths = list(filter(lambda f: f.is_file(),filepaths))
607
+ # without Qt or a display, copy from the terminal with copy_to_upload_server
608
+ reason = None
609
+ if sys.platform.startswith('linux') and not (os.environ.get('DISPLAY') or
610
+ os.environ.get('WAYLAND_DISPLAY')):
611
+ reason = 'There is no display for the copy window'
612
+ else:
613
+ try:
614
+ from .widgets import QApplication, ServerCopyWidget,QMessageBox
615
+ except ImportError as err:
616
+ reason = f'Qt is not available ({err})'
617
+ if not reason is None:
618
+ print(f'{reason}; copying from the terminal.', flush = True)
619
+ self._put_from_terminal(filepaths, args)
620
+ return
621
+ app = QApplication(sys.argv)
519
622
  try:
520
623
  w = ServerCopyWidget(src_filepaths = filepaths,
521
624
  upload_rule = args.rule,
@@ -531,6 +634,44 @@ Administration commands (don't run on experimental computers):
531
634
  raise(OSError(f"{tcolor['r']('Upload failed')} {err}."))
532
635
  app.exit()
533
636
  #sys.exit(app.exec_())
637
+
638
+ def _put_from_terminal(self, filepaths, args):
639
+ '''Copy files to the upload server with ``copy_to_upload_server``, without the Qt window.
640
+
641
+ ``labdata put`` uses this when Qt cannot be imported or there is no display.
642
+ '''
643
+ from .copy import copy_to_upload_server
644
+ filepaths = [Path(f).resolve() for f in filepaths]
645
+ if not len(filepaths):
646
+ print(tcolor['r']('No files to copy.'))
647
+ return
648
+ missing = [str(f) for f in filepaths if not f.is_file()]
649
+ if len(missing):
650
+ raise OSError(f"{tcolor['r']('Upload failed')}: these are not files {missing}.")
651
+ names = dict(subject_name = args.subject,
652
+ session_name = args.session,
653
+ dataset_name = args.datatype,
654
+ dataset_type = args.datatype_name)
655
+ names = {k: v[0] for k, v in names.items() if not v is None}
656
+ rule = args.rule[0] if not args.rule is None else None
657
+ nbytes = sum(f.stat().st_size for f in filepaths)
658
+ txt = f"{len(filepaths)} files ({nbytes/1e9:.2f} GB) to {prefs.get('upload_path')}"
659
+ if args.ask:
660
+ if input(f'Copy {txt}? [y/N] ').strip().lower() not in ('y', 'yes'):
661
+ print(tcolor['r']('Canceled the copy to the upload server.'))
662
+ return
663
+ print(f'Copying {txt}.', flush = True)
664
+ try:
665
+ res = copy_to_upload_server(filepaths,
666
+ job_rule = rule,
667
+ overwrite = args.overwrite,
668
+ project = args.project,
669
+ **names)
670
+ except Exception as err:
671
+ raise OSError(f"{tcolor['r']('Upload failed')} {err}.") from err
672
+ print(f"{tcolor['g']('Copied')} {len(res)} files: upload job {res[0]['job_id']} "
673
+ f"waits for labdata upload on {prefs.get('upload_host')}.", flush = True)
674
+
534
675
  def clean(self):
535
676
  '''Delete local copies of files that are confirmed uploaded to S3.
536
677
 
@@ -2305,26 +2446,112 @@ the preference file, or set them with LABDATA_DATABASE_PROJECT for the project.
2305
2446
  print(f"Build the image with: docker build -t {args.tag or '<tag>'} {args.docker}")
2306
2447
 
2307
2448
  def run_container(self):
2449
+ '''Open a shell or Jupyter Lab in a container, here or on a node of a remote cluster.
2450
+
2451
+ Usage
2452
+ -----
2453
+ ::
2454
+
2455
+ labdata run-container <container>
2456
+ labdata run-container <container> --jupyter
2457
+ labdata run-container <container> -t <remote> [--partition gpu] [--walltime 2:00:00]
2458
+ labdata run-container <container> -t <remote> --jupyter
2459
+ labdata run-container -t <remote>
2460
+ labdata run-container -t <remote> --attach <slurm job id>
2461
+
2462
+ With ``-t`` naming a remote of the preference file, the command submits
2463
+ a SLURM job there, waits until it starts and opens a shell in the
2464
+ container on its node, or with ``--jupyter`` starts Jupyter Lab there
2465
+ and tunnels it to this computer. The session has the environment of a
2466
+ labdata job on that remote (project, credentials, ``pre_cmds``). Ending
2467
+ the session ends the job; a lost connection leaves it running, to
2468
+ reconnect with ``--attach``. Without a container name on a remote the
2469
+ shell opens on the node itself.
2470
+
2471
+ Options
2472
+ -------
2473
+ container
2474
+ Container name: the .sif file in the containers folder, without extension.
2475
+ -t, --target
2476
+ ``local`` (default), or a remote of the preference file.
2477
+ --jupyter
2478
+ Start Jupyter Lab instead of a shell.
2479
+ --writable, --cuda
2480
+ Local only: open the container writable, give it the GPUs. On a
2481
+ remote the container gets the GPUs when the node has them.
2482
+ --partition, --ncpus, --gpus, --memory, --walltime, --exclusive, --no-exclusive
2483
+ Resources of the job on a remote. The defaults come from the
2484
+ ``interactive`` entry of the remote's ``analysis_options``; without
2485
+ it the job takes a whole node for 4 hours.
2486
+ --attach
2487
+ SLURM job id of a running job to open the session in.
2488
+ --keep
2489
+ Leave the job running when the session ends.
2490
+ -p, --project
2491
+ Project of the session on a remote.
2308
2492
  '''
2309
- Runs a container
2310
- '''
2493
+ remotes = list((prefs['compute'].get('remotes') or {}).keys())
2311
2494
  parser = argparse.ArgumentParser(
2312
- description = 'Run a container',
2313
- usage = '''labdata run-container <container> ''')
2495
+ description = 'Open a shell or Jupyter Lab in a container, here or on a node of a remote',
2496
+ usage = '''labdata run-container <container> [-t <remote>] [--jupyter]''')
2314
2497
  parser.add_argument('container_name',
2315
2498
  action = 'store',
2316
- type = str,
2317
- help = 'Container name (the .sif file in the local containers folder, without extension).')
2499
+ type = str, nargs = '?', default = None,
2500
+ help = 'Container name (the .sif file in the containers folder, without extension).')
2318
2501
  parser.add_argument('-t','--target',action = 'store', default = 'local', type = str,
2319
- help = 'Where to run it: local or slurm.')
2502
+ help = f'Where to run it: local, or a remote [{tcolor["y"](", ".join(remotes))}].')
2320
2503
  parser.add_argument('--jupyter', action = 'store_true', default = False,
2321
2504
  help = 'Start Jupyter Lab in the container instead of a shell.')
2322
2505
  parser.add_argument('--writable', action = 'store_true', default = False,
2323
- help = 'Open the container writable (for sandbox containers).')
2506
+ help = 'Open the container writable (for sandbox containers, local only).')
2324
2507
  parser.add_argument('--cuda',action = 'store_true', default = False,
2325
- help = 'Give the container access to the GPUs.')
2508
+ help = 'Give the container access to the GPUs (local only; on a remote when the node has them).')
2509
+ parser.add_argument('--partition', action = 'store', default = None, type = str,
2510
+ help = 'SLURM partition of the job on a remote.')
2511
+ parser.add_argument('--ncpus', action = 'store', default = None, type = int,
2512
+ help = 'CPUs of the job on a remote.')
2513
+ parser.add_argument('--gpus', action = 'store', default = None, type = str,
2514
+ help = 'GPUs of the job on a remote (SLURM --gpus).')
2515
+ parser.add_argument('--memory', action = 'store', default = None, type = str,
2516
+ help = 'Memory of the job on a remote, e.g. 32G.')
2517
+ parser.add_argument('--walltime', action = 'store', default = None, type = str,
2518
+ help = 'Time limit of the job on a remote (default 4:00:00).')
2519
+ parser.add_argument('--exclusive', action = 'store_const', const = True, dest = 'exclusive', default = None,
2520
+ help = 'Take the whole node (the default on a remote).')
2521
+ parser.add_argument('--no-exclusive', action = 'store_const', const = False, dest = 'exclusive',
2522
+ help = 'Share the node with other jobs.')
2523
+ parser.add_argument('--attach', action = 'store', default = None, type = int,
2524
+ help = 'SLURM job id (not a ComputeTask id) of a running job to open the session in.')
2525
+ parser.add_argument('--keep', action = 'store_true', default = False,
2526
+ help = 'Leave the job running when the session ends.')
2527
+ parser = self._add_default_arguments(parser, level = 0, include_project = True)
2326
2528
 
2327
2529
  args = parser.parse_args(sys.argv[2:])
2530
+ if args.target in remotes:
2531
+ from .compute.schedulers import run_container_remote
2532
+ try:
2533
+ run_container_remote(args.target,
2534
+ container = args.container_name,
2535
+ jupyter = args.jupyter,
2536
+ partition = args.partition,
2537
+ ncpus = args.ncpus,
2538
+ gpus = args.gpus,
2539
+ memory = args.memory,
2540
+ walltime = args.walltime,
2541
+ exclusive = args.exclusive,
2542
+ attach = args.attach,
2543
+ keep = args.keep,
2544
+ project = args.project)
2545
+ except (RuntimeError, ValueError) as err:
2546
+ print(tcolor['r'](str(err)))
2547
+ sys.exit(1)
2548
+ return
2549
+ if not args.target in ['local','slurm']:
2550
+ print(f"{tcolor['r'](args.target)} is not a target: use local or one of the remotes "
2551
+ f"[{', '.join(remotes)}].")
2552
+ sys.exit(1)
2553
+ if args.container_name is None:
2554
+ parser.error('the container name is needed here; it is optional only on a remote.')
2328
2555
  container = args.container_name
2329
2556
  container_file = None
2330
2557
  cmd = None
@@ -2334,11 +2561,10 @@ the preference file, or set them with LABDATA_DATABASE_PROJECT for the project.
2334
2561
  container_file = (container_store/container).with_suffix('.sif')
2335
2562
  if not container_file.exists():
2336
2563
  print( f"Container {tcolor['r'](container)} not found in {tcolor['r'](container_store)}")
2337
- return
2338
-
2564
+ return
2565
+
2339
2566
  if args.jupyter:
2340
2567
  cmd = 'jupyter lab --ip="*"'
2341
- print(args)
2342
2568
  if not cmd is None:
2343
2569
  from .compute.singularity import run_on_apptainer
2344
2570
  launchcmd = run_on_apptainer(container_file,
@@ -622,9 +622,10 @@ class SpksCompute(BaseCompute):
622
622
  **channel_statistics(segment.astype(np.float32)*gains)))
623
623
  del dat
624
624
  self.set_job_status(job_log = f'Completed {base_key}')
625
- from labdata.schema import UnitMetrics
626
- # limit number of jobs because of memory constraints
627
- self.schema.UnitMetrics.populate(base_key, display_progress = True, processes = int(max(1,np.ceil(n_jobs/2))))
625
+ # every dataset of the sorting; limit number of jobs because of memory constraints
626
+ self.schema.UnitMetrics.populate(base_keys, display_progress = True, processes = int(max(1,np.ceil(n_jobs/2))))
627
+ # label and count the units under every computed criteria of the project
628
+ self.schema.UnitCount.populate(base_keys, display_progress = True)
628
629
 
629
630
  def extract_waveforms(self, udict, nchannels, results_folder, n_pre_samples, n_jobs, offsets, gains):
630
631
  '''Collect the waveforms of the units of one dataset from the filtered binary.