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/eseries.h \
|
||||||
Tools/impedancematchdialog.h \
|
Tools/impedancematchdialog.h \
|
||||||
Traces/Math/medianfilter.h \
|
Traces/Math/medianfilter.h \
|
||||||
Traces/Math/tdrbandpass.h \
|
Traces/Math/tdr.h \
|
||||||
Traces/Math/tdrlowpass.h \
|
|
||||||
Traces/Math/tracemath.h \
|
Traces/Math/tracemath.h \
|
||||||
Traces/Math/tracematheditdialog.h \
|
Traces/Math/tracematheditdialog.h \
|
||||||
|
Traces/Math/windowfunction.h \
|
||||||
Traces/fftcomplex.h \
|
Traces/fftcomplex.h \
|
||||||
Traces/markerwidget.h \
|
Traces/markerwidget.h \
|
||||||
Traces/trace.h \
|
Traces/trace.h \
|
||||||
@ -83,10 +83,10 @@ SOURCES += \
|
|||||||
Tools/eseries.cpp \
|
Tools/eseries.cpp \
|
||||||
Tools/impedancematchdialog.cpp \
|
Tools/impedancematchdialog.cpp \
|
||||||
Traces/Math/medianfilter.cpp \
|
Traces/Math/medianfilter.cpp \
|
||||||
Traces/Math/tdrbandpass.cpp \
|
Traces/Math/tdr.cpp \
|
||||||
Traces/Math/tdrlowpass.cpp \
|
|
||||||
Traces/Math/tracemath.cpp \
|
Traces/Math/tracemath.cpp \
|
||||||
Traces/Math/tracematheditdialog.cpp \
|
Traces/Math/tracematheditdialog.cpp \
|
||||||
|
Traces/Math/windowfunction.cpp \
|
||||||
Traces/fftcomplex.cpp \
|
Traces/fftcomplex.cpp \
|
||||||
Traces/markerwidget.cpp \
|
Traces/markerwidget.cpp \
|
||||||
Traces/trace.cpp \
|
Traces/trace.cpp \
|
||||||
@ -134,6 +134,7 @@ FORMS += \
|
|||||||
Traces/Math/medianexplanationwidget.ui \
|
Traces/Math/medianexplanationwidget.ui \
|
||||||
Traces/Math/medianfilterdialog.ui \
|
Traces/Math/medianfilterdialog.ui \
|
||||||
Traces/Math/newtracemathdialog.ui \
|
Traces/Math/newtracemathdialog.ui \
|
||||||
|
Traces/Math/tdrdialog.ui \
|
||||||
Traces/Math/tracematheditdialog.ui \
|
Traces/Math/tracematheditdialog.ui \
|
||||||
Traces/markerwidget.ui \
|
Traces/markerwidget.ui \
|
||||||
Traces/smithchartdialog.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
|
#define TDRLOWPASS_H
|
||||||
|
|
||||||
#include "tracemath.h"
|
#include "tracemath.h"
|
||||||
|
#include "windowfunction.h"
|
||||||
|
|
||||||
namespace Math {
|
namespace Math {
|
||||||
|
|
||||||
class TDRLowpass : public TraceMath
|
class TDR : public TraceMath
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
TDRLowpass();
|
TDR();
|
||||||
|
|
||||||
DataType outputType(DataType inputType) override;
|
DataType outputType(DataType inputType) override;
|
||||||
QString description() override;
|
QString description() override;
|
||||||
@ -18,6 +19,18 @@ public:
|
|||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
void inputSamplesChanged(unsigned int begin, unsigned int end) override;
|
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 "tracemath.h"
|
||||||
|
|
||||||
#include "medianfilter.h"
|
#include "medianfilter.h"
|
||||||
#include "tdrbandpass.h"
|
#include "tdr.h"
|
||||||
#include "tdrlowpass.h"
|
|
||||||
|
|
||||||
TraceMath::TraceMath()
|
TraceMath::TraceMath()
|
||||||
{
|
{
|
||||||
@ -16,10 +15,8 @@ TraceMath *TraceMath::createMath(TraceMath::Type type)
|
|||||||
switch(type) {
|
switch(type) {
|
||||||
case Type::MedianFilter:
|
case Type::MedianFilter:
|
||||||
return new Math::MedianFilter();
|
return new Math::MedianFilter();
|
||||||
case Type::TDRlowpass:
|
case Type::TDR:
|
||||||
return new Math::TDRLowpass();
|
return new Math::TDR();
|
||||||
case Type::TDRbandpass:
|
|
||||||
return new Math::TDRBandpass();
|
|
||||||
default:
|
default:
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
@ -33,13 +30,9 @@ TraceMath::TypeInfo TraceMath::getInfo(TraceMath::Type type)
|
|||||||
ret.name = "Median filter";
|
ret.name = "Median filter";
|
||||||
ret.explanationWidget = Math::MedianFilter::createExplanationWidget();
|
ret.explanationWidget = Math::MedianFilter::createExplanationWidget();
|
||||||
break;
|
break;
|
||||||
case Type::TDRlowpass:
|
case Type::TDR:
|
||||||
ret.name = "TDR (lowpass)";
|
ret.name = "TDR";
|
||||||
ret.explanationWidget = Math::TDRLowpass::createExplanationWidget();
|
ret.explanationWidget = Math::TDR::createExplanationWidget();
|
||||||
break;
|
|
||||||
case Type::TDRbandpass:
|
|
||||||
ret.name = "TDR (bandpass)";
|
|
||||||
ret.explanationWidget = Math::TDRBandpass::createExplanationWidget();
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
@ -110,28 +103,25 @@ void TraceMath::assignInput(TraceMath *input)
|
|||||||
if(input != this->input) {
|
if(input != this->input) {
|
||||||
removeInput();
|
removeInput();
|
||||||
this->input = input;
|
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);
|
connect(input, &TraceMath::outputTypeChanged, this, &TraceMath::inputTypeChanged);
|
||||||
|
inputTypeChanged(input->dataType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void TraceMath::inputTypeChanged(TraceMath::DataType type)
|
void TraceMath::inputTypeChanged(TraceMath::DataType type)
|
||||||
{
|
{
|
||||||
auto newType = outputType(type);
|
auto newType = outputType(type);
|
||||||
if(newType != dataType) {
|
|
||||||
dataType = newType;
|
dataType = newType;
|
||||||
data.clear();
|
data.clear();
|
||||||
inputSamplesChanged(0, input->data.size());
|
|
||||||
emit outputTypeChanged(dataType);
|
|
||||||
if(dataType == DataType::Invalid) {
|
if(dataType == DataType::Invalid) {
|
||||||
error("Invalid input data");
|
error("Invalid input data");
|
||||||
|
disconnect(input, &TraceMath::outputSamplesChanged, this, &TraceMath::inputSamplesChanged);
|
||||||
updateStepResponse(false);
|
updateStepResponse(false);
|
||||||
|
} else {
|
||||||
|
connect(input, &TraceMath::outputSamplesChanged, this, &TraceMath::inputSamplesChanged);
|
||||||
|
inputSamplesChanged(0, input->data.size());
|
||||||
}
|
}
|
||||||
}
|
emit outputTypeChanged(dataType);
|
||||||
}
|
}
|
||||||
|
|
||||||
void TraceMath::warning(QString warn)
|
void TraceMath::warning(QString warn)
|
||||||
|
@ -66,8 +66,7 @@ public:
|
|||||||
|
|
||||||
enum class Type {
|
enum class Type {
|
||||||
MedianFilter,
|
MedianFilter,
|
||||||
TDRlowpass,
|
TDR,
|
||||||
TDRbandpass,
|
|
||||||
// Add new math operations here, do not explicitly assign values and keep the Last entry at the last position
|
// Add new math operations here, do not explicitly assign values and keep the Last entry at the last position
|
||||||
Last,
|
Last,
|
||||||
};
|
};
|
||||||
|
@ -57,6 +57,7 @@ TraceMathEditDialog::TraceMathEditDialog(Trace &t, QWidget *parent) :
|
|||||||
// always show the widget for the selected function
|
// always show the widget for the selected function
|
||||||
connect(ui->list, &QListWidget::currentRowChanged, ui->stack, &QStackedWidget::setCurrentIndex);
|
connect(ui->list, &QListWidget::currentRowChanged, ui->stack, &QStackedWidget::setCurrentIndex);
|
||||||
|
|
||||||
|
connect(ui->list, &QListWidget::doubleClicked, ui->buttonBox, &QDialogButtonBox::accepted);
|
||||||
connect(ui->buttonBox, &QDialogButtonBox::accepted, [=](){
|
connect(ui->buttonBox, &QDialogButtonBox::accepted, [=](){
|
||||||
auto newMath = TraceMath::createMath(static_cast<TraceMath::Type>(ui->list->currentRow()));
|
auto newMath = TraceMath::createMath(static_cast<TraceMath::Type>(ui->list->currentRow()));
|
||||||
if(newMath) {
|
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 <stdexcept>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include "fftcomplex.h"
|
#include "fftcomplex.h"
|
||||||
|
#include <algorithm>
|
||||||
|
|
||||||
using std::complex;
|
using std::complex;
|
||||||
using std::size_t;
|
using std::size_t;
|
||||||
@ -150,3 +151,15 @@ static size_t reverseBits(size_t val, int width) {
|
|||||||
result = (result << 1) | (val & 1U);
|
result = (result << 1) | (val & 1U);
|
||||||
return result;
|
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 <complex>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
|
||||||
namespace Fft {
|
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.
|
* 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.
|
* 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