File: time.py

package info (click to toggle)
scikit-rf 1.10.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 82,128 kB
  • sloc: python: 33,328; makefile: 130; sh: 19
file content (468 lines) | stat: -rw-r--r-- 16,109 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
"""
.. module:: skrf.time
========================================
time (:mod:`skrf.time`)
========================================

Time domain functions


.. autosummary::
   :toctree: generated/

   time_gate
   detect_span
   find_n_peaks
   indexes
   get_window

"""
from __future__ import annotations

import warnings
from collections.abc import Callable

# imports for type hinting
from typing import TYPE_CHECKING

import numpy as np
from numpy.fft import fft, fftshift, ifft, ifftshift, irfft, rfft
from scipy import signal
from scipy.ndimage import convolve1d

from .util import find_nearest_index

if TYPE_CHECKING:
    from .network import Network


def indexes(y: np.ndarray, thres: float = 0.3, min_dist: int = 1) -> np.ndarray:
    """
    Peak detection routine.

    Finds the numeric index of the peaks in *y* by taking its first order difference. By using
    *thres* and *min_dist* parameters, it is possible to reduce the number of
    detected peaks. *y* must be signed.

    Parameters
    ----------
    y : ndarray (signed)
        1D amplitude data to search for peaks.
    thres : float between [0., 1.], optional
        Normalized threshold. Only the peaks with amplitude higher than the
        threshold will be detected. Default is 0.3
    min_dist : int, optional
        Minimum distance between each detected peak. The peak with the highest
        amplitude is preferred to satisfy this constraint. Default is 1

    Returns
    -------
    ndarray
        Array containing the numeric indexes of the peaks that were detected

    Notes
    -----
    This function was taken from peakutils-1.1.0
    http://pythonhosted.org/PeakUtils/index.html

    """
    #This function was taken from peakutils, and is covered
    # by the MIT license, included below:

    #The MIT License (MIT)

    #Copyright (c) 2014 Lucas Hermann Negri

    #Permission is hereby granted, free of charge, to any person obtaining a copy
    #of this software and associated documentation files (the "Software"), to deal
    #in the Software without restriction, including without limitation the rights
    #to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
    #copies of the Software, and to permit persons to whom the Software is
    #furnished to do so, subject to the following conditions:

    #The above copyright notice and this permission notice shall be included in
    #all copies or substantial portions of the Software.

    #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    #OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    #THE SOFTWARE.

    if isinstance(y, np.ndarray) and np.issubdtype(y.dtype, np.unsignedinteger):
        raise ValueError("y must be signed")

    thres = thres * (np.max(y) - np.min(y)) + np.min(y)
    min_dist = int(min_dist)

    # compute first order difference
    dy = np.diff(y)

    # propagate left and right values successively to fill all plateau pixels (0-value)
    zeros, = np.where(dy == 0)

    # check if the singal is totally flat
    if len(zeros) == len(y) - 1:
        return np.array([])

    while len(zeros):
        # add pixels 2 by 2 to propagate left and right value onto the zero-value pixel
        zerosr = np.hstack([dy[1:], 0.])
        zerosl = np.hstack([0., dy[:-1]])

        # replace 0 with right value if non zero
        dy[zeros]=zerosr[zeros]
        zeros, = np.where(dy == 0)

        # replace 0 with left value if non zero
        dy[zeros] = zerosl[zeros]
        zeros, = np.where(dy == 0)

    # find the peaks by using the first order difference
    peaks = np.where((np.hstack([dy, 0.]) < 0.)
                     & (np.hstack([0., dy]) > 0.)
                     & (y > thres))[0]

    # handle multiple peaks, respecting the minimum distance
    if peaks.size > 1 and min_dist > 1:
        highest = peaks[np.argsort(y[peaks])][::-1]
        rem = np.ones(y.size, dtype=bool)
        rem[peaks] = False

        for peak in highest:
            if not rem[peak]:
                sl = slice(max(0, peak - min_dist), peak + min_dist + 1)
                rem[sl] = True
                rem[peak] = False

        peaks = np.arange(y.size)[~rem]

    return peaks


def find_n_peaks(x: np.ndarray, n: int, thres: float = 0.9, **kwargs) -> list[int]:
    """
    Find a given number of peaks in a signal.

    Parameters
    ----------
    x : np.ndarray
        signal
    n : int
        number of peaks to search for
    thres : float, optional
        threshold, default is 0.9
    **kwargs : optional keyword arguments passed to :func:`indexes`

    Returns
    -------
    peak_idxs : list of int
        List containing the numeric indexes of the peaks that were detected

    Raises
    ------
    ValueError
        If no peaks are found.
    """
    for _dummy in range(10):

        idx = indexes(x, **kwargs)
        if len(idx) < n:
            thres *= .5

        else:
            peak_vals = sorted(x[idx], reverse=True)[:n]
            peak_idxs = [x.tolist().index(k) for k in peak_vals]

            return peak_idxs
    raise ValueError('Couldnt find %i peaks' % n)

