rapidtide 3.0.10__py3-none-any.whl → 3.1__py3-none-any.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.
Files changed (141) hide show
  1. rapidtide/Colortables.py +492 -27
  2. rapidtide/OrthoImageItem.py +1053 -47
  3. rapidtide/RapidtideDataset.py +1533 -86
  4. rapidtide/_version.py +3 -3
  5. rapidtide/calccoherence.py +196 -29
  6. rapidtide/calcnullsimfunc.py +191 -40
  7. rapidtide/calcsimfunc.py +245 -42
  8. rapidtide/correlate.py +1210 -393
  9. rapidtide/data/examples/src/testLD +56 -0
  10. rapidtide/data/examples/src/testalign +1 -1
  11. rapidtide/data/examples/src/testdelayvar +0 -1
  12. rapidtide/data/examples/src/testfmri +19 -1
  13. rapidtide/data/examples/src/testglmfilt +5 -5
  14. rapidtide/data/examples/src/testhappy +30 -1
  15. rapidtide/data/examples/src/testppgproc +17 -0
  16. rapidtide/data/examples/src/testrolloff +11 -0
  17. rapidtide/data/models/model_cnn_pytorch/best_model.pth +0 -0
  18. rapidtide/data/models/model_cnn_pytorch/loss.png +0 -0
  19. rapidtide/data/models/model_cnn_pytorch/loss.txt +1 -0
  20. rapidtide/data/models/model_cnn_pytorch/model.pth +0 -0
  21. rapidtide/data/models/model_cnn_pytorch/model_meta.json +68 -0
  22. rapidtide/data/reference/JHU-ArterialTerritoriesNoVent-LVL1_space-MNI152NLin2009cAsym_2mm.nii.gz +0 -0
  23. rapidtide/data/reference/JHU-ArterialTerritoriesNoVent-LVL1_space-MNI152NLin2009cAsym_2mm_mask.nii.gz +0 -0
  24. rapidtide/decorators.py +91 -0
  25. rapidtide/dlfilter.py +2225 -108
  26. rapidtide/dlfiltertorch.py +4843 -0
  27. rapidtide/externaltools.py +327 -12
  28. rapidtide/fMRIData_class.py +79 -40
  29. rapidtide/filter.py +1899 -810
  30. rapidtide/fit.py +2004 -574
  31. rapidtide/genericmultiproc.py +93 -18
  32. rapidtide/happy_supportfuncs.py +2044 -171
  33. rapidtide/helper_classes.py +584 -43
  34. rapidtide/io.py +2363 -370
  35. rapidtide/linfitfiltpass.py +341 -75
  36. rapidtide/makelaggedtcs.py +211 -20
  37. rapidtide/maskutil.py +423 -53
  38. rapidtide/miscmath.py +827 -121
  39. rapidtide/multiproc.py +210 -22
  40. rapidtide/patchmatch.py +234 -33
  41. rapidtide/peakeval.py +32 -30
  42. rapidtide/ppgproc.py +2203 -0
  43. rapidtide/qualitycheck.py +352 -39
  44. rapidtide/refinedelay.py +422 -57
  45. rapidtide/refineregressor.py +498 -184
  46. rapidtide/resample.py +671 -185
  47. rapidtide/scripts/applyppgproc.py +28 -0
  48. rapidtide/simFuncClasses.py +1052 -77
  49. rapidtide/simfuncfit.py +260 -46
  50. rapidtide/stats.py +540 -238
  51. rapidtide/tests/happycomp +9 -0
  52. rapidtide/tests/test_dlfiltertorch.py +627 -0
  53. rapidtide/tests/test_findmaxlag.py +24 -8
  54. rapidtide/tests/test_fullrunhappy_v1.py +0 -2
  55. rapidtide/tests/test_fullrunhappy_v2.py +0 -2
  56. rapidtide/tests/test_fullrunhappy_v3.py +1 -0
  57. rapidtide/tests/test_fullrunhappy_v4.py +2 -2
  58. rapidtide/tests/test_fullrunrapidtide_v7.py +1 -1
  59. rapidtide/tests/test_simroundtrip.py +8 -8
  60. rapidtide/tests/utils.py +9 -8
  61. rapidtide/tidepoolTemplate.py +142 -38
  62. rapidtide/tidepoolTemplate_alt.py +165 -44
  63. rapidtide/tidepoolTemplate_big.py +189 -52
  64. rapidtide/util.py +1217 -118
  65. rapidtide/voxelData.py +684 -37
  66. rapidtide/wiener.py +19 -12
  67. rapidtide/wiener2.py +113 -7
  68. rapidtide/wiener_doc.py +255 -0
  69. rapidtide/workflows/adjustoffset.py +105 -3
  70. rapidtide/workflows/aligntcs.py +85 -2
  71. rapidtide/workflows/applydlfilter.py +87 -10
  72. rapidtide/workflows/applyppgproc.py +522 -0
  73. rapidtide/workflows/atlasaverage.py +210 -47
  74. rapidtide/workflows/atlastool.py +100 -3
  75. rapidtide/workflows/calcSimFuncMap.py +294 -64
  76. rapidtide/workflows/calctexticc.py +201 -9
  77. rapidtide/workflows/ccorrica.py +97 -4
  78. rapidtide/workflows/cleanregressor.py +168 -29
  79. rapidtide/workflows/delayvar.py +163 -10
  80. rapidtide/workflows/diffrois.py +81 -3
  81. rapidtide/workflows/endtidalproc.py +144 -4
  82. rapidtide/workflows/fdica.py +195 -15
  83. rapidtide/workflows/filtnifti.py +70 -3
  84. rapidtide/workflows/filttc.py +74 -3
  85. rapidtide/workflows/fitSimFuncMap.py +206 -48
  86. rapidtide/workflows/fixtr.py +73 -3
  87. rapidtide/workflows/gmscalc.py +113 -3
  88. rapidtide/workflows/happy.py +813 -201
  89. rapidtide/workflows/happy2std.py +144 -12
  90. rapidtide/workflows/happy_parser.py +149 -8
  91. rapidtide/workflows/histnifti.py +118 -2
  92. rapidtide/workflows/histtc.py +84 -3
  93. rapidtide/workflows/linfitfilt.py +117 -4
  94. rapidtide/workflows/localflow.py +328 -28
  95. rapidtide/workflows/mergequality.py +79 -3
  96. rapidtide/workflows/niftidecomp.py +322 -18
  97. rapidtide/workflows/niftistats.py +174 -4
  98. rapidtide/workflows/pairproc.py +88 -2
  99. rapidtide/workflows/pairwisemergenifti.py +85 -2
  100. rapidtide/workflows/parser_funcs.py +1421 -40
  101. rapidtide/workflows/physiofreq.py +137 -11
  102. rapidtide/workflows/pixelcomp.py +208 -5
  103. rapidtide/workflows/plethquality.py +103 -21
  104. rapidtide/workflows/polyfitim.py +151 -11
  105. rapidtide/workflows/proj2flow.py +75 -2
  106. rapidtide/workflows/rankimage.py +111 -4
  107. rapidtide/workflows/rapidtide.py +272 -15
  108. rapidtide/workflows/rapidtide2std.py +98 -2
  109. rapidtide/workflows/rapidtide_parser.py +109 -9
  110. rapidtide/workflows/refineDelayMap.py +143 -33
  111. rapidtide/workflows/refineRegressor.py +682 -93
  112. rapidtide/workflows/regressfrommaps.py +152 -31
  113. rapidtide/workflows/resamplenifti.py +85 -3
  114. rapidtide/workflows/resampletc.py +91 -3
  115. rapidtide/workflows/retrolagtcs.py +98 -6
  116. rapidtide/workflows/retroregress.py +165 -9
  117. rapidtide/workflows/roisummarize.py +173 -5
  118. rapidtide/workflows/runqualitycheck.py +71 -3
  119. rapidtide/workflows/showarbcorr.py +147 -4
  120. rapidtide/workflows/showhist.py +86 -2
  121. rapidtide/workflows/showstxcorr.py +160 -3
  122. rapidtide/workflows/showtc.py +159 -3
  123. rapidtide/workflows/showxcorrx.py +184 -4
  124. rapidtide/workflows/showxy.py +185 -15
  125. rapidtide/workflows/simdata.py +262 -36
  126. rapidtide/workflows/spatialfit.py +77 -2
  127. rapidtide/workflows/spatialmi.py +251 -27
  128. rapidtide/workflows/spectrogram.py +305 -32
  129. rapidtide/workflows/synthASL.py +154 -3
  130. rapidtide/workflows/tcfrom2col.py +76 -2
  131. rapidtide/workflows/tcfrom3col.py +74 -2
  132. rapidtide/workflows/tidepool.py +2972 -133
  133. rapidtide/workflows/utils.py +19 -14
  134. rapidtide/workflows/utils_doc.py +293 -0
  135. rapidtide/workflows/variabilityizer.py +116 -3
  136. {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/METADATA +10 -9
  137. {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/RECORD +141 -122
  138. {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/entry_points.txt +1 -0
  139. {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/WHEEL +0 -0
  140. {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/licenses/LICENSE +0 -0
  141. {rapidtide-3.0.10.dist-info → rapidtide-3.1.dist-info}/top_level.txt +0 -0
rapidtide/fit.py CHANGED
@@ -18,175 +18,273 @@
18
18
  #
19
19
  import sys
20
20
  import warnings
21
+ from typing import Any, Callable, Optional, Tuple, Union
21
22
 
22
23
  import matplotlib.pyplot as plt
23
24
  import numpy as np
24
25
  import scipy as sp
25
26
  import scipy.special as sps
26
27
  import statsmodels.api as sm
27
- import tqdm
28
28
  from numpy.polynomial import Polynomial
29
+ from numpy.typing import ArrayLike, NDArray
30
+ from scipy import signal
29
31
  from scipy.optimize import curve_fit
30
32
  from scipy.signal import find_peaks, hilbert
31
33
  from scipy.stats import entropy, moment
32
34
  from sklearn.linear_model import LinearRegression
33
35
  from statsmodels.robust import mad
36
+ from statsmodels.tsa.ar_model import AutoReg, ar_select_order
37
+ from tqdm import tqdm
34
38
 
39
+ import rapidtide.miscmath as tide_math
35
40
  import rapidtide.util as tide_util
41
+ from rapidtide.decorators import conditionaljit, conditionaljit2
36
42
 
37
43
  # ---------------------------------------- Global constants -------------------------------------------
38
44
  defaultbutterorder = 6
39
45
  MAXLINES = 10000000
40
- donotbeaggressive = True
41
-
42
- # ----------------------------------------- Conditional imports ---------------------------------------
43
- try:
44
- from numba import jit
45
- except ImportError:
46
- donotusenumba = True
47
- else:
48
- donotusenumba = False
49
-
50
-
51
- def conditionaljit():
52
- def resdec(f):
53
- if donotusenumba:
54
- return f
55
- return jit(f, nopython=True)
56
-
57
- return resdec
58
-
59
-
60
- def conditionaljit2():
61
- def resdec(f):
62
- if donotusenumba or donotbeaggressive:
63
- return f
64
- return jit(f, nopython=True)
65
-
66
- return resdec
67
-
68
-
69
- def disablenumba():
70
- global donotusenumba
71
- donotusenumba = True
72
46
 
73
47
 
74
48
  # --------------------------- Fitting functions -------------------------------------------------
75
- def gaussresidualssk(p, y, x):
49
+ def gaussskresiduals(p: NDArray, y: NDArray, x: NDArray) -> NDArray:
76
50
  """
51
+ Calculate residuals for skewed Gaussian fit.
77
52
 
78
- Parameters
79
- ----------
80
- p
81
- y
82
- x
83
-
84
- Returns
85
- -------
86
-
87
- """
88
- err = y - gausssk_eval(x, p)
89
- return err
90
-
91
-
92
- def gaussskresiduals(p, y, x):
93
- """
53
+ This function computes the residuals (observed values minus fitted values)
54
+ for a skewed Gaussian model. The residuals are used to assess the quality
55
+ of the fit and are commonly used in optimization routines.
94
56
 
95
57
  Parameters
96
58
  ----------
97
- p
98
- y
99
- x
59
+ p : NDArray
60
+ Skewed Gaussian parameters [amplitude, center, width, skewness]
61
+ y : NDArray
62
+ Observed y values
63
+ x : NDArray
64
+ x values
100
65
 
101
66
  Returns
102
67
  -------
68
+ residuals : NDArray
69
+ Residuals (y - fitted values) for the skewed Gaussian model
70
+
71
+ Notes
72
+ -----
73
+ The function relies on the `gausssk_eval` function to compute the fitted
74
+ values of the skewed Gaussian model given the parameters and x values.
103
75
 
76
+ Examples
77
+ --------
78
+ >>> import numpy as np
79
+ >>> x = np.linspace(-5, 5, 100)
80
+ >>> p = np.array([1.0, 0.0, 1.0, 0.5]) # amplitude, center, width, skewness
81
+ >>> y = gausssk_eval(x, p) + np.random.normal(0, 0.1, len(x))
82
+ >>> residuals = gaussskresiduals(p, y, x)
83
+ >>> print(f"Mean residual: {np.mean(residuals):.6f}")
104
84
  """
105
85
  return y - gausssk_eval(x, p)
106
86
 
107
87
 
108
88
  @conditionaljit()
109
- def gaussresiduals(p, y, x):
89
+ def gaussresiduals(p: NDArray, y: NDArray, x: NDArray) -> NDArray:
110
90
  """
91
+ Calculate residuals for Gaussian fit.
92
+
93
+ This function computes the residuals (observed values minus fitted values)
94
+ for a Gaussian function with parameters [amplitude, center, width].
111
95
 
112
96
  Parameters
113
97
  ----------
114
- p
115
- y
116
- x
98
+ p : NDArray
99
+ Gaussian parameters [amplitude, center, width]
100
+ y : NDArray
101
+ Observed y values
102
+ x : NDArray
103
+ x values
117
104
 
118
105
  Returns
119
106
  -------
107
+ NDArray
108
+ Residuals (y - fitted values) where fitted values are calculated as:
109
+ y_fit = amplitude * exp(-((x - center) ** 2) / (2 * width ** 2))
110
+
111
+ Notes
112
+ -----
113
+ The Gaussian function is defined as:
114
+ f(x) = amplitude * exp(-((x - center) ** 2) / (2 * width ** 2))
120
115
 
116
+ Examples
117
+ --------
118
+ >>> import numpy as np
119
+ >>> p = np.array([1.0, 0.0, 0.5]) # amplitude=1.0, center=0.0, width=0.5
120
+ >>> y = np.array([0.5, 0.8, 1.0, 0.8, 0.5])
121
+ >>> x = np.linspace(-2, 2, 5)
122
+ >>> residuals = gaussresiduals(p, y, x)
123
+ >>> print(residuals)
121
124
  """
122
125
  return y - p[0] * np.exp(-((x - p[1]) ** 2) / (2.0 * p[2] * p[2]))
123
126
 
124
127
 
125
- def trapezoidresiduals(p, y, x, toplength):
128
+ def trapezoidresiduals(p: NDArray, y: NDArray, x: NDArray, toplength: float) -> NDArray:
126
129
  """
130
+ Calculate residuals for trapezoid fit.
131
+
132
+ This function computes the residuals (observed values minus fitted values) for a trapezoid
133
+ function fit. The trapezoid is defined by amplitude, center, and width parameters, with
134
+ a specified flat top length.
127
135
 
128
136
  Parameters
129
137
  ----------
130
- p
131
- y
132
- x
133
- toplength
138
+ p : NDArray
139
+ Trapezoid parameters [amplitude, center, width]
140
+ y : NDArray
141
+ Observed y values
142
+ x : NDArray
143
+ x values
144
+ toplength : float
145
+ Length of the flat top of the trapezoid
134
146
 
135
147
  Returns
136
148
  -------
149
+ residuals : NDArray
150
+ Residuals (y - fitted values)
137
151
 
152
+ Notes
153
+ -----
154
+ The function uses `trapezoid_eval_loop` to evaluate the trapezoid function with the
155
+ given parameters and returns the difference between observed and predicted values.
156
+
157
+ Examples
158
+ --------
159
+ >>> import numpy as np
160
+ >>> x = np.linspace(0, 10, 100)
161
+ >>> y = trapezoid_eval_loop(x, 2.0, [1.0, 5.0, 3.0]) + np.random.normal(0, 0.1, 100)
162
+ >>> p = [1.0, 5.0, 3.0]
163
+ >>> residuals = trapezoidresiduals(p, y, x, 2.0)
138
164
  """
139
165
  return y - trapezoid_eval_loop(x, toplength, p)
140
166
 
141
167
 
142
- def risetimeresiduals(p, y, x):
168
+ def risetimeresiduals(p: NDArray, y: NDArray, x: NDArray) -> NDArray:
143
169
  """
170
+ Calculate residuals for rise time fit.
171
+
172
+ This function computes the residuals between observed data and fitted rise time model
173
+ by subtracting the evaluated model from the observed values.
144
174
 
145
175
  Parameters
146
176
  ----------
147
- p
148
- y
149
- x
177
+ p : NDArray
178
+ Rise time parameters [amplitude, start, rise time] where:
179
+ - amplitude: peak value of the rise time curve
180
+ - start: starting time of the rise
181
+ - rise time: time constant for the rise process
182
+ y : NDArray
183
+ Observed y values (dependent variable)
184
+ x : NDArray
185
+ x values (independent variable, typically time)
150
186
 
151
187
  Returns
152
188
  -------
189
+ residuals : NDArray
190
+ Residuals (y - fitted values) representing the difference between
191
+ observed data and model predictions
192
+
193
+ Notes
194
+ -----
195
+ This function assumes the existence of a `risetime_eval_loop` function that
196
+ evaluates the rise time model at given x values with parameters p.
153
197
 
198
+ Examples
199
+ --------
200
+ >>> import numpy as np
201
+ >>> p = np.array([1.0, 0.0, 0.5])
202
+ >>> y = np.array([0.1, 0.3, 0.7, 0.9])
203
+ >>> x = np.array([0.0, 0.2, 0.4, 0.6])
204
+ >>> residuals = risetimeresiduals(p, y, x)
205
+ >>> print(residuals)
154
206
  """
155
207
  return y - risetime_eval_loop(x, p)
156
208
 
157
209
 
158
- def gausssk_eval(x, p):
210
+ def gausssk_eval(x: NDArray, p: NDArray) -> NDArray:
159
211
  """
212
+ Evaluate a skewed Gaussian function.
213
+
214
+ This function computes a skewed Gaussian distribution using the method described
215
+ by Azzalini and Dacunha (1996) for generating skewed normal distributions.
160
216
 
161
217
  Parameters
162
218
  ----------
163
- x
164
- p
219
+ x : NDArray
220
+ x values at which to evaluate the function
221
+ p : NDArray
222
+ Skewed Gaussian parameters [amplitude, center, width, skewness]
223
+ - amplitude: scaling factor for the peak height
224
+ - center: location parameter (mean of the underlying normal distribution)
225
+ - width: scale parameter (standard deviation of the underlying normal distribution)
226
+ - skewness: skewness parameter (controls the asymmetry of the distribution)
165
227
 
166
228
  Returns
167
229
  -------
230
+ y : NDArray
231
+ Evaluated skewed Gaussian values
168
232
 
233
+ Notes
234
+ -----
235
+ The skewed Gaussian is defined as:
236
+ f(x) = amplitude * φ((x-center)/width) * Φ(skewness * (x-center)/width)
237
+ where φ is the standard normal PDF and Φ is the standard normal CDF.
238
+
239
+ Examples
240
+ --------
241
+ >>> import numpy as np
242
+ >>> x = np.linspace(-5, 5, 100)
243
+ >>> params = [1.0, 0.0, 1.0, 2.0] # amplitude, center, width, skewness
244
+ >>> y = gausssk_eval(x, params)
169
245
  """
170
246
  t = (x - p[1]) / p[2]
171
247
  return p[0] * sp.stats.norm.pdf(t) * sp.stats.norm.cdf(p[3] * t)
172
248
 
173
249
 
174
250
  # @conditionaljit()
175
- def kaiserbessel_eval(x, p):
251
+ def kaiserbessel_eval(x: NDArray, p: NDArray) -> NDArray:
176
252
  """
177
253
 
254
+ Evaluate the Kaiser-Bessel window function.
255
+
256
+ This function computes the Kaiser-Bessel window function, which is commonly used in
257
+ signal processing and medical imaging applications for gridding and convolution operations.
258
+ The window is defined by parameters alpha (or beta) and tau (or W/2).
259
+
178
260
  Parameters
179
261
  ----------
180
- x: array-like
181
- arguments to the KB function
182
- p: array-like
262
+ x : NDArray
263
+ Arguments to the KB function, typically representing spatial or frequency coordinates
264
+ p : NDArray
183
265
  The Kaiser-Bessel window parameters [alpha, tau] (wikipedia) or [beta, W/2] (Jackson, J. I., Meyer, C. H.,
184
266
  Nishimura, D. G. & Macovski, A. Selection of a convolution function for Fourier inversion using gridding
185
267
  [computerised tomography application]. IEEE Trans. Med. Imaging 10, 473–478 (1991))
186
268
 
187
269
  Returns
188
270
  -------
271
+ NDArray
272
+ The evaluated Kaiser-Bessel window function values corresponding to input x
273
+
274
+ Notes
275
+ -----
276
+ The Kaiser-Bessel window is defined as:
277
+ KB(x) = I0(α√(1-(x/τ)²)) / (τ * I0(α)) for |x| ≤ τ
278
+ KB(x) = 0 for |x| > τ
189
279
 
280
+ where I0 is the zeroth-order modified Bessel function of the first kind.
281
+
282
+ Examples
283
+ --------
284
+ >>> import numpy as np
285
+ >>> x = np.linspace(-1, 1, 100)
286
+ >>> p = np.array([4.0, 0.5]) # alpha=4.0, tau=0.5
287
+ >>> result = kaiserbessel_eval(x, p)
190
288
  """
191
289
  normfac = sps.i0(p[0] * np.sqrt(1.0 - np.square((0.0 / p[1])))) / p[1]
192
290
  sqrtargs = 1.0 - np.square((x / p[1]))
@@ -199,33 +297,83 @@ def kaiserbessel_eval(x, p):
199
297
 
200
298
 
201
299
  @conditionaljit()
202
- def gauss_eval(x, p):
300
+ def gauss_eval(
301
+ x: NDArray[np.floating[Any]], p: NDArray[np.floating[Any]]
302
+ ) -> NDArray[np.floating[Any]]:
203
303
  """
304
+ Evaluate a Gaussian function.
305
+
306
+ This function computes the values of a Gaussian (normal) distribution
307
+ at given x points with specified parameters.
204
308
 
205
309
  Parameters
206
310
  ----------
207
- x
208
- p
311
+ x : NDArray[np.floating[Any]]
312
+ x values at which to evaluate the Gaussian function
313
+ p : NDArray[np.floating[Any]]
314
+ Gaussian parameters [amplitude, center, width] where:
315
+ - amplitude: peak height of the Gaussian
316
+ - center: x-value of the Gaussian center
317
+ - width: standard deviation of the Gaussian
209
318
 
210
319
  Returns
211
320
  -------
321
+ y : NDArray[np.floating[Any]]
322
+ Evaluated Gaussian values with the same shape as x
212
323
 
324
+ Notes
325
+ -----
326
+ The Gaussian function is defined as:
327
+ f(x) = amplitude * exp(-((x - center)^2) / (2 * width^2))
328
+
329
+ Examples
330
+ --------
331
+ >>> import numpy as np
332
+ >>> x = np.linspace(-5, 5, 100)
333
+ >>> params = np.array([1.0, 0.0, 1.0]) # amplitude=1, center=0, width=1
334
+ >>> y = gauss_eval(x, params)
335
+ >>> print(y.shape)
336
+ (100,)
213
337
  """
214
338
  return p[0] * np.exp(-((x - p[1]) ** 2) / (2.0 * p[2] * p[2]))
215
339
 
216
340
 
217
- def trapezoid_eval_loop(x, toplength, p):
341
+ def trapezoid_eval_loop(x: NDArray, toplength: float, p: NDArray) -> NDArray:
218
342
  """
343
+ Evaluate a trapezoid function at multiple points using a loop.
344
+
345
+ This function evaluates a trapezoid-shaped function at given x values. The trapezoid
346
+ is defined by its amplitude, center, and total width, with the flat top length
347
+ specified separately.
219
348
 
220
349
  Parameters
221
350
  ----------
222
- x
223
- toplength
224
- p
351
+ x : NDArray
352
+ x values at which to evaluate the function
353
+ toplength : float
354
+ Length of the flat top of the trapezoid
355
+ p : NDArray
356
+ Trapezoid parameters [amplitude, center, width]
225
357
 
226
358
  Returns
227
359
  -------
360
+ y : NDArray
361
+ Evaluated trapezoid values
362
+
363
+ Notes
364
+ -----
365
+ The trapezoid function is defined as:
366
+ - Zero outside the range [center - width/2, center + width/2]
367
+ - Linearly increasing from 0 to amplitude in the range [center - width/2, center - width/2 + toplength/2]
368
+ - Constant at amplitude in the range [center - width/2 + toplength/2, center + width/2 - toplength/2]
369
+ - Linearly decreasing from amplitude to 0 in the range [center + width/2 - toplength/2, center + width/2]
228
370
 
371
+ Examples
372
+ --------
373
+ >>> import numpy as np
374
+ >>> x = np.linspace(0, 10, 100)
375
+ >>> p = [1.0, 5.0, 4.0] # amplitude=1.0, center=5.0, width=4.0
376
+ >>> result = trapezoid_eval_loop(x, 2.0, p)
229
377
  """
230
378
  r = np.zeros(len(x), dtype="float64")
231
379
  for i in range(0, len(x)):
@@ -233,17 +381,40 @@ def trapezoid_eval_loop(x, toplength, p):
233
381
  return r
234
382
 
235
383
 
236
- def risetime_eval_loop(x, p):
384
+ def risetime_eval_loop(x: NDArray, p: NDArray) -> NDArray:
237
385
  """
386
+ Evaluate a rise time function.
387
+
388
+ This function evaluates a rise time function for a given set of x values and parameters.
389
+ It iterates through each x value and applies the risetime_eval function to compute
390
+ the corresponding y values.
238
391
 
239
392
  Parameters
240
393
  ----------
241
- x
242
- p
394
+ x : NDArray
395
+ x values at which to evaluate the function
396
+ p : NDArray
397
+ Rise time parameters [amplitude, start, rise time]
243
398
 
244
399
  Returns
245
400
  -------
401
+ y : NDArray
402
+ Evaluated rise time function values
403
+
404
+ Notes
405
+ -----
406
+ This function uses a loop-based approach for evaluating the rise time function.
407
+ For better performance with large arrays, consider using vectorized operations
408
+ instead of this loop-based implementation.
246
409
 
410
+ Examples
411
+ --------
412
+ >>> import numpy as np
413
+ >>> x = np.array([0, 1, 2, 3, 4])
414
+ >>> p = np.array([1.0, 0.0, 1.0])
415
+ >>> result = risetime_eval_loop(x, p)
416
+ >>> print(result)
417
+ [0. 0.63212056 0.86466472 0.95021293 0.98168436]
247
418
  """
248
419
  r = np.zeros(len(x), dtype="float64")
249
420
  for i in range(0, len(x)):
@@ -252,40 +423,51 @@ def risetime_eval_loop(x, p):
252
423
 
253
424
 
254
425
  @conditionaljit()
255
- def trapezoid_eval(x, toplength, p):
426
+ def trapezoid_eval(
427
+ x: Union[float, NDArray], toplength: float, p: NDArray
428
+ ) -> Union[float, NDArray]:
256
429
  """
257
- Evaluates the trapezoidal function at a given point.
430
+ Evaluate the trapezoidal function at given points.
258
431
 
259
432
  The trapezoidal function is defined as:
260
433
 
261
- f(x) = A \* (1 - exp(-x / tau))
434
+ f(x) = A * (1 - exp(-x / tau)) if 0 <= x < L
262
435
 
263
- if 0 <= x < L
436
+ f(x) = A * exp(-(x - L) / gamma) if x >= L
264
437
 
265
- and
266
-
267
- f(x) = A * exp(-(x - L) / gamma)
268
-
269
- if x >= L
270
-
271
- where A, tau, and gamma are parameters.
438
+ where A, tau, and gamma are parameters, and L is the length of the top plateau.
272
439
 
273
440
  Parameters
274
441
  ----------
275
- x: float or array-like
276
- The point or vector at which to evaluate the trapezoidal function.
277
- toplength: float
442
+ x : float or NDArray
443
+ The point or vector at which to evaluate the trapezoidal function.
444
+ toplength : float
278
445
  The length of the top plateau of the trapezoid.
279
- p: list or tuple of floats
280
- A list of four values [A, tau, gamma, L].
446
+ p : NDArray
447
+ A list or tuple of four values [A, tau, gamma, L] where:
448
+ - A is the amplitude,
449
+ - tau is the time constant for the rising edge,
450
+ - gamma is the time constant for the falling edge,
451
+ - L is the length of the top plateau.
452
+
281
453
  Returns
282
454
  -------
283
- float or array-like
284
- The value of the trapezoidal function at x.
455
+ float or NDArray
456
+ The value of the trapezoidal function at x. Returns a scalar if x is scalar,
457
+ or an array if x is an array.
285
458
 
286
459
  Notes
287
460
  -----
288
461
  This function is vectorized and can handle arrays of input points.
462
+
463
+ Examples
464
+ --------
465
+ >>> import numpy as np
466
+ >>> p = [1.0, 2.0, 3.0, 4.0] # A=1.0, tau=2.0, gamma=3.0, L=4.0
467
+ >>> trapezoid_eval(2.0, 4.0, p)
468
+ 0.3934693402873665
469
+ >>> trapezoid_eval(np.array([1.0, 2.0, 5.0]), 4.0, p)
470
+ array([0.39346934, 0.63212056, 0.22313016])
289
471
  """
290
472
  corrx = x - p[0]
291
473
  if corrx < 0.0:
@@ -297,7 +479,9 @@ def trapezoid_eval(x, toplength, p):
297
479
 
298
480
 
299
481
  @conditionaljit()
300
- def risetime_eval(x, p):
482
+ def risetime_eval(
483
+ x: Union[float, NDArray[np.floating[Any]]], p: NDArray[np.floating[Any]]
484
+ ) -> Union[float, NDArray[np.floating[Any]]]:
301
485
  """
302
486
  Evaluates the rise time function at a given point.
303
487
 
@@ -309,18 +493,33 @@ def risetime_eval(x, p):
309
493
 
310
494
  Parameters
311
495
  ----------
312
- x: float or array-like
496
+ x : float or NDArray
313
497
  The point at which to evaluate the rise time function.
314
- p: list or tuple of floats
315
- A list of two values [A, tau].
498
+ p : NDArray
499
+ An array of three values [x0, A, tau] where:
500
+ - x0: offset parameter
501
+ - A: amplitude parameter
502
+ - tau: time constant parameter
503
+
316
504
  Returns
317
505
  -------
318
- float or array-like
319
- The value of the rise time function at x.
506
+ float or NDArray
507
+ The value of the rise time function at x. Returns 0.0 if x < x0.
320
508
 
321
509
  Notes
322
510
  -----
323
511
  This function is vectorized and can handle arrays of input points.
512
+ The function implements a shifted exponential rise function commonly used
513
+ in signal processing and physics applications.
514
+
515
+ Examples
516
+ --------
517
+ >>> import numpy as np
518
+ >>> p = [1.0, 2.0, 0.5] # x0=1.0, A=2.0, tau=0.5
519
+ >>> risetime_eval(2.0, p)
520
+ 1.2642411176571153
521
+ >>> risetime_eval(np.array([0.5, 1.5, 2.5]), p)
522
+ array([0. , 0.63212056, 1.26424112])
324
523
  """
325
524
  corrx = x - p[0]
326
525
  if corrx < 0.0:
@@ -330,22 +529,71 @@ def risetime_eval(x, p):
330
529
 
331
530
 
332
531
  def gasboxcar(
333
- data,
334
- samplerate,
335
- firstpeakstart,
336
- firstpeakend,
337
- secondpeakstart,
338
- secondpeakend,
339
- risetime=3.0,
340
- falltime=3.0,
341
- ):
532
+ data: NDArray[np.floating[Any]],
533
+ samplerate: float,
534
+ firstpeakstart: float,
535
+ firstpeakend: float,
536
+ secondpeakstart: float,
537
+ secondpeakend: float,
538
+ risetime: float = 3.0,
539
+ falltime: float = 3.0,
540
+ ) -> None:
541
+ """
542
+ Apply gas boxcar filtering to the input data.
543
+
544
+ This function applies a gas boxcar filtering operation to the provided data array,
545
+ which is commonly used in gas detection and analysis applications to smooth and
546
+ enhance specific signal features.
547
+
548
+ Parameters
549
+ ----------
550
+ data : NDArray
551
+ Input data array to be filtered
552
+ samplerate : float
553
+ Sampling rate of the input data in Hz
554
+ firstpeakstart : float
555
+ Start time of the first peak in seconds
556
+ firstpeakend : float
557
+ End time of the first peak in seconds
558
+ secondpeakstart : float
559
+ Start time of the second peak in seconds
560
+ secondpeakend : float
561
+ End time of the second peak in seconds
562
+ risetime : float, optional
563
+ Rise time parameter for the boxcar filter in seconds, default is 3.0
564
+ falltime : float, optional
565
+ Fall time parameter for the boxcar filter in seconds, default is 3.0
566
+
567
+ Returns
568
+ -------
569
+ None
570
+ This function modifies the input data in-place and returns None
571
+
572
+ Notes
573
+ -----
574
+ The gas boxcar filtering operation is designed to enhance gas detection signals
575
+ by applying specific filtering parameters based on the peak timing information.
576
+ The function assumes that the input data is properly formatted and that the
577
+ time parameters are within the valid range of the data.
578
+
579
+ Examples
580
+ --------
581
+ >>> import numpy as np
582
+ >>> data = np.random.rand(1000)
583
+ >>> gasboxcar(data, samplerate=100.0, firstpeakstart=10.0,
584
+ ... firstpeakend=15.0, secondpeakstart=20.0,
585
+ ... secondpeakend=25.0, risetime=2.0, falltime=2.0)
586
+ """
342
587
  return None
343
588
 
344
589
 
345
590
  # generate the polynomial fit timecourse from the coefficients
346
591
  @conditionaljit()
347
- def trendgen(thexvals, thefitcoffs, demean):
348
- """Generates a polynomial trend based on input x-values and coefficients.
592
+ def trendgen(
593
+ thexvals: NDArray[np.floating[Any]], thefitcoffs: NDArray[np.floating[Any]], demean: bool
594
+ ) -> NDArray[np.floating[Any]]:
595
+ """
596
+ Generate a polynomial trend based on input x-values and coefficients.
349
597
 
350
598
  This function constructs a polynomial trend using the provided x-values and
351
599
  a set of polynomial coefficients. The order of the polynomial is determined
@@ -354,10 +602,10 @@ def trendgen(thexvals, thefitcoffs, demean):
354
602
 
355
603
  Parameters
356
604
  ----------
357
- thexvals : array_like
605
+ thexvals : NDArray[np.floating[Any]]
358
606
  The x-values (independent variable) at which to evaluate the polynomial trend.
359
607
  Expected to be a numpy array or similar.
360
- thefitcoffs : array_like
608
+ thefitcoffs : NDArray[np.floating[Any]]
361
609
  A 1D array of polynomial coefficients. The length of this array minus one
362
610
  determines the order of the polynomial. Coefficients are expected to be
363
611
  ordered from the highest power of x down to the constant term (e.g.,
@@ -370,7 +618,7 @@ def trendgen(thexvals, thefitcoffs, demean):
370
618
 
371
619
  Returns
372
620
  -------
373
- numpy.ndarray
621
+ NDArray[np.floating[Any]]
374
622
  A numpy array containing the calculated polynomial trend, with the same
375
623
  shape as `thexvals`.
376
624
 
@@ -378,6 +626,16 @@ def trendgen(thexvals, thefitcoffs, demean):
378
626
  -----
379
627
  This function implicitly assumes that `thexvals` is a numpy array or
380
628
  behaves similarly for element-wise multiplication (`np.multiply`).
629
+
630
+ Examples
631
+ --------
632
+ >>> import numpy as np
633
+ >>> x = np.linspace(0, 1, 5)
634
+ >>> coeffs = np.array([1, 0, 1]) # x^2 + 1
635
+ >>> trendgen(x, coeffs, demean=True)
636
+ array([1. , 1.0625, 1.25 , 1.5625, 2. ])
637
+ >>> trendgen(x, coeffs, demean=False)
638
+ array([-0. , -0.0625, -0.25 , -0.5625, -1. ])
381
639
  """
382
640
  theshape = thefitcoffs.shape
383
641
  order = theshape[0] - 1
@@ -393,93 +651,209 @@ def trendgen(thexvals, thefitcoffs, demean):
393
651
 
394
652
 
395
653
  # @conditionaljit()
396
- def detrend(inputdata, order=1, demean=False):
397
- """Estimates and removes a polynomial trend timecourse.
398
-
399
- This routine calculates a polynomial defined by a set of coefficients
400
- at specified time points to create a trend timecourse, and subtracts it
401
- from the input signal. Optionally, it can remove the mean of the input
402
- data as well.
403
-
404
- Parameters
405
- ----------
406
- thetimepoints : numpy.ndarray
407
- A 1D NumPy array of time points at which to evaluate the polynomial.
408
- thecoffs : list or numpy.ndarray
409
- A list or 1D NumPy array of polynomial coefficients, typically in
410
- decreasing order of power (e.g., `[a, b, c]` for `ax^2 + bx + c`).
411
- demean : bool
412
- If True, the mean of the generated trend timecourse will be subtracted,
413
- effectively centering the trend around zero.
414
-
415
- Returns
416
- -------
417
- numpy.ndarray
418
- A 1D NumPy array representing the generated polynomial trend timecourse.
419
-
420
- Notes
421
- -----
422
- - This function utilizes `numpy.polyval` to evaluate the polynomial.
423
- - Requires the `numpy` library.
424
- """
654
+ def detrend(
655
+ inputdata: NDArray[np.floating[Any]], order: int = 1, demean: bool = False
656
+ ) -> NDArray[np.floating[Any]]:
657
+ """
658
+ Estimates and removes a polynomial trend timecourse.
659
+
660
+ This routine calculates a polynomial defined by a set of coefficients
661
+ at specified time points to create a trend timecourse, and subtracts it
662
+ from the input signal. Optionally, it can remove the mean of the input
663
+ data as well.
664
+
665
+ Parameters
666
+ ----------
667
+ inputdata : NDArray[np.floating[Any]]
668
+ A 1D NumPy array of input data from which the trend will be removed.
669
+ order : int, optional
670
+ The order of the polynomial to fit to the data. Default is 1 (linear).
671
+ demean : bool, optional
672
+ If True, the mean of the input data is subtracted before fitting the
673
+ polynomial trend. Default is False.
674
+
675
+ Returns
676
+ -------
677
+ NDArray[np.floating[Any]]
678
+ A 1D NumPy array of the detrended data, with the polynomial trend removed.
679
+
680
+ Notes
681
+ -----
682
+ - This function uses `numpy.polynomial.Polynomial.fit` to fit a polynomial
683
+ to the input data and then evaluates it using `trendgen`.
684
+ - If a `RankWarning` is raised during fitting (e.g., due to insufficient
685
+ data or poor conditioning), the function defaults to a zero-order
686
+ polynomial (constant trend).
687
+ - The time points are centered around zero, ranging from -N/2 to N/2,
688
+ where N is the length of the input data.
689
+
690
+ Examples
691
+ --------
692
+ >>> import numpy as np
693
+ >>> data = np.array([1, 2, 3, 4, 5])
694
+ >>> detrended = detrend(data, order=1)
695
+ >>> print(detrended)
696
+ [0. 0. 0. 0. 0.]
697
+ """
425
698
  thetimepoints = np.arange(0.0, len(inputdata), 1.0) - len(inputdata) / 2.0
426
699
  try:
427
700
  thecoffs = Polynomial.fit(thetimepoints, inputdata, order).convert().coef[::-1]
428
- except np.lib.polynomial.RankWarning:
429
- thecoffs = [0.0, 0.0]
701
+ except np.exceptions.RankWarning:
702
+ thecoffs = np.array([0.0, 0.0])
430
703
  thefittc = trendgen(thetimepoints, thecoffs, demean)
431
704
  return inputdata - thefittc
432
705
 
433
706
 
434
- @conditionaljit()
435
- def findfirstabove(theyvals, thevalue):
707
+ def prewhiten(
708
+ series: NDArray[np.floating[Any]], nlags: Optional[int] = None, debug: bool = False
709
+ ) -> NDArray[np.floating[Any]]:
436
710
  """
437
- Find the index of the first element in an array that is greater than or equal to a specified value.
438
-
439
- This function iterates through the input array `theyvals` and returns the index of the
440
- first element that is greater than or equal to `thevalue`. If no such element exists,
441
- it returns the length of the array.
711
+ Prewhiten a time series using an AR model estimated via statsmodels.
712
+ The resulting series has the same length as the input.
442
713
 
443
714
  Parameters
444
715
  ----------
445
- theyvals : array_like
446
- A 1D array of numeric values to be searched.
447
- thevalue : float or int
448
- The threshold value to compare against elements in `theyvals`.
716
+ series : NDArray[np.floating[Any]]
717
+ Input 1D time series data.
718
+ nlags : int, optional
719
+ Order of the autoregressive model. If None, automatically chosen via AIC.
720
+ Default is None.
721
+ debug : bool, optional
722
+ If True, additional debug information may be printed. Default is False.
723
+
449
724
  Returns
450
725
  -------
451
- int
452
- The index of the first element in `theyvals` that is greater than or equal to `thevalue`.
453
- If no such element exists, returns the length of `theyvals`.
726
+ whitened : NDArray[np.floating[Any]]
727
+ Prewhitened series of the same length as input. The prewhitening removes
728
+ the autoregressive structure from the data, leaving only the residuals.
729
+
730
+ Notes
731
+ -----
732
+ This function fits an AR(p) model to the input series using `statsmodels.tsa.ARIMA`
733
+ and applies the inverse AR filter to prewhiten the data. If `nlags` is not provided,
734
+ the function automatically selects the best model order based on the Akaike Information Criterion (AIC).
454
735
 
455
736
  Examples
456
737
  --------
457
- >>> findfirstabove([1, 2, 3, 4], 3)
458
- 2
459
- >>> findfirstabove([1, 2, 3, 4], 5)
460
- 4
738
+ >>> import numpy as np
739
+ >>> from statsmodels.tsa.arima.model import ARIMA
740
+ >>> series = np.random.randn(100)
741
+ >>> whitened = prewhiten(series)
742
+ >>> print(whitened.shape)
743
+ (100,)
744
+ """
745
+ series = np.asarray(series)
746
+
747
+ # Fit AR(p) model using ARIMA
748
+ if nlags is None:
749
+ best_aic, best_model, best_p = np.inf, None, None
750
+ for p in range(1, min(10, len(series) // 5)):
751
+ try:
752
+ model = sm.tsa.ARIMA(series, order=(p, 0, 0)).fit()
753
+ if model.aic < best_aic:
754
+ best_aic, best_model, best_p = model.aic, model, p
755
+ except Exception:
756
+ continue
757
+ model = best_model
758
+ if model is None:
759
+ raise RuntimeError("Failed to fit any AR model.")
760
+ else:
761
+ model = sm.tsa.ARIMA(series, order=(nlags, 0, 0)).fit()
762
+
763
+ # Extract AR coefficients and apply filter
764
+ ar_params = model.arparams
765
+ b = np.array([1.0]) # numerator (no MA component)
766
+ a = np.r_[1.0, -ar_params] # denominator (AR polynomial)
767
+
768
+ # Apply the inverse AR filter (prewhitening)
769
+ whitened = signal.lfilter(b, a, series)
770
+
771
+ # return whitened, model
772
+ return whitened
773
+
774
+
775
+ def prewhiten2(
776
+ timecourse: NDArray[np.floating[Any]], nlags: int, debug: bool = False, sel: bool = False
777
+ ) -> NDArray[np.floating[Any]]:
461
778
  """
462
- for i in range(0, len(theyvals)):
463
- if theyvals[i] >= thevalue:
464
- return i
465
- return len(theyvals)
779
+ Prewhiten a time course using autoregressive modeling.
780
+
781
+ This function applies prewhitening to a time course by fitting an autoregressive
782
+ model and then applying the corresponding filter to remove temporal autocorrelation.
783
+
784
+ Parameters
785
+ ----------
786
+ timecourse : NDArray[np.floating[Any]]
787
+ Input time course to be prewhitened, shape (n_times,)
788
+ nlags : int
789
+ Number of lags to use for the autoregressive model
790
+ debug : bool, optional
791
+ If True, print model summary and display diagnostic plots, by default False
792
+ sel : bool, optional
793
+ If True, use automatic lag selection, by default False
794
+
795
+ Returns
796
+ -------
797
+ NDArray[np.floating[Any]]
798
+ Prewhitened time course with standardized normalization applied
799
+
800
+ Notes
801
+ -----
802
+ The prewhitening process involves:
803
+ 1. Fitting an autoregressive model to the input time course
804
+ 2. Computing filter coefficients from the model parameters
805
+ 3. Applying the filter using scipy.signal.lfilter
806
+ 4. Standardizing the result using tide_math.stdnormalize
807
+
808
+ When `sel=True`, the function uses `ar_select_order` for automatic lag selection
809
+ instead of using the fixed number of lags specified by `nlags`.
810
+
811
+ Examples
812
+ --------
813
+ >>> import numpy as np
814
+ >>> timecourse = np.random.randn(100)
815
+ >>> whitened = prewhiten2(timecourse, nlags=3)
816
+ >>> # With debugging enabled
817
+ >>> whitened = prewhiten2(timecourse, nlags=3, debug=True)
818
+ """
819
+ if not sel:
820
+ ar_model = AutoReg(timecourse, lags=nlags)
821
+ ar_fit = ar_model.fit()
822
+ else:
823
+ ar_model = ar_select_order(timecourse, nlags)
824
+ ar_model.ar_lags
825
+ ar_fit = ar_model.model.fit()
826
+ if debug:
827
+ print(ar_fit.summary())
828
+ fig = plt.figure(figsize=(16, 9))
829
+ fig = ar_fit.plot_diagnostics(fig=fig, lags=nlags)
830
+ plt.show()
831
+ ar_params = ar_fit.params
832
+
833
+ # The prewhitening filter coefficients are 1 for the numerator and
834
+ # (1, -ar_params[1]) for the denominator
835
+ b = [1]
836
+ a = np.insert(-ar_params[1:], 0, 1)
837
+
838
+ # Apply the filter to prewhiten the signal
839
+ return tide_math.stdnormalize(signal.lfilter(b, a, timecourse))
466
840
 
467
841
 
468
842
  def findtrapezoidfunc(
469
- thexvals,
470
- theyvals,
471
- thetoplength,
472
- initguess=None,
473
- debug=False,
474
- minrise=0.0,
475
- maxrise=200.0,
476
- minfall=0.0,
477
- maxfall=200.0,
478
- minstart=-100.0,
479
- maxstart=100.0,
480
- refine=False,
481
- displayplots=False,
482
- ):
843
+ thexvals: NDArray[np.floating[Any]],
844
+ theyvals: NDArray[np.floating[Any]],
845
+ thetoplength: float,
846
+ initguess: NDArray[np.floating[Any]] | None = None,
847
+ debug: bool = False,
848
+ minrise: float = 0.0,
849
+ maxrise: float = 200.0,
850
+ minfall: float = 0.0,
851
+ maxfall: float = 200.0,
852
+ minstart: float = -100.0,
853
+ maxstart: float = 100.0,
854
+ refine: bool = False,
855
+ displayplots: bool = False,
856
+ ) -> Tuple[float, float, float, float, int]:
483
857
  """
484
858
  Find the best-fitting trapezoidal function parameters to a data set.
485
859
 
@@ -489,13 +863,13 @@ def findtrapezoidfunc(
489
863
 
490
864
  Parameters
491
865
  ----------
492
- thexvals : array_like
866
+ thexvals : NDArray[np.floating[Any]]
493
867
  Independent variable values (time points) for the data.
494
- theyvals : array_like
868
+ theyvals : NDArray[np.floating[Any]]
495
869
  Dependent variable values (signal intensity) corresponding to `thexvals`.
496
870
  thetoplength : float
497
871
  The length of the top plateau of the trapezoid function.
498
- initguess : array_like, optional
872
+ initguess : NDArray[np.floating[Any]], optional
499
873
  Initial guess for [start, amplitude, risetime, falltime].
500
874
  If None, uses defaults based on data statistics.
501
875
  debug : bool, optional
@@ -516,12 +890,30 @@ def findtrapezoidfunc(
516
890
  If True, perform additional refinement steps (not implemented in this version).
517
891
  displayplots : bool, optional
518
892
  If True, display plots during computation (not implemented in this version).
893
+
519
894
  Returns
520
895
  -------
521
896
  tuple of floats
522
897
  The fitted parameters [start, amplitude, risetime, falltime] if successful,
523
898
  or [0.0, 0.0, 0.0, 0.0] if the solution is outside the valid parameter bounds.
524
899
  A fifth value (integer) indicating success (1) or failure (0).
900
+
901
+ Notes
902
+ -----
903
+ The optimization is performed using `scipy.optimize.leastsq` with a residual
904
+ function `trapezoidresiduals`. The function returns a tuple of five elements:
905
+ (start, amplitude, risetime, falltime, success_flag), where success_flag is 1
906
+ if all parameters are within the specified bounds, and 0 otherwise.
907
+
908
+ Examples
909
+ --------
910
+ >>> import numpy as np
911
+ >>> x = np.linspace(0, 10, 100)
912
+ >>> y = trapezoid_eval(x, start=2, amplitude=5, risetime=1, falltime=1, top_length=4)
913
+ >>> y += np.random.normal(0, 0.1, len(y)) # Add noise
914
+ >>> params = findtrapezoidfunc(x, y, thetoplength=4)
915
+ >>> print(params)
916
+ (2.05, 4.98, 1.02, 1.01, 1)
525
917
  """
526
918
  # guess at parameters: risestart, riseamplitude, risetime
527
919
  if initguess is None:
@@ -555,35 +947,68 @@ def findtrapezoidfunc(
555
947
 
556
948
 
557
949
  def findrisetimefunc(
558
- thexvals,
559
- theyvals,
560
- initguess=None,
561
- debug=False,
562
- minrise=0.0,
563
- maxrise=200.0,
564
- minstart=-100.0,
565
- maxstart=100.0,
566
- refine=False,
567
- displayplots=False,
568
- ):
569
- """
950
+ thexvals: NDArray[np.floating[Any]],
951
+ theyvals: NDArray[np.floating[Any]],
952
+ initguess: NDArray[np.floating[Any]] | None = None,
953
+ debug: bool = False,
954
+ minrise: float = 0.0,
955
+ maxrise: float = 200.0,
956
+ minstart: float = -100.0,
957
+ maxstart: float = 100.0,
958
+ refine: bool = False,
959
+ displayplots: bool = False,
960
+ ) -> Tuple[float, float, float, int]:
961
+ """
962
+ Find the rise time of a signal by fitting a model to the data.
963
+
964
+ This function fits a rise time model to the provided signal data using least squares
965
+ optimization. It returns the estimated start time, amplitude, and rise time of the signal,
966
+ along with a success flag indicating whether the fit is within specified bounds.
570
967
 
571
968
  Parameters
572
969
  ----------
573
- thexvals
574
- theyvals
575
- initguess
576
- debug
577
- minrise
578
- maxrise
579
- minstart
580
- maxstart
581
- refine
582
- displayplots
970
+ thexvals : NDArray[np.floating[Any]]
971
+ Array of x-axis values (time or independent variable).
972
+ theyvals : NDArray[np.floating[Any]]
973
+ Array of y-axis values (signal or dependent variable).
974
+ initguess : NDArray[np.floating[Any]] | None, optional
975
+ Initial guess for [start_time, amplitude, rise_time]. If None, defaults are used.
976
+ debug : bool, optional
977
+ If True, prints the x and y values during processing (default is False).
978
+ minrise : float, optional
979
+ Minimum allowed rise time (default is 0.0).
980
+ maxrise : float, optional
981
+ Maximum allowed rise time (default is 200.0).
982
+ minstart : float, optional
983
+ Minimum allowed start time (default is -100.0).
984
+ maxstart : float, optional
985
+ Maximum allowed start time (default is 100.0).
986
+ refine : bool, optional
987
+ Placeholder for future refinement logic (default is False).
988
+ displayplots : bool, optional
989
+ Placeholder for future plotting logic (default is False).
583
990
 
584
991
  Returns
585
992
  -------
993
+ Tuple[float, float, float, int]
994
+ A tuple containing:
995
+ - start_time: Estimated start time of the rise.
996
+ - amplitude: Estimated amplitude of the rise.
997
+ - rise_time: Estimated rise time.
998
+ - success: 1 if the fit is within bounds, 0 otherwise.
586
999
 
1000
+ Notes
1001
+ -----
1002
+ The function uses `scipy.optimize.leastsq` to perform the fitting. The model being fitted
1003
+ is defined in the `risetimeresiduals` function, which must be defined elsewhere in the code.
1004
+
1005
+ Examples
1006
+ --------
1007
+ >>> import numpy as np
1008
+ >>> x = np.linspace(0, 10, 100)
1009
+ >>> y = np.exp(-x / 2) * np.sin(x)
1010
+ >>> start, amp, rise, success = findrisetimefunc(x, y)
1011
+ >>> print(f"Start: {start}, Amplitude: {amp}, Rise Time: {rise}, Success: {success}")
587
1012
  """
588
1013
  # guess at parameters: risestart, riseamplitude, risetime
589
1014
  if initguess is None:
@@ -611,8 +1036,14 @@ def findrisetimefunc(
611
1036
 
612
1037
 
613
1038
  def territorydecomp(
614
- inputmap, template, atlas, inputmask=None, intercept=True, fitorder=1, debug=False
615
- ):
1039
+ inputmap: NDArray[np.floating[Any]],
1040
+ template: NDArray,
1041
+ atlas: NDArray,
1042
+ inputmask: Optional[NDArray] = None,
1043
+ intercept: bool = True,
1044
+ fitorder: int = 1,
1045
+ debug: bool = False,
1046
+ ) -> Tuple[NDArray, NDArray, NDArray]:
616
1047
  """
617
1048
  Decompose an input map into territories defined by an atlas using polynomial regression.
618
1049
 
@@ -623,7 +1054,7 @@ def territorydecomp(
623
1054
 
624
1055
  Parameters
625
1056
  ----------
626
- inputmap : numpy.ndarray
1057
+ inputmap : NDArray[np.floating[Any]]
627
1058
  Input data to be decomposed. Can be 3D or 4D (e.g., time series).
628
1059
  template : numpy.ndarray
629
1060
  Template values corresponding to the spatial locations in `inputmap`.
@@ -640,6 +1071,7 @@ def territorydecomp(
640
1071
  The order of the polynomial to fit for each territory (default: 1).
641
1072
  debug : bool, optional
642
1073
  If True, print debugging information during computation (default: False).
1074
+
643
1075
  Returns
644
1076
  -------
645
1077
  tuple of numpy.ndarray
@@ -658,6 +1090,14 @@ def territorydecomp(
658
1090
  - If `inputmask` is not provided, all voxels are considered valid.
659
1091
  - The number of territories is determined by the maximum value in `atlas`.
660
1092
  - For each territory, a polynomial regression is performed using the template values as predictors.
1093
+
1094
+ Examples
1095
+ --------
1096
+ >>> import numpy as np
1097
+ >>> inputmap = np.random.rand(10, 10, 10)
1098
+ >>> template = np.random.rand(10, 10, 10)
1099
+ >>> atlas = np.ones((10, 10, 10), dtype=int)
1100
+ >>> fitmap, coeffs, r2s = territorydecomp(inputmap, template, atlas)
661
1101
  """
662
1102
  datadims = len(inputmap.shape)
663
1103
  if datadims > 3:
@@ -711,7 +1151,9 @@ def territorydecomp(
711
1151
  evs = []
712
1152
  for order in range(1, fitorder + 1):
713
1153
  evs.append(np.power(template[maskedvoxels], order))
714
- thefit, R2 = mlregress(evs, thismap[maskedvoxels], intercept=intercept)
1154
+ thefit, R2 = mlregress(
1155
+ np.asarray(evs), thismap[maskedvoxels], intercept=intercept
1156
+ )
715
1157
  thecoffs[whichmap, i - 1, :] = np.asarray(thefit[0]).reshape((-1))
716
1158
  theR2s[whichmap, i - 1] = 1.0 * R2
717
1159
  thisfit[maskedvoxels] = mlproject(thecoffs[whichmap, i - 1, :], evs, intercept)
@@ -724,20 +1166,72 @@ def territorydecomp(
724
1166
 
725
1167
 
726
1168
  def territorystats(
727
- inputmap, atlas, inputmask=None, entropybins=101, entropyrange=None, debug=False
728
- ):
1169
+ inputmap: NDArray[np.floating[Any]],
1170
+ atlas: NDArray,
1171
+ inputmask: NDArray | None = None,
1172
+ entropybins: int = 101,
1173
+ entropyrange: Tuple[float, float] | None = None,
1174
+ debug: bool = False,
1175
+ ) -> Tuple[NDArray, NDArray, NDArray, NDArray, NDArray, NDArray, NDArray, NDArray, NDArray]:
729
1176
  """
1177
+ Compute descriptive statistics for regions defined by an atlas within a multi-dimensional input map.
1178
+
1179
+ This function calculates various statistical measures (mean, standard deviation, median, etc.)
1180
+ for each region (territory) defined in the `atlas` array, based on the data in `inputmap`.
1181
+ It supports both single and multi-map inputs, and optionally uses a mask to define valid regions.
730
1182
 
731
1183
  Parameters
732
1184
  ----------
733
- inputmap
734
- atlas
735
- inputmask
736
- debug
1185
+ inputmap : NDArray[np.floating[Any]]
1186
+ Input data array of shape (X, Y, Z) or (X, Y, Z, N), where N is the number of maps.
1187
+ atlas : ndarray
1188
+ Atlas array defining regions of interest, with each region labeled by an integer.
1189
+ Must be the same spatial dimensions as `inputmap`.
1190
+ inputmask : ndarray, optional
1191
+ Boolean or binary mask array of the same shape as `inputmap`. If None, all voxels are considered valid.
1192
+ entropybins : int, default=101
1193
+ Number of bins to use when computing entropy.
1194
+ entropyrange : tuple of float, optional
1195
+ Range (min, max) for histogram binning when computing entropy. If None, uses the full range of data.
1196
+ debug : bool, default=False
1197
+ If True, prints debug information during computation.
737
1198
 
738
1199
  Returns
739
1200
  -------
1201
+ tuple of ndarray
1202
+ A tuple containing:
1203
+ - statsmap : ndarray
1204
+ Zero-initialized array of the same shape as `inputmap`, used for storing statistics.
1205
+ - themeans : ndarray
1206
+ Array of shape (N, max(atlas)) containing the mean values for each region in each map.
1207
+ - thestds : ndarray
1208
+ Array of shape (N, max(atlas)) containing the standard deviations for each region in each map.
1209
+ - themedians : ndarray
1210
+ Array of shape (N, max(atlas)) containing the median values for each region in each map.
1211
+ - themads : ndarray
1212
+ Array of shape (N, max(atlas)) containing the median absolute deviations for each region in each map.
1213
+ - thevariances : ndarray
1214
+ Array of shape (N, max(atlas)) containing the variance values for each region in each map.
1215
+ - theskewnesses : ndarray
1216
+ Array of shape (N, max(atlas)) containing the skewness values for each region in each map.
1217
+ - thekurtoses : ndarray
1218
+ Array of shape (N, max(atlas)) containing the kurtosis values for each region in each map.
1219
+ - theentropies : ndarray
1220
+ Array of shape (N, max(atlas)) containing the entropy values for each region in each map.
740
1221
 
1222
+ Notes
1223
+ -----
1224
+ - The function supports both 3D and 4D input arrays. For 4D arrays, each map is processed separately.
1225
+ - Entropy is computed using the probability distribution from a histogram of voxel values.
1226
+ - If `inputmask` is not provided, all voxels are considered valid.
1227
+ - The `atlas` labels are expected to start from 1, and regions are indexed accordingly.
1228
+
1229
+ Examples
1230
+ --------
1231
+ >>> import numpy as np
1232
+ >>> inputmap = np.random.rand(10, 10, 10)
1233
+ >>> atlas = np.ones((10, 10, 10), dtype=int)
1234
+ >>> statsmap, means, stds, medians, mads, variances, skewnesses, kurtoses, entropies = territorystats(inputmap, atlas)
741
1235
  """
742
1236
  datadims = len(inputmap.shape)
743
1237
  if datadims > 3:
@@ -774,7 +1268,7 @@ def territorystats(
774
1268
  thevoxels = inputmap[np.where(inputmask > 0.0)]
775
1269
  else:
776
1270
  thevoxels = inputmap
777
- entropyrange = [np.min(thevoxels), np.max(thevoxels)]
1271
+ entropyrange = (np.min(thevoxels), np.max(thevoxels))
778
1272
  if debug:
779
1273
  print(f"entropy bins: {entropybins}")
780
1274
  print(f"entropy range: {entropyrange}")
@@ -801,9 +1295,9 @@ def territorystats(
801
1295
  thestds[whichmap, i - 1] = np.std(thismap[maskedvoxels])
802
1296
  themedians[whichmap, i - 1] = np.median(thismap[maskedvoxels])
803
1297
  themads[whichmap, i - 1] = mad(thismap[maskedvoxels])
804
- thevariances[whichmap, i - 1] = moment(thismap[maskedvoxels], moment=2)
805
- theskewnesses[whichmap, i - 1] = moment(thismap[maskedvoxels], moment=3)
806
- thekurtoses[whichmap, i - 1] = moment(thismap[maskedvoxels], moment=4)
1298
+ thevariances[whichmap, i - 1] = moment(thismap[maskedvoxels], order=2)
1299
+ theskewnesses[whichmap, i - 1] = moment(thismap[maskedvoxels], order=3)
1300
+ thekurtoses[whichmap, i - 1] = moment(thismap[maskedvoxels], order=4)
807
1301
  theentropies[whichmap, i - 1] = entropy(
808
1302
  np.histogram(
809
1303
  thismap[maskedvoxels], bins=entropybins, range=entropyrange, density=True
@@ -824,7 +1318,9 @@ def territorystats(
824
1318
 
825
1319
 
826
1320
  @conditionaljit()
827
- def refinepeak_quad(x, y, peakindex, stride=1):
1321
+ def refinepeak_quad(
1322
+ x: NDArray[np.floating[Any]], y: NDArray[np.floating[Any]], peakindex: int, stride: int = 1
1323
+ ) -> Tuple[float, float, float, Optional[bool], bool]:
828
1324
  """
829
1325
  Refine the location and properties of a peak using quadratic interpolation.
830
1326
 
@@ -834,9 +1330,9 @@ def refinepeak_quad(x, y, peakindex, stride=1):
834
1330
 
835
1331
  Parameters
836
1332
  ----------
837
- x : array-like
1333
+ x : NDArray[np.floating[Any]]
838
1334
  Independent variable values (e.g., time points).
839
- y : array-like
1335
+ y : NDArray[np.floating[Any]]
840
1336
  Dependent variable values (e.g., signal intensity) corresponding to `x`.
841
1337
  peakindex : int
842
1338
  Index of the peak in the arrays `x` and `y`.
@@ -866,6 +1362,14 @@ def refinepeak_quad(x, y, peakindex, stride=1):
866
1362
  The function uses a quadratic fit to estimate peak properties. It checks for
867
1363
  valid conditions before performing the fit, including ensuring that the peak
868
1364
  is not at the edge of the data and that it's either a local maximum or minimum.
1365
+
1366
+ Examples
1367
+ --------
1368
+ >>> import numpy as np
1369
+ >>> x = np.linspace(0, 10, 100)
1370
+ >>> y = np.exp(-0.5 * (x - 5)**2) + 0.1 * np.random.random(100)
1371
+ >>> peakloc, peakval, peakwidth, ismax, badfit = refinepeak_quad(x, y, 50, stride=2)
1372
+ >>> print(f"Peak location: {peakloc:.2f}, Peak value: {peakval:.2f}")
869
1373
  """
870
1374
  # first make sure this actually is a peak
871
1375
  ismax = None
@@ -897,28 +1401,138 @@ def refinepeak_quad(x, y, peakindex, stride=1):
897
1401
 
898
1402
  @conditionaljit2()
899
1403
  def findmaxlag_gauss(
900
- thexcorr_x,
901
- thexcorr_y,
902
- lagmin,
903
- lagmax,
904
- widthmax,
905
- edgebufferfrac=0.0,
906
- threshval=0.0,
907
- uthreshval=30.0,
908
- debug=False,
909
- tweaklims=True,
910
- zerooutbadfit=True,
911
- refine=False,
912
- maxguess=0.0,
913
- useguess=False,
914
- searchfrac=0.5,
915
- fastgauss=False,
916
- lagmod=1000.0,
917
- enforcethresh=True,
918
- absmaxsigma=1000.0,
919
- absminsigma=0.1,
920
- displayplots=False,
921
- ):
1404
+ thexcorr_x: NDArray[np.floating[Any]],
1405
+ thexcorr_y: NDArray[np.floating[Any]],
1406
+ lagmin: float,
1407
+ lagmax: float,
1408
+ widthmax: float,
1409
+ edgebufferfrac: float = 0.0,
1410
+ threshval: float = 0.0,
1411
+ uthreshval: float = 30.0,
1412
+ debug: bool = False,
1413
+ tweaklims: bool = True,
1414
+ zerooutbadfit: bool = True,
1415
+ refine: bool = False,
1416
+ maxguess: float = 0.0,
1417
+ useguess: bool = False,
1418
+ searchfrac: float = 0.5,
1419
+ fastgauss: bool = False,
1420
+ lagmod: float = 1000.0,
1421
+ enforcethresh: bool = True,
1422
+ absmaxsigma: float = 1000.0,
1423
+ absminsigma: float = 0.1,
1424
+ displayplots: bool = False,
1425
+ ) -> Tuple[int, np.float64, np.float64, np.float64, np.uint16, np.uint16, int, int]:
1426
+ """
1427
+ Find the maximum lag in a cross-correlation function by fitting a Gaussian curve to the peak.
1428
+
1429
+ This function locates the peak in a cross-correlation function and optionally fits a Gaussian
1430
+ curve to determine the precise lag time, amplitude, and width. It includes extensive error
1431
+ checking and validation to ensure robust results.
1432
+
1433
+ Parameters
1434
+ ----------
1435
+ thexcorr_x : NDArray[np.floating[Any]]
1436
+ X-axis values (lag times) of the cross-correlation function.
1437
+ thexcorr_y : NDArray[np.floating[Any]]
1438
+ Y-axis values (correlation coefficients) of the cross-correlation function.
1439
+ lagmin : float
1440
+ Minimum allowable lag value in seconds.
1441
+ lagmax : float
1442
+ Maximum allowable lag value in seconds.
1443
+ widthmax : float
1444
+ Maximum allowable width of the Gaussian peak in seconds.
1445
+ edgebufferfrac : float, optional
1446
+ Fraction of array length to exclude from each edge during search. Default is 0.0.
1447
+ threshval : float, optional
1448
+ Minimum correlation threshold for a valid peak. Default is 0.0.
1449
+ uthreshval : float, optional
1450
+ Upper threshold value (currently unused). Default is 30.0.
1451
+ debug : bool, optional
1452
+ Enable debug output showing initial vs final parameter values. Default is False.
1453
+ tweaklims : bool, optional
1454
+ Automatically adjust search limits to avoid edge artifacts. Default is True.
1455
+ zerooutbadfit : bool, optional
1456
+ Set output to zero when fit fails rather than using initial guess. Default is True.
1457
+ refine : bool, optional
1458
+ Perform least-squares refinement of the Gaussian fit. Default is False.
1459
+ maxguess : float, optional
1460
+ Initial guess for maximum lag position. Used when useguess=True. Default is 0.0.
1461
+ useguess : bool, optional
1462
+ Use the provided maxguess instead of finding peak automatically. Default is False.
1463
+ searchfrac : float, optional
1464
+ Fraction of peak height used to determine initial width estimate. Default is 0.5.
1465
+ fastgauss : bool, optional
1466
+ Use fast non-iterative Gaussian fitting (less accurate). Default is False.
1467
+ lagmod : float, optional
1468
+ Modulus for lag values to handle wraparound. Default is 1000.0.
1469
+ enforcethresh : bool, optional
1470
+ Enforce minimum threshold requirements. Default is True.
1471
+ absmaxsigma : float, optional
1472
+ Absolute maximum allowed sigma (width) value. Default is 1000.0.
1473
+ absminsigma : float, optional
1474
+ Absolute minimum allowed sigma (width) value. Default is 0.1.
1475
+ displayplots : bool, optional
1476
+ Show matplotlib plots of data and fitted curve. Default is False.
1477
+
1478
+ Returns
1479
+ -------
1480
+ maxindex : int
1481
+ Array index of the maximum correlation value.
1482
+ maxlag : numpy.float64
1483
+ Time lag at maximum correlation in seconds.
1484
+ maxval : numpy.float64
1485
+ Maximum correlation coefficient value.
1486
+ maxsigma : numpy.float64
1487
+ Width (sigma) of the fitted Gaussian peak.
1488
+ maskval : numpy.uint16
1489
+ Validity mask (1 = valid fit, 0 = invalid fit).
1490
+ failreason : numpy.uint16
1491
+ Bitwise failure reason code. Possible values:
1492
+ - 0x01: Correlation amplitude below threshold
1493
+ - 0x02: Correlation amplitude above maximum (>1.0)
1494
+ - 0x04: Search window too narrow (<3 points)
1495
+ - 0x08: Fitted width exceeds widthmax
1496
+ - 0x10: Fitted lag outside [lagmin, lagmax] range
1497
+ - 0x20: Peak found at edge of search range
1498
+ - 0x40: Fitting procedure failed
1499
+ - 0x80: Initial parameter estimation failed
1500
+ fitstart : int
1501
+ Starting index used for fitting.
1502
+ fitend : int
1503
+ Ending index used for fitting.
1504
+
1505
+ Notes
1506
+ -----
1507
+ - The function assumes cross-correlation data where Y-values represent correlation
1508
+ coefficients (typically in range [-1, 1]).
1509
+ - When refine=False, uses simple peak-finding based on maximum value.
1510
+ - When refine=True, performs least-squares Gaussian fit for sub-bin precision.
1511
+ - All time-related parameters (lagmin, lagmax, widthmax) should be in the same
1512
+ units as thexcorr_x.
1513
+ - The fastgauss option provides faster but less accurate non-iterative fitting.
1514
+
1515
+ Examples
1516
+ --------
1517
+ Basic usage without refinement:
1518
+
1519
+ >>> maxindex, maxlag, maxval, maxsigma, maskval, failreason, fitstart, fitend = \\
1520
+ ... findmaxlag_gauss(lag_times, correlations, -10.0, 10.0, 5.0)
1521
+ >>> if maskval == 1:
1522
+ ... print(f"Peak found at lag: {maxlag:.3f} s, correlation: {maxval:.3f}")
1523
+
1524
+ Advanced usage with refinement:
1525
+
1526
+ >>> maxindex, maxlag, maxval, maxsigma, maskval, failreason, fitstart, fitend = \\
1527
+ ... findmaxlag_gauss(lag_times, correlations, -5.0, 5.0, 2.0,
1528
+ ... refine=True, threshval=0.1, displayplots=True)
1529
+
1530
+ Using an initial guess:
1531
+
1532
+ >>> maxindex, maxlag, maxval, maxsigma, maskval, failreason, fitstart, fitend = \\
1533
+ ... findmaxlag_gauss(lag_times, correlations, -10.0, 10.0, 3.0,
1534
+ ... useguess=True, maxguess=2.5, refine=True)
1535
+ """
922
1536
  """
923
1537
  Find the maximum lag in a cross-correlation function by fitting a Gaussian curve to the peak.
924
1538
 
@@ -928,9 +1542,9 @@ def findmaxlag_gauss(
928
1542
 
929
1543
  Parameters
930
1544
  ----------
931
- thexcorr_x : array_like
1545
+ thexcorr_x : NDArray
932
1546
  X-axis values (lag times) of the cross-correlation function.
933
- thexcorr_y : array_like
1547
+ thexcorr_y : NDArray
934
1548
  Y-axis values (correlation coefficients) of the cross-correlation function.
935
1549
  lagmin : float
936
1550
  Minimum allowable lag value in seconds.
@@ -1127,15 +1741,15 @@ def findmaxlag_gauss(
1127
1741
  )
1128
1742
  if maxsigma_init > widthmax:
1129
1743
  failreason += FML_BADWIDTH
1130
- maxsigma_init = widthmax
1744
+ maxsigma_init = np.float64(widthmax)
1131
1745
  if (maxval_init < threshval) and enforcethresh:
1132
1746
  failreason += FML_BADAMPLOW
1133
1747
  if maxval_init < 0.0:
1134
1748
  failreason += FML_BADAMPLOW
1135
- maxval_init = 0.0
1749
+ maxval_init = np.float64(0.0)
1136
1750
  if maxval_init > 1.0:
1137
1751
  failreason |= FML_BADAMPHIGH
1138
- maxval_init = 1.0
1752
+ maxval_init = np.float64(1.0)
1139
1753
  if failreason > 0:
1140
1754
  maskval = np.uint16(0)
1141
1755
  if failreason > 0 and zerooutbadfit:
@@ -1196,9 +1810,9 @@ def findmaxlag_gauss(
1196
1810
  maskval = np.uint16(0)
1197
1811
  else:
1198
1812
  if maxsigma > absmaxsigma:
1199
- maxsigma = absmaxsigma
1813
+ maxsigma = np.float64(absmaxsigma)
1200
1814
  else:
1201
- maxsigma = absminsigma
1815
+ maxsigma = np.float64(absminsigma)
1202
1816
 
1203
1817
  else:
1204
1818
  maxval = np.float64(maxval_init)
@@ -1244,18 +1858,52 @@ def findmaxlag_gauss(
1244
1858
 
1245
1859
 
1246
1860
  @conditionaljit2()
1247
- def maxindex_noedge(thexcorr_x, thexcorr_y, bipolar=False):
1861
+ def maxindex_noedge(
1862
+ thexcorr_x: NDArray, thexcorr_y: NDArray, bipolar: bool = False
1863
+ ) -> Tuple[int, float]:
1248
1864
  """
1865
+ Find the index of the maximum value in cross-correlation data, avoiding edge effects.
1866
+
1867
+ This function searches for the maximum value in the cross-correlation data while
1868
+ ensuring that the result is not located at the edges of the data array. It handles
1869
+ both unipolar and bipolar cases, returning the index and a flip factor for bipolar
1870
+ cases where the minimum absolute value might be larger than the maximum.
1249
1871
 
1250
1872
  Parameters
1251
1873
  ----------
1252
- thexcorr_x
1253
- thexcorr_y
1254
- bipolar
1874
+ thexcorr_x : NDArray
1875
+ Array containing the x-coordinates of the cross-correlation data
1876
+ thexcorr_y : NDArray
1877
+ Array containing the y-coordinates (cross-correlation values) of the data
1878
+ bipolar : bool, optional
1879
+ If True, considers both positive and negative values when finding the maximum.
1880
+ If False, only considers positive values. Default is False.
1255
1881
 
1256
1882
  Returns
1257
1883
  -------
1884
+ Tuple[int, float]
1885
+ A tuple containing:
1886
+ - int: The index of the maximum value in the cross-correlation data
1887
+ - float: Flip factor (-1.0 if bipolar case and minimum absolute value is larger,
1888
+ 1.0 otherwise)
1889
+
1890
+ Notes
1891
+ -----
1892
+ The function iteratively adjusts the search range to avoid edge effects by
1893
+ incrementing lowerlim when maxindex is 0, and decrementing upperlim when
1894
+ maxindex equals upperlim. This ensures the returned index is not at the boundaries
1895
+ of the input arrays.
1258
1896
 
1897
+ Examples
1898
+ --------
1899
+ >>> import numpy as np
1900
+ >>> x = np.array([0, 1, 2, 3, 4])
1901
+ >>> y = np.array([0.1, 0.5, 0.8, 0.3, 0.2])
1902
+ >>> index, flip = maxindex_noedge(x, y)
1903
+ >>> print(index)
1904
+ 2
1905
+ >>> print(flip)
1906
+ 1.0
1259
1907
  """
1260
1908
  lowerlim = 0
1261
1909
  upperlim = len(thexcorr_x) - 1
@@ -1284,24 +1932,53 @@ def maxindex_noedge(thexcorr_x, thexcorr_y, bipolar=False):
1284
1932
  return maxindex, flipfac
1285
1933
 
1286
1934
 
1287
- def gaussfitsk(height, loc, width, skewness, xvals, yvals):
1935
+ def gaussfitsk(
1936
+ height: float, loc: float, width: float, skewness: float, xvals: ArrayLike, yvals: ArrayLike
1937
+ ) -> NDArray:
1288
1938
  """
1939
+ Fit a skewed Gaussian function to data using least squares optimization.
1940
+
1941
+ This function performs least squares fitting of a skewed Gaussian model to the
1942
+ provided data points. The model includes parameters for height, location, width,
1943
+ and skewness of the Gaussian distribution.
1289
1944
 
1290
1945
  Parameters
1291
1946
  ----------
1292
- height
1293
- loc
1294
- width
1295
- skewness
1296
- xvals
1297
- yvals
1947
+ height : float
1948
+ The amplitude or height of the Gaussian peak.
1949
+ loc : float
1950
+ The location (mean) of the Gaussian peak.
1951
+ width : float
1952
+ The width (standard deviation) of the Gaussian peak.
1953
+ skewness : float
1954
+ The skewness parameter that controls the asymmetry of the Gaussian.
1955
+ xvals : array-like
1956
+ The x-coordinates of the data points to be fitted.
1957
+ yvals : array-like
1958
+ The y-coordinates of the data points to be fitted.
1298
1959
 
1299
1960
  Returns
1300
1961
  -------
1962
+ ndarray
1963
+ Array containing the optimized parameters [height, loc, width, skewness] that
1964
+ best fit the data according to the least squares method.
1301
1965
 
1966
+ Notes
1967
+ -----
1968
+ This function uses `scipy.optimize.leastsq` internally for the optimization
1969
+ process. The fitting is performed using the `gaussskresiduals` residual function
1970
+ which should be defined elsewhere in the codebase.
1971
+
1972
+ Examples
1973
+ --------
1974
+ >>> import numpy as np
1975
+ >>> x = np.linspace(-5, 5, 100)
1976
+ >>> y = gaussfitsk(1.0, 0.0, 1.0, 0.0, x, y_data)
1977
+ >>> print(y)
1978
+ [height_opt, loc_opt, width_opt, skewness_opt]
1302
1979
  """
1303
1980
  plsq, dummy = sp.optimize.leastsq(
1304
- gaussresidualssk,
1981
+ gaussskresiduals,
1305
1982
  np.array([height, loc, width, skewness]),
1306
1983
  args=(yvals, xvals),
1307
1984
  maxfev=5000,
@@ -1309,72 +1986,243 @@ def gaussfitsk(height, loc, width, skewness, xvals, yvals):
1309
1986
  return plsq
1310
1987
 
1311
1988
 
1312
- def gaussfunc(x, height, loc, FWHM):
1989
+ def gaussfunc(x: NDArray, height: float, loc: float, FWHM: float) -> NDArray:
1990
+ """
1991
+ Calculate a Gaussian function.
1992
+
1993
+ This function computes a Gaussian (normal) distribution with specified height,
1994
+ location, and Full Width at Half Maximum (FWHM).
1995
+
1996
+ Parameters
1997
+ ----------
1998
+ x : NDArray
1999
+ Array of values at which to evaluate the Gaussian function.
2000
+ height : float
2001
+ The maximum height of the Gaussian curve.
2002
+ loc : float
2003
+ The location (mean) of the Gaussian curve.
2004
+ FWHM : float
2005
+ The Full Width at Half Maximum of the Gaussian curve.
2006
+
2007
+ Returns
2008
+ -------
2009
+ NDArray
2010
+ Array of Gaussian function values evaluated at x.
2011
+
2012
+ Notes
2013
+ -----
2014
+ The Gaussian function is defined as:
2015
+ f(x) = height * exp(-((x - loc) ** 2) / (2 * (FWHM / 2.355) ** 2))
2016
+
2017
+ The conversion from FWHM to standard deviation (sigma) uses the relationship:
2018
+ sigma = FWHM / (2 * sqrt(2 * log(2))) ≈ FWHM / 2.355
2019
+
2020
+ Examples
2021
+ --------
2022
+ >>> import numpy as np
2023
+ >>> x = np.linspace(-5, 5, 100)
2024
+ >>> y = gaussfunc(x, height=1.0, loc=0.0, FWHM=2.0)
2025
+ >>> print(y.shape)
2026
+ (100,)
2027
+ """
1313
2028
  return height * np.exp(-((x - loc) ** 2) / (2 * (FWHM / 2.355) ** 2))
1314
2029
 
1315
2030
 
1316
- def gaussfit2(height, loc, width, xvals, yvals):
2031
+ def gaussfit2(
2032
+ height: float, loc: float, width: float, xvals: NDArray, yvals: NDArray
2033
+ ) -> Tuple[float, float, float]:
2034
+ """
2035
+ Calculate a Gaussian function.
2036
+
2037
+ This function computes a Gaussian (normal) distribution with specified height,
2038
+ location, and Full Width at Half Maximum (FWHM).
2039
+
2040
+ Parameters
2041
+ ----------
2042
+ x : array_like
2043
+ Input values for which to compute the Gaussian function
2044
+ height : float
2045
+ Height (amplitude) of the Gaussian peak
2046
+ loc : float
2047
+ Location (mean) of the Gaussian peak
2048
+ FWHM : float
2049
+ Full Width at Half Maximum of the Gaussian peak
2050
+
2051
+ Returns
2052
+ -------
2053
+ ndarray
2054
+ Array of Gaussian function values computed at input x values
2055
+
2056
+ Notes
2057
+ -----
2058
+ The Gaussian function is computed using the formula:
2059
+ f(x) = height * exp(-((x - loc)^2) / (2 * (FWHM / 2.355)^2))
2060
+
2061
+ The conversion from FWHM to sigma (standard deviation) uses the relationship:
2062
+ sigma = FWHM / 2.355
2063
+
2064
+ Examples
2065
+ --------
2066
+ >>> import numpy as np
2067
+ >>> x = np.linspace(-5, 5, 100)
2068
+ >>> y = gaussfunc(x, height=1.0, loc=0.0, FWHM=2.0)
2069
+ >>> print(y.shape)
2070
+ (100,)
2071
+ """
1317
2072
  popt, pcov = curve_fit(gaussfunc, xvals, yvals, p0=[height, loc, width])
1318
2073
  return popt[0], popt[1], popt[2]
1319
2074
 
1320
2075
 
1321
- def sincfunc(x, height, loc, FWHM, baseline):
2076
+ def sincfunc(x: NDArray, height: float, loc: float, FWHM: float, baseline: float) -> NDArray:
2077
+ """
2078
+ Compute a scaled and shifted sinc function.
2079
+
2080
+ This function evaluates a sinc function with specified height, location,
2081
+ full width at half maximum, and baseline offset. The sinc function is
2082
+ scaled by a factor that relates the FWHM to the sinc function's natural
2083
+ scaling.
2084
+
2085
+ Parameters
2086
+ ----------
2087
+ x : NDArray
2088
+ Input array of values where the function is evaluated.
2089
+ height : float
2090
+ Height of the sinc function peak.
2091
+ loc : float
2092
+ Location (center) of the sinc function peak.
2093
+ FWHM : float
2094
+ Full width at half maximum of the sinc function.
2095
+ baseline : float
2096
+ Baseline offset added to the sinc function values.
2097
+
2098
+ Returns
2099
+ -------
2100
+ NDArray
2101
+ Array of sinc function values with the same shape as input `x`.
2102
+
2103
+ Notes
2104
+ -----
2105
+ The sinc function is defined as sin(πx)/(πx) with the convention that
2106
+ sinc(0) = 1. The scaling factor 3.79098852 is chosen to relate the FWHM
2107
+ to the natural sinc function properties.
2108
+
2109
+ Examples
2110
+ --------
2111
+ >>> import numpy as np
2112
+ >>> x = np.linspace(-5, 5, 100)
2113
+ >>> y = sincfunc(x, height=2.0, loc=0.0, FWHM=1.0, baseline=1.0)
2114
+ >>> print(y.shape)
2115
+ (100,)
2116
+ """
1322
2117
  return height * np.sinc((3.79098852 / (FWHM * np.pi)) * (x - loc)) + baseline
1323
2118
 
1324
2119
 
1325
2120
  # found this sinc fitting routine (and optimization) here:
1326
2121
  # https://stackoverflow.com/questions/49676116/why-cant-scipy-optimize-curve-fit-fit-my-data-using-a-numpy-sinc-function
1327
- def sincfit(height, loc, width, baseline, xvals, yvals):
2122
+ def sincfit(
2123
+ height: float, loc: float, width: float, baseline: float, xvals: NDArray, yvals: NDArray
2124
+ ) -> Tuple[NDArray, NDArray]:
2125
+ """
2126
+ Sinc function for fitting and modeling.
2127
+
2128
+ This function implements a scaled and shifted sinc function commonly used in
2129
+ signal processing and data fitting applications.
2130
+
2131
+ Parameters
2132
+ ----------
2133
+ x : ndarray
2134
+ Array of x-values where the function is evaluated.
2135
+ height : float
2136
+ Height of the sinc function peak.
2137
+ loc : float
2138
+ Location (center) of the sinc function peak.
2139
+ FWHM : float
2140
+ Full Width at Half Maximum of the sinc function.
2141
+ baseline : float
2142
+ Baseline offset added to the sinc function.
2143
+
2144
+ Returns
2145
+ -------
2146
+ ndarray
2147
+ Array of sinc function values evaluated at x.
2148
+
2149
+ Notes
2150
+ -----
2151
+ The sinc function is defined as sin(πx)/(πx) with the convention that sinc(0) = 1.
2152
+ This implementation uses a scaled version with the specified FWHM parameter.
2153
+
2154
+ Examples
2155
+ --------
2156
+ >>> import numpy as np
2157
+ >>> x = np.linspace(-5, 5, 100)
2158
+ >>> y = sincfunc(x, height=1.0, loc=0.0, FWHM=2.0, baseline=0.0)
2159
+ >>> print(y.shape)
2160
+ (100,)
2161
+ """
1328
2162
  popt, pcov = curve_fit(sincfunc, xvals, yvals, p0=[height, loc, width, baseline])
1329
2163
  return popt, pcov
1330
2164
 
1331
2165
 
1332
- def gaussfit(height, loc, width, xvals, yvals):
1333
- """Performs a non-linear least squares fit of a Gaussian function to data.
1334
-
1335
- This routine uses `scipy.optimize.leastsq` to find the optimal parameters
1336
- (height, location, and width) that best describe a Gaussian curve fitted
1337
- to the provided `yvals` data against `xvals`. It requires an external
1338
- `gaussresiduals` function to compute the residuals.
1339
-
1340
- Parameters
1341
- ----------
1342
- height : float
1343
- Initial guess for the amplitude or peak height of the Gaussian.
1344
- loc : float
1345
- Initial guess for the mean (center) of the Gaussian.
1346
- width : float
1347
- Initial guess for the standard deviation (width) of the Gaussian.
1348
- xvals : numpy.ndarray or list
1349
- The independent variable data points.
1350
- yvals : numpy.ndarray or list
1351
- The dependent variable data points to which the Gaussian will be fitted.
1352
-
1353
- Returns
1354
- -------
1355
- tuple
1356
- A tuple containing the fitted parameters:
1357
- - float: The fitted height of the Gaussian.
1358
- - float: The fitted location (mean) of the Gaussian.
1359
- - float: The fitted width (standard deviation) of the Gaussian.
1360
-
1361
- Notes
1362
- -----
1363
- - This function relies on an external function `gaussresiduals(params, y, x)`
1364
- which should calculate the difference between the observed `y` values and
1365
- the Gaussian function evaluated at `x` with the given `params` (height, loc, width).
1366
- - `scipy.optimize.leastsq` is used for the optimization, which requires
1367
- `scipy` and `numpy` to be imported (e.g., `import scipy.optimize as sp`
1368
- and `import numpy as np`).
1369
- """
2166
+ def gaussfit(
2167
+ height: float, loc: float, width: float, xvals: NDArray, yvals: NDArray
2168
+ ) -> Tuple[float, float, float]:
2169
+ """
2170
+ Performs a non-linear least squares fit of a Gaussian function to data.
2171
+
2172
+ This routine uses `scipy.optimize.leastsq` to find the optimal parameters
2173
+ (height, location, and width) that best describe a Gaussian curve fitted
2174
+ to the provided `yvals` data against `xvals`. It requires an external
2175
+ `gaussresiduals` function to compute the residuals.
2176
+
2177
+ Parameters
2178
+ ----------
2179
+ height : float
2180
+ Initial guess for the amplitude or peak height of the Gaussian.
2181
+ loc : float
2182
+ Initial guess for the mean (center) of the Gaussian.
2183
+ width : float
2184
+ Initial guess for the standard deviation (width) of the Gaussian.
2185
+ xvals : NDArray
2186
+ The independent variable data points.
2187
+ yvals : NDArray
2188
+ The dependent variable data points to which the Gaussian will be fitted.
2189
+
2190
+ Returns
2191
+ -------
2192
+ tuple of float
2193
+ A tuple containing the fitted parameters:
2194
+ - height: The fitted height (amplitude) of the Gaussian.
2195
+ - loc: The fitted location (mean) of the Gaussian.
2196
+ - width: The fitted width (standard deviation) of the Gaussian.
2197
+
2198
+ Notes
2199
+ -----
2200
+ - This function relies on an external function `gaussresiduals(params, y, x)`
2201
+ which should calculate the difference between the observed `y` values and
2202
+ the Gaussian function evaluated at `x` with the given `params` (height, loc, width).
2203
+ - `scipy.optimize.leastsq` is used for the optimization, which requires
2204
+ `scipy` and `numpy` to be imported (e.g., `import scipy.optimize as sp`
2205
+ and `import numpy as np`).
2206
+ - The optimization may fail if initial guesses are too far from the true values
2207
+ or if the data does not well-support a Gaussian fit.
2208
+
2209
+ Examples
2210
+ --------
2211
+ >>> import numpy as np
2212
+ >>> x = np.linspace(-5, 5, 100)
2213
+ >>> y = 2 * np.exp(-0.5 * ((x - 1) / 0.5)**2) + np.random.normal(0, 0.1, 100)
2214
+ >>> height, loc, width = gaussfit(1.0, 0.0, 1.0, x, y)
2215
+ >>> print(f"Fitted height: {height:.2f}, location: {loc:.2f}, width: {width:.2f}")
2216
+ """
1370
2217
  plsq, dummy = sp.optimize.leastsq(
1371
2218
  gaussresiduals, np.array([height, loc, width]), args=(yvals, xvals), maxfev=5000
1372
2219
  )
1373
2220
  return plsq[0], plsq[1], plsq[2]
1374
2221
 
1375
2222
 
1376
- def gram_schmidt(theregressors, debug=False):
1377
- r"""Performs Gram-Schmidt orthogonalization on a set of vectors.
2223
+ def gram_schmidt(theregressors: NDArray, debug: bool = False) -> NDArray:
2224
+ """
2225
+ Performs Gram-Schmidt orthogonalization on a set of vectors.
1378
2226
 
1379
2227
  This routine takes a set of input vectors (rows of a 2D array) and
1380
2228
  transforms them into an orthonormal basis using the Gram-Schmidt process.
@@ -1382,29 +2230,45 @@ def gram_schmidt(theregressors, debug=False):
1382
2230
  have a unit norm. Linearly dependent vectors are effectively skipped
1383
2231
  if their orthogonal component is negligible.
1384
2232
 
1385
- Args:
1386
- theregressors (numpy.ndarray): A 2D NumPy array where each row
1387
- represents a vector to be orthogonalized.
1388
- debug (bool, optional): If True, prints debug information about
1389
- input and output dimensions. Defaults to False.
2233
+ Parameters
2234
+ ----------
2235
+ theregressors : numpy.ndarray
2236
+ A 2D NumPy array where each row represents a vector to be orthogonalized.
2237
+ debug : bool, optional
2238
+ If True, prints debug information about input and output dimensions.
2239
+ Default is False.
2240
+
2241
+ Returns
2242
+ -------
2243
+ numpy.ndarray
2244
+ A 2D NumPy array representing the orthonormal basis. Each row is an
2245
+ orthonormal vector. The number of rows may be less than the input if
2246
+ some vectors were linearly dependent.
1390
2247
 
1391
- Returns:
1392
- numpy.ndarray: A 2D NumPy array representing the orthonormal basis.
1393
- Each row is an orthonormal vector. The number of rows may be
1394
- less than the input if some vectors were linearly dependent.
2248
+ Notes
2249
+ -----
2250
+ - The function normalizes each orthogonalized vector to unit length.
2251
+ - A small tolerance (1e-10) is used to check if a vector's orthogonal
2252
+ component is effectively zero, indicating linear dependence.
2253
+ - Requires the `numpy` library for array operations and linear algebra.
1395
2254
 
1396
- Notes:
1397
- - The function normalizes each orthogonalized vector to unit length.
1398
- - A small tolerance (1e-10) is used to check if a vector's orthogonal
1399
- component is effectively zero, indicating linear dependence.
1400
- - Requires the `numpy` library for array operations and linear algebra.
2255
+ Examples
2256
+ --------
2257
+ >>> import numpy as np
2258
+ >>> vectors = np.array([[2, 1], [3, 4]])
2259
+ >>> basis = gram_schmidt(vectors)
2260
+ >>> print(basis)
2261
+ [[0.89442719 0.4472136 ]
2262
+ [-0.4472136 0.89442719]]
1401
2263
  """
1402
2264
 
1403
2265
  if debug:
1404
2266
  print("gram_schmidt, input dimensions:", theregressors.shape)
1405
- basis = []
2267
+ basis: list[float] = []
1406
2268
  for i in range(theregressors.shape[0]):
1407
- w = theregressors[i, :] - np.sum(np.dot(theregressors[i, :], b) * b for b in basis)
2269
+ w = theregressors[i, :] - np.sum(
2270
+ np.array(np.dot(theregressors[i, :], b) * b) for b in basis
2271
+ )
1408
2272
  if (np.fabs(w) > 1e-10).any():
1409
2273
  basis.append(w / np.linalg.norm(w))
1410
2274
  outputbasis = np.array(basis)
@@ -1413,36 +2277,51 @@ def gram_schmidt(theregressors, debug=False):
1413
2277
  return outputbasis
1414
2278
 
1415
2279
 
1416
- def mlproject(thefit, theevs, intercept):
1417
- r"""Calculates a linear combination (weighted sum) of explanatory variables.
2280
+ def mlproject(thefit: NDArray, theevs: list, intercept: bool) -> NDArray:
2281
+ """
2282
+ Calculates a linear combination (weighted sum) of explanatory variables.
1418
2283
 
1419
2284
  This routine computes a predicted output by multiplying a set of
1420
2285
  explanatory variables by corresponding coefficients and summing the results.
1421
2286
  It can optionally include an intercept term. This is a common operation
1422
2287
  in linear regression and other statistical models.
1423
2288
 
1424
- Args:
1425
- thefit (numpy.ndarray or list): A 1D array or list of coefficients
1426
- (weights) to be applied to the explanatory variables. If `intercept`
1427
- is True, the first element of `thefit` is treated as the intercept.
1428
- theevs (list of numpy.ndarray): A list where each element is a 1D NumPy
1429
- array representing an explanatory variable (feature time series).
1430
- The length of `theevs` should match the number of non-intercept
1431
- coefficients in `thefit`.
1432
- intercept (bool): If True, the first element of `thefit` is used as
1433
- an intercept term, and the remaining elements of `thefit` are
1434
- applied to `theevs`. If False, no intercept is added, and all
1435
- elements of `thefit` are applied to `theevs` starting from the
1436
- first element.
1437
-
1438
- Returns:
1439
- numpy.ndarray: A 1D NumPy array representing the calculated linear
1440
- combination. Its length will be the same as the explanatory variables.
1441
-
1442
- Notes:
1443
- The calculation performed is conceptually equivalent to:
1444
- `output = intercept_term + (coefficient_1 * ev_1) + (coefficient_2 * ev_2) + ...`
1445
- where `intercept_term` is `thefit[0]` if `intercept` is True, otherwise 0.
2289
+ Parameters
2290
+ ----------
2291
+ thefit : NDArray
2292
+ A 1D array or list of coefficients (weights) to be applied to the
2293
+ explanatory variables. If `intercept` is True, the first element of
2294
+ `thefit` is treated as the intercept.
2295
+ theevs : list of numpy.ndarray
2296
+ A list where each element is a 1D NumPy array representing an
2297
+ explanatory variable (feature time series). The length of `theevs`
2298
+ should match the number of non-intercept coefficients in `thefit`.
2299
+ intercept : bool
2300
+ If True, the first element of `thefit` is used as an intercept term,
2301
+ and the remaining elements of `thefit` are applied to `theevs`. If False,
2302
+ no intercept is added, and all elements of `thefit` are applied to
2303
+ `theevs` starting from the first element.
2304
+
2305
+ Returns
2306
+ -------
2307
+ NDArray
2308
+ A 1D NumPy array representing the calculated linear combination.
2309
+ Its length will be the same as the explanatory variables.
2310
+
2311
+ Notes
2312
+ -----
2313
+ The calculation performed is conceptually equivalent to:
2314
+ `output = intercept_term + (coefficient_1 * ev_1) + (coefficient_2 * ev_2) + ...`
2315
+ where `intercept_term` is `thefit[0]` if `intercept` is True, otherwise 0.
2316
+
2317
+ Examples
2318
+ --------
2319
+ >>> import numpy as np
2320
+ >>> thefit = np.array([1.0, 2.0, 3.0])
2321
+ >>> theevs = [np.array([1, 2, 3]), np.array([4, 5, 6])]
2322
+ >>> result = mlproject(thefit, theevs, intercept=True)
2323
+ >>> print(result)
2324
+ [ 9. 14. 19.]
1446
2325
  """
1447
2326
 
1448
2327
  thedest = theevs[0] * 0.0
@@ -1456,29 +2335,46 @@ def mlproject(thefit, theevs, intercept):
1456
2335
  return thedest
1457
2336
 
1458
2337
 
1459
- def olsregress(X, y, intercept=True, debug=False):
2338
+ def olsregress(
2339
+ X: ArrayLike, y: ArrayLike, intercept: bool = True, debug: bool = False
2340
+ ) -> Tuple[NDArray, float]:
1460
2341
  """
2342
+ Perform ordinary least squares regression.
1461
2343
 
1462
2344
  Parameters
1463
2345
  ----------
1464
- X
1465
- y
1466
- intercept
2346
+ X : array-like
2347
+ Independent variables (features) matrix of shape (n_samples, n_features).
2348
+ y : array-like
2349
+ Dependent variable (target) vector of shape (n_samples,).
2350
+ intercept : bool, optional
2351
+ Whether to add a constant term (intercept) to the model. Default is True.
2352
+ debug : bool, optional
2353
+ Whether to enable debug mode. Default is False.
1467
2354
 
1468
2355
  Returns
1469
2356
  -------
2357
+ tuple
2358
+ A tuple containing:
2359
+ - params : ndarray
2360
+ Estimated regression coefficients (including intercept if specified)
2361
+ - rsquared : float
2362
+ Square root of the coefficient of determination (R-squared)
1470
2363
 
1471
- """
1472
- """Return the coefficients from a multiple linear regression, along with R, the coefficient of determination.
1473
-
1474
- X: The independent variables (nxp).
1475
- y: The dependent variable (1xn or nx1).
1476
- intercept: Specifies whether or not the slope intercept should be considered.
1477
-
1478
- The routine computes the coefficients (b_0, b_1, ..., b_p) from the data (x,y) under
1479
- the assumption that y = b0 + b_1 * x_1 + b_2 * x_2 + ... + b_p * x_p.
2364
+ Notes
2365
+ -----
2366
+ This function uses statsmodels OLS regression to fit a linear model.
2367
+ If intercept is True, a constant term is added to the design matrix.
2368
+ The function returns the regression parameters and the square root of R-squared.
1480
2369
 
1481
- If intercept is False, the routine assumes that b0 = 0 and returns (b_1, b_2, ..., b_p).
2370
+ Examples
2371
+ --------
2372
+ >>> import numpy as np
2373
+ >>> X = np.array([[1, 2], [3, 4], [5, 6]])
2374
+ >>> y = np.array([1, 2, 3])
2375
+ >>> params, r_squared = olsregress(X, y)
2376
+ >>> print(params)
2377
+ [0.1 0.4 0.2]
1482
2378
  """
1483
2379
  if intercept:
1484
2380
  X = sm.add_constant(X, prepend=True)
@@ -1487,29 +2383,59 @@ def olsregress(X, y, intercept=True, debug=False):
1487
2383
  return thefit.params, np.sqrt(thefit.rsquared)
1488
2384
 
1489
2385
 
1490
- def mlregress(X, y, intercept=True, debug=False):
2386
+ # @conditionaljit()
2387
+ def mlregress(
2388
+ X: NDArray[np.floating[Any]],
2389
+ y: NDArray[np.floating[Any]],
2390
+ intercept: bool = True,
2391
+ debug: bool = False,
2392
+ ) -> Tuple[NDArray[np.floating[Any]], float]:
1491
2393
  """
2394
+ Perform multiple linear regression and return coefficients and R-squared value.
2395
+
2396
+ This function fits a multiple linear regression model to the input data and
2397
+ returns the regression coefficients (including intercept if specified) along
2398
+ with the coefficient of determination (R-squared).
1492
2399
 
1493
2400
  Parameters
1494
2401
  ----------
1495
- x
1496
- y
1497
- intercept
2402
+ X : NDArray[np.floating[Any]]
2403
+ Input feature matrix of shape (n_samples, n_features) or (n_samples,)
2404
+ If 1D array is provided, it will be treated as a single feature.
2405
+ y : NDArray[np.floating[Any]]
2406
+ Target values of shape (n_samples,) or (n_samples, 1)
2407
+ If 1D array is provided, it will be treated as a single target.
2408
+ intercept : bool, optional
2409
+ Whether to calculate and include intercept term in the model.
2410
+ Default is True.
2411
+ debug : bool, optional
2412
+ If True, print debug information about the input shapes and processing steps.
2413
+ Default is False.
1498
2414
 
1499
2415
  Returns
1500
2416
  -------
2417
+ Tuple[NDArray[np.floating[Any]], float]
2418
+ A tuple containing:
2419
+ - coefficients : NDArray[np.floating[Any]] of shape (n_features + 1, 1) where the first
2420
+ element is the intercept (if intercept=True) and subsequent elements
2421
+ are the regression coefficients for each feature
2422
+ - R2 : float, the coefficient of determination (R-squared) of the fitted model
1501
2423
 
1502
- """
1503
- """Return the coefficients from a multiple linear regression, along with R, the coefficient of determination.
1504
-
1505
- x: The independent variables (pxn or nxp).
1506
- y: The dependent variable (1xn or nx1).
1507
- intercept: Specifies whether or not the slope intercept should be considered.
1508
-
1509
- The routine computes the coefficients (b_0, b_1, ..., b_p) from the data (x,y) under
1510
- the assumption that y = b0 + b_1 * x_1 + b_2 * x_2 + ... + b_p * x_p.
2424
+ Notes
2425
+ -----
2426
+ The function automatically handles shape adjustments for input arrays,
2427
+ ensuring that the number of samples in X matches the number of target values in y.
2428
+ If the input X is 1D, it will be converted to 2D. If the shapes don't match initially,
2429
+ the function will attempt to transpose X to match the number of samples in y.
1511
2430
 
1512
- If intercept is False, the routine assumes that b0 = 0 and returns (b_1, b_2, ..., b_p).
2431
+ Examples
2432
+ --------
2433
+ >>> import numpy as np
2434
+ >>> X = np.array([[1, 2], [3, 4], [5, 6]])
2435
+ >>> y = np.array([3, 7, 11])
2436
+ >>> coeffs, r2 = mlregress(X, y)
2437
+ >>> print(f"Coefficients: {coeffs.flatten()}")
2438
+ >>> print(f"R-squared: {r2}")
1513
2439
  """
1514
2440
  if debug:
1515
2441
  print(f"mlregress initial: {X.shape=}, {y.shape=}")
@@ -1542,51 +2468,77 @@ def mlregress(X, y, intercept=True, debug=False):
1542
2468
 
1543
2469
 
1544
2470
  def calcexpandedregressors(
1545
- confounddict, labels=None, start=0, end=-1, deriv=True, order=1, debug=False
1546
- ):
1547
- r"""Calculates expanded regressors from a dictionary of confound vectors.
2471
+ confounddict: dict,
2472
+ labels: Optional[list] = None,
2473
+ start: int = 0,
2474
+ end: int = -1,
2475
+ deriv: bool = True,
2476
+ order: int = 1,
2477
+ debug: bool = False,
2478
+ ) -> Tuple[NDArray, list]:
2479
+ """
2480
+ Calculate expanded regressors from a dictionary of confound vectors.
1548
2481
 
1549
2482
  This routine generates a comprehensive set of motion-related regressors by
1550
2483
  including higher-order polynomial terms and derivatives of the original
1551
2484
  confound timecourses. It is commonly used in neuroimaging analysis to
1552
2485
  account for subject movement.
1553
2486
 
1554
- Args:
1555
- confounddict (dict): A dictionary where keys are labels (e.g., 'rot_x',
1556
- 'trans_y') and values are the corresponding 1D time series (NumPy
1557
- arrays or lists).
1558
- labels (list, optional): A list of specific confound labels from
1559
- `confounddict` to process. If None, all labels in `confounddict`
1560
- will be used. Defaults to None.
1561
- start (int, optional): The starting index (inclusive) for slicing the
1562
- timecourses. Defaults to 0.
1563
- end (int, optional): The ending index (exclusive) for slicing the
1564
- timecourses. If None, slicing continues to the end of the timecourse.
1565
- Defaults to None.
1566
- deriv (bool, optional): If True, the first derivative of each selected
1567
- timecourse (and its polynomial expansions) is calculated and
1568
- included as a regressor. Defaults to False.
1569
- order (int, optional): The polynomial order for expansion. If `order > 1`,
1570
- terms like `label^2`, `label^3`, up to `label^order` will be
1571
- included. Defaults to 1 (no polynomial expansion).
1572
- debug (bool, optional): If True, prints debug information during
1573
- processing. Defaults to False.
1574
-
1575
- Returns:
1576
- tuple: A tuple containing:
1577
- - outputregressors (numpy.ndarray): A 2D NumPy array where each row
1578
- represents a generated regressor (original, polynomial, or derivative)
1579
- and columns represent time points.
1580
- - outlabels (list): A list of strings, providing the labels for each
1581
- row in `outputregressors`, indicating what each regressor represents
1582
- (e.g., 'rot_x', 'rot_x^2', 'rot_x_deriv').
1583
-
1584
- Notes:
1585
- - The derivatives are calculated using `numpy.gradient`.
1586
- - The function handles slicing of the timecourses based on `start` and `end`
1587
- parameters.
1588
- - The output regressors are concatenated horizontally to form the final
1589
- `outputregressors` array.
2487
+ Parameters
2488
+ ----------
2489
+ confounddict : dict
2490
+ A dictionary where keys are labels (e.g., 'rot_x', 'trans_y') and values
2491
+ are the corresponding 1D time series (NumPy arrays or lists).
2492
+ labels : list, optional
2493
+ A list of specific confound labels from `confounddict` to process. If None,
2494
+ all labels in `confounddict` will be used. Default is None.
2495
+ start : int, optional
2496
+ The starting index (inclusive) for slicing the timecourses. Default is 0.
2497
+ end : int, optional
2498
+ The ending index (exclusive) for slicing the timecourses. If -1, slicing
2499
+ continues to the end of the timecourse. Default is -1.
2500
+ deriv : bool, optional
2501
+ If True, the first derivative of each selected timecourse (and its
2502
+ polynomial expansions) is calculated and included as a regressor.
2503
+ Default is True.
2504
+ order : int, optional
2505
+ The polynomial order for expansion. If `order > 1`, terms like `label^2`,
2506
+ `label^3`, up to `label^order` will be included. Default is 1 (no
2507
+ polynomial expansion).
2508
+ debug : bool, optional
2509
+ If True, prints debug information during processing. Default is False.
2510
+
2511
+ Returns
2512
+ -------
2513
+ tuple of (numpy.ndarray, list)
2514
+ A tuple containing:
2515
+ - outputregressors : numpy.ndarray
2516
+ A 2D NumPy array where each row represents a generated regressor
2517
+ (original, polynomial, or derivative) and columns represent time points.
2518
+ - outlabels : list of str
2519
+ A list of strings providing the labels for each row in `outputregressors`,
2520
+ indicating what each regressor represents (e.g., 'rot_x', 'rot_x^2',
2521
+ 'rot_x_deriv').
2522
+
2523
+ Notes
2524
+ -----
2525
+ - The derivatives are calculated using `numpy.gradient`.
2526
+ - The function handles slicing of the timecourses based on `start` and `end`
2527
+ parameters.
2528
+ - The output regressors are concatenated horizontally to form the final
2529
+ `outputregressors` array.
2530
+
2531
+ Examples
2532
+ --------
2533
+ >>> confounddict = {
2534
+ ... 'rot_x': [0.1, 0.2, 0.3],
2535
+ ... 'trans_y': [0.05, 0.1, 0.15]
2536
+ ... }
2537
+ >>> regressors, labels = calcexpandedregressors(confounddict, order=2, deriv=True)
2538
+ >>> print(regressors.shape)
2539
+ (4, 3)
2540
+ >>> print(labels)
2541
+ ['rot_x', 'trans_y', 'rot_x^2', 'trans_y^2', 'rot_x_deriv', 'trans_y_deriv']
1590
2542
  """
1591
2543
  if labels is None:
1592
2544
  localconfounddict = confounddict.copy()
@@ -1635,30 +2587,60 @@ def calcexpandedregressors(
1635
2587
  activecolumn += 1
1636
2588
  return outputregressors, outlabels
1637
2589
 
2590
+ @conditionaljit()
2591
+ def derivativelinfitfilt(
2592
+ thedata: NDArray, theevs: NDArray, nderivs: int = 1, debug: bool = False
2593
+ ) -> Tuple[NDArray, NDArray, NDArray, float, NDArray]:
2594
+ """
2595
+ Perform multicomponent expansion on explanatory variables and fit the data using linear regression.
1638
2596
 
1639
- def derivativelinfitfilt(thedata, theevs, nderivs=1, debug=False):
1640
- r"""First perform multicomponent expansion on theevs (each ev replaced by itself,
1641
- its square, its cube, etc.). Then perform a linear fit of thedata using the vectors
1642
- in thenewevs and return the result.
2597
+ First, each explanatory variable is expanded into multiple components by taking
2598
+ successive derivatives (or powers, in the case of scalar inputs). Then, a linear
2599
+ fit is performed on the input data using the expanded set of explanatory variables.
1643
2600
 
1644
2601
  Parameters
1645
2602
  ----------
1646
- thedata : 1D numpy array
1647
- Input data of length N to be filtered
1648
- :param thedata:
2603
+ thedata : NDArray
2604
+ Input data of length N to be filtered.
2605
+ theevs : NDArray
2606
+ NxP array of explanatory variables to be fit. If 1D, it is treated as a single
2607
+ explanatory variable.
2608
+ nderivs : int, optional
2609
+ Number of derivative components to compute for each explanatory variable.
2610
+ Default is 1. For each input variable, this creates a sequence of
2611
+ derivatives: original, first derivative, second derivative, etc.
2612
+ debug : bool, optional
2613
+ Flag to toggle debugging output. Default is False.
1649
2614
 
1650
- theevs : 2D numpy array
1651
- NxP array of explanatory variables to be fit
1652
- :param theevs:
2615
+ Returns
2616
+ -------
2617
+ tuple
2618
+ A tuple containing:
2619
+ - filtered : ndarray
2620
+ The filtered version of `thedata` after fitting.
2621
+ - thenewevs : ndarray
2622
+ The expanded set of explanatory variables (original + derivatives).
2623
+ - datatoremove : ndarray
2624
+ The part of the data that was removed during fitting.
2625
+ - R : float
2626
+ The coefficient of determination (R²) of the fit.
2627
+ - coffs : ndarray
2628
+ The coefficients of the linear fit.
1653
2629
 
1654
- nderivs : integer
1655
- Number of components to use for each ev. Each successive component is a
1656
- higher power of the initial ev (initial, square, cube, etc.)
1657
- :param nderivs:
2630
+ Notes
2631
+ -----
2632
+ This function is useful for filtering data when the underlying signal is expected
2633
+ to have smooth variations, and derivative information can improve the fit.
2634
+ The expansion of each variable into its derivatives allows for better modeling
2635
+ of local trends in the data.
1658
2636
 
1659
- debug: bool
1660
- Flag to toggle debugging output
1661
- :param debug:
2637
+ Examples
2638
+ --------
2639
+ >>> import numpy as np
2640
+ >>> from typing import Tuple
2641
+ >>> thedata = np.array([1, 2, 3, 4, 5])
2642
+ >>> theevs = np.array([[1, 2], [2, 3], [3, 4], [4, 5], [5, 6]])
2643
+ >>> filtered, expanded_ev, removed, R, coeffs = derivativelinfitfilt(thedata, theevs, nderivs=2)
1662
2644
  """
1663
2645
  if debug:
1664
2646
  print(f"{thedata.shape=}")
@@ -1682,36 +2664,65 @@ def derivativelinfitfilt(thedata, theevs, nderivs=1, debug=False):
1682
2664
  if debug:
1683
2665
  print(f"{nderivs=}")
1684
2666
  print(f"{thenewevs.shape=}")
1685
- filtered, datatoremove, R, coffs = linfitfilt(thedata, thenewevs, debug=debug)
2667
+ filtered, datatoremove, R, coffs, dummy = linfitfilt(thedata, thenewevs, debug=debug)
1686
2668
  if debug:
1687
2669
  print(f"{R=}")
1688
2670
 
1689
2671
  return filtered, thenewevs, datatoremove, R, coffs
1690
2672
 
2673
+ @conditionaljit()
2674
+ def expandedlinfitfilt(
2675
+ thedata: NDArray, theevs: NDArray, ncomps: int = 1, debug: bool = False
2676
+ ) -> Tuple[NDArray, NDArray, NDArray, float, NDArray]:
2677
+ """
2678
+ Perform multicomponent expansion on explanatory variables and fit a linear model.
1691
2679
 
1692
- def expandedlinfitfilt(thedata, theevs, ncomps=1, debug=False):
1693
- r"""First perform multicomponent expansion on theevs (each ev replaced by itself,
1694
- its square, its cube, etc.). Then perform a multiple regression fit of thedata using the vectors
1695
- in thenewevs and return the result.
2680
+ First, perform multicomponent expansion on the explanatory variables (`theevs`),
2681
+ where each variable is replaced by itself, its square, its cube, etc., up to `ncomps`
2682
+ components. Then, perform a multiple regression fit of `thedata` using the expanded
2683
+ explanatory variables and return the filtered data, the fitted model components,
2684
+ the residual sum of squares, and the coefficients.
1696
2685
 
1697
2686
  Parameters
1698
2687
  ----------
1699
- thedata : 1D numpy array
1700
- Input data of length N to be filtered
1701
- :param thedata:
2688
+ thedata : NDArray
2689
+ Input data of length N to be filtered.
2690
+ theevs : array_like
2691
+ NxP array of explanatory variables to be fit.
2692
+ ncomps : int, optional
2693
+ Number of components to use for each ev. Each successive component is a
2694
+ higher power of the initial ev (initial, square, cube, etc.). Default is 1.
2695
+ debug : bool, optional
2696
+ Flag to toggle debugging output. Default is False.
1702
2697
 
1703
- theevs : 2D numpy array
1704
- NxP array of explanatory variables to be fit
1705
- :param theevs:
2698
+ Returns
2699
+ -------
2700
+ filtered : ndarray
2701
+ The filtered version of `thedata` after fitting and removing the linear model.
2702
+ thenewevs : ndarray
2703
+ The expanded explanatory variables used in the fit.
2704
+ datatoremove : ndarray
2705
+ The portion of `thedata` that was removed during the fitting process.
2706
+ R : float
2707
+ Residual sum of squares from the linear fit.
2708
+ coffs : ndarray
2709
+ The coefficients of the linear fit.
1706
2710
 
1707
- ncomps : integer
1708
- Number of components to use for each ev. Each successive component is a
1709
- higher power of the initial ev (initial, square, cube, etc.)
1710
- :param ncomps:
2711
+ Notes
2712
+ -----
2713
+ If `ncomps` is 1, no expansion is performed and `theevs` is used directly.
2714
+ For each column in `theevs`, the expanded columns are created by taking powers
2715
+ of the original column (1st, 2nd, ..., ncomps-th power).
1711
2716
 
1712
- debug: bool
1713
- Flag to toggle debugging output
1714
- :param debug:
2717
+ Examples
2718
+ --------
2719
+ >>> import numpy as np
2720
+ >>> from typing import Tuple
2721
+ >>> thedata = np.array([1, 2, 3, 4, 5])
2722
+ >>> theevs = np.array([[1, 2], [2, 3], [3, 4], [4, 5], [5, 6]])
2723
+ >>> filtered, expanded_ev, removed, R, coeffs = expandedlinfitfilt(thedata, theevs, ncomps=2)
2724
+ >>> print(filtered)
2725
+ [0. 0. 0. 0. 0.]
1715
2726
  """
1716
2727
  if debug:
1717
2728
  print(f"{thedata.shape=}")
@@ -1735,30 +2746,63 @@ def expandedlinfitfilt(thedata, theevs, ncomps=1, debug=False):
1735
2746
  if debug:
1736
2747
  print(f"{ncomps=}")
1737
2748
  print(f"{thenewevs.shape=}")
1738
- filtered, datatoremove, R, coffs = linfitfilt(thedata, thenewevs, debug=debug)
2749
+ filtered, datatoremove, R, coffs, dummy = linfitfilt(thedata, thenewevs, debug=debug)
1739
2750
  if debug:
1740
2751
  print(f"{R=}")
1741
2752
 
1742
2753
  return filtered, thenewevs, datatoremove, R, coffs
1743
2754
 
1744
-
1745
- def linfitfilt(thedata, theevs, returnintercept=False, debug=False):
1746
- r"""Performs a multiple regression fit of thedata using the vectors in theevs
2755
+ @conditionaljit()
2756
+ def linfitfilt(
2757
+ thedata: NDArray, theevs: NDArray, debug: bool = False
2758
+ ) -> Tuple[NDArray, NDArray, float, NDArray, float]:
2759
+ """
2760
+ Performs a multiple regression fit of thedata using the vectors in theevs
1747
2761
  and returns the result.
1748
2762
 
2763
+ This function fits a linear model to the input data using the explanatory
2764
+ variables provided in `theevs`, then removes the fitted component from the
2765
+ original data to produce a filtered version.
2766
+
1749
2767
  Parameters
1750
2768
  ----------
1751
- thedata : 1D numpy array
1752
- Input data of length N to be filtered
1753
- :param thedata:
2769
+ thedata : NDArray
2770
+ Input data of length N to be filtered.
2771
+ theevs : NDArray
2772
+ NxP array of explanatory variables to be fit. If 1D, treated as a single
2773
+ explanatory variable.
2774
+ returnintercept : bool, optional
2775
+ If True, also return the intercept term from the regression. Default is False.
2776
+ debug : bool, optional
2777
+ If True, print debugging information during execution. Default is False.
2778
+
2779
+ Returns
2780
+ -------
2781
+ filtered : ndarray
2782
+ The filtered data, i.e., the original data with the fitted component removed.
2783
+ datatoremove : ndarray
2784
+ The component of thedata that was removed during filtering.
2785
+ R2 : float
2786
+ The coefficient of determination (R-squared) of the regression.
2787
+ retcoffs : ndarray
2788
+ The regression coefficients (excluding intercept) for each explanatory variable.
2789
+ theintercept : float, optional
2790
+ The intercept term from the regression. Only returned if `returnintercept=True`.
1754
2791
 
1755
- theevs : 2D numpy array
1756
- NxP array of explanatory variables to be fit
1757
- :param theevs:
2792
+ Notes
2793
+ -----
2794
+ This function uses `mlregress` internally to perform the linear regression.
2795
+ The intercept is always included in the model, but only returned if explicitly
2796
+ requested via `returnintercept`.
1758
2797
 
1759
- debug: bool
1760
- Flag to toggle debugging output
1761
- :param debug:
2798
+ Examples
2799
+ --------
2800
+ >>> import numpy as np
2801
+ >>> thedata = np.array([1, 2, 3, 4, 5])
2802
+ >>> theevs = np.array([[1, 1], [1, 2], [1, 3], [1, 4], [1, 5]])
2803
+ >>> filtered, datatoremove, R2, retcoffs, dummy = linfitfilt(thedata, theevs)
2804
+ >>> print(filtered)
2805
+ [0. 0. 0. 0. 0.]
1762
2806
  """
1763
2807
 
1764
2808
  if debug:
@@ -1790,35 +2834,59 @@ def linfitfilt(thedata, theevs, returnintercept=False, debug=False):
1790
2834
  filtered = thedata - datatoremove
1791
2835
  if debug:
1792
2836
  print(f"{retcoffs=}")
1793
- if returnintercept:
1794
- return filtered, datatoremove, R2, retcoffs, theintercept
1795
- else:
1796
- return filtered, datatoremove, R2, retcoffs
2837
+ return filtered, datatoremove, R2, retcoffs, theintercept
1797
2838
 
1798
2839
 
2840
+ @conditionaljit()
1799
2841
  def confoundregress(
1800
- data,
1801
- regressors,
1802
- debug=False,
1803
- showprogressbar=True,
1804
- rt_floatset=np.float64,
1805
- rt_floattype="float64",
1806
- ):
1807
- r"""Filters multiple regressors out of an array of data
2842
+ data: NDArray,
2843
+ regressors: NDArray,
2844
+ debug: bool = False,
2845
+ showprogressbar: bool = True,
2846
+ rt_floatset: type = np.float64,
2847
+ rt_floattype: str = "float64",
2848
+ ) -> Tuple[NDArray, NDArray]:
2849
+ """
2850
+ Filters multiple regressors out of an array of data using linear regression.
2851
+
2852
+ This function removes the effect of nuisance regressors from each voxel's timecourse
2853
+ by fitting a linear model and subtracting the predicted signal.
1808
2854
 
1809
2855
  Parameters
1810
2856
  ----------
1811
2857
  data : 2d numpy array
1812
- A data array. First index is the spatial dimension, second is the time (filtering) dimension.
1813
-
1814
- regressors: 2d numpy array
1815
- The set of regressors to filter out of each timecourse. The first dimension is the regressor number, second is the time (filtering) dimension:
1816
-
1817
- debug : boolean
1818
- Print additional diagnostic information if True
2858
+ A data array where the first index is the spatial dimension (e.g., voxels),
2859
+ and the second index is the time (filtering) dimension.
2860
+ regressors : 2d numpy array
2861
+ The set of regressors to filter out of each timecourse. The first dimension
2862
+ is the regressor number, and the second is the time (filtering) dimension.
2863
+ debug : bool, optional
2864
+ Print additional diagnostic information if True. Default is False.
2865
+ showprogressbar : bool, optional
2866
+ Show progress bar during processing. Default is True.
2867
+ rt_floatset : type, optional
2868
+ The data type used for floating-point calculations. Default is np.float64.
2869
+ rt_floattype : str, optional
2870
+ The string representation of the floating-point data type. Default is "float64".
1819
2871
 
1820
2872
  Returns
1821
2873
  -------
2874
+ filtereddata : 2d numpy array
2875
+ The data with regressors removed, same shape as input `data`.
2876
+ r2value : 1d numpy array
2877
+ The R-squared value for each voxel's regression fit, shape (data.shape[0],).
2878
+
2879
+ Notes
2880
+ -----
2881
+ This function uses `mlregress` internally to perform the linear regression for each voxel.
2882
+ The regressors are applied in the order they appear in the input array.
2883
+
2884
+ Examples
2885
+ --------
2886
+ >>> import numpy as np
2887
+ >>> data = np.random.rand(100, 1000)
2888
+ >>> regressors = np.random.rand(3, 1000)
2889
+ >>> filtered_data, r2_values = confoundregress(data, regressors, debug=True)
1822
2890
  """
1823
2891
  if debug:
1824
2892
  print("data shape:", data.shape)
@@ -1850,7 +2918,56 @@ def confoundregress(
1850
2918
  # You can redistribute it and/or modify it under the terms of the Do What The
1851
2919
  # Fuck You Want To Public License, Version 2, as published by Sam Hocevar. See
1852
2920
  # http://www.wtfpl.net/ for more details.
1853
- def getpeaks(xvals, yvals, xrange=None, bipolar=False, displayplots=False):
2921
+ def getpeaks(
2922
+ xvals: NDArray,
2923
+ yvals: NDArray,
2924
+ xrange: Optional[Tuple[float, float]] = None,
2925
+ bipolar: bool = False,
2926
+ displayplots: bool = False,
2927
+ ) -> list:
2928
+ """
2929
+ Find peaks in y-values within a specified range and optionally display results.
2930
+
2931
+ This function identifies local maxima (and optionally minima) in the input
2932
+ y-values and returns their coordinates along with an offset from the origin.
2933
+ It supports filtering by a range of x-values and can handle both unipolar and
2934
+ bipolar peak detection.
2935
+
2936
+ Parameters
2937
+ ----------
2938
+ xvals : NDArray
2939
+ X-axis values corresponding to the y-values.
2940
+ yvals : NDArray
2941
+ Y-axis values where peaks are to be detected.
2942
+ xrange : tuple of float, optional
2943
+ A tuple (min, max) specifying the range of x-values to consider.
2944
+ If None, the full range is used.
2945
+ bipolar : bool, optional
2946
+ If True, detect both positive and negative peaks (minima and maxima).
2947
+ If False, only detect positive peaks.
2948
+ displayplots : bool, optional
2949
+ If True, display a plot showing the data and detected peaks.
2950
+
2951
+ Returns
2952
+ -------
2953
+ list of lists
2954
+ A list of peaks, each represented as [x_value, y_value, offset_from_origin].
2955
+ The offset is calculated using `tide_util.valtoindex` relative to x=0.
2956
+
2957
+ Notes
2958
+ -----
2959
+ - The function uses `scipy.signal.find_peaks` to detect peaks.
2960
+ - If `bipolar` is True, both positive and negative peaks are included.
2961
+ - The `displayplots` option requires `matplotlib.pyplot` to be imported as `plt`.
2962
+
2963
+ Examples
2964
+ --------
2965
+ >>> x = np.linspace(-10, 10, 100)
2966
+ >>> y = np.sin(x)
2967
+ >>> peaks = getpeaks(x, y, xrange=(-5, 5), bipolar=True)
2968
+ >>> print(peaks)
2969
+ [[-1.5707963267948966, 1.0, -25], [1.5707963267948966, 1.0, 25]]
2970
+ """
1854
2971
  peaks, dummy = find_peaks(yvals, height=0)
1855
2972
  if bipolar:
1856
2973
  negpeaks, dummy = find_peaks(-yvals, height=0)
@@ -1898,19 +3015,44 @@ def getpeaks(xvals, yvals, xrange=None, bipolar=False, displayplots=False):
1898
3015
  return procpeaks
1899
3016
 
1900
3017
 
1901
- def parabfit(x_axis, y_axis, peakloc, points):
3018
+ def parabfit(x_axis: NDArray, y_axis: NDArray, peakloc: int, points: int) -> Tuple[float, float]:
1902
3019
  """
3020
+ Fit a parabola to a localized region around a peak and return the peak coordinates.
3021
+
3022
+ This function performs a quadratic curve fitting on a subset of data surrounding
3023
+ a specified peak location. It uses a parabolic model of the form a*(x-tau)^2 + c
3024
+ to estimate the precise peak position and amplitude.
1903
3025
 
1904
3026
  Parameters
1905
3027
  ----------
1906
- x_axis
1907
- y_axis
1908
- peakloc
1909
- peaksize
3028
+ x_axis : NDArray
3029
+ Array of x-axis values (typically time or frequency).
3030
+ y_axis : NDArray
3031
+ Array of y-axis values (typically signal amplitude).
3032
+ peakloc : int
3033
+ Index location of the peak in the data arrays.
3034
+ points : int
3035
+ Number of points to include in the local fit around the peak.
1910
3036
 
1911
3037
  Returns
1912
3038
  -------
3039
+ Tuple[float, float]
3040
+ A tuple containing (x_peak, y_peak) - the fitted peak coordinates.
3041
+
3042
+ Notes
3043
+ -----
3044
+ The function uses a least-squares fitting approach with scipy.optimize.curve_fit.
3045
+ Initial parameter estimates are derived analytically based on the peak location
3046
+ and a distance calculation. The parabolic model assumes the peak has a symmetric
3047
+ quadratic shape.
1913
3048
 
3049
+ Examples
3050
+ --------
3051
+ >>> import numpy as np
3052
+ >>> x = np.linspace(0, 10, 100)
3053
+ >>> y = 2 * (x - 5)**2 + 1
3054
+ >>> peak_x, peak_y = parabfit(x, y, 50, 10)
3055
+ >>> print(f"Peak at x={peak_x:.2f}, y={peak_y:.2f}")
1914
3056
  """
1915
3057
  func = lambda x, a, tau, c: a * ((x - tau) ** 2) + c
1916
3058
  distance = abs(x_axis[peakloc[1][0]] - x_axis[peakloc[0][0]]) / 4
@@ -1935,20 +3077,48 @@ def parabfit(x_axis, y_axis, peakloc, points):
1935
3077
  return x, y
1936
3078
 
1937
3079
 
1938
- def _datacheck_peakdetect(x_axis, y_axis):
3080
+ def _datacheck_peakdetect(x_axis: Optional[NDArray], y_axis: NDArray) -> Tuple[NDArray, NDArray]:
1939
3081
  """
3082
+ Validate and convert input arrays for peak detection.
1940
3083
 
1941
3084
  Parameters
1942
3085
  ----------
1943
- x_axis
1944
- y_axis
3086
+ x_axis : NDArray, optional
3087
+ X-axis values. If None, range(len(y_axis)) is used.
3088
+ y_axis : NDArray
3089
+ Y-axis values to be processed.
1945
3090
 
1946
3091
  Returns
1947
3092
  -------
3093
+ tuple of ndarray
3094
+ Tuple containing (x_axis, y_axis) as numpy arrays.
3095
+
3096
+ Raises
3097
+ ------
3098
+ ValueError
3099
+ If input vectors y_axis and x_axis have different lengths.
3100
+
3101
+ Notes
3102
+ -----
3103
+ This function ensures that both input arrays are converted to numpy arrays
3104
+ and have matching shapes. If x_axis is None, it defaults to a range
3105
+ corresponding to the length of y_axis.
1948
3106
 
3107
+ Examples
3108
+ --------
3109
+ >>> import numpy as np
3110
+ >>> x, y = _datacheck_peakdetect([1, 2, 3], [4, 5, 6])
3111
+ >>> print(x)
3112
+ [1 2 3]
3113
+ >>> print(y)
3114
+ [4 5 6]
3115
+
3116
+ >>> x, y = _datacheck_peakdetect(None, [4, 5, 6])
3117
+ >>> print(x)
3118
+ [0 1 2]
1949
3119
  """
1950
3120
  if x_axis is None:
1951
- x_axis = range(len(y_axis))
3121
+ x_axis = np.arange(0, len(y_axis))
1952
3122
 
1953
3123
  if np.shape(y_axis) != np.shape(x_axis):
1954
3124
  raise ValueError("Input vectors y_axis and x_axis must have same length")
@@ -1959,43 +3129,58 @@ def _datacheck_peakdetect(x_axis, y_axis):
1959
3129
  return x_axis, y_axis
1960
3130
 
1961
3131
 
1962
- def peakdetect(y_axis, x_axis=None, lookahead=200, delta=0.0):
3132
+ def peakdetect(
3133
+ y_axis: NDArray[np.floating[Any]],
3134
+ x_axis: Optional[NDArray[np.floating[Any]]] = None,
3135
+ lookahead: int = 200,
3136
+ delta: float = 0.0,
3137
+ ) -> list:
1963
3138
  """
1964
- Converted from/based on a MATLAB script at:
1965
- http://billauer.co.il/peakdet.html
1966
-
1967
- function for detecting local maxima and minima in a signal.
1968
- Discovers peaks by searching for values which are surrounded by lower
1969
- or larger values for maxima and minima respectively
3139
+ Detect local maxima and minima in a signal.
1970
3140
 
1971
- keyword arguments:
1972
- y_axis -- A list containing the signal over which to find peaks
3141
+ This function is based on a MATLAB script by Billauer, and identifies peaks
3142
+ by searching for values that are surrounded by lower (for maxima) or larger
3143
+ (for minima) values. It uses a lookahead window to confirm that a candidate
3144
+ is indeed a peak and not noise or jitter.
1973
3145
 
1974
- x_axis -- A x-axis whose values correspond to the y_axis list and is used
1975
- in the return to specify the position of the peaks. If omitted an
1976
- index of the y_axis is used.
1977
- (default: None)
1978
-
1979
- lookahead -- distance to look ahead from a peak candidate to determine if
1980
- it is the actual peak
1981
- (default: 200)
1982
- '(samples / period) / f' where '4 >= f >= 1.25' might be a good value
3146
+ Parameters
3147
+ ----------
3148
+ y_axis : NDArray[np.floating[Any]]
3149
+ A list or array containing the signal over which to find peaks.
3150
+ x_axis : NDArray[np.floating[Any]], optional
3151
+ An x-axis whose values correspond to the y_axis list. If omitted,
3152
+ an index of the y_axis is used. Default is None.
3153
+ lookahead : int, optional
3154
+ Distance to look ahead from a peak candidate to determine if it is
3155
+ the actual peak. Default is 200.
3156
+ delta : float, optional
3157
+ Minimum difference between a peak and the following points. If set,
3158
+ this helps avoid false peaks towards the end of the signal. Default is 0.0.
1983
3159
 
1984
- delta -- this specifies a minimum difference between a peak and
1985
- the following points, before a peak may be considered a peak. Useful
1986
- to hinder the function from picking up false peaks towards to end of
1987
- the signal. To work well delta should be set to delta >= RMSnoise * 5.
1988
- (default: 0)
1989
- When omitted delta function causes a 20% decrease in speed.
1990
- When used Correctly it can double the speed of the function
3160
+ Returns
3161
+ -------
3162
+ list of lists
3163
+ A list containing two sublists: ``[max_peaks, min_peaks]``.
3164
+ Each sublist contains tuples of the form ``(position, peak_value)``.
3165
+ For example, to unpack maxima into x and y coordinates:
3166
+ ``x, y = zip(*max_peaks)``.
1991
3167
 
3168
+ Notes
3169
+ -----
3170
+ - The function assumes that the input signal is sampled at regular intervals.
3171
+ - If ``delta`` is not provided, the function runs slower but may detect more
3172
+ peaks.
3173
+ - When ``delta`` is correctly specified (e.g., as 5 * RMS noise), it can
3174
+ significantly improve performance.
1992
3175
 
1993
- return: two lists [max_peaks, min_peaks] containing the positive and
1994
- negative peaks respectively. Each cell of the lists contains a tuple
1995
- of: (position, peak_value)
1996
- to get the average peak value do: np.mean(max_peaks, 0)[1] on the
1997
- results to unpack one of the lists into x, y coordinates do:
1998
- x, y = zip(*max_peaks)
3176
+ Examples
3177
+ --------
3178
+ >>> import numpy as np
3179
+ >>> x = np.linspace(0, 10, 100)
3180
+ >>> y = np.sin(x) + 0.5 * np.sin(3 * x)
3181
+ >>> max_peaks, min_peaks = peakdetect(y, x, lookahead=10, delta=0.1)
3182
+ >>> print("Max peaks:", max_peaks)
3183
+ >>> print("Min peaks:", min_peaks)
1999
3184
  """
2000
3185
  max_peaks = []
2001
3186
  min_peaks = []
@@ -2009,7 +3194,7 @@ def peakdetect(y_axis, x_axis=None, lookahead=200, delta=0.0):
2009
3194
  # perform some checks
2010
3195
  if lookahead < 1:
2011
3196
  raise ValueError("Lookahead must be '1' or above in value")
2012
- if not (np.isscalar(delta) and delta >= 0):
3197
+ if not (np.isscalar(delta) and (delta >= 0.0)):
2013
3198
  raise ValueError("delta must be a positive number")
2014
3199
 
2015
3200
  # maxima and minima candidates are temporarily stored in
@@ -2044,7 +3229,7 @@ def peakdetect(y_axis, x_axis=None, lookahead=200, delta=0.0):
2044
3229
  # mxpos = x_axis[np.where(y_axis[index:index+lookahead]==mx)]
2045
3230
 
2046
3231
  ####look for min####
2047
- if y > mn + delta and mn != -np.inf:
3232
+ if (y > mn + delta) and (mn != -np.inf):
2048
3233
  # Minima peak candidate found
2049
3234
  # look ahead in signal to ensure that this is a peak and not jitter
2050
3235
  if y_axis[index : index + lookahead].min() > mn:
@@ -2074,7 +3259,46 @@ def peakdetect(y_axis, x_axis=None, lookahead=200, delta=0.0):
2074
3259
  return [max_peaks, min_peaks]
2075
3260
 
2076
3261
 
2077
- def ocscreetest(eigenvals, debug=False, displayplots=False):
3262
+ def ocscreetest(eigenvals: NDArray, debug: bool = False, displayplots: bool = False) -> int:
3263
+ """
3264
+ Perform eigenvalue screening using the OCSCREE test to determine the number of retained components.
3265
+
3266
+ This function implements a variant of the scree test for determining the number of significant
3267
+ eigenvalues in a dataset. It uses a linear regression approach to model the eigenvalue decay
3268
+ and identifies the point where the observed eigenvalues fall below the predicted values.
3269
+
3270
+ Parameters
3271
+ ----------
3272
+ eigenvals : NDArray
3273
+ Array of eigenvalues, typically sorted in descending order.
3274
+ debug : bool, optional
3275
+ If True, print intermediate calculations for debugging purposes. Default is False.
3276
+ displayplots : bool, optional
3277
+ If True, display plots of the original eigenvalues, regression coefficients (a and b),
3278
+ and the predicted eigenvalue curve. Default is False.
3279
+
3280
+ Returns
3281
+ -------
3282
+ int
3283
+ The index of the last retained component based on the OCSCREE criterion.
3284
+
3285
+ Notes
3286
+ -----
3287
+ The function performs the following steps:
3288
+ 1. Initialize arrays for regression coefficients 'a' and 'b'.
3289
+ 2. Compute regression coefficients from the eigenvalues.
3290
+ 3. Predict eigenvalues using the regression model.
3291
+ 4. Identify the point where the actual eigenvalues drop below the predicted values.
3292
+ 5. Optionally display diagnostic plots.
3293
+
3294
+ Examples
3295
+ --------
3296
+ >>> import numpy as np
3297
+ >>> eigenvals = np.array([3.5, 2.1, 1.8, 1.2, 0.9, 0.5])
3298
+ >>> result = ocscreetest(eigenvals)
3299
+ >>> print(result)
3300
+ 3
3301
+ """
2078
3302
  num = len(eigenvals)
2079
3303
  a = eigenvals * 0.0
2080
3304
  b = eigenvals * 0.0
@@ -2111,7 +3335,45 @@ def ocscreetest(eigenvals, debug=False, displayplots=False):
2111
3335
  return i
2112
3336
 
2113
3337
 
2114
- def afscreetest(eigenvals, displayplots=False):
3338
+ def afscreetest(eigenvals: NDArray, displayplots: bool = False) -> int:
3339
+ """
3340
+ Detect the optimal number of components using the second derivative of eigenvalues.
3341
+
3342
+ This function applies a second derivative analysis to the eigenvalues to identify
3343
+ the point where the rate of change of eigenvalues begins to decrease significantly,
3344
+ which typically indicates the optimal number of components to retain.
3345
+
3346
+ Parameters
3347
+ ----------
3348
+ eigenvals : NDArray
3349
+ Array of eigenvalues, typically from a PCA or similar decomposition.
3350
+ Should be sorted in descending order.
3351
+ displayplots : bool, optional
3352
+ If True, display plots showing the original eigenvalues, first derivative,
3353
+ and second derivative (default is False).
3354
+
3355
+ Returns
3356
+ -------
3357
+ int
3358
+ The index of the optimal number of components, adjusted by subtracting 1
3359
+ from the location of maximum second derivative.
3360
+
3361
+ Notes
3362
+ -----
3363
+ The method works by:
3364
+ 1. Computing the first derivative of eigenvalues
3365
+ 2. Computing the second derivative of the first derivative
3366
+ 3. Finding the maximum of the second derivative
3367
+ 4. Returning the index of this maximum minus 1
3368
+
3369
+ Examples
3370
+ --------
3371
+ >>> import numpy as np
3372
+ >>> eigenvals = np.array([5.0, 3.0, 1.5, 0.8, 0.2])
3373
+ >>> optimal_components = afscreetest(eigenvals)
3374
+ >>> print(optimal_components)
3375
+ 1
3376
+ """
2115
3377
  num = len(eigenvals)
2116
3378
  firstderiv = np.gradient(eigenvals, edge_order=2)
2117
3379
  secondderiv = np.gradient(firstderiv, edge_order=2)
@@ -2129,10 +3391,56 @@ def afscreetest(eigenvals, displayplots=False):
2129
3391
  ax3.set_title("Second derivative")
2130
3392
  plt.plot(secondderiv, color="g")
2131
3393
  plt.show()
2132
- return maxaccloc - 1
3394
+ return int(maxaccloc - 1)
3395
+
3396
+
3397
+ def phaseanalysis(
3398
+ firstharmonic: NDArray, displayplots: bool = False
3399
+ ) -> Tuple[NDArray, NDArray, NDArray]:
3400
+ """
3401
+ Perform phase analysis on a signal using analytic signal representation.
3402
+
3403
+ This function computes the analytic signal of the input signal using the Hilbert transform,
3404
+ and extracts the instantaneous phase and amplitude envelope. Optionally displays plots
3405
+ of the analytic signal, phase, and amplitude.
3406
+
3407
+ Parameters
3408
+ ----------
3409
+ firstharmonic : NDArray
3410
+ Input signal to analyze. Should be a 1D NDArray object.
3411
+ displayplots : bool, optional
3412
+ If True, displays plots of the analytic signal, phase, and amplitude.
3413
+ Default is False.
3414
+
3415
+ Returns
3416
+ -------
3417
+ tuple of ndarray
3418
+ A tuple containing:
3419
+ - instantaneous_phase : ndarray
3420
+ The unwrapped instantaneous phase of the signal
3421
+ - amplitude_envelope : ndarray
3422
+ The amplitude envelope of the signal
3423
+ - analytic_signal : ndarray
3424
+ The analytic signal (complex-valued)
3425
+
3426
+ Notes
3427
+ -----
3428
+ The function uses `scipy.signal.hilbert` to compute the analytic signal,
3429
+ which is defined as: :math:`x_a(t) = x(t) + j\\hat{x}(t)` where :math:`\\hat{x}(t)`
3430
+ is the Hilbert transform of :math:`x(t)`.
2133
3431
 
3432
+ The instantaneous phase is computed as the angle of the analytic signal and is
3433
+ unwrapped to remove discontinuities.
2134
3434
 
2135
- def phaseanalysis(firstharmonic, displayplots=False):
3435
+ Examples
3436
+ --------
3437
+ >>> import numpy as np
3438
+ >>> from scipy.signal import hilbert
3439
+ >>> signal = np.sin(2 * np.pi * 5 * np.linspace(0, 1, 100))
3440
+ >>> phase, amp, analytic = phaseanalysis(signal)
3441
+ >>> print(f"Phase shape: {phase.shape}")
3442
+ Phase shape: (100,)
3443
+ """
2136
3444
  print("entering phaseanalysis")
2137
3445
  analytic_signal = hilbert(firstharmonic)
2138
3446
  amplitude_envelope = np.abs(analytic_signal)
@@ -2190,28 +3498,119 @@ FML_FITFAIL = (
2190
3498
 
2191
3499
 
2192
3500
  def simfuncpeakfit(
2193
- incorrfunc,
2194
- corrtimeaxis,
2195
- useguess=False,
2196
- maxguess=0.0,
2197
- displayplots=False,
2198
- functype="correlation",
2199
- peakfittype="gauss",
2200
- searchfrac=0.5,
2201
- lagmod=1000.0,
2202
- enforcethresh=True,
2203
- allowhighfitamps=False,
2204
- lagmin=-30.0,
2205
- lagmax=30.0,
2206
- absmaxsigma=1000.0,
2207
- absminsigma=0.25,
2208
- hardlimit=True,
2209
- bipolar=False,
2210
- lthreshval=0.0,
2211
- uthreshval=1.0,
2212
- zerooutbadfit=True,
2213
- debug=False,
2214
- ):
3501
+ incorrfunc: NDArray,
3502
+ corrtimeaxis: NDArray,
3503
+ useguess: bool = False,
3504
+ maxguess: float = 0.0,
3505
+ displayplots: bool = False,
3506
+ functype: str = "correlation",
3507
+ peakfittype: str = "gauss",
3508
+ searchfrac: float = 0.5,
3509
+ lagmod: float = 1000.0,
3510
+ enforcethresh: bool = True,
3511
+ allowhighfitamps: bool = False,
3512
+ lagmin: float = -30.0,
3513
+ lagmax: float = 30.0,
3514
+ absmaxsigma: float = 1000.0,
3515
+ absminsigma: float = 0.25,
3516
+ hardlimit: bool = True,
3517
+ bipolar: bool = False,
3518
+ lthreshval: float = 0.0,
3519
+ uthreshval: float = 1.0,
3520
+ zerooutbadfit: bool = True,
3521
+ debug: bool = False,
3522
+ ) -> Tuple[int, np.float64, np.float64, np.float64, np.uint16, np.uint32, int, int]:
3523
+ """
3524
+ Fit a peak in a correlation or mutual information function.
3525
+
3526
+ This function performs peak fitting on a correlation or mutual information
3527
+ function to extract peak parameters such as location, amplitude, and width.
3528
+ It supports various fitting methods and includes error handling and
3529
+ validation for fit parameters.
3530
+
3531
+ Parameters
3532
+ ----------
3533
+ incorrfunc : NDArray
3534
+ Input correlation or mutual information function values.
3535
+ corrtimeaxis : NDArray
3536
+ Time axis corresponding to the correlation function.
3537
+ useguess : bool, optional
3538
+ If True, use `maxguess` as an initial guess for the peak location.
3539
+ Default is False.
3540
+ maxguess : float, optional
3541
+ Initial guess for the peak location in seconds. Used only if `useguess` is True.
3542
+ Default is 0.0.
3543
+ displayplots : bool, optional
3544
+ If True, display plots of the peak and fit. Default is False.
3545
+ functype : str, optional
3546
+ Type of function to fit. Options are 'correlation', 'mutualinfo', or 'hybrid'.
3547
+ Default is 'correlation'.
3548
+ peakfittype : str, optional
3549
+ Type of peak fitting to perform. Options are 'gauss', 'fastgauss', 'quad',
3550
+ 'fastquad', 'COM', or 'None'. Default is 'gauss'.
3551
+ searchfrac : float, optional
3552
+ Fraction of the peak maximum to define the search range for peak width.
3553
+ Default is 0.5.
3554
+ lagmod : float, optional
3555
+ Modulus for lag values, used to wrap around the lag values.
3556
+ Default is 1000.0.
3557
+ enforcethresh : bool, optional
3558
+ If True, enforce amplitude thresholds. Default is True.
3559
+ allowhighfitamps : bool, optional
3560
+ If True, allow fit amplitudes to exceed 1.0. Default is False.
3561
+ lagmin : float, optional
3562
+ Minimum allowed lag value in seconds. Default is -30.0.
3563
+ lagmax : float, optional
3564
+ Maximum allowed lag value in seconds. Default is 30.0.
3565
+ absmaxsigma : float, optional
3566
+ Maximum allowed sigma value in seconds. Default is 1000.0.
3567
+ absminsigma : float, optional
3568
+ Minimum allowed sigma value in seconds. Default is 0.25.
3569
+ hardlimit : bool, optional
3570
+ If True, enforce hard limits on lag values. Default is True.
3571
+ bipolar : bool, optional
3572
+ If True, allow negative correlation values. Default is False.
3573
+ lthreshval : float, optional
3574
+ Lower threshold for amplitude validation. Default is 0.0.
3575
+ uthreshval : float, optional
3576
+ Upper threshold for amplitude validation. Default is 1.0.
3577
+ zerooutbadfit : bool, optional
3578
+ If True, set fit results to zero if fit fails. Default is True.
3579
+ debug : bool, optional
3580
+ If True, print debug information. Default is False.
3581
+
3582
+ Returns
3583
+ -------
3584
+ tuple of int, float, float, float, int, int, int, int
3585
+ A tuple containing:
3586
+ - maxindex: Index of the peak maximum.
3587
+ - maxlag: Fitted peak lag in seconds.
3588
+ - maxval: Fitted peak amplitude.
3589
+ - maxsigma: Fitted peak width (sigma) in seconds.
3590
+ - maskval: Mask indicating fit success (1 for success, 0 for failure).
3591
+ - failreason: Reason for fit failure (bitmask).
3592
+ - peakstart: Start index of the peak region used for fitting.
3593
+ - peakend: End index of the peak region used for fitting.
3594
+
3595
+ Notes
3596
+ -----
3597
+ - The function automatically handles different types of correlation functions
3598
+ and mutual information functions with appropriate baseline corrections.
3599
+ - Various fitting methods are supported, each with its own strengths and
3600
+ trade-offs in terms of speed and accuracy.
3601
+ - Fit results are validated against physical constraints and thresholds.
3602
+
3603
+ Examples
3604
+ --------
3605
+ >>> import numpy as np
3606
+ >>> from scipy import signal
3607
+ >>> # Create sample data
3608
+ >>> t = np.linspace(-50, 50, 1000)
3609
+ >>> corr = np.exp(-0.5 * (t / 2)**2) + 0.1 * np.random.randn(1000)
3610
+ >>> maxindex, maxlag, maxval, maxsigma, maskval, failreason, peakstart, peakend = \
3611
+ ... simfuncpeakfit(corr, t, peakfittype='gauss')
3612
+ >>> print(f"Peak lag: {maxlag:.2f} s, Amplitude: {maxval:.2f}, Width: {maxsigma:.2f} s")
3613
+ """
2215
3614
  # check to make sure xcorr_x and xcorr_y match
2216
3615
  if corrtimeaxis is None:
2217
3616
  print("Correlation time axis is not defined - exiting")
@@ -2276,7 +3675,7 @@ def simfuncpeakfit(
2276
3675
  baselinedev = 0.0
2277
3676
  else:
2278
3677
  # for mutual information, there is a nonzero baseline, so we want the difference from that.
2279
- baseline = np.median(corrfunc)
3678
+ baseline = float(np.median(corrfunc))
2280
3679
  baselinedev = mad(corrfunc)
2281
3680
  if debug:
2282
3681
  print("baseline, baselinedev:", baseline, baselinedev)
@@ -2309,8 +3708,8 @@ def simfuncpeakfit(
2309
3708
 
2310
3709
  peakpoints[0] = 0
2311
3710
  peakpoints[-1] = 0
2312
- peakstart = np.max([1, maxindex - 1])
2313
- peakend = np.min([len(corrtimeaxis) - 2, maxindex + 1])
3711
+ peakstart = int(np.max([1, maxindex - 1]))
3712
+ peakend = int(np.min([len(corrtimeaxis) - 2, maxindex + 1]))
2314
3713
  if debug:
2315
3714
  print("initial peakstart, peakend:", peakstart, peakend)
2316
3715
  if functype == "mutualinfo":
@@ -2376,7 +3775,7 @@ def simfuncpeakfit(
2376
3775
  print("bad initial")
2377
3776
  if maxsigma_init > absmaxsigma:
2378
3777
  failreason |= FML_INITWIDTHHIGH
2379
- maxsigma_init = absmaxsigma
3778
+ maxsigma_init = np.float64(absmaxsigma)
2380
3779
  if debug:
2381
3780
  print("bad initial width - too high")
2382
3781
  if peakend - peakstart < 2:
@@ -2429,7 +3828,7 @@ def simfuncpeakfit(
2429
3828
  data = corrfunc[peakstart : peakend + 1]
2430
3829
  maxval = maxval_init
2431
3830
  maxlag = np.sum(X * data) / np.sum(data)
2432
- maxsigma = 10.0
3831
+ maxsigma = np.float64(10.0)
2433
3832
  elif peakfittype == "gauss":
2434
3833
  X = corrtimeaxis[peakstart : peakend + 1] - baseline
2435
3834
  data = corrfunc[peakstart : peakend + 1]
@@ -2480,10 +3879,10 @@ def simfuncpeakfit(
2480
3879
  if debug:
2481
3880
  print("poly coffs:", a, b, c)
2482
3881
  print("maxlag, maxval, maxsigma:", maxlag, maxval, maxsigma)
2483
- except np.lib.polynomial.RankWarning:
2484
- maxlag = 0.0
2485
- maxval = 0.0
2486
- maxsigma = 0.0
3882
+ except np.exceptions.RankWarning:
3883
+ maxlag = np.float64(0.0)
3884
+ maxval = np.float64(0.0)
3885
+ maxsigma = np.float64(0.0)
2487
3886
  if debug:
2488
3887
  print("\n")
2489
3888
  for i in range(len(X)):
@@ -2508,7 +3907,7 @@ def simfuncpeakfit(
2508
3907
  if (functype == "correlation") or (functype == "hybrid"):
2509
3908
  if maxval < lowestcorrcoeff:
2510
3909
  failreason |= FML_FITAMPLOW
2511
- maxval = lowestcorrcoeff
3910
+ maxval = np.float64(lowestcorrcoeff)
2512
3911
  if debug:
2513
3912
  print("bad fit amp: maxval is lower than lower limit")
2514
3913
  fitfail = True
@@ -2545,22 +3944,22 @@ def simfuncpeakfit(
2545
3944
  print("bad lag after refinement")
2546
3945
  if lagmin > maxlag:
2547
3946
  failreason |= FML_FITLAGLOW
2548
- maxlag = lagmin
3947
+ maxlag = np.float64(lagmin)
2549
3948
  else:
2550
3949
  failreason |= FML_FITLAGHIGH
2551
- maxlag = lagmax
3950
+ maxlag = np.float64(lagmax)
2552
3951
  fitfail = True
2553
3952
  if maxsigma > absmaxsigma:
2554
3953
  failreason |= FML_FITWIDTHHIGH
2555
3954
  if debug:
2556
3955
  print("bad width after refinement:", maxsigma, ">", absmaxsigma)
2557
- maxsigma = absmaxsigma
3956
+ maxsigma = np.float64(absmaxsigma)
2558
3957
  fitfail = True
2559
3958
  if maxsigma < absminsigma:
2560
3959
  failreason |= FML_FITWIDTHLOW
2561
3960
  if debug:
2562
3961
  print("bad width after refinement:", maxsigma, "<", absminsigma)
2563
- maxsigma = absminsigma
3962
+ maxsigma = np.float64(absminsigma)
2564
3963
  fitfail = True
2565
3964
  if fitfail:
2566
3965
  if debug:
@@ -2616,16 +4015,47 @@ def simfuncpeakfit(
2616
4015
  )
2617
4016
 
2618
4017
 
2619
- def _maxindex_noedge(corrfunc, corrtimeaxis, bipolar=False):
4018
+ def _maxindex_noedge(
4019
+ corrfunc: NDArray, corrtimeaxis: NDArray, bipolar: bool = False
4020
+ ) -> Tuple[int, float]:
2620
4021
  """
4022
+ Find the index of the maximum correlation value, avoiding edge effects.
4023
+
4024
+ This function locates the maximum (or minimum, if bipolar=True) correlation value
4025
+ within the given time axis range, while avoiding edge effects by progressively
4026
+ narrowing the search window.
2621
4027
 
2622
4028
  Parameters
2623
4029
  ----------
2624
- corrfunc
4030
+ corrfunc : NDArray
4031
+ Correlation function values to search for maximum.
4032
+ corrtimeaxis : NDArray
4033
+ Time axis corresponding to the correlation function.
4034
+ bipolar : bool, optional
4035
+ If True, considers both positive and negative correlation values.
4036
+ Default is False.
2625
4037
 
2626
4038
  Returns
2627
4039
  -------
4040
+ Tuple[int, float]
4041
+ A tuple containing:
4042
+ - int: Index of the maximum correlation value
4043
+ - float: Flip factor (-1.0 if minimum was selected, 1.0 otherwise)
2628
4044
 
4045
+ Notes
4046
+ -----
4047
+ The function iteratively narrows the search range by excluding edges
4048
+ where the maximum was found. This helps avoid edge effects in correlation
4049
+ analysis. When bipolar=True, the function compares both maximum and minimum
4050
+ absolute values to determine the optimal selection.
4051
+
4052
+ Examples
4053
+ --------
4054
+ >>> corrfunc = np.array([0.1, 0.5, 0.3, 0.8, 0.2])
4055
+ >>> corrtimeaxis = np.array([0, 1, 2, 3, 4])
4056
+ >>> index, flip = _maxindex_noedge(corrfunc, corrtimeaxis)
4057
+ >>> print(index)
4058
+ 3
2629
4059
  """
2630
4060
  lowerlim = 0
2631
4061
  upperlim = len(corrtimeaxis) - 1