# 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 . import itertools as it import math from typing import Callable, List, Tuple import numpy as np # pylint: disable=import-error, no-name-in-module from scipy.signal import find_peaks from NanoVNASaver.RFTools import Datapoint 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) real_zeros = [n for n in np.where(np_data == 0.0)[0] if n not in {0, np_data.size - 1}] # 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 """ 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 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 """ 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 def take_from_idx(data: List[float], idx: int, predicate: Callable) -> List[int]: """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 """ 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))] return lower + upper def center_from_idx(gains: List[float], idx: int, delta: float = 3.0) -> int: """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 delta (float, optional): max gain delta from start. Defaults to 3.0. Returns: int: position of highest gain from start in range (-1 if no data) """ peak_db = gains[idx] rng = take_from_idx(gains, idx, lambda i: abs(peak_db - i[1]) < delta) return max(rng, key=lambda i: gains[i]) if rng else -1 def cut_off_left(gains: List[float], idx: int, peak_gain: float, attn: float = 3.0) -> int: """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) """ return next( (i for i in range(idx, -1, -1) if (peak_gain - gains[i]) > attn), -1) def cut_off_right(gains: List[float], idx: int, peak_gain: float, attn: float = 3.0) -> int: """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) """ return next( (i for i in range(idx, len(gains)) if (peak_gain - gains[i]) > attn), -1) def dip_cut_offs(gains: List[float], peak_gain: float, attn: float = 3.0) -> Tuple[int, int]: rng = np.where(np.array(gains) < (peak_gain - attn))[0].tolist() return (rng[0], rng[-1]) if rng else (math.nan, math.nan) def calculate_rolloff(s21: List[Datapoint], idx_1: int, idx_2: int) -> Tuple[float, float]: if idx_1 == idx_2: return (math.nan, math.nan) freq_1, freq_2 = s21[idx_1].freq, s21[idx_2].freq gain_1, gain_2 = s21[idx_1].gain, s21[idx_2].gain factor = freq_1 / freq_2 if freq_1 > freq_2 else freq_2 / freq_1 attn = abs(gain_1 - gain_2) decade_attn = attn / math.log10(factor) octave_attn = decade_attn * math.log10(2) return (octave_attn, decade_attn)