time_lookup_dict = {
    "s": 1,
    "ms": 1e-3,
    "us": 1e-6,
    "µs": 1e-6,
    "ns": 1e-9,
    "ps": 1e-12
}

def detect_span(ntwk: Network, t_unit: str = "") -> float:
    """
    Detect the correct time-span between two largest peaks.

    Parameters
    ----------
    ntwk : :class:`~skrf.network.Network`
        network to get data from

    t_unit : str
        Time unit for start, stop, center and span arguments, defaults to seconds (s).

        Possible values:
            * 's': seconds
            * 'ms': milliseconds
            * 'µs' or 'us': microseconds
            * 'ns': nanoseconds (default)
            * 'ps': picoseconds


    Returns
    -------
    span : float in unit t_unit
    """

    if t_unit == "":
        # Do not raise in autogate mode, where all parameters are None
        warnings.warn('''
                        Time unit not passed: uses 's' per default.
                        ''',
                        DeprecationWarning, stacklevel=2)
        t_unit = 's'

    x = ntwk.s_time_db.flatten()
    p1, p2 = find_n_peaks(x, n=2)
    # distance to nearest neighbor peak
    span = abs(ntwk.frequency.t[p1]-ntwk.frequency.t[p2])

    return span / time_lookup_dict[t_unit]

def get_window(window: str | tuple | Callable, Nx: int, **kwargs) -> np.ndarray:
    """Calls a custom window function or `scipy.signal.get_window()` depending on the window argument.

    Parameters
    ----------
    window : str, tuple or Callable
        The callable window function or a valid window string
        for `scipy.signal.get_window()`.
    Nx : int
        Number of samples

    Returns
    -------
    window : np.ndarray
        Window samples.
    """

    if callable(window):
        return window(Nx, **kwargs)
    else:
        return signal.get_window(window, Nx=Nx, **kwargs)

