1# NanoVNASaver 2# 3# A python program to view and export Touchstone data from a NanoVNA 4# Copyright (C) 2019, 2020 Rune B. Broberg 5# Copyright (C) 2020 NanoVNA-Saver Authors 6# 7# This program is free software: you can redistribute it and/or modify 8# it under the terms of the GNU General Public License as published by 9# the Free Software Foundation, either version 3 of the License, or 10# (at your option) any later version. 11# 12# This program is distributed in the hope that it will be useful, 13# but WITHOUT ANY WARRANTY; without even the implied warranty of 14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15# GNU General Public License for more details. 16# 17# You should have received a copy of the GNU General Public License 18# along with this program. If not, see <https://www.gnu.org/licenses/>. 19import logging 20import math 21 22from PyQt5 import QtWidgets 23 24from NanoVNASaver.Formatting import format_frequency 25 26from NanoVNASaver.Analysis import Analysis 27 28logger = logging.getLogger(__name__) 29 30 31class BandPassAnalysis(Analysis): 32 def __init__(self, app): 33 super().__init__(app) 34 35 self._widget = QtWidgets.QWidget() 36 37 layout = QtWidgets.QFormLayout() 38 self._widget.setLayout(layout) 39 layout.addRow(QtWidgets.QLabel("Band pass filter analysis")) 40 layout.addRow( 41 QtWidgets.QLabel( 42 f"Please place {self.app.markers[0].name} in the filter passband.")) 43 self.result_label = QtWidgets.QLabel() 44 self.lower_cutoff_label = QtWidgets.QLabel() 45 self.lower_six_db_label = QtWidgets.QLabel() 46 self.lower_sixty_db_label = QtWidgets.QLabel() 47 self.lower_db_per_octave_label = QtWidgets.QLabel() 48 self.lower_db_per_decade_label = QtWidgets.QLabel() 49 50 self.upper_cutoff_label = QtWidgets.QLabel() 51 self.upper_six_db_label = QtWidgets.QLabel() 52 self.upper_sixty_db_label = QtWidgets.QLabel() 53 self.upper_db_per_octave_label = QtWidgets.QLabel() 54 self.upper_db_per_decade_label = QtWidgets.QLabel() 55 layout.addRow("Result:", self.result_label) 56 57 layout.addRow(QtWidgets.QLabel("")) 58 59 self.center_frequency_label = QtWidgets.QLabel() 60 self.span_label = QtWidgets.QLabel() 61 self.six_db_span_label = QtWidgets.QLabel() 62 self.quality_label = QtWidgets.QLabel() 63 64 layout.addRow("Center frequency:", self.center_frequency_label) 65 layout.addRow("Bandwidth (-3 dB):", self.span_label) 66 layout.addRow("Quality factor:", self.quality_label) 67 layout.addRow("Bandwidth (-6 dB):", self.six_db_span_label) 68 69 layout.addRow(QtWidgets.QLabel("")) 70 71 layout.addRow(QtWidgets.QLabel("Lower side:")) 72 layout.addRow("Cutoff frequency:", self.lower_cutoff_label) 73 layout.addRow("-6 dB point:", self.lower_six_db_label) 74 layout.addRow("-60 dB point:", self.lower_sixty_db_label) 75 layout.addRow("Roll-off:", self.lower_db_per_octave_label) 76 layout.addRow("Roll-off:", self.lower_db_per_decade_label) 77 78 layout.addRow(QtWidgets.QLabel("")) 79 80 layout.addRow(QtWidgets.QLabel("Upper side:")) 81 layout.addRow("Cutoff frequency:", self.upper_cutoff_label) 82 layout.addRow("-6 dB point:", self.upper_six_db_label) 83 layout.addRow("-60 dB point:", self.upper_sixty_db_label) 84 layout.addRow("Roll-off:", self.upper_db_per_octave_label) 85 layout.addRow("Roll-off:", self.upper_db_per_decade_label) 86 87 def reset(self): 88 self.result_label.clear() 89 self.center_frequency_label.clear() 90 self.span_label.clear() 91 self.quality_label.clear() 92 self.six_db_span_label.clear() 93 94 self.upper_cutoff_label.clear() 95 self.upper_six_db_label.clear() 96 self.upper_sixty_db_label.clear() 97 self.upper_db_per_octave_label.clear() 98 self.upper_db_per_decade_label.clear() 99 100 self.lower_cutoff_label.clear() 101 self.lower_six_db_label.clear() 102 self.lower_sixty_db_label.clear() 103 self.lower_db_per_octave_label.clear() 104 self.lower_db_per_decade_label.clear() 105 106 def runAnalysis(self): 107 self.reset() 108 pass_band_location = self.app.markers[0].location 109 logger.debug("Pass band location: %d", pass_band_location) 110 111 if len(self.app.data21) == 0: 112 logger.debug("No data to analyse") 113 self.result_label.setText("No data to analyse.") 114 return 115 116 if pass_band_location < 0: 117 logger.debug("No location for %s", self.app.markers[0].name) 118 self.result_label.setText( 119 f"Please place {self.app.markers[0].name} in the passband.") 120 return 121 122 pass_band_db = self.app.data21[pass_band_location].gain 123 124 logger.debug("Initial passband gain: %d", pass_band_db) 125 126 initial_lower_cutoff_location = -1 127 for i in range(pass_band_location, -1, -1): 128 if (pass_band_db - self.app.data21[i].gain) > 3: 129 # We found a cutoff location 130 initial_lower_cutoff_location = i 131 break 132 133 if initial_lower_cutoff_location < 0: 134 self.result_label.setText("Lower cutoff location not found.") 135 return 136 137 initial_lower_cutoff_frequency = self.app.data21[initial_lower_cutoff_location].freq 138 139 logger.debug("Found initial lower cutoff frequency at %d", initial_lower_cutoff_frequency) 140 141 initial_upper_cutoff_location = -1 142 for i in range(pass_band_location, len(self.app.data21), 1): 143 if (pass_band_db - self.app.data21[i].gain) > 3: 144 # We found a cutoff location 145 initial_upper_cutoff_location = i 146 break 147 148 if initial_upper_cutoff_location < 0: 149 self.result_label.setText("Upper cutoff location not found.") 150 return 151 152 initial_upper_cutoff_frequency = self.app.data21[initial_upper_cutoff_location].freq 153 154 logger.debug("Found initial upper cutoff frequency at %d", initial_upper_cutoff_frequency) 155 156 peak_location = -1 157 peak_db = self.app.data21[initial_lower_cutoff_location].gain 158 for i in range(initial_lower_cutoff_location, initial_upper_cutoff_location, 1): 159 db = self.app.data21[i].gain 160 if db > peak_db: 161 peak_db = db 162 peak_location = i 163 164 logger.debug("Found peak of %f at %d", peak_db, self.app.data11[peak_location].freq) 165 166 lower_cutoff_location = -1 167 pass_band_db = peak_db 168 for i in range(peak_location, -1, -1): 169 if (pass_band_db - self.app.data21[i].gain) > 3: 170 # We found the cutoff location 171 lower_cutoff_location = i 172 break 173 174 lower_cutoff_frequency = self.app.data21[lower_cutoff_location].freq 175 lower_cutoff_gain = self.app.data21[lower_cutoff_location].gain - pass_band_db 176 177 if lower_cutoff_gain < -4: 178 logger.debug("Lower cutoff frequency found at %f dB" 179 " - insufficient data points for true -3 dB point.", 180 lower_cutoff_gain) 181 logger.debug("Found true lower cutoff frequency at %d", lower_cutoff_frequency) 182 183 self.lower_cutoff_label.setText( 184 f"{format_frequency(lower_cutoff_frequency)}" 185 f" ({round(lower_cutoff_gain, 1)} dB)") 186 187 self.app.markers[1].setFrequency(str(lower_cutoff_frequency)) 188 self.app.markers[1].frequencyInput.setText(str(lower_cutoff_frequency)) 189 190 upper_cutoff_location = -1 191 pass_band_db = peak_db 192 for i in range(peak_location, len(self.app.data21), 1): 193 if (pass_band_db - self.app.data21[i].gain) > 3: 194 # We found the cutoff location 195 upper_cutoff_location = i 196 break 197 198 upper_cutoff_frequency = self.app.data21[upper_cutoff_location].freq 199 upper_cutoff_gain = self.app.data21[upper_cutoff_location].gain - pass_band_db 200 if upper_cutoff_gain < -4: 201 logger.debug("Upper cutoff frequency found at %f dB" 202 " - insufficient data points for true -3 dB point.", 203 upper_cutoff_gain) 204 205 logger.debug("Found true upper cutoff frequency at %d", upper_cutoff_frequency) 206 207 self.upper_cutoff_label.setText( 208 f"{format_frequency(upper_cutoff_frequency)}" 209 f" ({round(upper_cutoff_gain, 1)} dB)") 210 self.app.markers[2].setFrequency(str(upper_cutoff_frequency)) 211 self.app.markers[2].frequencyInput.setText(str(upper_cutoff_frequency)) 212 213 span = upper_cutoff_frequency - lower_cutoff_frequency 214 center_frequency = math.sqrt( 215 lower_cutoff_frequency * upper_cutoff_frequency) 216 q = center_frequency / span 217 218 self.span_label.setText(format_frequency(span)) 219 self.center_frequency_label.setText( 220 format_frequency(center_frequency)) 221 self.quality_label.setText(str(round(q, 2))) 222 223 self.app.markers[0].setFrequency(str(round(center_frequency))) 224 self.app.markers[0].frequencyInput.setText(str(round(center_frequency))) 225 226 # Lower roll-off 227 228 lower_six_db_location = -1 229 for i in range(lower_cutoff_location, -1, -1): 230 if (pass_band_db - self.app.data21[i].gain) > 6: 231 # We found 6dB location 232 lower_six_db_location = i 233 break 234 235 if lower_six_db_location < 0: 236 self.result_label.setText("Lower 6 dB location not found.") 237 return 238 lower_six_db_cutoff_frequency = self.app.data21[lower_six_db_location].freq 239 self.lower_six_db_label.setText( 240 format_frequency(lower_six_db_cutoff_frequency)) 241 242 ten_db_location = -1 243 for i in range(lower_cutoff_location, -1, -1): 244 if (pass_band_db - self.app.data21[i].gain) > 10: 245 # We found 6dB location 246 ten_db_location = i 247 break 248 249 twenty_db_location = -1 250 for i in range(lower_cutoff_location, -1, -1): 251 if (pass_band_db - self.app.data21[i].gain) > 20: 252 # We found 6dB location 253 twenty_db_location = i 254 break 255 256 sixty_db_location = -1 257 for i in range(lower_six_db_location, -1, -1): 258 if (pass_band_db - self.app.data21[i].gain) > 60: 259 # We found 60dB location! Wow. 260 sixty_db_location = i 261 break 262 263 if sixty_db_location > 0: 264 if sixty_db_location > 0: 265 sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq 266 self.lower_sixty_db_label.setText( 267 format_frequency(sixty_db_cutoff_frequency)) 268 elif ten_db_location != -1 and twenty_db_location != -1: 269 ten = self.app.data21[ten_db_location].freq 270 twenty = self.app.data21[twenty_db_location].freq 271 sixty_db_frequency = ten * \ 272 10 ** (5 * (math.log10(twenty) - math.log10(ten))) 273 self.lower_sixty_db_label.setText( 274 f"{format_frequency(sixty_db_frequency)} (derived)") 275 else: 276 self.lower_sixty_db_label.setText("Not calculated") 277 278 if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location: 279 octave_attenuation, decade_attenuation = self.calculateRolloff( 280 ten_db_location, twenty_db_location) 281 self.lower_db_per_octave_label.setText( 282 str(round(octave_attenuation, 3)) + " dB / octave") 283 self.lower_db_per_decade_label.setText( 284 str(round(decade_attenuation, 3)) + " dB / decade") 285 else: 286 self.lower_db_per_octave_label.setText("Not calculated") 287 self.lower_db_per_decade_label.setText("Not calculated") 288 289 # Upper roll-off 290 291 upper_six_db_location = -1 292 for i in range(upper_cutoff_location, len(self.app.data21), 1): 293 if (pass_band_db - self.app.data21[i].gain) > 6: 294 # We found 6dB location 295 upper_six_db_location = i 296 break 297 298 if upper_six_db_location < 0: 299 self.result_label.setText("Upper 6 dB location not found.") 300 return 301 upper_six_db_cutoff_frequency = self.app.data21[upper_six_db_location].freq 302 self.upper_six_db_label.setText( 303 format_frequency(upper_six_db_cutoff_frequency)) 304 305 six_db_span = upper_six_db_cutoff_frequency - lower_six_db_cutoff_frequency 306 307 self.six_db_span_label.setText( 308 format_frequency(six_db_span)) 309 310 ten_db_location = -1 311 for i in range(upper_cutoff_location, len(self.app.data21), 1): 312 if (pass_band_db - self.app.data21[i].gain) > 10: 313 # We found 6dB location 314 ten_db_location = i 315 break 316 317 twenty_db_location = -1 318 for i in range(upper_cutoff_location, len(self.app.data21), 1): 319 if (pass_band_db - self.app.data21[i].gain) > 20: 320 # We found 6dB location 321 twenty_db_location = i 322 break 323 324 sixty_db_location = -1 325 for i in range(upper_six_db_location, len(self.app.data21), 1): 326 if (pass_band_db - self.app.data21[i].gain) > 60: 327 # We found 60dB location! Wow. 328 sixty_db_location = i 329 break 330 331 if sixty_db_location > 0: 332 sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq 333 self.upper_sixty_db_label.setText( 334 format_frequency(sixty_db_cutoff_frequency)) 335 elif ten_db_location != -1 and twenty_db_location != -1: 336 ten = self.app.data21[ten_db_location].freq 337 twenty = self.app.data21[twenty_db_location].freq 338 sixty_db_frequency = ten * \ 339 10 ** (5 * (math.log10(twenty) - math.log10(ten))) 340 self.upper_sixty_db_label.setText( 341 f"{format_frequency(sixty_db_frequency)} (derived)") 342 else: 343 self.upper_sixty_db_label.setText("Not calculated") 344 345 if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location: 346 octave_attenuation, decade_attenuation = self.calculateRolloff( 347 ten_db_location, twenty_db_location) 348 self.upper_db_per_octave_label.setText( 349 f"{round(octave_attenuation, 3)} dB / octave") 350 self.upper_db_per_decade_label.setText( 351 f"{round(decade_attenuation, 3)} dB / decade") 352 else: 353 self.upper_db_per_octave_label.setText("Not calculated") 354 self.upper_db_per_decade_label.setText("Not calculated") 355 356 if upper_cutoff_gain < -4 or lower_cutoff_gain < -4: 357 self.result_label.setText( 358 f"Analysis complete ({len(self.app.data11)} points)\n" 359 f"Insufficient data for analysis. Increase segment count.") 360 else: 361 self.result_label.setText( 362 f"Analysis complete ({len(self.app.data11)} points)") 363