CSV export + DFT math operation

This commit is contained in:
Jan Käberich 2020-12-11 20:28:40 +01:00
parent 79a990af47
commit 6e55eb02dd
27 changed files with 935 additions and 52 deletions

View File

@ -24,6 +24,7 @@ HEADERS += \
SpectrumAnalyzer/tracewidgetsa.h \
Tools/eseries.h \
Tools/impedancematchdialog.h \
Traces/Math/dft.h \
Traces/Math/medianfilter.h \
Traces/Math/tdr.h \
Traces/Math/tracemath.h \
@ -32,14 +33,15 @@ HEADERS += \
Traces/fftcomplex.h \
Traces/markerwidget.h \
Traces/trace.h \
Traces/tracecsvexport.h \
Traces/traceeditdialog.h \
Traces/traceexportdialog.h \
Traces/traceimportdialog.h \
Traces/tracemarker.h \
Traces/tracemarkermodel.h \
Traces/tracemodel.h \
Traces/traceplot.h \
Traces/tracesmithchart.h \
Traces/tracetouchstoneexport.h \
Traces/tracewidget.h \
Traces/tracexyplot.h \
Traces/xyplotaxisdialog.h \
@ -50,6 +52,7 @@ HEADERS += \
VNA/vna.h \
appwindow.h \
averaging.h \
csv.h \
json.hpp \
mode.h \
preferences.h \
@ -83,6 +86,7 @@ SOURCES += \
SpectrumAnalyzer/tracewidgetsa.cpp \
Tools/eseries.cpp \
Tools/impedancematchdialog.cpp \
Traces/Math/dft.cpp \
Traces/Math/medianfilter.cpp \
Traces/Math/tdr.cpp \
Traces/Math/tracemath.cpp \
@ -91,14 +95,15 @@ SOURCES += \
Traces/fftcomplex.cpp \
Traces/markerwidget.cpp \
Traces/trace.cpp \
Traces/tracecsvexport.cpp \
Traces/traceeditdialog.cpp \
Traces/traceexportdialog.cpp \
Traces/traceimportdialog.cpp \
Traces/tracemarker.cpp \
Traces/tracemarkermodel.cpp \
Traces/tracemodel.cpp \
Traces/traceplot.cpp \
Traces/tracesmithchart.cpp \
Traces/tracetouchstoneexport.cpp \
Traces/tracewidget.cpp \
Traces/tracexyplot.cpp \
Traces/xyplotaxisdialog.cpp \
@ -107,6 +112,7 @@ SOURCES += \
VNA/vna.cpp \
appwindow.cpp \
averaging.cpp \
csv.cpp \
main.cpp \
mode.cpp \
preferences.cpp \
@ -132,6 +138,8 @@ FORMS += \
Device/manualcontroldialog.ui \
Generator/signalgenwidget.ui \
Tools/impedancematchdialog.ui \
Traces/Math/dftdialog.ui \
Traces/Math/dftexplanationwidget.ui \
Traces/Math/medianexplanationwidget.ui \
Traces/Math/medianfilterdialog.ui \
Traces/Math/newtracemathdialog.ui \
@ -140,9 +148,10 @@ FORMS += \
Traces/Math/tracematheditdialog.ui \
Traces/markerwidget.ui \
Traces/smithchartdialog.ui \
Traces/tracecsvexport.ui \
Traces/traceeditdialog.ui \
Traces/traceexportdialog.ui \
Traces/traceimportdialog.ui \
Traces/tracetouchstoneexport.ui \
Traces/tracewidget.ui \
Traces/xyplotaxisdialog.ui \
VNA/portextensioneditdialog.ui \

View File

@ -1,7 +1,15 @@
#include "tracewidgetsa.h"
#include "Traces/tracecsvexport.h"
TraceWidgetSA::TraceWidgetSA(TraceModel &model, QWidget *parent)
: TraceWidget(model, parent)
{
}
void TraceWidgetSA::exportDialog()
{
auto csv = new TraceCSVExport(model);
csv->show();
}

View File

@ -8,7 +8,7 @@ class TraceWidgetSA : public TraceWidget
public:
TraceWidgetSA(TraceModel &model, QWidget *parent = nullptr);
protected slots:
virtual void exportDialog() override {};
virtual void exportDialog() override;
virtual void importDialog() override {};
protected:

View File

