dcnum 0.24.0__tar.gz → 0.25.1__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.

Potentially problematic release.


This version of dcnum might be problematic. Click here for more details.

Files changed (125) hide show
  1. {dcnum-0.24.0 → dcnum-0.25.1}/.github/workflows/check.yml +2 -4
  2. {dcnum-0.24.0 → dcnum-0.25.1}/CHANGELOG +9 -0
  3. {dcnum-0.24.0 → dcnum-0.25.1}/PKG-INFO +1 -1
  4. dcnum-0.25.1/benchmark/.gitignore +2 -0
  5. dcnum-0.25.1/benchmark/Readme.md +11 -0
  6. dcnum-0.25.1/benchmark/benchmark.py +54 -0
  7. dcnum-0.25.1/benchmark/bm_write_deque_writer_thread.py +46 -0
  8. dcnum-0.25.1/benchmark/bm_write_queue_collector_thread.py +45 -0
  9. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/_version.py +2 -2
  10. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_background/bg_sparse_median.py +2 -2
  11. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/logic/ctrl.py +7 -2
  12. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/meta/ppid.py +3 -2
  13. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/read/__init__.py +1 -0
  14. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/read/cache.py +4 -3
  15. dcnum-0.25.1/src/dcnum/read/detect_flicker.py +44 -0
  16. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/write/queue_collector_thread.py +7 -14
  17. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/write/writer.py +3 -3
  18. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum.egg-info/PKG-INFO +1 -1
  19. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum.egg-info/SOURCES.txt +7 -0
  20. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_meta_ppid_base.py +8 -0
  21. dcnum-0.25.1/tests/test_read_detect_flicker.py +87 -0
  22. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_read_hdf5.py +13 -0
  23. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_write_queue_collector_thread.py +1 -8
  24. {dcnum-0.24.0 → dcnum-0.25.1}/.github/workflows/deploy_pypi.yml +0 -0
  25. {dcnum-0.24.0 → dcnum-0.25.1}/.gitignore +0 -0
  26. {dcnum-0.24.0 → dcnum-0.25.1}/.readthedocs.yml +0 -0
  27. {dcnum-0.24.0 → dcnum-0.25.1}/LICENSE +0 -0
  28. {dcnum-0.24.0 → dcnum-0.25.1}/README.rst +0 -0
  29. {dcnum-0.24.0 → dcnum-0.25.1}/docs/conf.py +0 -0
  30. {dcnum-0.24.0 → dcnum-0.25.1}/docs/extensions/github_changelog.py +0 -0
  31. {dcnum-0.24.0 → dcnum-0.25.1}/docs/index.rst +0 -0
  32. {dcnum-0.24.0 → dcnum-0.25.1}/docs/requirements.txt +0 -0
  33. {dcnum-0.24.0 → dcnum-0.25.1}/pyproject.toml +0 -0
  34. {dcnum-0.24.0 → dcnum-0.25.1}/setup.cfg +0 -0
  35. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/__init__.py +0 -0
  36. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/__init__.py +0 -0
  37. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/event_extractor_manager_thread.py +0 -0
  38. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_background/__init__.py +0 -0
  39. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_background/base.py +0 -0
  40. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_background/bg_copy.py +0 -0
  41. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_background/bg_roll_median.py +0 -0
  42. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_brightness/__init__.py +0 -0
  43. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_brightness/bright_all.py +0 -0
  44. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_brightness/common.py +0 -0
  45. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_contour/__init__.py +0 -0
  46. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_contour/contour.py +0 -0
  47. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_contour/moments.py +0 -0
  48. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_contour/volume.py +0 -0
  49. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_texture/__init__.py +0 -0
  50. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_texture/common.py +0 -0
  51. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/feat_texture/tex_all.py +0 -0
  52. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/gate.py +0 -0
  53. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/feat/queue_event_extractor.py +0 -0
  54. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/logic/__init__.py +0 -0
  55. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/logic/job.py +0 -0
  56. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/logic/json_encoder.py +0 -0
  57. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/meta/__init__.py +0 -0
  58. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/meta/paths.py +0 -0
  59. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/read/const.py +0 -0
  60. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/read/hdf5_data.py +0 -0
  61. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/read/mapped.py +0 -0
  62. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/__init__.py +0 -0
  63. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segm_thresh.py +0 -0
  64. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segm_torch/__init__.py +0 -0
  65. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segm_torch/segm_torch_base.py +0 -0
  66. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segm_torch/segm_torch_mpo.py +0 -0
  67. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segm_torch/segm_torch_sto.py +0 -0
  68. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segm_torch/torch_model.py +0 -0
  69. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segm_torch/torch_postproc.py +0 -0
  70. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segm_torch/torch_preproc.py +0 -0
  71. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segmenter.py +0 -0
  72. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segmenter_manager_thread.py +0 -0
  73. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segmenter_mpo.py +0 -0
  74. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/segm/segmenter_sto.py +0 -0
  75. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/write/__init__.py +0 -0
  76. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum/write/deque_writer_thread.py +0 -0
  77. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum.egg-info/dependency_links.txt +0 -0
  78. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum.egg-info/requires.txt +0 -0
  79. {dcnum-0.24.0 → dcnum-0.25.1}/src/dcnum.egg-info/top_level.txt +0 -0
  80. {dcnum-0.24.0 → dcnum-0.25.1}/tests/conftest.py +0 -0
  81. {dcnum-0.24.0 → dcnum-0.25.1}/tests/data/fmt-hdf5_cytoshot_extended-moments-features.zip +0 -0
  82. {dcnum-0.24.0 → dcnum-0.25.1}/tests/data/fmt-hdf5_cytoshot_full-features_2023.zip +0 -0
  83. {dcnum-0.24.0 → dcnum-0.25.1}/tests/data/fmt-hdf5_cytoshot_full-features_2024.zip +0 -0
  84. {dcnum-0.24.0 → dcnum-0.25.1}/tests/data/fmt-hdf5_cytoshot_full-features_legacy_allev_2023.zip +0 -0
  85. {dcnum-0.24.0 → dcnum-0.25.1}/tests/data/fmt-hdf5_shapein_empty.zip +0 -0
  86. {dcnum-0.24.0 → dcnum-0.25.1}/tests/data/fmt-hdf5_shapein_raw-with-variable-length-logs.zip +0 -0
  87. {dcnum-0.24.0 → dcnum-0.25.1}/tests/data/segm-torch-model_unet-dcnum-test_g1_910c2.zip +0 -0
  88. {dcnum-0.24.0 → dcnum-0.25.1}/tests/data/segm-torch-test-data_unet-dcnum-test_g1_910c2.zip +0 -0
  89. {dcnum-0.24.0 → dcnum-0.25.1}/tests/helper_methods.py +0 -0
  90. {dcnum-0.24.0 → dcnum-0.25.1}/tests/requirements.txt +0 -0
  91. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_background_base.py +0 -0
  92. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_background_bg_copy.py +0 -0
  93. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_background_bg_roll_median.py +0 -0
  94. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_background_bg_sparsemed.py +0 -0
  95. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_brightness.py +0 -0
  96. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_event_extractor_manager.py +0 -0
  97. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_gate.py +0 -0
  98. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_haralick.py +0 -0
  99. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_moments_based.py +0 -0
  100. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_moments_based_extended.py +0 -0
  101. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_feat_volume.py +0 -0
  102. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_init.py +0 -0
  103. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_logic_job.py +0 -0
  104. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_logic_join.py +0 -0
  105. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_logic_json.py +0 -0
  106. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_logic_pipeline.py +0 -0
  107. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_meta_paths.py +0 -0
  108. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_meta_ppid_bg.py +0 -0
  109. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_meta_ppid_data.py +0 -0
  110. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_meta_ppid_feat.py +0 -0
  111. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_meta_ppid_gate.py +0 -0
  112. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_meta_ppid_segm.py +0 -0
  113. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_read_basin.py +0 -0
  114. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_read_concat_hdf5.py +0 -0
  115. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_read_hdf5_basins.py +0 -0
  116. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_read_hdf5_index_mapping.py +0 -0
  117. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_segm_base.py +0 -0
  118. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_segm_mpo.py +0 -0
  119. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_segm_no_mask_proc.py +0 -0
  120. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_segm_sto.py +0 -0
  121. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_segm_thresh.py +0 -0
  122. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_segm_torch.py +0 -0
  123. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_segm_torch_preproc.py +0 -0
  124. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_write_deque_writer_thread.py +0 -0
  125. {dcnum-0.24.0 → dcnum-0.25.1}/tests/test_write_writer.py +0 -0
