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