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
@@ -0,0 +1,4843 @@
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 glob
20
+ import logging
21
+ import os
22
+ import sys
23
+ import warnings
24
+
25
+ import matplotlib as mpl
26
+ import matplotlib.pyplot as plt
27
+ import numpy as np
28
+ import tqdm
29
+
30
+ with warnings.catch_warnings():
31
+ warnings.simplefilter("ignore")
32
+ try:
33
+ import pyfftw
34
+ except ImportError:
35
+ pyfftwpresent = False
36
+ else:
37
+ pyfftwpresent = True
38
+
39
+ from scipy import fftpack
40
+ from statsmodels.robust.scale import mad
41
+
42
+ if pyfftwpresent:
43
+ fftpack = pyfftw.interfaces.scipy_fftpack
44
+ pyfftw.interfaces.cache.enable()
45
+
46
+ import torch
47
+ import torch.nn as nn
48
+ import torch.optim as optim
49
+ from torch.utils.data import DataLoader, TensorDataset
50
+
51
+ import rapidtide.io as tide_io
52
+
53
+ LGR = logging.getLogger("GENERAL")
54
+ LGR.debug("setting backend to Agg")
55
+ mpl.use("Agg")
56
+
57
+ # Disable GPU if desired
58
+ if torch.cuda.is_available():
59
+ device = torch.device("cuda")
60
+ LGR.info(f"Using CUDA device: {torch.cuda.get_device_name(0)}")
61
+ elif torch.backends.mps.is_available():
62
+ device = torch.device("mps")
63
+ LGR.info(f"Using MPS device")
64
+ else:
65
+ device = torch.device("cpu")
66
+ LGR.info("Using CPU")
67
+
68
+ LGR.debug(f"pytorch version: >>>{torch.__version__}<<<")
69
+
70
+
71
+ class DeepLearningFilter:
72
+ """Base class for deep learning filter"""
73
+
74
+ thesuffix = "sliceres"
75
+ thedatadir = "/Users/frederic/Documents/MR_data/physioconn/timecourses"
76
+ inputfrag = "abc"
77
+ targetfrag = "xyz"
78
+ namesuffix = None
79
+ modelroot = "."
80
+ excludethresh = 4.0
81
+ modelname = None
82
+ intermediatemodelpath = None
83
+ usebadpts = False
84
+ activation = "tanh"
85
+ dofft = False
86
+ readlim = None
87
+ countlim = None
88
+ lossfilename = None
89
+ train_x = None
90
+ train_y = None
91
+ val_x = None
92
+ val_y = None
93
+ model = None
94
+ modelpath = None
95
+ inputsize = None
96
+ infodict = {}
97
+
98
+ def __init__(
99
+ self,
100
+ window_size: int = 128,
101
+ num_layers: int = 5,
102
+ dropout_rate: float = 0.3,
103
+ num_pretrain_epochs: int = 0,
104
+ num_epochs: int = 1,
105
+ activation: str = "relu",
106
+ modelroot: str = ".",
107
+ dofft: bool = False,
108
+ excludethresh: float = 4.0,
109
+ usebadpts: bool = False,
110
+ thesuffix: str = "25.0Hz",
111
+ modelpath: str = ".",
112
+ thedatadir: str = "/Users/frederic/Documents/MR_data/physioconn/timecourses",
113
+ inputfrag: str = "abc",
114
+ targetfrag: str = "xyz",
115
+ corrthresh: float = 0.5,
116
+ excludebysubject: bool = True,
117
+ startskip: int = 200,
118
+ endskip: int = 200,
119
+ step: int = 1,
120
+ namesuffix: str | None = None,
121
+ readlim: int | None = None,
122
+ readskip: int | None = None,
123
+ countlim: int | None = None,
124
+ **kwargs,
125
+ ) -> None:
126
+ """
127
+ Initialize the DeepLearningFilter with specified parameters.
128
+
129
+ This constructor sets up the configuration for a deep learning model used
130
+ for filtering physiological timecourses. It initializes various hyperparameters,
131
+ paths, and flags that control the behavior of the model and data processing.
132
+
133
+ Parameters
134
+ ----------
135
+ window_size : int, optional
136
+ Size of the sliding window used for processing time series data. Default is 128.
137
+ num_layers : int, optional
138
+ Number of layers in the neural network model. Default is 5.
139
+ dropout_rate : float, optional
140
+ Dropout rate for regularization during training. Default is 0.3.
141
+ num_pretrain_epochs : int, optional
142
+ Number of pre-training epochs. Default is 0.
143
+ num_epochs : int, optional
144
+ Number of training epochs. Default is 1.
145
+ activation : str, optional
146
+ Activation function to use in the model. Default is "relu".
147
+ modelroot : str, optional
148
+ Root directory for model storage. Default is ".".
149
+ dofft : bool, optional
150
+ Whether to apply FFT transformation to input data. Default is False.
151
+ excludethresh : float, optional
152
+ Threshold for excluding data points based on correlation. Default is 4.0.
153
+ usebadpts : bool, optional
154
+ Whether to include bad points in the input. Default is False.
155
+ thesuffix : str, optional
156
+ Suffix to append to filenames. Default is "25.0Hz".
157
+ modelpath : str, optional
158
+ Path to save or load the model. Default is ".".
159
+ thedatadir : str, optional
160
+ Directory containing the physiological data files. Default is
161
+ "/Users/frederic/Documents/MR_data/physioconn/timecourses".
162
+ inputfrag : str, optional
163
+ Fragment identifier for input data. Default is "abc".
164
+ targetfrag : str, optional
165
+ Fragment identifier for target data. Default is "xyz".
166
+ corrthresh : float, optional
167
+ Correlation threshold for filtering. Default is 0.5.
168
+ excludebysubject : bool, optional
169
+ Whether to exclude data by subject. Default is True.
170
+ startskip : int, optional
171
+ Number of samples to skip at the beginning of each timecourse. Default is 200.
172
+ endskip : int, optional
173
+ Number of samples to skip at the end of each timecourse. Default is 200.
174
+ step : int, optional
175
+ Step size for sliding window. Default is 1.
176
+ namesuffix : str, optional
177
+ Suffix to append to model name. Default is None.
178
+ readlim : int, optional
179
+ Limit on number of samples to read. Default is None.
180
+ readskip : int, optional
181
+ Number of samples to skip when reading data. Default is None.
182
+ countlim : int, optional
183
+ Limit on number of timecourses to process. Default is None.
184
+ **kwargs
185
+ Additional keyword arguments passed to the parent class.
186
+
187
+ Notes
188
+ -----
189
+ The `inputsize` is dynamically set based on the `usebadpts` flag:
190
+ - If `usebadpts` is True, input size is 2.
191
+ - Otherwise, input size is 1.
192
+
193
+ Examples
194
+ --------
195
+ >>> filter = DeepLearningFilter(
196
+ ... window_size=256,
197
+ ... num_layers=6,
198
+ ... dropout_rate=0.2,
199
+ ... modelroot="/models",
200
+ ... dofft=True
201
+ ... )
202
+ """
203
+ self.window_size = window_size
204
+ self.dropout_rate = dropout_rate
205
+ self.num_pretrain_epochs = num_pretrain_epochs
206
+ self.num_epochs = num_epochs
207
+ self.usebadpts = usebadpts
208
+ self.num_layers = num_layers
209
+ if self.usebadpts:
210
+ self.inputsize = 2
211
+ else:
212
+ self.inputsize = 1
213
+ self.activation = activation
214
+ self.modelroot = modelroot
215
+ self.dofft = dofft
216
+ self.thesuffix = thesuffix
217
+ self.thedatadir = thedatadir
218
+ self.modelpath = modelpath
219
+ LGR.info(f"modeldir from DeepLearningFilter: {self.modelpath}")
220
+ self.corrthresh = corrthresh
221
+ self.excludethresh = excludethresh
222
+ self.readlim = readlim
223
+ self.readskip = readskip
224
+ self.countlim = countlim
225
+ self.model = None
226
+ self.initialized = False
227
+ self.trained = False
228
+ self.usetensorboard = False
229
+ self.inputfrag = inputfrag
230
+ self.targetfrag = targetfrag
231
+ self.namesuffix = namesuffix
232
+ self.startskip = startskip
233
+ self.endskip = endskip
234
+ self.step = step
235
+ self.excludebysubject = excludebysubject
236
+ self.device = device
237
+
238
+ # populate infodict
239
+ self.infodict["window_size"] = self.window_size
240
+ self.infodict["usebadpts"] = self.usebadpts
241
+ self.infodict["dofft"] = self.dofft
242
+ self.infodict["corrthresh"] = self.corrthresh
243
+ self.infodict["excludethresh"] = self.excludethresh
244
+ self.infodict["num_pretrain_epochs"] = self.num_pretrain_epochs
245
+ self.infodict["num_epochs"] = self.num_epochs
246
+ self.infodict["modelname"] = self.modelname
247
+ self.infodict["dropout_rate"] = self.dropout_rate
248
+ self.infodict["startskip"] = self.startskip
249
+ self.infodict["endskip"] = self.endskip
250
+ self.infodict["step"] = self.step
251
+ self.infodict["train_arch"] = sys.platform
252
+
253
+ def loaddata(self) -> None:
254
+ """
255
+ Load and preprocess data for training and validation.
256
+
257
+ This method initializes the data loading process by calling the `prep` function
258
+ with a set of parameters derived from the instance attributes. It handles both
259
+ FFT and non-FFT modes of data preprocessing. The loaded data is stored in
260
+ instance variables for use in subsequent training steps.
261
+
262
+ Parameters
263
+ ----------
264
+ self : object
265
+ The instance of the class containing the following attributes:
266
+ - initialized : bool
267
+ Indicates whether the model has been initialized.
268
+ - dofft : bool
269
+ Whether to apply FFT transformation to the data.
270
+ - window_size : int
271
+ Size of the sliding window used for data segmentation.
272
+ - thesuffix : str
273
+ Suffix to append to filenames when reading data.
274
+ - thedatadir : str
275
+ Directory path where the data files are located.
276
+ - inputfrag : str
277
+ Fragment identifier for input data.
278
+ - targetfrag : str
279
+ Fragment identifier for target data.
280
+ - startskip : int
281
+ Number of samples to skip at the beginning of each file.
282
+ - endskip : int
283
+ Number of samples to skip at the end of each file.
284
+ - corrthresh : float
285
+ Correlation threshold for filtering data.
286
+ - step : int
287
+ Step size for sliding window.
288
+ - usebadpts : bool
289
+ Whether to include bad points in the data.
290
+ - excludethresh : float
291
+ Threshold for excluding data points.
292
+ - excludebysubject : bool
293
+ Whether to exclude data by subject.
294
+ - readlim : int
295
+ Limit on the number of samples to read.
296
+ - readskip : int
297
+ Number of samples to skip while reading.
298
+ - countlim : int
299
+ Limit on the number of data points to process.
300
+
301
+ Returns
302
+ -------
303
+ None
304
+ This method does not return any value. It modifies the instance attributes
305
+ in place.
306
+
307
+ Raises
308
+ ------
309
+ Exception
310
+ If the model is not initialized prior to calling this method.
311
+
312
+ Notes
313
+ -----
314
+ The method assigns the following attributes to the instance after loading:
315
+ - train_x : array-like
316
+ Training input data.
317
+ - train_y : array-like
318
+ Training target data.
319
+ - val_x : array-like
320
+ Validation input data.
321
+ - val_y : array-like
322
+ Validation target data.
323
+ - Ns : int
324
+ Number of samples.
325
+ - tclen : int
326
+ Length of time series.
327
+ - thebatchsize : int
328
+ Batch size for training.
329
+
330
+ Examples
331
+ --------
332
+ >>> model = MyModel()
333
+ >>> model.initialized = True
334
+ >>> model.loaddata()
335
+ >>> print(model.train_x.shape)
336
+ (1000, 10)
337
+ """
338
+ if not self.initialized:
339
+ raise Exception("model must be initialized prior to loading data")
340
+
341
+ if self.dofft:
342
+ (
343
+ self.train_x,
344
+ self.train_y,
345
+ self.val_x,
346
+ self.val_y,
347
+ self.Ns,
348
+ self.tclen,
349
+ self.thebatchsize,
350
+ dummy,
351
+ dummy,
352
+ ) = prep(
353
+ self.window_size,
354
+ thesuffix=self.thesuffix,
355
+ thedatadir=self.thedatadir,
356
+ inputfrag=self.inputfrag,
357
+ targetfrag=self.targetfrag,
358
+ startskip=self.startskip,
359
+ endskip=self.endskip,
360
+ corrthresh=self.corrthresh,
361
+ step=self.step,
362
+ dofft=self.dofft,
363
+ usebadpts=self.usebadpts,
364
+ excludethresh=self.excludethresh,
365
+ excludebysubject=self.excludebysubject,
366
+ readlim=self.readlim,
367
+ readskip=self.readskip,
368
+ countlim=self.countlim,
369
+ )
370
+ else:
371
+ (
372
+ self.train_x,
373
+ self.train_y,
374
+ self.val_x,
375
+ self.val_y,
376
+ self.Ns,
377
+ self.tclen,
378
+ self.thebatchsize,
379
+ ) = prep(
380
+ self.window_size,
381
+ thesuffix=self.thesuffix,
382
+ thedatadir=self.thedatadir,
383
+ inputfrag=self.inputfrag,
384
+ targetfrag=self.targetfrag,
385
+ startskip=self.startskip,
386
+ endskip=self.endskip,
387
+ corrthresh=self.corrthresh,
388
+ step=self.step,
389
+ dofft=self.dofft,
390
+ usebadpts=self.usebadpts,
391
+ excludethresh=self.excludethresh,
392
+ excludebysubject=self.excludebysubject,
393
+ readlim=self.readlim,
394
+ readskip=self.readskip,
395
+ countlim=self.countlim,
396
+ )
397
+
398
+ def predict_model(self, X: np.ndarray) -> np.ndarray:
399
+ """
400
+ Make predictions using the trained model.
401
+
402
+ Parameters
403
+ ----------
404
+ X : np.ndarray
405
+ Input features for prediction. Shape should be (n_samples, n_features)
406
+ where n_samples is the number of samples and n_features is the number
407
+ of features expected by the model.
408
+
409
+ Returns
410
+ -------
411
+ np.ndarray
412
+ Model predictions. Shape will depend on the specific model type but
413
+ typically follows (n_samples,) for regression or (n_samples, n_classes)
414
+ for classification.
415
+
416
+ Notes
417
+ -----
418
+ This method sets the model to inference mode by calling with training=False.
419
+ The predictions are made without computing gradients, making it efficient
420
+ for inference tasks. Input data is automatically converted to PyTorch tensors
421
+ and moved to the appropriate device. Special handling is included for
422
+ tensor dimension permutation to match model expectations.
423
+
424
+ Examples
425
+ --------
426
+ >>> # Assuming model is already trained
427
+ >>> X_test = np.array([[1.0, 2.0], [3.0, 4.0]])
428
+ >>> predictions = model.predict_model(X_test)
429
+ >>> print(predictions)
430
+ """
431
+ self.model.eval()
432
+ with torch.no_grad():
433
+ if isinstance(X, np.ndarray):
434
+ X = torch.from_numpy(X).float().to(self.device)
435
+ # PyTorch expects (batch, channels, length) but we have (batch, length, channels)
436
+ X = X.permute(0, 2, 1)
437
+ output = self.model(X)
438
+ # Convert back to (batch, length, channels)
439
+ output = output.permute(0, 2, 1)
440
+ return output.cpu().numpy()
441
+
442
+ def evaluate(self) -> tuple[list, list, float, float]:
443
+ """
444
+ Evaluate the model performance on validation data and compute loss metrics.
445
+
446
+ This method performs model evaluation by computing prediction errors and
447
+ saving training/validation loss curves. It calculates both prediction error
448
+ (difference between predicted and actual values) and raw error (difference
449
+ between input and actual values). The method also generates and saves a
450
+ plot of the training and validation loss over epochs.
451
+
452
+ Parameters
453
+ ----------
454
+ self : object
455
+ The instance of the class containing the model and data attributes.
456
+
457
+ Returns
458
+ -------
459
+ tuple[list, list, float, float]
460
+ A tuple containing:
461
+ - training_loss : list
462
+ List of training loss values per epoch
463
+ - validation_loss : list
464
+ List of validation loss values per epoch
465
+ - prediction_error : float
466
+ Mean squared error between predicted and actual values
467
+ - raw_error : float
468
+ Mean squared error between input features and actual values
469
+
470
+ Notes
471
+ -----
472
+ This method modifies the instance attributes:
473
+ - self.lossfilename: Path to the saved loss plot
474
+ - self.pred_error: Computed prediction error
475
+ - self.raw_error: Computed raw error
476
+ - self.loss: Training loss history
477
+ - self.val_loss: Validation loss history
478
+
479
+ The method saves:
480
+ - Loss plot as PNG file
481
+ - Loss metrics as text file
482
+
483
+ Examples
484
+ --------
485
+ >>> model = MyModel()
486
+ >>> train_loss, val_loss, pred_error, raw_error = model.evaluate()
487
+ >>> print(f"Prediction Error: {pred_error}")
488
+ Prediction Error: 0.1234
489
+ """
490
+ self.lossfilename = os.path.join(self.modelname, "loss.png")
491
+ LGR.info(f"lossfilename: {self.lossfilename}")
492
+
493
+ YPred = self.predict_model(self.val_x)
494
+
495
+ error = self.val_y - YPred
496
+ self.pred_error = np.mean(np.square(error))
497
+
498
+ error2 = self.val_x - self.val_y
499
+ self.raw_error = np.mean(np.square(error2))
500
+ LGR.info(f"Prediction Error: {self.pred_error}\tRaw Error: {self.raw_error}")
501
+
502
+ f = open(os.path.join(self.modelname, "loss.txt"), "w")
503
+ f.write(
504
+ self.modelname
505
+ + ": Prediction Error: "
506
+ + str(self.pred_error)
507
+ + " Raw Error: "
508
+ + str(self.raw_error)
509
+ + "\n"
510
+ )
511
+ f.close()
512
+
513
+ epochs = range(len(self.loss))
514
+
515
+ self.updatemetadata()
516
+
517
+ plt.figure()
518
+ plt.plot(epochs, self.loss, "bo", label="Training loss")
519
+ plt.plot(epochs, self.val_loss, "b", label="Validation loss")
520
+ plt.title("Training and validation loss")
521
+ plt.legend()
522
+ plt.savefig(self.lossfilename)
523
+ plt.close()
524
+
525
+ return self.loss, self.val_loss, self.pred_error, self.raw_error
526
+
527
+ def initmetadata(self) -> None:
528
+ """
529
+ Initialize and store metadata information for the model.
530
+
531
+ This function creates a dictionary containing various model configuration parameters
532
+ and writes them to a JSON file for future reference and reproducibility.
533
+
534
+ Parameters
535
+ ----------
536
+ self : object
537
+ The instance of the class containing the metadata attributes. Expected to have
538
+ the following attributes:
539
+ - `nettype`: Type of neural network
540
+ - `window_size`: Size of the window used for processing
541
+ - `usebadpts`: Flag indicating whether bad points are handled
542
+ - `dofft`: Flag indicating whether FFT is used
543
+ - `excludethresh`: Threshold for exclusion
544
+ - `num_epochs`: Number of training epochs
545
+ - `num_layers`: Number of layers in the model
546
+ - `dropout_rate`: Dropout rate for regularization
547
+ - `modelname`: Name of the model
548
+
549
+ Returns
550
+ -------
551
+ None
552
+ This function does not return any value but writes metadata to a JSON file.
553
+
554
+ Notes
555
+ -----
556
+ The metadata includes:
557
+ - Window size for processing
558
+ - Bad point handling flag
559
+ - FFT usage flag
560
+ - Exclusion threshold
561
+ - Number of epochs and layers
562
+ - Dropout rate
563
+ - Operating system platform
564
+ - Model name
565
+
566
+ The metadata is saved to ``{modelname}/model_meta.json`` where ``modelname``
567
+ is the model's name attribute.
568
+
569
+ Examples
570
+ --------
571
+ >>> model = MyModel()
572
+ >>> model.initmetadata()
573
+ >>> # Metadata stored in modelname/model_meta.json
574
+ """
575
+
576
+ self.infodict = {}
577
+ self.infodict["nettype"] = self.nettype
578
+ self.infodict["window_size"] = self.window_size
579
+ self.infodict["usebadpts"] = self.usebadpts
580
+ self.infodict["dofft"] = self.dofft
581
+ self.infodict["excludethresh"] = self.excludethresh
582
+ self.infodict["num_epochs"] = self.num_epochs
583
+ self.infodict["num_layers"] = self.num_layers
584
+ self.infodict["dropout_rate"] = self.dropout_rate
585
+ self.infodict["train_arch"] = sys.platform
586
+ self.infodict["modelname"] = self.modelname
587
+ tide_io.writedicttojson(self.infodict, os.path.join(self.modelname, "model_meta.json"))
588
+
589
+ def updatemetadata(self) -> None:
590
+ """
591
+ Update metadata dictionary with model metrics and save to JSON file.
592
+
593
+ This method updates the internal information dictionary with various model
594
+ performance metrics and writes the complete metadata to a JSON file for
595
+ model persistence and tracking.
596
+
597
+ Parameters
598
+ ----------
599
+ self : object
600
+ The instance of the class containing the metadata and model information.
601
+ Expected to have the following attributes:
602
+ - infodict : dict
603
+ Dictionary containing model metadata.
604
+ - loss : float
605
+ Training loss value.
606
+ - val_loss : float
607
+ Validation loss value.
608
+ - raw_error : float
609
+ Raw error metric.
610
+ - pred_error : float
611
+ Prediction error metric.
612
+ - modelname : str
613
+ Name/path of the model for file output.
614
+
615
+ Returns
616
+ -------
617
+ None
618
+ This method does not return any value but modifies the `infodict` in-place
619
+ and writes to a JSON file.
620
+
621
+ Notes
622
+ -----
623
+ The method writes metadata to ``{modelname}/model_meta.json`` where
624
+ ``modelname`` is the model name attribute of the instance.
625
+
626
+ Examples
627
+ --------
628
+ >>> model = MyModel()
629
+ >>> model.updatemetadata()
630
+ >>> # Creates model_meta.json with loss, val_loss, raw_error, and pred_error
631
+ """
632
+ self.infodict["loss"] = self.loss
633
+ self.infodict["val_loss"] = self.val_loss
634
+ self.infodict["raw_error"] = self.raw_error
635
+ self.infodict["prediction_error"] = self.pred_error
636
+ tide_io.writedicttojson(self.infodict, os.path.join(self.modelname, "model_meta.json"))
637
+
638
+ def savemodel(self, altname: str | None = None) -> None:
639
+ """
640
+ Save the model to disk with the specified name.
641
+
642
+ This method saves the current model to a Keras file format (.keras) in a
643
+ directory named according to the model name or an alternative name provided.
644
+
645
+ Parameters
646
+ ----------
647
+ altname : str, optional
648
+ Alternative name to use for saving the model. If None, uses the
649
+ model's default name stored in `self.modelname`. Default is None.
650
+
651
+ Returns
652
+ -------
653
+ None
654
+ This method does not return any value.
655
+
656
+ Notes
657
+ -----
658
+ The model is saved in the Keras format (.keras) and stored in a directory
659
+ with the same name as the model. The method logs the saving operation
660
+ using the logger instance `LGR`.
661
+
662
+ Examples
663
+ --------
664
+ >>> # Save model with default name
665
+ >>> savemodel()
666
+ >>>
667
+ >>> # Save model with alternative name
668
+ >>> savemodel(altname="my_custom_model")
669
+ """
670
+ if altname is None:
671
+ modelsavename = self.modelname
672
+ else:
673
+ modelsavename = altname
674
+ LGR.info(f"saving {modelsavename}")
675
+ torch.save(
676
+ {
677
+ "model_state_dict": self.model.state_dict(),
678
+ "model_config": (
679
+ self.model.get_config() if hasattr(self.model, "get_config") else None
680
+ ),
681
+ },
682
+ os.path.join(modelsavename, "model.pth"),
683
+ )
684
+
685
+ def loadmodel(self, modelname: str, verbose: bool = False) -> None:
686
+ """
687
+ Load a trained model from disk and initialize model parameters.
688
+
689
+ Load a Keras model from the specified model directory, along with associated
690
+ metadata and configuration information. The function attempts to load the model
691
+ in Keras format first, falling back to HDF5 format if the Keras format is not found.
692
+
693
+ Parameters
694
+ ----------
695
+ modelname : str
696
+ Name of the model to load, corresponding to a subdirectory in ``self.modelpath``.
697
+ verbose : bool, optional
698
+ If True, print model summary and metadata information. Default is False.
699
+
700
+ Returns
701
+ -------
702
+ None
703
+ This method modifies the instance attributes in-place and does not return anything.
704
+
705
+ Notes
706
+ -----
707
+ The function attempts to load the model in the following order:
708
+ 1. Keras format (model.keras)
709
+ 2. HDF5 format (model.h5)
710
+
711
+ If neither format is found, the function exits with an error message.
712
+
713
+ The loaded model metadata is stored in ``self.infodict``, and model configuration
714
+ is stored in ``self.config``. Additional attributes like ``window_size`` and
715
+ ``usebadpts`` are extracted from the metadata and stored as instance attributes.
716
+
717
+ Examples
718
+ --------
719
+ >>> loader = ModelLoader()
720
+ >>> loader.loadmodel("my_model", verbose=True)
721
+ loading my_model
722
+ Model: "sequential"
723
+ _________________________________________________________________
724
+ Layer (type) Output Shape Param #
725
+ =================================================================
726
+ ...
727
+ >>> print(loader.window_size)
728
+ 100
729
+ """
730
+ # read in the data
731
+ LGR.info(f"loading {modelname}")
732
+
733
+ # load additional information first to reconstruct model
734
+ self.infodict = tide_io.readdictfromjson(
735
+ os.path.join(self.modelpath, modelname, "model_meta.json")
736
+ )
737
+ if verbose:
738
+ print(self.infodict)
739
+ self.window_size = self.infodict["window_size"]
740
+ self.usebadpts = self.infodict["usebadpts"]
741
+
742
+ # Load the model as a dict
743
+ checkpoint = torch.load(
744
+ os.path.join(self.modelpath, modelname, "model.pth"), map_location=self.device
745
+ )
746
+
747
+ # Reconstruct the model architecture (must be done by subclass)
748
+ if self.infodict["nettype"] == "cnn":
749
+ self.num_filters = checkpoint["model_config"]["num_filters"]
750
+ self.kernel_size = checkpoint["model_config"]["kernel_size"]
751
+ self.num_layers = checkpoint["model_config"]["num_layers"]
752
+ self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
753
+ self.dilation_rate = checkpoint["model_config"]["dilation_rate"]
754
+ self.activation = checkpoint["model_config"]["activation"]
755
+ self.inputsize = checkpoint["model_config"]["inputsize"]
756
+
757
+ self.model = CNNModel(
758
+ self.num_filters,
759
+ self.kernel_size,
760
+ self.num_layers,
761
+ self.dropout_rate,
762
+ self.dilation_rate,
763
+ self.activation,
764
+ self.inputsize,
765
+ )
766
+ elif self.infodict["nettype"] == "autoencoder":
767
+ self.encoding_dim = checkpoint["model_config"]["encoding_dim"]
768
+ self.num_layers = checkpoint["model_config"]["num_layers"]
769
+ self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
770
+ self.activation = checkpoint["model_config"]["activation"]
771
+ self.inputsize = checkpoint["model_config"]["inputsize"]
772
+
773
+ self.model = DenseAutoencoderModel(
774
+ self.window_size,
775
+ self.encoding_dim,
776
+ self.num_layers,
777
+ self.dropout_rate,
778
+ self.activation,
779
+ self.inputsize,
780
+ )
781
+ elif self.infodict["nettype"] == "multiscalecnn":
782
+ self.num_filters = checkpoint["model_config"]["num_filters"]
783
+ self.kernel_sizes = checkpoint["model_config"]["kernel_sizes"]
784
+ self.input_lens = checkpoint["model_config"]["input_lens"]
785
+ self.input_width = checkpoint["model_config"]["input_width"]
786
+ self.dilation_rate = checkpoint["model_config"]["dilation_rate"]
787
+
788
+ self.model = MultiscaleCNNModel(
789
+ self.num_filters,
790
+ self.kernel_sizes,
791
+ self.input_lens,
792
+ self.input_width,
793
+ self.dilation_rate,
794
+ )
795
+ elif self.infodict["nettype"] == "convautoencoder":
796
+ self.encoding_dim = checkpoint["model_config"]["encoding_dim"]
797
+ self.num_filters = checkpoint["model_config"]["num_filters"]
798
+ self.kernel_size = checkpoint["model_config"]["kernel_size"]
799
+ self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
800
+ self.activation = checkpoint["model_config"]["activation"]
801
+ self.inputsize = checkpoint["model_config"]["inputsize"]
802
+
803
+ self.model = ConvAutoencoderModel(
804
+ self.window_size,
805
+ self.encoding_dim,
806
+ self.num_filters,
807
+ self.kernel_size,
808
+ self.dropout_rate,
809
+ self.activation,
810
+ self.inputsize,
811
+ )
812
+ elif self.infodict["nettype"] == "crnn":
813
+ self.num_filters = checkpoint["model_config"]["num_filters"]
814
+ self.kernel_size = checkpoint["model_config"]["kernel_size"]
815
+ self.encoding_dim = checkpoint["model_config"]["encoding_dim"]
816
+ self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
817
+ self.activation = checkpoint["model_config"]["activation"]
818
+ self.inputsize = checkpoint["model_config"]["inputsize"]
819
+
820
+ self.model = CRNNModel(
821
+ self.num_filters,
822
+ self.kernel_size,
823
+ self.encoding_dim,
824
+ self.dropout_rate,
825
+ self.activation,
826
+ self.inputsize,
827
+ )
828
+ elif self.infodict["nettype"] == "lstm":
829
+ self.num_units = checkpoint["model_config"]["num_units"]
830
+ self.num_layers = checkpoint["model_config"]["num_layers"]
831
+ self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
832
+ self.inputsize = checkpoint["model_config"]["inputsize"]
833
+
834
+ self.model = LSTMModel(
835
+ self.num_units,
836
+ self.num_layers,
837
+ self.dropout_rate,
838
+ self.window_size,
839
+ self.inputsize,
840
+ )
841
+ elif self.infodict["nettype"] == "hybrid":
842
+ self.num_filters = checkpoint["model_config"]["num_filters"]
843
+ self.kernel_size = checkpoint["model_config"]["kernel_size"]
844
+ self.num_units = checkpoint["model_config"]["num_units"]
845
+ self.num_layers = checkpoint["model_config"]["num_layers"]
846
+ self.dropout_rate = checkpoint["model_config"]["dropout_rate"]
847
+ self.activation = checkpoint["model_config"]["activation"]
848
+ self.inputsize = checkpoint["model_config"]["inputsize"]
849
+ self.invert = checkpoint["model_config"]["invert"]
850
+
851
+ self.model = HybridModel(
852
+ self.num_filters,
853
+ self.kernel_size,
854
+ self.num_units,
855
+ self.num_layers,
856
+ self.dropout_rate,
857
+ self.activation,
858
+ self.inputsize,
859
+ self.window_size,
860
+ self.invert,
861
+ )
862
+ else:
863
+ print(f"nettype {self.infodict['nettype']} is not supported!")
864
+ sys.exit()
865
+
866
+ self.model.load_state_dict(checkpoint["model_state_dict"])
867
+ self.model.to(self.device)
868
+
869
+ def initialize(self) -> None:
870
+ """
871
+ Initialize the model by setting up network architecture and metadata.
872
+
873
+ This method performs a series of initialization steps including retrieving
874
+ the model name, creating the network architecture, displaying model summary,
875
+ saving the model configuration, initializing metadata, and setting appropriate
876
+ flags to indicate initialization status.
877
+
878
+ Parameters
879
+ ----------
880
+ self : object
881
+ The instance of the model class being initialized.
882
+
883
+ Returns
884
+ -------
885
+ None
886
+ This method does not return any value.
887
+
888
+ Notes
889
+ -----
890
+ This method should be called before any training or prediction operations.
891
+ The initialization process sets `self.initialized` to True and `self.trained`
892
+ to False, indicating that the model is ready for training but has not been
893
+ trained yet.
894
+
895
+ Examples
896
+ --------
897
+ >>> model = MyModel()
898
+ >>> model.initialize()
899
+ >>> print(model.initialized)
900
+ True
901
+ >>> print(model.trained)
902
+ False
903
+ """
904
+ self.getname()
905
+ self.makenet()
906
+ print(self.model)
907
+ self.savemodel()
908
+ self.initmetadata()
909
+ self.initialized = True
910
+ self.trained = False
911
+
912
+ def train(self) -> None:
913
+ """
914
+ Train the model using the provided training and validation datasets.
915
+
916
+ This method performs model training with optional pretraining and logging. It supports
917
+ TensorBoard logging, model checkpointing, early stopping, and NaN termination. The trained
918
+ model is saved at the end of training.
919
+
920
+ Parameters
921
+ ----------
922
+ self : object
923
+ The instance of the class containing the model and training configuration.
924
+ Expected attributes include:
925
+ - `model`: PyTorch model to be trained.
926
+ - `train_x`, `train_y`, `val_x`, `val_y`: Training and validation data as numpy arrays.
927
+ - `device`: Device to run the training on (e.g., 'cpu' or 'cuda').
928
+ - `num_pretrain_epochs`: Number of pretraining epochs (default: 0).
929
+ - `num_epochs`: Number of main training epochs.
930
+ - `modelname`: Directory name to save model checkpoints and logs.
931
+ - `usetensorboard`: Boolean flag to enable TensorBoard logging.
932
+ - `savemodel()`: Method to save the final trained model.
933
+
934
+ Returns
935
+ -------
936
+ None
937
+ This function does not return any value.
938
+
939
+ Notes
940
+ -----
941
+ - If `self.usetensorboard` is True, TensorBoard logging is enabled.
942
+ - If `self.num_pretrain_epochs` is greater than 0, a pretraining phase is performed
943
+ before the main training loop.
944
+ - The model is saved after training using the `savemodel()` method.
945
+ - Training uses `ModelCheckpoint`, `EarlyStopping`, and `TerminateOnNaN` callbacks
946
+ to manage training process and prevent overfitting or NaN issues.
947
+ - Intermediate model checkpoints are saved during training.
948
+ - The best model (based on validation loss) is retained and restored upon early stopping.
949
+
950
+ Examples
951
+ --------
952
+ >>> trainer = ModelTrainer(model, train_x, train_y, val_x, val_y)
953
+ >>> trainer.train()
954
+ """
955
+ self.model.train()
956
+ self.model.to(self.device)
957
+
958
+ # Convert numpy arrays to PyTorch tensors and transpose for Conv1d
959
+ print("converting tensors")
960
+ train_x_tensor = torch.from_numpy(self.train_x).float().permute(0, 2, 1)
961
+ train_y_tensor = torch.from_numpy(self.train_y).float().permute(0, 2, 1)
962
+ val_x_tensor = torch.from_numpy(self.val_x).float().permute(0, 2, 1)
963
+ val_y_tensor = torch.from_numpy(self.val_y).float().permute(0, 2, 1)
964
+
965
+ print("setting data")
966
+ train_dataset = TensorDataset(train_x_tensor, train_y_tensor)
967
+ val_dataset = TensorDataset(val_x_tensor, val_y_tensor)
968
+
969
+ train_loader = DataLoader(train_dataset, batch_size=1024, shuffle=True)
970
+ val_loader = DataLoader(val_dataset, batch_size=1024, shuffle=False)
971
+
972
+ print("setting criterion")
973
+ criterion = nn.MSELoss()
974
+
975
+ print("setting optimizer")
976
+ optimizer = optim.RMSprop(self.model.parameters())
977
+
978
+ self.loss = []
979
+ self.val_loss = []
980
+
981
+ best_val_loss = float("inf")
982
+ patience = 10
983
+ patience_counter = 0
984
+
985
+ total_epochs = self.num_pretrain_epochs + self.num_epochs
986
+
987
+ for epoch in range(total_epochs):
988
+ print(f"Epoch {epoch+1}/{total_epochs}")
989
+ # Training phase
990
+ self.model.train()
991
+ train_loss_epoch = 0.0
992
+ # for batch_x, batch_y in train_loader:
993
+ for batch_x, batch_y in tqdm.tqdm(
994
+ train_loader,
995
+ desc="Batch",
996
+ unit="batches",
997
+ disable=False,
998
+ ):
999
+ batch_x, batch_y = batch_x.to(self.device), batch_y.to(self.device)
1000
+
1001
+ optimizer.zero_grad()
1002
+ outputs = self.model(batch_x)
1003
+ loss = criterion(outputs, batch_y)
1004
+
1005
+ if torch.isnan(loss):
1006
+ LGR.error("NaN loss detected, terminating training")
1007
+ break
1008
+
1009
+ loss.backward()
1010
+ optimizer.step()
1011
+ train_loss_epoch += loss.item()
1012
+
1013
+ train_loss_epoch /= len(train_loader)
1014
+ self.loss.append(train_loss_epoch)
1015
+
1016
+ # Validation phase
1017
+ self.model.eval()
1018
+ val_loss_epoch = 0.0
1019
+ with torch.no_grad():
1020
+ for batch_x, batch_y in val_loader:
1021
+ batch_x, batch_y = batch_x.to(self.device), batch_y.to(self.device)
1022
+ outputs = self.model(batch_x)
1023
+ loss = criterion(outputs, batch_y)
1024
+ val_loss_epoch += loss.item()
1025
+
1026
+ val_loss_epoch /= len(val_loader)
1027
+ self.val_loss.append(val_loss_epoch)
1028
+
1029
+ LGR.info(
1030
+ f"Epoch {epoch+1}/{total_epochs} - Loss: {train_loss_epoch:.4f} - Val Loss: {val_loss_epoch:.4f}"
1031
+ )
1032
+
1033
+ # Save checkpoint
1034
+ self.intermediatemodelpath = os.path.join(
1035
+ self.modelname, f"model_e{epoch+1:02d}_v{val_loss_epoch:.4f}.pth"
1036
+ )
1037
+ torch.save(
1038
+ {
1039
+ "epoch": epoch,
1040
+ "model_state_dict": self.model.state_dict(),
1041
+ "optimizer_state_dict": optimizer.state_dict(),
1042
+ "loss": train_loss_epoch,
1043
+ "val_loss": val_loss_epoch,
1044
+ },
1045
+ self.intermediatemodelpath,
1046
+ )
1047
+
1048
+ # Early stopping
1049
+ if val_loss_epoch < best_val_loss:
1050
+ best_val_loss = val_loss_epoch
1051
+ patience_counter = 0
1052
+ # Save best model
1053
+ torch.save(self.model.state_dict(), os.path.join(self.modelname, "best_model.pth"))
1054
+ else:
1055
+ patience_counter += 1
1056
+ if patience_counter >= patience:
1057
+ LGR.info(f"Early stopping triggered after {epoch+1} epochs")
1058
+ # Restore best weights
1059
+ self.model.load_state_dict(
1060
+ torch.load(
1061
+ os.path.join(self.modelname, "best_model.pth"), weights_only=True
1062
+ )
1063
+ )
1064
+ break
1065
+ self.evaluate()
1066
+
1067
+ self.savemodel()
1068
+ self.trained = True
1069
+
1070
+ def apply(self, inputdata: np.ndarray, badpts: np.ndarray | None = None) -> np.ndarray:
1071
+ """
1072
+ Apply a sliding-window prediction model to the input data, optionally incorporating bad points.
1073
+
1074
+ This function performs a sliding-window prediction using a pre-trained model. It scales the input
1075
+ data using the median absolute deviation (MAD), applies the model to overlapping windows of data,
1076
+ and aggregates predictions with a weighted scheme. Optionally, bad points can be included in
1077
+ the prediction process to influence the model's behavior.
1078
+
1079
+ Parameters
1080
+ ----------
1081
+ inputdata : np.ndarray
1082
+ Input data array of shape (N,) to be processed.
1083
+ badpts : np.ndarray | None, optional
1084
+ Array of same shape as `inputdata` indicating bad or invalid points. If None, no bad points
1085
+ are considered. Default is None.
1086
+
1087
+ Returns
1088
+ -------
1089
+ np.ndarray
1090
+ Predicted data array of the same shape as `inputdata`, with predictions aggregated and
1091
+ weighted across overlapping windows.
1092
+
1093
+ Notes
1094
+ -----
1095
+ - The function uses a sliding window of size `self.window_size` to process input data.
1096
+ - Predictions are aggregated by summing over overlapping windows.
1097
+ - A triangular weight scheme is applied to the aggregated predictions to reduce edge effects.
1098
+ - If `self.usebadpts` is True, `badpts` are included as an additional feature in the model input.
1099
+
1100
+ Examples
1101
+ --------
1102
+ >>> model = MyModel(window_size=10, usebadpts=True)
1103
+ >>> input_data = np.random.randn(100)
1104
+ >>> bad_points = np.zeros_like(input_data)
1105
+ >>> result = model.apply(input_data, bad_points)
1106
+ """
1107
+ initscale = mad(inputdata)
1108
+ scaleddata = inputdata / initscale
1109
+ predicteddata = scaleddata * 0.0
1110
+ weightarray = scaleddata * 0.0
1111
+ N_pts = len(scaleddata)
1112
+ if self.usebadpts:
1113
+ if badpts is None:
1114
+ badpts = scaleddata * 0.0
1115
+ X = np.zeros(((N_pts - self.window_size - 1), self.window_size, 2))
1116
+ for i in range(X.shape[0]):
1117
+ X[i, :, 0] = scaleddata[i : i + self.window_size]
1118
+ X[i, :, 1] = badpts[i : i + self.window_size]
1119
+ else:
1120
+ X = np.zeros(((N_pts - self.window_size - 1), self.window_size, 1))
1121
+ for i in range(X.shape[0]):
1122
+ X[i, :, 0] = scaleddata[i : i + self.window_size]
1123
+
1124
+ Y = self.predict_model(X)
1125
+ for i in range(X.shape[0]):
1126
+ predicteddata[i : i + self.window_size] += Y[i, :, 0]
1127
+
1128
+ weightarray[:] = self.window_size
1129
+ weightarray[0 : self.window_size] = np.linspace(
1130
+ 1.0, self.window_size, self.window_size, endpoint=False
1131
+ )
1132
+ weightarray[-(self.window_size + 1) : -1] = np.linspace(
1133
+ self.window_size, 1.0, self.window_size, endpoint=False
1134
+ )
1135
+ return initscale * predicteddata / weightarray
1136
+
1137
+
1138
+ class CNNModel(nn.Module):
1139
+ def __init__(
1140
+ self,
1141
+ num_filters: int,
1142
+ kernel_size: int,
1143
+ num_layers: int,
1144
+ dropout_rate: float,
1145
+ dilation_rate: int,
1146
+ activation: str,
1147
+ inputsize: int,
1148
+ ) -> None:
1149
+ """
1150
+ Initialize the CNNModel with specified architecture parameters.
1151
+
1152
+ Parameters
1153
+ ----------
1154
+ num_filters : int
1155
+ Number of convolutional filters in each layer.
1156
+ kernel_size : int
1157
+ Size of the convolutional kernel.
1158
+ num_layers : int
1159
+ Total number of layers in the network.
1160
+ dropout_rate : float
1161
+ Dropout rate for regularization.
1162
+ dilation_rate : int
1163
+ Dilation rate for dilated convolutions in intermediate layers.
1164
+ activation : str
1165
+ Activation function to use; options are 'relu' or 'tanh'.
1166
+ inputsize : int
1167
+ Size of the input features.
1168
+
1169
+ Returns
1170
+ -------
1171
+ None
1172
+ This method initializes the model in-place and does not return any value.
1173
+
1174
+ Notes
1175
+ -----
1176
+ The model consists of an input layer, intermediate layers with dilated convolutions,
1177
+ and an output layer. Batch normalization and dropout are applied after each convolutional
1178
+ layer except the output layer. The activation function is applied after each convolutional
1179
+ layer based on the `activation` parameter.
1180
+
1181
+ Examples
1182
+ --------
1183
+ >>> model = CNNModel(
1184
+ ... num_filters=64,
1185
+ ... kernel_size=3,
1186
+ ... num_layers=4,
1187
+ ... dropout_rate=0.2,
1188
+ ... dilation_rate=2,
1189
+ ... activation="relu",
1190
+ ... inputsize=10
1191
+ ... )
1192
+ """
1193
+ super(CNNModel, self).__init__()
1194
+
1195
+ self.num_filters = num_filters
1196
+ self.kernel_size = kernel_size
1197
+ self.num_layers = num_layers
1198
+ self.dropout_rate = dropout_rate
1199
+ self.dilation_rate = dilation_rate
1200
+ self.activation = activation
1201
+ self.inputsize = inputsize
1202
+
1203
+ self.layers = nn.ModuleList()
1204
+
1205
+ # Input layer
1206
+ self.layers.append(nn.Conv1d(inputsize, num_filters, kernel_size, padding="same"))
1207
+ self.layers.append(nn.BatchNorm1d(num_filters))
1208
+ self.layers.append(nn.Dropout(dropout_rate))
1209
+ if activation == "relu":
1210
+ self.layers.append(nn.ReLU())
1211
+ elif activation == "tanh":
1212
+ self.layers.append(nn.Tanh())
1213
+ else:
1214
+ self.layers.append(nn.ReLU())
1215
+
1216
+ # Intermediate layers
1217
+ for _ in range(num_layers - 2):
1218
+ self.layers.append(
1219
+ nn.Conv1d(
1220
+ num_filters,
1221
+ num_filters,
1222
+ kernel_size,
1223
+ dilation=dilation_rate,
1224
+ padding="same",
1225
+ )
1226
+ )
1227
+ self.layers.append(nn.BatchNorm1d(num_filters))
1228
+ self.layers.append(nn.Dropout(dropout_rate))
1229
+ if activation == "relu":
1230
+ self.layers.append(nn.ReLU())
1231
+ elif activation == "tanh":
1232
+ self.layers.append(nn.Tanh())
1233
+ else:
1234
+ self.layers.append(nn.ReLU())
1235
+
1236
+ # Output layer
1237
+ self.layers.append(nn.Conv1d(num_filters, inputsize, kernel_size, padding="same"))
1238
+
1239
+ def forward(self, x):
1240
+ """
1241
+ Forward pass through all layers.
1242
+
1243
+ Applies each layer in the network sequentially to the input tensor.
1244
+
1245
+ Parameters
1246
+ ----------
1247
+ x : torch.Tensor
1248
+ Input tensor to the forward pass. Shape should be compatible with the
1249
+ first layer's expected input dimensions.
1250
+
1251
+ Returns
1252
+ -------
1253
+ torch.Tensor
1254
+ Output tensor after passing through all layers. Shape will depend on
1255
+ the output dimensions of the last layer in the network.
1256
+
1257
+ Notes
1258
+ -----
1259
+ This method applies layers in the order they were added to the network.
1260
+ Each layer's forward method is called sequentially, with the output of
1261
+ one layer becoming the input to the next.
1262
+
1263
+ Examples
1264
+ --------
1265
+ >>> import torch
1266
+ >>> model = MyNetwork()
1267
+ >>> input_tensor = torch.randn(32, 10)
1268
+ >>> output = model.forward(input_tensor)
1269
+ >>> print(output.shape)
1270
+ torch.Size([32, 5])
1271
+ """
1272
+ for layer in self.layers:
1273
+ x = layer(x)
1274
+ return x
1275
+
1276
+ def get_config(self):
1277
+ """
1278
+ Get the configuration parameters of the model.
1279
+
1280
+ Returns
1281
+ -------
1282
+ dict
1283
+ A dictionary containing all configuration parameters with their current values:
1284
+ - num_filters: int, number of filters in the convolutional layers
1285
+ - kernel_size: int, size of the convolutional kernel
1286
+ - num_layers: int, number of layers in the network
1287
+ - dropout_rate: float, dropout rate for regularization
1288
+ - dilation_rate: int, dilation rate for dilated convolution
1289
+ - activation: str, activation function used in layers
1290
+ - inputsize: tuple, input dimensions of the model
1291
+
1292
+ Notes
1293
+ -----
1294
+ This method returns a copy of the current configuration. Modifications to the
1295
+ returned dictionary will not affect the original model configuration.
1296
+
1297
+ Examples
1298
+ --------
1299
+ >>> config = model.get_config()
1300
+ >>> print(config['num_filters'])
1301
+ 32
1302
+ """
1303
+ return {
1304
+ "num_filters": self.num_filters,
1305
+ "kernel_size": self.kernel_size,
1306
+ "num_layers": self.num_layers,
1307
+ "dropout_rate": self.dropout_rate,
1308
+ "dilation_rate": self.dilation_rate,
1309
+ "activation": self.activation,
1310
+ "inputsize": self.inputsize,
1311
+ }
1312
+
1313
+
1314
+ class CNNDLFilter(DeepLearningFilter):
1315
+ def __init__(
1316
+ self,
1317
+ num_filters: int = 10,
1318
+ kernel_size: int = 5,
1319
+ dilation_rate: int = 1,
1320
+ *args,
1321
+ **kwargs,
1322
+ ) -> None:
1323
+ """
1324
+ Initialize CNN deep learning filter.
1325
+
1326
+ Parameters
1327
+ ----------
1328
+ num_filters : int, optional
1329
+ Number of convolutional filters to use, by default 10
1330
+ kernel_size : int, optional
1331
+ Size of the convolutional kernel, by default 5
1332
+ dilation_rate : int, optional
1333
+ Dilation rate for the convolutional layers, by default 1
1334
+ *args
1335
+ Variable length argument list passed to parent class
1336
+ **kwargs
1337
+ Arbitrary keyword arguments passed to parent class
1338
+
1339
+ Returns
1340
+ -------
1341
+ None
1342
+ This method initializes the instance and does not return any value
1343
+
1344
+ Notes
1345
+ -----
1346
+ This constructor sets up the basic configuration for a CNN filter with
1347
+ specified number of filters, kernel size, and dilation rate. The network
1348
+ type is automatically set to "cnn" and information is stored in infodict
1349
+ for later reference.
1350
+
1351
+ Examples
1352
+ --------
1353
+ >>> filter = CNNDLFilter(num_filters=32, kernel_size=3, dilation_rate=2)
1354
+ >>> print(filter.num_filters)
1355
+ 32
1356
+ """
1357
+ self.num_filters = num_filters
1358
+ self.kernel_size = kernel_size
1359
+ self.dilation_rate = dilation_rate
1360
+ self.nettype = "cnn"
1361
+ self.infodict["nettype"] = self.nettype
1362
+ self.infodict["num_filters"] = self.num_filters
1363
+ self.infodict["kernel_size"] = self.kernel_size
1364
+ super(CNNDLFilter, self).__init__(*args, **kwargs)
1365
+
1366
+ def getname(self):
1367
+ """
1368
+ Generate and configure the model name and path based on current parameters.
1369
+
1370
+ This method constructs a descriptive model name string using various instance
1371
+ attributes and creates the corresponding directory path. The generated name
1372
+ includes information about model architecture, hyperparameters, and configuration
1373
+ options. The method also ensures the model directory exists by creating it if
1374
+ necessary.
1375
+
1376
+ Parameters
1377
+ ----------
1378
+ self : object
1379
+ The instance containing model configuration parameters.
1380
+
1381
+ Returns
1382
+ -------
1383
+ None
1384
+ This method does not return a value but modifies instance attributes:
1385
+ - self.modelname: Generated model name string
1386
+ - self.modelpath: Full path to the model directory
1387
+
1388
+ Notes
1389
+ -----
1390
+ The generated model name follows a specific format:
1391
+ "model_cnn_pytorch_wXXX_lYY_fnZZ_flZZ_eXXX_tY_ctZ_sZ_dZ_activation[options]"
1392
+
1393
+ Where:
1394
+ - XXX: window_size (3 digits zero-padded)
1395
+ - YY: num_layers (2 digits zero-padded)
1396
+ - ZZ: num_filters (2 digits zero-padded)
1397
+ - ZZ: kernel_size (2 digits zero-padded)
1398
+ - XXX: num_epochs (3 digits zero-padded)
1399
+ - Y: excludethresh (single digit)
1400
+ - Z: corrthresh (single digit)
1401
+ - Z: step (single digit)
1402
+ - Z: dilation_rate (single digit)
1403
+
1404
+ Options are appended if corresponding boolean flags are True:
1405
+ - _usebadpts: when usebadpts is True
1406
+ - _excludebysubject: when excludebysubject is True
1407
+
1408
+ Examples
1409
+ --------
1410
+ >>> model = MyModel()
1411
+ >>> model.window_size = 128
1412
+ >>> model.num_layers = 3
1413
+ >>> model.num_filters = 16
1414
+ >>> model.kernel_size = 3
1415
+ >>> model.num_epochs = 100
1416
+ >>> model.excludethresh = 0.5
1417
+ >>> model.corrthresh = 0.8
1418
+ >>> model.step = 1
1419
+ >>> model.dilation_rate = 2
1420
+ >>> model.activation = "relu"
1421
+ >>> model.usebadpts = True
1422
+ >>> model.excludebysubject = False
1423
+ >>> model.namesuffix = "test"
1424
+ >>> model.getname()
1425
+ >>> print(model.modelname)
1426
+ 'model_cnn_pytorch_w128_l03_fn16_fl03_e100_t0_ct0_s1_d2_relu_usebadpts_test'
1427
+ """
1428
+ self.modelname = "_".join(
1429
+ [
1430
+ "model",
1431
+ "cnn",
1432
+ "pytorch",
1433
+ "w" + str(self.window_size).zfill(3),
1434
+ "l" + str(self.num_layers).zfill(2),
1435
+ "fn" + str(self.num_filters).zfill(2),
1436
+ "fl" + str(self.kernel_size).zfill(2),
1437
+ "e" + str(self.num_epochs).zfill(3),
1438
+ "t" + str(self.excludethresh),
1439
+ "ct" + str(self.corrthresh),
1440
+ "s" + str(self.step),
1441
+ "d" + str(self.dilation_rate),
1442
+ self.activation,
1443
+ ]
1444
+ )
1445
+ if self.usebadpts:
1446
+ self.modelname += "_usebadpts"
1447
+ if self.excludebysubject:
1448
+ self.modelname += "_excludebysubject"
1449
+ if self.namesuffix is not None:
1450
+ self.modelname += "_" + self.namesuffix
1451
+ self.modelpath = os.path.join(self.modelroot, self.modelname)
1452
+
1453
+ try:
1454
+ os.makedirs(self.modelpath)
1455
+ except OSError:
1456
+ pass
1457
+
1458
+ def makenet(self):
1459
+ """
1460
+ Create and configure a CNN model for neural network training.
1461
+
1462
+ This method initializes a CNNModel with the specified parameters and moves
1463
+ it to the designated device (CPU or GPU). The model configuration is
1464
+ determined by the instance attributes set prior to calling this method.
1465
+
1466
+ Parameters
1467
+ ----------
1468
+ self : object
1469
+ The instance containing the following attributes:
1470
+ - num_filters : int
1471
+ Number of filters in each convolutional layer
1472
+ - kernel_size : int or tuple
1473
+ Size of the convolutional kernel
1474
+ - num_layers : int
1475
+ Number of convolutional layers in the network
1476
+ - dropout_rate : float
1477
+ Dropout rate for regularization
1478
+ - dilation_rate : int or tuple
1479
+ Dilation rate for dilated convolutions
1480
+ - activation : str or callable
1481
+ Activation function to use
1482
+ - inputsize : tuple
1483
+ Input dimensions for the model
1484
+ - device : torch.device
1485
+ Device to move the model to (CPU or GPU)
1486
+
1487
+ Returns
1488
+ -------
1489
+ None
1490
+ This method does not return any value. It modifies the instance
1491
+ by setting the `model` attribute to the created CNNModel.
1492
+
1493
+ Notes
1494
+ -----
1495
+ The method assumes that all required attributes are properly initialized
1496
+ before calling. The model is automatically moved to the specified device
1497
+ using the `.to()` method.
1498
+
1499
+ Examples
1500
+ --------
1501
+ >>> # Assuming all required attributes are set
1502
+ >>> makenet()
1503
+ >>> # Model is now available as self.model
1504
+ >>> print(self.model)
1505
+ CNNModel(...)
1506
+ """
1507
+ self.model = CNNModel(
1508
+ self.num_filters,
1509
+ self.kernel_size,
1510
+ self.num_layers,
1511
+ self.dropout_rate,
1512
+ self.dilation_rate,
1513
+ self.activation,
1514
+ self.inputsize,
1515
+ )
1516
+ self.model.to(self.device)
1517
+
1518
+
1519
+ class DenseAutoencoderModel(nn.Module):
1520
+ def __init__(
1521
+ self,
1522
+ window_size: int,
1523
+ encoding_dim: int,
1524
+ num_layers: int,
1525
+ dropout_rate: float,
1526
+ activation: str,
1527
+ inputsize: int,
1528
+ ) -> None:
1529
+ """
1530
+ Initialize a dense autoencoder model with configurable architecture.
1531
+
1532
+ This constructor builds a symmetric dense autoencoder with customizable number of layers,
1533
+ encoding dimension, dropout rate, and activation function. The model architecture follows
1534
+ a symmetric encoder-decoder structure, where the bottleneck layer reduces the feature space
1535
+ to `encoding_dim`, and then expands back to the original input size.
1536
+
1537
+ Parameters
1538
+ ----------
1539
+ window_size : int
1540
+ The size of the input window (number of time steps or features per sample).
1541
+ encoding_dim : int
1542
+ The dimension of the latent space (bottleneck layer size).
1543
+ num_layers : int
1544
+ Total number of layers in the autoencoder (including input and output).
1545
+ dropout_rate : float
1546
+ Dropout rate applied to all hidden layers to prevent overfitting.
1547
+ activation : str
1548
+ Activation function to use in hidden layers. Supported values: 'relu', 'tanh'.
1549
+ Defaults to 'relu' if an unsupported value is provided.
1550
+ inputsize : int
1551
+ The number of features per time step in the input data.
1552
+
1553
+ Returns
1554
+ -------
1555
+ None
1556
+ This method initializes the model in-place and does not return any value.
1557
+
1558
+ Notes
1559
+ -----
1560
+ - The model uses batch normalization after each linear layer except the output layer.
1561
+ - The architecture is symmetric: the number of neurons decreases from input to bottleneck
1562
+ and then increases back to the output size.
1563
+ - Logging is performed at various stages to track layer sizes during construction.
1564
+
1565
+ Examples
1566
+ --------
1567
+ >>> model = DenseAutoencoderModel(
1568
+ ... window_size=10,
1569
+ ... encoding_dim=5,
1570
+ ... num_layers=4,
1571
+ ... dropout_rate=0.2,
1572
+ ... activation="relu",
1573
+ ... inputsize=3
1574
+ ... )
1575
+ """
1576
+ super(DenseAutoencoderModel, self).__init__()
1577
+
1578
+ self.window_size = window_size
1579
+ self.encoding_dim = encoding_dim
1580
+ self.num_layers = num_layers
1581
+ self.dropout_rate = dropout_rate
1582
+ self.activation = activation
1583
+ self.inputsize = inputsize
1584
+
1585
+ self.layers = nn.ModuleList()
1586
+
1587
+ # Calculate initial size factor
1588
+ sizefac = 2
1589
+ for i in range(1, num_layers - 1):
1590
+ sizefac = int(sizefac * 2)
1591
+ LGR.info(f"input layer - sizefac: {sizefac}")
1592
+
1593
+ # Input layer
1594
+ self.layers.append(nn.Linear(window_size * inputsize, sizefac * encoding_dim))
1595
+ self.layers.append(nn.BatchNorm1d(sizefac * encoding_dim))
1596
+ self.layers.append(nn.Dropout(dropout_rate))
1597
+ if activation == "relu":
1598
+ self.layers.append(nn.ReLU())
1599
+ elif activation == "tanh":
1600
+ self.layers.append(nn.Tanh())
1601
+ else:
1602
+ self.layers.append(nn.ReLU())
1603
+
1604
+ # Encoding layers
1605
+ for i in range(1, num_layers - 1):
1606
+ sizefac = int(sizefac // 2)
1607
+ LGR.info(f"encoder layer {i + 1}, sizefac: {sizefac}")
1608
+ self.layers.append(nn.Linear(sizefac * 2 * encoding_dim, sizefac * encoding_dim))
1609
+ self.layers.append(nn.BatchNorm1d(sizefac * encoding_dim))
1610
+ self.layers.append(nn.Dropout(dropout_rate))
1611
+ if activation == "relu":
1612
+ self.layers.append(nn.ReLU())
1613
+ elif activation == "tanh":
1614
+ self.layers.append(nn.Tanh())
1615
+ else:
1616
+ self.layers.append(nn.ReLU())
1617
+
1618
+ # Encoding layer (bottleneck)
1619
+ sizefac = int(sizefac // 2)
1620
+ LGR.info(f"encoding layer - sizefac: {sizefac}")
1621
+ self.layers.append(nn.Linear(sizefac * 2 * encoding_dim, encoding_dim))
1622
+ self.layers.append(nn.BatchNorm1d(encoding_dim))
1623
+ self.layers.append(nn.Dropout(dropout_rate))
1624
+ if activation == "relu":
1625
+ self.layers.append(nn.ReLU())
1626
+ elif activation == "tanh":
1627
+ self.layers.append(nn.Tanh())
1628
+ else:
1629
+ self.layers.append(nn.ReLU())
1630
+
1631
+ # Decoding layers
1632
+ for i in range(1, num_layers):
1633
+ sizefac = int(sizefac * 2)
1634
+ LGR.info(f"decoding layer {i}, sizefac: {sizefac}")
1635
+ if i == 1:
1636
+ self.layers.append(nn.Linear(encoding_dim, sizefac * encoding_dim))
1637
+ else:
1638
+ self.layers.append(nn.Linear(sizefac // 2 * encoding_dim, sizefac * encoding_dim))
1639
+ self.layers.append(nn.BatchNorm1d(sizefac * encoding_dim))
1640
+ self.layers.append(nn.Dropout(dropout_rate))
1641
+ if activation == "relu":
1642
+ self.layers.append(nn.ReLU())
1643
+ elif activation == "tanh":
1644
+ self.layers.append(nn.Tanh())
1645
+ else:
1646
+ self.layers.append(nn.ReLU())
1647
+
1648
+ # Output layer
1649
+ self.layers.append(nn.Linear(sizefac * encoding_dim, window_size * inputsize))
1650
+
1651
+ def forward(self, x):
1652
+ """
1653
+ Forward pass through the network layers.
1654
+
1655
+ Applies a series of layers to the input tensor, flattening and reshaping
1656
+ as needed for processing.
1657
+
1658
+ Parameters
1659
+ ----------
1660
+ x : torch.Tensor
1661
+ Input tensor with shape (batch, channels, length) where batch is the
1662
+ number of samples, channels is the number of channels, and length is
1663
+ the sequence length.
1664
+
1665
+ Returns
1666
+ -------
1667
+ torch.Tensor
1668
+ Output tensor with shape (batch, channels, length) where the
1669
+ dimensions match the input shape after processing through the layers.
1670
+
1671
+ Notes
1672
+ -----
1673
+ The function first flattens the input from (batch, channels, length) to
1674
+ (batch, channels*length) to enable processing through linear layers,
1675
+ then reshapes the output back to the original format.
1676
+
1677
+ Examples
1678
+ --------
1679
+ >>> import torch
1680
+ >>> # Assuming self.layers contains appropriate layer definitions
1681
+ >>> x = torch.randn(32, 3, 100) # batch_size=32, channels=3, length=100
1682
+ >>> output = model.forward(x)
1683
+ >>> output.shape
1684
+ torch.Size([32, 3, 100])
1685
+ """
1686
+ # Flatten input from (batch, channels, length) to (batch, channels*length)
1687
+ batch_size = x.shape[0]
1688
+ x = x.reshape(batch_size, -1)
1689
+
1690
+ for layer in self.layers:
1691
+ x = layer(x)
1692
+
1693
+ # Reshape back to (batch, channels, length)
1694
+ x = x.reshape(batch_size, self.inputsize, self.window_size)
1695
+ return x
1696
+
1697
+ def get_config(self):
1698
+ """
1699
+ Get the configuration parameters of the model.
1700
+
1701
+ Returns
1702
+ -------
1703
+ dict
1704
+ A dictionary containing the model configuration parameters with the following keys:
1705
+ - "window_size" (int): The size of the sliding window used for input sequences
1706
+ - "encoding_dim" (int): The dimensionality of the encoding layer
1707
+ - "num_layers" (int): The number of layers in the model
1708
+ - "dropout_rate" (float): The dropout rate for regularization
1709
+ - "activation" (str): The activation function used in the model
1710
+ - "inputsize" (int): The size of the input features
1711
+
1712
+ Notes
1713
+ -----
1714
+ This method returns a copy of the internal configuration parameters. Modifications
1715
+ to the returned dictionary will not affect the original model configuration.
1716
+
1717
+ Examples
1718
+ --------
1719
+ >>> model = MyModel()
1720
+ >>> config = model.get_config()
1721
+ >>> print(config['window_size'])
1722
+ 10
1723
+ """
1724
+ return {
1725
+ "window_size": self.window_size,
1726
+ "encoding_dim": self.encoding_dim,
1727
+ "num_layers": self.num_layers,
1728
+ "dropout_rate": self.dropout_rate,
1729
+ "activation": self.activation,
1730
+ "inputsize": self.inputsize,
1731
+ }
1732
+
1733
+
1734
+ class DenseAutoencoderDLFilter(DeepLearningFilter):
1735
+ def __init__(self, encoding_dim: int = 10, *args, **kwargs) -> None:
1736
+ """
1737
+ Initialize DenseAutoencoderDLFilter instance.
1738
+
1739
+ Parameters
1740
+ ----------
1741
+ encoding_dim : int, default=10
1742
+ Dimension of the encoding layer in the autoencoder. This determines
1743
+ the size of the latent representation learned by the model.
1744
+ *args
1745
+ Variable length argument list passed to the parent class constructor.
1746
+ **kwargs
1747
+ Arbitrary keyword arguments passed to the parent class constructor.
1748
+
1749
+ Returns
1750
+ -------
1751
+ None
1752
+ This method initializes the instance and does not return any value.
1753
+
1754
+ Notes
1755
+ -----
1756
+ This constructor sets up the autoencoder architecture by:
1757
+ 1. Storing the encoding dimension as an instance attribute
1758
+ 2. Setting the network type to "autoencoder"
1759
+ 3. Updating the info dictionary with network type and encoding dimension
1760
+ 4. Calling the parent class constructor with any additional arguments
1761
+
1762
+ Examples
1763
+ --------
1764
+ >>> filter = DenseAutoencoderDLFilter(encoding_dim=15)
1765
+ >>> print(filter.encoding_dim)
1766
+ 15
1767
+ >>> print(filter.nettype)
1768
+ 'autoencoder'
1769
+ """
1770
+ self.encoding_dim = encoding_dim
1771
+ self.nettype = "autoencoder"
1772
+ self.infodict["nettype"] = self.nettype
1773
+ self.infodict["encoding_dim"] = self.encoding_dim
1774
+ super(DenseAutoencoderDLFilter, self).__init__(*args, **kwargs)
1775
+
1776
+ def getname(self):
1777
+ """
1778
+ Generate and return the model name and path based on current configuration parameters.
1779
+
1780
+ This method constructs a descriptive model name string using various configuration
1781
+ parameters and creates the corresponding directory path for model storage. The
1782
+ generated name includes information about window size, encoding dimensions, epochs,
1783
+ thresholds, step size, activation function, and additional flags.
1784
+
1785
+ Parameters
1786
+ ----------
1787
+ self : object
1788
+ The instance containing the following attributes:
1789
+ - window_size : int
1790
+ Size of the sliding window
1791
+ - encoding_dim : int
1792
+ Dimension of the encoding layer
1793
+ - num_epochs : int
1794
+ Number of training epochs
1795
+ - excludethresh : float
1796
+ Threshold for excluding data points
1797
+ - corrthresh : float
1798
+ Correlation threshold for filtering
1799
+ - step : int
1800
+ Step size for sliding window
1801
+ - activation : str
1802
+ Activation function name
1803
+ - usebadpts : bool
1804
+ Flag indicating whether to use bad points
1805
+ - excludebysubject : bool
1806
+ Flag indicating whether to exclude by subject
1807
+ - namesuffix : str, optional
1808
+ Additional suffix to append to model name
1809
+ - modelroot : str
1810
+ Root directory for model storage
1811
+
1812
+ Returns
1813
+ -------
1814
+ None
1815
+ This method modifies the instance attributes in-place:
1816
+ - self.modelname : str
1817
+ Generated model name string
1818
+ - self.modelpath : str
1819
+ Full path to the model directory
1820
+
1821
+ Notes
1822
+ -----
1823
+ The generated model name follows a specific format:
1824
+ "model_denseautoencoder_pytorch_wXXX_enXXX_eXXX_tX.XX_ctX.XX_sX_activation[flags]"
1825
+
1826
+ Examples
1827
+ --------
1828
+ >>> model = MyModel()
1829
+ >>> model.window_size = 100
1830
+ >>> model.encoding_dim = 50
1831
+ >>> model.num_epochs = 1000
1832
+ >>> model.excludethresh = 0.5
1833
+ >>> model.corrthresh = 0.8
1834
+ >>> model.step = 10
1835
+ >>> model.activation = 'relu'
1836
+ >>> model.usebadpts = True
1837
+ >>> model.excludebysubject = False
1838
+ >>> model.namesuffix = 'test'
1839
+ >>> model.modelroot = '/path/to/models'
1840
+ >>> model.getname()
1841
+ >>> print(model.modelname)
1842
+ 'model_denseautoencoder_pytorch_w100_en050_e1000_t0.5_ct0.8_s10_relu_usebadpts_test'
1843
+ """
1844
+ self.modelname = "_".join(
1845
+ [
1846
+ "model",
1847
+ "denseautoencoder",
1848
+ "pytorch",
1849
+ "w" + str(self.window_size).zfill(3),
1850
+ "en" + str(self.encoding_dim).zfill(3),
1851
+ "e" + str(self.num_epochs).zfill(3),
1852
+ "t" + str(self.excludethresh),
1853
+ "ct" + str(self.corrthresh),
1854
+ "s" + str(self.step),
1855
+ self.activation,
1856
+ ]
1857
+ )
1858
+ if self.usebadpts:
1859
+ self.modelname += "_usebadpts"
1860
+ if self.excludebysubject:
1861
+ self.modelname += "_excludebysubject"
1862
+ if self.namesuffix is not None:
1863
+ self.modelname += "_" + self.namesuffix
1864
+ self.modelpath = os.path.join(self.modelroot, self.modelname)
1865
+
1866
+ try:
1867
+ os.makedirs(self.modelpath)
1868
+ except OSError:
1869
+ pass
1870
+
1871
+ def makenet(self):
1872
+ """
1873
+ Create and configure a dense autoencoder model.
1874
+
1875
+ This method initializes a DenseAutoencoderModel with the specified parameters
1876
+ and moves it to the designated device (CPU or GPU).
1877
+
1878
+ Parameters
1879
+ ----------
1880
+ self : object
1881
+ The instance of the class containing this method. Expected to have the
1882
+ following attributes:
1883
+ - window_size : int
1884
+ - encoding_dim : int
1885
+ - num_layers : int
1886
+ - dropout_rate : float
1887
+ - activation : str or callable
1888
+ - inputsize : int
1889
+ - device : torch.device
1890
+
1891
+ Returns
1892
+ -------
1893
+ None
1894
+ This method does not return any value. It sets the model attribute
1895
+ of the instance to the created DenseAutoencoderModel.
1896
+
1897
+ Notes
1898
+ -----
1899
+ The model is automatically moved to the device specified by self.device.
1900
+ This method should be called after all required parameters have been set
1901
+ on the instance.
1902
+
1903
+ Examples
1904
+ --------
1905
+ >>> # Assuming a class with the makenet method
1906
+ >>> instance = MyClass()
1907
+ >>> instance.window_size = 100
1908
+ >>> instance.encoding_dim = 32
1909
+ >>> instance.num_layers = 3
1910
+ >>> instance.dropout_rate = 0.2
1911
+ >>> instance.activation = 'relu'
1912
+ >>> instance.inputsize = 100
1913
+ >>> instance.device = torch.device('cuda')
1914
+ >>> instance.makenet()
1915
+ >>> # Model is now available as instance.model
1916
+ """
1917
+ self.model = DenseAutoencoderModel(
1918
+ self.window_size,
1919
+ self.encoding_dim,
1920
+ self.num_layers,
1921
+ self.dropout_rate,
1922
+ self.activation,
1923
+ self.inputsize,
1924
+ )
1925
+ self.model.to(self.device)
1926
+
1927
+
1928
+ class MultiscaleCNNModel(nn.Module):
1929
+ def __init__(
1930
+ self,
1931
+ num_filters,
1932
+ kernel_sizes,
1933
+ input_lens,
1934
+ input_width,
1935
+ dilation_rate,
1936
+ ):
1937
+ """
1938
+ Initialize the MultiscaleCNNModel.
1939
+
1940
+ This constructor initializes a multiscale CNN model with three parallel branches
1941
+ processing input at different scales. Each branch uses dilated convolutions to
1942
+ capture features at different receptive fields.
1943
+
1944
+ Parameters
1945
+ ----------
1946
+ num_filters : int
1947
+ Number of filters (channels) to use in the convolutional layers.
1948
+ kernel_sizes : list of int
1949
+ List of three kernel sizes for the three branches (small, medium, large scales).
1950
+ input_lens : list of int
1951
+ List of input lengths for each branch, corresponding to the input sequence lengths.
1952
+ input_width : int
1953
+ Width of the input features (number of input channels).
1954
+ dilation_rate : int
1955
+ Dilation rate to use in the dilated convolutional layers.
1956
+
1957
+ Returns
1958
+ -------
1959
+ None
1960
+ This method initializes the model instance and does not return any value.
1961
+
1962
+ Notes
1963
+ -----
1964
+ The model creates three parallel branches with different kernel sizes to capture
1965
+ multi-scale temporal features. Each branch uses dilated convolutions to increase
1966
+ the receptive field without increasing the number of parameters significantly.
1967
+
1968
+ The final dense layer reduces the combined features to a single output value,
1969
+ followed by a sigmoid activation for binary classification.
1970
+
1971
+ Examples
1972
+ --------
1973
+ >>> model = MultiscaleCNNModel(
1974
+ ... num_filters=64,
1975
+ ... kernel_sizes=[3, 5, 7],
1976
+ ... input_lens=[100, 100, 100],
1977
+ ... input_width=10,
1978
+ ... dilation_rate=2
1979
+ ... )
1980
+ """
1981
+ super(MultiscaleCNNModel, self).__init__()
1982
+
1983
+ self.num_filters = num_filters
1984
+ self.kernel_sizes = kernel_sizes
1985
+ self.input_lens = input_lens
1986
+ self.input_width = input_width
1987
+ self.dilation_rate = dilation_rate
1988
+
1989
+ # Create three separate branches for different scales
1990
+ self.branch_small = self._make_branch(kernel_sizes[0])
1991
+ self.branch_med = self._make_branch(kernel_sizes[1])
1992
+ self.branch_large = self._make_branch(kernel_sizes[2])
1993
+
1994
+ # Final dense layer
1995
+ self.fc = nn.Linear(150, 1)
1996
+ self.sigmoid = nn.Sigmoid()
1997
+
1998
+ def _make_branch(self, kernel_size):
1999
+ """
2000
+ Create a convolutional branch for the neural network architecture.
2001
+
2002
+ This method constructs a sequential neural network branch consisting of
2003
+ convolutional, pooling, flattening, linear, activation, and dropout layers.
2004
+
2005
+ Parameters
2006
+ ----------
2007
+ kernel_size : int
2008
+ The size of the convolutional kernel to be used in the Conv1d layer.
2009
+
2010
+ Returns
2011
+ -------
2012
+ torch.nn.Sequential
2013
+ A sequential container containing the following layers:
2014
+ - Conv1d: 1D convolutional layer with input_width as input channels
2015
+ - AdaptiveMaxPool1d: 1D adaptive max pooling with output size 1
2016
+ - Flatten: Flattens the tensor for linear layer input
2017
+ - Linear: Linear layer with num_filters input features and 50 output features
2018
+ - Tanh: Hyperbolic tangent activation function
2019
+ - Dropout: 30% dropout regularization
2020
+
2021
+ Notes
2022
+ -----
2023
+ The branch is designed to process 1D input data through a convolutional
2024
+ feature extraction pathway followed by a fully connected classifier head.
2025
+ The padding="same" parameter ensures the output size matches the input size
2026
+ for the convolutional layer.
2027
+
2028
+ Examples
2029
+ --------
2030
+ >>> branch = self._make_branch(kernel_size=3)
2031
+ >>> print(type(branch))
2032
+ <class 'torch.nn.modules.container.Sequential'>
2033
+ """
2034
+ return nn.Sequential(
2035
+ nn.Conv1d(self.input_width, self.num_filters, kernel_size, padding="same"),
2036
+ nn.AdaptiveMaxPool1d(1),
2037
+ nn.Flatten(),
2038
+ nn.Linear(self.num_filters, 50),
2039
+ nn.Tanh(),
2040
+ nn.Dropout(0.3),
2041
+ )
2042
+
2043
+ def forward(self, x_small, x_med, x_large):
2044
+ """
2045
+ Forward pass of the multi-scale feature extraction network.
2046
+
2047
+ This function processes input tensors through three parallel branches with different
2048
+ receptive fields and concatenates the outputs before applying a final fully connected
2049
+ layer with sigmoid activation.
2050
+
2051
+ Parameters
2052
+ ----------
2053
+ x_small : torch.Tensor
2054
+ Input tensor for the small-scale branch with shape (batch_size, channels, height, width)
2055
+ x_med : torch.Tensor
2056
+ Input tensor for the medium-scale branch with shape (batch_size, channels, height, width)
2057
+ x_large : torch.Tensor
2058
+ Input tensor for the large-scale branch with shape (batch_size, channels, height, width)
2059
+
2060
+ Returns
2061
+ -------
2062
+ torch.Tensor
2063
+ Output tensor with shape (batch_size, num_classes) containing sigmoid-activated
2064
+ predictions for each class
2065
+
2066
+ Notes
2067
+ -----
2068
+ The function assumes that `self.branch_small`, `self.branch_med`, `self.branch_large`,
2069
+ `self.fc`, and `self.sigmoid` are properly initialized components of the class.
2070
+
2071
+ Examples
2072
+ --------
2073
+ >>> import torch
2074
+ >>> # Assuming model is initialized
2075
+ >>> x_small = torch.randn(1, 3, 32, 32)
2076
+ >>> x_med = torch.randn(1, 3, 64, 64)
2077
+ >>> x_large = torch.randn(1, 3, 128, 128)
2078
+ >>> output = model.forward(x_small, x_med, x_large)
2079
+ >>> print(output.shape)
2080
+ torch.Size([1, num_classes])
2081
+ """
2082
+ # Process each branch
2083
+ out_small = self.branch_small(x_small)
2084
+ out_med = self.branch_med(x_med)
2085
+ out_large = self.branch_large(x_large)
2086
+
2087
+ # Concatenate outputs
2088
+ merged = torch.cat([out_small, out_med, out_large], dim=1)
2089
+
2090
+ # Final output
2091
+ out = self.fc(merged)
2092
+ out = self.sigmoid(out)
2093
+
2094
+ return out
2095
+
2096
+ def get_config(self):
2097
+ """
2098
+ Get the configuration parameters of the layer.
2099
+
2100
+ Returns
2101
+ -------
2102
+ dict
2103
+ A dictionary containing the layer configuration parameters with the following keys:
2104
+ - "num_filters" (int): Number of filters in the layer
2105
+ - "kernel_sizes" (list of int): Size of the convolutional kernels
2106
+ - "input_lens" (list of int): Lengths of input sequences
2107
+ - "input_width" (int): Width of the input data
2108
+ - "dilation_rate" (int): Dilation rate for dilated convolution
2109
+
2110
+ Notes
2111
+ -----
2112
+ This method returns a copy of the internal configuration parameters
2113
+ that can be used to reconstruct the layer with the same settings.
2114
+
2115
+ Examples
2116
+ --------
2117
+ >>> config = layer.get_config()
2118
+ >>> print(config['num_filters'])
2119
+ 32
2120
+ """
2121
+ return {
2122
+ "num_filters": self.num_filters,
2123
+ "kernel_sizes": self.kernel_sizes,
2124
+ "input_lens": self.input_lens,
2125
+ "input_width": self.input_width,
2126
+ "dilation_rate": self.dilation_rate,
2127
+ }
2128
+
2129
+
2130
+ class MultiscaleCNNDLFilter(DeepLearningFilter):
2131
+ def __init__(
2132
+ self,
2133
+ num_filters=10,
2134
+ kernel_sizes=[4, 8, 12],
2135
+ input_lens=[64, 128, 192],
2136
+ input_width=1,
2137
+ dilation_rate=1,
2138
+ *args,
2139
+ **kwargs,
2140
+ ):
2141
+ """
2142
+ Initialize the MultiscaleCNNDLFilter layer.
2143
+
2144
+ This constructor initializes a multiscale CNN filter with configurable
2145
+ kernel sizes, input lengths, and dilation rates for multi-scale feature extraction.
2146
+
2147
+ Parameters
2148
+ ----------
2149
+ num_filters : int, optional
2150
+ Number of filters to use in each convolutional layer, default is 10
2151
+ kernel_sizes : list of int, optional
2152
+ List of kernel sizes for different convolutional layers, default is [4, 8, 12]
2153
+ input_lens : list of int, optional
2154
+ List of input sequence lengths for different scales, default is [64, 128, 192]
2155
+ input_width : int, optional
2156
+ Width of the input data, default is 1
2157
+ dilation_rate : int, optional
2158
+ Dilation rate for the convolutional layers, default is 1
2159
+ *args : tuple
2160
+ Variable length argument list passed to parent class
2161
+ **kwargs : dict
2162
+ Arbitrary keyword arguments passed to parent class
2163
+
2164
+ Returns
2165
+ -------
2166
+ None
2167
+ This method initializes the object and does not return any value
2168
+
2169
+ Notes
2170
+ -----
2171
+ The multiscale CNN filter uses multiple convolutional layers with different
2172
+ kernel sizes and dilation rates to capture features at multiple scales.
2173
+ The input data is processed through parallel convolutional branches,
2174
+ each with different kernel sizes and dilation rates.
2175
+
2176
+ Examples
2177
+ --------
2178
+ >>> filter_layer = MultiscaleCNNDLFilter(
2179
+ ... num_filters=20,
2180
+ ... kernel_sizes=[3, 6, 9],
2181
+ ... input_lens=[32, 64, 128],
2182
+ ... input_width=2,
2183
+ ... dilation_rate=2
2184
+ ... )
2185
+ """
2186
+ self.num_filters = num_filters
2187
+ self.kernel_sizes = kernel_sizes
2188
+ self.input_lens = input_lens
2189
+ self.input_width = input_width
2190
+ self.dilation_rate = dilation_rate
2191
+ self.nettype = "multiscalecnn"
2192
+ self.infodict["nettype"] = self.nettype
2193
+ self.infodict["num_filters"] = self.num_filters
2194
+ self.infodict["kernel_sizes"] = self.kernel_sizes
2195
+ self.infodict["input_lens"] = self.input_lens
2196
+ self.infodict["input_width"] = self.input_width
2197
+ super(MultiscaleCNNDLFilter, self).__init__(*args, **kwargs)
2198
+
2199
+ def getname(self):
2200
+ """
2201
+ Generate and return the model name and path based on current configuration parameters.
2202
+
2203
+ This method constructs a descriptive model name string by joining various configuration
2204
+ parameters with specific prefixes and zero-padded numeric values. The resulting name
2205
+ is used to create a unique directory path for model storage.
2206
+
2207
+ Parameters
2208
+ ----------
2209
+ self : object
2210
+ The instance containing model configuration parameters.
2211
+
2212
+ Returns
2213
+ -------
2214
+ None
2215
+ This method does not return a value but sets the following attributes:
2216
+ - self.modelname: str, the generated model name
2217
+ - self.modelpath: str, the full path to the model directory
2218
+
2219
+ Notes
2220
+ -----
2221
+ The generated model name includes the following components:
2222
+ - Model type: "model_multiscalecnn_pytorch"
2223
+ - Window size: "w" + zero-padded window size
2224
+ - Number of layers: "l" + zero-padded layer count
2225
+ - Number of filters: "fn" + zero-padded filter count
2226
+ - First kernel size: "fl" + zero-padded kernel size
2227
+ - Number of epochs: "e" + zero-padded epoch count
2228
+ - Exclusion threshold: "t" + threshold value
2229
+ - Correlation threshold: "ct" + threshold value
2230
+ - Step size: "s" + zero-padded step value
2231
+ - Dilation rate: "d" + dilation rate value
2232
+ - Activation function name
2233
+
2234
+ Additional suffixes are appended if:
2235
+ - usebadpts is True: "_usebadpts"
2236
+ - excludebysubject is True: "_excludebysubject"
2237
+ - namesuffix is not None: "_{namesuffix}"
2238
+
2239
+ Examples
2240
+ --------
2241
+ >>> model = MyModel()
2242
+ >>> model.window_size = 128
2243
+ >>> model.num_layers = 5
2244
+ >>> model.num_filters = 32
2245
+ >>> model.kernel_sizes = [3, 5, 7]
2246
+ >>> model.num_epochs = 100
2247
+ >>> model.excludethresh = 0.5
2248
+ >>> model.corrthresh = 0.8
2249
+ >>> model.step = 16
2250
+ >>> model.dilation_rate = 2
2251
+ >>> model.activation = "relu"
2252
+ >>> model.usebadpts = True
2253
+ >>> model.excludebysubject = False
2254
+ >>> model.namesuffix = "exp1"
2255
+ >>> model.getname()
2256
+ >>> print(model.modelname)
2257
+ 'model_multiscalecnn_pytorch_w128_l05_fn32_fl03_e100_t0.5_ct0.8_s16_d2_relu_usebadpts_exp1'
2258
+ """
2259
+ self.modelname = "_".join(
2260
+ [
2261
+ "model",
2262
+ "multiscalecnn",
2263
+ "pytorch",
2264
+ "w" + str(self.window_size).zfill(3),
2265
+ "l" + str(self.num_layers).zfill(2),
2266
+ "fn" + str(self.num_filters).zfill(2),
2267
+ "fl" + str(self.kernel_sizes[0]).zfill(2),
2268
+ "e" + str(self.num_epochs).zfill(3),
2269
+ "t" + str(self.excludethresh),
2270
+ "ct" + str(self.corrthresh),
2271
+ "s" + str(self.step),
2272
+ "d" + str(self.dilation_rate),
2273
+ self.activation,
2274
+ ]
2275
+ )
2276
+ if self.usebadpts:
2277
+ self.modelname += "_usebadpts"
2278
+ if self.excludebysubject:
2279
+ self.modelname += "_excludebysubject"
2280
+ if self.namesuffix is not None:
2281
+ self.modelname += "_" + self.namesuffix
2282
+ self.modelpath = os.path.join(self.modelroot, self.modelname)
2283
+
2284
+ try:
2285
+ os.makedirs(self.modelpath)
2286
+ except OSError:
2287
+ pass
2288
+
2289
+ def makenet(self):
2290
+ """
2291
+ Create and initialize a multiscale CNN model for network construction.
2292
+
2293
+ This method initializes a MultiscaleCNNModel with the specified parameters
2294
+ and moves the model to the designated device (CPU or GPU).
2295
+
2296
+ Parameters
2297
+ ----------
2298
+ self : object
2299
+ The instance containing the following attributes:
2300
+ - num_filters : int
2301
+ Number of filters for the CNN layers
2302
+ - kernel_sizes : list of int
2303
+ List of kernel sizes for different scales
2304
+ - input_lens : list of int
2305
+ List of input lengths for different scales
2306
+ - input_width : int
2307
+ Width of the input data
2308
+ - dilation_rate : int
2309
+ Dilation rate for the convolutional layers
2310
+ - device : torch.device
2311
+ Device to move the model to (e.g., 'cuda' or 'cpu')
2312
+
2313
+ Returns
2314
+ -------
2315
+ None
2316
+ This method does not return any value but modifies the instance
2317
+ by setting the `model` attribute to the created MultiscaleCNNModel.
2318
+
2319
+ Notes
2320
+ -----
2321
+ The method assumes that all required attributes are properly initialized
2322
+ in the instance before calling this method. The model is automatically
2323
+ moved to the specified device using the `.to()` method.
2324
+
2325
+ Examples
2326
+ --------
2327
+ >>> # Assuming instance with required attributes is created
2328
+ >>> instance.makenet()
2329
+ >>> print(instance.model)
2330
+ MultiscaleCNNModel(...)
2331
+ """
2332
+ self.model = MultiscaleCNNModel(
2333
+ self.num_filters,
2334
+ self.kernel_sizes,
2335
+ self.input_lens,
2336
+ self.input_width,
2337
+ self.dilation_rate,
2338
+ )
2339
+ self.model.to(self.device)
2340
+
2341
+
2342
+ class ConvAutoencoderModel(nn.Module):
2343
+ def __init__(
2344
+ self,
2345
+ window_size,
2346
+ encoding_dim,
2347
+ num_filters,
2348
+ kernel_size,
2349
+ dropout_rate,
2350
+ activation,
2351
+ inputsize,
2352
+ ):
2353
+ """
2354
+ Initialize the ConvAutoencoderModel.
2355
+
2356
+ This class implements a convolutional autoencoder for time series data. The model
2357
+ consists of an encoder and a decoder, with symmetric architecture. The encoder
2358
+ reduces the input dimensionality through convolutional and pooling layers, while
2359
+ the decoder reconstructs the input from the encoded representation.
2360
+
2361
+ Parameters
2362
+ ----------
2363
+ window_size : int
2364
+ The length of the input time series window.
2365
+ encoding_dim : int
2366
+ The dimensionality of the latent space representation.
2367
+ num_filters : int
2368
+ The number of filters in the first convolutional layer.
2369
+ kernel_size : int
2370
+ The size of the convolutional kernels.
2371
+ dropout_rate : float
2372
+ The dropout rate applied after each convolutional layer.
2373
+ activation : str
2374
+ The activation function to use. Supported values are "relu" and "tanh".
2375
+ inputsize : int
2376
+ The number of input channels (e.g., number of features in the time series).
2377
+
2378
+ Returns
2379
+ -------
2380
+ None
2381
+ This method initializes the model in-place and does not return any value.
2382
+
2383
+ Notes
2384
+ -----
2385
+ The model uses a symmetric encoder-decoder architecture. The encoder reduces
2386
+ the input size through 4 max-pooling layers, and the decoder reconstructs
2387
+ the input using upsample and convolutional layers. The final layer uses
2388
+ a convolution with padding to match the input size.
2389
+
2390
+ Examples
2391
+ --------
2392
+ >>> model = ConvAutoencoderModel(
2393
+ ... window_size=100,
2394
+ ... encoding_dim=32,
2395
+ ... num_filters=32,
2396
+ ... kernel_size=3,
2397
+ ... dropout_rate=0.2,
2398
+ ... activation="relu",
2399
+ ... inputsize=1
2400
+ ... )
2401
+ """
2402
+ super(ConvAutoencoderModel, self).__init__()
2403
+
2404
+ self.window_size = window_size
2405
+ self.encoding_dim = encoding_dim
2406
+ self.num_filters = num_filters
2407
+ self.kernel_size = kernel_size
2408
+ self.dropout_rate = dropout_rate
2409
+ self.activation = activation
2410
+ self.inputsize = inputsize
2411
+
2412
+ # Get activation function
2413
+ if activation == "relu":
2414
+ act_fn = nn.ReLU
2415
+ elif activation == "tanh":
2416
+ act_fn = nn.Tanh
2417
+ else:
2418
+ act_fn = nn.ReLU
2419
+
2420
+ # Initial conv block
2421
+ self.encoder_layers = nn.ModuleList()
2422
+ self.encoder_layers.append(nn.Conv1d(inputsize, num_filters, kernel_size, padding="same"))
2423
+ self.encoder_layers.append(nn.BatchNorm1d(num_filters))
2424
+ self.encoder_layers.append(nn.Dropout(dropout_rate))
2425
+ self.encoder_layers.append(act_fn())
2426
+ self.encoder_layers.append(nn.MaxPool1d(2, padding=1))
2427
+
2428
+ # Encoding path (3 layers)
2429
+ nfilters = num_filters
2430
+ self.filter_list = []
2431
+ for _ in range(3):
2432
+ nfilters *= 2
2433
+ self.filter_list.append(nfilters)
2434
+ self.encoder_layers.append(
2435
+ nn.Conv1d(nfilters // 2, nfilters, kernel_size, padding="same")
2436
+ )
2437
+ self.encoder_layers.append(nn.BatchNorm1d(nfilters))
2438
+ self.encoder_layers.append(nn.Dropout(dropout_rate))
2439
+ self.encoder_layers.append(act_fn())
2440
+ self.encoder_layers.append(nn.MaxPool1d(2, padding=1))
2441
+
2442
+ # Calculate size after pooling
2443
+ self.encoded_size = window_size
2444
+ for _ in range(4): # 4 pooling layers
2445
+ self.encoded_size = (self.encoded_size + 1) // 2
2446
+
2447
+ # Bottleneck
2448
+ self.flatten = nn.Flatten()
2449
+ self.encode_fc = nn.Linear(nfilters * self.encoded_size, encoding_dim)
2450
+ self.encode_act = act_fn()
2451
+ self.decode_fc = nn.Linear(encoding_dim, nfilters * self.encoded_size)
2452
+ self.decode_act = act_fn()
2453
+ self.unflatten_size = (nfilters, self.encoded_size)
2454
+
2455
+ # Decoding path (mirror)
2456
+ self.decoder_layers = nn.ModuleList()
2457
+ for i, filters in enumerate(reversed(self.filter_list)):
2458
+ self.decoder_layers.append(nn.Upsample(scale_factor=2, mode="nearest"))
2459
+ if i == 0:
2460
+ self.decoder_layers.append(
2461
+ nn.Conv1d(nfilters, filters, kernel_size, padding="same")
2462
+ )
2463
+ else:
2464
+ self.decoder_layers.append(
2465
+ nn.Conv1d(self.filter_list[-i], filters, kernel_size, padding="same")
2466
+ )
2467
+ self.decoder_layers.append(nn.BatchNorm1d(filters))
2468
+ self.decoder_layers.append(nn.Dropout(dropout_rate))
2469
+ self.decoder_layers.append(act_fn())
2470
+
2471
+ # Final upsampling
2472
+ self.decoder_layers.append(nn.Upsample(scale_factor=2, mode="nearest"))
2473
+ self.decoder_layers.append(nn.Conv1d(num_filters, inputsize, kernel_size, padding="same"))
2474
+
2475
+ def forward(self, x):
2476
+ """
2477
+ Forward pass of the autoencoder.
2478
+
2479
+ Applies encoding, bottleneck processing, and decoding to the input tensor.
2480
+
2481
+ Parameters
2482
+ ----------
2483
+ x : torch.Tensor
2484
+ Input tensor of shape (batch_size, channels, height, width) where
2485
+ height and width should match the expected window size.
2486
+
2487
+ Returns
2488
+ -------
2489
+ torch.Tensor
2490
+ Reconstructed tensor of shape (batch_size, channels, window_size, width)
2491
+ with the same spatial dimensions as the input.
2492
+
2493
+ Notes
2494
+ -----
2495
+ The forward pass consists of three main stages:
2496
+ 1. Encoding: Input is passed through encoder layers
2497
+ 2. Bottleneck: Flattening, encoding, and decoding with activation functions
2498
+ 3. Decoding: Reconstructed features are passed through decoder layers
2499
+
2500
+ The output is cropped or padded to match the original window size.
2501
+
2502
+ Examples
2503
+ --------
2504
+ >>> import torch
2505
+ >>> model = AutoEncoder()
2506
+ >>> x = torch.randn(1, 3, 64, 64)
2507
+ >>> output = model.forward(x)
2508
+ >>> print(output.shape)
2509
+ torch.Size([1, 3, 64, 64])
2510
+ """
2511
+ # Encoding
2512
+ for layer in self.encoder_layers:
2513
+ x = layer(x)
2514
+
2515
+ # Bottleneck
2516
+ x = self.flatten(x)
2517
+ x = self.encode_fc(x)
2518
+ x = self.encode_act(x)
2519
+ x = self.decode_fc(x)
2520
+ x = self.decode_act(x)
2521
+ x = x.view(x.size(0), *self.unflatten_size)
2522
+
2523
+ # Decoding
2524
+ for layer in self.decoder_layers:
2525
+ x = layer(x)
2526
+
2527
+ # Crop/pad to original window size
2528
+ if x.size(2) > self.window_size:
2529
+ x = x[:, :, : self.window_size]
2530
+ elif x.size(2) < self.window_size:
2531
+ pad_size = self.window_size - x.size(2)
2532
+ x = nn.functional.pad(x, (0, pad_size))
2533
+
2534
+ return x
2535
+
2536
+ def get_config(self):
2537
+ """
2538
+ Get the configuration parameters of the model.
2539
+
2540
+ Returns
2541
+ -------
2542
+ dict
2543
+ A dictionary containing all configuration parameters with their current values:
2544
+ - "window_size" (int): Size of the sliding window
2545
+ - "encoding_dim" (int): Dimension of the encoding layer
2546
+ - "num_filters" (int): Number of filters in the convolutional layers
2547
+ - "kernel_size" (int): Size of the convolutional kernel
2548
+ - "dropout_rate" (float): Dropout rate for regularization
2549
+ - "activation" (str): Activation function to use
2550
+ - "inputsize" (int): Size of the input data
2551
+
2552
+ Notes
2553
+ -----
2554
+ This method returns a copy of the current configuration. Modifications to the
2555
+ returned dictionary will not affect the original model configuration.
2556
+
2557
+ Examples
2558
+ --------
2559
+ >>> config = model.get_config()
2560
+ >>> print(config['window_size'])
2561
+ 100
2562
+ """
2563
+ return {
2564
+ "window_size": self.window_size,
2565
+ "encoding_dim": self.encoding_dim,
2566
+ "num_filters": self.num_filters,
2567
+ "kernel_size": self.kernel_size,
2568
+ "dropout_rate": self.dropout_rate,
2569
+ "activation": self.activation,
2570
+ "inputsize": self.inputsize,
2571
+ }
2572
+
2573
+
2574
+ class ConvAutoencoderDLFilter(DeepLearningFilter):
2575
+ def __init__(
2576
+ self,
2577
+ encoding_dim: int = 10,
2578
+ num_filters: int = 5,
2579
+ kernel_size: int = 5,
2580
+ dilation_rate: int = 1,
2581
+ *args,
2582
+ **kwargs,
2583
+ ) -> None:
2584
+ """
2585
+ Initialize ConvAutoencoderDLFilter instance.
2586
+
2587
+ Parameters
2588
+ ----------
2589
+ encoding_dim : int, optional
2590
+ Dimension of the encoded representation, by default 10
2591
+ num_filters : int, optional
2592
+ Number of filters in the convolutional layers, by default 5
2593
+ kernel_size : int, optional
2594
+ Size of the convolutional kernel, by default 5
2595
+ dilation_rate : int, optional
2596
+ Dilation rate for the convolutional layers, by default 1
2597
+ *args
2598
+ Variable length argument list
2599
+ **kwargs
2600
+ Arbitrary keyword arguments
2601
+
2602
+ Returns
2603
+ -------
2604
+ None
2605
+ This method does not return any value
2606
+
2607
+ Notes
2608
+ -----
2609
+ This constructor initializes a convolutional autoencoder with dilated filters.
2610
+ The network type is set to "convautoencoder" and various configuration parameters
2611
+ are stored in the infodict for later reference.
2612
+
2613
+ Examples
2614
+ --------
2615
+ >>> autoencoder = ConvAutoencoderDLFilter(
2616
+ ... encoding_dim=15,
2617
+ ... num_filters=8,
2618
+ ... kernel_size=3,
2619
+ ... dilation_rate=2
2620
+ ... )
2621
+ """
2622
+ self.encoding_dim = encoding_dim
2623
+ self.num_filters = num_filters
2624
+ self.kernel_size = kernel_size
2625
+ self.dilation_rate = dilation_rate
2626
+ self.nettype = "convautoencoder"
2627
+ self.infodict["num_filters"] = self.num_filters
2628
+ self.infodict["kernel_size"] = self.kernel_size
2629
+ self.infodict["nettype"] = self.nettype
2630
+ self.infodict["encoding_dim"] = self.encoding_dim
2631
+ super(ConvAutoencoderDLFilter, self).__init__(*args, **kwargs)
2632
+
2633
+ def getname(self):
2634
+ """
2635
+ Generate and configure the model name and path based on current parameters.
2636
+
2637
+ This method constructs a descriptive model name string using various
2638
+ configuration parameters and creates the corresponding directory path.
2639
+ The generated name includes information about window size, encoding dimensions,
2640
+ filters, kernel size, epochs, thresholds, and other model configuration options.
2641
+
2642
+ Parameters
2643
+ ----------
2644
+ self : object
2645
+ The instance of the class containing model configuration parameters.
2646
+ Expected attributes include:
2647
+ - window_size : int
2648
+ - encoding_dim : int
2649
+ - num_filters : int
2650
+ - kernel_size : int
2651
+ - num_epochs : int
2652
+ - excludethresh : float
2653
+ - corrthresh : float
2654
+ - step : int
2655
+ - activation : str
2656
+ - usebadpts : bool
2657
+ - excludebysubject : bool
2658
+ - namesuffix : str, optional
2659
+ - modelroot : str
2660
+
2661
+ Returns
2662
+ -------
2663
+ None
2664
+ This method does not return a value but modifies the instance attributes:
2665
+ - self.modelname : str
2666
+ - self.modelpath : str
2667
+
2668
+ Notes
2669
+ -----
2670
+ The model name is constructed with the following components:
2671
+ - "model_convautoencoder_pytorch" as base identifier
2672
+ - Window size with 3-digit zero-padded formatting (wXXX)
2673
+ - Encoding dimension with 3-digit zero-padded formatting (enXXX)
2674
+ - Number of filters with 2-digit zero-padded formatting (fnXX)
2675
+ - Kernel size with 2-digit zero-padded formatting (flXX)
2676
+ - Number of epochs with 3-digit zero-padded formatting (eXXX)
2677
+ - Exclusion threshold (tX.XX)
2678
+ - Correlation threshold (ctX.XX)
2679
+ - Step size (sX)
2680
+ - Activation function name
2681
+
2682
+ Additional suffixes are appended based on:
2683
+ - usebadpts flag
2684
+ - excludebysubject flag
2685
+ - namesuffix parameter
2686
+
2687
+ Examples
2688
+ --------
2689
+ >>> model = MyModel()
2690
+ >>> model.window_size = 100
2691
+ >>> model.encoding_dim = 50
2692
+ >>> model.getname()
2693
+ >>> print(model.modelname)
2694
+ 'model_convautoencoder_pytorch_w100_en050_fn10_fl05_e001_t0.5_ct0.8_s1_relu'
2695
+ """
2696
+ self.modelname = "_".join(
2697
+ [
2698
+ "model",
2699
+ "convautoencoder",
2700
+ "pytorch",
2701
+ "w" + str(self.window_size).zfill(3),
2702
+ "en" + str(self.encoding_dim).zfill(3),
2703
+ "fn" + str(self.num_filters).zfill(2),
2704
+ "fl" + str(self.kernel_size).zfill(2),
2705
+ "e" + str(self.num_epochs).zfill(3),
2706
+ "t" + str(self.excludethresh),
2707
+ "ct" + str(self.corrthresh),
2708
+ "s" + str(self.step),
2709
+ self.activation,
2710
+ ]
2711
+ )
2712
+ if self.usebadpts:
2713
+ self.modelname += "_usebadpts"
2714
+ if self.excludebysubject:
2715
+ self.modelname += "_excludebysubject"
2716
+ if self.namesuffix is not None:
2717
+ self.modelname += "_" + self.namesuffix
2718
+ self.modelpath = os.path.join(self.modelroot, self.modelname)
2719
+
2720
+ try:
2721
+ os.makedirs(self.modelpath)
2722
+ except OSError:
2723
+ pass
2724
+
2725
+ def makenet(self):
2726
+ """
2727
+ Create and initialize a convolutional autoencoder model.
2728
+
2729
+ This method constructs a ConvAutoencoderModel with the specified parameters
2730
+ and moves it to the designated device (CPU or GPU).
2731
+
2732
+ Parameters
2733
+ ----------
2734
+ self : object
2735
+ The instance of the class containing this method. Expected to have the
2736
+ following attributes:
2737
+ - window_size : int
2738
+ Size of the input window
2739
+ - encoding_dim : int
2740
+ Dimension of the encoded representation
2741
+ - num_filters : int
2742
+ Number of filters in the convolutional layers
2743
+ - kernel_size : int
2744
+ Size of the convolutional kernel
2745
+ - dropout_rate : float
2746
+ Dropout rate for regularization
2747
+ - activation : str or callable
2748
+ Activation function to use
2749
+ - inputsize : tuple
2750
+ Input size dimensions
2751
+ - device : torch.device
2752
+ Device to move the model to (CPU or GPU)
2753
+
2754
+ Returns
2755
+ -------
2756
+ None
2757
+ This method does not return any value. It initializes the model
2758
+ attribute of the class instance.
2759
+
2760
+ Notes
2761
+ -----
2762
+ The method assumes that `ConvAutoencoderModel` is a valid class that accepts
2763
+ the specified parameters. The model is automatically moved to the device
2764
+ specified by `self.device`.
2765
+
2766
+ Examples
2767
+ --------
2768
+ >>> class MyModel:
2769
+ ... def __init__(self):
2770
+ ... self.window_size = 100
2771
+ ... self.encoding_dim = 32
2772
+ ... self.num_filters = 64
2773
+ ... self.kernel_size = 3
2774
+ ... self.dropout_rate = 0.2
2775
+ ... self.activation = 'relu'
2776
+ ... self.inputsize = (1, 100)
2777
+ ... self.device = torch.device('cpu')
2778
+ ... self.model = None
2779
+ ...
2780
+ ... def makenet(self):
2781
+ ... self.model = ConvAutoencoderModel(
2782
+ ... self.window_size,
2783
+ ... self.encoding_dim,
2784
+ ... self.num_filters,
2785
+ ... self.kernel_size,
2786
+ ... self.dropout_rate,
2787
+ ... self.activation,
2788
+ ... self.inputsize,
2789
+ ... )
2790
+ ... self.model.to(self.device)
2791
+ ...
2792
+ >>> model = MyModel()
2793
+ >>> model.makenet()
2794
+ >>> print(model.model)
2795
+ """
2796
+ self.model = ConvAutoencoderModel(
2797
+ self.window_size,
2798
+ self.encoding_dim,
2799
+ self.num_filters,
2800
+ self.kernel_size,
2801
+ self.dropout_rate,
2802
+ self.activation,
2803
+ self.inputsize,
2804
+ )
2805
+ self.model.to(self.device)
2806
+
2807
+
2808
+ class CRNNModel(nn.Module):
2809
+ def __init__(
2810
+ self, num_filters, kernel_size, encoding_dim, dropout_rate, activation, inputsize
2811
+ ):
2812
+ """
2813
+ Initialize the CRNNModel.
2814
+
2815
+ This function initializes a Convolutional Recurrent Neural Network (CRNN) model
2816
+ with convolutional front-end, bidirectional LSTM layers, and output mapping.
2817
+ The model processes sequential data through convolutional layers, applies
2818
+ bidirectional LSTM encoding, and maps the output back to the original input size.
2819
+
2820
+ Parameters
2821
+ ----------
2822
+ num_filters : int
2823
+ Number of filters in the convolutional layers
2824
+ kernel_size : int
2825
+ Size of the convolutional kernel
2826
+ encoding_dim : int
2827
+ Dimension of the LSTM encoding (hidden state size)
2828
+ dropout_rate : float
2829
+ Dropout rate for regularization
2830
+ activation : str
2831
+ Activation function to use ('relu' or 'tanh')
2832
+ inputsize : int
2833
+ Size of the input features
2834
+
2835
+ Returns
2836
+ -------
2837
+ None
2838
+ Initializes the CRNNModel instance
2839
+
2840
+ Notes
2841
+ -----
2842
+ The model uses a bidirectional LSTM with batch_first=True.
2843
+ The convolutional layers use 'same' padding to maintain sequence length.
2844
+ Default activation function is ReLU if an invalid activation is provided.
2845
+
2846
+ Examples
2847
+ --------
2848
+ >>> model = CRNNModel(
2849
+ ... num_filters=32,
2850
+ ... kernel_size=3,
2851
+ ... encoding_dim=64,
2852
+ ... dropout_rate=0.2,
2853
+ ... activation='relu',
2854
+ ... inputsize=128
2855
+ ... )
2856
+ """
2857
+ super(CRNNModel, self).__init__()
2858
+
2859
+ self.num_filters = num_filters
2860
+ self.kernel_size = kernel_size
2861
+ self.encoding_dim = encoding_dim
2862
+ self.dropout_rate = dropout_rate
2863
+ self.activation = activation
2864
+ self.inputsize = inputsize
2865
+
2866
+ # Get activation function
2867
+ if activation == "relu":
2868
+ act_fn = nn.ReLU
2869
+ elif activation == "tanh":
2870
+ act_fn = nn.Tanh
2871
+ else:
2872
+ act_fn = nn.ReLU
2873
+
2874
+ # Convolutional front-end
2875
+ self.conv1 = nn.Conv1d(inputsize, num_filters, kernel_size, padding="same")
2876
+ self.bn1 = nn.BatchNorm1d(num_filters)
2877
+ self.dropout1 = nn.Dropout(dropout_rate)
2878
+ self.act1 = act_fn()
2879
+
2880
+ self.conv2 = nn.Conv1d(num_filters, num_filters * 2, kernel_size, padding="same")
2881
+ self.bn2 = nn.BatchNorm1d(num_filters * 2)
2882
+ self.dropout2 = nn.Dropout(dropout_rate)
2883
+ self.act2 = act_fn()
2884
+
2885
+ # Bidirectional LSTM
2886
+ self.lstm = nn.LSTM(num_filters * 2, encoding_dim, batch_first=True, bidirectional=True)
2887
+
2888
+ # Output mapping
2889
+ self.fc_out = nn.Linear(encoding_dim * 2, inputsize)
2890
+
2891
+ def forward(self, x):
2892
+ """
2893
+ Forward pass through the neural network architecture.
2894
+
2895
+ This function processes input data through a convolutional neural network
2896
+ followed by an LSTM layer and a fully connected output layer. The input
2897
+ is first processed through two convolutional blocks, then reshaped for
2898
+ LSTM processing, and finally converted back to the original output format.
2899
+
2900
+ Parameters
2901
+ ----------
2902
+ x : torch.Tensor
2903
+ Input tensor of shape (batch_size, channels, length) containing
2904
+ the input sequence data to be processed.
2905
+
2906
+ Returns
2907
+ -------
2908
+ torch.Tensor
2909
+ Output tensor of shape (batch_size, channels, length) containing
2910
+ the processed sequence data after passing through all layers.
2911
+
2912
+ Notes
2913
+ -----
2914
+ The function performs the following operations in sequence:
2915
+ 1. Two convolutional blocks with batch normalization, dropout, and activation
2916
+ 2. Permute operation to reshape data for LSTM processing (batch, seq_len, features)
2917
+ 3. LSTM layer processing
2918
+ 4. Fully connected output layer
2919
+ 5. Final permutation to restore original shape (batch, channels, length)
2920
+
2921
+ Examples
2922
+ --------
2923
+ >>> import torch
2924
+ >>> model = YourModelClass()
2925
+ >>> x = torch.randn(32, 1, 100) # batch_size=32, channels=1, length=100
2926
+ >>> output = model.forward(x)
2927
+ >>> print(output.shape) # torch.Size([32, 1, 100])
2928
+ """
2929
+ # Conv layers expect (batch, channels, length)
2930
+ x = self.conv1(x)
2931
+ x = self.bn1(x)
2932
+ x = self.dropout1(x)
2933
+ x = self.act1(x)
2934
+
2935
+ x = self.conv2(x)
2936
+ x = self.bn2(x)
2937
+ x = self.dropout2(x)
2938
+ x = self.act2(x)
2939
+
2940
+ # LSTM expects (batch, seq_len, features)
2941
+ x = x.permute(0, 2, 1)
2942
+ x, _ = self.lstm(x)
2943
+
2944
+ # Output layer
2945
+ x = self.fc_out(x)
2946
+
2947
+ # Convert back to (batch, channels, length)
2948
+ x = x.permute(0, 2, 1)
2949
+
2950
+ return x
2951
+
2952
+ def get_config(self):
2953
+ """
2954
+ Get the configuration parameters of the model.
2955
+
2956
+ Returns
2957
+ -------
2958
+ dict
2959
+ A dictionary containing the model configuration parameters with the following keys:
2960
+ - "num_filters" (int): Number of filters in the convolutional layers
2961
+ - "kernel_size" (int): Size of the convolutional kernel
2962
+ - "encoding_dim" (int): Dimension of the encoding layer
2963
+ - "dropout_rate" (float): Dropout rate for regularization
2964
+ - "activation" (str): Activation function used in the layers
2965
+ - "inputsize" (int): Size of the input data
2966
+
2967
+ Notes
2968
+ -----
2969
+ This method returns a copy of the current configuration parameters. Modifications
2970
+ to the returned dictionary will not affect the original model configuration.
2971
+
2972
+ Examples
2973
+ --------
2974
+ >>> model = MyModel()
2975
+ >>> config = model.get_config()
2976
+ >>> print(config['num_filters'])
2977
+ 32
2978
+ """
2979
+ return {
2980
+ "num_filters": self.num_filters,
2981
+ "kernel_size": self.kernel_size,
2982
+ "encoding_dim": self.encoding_dim,
2983
+ "dropout_rate": self.dropout_rate,
2984
+ "activation": self.activation,
2985
+ "inputsize": self.inputsize,
2986
+ }
2987
+
2988
+
2989
+ class CRNNDLFilter(DeepLearningFilter):
2990
+ def __init__(
2991
+ self,
2992
+ encoding_dim: int = 10,
2993
+ num_filters: int = 10,
2994
+ kernel_size: int = 5,
2995
+ dilation_rate: int = 1,
2996
+ *args,
2997
+ **kwargs,
2998
+ ) -> None:
2999
+ """
3000
+ Initialize CRNNDLFilter layer.
3001
+
3002
+ Parameters
3003
+ ----------
3004
+ encoding_dim : int, optional
3005
+ Dimension of the encoding layer, by default 10
3006
+ num_filters : int, optional
3007
+ Number of filters in the convolutional layer, by default 10
3008
+ kernel_size : int, optional
3009
+ Size of the convolutional kernel, by default 5
3010
+ dilation_rate : int, optional
3011
+ Dilation rate for the convolutional layer, by default 1
3012
+ *args : tuple
3013
+ Variable length argument list
3014
+ **kwargs : dict
3015
+ Arbitrary keyword arguments
3016
+
3017
+ Returns
3018
+ -------
3019
+ None
3020
+ This method does not return any value
3021
+
3022
+ Notes
3023
+ -----
3024
+ This constructor initializes a CRNN (Convolutional Recurrent Neural Network)
3025
+ with dilated filters. The layer type is set to "crnn" and configuration
3026
+ parameters are stored in infodict for later reference.
3027
+
3028
+ Examples
3029
+ --------
3030
+ >>> layer = CRNNDLFilter(encoding_dim=20, num_filters=15, kernel_size=3)
3031
+ >>> print(layer.nettype)
3032
+ 'crnn'
3033
+ """
3034
+ self.num_filters = num_filters
3035
+ self.kernel_size = kernel_size
3036
+ self.dilation_rate = dilation_rate
3037
+ self.encoding_dim = encoding_dim
3038
+ self.nettype = "crnn"
3039
+ self.infodict["nettype"] = self.nettype
3040
+ self.infodict["num_filters"] = self.num_filters
3041
+ self.infodict["kernel_size"] = self.kernel_size
3042
+ self.infodict["encoding_dim"] = self.encoding_dim
3043
+ super(CRNNDLFilter, self).__init__(*args, **kwargs)
3044
+
3045
+ def getname(self):
3046
+ """
3047
+ Generate and configure model name and path based on configuration parameters.
3048
+
3049
+ This method constructs a descriptive model name string based on various configuration
3050
+ parameters and creates the corresponding model directory path. The generated name
3051
+ includes information about window size, encoding dimensions, filters, kernel size,
3052
+ epochs, thresholds, step size, and activation function.
3053
+
3054
+ Parameters
3055
+ ----------
3056
+ self : object
3057
+ The instance containing configuration parameters for model naming.
3058
+
3059
+ Returns
3060
+ -------
3061
+ None
3062
+ This method modifies instance attributes in-place and does not return a value.
3063
+
3064
+ Notes
3065
+ -----
3066
+ The generated model name follows a consistent naming convention:
3067
+ 'model_crnn_pytorch_wXXX_enXXX_fnXX_flXX_eXXX_tX_ctX_sX_activation'
3068
+ where XXX represents zero-padded numeric values and X represents single digits.
3069
+
3070
+ Additional suffixes are appended based on:
3071
+ - usebadpts: '_usebadpts' if True
3072
+ - excludebysubject: '_excludebysubject' if True
3073
+ - namesuffix: '_{suffix}' if not None
3074
+
3075
+ Examples
3076
+ --------
3077
+ >>> model = ModelClass()
3078
+ >>> model.window_size = 100
3079
+ >>> model.encoding_dim = 128
3080
+ >>> model.num_filters = 32
3081
+ >>> model.kernel_size = 5
3082
+ >>> model.num_epochs = 100
3083
+ >>> model.excludethresh = 0.5
3084
+ >>> model.corrthresh = 0.8
3085
+ >>> model.step = 10
3086
+ >>> model.activation = 'relu'
3087
+ >>> model.modelroot = '/path/to/models'
3088
+ >>> model.getname()
3089
+ >>> print(model.modelname)
3090
+ 'model_crnn_pytorch_w100_en128_fn32_fl05_e100_t0.5_ct0.8_s10_relu'
3091
+ """
3092
+ self.modelname = "_".join(
3093
+ [
3094
+ "model",
3095
+ "crnn",
3096
+ "pytorch",
3097
+ "w" + str(self.window_size).zfill(3),
3098
+ "en" + str(self.encoding_dim).zfill(3),
3099
+ "fn" + str(self.num_filters).zfill(2),
3100
+ "fl" + str(self.kernel_size).zfill(2),
3101
+ "e" + str(self.num_epochs).zfill(3),
3102
+ "t" + str(self.excludethresh),
3103
+ "ct" + str(self.corrthresh),
3104
+ "s" + str(self.step),
3105
+ self.activation,
3106
+ ]
3107
+ )
3108
+ if self.usebadpts:
3109
+ self.modelname += "_usebadpts"
3110
+ if self.excludebysubject:
3111
+ self.modelname += "_excludebysubject"
3112
+ if self.namesuffix is not None:
3113
+ self.modelname += "_" + self.namesuffix
3114
+ self.modelpath = os.path.join(self.modelroot, self.modelname)
3115
+
3116
+ try:
3117
+ os.makedirs(self.modelpath)
3118
+ except OSError:
3119
+ pass
3120
+
3121
+ def makenet(self):
3122
+ """
3123
+ Create and initialize a CRNN model for neural network training.
3124
+
3125
+ This method initializes a CRNN (Convolutional Recurrent Neural Network) model
3126
+ using the specified configuration parameters and moves it to the designated
3127
+ device (CPU or GPU).
3128
+
3129
+ Parameters
3130
+ ----------
3131
+ self : object
3132
+ The instance of the class containing this method. Expected to have the
3133
+ following attributes:
3134
+ - num_filters : int
3135
+ Number of filters in the convolutional layers
3136
+ - kernel_size : int or tuple
3137
+ Size of the convolutional kernel
3138
+ - encoding_dim : int
3139
+ Dimension of the encoding layer
3140
+ - dropout_rate : float
3141
+ Dropout rate for regularization
3142
+ - activation : str or callable
3143
+ Activation function to use
3144
+ - inputsize : tuple
3145
+ Input dimensions for the model
3146
+ - device : torch.device
3147
+ Device to move the model to (CPU or GPU)
3148
+
3149
+ Returns
3150
+ -------
3151
+ None
3152
+ This method does not return any value. It initializes the model attribute
3153
+ of the class instance.
3154
+
3155
+ Notes
3156
+ -----
3157
+ The method assumes that the CRNNModel class is properly imported and available
3158
+ in the namespace. The model is automatically moved to the device specified
3159
+ in self.device.
3160
+
3161
+ Examples
3162
+ --------
3163
+ >>> class MyModel:
3164
+ ... def __init__(self):
3165
+ ... self.num_filters = 32
3166
+ ... self.kernel_size = 3
3167
+ ... self.encoding_dim = 128
3168
+ ... self.dropout_rate = 0.2
3169
+ ... self.activation = 'relu'
3170
+ ... self.inputsize = (1, 28, 28)
3171
+ ... self.device = torch.device('cpu')
3172
+ ...
3173
+ ... def makenet(self):
3174
+ ... self.model = CRNNModel(
3175
+ ... self.num_filters,
3176
+ ... self.kernel_size,
3177
+ ... self.encoding_dim,
3178
+ ... self.dropout_rate,
3179
+ ... self.activation,
3180
+ ... self.inputsize,
3181
+ ... )
3182
+ ... self.model.to(self.device)
3183
+ ...
3184
+ >>> model = MyModel()
3185
+ >>> model.makenet()
3186
+ >>> print(model.model)
3187
+ """
3188
+ self.model = CRNNModel(
3189
+ self.num_filters,
3190
+ self.kernel_size,
3191
+ self.encoding_dim,
3192
+ self.dropout_rate,
3193
+ self.activation,
3194
+ self.inputsize,
3195
+ )
3196
+ self.model.to(self.device)
3197
+
3198
+
3199
+ class LSTMModel(nn.Module):
3200
+ def __init__(self, num_units, num_layers, dropout_rate, window_size, inputsize):
3201
+ """
3202
+ Initialize the LSTMModel with specified architecture parameters.
3203
+
3204
+ Parameters
3205
+ ----------
3206
+ num_units : int
3207
+ Number of units in each LSTM layer
3208
+ num_layers : int
3209
+ Number of LSTM layers in the model
3210
+ dropout_rate : float
3211
+ Dropout rate for LSTM layers (applied only if num_layers > 1)
3212
+ window_size : int
3213
+ Size of the sliding window used for sequence processing
3214
+ inputsize : int
3215
+ Dimensionality of input features
3216
+
3217
+ Returns
3218
+ -------
3219
+ None
3220
+ Initializes the LSTMModel instance with the specified architecture
3221
+
3222
+ Notes
3223
+ -----
3224
+ This constructor creates a bidirectional LSTM model with residual connections.
3225
+ The model uses LSTM layers with bidirectional processing and time-distributed
3226
+ dense layers for output transformation. Dropout is applied between layers
3227
+ when multiple layers are present.
3228
+
3229
+ Examples
3230
+ --------
3231
+ >>> model = LSTMModel(num_units=128, num_layers=2, dropout_rate=0.2,
3232
+ ... window_size=10, inputsize=20)
3233
+ >>> print(model)
3234
+ """
3235
+ super(LSTMModel, self).__init__()
3236
+
3237
+ self.num_units = num_units
3238
+ self.num_layers = num_layers
3239
+ self.dropout_rate = dropout_rate
3240
+ self.window_size = window_size
3241
+ self.inputsize = inputsize
3242
+
3243
+ self.lstm_layers = nn.ModuleList()
3244
+ self.dense_layers = nn.ModuleList()
3245
+
3246
+ for _ in range(num_layers):
3247
+ # Bidirectional LSTM
3248
+ self.lstm_layers.append(
3249
+ nn.LSTM(
3250
+ inputsize if len(self.lstm_layers) == 0 else inputsize,
3251
+ num_units,
3252
+ batch_first=True,
3253
+ bidirectional=True,
3254
+ dropout=dropout_rate if num_layers > 1 else 0,
3255
+ )
3256
+ )
3257
+ # Time-distributed dense layer
3258
+ self.dense_layers.append(nn.Linear(num_units * 2, inputsize))
3259
+
3260
+ def forward(self, x):
3261
+ """
3262
+ Forward pass through LSTM and dense layers.
3263
+
3264
+ Apply a sequence of LSTM layers followed by dense layers to the input tensor,
3265
+ with appropriate dimension permutations to maintain correct data flow.
3266
+
3267
+ Parameters
3268
+ ----------
3269
+ x : torch.Tensor
3270
+ Input tensor with shape (batch, channels, length) containing the sequential data.
3271
+
3272
+ Returns
3273
+ -------
3274
+ torch.Tensor
3275
+ Output tensor with shape (batch, channels, length) after processing through
3276
+ LSTM and dense layers.
3277
+
3278
+ Notes
3279
+ -----
3280
+ The function performs the following operations:
3281
+ 1. Permutes input from (batch, channels, length) to (batch, length, channels)
3282
+ 2. Processes through LSTM layers sequentially
3283
+ 3. Applies dense layers to each time step
3284
+ 4. Permutes output back to (batch, channels, length)
3285
+
3286
+ Examples
3287
+ --------
3288
+ >>> import torch
3289
+ >>> # Assuming self.lstm_layers and self.dense_layers are initialized
3290
+ >>> x = torch.randn(32, 128, 100) # batch=32, channels=128, length=100
3291
+ >>> output = model.forward(x)
3292
+ >>> output.shape
3293
+ torch.Size([32, 128, 100])
3294
+ """
3295
+ # x is (batch, channels, length), convert to (batch, length, channels)
3296
+ x = x.permute(0, 2, 1)
3297
+
3298
+ for lstm, dense in zip(self.lstm_layers, self.dense_layers):
3299
+ x, _ = lstm(x)
3300
+ # Apply dense layer across time steps
3301
+ x = dense(x)
3302
+
3303
+ # Convert back to (batch, channels, length)
3304
+ x = x.permute(0, 2, 1)
3305
+
3306
+ return x
3307
+
3308
+ def get_config(self):
3309
+ """
3310
+ Get the configuration parameters of the model.
3311
+
3312
+ Returns
3313
+ -------
3314
+ dict
3315
+ A dictionary containing the model configuration parameters with the following keys:
3316
+ - "num_units" (int): Number of units in each layer
3317
+ - "num_layers" (int): Number of layers in the model
3318
+ - "dropout_rate" (float): Dropout rate for regularization
3319
+ - "window_size" (int): Size of the sliding window for sequence processing
3320
+ - "inputsize" (int): Size of the input features
3321
+
3322
+ Notes
3323
+ -----
3324
+ This method returns a copy of the internal configuration parameters.
3325
+ The returned dictionary can be used to recreate the model with the same configuration.
3326
+
3327
+ Examples
3328
+ --------
3329
+ >>> config = model.get_config()
3330
+ >>> print(config['num_units'])
3331
+ 128
3332
+ >>> new_model = ModelClass(**config)
3333
+ """
3334
+ return {
3335
+ "num_units": self.num_units,
3336
+ "num_layers": self.num_layers,
3337
+ "dropout_rate": self.dropout_rate,
3338
+ "window_size": self.window_size,
3339
+ "inputsize": self.inputsize,
3340
+ }
3341
+
3342
+
3343
+ class LSTMDLFilter(DeepLearningFilter):
3344
+ def __init__(self, num_units: int = 16, *args, **kwargs) -> None:
3345
+ """
3346
+ Initialize the LSTMDLFilter layer.
3347
+
3348
+ Parameters
3349
+ ----------
3350
+ num_units : int, optional
3351
+ Number of units in the LSTM layer, by default 16
3352
+ *args
3353
+ Variable length argument list passed to parent class
3354
+ **kwargs
3355
+ Arbitrary keyword arguments passed to parent class
3356
+
3357
+ Returns
3358
+ -------
3359
+ None
3360
+ This method initializes the instance and does not return any value
3361
+
3362
+ Notes
3363
+ -----
3364
+ This constructor sets up the LSTM layer with specified number of units and
3365
+ initializes the network type identifier. The infodict is updated with both
3366
+ the network type and number of units for tracking purposes.
3367
+
3368
+ Examples
3369
+ --------
3370
+ >>> layer = LSTMDLFilter(num_units=32)
3371
+ >>> print(layer.num_units)
3372
+ 32
3373
+ >>> print(layer.nettype)
3374
+ 'lstm'
3375
+ """
3376
+ self.num_units = num_units
3377
+ self.nettype = "lstm"
3378
+ self.infodict["nettype"] = self.nettype
3379
+ self.infodict["num_units"] = self.num_units
3380
+ super(LSTMDLFilter, self).__init__(*args, **kwargs)
3381
+
3382
+ def getname(self):
3383
+ """
3384
+ Generate and configure model name and path based on current parameters.
3385
+
3386
+ This method constructs a descriptive model name string using various
3387
+ hyperparameters and configuration settings. It then creates the
3388
+ corresponding directory path and ensures it exists.
3389
+
3390
+ Parameters
3391
+ ----------
3392
+ self : object
3393
+ The instance containing model configuration attributes.
3394
+
3395
+ Returns
3396
+ -------
3397
+ None
3398
+ This method modifies instance attributes in-place and does not return a value.
3399
+
3400
+ Notes
3401
+ -----
3402
+ The generated model name follows a specific format:
3403
+ "model_lstm_pytorch_wXXX_lYY_nuZZZ_dDD_rdDD_eFFF_tT_ctTT_sS"
3404
+ where XXX, YY, ZZZ, DD, FF, T, TT, S represent formatted parameter values.
3405
+
3406
+ Examples
3407
+ --------
3408
+ >>> model = MyModel()
3409
+ >>> model.window_size = 100
3410
+ >>> model.num_layers = 2
3411
+ >>> model.num_units = 128
3412
+ >>> model.dropout_rate = 0.2
3413
+ >>> model.num_epochs = 100
3414
+ >>> model.excludethresh = 0.5
3415
+ >>> model.corrthresh = 0.8
3416
+ >>> model.step = 1
3417
+ >>> model.excludebysubject = True
3418
+ >>> model.getname()
3419
+ >>> print(model.modelname)
3420
+ 'model_lstm_pytorch_w100_l02_nu128_d02_rd02_e100_t05_ct08_s1_excludebysubject'
3421
+ """
3422
+ self.modelname = "_".join(
3423
+ [
3424
+ "model",
3425
+ "lstm",
3426
+ "pytorch",
3427
+ "w" + str(self.window_size).zfill(3),
3428
+ "l" + str(self.num_layers).zfill(2),
3429
+ "nu" + str(self.num_units),
3430
+ "d" + str(self.dropout_rate),
3431
+ "rd" + str(self.dropout_rate),
3432
+ "e" + str(self.num_epochs).zfill(3),
3433
+ "t" + str(self.excludethresh),
3434
+ "ct" + str(self.corrthresh),
3435
+ "s" + str(self.step),
3436
+ ]
3437
+ )
3438
+ if self.excludebysubject:
3439
+ self.modelname += "_excludebysubject"
3440
+ self.modelpath = os.path.join(self.modelroot, self.modelname)
3441
+
3442
+ try:
3443
+ os.makedirs(self.modelpath)
3444
+ except OSError:
3445
+ pass
3446
+
3447
+ def makenet(self):
3448
+ """
3449
+ Create and initialize an LSTM model for neural network training.
3450
+
3451
+ This method initializes an LSTMModel with the specified architecture parameters
3452
+ and moves the model to the designated device (CPU or GPU).
3453
+
3454
+ Parameters
3455
+ ----------
3456
+ self : object
3457
+ The instance containing the following attributes:
3458
+ - num_units : int
3459
+ Number of units in each LSTM layer
3460
+ - num_layers : int
3461
+ Number of LSTM layers in the model
3462
+ - dropout_rate : float
3463
+ Dropout rate for regularization
3464
+ - window_size : int
3465
+ Size of the input window for time series data
3466
+ - inputsize : int
3467
+ Size of the input features
3468
+ - device : torch.device
3469
+ Device to move the model to (e.g., 'cpu' or 'cuda')
3470
+
3471
+ Returns
3472
+ -------
3473
+ None
3474
+ This method does not return any value. It initializes the model attribute
3475
+ and moves it to the specified device.
3476
+
3477
+ Notes
3478
+ -----
3479
+ The method creates an LSTMModel instance with the following parameters:
3480
+ - num_units: Number of hidden units in LSTM layers
3481
+ - num_layers: Number of stacked LSTM layers
3482
+ - dropout_rate: Dropout probability for regularization
3483
+ - window_size: Input sequence length
3484
+ - inputsize: Feature dimension of input data
3485
+
3486
+ Examples
3487
+ --------
3488
+ >>> # Assuming self is an instance with required attributes
3489
+ >>> self.makenet()
3490
+ >>> # Model is now initialized and moved to specified device
3491
+ """
3492
+ self.model = LSTMModel(
3493
+ self.num_units,
3494
+ self.num_layers,
3495
+ self.dropout_rate,
3496
+ self.window_size,
3497
+ self.inputsize,
3498
+ )
3499
+ self.model.to(self.device)
3500
+
3501
+
3502
+ class HybridModel(nn.Module):
3503
+ def __init__(
3504
+ self,
3505
+ num_filters,
3506
+ kernel_size,
3507
+ num_units,
3508
+ num_layers,
3509
+ dropout_rate,
3510
+ activation,
3511
+ inputsize,
3512
+ window_size,
3513
+ invert,
3514
+ ):
3515
+ """
3516
+ Initialize the HybridModel with configurable CNN-LSTM architecture.
3517
+
3518
+ Parameters
3519
+ ----------
3520
+ num_filters : int
3521
+ Number of filters in the convolutional layers.
3522
+ kernel_size : int
3523
+ Size of the convolutional kernel.
3524
+ num_units : int
3525
+ Number of units in the LSTM layers.
3526
+ num_layers : int
3527
+ Total number of layers in the model.
3528
+ dropout_rate : float
3529
+ Dropout rate for regularization.
3530
+ activation : str
3531
+ Activation function to use; options are 'relu' or 'tanh'.
3532
+ inputsize : int
3533
+ Size of the input features.
3534
+ window_size : int
3535
+ Size of the sliding window for input data.
3536
+ invert : bool
3537
+ If True, applies CNN first followed by LSTM. Otherwise, applies LSTM first followed by CNN.
3538
+
3539
+ Returns
3540
+ -------
3541
+ None
3542
+ This method initializes the model's layers and components but does not return any value.
3543
+
3544
+ Notes
3545
+ -----
3546
+ The model supports two architectures:
3547
+ - If `invert=False`: LSTM → CNN
3548
+ - If `invert=True`: CNN → LSTM
3549
+
3550
+ Examples
3551
+ --------
3552
+ >>> model = HybridModel(
3553
+ ... num_filters=64,
3554
+ ... kernel_size=3,
3555
+ ... num_units=128,
3556
+ ... num_layers=3,
3557
+ ... dropout_rate=0.2,
3558
+ ... activation="relu",
3559
+ ... inputsize=10,
3560
+ ... window_size=100,
3561
+ ... invert=True
3562
+ ... )
3563
+ """
3564
+ super(HybridModel, self).__init__()
3565
+
3566
+ self.num_filters = num_filters
3567
+ self.kernel_size = kernel_size
3568
+ self.num_units = num_units
3569
+ self.num_layers = num_layers
3570
+ self.dropout_rate = dropout_rate
3571
+ self.activation = activation
3572
+ self.inputsize = inputsize
3573
+ self.window_size = window_size
3574
+ self.invert = invert
3575
+
3576
+ # Get activation function
3577
+ if activation == "relu":
3578
+ act_fn = nn.ReLU
3579
+ elif activation == "tanh":
3580
+ act_fn = nn.Tanh
3581
+ else:
3582
+ act_fn = nn.ReLU
3583
+
3584
+ self.layers = nn.ModuleList()
3585
+
3586
+ if invert:
3587
+ # CNN first, then LSTM
3588
+ # Input layer
3589
+ self.layers.append(nn.Conv1d(inputsize, num_filters, kernel_size, padding="same"))
3590
+ self.layers.append(nn.BatchNorm1d(num_filters))
3591
+ self.layers.append(nn.Dropout(dropout_rate))
3592
+ self.layers.append(act_fn())
3593
+
3594
+ # Intermediate CNN layers
3595
+ for _ in range(num_layers - 2):
3596
+ self.layers.append(
3597
+ nn.Conv1d(num_filters, num_filters, kernel_size, padding="same")
3598
+ )
3599
+ self.layers.append(nn.BatchNorm1d(num_filters))
3600
+ self.layers.append(nn.Dropout(dropout_rate))
3601
+ self.layers.append(act_fn())
3602
+
3603
+ # LSTM layer
3604
+ self.lstm = nn.LSTM(
3605
+ num_filters, num_units, batch_first=True, bidirectional=True, dropout=dropout_rate
3606
+ )
3607
+ self.lstm_dense = nn.Linear(num_units * 2, inputsize)
3608
+
3609
+ else:
3610
+ # LSTM first, then CNN
3611
+ self.lstm = nn.LSTM(
3612
+ inputsize, num_units, batch_first=True, bidirectional=True, dropout=dropout_rate
3613
+ )
3614
+ self.lstm_dense = nn.Linear(num_units * 2, inputsize)
3615
+ self.lstm_dropout = nn.Dropout(dropout_rate)
3616
+
3617
+ # Intermediate CNN layers
3618
+ for _ in range(num_layers - 2):
3619
+ self.layers.append(nn.Conv1d(inputsize, num_filters, kernel_size, padding="same"))
3620
+ self.layers.append(nn.BatchNorm1d(num_filters))
3621
+ self.layers.append(nn.Dropout(dropout_rate))
3622
+ self.layers.append(act_fn())
3623
+
3624
+ # Output layer
3625
+ self.output_conv = nn.Conv1d(
3626
+ num_filters if num_layers > 2 else inputsize,
3627
+ inputsize,
3628
+ kernel_size,
3629
+ padding="same",
3630
+ )
3631
+
3632
+ def forward(self, x):
3633
+ """
3634
+ Forward pass of the model with optional CNN-LSTM hybrid architecture.
3635
+
3636
+ This method implements a flexible forward pass that can operate in two modes
3637
+ depending on the `invert` flag. When `invert` is True, the sequence processing
3638
+ follows CNN → LSTM → CNN structure. When `invert` is False, the sequence
3639
+ processing follows LSTM → CNN structure.
3640
+
3641
+ Parameters
3642
+ ----------
3643
+ x : torch.Tensor
3644
+ Input tensor of shape (batch_size, channels, sequence_length) or
3645
+ (batch_size, sequence_length, channels) depending on the processing path.
3646
+
3647
+ Returns
3648
+ -------
3649
+ torch.Tensor
3650
+ Output tensor with the same batch dimension as input, with shape
3651
+ dependent on the specific layers and processing path used.
3652
+
3653
+ Notes
3654
+ -----
3655
+ The function handles different tensor permutations based on the processing
3656
+ order:
3657
+ - CNN → LSTM path: permutes from (B, C, L) to (B, L, C) for LSTM, then back
3658
+ - LSTM → CNN path: permutes from (B, C, L) to (B, L, C) for LSTM, then back
3659
+ The `invert` flag determines which processing order is used.
3660
+
3661
+ Examples
3662
+ --------
3663
+ >>> model = MyModel()
3664
+ >>> x = torch.randn(32, 10, 128) # batch_size=32, seq_len=10, features=128
3665
+ >>> output = model.forward(x)
3666
+ >>> print(output.shape)
3667
+ torch.Size([32, 10, 256])
3668
+ """
3669
+ if self.invert:
3670
+ # Apply CNN layers
3671
+ for layer in self.layers:
3672
+ x = layer(x)
3673
+
3674
+ # LSTM expects (batch, seq_len, features)
3675
+ x = x.permute(0, 2, 1)
3676
+ x, _ = self.lstm(x)
3677
+ x = self.lstm_dense(x)
3678
+
3679
+ # Convert back to (batch, channels, length)
3680
+ x = x.permute(0, 2, 1)
3681
+
3682
+ else:
3683
+ # LSTM first
3684
+ x = x.permute(0, 2, 1)
3685
+ x, _ = self.lstm(x)
3686
+ x = self.lstm_dense(x)
3687
+ x = self.lstm_dropout(x)
3688
+ x = x.permute(0, 2, 1)
3689
+
3690
+ # CNN layers
3691
+ for layer in self.layers:
3692
+ x = layer(x)
3693
+
3694
+ # Output layer
3695
+ if hasattr(self, "output_conv"):
3696
+ x = self.output_conv(x)
3697
+
3698
+ return x
3699
+
3700
+ def get_config(self):
3701
+ """
3702
+ Get the configuration parameters of the model.
3703
+
3704
+ Returns
3705
+ -------
3706
+ dict
3707
+ A dictionary containing all configuration parameters with their current values:
3708
+ - num_filters: int, number of filters in the convolutional layers
3709
+ - kernel_size: int, size of the convolutional kernel
3710
+ - num_units: int, number of units in the dense layers
3711
+ - num_layers: int, number of layers in the model
3712
+ - dropout_rate: float, dropout rate for regularization
3713
+ - activation: str or callable, activation function to use
3714
+ - inputsize: int, size of the input features
3715
+ - window_size: int, size of the sliding window
3716
+ - invert: bool, whether to invert the input data
3717
+
3718
+ Notes
3719
+ -----
3720
+ This method returns a copy of the internal configuration dictionary.
3721
+ Modifications to the returned dictionary will not affect the original model configuration.
3722
+
3723
+ Examples
3724
+ --------
3725
+ >>> config = model.get_config()
3726
+ >>> print(config['num_filters'])
3727
+ 32
3728
+ """
3729
+ return {
3730
+ "num_filters": self.num_filters,
3731
+ "kernel_size": self.kernel_size,
3732
+ "num_units": self.num_units,
3733
+ "num_layers": self.num_layers,
3734
+ "dropout_rate": self.dropout_rate,
3735
+ "activation": self.activation,
3736
+ "inputsize": self.inputsize,
3737
+ "window_size": self.window_size,
3738
+ "invert": self.invert,
3739
+ }
3740
+
3741
+
3742
+ class HybridDLFilter(DeepLearningFilter):
3743
+ def __init__(
3744
+ self,
3745
+ invert: bool = False,
3746
+ num_filters: int = 10,
3747
+ kernel_size: int = 5,
3748
+ num_units: int = 16,
3749
+ *args,
3750
+ **kwargs,
3751
+ ) -> None:
3752
+ """
3753
+ Initialize HybridDLFilter layer.
3754
+
3755
+ Parameters
3756
+ ----------
3757
+ invert : bool, default=False
3758
+ If True, inverts the filter response. If False, applies normal filtering.
3759
+ num_filters : int, default=10
3760
+ Number of filters to apply in the convolutional layer.
3761
+ kernel_size : int, default=5
3762
+ Size of the convolutional kernel.
3763
+ num_units : int, default=16
3764
+ Number of units in the dense layer.
3765
+ *args
3766
+ Variable length argument list.
3767
+ **kwargs
3768
+ Arbitrary keyword arguments.
3769
+
3770
+ Returns
3771
+ -------
3772
+ None
3773
+ This method initializes the HybridDLFilter instance and does not return any value.
3774
+
3775
+ Notes
3776
+ -----
3777
+ This constructor sets up a hybrid deep learning filter that combines convolutional
3778
+ and dense layers. The infodict dictionary is populated with configuration parameters
3779
+ for tracking and logging purposes.
3780
+
3781
+ Examples
3782
+ --------
3783
+ >>> filter_layer = HybridDLFilter(
3784
+ ... invert=True,
3785
+ ... num_filters=20,
3786
+ ... kernel_size=3,
3787
+ ... num_units=32
3788
+ ... )
3789
+ """
3790
+ self.invert = invert
3791
+ self.num_filters = num_filters
3792
+ self.kernel_size = kernel_size
3793
+ self.num_units = num_units
3794
+ self.nettype = "hybrid"
3795
+ self.infodict["nettype"] = self.nettype
3796
+ self.infodict["num_filters"] = self.num_filters
3797
+ self.infodict["kernel_size"] = self.kernel_size
3798
+ self.infodict["invert"] = self.invert
3799
+ self.infodict["num_units"] = self.num_units
3800
+ super(HybridDLFilter, self).__init__(*args, **kwargs)
3801
+
3802
+ def getname(self):
3803
+ """
3804
+ Generate and configure the model name and path based on current parameters.
3805
+
3806
+ This method constructs a descriptive model name string using various
3807
+ hyperparameters and configuration settings. The generated name follows
3808
+ a standardized format that includes window size, layer count, filter count,
3809
+ kernel size, number of units, dropout rates, number of epochs, threshold
3810
+ values, step size, and activation function. The method also creates the
3811
+ corresponding model directory path and ensures it exists.
3812
+
3813
+ Parameters
3814
+ ----------
3815
+ self : object
3816
+ The instance of the class containing the model configuration attributes.
3817
+ Required attributes include:
3818
+ - window_size : int
3819
+ - num_layers : int
3820
+ - num_filters : int
3821
+ - kernel_size : int
3822
+ - num_units : int
3823
+ - dropout_rate : float
3824
+ - num_epochs : int
3825
+ - excludethresh : float
3826
+ - corrthresh : float
3827
+ - step : int
3828
+ - activation : str
3829
+ - invert : bool
3830
+ - excludebysubject : bool
3831
+ - modelroot : str
3832
+
3833
+ Returns
3834
+ -------
3835
+ None
3836
+ This method modifies the instance attributes in-place:
3837
+ - self.modelname : str
3838
+ - self.modelpath : str
3839
+
3840
+ Notes
3841
+ -----
3842
+ The model name format follows this pattern:
3843
+ "model_hybrid_pytorch_wXXX_lYY_fnZZ_flZZ_nuZZ_dZZ_rdZZ_eXXX_tX_ctX_sX_activation"
3844
+ where XXX, YY, ZZ, etc. represent zero-padded numerical values.
3845
+
3846
+ Additional suffixes are appended based on:
3847
+ - "_invert" if self.invert is True
3848
+ - "_excludebysubject" if self.excludebysubject is True
3849
+
3850
+ Examples
3851
+ --------
3852
+ >>> model = MyModel()
3853
+ >>> model.window_size = 100
3854
+ >>> model.num_layers = 2
3855
+ >>> model.getname()
3856
+ >>> print(model.modelname)
3857
+ 'model_hybrid_pytorch_w100_l02_fn08_fl08_nu128_d05_rd05_e100_t05_ct08_s1_relu'
3858
+ """
3859
+ self.modelname = "_".join(
3860
+ [
3861
+ "model",
3862
+ "hybrid",
3863
+ "pytorch",
3864
+ "w" + str(self.window_size).zfill(3),
3865
+ "l" + str(self.num_layers).zfill(2),
3866
+ "fn" + str(self.num_filters).zfill(2),
3867
+ "fl" + str(self.kernel_size).zfill(2),
3868
+ "nu" + str(self.num_units),
3869
+ "d" + str(self.dropout_rate),
3870
+ "rd" + str(self.dropout_rate),
3871
+ "e" + str(self.num_epochs).zfill(3),
3872
+ "t" + str(self.excludethresh),
3873
+ "ct" + str(self.corrthresh),
3874
+ "s" + str(self.step),
3875
+ self.activation,
3876
+ ]
3877
+ )
3878
+ if self.invert:
3879
+ self.modelname += "_invert"
3880
+ if self.excludebysubject:
3881
+ self.modelname += "_excludebysubject"
3882
+ self.modelpath = os.path.join(self.modelroot, self.modelname)
3883
+
3884
+ try:
3885
+ os.makedirs(self.modelpath)
3886
+ except OSError:
3887
+ pass
3888
+
3889
+ def makenet(self):
3890
+ """
3891
+ Create and initialize a hybrid neural network model.
3892
+
3893
+ This method constructs a HybridModel with the specified architecture parameters
3894
+ and moves it to the designated device (CPU or GPU).
3895
+
3896
+ Parameters
3897
+ ----------
3898
+ self : object
3899
+ The instance containing the following attributes:
3900
+ - num_filters : int
3901
+ Number of filters in the convolutional layers
3902
+ - kernel_size : int
3903
+ Size of the convolutional kernels
3904
+ - num_units : int
3905
+ Number of units in the dense layers
3906
+ - num_layers : int
3907
+ Number of layers in the model
3908
+ - dropout_rate : float
3909
+ Dropout rate for regularization
3910
+ - activation : str or callable
3911
+ Activation function to use
3912
+ - inputsize : int
3913
+ Size of the input features
3914
+ - window_size : int
3915
+ Size of the sliding window
3916
+ - invert : bool
3917
+ Whether to invert the model architecture
3918
+
3919
+ Returns
3920
+ -------
3921
+ None
3922
+ This method does not return any value. It initializes the model attribute
3923
+ and moves it to the specified device.
3924
+
3925
+ Notes
3926
+ -----
3927
+ The method assumes that the instance has all required attributes set before
3928
+ calling. The model is moved to the device specified by `self.device`.
3929
+
3930
+ Examples
3931
+ --------
3932
+ >>> model = MyModel()
3933
+ >>> model.num_filters = 32
3934
+ >>> model.kernel_size = 3
3935
+ >>> model.num_units = 64
3936
+ >>> model.num_layers = 2
3937
+ >>> model.dropout_rate = 0.2
3938
+ >>> model.activation = 'relu'
3939
+ >>> model.inputsize = 10
3940
+ >>> model.window_size = 5
3941
+ >>> model.invert = False
3942
+ >>> model.device = 'cuda'
3943
+ >>> model.makenet()
3944
+ >>> print(model.model)
3945
+ """
3946
+ self.model = HybridModel(
3947
+ self.num_filters,
3948
+ self.kernel_size,
3949
+ self.num_units,
3950
+ self.num_layers,
3951
+ self.dropout_rate,
3952
+ self.activation,
3953
+ self.inputsize,
3954
+ self.window_size,
3955
+ self.invert,
3956
+ )
3957
+ self.model.to(self.device)
3958
+
3959
+
3960
+ def filtscale(
3961
+ data: np.ndarray,
3962
+ scalefac: float = 1.0,
3963
+ reverse: bool = False,
3964
+ hybrid: bool = False,
3965
+ lognormalize: bool = True,
3966
+ epsilon: float = 1e-10,
3967
+ numorders: int = 6,
3968
+ ) -> tuple[np.ndarray, float] | np.ndarray:
3969
+ """
3970
+ Apply or reverse a scaling transformation to spectral data.
3971
+
3972
+ This function performs either forward or inverse scaling of input data,
3973
+ typically used in signal processing or spectral analysis. In forward mode,
3974
+ it computes the FFT of the input data and applies normalization and scaling
3975
+ to the magnitude and phase components. In reverse mode, it reconstructs
3976
+ the original time-domain signal from scaled magnitude and phase components.
3977
+
3978
+ Parameters
3979
+ ----------
3980
+ data : np.ndarray
3981
+ Input time-domain signal or scaled spectral data depending on `reverse` flag.
3982
+ scalefac : float, optional
3983
+ Scaling factor used in normalization. Default is 1.0.
3984
+ reverse : bool, optional
3985
+ If True, performs inverse transformation to reconstruct the original signal.
3986
+ If False, performs forward transformation. Default is False.
3987
+ hybrid : bool, optional
3988
+ If True, returns a hybrid output combining original signal and magnitude.
3989
+ Only applicable in forward mode. Default is False.
3990
+ lognormalize : bool, optional
3991
+ If True, applies logarithmic normalization to the magnitude. Default is True.
3992
+ epsilon : float, optional
3993
+ Small constant added to magnitude before log to avoid log(0). Default is 1e-10.
3994
+ numorders : int, optional
3995
+ Number of orders used in normalization scaling. Default is 6.
3996
+
3997
+ Returns
3998
+ -------
3999
+ tuple[np.ndarray, float] or np.ndarray
4000
+ - If `reverse` is False: Returns a tuple of (scaled_data, scalefac).
4001
+ `scaled_data` is a stacked array of magnitude and phase (or original signal
4002
+ and magnitude in hybrid mode).
4003
+ - If `reverse` is True: Returns the reconstructed time-domain signal as
4004
+ a numpy array.
4005
+
4006
+ Notes
4007
+ -----
4008
+ - In forward mode, the function computes the FFT of `data`, normalizes the
4009
+ magnitude, and scales it to a range suitable for further processing.
4010
+ - In reverse mode, the function reconstructs the time-domain signal using
4011
+ inverse FFT from the provided scaled magnitude and phase components.
4012
+ - The `hybrid` mode is useful for certain types of signal visualization or
4013
+ feature extraction where both time-domain and frequency-domain information
4014
+ are needed.
4015
+
4016
+ Examples
4017
+ --------
4018
+ >>> import numpy as np
4019
+ >>> from scipy import fftpack
4020
+ >>> x = np.random.randn(1024)
4021
+ >>> scaled_data, scalefac = filtscale(x)
4022
+ >>> reconstructed = filtscale(scaled_data, scalefac=scalefac, reverse=True)
4023
+ """
4024
+ if not reverse:
4025
+ specvals = fftpack.fft(data)
4026
+ if lognormalize:
4027
+ themag = np.log(np.absolute(specvals) + epsilon)
4028
+ scalefac = np.max(themag)
4029
+ themag = (themag - scalefac + numorders) / numorders
4030
+ themag[np.where(themag < 0.0)] = 0.0
4031
+ else:
4032
+ scalefac = np.std(data)
4033
+ themag = np.absolute(specvals) / scalefac
4034
+ thephase = np.angle(specvals)
4035
+ thephase = thephase / (2.0 * np.pi) - 0.5
4036
+ if hybrid:
4037
+ return np.stack((data, themag), axis=1), scalefac
4038
+ else:
4039
+ return np.stack((themag, thephase), axis=1), scalefac
4040
+ else:
4041
+ if hybrid:
4042
+ return data[:, 0]
4043
+ else:
4044
+ thephase = (data[:, 1] + 0.5) * 2.0 * np.pi
4045
+ if lognormalize:
4046
+ themag = np.exp(data[:, 0] * numorders - numorders + scalefac)
4047
+ else:
4048
+ themag = data[:, 0] * scalefac
4049
+ specvals = themag * np.exp(1.0j * thephase)
4050
+ return fftpack.ifft(specvals).real
4051
+
4052
+
4053
+ def tobadpts(name: str) -> str:
4054
+ """
4055
+ Convert a filename to its corresponding bad points filename.
4056
+
4057
+ This function takes a filename string and replaces the '.txt' extension
4058
+ with '_badpts.txt' to create a new filename for bad points data.
4059
+
4060
+ Parameters
4061
+ ----------
4062
+ name : str
4063
+ The input filename string, typically ending with '.txt'.
4064
+
4065
+ Returns
4066
+ -------
4067
+ str
4068
+ The converted filename with '_badpts.txt' extension instead of '.txt'.
4069
+
4070
+ Notes
4071
+ -----
4072
+ This function is useful for creating consistent naming conventions for
4073
+ bad points data files that correspond to original data files.
4074
+
4075
+ Examples
4076
+ --------
4077
+ >>> tobadpts("data.txt")
4078
+ 'data_badpts.txt'
4079
+
4080
+ >>> tobadpts("results.txt")
4081
+ 'results_badpts.txt'
4082
+
4083
+ >>> tobadpts("output.txt")
4084
+ 'output_badpts.txt'
4085
+ """
4086
+ return name.replace(".txt", "_badpts.txt")
4087
+
4088
+
4089
+ def targettoinput(name: str, targetfrag: str = "xyz", inputfrag: str = "abc") -> str:
4090
+ """
4091
+ Replace target fragment with input fragment in a string.
4092
+
4093
+ Parameters
4094
+ ----------
4095
+ name : str
4096
+ The input string to perform replacement on.
4097
+ targetfrag : str, default='xyz'
4098
+ The fragment to be replaced in the input string.
4099
+ inputfrag : str, default='abc'
4100
+ The fragment to replace the target fragment with.
4101
+
4102
+ Returns
4103
+ -------
4104
+ str
4105
+ The modified string with targetfrag replaced by inputfrag.
4106
+
4107
+ Notes
4108
+ -----
4109
+ This function uses Python's built-in string replace method, which replaces
4110
+ all occurrences of the target fragment with the input fragment.
4111
+
4112
+ Examples
4113
+ --------
4114
+ >>> targettoinput("hello xyz world")
4115
+ 'hello abc world'
4116
+
4117
+ >>> targettoinput("test xyz xyz test", "xyz", "123")
4118
+ 'test 123 123 test'
4119
+
4120
+ >>> targettoinput("abcdef", "cde", "XXX")
4121
+ 'abXXXf'
4122
+ """
4123
+ LGR.debug(f"replacing {targetfrag} with {inputfrag}")
4124
+ return name.replace(targetfrag, inputfrag)
4125
+
4126
+
4127
+ def getmatchedtcs(
4128
+ searchstring: str,
4129
+ usebadpts: bool = False,
4130
+ targetfrag: str = "xyz",
4131
+ inputfrag: str = "abc",
4132
+ debug: bool = False,
4133
+ ) -> tuple[list[str], int]:
4134
+ """
4135
+ Find and validate matched timecourse files based on a search pattern.
4136
+
4137
+ This function searches for timecourse files matching the given search string,
4138
+ verifies their completeness by checking for associated info files, and
4139
+ determines the length of the timecourses from the first valid file.
4140
+
4141
+ Parameters
4142
+ ----------
4143
+ searchstring : str
4144
+ A glob pattern to match target timecourse files.
4145
+ usebadpts : bool, optional
4146
+ Flag indicating whether bad points should be used (default is False).
4147
+ targetfrag : str, optional
4148
+ Target fragment identifier (default is "xyz").
4149
+ inputfrag : str, optional
4150
+ Input fragment identifier (default is "abc").
4151
+ debug : bool, optional
4152
+ If True, prints debug information including matched files (default is False).
4153
+
4154
+ Returns
4155
+ -------
4156
+ tuple[list[str], int]
4157
+ A tuple containing:
4158
+ - List of matched and validated file paths.
4159
+ - Length of the timecourses (number of timepoints).
4160
+
4161
+ Notes
4162
+ -----
4163
+ The function expects timecourse files to have a corresponding info file
4164
+ with the same base name but with "_info" appended. Only files with complete
4165
+ info files are considered valid.
4166
+
4167
+ Examples
4168
+ --------
4169
+ >>> matched_files, tc_length = getmatchedtcs("data/*cardiac*.tsv")
4170
+ >>> print(f"Found {len(matched_files)} files with {tc_length} timepoints")
4171
+ """
4172
+ # list all of the target files
4173
+ fromfile = sorted(glob.glob(searchstring))
4174
+ if debug:
4175
+ print(f"searchstring: {searchstring} -> {fromfile}")
4176
+
4177
+ # make sure all timecourses exist
4178
+ # we need cardiacfromfmri_25.0Hz as x, normpleth as y, and perhaps badpts
4179
+ matchedfilelist = []
4180
+ for targetname in fromfile:
4181
+ infofile = targetname.replace("_desc-stdrescardfromfmri_timeseries", "_info")
4182
+ if os.path.isfile(infofile):
4183
+ matchedfilelist.append(targetname)
4184
+ print(f"{targetname} is complete")
4185
+ LGR.debug(matchedfilelist[-1])
4186
+ else:
4187
+ print(f"{targetname} is incomplete")
4188
+ print(f"found {len(matchedfilelist)} matched files")
4189
+
4190
+ # find out how long the files are
4191
+ (
4192
+ samplerate,
4193
+ starttime,
4194
+ columns,
4195
+ inputarray,
4196
+ compression,
4197
+ columnsource,
4198
+ ) = tide_io.readbidstsv(
4199
+ matchedfilelist[0],
4200
+ colspec="cardiacfromfmri_25.0Hz,normpleth",
4201
+ )
4202
+ print(f"{inputarray.shape=}")
4203
+ tclen = inputarray.shape[1]
4204
+ LGR.info(f"tclen set to {tclen}")
4205
+ return matchedfilelist, tclen
4206
+
4207
+
4208
+ def readindata(
4209
+ matchedfilelist: list[str],
4210
+ tclen: int,
4211
+ targetfrag: str = "xyz",
4212
+ inputfrag: str = "abc",
4213
+ usebadpts: bool = False,
4214
+ startskip: int = 0,
4215
+ endskip: int = 0,
4216
+ corrthresh: float = 0.5,
4217
+ readlim: int | None = None,
4218
+ readskip: int | None = None,
4219
+ debug: bool = False,
4220
+ ) -> (
4221
+ tuple[np.ndarray, np.ndarray, list[str]] | tuple[np.ndarray, np.ndarray, list[str], np.ndarray]
4222
+ ):
4223
+ """
4224
+ Read and process time-series data from a list of matched files.
4225
+
4226
+ This function reads cardiac and plethysmographic time-series data from a list of
4227
+ files, performs quality checks, and returns the data in arrays suitable for
4228
+ training or analysis. It supports filtering based on correlation thresholds,
4229
+ NaN values, and signal standard deviations, and allows for optional skipping
4230
+ of data at the start and end of each time series.
4231
+
4232
+ Parameters
4233
+ ----------
4234
+ matchedfilelist : list of str
4235
+ List of file paths to be processed. Each file should contain time-series data
4236
+ in a format compatible with `tide_io.readbidstsv`.
4237
+ tclen : int
4238
+ Length of the time series to be read from each file.
4239
+ targetfrag : str, optional
4240
+ Fragment identifier for target files, used in naming conversions. Default is "xyz".
4241
+ inputfrag : str, optional
4242
+ Fragment identifier for input files, used in naming conversions. Default is "abc".
4243
+ usebadpts : bool, optional
4244
+ If True, include a third array with bad point indicators. Default is False.
4245
+ startskip : int, optional
4246
+ Number of samples to skip at the beginning of each time series. Default is 0.
4247
+ endskip : int, optional
4248
+ Number of samples to skip at the end of each time series. Default is 0.
4249
+ corrthresh : float, optional
4250
+ Minimum correlation threshold between raw and plethysmographic signals.
4251
+ Files with lower correlation are excluded. Default is 0.5.
4252
+ readlim : int, optional
4253
+ Maximum number of files to read. If None, all files are read. Default is None.
4254
+ readskip : int, optional
4255
+ Number of files to skip at the beginning of the file list. If None, no files are skipped. Default is None.
4256
+ debug : bool, optional
4257
+ If True, print debug information for each file. Default is False.
4258
+
4259
+ Returns
4260
+ -------
4261
+ tuple of (np.ndarray, np.ndarray, list[str]) or (np.ndarray, np.ndarray, list[str], np.ndarray)
4262
+ - `x1`: Array of shape `(tclen, count)` containing x-time series data.
4263
+ - `y1`: Array of shape `(tclen, count)` containing y-time series data.
4264
+ - `names`: List of file names that passed quality checks.
4265
+ - `bad1`: Optional array of shape `(tclen, count)` with bad point indicators if `usebadpts=True`.
4266
+
4267
+ Notes
4268
+ -----
4269
+ - Files with NaNs, short data, extreme standard deviations, or low correlation are excluded.
4270
+ - The function logs information about excluded files for debugging and quality control.
4271
+ - The `startskip` and `endskip` parameters are applied after filtering and before returning the data.
4272
+
4273
+ Examples
4274
+ --------
4275
+ >>> x, y, names = readindata(filelist, tclen=1000)
4276
+ >>> x, y, names, bad = readindata(filelist, tclen=1000, usebadpts=True)
4277
+ """
4278
+ LGR.info(
4279
+ "readindata called with usebadpts, startskip, endskip, readlim, readskip, targetfrag, inputfrag = "
4280
+ f"{usebadpts} {startskip} {endskip} {readlim} {readskip} {targetfrag} {inputfrag}"
4281
+ )
4282
+ # allocate target arrays
4283
+ LGR.info("allocating arrays")
4284
+ s = len(matchedfilelist[readskip:])
4285
+ if readlim is not None:
4286
+ if s > readlim:
4287
+ LGR.info(f"trimming read list to {readlim} from {s}")
4288
+ s = readlim
4289
+ x1 = np.zeros((tclen, s))
4290
+ y1 = np.zeros((tclen, s))
4291
+ names = []
4292
+ if usebadpts:
4293
+ bad1 = np.zeros((tclen, s))
4294
+
4295
+ # now read the data in
4296
+ count = 0
4297
+ LGR.info("checking data")
4298
+ lowcorrfiles = []
4299
+ nanfiles = []
4300
+ shortfiles = []
4301
+ strangemagfiles = []
4302
+ for i in range(readskip, readskip + s):
4303
+ lowcorrfound = False
4304
+ nanfound = False
4305
+ LGR.info(f"processing {matchedfilelist[i]}")
4306
+
4307
+ # read the info dict first
4308
+ infodict = tide_io.readdictfromjson(
4309
+ matchedfilelist[i].replace("_desc-stdrescardfromfmri_timeseries", "_info")
4310
+ )
4311
+ if infodict["corrcoeff_raw2pleth"] < corrthresh:
4312
+ lowcorrfound = True
4313
+ lowcorrfiles.append(matchedfilelist[i])
4314
+ (
4315
+ samplerate,
4316
+ starttime,
4317
+ columns,
4318
+ inputarray,
4319
+ compression,
4320
+ columnsource,
4321
+ ) = tide_io.readbidstsv(
4322
+ matchedfilelist[i],
4323
+ colspec="cardiacfromfmri_25.0Hz,normpleth",
4324
+ )
4325
+ tempy = inputarray[1, :]
4326
+ tempx = inputarray[0, :]
4327
+
4328
+ if np.any(np.isnan(tempy)):
4329
+ LGR.info(f"NaN found in file {matchedfilelist[i]} - discarding")
4330
+ nanfound = True
4331
+ nanfiles.append(matchedfilelist[i])
4332
+ if np.any(np.isnan(tempx)):
4333
+ nan_fname = targettoinput(
4334
+ matchedfilelist[i], targetfrag=targetfrag, inputfrag=inputfrag
4335
+ )
4336
+ LGR.info(f"NaN found in file {nan_fname} - discarding")
4337
+ nanfound = True
4338
+ nanfiles.append(nan_fname)
4339
+ strangefound = False
4340
+ if not (0.5 < np.std(tempx) < 20.0):
4341
+ strange_fname = matchedfilelist[i]
4342
+ LGR.info(
4343
+ f"file {strange_fname} has an extreme cardiacfromfmri standard deviation - discarding"
4344
+ )
4345
+ strangefound = True
4346
+ strangemagfiles.append(strange_fname)
4347
+ if not (0.5 < np.std(tempy) < 20.0):
4348
+ LGR.info(
4349
+ f"file {matchedfilelist[i]} has an extreme normpleth standard deviation - discarding"
4350
+ )
4351
+ strangefound = True
4352
+ strangemagfiles.append(matchedfilelist[i])
4353
+ shortfound = False
4354
+ ntempx = tempx.shape[0]
4355
+ ntempy = tempy.shape[0]
4356
+ if ntempx < tclen:
4357
+ short_fname = matchedfilelist[i]
4358
+ LGR.info(f"file {short_fname} is short - discarding")
4359
+ shortfound = True
4360
+ shortfiles.append(short_fname)
4361
+ if ntempy < tclen:
4362
+ LGR.info(f"file {matchedfilelist[i]} is short - discarding")
4363
+ shortfound = True
4364
+ shortfiles.append(matchedfilelist[i])
4365
+ if (
4366
+ (ntempx >= tclen)
4367
+ and (ntempy >= tclen)
4368
+ and (not nanfound)
4369
+ and (not shortfound)
4370
+ and (not strangefound)
4371
+ and (not lowcorrfound)
4372
+ ):
4373
+ x1[:tclen, count] = tempx[:tclen]
4374
+ y1[:tclen, count] = tempy[:tclen]
4375
+ names.append(matchedfilelist[i])
4376
+ if debug:
4377
+ print(f"{matchedfilelist[i]} included:")
4378
+ if usebadpts:
4379
+ bad1[:tclen, count] = inputarray[2, :]
4380
+ count += 1
4381
+ else:
4382
+ print(f"{matchedfilelist[i]} excluded:")
4383
+ if ntempx < tclen:
4384
+ print("\tx data too short")
4385
+ if ntempy < tclen:
4386
+ print("\ty data too short")
4387
+ print(f"\t{nanfound=}")
4388
+ print(f"\t{shortfound=}")
4389
+ print(f"\t{strangefound=}")
4390
+ print(f"\t{lowcorrfound=}")
4391
+ LGR.info(f"{count} runs pass file length check")
4392
+ if len(lowcorrfiles) > 0:
4393
+ LGR.info("files with low raw/pleth correlations:")
4394
+ for thefile in lowcorrfiles:
4395
+ LGR.info(f"\t{thefile}")
4396
+ if len(nanfiles) > 0:
4397
+ LGR.info("files with NaNs:")
4398
+ for thefile in nanfiles:
4399
+ LGR.info(f"\t{thefile}")
4400
+ if len(shortfiles) > 0:
4401
+ LGR.info("short files:")
4402
+ for thefile in shortfiles:
4403
+ LGR.info(f"\t{thefile}")
4404
+ if len(strangemagfiles) > 0:
4405
+ LGR.info("files with extreme standard deviations:")
4406
+ for thefile in strangemagfiles:
4407
+ LGR.info(f"\t{thefile}")
4408
+
4409
+ print(f"training set contains {count} runs of length {tclen}")
4410
+ if usebadpts:
4411
+ return (
4412
+ x1[startskip:-endskip, :count],
4413
+ y1[startskip:-endskip, :count],
4414
+ names[:count],
4415
+ bad1[startskip:-endskip, :count],
4416
+ )
4417
+ else:
4418
+ return (
4419
+ x1[startskip:-endskip, :count],
4420
+ y1[startskip:-endskip, :count],
4421
+ names[:count],
4422
+ )
4423
+
4424
+
4425
+ def prep(
4426
+ window_size: int,
4427
+ step: int = 1,
4428
+ excludethresh: float = 4.0,
4429
+ usebadpts: bool = False,
4430
+ startskip: int = 200,
4431
+ endskip: int = 200,
4432
+ excludebysubject: bool = True,
4433
+ thesuffix: str = "sliceres",
4434
+ thedatadir: str = "/data/frederic/physioconn/output_2025",
4435
+ inputfrag: str = "abc",
4436
+ targetfrag: str = "xyz",
4437
+ corrthresh: float = 0.5,
4438
+ dofft: bool = False,
4439
+ readlim: int | None = None,
4440
+ readskip: int | None = None,
4441
+ countlim: int | None = None,
4442
+ debug: bool = False,
4443
+ ) -> (
4444
+ tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, int, int, int]
4445
+ | tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray, int, int, int, np.ndarray, np.ndarray]
4446
+ ):
4447
+ """
4448
+ Prepare time-series data for training and validation by reading, normalizing,
4449
+ windowing, and splitting into batches.
4450
+
4451
+ This function reads physiological time-series data from JSON files, normalizes
4452
+ the data, and organizes it into overlapping windows for model training and
4453
+ validation. It supports filtering by subject or by window, and can optionally
4454
+ apply FFT transformations to the data.
4455
+
4456
+ Parameters
4457
+ ----------
4458
+ window_size : int
4459
+ Size of the sliding window used to segment time series data.
4460
+ step : int, optional
4461
+ Step size for sliding window (default is 1).
4462
+ excludethresh : float, optional
4463
+ Threshold for excluding data points based on maximum absolute value
4464
+ (default is 4.0).
4465
+ usebadpts : bool, optional
4466
+ If True, includes bad points in the data processing (default is False).
4467
+ startskip : int, optional
4468
+ Number of time points to skip at the beginning of each time series
4469
+ (default is 200).
4470
+ endskip : int, optional
4471
+ Number of time points to skip at the end of each time series
4472
+ (default is 200).
4473
+ excludebysubject : bool, optional
4474
+ If True, exclude subjects with any region exceeding `excludethresh`;
4475
+ otherwise, exclude windows (default is True).
4476
+ thesuffix : str, optional
4477
+ Suffix used in file search pattern (default is "sliceres").
4478
+ thedatadir : str, optional
4479
+ Directory path where the data files are stored (default is
4480
+ "/data/frederic/physioconn/output_2025").
4481
+ inputfrag : str, optional
4482
+ Fragment identifier for input data (default is "abc").
4483
+ targetfrag : str, optional
4484
+ Fragment identifier for target data (default is "xyz").
4485
+ corrthresh : float, optional
4486
+ Correlation threshold for data filtering (default is 0.5).
4487
+ dofft : bool, optional
4488
+ If True, apply FFT transformation to the data (default is False).
4489
+ readlim : int, optional
4490
+ Limit on number of time points to read (default is None).
4491
+ readskip : int, optional
4492
+ Number of time points to skip when reading data (default is None).
4493
+ countlim : int, optional
4494
+ Maximum number of subjects to include (default is None).
4495
+ debug : bool, optional
4496
+ If True, enable debug logging (default is False).
4497
+
4498
+ Returns
4499
+ -------
4500
+ tuple of (np.ndarray, np.ndarray, np.ndarray, np.ndarray, int, int, int)
4501
+ If `dofft` is False:
4502
+ - train_x : Training input data (shape: [n_windows, window_size, 1])
4503
+ - train_y : Training target data (shape: [n_windows, window_size, 1])
4504
+ - val_x : Validation input data (shape: [n_windows, window_size, 1])
4505
+ - val_y : Validation target data (shape: [n_windows, window_size, 1])
4506
+ - N_subjs : Number of subjects
4507
+ - tclen : Total time points after skipping
4508
+ - batchsize : Number of windows per subject
4509
+
4510
+ tuple of (np.ndarray, np.ndarray, np.ndarray, np.ndarray, int, int, int, np.ndarray, np.ndarray)
4511
+ If `dofft` is True:
4512
+ - train_x : Training input data (shape: [n_windows, window_size, 2])
4513
+ - train_y : Training target data (shape: [n_windows, window_size, 2])
4514
+ - val_x : Validation input data (shape: [n_windows, window_size, 2])
4515
+ - val_y : Validation target data (shape: [n_windows, window_size, 2])
4516
+ - N_subjs : Number of subjects
4517
+ - tclen : Total time points after skipping
4518
+ - batchsize : Number of windows per subject
4519
+ - Xscale_fourier : Fourier scaling for input data
4520
+ - Yscale_fourier : Fourier scaling for target data
4521
+
4522
+ Notes
4523
+ -----
4524
+ - Data normalization is performed using median absolute deviation (MAD).
4525
+ - The function supports both window-based and subject-based exclusion strategies.
4526
+ - If `usebadpts` is True, bad points are included in the output arrays.
4527
+ - FFT transformations are applied using a helper function `filtscale`.
4528
+
4529
+ Examples
4530
+ --------
4531
+ >>> train_x, train_y, val_x, val_y, N_subjs, tclen, batchsize = prep(
4532
+ ... window_size=100,
4533
+ ... step=10,
4534
+ ... excludethresh=3.0,
4535
+ ... excludebysubject=True,
4536
+ ... dofft=False
4537
+ ... )
4538
+ """
4539
+ searchstring = os.path.join(thedatadir, "*", "*_desc-stdrescardfromfmri_timeseries.json")
4540
+
4541
+ # find matched files
4542
+ matchedfilelist, tclen = getmatchedtcs(
4543
+ searchstring,
4544
+ usebadpts=usebadpts,
4545
+ targetfrag=targetfrag,
4546
+ inputfrag=inputfrag,
4547
+ debug=debug,
4548
+ )
4549
+ # print("matchedfilelist", matchedfilelist)
4550
+ print("tclen", tclen)
4551
+
4552
+ # read in the data from the matched files
4553
+ print("about to read in data")
4554
+ if usebadpts:
4555
+ x, y, names, bad = readindata(
4556
+ matchedfilelist,
4557
+ tclen,
4558
+ corrthresh=corrthresh,
4559
+ targetfrag=targetfrag,
4560
+ inputfrag=inputfrag,
4561
+ usebadpts=True,
4562
+ startskip=startskip,
4563
+ endskip=endskip,
4564
+ readlim=readlim,
4565
+ readskip=readskip,
4566
+ )
4567
+ else:
4568
+ x, y, names = readindata(
4569
+ matchedfilelist,
4570
+ tclen,
4571
+ corrthresh=corrthresh,
4572
+ targetfrag=targetfrag,
4573
+ inputfrag=inputfrag,
4574
+ startskip=startskip,
4575
+ endskip=endskip,
4576
+ readlim=readlim,
4577
+ readskip=readskip,
4578
+ )
4579
+ print("finished reading in data")
4580
+ LGR.info(f"xshape, yshape: {x.shape} {y.shape}")
4581
+
4582
+ # normalize input and output data
4583
+ LGR.info("normalizing data")
4584
+ LGR.info(f"count: {x.shape[1]}")
4585
+ if LGR.getEffectiveLevel() <= logging.DEBUG:
4586
+ # Only take these steps if the logger is set to DEBUG.
4587
+ for thesubj in range(x.shape[1]):
4588
+ LGR.debug(
4589
+ f"prenorm sub {thesubj} min, max, mean, std, MAD x, y: "
4590
+ f"{thesubj} "
4591
+ f"{np.min(x[:, thesubj])} {np.max(x[:, thesubj])} {np.mean(x[:, thesubj])} "
4592
+ f"{np.std(x[:, thesubj])} {mad(x[:, thesubj])} {np.min(y[:, thesubj])} "
4593
+ f"{np.max(y[:, thesubj])} {np.mean(y[:, thesubj])} {np.std(x[:, thesubj])} "
4594
+ f"{mad(y[:, thesubj])}"
4595
+ )
4596
+
4597
+ y -= np.mean(y, axis=0)
4598
+ themad = mad(y, axis=0)
4599
+ for thesubj in range(themad.shape[0]):
4600
+ if themad[thesubj] > 0.0:
4601
+ y[:, thesubj] /= themad[thesubj]
4602
+
4603
+ x -= np.mean(x, axis=0)
4604
+ themad = mad(x, axis=0)
4605
+ for thesubj in range(themad.shape[0]):
4606
+ if themad[thesubj] > 0.0:
4607
+ x[:, thesubj] /= themad[thesubj]
4608
+
4609
+ if LGR.getEffectiveLevel() <= logging.DEBUG:
4610
+ # Only take these steps if the logger is set to DEBUG.
4611
+ for thesubj in range(x.shape[1]):
4612
+ LGR.debug(
4613
+ f"postnorm sub {thesubj} min, max, mean, std, MAD x, y: "
4614
+ f"{thesubj} "
4615
+ f"{np.min(x[:, thesubj])} {np.max(x[:, thesubj])} {np.mean(x[:, thesubj])} "
4616
+ f"{np.std(x[:, thesubj])} {mad(x[:, thesubj])} {np.min(y[:, thesubj])} "
4617
+ f"{np.max(y[:, thesubj])} {np.mean(y[:, thesubj])} {np.std(x[:, thesubj])} "
4618
+ f"{mad(y[:, thesubj])}"
4619
+ )
4620
+
4621
+ # now decide what to keep and what to exclude
4622
+ thefabs = np.fabs(x)
4623
+ if not excludebysubject:
4624
+ N_pts = x.shape[0]
4625
+ N_subjs = x.shape[1]
4626
+ windowspersubject = np.int64((N_pts - window_size - 1) // step)
4627
+ LGR.info(
4628
+ f"{N_subjs} subjects with {N_pts} points will be evaluated with "
4629
+ f"{windowspersubject} windows per subject with step {step}"
4630
+ )
4631
+ usewindow = np.zeros(N_subjs * windowspersubject, dtype=np.int64)
4632
+ subjectstarts = np.zeros(N_subjs, dtype=np.int64)
4633
+ # check each window
4634
+ numgoodwindows = 0
4635
+ LGR.info("checking windows")
4636
+ subjectnames = []
4637
+ for subj in range(N_subjs):
4638
+ subjectstarts[subj] = numgoodwindows
4639
+ subjectnames.append(names[subj])
4640
+ LGR.info(f"{names[subj]} starts at {numgoodwindows}")
4641
+ for windownumber in range(windowspersubject):
4642
+ if (
4643
+ np.max(
4644
+ thefabs[
4645
+ step * windownumber : (step * windownumber + window_size),
4646
+ subj,
4647
+ ]
4648
+ )
4649
+ <= excludethresh
4650
+ ):
4651
+ usewindow[subj * windowspersubject + windownumber] = 1
4652
+ numgoodwindows += 1
4653
+ LGR.info(
4654
+ f"found {numgoodwindows} out of a potential {N_subjs * windowspersubject} "
4655
+ f"({100.0 * numgoodwindows / (N_subjs * windowspersubject)}%)"
4656
+ )
4657
+
4658
+ for subj in range(N_subjs):
4659
+ LGR.info(f"{names[subj]} starts at {subjectstarts[subj]}")
4660
+
4661
+ LGR.info("copying data into windows")
4662
+ Xb = np.zeros((numgoodwindows, window_size, 1))
4663
+ Yb = np.zeros((numgoodwindows, window_size, 1))
4664
+ if usebadpts:
4665
+ Xb_withbad = np.zeros((numgoodwindows, window_size, 1))
4666
+ LGR.info(f"dimensions of Xb: {Xb.shape}")
4667
+ thiswindow = 0
4668
+ for subj in range(N_subjs):
4669
+ for windownumber in range(windowspersubject):
4670
+ if usewindow[subj * windowspersubject + windownumber] == 1:
4671
+ Xb[thiswindow, :, 0] = x[
4672
+ step * windownumber : (step * windownumber + window_size), subj
4673
+ ]
4674
+ Yb[thiswindow, :, 0] = y[
4675
+ step * windownumber : (step * windownumber + window_size), subj
4676
+ ]
4677
+ if usebadpts:
4678
+ Xb_withbad[thiswindow, :, 0] = bad[
4679
+ step * windownumber : (step * windownumber + window_size),
4680
+ subj,
4681
+ ]
4682
+ thiswindow += 1
4683
+
4684
+ else:
4685
+ # now check for subjects that have regions that exceed the target
4686
+ themax = np.max(thefabs, axis=0)
4687
+
4688
+ cleansubjs = np.where(themax < excludethresh)[0]
4689
+
4690
+ totalcount = x.shape[1] + 0
4691
+ cleancount = len(cleansubjs)
4692
+ if countlim is not None:
4693
+ if cleancount > countlim:
4694
+ LGR.info(f"reducing count to {countlim} from {cleancount}")
4695
+ cleansubjs = cleansubjs[:countlim]
4696
+
4697
+ x = x[:, cleansubjs]
4698
+ y = y[:, cleansubjs]
4699
+ cleannames = []
4700
+ for theindex in cleansubjs:
4701
+ cleannames.append(names[theindex])
4702
+ if usebadpts:
4703
+ bad = bad[:, cleansubjs]
4704
+ subjectnames = cleannames
4705
+
4706
+ LGR.info(f"after filtering, shape of x is {x.shape}")
4707
+
4708
+ N_pts = y.shape[0]
4709
+ N_subjs = y.shape[1]
4710
+
4711
+ X = np.zeros((1, N_pts, N_subjs))
4712
+ Y = np.zeros((1, N_pts, N_subjs))
4713
+ if usebadpts:
4714
+ BAD = np.zeros((1, N_pts, N_subjs))
4715
+
4716
+ X[0, :, :] = x
4717
+ Y[0, :, :] = y
4718
+ if usebadpts:
4719
+ BAD[0, :, :] = bad
4720
+
4721
+ windowspersubject = int((N_pts - window_size - 1) // step)
4722
+ LGR.info(
4723
+ f"found {windowspersubject * cleancount} out of a potential "
4724
+ f"{windowspersubject * totalcount} "
4725
+ f"({100.0 * cleancount / totalcount}%)"
4726
+ )
4727
+ LGR.info(f"{windowspersubject} {cleancount} {totalcount}")
4728
+
4729
+ Xb = np.zeros((N_subjs * windowspersubject, window_size, 1))
4730
+ LGR.info(f"dimensions of Xb: {Xb.shape}")
4731
+ for j in range(N_subjs):
4732
+ LGR.info(
4733
+ f"sub {j} ({cleannames[j]}) min, max X, Y: "
4734
+ f"{j} {np.min(X[0, :, j])} {np.max(X[0, :, j])} {np.min(Y[0, :, j])} "
4735
+ f"{np.max(Y[0, :, j])}"
4736
+ )
4737
+ for i in range(windowspersubject):
4738
+ Xb[j * windowspersubject + i, :, 0] = X[0, step * i : (step * i + window_size), j]
4739
+
4740
+ Yb = np.zeros((N_subjs * windowspersubject, window_size, 1))
4741
+ LGR.info(f"dimensions of Yb: {Yb.shape}")
4742
+ for j in range(N_subjs):
4743
+ for i in range(windowspersubject):
4744
+ Yb[j * windowspersubject + i, :, 0] = Y[0, step * i : (step * i + window_size), j]
4745
+
4746
+ if usebadpts:
4747
+ Xb_withbad = np.zeros((N_subjs * windowspersubject, window_size, 2))
4748
+ LGR.info(f"dimensions of Xb_withbad: {Xb_withbad.shape}")
4749
+ for j in range(N_subjs):
4750
+ LGR.info(f"packing data for subject {j}")
4751
+ for i in range(windowspersubject):
4752
+ Xb_withbad[j * windowspersubject + i, :, 0] = X[
4753
+ 0, step * i : (step * i + window_size), j
4754
+ ]
4755
+ Xb_withbad[j * windowspersubject + i, :, 1] = BAD[
4756
+ 0, step * i : (step * i + window_size), j
4757
+ ]
4758
+ Xb = Xb_withbad
4759
+
4760
+ subjectstarts = [i * windowspersubject for i in range(N_subjs)]
4761
+ for subj in range(N_subjs):
4762
+ LGR.info(f"{names[subj]} starts at {subjectstarts[subj]}")
4763
+
4764
+ LGR.info(f"Xb.shape: {Xb.shape}")
4765
+ LGR.info(f"Yb.shape: {Yb.shape}")
4766
+
4767
+ if dofft:
4768
+ Xb_fourier = np.zeros((N_subjs * windowspersubject, window_size, 2))
4769
+ LGR.info(f"dimensions of Xb_fourier: {Xb_fourier.shape}")
4770
+ Xscale_fourier = np.zeros((N_subjs, windowspersubject))
4771
+ LGR.info(f"dimensions of Xscale_fourier: {Xscale_fourier.shape}")
4772
+ Yb_fourier = np.zeros((N_subjs * windowspersubject, window_size, 2))
4773
+ LGR.info(f"dimensions of Yb_fourier: {Yb_fourier.shape}")
4774
+ Yscale_fourier = np.zeros((N_subjs, windowspersubject))
4775
+ LGR.info(f"dimensions of Yscale_fourier: {Yscale_fourier.shape}")
4776
+ for j in range(N_subjs):
4777
+ LGR.info(f"transforming subject {j}")
4778
+ for i in range((N_pts - window_size - 1)):
4779
+ (
4780
+ Xb_fourier[j * windowspersubject + i, :, :],
4781
+ Xscale_fourier[j, i],
4782
+ ) = filtscale(X[0, step * i : (step * i + window_size), j])
4783
+ (
4784
+ Yb_fourier[j * windowspersubject + i, :, :],
4785
+ Yscale_fourier[j, i],
4786
+ ) = filtscale(Y[0, step * i : (step * i + window_size), j])
4787
+
4788
+ limit = np.int64(0.8 * Xb.shape[0])
4789
+ LGR.info(f"limit: {limit} out of {len(subjectstarts)}")
4790
+ # find nearest subject start
4791
+ firstvalsubject = np.abs(subjectstarts - limit).argmin()
4792
+ LGR.info(f"firstvalsubject: {firstvalsubject}")
4793
+ perm_train = np.random.permutation(np.int64(np.arange(subjectstarts[firstvalsubject])))
4794
+ perm_val = np.random.permutation(
4795
+ np.int64(np.arange(subjectstarts[firstvalsubject], Xb.shape[0]))
4796
+ )
4797
+
4798
+ LGR.info("training subjects:")
4799
+ for i in range(0, firstvalsubject):
4800
+ LGR.info(f"\t{i} {subjectnames[i]}")
4801
+ LGR.info("validation subjects:")
4802
+ for i in range(firstvalsubject, len(subjectstarts)):
4803
+ LGR.info(f"\t{i} {subjectnames[i]}")
4804
+
4805
+ perm = range(Xb.shape[0])
4806
+
4807
+ batchsize = windowspersubject
4808
+
4809
+ if dofft:
4810
+ train_x = Xb_fourier[perm[:limit], :, :]
4811
+ train_y = Yb_fourier[perm[:limit], :, :]
4812
+
4813
+ val_x = Xb_fourier[perm[limit:], :, :]
4814
+ val_y = Yb_fourier[perm[limit:], :, :]
4815
+ LGR.info(f"train, val dims: {train_x.shape} {train_y.shape} {val_x.shape} {val_y.shape}")
4816
+ return (
4817
+ train_x,
4818
+ train_y,
4819
+ val_x,
4820
+ val_y,
4821
+ N_subjs,
4822
+ tclen - startskip - endskip,
4823
+ batchsize,
4824
+ Xscale_fourier,
4825
+ Yscale_fourier,
4826
+ )
4827
+ else:
4828
+ train_x = Xb[perm_train, :, :]
4829
+ train_y = Yb[perm_train, :, :]
4830
+
4831
+ val_x = Xb[perm_val, :, :]
4832
+ val_y = Yb[perm_val, :, :]
4833
+
4834
+ LGR.info(f"train, val dims: {train_x.shape} {train_y.shape} {val_x.shape} {val_y.shape}")
4835
+ return (
4836
+ train_x,
4837
+ train_y,
4838
+ val_x,
4839
+ val_y,
4840
+ N_subjs,
4841
+ tclen - startskip - endskip,
4842
+ batchsize,
4843
+ )