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.Analysis import Analysis
25from NanoVNASaver.Formatting import format_frequency
26
27logger = logging.getLogger(__name__)
28
29
30class HighPassAnalysis(Analysis):
31    def __init__(self, app):
32        super().__init__(app)
33
34        self._widget = QtWidgets.QWidget()
35
36        layout = QtWidgets.QFormLayout()
37        self._widget.setLayout(layout)
38        layout.addRow(QtWidgets.QLabel("High pass filter analysis"))
39        layout.addRow(QtWidgets.QLabel(
40            f"Please place {self.app.markers[0].name} in the filter passband."))
41        self.result_label = QtWidgets.QLabel()
42        self.cutoff_label = QtWidgets.QLabel()
43        self.six_db_label = QtWidgets.QLabel()
44        self.sixty_db_label = QtWidgets.QLabel()
45        self.db_per_octave_label = QtWidgets.QLabel()
46        self.db_per_decade_label = QtWidgets.QLabel()
47        layout.addRow("Result:", self.result_label)
48        layout.addRow("Cutoff frequency:", self.cutoff_label)
49        layout.addRow("-6 dB point:", self.six_db_label)
50        layout.addRow("-60 dB point:", self.sixty_db_label)
51        layout.addRow("Roll-off:", self.db_per_octave_label)
52        layout.addRow("Roll-off:", self.db_per_decade_label)
53
54    def reset(self):
55        self.result_label.clear()
56        self.cutoff_label.clear()
57        self.six_db_label.clear()
58        self.sixty_db_label.clear()
59        self.db_per_octave_label.clear()
60        self.db_per_decade_label.clear()
61
62    def runAnalysis(self):
63        self.reset()
64        pass_band_location = self.app.markers[0].location
65        logger.debug("Pass band location: %d", pass_band_location)
66
67        if len(self.app.data21) == 0:
68            logger.debug("No data to analyse")
69            self.result_label.setText("No data to analyse.")
70            return
71
72        if pass_band_location < 0:
73            logger.debug("No location for %s", self.app.markers[0].name)
74            self.result_label.setText(
75                f"Please place {self.app.markers[0].name } in the passband.")
76            return
77
78        pass_band_db = self.app.data21[pass_band_location].gain
79
80        logger.debug("Initial passband gain: %d", pass_band_db)
81
82        initial_cutoff_location = -1
83        for i in range(pass_band_location, -1, -1):
84            db = self.app.data21[i].gain
85            if (pass_band_db - db) > 3:
86                # We found a cutoff location
87                initial_cutoff_location = i
88                break
89
90        if initial_cutoff_location < 0:
91            self.result_label.setText("Cutoff location not found.")
92            return
93
94        initial_cutoff_frequency = self.app.data21[initial_cutoff_location].freq
95
96        logger.debug("Found initial cutoff frequency at %d", initial_cutoff_frequency)
97
98        peak_location = -1
99        peak_db = self.app.data21[initial_cutoff_location].gain
100        for i in range(len(self.app.data21) - 1, initial_cutoff_location - 1, -1):
101            if self.app.data21[i].gain > peak_db:
102                peak_db = db
103                peak_location = i
104
105        logger.debug("Found peak of %f at %d", peak_db, self.app.data11[peak_location].freq)
106
107        self.app.markers[0].setFrequency(str(self.app.data21[peak_location].freq))
108        self.app.markers[0].frequencyInput.setText(str(self.app.data21[peak_location].freq))
109
110        cutoff_location = -1
111        pass_band_db = peak_db
112        for i in range(peak_location, -1, -1):
113            if (pass_band_db - self.app.data21[i].gain) > 3:
114                # We found the cutoff location
115                cutoff_location = i
116                break
117
118        cutoff_frequency = self.app.data21[cutoff_location].freq
119        cutoff_gain = self.app.data21[cutoff_location].gain - pass_band_db
120        if cutoff_gain < -4:
121            logger.debug("Cutoff frequency found at %f dB"
122                         " - insufficient data points for true -3 dB point.",
123                         cutoff_gain)
124        logger.debug("Found true cutoff frequency at %d", cutoff_frequency)
125
126        self.cutoff_label.setText(
127            f"{format_frequency(cutoff_frequency)}"
128            f" {round(cutoff_gain, 1)} dB)")
129        self.app.markers[1].setFrequency(str(cutoff_frequency))
130        self.app.markers[1].frequencyInput.setText(str(cutoff_frequency))
131
132        six_db_location = -1
133        for i in range(cutoff_location, -1, -1):
134            if (pass_band_db - self.app.data21[i].gain) > 6:
135                # We found 6dB location
136                six_db_location = i
137                break
138
139        if six_db_location < 0:
140            self.result_label.setText("6 dB location not found.")
141            return
142        six_db_cutoff_frequency = self.app.data21[six_db_location].freq
143        self.six_db_label.setText(
144            format_frequency(six_db_cutoff_frequency))
145
146        ten_db_location = -1
147        for i in range(cutoff_location, -1, -1):
148            if (pass_band_db - self.app.data21[i].gain) > 10:
149                # We found 6dB location
150                ten_db_location = i
151                break
152
153        twenty_db_location = -1
154        for i in range(cutoff_location, -1, -1):
155            if (pass_band_db - self.app.data21[i].gain) > 20:
156                # We found 6dB location
157                twenty_db_location = i
158                break
159
160        sixty_db_location = -1
161        for i in range(six_db_location, -1, -1):
162            if (pass_band_db - self.app.data21[i].gain) > 60:
163                # We found 60dB location! Wow.
164                sixty_db_location = i
165                break
166
167        if sixty_db_location > 0:
168            if sixty_db_location > 0:
169                sixty_db_cutoff_frequency = self.app.data21[sixty_db_location].freq
170                self.sixty_db_label.setText(
171                    format_frequency(sixty_db_cutoff_frequency))
172            elif ten_db_location != -1 and twenty_db_location != -1:
173                ten = self.app.data21[ten_db_location].freq
174                twenty = self.app.data21[twenty_db_location].freq
175                sixty_db_frequency = ten * 10 ** (5 * (math.log10(twenty) - math.log10(ten)))
176                self.sixty_db_label.setText(
177                    f"{format_frequency(sixty_db_frequency)} (derived)")
178            else:
179                self.sixty_db_label.setText("Not calculated")
180
181        if ten_db_location > 0 and twenty_db_location > 0 and ten_db_location != twenty_db_location:
182            octave_attenuation, decade_attenuation = self.calculateRolloff(
183                ten_db_location, twenty_db_location)
184            self.db_per_octave_label.setText(str(round(octave_attenuation, 3)) + " dB / octave")
185            self.db_per_decade_label.setText(str(round(decade_attenuation, 3)) + " dB / decade")
186        else:
187            self.db_per_octave_label.setText("Not calculated")
188            self.db_per_decade_label.setText("Not calculated")
189
190        self.result_label.setText(f"Analysis complete ({len(self.app.data11)}) points)")
191