phasorpy 0.2__cp312-cp312-macosx_10_13_x86_64.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.
phasorpy/_phasorpy.pyx ADDED
@@ -0,0 +1,2143 @@
1
+ # distutils: language = c
2
+ # cython: language_level = 3
3
+ # cython: boundscheck = False
4
+ # cython: wraparound = False
5
+ # cython: cdivision = True
6
+ # cython: nonecheck = False
7
+
8
+ """Cython implementation of low-level functions for the PhasorPy library."""
9
+
10
+ # TODO: replace short with unsigned char when Cython supports it
11
+ # https://github.com/cython/cython/pull/6196#issuecomment-2209509572
12
+
13
+ # TODO: use fused return types for functions returning more than two items
14
+ # https://github.com/cython/cython/issues/6328
15
+
16
+ cimport cython
17
+
18
+ from cython.parallel import parallel, prange
19
+
20
+ from libc.math cimport (
21
+ INFINITY,
22
+ M_PI,
23
+ NAN,
24
+ atan,
25
+ atan2,
26
+ copysign,
27
+ cos,
28
+ exp,
29
+ fabs,
30
+ floor,
31
+ hypot,
32
+ isnan,
33
+ sin,
34
+ sqrt,
35
+ tan,
36
+ )
37
+ from libc.stdint cimport (
38
+ int8_t,
39
+ int16_t,
40
+ int32_t,
41
+ int64_t,
42
+ uint8_t,
43
+ uint16_t,
44
+ uint32_t,
45
+ uint64_t,
46
+ )
47
+
48
+ ctypedef fused float_t:
49
+ float
50
+ double
51
+
52
+ ctypedef fused signal_t:
53
+ uint8_t
54
+ uint16_t
55
+ uint32_t
56
+ uint64_t
57
+ int8_t
58
+ int16_t
59
+ int32_t
60
+ int64_t
61
+ float
62
+ double
63
+
64
+ from libc.stdlib cimport free, malloc
65
+
66
+
67
+ def _phasor_from_signal(
68
+ float_t[:, :, ::1] phasor,
69
+ const signal_t[:, :, ::1] signal,
70
+ const double[:, :, ::1] sincos,
71
+ const int num_threads
72
+ ):
73
+ """Return phasor coordinates from signal along middle axis.
74
+
75
+ Parameters
76
+ ----------
77
+ phasor : 3D memoryview of float32 or float64
78
+ Writable buffer of three dimensions where calculated phasor
79
+ coordinates are stored:
80
+
81
+ 0. mean, real, and imaginary components
82
+ 1. lower dimensions flat
83
+ 2. upper dimensions flat
84
+
85
+ signal : 3D memoryview of float32 or float64
86
+ Buffer of three dimensions containing signal:
87
+
88
+ 0. lower dimensions flat
89
+ 1. dimension over which to compute FFT, number samples
90
+ 2. upper dimensions flat
91
+
92
+ sincos : 3D memoryview of float64
93
+ Buffer of three dimensions containing sine and cosine terms to be
94
+ multiplied with signal:
95
+
96
+ 0. number harmonics
97
+ 1. number samples
98
+ 2. cos and sin
99
+
100
+ num_threads : int
101
+ Number of OpenMP threads to use for parallelization.
102
+
103
+ Notes
104
+ -----
105
+ This implementation requires contiguous input arrays.
106
+
107
+ """
108
+ cdef:
109
+ float_t[:, ::1] mean
110
+ float_t[:, :, ::1] real, imag
111
+ ssize_t samples = signal.shape[1]
112
+ ssize_t harmonics = sincos.shape[0]
113
+ ssize_t i, j, k, h
114
+ double dc, re, im, sample
115
+
116
+ # TODO: use Numpy iterator API?
117
+ # https://numpy.org/devdocs/reference/c-api/iterator.html
118
+
119
+ if (
120
+ samples < 2
121
+ or harmonics > samples // 2
122
+ or phasor.shape[0] != harmonics * 2 + 1
123
+ or phasor.shape[1] != signal.shape[0]
124
+ or phasor.shape[2] != signal.shape[2]
125
+ ):
126
+ raise ValueError('invalid shape of phasor or signal')
127
+ if sincos.shape[1] != samples or sincos.shape[2] != 2:
128
+ raise ValueError('invalid shape of sincos')
129
+
130
+ mean = phasor[0]
131
+ real = phasor[1 : 1 + harmonics]
132
+ imag = phasor[1 + harmonics : 1 + harmonics * 2]
133
+
134
+ if num_threads > 1 and signal.shape[0] >= num_threads:
135
+ # parallelize outer dimensions
136
+ with nogil, parallel(num_threads=num_threads):
137
+ for i in prange(signal.shape[0]):
138
+ for h in range(harmonics):
139
+ for j in range(signal.shape[2]):
140
+ dc = 0.0
141
+ re = 0.0
142
+ im = 0.0
143
+ for k in range(samples):
144
+ sample = <double> signal[i, k, j]
145
+ dc = dc + sample
146
+ re = re + sample * sincos[h, k, 0]
147
+ 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
156
+ if h == 0:
157
+ mean[i, j] = <float_t> dc
158
+ real[h, i, j] = <float_t> re
159
+ imag[h, i, j] = <float_t> im
160
+
161
+ elif num_threads > 1 and signal.shape[2] >= num_threads:
162
+ # parallelize inner dimensions
163
+ # TODO: do not use when not built with OpenMP
164
+ with nogil, parallel(num_threads=num_threads):
165
+ for j in prange(signal.shape[2]):
166
+ for h in range(harmonics):
167
+ for i in range(signal.shape[0]):
168
+ dc = 0.0
169
+ re = 0.0
170
+ im = 0.0
171
+ for k in range(samples):
172
+ sample = <double> signal[i, k, j]
173
+ dc = dc + sample
174
+ re = re + sample * sincos[h, k, 0]
175
+ 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
184
+ if h == 0:
185
+ mean[i, j] = <float_t> dc
186
+ real[h, i, j] = <float_t> re
187
+ imag[h, i, j] = <float_t> im
188
+
189
+ else:
190
+ # do not parallelize
191
+ with nogil:
192
+ for h in range(harmonics):
193
+ # TODO: move harmonics to an inner loop?
194
+ for i in range(signal.shape[0]):
195
+ for j in range(signal.shape[2]):
196
+ dc = 0.0
197
+ re = 0.0
198
+ im = 0.0
199
+ for k in range(samples):
200
+ sample = <double> signal[i, k, j]
201
+ dc += sample
202
+ re += sample * sincos[h, k, 0]
203
+ 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
212
+ if h == 0:
213
+ mean[i, j] = <float_t> dc
214
+ real[h, i, j] = <float_t> re
215
+ imag[h, i, j] = <float_t> im
216
+
217
+
218
+ def _phasor_from_lifetime(
219
+ float_t[:, :, ::1] phasor,
220
+ const double[::1] frequency,
221
+ const double[:, ::1] lifetime,
222
+ const double[:, ::1] fraction,
223
+ const double unit_conversion,
224
+ const bint preexponential,
225
+ ):
226
+ """Calculate phasor coordinates from lifetime components.
227
+
228
+ Parameters
229
+ ----------
230
+ phasor : 3D memoryview of float32 or float64
231
+ Writable buffer of three dimensions where calculated phasor
232
+ coordinates are stored:
233
+
234
+ 0. real and imaginary components
235
+ 1. frequencies
236
+ 2. lifetimes or fractions
237
+
238
+ frequency : 2D memoryview of float64
239
+ One-dimensional sequence of laser-pulse or modulation frequencies.
240
+ lifetime : 2D memoryview of float64
241
+ Buffer of two dimensions:
242
+
243
+ 0. lifetimes
244
+ 1. components of lifetimes
245
+
246
+ fraction : 2D memoryview of float64
247
+ Buffer of two dimensions:
248
+
249
+ 0. fractions
250
+ 1. fractions of lifetime components
251
+
252
+ unit_conversion : float
253
+ Product of `frequency` and `lifetime` units' prefix factors.
254
+ 1e-3 for MHz and ns. 1.0 for Hz and s.
255
+ preexponential : bool
256
+ If true, fractions are pre-exponential amplitudes, else fractional
257
+ intensities.
258
+
259
+ """
260
+ cdef:
261
+ ssize_t nfreq = frequency.shape[0] # number frequencies
262
+ ssize_t ncomp = lifetime.shape[1] # number lifetime components
263
+ ssize_t ntau = lifetime.shape[0] # number lifetimes
264
+ ssize_t nfrac = fraction.shape[0] # number fractions
265
+ double twopi = 2.0 * M_PI * unit_conversion
266
+ double freq, tau, frac, sum, re, im, gs
267
+ ssize_t f, t, s
268
+
269
+ if phasor.shape[0] != 2 or phasor.shape[1] != nfreq:
270
+ raise ValueError(
271
+ f'invalid {phasor.shape=!r} != (2, {nfreq}, -1))'
272
+ )
273
+ if fraction.shape[1] != ncomp:
274
+ raise ValueError(f'{lifetime.shape[1]=} != {fraction.shape[1]=}')
275
+
276
+ if nfreq == 1 and ntau == 1 and nfrac == 1 and ncomp == 1:
277
+ # scalar
278
+ tau = lifetime[0, 0] * frequency[0] * twopi # omega_tau
279
+ gs = 1.0 / (1.0 + tau * tau)
280
+ phasor[0, 0, 0] = <float_t> gs
281
+ phasor[1, 0, 0] = <float_t> (gs * tau)
282
+ return
283
+
284
+ if ntau == nfrac:
285
+ # fractions specified for all lifetime components
286
+ if phasor.shape[2] != ntau:
287
+ raise ValueError(f'{phasor.shape[2]=} != {ntau}')
288
+ with nogil:
289
+ for f in range(nfreq):
290
+ freq = frequency[f] * twopi # omega
291
+ for t in range(ntau):
292
+ re = 0.0
293
+ im = 0.0
294
+ sum = 0.0
295
+ if preexponential:
296
+ for s in range(ncomp):
297
+ sum += fraction[t, s] * lifetime[t, s] # Fdc
298
+ else:
299
+ for s in range(ncomp):
300
+ sum += fraction[t, s]
301
+ if fabs(sum) < 1e-15:
302
+ phasor[0, f, t] = <float_t> NAN
303
+ phasor[1, f, t] = <float_t> NAN
304
+ continue
305
+ for s in range(ncomp):
306
+ tau = lifetime[t, s]
307
+ frac = fraction[t, s] / sum
308
+ if preexponential:
309
+ frac *= tau
310
+ tau *= freq # omega_tau
311
+ gs = frac / (1.0 + tau * tau)
312
+ re += gs
313
+ im += gs * tau
314
+ phasor[0, f, t] = <float_t> re
315
+ phasor[1, f, t] = <float_t> im
316
+ return
317
+
318
+ if ntau > 1 and nfrac == 1:
319
+ # varying lifetime components, same fractions
320
+ if phasor.shape[2] != ntau:
321
+ raise ValueError(f'{phasor.shape[2]=} != {ntau}')
322
+ with nogil:
323
+ for f in range(nfreq):
324
+ freq = frequency[f] * twopi # omega
325
+ sum = 0.0
326
+ if not preexponential:
327
+ for s in range(ncomp):
328
+ sum += fraction[0, s]
329
+ for t in range(ntau):
330
+ if preexponential:
331
+ sum = 0.0
332
+ for s in range(ncomp):
333
+ sum += fraction[0, s] * lifetime[t, s] # Fdc
334
+ if fabs(sum) < 1e-15:
335
+ phasor[0, f, t] = <float_t> NAN
336
+ phasor[1, f, t] = <float_t> NAN
337
+ continue
338
+ re = 0.0
339
+ im = 0.0
340
+ for s in range(ncomp):
341
+ tau = lifetime[t, s]
342
+ frac = fraction[0, s] / sum
343
+ if preexponential:
344
+ frac *= tau
345
+ tau *= freq # omega_tau
346
+ gs = frac / (1.0 + tau * tau)
347
+ re += gs
348
+ im += gs * tau
349
+ phasor[0, f, t] = <float_t> re
350
+ phasor[1, f, t] = <float_t> im
351
+ return
352
+
353
+ if ntau == 1 and nfrac > 1:
354
+ # same lifetime components, varying fractions
355
+ if phasor.shape[2] != nfrac:
356
+ raise ValueError(f'{phasor.shape[2]=} != {nfrac}')
357
+ with nogil:
358
+ for f in range(nfreq):
359
+ freq = frequency[f] * twopi # omega
360
+ for t in range(nfrac):
361
+ re = 0.0
362
+ im = 0.0
363
+ sum = 0.0
364
+ if preexponential:
365
+ for s in range(ncomp):
366
+ sum += fraction[t, s] * lifetime[0, s] # Fdc
367
+ else:
368
+ for s in range(ncomp):
369
+ sum += fraction[t, s]
370
+ if fabs(sum) < 1e-15:
371
+ phasor[0, f, t] = <float_t> NAN
372
+ phasor[1, f, t] = <float_t> NAN
373
+ continue
374
+ for s in range(ncomp):
375
+ tau = lifetime[0, s]
376
+ frac = fraction[t, s] / sum
377
+ if preexponential:
378
+ frac *= tau
379
+ tau *= freq # omega_tau
380
+ gs = frac / (1.0 + tau * tau)
381
+ re += gs
382
+ im += gs * tau
383
+ phasor[0, f, t] = <float_t> re
384
+ phasor[1, f, t] = <float_t> im
385
+ return
386
+
387
+ raise ValueError(
388
+ f'{lifetime.shape[0]=} and {fraction.shape[0]=} do not match'
389
+ )
390
+
391
+
392
+ def _gaussian_signal(
393
+ float_t[::1] signal,
394
+ const double mean,
395
+ const double stdev,
396
+ ):
397
+ """Return normal distribution, wrapped around at borders.
398
+
399
+ Parameters
400
+ ----------
401
+ signal : memoryview of float32 or float64
402
+ Writable buffer where calculated signal samples are stored.
403
+ mean : float
404
+ Mean of normal distribution.
405
+ stdev : float
406
+ Standard deviation of normal distribution.
407
+
408
+ """
409
+ cdef:
410
+ ssize_t samples = signal.shape[0]
411
+ ssize_t folds = 1 # TODO: calculate from stddev and samples
412
+ ssize_t i
413
+ double t, c
414
+
415
+ if stdev <= 0.0 or samples < 1:
416
+ return
417
+
418
+ with nogil:
419
+ c = 1.0 / sqrt(2.0 * M_PI) * stdev
420
+
421
+ for i in range(-folds * samples, (folds + 1) * samples):
422
+ t = (<double> i - mean) / stdev
423
+ t *= t
424
+ t = c * exp(-t / 2.0)
425
+ # i %= samples
426
+ i -= samples * <ssize_t> floor(<double> i / samples)
427
+ signal[i] += <float_t> t
428
+
429
+
430
+ ###############################################################################
431
+ # FRET model
432
+
433
+
434
+ @cython.ufunc
435
+ cdef (double, double) _phasor_from_fret_donor(
436
+ double omega,
437
+ double donor_lifetime,
438
+ double fret_efficiency,
439
+ double donor_freting,
440
+ double donor_background,
441
+ double background_real,
442
+ double background_imag,
443
+ ) noexcept nogil:
444
+ """Return phasor coordinates of FRET donor channel.
445
+
446
+ See :py:func:`phasor_from_fret_donor` for parameter definitions.
447
+
448
+ """
449
+ cdef:
450
+ double real, imag
451
+ double quenched_real, quenched_imag # quenched donor
452
+ double f_pure, f_quenched, sum
453
+
454
+ if fret_efficiency < 0.0:
455
+ fret_efficiency = 0.0
456
+ elif fret_efficiency > 1.0:
457
+ fret_efficiency = 1.0
458
+
459
+ if donor_freting < 0.0:
460
+ donor_freting = 0.0
461
+ elif donor_freting > 1.0:
462
+ donor_freting = 1.0
463
+
464
+ if donor_background < 0.0:
465
+ donor_background = 0.0
466
+
467
+ f_pure = 1.0 - donor_freting
468
+ f_quenched = (1.0 - fret_efficiency) * donor_freting
469
+ sum = f_pure + f_quenched + donor_background
470
+ if sum < 1e-9:
471
+ # no signal in donor channel
472
+ return 1.0, 0.0
473
+
474
+ # phasor of pure donor at frequency
475
+ real, imag = phasor_from_lifetime(donor_lifetime, omega)
476
+
477
+ # phasor of quenched donor
478
+ quenched_real, quenched_imag = phasor_from_lifetime(
479
+ donor_lifetime * (1.0 - fret_efficiency), omega
480
+ )
481
+
482
+ # weighted average
483
+ real = (
484
+ real * f_pure
485
+ + quenched_real * f_quenched
486
+ + donor_background * background_real
487
+ ) / sum
488
+
489
+ imag = (
490
+ imag * f_pure
491
+ + quenched_imag * f_quenched
492
+ + background_imag * donor_background
493
+ ) / sum
494
+
495
+ return real, imag
496
+
497
+
498
+ @cython.ufunc
499
+ cdef (double, double) _phasor_from_fret_acceptor(
500
+ double omega,
501
+ double donor_lifetime,
502
+ double acceptor_lifetime,
503
+ double fret_efficiency,
504
+ double donor_freting,
505
+ double donor_bleedthrough,
506
+ double acceptor_bleedthrough,
507
+ double acceptor_background,
508
+ double background_real,
509
+ double background_imag,
510
+ ) noexcept nogil:
511
+ """Return phasor coordinates of FRET acceptor channel.
512
+
513
+ See :py:func:`phasor_from_fret_acceptor` for parameter definitions.
514
+
515
+ """
516
+ cdef:
517
+ double phi, mod
518
+ double donor_real, donor_imag
519
+ double acceptor_real, acceptor_imag
520
+ double quenched_real, quenched_imag # quenched donor
521
+ double sensitized_real, sensitized_imag # sensitized acceptor
522
+ double sum, f_donor, f_acceptor
523
+
524
+ if fret_efficiency < 0.0:
525
+ fret_efficiency = 0.0
526
+ elif fret_efficiency > 1.0:
527
+ fret_efficiency = 1.0
528
+
529
+ if donor_freting < 0.0:
530
+ donor_freting = 0.0
531
+ elif donor_freting > 1.0:
532
+ donor_freting = 1.0
533
+
534
+ if donor_bleedthrough < 0.0:
535
+ donor_bleedthrough = 0.0
536
+ if acceptor_bleedthrough < 0.0:
537
+ acceptor_bleedthrough = 0.0
538
+ if acceptor_background < 0.0:
539
+ acceptor_background = 0.0
540
+
541
+ # phasor of pure donor at frequency
542
+ donor_real, donor_imag = phasor_from_lifetime(donor_lifetime, omega)
543
+
544
+ if fret_efficiency == 0.0:
545
+ quenched_real = donor_real
546
+ quenched_imag = donor_imag
547
+ else:
548
+ # phasor of quenched donor
549
+ quenched_real, quenched_imag = phasor_from_lifetime(
550
+ donor_lifetime * (1.0 - fret_efficiency), omega
551
+ )
552
+
553
+ # phasor of pure and quenched donor
554
+ donor_real, donor_imag = linear_combination(
555
+ 1.0,
556
+ 0.0,
557
+ donor_real,
558
+ donor_imag,
559
+ quenched_real,
560
+ quenched_imag,
561
+ 1.0,
562
+ 1.0 - fret_efficiency,
563
+ 1.0 - donor_freting
564
+ )
565
+
566
+ # phasor of acceptor at frequency
567
+ acceptor_real, acceptor_imag = phasor_from_lifetime(
568
+ acceptor_lifetime, omega
569
+ )
570
+
571
+ # phasor of acceptor sensitized by quenched donor
572
+ # TODO: use rotation formula
573
+ phi = (
574
+ atan2(quenched_imag, quenched_real)
575
+ + atan2(acceptor_imag, acceptor_real)
576
+ )
577
+ mod = (
578
+ hypot(quenched_real, quenched_imag)
579
+ * hypot(acceptor_real, acceptor_imag)
580
+ )
581
+ sensitized_real = mod * cos(phi)
582
+ sensitized_imag = mod * sin(phi)
583
+
584
+ # weighted average
585
+ f_donor = donor_bleedthrough * (1.0 - donor_freting * fret_efficiency)
586
+ f_acceptor = donor_freting * fret_efficiency
587
+ sum = f_donor + f_acceptor + acceptor_bleedthrough + acceptor_background
588
+ if sum < 1e-9:
589
+ # no signal in acceptor channel
590
+ # do not return 0, 0 to avoid discontinuities
591
+ return sensitized_real, sensitized_imag
592
+
593
+ acceptor_real = (
594
+ donor_real * f_donor
595
+ + sensitized_real * f_acceptor
596
+ + acceptor_real * acceptor_bleedthrough
597
+ + background_real * acceptor_background
598
+ ) / sum
599
+
600
+ acceptor_imag = (
601
+ donor_imag * f_donor
602
+ + sensitized_imag * f_acceptor
603
+ + acceptor_imag * acceptor_bleedthrough
604
+ + background_imag * acceptor_background
605
+ ) / sum
606
+
607
+ return acceptor_real, acceptor_imag
608
+
609
+
610
+ cdef inline (double, double) linear_combination(
611
+ const double real,
612
+ const double imag,
613
+ const double real1,
614
+ const double imag1,
615
+ const double real2,
616
+ const double imag2,
617
+ double int1,
618
+ double int2,
619
+ double frac,
620
+ ) noexcept nogil:
621
+ """Return linear combinations of phasor coordinates."""
622
+ int1 *= frac
623
+ int2 *= 1.0 - frac
624
+ frac = int1 + int2
625
+ if fabs(frac) < 1e-15:
626
+ return real, imag
627
+ return (
628
+ (int1 * real1 + int2 * real2) / frac,
629
+ (int1 * imag1 + int2 * imag2) / frac
630
+ )
631
+
632
+
633
+ cdef inline (float_t, float_t) phasor_from_lifetime(
634
+ float_t lifetime,
635
+ float_t omega,
636
+ ) noexcept nogil:
637
+ """Return phasor coordinates from single lifetime component."""
638
+ cdef:
639
+ double t = omega * lifetime
640
+ double mod = 1.0 / sqrt(1.0 + t * t)
641
+ double phi = atan(t)
642
+
643
+ return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
644
+
645
+
646
+ ###############################################################################
647
+ # Phasor conversions
648
+
649
+
650
+ @cython.ufunc
651
+ cdef (float_t, float_t) _phasor_transform(
652
+ float_t real,
653
+ float_t imag,
654
+ float_t angle,
655
+ float_t scale,
656
+ ) noexcept nogil:
657
+ """Return rotated and scaled phasor coordinates."""
658
+ cdef:
659
+ double g, s
660
+
661
+ if isnan(real) or isnan(imag) or isnan(angle) or isnan(scale):
662
+ return <float_t> NAN, <float_t> NAN
663
+
664
+ g = scale * cos(angle)
665
+ s = scale * sin(angle)
666
+
667
+ return <float_t> (real * g - imag * s), <float_t> (real * s + imag * g)
668
+
669
+
670
+ @cython.ufunc
671
+ cdef (float_t, float_t) _phasor_transform_const(
672
+ float_t real,
673
+ float_t imag,
674
+ float_t real2,
675
+ float_t imag2,
676
+ ) noexcept nogil:
677
+ """Return rotated and scaled phasor coordinates."""
678
+ if isnan(real) or isnan(imag) or isnan(real2) or isnan(imag2):
679
+ return <float_t> NAN, <float_t> NAN
680
+
681
+ return real * real2 - imag * imag2, real * imag2 + imag * real2
682
+
683
+
684
+ @cython.ufunc
685
+ cdef (float_t, float_t) _phasor_to_polar(
686
+ float_t real,
687
+ float_t imag,
688
+ ) noexcept nogil:
689
+ """Return polar from phasor coordinates."""
690
+ if isnan(real) or isnan(imag):
691
+ return <float_t> NAN, <float_t> NAN
692
+
693
+ return (
694
+ <float_t> atan2(imag, real),
695
+ <float_t> sqrt(real * real + imag * imag)
696
+ )
697
+
698
+
699
+ @cython.ufunc
700
+ cdef (float_t, float_t) _phasor_from_polar(
701
+ float_t phase,
702
+ float_t modulation,
703
+ ) noexcept nogil:
704
+ """Return phasor from polar coordinates."""
705
+ if isnan(phase) or isnan(modulation):
706
+ return <float_t> NAN, <float_t> NAN
707
+
708
+ return (
709
+ modulation * <float_t> cos(phase),
710
+ modulation * <float_t> sin(phase)
711
+ )
712
+
713
+
714
+ @cython.ufunc
715
+ cdef (float_t, float_t) _phasor_to_apparent_lifetime(
716
+ float_t real,
717
+ float_t imag,
718
+ float_t omega,
719
+ ) noexcept nogil:
720
+ """Return apparent single lifetimes from phasor coordinates."""
721
+ cdef:
722
+ double tauphi = INFINITY
723
+ double taumod = INFINITY
724
+ double t
725
+
726
+ if isnan(real) or isnan(imag):
727
+ return <float_t> NAN, <float_t> NAN
728
+
729
+ t = real * real + imag * imag
730
+ if omega > 0.0 and t > 0.0:
731
+ if fabs(real * omega) > 0.0:
732
+ tauphi = imag / (real * omega)
733
+ if t <= 1.0:
734
+ taumod = sqrt(1.0 / t - 1.0) / omega
735
+ else:
736
+ taumod = 0.0
737
+
738
+ return <float_t> tauphi, <float_t> taumod
739
+
740
+
741
+ @cython.ufunc
742
+ cdef (float_t, float_t) _phasor_from_apparent_lifetime(
743
+ float_t tauphi,
744
+ float_t taumod,
745
+ float_t omega,
746
+ ) noexcept nogil:
747
+ """Return phasor coordinates from apparent single lifetimes."""
748
+ cdef:
749
+ double phi, mod, t
750
+
751
+ if isnan(tauphi) or isnan(taumod):
752
+ return <float_t> NAN, <float_t> NAN
753
+
754
+ t = omega * taumod
755
+ mod = 1.0 / sqrt(1.0 + t * t)
756
+ phi = atan(omega * tauphi)
757
+ return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
758
+
759
+
760
+ @cython.ufunc
761
+ cdef (float_t, float_t) _phasor_from_single_lifetime(
762
+ float_t lifetime,
763
+ float_t omega,
764
+ ) noexcept nogil:
765
+ """Return phasor coordinates from single lifetime component."""
766
+ cdef:
767
+ double phi, mod, t
768
+
769
+ if isnan(lifetime):
770
+ return <float_t> NAN, <float_t> NAN
771
+
772
+ t = omega * lifetime
773
+ phi = atan(t)
774
+ mod = 1.0 / sqrt(1.0 + t * t)
775
+ return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
776
+
777
+
778
+ @cython.ufunc
779
+ cdef (float_t, float_t) _polar_from_single_lifetime(
780
+ float_t lifetime,
781
+ float_t omega,
782
+ ) noexcept nogil:
783
+ """Return polar coordinates from single lifetime component."""
784
+ cdef:
785
+ double t
786
+
787
+ if isnan(lifetime):
788
+ return <float_t> NAN, <float_t> NAN
789
+
790
+ t = omega * lifetime
791
+ return <float_t> atan(t), <float_t> (1.0 / sqrt(1.0 + t * t))
792
+
793
+
794
+ @cython.ufunc
795
+ cdef (float_t, float_t) _polar_to_apparent_lifetime(
796
+ float_t phase,
797
+ float_t modulation,
798
+ float_t omega,
799
+ ) noexcept nogil:
800
+ """Return apparent single lifetimes from polar coordinates."""
801
+ cdef:
802
+ double tauphi = INFINITY
803
+ double taumod = INFINITY
804
+ double t
805
+
806
+ if isnan(phase) or isnan(modulation):
807
+ return <float_t> NAN, <float_t> NAN
808
+
809
+ t = modulation * modulation
810
+ if omega > 0.0 and t > 0.0:
811
+ tauphi = tan(phase) / omega
812
+ if t <= 1.0:
813
+ taumod = sqrt(1.0 / t - 1.0) / omega
814
+ else:
815
+ taumod = 0.0
816
+ return <float_t> tauphi, <float_t> taumod
817
+
818
+
819
+ @cython.ufunc
820
+ cdef (float_t, float_t) _polar_from_apparent_lifetime(
821
+ float_t tauphi,
822
+ float_t taumod,
823
+ float_t omega,
824
+ ) noexcept nogil:
825
+ """Return polar coordinates from apparent single lifetimes."""
826
+ cdef:
827
+ double t
828
+
829
+ if isnan(tauphi) or isnan(taumod):
830
+ return <float_t> NAN, <float_t> NAN
831
+
832
+ t = omega * taumod
833
+ return (
834
+ <float_t> (atan(omega * tauphi)),
835
+ <float_t> (1.0 / sqrt(1.0 + t * t))
836
+ )
837
+
838
+
839
+ @cython.ufunc
840
+ cdef (float_t, float_t) _polar_from_reference(
841
+ float_t measured_phase,
842
+ float_t measured_modulation,
843
+ float_t known_phase,
844
+ float_t known_modulation,
845
+ ) noexcept nogil:
846
+ """Return polar coordinates for calibration from reference coordinates."""
847
+ if (
848
+ isnan(measured_phase)
849
+ or isnan(measured_modulation)
850
+ or isnan(known_phase)
851
+ or isnan(known_modulation)
852
+ ):
853
+ return <float_t> NAN, <float_t> NAN
854
+
855
+ if fabs(measured_modulation) == 0.0:
856
+ # return known_phase - measured_phase, <float_t> INFINITY
857
+ return (
858
+ known_phase - measured_phase,
859
+ <float_t> (NAN if known_modulation == 0.0 else INFINITY)
860
+ )
861
+ return known_phase - measured_phase, known_modulation / measured_modulation
862
+
863
+
864
+ @cython.ufunc
865
+ cdef (float_t, float_t) _polar_from_reference_phasor(
866
+ float_t measured_real,
867
+ float_t measured_imag,
868
+ float_t known_real,
869
+ float_t known_imag,
870
+ ) noexcept nogil:
871
+ """Return polar coordinates for calibration from reference phasor."""
872
+ cdef:
873
+ double measured_phase, measured_modulation
874
+ double known_phase, known_modulation
875
+
876
+ if (
877
+ isnan(measured_real)
878
+ or isnan(measured_imag)
879
+ or isnan(known_real)
880
+ or isnan(known_imag)
881
+ ):
882
+ return <float_t> NAN, <float_t> NAN
883
+
884
+ measured_phase = atan2(measured_imag, measured_real)
885
+ known_phase = atan2(known_imag, known_real)
886
+ measured_modulation = hypot(measured_real, measured_imag)
887
+ known_modulation = hypot(known_real, known_imag)
888
+
889
+ if fabs(measured_modulation) == 0.0:
890
+ # return <float_t> (known_phase - measured_phase), <float_t> INFINITY
891
+ return (
892
+ <float_t> (known_phase - measured_phase),
893
+ <float_t> (NAN if known_modulation == 0.0 else INFINITY)
894
+ )
895
+ return (
896
+ <float_t> (known_phase - measured_phase),
897
+ <float_t> (known_modulation / measured_modulation)
898
+ )
899
+
900
+
901
+ @cython.ufunc
902
+ cdef (float_t, float_t) _phasor_at_harmonic(
903
+ float_t real,
904
+ int harmonic,
905
+ int other_harmonic,
906
+ ) noexcept nogil:
907
+ """Return phasor coordinates on semicircle at other harmonic."""
908
+ if isnan(real):
909
+ return <float_t> NAN, <float_t> NAN
910
+
911
+ if real <= 0.0:
912
+ return 0.0, 0.0
913
+ if real >= 1.0:
914
+ return 1.0, 0.0
915
+
916
+ harmonic *= harmonic
917
+ other_harmonic *= other_harmonic
918
+ real = (
919
+ harmonic * real / (other_harmonic + (harmonic - other_harmonic) * real)
920
+ )
921
+
922
+ return real, <float_t> sqrt(real - real * real)
923
+
924
+
925
+ @cython.ufunc
926
+ cdef (float_t, float_t) _phasor_multiply(
927
+ float_t real1,
928
+ float_t imag1,
929
+ float_t real2,
930
+ float_t imag2,
931
+ ) noexcept nogil:
932
+ """Return multiplication of two phasors."""
933
+ return real1 * real2 - imag1 * imag2, real1 * imag2 + imag1 * real2
934
+
935
+
936
+ @cython.ufunc
937
+ cdef (float_t, float_t) _phasor_divide(
938
+ float_t real1,
939
+ float_t imag1,
940
+ float_t real2,
941
+ float_t imag2,
942
+ ) noexcept nogil:
943
+ """Return division of two phasors."""
944
+ cdef:
945
+ float_t denom = real2 * real2 + imag2 * imag2
946
+
947
+ if isnan(denom) or denom == 0.0:
948
+ return <float_t> NAN, <float_t> NAN
949
+
950
+ return (
951
+ (real1 * real2 + imag1 * imag2) / denom,
952
+ (imag1 * real2 - real1 * imag2) / denom
953
+ )
954
+
955
+
956
+ ###############################################################################
957
+ # Geometry ufuncs
958
+
959
+
960
+ @cython.ufunc
961
+ cdef short _is_inside_range(
962
+ float_t x, # point
963
+ float_t y,
964
+ float_t xmin, # x range
965
+ float_t xmax,
966
+ float_t ymin, # y range
967
+ float_t ymax
968
+ ) noexcept nogil:
969
+ """Return whether point is inside range.
970
+
971
+ Range includes lower but not upper limit.
972
+
973
+ """
974
+ if isnan(x) or isnan(y):
975
+ return False
976
+
977
+ return x >= xmin and x < xmax and y >= ymin and y < ymax
978
+
979
+
980
+ @cython.ufunc
981
+ cdef short _is_inside_rectangle(
982
+ float_t x, # point
983
+ float_t y,
984
+ float_t x0, # segment start
985
+ float_t y0,
986
+ float_t x1, # segment end
987
+ float_t y1,
988
+ float_t r, # half width
989
+ ) noexcept nogil:
990
+ """Return whether point is in rectangle.
991
+
992
+ The rectangle is defined by central line segment and half width.
993
+
994
+ """
995
+ cdef:
996
+ float_t t
997
+
998
+ if r <= 0.0 or isnan(x) or isnan(y):
999
+ return False
1000
+
1001
+ # normalize coordinates
1002
+ # x1 = 0
1003
+ # y1 = 0
1004
+ x0 -= x1
1005
+ y0 -= y1
1006
+ x -= x1
1007
+ y -= y1
1008
+ # square of line length
1009
+ t = x0 * x0 + y0 * y0
1010
+ if t <= 0.0:
1011
+ return x * x + y * y <= r * r
1012
+ # projection of point on line using clamped dot product
1013
+ t = (x * x0 + y * y0) / t
1014
+ if t < 0.0 or t > 1.0:
1015
+ return False
1016
+ # compare square of lengths of projection and radius
1017
+ x -= t * x0
1018
+ y -= t * y0
1019
+ return x * x + y * y <= r * r
1020
+
1021
+
1022
+ @cython.ufunc
1023
+ cdef short _is_inside_polar_rectangle(
1024
+ float_t x, # point
1025
+ float_t y,
1026
+ float_t angle_min, # phase, -pi to pi
1027
+ float_t angle_max,
1028
+ float_t distance_min, # modulation
1029
+ float_t distance_max,
1030
+ ) noexcept nogil:
1031
+ """Return whether point is inside polar rectangle.
1032
+
1033
+ Angles should be in range -pi to pi, else performance is degraded.
1034
+
1035
+ """
1036
+ cdef:
1037
+ double t
1038
+
1039
+ if isnan(x) or isnan(y):
1040
+ return False
1041
+
1042
+ if distance_min > distance_max:
1043
+ distance_min, distance_max = distance_max, distance_min
1044
+ t = hypot(x, y)
1045
+ if t < distance_min or t > distance_max or t == 0.0:
1046
+ return False
1047
+
1048
+ if angle_min < -M_PI or angle_min > M_PI:
1049
+ angle_min = <float_t> atan2(sin(angle_min), cos(angle_min))
1050
+ if angle_max < -M_PI or angle_max > M_PI:
1051
+ angle_max = <float_t> atan2(sin(angle_max), cos(angle_max))
1052
+ if angle_min > angle_max:
1053
+ angle_min, angle_max = angle_max, angle_min
1054
+ t = <float_t> atan2(y, x)
1055
+ if t < angle_min or t > angle_max:
1056
+ return False
1057
+
1058
+ return True
1059
+
1060
+
1061
+ @cython.ufunc
1062
+ cdef short _is_inside_circle(
1063
+ float_t x, # point
1064
+ float_t y,
1065
+ float_t x0, # circle center
1066
+ float_t y0,
1067
+ float_t r, # circle radius
1068
+ ) noexcept nogil:
1069
+ """Return whether point is inside circle."""
1070
+ if r <= 0.0 or isnan(x) or isnan(y):
1071
+ return False
1072
+
1073
+ x -= x0
1074
+ y -= y0
1075
+ return x * x + y * y <= r * r
1076
+
1077
+
1078
+ @cython.ufunc
1079
+ cdef short _is_inside_ellipse(
1080
+ float_t x, # point
1081
+ float_t y,
1082
+ float_t x0, # ellipse center
1083
+ float_t y0,
1084
+ float_t a, # ellipse radii
1085
+ float_t b,
1086
+ float_t phi, # ellipse angle
1087
+ ) noexcept nogil:
1088
+ """Return whether point is inside ellipse.
1089
+
1090
+ Same as _is_inside_circle if a == b.
1091
+ Consider using _is_inside_ellipse_ instead, which should be faster
1092
+ for arrays.
1093
+
1094
+ """
1095
+ cdef:
1096
+ float_t sina, cosa
1097
+
1098
+ if a <= 0.0 or b <= 0.0 or isnan(x) or isnan(y):
1099
+ return False
1100
+
1101
+ x -= x0
1102
+ y -= y0
1103
+ if a == b:
1104
+ # circle
1105
+ return x * x + y * y <= a * a
1106
+ sina = <float_t> sin(phi)
1107
+ cosa = <float_t> cos(phi)
1108
+ x0 = (cosa * x + sina * y) / a
1109
+ y0 = (sina * x - cosa * y) / b
1110
+ return x0 * x0 + y0 * y0 <= 1.0
1111
+
1112
+
1113
+ @cython.ufunc
1114
+ cdef short _is_inside_ellipse_(
1115
+ float_t x, # point
1116
+ float_t y,
1117
+ float_t x0, # ellipse center
1118
+ float_t y0,
1119
+ float_t a, # ellipse radii
1120
+ float_t b,
1121
+ float_t sina, # sin/cos of ellipse angle
1122
+ float_t cosa,
1123
+ ) noexcept nogil:
1124
+ """Return whether point is inside ellipse.
1125
+
1126
+ Use pre-calculated sin(angle) and cos(angle).
1127
+
1128
+ """
1129
+ if a <= 0.0 or b <= 0.0 or isnan(x) or isnan(y):
1130
+ return False
1131
+
1132
+ x -= x0
1133
+ y -= y0
1134
+ if a == b:
1135
+ # circle
1136
+ return x * x + y * y <= a * a
1137
+ x0 = (cosa * x + sina * y) / a
1138
+ y0 = (sina * x - cosa * y) / b
1139
+ return x0 * x0 + y0 * y0 <= 1.0
1140
+
1141
+
1142
+ @cython.ufunc
1143
+ cdef short _is_inside_stadium(
1144
+ float_t x, # point
1145
+ float_t y,
1146
+ float_t x0, # line start
1147
+ float_t y0,
1148
+ float_t x1, # line end
1149
+ float_t y1,
1150
+ float_t r, # radius
1151
+ ) noexcept nogil:
1152
+ """Return whether point is inside stadium.
1153
+
1154
+ A stadium shape is a thick line with rounded ends.
1155
+ Same as _is_near_segment.
1156
+
1157
+ """
1158
+ cdef:
1159
+ float_t t
1160
+
1161
+ if r <= 0.0 or isnan(x) or isnan(y):
1162
+ return False
1163
+
1164
+ # normalize coordinates
1165
+ # x1 = 0
1166
+ # y1 = 0
1167
+ x0 -= x1
1168
+ y0 -= y1
1169
+ x -= x1
1170
+ y -= y1
1171
+ # square of line length
1172
+ t = x0 * x0 + y0 * y0
1173
+ if t <= 0.0:
1174
+ return x * x + y * y <= r * r
1175
+ # projection of point on line using clamped dot product
1176
+ t = (x * x0 + y * y0) / t
1177
+ t = <float_t> max(0.0, min(1.0, t))
1178
+ # compare square of lengths of projection and radius
1179
+ x -= t * x0
1180
+ y -= t * y0
1181
+ return x * x + y * y <= r * r
1182
+
1183
+
1184
+ # function alias
1185
+ _is_near_segment = _is_inside_stadium
1186
+
1187
+
1188
+ @cython.ufunc
1189
+ cdef short _is_near_line(
1190
+ float_t x, # point
1191
+ float_t y,
1192
+ float_t x0, # line start
1193
+ float_t y0,
1194
+ float_t x1, # line end
1195
+ float_t y1,
1196
+ float_t r, # distance
1197
+ ) noexcept nogil:
1198
+ """Return whether point is close to line."""
1199
+ cdef:
1200
+ float_t t
1201
+
1202
+ if r <= 0.0 or isnan(x) or isnan(y):
1203
+ return False
1204
+
1205
+ # normalize coordinates
1206
+ # x1 = 0
1207
+ # y1 = 0
1208
+ x0 -= x1
1209
+ y0 -= y1
1210
+ x -= x1
1211
+ y -= y1
1212
+ # square of line length
1213
+ t = x0 * x0 + y0 * y0
1214
+ if t <= 0.0:
1215
+ return x * x + y * y <= r * r
1216
+ # projection of point on line using clamped dot product
1217
+ t = (x * x0 + y * y0) / t
1218
+ # compare square of lengths of projection and radius
1219
+ x -= t * x0
1220
+ y -= t * y0
1221
+ return x * x + y * y <= r * r
1222
+
1223
+
1224
+ @cython.ufunc
1225
+ cdef (float_t, float_t) _point_on_segment(
1226
+ float_t x, # point
1227
+ float_t y,
1228
+ float_t x0, # segment start
1229
+ float_t y0,
1230
+ float_t x1, # segment end
1231
+ float_t y1,
1232
+ ) noexcept nogil:
1233
+ """Return point projected onto line segment."""
1234
+ cdef:
1235
+ float_t t
1236
+
1237
+ if isnan(x) or isnan(y):
1238
+ return <float_t> NAN, <float_t> NAN
1239
+
1240
+ # normalize coordinates
1241
+ # x1 = 0
1242
+ # y1 = 0
1243
+ x0 -= x1
1244
+ y0 -= y1
1245
+ x -= x1
1246
+ y -= y1
1247
+ # square of line length
1248
+ t = x0 * x0 + y0 * y0
1249
+ if t <= 0.0:
1250
+ return x0, y0
1251
+ # projection of point on line
1252
+ t = (x * x0 + y * y0) / t
1253
+ # clamp to line segment
1254
+ if t < 0.0:
1255
+ t = 0.0
1256
+ elif t > 1.0:
1257
+ t = 1.0
1258
+ x1 += t * x0
1259
+ y1 += t * y0
1260
+ return x1, y1
1261
+
1262
+
1263
+ @cython.ufunc
1264
+ cdef (float_t, float_t) _point_on_line(
1265
+ float_t x, # point
1266
+ float_t y,
1267
+ float_t x0, # line start
1268
+ float_t y0,
1269
+ float_t x1, # line end
1270
+ float_t y1,
1271
+ ) noexcept nogil:
1272
+ """Return point projected onto line."""
1273
+ cdef:
1274
+ float_t t
1275
+
1276
+ if isnan(x) or isnan(y):
1277
+ return <float_t> NAN, <float_t> NAN
1278
+
1279
+ # normalize coordinates
1280
+ # x1 = 0
1281
+ # y1 = 0
1282
+ x0 -= x1
1283
+ y0 -= y1
1284
+ x -= x1
1285
+ y -= y1
1286
+ # square of line length
1287
+ t = x0 * x0 + y0 * y0
1288
+ if t <= 0.0:
1289
+ return x0, y0
1290
+ # projection of point on line
1291
+ t = (x * x0 + y * y0) / t
1292
+ x1 += t * x0
1293
+ y1 += t * y0
1294
+ return x1, y1
1295
+
1296
+
1297
+ @cython.ufunc
1298
+ cdef float_t _fraction_on_segment(
1299
+ float_t x, # point
1300
+ float_t y,
1301
+ float_t x0, # segment start
1302
+ float_t y0,
1303
+ float_t x1, # segment end
1304
+ float_t y1,
1305
+ ) noexcept nogil:
1306
+ """Return normalized fraction of point projected onto line segment."""
1307
+ cdef:
1308
+ float_t t
1309
+
1310
+ if isnan(x) or isnan(y):
1311
+ return <float_t> NAN
1312
+
1313
+ # normalize coordinates
1314
+ x -= x1
1315
+ y -= y1
1316
+ x0 -= x1
1317
+ y0 -= y1
1318
+ # x1 = 0
1319
+ # y1 = 0
1320
+ # square of line length
1321
+ t = x0 * x0 + y0 * y0
1322
+ if t <= 0.0:
1323
+ # not a line segment
1324
+ return 0.0
1325
+ # projection of point on line
1326
+ t = (x * x0 + y * y0) / t
1327
+ # clamp to line segment
1328
+ if t < 0.0:
1329
+ t = 0.0
1330
+ elif t > 1.0:
1331
+ t = 1.0
1332
+ return t
1333
+
1334
+
1335
+ @cython.ufunc
1336
+ cdef float_t _fraction_on_line(
1337
+ float_t x, # point
1338
+ float_t y,
1339
+ float_t x0, # line start
1340
+ float_t y0,
1341
+ float_t x1, # line end
1342
+ float_t y1,
1343
+ ) noexcept nogil:
1344
+ """Return normalized fraction of point projected onto line."""
1345
+ cdef:
1346
+ float_t t
1347
+
1348
+ if isnan(x) or isnan(y):
1349
+ return <float_t> NAN
1350
+
1351
+ # normalize coordinates
1352
+ x -= x1
1353
+ y -= y1
1354
+ x0 -= x1
1355
+ y0 -= y1
1356
+ # x1 = 0
1357
+ # y1 = 0
1358
+ # square of line length
1359
+ t = x0 * x0 + y0 * y0
1360
+ if t <= 0.0:
1361
+ # not a line segment
1362
+ return 1.0
1363
+ # projection of point on line
1364
+ t = (x * x0 + y * y0) / t
1365
+ return t
1366
+
1367
+
1368
+ @cython.ufunc
1369
+ cdef float_t _distance_from_point(
1370
+ float_t x, # point
1371
+ float_t y,
1372
+ float_t x0, # other point
1373
+ float_t y0,
1374
+ ) noexcept nogil:
1375
+ """Return distance from point."""
1376
+ if isnan(x) or isnan(y): # or isnan(x0) or isnan(y0)
1377
+ return <float_t> NAN
1378
+
1379
+ return <float_t> hypot(x - x0, y - y0)
1380
+
1381
+
1382
+ @cython.ufunc
1383
+ cdef float_t _distance_from_segment(
1384
+ float_t x, # point
1385
+ float_t y,
1386
+ float_t x0, # segment start
1387
+ float_t y0,
1388
+ float_t x1, # segment end
1389
+ float_t y1,
1390
+ ) noexcept nogil:
1391
+ """Return distance from segment."""
1392
+ cdef:
1393
+ float_t t
1394
+
1395
+ if isnan(x) or isnan(y):
1396
+ return <float_t> NAN
1397
+
1398
+ # normalize coordinates
1399
+ # x1 = 0
1400
+ # y1 = 0
1401
+ x0 -= x1
1402
+ y0 -= y1
1403
+ x -= x1
1404
+ y -= y1
1405
+ # square of line length
1406
+ t = x0 * x0 + y0 * y0
1407
+ if t <= 0.0:
1408
+ return <float_t> hypot(x, y)
1409
+ # projection of point on line using dot product
1410
+ t = (x * x0 + y * y0) / t
1411
+ if t > 1.0:
1412
+ x -= x0
1413
+ y -= y0
1414
+ elif t > 0.0:
1415
+ x -= t * x0
1416
+ y -= t * y0
1417
+ return <float_t> hypot(x, y)
1418
+
1419
+
1420
+ @cython.ufunc
1421
+ cdef float_t _distance_from_line(
1422
+ float_t x, # point
1423
+ float_t y,
1424
+ float_t x0, # line start
1425
+ float_t y0,
1426
+ float_t x1, # line end
1427
+ float_t y1,
1428
+ ) noexcept nogil:
1429
+ """Return distance from line."""
1430
+ cdef:
1431
+ float_t t
1432
+
1433
+ if isnan(x) or isnan(y):
1434
+ return <float_t> NAN
1435
+
1436
+ # normalize coordinates
1437
+ # x1 = 0
1438
+ # y1 = 0
1439
+ x0 -= x1
1440
+ y0 -= y1
1441
+ x -= x1
1442
+ y -= y1
1443
+ # square of line length
1444
+ t = x0 * x0 + y0 * y0
1445
+ if t <= 0.0:
1446
+ return <float_t> hypot(x, y)
1447
+ # projection of point on line using dot product
1448
+ t = (x * x0 + y * y0) / t
1449
+ x -= t * x0
1450
+ y -= t * y0
1451
+ return <float_t> hypot(x, y)
1452
+
1453
+
1454
+ @cython.ufunc
1455
+ cdef (double, double, double) _segment_direction_and_length(
1456
+ float_t x0, # segment start
1457
+ float_t y0,
1458
+ float_t x1, # segment end
1459
+ float_t y1,
1460
+ ) noexcept nogil:
1461
+ """Return direction and length of line segment."""
1462
+ cdef:
1463
+ float_t length
1464
+
1465
+ if isnan(x0) or isnan(y0) or isnan(x1) or isnan(y1):
1466
+ return NAN, NAN, 0.0
1467
+
1468
+ x1 -= x0
1469
+ y1 -= y0
1470
+ length = <float_t> hypot(x1, y1)
1471
+ if length <= 0.0:
1472
+ return NAN, NAN, 0.0
1473
+ x1 /= length
1474
+ y1 /= length
1475
+ return x1, y1, length
1476
+
1477
+
1478
+ @cython.ufunc
1479
+ cdef (double, double, double, double) _intersection_circle_circle(
1480
+ float_t x0, # circle 0
1481
+ float_t y0,
1482
+ float_t r0,
1483
+ float_t x1, # circle 1
1484
+ float_t y1,
1485
+ float_t r1,
1486
+ ) noexcept nogil:
1487
+ """Return coordinates of intersections of two circles."""
1488
+ cdef:
1489
+ double dx, dy, dr, ll, dd, hd, ld
1490
+
1491
+ if (
1492
+ isnan(x0)
1493
+ or isnan(y0)
1494
+ or isnan(r0)
1495
+ or isnan(x1)
1496
+ or isnan(y1)
1497
+ or isnan(r1)
1498
+ or r0 == 0.0
1499
+ or r1 == 0.0
1500
+ ):
1501
+ return NAN, NAN, NAN, NAN
1502
+
1503
+ dx = x1 - x0
1504
+ dy = y1 - y0
1505
+ dr = hypot(dx, dy)
1506
+ if dr <= 0.0:
1507
+ # circle positions identical
1508
+ return NAN, NAN, NAN, NAN
1509
+ ll = (r0 * r0 - r1 * r1 + dr * dr) / (dr + dr)
1510
+ dd = r0 * r0 - ll * ll
1511
+ if dd < 0.0 or dr <= 0.0:
1512
+ # circles not intersecting
1513
+ return NAN, NAN, NAN, NAN
1514
+ hd = sqrt(dd) / dr
1515
+ ld = ll / dr
1516
+ return (
1517
+ ld * dx + hd * dy + x0,
1518
+ ld * dy - hd * dx + y0,
1519
+ ld * dx - hd * dy + x0,
1520
+ ld * dy + hd * dx + y0,
1521
+ )
1522
+
1523
+
1524
+ @cython.ufunc
1525
+ cdef (double, double, double, double) _intersection_circle_line(
1526
+ float_t x, # circle
1527
+ float_t y,
1528
+ float_t r,
1529
+ float_t x0, # line start
1530
+ float_t y0,
1531
+ float_t x1, # line end
1532
+ float_t y1,
1533
+ ) noexcept nogil:
1534
+ """Return coordinates of intersections of circle and line."""
1535
+ cdef:
1536
+ double dx, dy, dr, dd, rdd
1537
+
1538
+ if (
1539
+ isnan(r)
1540
+ or isnan(x)
1541
+ or isnan(y)
1542
+ or isnan(x0)
1543
+ or isnan(y0)
1544
+ or isnan(x1)
1545
+ or isnan(y1)
1546
+ or r == 0.0
1547
+ ):
1548
+ return NAN, NAN, NAN, NAN
1549
+
1550
+ dx = x1 - x0
1551
+ dy = y1 - y0
1552
+ dr = dx * dx + dy * dy
1553
+ dd = (x0 - x) * (y1 - y) - (x1 - x) * (y0 - y)
1554
+ rdd = r * r * dr - dd * dd # discriminant
1555
+ if rdd < 0.0 or dr <= 0.0:
1556
+ # no intersection
1557
+ return NAN, NAN, NAN, NAN
1558
+ rdd = sqrt(rdd)
1559
+ return (
1560
+ x + (dd * dy + copysign(1.0, dy) * dx * rdd) / dr,
1561
+ y + (-dd * dx + fabs(dy) * rdd) / dr,
1562
+ x + (dd * dy - copysign(1.0, dy) * dx * rdd) / dr,
1563
+ y + (-dd * dx - fabs(dy) * rdd) / dr,
1564
+ )
1565
+
1566
+
1567
+ ###############################################################################
1568
+ # Blend ufuncs
1569
+
1570
+
1571
+ @cython.ufunc
1572
+ cdef float_t _blend_normal(
1573
+ float_t a, # base layer
1574
+ float_t b, # blend layer
1575
+ ) noexcept nogil:
1576
+ """Return blended layers using `normal` mode."""
1577
+ if isnan(b):
1578
+ return a
1579
+ return b
1580
+
1581
+
1582
+ @cython.ufunc
1583
+ cdef float_t _blend_multiply(
1584
+ float_t a, # base layer
1585
+ float_t b, # blend layer
1586
+ ) noexcept nogil:
1587
+ """Return blended layers using `multiply` mode."""
1588
+ if isnan(b):
1589
+ return a
1590
+ return a * b
1591
+
1592
+
1593
+ @cython.ufunc
1594
+ cdef float_t _blend_screen(
1595
+ float_t a, # base layer
1596
+ float_t b, # blend layer
1597
+ ) noexcept nogil:
1598
+ """Return blended layers using `screen` mode."""
1599
+ if isnan(b):
1600
+ return a
1601
+ return <float_t> (1.0 - (1.0 - a) * (1.0 - b))
1602
+
1603
+
1604
+ @cython.ufunc
1605
+ cdef float_t _blend_overlay(
1606
+ float_t a, # base layer
1607
+ float_t b, # blend layer
1608
+ ) noexcept nogil:
1609
+ """Return blended layers using `overlay` mode."""
1610
+ if isnan(b) or isnan(a):
1611
+ return a
1612
+ if a < 0.5:
1613
+ return <float_t> (2.0 * a * b)
1614
+ return <float_t> (1.0 - 2.0 * (1.0 - a) * (1.0 - b))
1615
+
1616
+
1617
+ @cython.ufunc
1618
+ cdef float_t _blend_darken(
1619
+ float_t a, # base layer
1620
+ float_t b, # blend layer
1621
+ ) noexcept nogil:
1622
+ """Return blended layers using `darken` mode."""
1623
+ if isnan(b) or isnan(a):
1624
+ return a
1625
+ return <float_t> min(a, b)
1626
+
1627
+
1628
+ @cython.ufunc
1629
+ cdef float_t _blend_lighten(
1630
+ float_t a, # base layer
1631
+ float_t b, # blend layer
1632
+ ) noexcept nogil:
1633
+ """Return blended layers using `lighten` mode."""
1634
+ if isnan(b) or isnan(a):
1635
+ return a
1636
+ return <float_t> max(a, b)
1637
+
1638
+
1639
+ ###############################################################################
1640
+ # Threshold ufuncs
1641
+
1642
+
1643
+ @cython.ufunc
1644
+ cdef (double, double, double) _phasor_threshold_open(
1645
+ float_t mean,
1646
+ float_t real,
1647
+ float_t imag,
1648
+ float_t mean_min,
1649
+ float_t mean_max,
1650
+ float_t real_min,
1651
+ float_t real_max,
1652
+ float_t imag_min,
1653
+ float_t imag_max,
1654
+ float_t phase_min,
1655
+ float_t phase_max,
1656
+ float_t modulation_min,
1657
+ float_t modulation_max,
1658
+ ) noexcept nogil:
1659
+ """Return thresholded values by open intervals."""
1660
+ cdef:
1661
+ double phi = NAN
1662
+ double mod = NAN
1663
+
1664
+ if isnan(mean) or isnan(real) or isnan(imag):
1665
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1666
+
1667
+ if not isnan(mean_min) and mean <= mean_min:
1668
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1669
+ if not isnan(mean_max) and mean >= mean_max:
1670
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1671
+
1672
+ if not isnan(real_min) and real <= real_min:
1673
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1674
+ if not isnan(real_max) and real >= real_max:
1675
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1676
+
1677
+ if not isnan(imag_min) and imag <= imag_min:
1678
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1679
+ if not isnan(imag_max) and imag >= imag_max:
1680
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1681
+
1682
+ if not isnan(modulation_min):
1683
+ mod = real * real + imag * imag
1684
+ if mod <= modulation_min * modulation_min:
1685
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1686
+ if not isnan(modulation_max):
1687
+ if isnan(mod):
1688
+ mod = real * real + imag * imag
1689
+ if mod >= modulation_max * modulation_max:
1690
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1691
+
1692
+ if not isnan(phase_min):
1693
+ phi = atan2(imag, real)
1694
+ if phi <= phase_min:
1695
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1696
+ if not isnan(phase_max):
1697
+ if isnan(phi):
1698
+ phi = atan2(imag, real)
1699
+ if phi >= phase_max:
1700
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1701
+
1702
+ return mean, real, imag
1703
+
1704
+
1705
+ @cython.ufunc
1706
+ cdef (double, double, double) _phasor_threshold_closed(
1707
+ float_t mean,
1708
+ float_t real,
1709
+ float_t imag,
1710
+ float_t mean_min,
1711
+ float_t mean_max,
1712
+ float_t real_min,
1713
+ float_t real_max,
1714
+ float_t imag_min,
1715
+ float_t imag_max,
1716
+ float_t phase_min,
1717
+ float_t phase_max,
1718
+ float_t modulation_min,
1719
+ float_t modulation_max,
1720
+ ) noexcept nogil:
1721
+ """Return thresholded values by closed intervals."""
1722
+ cdef:
1723
+ double phi = NAN
1724
+ double mod = NAN
1725
+
1726
+ if isnan(mean) or isnan(real) or isnan(imag):
1727
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1728
+
1729
+ if not isnan(mean_min) and mean < mean_min:
1730
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1731
+ if not isnan(mean_max) and mean > mean_max:
1732
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1733
+
1734
+ if not isnan(real_min) and real < real_min:
1735
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1736
+ if not isnan(real_max) and real > real_max:
1737
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1738
+
1739
+ if not isnan(imag_min) and imag < imag_min:
1740
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1741
+ if not isnan(imag_max) and imag > imag_max:
1742
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1743
+
1744
+ if not isnan(modulation_min):
1745
+ mod = real * real + imag * imag
1746
+ if mod < modulation_min * modulation_min:
1747
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1748
+ if not isnan(modulation_max):
1749
+ if isnan(mod):
1750
+ mod = real * real + imag * imag
1751
+ if mod > modulation_max * modulation_max:
1752
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1753
+
1754
+ if not isnan(phase_min):
1755
+ phi = atan2(imag, real)
1756
+ if phi < phase_min:
1757
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1758
+ if not isnan(phase_max):
1759
+ if isnan(phi):
1760
+ phi = atan2(imag, real)
1761
+ if phi > phase_max:
1762
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1763
+
1764
+ return mean, real, imag
1765
+
1766
+
1767
+ @cython.ufunc
1768
+ cdef (double, double, double) _phasor_threshold_mean_open(
1769
+ float_t mean,
1770
+ float_t real,
1771
+ float_t imag,
1772
+ float_t mean_min,
1773
+ float_t mean_max,
1774
+ ) noexcept nogil:
1775
+ """Return thresholded values only by open interval of `mean`."""
1776
+ if isnan(mean) or isnan(real) or isnan(imag):
1777
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1778
+
1779
+ if not isnan(mean_min) and mean <= mean_min:
1780
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1781
+ if not isnan(mean_max) and mean >= mean_max:
1782
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1783
+
1784
+ return mean, real, imag
1785
+
1786
+
1787
+ @cython.ufunc
1788
+ cdef (double, double, double) _phasor_threshold_mean_closed(
1789
+ float_t mean,
1790
+ float_t real,
1791
+ float_t imag,
1792
+ float_t mean_min,
1793
+ float_t mean_max,
1794
+ ) noexcept nogil:
1795
+ """Return thresholded values only by closed interval of `mean`."""
1796
+ if isnan(mean) or isnan(real) or isnan(imag):
1797
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1798
+
1799
+ if not isnan(mean_min) and mean < mean_min:
1800
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1801
+ if not isnan(mean_max) and mean > mean_max:
1802
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1803
+
1804
+ return mean, real, imag
1805
+
1806
+
1807
+ @cython.ufunc
1808
+ cdef (double, double, double) _phasor_threshold_nan(
1809
+ float_t mean,
1810
+ float_t real,
1811
+ float_t imag,
1812
+ ) noexcept nogil:
1813
+ """Return the input values if any of them is not NaN."""
1814
+ if isnan(mean) or isnan(real) or isnan(imag):
1815
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1816
+
1817
+ return mean, real, imag
1818
+
1819
+
1820
+ ###############################################################################
1821
+ # Unary ufuncs
1822
+
1823
+
1824
+ @cython.ufunc
1825
+ cdef float_t _anscombe(
1826
+ float_t x,
1827
+ ) noexcept nogil:
1828
+ """Return anscombe variance stabilizing transformation."""
1829
+ if isnan(x):
1830
+ return <float_t> NAN
1831
+
1832
+ return <float_t> (2.0 * sqrt(<double> x + 0.375))
1833
+
1834
+
1835
+ @cython.ufunc
1836
+ cdef float_t _anscombe_inverse(
1837
+ float_t x,
1838
+ ) noexcept nogil:
1839
+ """Return inverse anscombe transformation."""
1840
+ if isnan(x):
1841
+ return <float_t> NAN
1842
+
1843
+ return <float_t> (x * x / 4.0 - 0.375) # 3/8
1844
+
1845
+
1846
+ @cython.ufunc
1847
+ cdef float_t _anscombe_inverse_approx(
1848
+ float_t x,
1849
+ ) noexcept nogil:
1850
+ """Return inverse anscombe transformation.
1851
+
1852
+ Using approximation of exact unbiased inverse.
1853
+
1854
+ """
1855
+ if isnan(x):
1856
+ return <float_t> NAN
1857
+
1858
+ return <float_t> (
1859
+ 0.25 * x * x # 1/4
1860
+ + 0.30618621784789724 / x # 1/4 * sqrt(3/2)
1861
+ - 1.375 / (x * x) # 11/8
1862
+ + 0.7654655446197431 / (x * x * x) # 5/8 * sqrt(3/2)
1863
+ - 0.125 # 1/8
1864
+ )
1865
+
1866
+
1867
+ ###############################################################################
1868
+ # Denoising in spectral space
1869
+
1870
+
1871
+ def _phasor_from_signal_vector(
1872
+ float_t[:, ::1] phasor,
1873
+ const signal_t[:, ::1] signal,
1874
+ const double[:, :, ::1] sincos,
1875
+ const int num_threads
1876
+ ):
1877
+ """Calculate phasor coordinate vectors from signal along last axis.
1878
+
1879
+ Parameters
1880
+ ----------
1881
+ phasor : 2D memoryview of float32 or float64
1882
+ Writable buffer of two dimensions where calculated phasor
1883
+ vectors are stored:
1884
+
1885
+ 0. other dimensions flat
1886
+ 1. real and imaginary components
1887
+
1888
+ signal : 2D memoryview of float32 or float64
1889
+ Buffer of two dimensions containing signal:
1890
+
1891
+ 0. other dimensions flat
1892
+ 1. dimension over which to compute FFT, number samples
1893
+
1894
+ sincos : 3D memoryview of float64
1895
+ Buffer of three dimensions containing sine and cosine terms to be
1896
+ multiplied with signal:
1897
+
1898
+ 0. number harmonics
1899
+ 1. number samples
1900
+ 2. cos and sin
1901
+
1902
+ num_threads : int
1903
+ Number of OpenMP threads to use for parallelization.
1904
+
1905
+ Notes
1906
+ -----
1907
+ This implementation requires contiguous input arrays.
1908
+
1909
+ """
1910
+ cdef:
1911
+ ssize_t size = signal.shape[0]
1912
+ ssize_t samples = signal.shape[1]
1913
+ ssize_t harmonics = sincos.shape[0]
1914
+ ssize_t i, j, k, h
1915
+ double dc, re, im, sample
1916
+
1917
+ if (
1918
+ samples < 2
1919
+ or harmonics > samples // 2
1920
+ or phasor.shape[0] != size
1921
+ or phasor.shape[1] != harmonics * 2
1922
+ ):
1923
+ raise ValueError('invalid shape of phasor or signal')
1924
+ if sincos.shape[1] != samples or sincos.shape[2] != 2:
1925
+ raise ValueError('invalid shape of sincos')
1926
+
1927
+ with nogil, parallel(num_threads=num_threads):
1928
+ for i in prange(signal.shape[0]):
1929
+ j = 0
1930
+ for h in range(harmonics):
1931
+ dc = 0.0
1932
+ re = 0.0
1933
+ im = 0.0
1934
+ for k in range(samples):
1935
+ sample = <double> signal[i, k]
1936
+ dc = dc + sample
1937
+ re = re + sample * sincos[h, k, 0]
1938
+ im = im + sample * sincos[h, k, 1]
1939
+ if dc != 0.0:
1940
+ re = re / dc
1941
+ im = im / dc
1942
+ else:
1943
+ re = NAN if re == 0.0 else re * INFINITY
1944
+ im = NAN if im == 0.0 else im * INFINITY
1945
+ phasor[i, j] = <float_t> re
1946
+ j = j + 1
1947
+ phasor[i, j] = <float_t> im
1948
+ j = j + 1
1949
+
1950
+
1951
+ def _signal_denoise_vector(
1952
+ float_t[:, ::1] denoised,
1953
+ float_t[::1] integrated,
1954
+ const signal_t[:, ::1] signal,
1955
+ const float_t[:, ::1] spectral_vector,
1956
+ const double sigma,
1957
+ const double vmin,
1958
+ const int num_threads
1959
+ ):
1960
+ """Calculate denoised signal from spectral_vector."""
1961
+ cdef:
1962
+ ssize_t size = signal.shape[0]
1963
+ ssize_t samples = signal.shape[1]
1964
+ ssize_t dims = spectral_vector.shape[1]
1965
+ ssize_t i, j, m
1966
+ float_t n
1967
+ double weight, sum, t
1968
+ double sigma2 = -1.0 / (2.0 * sigma * sigma)
1969
+ double threshold = 9.0 * sigma * sigma
1970
+
1971
+ if denoised.shape[0] != size or denoised.shape[1] != samples:
1972
+ raise ValueError('signal and denoised shape mismatch')
1973
+ if integrated.shape[0] != size:
1974
+ raise ValueError('integrated.shape[0] != signal.shape[0]')
1975
+ if spectral_vector.shape[0] != size:
1976
+ raise ValueError('spectral_vector.shape[0] != signal.shape[0]')
1977
+
1978
+ with nogil, parallel(num_threads=num_threads):
1979
+
1980
+ # integrate channel intensities for each pixel
1981
+ # and filter low intensities
1982
+ for i in prange(size):
1983
+ sum = 0.0
1984
+ for m in range(samples):
1985
+ sum = sum + <double> signal[i, m]
1986
+ if sum < vmin:
1987
+ sum = NAN
1988
+ integrated[i] = <float_t> sum
1989
+
1990
+ # loop over all pixels
1991
+ for i in prange(size):
1992
+
1993
+ n = integrated[i]
1994
+ if not n > 0.0:
1995
+ # n is NaN or zero; cannot denoise; return original signal
1996
+ continue
1997
+
1998
+ for m in range(samples):
1999
+ denoised[i, m] /= n # weight = 1.0
2000
+
2001
+ # loop over other pixels
2002
+ for j in range(size):
2003
+ if i == j:
2004
+ # weight = 1.0 already accounted for
2005
+ continue
2006
+
2007
+ n = integrated[j]
2008
+ if not n > 0.0:
2009
+ # n is NaN or zero
2010
+ continue
2011
+
2012
+ # calculate weight from Euclidean distance of
2013
+ # pixels i and j in spectral vector space
2014
+ sum = 0.0
2015
+ for m in range(dims):
2016
+ t = spectral_vector[i, m] - spectral_vector[j, m]
2017
+ sum = sum + t * t
2018
+ if sum > threshold:
2019
+ sum = -1.0
2020
+ break
2021
+ if sum >= 0.0:
2022
+ weight = exp(sum * sigma2) / n
2023
+ else:
2024
+ # sum is NaN or greater than threshold
2025
+ continue
2026
+
2027
+ # add weighted signal[j] to denoised[i]
2028
+ for m in range(samples):
2029
+ denoised[i, m] += <float_t> (weight * signal[j, m])
2030
+
2031
+ # re-normalize to original intensity
2032
+ # sum cannot be zero because integrated == 0 was filtered
2033
+ sum = 0.0
2034
+ for m in range(samples):
2035
+ sum = sum + denoised[i, m]
2036
+ n = <float_t> (<double> integrated[i] / sum)
2037
+ for m in range(samples):
2038
+ denoised[i, m] *= n
2039
+
2040
+
2041
+ ###############################################################################
2042
+ # Filtering functions
2043
+
2044
+
2045
+ cdef float_t _median(float_t *values, const ssize_t size) noexcept nogil:
2046
+ """Return median of array values using Quickselect algorithm."""
2047
+ cdef:
2048
+ ssize_t i, pivot_index, pivot_index_new
2049
+ ssize_t left = 0
2050
+ ssize_t right = size - 1
2051
+ ssize_t middle = size // 2
2052
+ float_t pivot_value, temp
2053
+
2054
+ if size % 2 == 0:
2055
+ middle -= 1 # Quickselect sorts on right
2056
+
2057
+ while left <= right:
2058
+ pivot_index = left + (right - left) // 2
2059
+ pivot_value = values[pivot_index]
2060
+ temp = values[pivot_index]
2061
+ values[pivot_index] = values[right]
2062
+ values[right] = temp
2063
+ pivot_index_new = left
2064
+ for i in range(left, right):
2065
+ if values[i] < pivot_value:
2066
+ temp = values[i]
2067
+ values[i] = values[pivot_index_new]
2068
+ values[pivot_index_new] = temp
2069
+ pivot_index_new += 1
2070
+ temp = values[right]
2071
+ values[right] = values[pivot_index_new]
2072
+ values[pivot_index_new] = temp
2073
+
2074
+ if pivot_index_new == middle:
2075
+ if size % 2 == 0:
2076
+ return (values[middle] + values[middle + 1]) / <float_t> 2.0
2077
+ return values[middle]
2078
+ if pivot_index_new < middle:
2079
+ left = pivot_index_new + 1
2080
+ else:
2081
+ right = pivot_index_new - 1
2082
+
2083
+ return values[middle] # unreachable code?
2084
+
2085
+
2086
+ def _median_filter_2d(
2087
+ float_t[:, :] image,
2088
+ float_t[:, ::1] filtered_image,
2089
+ const ssize_t kernel_size,
2090
+ const int repeat=1,
2091
+ const int num_threads=1,
2092
+ ):
2093
+ """Apply 2D median filter ignoring NaN."""
2094
+ cdef:
2095
+ ssize_t rows = image.shape[0]
2096
+ ssize_t cols = image.shape[1]
2097
+ ssize_t k = kernel_size // 2
2098
+ ssize_t i, j, r, di, dj, ki, kj, valid_count
2099
+ float_t element
2100
+ float_t *kernel
2101
+
2102
+ if kernel_size <= 0:
2103
+ raise ValueError('kernel_size must be greater than 0')
2104
+
2105
+ with nogil, parallel(num_threads=num_threads):
2106
+
2107
+ kernel = <float_t *> malloc(
2108
+ kernel_size * kernel_size * sizeof(float_t)
2109
+ )
2110
+ if kernel == NULL:
2111
+ with gil:
2112
+ raise MemoryError('failed to allocate kernel')
2113
+
2114
+ for r in range(repeat):
2115
+ for i in prange(rows):
2116
+ for j in range(cols):
2117
+ if isnan(image[i, j]):
2118
+ filtered_image[i, j] = <float_t> NAN
2119
+ continue
2120
+ valid_count = 0
2121
+ for di in range(kernel_size):
2122
+ ki = i - k + di
2123
+ if ki < 0:
2124
+ ki = 0
2125
+ elif ki >= rows:
2126
+ ki = rows - 1
2127
+ for dj in range(kernel_size):
2128
+ kj = j - k + dj
2129
+ if kj < 0:
2130
+ kj = 0
2131
+ elif kj >= cols:
2132
+ kj = cols - 1
2133
+ element = image[ki, kj]
2134
+ if not isnan(element):
2135
+ kernel[valid_count] = element
2136
+ valid_count = valid_count + 1
2137
+ filtered_image[i, j] = _median(kernel, valid_count)
2138
+
2139
+ for i in prange(rows):
2140
+ for j in range(cols):
2141
+ image[i, j] = filtered_image[i, j]
2142
+
2143
+ free(kernel)