@@ -19,7 +19,7 @@ jobs:
19
19
  with:
20
20
  fetch-depth: 0
21
21
  - name: Set up Python ${{ matrix.python-version }}
22
- uses: actions/setup-python@v4
22
+ uses: actions/setup-python@main
23
23
  with:
24
24
  python-version: ${{ matrix.python-version }}
25
25
  - name: Install dependencies
@@ -29,8 +29,6 @@ jobs:
29
29
  python -m pip install coverage flake8 pytest
30
30
  - name: Install dcnum
31
31
  run: |
32
- # mahotas 1.4.15 does not yet support numpy 2.0
33
- pip install "numpy<2"
34
32
  pip install .[torch]
35
33
  - name: List installed packages
36
34
  run: |
@@ -42,4 +40,4 @@ jobs:
42
40
  run: |
43
41
  coverage run --source=dcnum -m pytest tests
44
42
  - name: Upload coverage to Codecov
45
- uses: codecov/codecov-action@v3
43
+ uses: codecov/codecov-action@v4
@@ -1,3 +1,12 @@
1
+ 0.25.1
2
+ - fix: catch ValueError when computing relative path on different anchors
3
+ - ref: replace np.string_ with np.bytes_
4
+ - ref: remove unused `data` argument to QueueCollectorThread
5
+ - ref: minor speed-up for QueueCollectorThread
6
+ 0.25.0
7
+ - feat: identify flickering in raw data via dcnum.read.detect_flickering
8
+ - fix: handle out-of-bounds slice indexing for BaseImageChunkCache
9
+ - fix: np.bool_ and np.floating not recognized in PPID parsing
1
10
  0.24.0
