rapidtide 3.0.11__py3-none-any.whl → 3.1.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 (144) hide show
  1. rapidtide/Colortables.py +492 -27
  2. rapidtide/OrthoImageItem.py +1049 -46
  3. rapidtide/RapidtideDataset.py +1533 -86
  4. rapidtide/_version.py +3 -3
  5. rapidtide/calccoherence.py +196 -29
  6. rapidtide/calcnullsimfunc.py +188 -40
  7. rapidtide/calcsimfunc.py +242 -42
  8. rapidtide/correlate.py +1203 -383
  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 +53 -3
  13. rapidtide/data/examples/src/testglmfilt +5 -5
  14. rapidtide/data/examples/src/testhappy +29 -7
  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/decorators.py +91 -0
  23. rapidtide/dlfilter.py +2226 -110
  24. rapidtide/dlfiltertorch.py +4842 -0
  25. rapidtide/externaltools.py +327 -12
  26. rapidtide/fMRIData_class.py +79 -40
  27. rapidtide/filter.py +1899 -810
  28. rapidtide/fit.py +2011 -581
  29. rapidtide/genericmultiproc.py +93 -18
  30. rapidtide/happy_supportfuncs.py +2047 -172
  31. rapidtide/helper_classes.py +584 -43
  32. rapidtide/io.py +2370 -372
  33. rapidtide/linfitfiltpass.py +346 -99
  34. rapidtide/makelaggedtcs.py +210 -24
  35. rapidtide/maskutil.py +448 -62
  36. rapidtide/miscmath.py +827 -121
  37. rapidtide/multiproc.py +210 -22
  38. rapidtide/patchmatch.py +242 -42
  39. rapidtide/peakeval.py +31 -31
  40. rapidtide/ppgproc.py +2203 -0
  41. rapidtide/qualitycheck.py +352 -39
  42. rapidtide/refinedelay.py +431 -57
  43. rapidtide/refineregressor.py +494 -189
  44. rapidtide/resample.py +671 -185
  45. rapidtide/scripts/applyppgproc.py +28 -0
  46. rapidtide/scripts/showxcorr_legacy.py +7 -7
  47. rapidtide/scripts/stupidramtricks.py +15 -17
  48. rapidtide/simFuncClasses.py +1052 -77
  49. rapidtide/simfuncfit.py +269 -69
  50. rapidtide/stats.py +540 -238
  51. rapidtide/tests/happycomp +9 -0
  52. rapidtide/tests/test_cleanregressor.py +1 -2
  53. rapidtide/tests/test_dlfiltertorch.py +627 -0
  54. rapidtide/tests/test_findmaxlag.py +24 -8
  55. rapidtide/tests/test_fullrunhappy_v1.py +0 -2
  56. rapidtide/tests/test_fullrunhappy_v2.py +0 -2
  57. rapidtide/tests/test_fullrunhappy_v3.py +11 -4
  58. rapidtide/tests/test_fullrunhappy_v4.py +10 -2
  59. rapidtide/tests/test_fullrunrapidtide_v7.py +1 -1
  60. rapidtide/tests/test_getparsers.py +11 -3
  61. rapidtide/tests/test_refinedelay.py +0 -1
  62. rapidtide/tests/test_simroundtrip.py +16 -8
  63. rapidtide/tests/test_stcorrelate.py +3 -1
  64. rapidtide/tests/utils.py +9 -8
  65. rapidtide/tidepoolTemplate.py +142 -38
  66. rapidtide/tidepoolTemplate_alt.py +165 -44
  67. rapidtide/tidepoolTemplate_big.py +189 -52
  68. rapidtide/util.py +1217 -118
  69. rapidtide/voxelData.py +684 -37
  70. rapidtide/wiener.py +136 -23
  71. rapidtide/wiener2.py +113 -7
  72. rapidtide/workflows/adjustoffset.py +105 -3
  73. rapidtide/workflows/aligntcs.py +85 -2
  74. rapidtide/workflows/applydlfilter.py +87 -10
  75. rapidtide/workflows/applyppgproc.py +540 -0
  76. rapidtide/workflows/atlasaverage.py +210 -47
  77. rapidtide/workflows/atlastool.py +100 -3
  78. rapidtide/workflows/calcSimFuncMap.py +288 -69
  79. rapidtide/workflows/calctexticc.py +201 -9
  80. rapidtide/workflows/ccorrica.py +101 -6
  81. rapidtide/workflows/cleanregressor.py +165 -31
  82. rapidtide/workflows/delayvar.py +171 -23
  83. rapidtide/workflows/diffrois.py +81 -3
  84. rapidtide/workflows/endtidalproc.py +144 -4
  85. rapidtide/workflows/fdica.py +195 -15
  86. rapidtide/workflows/filtnifti.py +70 -3
  87. rapidtide/workflows/filttc.py +74 -3
  88. rapidtide/workflows/fitSimFuncMap.py +202 -51
  89. rapidtide/workflows/fixtr.py +73 -3
  90. rapidtide/workflows/gmscalc.py +113 -3
  91. rapidtide/workflows/happy.py +801 -199
  92. rapidtide/workflows/happy2std.py +144 -12
  93. rapidtide/workflows/happy_parser.py +163 -23
  94. rapidtide/workflows/histnifti.py +118 -2
  95. rapidtide/workflows/histtc.py +84 -3
  96. rapidtide/workflows/linfitfilt.py +117 -4
  97. rapidtide/workflows/localflow.py +328 -28
  98. rapidtide/workflows/mergequality.py +79 -3
  99. rapidtide/workflows/niftidecomp.py +322 -18
  100. rapidtide/workflows/niftistats.py +174 -4
  101. rapidtide/workflows/pairproc.py +98 -4
  102. rapidtide/workflows/pairwisemergenifti.py +85 -2
  103. rapidtide/workflows/parser_funcs.py +1421 -40
  104. rapidtide/workflows/physiofreq.py +137 -11
  105. rapidtide/workflows/pixelcomp.py +207 -5
  106. rapidtide/workflows/plethquality.py +103 -21
  107. rapidtide/workflows/polyfitim.py +151 -11
  108. rapidtide/workflows/proj2flow.py +75 -2
  109. rapidtide/workflows/rankimage.py +111 -4
  110. rapidtide/workflows/rapidtide.py +368 -76
  111. rapidtide/workflows/rapidtide2std.py +98 -2
  112. rapidtide/workflows/rapidtide_parser.py +109 -9
  113. rapidtide/workflows/refineDelayMap.py +144 -33
  114. rapidtide/workflows/refineRegressor.py +675 -96
  115. rapidtide/workflows/regressfrommaps.py +161 -37
  116. rapidtide/workflows/resamplenifti.py +85 -3
  117. rapidtide/workflows/resampletc.py +91 -3
  118. rapidtide/workflows/retrolagtcs.py +99 -9
  119. rapidtide/workflows/retroregress.py +176 -26
  120. rapidtide/workflows/roisummarize.py +174 -5
  121. rapidtide/workflows/runqualitycheck.py +71 -3
  122. rapidtide/workflows/showarbcorr.py +149 -6
  123. rapidtide/workflows/showhist.py +86 -2
  124. rapidtide/workflows/showstxcorr.py +160 -3
  125. rapidtide/workflows/showtc.py +159 -3
  126. rapidtide/workflows/showxcorrx.py +190 -10
  127. rapidtide/workflows/showxy.py +185 -15
  128. rapidtide/workflows/simdata.py +264 -38
  129. rapidtide/workflows/spatialfit.py +77 -2
  130. rapidtide/workflows/spatialmi.py +250 -27
  131. rapidtide/workflows/spectrogram.py +305 -32
  132. rapidtide/workflows/synthASL.py +154 -3
  133. rapidtide/workflows/tcfrom2col.py +76 -2
  134. rapidtide/workflows/tcfrom3col.py +74 -2
  135. rapidtide/workflows/tidepool.py +2971 -130
  136. rapidtide/workflows/utils.py +19 -14
  137. rapidtide/workflows/utils_doc.py +293 -0
  138. rapidtide/workflows/variabilityizer.py +116 -3
  139. {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/METADATA +10 -8
  140. {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/RECORD +144 -128
  141. {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/entry_points.txt +1 -0
  142. {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/WHEEL +0 -0
  143. {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/licenses/LICENSE +0 -0
  144. {rapidtide-3.0.11.dist-info → rapidtide-3.1.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,540 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # Copyright 2016-2025 Blaise Frederick
5
+ #
6
+ # Licensed under the Apache License, Version 2.0 (the "License");
7
+ # you may not use this file except in compliance with the License.
8
+ # You may obtain a copy of the License at
9
+ #
10
+ # http://www.apache.org/licenses/LICENSE-2.0
11
+ #
12
+ # Unless required by applicable law or agreed to in writing, software
13
+ # distributed under the License is distributed on an "AS IS" BASIS,
14
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ # See the License for the specific language governing permissions and
16
+ # limitations under the License.
17
+ #
18
+ #
19
+ import argparse
20
+ from typing import Any, Tuple
21
+
22
+ import matplotlib.pyplot as plt
23
+ import numpy as np
24
+ from numpy.typing import NDArray
25
+
26
+ import rapidtide.filter as tide_filt
27
+ import rapidtide.ppgproc as tide_ppg
28
+ import rapidtide.workflows.parser_funcs as pf
29
+
30
+ DEFAULT_PROCESSNOISE = 0.001
31
+ DEFAULT_HRESTIMATE = 70.0
32
+ DEFAULT_MEASUREMENTNOISE = 0.05
33
+ DEFAULT_QUALTHRESH = 0.5
34
+
35
+
36
+ def _get_parser() -> Any:
37
+ """
38
+ Argument parser for applyppgproc.
39
+
40
+ This function constructs and returns an `argparse.ArgumentParser` object configured
41
+ to parse command-line arguments for the `applyppgproc` tool. The tool calculates
42
+ PPG (Photoplethysmography) metrics from a cardiacfromfmri output file.
43
+
44
+ Returns
45
+ -------
46
+ argparse.ArgumentParser
47
+ Configured argument parser for the applyppgproc tool.
48
+
49
+ Notes
50
+ -----
51
+ The parser includes both required and optional arguments. Required arguments are:
52
+ - `infileroot`: Root name of the cardiacfromfmri input file (without extension).
53
+ - `outfileroot`: Root name of the output files.
54
+
55
+ Optional arguments include:
56
+ - `--process_noise`: Process noise for the PPG filter (default: `DEFAULT_PROCESSNOISE`).
57
+ - `--hr_estimate`: Starting guess for heart rate in BPM (default: `DEFAULT_HRESTIMATE`).
58
+ - `--qual_thresh`: Quality threshold for PPG, between 0 and 1 (default: `DEFAULT_QUALTHRESH`).
59
+ - `--measurement_noise`: Assumed measurement noise (default: `DEFAULT_MEASUREMENTNOISE`).
60
+ - `--display`: Graph the processed waveforms.
61
+ - `--debug`: Print debugging information.
62
+
63
+ Examples
64
+ --------
65
+ >>> parser = _get_parser()
66
+ >>> args = parser.parse_args()
67
+ """
68
+ parser = argparse.ArgumentParser(
69
+ prog="applyppgproc",
70
+ description=("Calculate PPG metrics from a happy cardiacfromfmri output file."),
71
+ allow_abbrev=False,
72
+ )
73
+
74
+ # Required arguments
75
+ parser.add_argument(
76
+ "infileroot",
77
+ help="The root name of the cardiacfromfmri file (without the json or tsv.gz extension).",
78
+ )
79
+ parser.add_argument(
80
+ "outfileroot",
81
+ help="The root name of the output files.",
82
+ )
83
+
84
+ # optional arguments
85
+ parser.add_argument(
86
+ "--process_noise",
87
+ dest="process_noise",
88
+ metavar="NOISE",
89
+ action="store",
90
+ type=lambda x: pf.is_float(parser, x),
91
+ help=f"Process noise for the PPG filter (default is {DEFAULT_PROCESSNOISE}). ",
92
+ default=DEFAULT_PROCESSNOISE,
93
+ )
94
+ parser.add_argument(
95
+ "--hr_estimate",
96
+ dest="hr_estimate",
97
+ metavar="BPM",
98
+ action="store",
99
+ type=lambda x: pf.is_float(parser, x),
100
+ help=f"Starting guess for heart rate in BPM (default is {DEFAULT_HRESTIMATE}). ",
101
+ default=DEFAULT_HRESTIMATE,
102
+ )
103
+ parser.add_argument(
104
+ "--qual_thresh",
105
+ dest="qual_thresh",
106
+ metavar="THRESH",
107
+ action="store",
108
+ type=lambda x: pf.is_float(parser, x),
109
+ help=f"Quality threshold for PPG, between 0 and 1 (default is {DEFAULT_QUALTHRESH}). ",
110
+ default=DEFAULT_QUALTHRESH,
111
+ )
112
+ parser.add_argument(
113
+ "--measurement_noise",
114
+ dest="measurement_noise",
115
+ metavar="NOISE",
116
+ action="store",
117
+ type=lambda x: pf.is_float(parser, x),
118
+ help=f"Assumed measurement noise (default is {DEFAULT_MEASUREMENTNOISE}). ",
119
+ default=DEFAULT_MEASUREMENTNOISE,
120
+ )
121
+ parser.add_argument(
122
+ "--display",
123
+ dest="display",
124
+ action="store_true",
125
+ help=("Graph the processed waveforms."),
126
+ default=False,
127
+ )
128
+ parser.add_argument(
129
+ "--debug",
130
+ dest="debug",
131
+ action="store_true",
132
+ help=("Print debugging information."),
133
+ default=False,
134
+ )
135
+ return parser
136
+
137
+
138
+ def procppg(
139
+ args: Any,
140
+ ) -> Tuple[
141
+ dict,
142
+ NDArray,
143
+ NDArray,
144
+ NDArray,
145
+ NDArray,
146
+ NDArray,
147
+ NDArray,
148
+ NDArray,
149
+ NDArray,
150
+ NDArray,
151
+ NDArray,
152
+ NDArray,
153
+ NDArray,
154
+ ]:
155
+ """
156
+ Process PPG (Photoplethysmography) signal using a combination of filtering,
157
+ heart rate extraction, and signal quality assessment techniques.
158
+
159
+ This function performs a complete PPG signal processing pipeline including:
160
+ - Reading and preprocessing PPG data
161
+ - Applying Extended Kalman Filter with sinusoidal model
162
+ - Extracting heart rate using FFT, EKF, and peak detection methods
163
+ - Assessing signal quality
164
+ - Computing performance metrics and additional features like HRV and pulse morphology
165
+ - Optionally displaying plots and printing detailed results
166
+
167
+ Parameters
168
+ ----------
169
+ args : Any
170
+ An object containing the following attributes:
171
+ - infileroot : str
172
+ Root path to the input data file(s)
173
+ - display : bool
174
+ Whether to display plots
175
+ - debug : bool
176
+ Whether to print debug information
177
+ - hr_estimate : float
178
+ Initial heart rate estimate for EKF
179
+ - process_noise : float
180
+ Process noise for EKF
181
+ - measurement_noise : float
182
+ Measurement noise for EKF
183
+ - qual_thresh : float
184
+ Threshold for determining good quality signal segments
185
+
186
+ Returns
187
+ -------
188
+ tuple
189
+ A tuple containing:
190
+ - ppginfo : dict
191
+ Dictionary with various performance metrics and computed features
192
+ - peak_indices : NDArray
193
+ Indices of detected peaks in the filtered signal
194
+ - rri : NDArray
195
+ Inter-beat intervals (RRIs) derived from peak detection
196
+ - hr_waveform_from_peaks : NDArray
197
+ Heart rate waveform computed from peaks
198
+ - hr_times : NDArray
199
+ Time points for heart rate estimates
200
+ - hr_values : NDArray
201
+ Heart rate values (FFT-based)
202
+ - filtered_ekf : NDArray
203
+ Signal filtered using Extended Kalman Filter
204
+ - ekf_heart_rates : NDArray
205
+ Heart rate estimates from EKF
206
+ - cardiacfromfmri_qual_times : NDArray
207
+ Time points for quality scores from raw fMRI signal
208
+ - cardiacfromfmri_qual_scores : NDArray
209
+ Quality scores for raw fMRI signal
210
+ - dlfiltered_qual_times : NDArray
211
+ Time points for quality scores from DL-filtered signal
212
+ - dlfiltered_qual_scores : NDArray
213
+ Quality scores for DL-filtered signal
214
+
215
+ Notes
216
+ -----
217
+ The function uses the `tide_ppg` module for signal processing and analysis.
218
+ It supports visualization via matplotlib when `args.display` is True.
219
+ Heart rate variability (HRV) and pulse morphology features are computed if sufficient peaks are detected.
220
+
221
+ Examples
222
+ --------
223
+ >>> import argparse
224
+ >>> args = argparse.Namespace(
225
+ ... infileroot='data/ppg_data',
226
+ ... display=True,
227
+ ... debug=False,
228
+ ... hr_estimate=70.0,
229
+ ... process_noise=1e-4,
230
+ ... measurement_noise=1e-2,
231
+ ... qual_thresh=0.7
232
+ ... )
233
+ >>> procppg(args)
234
+ """
235
+ if args.display:
236
+ import matplotlib as mpl
237
+
238
+ mpl.use("TkAgg")
239
+ import matplotlib.pyplot as plt
240
+
241
+ ppginfo = {}
242
+
243
+ # read in a happy data file
244
+ t, Fs, dlfiltered_ppg, cardiacfromfmri_ppg, pleth_ppg, missing_indices = (
245
+ tide_ppg.read_happy_ppg(args.infileroot, debug=True)
246
+ )
247
+ dlfiltered_ppg /= np.std(dlfiltered_ppg)
248
+ rollofffilter = tide_filt.NoncausalFilter(filtertype="arb")
249
+ rollofffilter.setfreqs(0.0, 0.0, 1.0, 4.0)
250
+ # cardiacfromfmri_ppg = rollofffilter.apply(Fs, cardiacfromfmri_ppg)
251
+ cardiacfromfmri_ppg /= np.std(cardiacfromfmri_ppg)
252
+
253
+ # Apply Extended Kalman filter with sinusoidal model to the dlfiltered timecourse
254
+ ekf = tide_ppg.ExtendedPPGKalmanFilter(
255
+ dt=(1.0 / Fs),
256
+ hr_estimate=args.hr_estimate,
257
+ process_noise=args.process_noise,
258
+ measurement_noise=args.measurement_noise,
259
+ )
260
+ filtered_ekf, ekf_heart_rates = ekf.filter_signal(dlfiltered_ppg, missing_indices)
261
+
262
+ # Extract heart rate using frequency methods
263
+ hr_extractor = tide_ppg.HeartRateExtractor(fs=Fs)
264
+ hr_times, hr_values = hr_extractor.extract_continuous(
265
+ filtered_ekf, window_size=10.0, stride=2.0, method="fft"
266
+ )
267
+
268
+ # Assess signal quality
269
+ quality_assessor = tide_ppg.SignalQualityAssessor(fs=Fs, window_size=5.0)
270
+ cardiacfromfmri_qual_times, cardiacfromfmri_qual_scores = quality_assessor.assess_continuous(
271
+ cardiacfromfmri_ppg, filtered_ekf, stride=1.0
272
+ )
273
+ dlfiltered_qual_times, dlfiltered_qual_scores = quality_assessor.assess_continuous(
274
+ dlfiltered_ppg, filtered_ekf, stride=1.0
275
+ )
276
+
277
+ # Also get single HR estimate from peaks and beat to beat
278
+ ppginfo["hr_from_peaks"], peak_indices, rri, hr_waveform_from_peaks = (
279
+ hr_extractor.extract_from_peaks(filtered_ekf)
280
+ )
281
+
282
+ if args.debug:
283
+ print(f"HR from peaks: {ppginfo["hr_from_peaks"]}")
284
+ print(f"Peak indices: {peak_indices}")
285
+ print(f"RRIs: {rri}")
286
+ print(f"hr_waveform_from_peaks: {hr_waveform_from_peaks}")
287
+
288
+ # Plot results
289
+ if args.display:
290
+ fig = plt.figure(figsize=(15, 10))
291
+ gs = fig.add_gridspec(4, 1, hspace=0.3)
292
+
293
+ thissubfig = 0
294
+ # Plot 1: Original and corrupted
295
+ ax1 = fig.add_subplot(gs[thissubfig, 0])
296
+ thissubfig += 1
297
+ ax1.plot(t, dlfiltered_ppg, "g-", label="DL filtered PPG", alpha=0.7, linewidth=1.5)
298
+ ax1.plot(
299
+ t,
300
+ cardiacfromfmri_ppg,
301
+ "r.",
302
+ label="Raw cardiac from fMRI with bad points",
303
+ markersize=3,
304
+ alpha=0.5,
305
+ )
306
+ ax1.set_ylabel("Amplitude")
307
+ ax1.set_title("PPG Signal: Raw cardiac from fmri vs DL filtered")
308
+ ax1.legend(loc="upper right")
309
+ ax1.grid(True, alpha=0.3)
310
+
311
+ # Plot 4: Extended Kalman filter (sinusoidal model)
312
+ ax4 = fig.add_subplot(gs[thissubfig, 0])
313
+ thissubfig += 1
314
+ ax4.plot(t, dlfiltered_ppg, "g-", label="DL filtered PPG", alpha=0.7, linewidth=1)
315
+ ax4.plot(
316
+ t,
317
+ cardiacfromfmri_ppg,
318
+ "r.",
319
+ label="Raw cardiac from fMRI with bad points",
320
+ markersize=3,
321
+ alpha=0.5,
322
+ )
323
+ ax4.plot(t, filtered_ekf, "m-", label="Extended Kalman Filter", linewidth=1.5)
324
+ ax4.plot(
325
+ t[missing_indices],
326
+ filtered_ekf[missing_indices],
327
+ "ro",
328
+ label="Interpolated points",
329
+ markersize=5,
330
+ alpha=0.7,
331
+ )
332
+ # Mark detected peaks
333
+ ax4.plot(
334
+ t[peak_indices],
335
+ filtered_ekf[peak_indices],
336
+ "kx",
337
+ label="Detected peaks",
338
+ markersize=8,
339
+ markeredgewidth=2,
340
+ )
341
+ ax4.set_ylabel("Amplitude")
342
+ ax4.set_title("Extended Kalman Filter (Sinusoidal Model)")
343
+ ax4.legend(loc="upper right")
344
+ ax4.grid(True, alpha=0.3)
345
+
346
+ # Plot 6: Heart rate extraction
347
+ ax6 = fig.add_subplot(gs[thissubfig, 0])
348
+ thissubfig += 1
349
+ ax6.plot(hr_times, hr_values, "b-", label="FFT-based HR", linewidth=2, marker="o")
350
+ ax6.plot(t, ekf_heart_rates, "r-", label="EKF-based HR", linewidth=1.5, alpha=0.7)
351
+
352
+ if hr_waveform_from_peaks is not None:
353
+ ax6.plot(
354
+ t, hr_waveform_from_peaks, "g-", label="Peak-based HR", linewidth=1.5, alpha=0.7
355
+ )
356
+
357
+ if ppginfo["hr_from_peaks"] is not None:
358
+ ax6.axhline(
359
+ y=ppginfo["hr_from_peaks"],
360
+ color="g",
361
+ linestyle="--",
362
+ label=f"Peak-based HR: {ppginfo["hr_from_peaks"]:.1f} BPM",
363
+ linewidth=2,
364
+ )
365
+ ax6.set_ylabel("Heart Rate (BPM)")
366
+ ax6.set_title("Heart Rate Extraction")
367
+ ax6.legend(loc="upper right")
368
+ ax6.grid(True, alpha=0.3)
369
+ ax6.set_ylim((40.0, 110.0))
370
+
371
+ # Plot 7: Signal quality assessment
372
+ ax7 = fig.add_subplot(gs[thissubfig, 0])
373
+ thissubfig += 1
374
+ quality_colors = plt.cm.RdYlGn(dlfiltered_qual_scores) # Red=poor, Green=good
375
+ for i in range(len(dlfiltered_qual_times) - 1):
376
+ ax7.axvspan(
377
+ dlfiltered_qual_times[i],
378
+ dlfiltered_qual_times[i + 1],
379
+ alpha=0.3,
380
+ color=quality_colors[i],
381
+ )
382
+ ax7.plot(
383
+ dlfiltered_qual_times, dlfiltered_qual_scores, "k-", linewidth=2, label="Quality Score"
384
+ )
385
+ ax7.axhline(
386
+ y=args.qual_thresh, color="orange", linestyle="--", label="Good quality threshold"
387
+ )
388
+ ax7.set_xlabel("Time (s)")
389
+ ax7.set_ylabel("Quality Score")
390
+ ax7.set_title("Signal Quality Assessment (0=Poor, 1=Excellent)")
391
+ ax7.legend(loc="upper right")
392
+ ax7.grid(True, alpha=0.3)
393
+ ax7.set_ylim((0.0, 1.0))
394
+
395
+ plt.tight_layout()
396
+ plt.show()
397
+
398
+ # Print comprehensive performance metrics
399
+ ppginfo["mse_ekf"] = np.mean((dlfiltered_ppg - filtered_ekf) ** 2)
400
+ ppginfo["mean_fft_hr"] = np.mean(hr_values)
401
+ ppginfo["std_fft_hr"] = np.std(hr_values)
402
+ ppginfo["mean_ekf_hr"] = np.mean(ekf_heart_rates)
403
+ ppginfo["std_ekf_hr"] = np.std(ekf_heart_rates)
404
+ if hr_waveform_from_peaks is not None:
405
+ ppginfo["mean_peaks_hr"] = np.mean(hr_waveform_from_peaks)
406
+ ppginfo["std_peaks_hr"] = np.std(hr_waveform_from_peaks)
407
+ ppginfo["num_detected_peaks"] = len(peak_indices)
408
+ ppginfo["mean_dlfiltered_qual_scores"] = np.mean(dlfiltered_qual_scores)
409
+ ppginfo["mean_cardiacfromfmri_qual_scores"] = np.mean(cardiacfromfmri_qual_scores)
410
+
411
+ print(f"\n{'='*60}")
412
+ print(f"PERFORMANCE METRICS")
413
+ print(f"{'='*60}")
414
+ print(f"\nFiltering Performance:")
415
+ print(f" Extended Kalman MSE: {ppginfo['mse_ekf']:.6f}")
416
+ print(f"\nData Recovery:")
417
+ print(
418
+ f" Missing data points: {len(missing_indices)} ({len(missing_indices)/len(t)*100:.1f}%)"
419
+ )
420
+ print(f"\nHeart Rate Analysis:")
421
+ print(
422
+ f" Peak-based HR: {ppginfo["hr_from_peaks"]:.1f} BPM"
423
+ if ppginfo["hr_from_peaks"]
424
+ else " Peak-based HR: Unable to detect"
425
+ )
426
+ print(f" FFT-based HR (mean): {ppginfo['mean_fft_hr']:.1f} ± {ppginfo['std_fft_hr']:.1f} BPM")
427
+ print(f" EKF-based HR (mean): {ppginfo['mean_ekf_hr']:.1f} ± {ppginfo['std_ekf_hr']:.1f} BPM")
428
+
429
+ if hr_waveform_from_peaks is not None:
430
+ print(
431
+ f" Peak-based HR (mean): {ppginfo['mean_peaks_hr']:.1f} ± {ppginfo['std_peaks_hr']:.1f} BPM"
432
+ )
433
+ print(f" Number of detected peaks: {ppginfo['num_detected_peaks']}")
434
+ print(f"\nSignal Quality:")
435
+ print(f" Mean quality score: {ppginfo['mean_dlfiltered_qual_scores']:.3f}")
436
+ print(
437
+ f" Percentage of good quality signal (>0.7): {np.sum(dlfiltered_qual_scores > 0.7)/len(dlfiltered_qual_scores)*100:.1f}%"
438
+ )
439
+ print(
440
+ f" Percentage of poor quality signal (<0.3): {np.sum(dlfiltered_qual_scores < 0.3)/len(dlfiltered_qual_scores)*100:.1f}%"
441
+ )
442
+ print(f"\n{'='*60}")
443
+
444
+ # Demonstrate the complete processing pipeline
445
+ print(f"\n{'='*60}")
446
+ print(f"COMPLETE PROCESSING PIPELINE DEMO")
447
+ print(f"{'='*60}")
448
+
449
+ processor = tide_ppg.RobustPPGProcessor(
450
+ fs=Fs, method="ekf", hr_estimate=args.hr_estimate, process_noise=args.process_noise
451
+ )
452
+ pipeline_results = processor.process(
453
+ filtered_ekf, missing_indices, quality_threshold=args.qual_thresh
454
+ )
455
+ if args.debug:
456
+ print(pipeline_results)
457
+
458
+ print(f"\nPipeline Results:")
459
+ print(f" Mean quality score: {pipeline_results['mean_quality']:.3f}")
460
+ print(f" Good quality segments: {pipeline_results['good_quality_percentage']:.1f}%")
461
+ if pipeline_results["hr_overall"] is not None:
462
+ print(f" Overall heart rate: {pipeline_results['hr_overall']:.1f} BPM")
463
+ print(f" Detected {len(pipeline_results['peak_indices'])} heartbeats")
464
+
465
+ # Extract additional features
466
+ feature_extractor = tide_ppg.PPGFeatureExtractor(fs=Fs)
467
+
468
+ if len(peak_indices) > 5:
469
+ hrv_features = feature_extractor.extract_hrv_features(peak_indices)
470
+ ppginfo.update(hrv_features)
471
+
472
+ if hrv_features is not None:
473
+ print(f"\nHeart Rate Variability (HRV) Features:")
474
+ print(f" Mean IBI: {hrv_features['mean_ibi']:.1f} ms")
475
+ print(f" SDNN: {hrv_features['sdnn']:.1f} ms")
476
+ print(f" RMSSD: {hrv_features['rmssd']:.1f} ms")
477
+ print(f" pNN50: {hrv_features['pnn50']:.1f}%")
478
+
479
+ if "lf_power" in hrv_features:
480
+ print(f"\n Frequency Domain:")
481
+ print(f" LF Power: {hrv_features['lf_power']:.2f}")
482
+ print(f" HF Power: {hrv_features['hf_power']:.2f}")
483
+ print(f" LF/HF Ratio: {hrv_features['lf_hf_ratio']:.2f}")
484
+
485
+ # Extract morphology from a good segment
486
+ if len(peak_indices) > 0:
487
+ # Find a peak in the middle of the signal
488
+ mid_peak = peak_indices[len(peak_indices) // 2]
489
+
490
+ # Extract segment around this peak
491
+ segment_start = max(0, mid_peak - int(0.4 * 100))
492
+ segment_end = min(len(filtered_ekf), mid_peak + int(0.6 * 100))
493
+ segment = filtered_ekf[segment_start:segment_end]
494
+ peak_in_segment = mid_peak - segment_start
495
+
496
+ morph_features = feature_extractor.extract_morphology_features(segment, peak_in_segment)
497
+ ppginfo.update(morph_features)
498
+
499
+ print(f"\nPulse Morphology Features (single pulse):")
500
+ print(f" Pulse amplitude: {morph_features['pulse_amplitude']:.3f}")
501
+ print(f" Rising time: {morph_features['rising_time']:.3f} s")
502
+ if "augmentation_index" in morph_features:
503
+ print(f" Augmentation index: {morph_features['augmentation_index']:.3f}")
504
+ if "pulse_width" in morph_features:
505
+ print(f" Pulse width (FWHM): {morph_features['pulse_width']:.3f} s")
506
+
507
+ # SpO2 proxy
508
+ spo2_proxy = feature_extractor.compute_spo2_proxy(filtered_ekf)
509
+ ppginfo["spo2_proxy"] = spo2_proxy
510
+ print(f"\nSpO2 Proxy: {spo2_proxy:.1f}% (Note: This is not a real SpO2 measurement!)")
511
+
512
+ print(f"\n{'='*60}")
513
+ print(f"USAGE TIPS")
514
+ print(f"{'='*60}")
515
+
516
+ if args.display:
517
+ plt.plot(
518
+ cardiacfromfmri_qual_times, cardiacfromfmri_qual_scores, label="Cardiac from fMRI"
519
+ )
520
+ plt.plot(dlfiltered_qual_times, dlfiltered_qual_scores, label="DLfiltered")
521
+ plt.show()
522
+
523
+ if args.debug:
524
+ print(f"{ppginfo=}")
525
+
526
+ return (
527
+ ppginfo,
528
+ peak_indices,
529
+ rri,
530
+ hr_waveform_from_peaks,
531
+ peak_indices,
532
+ hr_times,
533
+ hr_values,
534
+ filtered_ekf,
535
+ ekf_heart_rates,
536
+ cardiacfromfmri_qual_times,
537
+ cardiacfromfmri_qual_scores,
538
+ dlfiltered_qual_times,
539
+ dlfiltered_qual_scores,
540
+ )