@ -0,0 +1,183 @@
#include "dft.h"
#include "tdr.h"
#include "Traces/fftcomplex.h"
#include "unit.h"
#include "ui_dftdialog.h"
#include "ui_dftexplanationwidget.h"
using namespace std;
Math::DFT::DFT()
{
automaticDC = true;
DCfreq = 1000000000.0;
connect(&window, &WindowFunction::changed, this, &DFT::updateDFT);
}
TraceMath::DataType Math::DFT::outputType(TraceMath::DataType inputType)
{
if(inputType == DataType::Time) {
return DataType::Frequency;
} else {
return DataType::Invalid;
}
}
QString Math::DFT::description()
{
QString ret = "DFT (";
if(automaticDC) {
ret += "automatic DC)";
} else {
ret += "DC:" + Unit::ToString(DCfreq, "Hz", " kMG", 6) + ")";
}
ret += ", window: " + window.getDescription();
return ret;
}
void Math::DFT::edit()
{
auto d = new QDialog();
auto ui = new Ui::DFTDialog;
ui->setupUi(d);
ui->windowBox->setLayout(new QVBoxLayout);
ui->windowBox->layout()->addWidget(window.createEditor());
connect(ui->DCautomatic, &QRadioButton::toggled, [=](bool automatic){
automaticDC = automatic;
ui->freq->setEnabled(!automatic);
updateDFT();
});
if(automaticDC) {
ui->DCautomatic->setChecked(true);
} else {
ui->DCmanual->setChecked(true);
}
ui->freq->setUnit("Hz");
ui->freq->setPrecision(6);
ui->freq->setPrefixes(" kMG");
ui->freq->setValue(DCfreq);
connect(ui->freq, &SIUnitEdit::valueChanged, [=](double newval){
DCfreq = newval;
updateDFT();
});
connect(ui->buttonBox, &QDialogButtonBox::accepted, d, &QDialog::accept);
d->show();
}
QWidget *Math::DFT::createExplanationWidget()
{
auto w = new QWidget();
auto ui = new Ui::DFTExplanationWidget;
ui->setupUi(w);
return w;
}
nlohmann::json Math::DFT::toJSON()
{
nlohmann::json j;
j["automatic_DC"] = automaticDC;
j["window"] = window.toJSON();
if(!automaticDC) {
j["DC"] = DCfreq;
}
return j;
}
void Math::DFT::fromJSON(nlohmann::json j)
{
automaticDC = j.value("automatic_DC", true);
DCfreq = j.value("DC", 1000000000.0);
if(j.contains("window")) {
window.fromJSON(j["window"]);
}
}
void Math::DFT::inputSamplesChanged(unsigned int begin, unsigned int end)
{
Q_UNUSED(end);
if(input->rData().size() < 2) {
// not enough input data
data.clear();
emit outputSamplesChanged(0, 0);
warning("Not enough input samples");
return;
}
// DFT is computationally expensive, only update at the end of sweep -> check if this is the first changed data in the next sweep
if(begin != 0) {
// not the end, do nothing
return;
}
double DC = DCfreq;
TDR *tdr = nullptr;
if(automaticDC) {
// find the last operation that transformed from the frequency domain to the time domain
auto in = input;
while(in->getInput()->getDataType() != DataType::Frequency) {
in = input->getInput();
}
switch(in->getType()) {
case Type::TDR: {
tdr = static_cast<TDR*>(in);
if(tdr->getMode() == TDR::Mode::Lowpass) {
DC = 0;
} else {
// bandpass mode, assume DC is in the middle of the frequency data
DC = tdr->getInput()->getSample(tdr->getInput()->numSamples()/2).x;
}
}
break;
default:
// unknown, assume DC is in the middle of the frequency data
DC = in->getInput()->getSample(in->getInput()->numSamples()/2).x;
break;
}
}
auto samples = input->rData().size();
auto timeSpacing = input->rData()[1].x - input->rData()[0].x;
vector<complex<double>> timeDomain(samples);
for(unsigned int i=0;i<samples;i++) {
timeDomain.at(i) = input->rData()[i].y;
}
Fft::shift(timeDomain, false);
window.apply(timeDomain);
Fft::shift(timeDomain, true);
Fft::transform(timeDomain, false);
// shift DC bin into the middle
Fft::shift(timeDomain, false);
double binSpacing = 1.0 / (timeSpacing * timeDomain.size());
data.clear();
int DCbin = timeDomain.size() / 2, startBin = 0;
if(DC > 0) {
data.resize(timeDomain.size());
} else {
startBin = (timeDomain.size()+1) / 2;
data.resize(timeDomain.size()/2);
}
// reverse effect of frequency domain window function from TDR (if available)
if(tdr) {
tdr->getWindow().reverse(timeDomain);
}
for(int i = startBin;(unsigned int) i<timeDomain.size();i++) {
auto freq = (i - DCbin) * binSpacing + DC;
data[i - startBin].x = round(freq);
data[i - startBin].y = timeDomain.at(i);
}
emit outputSamplesChanged(0, data.size());
success();
}
void Math::DFT::updateDFT()
{
if(dataType != DataType::Invalid) {
inputSamplesChanged(0, input->rData().size());
}
}