2
11
  - feat: add support for internal basins
3
12
  - feat: "image_bg" as internal basin for "sparsemed" background computer
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcnum
3
- Version: 0.24.0
3
+ Version: 0.25.1
4
4
  Summary: numerics toolbox for imaging deformability cytometry
5
5
  Author: Maximilian Schlögel, Paul Müller, Raghava Alajangi
6
6
  Maintainer-email: Paul Müller <dev@craban.de>
@@ -0,0 +1,2 @@
1
+ bm_*.txt
2
+ *.lprof
@@ -0,0 +1,11 @@
1
+ This directory contains benchmarking scripts used for optimizing dcnum performance.
2
+ To run all benchmarks, execute `python benchmark.py`. You can also specify
3
+ individual benchmarks or a list of benchmarks (path to `bm_*.py` file)
4
+ as arguments to `benchmark.py`.
5
+
6
+ The benchmarks are also ideal use cases for identifying bottlenecks with
7
+ tools such as [line profiler](https://kernprof.readthedocs.io/en/latest/),
8
+ since benchmarks can be designed to run in single threads.
9
+
10
+ pip install line_profiler
11
+ kernprof -lv benchmark.py
@@ -0,0 +1,54 @@
1
+ import importlib
2
+ import os
3
+ import pathlib
4
+ import sys
5
+ import time
6
+ import timeit
7
+
8
+ import numpy as np
9
+
10
+
11
+ here = pathlib.Path(__file__).parent
12
+
13
+
14
+ def print_underline(msg):
15
+ print(msg)
16
+ print("-" * len(msg))
17
+
18
+
19
+ def run_benchmark(bm_path, repeats=5):
20
+ print_underline(f"Running {bm_path}")
21
+ bm_path = pathlib.Path(bm_path).resolve()
22
+ os.chdir(f"{bm_path.parent}")
23
+ bm_mod = importlib.import_module(f"{bm_path.stem}")
24
+
25
+ reps = []
26
+ print("Running...", end="\r")
27
+ for ii in range(repeats):
28
+ t = timeit.timeit(bm_mod.main, setup=bm_mod.setup, number=1)
29
+ reps.append(t)
30
+ print(f"Running {ii + 1}/{repeats}", end="\r")
31
+ print(reps)
32
+ print(f"best={min(reps):.3g}, mean={np.mean(reps):.3g}")
33
+ return reps
34
+
35
+
36
+ if __name__ == "__main__":
37
+ benchmark_paths = []
38
+ for arg in sys.argv[1:]:
39
+ if arg.startswith("bm_"):
40
+ benchmark_paths.append(arg)
41
+
42
+ if not benchmark_paths:
43
+ print("No benchmarking script specified, running all benchmarks.")
44
+ benchmark_paths = here.glob("bm_*.py")
45
+
46
+ results = {}
47
+
48
+ for bmp in sorted(benchmark_paths):
49
+ bmp = pathlib.Path(bmp)
50
+ print("")
51
+ res = run_benchmark(bmp)
52
+ with bmp.with_suffix(".txt").open("a") as fd:
53
+ fd.write(time.strftime(f"%Y-%m-%d_%H.%M.%S {res}\n"))
54
+ print("")
@@ -0,0 +1,46 @@
1
+ import atexit
2
+ from collections import deque
3
+ import pathlib
4
+ import shutil
5
+ import tempfile
6
+
7
+ import multiprocessing as mp
8
+
9
+ import numpy as np
10
+
11
+ from dcnum import write
12
+
13
+
14
+ mp_spawn = mp.get_context('spawn')
15
+
16
+
17
+ def setup():
18
+ global path_out
19
+ global writer_dq
20
+ total_frames = 3000
21
+ batch_size = 500
22
+ num_batches = 6
23
+ assert batch_size * num_batches == total_frames
24
+
25
+ writer_dq = deque()
26
+ # Create 1000 events with at most two repetitions in a frame
27
+ np.random.seed(42)
28
+ rng = np.random.default_rng()
29
+
30
+ # create a sample event
31
+ for ii in range(num_batches):
32
+ writer_dq.append(("mask", rng.random((batch_size, 80, 320)) > .5))
33
+ writer_dq.append(("temp", rng.normal(23, size=batch_size)))
34
+
35
+ temp_dir = tempfile.mkdtemp(prefix=pathlib.Path(__file__).name)
36
+ atexit.register(shutil.rmtree, temp_dir, ignore_errors=True, onerror=None)
37
+ path_out = pathlib.Path(temp_dir) / "out.rtdc"
38
+
39
+
40
+ def main():
41
+ thr_drw = write.DequeWriterThread(
42
+ path_out=path_out,
43
+ dq=writer_dq,
44
+ )
45
+ thr_drw.may_stop_loop = True
46
+ thr_drw.run()
@@ -0,0 +1,45 @@
1
+ from collections import deque
2
+ import multiprocessing as mp
3
+
4
+ import numpy as np
5
+
6
+ from dcnum import write
7
+
8
+
9
+ mp_spawn = mp.get_context('spawn')
10
+
11
+
12
+ def setup():
13
+ global event_queue
14
+ global writer_dq
15
+ global feat_nevents
16
+ batch_size = 1000
17
+ num_batches = 3
18
+ num_events = batch_size * num_batches
19
+ event_queue = mp.Queue()
20
+ writer_dq = deque()
21
+ feat_nevents = mp_spawn.Array("i", num_events)
22
+
23
+ # Create 1000 events with at most two repetitions in a frame
24
+ np.random.seed(42)
25
+ rng = np.random.default_rng()
26
+ number_order = rng.choice(batch_size, size=batch_size, replace=False)
27
+
28
+ # create a sample event
29
+ event = {
30
+ "temp": np.atleast_1d(rng.normal(23)),
31
+ "mask": rng.random((1, 80, 320)) > .5,
32
+ }
33
+ for ii in range(num_batches):
34
+ for idx in number_order:
35
+ event_queue.put((ii*batch_size + idx, event))
36
+
37
+
38
+ def main():
39
+ thr_coll = write.QueueCollectorThread(
40
+ event_queue=event_queue,
41
+ writer_dq=writer_dq,
42
+ feat_nevents=feat_nevents,
43
+ write_threshold=500,
44
+ )
45
+ thr_coll.run()
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.24.0'
16
- __version_tuple__ = version_tuple = (0, 24, 0)
15
+ __version__ = version = '0.25.1'
16
+ __version_tuple__ = version_tuple = (0, 25, 1)
@@ -59,7 +59,7 @@ class BackgroundSparseMed(Background):
59
59
  offset_correction: bool
60
60
  The sparse median background correction produces one median
61
61
  image for multiple input frames (BTW this also leads to very
62
- efficient data storage with HDF5 data compression filters). In
62
+ efficient data storage with internal HDF5 basins). In
63
63
  case the input frames are subject to frame-by-frame brightness
