phasorpy 0.1__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.
phasorpy/_phasorpy.pyx ADDED
@@ -0,0 +1,1811 @@
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
+
65
+ def _phasor_from_signal(
66
+ float_t[:, :, ::1] phasor,
67
+ const signal_t[:, :, ::1] signal,
68
+ const double[:, :, ::1] sincos,
69
+ const int num_threads
70
+ ):
71
+ """Return phasor coordinates from signal along middle axis.
72
+
73
+ Parameters
74
+ ----------
75
+ phasor : 3D memoryview of float32 or float64
76
+ Writable buffer of three dimensions where calculated phasor
77
+ coordinates are stored:
78
+
79
+ 0. mean, real, and imaginary components
80
+ 1. lower dimensions flat
81
+ 2. upper dimensions flat
82
+
83
+ signal : 3D memoryview of float32 or float64
84
+ Buffer of three dimensions containing signal:
85
+
86
+ 0. lower dimensions flat
87
+ 1. dimension over which to compute FFT, number samples
88
+ 2. upper dimensions flat
89
+
90
+ sincos : 3D memoryview of float64
91
+ Buffer of three dimensions containing sine and cosine terms to be
92
+ multiplied with signal:
93
+
94
+ 0. number harmonics
95
+ 1. number samples
96
+ 2. cos and sin
97
+
98
+ num_threads : int
99
+ Number of OpenMP threads to use for parallelization.
100
+
101
+ Notes
102
+ -----
103
+ This implementation requires contiguous input arrays.
104
+
105
+ """
106
+ cdef:
107
+ float_t[:, ::1] mean
108
+ float_t[:, :, ::1] real, imag
109
+ ssize_t samples = signal.shape[1]
110
+ ssize_t harmonics = sincos.shape[0]
111
+ ssize_t i, j, k, h
112
+ double dc, re, im, sample
113
+
114
+ # TODO: use Numpy iterator API?
115
+ # https://numpy.org/devdocs/reference/c-api/iterator.html
116
+
117
+ if (
118
+ samples < 3
119
+ or harmonics > samples // 2
120
+ or phasor.shape[0] != harmonics * 2 + 1
121
+ or phasor.shape[1] != signal.shape[0]
122
+ or phasor.shape[2] != signal.shape[2]
123
+ ):
124
+ raise ValueError('invalid shape of phasor or signal')
125
+ if sincos.shape[1] != samples or sincos.shape[2] != 2:
126
+ raise ValueError('invalid shape of sincos')
127
+
128
+ mean = phasor[0]
129
+ real = phasor[1 : 1 + harmonics]
130
+ imag = phasor[1 + harmonics : 1 + harmonics * 2]
131
+
132
+ if num_threads > 1 and signal.shape[0] >= num_threads:
133
+ # parallelize outer dimensions
134
+ with nogil, parallel(num_threads=num_threads):
135
+ for i in prange(signal.shape[0]):
136
+ for h in range(harmonics):
137
+ for j in range(signal.shape[2]):
138
+ dc = 0.0
139
+ re = 0.0
140
+ im = 0.0
141
+ for k in range(samples):
142
+ sample = <double> signal[i, k, j]
143
+ dc = dc + sample
144
+ re = re + sample * sincos[h, k, 0]
145
+ im = im + sample * sincos[h, k, 1]
146
+ if dc != 0.0:
147
+ re = re / dc
148
+ im = im / dc
149
+ dc = dc /samples
150
+ else:
151
+ dc = 0.0
152
+ re = NAN if re == 0.0 else re * INFINITY
153
+ im = NAN if im == 0.0 else im * INFINITY
154
+ if h == 0:
155
+ mean[i, j] = <float_t> dc
156
+ real[h, i, j] = <float_t> re
157
+ imag[h, i, j] = <float_t> im
158
+
159
+ elif num_threads > 1 and signal.shape[2] >= num_threads:
160
+ # parallelize inner dimensions
161
+ # TODO: do not use when not built with OpenMP
162
+ with nogil, parallel(num_threads=num_threads):
163
+ for j in prange(signal.shape[2]):
164
+ for h in range(harmonics):
165
+ for i in range(signal.shape[0]):
166
+ dc = 0.0
167
+ re = 0.0
168
+ im = 0.0
169
+ for k in range(samples):
170
+ sample = <double> signal[i, k, j]
171
+ dc = dc + sample
172
+ re = re + sample * sincos[h, k, 0]
173
+ im = im + sample * sincos[h, k, 1]
174
+ if dc != 0.0:
175
+ re = re / dc
176
+ im = im / dc
177
+ dc = dc /samples
178
+ else:
179
+ dc = 0.0
180
+ re = NAN if re == 0.0 else re * INFINITY
181
+ im = NAN if im == 0.0 else im * INFINITY
182
+ if h == 0:
183
+ mean[i, j] = <float_t> dc
184
+ real[h, i, j] = <float_t> re
185
+ imag[h, i, j] = <float_t> im
186
+
187
+ else:
188
+ # do not parallelize
189
+ with nogil:
190
+ for h in range(harmonics):
191
+ # TODO: move harmonics to an inner loop?
192
+ for i in range(signal.shape[0]):
193
+ for j in range(signal.shape[2]):
194
+ dc = 0.0
195
+ re = 0.0
196
+ im = 0.0
197
+ for k in range(samples):
198
+ sample = <double> signal[i, k, j]
199
+ dc += sample
200
+ re += sample * sincos[h, k, 0]
201
+ im += sample * sincos[h, k, 1]
202
+ if dc != 0.0:
203
+ re /= dc
204
+ im /= dc
205
+ dc /= samples
206
+ else:
207
+ dc = 0.0
208
+ re = NAN if re == 0.0 else re * INFINITY
209
+ im = NAN if im == 0.0 else im * INFINITY
210
+ if h == 0:
211
+ mean[i, j] = <float_t> dc
212
+ real[h, i, j] = <float_t> re
213
+ imag[h, i, j] = <float_t> im
214
+
215
+
216
+ def _phasor_from_lifetime(
217
+ float_t[:, :, ::1] phasor,
218
+ const double[::1] frequency,
219
+ const double[:, ::1] lifetime,
220
+ const double[:, ::1] fraction,
221
+ const double unit_conversion,
222
+ const bint preexponential,
223
+ ):
224
+ """Calculate phasor coordinates from lifetime components.
225
+
226
+ Parameters
227
+ ----------
228
+ phasor : 3D memoryview of float32 or float64
229
+ Writable buffer of three dimensions where calculated phasor
230
+ coordinates are stored:
231
+
232
+ 0. real and imaginary components
233
+ 1. frequencies
234
+ 2. lifetimes or fractions
235
+
236
+ frequency : 2D memoryview of float64
237
+ One-dimensional sequence of laser-pulse or modulation frequencies.
238
+ lifetime : 2D memoryview of float64
239
+ Buffer of two dimensions:
240
+
241
+ 0. lifetimes
242
+ 1. components of lifetimes
243
+
244
+ fraction : 2D memoryview of float64
245
+ Buffer of two dimensions:
246
+
247
+ 0. fractions
248
+ 1. fractions of lifetime components
249
+
250
+ unit_conversion : float
251
+ Product of `frequency` and `lifetime` units' prefix factors.
252
+ 1e-3 for MHz and ns. 1.0 for Hz and s.
253
+ preexponential : bool
254
+ If true, fractions are pre-exponential amplitudes, else fractional
255
+ intensities.
256
+
257
+ """
258
+ cdef:
259
+ ssize_t nfreq = frequency.shape[0] # number frequencies
260
+ ssize_t ncomp = lifetime.shape[1] # number lifetime components
261
+ ssize_t ntau = lifetime.shape[0] # number lifetimes
262
+ ssize_t nfrac = fraction.shape[0] # number fractions
263
+ double twopi = 2.0 * M_PI * unit_conversion
264
+ double freq, tau, frac, sum, re, im, gs
265
+ ssize_t f, t, s
266
+
267
+ if phasor.shape[0] != 2 or phasor.shape[1] != nfreq:
268
+ raise ValueError(
269
+ f'invalid {phasor.shape=!r} != (2, {nfreq}, -1))'
270
+ )
271
+ if fraction.shape[1] != ncomp:
272
+ raise ValueError(f'{lifetime.shape[1]=} != {fraction.shape[1]=}')
273
+
274
+ if nfreq == 1 and ntau == 1 and nfrac == 1 and ncomp == 1:
275
+ # scalar
276
+ tau = lifetime[0, 0] * frequency[0] * twopi # omega_tau
277
+ gs = 1.0 / (1.0 + tau * tau)
278
+ phasor[0, 0, 0] = <float_t> gs
279
+ phasor[1, 0, 0] = <float_t> (gs * tau)
280
+ return
281
+
282
+ if ntau == nfrac:
283
+ # fractions specified for all lifetime components
284
+ if phasor.shape[2] != ntau:
285
+ raise ValueError(f'{phasor.shape[2]=} != {ntau}')
286
+ with nogil:
287
+ for f in range(nfreq):
288
+ freq = frequency[f] * twopi # omega
289
+ for t in range(ntau):
290
+ re = 0.0
291
+ im = 0.0
292
+ sum = 0.0
293
+ if preexponential:
294
+ for s in range(ncomp):
295
+ sum += fraction[t, s] * lifetime[t, s] # Fdc
296
+ else:
297
+ for s in range(ncomp):
298
+ sum += fraction[t, s]
299
+ if fabs(sum) < 1e-15:
300
+ phasor[0, f, t] = <float_t> NAN
301
+ phasor[1, f, t] = <float_t> NAN
302
+ continue
303
+ for s in range(ncomp):
304
+ tau = lifetime[t, s]
305
+ frac = fraction[t, s] / sum
306
+ if preexponential:
307
+ frac *= tau
308
+ tau *= freq # omega_tau
309
+ gs = frac / (1.0 + tau * tau)
310
+ re += gs
311
+ im += gs * tau
312
+ phasor[0, f, t] = <float_t> re
313
+ phasor[1, f, t] = <float_t> im
314
+ return
315
+
316
+ if ntau > 1 and nfrac == 1:
317
+ # varying lifetime components, same fractions
318
+ if phasor.shape[2] != ntau:
319
+ raise ValueError(f'{phasor.shape[2]=} != {ntau}')
320
+ with nogil:
321
+ for f in range(nfreq):
322
+ freq = frequency[f] * twopi # omega
323
+ sum = 0.0
324
+ if not preexponential:
325
+ for s in range(ncomp):
326
+ sum += fraction[0, s]
327
+ for t in range(ntau):
328
+ if preexponential:
329
+ sum = 0.0
330
+ for s in range(ncomp):
331
+ sum += fraction[0, s] * lifetime[t, s] # Fdc
332
+ if fabs(sum) < 1e-15:
333
+ phasor[0, f, t] = <float_t> NAN
334
+ phasor[1, f, t] = <float_t> NAN
335
+ continue
336
+ re = 0.0
337
+ im = 0.0
338
+ for s in range(ncomp):
339
+ tau = lifetime[t, s]
340
+ frac = fraction[0, s] / sum
341
+ if preexponential:
342
+ frac *= tau
343
+ tau *= freq # omega_tau
344
+ gs = frac / (1.0 + tau * tau)
345
+ re += gs
346
+ im += gs * tau
347
+ phasor[0, f, t] = <float_t> re
348
+ phasor[1, f, t] = <float_t> im
349
+ return
350
+
351
+ if ntau == 1 and nfrac > 1:
352
+ # same lifetime components, varying fractions
353
+ if phasor.shape[2] != nfrac:
354
+ raise ValueError(f'{phasor.shape[2]=} != {nfrac}')
355
+ with nogil:
356
+ for f in range(nfreq):
357
+ freq = frequency[f] * twopi # omega
358
+ for t in range(nfrac):
359
+ re = 0.0
360
+ im = 0.0
361
+ sum = 0.0
362
+ if preexponential:
363
+ for s in range(ncomp):
364
+ sum += fraction[t, s] * lifetime[0, s] # Fdc
365
+ else:
366
+ for s in range(ncomp):
367
+ sum += fraction[t, s]
368
+ if fabs(sum) < 1e-15:
369
+ phasor[0, f, t] = <float_t> NAN
370
+ phasor[1, f, t] = <float_t> NAN
371
+ continue
372
+ for s in range(ncomp):
373
+ tau = lifetime[0, s]
374
+ frac = fraction[t, s] / sum
375
+ if preexponential:
376
+ frac *= tau
377
+ tau *= freq # omega_tau
378
+ gs = frac / (1.0 + tau * tau)
379
+ re += gs
380
+ im += gs * tau
381
+ phasor[0, f, t] = <float_t> re
382
+ phasor[1, f, t] = <float_t> im
383
+ return
384
+
385
+ raise ValueError(
386
+ f'{lifetime.shape[0]=} and {fraction.shape[0]=} do not match'
387
+ )
388
+
389
+
390
+ def _gaussian_signal(
391
+ float_t[::1] signal,
392
+ const double mean,
393
+ const double stdev,
394
+ ):
395
+ """Return normal distribution, wrapped around at borders.
396
+
397
+ Parameters
398
+ ----------
399
+ signal : memoryview of float32 or float64
400
+ Writable buffer where calculated signal samples are stored.
401
+ mean : float
402
+ Mean of normal distribution.
403
+ stdev : float
404
+ Standard deviation of normal distribution.
405
+
406
+ """
407
+ cdef:
408
+ ssize_t samples = signal.shape[0]
409
+ ssize_t folds = 1 # TODO: calculate from stddev and samples
410
+ ssize_t i
411
+ double t, c
412
+
413
+ if stdev <= 0.0 or samples < 1:
414
+ return
415
+
416
+ with nogil:
417
+ c = 1.0 / sqrt(2.0 * M_PI) * stdev
418
+
419
+ for i in range(-folds * samples, (folds + 1) * samples):
420
+ t = (<double> i - mean) / stdev
421
+ t *= t
422
+ t = c * exp(-t / 2.0)
423
+ # i %= samples
424
+ i -= samples * <ssize_t> floor(<double> i / samples)
425
+ signal[i] += <float_t> t
426
+
427
+
428
+ ###############################################################################
429
+ # FRET model
430
+
431
+ @cython.ufunc
432
+ cdef (double, double) _phasor_from_fret_donor(
433
+ double omega,
434
+ double donor_lifetime,
435
+ double fret_efficiency,
436
+ double donor_freting,
437
+ double donor_background,
438
+ double background_real,
439
+ double background_imag,
440
+ ) noexcept nogil:
441
+ """Return phasor coordinates of FRET donor channel.
442
+
443
+ See :py:func:`phasor_from_fret_donor` for parameter definitions.
444
+
445
+ """
446
+ cdef:
447
+ double real, imag
448
+ double quenched_real, quenched_imag # quenched donor
449
+ double f_pure, f_quenched, sum
450
+
451
+ if fret_efficiency < 0.0:
452
+ fret_efficiency = 0.0
453
+ elif fret_efficiency > 1.0:
454
+ fret_efficiency = 1.0
455
+
456
+ if donor_freting < 0.0:
457
+ donor_freting = 0.0
458
+ elif donor_freting > 1.0:
459
+ donor_freting = 1.0
460
+
461
+ if donor_background < 0.0:
462
+ donor_background = 0.0
463
+
464
+ f_pure = 1.0 - donor_freting
465
+ f_quenched = (1.0 - fret_efficiency) * donor_freting
466
+ sum = f_pure + f_quenched + donor_background
467
+ if sum < 1e-9:
468
+ # no signal in donor channel
469
+ return 1.0, 0.0
470
+
471
+ # phasor of pure donor at frequency
472
+ real, imag = phasor_from_lifetime(donor_lifetime, omega)
473
+
474
+ # phasor of quenched donor
475
+ quenched_real, quenched_imag = phasor_from_lifetime(
476
+ donor_lifetime * (1.0 - fret_efficiency), omega
477
+ )
478
+
479
+ # weighted average
480
+ real = (
481
+ real * f_pure
482
+ + quenched_real * f_quenched
483
+ + donor_background * background_real
484
+ ) / sum
485
+
486
+ imag = (
487
+ imag * f_pure
488
+ + quenched_imag * f_quenched
489
+ + background_imag * donor_background
490
+ ) / sum
491
+
492
+ return real, imag
493
+
494
+
495
+ @cython.ufunc
496
+ cdef (double, double) _phasor_from_fret_acceptor(
497
+ double omega,
498
+ double donor_lifetime,
499
+ double acceptor_lifetime,
500
+ double fret_efficiency,
501
+ double donor_freting,
502
+ double donor_bleedthrough,
503
+ double acceptor_bleedthrough,
504
+ double acceptor_background,
505
+ double background_real,
506
+ double background_imag,
507
+ ) noexcept nogil:
508
+ """Return phasor coordinates of FRET acceptor channel.
509
+
510
+ See :py:func:`phasor_from_fret_acceptor` for parameter definitions.
511
+
512
+ """
513
+ cdef:
514
+ double phi, mod
515
+ double donor_real, donor_imag
516
+ double acceptor_real, acceptor_imag
517
+ double quenched_real, quenched_imag # quenched donor
518
+ double sensitized_real, sensitized_imag # sensitized acceptor
519
+ double sum, f_donor, f_acceptor
520
+
521
+ if fret_efficiency < 0.0:
522
+ fret_efficiency = 0.0
523
+ elif fret_efficiency > 1.0:
524
+ fret_efficiency = 1.0
525
+
526
+ if donor_freting < 0.0:
527
+ donor_freting = 0.0
528
+ elif donor_freting > 1.0:
529
+ donor_freting = 1.0
530
+
531
+ if donor_bleedthrough < 0.0:
532
+ donor_bleedthrough = 0.0
533
+ if acceptor_bleedthrough < 0.0:
534
+ acceptor_bleedthrough = 0.0
535
+ if acceptor_background < 0.0:
536
+ acceptor_background = 0.0
537
+
538
+ # phasor of pure donor at frequency
539
+ donor_real, donor_imag = phasor_from_lifetime(donor_lifetime, omega)
540
+
541
+ if fret_efficiency == 0.0:
542
+ quenched_real = donor_real
543
+ quenched_imag = donor_imag
544
+ else:
545
+ # phasor of quenched donor
546
+ quenched_real, quenched_imag = phasor_from_lifetime(
547
+ donor_lifetime * (1.0 - fret_efficiency), omega
548
+ )
549
+
550
+ # phasor of pure and quenched donor
551
+ donor_real, donor_imag = linear_combination(
552
+ 1.0,
553
+ 0.0,
554
+ donor_real,
555
+ donor_imag,
556
+ quenched_real,
557
+ quenched_imag,
558
+ 1.0,
559
+ 1.0 - fret_efficiency,
560
+ 1.0 - donor_freting
561
+ )
562
+
563
+ # phasor of acceptor at frequency
564
+ acceptor_real, acceptor_imag = phasor_from_lifetime(
565
+ acceptor_lifetime, omega
566
+ )
567
+
568
+ # phasor of acceptor sensitized by quenched donor
569
+ # TODO: use rotation formula
570
+ phi = (
571
+ atan2(quenched_imag, quenched_real)
572
+ + atan2(acceptor_imag, acceptor_real)
573
+ )
574
+ mod = (
575
+ hypot(quenched_real, quenched_imag)
576
+ * hypot(acceptor_real, acceptor_imag)
577
+ )
578
+ sensitized_real = mod * cos(phi)
579
+ sensitized_imag = mod * sin(phi)
580
+
581
+ # weighted average
582
+ f_donor = donor_bleedthrough * (1.0 - donor_freting * fret_efficiency)
583
+ f_acceptor = donor_freting * fret_efficiency
584
+ sum = f_donor + f_acceptor + acceptor_bleedthrough + acceptor_background
585
+ if sum < 1e-9:
586
+ # no signal in acceptor channel
587
+ # do not return 0, 0 to avoid discontinuities
588
+ return sensitized_real, sensitized_imag
589
+
590
+ acceptor_real = (
591
+ donor_real * f_donor
592
+ + sensitized_real * f_acceptor
593
+ + acceptor_real * acceptor_bleedthrough
594
+ + background_real * acceptor_background
595
+ ) / sum
596
+
597
+ acceptor_imag = (
598
+ donor_imag * f_donor
599
+ + sensitized_imag * f_acceptor
600
+ + acceptor_imag * acceptor_bleedthrough
601
+ + background_imag * acceptor_background
602
+ ) / sum
603
+
604
+ return acceptor_real, acceptor_imag
605
+
606
+
607
+ cdef inline (double, double) linear_combination(
608
+ const double real,
609
+ const double imag,
610
+ const double real1,
611
+ const double imag1,
612
+ const double real2,
613
+ const double imag2,
614
+ double int1,
615
+ double int2,
616
+ double frac,
617
+ ) noexcept nogil:
618
+ """Return linear combinations of phasor coordinates."""
619
+ int1 *= frac
620
+ int2 *= 1.0 - frac
621
+ frac = int1 + int2
622
+ if fabs(frac) < 1e-15:
623
+ return real, imag
624
+ return (
625
+ (int1 * real1 + int2 * real2) / frac,
626
+ (int1 * imag1 + int2 * imag2) / frac
627
+ )
628
+
629
+
630
+ cdef inline (float_t, float_t) phasor_from_lifetime(
631
+ float_t lifetime,
632
+ float_t omega,
633
+ ) noexcept nogil:
634
+ """Return phasor coordinates from single lifetime component."""
635
+ cdef:
636
+ double t = omega * lifetime
637
+ double mod = 1.0 / sqrt(1.0 + t * t)
638
+ double phi = atan(t)
639
+
640
+ return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
641
+
642
+
643
+ ###############################################################################
644
+ # Phasor conversions
645
+
646
+
647
+ @cython.ufunc
648
+ cdef (float_t, float_t) _phasor_transform(
649
+ float_t real,
650
+ float_t imag,
651
+ float_t angle,
652
+ float_t scale,
653
+ ) noexcept nogil:
654
+ """Return rotated and scaled phasor coordinates."""
655
+ cdef:
656
+ double g, s
657
+
658
+ if isnan(real) or isnan(imag) or isnan(angle) or isnan(scale):
659
+ return <float_t> NAN, <float_t> NAN
660
+
661
+ g = scale * cos(angle)
662
+ s = scale * sin(angle)
663
+
664
+ return <float_t> (real * g - imag * s), <float_t> (real * s + imag * g)
665
+
666
+
667
+ @cython.ufunc
668
+ cdef (float_t, float_t) _phasor_transform_const(
669
+ float_t real,
670
+ float_t imag,
671
+ float_t real2,
672
+ float_t imag2,
673
+ ) noexcept nogil:
674
+ """Return rotated and scaled phasor coordinates."""
675
+ if isnan(real) or isnan(imag) or isnan(real2) or isnan(imag2):
676
+ return <float_t> NAN, <float_t> NAN
677
+
678
+ return real * real2 - imag * imag2, real * imag2 + imag * real2
679
+
680
+
681
+ @cython.ufunc
682
+ cdef (float_t, float_t) _phasor_to_polar(
683
+ float_t real,
684
+ float_t imag,
685
+ ) noexcept nogil:
686
+ """Return polar from phasor coordinates."""
687
+ if isnan(real) or isnan(imag):
688
+ return <float_t> NAN, <float_t> NAN
689
+
690
+ return (
691
+ <float_t> atan2(imag, real),
692
+ <float_t> sqrt(real * real + imag * imag)
693
+ )
694
+
695
+
696
+ @cython.ufunc
697
+ cdef (float_t, float_t) _phasor_from_polar(
698
+ float_t phase,
699
+ float_t modulation,
700
+ ) noexcept nogil:
701
+ """Return phasor from polar coordinates."""
702
+ if isnan(phase) or isnan(modulation):
703
+ return <float_t> NAN, <float_t> NAN
704
+
705
+ return (
706
+ modulation * <float_t> cos(phase),
707
+ modulation * <float_t> sin(phase)
708
+ )
709
+
710
+
711
+ @cython.ufunc
712
+ cdef (float_t, float_t) _phasor_to_apparent_lifetime(
713
+ float_t real,
714
+ float_t imag,
715
+ float_t omega,
716
+ ) noexcept nogil:
717
+ """Return apparent single lifetimes from phasor coordinates."""
718
+ cdef:
719
+ double tauphi = INFINITY
720
+ double taumod = INFINITY
721
+ double t
722
+
723
+ if isnan(real) or isnan(imag):
724
+ return <float_t> NAN, <float_t> NAN
725
+
726
+ t = real * real + imag * imag
727
+ if omega > 0.0 and t > 0.0:
728
+ if fabs(real * omega) > 0.0:
729
+ tauphi = imag / (real * omega)
730
+ if t <= 1.0:
731
+ taumod = sqrt(1.0 / t - 1.0) / omega
732
+ else:
733
+ taumod = 0.0
734
+
735
+ return <float_t> tauphi, <float_t> taumod
736
+
737
+
738
+ @cython.ufunc
739
+ cdef (float_t, float_t) _phasor_from_apparent_lifetime(
740
+ float_t tauphi,
741
+ float_t taumod,
742
+ float_t omega,
743
+ ) noexcept nogil:
744
+ """Return phasor coordinates from apparent single lifetimes."""
745
+ cdef:
746
+ double phi, mod, t
747
+
748
+ if isnan(tauphi) or isnan(taumod):
749
+ return <float_t> NAN, <float_t> NAN
750
+
751
+ t = omega * taumod
752
+ mod = 1.0 / sqrt(1.0 + t * t)
753
+ phi = atan(omega * tauphi)
754
+ return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
755
+
756
+
757
+ @cython.ufunc
758
+ cdef (float_t, float_t) _phasor_from_single_lifetime(
759
+ float_t lifetime,
760
+ float_t omega,
761
+ ) noexcept nogil:
762
+ """Return phasor coordinates from single lifetime component."""
763
+ cdef:
764
+ double phi, mod, t
765
+
766
+ if isnan(lifetime):
767
+ return <float_t> NAN, <float_t> NAN
768
+
769
+ t = omega * lifetime
770
+ phi = atan(t)
771
+ mod = 1.0 / sqrt(1.0 + t * t)
772
+ return <float_t> (mod * cos(phi)), <float_t> (mod * sin(phi))
773
+
774
+
775
+ @cython.ufunc
776
+ cdef (float_t, float_t) _polar_from_single_lifetime(
777
+ float_t lifetime,
778
+ float_t omega,
779
+ ) noexcept nogil:
780
+ """Return polar coordinates from single lifetime component."""
781
+ cdef:
782
+ double t
783
+
784
+ if isnan(lifetime):
785
+ return <float_t> NAN, <float_t> NAN
786
+
787
+ t = omega * lifetime
788
+ return <float_t> atan(t), <float_t> (1.0 / sqrt(1.0 + t * t))
789
+
790
+
791
+ @cython.ufunc
792
+ cdef (float_t, float_t) _polar_to_apparent_lifetime(
793
+ float_t phase,
794
+ float_t modulation,
795
+ float_t omega,
796
+ ) noexcept nogil:
797
+ """Return apparent single lifetimes from polar coordinates."""
798
+ cdef:
799
+ double tauphi = INFINITY
800
+ double taumod = INFINITY
801
+ double t
802
+
803
+ if isnan(phase) or isnan(modulation):
804
+ return <float_t> NAN, <float_t> NAN
805
+
806
+ t = modulation * modulation
807
+ if omega > 0.0 and t > 0.0:
808
+ tauphi = tan(phase) / omega
809
+ if t <= 1.0:
810
+ taumod = sqrt(1.0 / t - 1.0) / omega
811
+ else:
812
+ taumod = 0.0
813
+ return <float_t> tauphi, <float_t> taumod
814
+
815
+
816
+ @cython.ufunc
817
+ cdef (float_t, float_t) _polar_from_apparent_lifetime(
818
+ float_t tauphi,
819
+ float_t taumod,
820
+ float_t omega,
821
+ ) noexcept nogil:
822
+ """Return polar coordinates from apparent single lifetimes."""
823
+ cdef:
824
+ double t
825
+
826
+ if isnan(tauphi) or isnan(taumod):
827
+ return <float_t> NAN, <float_t> NAN
828
+
829
+ t = omega * taumod
830
+ return (
831
+ <float_t> (atan(omega * tauphi)),
832
+ <float_t> (1.0 / sqrt(1.0 + t * t))
833
+ )
834
+
835
+
836
+ @cython.ufunc
837
+ cdef (float_t, float_t) _polar_from_reference(
838
+ float_t measured_phase,
839
+ float_t measured_modulation,
840
+ float_t known_phase,
841
+ float_t known_modulation,
842
+ ) noexcept nogil:
843
+ """Return polar coordinates for calibration from reference coordinates."""
844
+ if (
845
+ isnan(measured_phase)
846
+ or isnan(measured_modulation)
847
+ or isnan(known_phase)
848
+ or isnan(known_modulation)
849
+ ):
850
+ return <float_t> NAN, <float_t> NAN
851
+
852
+ if fabs(measured_modulation) == 0.0:
853
+ # return known_phase - measured_phase, <float_t> INFINITY
854
+ return (
855
+ known_phase - measured_phase,
856
+ <float_t> (NAN if known_modulation == 0.0 else INFINITY)
857
+ )
858
+ return known_phase - measured_phase, known_modulation / measured_modulation
859
+
860
+
861
+ @cython.ufunc
862
+ cdef (float_t, float_t) _polar_from_reference_phasor(
863
+ float_t measured_real,
864
+ float_t measured_imag,
865
+ float_t known_real,
866
+ float_t known_imag,
867
+ ) noexcept nogil:
868
+ """Return polar coordinates for calibration from reference phasor."""
869
+ cdef:
870
+ double measured_phase, measured_modulation
871
+ double known_phase, known_modulation
872
+
873
+ if (
874
+ isnan(measured_real)
875
+ or isnan(measured_imag)
876
+ or isnan(known_real)
877
+ or isnan(known_imag)
878
+ ):
879
+ return <float_t> NAN, <float_t> NAN
880
+
881
+ measured_phase = atan2(measured_imag, measured_real)
882
+ known_phase = atan2(known_imag, known_real)
883
+ measured_modulation = hypot(measured_real, measured_imag)
884
+ known_modulation = hypot(known_real, known_imag)
885
+
886
+ if fabs(measured_modulation) == 0.0:
887
+ # return <float_t> (known_phase - measured_phase), <float_t> INFINITY
888
+ return (
889
+ <float_t> (known_phase - measured_phase),
890
+ <float_t> (NAN if known_modulation == 0.0 else INFINITY)
891
+ )
892
+ return (
893
+ <float_t> (known_phase - measured_phase),
894
+ <float_t> (known_modulation / measured_modulation)
895
+ )
896
+
897
+
898
+ @cython.ufunc
899
+ cdef (float_t, float_t) _phasor_at_harmonic(
900
+ float_t real,
901
+ int harmonic,
902
+ int other_harmonic,
903
+ ) noexcept nogil:
904
+ """Return phasor coordinates on semicircle at other harmonic."""
905
+ if isnan(real):
906
+ return <float_t> NAN, <float_t> NAN
907
+
908
+ if real <= 0.0:
909
+ return 0.0, 0.0
910
+ if real >= 1.0:
911
+ return 1.0, 0.0
912
+
913
+ harmonic *= harmonic
914
+ other_harmonic *= other_harmonic
915
+ real = (
916
+ harmonic * real / (other_harmonic + (harmonic - other_harmonic) * real)
917
+ )
918
+
919
+ return real, <float_t> sqrt(real - real * real)
920
+
921
+
922
+ @cython.ufunc
923
+ cdef (float_t, float_t) _phasor_multiply(
924
+ float_t real1,
925
+ float_t imag1,
926
+ float_t real2,
927
+ float_t imag2,
928
+ ) noexcept nogil:
929
+ """Return multiplication of two phasors."""
930
+ return real1 * real2 - imag1 * imag2, real1 * imag2 + imag1 * real2
931
+
932
+
933
+ @cython.ufunc
934
+ cdef (float_t, float_t) _phasor_divide(
935
+ float_t real1,
936
+ float_t imag1,
937
+ float_t real2,
938
+ float_t imag2,
939
+ ) noexcept nogil:
940
+ """Return division of two phasors."""
941
+ cdef:
942
+ float_t denom = real2 * real2 + imag2 * imag2
943
+
944
+ if isnan(denom) or denom == 0.0:
945
+ return <float_t> NAN, <float_t> NAN
946
+
947
+ return (
948
+ (real1 * real2 + imag1 * imag2) / denom,
949
+ (imag1 * real2 - real1 * imag2) / denom
950
+ )
951
+
952
+
953
+ ###############################################################################
954
+ # Geometry ufuncs
955
+
956
+ @cython.ufunc
957
+ cdef short _is_inside_range(
958
+ float_t x, # point
959
+ float_t y,
960
+ float_t xmin, # x range
961
+ float_t xmax,
962
+ float_t ymin, # y range
963
+ float_t ymax
964
+ ) noexcept nogil:
965
+ """Return whether point is inside range.
966
+
967
+ Range includes lower but not upper limit.
968
+
969
+ """
970
+ if isnan(x) or isnan(y):
971
+ return False
972
+
973
+ return x >= xmin and x < xmax and y >= ymin and y < ymax
974
+
975
+
976
+ @cython.ufunc
977
+ cdef short _is_inside_rectangle(
978
+ float_t x, # point
979
+ float_t y,
980
+ float_t x0, # segment start
981
+ float_t y0,
982
+ float_t x1, # segment end
983
+ float_t y1,
984
+ float_t r, # half width
985
+ ) noexcept nogil:
986
+ """Return whether point is in rectangle.
987
+
988
+ The rectangle is defined by central line segment and half width.
989
+
990
+ """
991
+ cdef:
992
+ float_t t
993
+
994
+ if r <= 0.0 or isnan(x) or isnan(y):
995
+ return False
996
+
997
+ # normalize coordinates
998
+ # x1 = 0
999
+ # y1 = 0
1000
+ x0 -= x1
1001
+ y0 -= y1
1002
+ x -= x1
1003
+ y -= y1
1004
+ # square of line length
1005
+ t = x0 * x0 + y0 * y0
1006
+ if t <= 0.0:
1007
+ return x * x + y * y <= r * r
1008
+ # projection of point on line using clamped dot product
1009
+ t = (x * x0 + y * y0) / t
1010
+ if t < 0.0 or t > 1.0:
1011
+ return False
1012
+ # compare square of lengths of projection and radius
1013
+ x -= t * x0
1014
+ y -= t * y0
1015
+ return x * x + y * y <= r * r
1016
+
1017
+
1018
+ @cython.ufunc
1019
+ cdef short _is_inside_polar_rectangle(
1020
+ float_t x, # point
1021
+ float_t y,
1022
+ float_t angle_min, # phase, -pi to pi
1023
+ float_t angle_max,
1024
+ float_t distance_min, # modulation
1025
+ float_t distance_max,
1026
+ ) noexcept nogil:
1027
+ """Return whether point is inside polar rectangle.
1028
+
1029
+ Angles should be in range -pi to pi, else performance is degraded.
1030
+
1031
+ """
1032
+ cdef:
1033
+ double t
1034
+
1035
+ if isnan(x) or isnan(y):
1036
+ return False
1037
+
1038
+ if distance_min > distance_max:
1039
+ distance_min, distance_max = distance_max, distance_min
1040
+ t = hypot(x, y)
1041
+ if t < distance_min or t > distance_max or t == 0.0:
1042
+ return False
1043
+
1044
+ if angle_min < -M_PI or angle_min > M_PI:
1045
+ angle_min = <float_t> atan2(sin(angle_min), cos(angle_min))
1046
+ if angle_max < -M_PI or angle_max > M_PI:
1047
+ angle_max = <float_t> atan2(sin(angle_max), cos(angle_max))
1048
+ if angle_min > angle_max:
1049
+ angle_min, angle_max = angle_max, angle_min
1050
+ t = <float_t> atan2(y, x)
1051
+ if t < angle_min or t > angle_max:
1052
+ return False
1053
+
1054
+ return True
1055
+
1056
+
1057
+ @cython.ufunc
1058
+ cdef short _is_inside_circle(
1059
+ float_t x, # point
1060
+ float_t y,
1061
+ float_t x0, # circle center
1062
+ float_t y0,
1063
+ float_t r, # circle radius
1064
+ ) noexcept nogil:
1065
+ """Return whether point is inside circle."""
1066
+ if r <= 0.0 or isnan(x) or isnan(y):
1067
+ return False
1068
+
1069
+ x -= x0
1070
+ y -= y0
1071
+ return x * x + y * y <= r * r
1072
+
1073
+
1074
+ @cython.ufunc
1075
+ cdef short _is_inside_ellipse(
1076
+ float_t x, # point
1077
+ float_t y,
1078
+ float_t x0, # ellipse center
1079
+ float_t y0,
1080
+ float_t a, # ellipse radii
1081
+ float_t b,
1082
+ float_t phi, # ellipse angle
1083
+ ) noexcept nogil:
1084
+ """Return whether point is inside ellipse.
1085
+
1086
+ Same as _is_inside_circle if a == b.
1087
+ Consider using _is_inside_ellipse_ instead, which should be faster
1088
+ for arrays.
1089
+
1090
+ """
1091
+ cdef:
1092
+ float_t sina, cosa
1093
+
1094
+ if a <= 0.0 or b <= 0.0 or isnan(x) or isnan(y):
1095
+ return False
1096
+
1097
+ x -= x0
1098
+ y -= y0
1099
+ if a == b:
1100
+ # circle
1101
+ return x * x + y * y <= a * a
1102
+ sina = <float_t> sin(phi)
1103
+ cosa = <float_t> cos(phi)
1104
+ x0 = (cosa * x + sina * y) / a
1105
+ y0 = (sina * x - cosa * y) / b
1106
+ return x0 * x0 + y0 * y0 <= 1.0
1107
+
1108
+
1109
+ @cython.ufunc
1110
+ cdef short _is_inside_ellipse_(
1111
+ float_t x, # point
1112
+ float_t y,
1113
+ float_t x0, # ellipse center
1114
+ float_t y0,
1115
+ float_t a, # ellipse radii
1116
+ float_t b,
1117
+ float_t sina, # sin/cos of ellipse angle
1118
+ float_t cosa,
1119
+ ) noexcept nogil:
1120
+ """Return whether point is inside ellipse.
1121
+
1122
+ Use pre-calculated sin(angle) and cos(angle).
1123
+
1124
+ """
1125
+ if a <= 0.0 or b <= 0.0 or isnan(x) or isnan(y):
1126
+ return False
1127
+
1128
+ x -= x0
1129
+ y -= y0
1130
+ if a == b:
1131
+ # circle
1132
+ return x * x + y * y <= a * a
1133
+ x0 = (cosa * x + sina * y) / a
1134
+ y0 = (sina * x - cosa * y) / b
1135
+ return x0 * x0 + y0 * y0 <= 1.0
1136
+
1137
+
1138
+ @cython.ufunc
1139
+ cdef short _is_inside_stadium(
1140
+ float_t x, # point
1141
+ float_t y,
1142
+ float_t x0, # line start
1143
+ float_t y0,
1144
+ float_t x1, # line end
1145
+ float_t y1,
1146
+ float_t r, # radius
1147
+ ) noexcept nogil:
1148
+ """Return whether point is inside stadium.
1149
+
1150
+ A stadium shape is a thick line with rounded ends.
1151
+ Same as _is_near_segment.
1152
+
1153
+ """
1154
+ cdef:
1155
+ float_t t
1156
+
1157
+ if r <= 0.0 or isnan(x) or isnan(y):
1158
+ return False
1159
+
1160
+ # normalize coordinates
1161
+ # x1 = 0
1162
+ # y1 = 0
1163
+ x0 -= x1
1164
+ y0 -= y1
1165
+ x -= x1
1166
+ y -= y1
1167
+ # square of line length
1168
+ t = x0 * x0 + y0 * y0
1169
+ if t <= 0.0:
1170
+ return x * x + y * y <= r * r
1171
+ # projection of point on line using clamped dot product
1172
+ t = (x * x0 + y * y0) / t
1173
+ t = <float_t> max(0.0, min(1.0, t))
1174
+ # compare square of lengths of projection and radius
1175
+ x -= t * x0
1176
+ y -= t * y0
1177
+ return x * x + y * y <= r * r
1178
+
1179
+
1180
+ # function alias
1181
+ _is_near_segment = _is_inside_stadium
1182
+
1183
+
1184
+ @cython.ufunc
1185
+ cdef short _is_near_line(
1186
+ float_t x, # point
1187
+ float_t y,
1188
+ float_t x0, # line start
1189
+ float_t y0,
1190
+ float_t x1, # line end
1191
+ float_t y1,
1192
+ float_t r, # distance
1193
+ ) noexcept nogil:
1194
+ """Return whether point is close to line."""
1195
+ cdef:
1196
+ float_t t
1197
+
1198
+ if r <= 0.0 or isnan(x) or isnan(y):
1199
+ return False
1200
+
1201
+ # normalize coordinates
1202
+ # x1 = 0
1203
+ # y1 = 0
1204
+ x0 -= x1
1205
+ y0 -= y1
1206
+ x -= x1
1207
+ y -= y1
1208
+ # square of line length
1209
+ t = x0 * x0 + y0 * y0
1210
+ if t <= 0.0:
1211
+ return x * x + y * y <= r * r
1212
+ # projection of point on line using clamped dot product
1213
+ t = (x * x0 + y * y0) / t
1214
+ # compare square of lengths of projection and radius
1215
+ x -= t * x0
1216
+ y -= t * y0
1217
+ return x * x + y * y <= r * r
1218
+
1219
+
1220
+ @cython.ufunc
1221
+ cdef (float_t, float_t) _point_on_segment(
1222
+ float_t x, # point
1223
+ float_t y,
1224
+ float_t x0, # segment start
1225
+ float_t y0,
1226
+ float_t x1, # segment end
1227
+ float_t y1,
1228
+ ) noexcept nogil:
1229
+ """Return point projected onto line segment."""
1230
+ cdef:
1231
+ float_t t
1232
+
1233
+ if isnan(x) or isnan(y):
1234
+ return <float_t> NAN, <float_t> NAN
1235
+
1236
+ # normalize coordinates
1237
+ # x1 = 0
1238
+ # y1 = 0
1239
+ x0 -= x1
1240
+ y0 -= y1
1241
+ x -= x1
1242
+ y -= y1
1243
+ # square of line length
1244
+ t = x0 * x0 + y0 * y0
1245
+ if t <= 0.0:
1246
+ return x0, y0
1247
+ # projection of point on line
1248
+ t = (x * x0 + y * y0) / t
1249
+ # clamp to line segment
1250
+ if t < 0.0:
1251
+ t = 0.0
1252
+ elif t > 1.0:
1253
+ t = 1.0
1254
+ x1 += t * x0
1255
+ y1 += t * y0
1256
+ return x1, y1
1257
+
1258
+
1259
+ @cython.ufunc
1260
+ cdef (float_t, float_t) _point_on_line(
1261
+ float_t x, # point
1262
+ float_t y,
1263
+ float_t x0, # line start
1264
+ float_t y0,
1265
+ float_t x1, # line end
1266
+ float_t y1,
1267
+ ) noexcept nogil:
1268
+ """Return point projected onto line."""
1269
+ cdef:
1270
+ float_t t
1271
+
1272
+ if isnan(x) or isnan(y):
1273
+ return <float_t> NAN, <float_t> NAN
1274
+
1275
+ # normalize coordinates
1276
+ # x1 = 0
1277
+ # y1 = 0
1278
+ x0 -= x1
1279
+ y0 -= y1
1280
+ x -= x1
1281
+ y -= y1
1282
+ # square of line length
1283
+ t = x0 * x0 + y0 * y0
1284
+ if t <= 0.0:
1285
+ return x0, y0
1286
+ # projection of point on line
1287
+ t = (x * x0 + y * y0) / t
1288
+ x1 += t * x0
1289
+ y1 += t * y0
1290
+ return x1, y1
1291
+
1292
+
1293
+ @cython.ufunc
1294
+ cdef float_t _fraction_on_segment(
1295
+ float_t x, # point
1296
+ float_t y,
1297
+ float_t x0, # segment start
1298
+ float_t y0,
1299
+ float_t x1, # segment end
1300
+ float_t y1,
1301
+ ) noexcept nogil:
1302
+ """Return normalized fraction of point projected onto line segment."""
1303
+ cdef:
1304
+ float_t t
1305
+
1306
+ if isnan(x) or isnan(y):
1307
+ return <float_t> NAN
1308
+
1309
+ # normalize coordinates
1310
+ x -= x1
1311
+ y -= y1
1312
+ x0 -= x1
1313
+ y0 -= y1
1314
+ # x1 = 0
1315
+ # y1 = 0
1316
+ # square of line length
1317
+ t = x0 * x0 + y0 * y0
1318
+ if t <= 0.0:
1319
+ # not a line segment
1320
+ return 0.0
1321
+ # projection of point on line
1322
+ t = (x * x0 + y * y0) / t
1323
+ # clamp to line segment
1324
+ if t < 0.0:
1325
+ t = 0.0
1326
+ elif t > 1.0:
1327
+ t = 1.0
1328
+ return t
1329
+
1330
+
1331
+ @cython.ufunc
1332
+ cdef float_t _fraction_on_line(
1333
+ float_t x, # point
1334
+ float_t y,
1335
+ float_t x0, # line start
1336
+ float_t y0,
1337
+ float_t x1, # line end
1338
+ float_t y1,
1339
+ ) noexcept nogil:
1340
+ """Return normalized fraction of point projected onto line."""
1341
+ cdef:
1342
+ float_t t
1343
+
1344
+ if isnan(x) or isnan(y):
1345
+ return <float_t> NAN
1346
+
1347
+ # normalize coordinates
1348
+ x -= x1
1349
+ y -= y1
1350
+ x0 -= x1
1351
+ y0 -= y1
1352
+ # x1 = 0
1353
+ # y1 = 0
1354
+ # square of line length
1355
+ t = x0 * x0 + y0 * y0
1356
+ if t <= 0.0:
1357
+ # not a line segment
1358
+ return 1.0
1359
+ # projection of point on line
1360
+ t = (x * x0 + y * y0) / t
1361
+ return t
1362
+
1363
+
1364
+ @cython.ufunc
1365
+ cdef float_t _distance_from_point(
1366
+ float_t x, # point
1367
+ float_t y,
1368
+ float_t x0, # other point
1369
+ float_t y0,
1370
+ ) noexcept nogil:
1371
+ """Return distance from point."""
1372
+ if isnan(x) or isnan(y): # or isnan(x0) or isnan(y0)
1373
+ return <float_t> NAN
1374
+
1375
+ return <float_t> hypot(x - x0, y - y0)
1376
+
1377
+
1378
+ @cython.ufunc
1379
+ cdef float_t _distance_from_segment(
1380
+ float_t x, # point
1381
+ float_t y,
1382
+ float_t x0, # segment start
1383
+ float_t y0,
1384
+ float_t x1, # segment end
1385
+ float_t y1,
1386
+ ) noexcept nogil:
1387
+ """Return distance from segment."""
1388
+ cdef:
1389
+ float_t t
1390
+
1391
+ if isnan(x) or isnan(y):
1392
+ return <float_t> NAN
1393
+
1394
+ # normalize coordinates
1395
+ # x1 = 0
1396
+ # y1 = 0
1397
+ x0 -= x1
1398
+ y0 -= y1
1399
+ x -= x1
1400
+ y -= y1
1401
+ # square of line length
1402
+ t = x0 * x0 + y0 * y0
1403
+ if t <= 0.0:
1404
+ return <float_t> hypot(x, y)
1405
+ # projection of point on line using dot product
1406
+ t = (x * x0 + y * y0) / t
1407
+ if t > 1.0:
1408
+ x -= x0
1409
+ y -= y0
1410
+ elif t > 0.0:
1411
+ x -= t * x0
1412
+ y -= t * y0
1413
+ return <float_t> hypot(x, y)
1414
+
1415
+
1416
+ @cython.ufunc
1417
+ cdef float_t _distance_from_line(
1418
+ float_t x, # point
1419
+ float_t y,
1420
+ float_t x0, # line start
1421
+ float_t y0,
1422
+ float_t x1, # line end
1423
+ float_t y1,
1424
+ ) noexcept nogil:
1425
+ """Return distance from line."""
1426
+ cdef:
1427
+ float_t t
1428
+
1429
+ if isnan(x) or isnan(y):
1430
+ return <float_t> NAN
1431
+
1432
+ # normalize coordinates
1433
+ # x1 = 0
1434
+ # y1 = 0
1435
+ x0 -= x1
1436
+ y0 -= y1
1437
+ x -= x1
1438
+ y -= y1
1439
+ # square of line length
1440
+ t = x0 * x0 + y0 * y0
1441
+ if t <= 0.0:
1442
+ return <float_t> hypot(x, y)
1443
+ # projection of point on line using dot product
1444
+ t = (x * x0 + y * y0) / t
1445
+ x -= t * x0
1446
+ y -= t * y0
1447
+ return <float_t> hypot(x, y)
1448
+
1449
+
1450
+ @cython.ufunc
1451
+ cdef (double, double, double) _segment_direction_and_length(
1452
+ float_t x0, # segment start
1453
+ float_t y0,
1454
+ float_t x1, # segment end
1455
+ float_t y1,
1456
+ ) noexcept nogil:
1457
+ """Return direction and length of line segment."""
1458
+ cdef:
1459
+ float_t length
1460
+
1461
+ if isnan(x0) or isnan(y0) or isnan(x1) or isnan(y1):
1462
+ return NAN, NAN, 0.0
1463
+
1464
+ x1 -= x0
1465
+ y1 -= y0
1466
+ length = <float_t> hypot(x1, y1)
1467
+ if length <= 0.0:
1468
+ return NAN, NAN, 0.0
1469
+ x1 /= length
1470
+ y1 /= length
1471
+ return x1, y1, length
1472
+
1473
+
1474
+ @cython.ufunc
1475
+ cdef (double, double, double, double) _intersection_circle_circle(
1476
+ float_t x0, # circle 0
1477
+ float_t y0,
1478
+ float_t r0,
1479
+ float_t x1, # circle 1
1480
+ float_t y1,
1481
+ float_t r1,
1482
+ ) noexcept nogil:
1483
+ """Return coordinates of intersections of two circles."""
1484
+ cdef:
1485
+ double dx, dy, dr, ll, dd, hd, ld
1486
+
1487
+ if (
1488
+ isnan(x0)
1489
+ or isnan(y0)
1490
+ or isnan(r0)
1491
+ or isnan(x1)
1492
+ or isnan(y1)
1493
+ or isnan(r1)
1494
+ or r0 == 0.0
1495
+ or r1 == 0.0
1496
+ ):
1497
+ return NAN, NAN, NAN, NAN
1498
+
1499
+ dx = x1 - x0
1500
+ dy = y1 - y0
1501
+ dr = hypot(dx, dy)
1502
+ if dr <= 0.0:
1503
+ # circle positions identical
1504
+ return NAN, NAN, NAN, NAN
1505
+ ll = (r0 * r0 - r1 * r1 + dr * dr) / (dr + dr)
1506
+ dd = r0 * r0 - ll * ll
1507
+ if dd < 0.0 or dr <= 0.0:
1508
+ # circles not intersecting
1509
+ return NAN, NAN, NAN, NAN
1510
+ hd = sqrt(dd) / dr
1511
+ ld = ll / dr
1512
+ return (
1513
+ ld * dx + hd * dy + x0,
1514
+ ld * dy - hd * dx + y0,
1515
+ ld * dx - hd * dy + x0,
1516
+ ld * dy + hd * dx + y0,
1517
+ )
1518
+
1519
+
1520
+ @cython.ufunc
1521
+ cdef (double, double, double, double) _intersection_circle_line(
1522
+ float_t x, # circle
1523
+ float_t y,
1524
+ float_t r,
1525
+ float_t x0, # line start
1526
+ float_t y0,
1527
+ float_t x1, # line end
1528
+ float_t y1,
1529
+ ) noexcept nogil:
1530
+ """Return coordinates of intersections of circle and line."""
1531
+ cdef:
1532
+ double dx, dy, dr, dd, rdd
1533
+
1534
+ if (
1535
+ isnan(r)
1536
+ or isnan(x)
1537
+ or isnan(y)
1538
+ or isnan(x0)
1539
+ or isnan(y0)
1540
+ or isnan(x1)
1541
+ or isnan(y1)
1542
+ or r == 0.0
1543
+ ):
1544
+ return NAN, NAN, NAN, NAN
1545
+
1546
+ dx = x1 - x0
1547
+ dy = y1 - y0
1548
+ dr = dx * dx + dy * dy
1549
+ dd = (x0 - x) * (y1 - y) - (x1 - x) * (y0 - y)
1550
+ rdd = r * r * dr - dd * dd # discriminant
1551
+ if rdd < 0.0 or dr <= 0.0:
1552
+ # no intersection
1553
+ return NAN, NAN, NAN, NAN
1554
+ rdd = sqrt(rdd)
1555
+ return (
1556
+ x + (dd * dy + copysign(1.0, dy) * dx * rdd) / dr,
1557
+ y + (-dd * dx + fabs(dy) * rdd) / dr,
1558
+ x + (dd * dy - copysign(1.0, dy) * dx * rdd) / dr,
1559
+ y + (-dd * dx - fabs(dy) * rdd) / dr,
1560
+ )
1561
+
1562
+ ###############################################################################
1563
+ # Blend ufuncs
1564
+
1565
+
1566
+ @cython.ufunc
1567
+ cdef float_t _blend_normal(
1568
+ float_t a, # base layer
1569
+ float_t b, # blend layer
1570
+ ) noexcept nogil:
1571
+ """Return blended layers using `normal` mode."""
1572
+ if isnan(b):
1573
+ return a
1574
+ return b
1575
+
1576
+
1577
+ @cython.ufunc
1578
+ cdef float_t _blend_multiply(
1579
+ float_t a, # base layer
1580
+ float_t b, # blend layer
1581
+ ) noexcept nogil:
1582
+ """Return blended layers using `multiply` mode."""
1583
+ if isnan(b):
1584
+ return a
1585
+ return a * b
1586
+
1587
+
1588
+ @cython.ufunc
1589
+ cdef float_t _blend_screen(
1590
+ float_t a, # base layer
1591
+ float_t b, # blend layer
1592
+ ) noexcept nogil:
1593
+ """Return blended layers using `screen` mode."""
1594
+ if isnan(b):
1595
+ return a
1596
+ return <float_t> (1.0 - (1.0 - a) * (1.0 - b))
1597
+
1598
+
1599
+ @cython.ufunc
1600
+ cdef float_t _blend_overlay(
1601
+ float_t a, # base layer
1602
+ float_t b, # blend layer
1603
+ ) noexcept nogil:
1604
+ """Return blended layers using `overlay` mode."""
1605
+ if isnan(b) or isnan(a):
1606
+ return a
1607
+ if a < 0.5:
1608
+ return <float_t> (2.0 * a * b)
1609
+ return <float_t> (1.0 - 2.0 * (1.0 - a) * (1.0 - b))
1610
+
1611
+
1612
+ @cython.ufunc
1613
+ cdef float_t _blend_darken(
1614
+ float_t a, # base layer
1615
+ float_t b, # blend layer
1616
+ ) noexcept nogil:
1617
+ """Return blended layers using `darken` mode."""
1618
+ if isnan(b) or isnan(a):
1619
+ return a
1620
+ return <float_t> min(a, b)
1621
+
1622
+
1623
+ @cython.ufunc
1624
+ cdef float_t _blend_lighten(
1625
+ float_t a, # base layer
1626
+ float_t b, # blend layer
1627
+ ) noexcept nogil:
1628
+ """Return blended layers using `lighten` mode."""
1629
+ if isnan(b) or isnan(a):
1630
+ return a
1631
+ return <float_t> max(a, b)
1632
+
1633
+ ###############################################################################
1634
+ # Threshold ufuncs
1635
+
1636
+
1637
+ @cython.ufunc
1638
+ cdef (double, double, double) _phasor_threshold_open(
1639
+ float_t mean,
1640
+ float_t real,
1641
+ float_t imag,
1642
+ float_t mean_min,
1643
+ float_t mean_max,
1644
+ float_t real_min,
1645
+ float_t real_max,
1646
+ float_t imag_min,
1647
+ float_t imag_max,
1648
+ float_t phase_min,
1649
+ float_t phase_max,
1650
+ float_t modulation_min,
1651
+ float_t modulation_max,
1652
+ ) noexcept nogil:
1653
+ """Return thresholded values by open intervals."""
1654
+ cdef:
1655
+ double phi = NAN
1656
+ double mod = NAN
1657
+
1658
+ if isnan(mean) or isnan(real) or isnan(imag):
1659
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1660
+
1661
+ if not isnan(mean_min) and mean <= mean_min:
1662
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1663
+ if not isnan(mean_max) and mean >= mean_max:
1664
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1665
+
1666
+ if not isnan(real_min) and real <= real_min:
1667
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1668
+ if not isnan(real_max) and real >= real_max:
1669
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1670
+
1671
+ if not isnan(imag_min) and imag <= imag_min:
1672
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1673
+ if not isnan(imag_max) and imag >= imag_max:
1674
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1675
+
1676
+ if not isnan(modulation_min):
1677
+ mod = real * real + imag * imag
1678
+ if mod <= modulation_min * modulation_min:
1679
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1680
+ if not isnan(modulation_max):
1681
+ if isnan(mod):
1682
+ mod = real * real + imag * imag
1683
+ if mod >= modulation_max * modulation_max:
1684
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1685
+
1686
+ if not isnan(phase_min):
1687
+ phi = atan2(imag, real)
1688
+ if phi <= phase_min:
1689
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1690
+ if not isnan(phase_max):
1691
+ if isnan(phi):
1692
+ phi = atan2(imag, real)
1693
+ if phi >= phase_max:
1694
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1695
+
1696
+ return mean, real, imag
1697
+
1698
+
1699
+ @cython.ufunc
1700
+ cdef (double, double, double) _phasor_threshold_closed(
1701
+ float_t mean,
1702
+ float_t real,
1703
+ float_t imag,
1704
+ float_t mean_min,
1705
+ float_t mean_max,
1706
+ float_t real_min,
1707
+ float_t real_max,
1708
+ float_t imag_min,
1709
+ float_t imag_max,
1710
+ float_t phase_min,
1711
+ float_t phase_max,
1712
+ float_t modulation_min,
1713
+ float_t modulation_max,
1714
+ ) noexcept nogil:
1715
+ """Return thresholded values by closed intervals."""
1716
+ cdef:
1717
+ double phi = NAN
1718
+ double mod = NAN
1719
+
1720
+ if isnan(mean) or isnan(real) or isnan(imag):
1721
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1722
+
1723
+ if not isnan(mean_min) and mean < mean_min:
1724
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1725
+ if not isnan(mean_max) and mean > mean_max:
1726
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1727
+
1728
+ if not isnan(real_min) and real < real_min:
1729
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1730
+ if not isnan(real_max) and real > real_max:
1731
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1732
+
1733
+ if not isnan(imag_min) and imag < imag_min:
1734
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1735
+ if not isnan(imag_max) and imag > imag_max:
1736
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1737
+
1738
+ if not isnan(modulation_min):
1739
+ mod = real * real + imag * imag
1740
+ if mod < modulation_min * modulation_min:
1741
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1742
+ if not isnan(modulation_max):
1743
+ if isnan(mod):
1744
+ mod = real * real + imag * imag
1745
+ if mod > modulation_max * modulation_max:
1746
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1747
+
1748
+ if not isnan(phase_min):
1749
+ phi = atan2(imag, real)
1750
+ if phi < phase_min:
1751
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1752
+ if not isnan(phase_max):
1753
+ if isnan(phi):
1754
+ phi = atan2(imag, real)
1755
+ if phi > phase_max:
1756
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1757
+
1758
+ return mean, real, imag
1759
+
1760
+
1761
+ @cython.ufunc
1762
+ cdef (double, double, double) _phasor_threshold_mean_open(
1763
+ float_t mean,
1764
+ float_t real,
1765
+ float_t imag,
1766
+ float_t mean_min,
1767
+ float_t mean_max,
1768
+ ) noexcept nogil:
1769
+ """Return thresholded values only by open interval of `mean`."""
1770
+ if isnan(mean) or isnan(real) or isnan(imag):
1771
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1772
+
1773
+ if not isnan(mean_min) and mean <= mean_min:
1774
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1775
+ if not isnan(mean_max) and mean >= mean_max:
1776
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1777
+
1778
+ return mean, real, imag
1779
+
1780
+
1781
+ @cython.ufunc
1782
+ cdef (double, double, double) _phasor_threshold_mean_closed(
1783
+ float_t mean,
1784
+ float_t real,
1785
+ float_t imag,
1786
+ float_t mean_min,
1787
+ float_t mean_max,
1788
+ ) noexcept nogil:
1789
+ """Return thresholded values only by closed interval of `mean`."""
1790
+ if isnan(mean) or isnan(real) or isnan(imag):
1791
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1792
+
1793
+ if not isnan(mean_min) and mean < mean_min:
1794
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1795
+ if not isnan(mean_max) and mean > mean_max:
1796
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1797
+
1798
+ return mean, real, imag
1799
+
1800
+
1801
+ @cython.ufunc
1802
+ cdef (double, double, double) _phasor_threshold_nan(
1803
+ float_t mean,
1804
+ float_t real,
1805
+ float_t imag,
1806
+ ) noexcept nogil:
1807
+ """Return the input values if any of them is not NaN."""
1808
+ if isnan(mean) or isnan(real) or isnan(imag):
1809
+ return <float_t> NAN, <float_t> NAN, <float_t> NAN
1810
+
1811
+ return mean, real, imag