def time_gate(ntwk: Network, start: float = None, stop: float = None, center: float = None, span: float = None,
              mode: str = 'bandpass', window=('kaiser', 6),
              method: str ='fft', fft_window: str='cosine', conv_mode: str='wrap', t_unit: str = "") -> Network:
    """
    Time-domain gating of one-port s-parameters with a window function from scipy.signal.windows.

    The gate can be defined with start/stop times, or by center/span. All times are in units of seconds but
    can be changed using the `t_unit` parameter.
    Common windows are:

    * ('kaiser', 6)
    * 6 # integers are interpreted as kaiser beta-values
    * 'hamming'
    * 'boxcar'  # a straight up rect
    * Callable function which accepts integer with window size as argument

    If no parameters are passed this will try to auto-gate the largest
    peak.

    Parameters
    ----------
    ntwk : :class:`~skrf.network.Network`
        network to operate on
    start : number, or None
        start of time gate in t_unit.
    stop : number, or None
        stop of time gate in t_unit.
    center : number, or None
        center of time gate, in t_unit. If None, and span is given,
        the gate will be centered on the peak.
    span : number, or None
        span of time gate, in t_unit.  If None span will be half of the
        distance to the second tallest peak
    mode : ['bandpass', 'bandstop']
        mode of gate
    window : string, float, or tuple
        passed to `window` arg of `scipy.signal.get_window()` or callable function
    method : str
        Gating method. There are 3 option: 'convolution', 'fft', 'rfft'.

        With *'convolution'*, the time-domain gate gets transformed into frequency-domain using inverse FFT and the
        gating is then achieved by convolution with the frequency-domain data.

        With *'fft'* (default), the data gets transformed into time-domain using inverse FFT and the gating is achieved
        by multiplication with the time-domain gate. The gated time-domain signal is then transformed back into
        frequency-domain using FFT. As only positive signal frequencies are considered for the inverse FFT
        (with or without a dc component), the resulting time-domain signal has the same number of samples as in the
        frequency-domain, but is complex-valued. This method is also know as *time-domain band-pass mode*.

        With *'rfft'*, the procedure is the same as with *'fft'*, but the inverse FFT uses a complex-conjugate copy of
        the positive signal frequencies for the negative frequencies (Hermitian frequency response). A dc sample is
        also required. The resulting time-domain signal is real-valued and has twice the number of samples, which gives
        an improved time resolution. This method is also known as *time-domain low-pass mode*.

    fft_window : str or tuple or None
        Frequency-domain window applied before the inverse FFT in case of the (R)FFT method.
        This parameter takes the same values as the `window` parameter.
        Example: `window='hann` (default), or `window=('kaiser', 5)`, or `window=None`.
        The window helps to remove artefacts such as time-domain sidelobes of the pulses, but it is a trade-off with
        the achievable pulse width. The window is removed when the gated time-domain signals is transformed back into
        frequency-domain.

    conv_mode : str
        Extension mode for the convolution (if selected) determining how the frequency-domain gate is extended beyond
        the boundaries. This has a large effect on the generation of gating artefacts due to boundary effects. The
        optimal mode depends on the data. See the parameter description of `scipy.ndimage.convolve1d` for the available
        options.

    t_unit : str
        Time unit for start, stop, center and span arguments, defaults to seconds (s).

        Possible values:
            * 's': seconds (default)
            * 'ms': milliseconds
            * 'µs' or 'us': microseconds
            * 'ns': nanoseconds
            * 'ps': picoseconds


    Note
    ----
    You can't gate things that are 'behind' strong reflections. This
    is due to the multiple reflections that occur.

    If you need to time-gate an N-port network, then you should
    gate each s-parameter independently.

    Returns
    -------
    ntwk : Network
        copy of ntwk with time-gated s-parameters

    .. warning::
        Depending on sharpness of the gate, the band edges may be
        inaccurate, due to properties of FFT. We do not re-normalize
        anything.
    """

    if ntwk.nports >1:
        raise ValueError('Time-gating only works on one-ports. Try passing `ntwk.s11` or `ntwk.s21`.')

    if t_unit == "":
        if not all([e is None for e in [start, stop, center, span]]):
            # Do not raise in autogate mode, where all parameters are None
            warnings.warn('''
                            Time unit not passed: uses 's' per default.
                            ''',
                            DeprecationWarning, stacklevel=2)
        t_unit = 's'

    t_mult = time_lookup_dict[t_unit]

    if start is not None and stop is not None:
        start *= t_mult
        stop *= t_mult
        span = abs(stop-start)
        center = (stop+start)/2.

    else:
        if center is None:
            # they didn't provide center, so find the peak
            n = ntwk.s_time_mag.argmax()
            center = ntwk.frequency.t_ns[n]

        if span is None:
            span = detect_span(ntwk, t_unit='ns')

        center *= t_mult
        span *= t_mult
        start = center - span / 2.
        stop = center + span / 2.

    ntwk_gated = ntwk.copy()
    method = method.lower()
    n_fd = ntwk.frequency.npoints
    df = ntwk.frequency.step

    if method == 'convolution':
        # frequency-domain gating
        n_td = n_fd
        # create dummy-window
        window_fd = np.ones(n_fd)

    elif method == 'fft':
        # time-domain band-pass mode
        n_td = n_fd
        if fft_window is not None:
            # create band-pass window (zero on both lower and upper limit, one at center)
            window_fd = get_window(fft_window, n_fd)
        else:
            # create dummy-window
            window_fd = np.ones(n_fd)

    elif method == 'rfft':
        # time-domain low-pass mode
        if ntwk.f[0] > 0.0:
            # no dc point included
            warnings.warn('The network data to be gated does not contain the dc point (0 Hz). This is required for the '
                          'selected low-pass gating mode. Please consider to include the dc point if the results are '
                          'inaccurate, either by direct measurement of by extrapolation using '
                          'skrf.Network.extrapolate_to_dc().', UserWarning, stacklevel=2)
        n_td = 2 * n_fd - 1
        if fft_window is not None:
            # create low-pass window (one at lower limit at f=0, zero on upper limit)
            window_fd = get_window(fft_window, 2 * n_fd)
            window_fd = window_fd[n_fd:]
        else:
            # create dummy-window
            window_fd = np.ones(n_fd)

    else:
        raise ValueError(f'Invalid parameter method=`{method}`')

    # apply frequency-domain window
    ntwk_gated.s[:, 0, 0] = ntwk_gated.s[:, 0, 0] * window_fd

    # create time vector
    t = np.fft.fftshift(np.fft.fftfreq(n_td, df))
    # find start/stop gate indices
    start_idx = find_nearest_index(t, start)
    stop_idx = find_nearest_index(t, stop)

    # create gating window
    window_width = abs(stop_idx - start_idx) + 1
    window = get_window(window, window_width)

    # create the gate by padding the window with zeros
    gate = np.zeros_like(t)
    gate[start_idx:stop_idx+1] = window

    if method == 'convolution':
        # frequency-domain gating
        kernel = fftshift(fft(ifftshift(gate), norm='forward'))
        ntwk_gated.s[:, 0, 0] = convolve1d(ntwk_gated.s[:, 0, 0], kernel, mode=conv_mode)
    elif method == 'fft':
        # time-domain band-pass mode
        s_td = fftshift(ifft(ntwk_gated.s[:, 0, 0]))
        s_td_g = s_td * gate
        ntwk_gated.s[:, 0, 0] = fft(ifftshift(s_td_g))
    elif method == 'rfft':
        # time-domain low-pass mode
        s_td = fftshift(irfft(ntwk_gated.s[:, 0, 0], n=len(t)))
        s_td_g = s_td * gate
        ntwk_gated.s[:, 0, 0] = rfft(ifftshift(s_td_g))

    # remove frequency-domain window
    ntwk_gated.s[:, 0, 0] = ntwk_gated.s[:, 0, 0] / window_fd

    if mode == 'bandstop':
        ntwk_gated = ntwk - ntwk_gated
    elif mode == 'bandpass':
        pass
    else:
        raise ValueError('mode should be \'bandpass\' or \'bandstop\'')

    return ntwk_gated