lbm_suite2p_python 2.0.2__tar.gz → 2.0.4__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 (23) hide show
  1. {lbm_suite2p_python-2.0.2/lbm_suite2p_python.egg-info → lbm_suite2p_python-2.0.4}/PKG-INFO +2 -2
  2. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python/postprocessing.py +42 -0
  3. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python/run_lsp.py +150 -81
  4. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4/lbm_suite2p_python.egg-info}/PKG-INFO +2 -2
  5. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python.egg-info/requires.txt +1 -1
  6. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/pyproject.toml +2 -2
  7. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/LICENSE.md +0 -0
  8. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/MANIFEST.in +0 -0
  9. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/README.md +0 -0
  10. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python/__init__.py +0 -0
  11. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python/__main__.py +0 -0
  12. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python/_benchmarking.py +0 -0
  13. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python/default_ops.py +0 -0
  14. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python/merging.py +0 -0
  15. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python/utils.py +0 -0
  16. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python/volume.py +0 -0
  17. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python/zplane.py +0 -0
  18. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python.egg-info/SOURCES.txt +0 -0
  19. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python.egg-info/dependency_links.txt +0 -0
  20. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python.egg-info/entry_points.txt +0 -0
  21. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/lbm_suite2p_python.egg-info/top_level.txt +0 -0
  22. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/setup.cfg +0 -0
  23. {lbm_suite2p_python-2.0.2 → lbm_suite2p_python-2.0.4}/tests/test_run_volume.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lbm_suite2p_python
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: Light Beads Microscopy Pipeline using Suite2p
5
5
  License-Expression: BSD-3-Clause
6
6
  Project-URL: homepage, https://github.com/MillerBrainObservatory/LBM-Suite2p-Python
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
11
11
  Requires-Python: <3.12.10,>=3.12.7
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE.md
14
- Requires-Dist: mbo_utilities>=2.0.2
14
+ Requires-Dist: mbo_utilities>=2.0.4
15
15
  Provides-Extra: cpsam
16
16
  Requires-Dist: cellpose==4.0.6; extra == "cpsam"
17
17
  Requires-Dist: pytorch; extra == "cpsam"
@@ -13,6 +13,7 @@ def _normalize_iscell(iscell):
13
13
  iscell = iscell[:, 0]
14
14
  return iscell.astype(bool)
15
15
 
16
+
16
17
  def filter_by_diameter(iscell, stat, ops, min_mult=0.3, max_mult=3.0):
17
18
  """
18
19
  Set iscell=False for ROIs whose radius is out of range relative to ops['diameter'].
@@ -97,6 +98,7 @@ def mode_robust(x):
97
98
  j = i
98
99
  return mode_robust(x[j:j+N+1])
99
100
 
101
+
100
102
  def compute_event_exceptionality(traces, N=5, robust_std=False):
101
103
  """
102
104
  traces: ndarray (n_cells x T)
@@ -430,3 +432,43 @@ def load_ops(ops_input: str | Path | list[str | Path]) -> dict:
430
432
  return {}
431
433
 
432
434
 
