diff --git a/Software/PC_Application/Application.pro b/Software/PC_Application/Application.pro index a67569d..2326e5c 100644 --- a/Software/PC_Application/Application.pro +++ b/Software/PC_Application/Application.pro @@ -26,6 +26,8 @@ HEADERS += \ Tools/eseries.h \ Tools/impedancematchdialog.h \ Traces/Math/medianfilter.h \ + Traces/Math/tdrbandpass.h \ + Traces/Math/tdrlowpass.h \ Traces/Math/tracemath.h \ Traces/Math/tracematheditdialog.h \ Traces/fftcomplex.h \ @@ -81,6 +83,8 @@ SOURCES += \ Tools/eseries.cpp \ Tools/impedancematchdialog.cpp \ Traces/Math/medianfilter.cpp \ + Traces/Math/tdrbandpass.cpp \ + Traces/Math/tdrlowpass.cpp \ Traces/Math/tracemath.cpp \ Traces/Math/tracematheditdialog.cpp \ Traces/fftcomplex.cpp \ @@ -127,7 +131,9 @@ FORMS += \ Device/manualcontroldialog.ui \ Generator/signalgenwidget.ui \ Tools/impedancematchdialog.ui \ + Traces/Math/medianexplanationwidget.ui \ Traces/Math/medianfilterdialog.ui \ + Traces/Math/newtracemathdialog.ui \ Traces/Math/tracematheditdialog.ui \ Traces/markerwidget.ui \ Traces/smithchartdialog.ui \ diff --git a/Software/PC_Application/Traces/Math/medianexplanationwidget.ui b/Software/PC_Application/Traces/Math/medianexplanationwidget.ui new file mode 100644 index 0000000..0a6c074 --- /dev/null +++ b/Software/PC_Application/Traces/Math/medianexplanationwidget.ui @@ -0,0 +1,44 @@ + + + MedianFilterExplanationWidget + + + + 0 + 0 + 364 + 412 + + + + Form + + + + + + <html><head/><body><p><span style=" font-weight:600;">Median Filter</span></p><p>The median filter looks at a number of input samples, sorts them and uses the one in the middle as the output sample. This filters the data and especially eliminates outliers.</p><p>Parameters: </p><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;"><li style=" margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Kernel Size: the amount of input samples used for each output sample. Higher values result in more aggressive filtering</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Sorting method: Since the input data is complex, there are several options for sorting:</li></ul><ul style="margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 2;"><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Absolute value</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Phase</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Real</li><li style=" margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;">Imaginary</li></ul><p><br/></p><p><br/></p></body></html> + + + true + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + diff --git a/Software/PC_Application/Traces/Math/medianfilter.cpp b/Software/PC_Application/Traces/Math/medianfilter.cpp index 759ac4e..0b21a7d 100644 --- a/Software/PC_Application/Traces/Math/medianfilter.cpp +++ b/Software/PC_Application/Traces/Math/medianfilter.cpp @@ -1,15 +1,12 @@ #include "medianfilter.h" #include "ui_medianfilterdialog.h" +#include "ui_medianexplanationwidget.h" #include using namespace Math; using namespace std; -namespace Ui { -class MedianFilterDialog; -} - MedianFilter::MedianFilter() { kernelSize = 3; @@ -50,18 +47,26 @@ void MedianFilter::edit() d->show(); } +QWidget *MedianFilter::createExplanationWidget() +{ + auto w = new QWidget(); + auto ui = new Ui::MedianFilterExplanationWidget; + ui->setupUi(w); + return w; +} + void MedianFilter::inputSamplesChanged(unsigned int begin, unsigned int end) { if(data.size() != input->rData().size()) { data.resize(input->rData().size()); } if(data.size() > 0) { auto kernelOffset = (kernelSize-1)/2; - int start = (int) begin - kernelOffset; - unsigned int stop = (int) end + kernelOffset; + int start = (int) begin - (int) kernelOffset; + unsigned int stop = end + kernelOffset; if(start < 0) { start = 0; } - if(stop >= input->rData().size()) { + if(stop > input->rData().size()) { stop = input->rData().size(); } @@ -71,6 +76,7 @@ void MedianFilter::inputSamplesChanged(unsigned int begin, unsigned int end) { case Order::Phase: return arg(a) < arg(b); case Order::Real: return real(a) < real(b); case Order::Imag: return imag(a) < imag(b); + default: return false; } }; @@ -82,16 +88,13 @@ void MedianFilter::inputSamplesChanged(unsigned int begin, unsigned int end) { unsigned int inputSample; if(kernelOffset > in + out) { inputSample = 0; - } else if(in + kernelOffset + out >= input->rData().size()) { + } else if(in + out >= input->rData().size() + kernelOffset) { inputSample = input->rData().size() - 1; } else { inputSample = in + out - kernelOffset; } auto sample = input->rData().at(inputSample).y; - if(out == (unsigned int) start) { - // this is the first sample to update, fill initial kernel - kernel[in] = sample; - } + kernel[in] = sample; } // sort initial kernel sort(kernel.begin(), kernel.end(), comp); @@ -131,5 +134,6 @@ QString MedianFilter::orderToString(MedianFilter::Order o) case Order::Phase: return "Phase"; case Order::Real: return "Real"; case Order::Imag: return "Imag"; + default: return QString(); } } diff --git a/Software/PC_Application/Traces/Math/medianfilter.h b/Software/PC_Application/Traces/Math/medianfilter.h index f28a457..2cbba2f 100644 --- a/Software/PC_Application/Traces/Math/medianfilter.h +++ b/Software/PC_Application/Traces/Math/medianfilter.h @@ -15,6 +15,8 @@ public: virtual void edit() override; + static QWidget *createExplanationWidget(); + public slots: // a single value of the input data has changed, index determines which sample has changed virtual void inputSamplesChanged(unsigned int begin, unsigned int end) override; diff --git a/Software/PC_Application/Traces/Math/medianfilterdialog.ui b/Software/PC_Application/Traces/Math/medianfilterdialog.ui index a7fa5ec..b387522 100644 --- a/Software/PC_Application/Traces/Math/medianfilterdialog.ui +++ b/Software/PC_Application/Traces/Math/medianfilterdialog.ui @@ -17,7 +17,7 @@ Median Filter - false + true diff --git a/Software/PC_Application/Traces/Math/newtracemathdialog.ui b/Software/PC_Application/Traces/Math/newtracemathdialog.ui new file mode 100644 index 0000000..d7e06b1 --- /dev/null +++ b/Software/PC_Application/Traces/Math/newtracemathdialog.ui @@ -0,0 +1,84 @@ + + + NewTraceMathDialog + + + Qt::ApplicationModal + + + + 0 + 0 + 840 + 596 + + + + Available math operations + + + true + + + + + + + + + + + -1 + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + buttonBox + accepted() + NewTraceMathDialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + NewTraceMathDialog + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/Software/PC_Application/Traces/Math/tdrbandpass.cpp b/Software/PC_Application/Traces/Math/tdrbandpass.cpp new file mode 100644 index 0000000..afd8f14 --- /dev/null +++ b/Software/PC_Application/Traces/Math/tdrbandpass.cpp @@ -0,0 +1,85 @@ +#include "tdrbandpass.h" + +#include "Traces/fftcomplex.h" + + +#include +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> 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;irData()[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 +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 (bandpass 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> 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 data.back().x) { + ret.y = std::numeric_limits>::quiet_NaN(); + ret.x = std::numeric_limits::quiet_NaN(); + } else { + auto it = lower_bound(data.begin(), data.end(), x, [](const Data &lhs, const double x) -> bool { + return lhs.x < x; + }); + if(it->x == x) { + ret = *it; + } else { + // no exact match, needs to interpolate + auto high = *it; + it--; + auto low = *it; + double alpha = (x - low.x) / (high.x - low.x); + ret.y = low.y * (1 - alpha) + high.y * alpha; + ret.x = x; + } + } + return ret; +} + unsigned int TraceMath::numSamples() { return data.size(); diff --git a/Software/PC_Application/Traces/Math/tracemath.h b/Software/PC_Application/Traces/Math/tracemath.h index 2e794e1..2293f41 100644 --- a/Software/PC_Application/Traces/Math/tracemath.h +++ b/Software/PC_Application/Traces/Math/tracemath.h @@ -5,6 +5,42 @@ #include #include +/* + * How to implement a new type of math operation: + * 1. Create your new math operation class by deriving from this class. Put the new class in the namespace + * "Math" to avoid name collisions. + * 2. Implement required virtual functions: + * a. outputType(DataType inputType): + * Indicates what kind of result your math operation creates, depending on the type of its input. + * If the input type is not applicable for your operation (e.g. frequency domain data as input for + * a (forward) DFT, you can return DataType::Invalid + * b. description(): + * Return a short, user-readable description how the operation is set up. This will be displayed + * in the math edit dialog. + * c. edit(): + * Optional. If your operation has customizable parameters, calling this function should start a + * dialog that allows the user to change these parameters. + * d. inputSamplesChanged(unsigned int begin, unsigned int end) + * This slot gets called whenever the input data has changed. Override it and implement your math + * operation in it. Parameters begin and end indicate which input samples have changed: If, for + * example, only the 2nd and third input values have changed, they are set like this: begin=1 end=3 + * CAUTION: the size of the input vector may have changed, check before accessing it. + * + * Emit the signal outputSamplesChanged(unsigned int begin, unsigned int end) after your operation is + * finished. Also call either success(), warning() or error() at the end of this slot, depending on + * whether the operation succeeded: + * success(): everything went well, output data contains valid values + * warning(): something might be wrong (e.g. not enough input samples to create meaningful data, ...). + * Provide a hint by passing a short description string + * error(): something went wrong (called with wrong type of data, mathematical error, ...). + * Provide a hint by passing a short description string + * 3. Add a new type to the Type enum for your operation + * 4. Extend the createMath(Type type) factory function to create an instance of your operation + * 5. Add a static function "createExplanationWidget" which returns a QWidget explaining what your operation does. + * This will be displayed when the user chooses to add a new math operation. + * 6. Extend the function getInfo(Type type) to set a name and create the explanation widget for your operation + */ + class TraceMath : public QObject { Q_OBJECT public: @@ -28,7 +64,24 @@ public: Error, }; + enum class Type { + MedianFilter, + TDRlowpass, + TDRbandpass, + // Add new math operations here, do not explicitly assign values and keep the Last entry at the last position + Last, + }; + + static TraceMath *createMath(Type type); + class TypeInfo { + public: + QString name; + QWidget *explanationWidget; + }; + static TypeInfo getInfo(Type type); + Data getSample(unsigned int index); + Data getInterpolatedSample(double x); unsigned int numSamples(); // indicate whether this function produces time or frequency domain data diff --git a/Software/PC_Application/Traces/Math/tracematheditdialog.cpp b/Software/PC_Application/Traces/Math/tracematheditdialog.cpp index fd4a012..d592f83 100644 --- a/Software/PC_Application/Traces/Math/tracematheditdialog.cpp +++ b/Software/PC_Application/Traces/Math/tracematheditdialog.cpp @@ -2,6 +2,11 @@ #include "ui_tracematheditdialog.h" #include +#include "ui_newtracemathdialog.h" +namespace Ui { +class NewTraceMathDialog; +} + #include "medianfilter.h" TraceMathEditDialog::TraceMathEditDialog(Trace &t, QWidget *parent) : @@ -18,6 +23,7 @@ TraceMathEditDialog::TraceMathEditDialog(Trace &t, QWidget *parent) : headerView->setSectionResizeMode(MathModel::ColIndexDomain, QHeaderView::ResizeToContents); connect(ui->view->selectionModel(), &QItemSelectionModel::currentRowChanged, [=](const QModelIndex ¤t, const QModelIndex &previous){ + Q_UNUSED(previous) if(!current.isValid()) { ui->bDelete->setEnabled(false); ui->bMoveUp->setEnabled(false); @@ -37,7 +43,30 @@ TraceMathEditDialog::TraceMathEditDialog(Trace &t, QWidget *parent) : }); connect(ui->bAdd, &QPushButton::clicked, [=](){ - model->addOperation(new Math::MedianFilter); + auto d = new QDialog(); + auto ui = new Ui::NewTraceMathDialog(); + ui->setupUi(d); + for(int i = 0; i < (int) TraceMath::Type::Last;i++) { + auto info = TraceMath::getInfo(static_cast(i)); + ui->list->addItem(info.name); + if(!info.explanationWidget) { + info.explanationWidget = new QWidget(); + } + ui->stack->addWidget(info.explanationWidget); + } + // always show the widget for the selected function + connect(ui->list, &QListWidget::currentRowChanged, ui->stack, &QStackedWidget::setCurrentIndex); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, [=](){ + auto newMath = TraceMath::createMath(static_cast(ui->list->currentRow())); + if(newMath) { + model->addOperation(newMath); + } + }); + ui->list->setCurrentRow(0); + ui->stack->setCurrentIndex(0); + + d->show(); }); connect(ui->bDelete, &QPushButton::clicked, [=](){ model->deleteRow(ui->view->currentIndex().row()); @@ -45,13 +74,11 @@ TraceMathEditDialog::TraceMathEditDialog(Trace &t, QWidget *parent) : connect(ui->bMoveUp, &QPushButton::clicked, [&](){ auto index = ui->view->currentIndex(); t.swapMathOrder(index.row() - 1); - model->rowsSwapped(index.row() - 1); ui->view->setCurrentIndex(index.sibling(index.row() - 1, 0)); }); connect(ui->bMoveDown, &QPushButton::clicked, [&](){ auto index = ui->view->currentIndex(); t.swapMathOrder(index.row()); - model->rowsSwapped(index.row()); ui->view->setCurrentIndex(index.sibling(index.row() + 1, 0)); }); } @@ -70,11 +97,13 @@ MathModel::MathModel(Trace &t, QObject *parent) int MathModel::rowCount(const QModelIndex &parent) const { + Q_UNUSED(parent); return t.getMathOperations().size(); } int MathModel::columnCount(const QModelIndex &parent) const { + Q_UNUSED(parent); return ColIndexLast; } @@ -168,7 +197,3 @@ void MathModel::deleteRow(unsigned int row) endRemoveRows(); } -void MathModel::rowsSwapped(unsigned int top) -{ -// emit dataChanged(createIndex(top, 0), createIndex(top+1, ColIndexLast - 1)); -} diff --git a/Software/PC_Application/Traces/Math/tracematheditdialog.h b/Software/PC_Application/Traces/Math/tracematheditdialog.h index f8bcc37..4f0f54e 100644 --- a/Software/PC_Application/Traces/Math/tracematheditdialog.h +++ b/Software/PC_Application/Traces/Math/tracematheditdialog.h @@ -30,7 +30,6 @@ public: void addOperation(TraceMath *math); void deleteRow(unsigned int row); - void rowsSwapped(unsigned int top); private: Trace &t; diff --git a/Software/PC_Application/Traces/Math/tracematheditdialog.ui b/Software/PC_Application/Traces/Math/tracematheditdialog.ui index 10bd270..44b44d1 100644 --- a/Software/PC_Application/Traces/Math/tracematheditdialog.ui +++ b/Software/PC_Application/Traces/Math/tracematheditdialog.ui @@ -17,7 +17,7 @@ Math functions - false + true diff --git a/Software/PC_Application/Traces/trace.cpp b/Software/PC_Application/Traces/trace.cpp index 22b0157..6bd3a9e 100644 --- a/Software/PC_Application/Traces/trace.cpp +++ b/Software/PC_Application/Traces/trace.cpp @@ -263,6 +263,7 @@ void Trace::updateLastMath(vector::reverse_iterator start) lastMath = newLast; // relay signals of end of math chain connect(lastMath, &TraceMath::outputSamplesChanged, this, &Trace::dataChanged); + emit outputSamplesChanged(0, data.size()); } } diff --git a/Software/PC_Application/Traces/traceeditdialog.ui b/Software/PC_Application/Traces/traceeditdialog.ui index 4842052..6a3f4bb 100644 --- a/Software/PC_Application/Traces/traceeditdialog.ui +++ b/Software/PC_Application/Traces/traceeditdialog.ui @@ -17,7 +17,7 @@ Edit Trace - false + true diff --git a/Software/PC_Application/icons/math_disabled.svg b/Software/PC_Application/icons/math_disabled.svg index d23978e..721c347 100644 --- a/Software/PC_Application/icons/math_disabled.svg +++ b/Software/PC_Application/icons/math_disabled.svg @@ -9,13 +9,13 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="17.410528mm" - height="9.36625mm" - viewBox="0 0 17.410529 9.3662499" + width="9.5429888mm" + height="8.6545076mm" + viewBox="0 0 9.5429888 8.6545077" version="1.1" id="svg8" - inkscape:version="0.92.3 (2405546, 2018-03-11)" - sodipodi:docname="math_disabled.svg"> + sodipodi:docname="math_disabled.svg" + inkscape:version="0.92.3 (2405546, 2018-03-11)"> + transform="translate(-84.809019,-129.3552)"> - f(x) + x="85.422623" + y="136.7381" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Padauk Book';-inkscape-font-specification:'Padauk Book, ';stroke-width:0.26458332px">M + style="opacity:1;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#fcffff;stroke-width:1.70000005;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 85.314335,137.50494 6.93903,-6.21735 0.812743,-0.72822 0.780583,-0.6994" + id="path819" + inkscape:connector-curvature="0" + sodipodi:nodetypes="cccc" /> + diff --git a/Software/PC_Application/icons/math_enabled.svg b/Software/PC_Application/icons/math_enabled.svg index e00ef46..00f3fa4 100644 --- a/Software/PC_Application/icons/math_enabled.svg +++ b/Software/PC_Application/icons/math_enabled.svg @@ -9,13 +9,13 @@ xmlns="http://www.w3.org/2000/svg" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - width="16.50322mm" - height="9.36625mm" - viewBox="0 0 16.50322 9.36625" + width="9.5429888mm" + height="8.6545076mm" + viewBox="0 0 9.5429888 8.6545077" version="1.1" id="svg8" - inkscape:version="0.92.3 (2405546, 2018-03-11)" - sodipodi:docname="math_enabled.svg"> + sodipodi:docname="math_enabled.svg" + inkscape:version="0.92.3 (2405546, 2018-03-11)"> + transform="translate(-84.809019,-129.3552)"> - f(x) + x="85.422623" + y="136.7381" + style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-family:'Padauk Book';-inkscape-font-specification:'Padauk Book, ';stroke-width:0.26458332px">M + +