pelmesha 0.0.1.dev1__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.
- pelmesha/__init__.py +8 -0
- pelmesha/align.py +412 -0
- pelmesha/loaders.py +488 -0
- pelmesha/pfeats.py +768 -0
- pelmesha/pspectra.py +3007 -0
- pelmesha/utilities.py +168 -0
- pelmesha-0.0.1.dev1.dist-info/METADATA +37 -0
- pelmesha-0.0.1.dev1.dist-info/RECORD +11 -0
- pelmesha-0.0.1.dev1.dist-info/WHEEL +5 -0
- pelmesha-0.0.1.dev1.dist-info/licenses/LICENSE.txt +202 -0
- pelmesha-0.0.1.dev1.dist-info/top_level.txt +1 -0
pelmesha/__init__.py
ADDED
pelmesha/align.py
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
"""Main alignment class"""
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
import typing as ty
|
|
5
|
+
import warnings
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .utilities import check_xy, convert_peak_values_to_index, generate_function, shift, time_loop
|
|
10
|
+
|
|
11
|
+
METHODS = ["pchip", "zero", "slinear", "quadratic", "cubic", "linear"]
|
|
12
|
+
LOGGER = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Aligner:
|
|
16
|
+
"""Main alignment class"""
|
|
17
|
+
|
|
18
|
+
_method, _gaussian_ratio, _gaussian_resolution, _gaussian_width, _n_iterations = None, None, None, None, None
|
|
19
|
+
_corr_sig_l, _corr_sig_x, _corr_sig_y, _reduce_range_factor, _scale_range = None, None, None, None, None
|
|
20
|
+
_search_space, _computed = None, False
|
|
21
|
+
|
|
22
|
+
def __init__(
|
|
23
|
+
self,
|
|
24
|
+
x: np.ndarray,
|
|
25
|
+
array: ty.Optional[np.ndarray],
|
|
26
|
+
peaks: ty.Iterable[float],
|
|
27
|
+
method: str = "quadratic",
|
|
28
|
+
width: float = 0.1,
|
|
29
|
+
ratio: float = 2.5,
|
|
30
|
+
resolution: int = 100,
|
|
31
|
+
iterations: int = 3,
|
|
32
|
+
grid_steps: int = 20,
|
|
33
|
+
shift_range: ty.Optional[ty.Tuple[int, int]] = None,
|
|
34
|
+
weights: ty.Optional[ty.List[float]] = None,
|
|
35
|
+
return_shifts: bool = False,
|
|
36
|
+
align_by_index: bool = False,
|
|
37
|
+
only_shift: bool = True,
|
|
38
|
+
):
|
|
39
|
+
"""Signal calibration and alignment by reference peaks
|
|
40
|
+
|
|
41
|
+
A simplified version of the MSALIGN function found in MATLAB (see references for link)
|
|
42
|
+
|
|
43
|
+
This version of the msalign function accepts most of the parameters that MATLAB's function accepts with the
|
|
44
|
+
following exceptions: GroupValue, ShowPlotValue. A number of other parameters is allowed, although they have
|
|
45
|
+
been renamed to comply with PEP8 conventions. The Python version is 8-60 times slower than the MATLAB
|
|
46
|
+
implementation, which is mostly caused by a really slow instantiation of the
|
|
47
|
+
`scipy.interpolate.PchipInterpolator` interpolator. In order to speed things up, I've also included several
|
|
48
|
+
other interpolation methods which are significantly faster and give similar results.
|
|
49
|
+
|
|
50
|
+
References
|
|
51
|
+
----------
|
|
52
|
+
Monchamp, P., Andrade-Cetto, L., Zhang, J.Y., and Henson, R. (2007) Signal Processing Methods for Mass
|
|
53
|
+
Spectrometry. In Systems Bioinformatics: An Engineering Case-Based Approach, G. Alterovitz and M.F. Ramoni, eds.
|
|
54
|
+
Artech House Publishers).
|
|
55
|
+
MSALIGN: https://nl.mathworks.com/help/bioinfo/ref/msalign.html
|
|
56
|
+
|
|
57
|
+
Parameters
|
|
58
|
+
----------
|
|
59
|
+
x : np.ndarray
|
|
60
|
+
1D array of separation units (N). The number of elements of xvals must equal the number of elements of
|
|
61
|
+
zvals.shape[1]
|
|
62
|
+
array : np.ndarray
|
|
63
|
+
2D array of intensities that must have common separation units (M x N) where M is the number of vectors
|
|
64
|
+
and N is number of points in the vector
|
|
65
|
+
peaks : list
|
|
66
|
+
list of reference peaks that must be found in the xvals vector
|
|
67
|
+
method : str
|
|
68
|
+
interpolation method. Default: 'cubic'. MATLAB version uses 'pchip' which is significantly slower in Python
|
|
69
|
+
weights: list (optional)
|
|
70
|
+
list of weights associated with the list of peaks. Must be the same length as list of peaks
|
|
71
|
+
width : float (optional)
|
|
72
|
+
width of the gaussian peak in separation units. Default: 10
|
|
73
|
+
ratio : float (optional)
|
|
74
|
+
scaling value that determines the size of the window around every alignment peak. The synthetic signal is
|
|
75
|
+
compared to the input signal within these regions. Default: 2.5
|
|
76
|
+
resolution : int (optional)
|
|
77
|
+
Default: 100
|
|
78
|
+
iterations : int (optional)
|
|
79
|
+
number of iterations. Increasing this value will (slightly) slow down the function but will improve
|
|
80
|
+
performance. Default: 5
|
|
81
|
+
grid_steps : int (optional)
|
|
82
|
+
number of steps to be used in the grid search. Default: 20
|
|
83
|
+
shift_range : list / numpy array (optional)
|
|
84
|
+
maximum allowed shifts. Default: [-100, 100]
|
|
85
|
+
only_shift : bool
|
|
86
|
+
determines if signal should be shifted (True) or rescaled (False). Default: True
|
|
87
|
+
return_shifts : bool
|
|
88
|
+
decide whether shift parameter `shift_opt` should also be returned. Default: False
|
|
89
|
+
align_by_index : bool
|
|
90
|
+
decide whether alignment should be done based on index rather than `xvals` array. Default: False
|
|
91
|
+
"""
|
|
92
|
+
self.x = np.asarray(x)
|
|
93
|
+
if array.ndim==1:
|
|
94
|
+
self.array = [array]
|
|
95
|
+
self.n_signals=1
|
|
96
|
+
else:
|
|
97
|
+
if array is not None:
|
|
98
|
+
self.array = check_xy(self.x, np.asarray(array))
|
|
99
|
+
else:
|
|
100
|
+
self.array = np.empty((0, len(self.x)))
|
|
101
|
+
|
|
102
|
+
self.n_signals = self.array.shape[0]
|
|
103
|
+
self.array_aligned = np.zeros_like(self.array)
|
|
104
|
+
self.peaks = list(peaks)
|
|
105
|
+
|
|
106
|
+
# set attributes
|
|
107
|
+
self.n_peaks = len(self.peaks)
|
|
108
|
+
|
|
109
|
+
# accessible attributes
|
|
110
|
+
self.scale_opt = np.ones((self.n_signals, 1), dtype=np.float32)
|
|
111
|
+
self.shift_opt = np.zeros((self.n_signals, 1), dtype=np.float32)
|
|
112
|
+
self.shift_values = np.zeros_like(self.shift_opt)
|
|
113
|
+
|
|
114
|
+
self.method = method
|
|
115
|
+
self.gaussian_ratio = ratio
|
|
116
|
+
self.gaussian_resolution = resolution
|
|
117
|
+
self.gaussian_width = width
|
|
118
|
+
self.n_iterations = iterations
|
|
119
|
+
self.grid_steps = grid_steps
|
|
120
|
+
if shift_range is None:
|
|
121
|
+
shift_range = [-100, 100]
|
|
122
|
+
self.shift_range = shift_range
|
|
123
|
+
if weights is None:
|
|
124
|
+
weights = np.ones(self.n_peaks)
|
|
125
|
+
self.weights = weights
|
|
126
|
+
|
|
127
|
+
# return shift vector
|
|
128
|
+
self._return_shifts = return_shifts
|
|
129
|
+
# If the number of points is equal to 1, then only shift
|
|
130
|
+
if self.n_peaks == 1:
|
|
131
|
+
only_shift = True
|
|
132
|
+
if only_shift and not align_by_index:
|
|
133
|
+
align_by_index = True
|
|
134
|
+
LOGGER.warning("Only computing shifts - changed `align_by_index` to `True`.")
|
|
135
|
+
|
|
136
|
+
# align signals by index rather than peak value
|
|
137
|
+
self._align_by_index = align_by_index
|
|
138
|
+
# align by index - rather than aligning to arbitrary non-integer values in the xvals, you can instead
|
|
139
|
+
# use index of those values
|
|
140
|
+
if self._align_by_index:
|
|
141
|
+
self.peaks = convert_peak_values_to_index(self.x, self.peaks)
|
|
142
|
+
self.x = np.arange(self.x.shape[0])
|
|
143
|
+
LOGGER.debug(f"Aligning by index - peak positions: {self.peaks}")
|
|
144
|
+
self._only_shift = only_shift
|
|
145
|
+
|
|
146
|
+
self._initialize()
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def method(self):
|
|
150
|
+
"""Interpolation method."""
|
|
151
|
+
return self._method
|
|
152
|
+
|
|
153
|
+
@method.setter
|
|
154
|
+
def method(self, value: str):
|
|
155
|
+
if value not in METHODS:
|
|
156
|
+
raise ValueError(f"Method `{value}` not found in the method options: {METHODS}")
|
|
157
|
+
self._method = value
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def gaussian_ratio(self):
|
|
161
|
+
"""Gaussian ratio."""
|
|
162
|
+
return self._gaussian_ratio
|
|
163
|
+
|
|
164
|
+
@gaussian_ratio.setter
|
|
165
|
+
def gaussian_ratio(self, value: float):
|
|
166
|
+
if value <= 0:
|
|
167
|
+
raise ValueError("Value of 'ratio' must be above 0!")
|
|
168
|
+
self._gaussian_ratio = value
|
|
169
|
+
|
|
170
|
+
@property
|
|
171
|
+
def gaussian_resolution(self):
|
|
172
|
+
"""Gaussian resolution of every Gaussian pulse (number of points)."""
|
|
173
|
+
return self._gaussian_resolution
|
|
174
|
+
|
|
175
|
+
@gaussian_resolution.setter
|
|
176
|
+
def gaussian_resolution(self, value: float):
|
|
177
|
+
if value <= 0:
|
|
178
|
+
raise ValueError("Value of 'resolution' must be above 0!")
|
|
179
|
+
self._gaussian_resolution = value
|
|
180
|
+
|
|
181
|
+
@property
|
|
182
|
+
def gaussian_width(self):
|
|
183
|
+
"""Width of the Gaussian pulse in std dev of the Gaussian pulses (in X)."""
|
|
184
|
+
return self._gaussian_width
|
|
185
|
+
|
|
186
|
+
@gaussian_width.setter
|
|
187
|
+
def gaussian_width(self, value: float):
|
|
188
|
+
self._gaussian_width = value
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def n_iterations(self):
|
|
192
|
+
"""Total number of iterations - increase to improve accuracy."""
|
|
193
|
+
return self._n_iterations
|
|
194
|
+
|
|
195
|
+
@n_iterations.setter
|
|
196
|
+
def n_iterations(self, value: int):
|
|
197
|
+
if value < 1 or not isinstance(value, int):
|
|
198
|
+
raise ValueError("Value of 'iterations' must be above 0 and be an integer!")
|
|
199
|
+
self._n_iterations = value
|
|
200
|
+
|
|
201
|
+
@property
|
|
202
|
+
def grid_steps(self):
|
|
203
|
+
"""Total number of iterations - increase to improve accuracy."""
|
|
204
|
+
return self._grid_steps
|
|
205
|
+
|
|
206
|
+
@grid_steps.setter
|
|
207
|
+
def grid_steps(self, value: int):
|
|
208
|
+
if value < 1 or not isinstance(value, int):
|
|
209
|
+
raise ValueError("Value of 'iterations' must be above 0 and be an integer!")
|
|
210
|
+
self._grid_steps = value
|
|
211
|
+
|
|
212
|
+
@property
|
|
213
|
+
def shift_range(self):
|
|
214
|
+
"""Total number of iterations - increase to improve accuracy."""
|
|
215
|
+
return self._shift_range
|
|
216
|
+
|
|
217
|
+
@shift_range.setter
|
|
218
|
+
def shift_range(self, value: ty.Tuple[float, float]):
|
|
219
|
+
if len(value) != 2:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
"Number of 'shift_values' is not correct. Shift range accepts" " numpy array with two values."
|
|
222
|
+
)
|
|
223
|
+
if np.diff(value) == 0:
|
|
224
|
+
raise ValueError("Values of 'shift_values' must not be the same!")
|
|
225
|
+
self._shift_range = np.asarray(value)
|
|
226
|
+
|
|
227
|
+
@property
|
|
228
|
+
def weights(self):
|
|
229
|
+
"""Total number of iterations - increase to improve accuracy."""
|
|
230
|
+
return self._weights
|
|
231
|
+
|
|
232
|
+
@weights.setter
|
|
233
|
+
def weights(self, value: ty.Optional[ty.Iterable[float]]):
|
|
234
|
+
if value is None:
|
|
235
|
+
value = np.ones(self.n_peaks)
|
|
236
|
+
if not isinstance(value, ty.Iterable):
|
|
237
|
+
raise ValueError("Weights must be provided as an iterable.")
|
|
238
|
+
if len(value) != self.n_peaks:
|
|
239
|
+
raise ValueError("Number of weights does not match the number of peaks.")
|
|
240
|
+
self._weights = np.asarray(value)
|
|
241
|
+
|
|
242
|
+
def _initialize(self):
|
|
243
|
+
"""Prepare dataset for alignment"""
|
|
244
|
+
# check that values for gaussian_width are valid
|
|
245
|
+
gaussian_widths = np.zeros((self.n_peaks, 1))
|
|
246
|
+
for i in range(self.n_peaks):
|
|
247
|
+
gaussian_widths[i] = self.gaussian_width
|
|
248
|
+
|
|
249
|
+
# set the synthetic target signal
|
|
250
|
+
corr_sig_x = np.zeros((self.gaussian_resolution + 1, self.n_peaks))
|
|
251
|
+
corr_sig_y = np.zeros((self.gaussian_resolution + 1, self.n_peaks))
|
|
252
|
+
|
|
253
|
+
gaussian_resolution_range = np.arange(0, self.gaussian_resolution + 1)
|
|
254
|
+
for i in range(self.n_peaks):
|
|
255
|
+
left_l = self.peaks[i] - self.gaussian_ratio * gaussian_widths[i] # noqa
|
|
256
|
+
right_l = self.peaks[i] + self.gaussian_ratio * gaussian_widths[i] # noqa
|
|
257
|
+
corr_sig_x[:, i] = left_l + (gaussian_resolution_range * (right_l - left_l) / self.gaussian_resolution)
|
|
258
|
+
corr_sig_y[:, i] = self.weights[i] * np.exp(
|
|
259
|
+
-np.square((corr_sig_x[:, i] - self.peaks[i]) / gaussian_widths[i]) # noqa
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
self._corr_sig_l = (self.gaussian_resolution + 1) * self.n_peaks
|
|
263
|
+
self._corr_sig_x = corr_sig_x.flatten("F")
|
|
264
|
+
self._corr_sig_y = corr_sig_y.flatten("F")
|
|
265
|
+
|
|
266
|
+
# set reduce_range_factor to take 5 points of the previous ranges or half of
|
|
267
|
+
# the previous range if grid_steps < 10
|
|
268
|
+
self._reduce_range_factor = min(0.5, 5 / self.grid_steps)
|
|
269
|
+
|
|
270
|
+
# set scl such that the maximum peak can shift no more than the limits imposed by shift when scaling
|
|
271
|
+
self._scale_range = 1 + self.shift_range / max(self.peaks)
|
|
272
|
+
|
|
273
|
+
if self._only_shift:
|
|
274
|
+
self._scale_range = np.array([1, 1])
|
|
275
|
+
|
|
276
|
+
# create the mesh-grid only once
|
|
277
|
+
mesh_a, mesh_b = np.meshgrid(
|
|
278
|
+
np.divide(np.arange(0, self.grid_steps), self.grid_steps - 1),
|
|
279
|
+
np.divide(np.arange(0, self.grid_steps), self.grid_steps - 1),
|
|
280
|
+
)
|
|
281
|
+
self._search_space = np.tile(
|
|
282
|
+
np.vstack([mesh_a.flatten(order="F"), mesh_b.flatten(order="F")]).T, [1, self._n_iterations]
|
|
283
|
+
)
|
|
284
|
+
|
|
285
|
+
def run(self, n_iterations: int = None):
|
|
286
|
+
"""Execute the alignment procedure for each signal in the 2D array and collate the shift/scale vectors"""
|
|
287
|
+
self.n_iterations = n_iterations or self.n_iterations
|
|
288
|
+
# iterate for every signal
|
|
289
|
+
t_start = time.time()
|
|
290
|
+
|
|
291
|
+
# main loop: searches for the optimum values of Scale and Shift factors by search over a multi-resolution
|
|
292
|
+
# grid, getting better at each iteration. Increasing the number of iterations improves the shift and scale
|
|
293
|
+
# parameters
|
|
294
|
+
for n_signal, y in enumerate(self.array):
|
|
295
|
+
self.shift_opt[n_signal], self.scale_opt[n_signal] = self.compute(y)
|
|
296
|
+
LOGGER.debug(f"Processed {self.n_signals} signals " + time_loop(t_start, self.n_signals + 1, self.n_signals))
|
|
297
|
+
self._computed = True
|
|
298
|
+
|
|
299
|
+
def compute(self, y: np.ndarray) -> ty.Tuple[float, float]:
|
|
300
|
+
"""Compute correction factors.
|
|
301
|
+
|
|
302
|
+
This function does not set value in any of the class attributes so can be used in a iterator where values
|
|
303
|
+
are computed lazily.
|
|
304
|
+
"""
|
|
305
|
+
_scale_range = np.array([-0.5, 0.5])
|
|
306
|
+
scale_opt, shift_opt = 0.0, 1.0
|
|
307
|
+
|
|
308
|
+
# set to back to the user input arguments (or default)
|
|
309
|
+
_shift = self.shift_range.copy()
|
|
310
|
+
_scale = self._scale_range.copy()
|
|
311
|
+
|
|
312
|
+
# generate interpolation function for each signal - instantiation of the interpolator can be quite slow,
|
|
313
|
+
# so you can slightly increase the number of iterations without significant slowdown of the process
|
|
314
|
+
func = generate_function(self.method, self.x, y)
|
|
315
|
+
|
|
316
|
+
# iterate to estimate the shift and scale - at each iteration, the grid search is readjusted and the
|
|
317
|
+
# shift/scale values are optimized further
|
|
318
|
+
for n_iter in range(self.n_iterations):
|
|
319
|
+
# scale and shift search space
|
|
320
|
+
scale_grid = _scale[0] + self._search_space[:, (n_iter * 2) - 2] * np.diff(_scale)
|
|
321
|
+
shift_grid = _shift[0] + self._search_space[:, (n_iter * 2) + 1] * np.diff(_shift)
|
|
322
|
+
temp = (
|
|
323
|
+
np.reshape(scale_grid, (scale_grid.shape[0], 1)) * np.reshape(self._corr_sig_x, (1, self._corr_sig_l))
|
|
324
|
+
+ np.tile(shift_grid, [self._corr_sig_l, 1]).T
|
|
325
|
+
)
|
|
326
|
+
# interpolate at each iteration. Need to remove NaNs which can be introduced by certain (e.g.
|
|
327
|
+
# PCHIP) interpolator
|
|
328
|
+
temp = np.nan_to_num(func(temp.flatten("C")).reshape(temp.shape))
|
|
329
|
+
|
|
330
|
+
# determine the best position
|
|
331
|
+
i_max = np.dot(temp, self._corr_sig_y).argmax()
|
|
332
|
+
|
|
333
|
+
# save optimum value
|
|
334
|
+
scale_opt = scale_grid[i_max]
|
|
335
|
+
shift_opt = shift_grid[i_max]
|
|
336
|
+
|
|
337
|
+
# readjust grid for next iteration_reduce_range_factor
|
|
338
|
+
_scale = scale_opt + _scale_range * np.diff(_scale) * self._reduce_range_factor
|
|
339
|
+
_shift = shift_opt + _scale_range * np.diff(_shift) * self._reduce_range_factor
|
|
340
|
+
return shift_opt, scale_opt
|
|
341
|
+
|
|
342
|
+
def apply(self, return_shifts: bool = None):
|
|
343
|
+
"""Align the signals against the computed values"""
|
|
344
|
+
if not self._computed:
|
|
345
|
+
warnings.warn("Aligning data without computing optimal alignment parameters", UserWarning)
|
|
346
|
+
self._return_shifts = return_shifts if return_shifts is not None else self._return_shifts
|
|
347
|
+
|
|
348
|
+
if self._only_shift:
|
|
349
|
+
self.shift()
|
|
350
|
+
else:
|
|
351
|
+
self.align()
|
|
352
|
+
|
|
353
|
+
# return aligned data and shifts
|
|
354
|
+
if self._return_shifts:
|
|
355
|
+
return self.array_aligned, self.shift_values
|
|
356
|
+
# only return data
|
|
357
|
+
return self.array_aligned
|
|
358
|
+
|
|
359
|
+
def align(self, shift_opt=None, scale_opt=None):
|
|
360
|
+
"""Realign array based on the optimized shift and scale parameters
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
shift_opt: Optional[np.ndarray]
|
|
365
|
+
vector containing values by which to shift the array
|
|
366
|
+
scale_opt : Optional[np.ndarray]
|
|
367
|
+
vector containing values by which to rescale the array
|
|
368
|
+
"""
|
|
369
|
+
t_start = time.time()
|
|
370
|
+
if shift_opt is None:
|
|
371
|
+
shift_opt = self.shift_opt
|
|
372
|
+
if scale_opt is None:
|
|
373
|
+
scale_opt = self.scale_opt
|
|
374
|
+
|
|
375
|
+
# realign based on provided values
|
|
376
|
+
for iteration, y in enumerate(self.array):
|
|
377
|
+
# interpolate back to the original domain
|
|
378
|
+
self.array_aligned[iteration] = self._apply(y, shift_opt[iteration], scale_opt[iteration])
|
|
379
|
+
self.shift_values = self.shift_opt
|
|
380
|
+
|
|
381
|
+
LOGGER.debug(f"Re-aligned {self.n_signals} signals " + time_loop(t_start, self.n_signals + 1, self.n_signals))
|
|
382
|
+
|
|
383
|
+
def _apply(self, y: np.ndarray, shift_value: float, scale_value: float):
|
|
384
|
+
"""Apply alignment correction to array `y`."""
|
|
385
|
+
func = generate_function(self.method, (self.x - shift_value) / scale_value, y)
|
|
386
|
+
return np.nan_to_num(func(self.x))
|
|
387
|
+
|
|
388
|
+
def shift(self, shift_opt=None):
|
|
389
|
+
"""Quickly shift array based on the optimized shift parameters.
|
|
390
|
+
|
|
391
|
+
This method does not interpolate but rather moves the data left and right without applying any scaling.
|
|
392
|
+
|
|
393
|
+
Parameters
|
|
394
|
+
----------
|
|
395
|
+
shift_opt: Optional[np.ndarray]
|
|
396
|
+
vector containing values by which to shift the array
|
|
397
|
+
"""
|
|
398
|
+
t_start = time.time()
|
|
399
|
+
if shift_opt is None:
|
|
400
|
+
shift_opt = np.round(self.shift_opt).astype(np.int32)
|
|
401
|
+
|
|
402
|
+
# quickly shift based on provided values
|
|
403
|
+
for iteration, y in enumerate(self.array):
|
|
404
|
+
self.array_aligned[iteration] = self._shift(y, shift_opt[iteration])
|
|
405
|
+
self.shift_values = shift_opt
|
|
406
|
+
|
|
407
|
+
LOGGER.debug(f"Re-aligned {self.n_signals} signals " + time_loop(t_start, self.n_signals + 1, self.n_signals))
|
|
408
|
+
|
|
409
|
+
@staticmethod
|
|
410
|
+
def _shift(y: np.ndarray, shift_value: float):
|
|
411
|
+
"""Apply shift correction to array `y`."""
|
|
412
|
+
return shift(y, -int(shift_value),fill_value=y[-shift_value])
|