435
+ def load_traces(ops):
436
+ """
437
+ Load fluorescence traces and related data from an ops file directory and return valid cells.
438
+
439
+ This function loads the raw fluorescence traces, neuropil traces, and spike data from the directory
440
+ specified in the ops dictionary. It also loads the 'iscell' file and returns only the traces corresponding
441
+ to valid cells (i.e. where iscell is True).
442
+
443
+ Parameters
444
+ ----------
445
+ ops : dict
446
+ Dictionary containing at least the key 'save_path', which specifies the directory where the following
447
+ files are stored: 'F.npy', 'Fneu.npy', 'spks.npy', and 'iscell.npy'.
448
+
449
+ Returns
450
+ -------
451
+ F_valid : ndarray
452
+ Array of fluorescence traces for valid cells (n_valid x n_timepoints).
453
+ Fneu_valid : ndarray
454
+ Array of neuropil fluorescence traces for valid cells (n_valid x n_timepoints).
455
+ spks_valid : ndarray
456
+ Array of spike data for valid cells (n_valid x n_timepoints).
457
+
458
+ Notes
459
+ -----
460
+ The 'iscell.npy' file is expected to be an array where the first column (iscell[:, 0]) contains
461
+ boolean values indicating valid cells.
462
+ """
463
+ save_path = Path(ops['save_path'])
464
+ F = np.load(save_path.joinpath('F.npy'))
465
+ Fneu = np.load(save_path.joinpath('Fneu.npy'))
466
+ spks = np.load(save_path.joinpath('spks.npy'))
467
+ iscell = np.load(save_path.joinpath('iscell.npy'), allow_pickle=True)[:, 0].astype(bool)
468
+
469
+ F_valid = F[iscell]
470
+ Fneu_valid = Fneu[iscell]
471
+ spks_valid = spks[iscell]
472
+
473
+ return F_valid, Fneu_valid, spks_valid
474
+
@@ -18,10 +18,7 @@ from lbm_suite2p_python.postprocessing import (
18
18
  )
19
19
  from mbo_utilities.log import get as get_logger
20
20
 
21
- from lbm_suite2p_python.zplane import (
22
- save_pc_panels_and_metrics,
23
- plot_zplane_figures
24
- )
21
+ from lbm_suite2p_python.zplane import save_pc_panels_and_metrics, plot_zplane_figures
25
22
 
26
23
  logger = get_logger("run_lsp")
27
24
 
@@ -31,7 +28,9 @@ from lbm_suite2p_python.volume import (
31
28
  plot_volume_neuron_counts,
32
29
  get_volume_stats,
33
30
  )
34
- from mbo_utilities.file_io import get_plane_from_filename, get_files # derive_tag_from_filename, PIPELINE_TAGS
31
+ from mbo_utilities.file_io import (
32
+ get_plane_from_filename,
33
+ ) # derive_tag_from_filename, PIPELINE_TAGS
35
34
 
36
35
  PIPELINE_TAGS = ("plane", "roi", "z", "plane_", "roi_", "z_")
37
36
 
