ocdkit 0.0.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.
Files changed (78) hide show
  1. ocdkit-0.0.1/.github/workflows/test_and_deploy.yml +111 -0
  2. ocdkit-0.0.1/.gitignore +89 -0
  3. ocdkit-0.0.1/LICENSE +28 -0
  4. ocdkit-0.0.1/PKG-INFO +66 -0
  5. ocdkit-0.0.1/README.md +39 -0
  6. ocdkit-0.0.1/pyproject.toml +51 -0
  7. ocdkit-0.0.1/scripts/bench_colorize.py +104 -0
  8. ocdkit-0.0.1/scripts/coverage_cross_device.sh +41 -0
  9. ocdkit-0.0.1/setup.cfg +4 -0
  10. ocdkit-0.0.1/src/ocdkit/__init__.py +10 -0
  11. ocdkit-0.0.1/src/ocdkit/array/__init__.py +3 -0
  12. ocdkit-0.0.1/src/ocdkit/array/convert.py +121 -0
  13. ocdkit-0.0.1/src/ocdkit/array/filters.py +56 -0
  14. ocdkit-0.0.1/src/ocdkit/array/imports.py +4 -0
  15. ocdkit-0.0.1/src/ocdkit/array/index.py +242 -0
  16. ocdkit-0.0.1/src/ocdkit/array/morphology.py +194 -0
  17. ocdkit-0.0.1/src/ocdkit/array/normalize.py +425 -0
  18. ocdkit-0.0.1/src/ocdkit/array/ops.py +134 -0
  19. ocdkit-0.0.1/src/ocdkit/array/spatial.py +410 -0
  20. ocdkit-0.0.1/src/ocdkit/array/transform.py +261 -0
  21. ocdkit-0.0.1/src/ocdkit/array/union_find.py +52 -0
  22. ocdkit-0.0.1/src/ocdkit/array/warp.py +28 -0
  23. ocdkit-0.0.1/src/ocdkit/imports.py +8 -0
  24. ocdkit-0.0.1/src/ocdkit/io/__init__.py +3 -0
  25. ocdkit-0.0.1/src/ocdkit/io/files.py +141 -0
  26. ocdkit-0.0.1/src/ocdkit/io/image.py +138 -0
  27. ocdkit-0.0.1/src/ocdkit/io/imports.py +4 -0
  28. ocdkit-0.0.1/src/ocdkit/io/path.py +68 -0
  29. ocdkit-0.0.1/src/ocdkit/io/result.py +34 -0
  30. ocdkit-0.0.1/src/ocdkit/load/__init__.py +5 -0
  31. ocdkit-0.0.1/src/ocdkit/load/module.py +132 -0
  32. ocdkit-0.0.1/src/ocdkit/load/object.py +136 -0
  33. ocdkit-0.0.1/src/ocdkit/logging/__init__.py +3 -0
  34. ocdkit-0.0.1/src/ocdkit/logging/handler.py +206 -0
  35. ocdkit-0.0.1/src/ocdkit/measure/__init__.py +3 -0
  36. ocdkit-0.0.1/src/ocdkit/measure/bbox.py +188 -0
  37. ocdkit-0.0.1/src/ocdkit/measure/diameter.py +185 -0
  38. ocdkit-0.0.1/src/ocdkit/measure/imports.py +4 -0
  39. ocdkit-0.0.1/src/ocdkit/measure/medoid.py +181 -0
  40. ocdkit-0.0.1/src/ocdkit/measure/metrics.py +43 -0
  41. ocdkit-0.0.1/src/ocdkit/plot/__init__.py +5 -0
  42. ocdkit-0.0.1/src/ocdkit/plot/color.py +215 -0
  43. ocdkit-0.0.1/src/ocdkit/plot/contour.py +102 -0
  44. ocdkit-0.0.1/src/ocdkit/plot/defaults.py +147 -0
  45. ocdkit-0.0.1/src/ocdkit/plot/display.py +133 -0
  46. ocdkit-0.0.1/src/ocdkit/plot/export.py +108 -0
  47. ocdkit-0.0.1/src/ocdkit/plot/figure.py +24 -0
  48. ocdkit-0.0.1/src/ocdkit/plot/grid.py +306 -0
  49. ocdkit-0.0.1/src/ocdkit/plot/imports.py +9 -0
  50. ocdkit-0.0.1/src/ocdkit/plot/label.py +733 -0
  51. ocdkit-0.0.1/src/ocdkit/plot/ncolor.py +54 -0
  52. ocdkit-0.0.1/src/ocdkit/utils/__init__.py +3 -0
  53. ocdkit-0.0.1/src/ocdkit/utils/collections.py +97 -0
  54. ocdkit-0.0.1/src/ocdkit/utils/gpu.py +210 -0
  55. ocdkit-0.0.1/src/ocdkit/utils/kwargs.py +136 -0
  56. ocdkit-0.0.1/src/ocdkit.egg-info/PKG-INFO +66 -0
  57. ocdkit-0.0.1/src/ocdkit.egg-info/SOURCES.txt +76 -0
  58. ocdkit-0.0.1/src/ocdkit.egg-info/dependency_links.txt +1 -0
  59. ocdkit-0.0.1/src/ocdkit.egg-info/requires.txt +17 -0
  60. ocdkit-0.0.1/src/ocdkit.egg-info/top_level.txt +1 -0
  61. ocdkit-0.0.1/tests/fixtures/multichan_3c_4x4.czi +0 -0
  62. ocdkit-0.0.1/tests/fixtures/tiny_8x8.czi +0 -0
  63. ocdkit-0.0.1/tests/test_array.py +798 -0
  64. ocdkit-0.0.1/tests/test_gpu.py +243 -0
  65. ocdkit-0.0.1/tests/test_io.py +299 -0
  66. ocdkit-0.0.1/tests/test_measure.py +423 -0
  67. ocdkit-0.0.1/tests/test_morphology.py +184 -0
  68. ocdkit-0.0.1/tests/test_plot_color.py +194 -0
  69. ocdkit-0.0.1/tests/test_plot_contour.py +48 -0
  70. ocdkit-0.0.1/tests/test_plot_display.py +37 -0
  71. ocdkit-0.0.1/tests/test_plot_export.py +38 -0
  72. ocdkit-0.0.1/tests/test_plot_figure.py +55 -0
  73. ocdkit-0.0.1/tests/test_plot_grid.py +67 -0
  74. ocdkit-0.0.1/tests/test_plot_label.py +74 -0
  75. ocdkit-0.0.1/tests/test_plot_notebook.py +115 -0
  76. ocdkit-0.0.1/tests/test_registration.py +154 -0
  77. ocdkit-0.0.1/tests/test_slice.py +31 -0
  78. ocdkit-0.0.1/tests/test_spatial.py +259 -0