64
64
  variations (e.g. flickering of the illumination source), it
65
65
  is useful to have an offset value per frame that can then be
@@ -226,7 +226,7 @@ class BackgroundSparseMed(Background):
226
226
  offset_correction: bool
227
227
  The sparse median background correction produces one median
228
228
  image for multiple input frames (BTW this also leads to very
229
- efficient data storage with HDF5 data compression filters). In
229
+ efficient data storage with internal HDF5 basins). In
230
230
  case the input frames are subject to frame-by-frame brightness
231
231
  variations (e.g. flickering of the illumination source), it
232
232
  is useful to have an offset value per frame that can then be
@@ -679,8 +679,14 @@ class DCNumJobRunner(threading.Thread):
679
679
  self.logger.debug(f"Creating basin for {feats}")
680
680
  # Relative and absolute paths.
681
681
  pin = pathlib.Path(hin.filename).resolve()
682
+ paths = [pin]
682
683
  pout = pathlib.Path(hout.filename).resolve().parent
683
- paths = [pin, os.path.relpath(pin, pout)]
684
+ try:
685
+ paths.append(os.path.relpath(pin, pout))
686
+ except ValueError:
687
+ # This means it is impossible to compute a relative
688
+ # path (e.g. different drive letter on Windows).
689
+ pass
684
690
  hw.store_basin(name="dcnum basin",
685
691
  features=feats,
686
692
  mapping=basinmap0,
@@ -776,7 +782,6 @@ class DCNumJobRunner(threading.Thread):
776
782
 
777
783
  # Start the data collection thread
778
784
  thr_coll = QueueCollectorThread(
779
- data=self.dtin,
780
785
  event_queue=fe_kwargs["event_queue"],
781
786
  writer_dq=writer_dq,
782
787
  feat_nevents=fe_kwargs["feat_nevents"],
@@ -7,6 +7,7 @@ import pathlib
7
7
  from typing import Dict, List, Protocol
8
8
  import warnings
9
9
 
10
+ import numpy as np
10
11
 
11
12
  #: Increment this string if there are breaking changes that make
12
13
  #: previous pipelines unreproducible.
@@ -140,9 +141,9 @@ def kwargs_to_ppid(cls: ClassWithPPIDCapabilities,
140
141
  path = pathlib.Path(val)
141
142
  if path.exists():
142
143
  val = path.name
143
- if isinstance(val, bool):
144
+ if isinstance(val, (bool, np.bool_)):
144
145
  val = int(val) # do not print e.g. "True"
145
- elif isinstance(val, float):
146
+ elif isinstance(val, (float, np.floating)):
146
147
  if val == int(val):
147
148
  val = int(val) # omit the ".0" at the end
148
149
  concat_strings.append(f"{abr}={val}")
@@ -1,5 +1,6 @@
1
1
  # flake8: noqa: F401
2
2
  from .cache import md5sum
3
3
  from .const import PROTECTED_FEATURES
4
+ from .detect_flicker import detect_flickering
4
5
  from .hdf5_data import HDF5Data, HDF5ImageCache, concatenated_hdf5_data
5
6
  from .mapped import get_mapping_indices, get_mapped_object
@@ -36,9 +36,10 @@ class BaseImageChunkCache(abc.ABC):
36
36
  def __getitem__(self, index):
37
37
  if isinstance(index, (slice, list, np.ndarray)):
38
38
  if isinstance(index, slice):
39
- indices = np.arange(index.start or 0,
40
- index.stop or len(self),
41
- index.step)
39
+ indices = np.arange(
40
+ index.start or 0,
41
+ min(index.stop, len(self)) if index.stop else len(self),
42
+ index.step)
42
43
  else:
43
44
  indices = index
44
45
  array_out = np.empty((len(indices),) + self.image_shape,
@@ -0,0 +1,44 @@
1
+ import numpy as np
2
+
3
+ from .hdf5_data import HDF5Data
4
+
5
+
6
+ def detect_flickering(image_data: np.ndarray | HDF5Data,
7
+ roi_height: int = 10,
8
+ brightness_threshold: float = 2.5,
9
+ count_threshold: int = 5,
10
+ max_frames: int = 1000):
11
+ """Determine whether an image series experiences flickering
12
+
13
+ Flickering is an unwelcome phenomenon due to a faulty data
14
+ acquisition device. For instance, if there is random voltage noise in
15
+ the electronics managing the LED power, then the brightness of the
16
+ LED will vary randomly when the noise signal overlaps with the flash
17
+ triggering signal.
18
+
19
+ If flickering is detected, you should use the "sparsemed" background
20
+ computation with `offset_correction` set to True.
21
+
22
+ Parameters
23
+ ----------
24
+ image_data:
25
+ sliceable object (e.g. numpy array or HDF5Data) containing
26
+ image data.
27
+ roi_height: int
28
+ height of the ROI in pixels for which to search for flickering;
29
+ the entire width of the image is used
30
+ brightness_threshold: float
31
+ brightness difference between individual ROIs median and median
32
+ of all ROI medians leading to a positive flickering event
33
+ count_threshold: int
34
+ minimum number of flickering events that would lead to a positive
35
+ flickering decision
36
+ max_frames: int
37
+ maximum number of frames to include in the flickering analysis
38
+ """
39
+ # slice event axis first in case we have and HDF5Data instance
40
+ roi_data = image_data[:max_frames][:, :roi_height, :]
41
+ roi_median = np.median(roi_data, axis=(1, 2))
42
+ roi_offset = roi_median - np.median(roi_median)
43
+ flickering_events = np.sum(np.abs(roi_offset) >= abs(brightness_threshold))
44
+ return flickering_events >= count_threshold
@@ -8,8 +8,6 @@ from typing import List
8
8
 
9
9
  import numpy as np
10
10
 
11
- from ..read import HDF5Data
12
-
13
11
 
14
12
  class EventStash:
15
13
  def __init__(self,
@@ -61,11 +59,10 @@ class EventStash:
61
59
  Event dictionary
62
60
  """
63
61
  idx_loc = index - self.index_offset
64
- idx_stop = self.nev_idx[idx_loc]
65
- self._tracker[idx_loc] = True
66
62
 
67
63
  if events:
68
64
  slice_loc = None
65
+ idx_stop = self.nev_idx[idx_loc]
69
66
  for feat in events:
70
67
  dev = events[feat]
71
68
  if dev.size:
@@ -76,6 +73,8 @@ class EventStash:
76
73
  if slice_loc:
77
74
  self.indices_for_data[slice_loc] = index
78
75
 
76
+ self._tracker[idx_loc] = True
77
+
79
78
  def require_feature(self, feat, sample_data):
80
79
  """Create a new empty feature array in `self.events` and return it
81
80
 
@@ -87,10 +86,10 @@ class EventStash:
87
86
  Sample data for one event of the feature (used to determine
88
87
  shape and dtype of the feature array)
89
88
  """
90
- sample_data = np.array(sample_data)
91
- event_shape = sample_data.shape
92
- dtype = sample_data.dtype
93
89
  if feat not in self.events:
90
+ sample_data = np.array(sample_data)
91
+ event_shape = sample_data.shape
92
+ dtype = sample_data.dtype
94
93
  darr = np.zeros((self.size,) + tuple(event_shape),
95
94
  dtype=dtype)
96
95
  self.events[feat] = darr
@@ -99,7 +98,6 @@ class EventStash:
99
98
 
100
99
  class QueueCollectorThread(threading.Thread):
101
100
  def __init__(self,
102
- data: HDF5Data,
103
101
  event_queue: mp.Queue,
104
102
  writer_dq: deque,
105
103
  feat_nevents: mp.Array,
@@ -115,9 +113,6 @@ class QueueCollectorThread(threading.Thread):
115
113
 
116
114
  Parameters
117
115
  ----------
118
- data:
119
- Data source object. This is used for appending additional
120
- information
121
116
  event_queue:
122
117
  A queue object to which other processes or threads write
123
118
  events as tuples `(frame_index, events_dict)`.
@@ -146,8 +141,6 @@ class QueueCollectorThread(threading.Thread):
146
141
  super(QueueCollectorThread, self).__init__(
147
142
  name="QueueCollector", *args, **kwargs)
148
143
  self.logger = logging.getLogger("dcnum.write.QueueCollector")
149
- #: HDF5 data instance
150
- self.data = data
151
144
  #: Event queue from which to collect event data
152
145
  self.event_queue = event_queue
153
146
  #: Writer deque to which event arrays are appended
@@ -169,7 +162,7 @@ class QueueCollectorThread(threading.Thread):
169
162
  # We are not writing to `event_queue` so we can safely cancel
170
163
  # our queue thread if we are told to stop.
171
164
  self.event_queue.cancel_join_thread()
172
- # Indexes the current frame in `self.data`.
165
+ # Indexes the current frame in the input HDF5Data instance.
173
166
  last_idx = 0
174
167
  self.logger.debug("Started collector thread")
175
168
  while True:
@@ -140,10 +140,10 @@ class HDF5Writer:
140
140
  feat_dtype=feat_dtype),
141
141
  **ds_kwds)
142
142
  if len(item_shape) == 2:
143
- dset.attrs.create('CLASS', np.string_('IMAGE'))
144
- dset.attrs.create('IMAGE_VERSION', np.string_('1.2'))
143
+ dset.attrs.create('CLASS', np.bytes_('IMAGE'))
144
+ dset.attrs.create('IMAGE_VERSION', np.bytes_('1.2'))
145
145
  dset.attrs.create('IMAGE_SUBCLASS',
146
- np.string_('IMAGE_GRAYSCALE'))
146
+ np.bytes_('IMAGE_GRAYSCALE'))
147
147
  offset = 0
148
148
  else:
149
149
  dset = egroup[feat]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: dcnum
3
- Version: 0.24.0
3
+ Version: 0.25.1
4
4
  Summary: numerics toolbox for imaging deformability cytometry
5
5
  Author: Maximilian Schlögel, Paul Müller, Raghava Alajangi
6
6
  Maintainer-email: Paul Müller <dev@craban.de>
@@ -6,6 +6,11 @@ README.rst
6
6
  pyproject.toml
7
7
  .github/workflows/check.yml
8
8
  .github/workflows/deploy_pypi.yml
9
+ benchmark/.gitignore
10
+ benchmark/Readme.md
11
+ benchmark/benchmark.py
12
+ benchmark/bm_write_deque_writer_thread.py
13
+ benchmark/bm_write_queue_collector_thread.py
9
14
  docs/conf.py
10
15
  docs/index.rst
11
16
  docs/requirements.txt
@@ -46,6 +51,7 @@ src/dcnum/meta/ppid.py
46
51
  src/dcnum/read/__init__.py
47
52
  src/dcnum/read/cache.py
48
53
  src/dcnum/read/const.py
54
+ src/dcnum/read/detect_flicker.py
49
55
  src/dcnum/read/hdf5_data.py
50
56
  src/dcnum/read/mapped.py
51
57
  src/dcnum/segm/__init__.py
@@ -93,6 +99,7 @@ tests/test_meta_ppid_gate.py
93
99
  tests/test_meta_ppid_segm.py
94
100
  tests/test_read_basin.py
95
101
  tests/test_read_concat_hdf5.py
102
+ tests/test_read_detect_flicker.py
96
103
  tests/test_read_hdf5.py
97
104
  tests/test_read_hdf5_basins.py
98
105
  tests/test_read_hdf5_index_mapping.py
@@ -1,5 +1,7 @@
1
1
  import inspect
2
2
 
3
+ import numpy as np
4
+
3
5
  import pytest
4
6
 
5
7
  from dcnum.meta import ppid
@@ -80,8 +82,14 @@ def test_unique_prefix_unordered(in_list, out_list):
80
82
  "tem=90^te=a^o=0^wit=1^a=1000^win=red^tes=1"),
81
83
  ({"temperature": 10.1},
82
84
  "tem=10.1^te=a^o=0^wit=1^a=1000^win=red^tes=1"),
85
+ ({"temperature": 10.0},
86
+ "tem=10^te=a^o=0^wit=1^a=1000^win=red^tes=1"),
87
+ ({"temperature": np.float16(9.0)},
88
+ "tem=9^te=a^o=0^wit=1^a=1000^win=red^tes=1"),
83
89
  ({"with_water": False, "wine_type": "blue"},
84
90
  "tem=90^te=a^o=0^wit=0^a=1000^win=blue^tes=1"),
91
+ ({"with_water": np.bool_(1), "wine_type": "blue"},
92
+ "tem=90^te=a^o=0^wit=1^a=1000^win=blue^tes=1"),
85
93
  ])
86
94
  def test_kwargs_to_ppid(kwargs, pid):
87
95
  ptest = ppid.kwargs_to_ppid(ExampleClass, "cook", kwargs)
@@ -0,0 +1,87 @@
1
+ import numpy as np
2
+
3
+ from dcnum.read import concatenated_hdf5_data, detect_flickering
4
+
5
+ from helper_methods import retrieve_data
6
+
7
+
8
+ def test_detect_flicker_basic():
9
+ image_data = np.full((500, 80, 320), 145)
10
+ flicker_indices = [4, 9, 10, 23, 439]
11
+ for idx in flicker_indices:
12
+ image_data[idx] += 5
13
+ assert detect_flickering(image_data,
14
+ roi_height=10,
15
+ brightness_threshold=5,
16
+ count_threshold=5,
17
+ max_frames=500,
18
+ )
19
+
20
+ assert not detect_flickering(image_data,
21
+ roi_height=10,
22
+ brightness_threshold=5,
23
+ count_threshold=6, # threshold too low
24
+ max_frames=500,
25
+ )
26
+
27
+ assert not detect_flickering(image_data,
28
+ roi_height=10,
29
+ brightness_threshold=6, # threshold too low
30
+ count_threshold=5,
31
+ max_frames=500,
32
+ )
33
+
34
+ assert not detect_flickering(image_data,
35
+ roi_height=10,
36
+ brightness_threshold=5,
37
+ count_threshold=5,
38
+ max_frames=400, # too few frames
39
+ )
40
+
41
+
42
+ def test_detect_flicker_hdf5data_instance():
43
+ path = retrieve_data("fmt-hdf5_cytoshot_full-features_2023.zip")
44
+ path_out = path.with_name("input.rtdc")
45
+ # create simple concatenated dataset, repeating a file
46
+ with concatenated_hdf5_data([path]*25, path_out=path_out) as hd:
47
+ assert len(hd) == 1000
48
+ assert not detect_flickering(hd.image)
49
+ assert detect_flickering(hd.image, brightness_threshold=1)
50
+
51
+
52
+ def test_detect_flicker_none():
53
+ image_data = np.full((500, 80, 320), 145)
54
+ assert not detect_flickering(image_data,
55
+ roi_height=10,
56
+ brightness_threshold=5,
57
+ count_threshold=5,
58
+ max_frames=500,
59
+ )
60
+
61
+
62
+ def test_detect_flicker_not_outside_roi():
63
+ image_data = np.full((500, 80, 320), 145)
64
+ flicker_indices = [4, 9, 10, 23, 439]
65
+ for idx in flicker_indices:
66
+ # only modify data outside the ROI
67
+ image_data[idx, 11:, :] += 5
68
+ assert not detect_flickering(image_data,
69
+ roi_height=10,
70
+ brightness_threshold=5,
71
+ count_threshold=5,
72
+ max_frames=500,
73
+ )
74
+
75
+
76
+ def test_detect_flicker_only_inside_roi():
77
+ image_data = np.full((500, 80, 320), 145)
78
+ flicker_indices = [4, 9, 10, 23, 439]
79
+ for idx in flicker_indices:
80
+ # only modify data inside the ROI
81
+ image_data[idx, :10, :] += 5
82
+ assert detect_flickering(image_data,
83
+ roi_height=10,
84
+ brightness_threshold=5,
85
+ count_threshold=5,
86
+ max_frames=500,
87
+ )
@@ -106,6 +106,19 @@ def test_image_cache(tmp_path):
106
106
  assert 2 in hic.cache
107
107
 
108
108
 
109
+ def test_image_cache_slice_out_of_bounds(tmp_path):
110
+ path = tmp_path / "test.hdf5"
111
+ with h5py.File(path, "w") as hw:
112
+ hw["events/image"] = np.random.rand(210, 80, 180)
113
+
114
+ with h5py.File(path, "r") as h5:
115
+ hic = read.HDF5ImageCache(h5["events/image"],
116
+ chunk_size=100,
117
+ cache_size=2)
118
+ assert len(hic) == 210
119
+ assert len(hic[:300]) == 210
120
+
121
+
109
122
  def test_image_cache_index_out_of_range(tmp_path):
110
123
  path = tmp_path / "test.hdf5"
111
124
  size = 20
@@ -4,9 +4,8 @@ import pathlib
4
4
 
5
5
  import numpy as np
6
6
 
7
- from dcnum import read, write
7
+ from dcnum import write
8
8
 
9
- from helper_methods import retrieve_data
10
9
 
11
10
  data_path = pathlib.Path(__file__).parent / "data"
12
11
 
@@ -44,15 +43,12 @@ def test_event_stash():
44
43
 
45
44
  def test_queue_collector_thread():
46
45
  # keyword arguments
47
- data = read.HDF5Data(
48
- retrieve_data("fmt-hdf5_cytoshot_full-features_2023.zip"))
49
46
  event_queue = mp.Queue()
50
47
  writer_dq = collections.deque()
51
48
  feat_nevents = np.array([1, 3, 1, 5])
52
49
  write_threshold = 2
53
50
  # queue collector thread
54
51
  qct = write.QueueCollectorThread(
55
- data=data,
56
52
  event_queue=event_queue,
57
53
  writer_dq=writer_dq,
58
54
  feat_nevents=feat_nevents,
@@ -99,15 +95,12 @@ def test_queue_collector_thread():
99
95
 
100
96
  def test_queue_collector_thread_with_full_stash():
101
97
  # keyword arguments
102
- data = read.HDF5Data(
103
- retrieve_data("fmt-hdf5_cytoshot_full-features_2023.zip"))
104
98
  event_queue = mp.Queue()
105
99
  writer_dq = collections.deque()
106
100
  feat_nevents = np.array([1, 3, 1, 5])
107
101
  write_threshold = 2
108
102
  # queue collector thread
109
103
  qct = write.QueueCollectorThread(
110
- data=data,
111
104
  event_queue=event_queue,
112
105
  writer_dq=writer_dq,
113
106
  feat_nevents=feat_nevents,
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes