iivs-lib 0.1.0__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 (47) hide show
  1. iivs_lib-0.1.0/LICENSE +21 -0
  2. iivs_lib-0.1.0/PKG-INFO +182 -0
  3. iivs_lib-0.1.0/README.md +154 -0
  4. iivs_lib-0.1.0/iivs/__init__.py +0 -0
  5. iivs_lib-0.1.0/iivs/dhm/__init__.py +0 -0
  6. iivs_lib-0.1.0/iivs/dhm/analysis/__init__.py +24 -0
  7. iivs_lib-0.1.0/iivs/dhm/analysis/drymass.py +233 -0
  8. iivs_lib-0.1.0/iivs/dhm/analysis/opd.py +111 -0
  9. iivs_lib-0.1.0/iivs/dhm/analysis/pytorch/__init__.py +37 -0
  10. iivs_lib-0.1.0/iivs/dhm/analysis/pytorch/drymass.py +212 -0
  11. iivs_lib-0.1.0/iivs/dhm/analysis/pytorch/opd.py +90 -0
  12. iivs_lib-0.1.0/iivs/dhm/data/__init__.py +19 -0
  13. iivs_lib-0.1.0/iivs/dhm/data/common/__init__.py +65 -0
  14. iivs_lib-0.1.0/iivs/dhm/data/common/bin.py +244 -0
  15. iivs_lib-0.1.0/iivs/dhm/data/common/float.py +160 -0
  16. iivs_lib-0.1.0/iivs/dhm/data/common/image.py +177 -0
  17. iivs_lib-0.1.0/iivs/dhm/data/common/npy.py +48 -0
  18. iivs_lib-0.1.0/iivs/dhm/data/common/sequence.py +122 -0
  19. iivs_lib-0.1.0/iivs/dhm/data/common/txt.py +168 -0
  20. iivs_lib-0.1.0/iivs/dhm/data/common/utils.py +13 -0
  21. iivs_lib-0.1.0/iivs/dhm/data/common/validation.py +114 -0
  22. iivs_lib-0.1.0/iivs/dhm/data/constants.py +40 -0
  23. iivs_lib-0.1.0/iivs/dhm/data/hologram/__init__.py +39 -0
  24. iivs_lib-0.1.0/iivs/dhm/data/hologram/base.py +21 -0
  25. iivs_lib-0.1.0/iivs/dhm/data/hologram/convert.py +65 -0
  26. iivs_lib-0.1.0/iivs/dhm/data/hologram/npy.py +68 -0
  27. iivs_lib-0.1.0/iivs/dhm/data/hologram/raw.py +306 -0
  28. iivs_lib-0.1.0/iivs/dhm/data/hologram/tif.py +105 -0
  29. iivs_lib-0.1.0/iivs/dhm/data/intensity/__init__.py +58 -0
  30. iivs_lib-0.1.0/iivs/dhm/data/intensity/base.py +82 -0
  31. iivs_lib-0.1.0/iivs/dhm/data/intensity/bin.py +274 -0
  32. iivs_lib-0.1.0/iivs/dhm/data/intensity/convert.py +107 -0
  33. iivs_lib-0.1.0/iivs/dhm/data/intensity/npy.py +114 -0
  34. iivs_lib-0.1.0/iivs/dhm/data/intensity/tif.py +44 -0
  35. iivs_lib-0.1.0/iivs/dhm/data/intensity/txt.py +218 -0
  36. iivs_lib-0.1.0/iivs/dhm/data/phase/__init__.py +62 -0
  37. iivs_lib-0.1.0/iivs/dhm/data/phase/base.py +342 -0
  38. iivs_lib-0.1.0/iivs/dhm/data/phase/bin.py +370 -0
  39. iivs_lib-0.1.0/iivs/dhm/data/phase/bounds.py +162 -0
  40. iivs_lib-0.1.0/iivs/dhm/data/phase/convert.py +116 -0
  41. iivs_lib-0.1.0/iivs/dhm/data/phase/npy.py +137 -0
  42. iivs_lib-0.1.0/iivs/dhm/data/phase/tif.py +42 -0
  43. iivs_lib-0.1.0/iivs/dhm/data/phase/txt.py +319 -0
  44. iivs_lib-0.1.0/iivs/dhm/data/phase/unit.py +103 -0
  45. iivs_lib-0.1.0/iivs/dhm/data/timestamp.py +257 -0
  46. iivs_lib-0.1.0/iivs/py.typed +0 -0
  47. iivs_lib-0.1.0/pyproject.toml +325 -0
iivs_lib-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jaewoo Park
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,182 @@
1
+ Metadata-Version: 2.4
2
+ Name: iivs-lib
3
+ Version: 0.1.0
4
+ Summary: A Python toolkit for multi-modal holographic systems and cellular analysis.
5
+ Keywords:
6
+ Author: Jaewoo Park
7
+ Author-email: Jaewoo Park <kaparoo2001@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Programming Language :: Python :: Implementation :: CPython
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: kaparoo-python>=0.7.0
18
+ Requires-Dist: natsort>=8.4.0
19
+ Requires-Dist: numpy>=2.4.6
20
+ Requires-Dist: tifffile>=2026.6.1
21
+ Requires-Dist: imagecodecs>=2026.5.10 ; extra == 'image'
22
+ Requires-Dist: torch>=2.6 ; extra == 'torch'
23
+ Requires-Python: >=3.14
24
+ Project-URL: GitHub, https://www.github.com/iivs-lab/iivs-lib
25
+ Provides-Extra: image
26
+ Provides-Extra: torch
27
+ Description-Content-Type: text/markdown
28
+
29
+ # iivs-lib
30
+
31
+ [![PyPI version](https://img.shields.io/pypi/v/iivs-lib.svg)](https://pypi.org/project/iivs-lib/)
32
+ [![Downloads](https://pepy.tech/badge/iivs-lib)](https://pypi.org/project/iivs-lib/)
33
+ [![Python](https://img.shields.io/badge/python-3.14+-blue.svg)](https://www.python.org/)
34
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
35
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
36
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
37
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
38
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier)
39
+
40
+ *A Python toolkit for multi-modal holographic systems and cellular analysis.*
41
+
42
+ ## 📦 Installation
43
+
44
+ Requires Python 3.14+.
45
+
46
+ ```bash
47
+ # With uv (recommended)
48
+ uv add iivs-lib
49
+
50
+ # With pip
51
+ pip install iivs-lib
52
+ ```
53
+
54
+ The `[image]` extra adds `imagecodecs`, needed only to decode the
55
+ LZW-compressed Koala `Image/*.tif` uint8 previews (`uv add "iivs-lib[image]"`).
56
+ The `[torch]` extra adds PyTorch for `iivs.dhm.analysis.pytorch` — tensor-in /
57
+ tensor-out OPD / dry-mass twins with autograd (`uv add "iivs-lib[torch]"`).
58
+
59
+ ## 🚀 Quick start
60
+
61
+ ```python
62
+ from iivs.dhm.data.phase import PhaseBinFolder, PhaseUnit
63
+ from iivs.dhm.analysis import DryMassCalculator
64
+
65
+ # Lazily open a phase acquisition (a folder of numbered .bin frames).
66
+ phase = PhaseBinFolder("scan/Phase/Float/Bin", target_unit=PhaseUnit.RADIANS)
67
+ phase.frame_shape # (H, W), shared across frames
68
+ img = phase[0] # first frame as a float32 array (decoded on access)
69
+
70
+ # Per-frame dry mass over a segmented cell:
71
+ calc = DryMassCalculator(pixel_size=phase.header.pixel_size)
72
+ mass_pg = calc.calc_from_phase(img, mask=cell_mask)
73
+ ```
74
+
75
+ ## 🧩 Modules
76
+
77
+ Each package ships a detailed README in the source tree — the endpoints,
78
+ examples, and the inherited sequence interface:
79
+ [`hologram`](https://github.com/iivs-lab/iivs-lib/tree/main/iivs/dhm/data/hologram),
80
+ [`phase`](https://github.com/iivs-lab/iivs-lib/tree/main/iivs/dhm/data/phase),
81
+ [`intensity`](https://github.com/iivs-lab/iivs-lib/tree/main/iivs/dhm/data/intensity),
82
+ [`data`](https://github.com/iivs-lab/iivs-lib/tree/main/iivs/dhm/data) (overview
83
+ and `timestamp`),
84
+ [`analysis`](https://github.com/iivs-lab/iivs-lib/tree/main/iivs/dhm/analysis).
85
+
86
+ ### `iivs.dhm.data`
87
+
88
+ Readers, writers, and lazy sequences for Lyncée Tec Koala acquisition data
89
+ (validated end-to-end against a real Koala acquisition — every format, the
90
+ sequences, and the round trips between them).
91
+
92
+ - **`hologram`** — uint8 holograms: `.tif` via `load_hologram_tif` /
93
+ `save_hologram_tif` with `HologramTifFolder` / `HologramTifList`; a single
94
+ multi-frame `.raw` via `HologramRawFile` (a lazy `np.memmap`) and
95
+ `read_hologram_raw_header`; header-less `.npy` frames via `HologramNpyFolder`.
96
+ - **`phase`** — float32 `.bin` phase images: `load_phase_bin` /
97
+ `save_phase_bin` / `read_phase_bin_header`, the typed `PhaseBinHeader` and
98
+ `PhaseUnit`, and `convert_phase_unit`; folder/list sequences
99
+ `PhaseBinFolder` / `PhaseBinList`. The same quantitative phase from Koala's
100
+ `Float/Txt` export via `load_phase_txt`, `PhaseTxtFolder` / `PhaseTxtList`.
101
+ The uint8 `Image/*.tif` display previews (not quantitative) via
102
+ `PhaseTifFolder` / `PhaseTifList`. Header-less `.npy` frames via
103
+ `PhaseNpyFolder` (`pixel_size` / `unit` / `height_scale` passed to the
104
+ constructor; `numpy.load`, pickle disabled).
105
+ - **`intensity`** — float32 `.bin` intensity reconstructions (exported
106
+ alongside phase): `load_intensity_bin` / `save_intensity_bin` /
107
+ `read_intensity_bin_header`, the typed `IntensityBinHeader`, and folder/list
108
+ sequences `IntensityBinFolder` / `IntensityBinList`; plus the `Float/Txt`
109
+ twins `load_intensity_txt`, `IntensityTxtFolder` / `IntensityTxtList`, the
110
+ uint8 `Image/*.tif` previews `IntensityTifFolder` / `IntensityTifList`, and
111
+ header-less `.npy` frames via `IntensityNpyFolder` (`pixel_size` passed in).
112
+ The phase and intensity `.bin` formats share the `common.KoalaBinHeader` base.
113
+ - **`timestamp`** — per-frame acquisition timing: the `Timestamp` record,
114
+ `TimestampsTxtFile` (Koala `timestamps.txt`), and `TimestampsFixedFPS`
115
+ (synthesized from a frame rate).
116
+
117
+ Every sequence is a `kaparoo.data.sequences.DataSequence`, so it indexes,
118
+ slices, and iterates lazily; same-shape sources also expose `frame_shape` by
119
+ mixing in `common.FrameShapedMixin` (so a uniform source is its
120
+ `<Modality>FloatSequence` / `<Modality>ImageSequence` plus that mixin). For
121
+ phase and intensity the quantitative float32 sources are
122
+ `<Modality>FloatSequence` and the uint8 `Image/*.tif` previews are
123
+ `<Modality>ImageSequence`, both under the `<Modality>Sequence` base.
124
+ Numbered-folder sequences share the `common.SequentialFileFolder`
125
+ discovery/validation base, and validate their arrays via
126
+ `common.validate_float32_image` / `validate_uint8_image`. These cross-modality
127
+ building blocks live in `iivs.dhm.data.common`.
128
+
129
+ ### `iivs.dhm.analysis`
130
+
131
+ Physical quantities derived from phase, each via an engine object that
132
+ precomputes its conversion factor (with one-shot function conveniences):
133
+
134
+ - **`opd`** — optical path difference (`OPD = phase * wavelength / (2*pi)`, in
135
+ nm). `OPDConverter` (`convert_to_opd` / `convert_to_phase`, scale
136
+ `opd_scale`); `phase_to_opd` / `opd_to_phase`.
137
+ - **`drymass`** — dry mass (pg) via the Barer relation. `DryMassCalculator`
138
+ (`calc_from_opd` / `calc_from_phase` over a background-corrected, optionally
139
+ masked map; scale `drymass_scale`); `calc_drymass` / `calc_drymass_from_phase`.
140
+
141
+ #### Using with PyTorch (autograd)
142
+
143
+ The `convert_*` / `calc_*` methods operate on NumPy arrays. Install the
144
+ `iivs-lib[torch]` extra for `iivs.dhm.analysis.pytorch` — tensor-in / tensor-out
145
+ twins that keep the input tensor's device, dtype, and autograd graph (the
146
+ calibration scalars are shared with the NumPy engines):
147
+
148
+ ```python
149
+ from iivs.dhm.analysis.pytorch.opd import phase_to_opd
150
+ from iivs.dhm.analysis.pytorch.drymass import calc_drymass_from_phase
151
+
152
+ opd = phase_to_opd(phase, wavelength=666e-9) # Tensor (CPU/GPU), grad kept
153
+ mass = calc_drymass_from_phase(phase, pixel_size=px, mask=cell) # 0-dim Tensor, grad kept
154
+ ```
155
+
156
+ Or, without the dependency, multiply by the cached scale factors (plain floats)
157
+ with native ops yourself:
158
+
159
+ ```python
160
+ opd = phase * conv.opd_scale # phase: Tensor -> OPD (nm), grad kept
161
+ mass = opd[mask].sum() * calc.drymass_scale # OPD -> dry mass (pg), grad kept
162
+ ```
163
+
164
+ ## 📋 TODO
165
+
166
+ See [TODO.md](./TODO.md) for tracked open items.
167
+
168
+ ## 📜 Changelog
169
+
170
+ See [CHANGELOG.md](./CHANGELOG.md) for the version history.
171
+
172
+ ## 🙏 Acknowledgements
173
+
174
+ The Koala file formats read and written by `iivs.dhm.data` originate from
175
+ [Lyncée Tec](https://www.lynceetec.com/)'s Koala software. The phase `.bin`
176
+ format was cross-checked against their reference implementation,
177
+ [`pyKoalaUtils`](https://github.com/lynceetec/pyKoalaUtils) (MIT). iivs-lib is
178
+ an independent reimplementation and contains no code from it.
179
+
180
+ ## ⚖️ License
181
+
182
+ This project is distributed under the terms of the [MIT](./LICENSE) license.
@@ -0,0 +1,154 @@
1
+ # iivs-lib
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/iivs-lib.svg)](https://pypi.org/project/iivs-lib/)
4
+ [![Downloads](https://pepy.tech/badge/iivs-lib)](https://pypi.org/project/iivs-lib/)
5
+ [![Python](https://img.shields.io/badge/python-3.14+-blue.svg)](https://www.python.org/)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
7
+ [![uv](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/uv/main/assets/badge/v0.json)](https://github.com/astral-sh/uv)
8
+ [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff)
9
+ [![ty](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ty/main/assets/badge/v0.json)](https://github.com/astral-sh/ty)
10
+ [![Copier](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/copier-org/copier/master/img/badge/badge-grayscale-inverted-border-orange.json)](https://github.com/copier-org/copier)
11
+
12
+ *A Python toolkit for multi-modal holographic systems and cellular analysis.*
13
+
14
+ ## 📦 Installation
15
+
16
+ Requires Python 3.14+.
17
+
18
+ ```bash
19
+ # With uv (recommended)
20
+ uv add iivs-lib
21
+
22
+ # With pip
23
+ pip install iivs-lib
24
+ ```
25
+
26
+ The `[image]` extra adds `imagecodecs`, needed only to decode the
27
+ LZW-compressed Koala `Image/*.tif` uint8 previews (`uv add "iivs-lib[image]"`).
28
+ The `[torch]` extra adds PyTorch for `iivs.dhm.analysis.pytorch` — tensor-in /
29
+ tensor-out OPD / dry-mass twins with autograd (`uv add "iivs-lib[torch]"`).
30
+
31
+ ## 🚀 Quick start
32
+
33
+ ```python
34
+ from iivs.dhm.data.phase import PhaseBinFolder, PhaseUnit
35
+ from iivs.dhm.analysis import DryMassCalculator
36
+
37
+ # Lazily open a phase acquisition (a folder of numbered .bin frames).
38
+ phase = PhaseBinFolder("scan/Phase/Float/Bin", target_unit=PhaseUnit.RADIANS)
39
+ phase.frame_shape # (H, W), shared across frames
40
+ img = phase[0] # first frame as a float32 array (decoded on access)
41
+
42
+ # Per-frame dry mass over a segmented cell:
43
+ calc = DryMassCalculator(pixel_size=phase.header.pixel_size)
44
+ mass_pg = calc.calc_from_phase(img, mask=cell_mask)
45
+ ```
46
+
47
+ ## 🧩 Modules
48
+
49
+ Each package ships a detailed README in the source tree — the endpoints,
50
+ examples, and the inherited sequence interface:
51
+ [`hologram`](https://github.com/iivs-lab/iivs-lib/tree/main/iivs/dhm/data/hologram),
52
+ [`phase`](https://github.com/iivs-lab/iivs-lib/tree/main/iivs/dhm/data/phase),
53
+ [`intensity`](https://github.com/iivs-lab/iivs-lib/tree/main/iivs/dhm/data/intensity),
54
+ [`data`](https://github.com/iivs-lab/iivs-lib/tree/main/iivs/dhm/data) (overview
55
+ and `timestamp`),
56
+ [`analysis`](https://github.com/iivs-lab/iivs-lib/tree/main/iivs/dhm/analysis).
57
+
58
+ ### `iivs.dhm.data`
59
+
60
+ Readers, writers, and lazy sequences for Lyncée Tec Koala acquisition data
61
+ (validated end-to-end against a real Koala acquisition — every format, the
62
+ sequences, and the round trips between them).
63
+
64
+ - **`hologram`** — uint8 holograms: `.tif` via `load_hologram_tif` /
65
+ `save_hologram_tif` with `HologramTifFolder` / `HologramTifList`; a single
66
+ multi-frame `.raw` via `HologramRawFile` (a lazy `np.memmap`) and
67
+ `read_hologram_raw_header`; header-less `.npy` frames via `HologramNpyFolder`.
68
+ - **`phase`** — float32 `.bin` phase images: `load_phase_bin` /
69
+ `save_phase_bin` / `read_phase_bin_header`, the typed `PhaseBinHeader` and
70
+ `PhaseUnit`, and `convert_phase_unit`; folder/list sequences
71
+ `PhaseBinFolder` / `PhaseBinList`. The same quantitative phase from Koala's
72
+ `Float/Txt` export via `load_phase_txt`, `PhaseTxtFolder` / `PhaseTxtList`.
73
+ The uint8 `Image/*.tif` display previews (not quantitative) via
74
+ `PhaseTifFolder` / `PhaseTifList`. Header-less `.npy` frames via
75
+ `PhaseNpyFolder` (`pixel_size` / `unit` / `height_scale` passed to the
76
+ constructor; `numpy.load`, pickle disabled).
77
+ - **`intensity`** — float32 `.bin` intensity reconstructions (exported
78
+ alongside phase): `load_intensity_bin` / `save_intensity_bin` /
79
+ `read_intensity_bin_header`, the typed `IntensityBinHeader`, and folder/list
80
+ sequences `IntensityBinFolder` / `IntensityBinList`; plus the `Float/Txt`
81
+ twins `load_intensity_txt`, `IntensityTxtFolder` / `IntensityTxtList`, the
82
+ uint8 `Image/*.tif` previews `IntensityTifFolder` / `IntensityTifList`, and
83
+ header-less `.npy` frames via `IntensityNpyFolder` (`pixel_size` passed in).
84
+ The phase and intensity `.bin` formats share the `common.KoalaBinHeader` base.
85
+ - **`timestamp`** — per-frame acquisition timing: the `Timestamp` record,
86
+ `TimestampsTxtFile` (Koala `timestamps.txt`), and `TimestampsFixedFPS`
87
+ (synthesized from a frame rate).
88
+
89
+ Every sequence is a `kaparoo.data.sequences.DataSequence`, so it indexes,
90
+ slices, and iterates lazily; same-shape sources also expose `frame_shape` by
91
+ mixing in `common.FrameShapedMixin` (so a uniform source is its
92
+ `<Modality>FloatSequence` / `<Modality>ImageSequence` plus that mixin). For
93
+ phase and intensity the quantitative float32 sources are
94
+ `<Modality>FloatSequence` and the uint8 `Image/*.tif` previews are
95
+ `<Modality>ImageSequence`, both under the `<Modality>Sequence` base.
96
+ Numbered-folder sequences share the `common.SequentialFileFolder`
97
+ discovery/validation base, and validate their arrays via
98
+ `common.validate_float32_image` / `validate_uint8_image`. These cross-modality
99
+ building blocks live in `iivs.dhm.data.common`.
100
+
101
+ ### `iivs.dhm.analysis`
102
+
103
+ Physical quantities derived from phase, each via an engine object that
104
+ precomputes its conversion factor (with one-shot function conveniences):
105
+
106
+ - **`opd`** — optical path difference (`OPD = phase * wavelength / (2*pi)`, in
107
+ nm). `OPDConverter` (`convert_to_opd` / `convert_to_phase`, scale
108
+ `opd_scale`); `phase_to_opd` / `opd_to_phase`.
109
+ - **`drymass`** — dry mass (pg) via the Barer relation. `DryMassCalculator`
110
+ (`calc_from_opd` / `calc_from_phase` over a background-corrected, optionally
111
+ masked map; scale `drymass_scale`); `calc_drymass` / `calc_drymass_from_phase`.
112
+
113
+ #### Using with PyTorch (autograd)
114
+
115
+ The `convert_*` / `calc_*` methods operate on NumPy arrays. Install the
116
+ `iivs-lib[torch]` extra for `iivs.dhm.analysis.pytorch` — tensor-in / tensor-out
117
+ twins that keep the input tensor's device, dtype, and autograd graph (the
118
+ calibration scalars are shared with the NumPy engines):
119
+
120
+ ```python
121
+ from iivs.dhm.analysis.pytorch.opd import phase_to_opd
122
+ from iivs.dhm.analysis.pytorch.drymass import calc_drymass_from_phase
123
+
124
+ opd = phase_to_opd(phase, wavelength=666e-9) # Tensor (CPU/GPU), grad kept
125
+ mass = calc_drymass_from_phase(phase, pixel_size=px, mask=cell) # 0-dim Tensor, grad kept
126
+ ```
127
+
128
+ Or, without the dependency, multiply by the cached scale factors (plain floats)
129
+ with native ops yourself:
130
+
131
+ ```python
132
+ opd = phase * conv.opd_scale # phase: Tensor -> OPD (nm), grad kept
133
+ mass = opd[mask].sum() * calc.drymass_scale # OPD -> dry mass (pg), grad kept
134
+ ```
135
+
136
+ ## 📋 TODO
137
+
138
+ See [TODO.md](./TODO.md) for tracked open items.
139
+
140
+ ## 📜 Changelog
141
+
142
+ See [CHANGELOG.md](./CHANGELOG.md) for the version history.
143
+
144
+ ## 🙏 Acknowledgements
145
+
146
+ The Koala file formats read and written by `iivs.dhm.data` originate from
147
+ [Lyncée Tec](https://www.lynceetec.com/)'s Koala software. The phase `.bin`
148
+ format was cross-checked against their reference implementation,
149
+ [`pyKoalaUtils`](https://github.com/lynceetec/pyKoalaUtils) (MIT). iivs-lib is
150
+ an independent reimplementation and contains no code from it.
151
+
152
+ ## ⚖️ License
153
+
154
+ This project is distributed under the terms of the [MIT](./LICENSE) license.
File without changes
File without changes
@@ -0,0 +1,24 @@
1
+ """Quantitative analysis derived from DHM phase data (OPD, dry mass, ...).
2
+
3
+ The NumPy engines and one-shot helpers are re-exported here; the Torch twins
4
+ live in `iivs.dhm.analysis.pytorch` (install the ``iivs-lib[torch]`` extra) and
5
+ are *not* re-exported, so importing this package never requires PyTorch.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ __all__ = (
11
+ "DryMassCalculator",
12
+ "OPDConverter",
13
+ "calc_drymass",
14
+ "calc_drymass_from_phase",
15
+ "opd_to_phase",
16
+ "phase_to_opd",
17
+ )
18
+
19
+ from iivs.dhm.analysis.drymass import (
20
+ DryMassCalculator,
21
+ calc_drymass,
22
+ calc_drymass_from_phase,
23
+ )
24
+ from iivs.dhm.analysis.opd import OPDConverter, opd_to_phase, phase_to_opd
@@ -0,0 +1,233 @@
1
+ from __future__ import annotations
2
+
3
+ __all__ = ("DryMassCalculator", "calc_drymass", "calc_drymass_from_phase")
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ import numpy as np
9
+
10
+ from iivs.dhm.analysis.opd import OPDConverter
11
+ from iivs.dhm.data.constants import (
12
+ DEFAULT_SPECIFIC_REFRACTIVE_INCREMENT,
13
+ DEFAULT_WAVELENGTH,
14
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ from typing import Self
18
+
19
+ from numpy.typing import NDArray
20
+
21
+
22
+ @dataclass(frozen=True, slots=True)
23
+ class DryMassCalculator:
24
+ """Integrate OPD (or phase) into dry mass (pg) at a fixed pixel size and alpha.
25
+
26
+ Bind the pixel size, specific refractive increment, and -- for the phase
27
+ path -- an `OPDConverter` once; the per-pixel mass factor is precomputed::
28
+
29
+ dmc = DryMassCalculator(pixel_size=px) # alpha, wavelength default
30
+ dmc = DryMassCalculator.from_wavelength(pixel_size=px, wavelength=666e-9)
31
+ mass = dmc.calc_from_opd(opd, mask=cell) # opd in nm
32
+ mass = dmc.calc_from_phase(phase, mask=cell) # phase in rad
33
+
34
+ Dry mass is ``(1 / alpha) * sum(OPD * pixel_area)`` (Barer), in pg, summed in
35
+ float64 over the last two axes (H, W) and returned as float32. Inputs are
36
+ batched (``(..., H, W)``)
37
+ and a ``(N, H, W)`` mask adds a trailing channel axis -- see `calc_from_opd`
38
+ for the shape / `reduce` details. The OPD must already be background-corrected
39
+ (≈ 0 outside the object); segmentation and background estimation stay the
40
+ caller's responsibility.
41
+
42
+ The free `calc_drymass` / `calc_drymass_from_phase` are one-shot conveniences
43
+ over this class. For PyTorch, use `iivs.dhm.analysis.pytorch.DryMass`.
44
+
45
+ Attributes:
46
+ pixel_size: Physical size of one (square) pixel, in m.
47
+ alpha: Specific refractive increment, in m^3/kg.
48
+ opd_converter: Phase-to-OPD converter used by `calc_from_phase`.
49
+ Defaults to one at the default wavelength; inject your own or use
50
+ `from_wavelength`.
51
+ """
52
+
53
+ pixel_size: float
54
+ alpha: float = DEFAULT_SPECIFIC_REFRACTIVE_INCREMENT
55
+ opd_converter: OPDConverter = field(default_factory=OPDConverter)
56
+
57
+ # pg of dry mass per nm of OPD summed over pixels:
58
+ _scale: float = field(init=False, repr=False, compare=False)
59
+
60
+ def __post_init__(self) -> None:
61
+ """Validate inputs and precompute the per-pixel mass factor."""
62
+ if self.pixel_size <= 0:
63
+ msg = f"pixel_size must be positive (got {self.pixel_size})"
64
+ raise ValueError(msg)
65
+
66
+ if self.alpha <= 0:
67
+ msg = f"alpha must be positive (got {self.alpha})"
68
+ raise ValueError(msg)
69
+
70
+ # pg per summed-nm OPD: px_area(m^2) * (nm->m 1e-9) * (kg->pg 1e15) / alpha.
71
+ object.__setattr__(self, "_scale", self.pixel_size**2 * 1e6 / self.alpha)
72
+
73
+ @classmethod
74
+ def from_wavelength(
75
+ cls,
76
+ *,
77
+ pixel_size: float,
78
+ wavelength: float = DEFAULT_WAVELENGTH,
79
+ alpha: float = DEFAULT_SPECIFIC_REFRACTIVE_INCREMENT,
80
+ ) -> Self:
81
+ """Build a calculator whose phase path uses `wavelength` (in m)."""
82
+ return cls(
83
+ pixel_size=pixel_size,
84
+ alpha=alpha,
85
+ opd_converter=OPDConverter(wavelength=wavelength),
86
+ )
87
+
88
+ @property
89
+ def wavelength(self) -> float:
90
+ """The bound OPD converter's wavelength, in m."""
91
+ return self.opd_converter.wavelength
92
+
93
+ @property
94
+ def wavelength_nm(self) -> float:
95
+ """The bound OPD converter's wavelength, in nm."""
96
+ return self.opd_converter.wavelength_nm
97
+
98
+ @property
99
+ def drymass_scale(self) -> float:
100
+ """pg of dry mass per nm of OPD summed over pixels.
101
+
102
+ The cached ``pixel_area / alpha`` factor: ``mass == drymass_scale *
103
+ sum(opd_nm)``. The OPD analogue is `OPDConverter.opd_scale`.
104
+ """
105
+ return self._scale
106
+
107
+ def calc_from_opd(
108
+ self,
109
+ opd: NDArray[np.float32],
110
+ *,
111
+ mask: NDArray[np.bool_] | None = None,
112
+ reduce: bool = True,
113
+ ) -> NDArray[np.float32]:
114
+ """Dry mass [pg] from an OPD map (nm), summed over the last two axes (H, W).
115
+
116
+ Args:
117
+ opd: OPD map(s), in nm, shape ``(..., H, W)``.
118
+ mask: Optional boolean mask, shape ``(H, W)`` or ``(N, H, W)`` for
119
+ `N` objects; multiplied in (broadcast), the 3-D form adding a
120
+ trailing channel axis.
121
+ reduce: If True (default), sum the per-pixel mass over (H, W) and
122
+ return the dry mass, shape ``(...)`` (or ``(..., N)`` with a
123
+ ``(N, H, W)`` mask). If False, return the per-pixel mass-density
124
+ map (``opd * scale``, masked) without summing, shape
125
+ ``(..., H, W)`` (or ``(..., N, H, W)``).
126
+
127
+ Raises:
128
+ ValueError: If `opd` is not at least 2-D ``(..., H, W)``; if `mask`
129
+ is not 2-D ``(H, W)`` or 3-D ``(N, H, W)`` (a per-frame /
130
+ higher-rank mask like ``(T, N, H, W)`` is unsupported -- loop
131
+ over its leading axes); or if `mask`'s ``(H, W)`` does not match
132
+ `opd`'s.
133
+ """
134
+ if opd.ndim < 2:
135
+ msg = f"opd must be at least 2D (..., H, W) (got {opd.ndim}D)"
136
+ raise ValueError(msg)
137
+
138
+ use_mask = mask is not None
139
+
140
+ if use_mask:
141
+ if mask.ndim not in (2, 3):
142
+ msg = f"mask must be (H, W) or (N, H, W) (got {mask.ndim}D); loop over the extra (e.g. time) axis"
143
+ raise ValueError(msg)
144
+ if mask.shape[-2:] != opd.shape[-2:]:
145
+ msg = f"opd and mask (H, W) must match (got {opd.shape[-2:]} vs {mask.shape[-2:]})"
146
+ raise ValueError(msg)
147
+
148
+ if reduce:
149
+ opd = opd.astype(np.float64, copy=False)
150
+ if use_mask:
151
+ result = np.tensordot(opd, mask, axes=([-2, -1], [-2, -1]))
152
+ else:
153
+ result = np.sum(opd, axis=(-2, -1))
154
+ elif use_mask:
155
+ if mask.ndim == 3: # (N, H, W): object axis before (H, W)
156
+ opd = opd[..., None, :, :]
157
+ result = opd * mask
158
+ else:
159
+ result = opd
160
+
161
+ # OPD (nm) -> dry mass (pg); accumulated in float64, returned as float32.
162
+ return (result * self._scale).astype(np.float32, copy=False)
163
+
164
+ def calc_from_phase(
165
+ self,
166
+ phase: NDArray[np.float32],
167
+ *,
168
+ mask: NDArray[np.bool_] | None = None,
169
+ reduce: bool = True,
170
+ ) -> NDArray[np.float32]:
171
+ """Dry mass [pg] from a phase map (rad): to OPD, then `calc_from_opd`."""
172
+ opd = self.opd_converter.convert_to_opd(phase)
173
+ return self.calc_from_opd(opd, mask=mask, reduce=reduce)
174
+
175
+
176
+ def calc_drymass(
177
+ opd: NDArray[np.float32],
178
+ *,
179
+ pixel_size: float,
180
+ alpha: float = DEFAULT_SPECIFIC_REFRACTIVE_INCREMENT,
181
+ mask: NDArray[np.bool_] | None = None,
182
+ reduce: bool = True,
183
+ ) -> NDArray[np.float32]:
184
+ """Dry mass [pg] of an OPD map (nm); one-shot `DryMassCalculator.calc_from_opd`.
185
+
186
+ Args:
187
+ opd: OPD map(s), in nm (e.g. from `phase_to_opd`), shape ``(..., H, W)``,
188
+ already background-corrected.
189
+ pixel_size: Physical size of one (square) pixel, in m.
190
+ alpha: Specific refractive increment, in m^3/kg.
191
+ mask: Optional boolean mask, shape ``(H, W)`` or ``(N, H, W)``.
192
+ reduce: Sum over (H, W) to a dry mass (True), or return the per-pixel
193
+ mass-density map (False). See `DryMassCalculator.calc_from_opd`.
194
+
195
+ Returns:
196
+ Dry mass in pg, shape ``(...)`` (or ``(..., N)``); or the unreduced
197
+ density map when `reduce` is False.
198
+ """
199
+ return DryMassCalculator(pixel_size=pixel_size, alpha=alpha).calc_from_opd(
200
+ opd, mask=mask, reduce=reduce
201
+ )
202
+
203
+
204
+ def calc_drymass_from_phase(
205
+ phase: NDArray[np.float32],
206
+ *,
207
+ pixel_size: float,
208
+ wavelength: float = DEFAULT_WAVELENGTH,
209
+ alpha: float = DEFAULT_SPECIFIC_REFRACTIVE_INCREMENT,
210
+ mask: NDArray[np.bool_] | None = None,
211
+ reduce: bool = True,
212
+ ) -> NDArray[np.float32]:
213
+ """Dry mass [pg] from a phase map (rad); one-shot `DryMassCalculator`.
214
+
215
+ Converts `phase` to OPD at `wavelength`, then integrates as `calc_drymass`.
216
+
217
+ Args:
218
+ phase: Phase map(s), in rad, shape ``(..., H, W)``, already
219
+ background-corrected.
220
+ pixel_size: Physical size of one (square) pixel, in m.
221
+ wavelength: Illumination wavelength, in m.
222
+ alpha: Specific refractive increment, in m^3/kg.
223
+ mask: Optional boolean mask, shape ``(H, W)`` or ``(N, H, W)``.
224
+ reduce: Sum over (H, W) to a dry mass (True), or return the per-pixel
225
+ mass-density map (False).
226
+
227
+ Returns:
228
+ Dry mass in pg, shape ``(...)`` (or ``(..., N)``); or the unreduced
229
+ density map when `reduce` is False.
230
+ """
231
+ return DryMassCalculator.from_wavelength(
232
+ pixel_size=pixel_size, alpha=alpha, wavelength=wavelength
233
+ ).calc_from_phase(phase, mask=mask, reduce=reduce)