@@ -0,0 +1,111 @@
1
+ name: CI/CD
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*" # e.g. v1.0, v2.0
7
+
8
+ jobs:
9
+ test:
10
+ name: Test on ${{ matrix.os }} (Python ${{ matrix.python_version }})
11
+ runs-on: ${{ matrix.os }}
12
+ strategy:
13
+ fail-fast: false
14
+ matrix:
15
+ os: [ubuntu-latest, windows-latest, macos-latest] # macos-latest = Apple Silicon (MPS)
16
+ python_version: ['3.11', '3.12']
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ with:
20
+ fetch-depth: 0 # needed for setuptools_scm
21
+
22
+ - name: Set up Python
23
+ uses: actions/setup-python@v5
24
+ with:
25
+ python-version: ${{ matrix.python_version }}
26
+ cache: pip
27
+
28
+ - name: Install package with dev deps
29
+ shell: bash
30
+ run: |
31
+ python -m pip install --upgrade pip
32
+ pip install --extra-index-url https://download.pytorch.org/whl/cpu \
33
+ -e . pytest pytest-cov
34
+
35
+ - name: Run tests
36
+ shell: bash
37
+ env:
38
+ PYTORCH_ENABLE_MPS_FALLBACK: "1"
39
+ run: |
40
+ python -m pytest tests/ \
41
+ --cov=ocdkit --cov-report=xml --cov-report=term \
42
+ --junitxml=junit.xml \
43
+ -v
44
+
45
+ - name: Upload coverage & junit artifacts
46
+ if: matrix.os == 'macos-latest' && matrix.python_version == '3.11'
47
+ uses: actions/upload-artifact@v4
48
+ with:
49
+ name: test-results
50
+ path: |
51
+ coverage.xml
52
+ junit.xml
53
+
54
+ deploy:
55
+ name: Deploy to PyPI
56
+ runs-on: ubuntu-latest
57
+ needs: test
58
+ if: startsWith(github.ref, 'refs/tags/v')
59
+ steps:
60
+ - uses: actions/checkout@v4
61
+ with:
62
+ fetch-depth: 0
63
+
64
+ - name: Set up Python
65
+ uses: actions/setup-python@v5
66
+ with:
67
+ python-version: '3.11'
68
+
69
+ - name: Install build tools
70
+ run: |
71
+ python -m pip install --upgrade pip
72
+ pip install build twine setuptools setuptools_scm wheel
73
+
74
+ - name: Build distribution
75
+ run: python -m build --sdist --wheel
76
+
77
+ - name: Publish to PyPI
78
+ env:
79
+ TWINE_USERNAME: __token__
80
+ TWINE_PASSWORD: ${{ secrets.TWINE_API_KEY }}
81
+ run: python -m twine upload dist/*
82
+
83
+ badges:
84
+ name: Build and commit badges
85
+ runs-on: ubuntu-latest
86
+ needs: test
87
+ if: startsWith(github.ref, 'refs/tags/v')
88
+ steps:
89
+ - uses: actions/checkout@v4
90
+
91
+ - name: Download test artifacts
92
+ uses: actions/download-artifact@v4
93
+ with:
94
+ name: test-results
95
+
96
+ - name: Install badge dependencies
97
+ run: |
98
+ python -m pip install --upgrade pip
99
+ pip install 'genbadge[coverage,tests]'
100
+
101
+ - name: Generate badges
102
+ run: |
103
+ mkdir -p badges
104
+ genbadge coverage -i coverage.xml -o badges/coverage.svg
105
+ genbadge tests -i junit.xml -o badges/tests.svg
106
+
107
+ - name: Commit badges
108
+ uses: EndBug/add-and-commit@v9
109
+ with:
110
+ add: 'badges/*.svg'
111
+ message: 'CI: update coverage and test badges'
@@ -0,0 +1,89 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ **/__pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # macOS resource fork / metadata junk
10
+ ._*
11
+ .DS_Store
12
+ .AppleDouble
13
+ .AppleDB
14
+
15
+ # Distribution / packaging
16
+ .Python
17
+ build/
18
+ develop-eggs/
19
+ dist/
20
+ downloads/
21
+ eggs/
22
+ .eggs/
23
+ lib/
24
+ lib64/
25
+ parts/
26
+ sdist/
27
+ var/
28
+ wheels/
29
+ pip-wheel-metadata/
30
+ share/python-wheels/
31
+ *.egg-info/
32
+ .installed.cfg
33
+ *.egg
34
+ MANIFEST
35
+ *_version.py
36
+
37
+ # PyInstaller
38
+ *.manifest
39
+ *.spec
40
+
41
+ # Installer logs
42
+ pip-log.txt
43
+ pip-delete-this-directory.txt
44
+
45
+ # Unit test / coverage reports
46
+ htmlcov/
47
+ .tox/
48
+ .nox/
49
+ .coverage
50
+ .coverage.*
51
+ .cache
52
+ nosetests.xml
53
+ coverage.xml
54
+ *.cover
55
+ *.py,cover
56
+ .hypothesis/
57
+ .pytest_cache/
58
+
59
+ # Sphinx documentation
60
+ docs/_build/
61
+
62
+ # Jupyter Notebook
63
+ .ipynb_checkpoints
64
+ */.ipynb_checkpoints/*
65
+
66
+ # IPython
67
+ profile_default/
68
+ ipython_config.py
69
+
70
+ # pyenv
71
+ .python-version
72
+
73
+ # Environments
74
+ .env
75
+ .venv
76
+ env/
77
+ venv/
78
+ ENV/
79
+ env.bak/
80
+ venv.bak/
81
+
82
+ # IDE
83
+ .idea/
84
+ .vscode/
85
+
86
+ # mypy
87
+ .mypy_cache/
88
+ .dmypy.json
89
+ dmypy.json
ocdkit-0.0.1/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, Kevin Cutler
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
ocdkit-0.0.1/PKG-INFO ADDED
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: ocdkit
3
+ Version: 0.0.1
4
+ Summary: Obsessively precise utilities for array manipulation, GPU dispatch, image I/O, morphology, and plotting.
5
+ License: BSD-3-Clause
6
+ Requires-Python: >=3.9
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: numpy
10
+ Requires-Dist: scipy
11
+ Requires-Dist: scikit-image
12
+ Requires-Dist: tifffile
13
+ Requires-Dist: imagecodecs
14
+ Requires-Dist: matplotlib
15
+ Requires-Dist: fastremap
16
+ Requires-Dist: edt
17
+ Requires-Dist: torch>=1.12
18
+ Requires-Dist: dask[array]
19
+ Requires-Dist: natsort
20
+ Requires-Dist: numba
21
+ Requires-Dist: bioio
22
+ Requires-Dist: bioio-czi
23
+ Requires-Dist: ncolor>=1.5.1
24
+ Requires-Dist: cmap
25
+ Requires-Dist: tqdm
26
+ Dynamic: license-file
27
+
28
+ # ocdkit
29
+
30
+ Obsessively precise utilities for scientific Python — array manipulation, GPU dispatch, image I/O, spatial operations, morphology, and plotting.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install ocdkit # core (numpy, scipy, scikit-image, tifffile, matplotlib)
36
+ pip install ocdkit[torch] # + PyTorch GPU support
37
+ pip install ocdkit[plot] # + ncolor, cmap, opt_einsum
38
+ pip install ocdkit[spatial] # + numba, fastremap (contour extraction, skeletonization)
39
+ pip install ocdkit[all] # everything
40
+ ```
41
+
42
+ ## Modules
43
+
44
+ | Module | What's in it |
45
+ |---|---|
46
+ | `ocdkit.array` | `rescale`, `safe_divide`, `is_integer`, `get_module`, `unique_nonzero` |
47
+ | `ocdkit.gpu` | `resolve_device`, `empty_cache`, `torch_GPU`, `torch_CPU` |
48
+ | `ocdkit.io` | `imread`, `imwrite`, `getname`, `check_dir` |
49
+ | `ocdkit.spatial` | `kernel_setup`, `get_neighbors`, `get_neigh_inds`, `masks_to_affinity`, `get_contour`, `boundary_to_masks` |
50
+ | `ocdkit.morphology` | `find_boundaries`, `skeletonize` |
51
+ | `ocdkit.measure` | `crop_bbox`, `bbox_to_slice`, `make_square`, `diameters` |
52
+ | `ocdkit.plot` | `figure`, `image_grid`, `split_list`, `colorize`, `rgb_flow`, `vector_contours`, `apply_ncolor`, `color_swatches`, `recolor_label`, `add_label_background` |
53
+
54
+ ## Quick start
55
+
56
+ ```python
57
+ from ocdkit.array import rescale
58
+ from ocdkit.gpu import resolve_device
59
+ from ocdkit.plot import figure, image_grid
60
+
61
+ device = resolve_device() # auto-detect CUDA / MPS / CPU
62
+ ```
63
+
64
+ ## License
65
+
66
+ BSD-3-Clause
ocdkit-0.0.1/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # ocdkit
2
+
3
+ Obsessively precise utilities for scientific Python — array manipulation, GPU dispatch, image I/O, spatial operations, morphology, and plotting.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install ocdkit # core (numpy, scipy, scikit-image, tifffile, matplotlib)
9
+ pip install ocdkit[torch] # + PyTorch GPU support
10
+ pip install ocdkit[plot] # + ncolor, cmap, opt_einsum
11
+ pip install ocdkit[spatial] # + numba, fastremap (contour extraction, skeletonization)
12
+ pip install ocdkit[all] # everything
13
+ ```
14
+
15
+ ## Modules
16
+
17
+ | Module | What's in it |
18
+ |---|---|
19
+ | `ocdkit.array` | `rescale`, `safe_divide`, `is_integer`, `get_module`, `unique_nonzero` |
20
+ | `ocdkit.gpu` | `resolve_device`, `empty_cache`, `torch_GPU`, `torch_CPU` |
21
+ | `ocdkit.io` | `imread`, `imwrite`, `getname`, `check_dir` |
22
+ | `ocdkit.spatial` | `kernel_setup`, `get_neighbors`, `get_neigh_inds`, `masks_to_affinity`, `get_contour`, `boundary_to_masks` |
23
+ | `ocdkit.morphology` | `find_boundaries`, `skeletonize` |
24
+ | `ocdkit.measure` | `crop_bbox`, `bbox_to_slice`, `make_square`, `diameters` |
25
+ | `ocdkit.plot` | `figure`, `image_grid`, `split_list`, `colorize`, `rgb_flow`, `vector_contours`, `apply_ncolor`, `color_swatches`, `recolor_label`, `add_label_background` |
26
+
27
+ ## Quick start
28
+
29
+ ```python
30
+ from ocdkit.array import rescale
31
+ from ocdkit.gpu import resolve_device
32
+ from ocdkit.plot import figure, image_grid
33
+
34
+ device = resolve_device() # auto-detect CUDA / MPS / CPU
35
+ ```
36
+
37
+ ## License
38
+
39
+ BSD-3-Clause
@@ -0,0 +1,51 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "setuptools_scm>=6.2"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ocdkit"
7
+ dynamic = ["version"]
8
+ description = "Obsessively precise utilities for array manipulation, GPU dispatch, image I/O, morphology, and plotting."
9
+ readme = "README.md"
10
+ license = {text = "BSD-3-Clause"}
11
+ requires-python = ">=3.9"
12
+ dependencies = [
13
+ "numpy",
14
+ "scipy",
15
+ "scikit-image",
16
+ "tifffile",
17
+ "imagecodecs",
18
+ "matplotlib",
19
+ "fastremap",
20
+ "edt",
21
+ "torch>=1.12",
22
+ "dask[array]",
23
+ "natsort",
24
+ "numba",
25
+ "bioio",
26
+ "bioio-czi",
27
+ "ncolor>=1.5.1",
28
+ "cmap",
29
+ "tqdm",
30
+ ]
31
+
32
+ [tool.setuptools.packages.find]
33
+ where = ["src"]
34
+
35
+ [tool.pytest.ini_options]
36
+ testpaths = ["tests"]
37
+
38
+ [tool.coverage.run]
39
+ source = ["ocdkit"]
40
+
41
+ [tool.coverage.paths]
42
+ source = [
43
+ "src/ocdkit",
44
+ "/Volumes/DataDrive/ocdkit/src/ocdkit",
45
+ "/home/kcutler/DataDrive/ocdkit/src/ocdkit",
46
+ ]
47
+
48
+ [tool.coverage.report]
49
+ show_missing = true
50
+
51
+ [tool.setuptools_scm]
@@ -0,0 +1,104 @@
1
+ """Benchmark colorize: matmul vs opt_einsum across backends and shapes."""
2
+
3
+ import time, string
4
+ import numpy as np
5
+ import torch
6
+ from opt_einsum import contract
7
+
8
+
9
+ def make_colors(C):
10
+ angle = np.linspace(0, 1, C, endpoint=False) * 2 * np.pi
11
+ angles = np.stack((angle, angle + 2*np.pi/3, angle + 4*np.pi/3), axis=-1)
12
+ return ((np.cos(angles) + 1) / 2).astype(np.float32)
13
+
14
+
15
+ def bench(fn, n_warmup=3, n_iter=10):
16
+ for _ in range(n_warmup):
17
+ fn()
18
+ if torch.cuda.is_available():
19
+ torch.cuda.synchronize()
20
+ t0 = time.perf_counter()
21
+ for _ in range(n_iter):
22
+ fn()
23
+ if torch.cuda.is_available():
24
+ torch.cuda.synchronize()
25
+ return (time.perf_counter() - t0) / n_iter * 1000
26
+
27
+
28
+ configs = [
29
+ (126, (2000, 2000), [126], '126ch 2kx2k single'),
30
+ (126, (2000, 2000), [42,42,42], '126ch 2kx2k 3-int'),
31
+ (126, (10000, 100), [126], '126ch 10kx100 single'),
32
+ (256, (5000, 50), [64]*4, '256ch 5kx50 4-int'),
33
+ (512, (1000, 1000), [512], '512ch 1kx1k single'),
34
+ (512, (1000, 1000), [128]*4, '512ch 1kx1k 4-int'),
35
+ (1024,(500, 500), [1024], '1024ch 500x500 single'),
36
+ (126, (100, 100, 100),[126], '126ch 100^3 single'),
37
+ (256, (32, 32), [256], '256ch 32x32 single'),
38
+ (3, (4000, 4000), [3], '3ch 4kx4k single'),
39
+ (8, (4000, 4000), [4,4], '8ch 4kx4k 2-int'),
40
+ ]
41
+
42
+ devices = ['cpu']
43
+ if torch.cuda.is_available():
44
+ devices.append('cuda')
45
+ if torch.backends.mps.is_available():
46
+ devices.append('mps')
47
+
48
+ for device_name in devices:
49
+ device = torch.device(device_name)
50
+ print(f"\n{'='*95}")
51
+ print(f" Device: {device_name.upper()}")
52
+ print(f"{'='*95}")
53
+ print(f"{'Config':<35} {'np mm':>8} {'np ein':>8} {'t mm':>8} {'t ein':>8} np torch")
54
+ print('-' * 95)
55
+
56
+ for C, spatial, intervals, label in configs:
57
+ N = len(intervals)
58
+ im_np = np.random.rand(C, *spatial).astype(np.float32)
59
+ colors = make_colors(C)
60
+ agg = np.zeros((C, N), dtype=np.float32)
61
+ s = 0
62
+ for i, sz in enumerate(intervals):
63
+ agg[s:s+sz, i] = 1.0/sz
64
+ s += sz
65
+
66
+ idx = ''.join(c for c in string.ascii_lowercase if c not in 'cl')
67
+ sp = idx[:len(spatial)]
68
+ eq = f'c{sp},cN,cl->N{sp}l'
69
+
70
+ # Numpy (always CPU)
71
+ def np_mm():
72
+ w = (agg[...,None]*colors[:,None,:]).reshape(C,N*3)
73
+ return w.T @ im_np.reshape(C,-1)
74
+ def np_ein():
75
+ return contract(eq, im_np, agg, colors)
76
+
77
+ t1 = bench(np_mm)
78
+ t2 = bench(np_ein)
79
+
80
+ # Torch on target device
81
+ im_t = torch.from_numpy(im_np).to(device)
82
+ agg_t = torch.from_numpy(agg).to(device)
83
+ col_t = torch.from_numpy(colors).to(device)
84
+
85
+ def t_mm():
86
+ w = (agg_t[...,None]*col_t[:,None,:]).reshape(C,N*3).float()
87
+ return w.T @ im_t.reshape(C,-1).float()
88
+ def t_ein():
89
+ return contract(eq, im_t.float(), agg_t, col_t)
90
+
91
+ t3 = bench(t_mm)
92
+ t4 = bench(t_ein)
93
+
94
+ np_w = 'ein' if t2 < t1 else 'mm'
95
+ t_w = 'ein' if t4 < t3 else 'mm'
96
+ np_r = max(t1,t2)/min(t1,t2)
97
+ t_r = max(t3,t4)/min(t3,t4)
98
+
99
+ print(f'{label:<35} {t1:>6.1f}ms {t2:>6.1f}ms {t3:>6.1f}ms {t4:>6.1f}ms {np_w}({np_r:.1f}x) {t_w}({t_r:.1f}x)')
100
+
101
+ # Free GPU memory
102
+ del im_t, agg_t, col_t
103
+ if device_name != 'cpu':
104
+ torch.cuda.empty_cache() if device_name == 'cuda' else torch.mps.empty_cache()
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ #
3
+ # Run ocdkit tests on Mac (MPS), Threadripper (CUDA), and
4
+ # Threadripper with CUDA disabled (CPU-only), then combine coverage.
5
+ # Notebook-based tests (IPython/Jupyter-dependent code) run on Mac only.
6
+ #
7
+ # Usage: bash scripts/coverage_cross_device.sh
8
+ #
9
+ set -euo pipefail
10
+
11
+ MAC_ROOT="/Volumes/DataDrive/ocdkit"
12
+ REMOTE_ROOT="/home/kcutler/DataDrive/ocdkit"
13
+ REMOTE="kcutler@threadripper.local"
14
+ PYENV='export PATH="$HOME/.pyenv/shims:$HOME/.pyenv/bin:$PATH"'
15
+ COV_DIR="$MAC_ROOT/.coverage_combined"
16
+
17
+ rm -rf "$COV_DIR"
18
+ mkdir -p "$COV_DIR"
19
+
20
+ echo "=== Mac (MPS) ==="
21
+ cd "$MAC_ROOT"
22
+ python -m coverage run --data-file="$COV_DIR/.coverage.mac" -m pytest tests/ -q
23
+
24
+ echo ""
25
+ echo "=== Threadripper (CUDA) ==="
26
+ ssh "$REMOTE" "$PYENV && cd $REMOTE_ROOT && python -m coverage run --data-file=/tmp/.coverage.cuda -m pytest tests/ -q --ignore=tests/test_plot_notebook.py"
27
+ scp "$REMOTE":/tmp/.coverage.cuda "$COV_DIR/.coverage.cuda"
28
+
29
+ echo ""
30
+ echo "=== Threadripper (CPU-only) ==="
31
+ ssh "$REMOTE" "$PYENV && cd $REMOTE_ROOT && CUDA_VISIBLE_DEVICES='' python -m coverage run --data-file=/tmp/.coverage.cpu -m pytest tests/ -q --ignore=tests/test_plot_notebook.py"
32
+ scp "$REMOTE":/tmp/.coverage.cpu "$COV_DIR/.coverage.cpu"
33
+
34
+ echo ""
35
+ echo "=== Combining coverage ==="
36
+ cd "$MAC_ROOT"
37
+ python -m coverage combine --data-file="$COV_DIR/.coverage" "$COV_DIR"/.coverage.*
38
+ python -m coverage report --data-file="$COV_DIR/.coverage" --show-missing
39
+ echo ""
40
+ python -m coverage html --data-file="$COV_DIR/.coverage" -d "$COV_DIR/htmlcov"
41
+ echo "HTML report: file://$COV_DIR/htmlcov/index.html"
ocdkit-0.0.1/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,10 @@
1
+ """
2
+ ocdkit — Obsessively precise utilities for scientific Python.
3
+
4
+ Array manipulation, GPU dispatch, image I/O, morphology, and plotting
5
+ tools shared across projects.
6
+ """
7
+
8
+ from .load import enable_submodules
9
+
10
+ enable_submodules(__name__)
@@ -0,0 +1,3 @@
1
+ from ..load import enable_submodules
2
+
3
+ enable_submodules(__name__)
@@ -0,0 +1,121 @@
1
+ """Type detection, conversion, and rescaling utilities."""
2
+
3
+ from .imports import *
4
+
5
+
6
+ def get_module(x):
7
+ """Return ``np`` or ``torch`` depending on the type of *x*."""
8
+ if isinstance(x, da.Array):
9
+ return np
10
+ if isinstance(x, (np.ndarray, tuple, int, float)) or np.isscalar(x):
11
+ return np
12
+ if torch.is_tensor(x):
13
+ return torch
14
+ raise ValueError(
15
+ "Input must be a numpy array, a tuple, a torch tensor, "
16
+ "an integer, or a float"
17
+ )
18
+
19
+
20
+ def safe_divide(num, den, cutoff=0):
21
+ """Division ignoring zeros and NaNs in the denominator."""
22
+ module = get_module(num)
23
+ valid_den = (den > cutoff) & module.isfinite(den)
24
+
25
+ if isinstance(num, da.Array) or isinstance(den, da.Array):
26
+ return da.where(valid_den, num / den, 0)
27
+ elif module == np:
28
+ r = num.astype(np.float32, copy=False)
29
+ r = np.divide(r, den, out=np.zeros_like(r), where=valid_den)
30
+ elif module == torch:
31
+ r = num.float()
32
+ den = den.float()
33
+ safe_den = torch.where(valid_den, den, torch.ones_like(den))
34
+ r = torch.where(valid_den, torch.div(r, safe_den), torch.zeros_like(r))
35
+ else:
36
+ raise TypeError("num must be a numpy array or a PyTorch tensor")
37
+
38
+ return r
39
+
40
+
41
+ def rescale(T, floor=None, ceiling=None, exclude_dims=None):
42
+ """Min-max rescale to [0, 1].
43
+
44
+ Works on numpy arrays and torch tensors. When *exclude_dims* is
45
+ given, normalization is applied independently along those axes.
46
+ """
47
+ module = get_module(T)
48
+ if exclude_dims is not None:
49
+ if isinstance(exclude_dims, int):
50
+ exclude_dims = (exclude_dims,)
51
+ axes = tuple(i for i in range(T.ndim) if i not in exclude_dims)
52
+ newshape = [T.shape[i] if i in exclude_dims else 1 for i in range(T.ndim)]
53
+ else:
54
+ axes = None
55
+ newshape = T.shape
56
+
57
+ if ceiling is None:
58
+ ceiling = module.amax(T, axis=axes)
59
+ if exclude_dims is not None:
60
+ ceiling = ceiling.reshape(*newshape)
61
+ if floor is None:
62
+ floor = module.amin(T, axis=axes)
63
+ if exclude_dims is not None:
64
+ floor = floor.reshape(*newshape)
65
+
66
+ T = safe_divide(T - floor, ceiling - floor)
67
+
68
+ return T
69
+
70
+
71
+ def to_16_bit(im):
72
+ """Rescale image to [0, 2**16 - 1] and cast to uint16."""
73
+ return np.uint16(rescale(im) * (2 ** 16 - 1))
74
+
75
+
76
+ def to_8_bit(im):
77
+ """Rescale image to [0, 2**8 - 1] and cast to uint8."""
78
+ return np.uint8(rescale(im) * (2 ** 8 - 1))
79
+
80
+
81
+ def is_integer(var):
82
+ """Check whether *var* is an integer or integer-typed array/tensor."""
83
+ if isinstance(var, int):
84
+ return True
85
+ if isinstance(var, np.integer):
86
+ return True
87
+ if isinstance(var, (np.ndarray, np.memmap)) and np.issubdtype(var.dtype, np.integer):
88
+ return True
89
+ if isinstance(var, da.Array) and np.issubdtype(var.dtype, np.integer):
90
+ return True
91
+ if isinstance(var, torch.Tensor) and not var.is_floating_point():
92
+ return True
93
+ return False
94
+
95
+
96
+ def move_axis(img, axis=-1, pos="last"):
97
+ """Move ndarray axis to a new location, preserving order of other axes."""
98
+ if axis == -1:
99
+ axis = img.ndim - 1
100
+ axis = min(img.ndim - 1, axis)
101
+ if pos in ("first", 0):
102
+ pos = 0
103
+ elif pos in ("last", -1):
104
+ pos = img.ndim - 1
105
+ perm = list(range(img.ndim))
106
+ perm.pop(axis)
107
+ perm.insert(pos, axis)
108
+ return np.transpose(img, perm)
109
+
110
+
111
+ def move_min_dim(img, force=False):
112
+ """Move the minimum-sized dimension last (as channels) if < 10."""
113
+ if len(img.shape) > 2:
114
+ min_dim = min(img.shape)
115
+ if min_dim < 10 or force:
116
+ if img.shape[-1] == min_dim:
117
+ channel_axis = -1
118
+ else:
119
+ channel_axis = (img.shape).index(min_dim)
120
+ img = move_axis(img, axis=channel_axis, pos="last")
121
+ return img