consolidated TDR bandpass/lowpass mode, configuration dialog for TDR
This commit is contained in:
parent
2039c8f74d
commit
b8ccca5ebc
@ -26,10 +26,10 @@ HEADERS += \
|
||||
Tools/eseries.h \
|
||||
Tools/impedancematchdialog.h \
|
||||
Traces/Math/medianfilter.h \
|
||||
Traces/Math/tdrbandpass.h \
|
||||
Traces/Math/tdrlowpass.h \
|
||||
Traces/Math/tdr.h \
|
||||
Traces/Math/tracemath.h \
|
||||
Traces/Math/tracematheditdialog.h \
|
||||
Traces/Math/windowfunction.h \
|
||||
Traces/fftcomplex.h \
|
||||
Traces/markerwidget.h \
|
||||
Traces/trace.h \
|
||||
@ -83,10 +83,10 @@ SOURCES += \
|
||||
Tools/eseries.cpp \
|
||||
Tools/impedancematchdialog.cpp \
|
||||
Traces/Math/medianfilter.cpp \
|
||||
Traces/Math/tdrbandpass.cpp \
|
||||
Traces/Math/tdrlowpass.cpp \
|
||||
Traces/Math/tdr.cpp \
|
||||
Traces/Math/tracemath.cpp \
|
||||
Traces/Math/tracematheditdialog.cpp \
|
||||
Traces/Math/windowfunction.cpp \
|
||||
Traces/fftcomplex.cpp \
|
||||
Traces/markerwidget.cpp \
|
||||
Traces/trace.cpp \
|
||||
@ -134,6 +134,7 @@ FORMS += \
|
||||
Traces/Math/medianexplanationwidget.ui \
|
||||
Traces/Math/medianfilterdialog.ui \
|
||||
Traces/Math/newtracemathdialog.ui \
|
||||
Traces/Math/tdrdialog.ui \
|
||||
Traces/Math/tracematheditdialog.ui \
|
||||
Traces/markerwidget.ui \
|
||||
Traces/smithchartdialog.ui \
|
||||
|
216
Software/PC_Application/Traces/Math/tdr.cpp
Normal file
216
Software/PC_Application/Traces/Math/tdr.cpp
Normal file
@ -0,0 +1,216 @@
|
||||
#include "tdr.h"
|
||||
|
||||
#include "Traces/fftcomplex.h"
|
||||
#include "ui_tdrdialog.h"
|
||||
#include <QVBoxLayout>
|
||||
#include <QLabel>
|
||||
using namespace Math;
|
||||
using namespace std;
|
||||
|
||||
TDR::TDR()
|
||||
{
|
||||
automaticDC = true;
|
||||
manualDC = 1.0;
|
||||
stepResponse = true;
|
||||
mode = Mode::Lowpass;
|
||||
|
||||
connect(&window, &WindowFunction::changed, this, &TDR::updateTDR);
|
||||
}
|
||||
|
||||
TraceMath::DataType TDR::outputType(TraceMath::DataType inputType)
|
||||
{
|
||||
if(inputType == DataType::Frequency) {
|
||||
return DataType::Time;
|
||||
} else {
|
||||
return DataType::Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
QString TDR::description()
|
||||
{
|
||||
QString ret = "TDR (";
|
||||
if(mode == Mode::Lowpass) {
|
||||
ret += "lowpass)";
|
||||
if(stepResponse) {
|
||||
ret += ", with step response";
|
||||
}
|
||||
} else {
|
||||
ret += "bandpass)";
|
||||
}
|
||||
ret += ", window: " + window.getDescription();
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
void TDR::edit()
|
||||
{
|
||||
auto d = new QDialog();
|
||||
auto ui = new Ui::TDRDialog;
|
||||
ui->setupUi(d);
|
||||
ui->windowBox->setLayout(new QVBoxLayout);
|
||||
ui->windowBox->layout()->addWidget(window.createEditor());
|
||||
|
||||
auto updateEnabledWidgets = [=]() {
|
||||
bool enable = mode == Mode::Lowpass;
|
||||
ui->computeStepResponse->setEnabled(enable);
|
||||
enable &= stepResponse;
|
||||
ui->DCmanual->setEnabled(enable);
|
||||
ui->DCautomatic->setEnabled(enable);
|
||||
enable &= !automaticDC;
|
||||
ui->manualPhase->setEnabled(enable);
|
||||
ui->manualMag->setEnabled(enable);
|
||||
};
|
||||
|
||||
connect(ui->mode, qOverload<int>(&QComboBox::currentIndexChanged), [=](int index){
|
||||
mode = (Mode) index;
|
||||
updateEnabledWidgets();
|
||||
updateTDR();
|
||||
});
|
||||
|
||||
connect(ui->computeStepResponse, &QCheckBox::toggled, [=](bool computeStep) {
|
||||
stepResponse = computeStep;
|
||||
updateEnabledWidgets();
|
||||
updateTDR();
|
||||
});
|
||||
|
||||
connect(ui->DCmanual, &QRadioButton::toggled, [=](bool manual) {
|
||||
automaticDC = !manual;
|
||||
updateEnabledWidgets();
|
||||
updateTDR();
|
||||
});
|
||||
|
||||
if(automaticDC) {
|
||||
ui->DCautomatic->setChecked(true);
|
||||
} else {
|
||||
ui->DCmanual->setChecked(true);
|
||||
}
|
||||
ui->computeStepResponse->setChecked(stepResponse);
|
||||
|
||||
ui->manualMag->setUnit("dBm");
|
||||
ui->manualMag->setPrecision(3);
|
||||
ui->manualMag->setValue(20*log10(abs(manualDC)));
|
||||
ui->manualPhase->setUnit("°");
|
||||
ui->manualPhase->setPrecision(4);
|
||||
ui->manualPhase->setValue(180.0/M_PI * arg(manualDC));
|
||||
|
||||
connect(ui->manualMag, &SIUnitEdit::valueChanged, [=](double newval){
|
||||
manualDC = polar(pow(10, newval / 20.0), arg(manualDC));
|
||||
updateTDR();
|
||||
});
|
||||
connect(ui->manualPhase, &SIUnitEdit::valueChanged, [=](double newval){
|
||||
manualDC = polar(abs(manualDC), newval * M_PI / 180.0);
|
||||
updateTDR();
|
||||
});
|
||||
|
||||
connect(ui->buttonBox, &QDialogButtonBox::accepted, d, &QDialog::accept);
|
||||
d->show();
|
||||
}
|
||||
|
||||
QWidget *TDR::createExplanationWidget()
|
||||
{
|
||||
return new QLabel("Test");
|
||||
}
|
||||
|
||||
void TDR::inputSamplesChanged(unsigned int begin, unsigned int end)
|
||||
{
|
||||
Q_UNUSED(begin);
|
||||
if(input->rData().size() >= 2) {
|
||||
// TDR is computationally expensive, only update at the end of sweep
|
||||
if(end != input->rData().size()) {
|
||||
// not the end, do nothing
|
||||
return;
|
||||
}
|
||||
vector<complex<double>> frequencyDomain;
|
||||
auto stepSize = input->rData()[1].x - input->rData()[0].x;
|
||||
if(mode == Mode::Lowpass) {
|
||||
if(stepResponse) {
|
||||
auto steps = input->rData().size();
|
||||
auto firstStep = input->rData().front().x;
|
||||
// frequency points need to be evenly spaced all the way to DC
|
||||
if(firstStep == 0) {
|
||||
// zero as first step would result in infinite number of points, skip and start with second
|
||||
firstStep = input->rData()[1].x;
|
||||
steps--;
|
||||
}
|
||||
if(firstStep * steps != input->rData().back().x) {
|
||||
// data is not available with correct frequency spacing, calculate required steps
|
||||
steps = input->rData().back().x / firstStep;
|
||||
}
|
||||
frequencyDomain.resize(2 * steps + 1);
|
||||
// copy frequencies, use the flipped conjugate for negative part
|
||||
for(unsigned int i = 1;i<=steps;i++) {
|
||||
auto S = input->getInterpolatedSample(stepSize * i).y;
|
||||
frequencyDomain[steps - i] = conj(S);
|
||||
frequencyDomain[steps + i] = S;
|
||||
}
|
||||
if(automaticDC) {
|
||||
// use simple extrapolation from lowest two points to extract DC value
|
||||
auto abs_DC = 2.0 * abs(frequencyDomain[steps + 1]) - abs(frequencyDomain[steps + 2]);
|
||||
auto phase_DC = 2.0 * arg(frequencyDomain[steps + 1]) - arg(frequencyDomain[steps + 2]);
|
||||
frequencyDomain[steps] = polar(abs_DC, phase_DC);
|
||||
} else {
|
||||
frequencyDomain[steps] = manualDC;
|
||||
}
|
||||
} else {
|
||||
auto steps = input->rData().size();
|
||||
unsigned int offset = 0;
|
||||
if(input->rData().front().x == 0) {
|
||||
// DC measurement is inaccurate, skip
|
||||
steps--;
|
||||
offset++;
|
||||
}
|
||||
// no step response required, can use frequency values as they are. No extra extrapolated DC value here -> 2 values less than with step response
|
||||
frequencyDomain.resize(2 * steps - 1);
|
||||
frequencyDomain[steps - 1] = input->rData()[offset].y;
|
||||
for(unsigned int i = 1;i<steps;i++) {
|
||||
auto S = input->rData()[i + offset].y;
|
||||
frequencyDomain[steps - i - 1] = conj(S);
|
||||
frequencyDomain[steps + i - 1] = S;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// bandpass mode
|
||||
// Can use input data directly, no need to extend with complex conjugate
|
||||
frequencyDomain.resize(input->rData().size());
|
||||
for(unsigned int i=0;i<input->rData().size();i++) {
|
||||
frequencyDomain[i] = input->rData()[i].y;
|
||||
}
|
||||
}
|
||||
|
||||
window.apply(frequencyDomain);
|
||||
Fft::shift(frequencyDomain, true);
|
||||
|
||||
auto fft_bins = frequencyDomain.size();
|
||||
const double fs = 1.0 / (stepSize * fft_bins);
|
||||
|
||||
Fft::transform(frequencyDomain, true);
|
||||
|
||||
data.clear();
|
||||
data.resize(fft_bins);
|
||||
|
||||
for(unsigned int i = 0;i<fft_bins;i++) {
|
||||
data[i].x = fs * i;
|
||||
data[i].y = frequencyDomain[i] / (double) fft_bins;
|
||||
}
|
||||
if(stepResponse && mode == Mode::Lowpass) {
|
||||
updateStepResponse(true);
|
||||
} else {
|
||||
updateStepResponse(false);
|
||||
}
|
||||
emit outputSamplesChanged(0, data.size());
|
||||
success();
|
||||
} else {
|
||||
// not enough input data
|
||||
data.clear();
|
||||
updateStepResponse(false);
|
||||
emit outputSamplesChanged(0, 0);
|
||||
warning("Not enough input samples");
|
||||
}
|
||||
}
|
||||
|
||||
void TDR::updateTDR()
|
||||
{
|
||||
if(dataType != DataType::Invalid) {
|
||||
inputSamplesChanged(0, input->rData().size());
|
||||
}
|
||||
}
|
@ -2,13 +2,14 @@
|
||||
#define TDRLOWPASS_H
|
||||
|
||||
#include "tracemath.h"
|
||||
#include "windowfunction.h"
|
||||
|
||||
namespace Math {
|
||||
|
||||
class TDRLowpass : public TraceMath
|
||||
class TDR : public TraceMath
|
||||
{
|
||||
public:
|
||||
TDRLowpass();
|
||||
TDR();
|
||||
|
||||
DataType outputType(DataType inputType) override;
|
||||
QString description() override;
|
||||
@ -18,6 +19,18 @@ public:
|
||||
|
||||
public slots:
|
||||
void inputSamplesChanged(unsigned int begin, unsigned int end) override;
|
||||
|
||||
private:
|
||||
void updateTDR();
|
||||
enum class Mode {
|
||||
Lowpass,
|
||||
Bandpass,
|
||||
};
|
||||
Mode mode;
|
||||
WindowFunction window;
|
||||
bool stepResponse;
|
||||
bool automaticDC;
|
||||
std::complex<double> manualDC;
|
||||
};
|
||||
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
#include "tdrbandpass.h"
|
||||
|
||||
#include "Traces/fftcomplex.h"
|
||||
|
||||
|
||||
#include <QLabel>
|
||||
using namespace Math;
|
||||
using namespace std;
|
||||
|
||||
TDRBandpass::TDRBandpass()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
TraceMath::DataType TDRBandpass::outputType(TraceMath::DataType inputType)
|
||||
{
|
||||
if(inputType == DataType::Frequency) {
|
||||
return DataType::Time;
|
||||
} else {
|
||||
return DataType::Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
QString TDRBandpass::description()
|
||||
{
|
||||
return "TDR (bandpass mode)";
|
||||
}
|
||||
|
||||
void TDRBandpass::edit()
|
||||
{
|
||||
// nothing to do for now
|
||||
}
|
||||
|
||||
QWidget *TDRBandpass::createExplanationWidget()
|
||||
{
|
||||
return new QLabel("Test");
|
||||
}
|
||||
|
||||
void TDRBandpass::inputSamplesChanged(unsigned int begin, unsigned int end)
|
||||
{
|
||||
Q_UNUSED(begin);
|
||||
const double PI = 3.141592653589793238463;
|
||||
if(input->rData().size() >= 2) {
|
||||
// TDR is computationally expensive, only update at the end of sweep
|
||||
if(end != input->rData().size()) {
|
||||
// not the end, do nothing
|
||||
return;
|
||||
}
|
||||
// create vector for frequency data
|
||||
vector<complex<double>> dft(end);
|
||||
// window data and perform FFTshift ("zero"-bin expected at index 0 but is in the middle of input data)
|
||||
for(unsigned int i=0;i<end;i++) {
|
||||
// TODO implement other windows as well
|
||||
constexpr double alpha0 = 0.54;
|
||||
auto hamming = alpha0 - (1.0 - alpha0) * cos(2 * PI * i / end);
|
||||
auto windowed = input->rData()[i].y * hamming;
|
||||
// swap upper and lower half
|
||||
if(i < end/2) {
|
||||
dft[i + (end+1)/2] = windowed;
|
||||
} else {
|
||||
dft[i - end/2] = windowed;
|
||||
}
|
||||
}
|
||||
// perform IFT operation on data
|
||||
Fft::transform(dft, true);
|
||||
|
||||
// calculate sample distance
|
||||
auto freqStep = input->rData()[1].x - input->rData()[0].x;
|
||||
auto timeStep = 1.0 / (freqStep * end);
|
||||
|
||||
// copy DFT data to output data
|
||||
data.resize(dft.size());
|
||||
for(unsigned int i=0;i<dft.size();i++) {
|
||||
data[i].y = dft[i];
|
||||
data[i].x = i * timeStep;
|
||||
}
|
||||
emit outputSamplesChanged(0, data.size());
|
||||
success();
|
||||
} else {
|
||||
// not enough input data
|
||||
data.clear();
|
||||
emit outputSamplesChanged(0, 0);
|
||||
warning("Not enough input samples");
|
||||
}
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
#ifndef TDRBANDPASS_H
|
||||
#define TDRBANDPASS_H
|
||||
|
||||
#include "tracemath.h"
|
||||
|
||||
namespace Math {
|
||||
|
||||
class TDRBandpass : public TraceMath
|
||||
{
|
||||
public:
|
||||
TDRBandpass();
|
||||
|
||||
DataType outputType(DataType inputType) override;
|
||||
QString description() override;
|
||||
void edit() override;
|
||||
|
||||
static QWidget* createExplanationWidget();
|
||||
|
||||
public slots:
|
||||
void inputSamplesChanged(unsigned int begin, unsigned int end) override;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // TDRBANDPASS_H
|
150
Software/PC_Application/Traces/Math/tdrdialog.ui
Normal file
150
Software/PC_Application/Traces/Math/tdrdialog.ui
Normal file
@ -0,0 +1,150 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>TDRDialog</class>
|
||||
<widget class="QDialog" name="TDRDialog">
|
||||
<property name="windowModality">
|
||||
<enum>Qt::ApplicationModal</enum>
|
||||
</property>
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>268</width>
|
||||
<height>421</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>TDR</string>
|
||||
</property>
|
||||
<property name="modal">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,0,0,1,0">
|
||||
<item>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string>Mode:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="mode">
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Lowpass</string>
|
||||
</property>
|
||||
</item>
|
||||
<item>
|
||||
<property name="text">
|
||||
<string>Bandpass</string>
|
||||
</property>
|
||||
</item>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="computeStepResponse">
|
||||
<property name="text">
|
||||
<string>Compute Step Response</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>DC point</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<item>
|
||||
<widget class="QRadioButton" name="DCautomatic">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Automatic (extrapolate)</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">buttonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QRadioButton" name="DCmanual">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Specify manually:</string>
|
||||
</property>
|
||||
<attribute name="buttonGroup">
|
||||
<string notr="true">buttonGroup</string>
|
||||
</attribute>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Magnitude:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="SIUnitEdit" name="manualMag">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="text">
|
||||
<string>Phase:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="SIUnitEdit" name="manualPhase">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="windowBox">
|
||||
<property name="title">
|
||||
<string>Window</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<customwidgets>
|
||||
<customwidget>
|
||||
<class>SIUnitEdit</class>
|
||||
<extends>QLineEdit</extends>
|
||||
<header>CustomWidgets/siunitedit.h</header>
|
||||
</customwidget>
|
||||
</customwidgets>
|
||||
<resources/>
|
||||
<connections/>
|
||||
<buttongroups>
|
||||
<buttongroup name="buttonGroup"/>
|
||||
</buttongroups>
|
||||
</ui>
|
@ -1,98 +0,0 @@
|
||||
#include "tdrlowpass.h"
|
||||
|
||||
#include "Traces/fftcomplex.h"
|
||||
|
||||
|
||||
#include <QLabel>
|
||||
using namespace Math;
|
||||
using namespace std;
|
||||
|
||||
TDRLowpass::TDRLowpass()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
TraceMath::DataType TDRLowpass::outputType(TraceMath::DataType inputType)
|
||||
{
|
||||
if(inputType == DataType::Frequency) {
|
||||
return DataType::Time;
|
||||
} else {
|
||||
return DataType::Invalid;
|
||||
}
|
||||
}
|
||||
|
||||
QString TDRLowpass::description()
|
||||
{
|
||||
return "TDR (lowpass mode)";
|
||||
}
|
||||
|
||||
void TDRLowpass::edit()
|
||||
{
|
||||
// nothing to do for now
|
||||
}
|
||||
|
||||
QWidget *TDRLowpass::createExplanationWidget()
|
||||
{
|
||||
return new QLabel("Test");
|
||||
}
|
||||
|
||||
void TDRLowpass::inputSamplesChanged(unsigned int begin, unsigned int end)
|
||||
{
|
||||
Q_UNUSED(begin);
|
||||
if(input->rData().size() >= 2) {
|
||||
// TDR is computationally expensive, only update at the end of sweep
|
||||
if(end != input->rData().size()) {
|
||||
// not the end, do nothing
|
||||
return;
|
||||
}
|
||||
auto steps = input->rData().size();
|
||||
auto firstStep = input->rData().front().x;
|
||||
if(firstStep == 0) {
|
||||
// zero as first step would result in infinite number of points, skip and start with second
|
||||
firstStep = input->rData()[1].x;
|
||||
steps--;
|
||||
}
|
||||
if(firstStep * steps != input->rData().back().x) {
|
||||
// data is not available with correct frequency spacing, calculate required steps
|
||||
steps = input->rData().back().x / firstStep;
|
||||
}
|
||||
const double PI = 3.141592653589793238463;
|
||||
// reserve vector for negative frequenies and DC as well
|
||||
vector<complex<double>> frequencyDomain(2*steps + 1);
|
||||
// copy frequencies, use the flipped conjugate for negative part
|
||||
for(unsigned int i = 1;i<=steps;i++) {
|
||||
auto S = input->getInterpolatedSample(firstStep * i).y;
|
||||
constexpr double alpha0 = 0.54;
|
||||
auto hamming = alpha0 - (1.0 - alpha0) * -cos(PI * i / steps);
|
||||
S *= hamming;
|
||||
frequencyDomain[2 * steps - i + 1] = conj(S);
|
||||
frequencyDomain[i] = S;
|
||||
}
|
||||
// use simple extrapolation from lowest two points to extract DC value
|
||||
auto abs_DC = 2.0 * abs(frequencyDomain[1]) - abs(frequencyDomain[2]);
|
||||
auto phase_DC = 2.0 * arg(frequencyDomain[1]) - arg(frequencyDomain[2]);
|
||||
frequencyDomain[0] = polar(abs_DC, phase_DC);
|
||||
|
||||
auto fft_bins = frequencyDomain.size();
|
||||
const double fs = 1.0 / (firstStep * fft_bins);
|
||||
|
||||
Fft::transform(frequencyDomain, true);
|
||||
|
||||
data.clear();
|
||||
data.resize(fft_bins);
|
||||
|
||||
for(unsigned int i = 0;i<fft_bins;i++) {
|
||||
data[i].x = fs * i;
|
||||
data[i].y = frequencyDomain[i] / (double) fft_bins;
|
||||
}
|
||||
updateStepResponse(true);
|
||||
emit outputSamplesChanged(0, data.size());
|
||||
success();
|
||||
} else {
|
||||
// not enough input data
|
||||
data.clear();
|
||||
updateStepResponse(false);
|
||||
emit outputSamplesChanged(0, 0);
|
||||
warning("Not enough input samples");
|
||||
}
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
#include "tracemath.h"
|
||||
|
||||
#include "medianfilter.h"
|
||||
#include "tdrbandpass.h"
|
||||
#include "tdrlowpass.h"
|
||||
#include "tdr.h"
|
||||
|
||||
TraceMath::TraceMath()
|
||||
{
|
||||
@ -16,10 +15,8 @@ TraceMath *TraceMath::createMath(TraceMath::Type type)
|
||||
switch(type) {
|
||||
case Type::MedianFilter:
|
||||
return new Math::MedianFilter();
|
||||
case Type::TDRlowpass:
|
||||
return new Math::TDRLowpass();
|
||||
case Type::TDRbandpass:
|
||||
return new Math::TDRBandpass();
|
||||
case Type::TDR:
|
||||
return new Math::TDR();
|
||||
default:
|
||||
return nullptr;
|
||||
}
|
||||
@ -33,13 +30,9 @@ TraceMath::TypeInfo TraceMath::getInfo(TraceMath::Type type)
|
||||
ret.name = "Median filter";
|
||||
ret.explanationWidget = Math::MedianFilter::createExplanationWidget();
|
||||
break;
|
||||
case Type::TDRlowpass:
|
||||
ret.name = "TDR (lowpass)";
|
||||
ret.explanationWidget = Math::TDRLowpass::createExplanationWidget();
|
||||
break;
|
||||
case Type::TDRbandpass:
|
||||
ret.name = "TDR (bandpass)";
|
||||
ret.explanationWidget = Math::TDRBandpass::createExplanationWidget();
|
||||
case Type::TDR:
|
||||
ret.name = "TDR";
|
||||
ret.explanationWidget = Math::TDR::createExplanationWidget();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@ -110,28 +103,25 @@ void TraceMath::assignInput(TraceMath *input)
|
||||
if(input != this->input) {
|
||||
removeInput();
|
||||
this->input = input;
|
||||
inputTypeChanged(input->dataType);
|
||||
// do initial calculation
|
||||
inputSamplesChanged(0, input->data.size());
|
||||
// connect to input
|
||||
connect(input, &TraceMath::outputSamplesChanged, this, &TraceMath::inputSamplesChanged);
|
||||
connect(input, &TraceMath::outputTypeChanged, this, &TraceMath::inputTypeChanged);
|
||||
inputTypeChanged(input->dataType);
|
||||
}
|
||||
}
|
||||
|
||||
void TraceMath::inputTypeChanged(TraceMath::DataType type)
|
||||
{
|
||||
auto newType = outputType(type);
|
||||
if(newType != dataType) {
|
||||
dataType = newType;
|
||||
data.clear();
|
||||
dataType = newType;
|
||||
data.clear();
|
||||
if(dataType == DataType::Invalid) {
|
||||
error("Invalid input data");
|
||||
disconnect(input, &TraceMath::outputSamplesChanged, this, &TraceMath::inputSamplesChanged);
|
||||
updateStepResponse(false);
|
||||
} else {
|
||||
connect(input, &TraceMath::outputSamplesChanged, this, &TraceMath::inputSamplesChanged);
|
||||
inputSamplesChanged(0, input->data.size());
|
||||
emit outputTypeChanged(dataType);
|
||||
if(dataType == DataType::Invalid) {
|
||||
error("Invalid input data");
|
||||
updateStepResponse(false);
|
||||
}
|
||||
}
|
||||
emit outputTypeChanged(dataType);
|
||||
}
|
||||
|
||||
void TraceMath::warning(QString warn)
|
||||
|
@ -66,8 +66,7 @@ public:
|
||||
|
||||
enum class Type {
|
||||
MedianFilter,
|
||||
TDRlowpass,
|
||||
TDRbandpass,
|
||||
TDR,
|
||||
// Add new math operations here, do not explicitly assign values and keep the Last entry at the last position
|
||||
Last,
|
||||
};
|
||||
|
@ -57,6 +57,7 @@ TraceMathEditDialog::TraceMathEditDialog(Trace &t, QWidget *parent) :
|
||||
// always show the widget for the selected function
|
||||
connect(ui->list, &QListWidget::currentRowChanged, ui->stack, &QStackedWidget::setCurrentIndex);
|
||||
|
||||
connect(ui->list, &QListWidget::doubleClicked, ui->buttonBox, &QDialogButtonBox::accepted);
|
||||
connect(ui->buttonBox, &QDialogButtonBox::accepted, [=](){
|
||||
auto newMath = TraceMath::createMath(static_cast<TraceMath::Type>(ui->list->currentRow()));
|
||||
if(newMath) {
|
||||
|
145
Software/PC_Application/Traces/Math/windowfunction.cpp
Normal file
145
Software/PC_Application/Traces/Math/windowfunction.cpp
Normal file
@ -0,0 +1,145 @@
|
||||
#include "windowfunction.h"
|
||||
#define _USE_MATH_DEFINES
|
||||
#include <math.h>
|
||||
#include <QComboBox>
|
||||
#include <QLabel>
|
||||
#include <QFormLayout>
|
||||
#include "CustomWidgets/siunitedit.h"
|
||||
|
||||
QString WindowFunction::typeToName(WindowFunction::Type type)
|
||||
{
|
||||
switch(type) {
|
||||
case Type::Rectangular: return "Rectangular"; break;
|
||||
// case Type::Kaiser: return "Kaiser"; break;
|
||||
case Type::Hamming: return "Hamming"; break;
|
||||
case Type::Hann: return "Hann"; break;
|
||||
case Type::Blackman: return "Blackman"; break;
|
||||
case Type::Gaussian: return "Gaussian"; break;
|
||||
case Type::Chebyshev: return "Chebyshev"; break;
|
||||
default: return "Invalid"; break;
|
||||
}
|
||||
}
|
||||
|
||||
WindowFunction::WindowFunction(WindowFunction::Type type)
|
||||
{
|
||||
this->type = type;
|
||||
// set default parameters
|
||||
kaiser_alpha = 3.0;
|
||||
gaussian_sigma = 0.4;
|
||||
chebyshev_alpha = 5;
|
||||
}
|
||||
|
||||
void WindowFunction::apply(std::vector<std::complex<double> > &data)
|
||||
{
|
||||
unsigned int N = data.size();
|
||||
for(unsigned int n = 0;n<N;n++) {
|
||||
data[n] *= getFactor(n, N);
|
||||
}
|
||||
}
|
||||
|
||||
QWidget *WindowFunction::createEditor()
|
||||
{
|
||||
auto top = new QWidget();
|
||||
auto layout = new QFormLayout();
|
||||
top->setLayout(layout);
|
||||
auto cbType = new QComboBox();
|
||||
for(unsigned int i=0;i<(unsigned int) Type::Last;i++) {
|
||||
cbType->addItem(typeToName((Type) i));
|
||||
}
|
||||
|
||||
layout->addRow(new QLabel("Type:"), cbType);
|
||||
|
||||
QObject::connect(cbType, qOverload<int>(&QComboBox::currentIndexChanged), [=](int newIndex){
|
||||
if(layout->rowCount() > 1) {
|
||||
layout->removeRow(1);
|
||||
}
|
||||
type = (Type) newIndex;
|
||||
QLabel *paramLabel = nullptr;
|
||||
SIUnitEdit *paramEdit = nullptr;
|
||||
// add GUI elements for window types that have a parameter
|
||||
switch(type) {
|
||||
case Type::Gaussian:
|
||||
paramLabel = new QLabel("Parameter σ:");
|
||||
paramEdit = new SIUnitEdit("", " ", 3);
|
||||
paramEdit->setValue(gaussian_sigma);
|
||||
QObject::connect(paramEdit, &SIUnitEdit::valueChanged, [=](double newval) {
|
||||
gaussian_sigma = newval;
|
||||
});
|
||||
break;
|
||||
case Type::Chebyshev:
|
||||
paramLabel = new QLabel("Parameter α:");
|
||||
paramEdit = new SIUnitEdit("", " ", 3);
|
||||
paramEdit->setValue(chebyshev_alpha);
|
||||
QObject::connect(paramEdit, &SIUnitEdit::valueChanged, [=](double newval) {
|
||||
chebyshev_alpha = newval;
|
||||
});
|
||||
break;
|
||||
// case Type::Kaiser:
|
||||
// // TODO
|
||||
// break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if(paramLabel != nullptr && paramEdit != nullptr) {
|
||||
layout->addRow(paramLabel, paramEdit);
|
||||
QObject::connect(paramEdit, &SIUnitEdit::valueChanged, this, &WindowFunction::changed);
|
||||
}
|
||||
emit changed();
|
||||
});
|
||||
|
||||
cbType->setCurrentIndex((int) type);
|
||||
|
||||
return top;
|
||||
}
|
||||
|
||||
WindowFunction::Type WindowFunction::getType() const
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
QString WindowFunction::getDescription()
|
||||
{
|
||||
QString ret = typeToName(type);
|
||||
if(type == Type::Gaussian) {
|
||||
ret += ", σ=" + QString::number(gaussian_sigma);
|
||||
} else if(type == Type::Chebyshev) {
|
||||
ret += ", α=" + QString::number(chebyshev_alpha);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
double WindowFunction::getFactor(unsigned int n, unsigned int N)
|
||||
{
|
||||
// all formulas from https://en.wikipedia.org/wiki/Window_function
|
||||
switch(type) {
|
||||
case Type::Rectangular:
|
||||
// nothing to do
|
||||
return 1.0;
|
||||
// case Type::Kaiser:
|
||||
// // TODO
|
||||
// break;
|
||||
case Type::Hamming:
|
||||
return 25.0/46.0 - (21.0/46.0) * cos(2*M_PI*n / N);
|
||||
case Type::Hann:
|
||||
return pow(sin(M_PI*n / N), 2.0);
|
||||
case Type::Blackman:
|
||||
return 0.42 - 0.5 * cos(2*M_PI*n / N) + 0.08 * cos(4*M_PI*n / N);
|
||||
case Type::Gaussian:
|
||||
return exp(-0.5 * pow((n - (double) N/2) / (gaussian_sigma * N / 2), 2));
|
||||
case Type::Chebyshev: {
|
||||
double beta = cosh(1.0 / N * acosh(pow(10, chebyshev_alpha)));
|
||||
double T_N_arg = beta * cos(M_PI*n/(N+1));
|
||||
double T_N;
|
||||
if(T_N_arg >= 1.0) {
|
||||
T_N = cosh(N * acosh(T_N_arg));
|
||||
} else if(T_N_arg <= -1.0) {
|
||||
T_N = pow(-1.0, N) * cosh(N * acosh(T_N_arg));
|
||||
} else {
|
||||
T_N = cos(N * acos(T_N_arg));
|
||||
}
|
||||
return T_N / pow(10.0, chebyshev_alpha);
|
||||
}
|
||||
default:
|
||||
return 1.0;
|
||||
}
|
||||
}
|
48
Software/PC_Application/Traces/Math/windowfunction.h
Normal file
48
Software/PC_Application/Traces/Math/windowfunction.h
Normal file
@ -0,0 +1,48 @@
|
||||
#ifndef WINDOWFUNCTION_H
|
||||
#define WINDOWFUNCTION_H
|
||||
|
||||
#include <QWidget>
|
||||
#include <complex>
|
||||
#include <vector>
|
||||
|
||||
class WindowFunction : public QObject
|
||||
{
|
||||
Q_OBJECT;
|
||||
public:
|
||||
enum class Type {
|
||||
Rectangular,
|
||||
// Kaiser,
|
||||
Gaussian,
|
||||
Chebyshev,
|
||||
Hann,
|
||||
Hamming,
|
||||
Blackman,
|
||||
// always has to be the last entry
|
||||
Last,
|
||||
};
|
||||
static QString typeToName(Type type);
|
||||
|
||||
WindowFunction(Type type = Type::Hamming);
|
||||
|
||||
void apply(std::vector<std::complex<double>>& data);
|
||||
|
||||
QWidget *createEditor();
|
||||
|
||||
Type getType() const;
|
||||
QString getDescription();
|
||||
|
||||
signals:
|
||||
void changed();
|
||||
|
||||
private:
|
||||
double getFactor(unsigned int n, unsigned int N);
|
||||
Type type;
|
||||
// parameters for the different types. Not all windows use one and most only one.
|
||||
// But keeping all parameters for all windows allows switching between window types
|
||||
// while remembering the settings for each type
|
||||
double kaiser_alpha;
|
||||
double gaussian_sigma;
|
||||
double chebyshev_alpha;
|
||||
};
|
||||
|
||||
#endif // WINDOWFUNCTION_H
|
@ -26,6 +26,7 @@
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
#include "fftcomplex.h"
|
||||
#include <algorithm>
|
||||
|
||||
using std::complex;
|
||||
using std::size_t;
|
||||
@ -150,3 +151,15 @@ static size_t reverseBits(size_t val, int width) {
|
||||
result = (result << 1) | (val & 1U);
|
||||
return result;
|
||||
}
|
||||
|
||||
void Fft::shift(std::vector<std::complex<double> > &vec, bool inverse)
|
||||
{
|
||||
int rotate_len = vec.size() / 2;
|
||||
if(vec.size() % 0x01 != 0) {
|
||||
// odd size, behavior depends on whether this is an inverse shift
|
||||
if(!inverse) {
|
||||
rotate_len++;
|
||||
}
|
||||
}
|
||||
std::rotate(vec.begin(), vec.begin() + rotate_len, vec.end());
|
||||
}
|
||||
|
@ -26,9 +26,11 @@
|
||||
#include <complex>
|
||||
#include <vector>
|
||||
|
||||
|
||||
namespace Fft {
|
||||
|
||||
// swap left/right halves of the vector, similar to matlabs fftshift/ifftshift
|
||||
void shift(std::vector<std::complex<double> > &vec, bool inverse);
|
||||
|
||||
/*
|
||||
* Computes the discrete Fourier transform (DFT) of the given complex vector, storing the result back into the vector.
|
||||
* The vector can have any length. This is a wrapper function. The inverse transform does not perform scaling, so it is not a true inverse.
|
||||
|
Loading…
Reference in New Issue
Block a user