@@ -160,6 +159,7 @@ def run_volume(
160
159
  - Optional rastermap clustering results
161
160
  """
162
161
  from mbo_utilities.file_io import get_files, get_plane_from_filename
162
+
163
163
  start = time.time()
164
164
  if save_path is None:
165
165
  save_path = Path(input_files[0]).parent
@@ -204,6 +204,7 @@ def run_volume(
204
204
  if "roi" in Path(input_files[0]).stem.lower():
205
205
  print("Detected mROI data, merging ROIs for each z-plane...")
206
206
  from .merging import merge_mrois
207
+
207
208
  merged_savepath = save_path.joinpath("merged_mrois")
208
209
  merge_mrois(save_path, merged_savepath)
209
210
  save_path = merged_savepath
@@ -271,90 +272,144 @@ def run_volume(
271
272
 
272
273
  def _should_write_bin(ops_path: Path, force: bool = False) -> bool:
273
274
  """
274
- Decide whether a data_raw.bin should be re-written.
275
-
276
- Conditions that trigger re-write:
277
- - force=True
278
- - bin file missing
279
- - ops.npy missing
280
- - mismatch between ops metadata (Ly, Lx, nframes) and bin file size
281
- - bin file cannot be read or has wrong shape
275
+ Return True if data_raw.bin should be re-written.
282
276
  """
283
277
  if force:
284
278
  return True
279
+
285
280
  ops_path = Path(ops_path)
286
281
  if not ops_path.is_file():
287
282
  return True
288
283
 
289
- bin_path = ops_path.parent / "data.bin"
290
- tiff_path = ops_path.parent / "reg_tif"
284
+ raw_path = ops_path.parent / "data_raw.bin"
285
+ chan2_path = ops_path.parent / "data_chan2.bin"
291
286
 
292
- if not bin_path.is_file() and not tiff_path.is_dir():
287
+ # no raw data at all
288
+ if not raw_path.is_file() and not chan2_path.is_file():
293
289
  return True
294
290
 
295
291
  try:
296
292
  ops = np.load(ops_path, allow_pickle=True).item()
297
- Ly, Lx = ops.get("Ly"), ops.get("Lx")
298
- nframes = ops.get("nframes", ops.get("n_frames"))
299
293
 
300
- if None in (Ly, Lx, nframes):
301
- return True
294
+ for bin_path in (raw_path, chan2_path):
295
+ if not bin_path.is_file():
296
+ continue
302
297
 
303
- expected_size = nframes * Ly * Lx * np.dtype(np.int16).itemsize
304
- actual_size = bin_path.stat().st_size
298
+ if "chan2" in bin_path.name:
299
+ nframes = ops.get("nframes_chan2")
300
+ else:
301
+ nframes = (
302
+ ops.get("nframes_chan1")
303
+ or ops.get("nframes")
304
+ or ops.get("num_frames")
305
+ )
305
306
 
306
- if actual_size != expected_size:
307
- return True
307
+ Ly, Lx = ops.get("Ly"), ops.get("Lx")
308
+ if None in (Ly, Lx, nframes):
309
+ return True
308
310
 
309
- # Try opening first few frames to verify integrity
310
- arr = np.memmap(bin_path, dtype=np.int16, mode="r", shape=(nframes, Ly, Lx))
311
- _ = arr[0].sum() # touch data
312
- del arr
311
+ expected_size = nframes * Ly * Lx * np.dtype(np.int16).itemsize
312
+ actual_size = bin_path.stat().st_size
313
+ if actual_size != expected_size:
314
+ return True
315
+
316
+ # lightweight validation read
317
+ arr = np.memmap(bin_path, dtype=np.int16, mode="r", shape=(nframes, Ly, Lx))
318
+ _ = arr[0, 0, 0]
319
+ del arr
313
320
 
314
321
  return False # all checks passed
322
+
315
323
  except Exception as e:
316
- print(f"Bin validation failed: {e}")
324
+ print(f"Bin validation failed for {ops_path.parent}: {e}")
317
325
  return True
318
326
 
319
327
 
328
+ def _should_register(ops_path: str | Path) -> bool:
329
+ """
330
+ Determine whether Suite2p registration still needs to be performed.
331
+
332
+ Registration is considered complete if any of the following hold:
333
+ - A reference image (refImg) exists and is a valid ndarray
334
+ - meanImg exists (Suite2p always produces it post-registration)
335
+ - Valid registration offsets (xoff/yoff) are present
336
+
337
+ Returns True if registration *should* be run, False otherwise.
338
+ """
339
+ ops = load_ops(ops_path)
340
+
341
+ has_ref = isinstance(ops.get("refImg"), np.ndarray)
342
+ has_mean = isinstance(ops.get("meanImg"), np.ndarray)
343
+ has_offsets = ("xoff" in ops and np.any(np.isfinite(ops["xoff"]))) or (
344
+ "yoff" in ops and np.any(np.isfinite(ops["yoff"]))
345
+ )
346
+ has_metrics = any(k in ops for k in ("regDX", "regPC", "regPC1", "regDX1"))
347
+
348
+ # registration done if any of these are true
349
+ registration_done = has_ref or has_mean or has_offsets or has_metrics
350
+ return not registration_done
351
+
352
+
320
353
  def run_plane_bin(ops) -> bool:
321
- from suite2p.io.binary import BinaryFile
354
+ from mbo_utilities._binary import BinaryFile
322
355
  from suite2p.run_s2p import pipeline
356
+ from contextlib import nullcontext
357
+
323
358
  ops = load_ops(ops)
324
- if "nframes" in ops and "n_frames" not in ops:
325
- ops["n_frames"] = ops["nframes"]
326
- if "n_frames" not in ops:
327
- raise KeyError("run_plane_bin: missing frame count (nframes or n_frames)")
328
- n_frames = ops["n_frames"]
329
359
  Ly, Lx = ops["Ly"], ops["Lx"]
330
360
 
331
- # make sure diam is not nan
332
- if ops["diameter"] is not None and np.isnan(ops["diameter"]):
333
- ops["diameter"] = 8
334
- if ops["diameter"] is None or ops["diameter"] == 0 and ops["anatomical_only"] > 0:
335
- ops["diameter"] = 8
336
- print("Warning: diameter was not set, defaulting to 8."
337
- "Cellpose-SAM currently does not estimate diameter.")
338
- with (
361
+ # input functional channel (unregistered)
362
+ raw_file = ops.get("raw_file")
363
+ nframes_chan1 = (
364
+ ops.get("nframes_chan1") or ops.get("nframes") or ops.get("n_frames")
365
+ )
366
+ if raw_file is None or nframes_chan1 is None:
367
+ raise KeyError("Missing raw_file or nframes_chan1")
368
+
369
+ # optional structural channel
370
+ chan2_file = ops.get("chan2_file", "")
371
+ nframes_chan2 = ops.get("nframes_chan2", 0)
372
+
373
+ ops_parent = Path(ops.get("ops_path")).parent
374
+ ops["save_path"] = ops_parent
339
375
 
376
+ align_structural = ops.get("align_by_chan", 1) == 2
377
+ ops["align_structural"] = align_structural
378
+
379
+ reg_file = ops_parent / "data.bin"
380
+ ops["reg_file"] = str(reg_file)
381
+
382
+ # sanity fix for diameter
383
+ if "diameter" in ops:
384
+ if ops["diameter"] is not None and np.isnan(ops["diameter"]):
385
+ ops["diameter"] = 8
386
+ if (ops["diameter"] is None or ops["diameter"] == 0) and ops.get(
387
+ "anatomical_only", 0
388
+ ) > 0:
389
+ ops["diameter"] = 8
390
+ print("Warning: diameter was not set, defaulting to 8.")
391
+
392
+ with (
340
393
  BinaryFile(
341
- Ly=Ly, Lx=Lx, filename=ops["reg_file"], n_frames=n_frames
394
+ Ly=Ly, Lx=Lx, filename=str(reg_file), n_frames=nframes_chan1
342
395
  ) as f_reg,
396
+ BinaryFile(Ly=Ly, Lx=Lx, filename=raw_file, n_frames=nframes_chan1) as f_raw,
343
397
  (
344
- BinaryFile(
345
- Ly=Ly, Lx=Lx, filename=ops["raw_file"], n_frames=n_frames
346
- )
347
- if "raw_file" in ops and ops["raw_file"] is not None
398
+ BinaryFile(Ly=Ly, Lx=Lx, filename=chan2_file, n_frames=nframes_chan2)
399
+ if align_structural
348
400
  else nullcontext()
349
- ) as f_raw,
401
+ ) as f_reg_chan2,
350
402
  ):
351
403
  ops = pipeline(
352
- f_reg, f_raw, None, None, ops["do_registration"], ops, stat=None
404
+ f_reg=f_reg,
405
+ f_raw=f_raw,
406
+ f_reg_chan2=f_reg_chan2,
407
+ f_raw_chan2=f_reg_chan2 if align_structural else None,
408
+ run_registration=ops.get("do_registration", True),
409
+ ops=ops,
410
+ stat=None,
353
411
  )
354
-
355
412
  np.save(ops["ops_path"], ops)
356
- del f_reg, f_raw, ops
357
-
358
413
  return True
359
414
 
360
415
 
@@ -441,18 +496,18 @@ def run_plane(
441
496
  logger.setLevel(logging.DEBUG)
442
497
  logger.info("Debug mode enabled.")
443
498
 
444
- assert isinstance(input_path, (Path, str)), (
445
- f"input_path should be a pathlib.Path or string, not: {type(input_path)}"
446
- )
499
+ assert isinstance(
500
+ input_path, (Path, str)
501
+ ), f"input_path should be a pathlib.Path or string, not: {type(input_path)}"
447
502
  input_path = Path(input_path)
448
503
  if not input_path.is_file():
449
504
  if input_path.suffix != ".zarr":
450
505
  raise ValueError(f"Input file does not exist: {input_path}")
451
506
  input_parent = input_path.parent
452
507
 
453
- assert isinstance(save_path, (Path, str, type(None))), (
454
- f"save_path should be a pathlib.Path or string, not: {type(save_path)}"
455
- )
508
+ assert isinstance(
509
+ save_path, (Path, str, type(None))
510
+ ), f"save_path should be a pathlib.Path or string, not: {type(save_path)}"
456
511
  if save_path is None:
457
512
  logger.debug(f"save_path is None, using parent of input file: {input_parent}")
458
513
  save_path = input_parent
@@ -469,8 +524,11 @@ def run_plane(
469
524
  ops = {**ops_default, **ops_user, "data_path": str(input_path.resolve())}
470
525
 
471
526
  # suite2p diameter handling
472
- if isinstance(ops["diameter"], list) and len(
473
- ops["diameter"]) > 1 and ops["aspect"] == 1.0:
527
+ if (
528
+ isinstance(ops["diameter"], list)
529
+ and len(ops["diameter"]) > 1
530
+ and ops["aspect"] == 1.0
531
+ ):
474
532
  ops["aspect"] = ops["diameter"][0] / ops["diameter"][1] # noqa
475
533
 
476
534
  file = imread(input_path)
@@ -519,7 +577,8 @@ def run_plane(
519
577
  else:
520
578
  print(
521
579
  f"ops['roidetect'] is True with no stat.npy file present, "
522
- f"proceeding with segmentation/detection for plane {plane}.")
580
+ f"proceeding with segmentation/detection for plane {plane}."
581
+ )
523
582
  needs_detect = True
524
583
  elif (plane_dir / "stat.npy").is_file():
525
584
  # check contents of stat.npy
@@ -532,8 +591,6 @@ def run_plane(
532
591
  needs_detect = True
533
592
 
534
593
  ops_file = plane_dir / "ops.npy"
535
- reg_data_file = plane_dir / "data.bin"
536
- reg_data_file_tiff = plane_dir / "reg_tif"
537
594
 
538
595
  if _should_write_bin(ops_file, force=kwargs.get("force_save", False)):
539
596
  md_combined = {**metadata, **ops}
@@ -549,17 +606,13 @@ def run_plane(
549
606
  else {}
550
607
  )
551
608
 
552
- exists = False
553
- if reg_data_file.exists():
554
- exists = True
555
- if reg_data_file_tiff.exists():
556
- exists = True
557
609
  if force_reg:
558
610
  needs_reg = True
559
611
  else:
560
- # if either reg data file exists, we assume registration is done
561
- needs_reg = not exists
562
-
612
+ if not ops_file.exists():
613
+ needs_reg = True
614
+ else:
615
+ needs_reg = _should_register(ops_file)
563
616
  ops = {
564
617
  **ops_default,
565
618
  **ops_outpath,
@@ -598,11 +651,13 @@ def run_plane(
598
651
  print(f"Skipping {ops_file.name}, processing was not completed.")
599
652
  return ops_file
600
653
 
601
- # cleanup ourselves
602
- if not keep_raw:
603
- (plane_dir / "data_raw.bin").unlink(missing_ok=True)
604
- if not keep_reg:
605
- (plane_dir / "data.bin").unlink(missing_ok=True)
654
+ raw_file = Path(ops.get("raw_file", plane_dir / "data_raw.bin"))
655
+ reg_file = Path(ops.get("reg_file", plane_dir / "data.bin"))
656
+
657
+ if not keep_raw and raw_file.exists():
658
+ raw_file.unlink(missing_ok=True)
659
+ if not keep_reg and reg_file.exists():
660
+ reg_file.unlink(missing_ok=True)
606
661
 
607
662
  save_pc_panels_and_metrics(ops_file, plane_dir / "pc_metrics")
608
663
 
@@ -623,6 +678,8 @@ def run_grid_search(
623
678
  grid_search_dict: dict,
624
679
  input_file: Path | str,
625
680
  save_root: Path | str,
681
+ force_reg: bool,
682
+ force_detect: bool,
626
683
  ):
627
684
  """
628
685
  Run a grid search over all combinations of the input suite2p parameters.
@@ -643,6 +700,12 @@ def run_grid_search(
643
700
  Root directory where each parameter combination's output will be saved.
644
701
  A subdirectory will be created for each run using a short parameter tag.
645
702
 
703
+ force_reg : bool
704
+ Whether to force suite2p registration.
705
+
706
+ force_detect : bool
707
+ Whether to force suite2p detection.
708
+
646
709
  Notes
647
710
  -----
648
711
  - Subfolder names for each parameter are abbreviated to 3-character keys and truncated/rounded values.
@@ -695,16 +758,22 @@ def run_grid_search(
695
758
  for k, v in combo_dict.items()
696
759
  ]
697
760
  tag = "_".join(tag_parts)
761
+ save_path = save_root / tag
762
+ print(f"\nRunning grid search combination: {tag}")
698
763
 
699
- print(f"Running grid search in: {save_root.joinpath(tag)}")
764
+ ops_file = save_path / "ops.npy"
765
+
766
+ # Skip runs that are already registered
767
+ if ops_file.exists() and not force_reg and not _should_register(ops_file):
768
+ print(f"Skipping {tag}: registration already complete.")
769
+ continue
700
770
 
701
- save_path = save_root / tag
702
771
  run_plane(
703
772
  input_path=input_file,
704
773
  save_path=save_path,
705
774
  ops=ops,
706
775
  keep_reg=True,
707
776
  keep_raw=True,
708
- force_reg=True,
709
- force_detect=True,
777
+ force_reg=force_reg,
778
+ force_detect=force_detect,
710
779
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lbm_suite2p_python
3
- Version: 2.0.2
3
+ Version: 2.0.4
4
4
  Summary: Light Beads Microscopy Pipeline using Suite2p
5
5
  License-Expression: BSD-3-Clause
6
6
  Project-URL: homepage, https://github.com/MillerBrainObservatory/LBM-Suite2p-Python
@@ -11,7 +11,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
11
11
  Requires-Python: <3.12.10,>=3.12.7
12
12
  Description-Content-Type: text/markdown
13
13
  License-File: LICENSE.md
14
- Requires-Dist: mbo_utilities>=2.0.2
14
+ Requires-Dist: mbo_utilities>=2.0.4
15
15
  Provides-Extra: cpsam
16
16
  Requires-Dist: cellpose==4.0.6; extra == "cpsam"
17
17
  Requires-Dist: pytorch; extra == "cpsam"
@@ -1,4 +1,4 @@
1
- mbo_utilities>=2.0.2
1
+ mbo_utilities>=2.0.4
2
2
 
3
3
  [cpsam]
4
4
  cellpose==4.0.6
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lbm_suite2p_python"
7
- version = "2.0.2"
7
+ version = "2.0.4"
8
8
  description = "Light Beads Microscopy Pipeline using Suite2p"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -18,7 +18,7 @@ classifiers=[
18
18
  "Programming Language :: Python :: 3 :: Only",
19
19
  ]
20
20
  dependencies = [
21
- "mbo_utilities>=2.0.2",
21
+ "mbo_utilities>=2.0.4",
22
22
  ]
23
23
 
24
24
  [project.optional-dependencies]