phasorpy 0.2__cp313-cp313-win_amd64.whl → 0.4__cp313-cp313-win_amd64.whl

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.
Binary file
phasorpy/_phasorpy.pyx CHANGED
@@ -49,6 +49,12 @@ ctypedef fused float_t:
49
49
  float
50
50
  double
51
51
 
52
+ ctypedef fused uint_t:
53
+ uint8_t
54
+ uint16_t
55
+ uint32_t
56
+ uint64_t
57
+
52
58
  ctypedef fused signal_t:
53
59
  uint8_t
54
60
  uint16_t
@@ -68,7 +74,8 @@ def _phasor_from_signal(
68
74
  float_t[:, :, ::1] phasor,
69
75
  const signal_t[:, :, ::1] signal,
70
76
  const double[:, :, ::1] sincos,
71
- const int num_threads
77
+ const bint normalize,
78
+ const int num_threads,
72
79
  ):
73
80
  """Return phasor coordinates from signal along middle axis.
74
81
 
@@ -97,6 +104,8 @@ def _phasor_from_signal(
97
104
  1. number samples
98
105
  2. cos and sin
99
106
 
107
+ normalize : bool
108
+ Normalize phasor coordinates.
100
109
  num_threads : int
101
110
  Number of OpenMP threads to use for parallelization.
102
111
 
@@ -145,14 +154,16 @@ def _phasor_from_signal(
145
154
  dc = dc + sample
146
155
  re = re + sample * sincos[h, k, 0]
147
156
  im = im + sample * sincos[h, k, 1]
148
- if dc != 0.0:
149
- re = re / dc
150
- im = im / dc
151
- dc = dc /samples
152
- else:
153
- dc = 0.0
154
- re = NAN if re == 0.0 else re * INFINITY
155
- im = NAN if im == 0.0 else im * INFINITY
157
+ if normalize:
158
+ if dc != 0.0:
159
+ # includes isnan(dc)
160
+ re = re / dc
161
+ im = im / dc
162
+ dc = dc / samples
163
+ else:
164
+ # dc = 0.0
165
+ re = NAN if re == 0.0 else re * INFINITY
166
+ im = NAN if im == 0.0 else im * INFINITY
156
167
  if h == 0:
157
168
  mean[i, j] = <float_t> dc
158
169
  real[h, i, j] = <float_t> re
@@ -173,14 +184,16 @@ def _phasor_from_signal(
173
184
  dc = dc + sample
174
185
  re = re + sample * sincos[h, k, 0]
175
186
  im = im + sample * sincos[h, k, 1]
176
- if dc != 0.0:
177
- re = re / dc
178
- im = im / dc
179
- dc = dc /samples
180
- else:
181
- dc = 0.0
182
- re = NAN if re == 0.0 else re * INFINITY
183
- im = NAN if im == 0.0 else im * INFINITY
187
+ if normalize:
188
+ if dc != 0.0:
189
+ # includes isnan(dc)
190
+ re = re / dc
191
+ im = im / dc
192
+ dc = dc / samples
193
+ else:
194
+ # dc = 0.0
195
+ re = NAN if re == 0.0 else re * INFINITY
196
+ im = NAN if im == 0.0 else im * INFINITY
184
197
  if h == 0:
185
198
  mean[i, j] = <float_t> dc
186
199
  real[h, i, j] = <float_t> re
@@ -201,14 +214,16 @@ def _phasor_from_signal(
201
214
  dc += sample
202
215
  re += sample * sincos[h, k, 0]
203
216
  im += sample * sincos[h, k, 1]
204
- if dc != 0.0:
205
- re /= dc
206
- im /= dc
207
- dc /= samples
208
- else:
209
- dc = 0.0
210
- re = NAN if re == 0.0 else re * INFINITY
211
- im = NAN if im == 0.0 else im * INFINITY
217
+ if normalize:
218
+ if dc != 0.0:
219
+ # includes isnan(dc)
220
+ re /= dc
221
+ im /= dc
222
+ dc = dc / samples
223
+ else:
224
+ # dc = 0.0
225
+ re = NAN if re == 0.0 else re * INFINITY
226
+ im = NAN if im == 0.0 else im * INFINITY
212
227
  if h == 0:
213
228
  mean[i, j] = <float_t> dc
214
229
  real[h, i, j] = <float_t> re
@@ -924,32 +939,41 @@ cdef (float_t, float_t) _phasor_at_harmonic(
924
939
 
925
940
  @cython.ufunc
926
941
  cdef (float_t, float_t) _phasor_multiply(
927
- float_t real1,
928
- float_t imag1,
942
+ float_t real,
943
+ float_t imag,
929
944
  float_t real2,
930
945
  float_t imag2,
931
946
  ) noexcept nogil:
932
- """Return multiplication of two phasors."""
933
- return real1 * real2 - imag1 * imag2, real1 * imag2 + imag1 * real2
947
+ """Return complex multiplication of two phasors."""
948
+ return (
949
+ real * real2 - imag * imag2,
950
+ real * imag2 + imag * real2
951
+ )
934
952
 
935
953
 
936
954
  @cython.ufunc
937
955
  cdef (float_t, float_t) _phasor_divide(
938
- float_t real1,
939
- float_t imag1,
956
+ float_t real,
957
+ float_t imag,
940
958
  float_t real2,
941
959
  float_t imag2,
942
960
  ) noexcept nogil:
943
- """Return division of two phasors."""
961
+ """Return complex division of two phasors."""
944
962
  cdef:
945
- float_t denom = real2 * real2 + imag2 * imag2
963
+ float_t divisor = real2 * real2 + imag2 * imag2
946
964
 
947
- if isnan(denom) or denom == 0.0:
948
- return <float_t> NAN, <float_t> NAN
965
+ if divisor != 0.0:
966
+ # includes isnan(divisor)
967
+ return (
968
+ (real * real2 + imag * imag2) / divisor,
969
+ (imag * real2 - real * imag2) / divisor
970
+ )
949
971
 
972
+ real = real * real2 + imag * imag2
973
+ imag = imag * real2 - real * imag2
950
974
  return (
951
- (real1 * real2 + imag1 * imag2) / denom,
952
- (imag1 * real2 - real1 * imag2) / denom
975
+ NAN if real == 0.0 else real * INFINITY,
976
+ NAN if imag == 0.0 else imag * INFINITY
953
977
  )
954
978
 
955
979
 
@@ -2141,3 +2165,66 @@ def _median_filter_2d(
2141
2165
  image[i, j] = filtered_image[i, j]
2142
2166
 
2143
2167
  free(kernel)
2168
+
2169
+
2170
+ ###############################################################################
2171
+ # Decoder functions
2172
+
2173
+
2174
+ def _flimlabs_signal(
2175
+ uint_t[:, :, ::] signal, # channel, pixel, bin
2176
+ list data, # list[list[list[[int, int]]]]
2177
+ ssize_t channel = -1 # -1 == None
2178
+ ):
2179
+ """Return TCSPC histogram image from FLIM LABS JSON intensity data."""
2180
+ cdef:
2181
+ list channels, pixels
2182
+ ssize_t c, i, h, count
2183
+
2184
+ if channel < 0:
2185
+ c = 0
2186
+ for channels in data:
2187
+ i = 0
2188
+ for pixels in channels:
2189
+ for h, count in pixels:
2190
+ signal[c, i, h] = <uint_t> count
2191
+ i += 1
2192
+ c += 1
2193
+ else:
2194
+ i = 0
2195
+ for pixels in data[channel]:
2196
+ for h, count in pixels:
2197
+ signal[0, i, h] = <uint_t> count
2198
+ i += 1
2199
+
2200
+
2201
+ def _flimlabs_mean(
2202
+ float_t[:, ::] mean, # channel, pixel
2203
+ list data, # list[list[list[[int, int]]]]
2204
+ ssize_t channel = -1 # -1 == None
2205
+ ):
2206
+ """Return mean intensity image from FLIM LABS JSON intensity data."""
2207
+ cdef:
2208
+ list channels, pixels
2209
+ ssize_t c, i, h, count
2210
+ double sum
2211
+
2212
+ if channel < 0:
2213
+ c = 0
2214
+ for channels in data:
2215
+ i = 0
2216
+ for pixels in channels:
2217
+ sum = 0.0
2218
+ for h, count in pixels:
2219
+ sum += <double> count
2220
+ mean[c, i] = <float_t> (sum / 255.0)
2221
+ i += 1
2222
+ c += 1
2223
+ else:
2224
+ i = 0
2225
+ for pixels in data[channel]:
2226
+ sum = 0.0
2227
+ for h, count in pixels:
2228
+ sum += <double> count
2229
+ mean[0, i] = <float_t> (sum / 255.0)
2230
+ i += 1
phasorpy/_utils.py CHANGED
@@ -1,6 +1,4 @@
1
- """Private auxiliary and convenience functions.
2
-
3
- """
1
+ """Private auxiliary and convenience functions."""
4
2
 
5
3
  from __future__ import annotations
6
4
 
@@ -10,6 +8,7 @@ __all__: list[str] = [
10
8
  'kwargs_notnone',
11
9
  'parse_harmonic',
12
10
  'parse_kwargs',
11
+ 'parse_signal_axis',
13
12
  'phasor_from_polar_scalar',
14
13
  'phasor_to_polar_scalar',
15
14
  'scale_matrix',
@@ -247,6 +246,72 @@ def phasor_from_polar_scalar(
247
246
  return real, imag
248
247
 
249
248
 
249
+ def parse_signal_axis(
250
+ signal: ArrayLike,
251
+ /,
252
+ axis: int | str | None = None,
253
+ ) -> tuple[int, str]:
254
+ """Return axis over which phasor coordinates are computed.
255
+
256
+ The axis parameter is not validated against the signal shape.
257
+
258
+ Parameters
259
+ ----------
260
+ signal : array_like
261
+ Image stack.
262
+ axis : int or str, optional
263
+ Axis over which phasor coordinates are computed.
264
+ By default, the 'H' or 'C' axes if `signal` contains such
265
+ dimension names, else the last axis (-1).
266
+
267
+ Returns
268
+ -------
269
+ axis : int
270
+ Axis over which phasor coordinates are computed.
271
+ axis_label: str
272
+ Axis label from `signal.dims` if any.
273
+
274
+ Raises
275
+ ------
276
+ ValueError
277
+ Axis not found in signal.dims or invalid for signal type.
278
+
279
+ Examples
280
+ --------
281
+ >>> parse_signal_axis([])
282
+ (-1, '')
283
+ >>> parse_signal_axis([], 1)
284
+ (1, '')
285
+ >>> class DataArray:
286
+ ... dims = ('C', 'H', 'Y', 'X')
287
+ ...
288
+ >>> parse_signal_axis(DataArray())
289
+ (1, 'H')
290
+ >>> parse_signal_axis(DataArray(), 'C')
291
+ (0, 'C')
292
+ >>> parse_signal_axis(DataArray(), 1)
293
+ (1, 'H')
294
+
295
+ """
296
+ if hasattr(signal, 'dims'):
297
+ assert isinstance(signal.dims, tuple)
298
+ if axis is None:
299
+ for ax in 'HC':
300
+ if ax in signal.dims:
301
+ return signal.dims.index(ax), ax
302
+ return -1, signal.dims[-1]
303
+ if isinstance(axis, int):
304
+ return axis, signal.dims[axis]
305
+ if axis in signal.dims:
306
+ return signal.dims.index(axis), axis
307
+ raise ValueError(f'{axis=} not found in {signal.dims}')
308
+ if axis is None:
309
+ return -1, ''
310
+ if isinstance(axis, int):
311
+ return axis, ''
312
+ raise ValueError(f'{axis=} not valid for {type(signal)=}')
313
+
314
+
250
315
  def parse_harmonic(
251
316
  harmonic: int | Sequence[int] | Literal['all'] | str | None,
252
317
  harmonic_max: int | None = None,
@@ -259,7 +324,7 @@ def parse_harmonic(
259
324
 
260
325
  Parameters
261
326
  ----------
262
- harmonic : int, list of int, 'all', or None
327
+ harmonic : int, sequence of int, 'all', or None
263
328
  Harmonic parameter to parse.
264
329
  harmonic_max : int, optional
265
330
  Maximum value allowed in `hamonic`. Must be one or greater.
@@ -329,7 +394,7 @@ def chunk_iter(
329
394
  shape: tuple[int, ...],
330
395
  chunk_shape: tuple[int, ...],
331
396
  /,
332
- axes: str | Sequence[str] | None = None,
397
+ dims: Sequence[str] | None = None,
333
398
  *,
334
399
  pattern: str | None = None,
335
400
  squeeze: bool = False,
@@ -343,11 +408,11 @@ def chunk_iter(
343
408
  Shape of C-order ndarray to chunk.
344
409
  chunk_shape : tuple of int
345
410
  Shape of chunks in the most significant dimensions.
346
- axes : str or sequence of str, optional
411
+ dims : sequence of str, optional
347
412
  Labels for each axis in shape if `pattern` is None.
348
413
  pattern : str, optional
349
414
  String to format chunk indices.
350
- If None, use ``_[{axes[index]}{chunk_index[index]}]`` for each axis.
415
+ If None, use ``_[{dims[index]}{chunk_index[index]}]`` for each axis.
351
416
  squeeze : bool
352
417
  If true, do not include length-1 chunked dimensions in label
353
418
  unless dimensions are part of `chunk_shape`.
@@ -384,11 +449,11 @@ def chunk_iter(
384
449
  ndim = len(shape)
385
450
 
386
451
  sep = '_'
387
- if axes is None:
388
- axes = sep * ndim
452
+ if dims is None:
453
+ dims = sep * ndim
389
454
  sep = ''
390
- elif ndim != len(axes):
391
- raise ValueError(f'{len(shape)=} != {len(axes)=}')
455
+ elif ndim != len(dims):
456
+ raise ValueError(f'{len(shape)=} != {len(dims)=}')
392
457
 
393
458
  if pattern is not None:
394
459
  try:
@@ -406,7 +471,7 @@ def chunk_iter(
406
471
 
407
472
  chunked_shape = []
408
473
  pattern_list = []
409
- for i, (size, chunk_size, ax) in enumerate(zip(shape, chunk_shape, axes)):
474
+ for i, (size, chunk_size, ax) in enumerate(zip(shape, chunk_shape, dims)):
410
475
  if size <= 0:
411
476
  raise ValueError('shape must contain positive sizes')
412
477
  if chunk_size <= 0:
phasorpy/color.py CHANGED
@@ -59,8 +59,7 @@ def wavelength2rgb(
59
59
  else:
60
60
  rgb = rgb.astype(dtype)
61
61
  if astuple:
62
- rgb_list = rgb.tolist()
63
- return (rgb_list[0], rgb_list[1], rgb_list[2])
62
+ return tuple(rgb.tolist()[:3])
64
63
  return rgb
65
64
 
66
65
 
phasorpy/datasets.py CHANGED
@@ -305,6 +305,85 @@ CONVALLARIA_FBD = pooch.create(
305
305
  },
306
306
  )
307
307
 
308
+ FLIMLABS = pooch.create(
309
+ path=pooch.os_cache('phasorpy'),
310
+ base_url='https://github.com/phasorpy/phasorpy-data/raw/main/flimlabs',
311
+ env=ENV,
312
+ registry={
313
+ 'calibrator_2_5_1737112045_imaging.json': (
314
+ 'sha256:'
315
+ 'a34c7077e88d1e7272953a46b2bb4e3ab8adf5a2f61c824dfc27032d952b920e'
316
+ ),
317
+ 'calibrator_2_5_1737112045_imaging.json.zip': (
318
+ 'sha256:'
319
+ 'fea791b28afd8365152018810cbbaaac1177cb72827578073587a1050d1af329'
320
+ ),
321
+ 'calibrator_2_5_1737112045_imaging_calibration.json': (
322
+ 'sha256:'
323
+ '8f2ebe9b544fae9524dc13221c1a5ab1b57d9dfd40ec2eb06a7b1475fcd63057'
324
+ ),
325
+ 'calibrator_2_5_bis_1737112494_imaging.json': (
326
+ 'sha256:'
327
+ '0509c5aba066419b03f83264eba58acbf4aae470aa1057c52f45e60225e033a4'
328
+ ),
329
+ 'calibrator_2_5_bis_1737112494_imaging.json.zip': (
330
+ 'sha256:'
331
+ 'bdc5df2a3f08a64ec7b7bb57b36e21546de142e67c59c052318252dbb66d8abf'
332
+ ),
333
+ 'calibrator_2_5_bis_1737112494_imaging_calibration.json': (
334
+ 'sha256:'
335
+ '9bb0e21b1e7c04add672aa8a78048b09908c860fcaf907ca33c0c87d161f6ebf'
336
+ ),
337
+ 'convallaria_1_1737112980_phasor_ch1.json': (
338
+ 'sha256:'
339
+ '4a296a0d7898dc660a388e1bba5cf98b43c35fe12d94b7aba48d00245e37242d'
340
+ ),
341
+ 'convallaria_1_1737112980_phasor_ch1.json.zip': (
342
+ 'sha256:'
343
+ '79c416b9099c9f58d2092fe5b26ea6d0f695977b877784cf564d3ead896d9354'
344
+ ),
345
+ 'convallaria_2_1737113097_phasor_ch1.json': (
346
+ 'sha256:'
347
+ 'da549645ffd898238c26f7a1eac3aca4ffccec86653c0d241a6ece674dfce90d'
348
+ ),
349
+ 'convallaria_2_1737113097_phasor_ch1.json.zip': (
350
+ 'sha256:'
351
+ '8801bb14b457dceaef42e8b3bf6af770a2e14264cd2b282ba7e3d70b91ea954c'
352
+ ),
353
+ 'data_2_calibrator_2_5_1737112409_phasor_ch1.json': (
354
+ 'sha256:'
355
+ 'ea8683892eb76f52231e5d6ceab64a3737454aa95fe73185366de8f758fd9b70'
356
+ ),
357
+ 'data_2_calibrator_2_5_1737112409_phasor_ch1.json.zip': (
358
+ 'sha256:'
359
+ '40d2aa90b95fd8864a2392337c83a1a4f4931d7359cb30a486f65173f208de0a'
360
+ ),
361
+ 'data_calibrator_2_5_1737112133_phasor_ch1.json': (
362
+ 'sha256:'
363
+ '6a8790212bc62014d597402ec5feb0e50ec6ae2aa62d63fae8cb62c6c5656268'
364
+ ),
365
+ 'data_calibrator_2_5_1737112133_phasor_ch1.json.zip': (
366
+ 'sha256:'
367
+ 'c9ef343bdbd7a51d23fdf4082e379dcdb1ce9f3e2ba065289bf2925d68ef55ba'
368
+ ),
369
+ },
370
+ )
371
+
372
+ FIGSHARE_22336594 = pooch.create(
373
+ path=pooch.os_cache('phasorpy'),
374
+ base_url=(
375
+ 'https://github.com/phasorpy/phasorpy-data/raw/main/figshare_22336594'
376
+ if DATA_ON_GITHUB
377
+ else 'doi:10.6084/m9.figshare.22336594.v1'
378
+ ),
379
+ env=ENV,
380
+ registry={
381
+ 'FLIM_testdata.lif': (
382
+ 'sha256:'
383
+ '902d8fa6cd39da7cf062b32d43aab518fa2a851eab72b4bd8b8eca1bad591850'
384
+ ),
385
+ },
386
+ )
308
387
 
309
388
  REPOSITORIES: dict[str, pooch.Pooch] = {
310
389
  'tests': TESTS,
@@ -313,6 +392,8 @@ REPOSITORIES: dict[str, pooch.Pooch] = {
313
392
  'napari-flim-phasor-plotter': NAPARI_FLIM_PHASOR_PLOTTER,
314
393
  'zenodo-13625087': ZENODO_13625087,
315
394
  'convallaria-fbd': CONVALLARIA_FBD,
395
+ 'flimlabs': FLIMLABS,
396
+ 'figshare_22336594': FIGSHARE_22336594,
316
397
  }
317
398
  """Pooch repositories."""
318
399