View File

@ -0,0 +1,36 @@
#ifndef DFT_H
#define DFT_H
#include "tracemath.h"
#include "windowfunction.h"
namespace Math {
class DFT : public TraceMath
{
public:
DFT();
DataType outputType(DataType inputType) override;
QString description() override;
void edit() override;
static QWidget* createExplanationWidget();
virtual nlohmann::json toJSON() override;
virtual void fromJSON(nlohmann::json j) override;
Type getType() override {return Type::DFT;};
public slots:
void inputSamplesChanged(unsigned int begin, unsigned int end) override;
private:
void updateDFT();
bool automaticDC;
double DCfreq;
WindowFunction window;
};
}
#endif // DFT_H

View File

@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DFTDialog</class>
<widget class="QDialog" name="DFTDialog">
<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>DFT</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,0">
<item>
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>DC frequency (center)</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="DCautomatic">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Automatic</string>
</property>
<attribute name="buttonGroup">
<string notr="true">buttonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="DCmanual">
<property name="enabled">
<bool>true</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>Frequency:</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="SIUnitEdit" name="freq">
<property name="enabled">
<bool>true</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>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DFTExplanationWidget</class>
<widget class="QWidget" name="DFTExplanationWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>364</width>
<height>412</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="widget">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;&lt;span style=&quot; font-weight:600;&quot;&gt;DFT&lt;/span&gt;&lt;/p&gt;&lt;p&gt;Performs a DFT on the time domain data, transforming it back into the frequency domain.&lt;/p&gt;&lt;p&gt;There are two possible choices for the DC bin frequency:&lt;/p&gt;&lt;ul style=&quot;margin-top: 0px; margin-bottom: 0px; margin-left: 0px; margin-right: 0px; -qt-list-indent: 1;&quot;&gt;&lt;li style=&quot; margin-top:12px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Automatic: Check frequency span of the original data that was used to create the time domain values and use the same frequency values for the DFT.&lt;/li&gt;&lt;li style=&quot; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px;&quot;&gt;Manual: User selectable frequency which is used for the DC bin.&lt;/li&gt;&lt;/ul&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;p&gt;&lt;br/&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -138,6 +138,9 @@ nlohmann::json TDR::toJSON()
void TDR::fromJSON(nlohmann::json j)
{
if(j.contains("window")) {
window.fromJSON(j["window"]);
}
if(j.value("bandpass_mode", true)) {
mode = Mode::Bandpass;
} else {
@ -166,7 +169,7 @@ void TDR::inputSamplesChanged(unsigned int begin, unsigned int end)
return;
}
vector<complex<double>> frequencyDomain;
auto stepSize = input->rData()[1].x - input->rData()[0].x;
auto stepSize = (input->rData().back().x - input->rData().front().x) / (input->rData().size() - 1);
if(mode == Mode::Lowpass) {
if(stepResponse) {
auto steps = input->rData().size();
@ -180,6 +183,7 @@ void TDR::inputSamplesChanged(unsigned int begin, unsigned int end)
if(firstStep * steps != input->rData().back().x) {
// data is not available with correct frequency spacing, calculate required steps
steps = input->rData().back().x / firstStep;
stepSize = firstStep;
}
frequencyDomain.resize(2 * steps + 1);
// copy frequencies, use the flipped conjugate for negative part
@ -259,3 +263,13 @@ void TDR::updateTDR()
inputSamplesChanged(0, input->rData().size());
}
}
const WindowFunction& TDR::getWindow() const
{
return window;
}
TDR::Mode TDR::getMode() const
{
return mode;
}

View File

@ -21,15 +21,18 @@ public:
virtual void fromJSON(nlohmann::json j) override;
Type getType() override {return Type::TDR;};
enum class Mode {
Lowpass,
Bandpass,
};
Mode getMode() const;
const WindowFunction& getWindow() const;
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;

View File

@ -2,6 +2,7 @@
#include "medianfilter.h"
#include "tdr.h"
#include "dft.h"
#include "Traces/trace.h"
TraceMath::TraceMath()
@ -18,6 +19,8 @@ TraceMath *TraceMath::createMath(TraceMath::Type type)
return new Math::MedianFilter();
case Type::TDR:
return new Math::TDR();
case Type::DFT:
return new Math::DFT();
default:
return nullptr;
}
@ -35,6 +38,10 @@ TraceMath::TypeInfo TraceMath::getInfo(TraceMath::Type type)
ret.name = "TDR";
ret.explanationWidget = Math::TDR::createExplanationWidget();
break;
case Type::DFT:
ret.name = "DFT";
ret.explanationWidget = Math::DFT::createExplanationWidget();
break;
default:
break;
}
@ -161,6 +168,11 @@ void TraceMath::updateStepResponse(bool valid)
}
}
TraceMath *TraceMath::getInput() const
{
return input;
}
QString TraceMath::getStatusDescription() const
{
return statusString;

View File

@ -72,6 +72,7 @@ public:
enum class Type {
MedianFilter,
TDR,
DFT,
// Add new math operations here, do not explicitly assign values and keep the Last entry at the last position
Last,
};
@ -106,6 +107,8 @@ public:
// returns the trace this math operation is attached to
Trace* root();
TraceMath *getInput() const;
public slots:
// some values of the input data have changed, begin/end determine which sample(s) has changed
virtual void inputSamplesChanged(unsigned int begin, unsigned int end){Q_UNUSED(begin) Q_UNUSED(end)};

View File

@ -30,7 +30,7 @@ WindowFunction::WindowFunction(WindowFunction::Type type)
chebyshev_alpha = 5;
}
void WindowFunction::apply(std::vector<std::complex<double> > &data)
void WindowFunction::apply(std::vector<std::complex<double> > &data) const
{
unsigned int N = data.size();
for(unsigned int n = 0;n<N;n++) {
@ -38,6 +38,14 @@ void WindowFunction::apply(std::vector<std::complex<double> > &data)
}
}
void WindowFunction::reverse(std::vector<std::complex<double> > &data) const
{
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();
@ -148,7 +156,7 @@ void WindowFunction::fromJSON(nlohmann::json j)
}
}
double WindowFunction::getFactor(unsigned int n, unsigned int N)
double WindowFunction::getFactor(unsigned int n, unsigned int N) const
{
// all formulas from https://en.wikipedia.org/wiki/Window_function
switch(type) {

View File

@ -25,7 +25,8 @@ public:
WindowFunction(Type type = Type::Hamming);
void apply(std::vector<std::complex<double>>& data);
void apply(std::vector<std::complex<double>>& data) const;
void reverse(std::vector<std::complex<double>>& data) const;
QWidget *createEditor();
@ -39,7 +40,7 @@ signals:
void changed();
private:
double getFactor(unsigned int n, unsigned int N);
double getFactor(unsigned int n, unsigned int N) const;
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

View File

@ -155,7 +155,7 @@ static size_t reverseBits(size_t val, int width) {
void Fft::shift(std::vector<std::complex<double> > &vec, bool inverse)
{
int rotate_len = vec.size() / 2;
if(vec.size() % 0x01 != 0) {
if((vec.size() & 0x01) != 0) {
// odd size, behavior depends on whether this is an inverse shift
if(!inverse) {
rotate_len++;

View File

@ -627,6 +627,10 @@ int Trace::index(double x)
auto lower = lower_bound(lastMath->rData().begin(), lastMath->rData().end(), x, [](const Data &lhs, const double x) -> bool {
return lhs.x < x;
});
if(lower == lastMath->rData().end()) {
// actually beyond the last sample, return the index of the last anyway to avoid access past data
return lastMath->rData().size() - 1;
}
return lower - lastMath->rData().begin();
}

View File

@ -0,0 +1,184 @@
#include "tracecsvexport.h"
#include "ui_tracecsvexport.h"
#include <QDialogButtonBox>
#include <QPushButton>
#include <QFileDialog>
#include "csv.h"
using namespace std;
TraceCSVExport::TraceCSVExport(TraceModel &traceModel, QWidget *parent) :
QDialog(parent),
ui(new Ui::TraceCSVExport),
model(traceModel.getTraces())
{
ui->setupUi(this);
ui->listView->setModel(&model);
ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
connect(&model, &TraceCSVModel::selectionChanged, ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::setEnabled);
}
TraceCSVExport::~TraceCSVExport()
{
delete ui;
}
void TraceCSVExport::on_buttonBox_accepted()
{
auto traces = model.tracesToExport();
if(traces.size() == 0) {
return;
}
auto filename = QFileDialog::getSaveFileName(nullptr, "Save calibration data", "", "CSV files (*.csv)", nullptr, QFileDialog::DontUseNativeDialog);
if(filename.isEmpty()) {
// aborted selection
return;
}
if(!filename.endsWith(".csv")) {
filename.append(".csv");
}
CSV csv;
// create the first column (X data)
vector<double> X;
QString Xname = traces[0]->outputType() == Trace::DataType::Frequency ? "Frequency" : "Time";
auto samples = traces[0]->numSamples();
for(unsigned int i=0;i<samples;i++) {
X.push_back(traces[0]->sample(i).x);
}
csv.addColumn(Xname, X);
// add the trace data
for(auto t : traces) {
vector<double> real;
vector<double> imag;
auto samples = t->numSamples();
for(unsigned int i=0;i<samples;i++) {
real.push_back(t->sample(i).y.real());
imag.push_back(t->sample(i).y.imag());
}
// check if this is a real or complex trace
bool allZeros = std::all_of(imag.begin(), imag.end(), [](double i) { return i==0.0; });
if(allZeros) {
// only real values, one column is enough
csv.addColumn(t->name(), real);
} else {
// complex values, need two columns
csv.addColumn(t->name()+"_real", real);
csv.addColumn(t->name()+"_imag", imag);
}
}
csv.toFile(filename);
}
TraceCSVModel::TraceCSVModel(std::vector<Trace *> traces, QObject *parent)
: QAbstractListModel(parent)
{
this->traces = traces;
save.resize(traces.size(), false);
enabled.resize(traces.size(), true);
points = 0;
updateEnabledTraces();
}
int TraceCSVModel::rowCount(const QModelIndex &parent) const
{
Q_UNUSED(parent);
return traces.size();
}
QVariant TraceCSVModel::data(const QModelIndex &index, int role) const
{
if(!index.isValid()) {
return QVariant();
}
switch(role) {
case Qt::CheckStateRole:
if(save[index.row()]) {
return Qt::Checked;
} else {
return Qt::Unchecked;
}
case Qt::DisplayRole:
return traces[index.row()]->name();
default:
return QVariant();
}
}
bool TraceCSVModel::setData(const QModelIndex &index, const QVariant &value, int role)
{
if(!index.isValid()) {
return false;
}
if(role==Qt::CheckStateRole) {
save[index.row()] = !save[index.row()];
updateEnabledTraces();
return true;
} else {
return QAbstractItemModel::setData(index, value, role);
}
}
Qt::ItemFlags TraceCSVModel::flags(const QModelIndex &index) const
{
unsigned int flags = Qt::ItemIsUserCheckable;
if(index.isValid()) {
if(enabled[index.row()]) {
flags |= Qt::ItemIsEnabled;
}
}
return (Qt::ItemFlags) flags;
}
std::vector<Trace *> TraceCSVModel::tracesToExport()
{
vector<Trace*> ret;
for(unsigned int i=0;i<traces.size();i++) {
if(save[i]) {
ret.push_back(traces[i]);
}
}
return ret;
}
void TraceCSVModel::updateEnabledTraces()
{
beginResetModel();
// find first selected trace and use its settings
points = 0;
type = Trace::DataType::Invalid;
for(unsigned int i=0;i<traces.size();i++) {
if(save[i]) {
points = traces[i]->numSamples();
minX = traces[i]->minX();
maxX = traces[i]->maxX();
type = traces[i]->outputType();
}
}
// second pass: compare to the settings and only enable identical traces
for(unsigned int i=0;i<traces.size();i++) {
auto enableTrace = true;
if(traces[i]->numSamples() == 0 || traces[i]->outputType() == Trace::DataType::Invalid) {
// trace has no valid data, unable to export
enableTrace = false;
}
if(points > 0 && type != Trace::DataType::Invalid) {
// check if this trace matches the already selected one
if((traces[i]->numSamples() != points)
|| (traces[i]->minX() != minX)
|| (traces[i]->maxX() != maxX)
|| (traces[i]->outputType() != type)) {
// different settings, not possible to export in this combination
enableTrace = false;
}
}
enabled[i] = enableTrace;
if(!enableTrace) {
save[i] = false;
}
}
endResetModel();
emit selectionChanged(points > 0);
}

View File

@ -0,0 +1,53 @@
#ifndef TRACECSVEXPORT_H
#define TRACECSVEXPORT_H
#include <QDialog>
#include "tracemodel.h"
namespace Ui {
class TraceCSVExport;
}
class TraceCSVModel : public QAbstractListModel
{
Q_OBJECT
public:
TraceCSVModel(std::vector<Trace*> traces, QObject *parent = 0);
~TraceCSVModel(){};
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
Qt::ItemFlags flags(const QModelIndex &index) const override;
std::vector<Trace*> tracesToExport();
signals:
void selectionChanged(bool anySelected);
private:
// check which traces can be exported (only allow traces on the same domain with identical span/time)
void updateEnabledTraces();
unsigned int points;
double minX, maxX;
Trace::DataType type;
std::vector<Trace*> traces;
std::vector<bool> enabled;
std::vector<bool> save;
};
class TraceCSVExport : public QDialog
{
Q_OBJECT
public:
explicit TraceCSVExport(TraceModel &model, QWidget *parent = nullptr);
~TraceCSVExport();
private slots:
void on_buttonBox_accepted();
private:
Ui::TraceCSVExport *ui;
TraceCSVModel model;
};
#endif // TRACECSVEXPORT_H

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TraceCSVExport</class>
<widget class="QDialog" name="TraceCSVExport">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>286</width>
<height>322</height>
</rect>
</property>
<property name="windowTitle">
<string>CSV Export</string>
</property>
<property name="modal">
<bool>true</bool>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QListView" name="listView">
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TraceCSVExport</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>298</x>
<y>300</y>
</hint>
<hint type="destinationlabel">
<x>298</x>
<y>160</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TraceCSVExport</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>298</x>
<y>300</y>
</hint>
<hint type="destinationlabel">
<x>298</x>
<y>160</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -220,7 +220,7 @@ bool TraceMarkerModel::setData(const QModelIndex &index, const QVariant &value,
Qt::ItemFlags TraceMarkerModel::flags(const QModelIndex &index) const
{
int flags = Qt::NoItemFlags;
int flags = Qt::ItemIsSelectable;
switch(index.column()) {
case ColIndexNumber: flags |= Qt::ItemIsEnabled | Qt::ItemIsEditable; break;
case ColIndexTrace: flags |= Qt::ItemIsEnabled | Qt::ItemIsEditable; break;

View File

@ -1,13 +1,13 @@
#include "traceexportdialog.h"
#include "tracetouchstoneexport.h"
#include "ui_traceexportdialog.h"
#include <QDebug>
#include <QFileDialog>
#include "touchstone.h"
#include <QPushButton>
TraceExportDialog::TraceExportDialog(TraceModel &model, QWidget *parent) :
TraceTouchstoneExport::TraceTouchstoneExport(TraceModel &model, QWidget *parent) :
QDialog(parent),
ui(new Ui::TraceExportDialog),
ui(new Ui::TraceTouchstoneExport),
model(model),
freqsSet(false)
{
@ -17,12 +17,12 @@ TraceExportDialog::TraceExportDialog(TraceModel &model, QWidget *parent) :
on_sbPorts_valueChanged(ui->sbPorts->value());
}
TraceExportDialog::~TraceExportDialog()
TraceTouchstoneExport::~TraceTouchstoneExport()
{
delete ui;
}
bool TraceExportDialog::setTrace(int portFrom, int portTo, Trace *t)
bool TraceTouchstoneExport::setTrace(int portFrom, int portTo, Trace *t)
{
if(portFrom < 0 || portFrom >= ui->sbPorts->value() || portTo < 0 || portTo >= ui->sbPorts->value()) {
// invalid port selection
@ -46,7 +46,7 @@ bool TraceExportDialog::setTrace(int portFrom, int portTo, Trace *t)
}
}
bool TraceExportDialog::setPortNum(int ports)
bool TraceTouchstoneExport::setPortNum(int ports)
{
if(ports < 1 || ports > 4) {
return false;
@ -55,7 +55,7 @@ bool TraceExportDialog::setPortNum(int ports)
return true;
}
void TraceExportDialog::on_buttonBox_accepted()
void TraceTouchstoneExport::on_buttonBox_accepted()
{
auto filename = QFileDialog::getSaveFileName(this, "Select file for exporting traces", "", "Touchstone files (*.s1p *.s2p *.s3p *.s4p)", nullptr, QFileDialog::DontUseNativeDialog);
if(filename.length() > 0) {
@ -99,7 +99,7 @@ void TraceExportDialog::on_buttonBox_accepted()
}
}
void TraceExportDialog::on_sbPorts_valueChanged(int ports)
void TraceTouchstoneExport::on_sbPorts_valueChanged(int ports)
{
// remove the previous widgets
QGridLayout *layout = static_cast<QGridLayout*>(ui->gbTraces->layout());
@ -118,6 +118,10 @@ void TraceExportDialog::on_sbPorts_valueChanged(int ports)
// create possible trace selections
c->addItem("None");
for(auto t : availableTraces) {
if(t->getDataType() != Trace::DataType::Frequency) {
// can only add frequency traces
continue;
}
if(i == j && !t->isReflection()) {
// can not add through measurement at reflection port
continue;
@ -137,7 +141,7 @@ void TraceExportDialog::on_sbPorts_valueChanged(int ports)
}
}
void TraceExportDialog::selectionChanged(QComboBox *w)
void TraceTouchstoneExport::selectionChanged(QComboBox *w)
{
if(w->currentIndex() != 0 && !freqsSet) {
// the first trace has been selected, extract frequency info

View File

@ -1,5 +1,5 @@
#ifndef TRACEEXPORTDIALOG_H
#define TRACEEXPORTDIALOG_H
#ifndef TRACETOUCHSTONEEXPORT_H
#define TRACETOUCHSTONEEXPORT_H
#include <QDialog>
#include <QComboBox>
@ -7,16 +7,16 @@
#include <QSignalMapper>
namespace Ui {
class TraceExportDialog;
class TraceTouchstoneExport;
}
class TraceExportDialog : public QDialog
class TraceTouchstoneExport : public QDialog
{
Q_OBJECT
public:
explicit TraceExportDialog(TraceModel &model, QWidget *parent = nullptr);
~TraceExportDialog();
explicit TraceTouchstoneExport(TraceModel &model, QWidget *parent = nullptr);
~TraceTouchstoneExport();
bool setTrace(int portFrom, int portTo, Trace *t);
bool setPortNum(int ports);
@ -26,7 +26,7 @@ private slots:
void selectionChanged(QComboBox *w);
private:
Ui::TraceExportDialog *ui;
Ui::TraceTouchstoneExport *ui;
TraceModel &model;
std::vector<std::vector<QComboBox*>> cTraces;

View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TraceExportDialog</class>
<widget class="QDialog" name="TraceExportDialog">
<class>TraceTouchstoneExport</class>
<widget class="QDialog" name="TraceTouchstoneExport">
<property name="geometry">
<rect>
<x>0</x>
@ -11,7 +11,7 @@
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
<string>Touchstone Export</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="0,1,0,0">
<item>
@ -226,7 +226,7 @@
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TraceExportDialog</receiver>
<receiver>TraceTouchstoneExport</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">

View File

@ -4,7 +4,7 @@
#include <QKeyEvent>
#include "traceeditdialog.h"
#include "traceimportdialog.h"
#include "traceexportdialog.h"
#include "tracetouchstoneexport.h"
#include <QFileDialog>
#include <QDrag>
#include <QMimeData>

View File

@ -2,28 +2,42 @@
#include <QFileDialog>
#include "Traces/traceimportdialog.h"
#include "Traces/traceexportdialog.h"
#include "Traces/tracetouchstoneexport.h"
#include "Traces/tracecsvexport.h"
#include "ui_tracewidget.h"
#include <QMenu>
TraceWidgetVNA::TraceWidgetVNA(TraceModel &model, QWidget *parent)
: TraceWidget(model, parent)
{
auto exportMenu = new QMenu();
auto exportTouchstone = new QAction("Touchstone");
auto exportCSV = new QAction("CSV");
exportMenu->addAction(exportTouchstone);
exportMenu->addAction(exportCSV);
}
ui->bExport->setMenu(exportMenu);
void TraceWidgetVNA::exportDialog()
{
auto e = new TraceExportDialog(model);
// Attempt to set default traces (this will result in correctly populated
// 2 port export if the initial 4 traces have not been modified)
e->setPortNum(2);
auto traces = model.getTraces();
for(unsigned int i=0;i<4;i++) {
if(i >= traces.size()) {
break;
connect(exportTouchstone, &QAction::triggered, [&]() {
auto e = new TraceTouchstoneExport(model);
// Attempt to set default traces (this will result in correctly populated
// 2 port export if the initial 4 traces have not been modified)
e->setPortNum(2);
auto traces = model.getTraces();
for(unsigned int i=0;i<4;i++) {
if(i >= traces.size()) {
break;
}
e->setTrace(i%2, i/2, traces[i]);
}
e->setTrace(i%2, i/2, traces[i]);
}
e->show();
e->show();
});
connect(exportCSV, &QAction::triggered, [&]() {
auto e = new TraceCSVExport(model);
e->show();
});
}
void TraceWidgetVNA::importDialog()

View File

@ -8,7 +8,7 @@ class TraceWidgetVNA : public TraceWidget
public:
TraceWidgetVNA(TraceModel &model, QWidget *parent = nullptr);
protected slots:
virtual void exportDialog() override;
virtual void exportDialog() override {};
virtual void importDialog() override;
protected:

View File

@ -0,0 +1,95 @@
#include "csv.h"
#include <exception>
#include <fstream>
#include <QStringList>
#include <iomanip>
using namespace std;
CSV::CSV()
{
}
CSV CSV::fromFile(QString filename, char sep)
{
CSV csv;
ifstream file;
file.open(filename.toStdString());
if(!file.is_open()) {
throw runtime_error("Unable to open file:"+filename.toStdString());
}
string line;
bool firstLine = true;
while(getline(file, line)) {
auto qline = QString::fromStdString(line);
auto stringList = qline.split(sep);
if(firstLine) {
// create columns and set headers
for(auto l : stringList) {
Column c;
c.header = l;
csv._columns.push_back(c);
}
firstLine = false;
} else {
// not the header, attempt to parse data
for(unsigned int i=0;i < csv._columns.size();i++) {
double value = 0.0;
if(i < (unsigned int) stringList.size()) {
value = stringList[i].toDouble();
}
csv._columns[i].data.push_back(value);
}
}
}
return csv;
}
void CSV::toFile(QString filename, char sep)
{
ofstream file;
file.open(filename.toStdString());
file << setprecision(10);
unsigned maxlen = 0;
for(auto c : _columns) {
file << c.header.toStdString();
file << sep;
if(c.data.size() > maxlen) {
maxlen = c.data.size();
}
}
file << endl;
for(unsigned int i=0;i<maxlen;i++) {
for(auto c : _columns) {
if(i < c.data.size()) {
file << c.data[i] << sep;
}
}
file << endl;
}
file.close();
}
std::vector<double> CSV::getColumn(QString header)
{
for(auto c : _columns) {
if(c.header == header) {
return c.data;
}
}
throw runtime_error("Header name not found");
}
std::vector<double> CSV::getColumn(unsigned int index)
{
return _columns.at(index).data;
}
void CSV::addColumn(QString name, const std::vector<double> &data)
{
Column c;
c.header = name;
c.data = data;
_columns.push_back(c);
}

View File

@ -0,0 +1,30 @@
#ifndef CSV_H
#define CSV_H
#include <QString>
#include <vector>
class CSV
{
public:
CSV();
static CSV fromFile(QString filename, char sep = ',');
void toFile(QString filename, char sep = ',');
std::vector<double> getColumn(QString header);
std::vector<double> getColumn(unsigned int index);
unsigned int columns() { return _columns.size();}
void addColumn(QString name, const std::vector<double> &data);
private:
class Column {
public:
QString header;
std::vector<double> data;
};
std::vector<Column> _columns;
};
#endif // CSV_H