2022-09-18 16:03:52 +00:00
|
|
|
# NanoVNASaver
|
|
|
|
#
|
|
|
|
# A python program to view and export Touchstone data from a NanoVNA
|
|
|
|
# Copyright (C) 2019, 2020 Rune B. Broberg
|
|
|
|
# Copyright (C) 2020ff NanoVNA-Saver Authors
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
#
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU General Public License for more details.
|
|
|
|
#
|
|
|
|
# You should have received a copy of the GNU General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
import itertools as it
|
2022-09-19 17:21:22 +00:00
|
|
|
import math
|
|
|
|
from typing import Callable, List, Tuple
|
2022-09-18 16:03:52 +00:00
|
|
|
|
|
|
|
import numpy as np
|
2023-03-08 08:40:39 +00:00
|
|
|
|
2022-10-08 07:25:42 +00:00
|
|
|
# pylint: disable=import-error, no-name-in-module
|
2022-10-06 16:15:59 +00:00
|
|
|
from scipy.signal import find_peaks
|
2022-09-18 16:03:52 +00:00
|
|
|
|
2022-09-20 17:52:34 +00:00
|
|
|
from NanoVNASaver.RFTools import Datapoint
|
2022-09-18 16:03:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
def zero_crossings(data: List[float]) -> List[int]:
|
|
|
|
"""find zero crossings
|
|
|
|
|
|
|
|
Args:
|
|
|
|
data (List[float]): data list execute
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
List[int]: sorted indices of zero crossing points
|
|
|
|
"""
|
|
|
|
if not data:
|
|
|
|
return []
|
|
|
|
|
|
|
|
np_data = np.array(data)
|
|
|
|
|
|
|
|
# start with real zeros (ignore first and last element)
|
2023-03-08 08:40:39 +00:00
|
|
|
real_zeros = [
|
|
|
|
n for n in np.where(np_data == 0.0)[0] if n not in {0, np_data.size - 1}
|
|
|
|
]
|
2022-09-18 16:03:52 +00:00
|
|
|
# now multipy elements to find change in signess
|
|
|
|
crossings = [
|
|
|
|
n if abs(np_data[n]) < abs(np_data[n + 1]) else n + 1
|
|
|
|
for n in np.where((np_data[:-1] * np_data[1:]) < 0.0)[0]
|
|
|
|
]
|
|
|
|
return sorted(real_zeros + crossings)
|
|
|
|
|
|
|
|
|
|
|
|
def maxima(data: List[float], threshold: float = 0.0) -> List[int]:
|
|
|
|
"""maxima
|
|
|
|
|
|
|
|
Args:
|
|
|
|
data (List[float]): data list to execute
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
List[int]: indices of maxima
|
|
|
|
"""
|
2023-03-08 08:40:39 +00:00
|
|
|
peaks = find_peaks(data, width=2, distance=3, prominence=1)[0].tolist()
|
|
|
|
return [i for i in peaks if data[i] > threshold] if threshold else peaks
|
2022-09-18 16:03:52 +00:00
|
|
|
|
|
|
|
|
|
|
|
def minima(data: List[float], threshold: float = 0.0) -> List[int]:
|
|
|
|
"""minima
|
|
|
|
|
|
|
|
Args:
|
|
|
|
data (List[float]): data list to execute
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
List[int]: indices of minima
|
|
|
|
"""
|
2023-03-08 08:40:39 +00:00
|
|
|
bottoms = find_peaks(-np.array(data), width=2, distance=3, prominence=1)[
|
|
|
|
0
|
|
|
|
].tolist()
|
|
|
|
return [i for i in bottoms if data[i] < threshold] if threshold else bottoms
|
2022-09-18 16:03:52 +00:00
|
|
|
|
|
|
|
|
2023-03-08 08:40:39 +00:00
|
|
|
def take_from_idx(
|
|
|
|
data: List[float], idx: int, predicate: Callable
|
|
|
|
) -> List[int]:
|
2022-09-18 16:03:52 +00:00
|
|
|
"""take_from_center
|
|
|
|
|
|
|
|
Args:
|
|
|
|
data (List[float]): data list to execute
|
|
|
|
idx (int): index of a start position
|
|
|
|
predicate (Callable): predicate on which elements to take
|
|
|
|
from center. (e.g. lambda i: i[1] < threshold)
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
List[int]: indices of element matching predicate left
|
|
|
|
and right from index
|
|
|
|
"""
|
2023-03-08 08:40:39 +00:00
|
|
|
lower = list(
|
|
|
|
reversed(
|
|
|
|
[
|
|
|
|
i
|
|
|
|
for i, _ in it.takewhile(
|
|
|
|
predicate, reversed(list(enumerate(data[:idx])))
|
|
|
|
)
|
|
|
|
]
|
|
|
|
)
|
|
|
|
)
|
|
|
|
upper = [i for i, _ in it.takewhile(predicate, enumerate(data[idx:], idx))]
|
2022-09-18 16:03:52 +00:00
|
|
|
return lower + upper
|
|
|
|
|
|
|
|
|
2023-03-08 08:40:39 +00:00
|
|
|
def center_from_idx(gains: List[float], idx: int, delta: float = 3.0) -> int:
|
2022-09-18 16:03:52 +00:00
|
|
|
"""find maximum from index postion of gains in a attn dB gain span
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gains (List[float]): gain values
|
|
|
|
idx (int): start position to search from
|
2022-09-21 15:45:08 +00:00
|
|
|
delta (float, optional): max gain delta from start. Defaults to 3.0.
|
2022-09-18 16:03:52 +00:00
|
|
|
|
|
|
|
Returns:
|
|
|
|
int: position of highest gain from start in range (-1 if no data)
|
|
|
|
"""
|
|
|
|
peak_db = gains[idx]
|
2023-03-08 08:40:39 +00:00
|
|
|
rng = take_from_idx(gains, idx, lambda i: abs(peak_db - i[1]) < delta)
|
2022-09-18 16:03:52 +00:00
|
|
|
return max(rng, key=lambda i: gains[i]) if rng else -1
|
|
|
|
|
|
|
|
|
2023-03-08 08:40:39 +00:00
|
|
|
def cut_off_left(
|
|
|
|
gains: List[float], idx: int, peak_gain: float, attn: float = 3.0
|
|
|
|
) -> int:
|
2022-09-21 15:45:08 +00:00
|
|
|
"""find first position in list where gain in attn lower then peak
|
|
|
|
left from index
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gains (List[float]): gain values
|
|
|
|
idx (int): start position to search from
|
|
|
|
peak_gain (float): reference gain value
|
|
|
|
attn (float, optional): attenuation to search position for.
|
|
|
|
Defaults to 3.0.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
int: position of attenuation point. (-1 if no data)
|
|
|
|
"""
|
2022-09-18 16:03:52 +00:00
|
|
|
return next(
|
2023-03-08 08:40:39 +00:00
|
|
|
(i for i in range(idx, -1, -1) if (peak_gain - gains[i]) > attn), -1
|
|
|
|
)
|
2022-09-18 16:03:52 +00:00
|
|
|
|
|
|
|
|
2023-03-08 08:40:39 +00:00
|
|
|
def cut_off_right(
|
|
|
|
gains: List[float], idx: int, peak_gain: float, attn: float = 3.0
|
|
|
|
) -> int:
|
2022-09-21 15:45:08 +00:00
|
|
|
"""find first position in list where gain in attn lower then peak
|
|
|
|
right from index
|
|
|
|
|
|
|
|
Args:
|
|
|
|
gains (List[float]): gain values
|
|
|
|
idx (int): start position to search from
|
|
|
|
peak_gain (float): reference gain value
|
|
|
|
attn (float, optional): attenuation to search position for.
|
|
|
|
Defaults to 3.0.
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
int: position of attenuation point. (-1 if no data)
|
|
|
|
"""
|
|
|
|
|
2022-09-18 16:03:52 +00:00
|
|
|
return next(
|
2023-03-08 08:40:39 +00:00
|
|
|
(i for i in range(idx, len(gains)) if (peak_gain - gains[i]) > attn), -1
|
|
|
|
)
|
2022-09-19 17:21:22 +00:00
|
|
|
|
|
|
|
|
2023-03-08 08:40:39 +00:00
|
|
|
def dip_cut_offs(
|
|
|
|
gains: List[float], peak_gain: float, attn: float = 3.0
|
|
|
|
) -> Tuple[int, int]:
|
2022-09-19 17:21:22 +00:00
|
|
|
rng = np.where(np.array(gains) < (peak_gain - attn))[0].tolist()
|
|
|
|
return (rng[0], rng[-1]) if rng else (math.nan, math.nan)
|
2022-09-20 17:52:34 +00:00
|
|
|
|
|
|
|
|
2023-03-08 08:40:39 +00:00
|
|
|
def calculate_rolloff(
|
|
|
|
s21: List[Datapoint], idx_1: int, idx_2: int
|
|
|
|
) -> Tuple[float, float]:
|
2022-09-20 17:52:34 +00:00
|
|
|
if idx_1 == idx_2:
|
|
|
|
return (math.nan, math.nan)
|
2022-09-21 15:45:08 +00:00
|
|
|
freq_1, freq_2 = s21[idx_1].freq, s21[idx_2].freq
|
|
|
|
gain_1, gain_2 = s21[idx_1].gain, s21[idx_2].gain
|
2022-09-20 17:52:34 +00:00
|
|
|
factor = freq_1 / freq_2 if freq_1 > freq_2 else freq_2 / freq_1
|
2022-09-21 15:45:08 +00:00
|
|
|
attn = abs(gain_1 - gain_2)
|
2022-09-20 17:52:34 +00:00
|
|
|
decade_attn = attn / math.log10(factor)
|
2022-09-21 15:45:08 +00:00
|
|
|
octave_attn = decade_attn * math.log10(2)
|
2022-09-20 17:52:34 +00:00
|
|
|
return (octave_attn, decade_attn)
|