From 72f2a7244573a4309a83b3242a55fb1601ae0599 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Thu, 11 Nov 2021 21:01:52 +0100 Subject: [PATCH 01/24] fix plot drawing in case of autoscale and no trace data --- Software/PC_Application/Traces/tracexyplot.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/Software/PC_Application/Traces/tracexyplot.cpp b/Software/PC_Application/Traces/tracexyplot.cpp index 56f6e75..37f540a 100644 --- a/Software/PC_Application/Traces/tracexyplot.cpp +++ b/Software/PC_Application/Traces/tracexyplot.cpp @@ -740,10 +740,14 @@ void TraceXYPlot::updateAxisTicks() min -= range * 0.05; max += range * 0.05; } - YAxis[i].rangeMin = min; - YAxis[i].rangeMax = max; - YAxis[i].rangeDiv = createAutomaticTicks(YAxis[i].ticks, min, max, 8); + } else { + // max/min still at default values, no valid samples are available for this axis, use default range + max = 1.0; + min = -1.0; } + YAxis[i].rangeMin = min; + YAxis[i].rangeMax = max; + YAxis[i].rangeDiv = createAutomaticTicks(YAxis[i].ticks, min, max, 8); } } } From 71b095cf2e0da31312a65401813fdcbd92817f9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Thu, 11 Nov 2021 22:09:45 +0100 Subject: [PATCH 02/24] Fix pointNum in SA result for DFT mode with narrow spans --- Software/VNA_embedded/Application/SpectrumAnalyzer.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Software/VNA_embedded/Application/SpectrumAnalyzer.cpp b/Software/VNA_embedded/Application/SpectrumAnalyzer.cpp index 0d1d973..e11da5e 100644 --- a/Software/VNA_embedded/Application/SpectrumAnalyzer.cpp +++ b/Software/VNA_embedded/Application/SpectrumAnalyzer.cpp @@ -300,6 +300,10 @@ void SA::Work() { if(!s.SignalID || signalIDstep >= signalIDsteps - 1) { // this measurement point is done, handle result according to detector for(uint16_t i=0;i= points) { + // DFT covered more points than are required for the remaining sweep, can abort here + break; + } uint16_t binIndex = (pointCnt + i) / binSize; uint32_t pointInBin = (pointCnt + i) % binSize; bool lastPointInBin = pointInBin >= binSize - 1; @@ -386,7 +390,7 @@ void SA::Work() { Communication::Send(packet); } - if(pointCnt < points - DFTpoints) { + if(pointCnt + DFTpoints < points) { pointCnt += DFTpoints; } else { pointCnt = 0; From f3083d70696400c4d33a9ffbfa46977ee4fcc237 Mon Sep 17 00:00:00 2001 From: Kiara Navarro Date: Sat, 6 Nov 2021 21:35:18 -0300 Subject: [PATCH 03/24] app: add LibreVNA logo and use it as icon launcher Now it's possible to identify LibreVNA with a logo. This logo would be visible as an icon when app is launched. A banner containing same logo has been added to README. Also, a desktop launcher file is created to be used in GNU/Linux environments enabling in this way the ability to add as favorite. Closes #57 --- README.md | 3 ++- Software/PC_Application/LibreVNA-GUI.pro | 3 ++- Software/PC_Application/appwindow.cpp | 2 ++ Software/PC_Application/resources/banner.png | Bin 0 -> 31897 bytes .../PC_Application/resources/librevna.desktop | 9 +++++++++ Software/PC_Application/resources/librevna.png | Bin 0 -> 52696 bytes Software/PC_Application/resources/librevna.qrc | 5 +++++ 7 files changed, 20 insertions(+), 2 deletions(-) create mode 100644 Software/PC_Application/resources/banner.png create mode 100644 Software/PC_Application/resources/librevna.desktop create mode 100644 Software/PC_Application/resources/librevna.png create mode 100644 Software/PC_Application/resources/librevna.qrc diff --git a/README.md b/README.md index d1dd414..a7146a6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -# LibreVNA +![LibreVNA](Software/PC_Application/resources/banner.png) + **100kHz to 6GHz VNA** This is the improved version of my [first attempt](https://www.github.com/jankae/VNA) at a VNA. diff --git a/Software/PC_Application/LibreVNA-GUI.pro b/Software/PC_Application/LibreVNA-GUI.pro index 702eb76..2f02f04 100644 --- a/Software/PC_Application/LibreVNA-GUI.pro +++ b/Software/PC_Application/LibreVNA-GUI.pro @@ -298,7 +298,8 @@ FORMS += \ DISTFILES += RESOURCES += \ - icons.qrc + icons.qrc \ + resources/librevna.qrc CONFIG += c++17 REVISION = $$system(git rev-parse HEAD) diff --git a/Software/PC_Application/appwindow.cpp b/Software/PC_Application/appwindow.cpp index 84eaa70..ede5e71 100644 --- a/Software/PC_Application/appwindow.cpp +++ b/Software/PC_Application/appwindow.cpp @@ -78,6 +78,8 @@ AppWindow::AppWindow(QWidget *parent) // qDebug().setVerbosity(0); qDebug() << "Application start"; + this->setWindowIcon(QIcon(":/app/logo.png")); + parser.setApplicationDescription(qlibrevnaApp->applicationName()); parser.addHelpOption(); parser.addVersionOption(); diff --git a/Software/PC_Application/resources/banner.png b/Software/PC_Application/resources/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..c6a617e44787c299f24a32708d792354eb7b0550 GIT binary patch literal 31897 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 (u)=wv#7SE@E#2ZPH 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⋘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 z zKgDk!Q1ismir)T?S(m&?u_hMAPT3piM5mKQ@-{y57-o7uk>^nn;*^dRXS8#RyA0( zvygG$rV-DXKDEg*5e+BaLd#lJ@cuZ!=jd;C?qV|!_jzcRIBq^cQ<%nE2pa&ax_jAy zeN)X@PyR-IM<$$w7ugp#8x8D-fw}d9X32aDt&E$ie?(0AjCa}is|VLD*M(5J2QF66 zX4>o07dUB(BHrew4A$0m?D&44H1~Vvx%4sZ{BG^ijVK$mihKW5#-@Okz;tVLH}OHs3L?5RhBbL%QWduu~s~!^8)(U z=^3RD#@toN+HVU`BiqKh=q0aW1yelt0HbnbGg(Yfy*eE% zGj_<^Qt%EU0Dy3InPFTXcaJzHhHHKXJSR2)M-t#29bY0cYhBYydA7}d{Bf4QI*^Ng z#&%B)ZR6%SqnG?f#P&75Krwguk95KcliuLE{HLWx%II7j1)hyN3iITJH+#*xQ=ro! zLoDxGj;G7xQUjquVF5fRo46S+XgwyrHbjJ&D6kVkXS-<~CL*p%Hc~S|E%9|5wUGsu#K~Buqk%_U>r`Il7%E>{)MM$N zd=IGr4hp3E{lgxNQ1jTNjas~2)!&${HSd#;+|^~^lm~`d+~H^A9;1KEEHG1CPL#(o zrMqY_#xK?P@oO9T=EfhhP1ocN_X%g|z!H4^y%d5wqdB0g2M59=p-nXAA(Iej;v8M| zNo0z9dAR>+r<;jrZIQheJ;te4&f2q&uBQv8?nhJiDW2dJZu85-1W#`58ARKWDX>zd z^iF^aE4M^0C*`=3H88C^vtKYclVy5Pn-4%KyEd~zTkU+Dx;!Kn&E0c)y9V|0GSUJq z_6X85z9u8UC4V}3dwM%RpNwO+iA4($ zhyWzEdrxDlxxdUUZ`O=9>292c<&GgCL^5})xiDU0<5PPi&ATp$+$wp@eY@nxBV*R! zSH!@CBWKgkxmV8q#1$l3^SNQp#8p5WddXq_kN>(d*fEN8(8gC5sE-SnE|1A)v#(9W zl6v6iDZ?;fqN4(*ZYLkSc5AJDc!DnMSVzvQz1-2oQ_NxX^?^Ry*kD(jzEGW5|BwHM z-I_ykxoujwutY#lH6G}b-_2zD&^_cFIq5J-Tz6{odwHN@^H)mk(6oK+i`E0jzOEJg zVY{a0IZkiCRrqZ?uZk19y3XYE;KFxgyhFqwA=vLC?YgA0YDn!0hgx-4M{AvG?vQ8C zlS>#Xzjar#jhnqqyLEmtlVC|bF#uwH$+C4|CR6VGUn(!b#Tu|1B2~4Wx4hV`<~dBE z)D31yiY3MYY(|o%+BIHAXkR;}lznfnE4X&&sVq^yf?4%OoT^~Tɬz@eE+Ei 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 + + librevna.png + + From 16bde62cbd37bb33f685448ed4a936b9d4714dfc Mon Sep 17 00:00:00 2001 From: Kiara Navarro Date: Sat, 13 Nov 2021 06:41:09 -0300 Subject: [PATCH 04/24] gui/plotxy: fix hidden description in filter markers related --- Software/PC_Application/Traces/traceplot.cpp | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Software/PC_Application/Traces/traceplot.cpp b/Software/PC_Application/Traces/traceplot.cpp index a7c9398..609b03f 100644 --- a/Software/PC_Application/Traces/traceplot.cpp +++ b/Software/PC_Application/Traces/traceplot.cpp @@ -165,13 +165,12 @@ void TracePlot::paintEvent(QPaintEvent *event) continue; } + // Trace name auto textArea = QRect(x, areaTextTop, width() - x, marginTop); - QFont font = p.font(); font.setPixelSize(12); p.setFont(font); p.setPen(t.first->color()); - auto space = " "; auto label = space + t.first->name() + space; QRectF usedLabelArea = p.boundingRect(textArea, 0, label); @@ -185,9 +184,6 @@ void TracePlot::paintEvent(QPaintEvent *event) x += usedLabelArea.width()+labelMarginRight; auto tmarkers = t.first->getMarkers(); - - font.setPixelSize(12); - p.setFont(font); for(auto m : tmarkers) { if(!xCoordinateVisible(m->getPosition())) { // marker not visible with current plot settings @@ -199,9 +195,9 @@ void TracePlot::paintEvent(QPaintEvent *event) } hasMarkerData = true; + // Rounded box auto space = " "; auto textArea = QRect(width() - marginRight - marginMarkerData, y, width() - marginRight, y + 100); - auto description = m->getSuffix() + space + m->readablePosition(); auto label = space + QString::number(m->getNumber()) + space; QRectF textAreaConsumed = p.boundingRect(textArea, 0, label); QPainterPath pathM; @@ -209,10 +205,14 @@ void TracePlot::paintEvent(QPaintEvent *event) p.fillPath(pathM, t.first->color()); p.drawPath(pathM); + // Over box p.setPen(Util::getFontColorFromBackground(t.first->color())); p.drawText(textArea, 0, label); + + // Non-rounded description + auto description = m->getSuffix() + space + m->readablePosition(); p.setPen(t.first->color()); - p.drawText(textAreaConsumed.x()+textAreaConsumed.width(), textAreaConsumed.y(), textArea.width(), textArea.height(), 0, description); + p.drawText(width() - marginRight - marginMarkerData + textAreaConsumed.width() + 5, textAreaConsumed.y(), width() - marginRight, textArea.height(), 0, description); y += textAreaConsumed.height(); for(auto f : m->getGraphDisplayFormats()) { From f688aaa2205ca2408f3cfc2a55327bd35ce9522b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sat, 13 Nov 2021 19:26:27 +0100 Subject: [PATCH 05/24] Additional debug output for point numbers --- .../SpectrumAnalyzer/spectrumanalyzer.cpp | 10 ++++++++++ Software/PC_Application/VNA/vna.cpp | 5 +++++ 2 files changed, 15 insertions(+) diff --git a/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp b/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp index cb99744..b66be51 100644 --- a/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp +++ b/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp @@ -413,6 +413,11 @@ using namespace std; void SpectrumAnalyzer::NewDatapoint(Protocol::SpectrumAnalyzerResult d) { + if(d.pointNum >= settings.pointNum) { + qWarning() << "Ignoring point with too large point number (" << d.pointNum << ")"; + return; + } + d = average.process(d); if(normalize.measuring) { @@ -451,6 +456,11 @@ void SpectrumAnalyzer::NewDatapoint(Protocol::SpectrumAnalyzerResult d) UpdateAverageCount(); markerModel->updateMarkers(); } + static unsigned int lastPoint = 0; + if(d.pointNum > 0 && d.pointNum != lastPoint + 1) { + qWarning() << "Got point" << d.pointNum << "but last received point was" << lastPoint << "("<<(d.pointNum-lastPoint-1)<<"missed points)"; + } + lastPoint = d.pointNum; } void SpectrumAnalyzer::SettingsChanged() diff --git a/Software/PC_Application/VNA/vna.cpp b/Software/PC_Application/VNA/vna.cpp index 13b8881..40073fa 100644 --- a/Software/PC_Application/VNA/vna.cpp +++ b/Software/PC_Application/VNA/vna.cpp @@ -760,6 +760,11 @@ using namespace std; void VNA::NewDatapoint(Protocol::Datapoint d) { + if(d.pointNum >= settings.npoints) { + qWarning() << "Ignoring point with too large point number (" << d.pointNum << ")"; + return; + } + d = average.process(d); if(calMeasuring) { if(average.currentSweep() == averages) { From 8a26dec66853a48338d80c2580462e82e17e3eeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sat, 13 Nov 2021 19:26:54 +0100 Subject: [PATCH 06/24] Fix example of :DEV:MODE command --- Documentation/UserManual/ProgrammingGuide.pdf | Bin 249200 -> 249205 bytes Documentation/UserManual/ProgrammingGuide.tex | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Documentation/UserManual/ProgrammingGuide.pdf b/Documentation/UserManual/ProgrammingGuide.pdf index c9ef6c069d4694c13e5725c1f5254eb2d8aa5d7b..c69a0feed051cb5e1687557d9420eb8e0bd9c634 100644 GIT binary patch delta 5446 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 YnV-LH#2?6O42Zn*6yOE)!Q&Oan?qO(qs2r5oh( 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 r+vS<=`K(9ujoqDQJgAF}rsN($(_nl7} diff --git a/Documentation/UserManual/ProgrammingGuide.tex b/Documentation/UserManual/ProgrammingGuide.tex index 09e5808..c7571db 100644 --- a/Documentation/UserManual/ProgrammingGuide.tex +++ b/Documentation/UserManual/ProgrammingGuide.tex @@ -241,11 +241,11 @@ This section contains general device commands, available regardless of the curre \subsubsection{DEVice:MODE} \event{Switches the device to the specified mode}{DEVice:MODE }{:\\ \hspace{1cm} VNA: set to vector analyzer\\ \hspace{1cm} GEN: set to signal generator\\ \hspace{1cm} SA: set to spectrum analyzer} \begin{example} -:MODE VNA +:DEV:MODE VNA \end{example} \query{Queries the currently active mode}{DEVice:MODE?}{None}{:\\ \hspace{1cm} VNA: set to vector analyzer\\ \hspace{1cm} GEN: set to signal generator\\ \hspace{1cm} SA: set to spectrum analyzer} \begin{example} -:MODE? +:DEV:MODE? VNA \end{example} From e0c9f4dcee8672866fa0f6df0ef8eec6a148da02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sat, 13 Nov 2021 19:32:58 +0100 Subject: [PATCH 07/24] Fix fractional calculation for frequencies close to integer multiples --- Software/VNA_embedded/Application/Drivers/max2871.cpp | 8 ++++++++ Software/VNA_embedded/Application/VNA.cpp | 6 ++++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/Software/VNA_embedded/Application/Drivers/max2871.cpp b/Software/VNA_embedded/Application/Drivers/max2871.cpp index 9781898..54fa29b 100644 --- a/Software/VNA_embedded/Application/Drivers/max2871.cpp +++ b/Software/VNA_embedded/Application/Drivers/max2871.cpp @@ -190,6 +190,14 @@ bool MAX2871::SetFrequency(uint64_t f) { auto approx = Algorithm::BestRationalApproximation(fraction, 4095); + if (approx.denom == approx.num) { + // got an impossible result due to floating point limitations(?) + // Set fractional part to zero, increase integer part instead + approx.num = 0; + approx.denom = 2; + N++; + } + if(approx.denom == 1) { // M value must be at least 2 approx.denom = 2; diff --git a/Software/VNA_embedded/Application/VNA.cpp b/Software/VNA_embedded/Application/VNA.cpp index 9e0b4c3..65b0973 100644 --- a/Software/VNA_embedded/Application/VNA.cpp +++ b/Software/VNA_embedded/Application/VNA.cpp @@ -180,9 +180,11 @@ bool VNA::Setup(Protocol::SweepSettings s) { // Configure LO2 for the changed IF1. This is not necessary right now but it will generate // the correct clock settings last_LO2 = actualFirstIF - HW::IF2; - LOG_INFO("Changing 2.LO to %lu at point %lu (%lu%06luHz) to reach correct 2.IF frequency", + LOG_INFO("Changing 2.LO to %lu at point %lu (%lu%06luHz) to reach correct 2.IF frequency (1.LO: %lu%06luHz, 1.IF: %lu%06luHz)", last_LO2, i, (uint32_t ) (freq / 1000000), - (uint32_t ) (freq % 1000000)); + (uint32_t ) (freq % 1000000), (uint32_t ) (actualLO1 / 1000000), + (uint32_t ) (actualLO1 % 1000000), (uint32_t ) (actualFirstIF / 1000000), + (uint32_t ) (actualFirstIF % 1000000)); } else { // last entry in IF table, revert LO2 to default last_LO2 = HW::IF1 - HW::IF2; From 1bf0e45f7c2008da6850146379d7f0fcdc21371d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sat, 13 Nov 2021 19:48:03 +0100 Subject: [PATCH 08/24] Prevent crash when attempting to move marker on empty trace --- Software/PC_Application/Traces/Marker/marker.cpp | 3 +++ Software/PC_Application/Traces/Math/tracemath.cpp | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/Software/PC_Application/Traces/Marker/marker.cpp b/Software/PC_Application/Traces/Marker/marker.cpp index c39f4b7..0111c05 100644 --- a/Software/PC_Application/Traces/Marker/marker.cpp +++ b/Software/PC_Application/Traces/Marker/marker.cpp @@ -1560,6 +1560,9 @@ bool Marker::isMovable() // helper traces are never movable by the user return false; } + if(trace()->size() == 0) { + return false; + } switch(type) { case Type::Manual: case Type::Delta: diff --git a/Software/PC_Application/Traces/Math/tracemath.cpp b/Software/PC_Application/Traces/Math/tracemath.cpp index 2055c75..59e5695 100644 --- a/Software/PC_Application/Traces/Math/tracemath.cpp +++ b/Software/PC_Application/Traces/Math/tracemath.cpp @@ -84,7 +84,14 @@ TraceMath::TypeInfo TraceMath::getInfo(TraceMath::Type type) TraceMath::Data TraceMath::getSample(unsigned int index) { - return data.at(index); + if(index < data.size()) { + return data[index]; + } else { + TraceMath::Data d; + d.x = 0; + d.y = 0; + return d; + } } double TraceMath::getStepResponse(unsigned int index) From 7cd0b1e0fd5d1cce2ca350f707fb1b8b3e8ac185 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sat, 13 Nov 2021 20:07:49 +0100 Subject: [PATCH 09/24] omit invisible traces for Y axis autoscale calculation --- Software/PC_Application/Traces/tracexyplot.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Software/PC_Application/Traces/tracexyplot.cpp b/Software/PC_Application/Traces/tracexyplot.cpp index 37f540a..4f59d7e 100644 --- a/Software/PC_Application/Traces/tracexyplot.cpp +++ b/Software/PC_Application/Traces/tracexyplot.cpp @@ -705,6 +705,9 @@ void TraceXYPlot::updateAxisTicks() double max = std::numeric_limits::lowest(); double min = std::numeric_limits::max(); for(auto t : tracesAxis[i]) { + if(!t->isVisible()) { + continue; + } unsigned int samples = t->size(); for(unsigned int j=0;j Date: Fri, 19 Nov 2021 21:18:54 +0100 Subject: [PATCH 10/24] rename clk160 -> clk_pll --- FPGA/VNA/top.vhd | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/FPGA/VNA/top.vhd b/FPGA/VNA/top.vhd index 391a933..ebd5e22 100644 --- a/FPGA/VNA/top.vhd +++ b/FPGA/VNA/top.vhd @@ -311,7 +311,7 @@ architecture Behavioral of top is ); END COMPONENT; - signal clk160 : std_logic; + signal clk_pll : std_logic; signal clk_locked : std_logic; signal inv_clk_locked : std_logic; signal int_reset : std_logic; @@ -453,7 +453,7 @@ begin -- Clock in ports CLK_IN1 => CLK, -- Clock out ports - CLK_OUT1 => clk160, + CLK_OUT1 => clk_pll, -- Status and control signals RESET => RESET, LOCKED => clk_locked @@ -464,7 +464,7 @@ begin Inst_ResetDelay: ResetDelay GENERIC MAP(CLK_DELAY => 100) PORT MAP( - CLK => clk160, + CLK => clk_pll, IN_RESET => inv_clk_locked, OUT_RESET => int_reset ); @@ -472,42 +472,42 @@ begin Sync_AUX1 : Synchronizer GENERIC MAP(stages => 2) PORT MAP( - CLK => clk160, + CLK => clk_pll, SYNC_IN => MCU_AUX1, SYNC_OUT => aux1_sync ); Sync_AUX2 : Synchronizer GENERIC MAP(stages => 2) PORT MAP( - CLK => clk160, + CLK => clk_pll, SYNC_IN => MCU_AUX2, SYNC_OUT => aux2_sync ); Sync_AUX3 : Synchronizer GENERIC MAP(stages => 2) PORT MAP( - CLK => clk160, + CLK => clk_pll, SYNC_IN => MCU_AUX3, SYNC_OUT => aux3_sync ); Sync_LO_LD : Synchronizer GENERIC MAP(stages => 2) PORT MAP( - CLK => clk160, + CLK => clk_pll, SYNC_IN => LO1_LD, SYNC_OUT => lo_ld_sync ); Sync_SOURCE_LD : Synchronizer GENERIC MAP(stages => 2) PORT MAP( - CLK => clk160, + CLK => clk_pll, SYNC_IN => SOURCE_LD, SYNC_OUT => source_ld_sync ); Sync_NSS : Synchronizer GENERIC MAP(stages => 2) PORT MAP( - CLK => clk160, + CLK => clk_pll, SYNC_IN => MCU_NSS, SYNC_OUT => nss_sync ); @@ -516,7 +516,7 @@ begin Source: MAX2871 GENERIC MAP(CLK_DIV => 10) PORT MAP( - CLK => clk160, + CLK => clk_pll, RESET => int_reset, REG4 => source_reg_4, REG3 => source_reg_3, @@ -531,7 +531,7 @@ begin LO1: MAX2871 GENERIC MAP(CLK_DIV => 10) PORT MAP( - CLK => clk160, + CLK => clk_pll, RESET => int_reset, REG4 => lo_reg_4, REG3 => lo_reg_3, @@ -550,7 +550,7 @@ begin GENERIC MAP(CLK_DIV => 2, CONVCYCLES => 77) PORT MAP( - CLK => clk160, + CLK => clk_pll, RESET => int_reset, START => adc_trigger_sample, READY => adc_port1_ready, @@ -566,7 +566,7 @@ begin GENERIC MAP(CLK_DIV => 2, CONVCYCLES => 77) PORT MAP( - CLK => clk160, + CLK => clk_pll, RESET => int_reset, START => adc_trigger_sample, READY => open, -- synchronous ADCs, ready indicated by port 1 ADC @@ -582,7 +582,7 @@ begin GENERIC MAP(CLK_DIV => 2, CONVCYCLES => 77) PORT MAP( - CLK => clk160, + CLK => clk_pll, RESET => int_reset, START => adc_trigger_sample, READY => open, -- synchronous ADCs, ready indicated by port 1 ADC @@ -597,7 +597,7 @@ begin Windower: Windowing PORT MAP( - CLK => clk160, + CLK => clk_pll, RESET => sampling_start, WINDOW_TYPE => sampling_window, PORT1_RAW => adc_port1_data, @@ -614,7 +614,7 @@ begin Sampler: Sampling GENERIC MAP(CLK_CYCLES_PRE_DONE => 0) PORT MAP( - CLK => clk160, + CLK => clk_pll, RESET => sweep_reset, ADC_PRESCALER => sampling_prescaler, PHASEINC => sampling_phaseinc, @@ -639,7 +639,7 @@ begin sweep_reset <= not aux3_sync; SweepModule: Sweep PORT MAP( - CLK => clk160, + CLK => clk_pll, RESET => sweep_reset, NPOINTS => sweep_points, CONFIG_ADDRESS => sweep_config_address, @@ -703,7 +703,7 @@ begin source_unlocked <= not source_ld_sync; SPI: SPICommands PORT MAP( - CLK => clk160, + CLK => clk_pll, RESET => int_reset, SCLK => MCU_SCK, MOSI => MCU_MOSI, @@ -755,7 +755,7 @@ begin SA_DFT: DFT GENERIC MAP(BINS => 96) PORT MAP( - CLK => clk160, + CLK => clk_pll, RESET => dft_reset, PORT1 => port1_windowed, PORT2 => port2_windowed, @@ -770,12 +770,12 @@ begin ConfigMem : SweepConfigMem PORT MAP ( - clka => clk160, + clka => clk_pll, ena => '1', wea => sweep_config_write, addra => sweep_config_write_address, dina => sweep_config_write_data, - clkb => clk160, + clkb => clk_pll, addrb => sweep_config_address, doutb => sweep_config_data ); From 1a7de82cf752b04e62638d89a34af1334aa0c476 Mon Sep 17 00:00:00 2001 From: Kiara Navarro Date: Fri, 19 Nov 2021 21:47:39 -0300 Subject: [PATCH 11/24] theme: include logo in svg --- .../PC_Application/resources/librevna.svg | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Software/PC_Application/resources/librevna.svg diff --git a/Software/PC_Application/resources/librevna.svg b/Software/PC_Application/resources/librevna.svg new file mode 100644 index 0000000..d0a6b51 --- /dev/null +++ b/Software/PC_Application/resources/librevna.svg @@ -0,0 +1,32 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + From 8269fdfa57b4be8c079c85fbc416de6afa15a7b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sat, 27 Nov 2021 19:11:45 +0100 Subject: [PATCH 12/24] de-embed touchstone-defined through --- .../SpectrumAnalyzer/spectrumanalyzer.cpp | 2 +- .../PC_Application/Traces/Marker/marker.cpp | 2 +- .../VNA/Deembedding/matchingnetwork.cpp | 208 ++++- .../VNA/Deembedding/matchingnetwork.h | 15 +- .../VNA/Deembedding/matchingnetworkdialog.ui | 738 +++++++++--------- Software/PC_Application/icons.qrc | 8 + .../PC_Application/icons/definedThrough.png | Bin 0 -> 884 bytes .../PC_Application/icons/definedThrough.svg | 44 ++ Software/PC_Application/icons/parallelC.png | Bin 0 -> 466 bytes Software/PC_Application/icons/parallelL.png | Bin 0 -> 1064 bytes Software/PC_Application/icons/parallelR.png | Bin 0 -> 1077 bytes Software/PC_Application/icons/port1.svg | 31 + Software/PC_Application/icons/port2.svg | 31 + Software/PC_Application/icons/seriesC.png | Bin 0 -> 361 bytes Software/PC_Application/icons/seriesL.png | Bin 0 -> 808 bytes Software/PC_Application/icons/seriesR.png | Bin 0 -> 759 bytes Software/PC_Application/touchstone.cpp | 54 ++ Software/PC_Application/touchstone.h | 8 +- 18 files changed, 744 insertions(+), 397 deletions(-) create mode 100644 Software/PC_Application/icons/definedThrough.png create mode 100644 Software/PC_Application/icons/definedThrough.svg create mode 100644 Software/PC_Application/icons/parallelC.png create mode 100644 Software/PC_Application/icons/parallelL.png create mode 100644 Software/PC_Application/icons/parallelR.png create mode 100644 Software/PC_Application/icons/seriesC.png create mode 100644 Software/PC_Application/icons/seriesL.png create mode 100644 Software/PC_Application/icons/seriesR.png diff --git a/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp b/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp index b66be51..1f497d4 100644 --- a/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp +++ b/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp @@ -315,7 +315,7 @@ nlohmann::json SpectrumAnalyzer::toJSON() tracking["enabled"] = settings.trackingGenerator ? true : false; tracking["port"] = settings.trackingGeneratorPort ? 2 : 1; tracking["offset"] = settings.trackingGeneratorOffset; - tracking["power"] = settings.trackingPower; + tracking["power"] = (double) settings.trackingPower / 100.0; // convert to dBm sweep["trackingGenerator"] = tracking; if(normalize.active) { diff --git a/Software/PC_Application/Traces/Marker/marker.cpp b/Software/PC_Application/Traces/Marker/marker.cpp index 0111c05..1e76a93 100644 --- a/Software/PC_Application/Traces/Marker/marker.cpp +++ b/Software/PC_Application/Traces/Marker/marker.cpp @@ -1560,7 +1560,7 @@ bool Marker::isMovable() // helper traces are never movable by the user return false; } - if(trace()->size() == 0) { + if(!parentTrace || parentTrace->size() == 0) { return false; } switch(type) { diff --git a/Software/PC_Application/VNA/Deembedding/matchingnetwork.cpp b/Software/PC_Application/VNA/Deembedding/matchingnetwork.cpp index f6592c0..3b08796 100644 --- a/Software/PC_Application/VNA/Deembedding/matchingnetwork.cpp +++ b/Software/PC_Application/VNA/Deembedding/matchingnetwork.cpp @@ -1,6 +1,8 @@ #include "matchingnetwork.h" #include "ui_matchingnetworkdialog.h" +#include "unit.h" +#include "CustomWidgets/informationbox.h" #include #include @@ -11,6 +13,7 @@ #include #include #include +#include using namespace std; @@ -83,24 +86,25 @@ void MatchingNetwork::edit() ui->lParallelC->installEventFilter(this); ui->lParallelL->installEventFilter(this); ui->lParallelR->installEventFilter(this); + ui->lDefinedThrough->installEventFilter(this); layout->setContentsMargins(0,0,0,0); layout->setSpacing(0); layout->addStretch(1); auto p1 = new QWidget(); - p1->setMinimumSize(portWidth, 150); - p1->setMaximumSize(portWidth, 150); + p1->setMinimumSize(portWidth, 151); + p1->setMaximumSize(portWidth, 151); p1->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - p1->setStyleSheet("image: url(:/icons/port1.svg);"); + p1->setStyleSheet("image: url(:/icons/port1.png);"); auto DUT = new QWidget(); - DUT->setMinimumSize(DUTWidth, 150); - DUT->setMaximumSize(DUTWidth, 150); + DUT->setMinimumSize(DUTWidth, 151); + DUT->setMaximumSize(DUTWidth, 151); DUT->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - DUT->setStyleSheet("image: url(:/icons/DUT.svg);"); + DUT->setStyleSheet("image: url(:/icons/DUT.png);"); auto p2 = new QWidget(); - p2->setMinimumSize(portWidth, 150); - p2->setMaximumSize(portWidth, 150); + p2->setMinimumSize(portWidth, 151); + p2->setMaximumSize(portWidth, 151); p2->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - p2->setStyleSheet("image: url(:/icons/port2.svg);"); + p2->setStyleSheet("image: url(:/icons/port2.png);"); layout->addWidget(p1); for(auto w : p1Network) { layout->addWidget(w); @@ -136,27 +140,31 @@ void MatchingNetwork::edit() nlohmann::json MatchingNetwork::toJSON() { nlohmann::json j; - for(int i=0;i<2;i++) { - auto network = i==0 ? p1Network : p2Network; - nlohmann::json jn; - for(auto c : network) { - nlohmann::json jc; - jc["component"] = c->getName().toStdString(); - jc["params"] = c->toJSON(); - jn.push_back(jc); - } - j.push_back(jn); + nlohmann::json jn1, jn2; + for(auto c : p1Network) { + nlohmann::json jc; + jc["component"] = c->getName().toStdString(); + jc["params"] = c->toJSON(); + jn1.push_back(jc); } + for(auto c : p2Network) { + nlohmann::json jc; + jc["component"] = c->getName().toStdString(); + jc["params"] = c->toJSON(); + jn2.push_back(jc); + } + j["port1"] = jn1; + j["port2"] = jn2; + j["addNetwork"] = addNetwork; return j; } void MatchingNetwork::fromJSON(nlohmann::json j) { - for(int i=0;i<2;i++) { - auto jn = j[i]; - auto &network = i==0 ? p1Network : p2Network; - network.clear(); - for(auto jc : jn) { + p1Network.clear(); + p2Network.clear(); + if(j.contains("port1")) { + for(auto jc : j["port1"]) { if(!jc.contains("component")) { continue; } @@ -165,9 +173,23 @@ void MatchingNetwork::fromJSON(nlohmann::json j) continue; } c->fromJSON(jc["params"]); - network.push_back(c); + p1Network.push_back(c); } } + if(j.contains("port2")) { + for(auto jc : j["port2"]) { + if(!jc.contains("component")) { + continue; + } + auto c = MatchingComponent::createFromName(QString::fromStdString(jc["component"])); + if(!c) { + continue; + } + c->fromJSON(jc["params"]); + p2Network.push_back(c); + } + } + addNetwork = j.value("addNetwork", true); matching.clear(); } @@ -329,8 +351,8 @@ bool MatchingNetwork::eventFilter(QObject *object, QEvent *event) dropComponent = (MatchingComponent*) dropPtr; dragEvent->acceptProposedAction(); insertIndicator = new QWidget(); - insertIndicator->setMinimumSize(2, 150); - insertIndicator->setMaximumSize(2, 150); + insertIndicator->setMinimumSize(2, imageHeight); + insertIndicator->setMaximumSize(2, imageHeight); insertIndicator->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); insertIndicator->setStyleSheet("background-color:red;"); updateInsertIndicator(dragEvent->pos().x()); @@ -368,6 +390,8 @@ bool MatchingNetwork::eventFilter(QObject *object, QEvent *event) dragComponent = new MatchingComponent(MatchingComponent::Type::ParallelL); } else if(object->objectName() == "lParallelR") { dragComponent = new MatchingComponent(MatchingComponent::Type::ParallelR); + } else if(object->objectName() == "lDefinedThrough") { + dragComponent = new MatchingComponent(MatchingComponent::Type::DefinedThrough); } else { dragComponent = nullptr; } @@ -397,18 +421,21 @@ bool MatchingNetwork::eventFilter(QObject *object, QEvent *event) MatchingComponent::MatchingComponent(Type type) { this->type = type; - setMinimumSize(150, 150); - setMaximumSize(150, 150); + eValue = nullptr; + touchstone = nullptr; + touchstoneLabel = nullptr; + setMinimumSize(151, 151); + setMaximumSize(151, 151); setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); - eValue = new SIUnitEdit(); - eValue->setPrecision(4); - eValue->setPrefixes("fpnum k"); - connect(eValue, &SIUnitEdit::valueChanged, this, &MatchingComponent::valueChanged); setFocusPolicy(Qt::FocusPolicy::ClickFocus); switch(type) { case Type::SeriesR: case Type::SeriesL: case Type::SeriesC: { + eValue = new SIUnitEdit(); + eValue->setPrecision(4); + eValue->setPrefixes("fpnum k"); + connect(eValue, &SIUnitEdit::valueChanged, this, &MatchingComponent::valueChanged); auto layout = new QVBoxLayout(); layout->addWidget(eValue); setLayout(layout); @@ -417,13 +444,16 @@ MatchingComponent::MatchingComponent(Type type) case Type::ParallelR: case Type::ParallelL: case Type::ParallelC: { + eValue = new SIUnitEdit(); + eValue->setPrecision(4); + eValue->setPrefixes("fpnum k"); + connect(eValue, &SIUnitEdit::valueChanged, this, &MatchingComponent::valueChanged); auto layout = new QVBoxLayout(); layout->addWidget(eValue); layout->addStretch(1); layout->setContentsMargins(9, 5, 9, 9); setLayout(layout); } - break; default: break; } @@ -431,38 +461,58 @@ MatchingComponent::MatchingComponent(Type type) case Type::SeriesR: eValue->setUnit("Ω"); eValue->setValue(50); - setStyleSheet("image: url(:/icons/seriesR.svg);"); + setStyleSheet("image: url(:/icons/seriesR.png);"); break; case Type::SeriesL: eValue->setUnit("H"); eValue->setValue(1e-9); - setStyleSheet("image: url(:/icons/seriesL.svg);"); + setStyleSheet("image: url(:/icons/seriesL.png);"); break; case Type::SeriesC: eValue->setUnit("F"); eValue->setValue(1e-12); - setStyleSheet("image: url(:/icons/seriesC.svg);"); + setStyleSheet("image: url(:/icons/seriesC.png);"); break; case Type::ParallelR: eValue->setUnit("Ω"); eValue->setValue(50); - setStyleSheet("image: url(:/icons/parallelR.svg);"); + setStyleSheet("image: url(:/icons/parallelR.png);"); break; case Type::ParallelL: eValue->setUnit("H"); eValue->setValue(1e-9); - setStyleSheet("image: url(:/icons/parallelL.svg);"); + setStyleSheet("image: url(:/icons/parallelL.png);"); break; case Type::ParallelC: eValue->setUnit("F"); eValue->setValue(1e-12); - setStyleSheet("image: url(:/icons/parallelC.svg);"); + setStyleSheet("image: url(:/icons/parallelC.png);"); + break; + case Type::DefinedThrough: { + touchstone = new Touchstone(2); + touchstoneLabel = new QLabel(); + touchstoneLabel->setWordWrap(true); + touchstoneLabel->setAlignment(Qt::AlignCenter); + auto layout = new QVBoxLayout(); + layout->addWidget(touchstoneLabel); + layout->setContentsMargins(0, 0, 0, 0); + setLayout(layout); + setStyleSheet("image: url(:/icons/definedThrough.png);"); + updateTouchstoneLabel(); + } break; default: break; } } +MatchingComponent::~MatchingComponent() +{ + delete eValue; + delete touchstone; + delete touchstoneLabel; +} + ABCDparam MatchingComponent::parameters(double freq) { switch(type) { @@ -478,6 +528,15 @@ ABCDparam MatchingComponent::parameters(double freq) return ABCDparam(1.0, 0.0, 1.0/complex(0, freq * 2 * M_PI * eValue->value()), 1.0); case Type::ParallelC: return ABCDparam(1.0, 0.0, 1.0/complex(0, -1.0 / (freq * 2 * M_PI * eValue->value())), 1.0); + case Type::DefinedThrough: + if(touchstone->points() == 0 || freq < touchstone->minFreq() || freq > touchstone->maxFreq()) { + // outside of provided frequency range, pass through unchanged + return ABCDparam(1.0, 0.0, 0.0, 1.0); + } else { + auto d = touchstone->interpolate(freq); + auto S = Sparam(d.S[0], d.S[1], d.S[2], d.S[3]); + return ABCDparam(S, 50.0); + } default: return ABCDparam(1.0, 0.0, 0.0, 1.0); } @@ -485,7 +544,9 @@ ABCDparam MatchingComponent::parameters(double freq) void MatchingComponent::MatchingComponent::setValue(double v) { - eValue->setValue(v); + if(eValue) { + eValue->setValue(v); + } } MatchingComponent *MatchingComponent::createFromName(QString name) @@ -507,13 +568,73 @@ QString MatchingComponent::getName() nlohmann::json MatchingComponent::toJSON() { nlohmann::json j; - j["value"] = eValue->value(); + switch(type) { + case Type::SeriesC: + case Type::SeriesR: + case Type::SeriesL: + case Type::ParallelC: + case Type::ParallelR: + case Type::ParallelL: + j["value"] = eValue->value(); + break; + case Type::DefinedThrough: + j["touchstone"] = touchstone->toJSON(); + break; + case Type::Last: + break; + } return j; } void MatchingComponent::fromJSON(nlohmann::json j) { - eValue->setValue(j.value("value", 1e-12)); + switch(type) { + case Type::SeriesC: + case Type::SeriesR: + case Type::SeriesL: + case Type::ParallelC: + case Type::ParallelR: + case Type::ParallelL: + eValue->setValue(j.value("value", 1e-12)); + break; + case Type::DefinedThrough: + touchstone->fromJSON(j["touchstone"]); + updateTouchstoneLabel(); + break; + case Type::Last: + break; + } +} + +void MatchingComponent::mouseDoubleClickEvent(QMouseEvent *e) +{ + Q_UNUSED(e); + if(type == Type::DefinedThrough) { + // select new touchstone file + auto filename = QFileDialog::getOpenFileName(nullptr, "Open measurement file", "", "Touchstone files (*.s2p)", nullptr, QFileDialog::DontUseNativeDialog); + if (!filename.isEmpty()) { + try { + *touchstone = Touchstone::fromFile(filename.toStdString()); + } catch(const std::exception& e) { + InformationBox::ShowError("Failed to load file", QString("Attempt to load file ended with error: \"") + e.what()+"\""); + } + updateTouchstoneLabel(); + } + } +} + +void MatchingComponent::updateTouchstoneLabel() +{ + if(!touchstone || !touchstoneLabel) { + return; + } + if(touchstone->points() == 0) { + touchstoneLabel->setText("No data. Double-click to select touchstone file"); + } else { + QString text = QString::number(touchstone->points()) + " points from "+Unit::ToString(touchstone->minFreq(), "Hz", " kMG", 4) + + " to "+Unit::ToString(touchstone->maxFreq(), "Hz", " kMG", 4); + touchstoneLabel->setText(text); + } } QString MatchingComponent::typeToName(MatchingComponent::Type type) @@ -525,6 +646,7 @@ QString MatchingComponent::typeToName(MatchingComponent::Type type) case Type::ParallelR: return "ParallelR"; case Type::ParallelL: return "ParallelL"; case Type::ParallelC: return "ParallelC"; + case Type::DefinedThrough: return "Touchstone Through"; default: return ""; } } diff --git a/Software/PC_Application/VNA/Deembedding/matchingnetwork.h b/Software/PC_Application/VNA/Deembedding/matchingnetwork.h index 7ea983e..afc1fc7 100644 --- a/Software/PC_Application/VNA/Deembedding/matchingnetwork.h +++ b/Software/PC_Application/VNA/Deembedding/matchingnetwork.h @@ -5,8 +5,10 @@ #include "deembeddingoption.h" #include "Tools/parameters.h" #include "savable.h" +#include "touchstone.h" #include +#include #include @@ -21,11 +23,13 @@ public: ParallelR, ParallelL, ParallelC, + DefinedThrough, // Add new matching components here, do not explicitly assign values and keep the Last entry at the last position Last, }; MatchingComponent(Type type); + ~MatchingComponent(); ABCDparam parameters(double freq); void setValue(double v); @@ -40,7 +44,11 @@ signals: void deleted(MatchingComponent* m); protected: SIUnitEdit *eValue; + Touchstone *touchstone; + QLabel *touchstoneLabel; private: + void mouseDoubleClickEvent(QMouseEvent *e) override; + void updateTouchstoneLabel(); static QString typeToName(Type type); Type type; void keyPressEvent(QKeyEvent *event) override; @@ -62,9 +70,10 @@ public: nlohmann::json toJSON() override; void fromJSON(nlohmann::json j) override; private: - static constexpr int componentWidth = 150; - static constexpr int DUTWidth = 150; - static constexpr int portWidth = 75; + static constexpr int imageHeight = 151; + static constexpr int componentWidth = 151; + static constexpr int DUTWidth = 151; + static constexpr int portWidth = 76; MatchingComponent *componentAtPosition(int pos); unsigned int findInsertPosition(int xcoord); void addComponentAtPosition(int pos, MatchingComponent *c); diff --git a/Software/PC_Application/VNA/Deembedding/matchingnetworkdialog.ui b/Software/PC_Application/VNA/Deembedding/matchingnetworkdialog.ui index 2a52e6f..a1c67f3 100644 --- a/Software/PC_Application/VNA/Deembedding/matchingnetworkdialog.ui +++ b/Software/PC_Application/VNA/Deembedding/matchingnetworkdialog.ui @@ -7,7 +7,7 @@ 0 0 772 - 442 + 443 @@ -131,20 +131,8 @@ Matching Components - - - 0 - - - 0 - - - 0 - - - 0 - - + + @@ -159,369 +147,423 @@ - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 + + + + + 0 + 0 + + + + + 80 + 80 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + border-image: url(:/icons/seriesC.svg); - - 0 + + QFrame::NoFrame - - 0 + + QFrame::Plain - - 0 + + Series C - - 0 + + Qt::AlignCenter - - - - border-image: url(:/icons/seriesC.svg); - - - QFrame::NoFrame - - - QFrame::Plain - - - Series C - - - Qt::AlignCenter - - - - - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 + + + + + + + + + + 0 + 0 + + + + + 80 + 80 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + border-image: url(:/icons/seriesL.svg); - - 0 + + QFrame::NoFrame - - 0 + + QFrame::Plain - - 0 + + Series L - - 0 + + Qt::AlignCenter - - - - border-image: url(:/icons/seriesL.svg); - - - QFrame::NoFrame - - - QFrame::Plain - - - Series L - - - Qt::AlignCenter - - - - - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 + + + + + + + + + + 0 + 0 + + + + + 80 + 80 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + border-image: url(:/icons/seriesR.svg); - - 0 + + QFrame::NoFrame - - 0 + + QFrame::Plain - - 0 + + Series R - - 0 + + Qt::AlignCenter - - - - border-image: url(:/icons/seriesR.svg); - - - QFrame::NoFrame - - - QFrame::Plain - - - Series R - - - Qt::AlignCenter - - - - - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - - 80 - 80 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 + + + + + + + + + + 0 + 0 + + + + + 80 + 80 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + border-image: url(:/icons/definedThrough.svg); - - 0 + + QFrame::NoFrame - - 0 + + QFrame::Plain - - 0 + + <html><head/><body><p align="center"><br/><br/>Defined<br/>Through</p></body></html> - - 0 + + Qt::AlignHCenter|Qt::AlignTop - - - - border-image: url(:/icons/parallelC.svg); - - - QFrame::NoFrame - - - QFrame::Plain - - - Parallel C - - - Qt::AlignHCenter|Qt::AlignTop - - - - - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 + + + + + + + + + + 0 + 0 + + + + + 80 + 80 + + + + + 80 + 80 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + border-image: url(:/icons/parallelC.svg); - - 0 + + QFrame::NoFrame - - 0 + + QFrame::Plain - - 0 + + Parallel C - - 0 + + Qt::AlignHCenter|Qt::AlignTop - - - - - 0 - 0 - - - - border-image: url(:/icons/parallelL.svg); - - - QFrame::NoFrame - - - QFrame::Plain - - - Parallel L - - - Qt::AlignHCenter|Qt::AlignTop - - - - - - - - - - - 0 - 0 - - - - - 80 - 80 - - - - QFrame::StyledPanel - - - QFrame::Raised - - - - 0 + + + + + + + + + + 0 + 0 + + + + + 80 + 80 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + - - 0 + + border-image: url(:/icons/parallelL.svg); - - 0 + + QFrame::NoFrame - - 0 + + QFrame::Plain - - 0 + + Parallel L - - - - border-image: url(:/icons/parallelR.svg); - - - QFrame::NoFrame - - - QFrame::Plain - - - Parallel R - - - Qt::AlignHCenter|Qt::AlignTop - - - - - - - + + Qt::AlignHCenter|Qt::AlignTop + + + + + + + + + + + 0 + 0 + + + + + 80 + 80 + + + + QFrame::StyledPanel + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + border-image: url(:/icons/parallelR.svg); + + + QFrame::NoFrame + + + QFrame::Plain + + + Parallel R + + + Qt::AlignHCenter|Qt::AlignTop + + + + + diff --git a/Software/PC_Application/icons.qrc b/Software/PC_Application/icons.qrc index b4b2c2f..148fb55 100644 --- a/Software/PC_Application/icons.qrc +++ b/Software/PC_Application/icons.qrc @@ -50,5 +50,13 @@ icons/down.png icons/up.png icons/chainlink.png + icons/definedThrough.svg + icons/seriesR.png + icons/seriesL.png + icons/seriesC.png + icons/parallelR.png + icons/parallelL.png + icons/parallelC.png + icons/definedThrough.png diff --git a/Software/PC_Application/icons/definedThrough.png b/Software/PC_Application/icons/definedThrough.png new file mode 100644 index 0000000000000000000000000000000000000000..71b41820ecdae0060c2d1119bae058ae29474e2f GIT binary patch literal 884 zcmeAS@N?(olHy`uVBq!ia0vp^(?OVn2}J&l(*shR1s;*b3=Dj8K$wwzO7L9<24*!+ z7srr_TW@dg_qIxuIq>oMK8;I@+`Kh%)%8> zQCjGsvB*P12*@J9u{~-0V|s>!TfKh5hyN3vTzs_o+VR3Sztb}+MCvaeJ$m%>=g*ff zU;gr?#A(%wIU%#F>abj;T@A1M#7grpA92pby3zaqG+T^4|UV zGiT=t$Mx&iFAdTZ;o7-wwug$Dx%up~Y30*XBR59)Xih!~k9yos2ms&q2t6VT@D-I=o{PoCWCc6ieoucebtrttCcxjcFGN^ADn zZ?E3eN-t6jj){z%IdkUC`}gy=M#W3p+u2Q%!IQq&4+Y zT;U0^9>3+m8X}KA{`mB%sJ#32uAMRK;?{Scxc>Uho1F6Z*REZ=ckiCO)Plyj-P5N} z-#oNZ`uh6q?d?iMMMazT?$uRV8%8t;_ZQ#VHur3ryuAF>AfN{xKYn~QYioqgvm(o-S8m?CdF$4#BS%`Uzm}DjerHp?`7s!;r)_A%C Qn2i`bUHx3vIVCg!0L&+y + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Software/PC_Application/icons/parallelC.png b/Software/PC_Application/icons/parallelC.png new file mode 100644 index 0000000000000000000000000000000000000000..9acfd382dad616d1c6147d3451ec1b79d74fef5e GIT binary patch literal 466 zcmeAS@N?(olHy`uVBq!ia0vp^(?OVn2}J&l(*shR1s;*b3=Dj8K$wwzO7L9<2F4ao z7srr_TW{~~&OK}(;t*(9pzgcS@W@LGmg9+&&SJ)6Hj69^D-4HR+hTBOmn$OFg-6ww5+l|X`lOMrqJU0?+;@uLS{?cIK@=-k?^ z_o`ElS?4XCaz1rh5YzfACS3tsIx~yy`X2xNwk`MfzUp|H^--^Pt-EZp_H|Ttsb1fn zLn8IP`!b2OPoB5ZHqh(kbGbh@=PT!Fo^la|JMyc1RnLzlf6G39ZMd=Z1W~6FbG&u zx&Mdg0uK$Lt`;RATIiv%h!AIC2S}Mvmqu2Y086i%@_+aqp$%8)L%#a=5jOro@=&gi!4zc9((Eldh* zJ=|Q7TmIH><7|$JjXj%Uw0QAiZ*T8yxyIsOv$L~L7tV=VyKU1ZBSXW7yVmWwEIrL~ z{`c>{|E^xW`tIGkD_5?p+N+lt6t!&btq-;KV%?n0TX<}$AOHS$`BK`kKFU!emm4_?%cUkPe0W^_O4aIj?syz29~8{87K}B)}prE)I+tjVzxT ze#@r@eg6DeBP(+C)md}r=5D_|@42VdmXD`dEU%ZAmzAZZrA<2d1qWtsx&nu=+pML1v;v%y-vKDtYCN4Y z`@equ`riC7@XFTZ%hk`dy`H=0{nyVuyz5p(U48#O_WD%!6+RZ_oA>UuH8FYeyKZX~ zZ~Ni)_V$}OVK=_@3R)NJjPd(c{NTrZvpxEtRECn|@Nhu6;(u7tKGAFMzq`A+{rmG= zwO01rk>$oGydQ~Y*W2v#|D%(lS5)pC6ZrpHY3Qrgc5SWj5T}U&Dk6a@BAl*Hg02Y8 cugi7Jw=x1YL|QL#1ZI5(Pgg&ebxsLQ0P5NMNB{r; literal 0 HcmV?d00001 diff --git a/Software/PC_Application/icons/parallelR.png b/Software/PC_Application/icons/parallelR.png new file mode 100644 index 0000000000000000000000000000000000000000..78feadccda33054d4653dd0676d7169dd0fe4461 GIT binary patch literal 1077 zcmeAS@N?(olHy`uVBq!ia0vp^(?OVn2}J&l(*shR1s;*b3=Dj8K$wwzO7L9<2Il9U zE{-7;x8BY*^mrX0;t+3t_tw@R^^L19^faBk;3$6GZ{q_!Cz(5~JFKT{+rfSE!M!Gr zmy=5JV_KhU6#6b_`_=BCS^w+y|M&Zf)!3&$|G2mM{@$IQxx4Rb8O(|Q!?}SYf<=c( zY$MSWV>eg>kTU7nr1Sjq&eyv>|JO# zK&JiQqJurhA1~7Q^>^R%%B0x5wF@=66gZ{?-OV%i6!P@mtg$=OMC$Ovfb80$qDPhY z?%tJEUo~+`>dqKF@$S~3S6{wN5x%% zax>@2+Q3hpvzEO)di3bWj~~yU_n)M)`s%A!uU5saFTQM`{Nrfh8;ic@m21Piv;Y44 z_iop^Ck^^KKYHC3m+f9Vh3jxY#D~pFdTxOss`^3A^F<3ke*PS+Sz1s~P*S2|^dnMG zTwYFY%eB36=RfZ`eE6_MU#tJ_w~8NLe*aqa_sf?rfBvkQvTNtgo%{Fan@Alp_2Lzs zeO9e&k=A9-CWma_vSb6#NhP~umri;A`%u>dvCzZ7a9tY~mAy3N>YQ0;(=ItJ3ze*# z_k4HOs}ieJeJRh?S9k5(XJ>E!|KGp5>gvbK5(^?t&wp0&Y&)zd=Bu^(T-xTpn_gvY zofH-}^=|N%C2Lk4NjD4>>D2G`-aI4q<~sE`DxIpSg_eaMA3r`^IOkPU{MVM)>sy!V z=so+t@mABm_qQ*HiR=)MkBeKR5v3z`HtjPo#9qHXy{LP`9;F|34^2)ReOpv{<@MK9 z1+!8kr#=qx|N1%2xA|h!`*_PG%hrU296lQVU-wmJNbTd;6H~63Z(D5~y7SIm&kY$* zcJ8WfQ|38rbADsS4EuwN%|7YA>%Zl*Jb0z$svG~3W-aS`{HFg_OkA9o?rN{iJM6PA zzy7*LV(Y|`*Xx_+w=OnY^e9R6f0M$7+$(NVj@th^9ARRuxi+pn=_<#ZGx@%6`0srA z{r6*qj`hxoe~wPrq!S+>&)$46?9cT0Z8f*sek&yi0&~L=qA3=gB*A~`PkdU}UVojo z`RDij`k69*|8nN@a@2JDuMOLrF1|_pWL|^t*Z8@sgZSnALi|DwfDG?&#iagCyZ`K+ WN7!^$Q()d_VDNPHb6Mw<&;$U5@CQQx literal 0 HcmV?d00001 diff --git a/Software/PC_Application/icons/port1.svg b/Software/PC_Application/icons/port1.svg index c93f08d..12252e3 100644 --- a/Software/PC_Application/icons/port1.svg +++ b/Software/PC_Application/icons/port1.svg @@ -1,6 +1,26 @@ + + + + + + + + + + + + + + + + + + + + @@ -12,6 +32,17 @@ + + + + + + + + + + + diff --git a/Software/PC_Application/icons/port2.svg b/Software/PC_Application/icons/port2.svg index f98b346..9ead629 100644 --- a/Software/PC_Application/icons/port2.svg +++ b/Software/PC_Application/icons/port2.svg @@ -1,12 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Software/PC_Application/icons/seriesC.png b/Software/PC_Application/icons/seriesC.png new file mode 100644 index 0000000000000000000000000000000000000000..3e5155aaaa02383b1c9e45892f6044cb02f85158 GIT binary patch literal 361 zcmeAS@N?(olHy`uVBq!ia0vp^(?OVn2}J&l(*shR1s;*b3=Dj8K$wwzO7L9<21aR5 z7srr_TW{|eavd<>aJU#z^Ki(YQH_EO>qg8<$eE=Q=@B^VNs+EoVM&1x zw_jmlM=;mUhmI4L?X`ZFm+|nkte4Ncl}|2DmzTB4x^4F2!0(3w0V1w}BCcJFG`bdf z02zTInjp3kNHA~-P*9@_B=|L6O6J1-Umrhy{PgM5&!3g0rKOdXmJ&R_nCH!#=fq)d zZZ0k^{?}G#`swG-p9>s%`}S>t#hn|w;@024cP}nJ-rwJU{rdI4nZIrS<}k}|xnhgX z>7<^_g{xNOWnVVA`^)D1Z{}62PHoa@nee9IC_p-b|vJdy=mtO+-{45j>-OQO* zx8}8#?CU2}I5z8OpWdYW(@uf&sD$mP&@M&p&Ni4CbHT zzuu>)a-RC*-kH~VCHjx&-tIdNVxLr94zh0BKK-NnbG8$ElO=kx<7Q#O@4 zgod&k%6|XrnY=aXs6_c)y?giXw+px}4*Xwd`t`$y3w!f+I?D3^`P=8$9e!wVY)8~u z8~uO5-A5mZm;X^$S66J=y?b{o^SSfqcVAz-^uF)!o2)w{bZjR5%Z^@Oc~|q}@4eak zH|aQY+y&Alfpg)f% zZCD#G_~+#A;3@9|T_=JfS0e~VM7KBKl}4lkeCY#f2S&QVv>TG1VB%Nez6*0zoced@ T{Kpbt(r55=^>bP0l+XkKScrBm literal 0 HcmV?d00001 diff --git a/Software/PC_Application/icons/seriesR.png b/Software/PC_Application/icons/seriesR.png new file mode 100644 index 0000000000000000000000000000000000000000..f4028ee5caad60f23902da00a8af4df74002e636 GIT binary patch literal 759 zcmeAS@N?(olHy`uVBq!ia0vp^(?OVn2}J&l(*shR1s;*b3=Dj8K$wwzO7L9<2Bwvs zE{-7;x8B~_*!##qqT%Ar)k&*nXl>&(cjRB!DCNT*z{sg7!eD6V!dG+9xwq@~SS%HB+7Y*2 ze*K!)RxKAye78RfTO1J@y6xus?@zV+k0*ayw`x^Q>g{ccEmJO?IqF#0=x8I?UmCl7 zid|KB=u}<#e)or`mSraOeE(g0?$LrB2Nde+>&s)mSJ)V(-~3%;_xyQQ?Df|EUc+bH z&vf{@xOpCBPp_n5b0|Gf)2ugbpruMJ*zI5_*@)JI8sZS7BQ z-~IoO&i>U;%uif@G~-r-re4^8o#1_U|872Wr80eoMOewb$c>M(S3SG)?#~|G{ndMP z`#yDR9#uVVxYXo%*>m@IWxN0GIREtM#P4^v*6?r0J8IKfoL4^Ykieli%+J#{pS(Y* z`t|?f`)|(dxm&sCnT*BfM-dmlKaQXK|KyAOzwp=+(FDbu685OYC5{y3c#{IeaLgov gY3ZUL`d@@2CM^EEq%i0?FeNj1y85}Sb4q9e0K+3<*#H0l literal 0 HcmV?d00001 diff --git a/Software/PC_Application/touchstone.cpp b/Software/PC_Application/touchstone.cpp index 555667e..5525dbe 100644 --- a/Software/PC_Application/touchstone.cpp +++ b/Software/PC_Application/touchstone.cpp @@ -384,3 +384,57 @@ QString Touchstone::getFilename() const { return filename; } + +nlohmann::json Touchstone::toJSON() +{ + nlohmann::json j; + j["ports"] = m_ports; + j["filename"] = filename.toStdString(); + if(m_datapoints.size() > 0) { + nlohmann::json json_points; + for(auto d : m_datapoints) { + nlohmann::json point; + point["frequency"] = d.frequency; + nlohmann::json sparams; + for(auto s : d.S) { + nlohmann::json sparam; + sparam["real"] = s.real(); + sparam["imag"] = s.imag(); + sparams.push_back(sparam); + } + point["Sparams"] = sparams; + json_points.push_back(point); + } + j["datapoints"] = json_points; + } + return j; +} + +void Touchstone::fromJSON(nlohmann::json j) +{ + m_datapoints.clear(); + filename = QString::fromStdString(j.value("filename", "")); + m_ports = j.value("ports", 0); + if(!m_ports || !j.contains("datapoints")) { + return; + } + auto json_points = j["datapoints"]; + for(auto point : json_points) { + Datapoint d; + if(!point.contains("frequency") || !point.contains("Sparams")) { + // missing data, abort here + qWarning() << "Touchstone data point does not contain frequency or S parameters"; + break; + } + d.frequency = point["frequency"]; + if(point["Sparams"].size() != m_ports * m_ports) { + // invalid number of Sparams, abort here + qWarning() << "Invalid number of S parameters, got" << point["Sparams"].size() << "expected" << m_ports*m_ports; + break; + } + for(auto Sparam : point["Sparams"]) { + d.S.push_back(complex(Sparam.value("real", 0.0), Sparam.value("imag", 0.0))); + } + m_datapoints.push_back(d); + } +} diff --git a/Software/PC_Application/touchstone.h b/Software/PC_Application/touchstone.h index a73b4ce..fa34fa1 100644 --- a/Software/PC_Application/touchstone.h +++ b/Software/PC_Application/touchstone.h @@ -1,12 +1,14 @@ #ifndef TOUCHSTONE_H #define TOUCHSTONE_H +#include "savable.h" + #include #include #include #include -class Touchstone +class Touchstone : public Savable { public: enum class Scale { @@ -29,6 +31,7 @@ public: }; Touchstone(unsigned int m_ports); + virtual ~Touchstone(){}; void AddDatapoint(Datapoint p); void toFile(std::string filename, Scale unit = Scale::GHz, Format format = Format::RealImaginary); std::stringstream toString(Scale unit = Scale::GHz, Format format = Format::RealImaginary); @@ -45,6 +48,9 @@ public: unsigned int ports() { return m_ports; } QString getFilename() const; + virtual nlohmann::json toJSON(); + virtual void fromJSON(nlohmann::json j); + private: unsigned int m_ports; std::vector m_datapoints; From 55ac50ee84e457554d1daa184a6cb5ac3cbe7a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sat, 27 Nov 2021 23:32:41 +0100 Subject: [PATCH 13/24] allow different paramaters for male/female standards --- .../Calibration/calibration.cpp | 9 +- .../Calibration/calibrationtracedialog.cpp | 3 +- .../PC_Application/Calibration/calkit.cpp | 269 +-- Software/PC_Application/Calibration/calkit.h | 114 +- .../Calibration/calkitdialog.cpp | 258 ++- .../PC_Application/Calibration/calkitdialog.h | 2 +- .../Calibration/calkitdialog.ui | 1439 +++++++++++------ 7 files changed, 1416 insertions(+), 678 deletions(-) diff --git a/Software/PC_Application/Calibration/calibration.cpp b/Software/PC_Application/Calibration/calibration.cpp index 9029134..3fce2b9 100644 --- a/Software/PC_Application/Calibration/calibration.cpp +++ b/Software/PC_Application/Calibration/calibration.cpp @@ -122,13 +122,14 @@ bool Calibration::constructErrorTerms(Calibration::Type type) } qDebug() << "Constructing error terms for" << TypeToString(type) << "calibration"; bool isTRL = type == Type::TRL; - double kit_minFreq = kit.minFreq(isTRL); - double kit_maxFreq = kit.maxFreq(isTRL); - if(minFreq < kit_minFreq || maxFreq > kit_maxFreq) { + bool uses_male = true; + bool uses_female = true; + if(kit.checkIfValid(minFreq, maxFreq, isTRL, uses_male, uses_female)) { + // TODO adjust for male/female standards // Calkit does not support complete calibration range QString msg = QString("The calibration kit does not support the complete span.\n\n") + "The measured calibration data covers " + Unit::ToString(minFreq, "Hz", " kMG", 4) + " to " + Unit::ToString(maxFreq, "Hz", " kMG", 4) - + ", however the calibration kit is only valid from " + Unit::ToString(kit_minFreq, "Hz", " kMG", 4) + " to " + Unit::ToString(kit_maxFreq, "Hz", " kMG", 4) + ".\n\n" + + ", however the calibration kit does not support the whole frequency range.\n\n" + "Please adjust the calibration kit or the span and take the calibration measurements again."; InformationBox::ShowError("Unable to perform calibration", msg); qWarning() << msg; diff --git a/Software/PC_Application/Calibration/calibrationtracedialog.cpp b/Software/PC_Application/Calibration/calibrationtracedialog.cpp index 434dbe5..d1d8260 100644 --- a/Software/PC_Application/Calibration/calibrationtracedialog.cpp +++ b/Software/PC_Application/Calibration/calibrationtracedialog.cpp @@ -30,7 +30,8 @@ CalibrationTraceDialog::CalibrationTraceDialog(Calibration *cal, double f_min, d if(type != Calibration::Type::None) { auto kit = cal->getCalibrationKit(); auto isTRL = type == Calibration::Type::TRL; - if(kit.minFreq(isTRL) > f_min || kit.maxFreq(isTRL) < f_max) { + if(isTRL && (kit.minFreqTRL() > f_min || kit.maxFreqTRL() < f_max)) { + // TODO check SOLT frequency range depending on selected male/female kit InformationBox::ShowMessage("Warning", "The calibration kit does not completely cover the currently selected span. " "Applying a calibration will not be possible for any measurements taken with these settings."); } diff --git a/Software/PC_Application/Calibration/calkit.cpp b/Software/PC_Application/Calibration/calkit.cpp index 890d9a2..8a6ecbc 100644 --- a/Software/PC_Application/Calibration/calkit.cpp +++ b/Software/PC_Application/Calibration/calkit.cpp @@ -14,9 +14,12 @@ using json = nlohmann::json; using namespace std; Calkit::Calkit() - : ts_open(nullptr), - ts_short(nullptr), - ts_load(nullptr), + : ts_open_m(nullptr), + ts_short_m(nullptr), + ts_load_m(nullptr), + ts_open_f(nullptr), + ts_short_f(nullptr), + ts_load_f(nullptr), ts_through(nullptr), ts_cached(false) { @@ -128,39 +131,39 @@ Calkit Calkit::fromFile(QString filename) // legacy file format, return to beginning of file file.clear(); file.seekg(0); - c.SOLT.Open.useMeasurements = readLine(file).toInt(); - c.SOLT.Short.useMeasurements = readLine(file).toInt(); - c.SOLT.Load.useMeasurements = readLine(file).toInt(); + c.SOLT.open_m.useMeasurements = readLine(file).toInt(); + c.SOLT.short_m.useMeasurements = readLine(file).toInt(); + c.SOLT.load_m.useMeasurements = readLine(file).toInt(); c.SOLT.Through.useMeasurements = readLine(file).toInt(); - c.SOLT.Open.Z0 = readLine(file).toDouble(); - c.SOLT.Open.delay = readLine(file).toDouble(); - c.SOLT.Open.loss = readLine(file).toDouble(); - c.SOLT.Open.C0 = readLine(file).toDouble(); - c.SOLT.Open.C1 = readLine(file).toDouble(); - c.SOLT.Open.C2 = readLine(file).toDouble(); - c.SOLT.Open.C3 = readLine(file).toDouble(); - c.SOLT.Short.Z0 = readLine(file).toDouble(); - c.SOLT.Short.delay = readLine(file).toDouble(); - c.SOLT.Short.loss = readLine(file).toDouble(); - c.SOLT.Short.L0 = readLine(file).toDouble(); - c.SOLT.Short.L1 = readLine(file).toDouble(); - c.SOLT.Short.L2 = readLine(file).toDouble(); - c.SOLT.Short.L3 = readLine(file).toDouble(); - c.SOLT.Load.Z0 = readLine(file).toDouble(); + c.SOLT.open_m.Z0 = readLine(file).toDouble(); + c.SOLT.open_m.delay = readLine(file).toDouble(); + c.SOLT.open_m.loss = readLine(file).toDouble(); + c.SOLT.open_m.C0 = readLine(file).toDouble(); + c.SOLT.open_m.C1 = readLine(file).toDouble(); + c.SOLT.open_m.C2 = readLine(file).toDouble(); + c.SOLT.open_m.C3 = readLine(file).toDouble(); + c.SOLT.short_m.Z0 = readLine(file).toDouble(); + c.SOLT.short_m.delay = readLine(file).toDouble(); + c.SOLT.short_m.loss = readLine(file).toDouble(); + c.SOLT.short_m.L0 = readLine(file).toDouble(); + c.SOLT.short_m.L1 = readLine(file).toDouble(); + c.SOLT.short_m.L2 = readLine(file).toDouble(); + c.SOLT.short_m.L3 = readLine(file).toDouble(); + c.SOLT.load_m.Z0 = readLine(file).toDouble(); c.SOLT.Through.Z0 = readLine(file).toDouble(); c.SOLT.Through.delay = readLine(file).toDouble(); c.SOLT.Through.loss = readLine(file).toDouble(); - if(c.SOLT.Open.useMeasurements) { - c.SOLT.Open.file = readLine(file); - c.SOLT.Open.Sparam = readLine(file).toInt(); + if(c.SOLT.open_m.useMeasurements) { + c.SOLT.open_m.file = readLine(file); + c.SOLT.open_m.Sparam = readLine(file).toInt(); } - if(c.SOLT.Short.useMeasurements) { - c.SOLT.Short.file = readLine(file); - c.SOLT.Short.Sparam = readLine(file).toInt(); + if(c.SOLT.short_m.useMeasurements) { + c.SOLT.short_m.file = readLine(file); + c.SOLT.short_m.Sparam = readLine(file).toInt(); } - if(c.SOLT.Load.useMeasurements) { - c.SOLT.Load.file = readLine(file); - c.SOLT.Load.Sparam = readLine(file).toInt(); + if(c.SOLT.load_m.useMeasurements) { + c.SOLT.load_m.file = readLine(file); + c.SOLT.load_m.Sparam = readLine(file).toInt(); } if(c.SOLT.Through.useMeasurements) { c.SOLT.Through.file = readLine(file); @@ -173,6 +176,8 @@ Calkit Calkit::fromFile(QString filename) c.TRL.Line.minFreq = readLine(file).toDouble(); c.TRL.Line.maxFreq = readLine(file).toDouble(); + c.SOLT.separate_male_female = false; + InformationBox::ShowMessage("Loading calkit file", "The file \"" + filename + "\" is stored in a deprecated" " calibration kit format. Future versions of this application might not support" " it anymore. Please save the calibration kit to update to the new format"); @@ -199,7 +204,7 @@ void Calkit::edit(std::function done) dialog->show(); } -class Calkit::SOLT Calkit::toSOLT(double frequency) +class Calkit::SOLT Calkit::toSOLT(double frequency, bool male_standards) { auto addTransmissionLine = [](complex termination_reflection, double offset_impedance, double offset_delay, double offset_loss, double frequency) -> complex { // nomenclature and formulas from https://loco.lab.asu.edu/loco-memos/edges_reports/report_20130807.pdf @@ -221,29 +226,36 @@ class Calkit::SOLT Calkit::toSOLT(double frequency) return Gamma_i; }; + auto Load = male_standards ? SOLT.load_m : SOLT.load_f; + auto Short = male_standards ? SOLT.short_m : SOLT.short_f; + auto Open = male_standards ? SOLT.open_m : SOLT.open_f; + auto ts_load = male_standards ? ts_load_m : ts_load_f; + auto ts_short = male_standards ? ts_short_m : ts_short_f; + auto ts_open = male_standards ? ts_open_m : ts_open_f; + fillTouchstoneCache(); class SOLT ref; - if(SOLT.Load.useMeasurements) { + if(Load.useMeasurements) { ref.Load = ts_load->interpolate(frequency).S[0]; } else { - auto imp_load = complex(SOLT.Load.Z0, 0); + auto imp_load = complex(Load.Z0, 0); // Add parallel capacitor to impedance - if(SOLT.Load.Cparallel > 0) { - auto imp_C = complex(0, -1.0 / (frequency * 2 * M_PI * SOLT.Load.Cparallel)); + if(Load.Cparallel > 0) { + auto imp_C = complex(0, -1.0 / (frequency * 2 * M_PI * Load.Cparallel)); imp_load = (imp_load * imp_C) / (imp_load + imp_C); } // add series inductor to impedance - auto imp_L = complex(0, frequency * 2 * M_PI * SOLT.Load.Lseries); + auto imp_L = complex(0, frequency * 2 * M_PI * Load.Lseries); imp_load += imp_L; ref.Load = (imp_load - complex(50.0)) / (imp_load + complex(50.0)); - ref.Load = addTransmissionLine(ref.Load, SOLT.Load.Z0, SOLT.Load.delay*1e-12, 0, frequency); + ref.Load = addTransmissionLine(ref.Load, Load.Z0, Load.delay*1e-12, 0, frequency); } - if(SOLT.Open.useMeasurements) { + if(Open.useMeasurements) { ref.Open = ts_open->interpolate(frequency).S[0]; } else { // calculate fringing capacitance for open - double Cfringing = SOLT.Open.C0 * 1e-15 + SOLT.Open.C1 * 1e-27 * frequency + SOLT.Open.C2 * 1e-36 * pow(frequency, 2) + SOLT.Open.C3 * 1e-45 * pow(frequency, 3); + double Cfringing = Open.C0 * 1e-15 + Open.C1 * 1e-27 * frequency + Open.C2 * 1e-36 * pow(frequency, 2) + Open.C3 * 1e-45 * pow(frequency, 3); // convert to impedance if (Cfringing == 0) { // special case to avoid issues with infinity @@ -252,18 +264,18 @@ class Calkit::SOLT Calkit::toSOLT(double frequency) auto imp_open = complex(0, -1.0 / (frequency * 2 * M_PI * Cfringing)); ref.Open = (imp_open - complex(50.0)) / (imp_open + complex(50.0)); } - ref.Open = addTransmissionLine(ref.Open, SOLT.Open.Z0, SOLT.Open.delay*1e-12, SOLT.Open.loss*1e9, frequency); + ref.Open = addTransmissionLine(ref.Open, Open.Z0, Open.delay*1e-12, Open.loss*1e9, frequency); } - if(SOLT.Short.useMeasurements) { + if(Short.useMeasurements) { ref.Short = ts_short->interpolate(frequency).S[0]; } else { // calculate inductance for short - double Lseries = SOLT.Short.L0 * 1e-12 + SOLT.Short.L1 * 1e-24 * frequency + SOLT.Short.L2 * 1e-33 * pow(frequency, 2) + SOLT.Short.L3 * 1e-42 * pow(frequency, 3); + double Lseries = Short.L0 * 1e-12 + Short.L1 * 1e-24 * frequency + Short.L2 * 1e-33 * pow(frequency, 2) + Short.L3 * 1e-42 * pow(frequency, 3); // convert to impedance auto imp_short = complex(0, frequency * 2 * M_PI * Lseries); ref.Short = (imp_short - complex(50.0)) / (imp_short + complex(50.0)); - ref.Short = addTransmissionLine(ref.Short, SOLT.Short.Z0, SOLT.Short.delay*1e-12, SOLT.Short.loss*1e9, frequency); + ref.Short = addTransmissionLine(ref.Short, Short.Z0, Short.delay*1e-12, Short.loss*1e9, frequency); } if(SOLT.Through.useMeasurements) { @@ -300,47 +312,91 @@ class Calkit::TRL Calkit::toTRL(double) return trl; } -double Calkit::minFreq(bool trl) +double Calkit::minFreqTRL() { - if(trl) { - return TRL.Line.minFreq; - } else { - fillTouchstoneCache(); - double min = 0; - array ts_list = {ts_open, ts_short, ts_load, ts_through}; - // find the highest minimum frequency in all measurement files - for(auto ts : ts_list) { - if(!ts) { - // this calibration standard is defined by coefficients, no minimum frequency - continue; - } - if(ts->minFreq() > min) { - min = ts->minFreq(); - } - } - return min; - } + return TRL.Line.minFreq; } -double Calkit::maxFreq(bool trl) +double Calkit::maxFreqTRL() { - if(trl) { - return TRL.Line.maxFreq; + return TRL.Line.maxFreq; +} + +double Calkit::minFreqSOLT(bool male_standards) +{ + fillTouchstoneCache(); + double min = 0; + auto ts_load = male_standards ? ts_load_m : ts_load_f; + auto ts_short = male_standards ? ts_short_m : ts_short_f; + auto ts_open = male_standards ? ts_open_m : ts_open_f; + array ts_list = {ts_open, ts_short, ts_load, ts_through}; + // find the highest minimum frequency in all measurement files + for(auto ts : ts_list) { + if(!ts) { + // this calibration standard is defined by coefficients, no minimum frequency + continue; + } + if(ts->minFreq() > min) { + min = ts->minFreq(); + } + } + return min; +} + +double Calkit::maxFreqSOLT(bool male_standards) +{ + fillTouchstoneCache(); + double max = std::numeric_limits::max(); + auto ts_load = male_standards ? ts_load_m : ts_load_f; + auto ts_short = male_standards ? ts_short_m : ts_short_f; + auto ts_open = male_standards ? ts_open_m : ts_open_f; + array ts_list = {ts_open, ts_short, ts_load, ts_through}; + // find the highest minimum frequency in all measurement files + for(auto ts : ts_list) { + if(!ts) { + // this calibration standard is defined by coefficients, no minimum frequency + continue; + } + if(ts->maxFreq() < max) { + max = ts->maxFreq(); + } + } + return max; +} + +bool Calkit::checkIfValid(double min_freq, double max_freq, bool isTRL, bool include_male, bool include_female) +{ + auto min_supported = std::numeric_limits::min(); + auto max_supported = std::numeric_limits::max(); + if(isTRL) { + min_supported = TRL.Line.minFreq; + max_supported = TRL.Line.maxFreq; } else { - fillTouchstoneCache(); - double max = std::numeric_limits::max(); - array ts_list = {ts_open, ts_short, ts_load, ts_through}; - // find the highest minimum frequency in all measurement files - for(auto ts : ts_list) { - if(!ts) { - // this calibration standard is defined by coefficients, no minimum frequency - continue; + if(include_male) { + auto min_male = minFreqSOLT(true); + auto max_male = maxFreqSOLT(true); + if(min_male > min_supported) { + min_supported = min_male; } - if(ts->maxFreq() < max) { - max = ts->maxFreq(); + if(max_male > max_supported) { + max_supported = max_male; } } - return max; + if(include_female) { + auto min_female = minFreqSOLT(false); + auto max_female = maxFreqSOLT(false); + if(min_female > min_supported) { + min_supported = min_female; + } + if(max_female > max_supported) { + max_supported = max_female; + } + } + } + if(min_supported <= min_freq && max_supported >= max_freq) { + return true; + } else { + return false; } } @@ -351,7 +407,7 @@ bool Calkit::isTRLReflectionShort() const void Calkit::TransformPathsToRelative(QFileInfo d) { - vector filenames = {&SOLT.Short.file, &SOLT.Open.file, &SOLT.Load.file, &SOLT.Through.file}; + vector filenames = {&SOLT.short_m.file, &SOLT.open_m.file, &SOLT.load_m.file, &SOLT.short_f.file, &SOLT.open_f.file, &SOLT.load_f.file, &SOLT.Through.file}; for(auto f : filenames) { if(f->isEmpty()) { continue; @@ -366,7 +422,7 @@ void Calkit::TransformPathsToRelative(QFileInfo d) void Calkit::TransformPathsToAbsolute(QFileInfo d) { - vector filenames = {&SOLT.Short.file, &SOLT.Open.file, &SOLT.Load.file, &SOLT.Through.file}; + vector filenames = {&SOLT.short_m.file, &SOLT.open_m.file, &SOLT.load_m.file, &SOLT.short_f.file, &SOLT.open_f.file, &SOLT.load_f.file, &SOLT.Through.file}; for(auto f : filenames) { if(f->isEmpty()) { continue; @@ -382,12 +438,18 @@ void Calkit::TransformPathsToAbsolute(QFileInfo d) void Calkit::clearTouchstoneCache() { - delete ts_open; - ts_open = nullptr; - delete ts_short; - ts_short = nullptr; - delete ts_load; - ts_load = nullptr; + delete ts_open_m; + ts_open_m = nullptr; + delete ts_short_m; + ts_short_m = nullptr; + delete ts_load_m; + ts_load_m = nullptr; + delete ts_open_f; + ts_open_f = nullptr; + delete ts_short_f; + ts_short_f = nullptr; + delete ts_load_f; + ts_load_f = nullptr; delete ts_through; ts_through = nullptr; ts_cached = false; @@ -398,20 +460,35 @@ void Calkit::fillTouchstoneCache() if(ts_cached) { return; } - if(SOLT.Open.useMeasurements) { - ts_open = new Touchstone(1); - *ts_open = Touchstone::fromFile(SOLT.Open.file.toStdString()); - ts_open->reduceTo1Port(SOLT.Open.Sparam); + if(SOLT.open_m.useMeasurements) { + ts_open_m = new Touchstone(1); + *ts_open_m = Touchstone::fromFile(SOLT.open_m.file.toStdString()); + ts_open_m->reduceTo1Port(SOLT.open_m.Sparam); } - if(SOLT.Short.useMeasurements) { - ts_short = new Touchstone(1); - *ts_short = Touchstone::fromFile(SOLT.Short.file.toStdString()); - ts_short->reduceTo1Port(SOLT.Short.Sparam); + if(SOLT.short_m.useMeasurements) { + ts_short_m = new Touchstone(1); + *ts_short_m = Touchstone::fromFile(SOLT.short_m.file.toStdString()); + ts_short_m->reduceTo1Port(SOLT.short_m.Sparam); } - if(SOLT.Load.useMeasurements) { - ts_load = new Touchstone(1); - *ts_load = Touchstone::fromFile(SOLT.Load.file.toStdString()); - ts_load->reduceTo1Port(SOLT.Load.Sparam); + if(SOLT.load_m.useMeasurements) { + ts_load_m = new Touchstone(1); + *ts_load_m = Touchstone::fromFile(SOLT.load_m.file.toStdString()); + ts_load_m->reduceTo1Port(SOLT.load_m.Sparam); + } + if(SOLT.open_f.useMeasurements) { + ts_open_f = new Touchstone(1); + *ts_open_f = Touchstone::fromFile(SOLT.open_f.file.toStdString()); + ts_open_f->reduceTo1Port(SOLT.open_f.Sparam); + } + if(SOLT.short_f.useMeasurements) { + ts_short_f = new Touchstone(1); + *ts_short_f = Touchstone::fromFile(SOLT.short_f.file.toStdString()); + ts_short_f->reduceTo1Port(SOLT.short_f.Sparam); + } + if(SOLT.load_f.useMeasurements) { + ts_load_f = new Touchstone(1); + *ts_load_f = Touchstone::fromFile(SOLT.load_f.file.toStdString()); + ts_load_f->reduceTo1Port(SOLT.load_f.Sparam); } if(SOLT.Through.useMeasurements) { ts_through = new Touchstone(2); diff --git a/Software/PC_Application/Calibration/calkit.h b/Software/PC_Application/Calibration/calkit.h index 093fbeb..21eb063 100644 --- a/Software/PC_Application/Calibration/calkit.h +++ b/Software/PC_Application/Calibration/calkit.h @@ -41,10 +41,13 @@ public: void toFile(QString filename); static Calkit fromFile(QString filename); void edit(std::function done = nullptr); - SOLT toSOLT(double frequency); + SOLT toSOLT(double frequency, bool male_standards = true); TRL toTRL(double frequency); - double minFreq(bool trl = false); - double maxFreq(bool trl = false); + double minFreqTRL(); + double maxFreqTRL(); + double minFreqSOLT(bool male_standards = true); + double maxFreqSOLT(bool male_standards = true); + bool checkIfValid(double min_freq, double max_freq, bool isTRL, bool include_male, bool include_female); bool isTRLReflectionShort() const; private: @@ -54,30 +57,34 @@ private: QString manufacturer, serialnumber, description; // SOLT standard definitions struct { - struct { + using Open = struct { double Z0, delay, loss, C0, C1, C2, C3; QString file; bool useMeasurements; int Sparam; - } Open; - struct { + }; + Open open_m, open_f; + using Short = struct { double Z0, delay, loss, L0, L1, L2, L3; QString file; bool useMeasurements; int Sparam; - } Short; - struct { + }; + Short short_m, short_f; + using Load = struct { double Z0, delay, Cparallel, Lseries; QString file; bool useMeasurements; int Sparam; - } Load; + }; + Load load_m, load_f; struct { double Z0, delay, loss; QString file; bool useMeasurements; int Sparam1, Sparam2; } Through; + bool separate_male_female; } SOLT; struct { struct { @@ -92,7 +99,9 @@ private: } TRL; bool startDialogWithSOLT; - Touchstone *ts_open, *ts_short, *ts_load, *ts_through; + Touchstone *ts_open_m, *ts_short_m, *ts_load_m; + Touchstone *ts_open_f, *ts_short_f, *ts_load_f; + Touchstone *ts_through; bool ts_cached; using JSONDescription = struct _jsondescr { @@ -100,40 +109,67 @@ private: QString name; QVariant def; }; - const std::array json_descr = {{ + const std::array json_descr = {{ {&manufacturer, "Manufacturer", ""}, {&serialnumber, "Serialnumber", ""}, {&description, "Description", ""}, - {&SOLT.Open.Z0, "SOLT/Open/Param/Z0", 50.0}, - {&SOLT.Open.delay, "SOLT/Open/Param/Delay", 0.0}, - {&SOLT.Open.loss, "SOLT/Open/Param/Loss", 0.0}, - {&SOLT.Open.C0, "SOLT/Open/Param/C0", 0.0}, - {&SOLT.Open.C1, "SOLT/Open/Param/C1", 0.0}, - {&SOLT.Open.C2, "SOLT/Open/Param/C2", 0.0}, - {&SOLT.Open.C3, "SOLT/Open/Param/C3", 0.0}, - {&SOLT.Open.useMeasurements, "SOLT/Open/Measurements/Use", false}, - {&SOLT.Open.file, "SOLT/Open/Measurements/File", ""}, - {&SOLT.Open.Sparam, "SOLT/Open/Measurements/Port", 0}, + {&SOLT.open_m.Z0, "SOLT/Open/Param/Z0", 50.0}, + {&SOLT.open_m.delay, "SOLT/Open/Param/Delay", 0.0}, + {&SOLT.open_m.loss, "SOLT/Open/Param/Loss", 0.0}, + {&SOLT.open_m.C0, "SOLT/Open/Param/C0", 0.0}, + {&SOLT.open_m.C1, "SOLT/Open/Param/C1", 0.0}, + {&SOLT.open_m.C2, "SOLT/Open/Param/C2", 0.0}, + {&SOLT.open_m.C3, "SOLT/Open/Param/C3", 0.0}, + {&SOLT.open_m.useMeasurements, "SOLT/Open/Measurements/Use", false}, + {&SOLT.open_m.file, "SOLT/Open/Measurements/File", ""}, + {&SOLT.open_m.Sparam, "SOLT/Open/Measurements/Port", 0}, + {&SOLT.open_f.Z0, "SOLT/Open/Param/Z0_Female", 50.0}, + {&SOLT.open_f.delay, "SOLT/Open/Param/Delay_Female", 0.0}, + {&SOLT.open_f.loss, "SOLT/Open/Param/Loss_Female", 0.0}, + {&SOLT.open_f.C0, "SOLT/Open/Param/C0_Female", 0.0}, + {&SOLT.open_f.C1, "SOLT/Open/Param/C1_Female", 0.0}, + {&SOLT.open_f.C2, "SOLT/Open/Param/C2_Female", 0.0}, + {&SOLT.open_f.C3, "SOLT/Open/Param/C3_Female", 0.0}, + {&SOLT.open_f.useMeasurements, "SOLT/Open/Measurements/Use_Female", false}, + {&SOLT.open_f.file, "SOLT/Open/Measurements/File_Female", ""}, + {&SOLT.open_f.Sparam, "SOLT/Open/Measurements/Port_Female", 0}, - {&SOLT.Short.Z0, "SOLT/Short/Param/Z0", 50.0}, - {&SOLT.Short.delay, "SOLT/Short/Param/Delay", 0.0}, - {&SOLT.Short.loss, "SOLT/Short/Param/Loss", 0.0}, - {&SOLT.Short.L0, "SOLT/Short/Param/L0", 0.0}, - {&SOLT.Short.L1, "SOLT/Short/Param/L1", 0.0}, - {&SOLT.Short.L2, "SOLT/Short/Param/L2", 0.0}, - {&SOLT.Short.L3, "SOLT/Short/Param/L3", 0.0}, - {&SOLT.Short.useMeasurements, "SOLT/Short/Measurements/Use", false}, - {&SOLT.Short.file, "SOLT/Short/Measurements/File", ""}, - {&SOLT.Short.Sparam, "SOLT/Short/Measurements/Port", 0}, + {&SOLT.short_m.Z0, "SOLT/Short/Param/Z0", 50.0}, + {&SOLT.short_m.delay, "SOLT/Short/Param/Delay", 0.0}, + {&SOLT.short_m.loss, "SOLT/Short/Param/Loss", 0.0}, + {&SOLT.short_m.L0, "SOLT/Short/Param/L0", 0.0}, + {&SOLT.short_m.L1, "SOLT/Short/Param/L1", 0.0}, + {&SOLT.short_m.L2, "SOLT/Short/Param/L2", 0.0}, + {&SOLT.short_m.L3, "SOLT/Short/Param/L3", 0.0}, + {&SOLT.short_m.useMeasurements, "SOLT/Short/Measurements/Use", false}, + {&SOLT.short_m.file, "SOLT/Short/Measurements/File", ""}, + {&SOLT.short_m.Sparam, "SOLT/Short/Measurements/Port", 0}, + {&SOLT.short_f.Z0, "SOLT/Short/Param/Z0_Female", 50.0}, + {&SOLT.short_f.delay, "SOLT/Short/Param/Delay_Female", 0.0}, + {&SOLT.short_f.loss, "SOLT/Short/Param/Loss_Female", 0.0}, + {&SOLT.short_f.L0, "SOLT/Short/Param/L0_Female", 0.0}, + {&SOLT.short_f.L1, "SOLT/Short/Param/L1_Female", 0.0}, + {&SOLT.short_f.L2, "SOLT/Short/Param/L2_Female", 0.0}, + {&SOLT.short_f.L3, "SOLT/Short/Param/L3_Female", 0.0}, + {&SOLT.short_f.useMeasurements, "SOLT/Short/Measurements/Use_Female", false}, + {&SOLT.short_f.file, "SOLT/Short/Measurements/File_Female", ""}, + {&SOLT.short_f.Sparam, "SOLT/Short/Measurements/Port_Female", 0}, - {&SOLT.Load.Z0, "SOLT/Load/Param/Z0", 50.0}, - {&SOLT.Load.delay, "SOLT/Load/Param/Delay", 0.0}, - {&SOLT.Load.Cparallel, "SOLT/Load/Param/C", 0.0}, - {&SOLT.Load.Lseries, "SOLT/Load/Param/L", 0.0}, - {&SOLT.Load.useMeasurements, "SOLT/Load/Measurements/Use", false}, - {&SOLT.Load.file, "SOLT/Load/Measurements/File", ""}, - {&SOLT.Load.Sparam, "SOLT/Load/Measurements/Port", 0}, + {&SOLT.load_m.Z0, "SOLT/Load/Param/Z0", 50.0}, + {&SOLT.load_m.delay, "SOLT/Load/Param/Delay", 0.0}, + {&SOLT.load_m.Cparallel, "SOLT/Load/Param/C", 0.0}, + {&SOLT.load_m.Lseries, "SOLT/Load/Param/L", 0.0}, + {&SOLT.load_m.useMeasurements, "SOLT/Load/Measurements/Use", false}, + {&SOLT.load_m.file, "SOLT/Load/Measurements/File", ""}, + {&SOLT.load_m.Sparam, "SOLT/Load/Measurements/Port", 0}, + {&SOLT.load_f.Z0, "SOLT/Load/Param/Z0_Female", 50.0}, + {&SOLT.load_f.delay, "SOLT/Load/Param/Delay_Female", 0.0}, + {&SOLT.load_f.Cparallel, "SOLT/Load/Param/C_Female", 0.0}, + {&SOLT.load_f.Lseries, "SOLT/Load/Param/L_Female", 0.0}, + {&SOLT.load_f.useMeasurements, "SOLT/Load/Measurements/Use_Female", false}, + {&SOLT.load_f.file, "SOLT/Load/Measurements/File_Female", ""}, + {&SOLT.load_f.Sparam, "SOLT/Load/Measurements/Port_Female", 0}, {&SOLT.Through.Z0, "SOLT/Through/Param/Z0", 50.0}, {&SOLT.Through.delay, "SOLT/Through/Param/Delay", 0.0}, @@ -143,6 +179,8 @@ private: {&SOLT.Through.Sparam1, "SOLT/Through/Measurements/Port1", 0}, {&SOLT.Through.Sparam2, "SOLT/Through/Measurements/Port2", 1}, + {&SOLT.separate_male_female, "SOLT/SeparateMaleFemale", false}, + {&TRL.Through.Z0, "TRL/Through/Z0", 50.0}, {&TRL.Reflection.isShort, "TRL/Reflect/isShort", false}, {&TRL.Line.delay, "TRL/Line/Delay", 74.0}, diff --git a/Software/PC_Application/Calibration/calkitdialog.cpp b/Software/PC_Application/Calibration/calkitdialog.cpp index 2687411..a76921b 100644 --- a/Software/PC_Application/Calibration/calkitdialog.cpp +++ b/Software/PC_Application/Calibration/calkitdialog.cpp @@ -47,6 +47,29 @@ CalkitDialog::CalkitDialog(Calkit &c, QWidget *parent) : ui->load_parC->setPrefixes("fpnum "); ui->load_serL->setUnit("H"); ui->load_serL->setPrefixes("fpnum "); + + // Same setup for female standards + ui->OpenType_f->setId(ui->open_coefficients_f, 0); + ui->OpenType_f->setId(ui->open_measurement_f, 1); + + ui->ShortType_f->setId(ui->short_coefficients_f, 0); + ui->ShortType_f->setId(ui->short_measurement_f, 1); + + ui->LoadType_f->setId(ui->load_coefficients_f, 0); + ui->LoadType_f->setId(ui->load_measurement_f, 1); + + ui->open_touchstone_f->setPorts(1); + ui->short_touchstone_f->setPorts(1); + ui->load_touchstone_f->setPorts(1); + + ui->short_Z0_f->setUnit("Ω"); + ui->open_Z0_f->setUnit("Ω"); + ui->load_Z0_f->setUnit("Ω"); + ui->load_parC_f->setUnit("F"); + ui->load_parC_f->setPrefixes("fpnum "); + ui->load_serL_f->setUnit("H"); + ui->load_serL_f->setPrefixes("fpnum "); + ui->through_Z0->setUnit("Ω"); ui->TRL_through_Z0->setUnit("Ω"); @@ -59,6 +82,24 @@ CalkitDialog::CalkitDialog(Calkit &c, QWidget *parent) : editKit.clearTouchstoneCache(); ownKit = editKit; + + connect(ui->cbStandardDefinition, qOverload(&QComboBox::currentIndexChanged), [=](int index){ + if (index == 0) { + // common definition, hide tab bars, set all to male tab + ui->mf_short->setCurrentIndex(0); + ui->mf_short->tabBar()->hide(); + ui->mf_open->setCurrentIndex(0); + ui->mf_open->tabBar()->hide(); + ui->mf_load->setCurrentIndex(0); + ui->mf_load->tabBar()->hide(); + } else { + // separate definitions for male/female standards + ui->mf_short->tabBar()->show(); + ui->mf_open->tabBar()->show(); + ui->mf_load->tabBar()->show(); + } + }); + updateEntries(); connect(ui->TRL_line_min, &SIUnitEdit::valueChanged, [=](double newval){ @@ -85,6 +126,15 @@ CalkitDialog::CalkitDialog(Calkit &c, QWidget *parent) : if(ui->load_measurement->isChecked() && !ui->load_touchstone->getStatus()) { ok = false; } + if(ui->open_measurement_f->isChecked() && !ui->open_touchstone_f->getStatus()) { + ok = false; + } + if(ui->short_measurement_f->isChecked() && !ui->short_touchstone_f->getStatus()) { + ok = false; + } + if(ui->load_measurement_f->isChecked() && !ui->load_touchstone_f->getStatus()) { + ok = false; + } if(ui->through_measurement->isChecked() && !ui->through_touchstone->getStatus()) { ok = false; } @@ -95,6 +145,9 @@ CalkitDialog::CalkitDialog(Calkit &c, QWidget *parent) : connect(ui->open_touchstone, &TouchstoneImport::statusChanged, UpdateStatus); connect(ui->short_touchstone, &TouchstoneImport::statusChanged, UpdateStatus); connect(ui->load_touchstone, &TouchstoneImport::statusChanged, UpdateStatus); + connect(ui->open_touchstone_f, &TouchstoneImport::statusChanged, UpdateStatus); + connect(ui->short_touchstone_f, &TouchstoneImport::statusChanged, UpdateStatus); + connect(ui->load_touchstone_f, &TouchstoneImport::statusChanged, UpdateStatus); connect(ui->through_touchstone, &TouchstoneImport::statusChanged, UpdateStatus); connect(ui->OpenType, qOverload(&QButtonGroup::buttonClicked), [=](int) { @@ -106,6 +159,15 @@ CalkitDialog::CalkitDialog(Calkit &c, QWidget *parent) : connect(ui->LoadType, qOverload(&QButtonGroup::buttonClicked), [=](int) { UpdateStatus(); }); + connect(ui->OpenType_f, qOverload(&QButtonGroup::buttonClicked), [=](int) { + UpdateStatus(); + }); + connect(ui->ShortType_f, qOverload(&QButtonGroup::buttonClicked), [=](int) { + UpdateStatus(); + }); + connect(ui->LoadType_f, qOverload(&QButtonGroup::buttonClicked), [=](int) { + UpdateStatus(); + }); connect(ui->ThroughType, qOverload(&QButtonGroup::buttonClicked), [=](int) { UpdateStatus(); }); @@ -152,49 +214,83 @@ void CalkitDialog::parseEntries() ownKit.description = ui->description->toPlainText(); // type - ownKit.SOLT.Open.useMeasurements = ui->open_measurement->isChecked(); - ownKit.SOLT.Short.useMeasurements = ui->short_measurement->isChecked(); - ownKit.SOLT.Load.useMeasurements = ui->load_measurement->isChecked(); + ownKit.SOLT.open_m.useMeasurements = ui->open_measurement->isChecked(); + ownKit.SOLT.short_m.useMeasurements = ui->short_measurement->isChecked(); + ownKit.SOLT.load_m.useMeasurements = ui->load_measurement->isChecked(); + ownKit.SOLT.open_f.useMeasurements = ui->open_measurement_f->isChecked(); + ownKit.SOLT.short_f.useMeasurements = ui->short_measurement_f->isChecked(); + ownKit.SOLT.load_f.useMeasurements = ui->load_measurement_f->isChecked(); ownKit.SOLT.Through.useMeasurements = ui->through_measurement->isChecked(); // coefficients - ownKit.SOLT.Open.Z0 = ui->open_Z0->value(); - ownKit.SOLT.Open.delay = ui->open_delay->value(); - ownKit.SOLT.Open.loss = ui->open_loss->value(); - ownKit.SOLT.Open.C0 = ui->open_C0->value(); - ownKit.SOLT.Open.C1 = ui->open_C1->value(); - ownKit.SOLT.Open.C2 = ui->open_C2->value(); - ownKit.SOLT.Open.C3 = ui->open_C3->value(); + ownKit.SOLT.open_m.Z0 = ui->open_Z0->value(); + ownKit.SOLT.open_m.delay = ui->open_delay->value(); + ownKit.SOLT.open_m.loss = ui->open_loss->value(); + ownKit.SOLT.open_m.C0 = ui->open_C0->value(); + ownKit.SOLT.open_m.C1 = ui->open_C1->value(); + ownKit.SOLT.open_m.C2 = ui->open_C2->value(); + ownKit.SOLT.open_m.C3 = ui->open_C3->value(); - ownKit.SOLT.Short.Z0 = ui->short_Z0->value(); - ownKit.SOLT.Short.delay = ui->short_delay->value(); - ownKit.SOLT.Short.loss = ui->short_loss->value(); - ownKit.SOLT.Short.L0 = ui->short_L0->value(); - ownKit.SOLT.Short.L1 = ui->short_L1->value(); - ownKit.SOLT.Short.L2 = ui->short_L2->value(); - ownKit.SOLT.Short.L3 = ui->short_L3->value(); + ownKit.SOLT.short_m.Z0 = ui->short_Z0->value(); + ownKit.SOLT.short_m.delay = ui->short_delay->value(); + ownKit.SOLT.short_m.loss = ui->short_loss->value(); + ownKit.SOLT.short_m.L0 = ui->short_L0->value(); + ownKit.SOLT.short_m.L1 = ui->short_L1->value(); + ownKit.SOLT.short_m.L2 = ui->short_L2->value(); + ownKit.SOLT.short_m.L3 = ui->short_L3->value(); - ownKit.SOLT.Load.Z0 = ui->load_Z0->value(); - ownKit.SOLT.Load.delay = ui->load_delay->value(); - ownKit.SOLT.Load.Cparallel = ui->load_parC->value(); - ownKit.SOLT.Load.Lseries = ui->load_serL->value(); + ownKit.SOLT.load_m.Z0 = ui->load_Z0->value(); + ownKit.SOLT.load_m.delay = ui->load_delay->value(); + ownKit.SOLT.load_m.Cparallel = ui->load_parC->value(); + ownKit.SOLT.load_m.Lseries = ui->load_serL->value(); + + ownKit.SOLT.open_f.Z0 = ui->open_Z0_f->value(); + ownKit.SOLT.open_f.delay = ui->open_delay_f->value(); + ownKit.SOLT.open_f.loss = ui->open_loss_f->value(); + ownKit.SOLT.open_f.C0 = ui->open_C0_f->value(); + ownKit.SOLT.open_f.C1 = ui->open_C1_f->value(); + ownKit.SOLT.open_f.C2 = ui->open_C2_f->value(); + ownKit.SOLT.open_f.C3 = ui->open_C3_f->value(); + + ownKit.SOLT.short_f.Z0 = ui->short_Z0_f->value(); + ownKit.SOLT.short_f.delay = ui->short_delay_f->value(); + ownKit.SOLT.short_f.loss = ui->short_loss_f->value(); + ownKit.SOLT.short_f.L0 = ui->short_L0_f->value(); + ownKit.SOLT.short_f.L1 = ui->short_L1_f->value(); + ownKit.SOLT.short_f.L2 = ui->short_L2_f->value(); + ownKit.SOLT.short_f.L3 = ui->short_L3_f->value(); + + ownKit.SOLT.load_f.Z0 = ui->load_Z0_f->value(); + ownKit.SOLT.load_f.delay = ui->load_delay_f->value(); + ownKit.SOLT.load_f.Cparallel = ui->load_parC_f->value(); + ownKit.SOLT.load_f.Lseries = ui->load_serL_f->value(); ownKit.SOLT.Through.Z0 = ui->through_Z0->value(); ownKit.SOLT.Through.delay = ui->through_delay->value(); ownKit.SOLT.Through.loss = ui->through_loss->value(); + ownKit.SOLT.separate_male_female = ui->cbStandardDefinition->currentIndex() == 1; + // file - ownKit.SOLT.Open.file = ui->open_touchstone->getFilename(); - ownKit.SOLT.Short.file = ui->short_touchstone->getFilename(); - ownKit.SOLT.Load.file = ui->load_touchstone->getFilename(); + ownKit.SOLT.open_m.file = ui->open_touchstone->getFilename(); + ownKit.SOLT.short_m.file = ui->short_touchstone->getFilename(); + ownKit.SOLT.load_m.file = ui->load_touchstone->getFilename(); ownKit.SOLT.Through.file = ui->through_touchstone->getFilename(); - ownKit.SOLT.Open.Sparam = ui->open_touchstone->getPorts()[0]; - ownKit.SOLT.Short.Sparam = ui->short_touchstone->getPorts()[0]; - ownKit.SOLT.Load.Sparam = ui->load_touchstone->getPorts()[0]; + ownKit.SOLT.open_m.Sparam = ui->open_touchstone->getPorts()[0]; + ownKit.SOLT.short_m.Sparam = ui->short_touchstone->getPorts()[0]; + ownKit.SOLT.load_m.Sparam = ui->load_touchstone->getPorts()[0]; ownKit.SOLT.Through.Sparam1 = ui->through_touchstone->getPorts()[0]; ownKit.SOLT.Through.Sparam2 = ui->through_touchstone->getPorts()[1]; + ownKit.SOLT.open_f.file = ui->open_touchstone_f->getFilename(); + ownKit.SOLT.short_f.file = ui->short_touchstone_f->getFilename(); + ownKit.SOLT.load_f.file = ui->load_touchstone_f->getFilename(); + + ownKit.SOLT.open_f.Sparam = ui->open_touchstone_f->getPorts()[0]; + ownKit.SOLT.short_f.Sparam = ui->short_touchstone_f->getPorts()[0]; + ownKit.SOLT.load_f.Sparam = ui->load_touchstone_f->getPorts()[0]; + // TRL ownKit.TRL.Through.Z0 = ui->TRL_through_Z0->value(); ownKit.TRL.Reflection.isShort = ui->TRL_R_short->isChecked(); @@ -212,70 +308,124 @@ void CalkitDialog::updateEntries() ui->description->setPlainText(ownKit.description); // Coefficients - ui->open_Z0->setValueQuiet(ownKit.SOLT.Open.Z0); - ui->open_delay->setValueQuiet(ownKit.SOLT.Open.delay); - ui->open_loss->setValueQuiet(ownKit.SOLT.Open.loss); - ui->open_C0->setValueQuiet(ownKit.SOLT.Open.C0); - ui->open_C1->setValueQuiet(ownKit.SOLT.Open.C1); - ui->open_C2->setValueQuiet(ownKit.SOLT.Open.C2); - ui->open_C3->setValueQuiet(ownKit.SOLT.Open.C3); + ui->open_Z0->setValueQuiet(ownKit.SOLT.open_m.Z0); + ui->open_delay->setValueQuiet(ownKit.SOLT.open_m.delay); + ui->open_loss->setValueQuiet(ownKit.SOLT.open_m.loss); + ui->open_C0->setValueQuiet(ownKit.SOLT.open_m.C0); + ui->open_C1->setValueQuiet(ownKit.SOLT.open_m.C1); + ui->open_C2->setValueQuiet(ownKit.SOLT.open_m.C2); + ui->open_C3->setValueQuiet(ownKit.SOLT.open_m.C3); - ui->short_Z0->setValueQuiet(ownKit.SOLT.Short.Z0); - ui->short_delay->setValueQuiet(ownKit.SOLT.Short.delay); - ui->short_loss->setValueQuiet(ownKit.SOLT.Short.loss); - ui->short_L0->setValueQuiet(ownKit.SOLT.Short.L0); - ui->short_L1->setValueQuiet(ownKit.SOLT.Short.L1); - ui->short_L2->setValueQuiet(ownKit.SOLT.Short.L2); - ui->short_L3->setValueQuiet(ownKit.SOLT.Short.L3); + ui->short_Z0->setValueQuiet(ownKit.SOLT.short_m.Z0); + ui->short_delay->setValueQuiet(ownKit.SOLT.short_m.delay); + ui->short_loss->setValueQuiet(ownKit.SOLT.short_m.loss); + ui->short_L0->setValueQuiet(ownKit.SOLT.short_m.L0); + ui->short_L1->setValueQuiet(ownKit.SOLT.short_m.L1); + ui->short_L2->setValueQuiet(ownKit.SOLT.short_m.L2); + ui->short_L3->setValueQuiet(ownKit.SOLT.short_m.L3); - ui->load_Z0->setValueQuiet(ownKit.SOLT.Load.Z0); - ui->load_delay->setValueQuiet(ownKit.SOLT.Load.delay); - ui->load_parC->setValueQuiet(ownKit.SOLT.Load.Cparallel); - ui->load_serL->setValueQuiet(ownKit.SOLT.Load.Lseries); + ui->load_Z0->setValueQuiet(ownKit.SOLT.load_m.Z0); + ui->load_delay->setValueQuiet(ownKit.SOLT.load_m.delay); + ui->load_parC->setValueQuiet(ownKit.SOLT.load_m.Cparallel); + ui->load_serL->setValueQuiet(ownKit.SOLT.load_m.Lseries); + + ui->open_Z0_f->setValueQuiet(ownKit.SOLT.open_f.Z0); + ui->open_delay_f->setValueQuiet(ownKit.SOLT.open_f.delay); + ui->open_loss_f->setValueQuiet(ownKit.SOLT.open_f.loss); + ui->open_C0_f->setValueQuiet(ownKit.SOLT.open_f.C0); + ui->open_C1_f->setValueQuiet(ownKit.SOLT.open_f.C1); + ui->open_C2_f->setValueQuiet(ownKit.SOLT.open_f.C2); + ui->open_C3_f->setValueQuiet(ownKit.SOLT.open_f.C3); + + ui->short_Z0_f->setValueQuiet(ownKit.SOLT.short_f.Z0); + ui->short_delay_f->setValueQuiet(ownKit.SOLT.short_f.delay); + ui->short_loss_f->setValueQuiet(ownKit.SOLT.short_f.loss); + ui->short_L0_f->setValueQuiet(ownKit.SOLT.short_f.L0); + ui->short_L1_f->setValueQuiet(ownKit.SOLT.short_f.L1); + ui->short_L2_f->setValueQuiet(ownKit.SOLT.short_f.L2); + ui->short_L3_f->setValueQuiet(ownKit.SOLT.short_f.L3); + + ui->load_Z0_f->setValueQuiet(ownKit.SOLT.load_f.Z0); + ui->load_delay_f->setValueQuiet(ownKit.SOLT.load_f.delay); + ui->load_parC_f->setValueQuiet(ownKit.SOLT.load_f.Cparallel); + ui->load_serL_f->setValueQuiet(ownKit.SOLT.load_f.Lseries); ui->through_Z0->setValueQuiet(ownKit.SOLT.Through.Z0); ui->through_delay->setValueQuiet(ownKit.SOLT.Through.delay); ui->through_loss->setValueQuiet(ownKit.SOLT.Through.loss); // Measurements - ui->open_touchstone->setFile(ownKit.SOLT.Open.file); - ui->open_touchstone->selectPort(0, ownKit.SOLT.Open.Sparam); + ui->open_touchstone->setFile(ownKit.SOLT.open_m.file); + ui->open_touchstone->selectPort(0, ownKit.SOLT.open_m.Sparam); - ui->short_touchstone->setFile(ownKit.SOLT.Short.file); - ui->short_touchstone->selectPort(0, ownKit.SOLT.Short.Sparam); + ui->short_touchstone->setFile(ownKit.SOLT.short_m.file); + ui->short_touchstone->selectPort(0, ownKit.SOLT.short_m.Sparam); - ui->load_touchstone->setFile(ownKit.SOLT.Load.file); - ui->load_touchstone->selectPort(0, ownKit.SOLT.Load.Sparam); + ui->load_touchstone->setFile(ownKit.SOLT.load_m.file); + ui->load_touchstone->selectPort(0, ownKit.SOLT.load_m.Sparam); + + ui->open_touchstone_f->setFile(ownKit.SOLT.open_f.file); + ui->open_touchstone_f->selectPort(0, ownKit.SOLT.open_f.Sparam); + + ui->short_touchstone_f->setFile(ownKit.SOLT.short_f.file); + ui->short_touchstone_f->selectPort(0, ownKit.SOLT.short_f.Sparam); + + ui->load_touchstone_f->setFile(ownKit.SOLT.load_f.file); + ui->load_touchstone_f->selectPort(0, ownKit.SOLT.load_f.Sparam); ui->through_touchstone->setFile(ownKit.SOLT.Through.file); ui->through_touchstone->selectPort(0, ownKit.SOLT.Through.Sparam1); ui->through_touchstone->selectPort(1, ownKit.SOLT.Through.Sparam2); // Type - if (ownKit.SOLT.Open.useMeasurements) { + if (ownKit.SOLT.open_m.useMeasurements) { ui->open_measurement->click(); } else { ui->open_coefficients->click(); } - if (ownKit.SOLT.Short.useMeasurements) { + if (ownKit.SOLT.short_m.useMeasurements) { ui->short_measurement->click(); } else { ui->short_coefficients->click(); } - if (ownKit.SOLT.Load.useMeasurements) { + if (ownKit.SOLT.load_m.useMeasurements) { ui->load_measurement->click(); } else { ui->load_coefficients->click(); } + if (ownKit.SOLT.open_f.useMeasurements) { + ui->open_measurement_f->click(); + } else { + ui->open_coefficients_f->click(); + } + + if (ownKit.SOLT.short_f.useMeasurements) { + ui->short_measurement_f->click(); + } else { + ui->short_coefficients_f->click(); + } + + if (ownKit.SOLT.load_f.useMeasurements) { + ui->load_measurement_f->click(); + } else { + ui->load_coefficients_f->click(); + } + if (ownKit.SOLT.Through.useMeasurements) { ui->through_measurement->click(); } else { ui->through_coefficients->click(); } + if (ownKit.SOLT.separate_male_female) { + ui->cbStandardDefinition->setCurrentIndex(1); + } else { + ui->cbStandardDefinition->setCurrentIndex(0); + } + // TRL ui->TRL_through_Z0->setValueQuiet(ownKit.TRL.Through.Z0); if(ownKit.TRL.Reflection.isShort) { diff --git a/Software/PC_Application/Calibration/calkitdialog.h b/Software/PC_Application/Calibration/calkitdialog.h index b06e073..a8b02b5 100644 --- a/Software/PC_Application/Calibration/calkitdialog.h +++ b/Software/PC_Application/Calibration/calkitdialog.h @@ -20,7 +20,7 @@ public: explicit CalkitDialog(Calkit &c, QWidget *parent = nullptr); ~CalkitDialog(); -private: +private: void parseEntries(); void updateEntries(); Ui::CalkitDialog *ui; diff --git a/Software/PC_Application/Calibration/calkitdialog.ui b/Software/PC_Application/Calibration/calkitdialog.ui index b904fea..a85820d 100644 --- a/Software/PC_Application/Calibration/calkitdialog.ui +++ b/Software/PC_Application/Calibration/calkitdialog.ui @@ -9,8 +9,8 @@ 0 0 - 1141 - 602 + 1213 + 626 @@ -28,7 +28,7 @@ false - + @@ -93,512 +93,945 @@ SOLT - + - + - - - - 16 - - - - Short - - - Qt::AlignCenter - - - - - + - - - Coefficients - - - ShortType - - - - - - - Measurement file - - - ShortType - - - - - - - - - 0 - - - + - - - + + + Standard definition: + + + + + + + 1 + + + + Use one common definition for open/short/load standard + + + + + Use separate male/female definitions for open/short/load standard + + + + + + + + + + + + + + + 16 + + - Offset delay [ps]: + Short + + + Qt::AlignCenter - - + + + + 1 + + + + Male + + + + + + + + Coefficients + + + ShortType + + + + + + + Measurement file + + + ShortType + + + + + + + + + 0 + + + + + + + + + Offset delay [ps]: + + + + + + + + + + Offset loss [GΩ/s]: + + + + + + + + + + <html><head/><body><p>L0 [10<span style=" vertical-align:super;">-12</span>H]:</p></body></html> + + + + + + + + + + <html><head/><body><p>L1 [10<span style=" vertical-align:super;">-24</span>H/Hz]:</p></body></html> + + + + + + + + + + <html><head/><body><p>L2 [10<span style=" vertical-align:super;">-33</span>H/Hz<span style=" vertical-align:super;">2</span>]:</p></body></html> + + + + + + + + + + <html><head/><body><p>L3 [10<span style=" vertical-align:super;">-42</span>H/Hz<span style=" vertical-align:super;">3</span>]:</p></body></html> + + + + + + + + + + true + + + + + + + Z0: + + + + + + + + + + + + + + + + + + + + + Female + + + + + + + + Coefficients + + + ShortType_f + + + + + + + Measurement file + + + ShortType_f + + + + + + + + + 0 + + + + + + + + + Offset delay [ps]: + + + + + + + + + + Offset loss [GΩ/s]: + + + + + + + + + + <html><head/><body><p>L0 [10<span style=" vertical-align:super;">-12</span>H]:</p></body></html> + + + + + + + + + + <html><head/><body><p>L1 [10<span style=" vertical-align:super;">-24</span>H/Hz]:</p></body></html> + + + + + + + + + + <html><head/><body><p>L2 [10<span style=" vertical-align:super;">-33</span>H/Hz<span style=" vertical-align:super;">2</span>]:</p></body></html> + + + + + + + + + + <html><head/><body><p>L3 [10<span style=" vertical-align:super;">-42</span>H/Hz<span style=" vertical-align:super;">3</span>]:</p></body></html> + + + + + + + + + + true + + + + + + + Z0: + + + + + + + + + + + + + + + + + + + - - + + + + + + Qt::Vertical + + + + + + + + + + 16 + + - Offset loss [GΩ/s]: + Open + + + Qt::AlignCenter - - + + + + 1 + + + + Male + + + + + + + + Coefficients + + + OpenType + + + + + + + Measurement file + + + OpenType + + + + + + + + + 0 + + + + + + + + + Offset delay [ps]: + + + + + + + + + + Offset loss [GΩ/s]: + + + + + + + + + + C0 [10<sup>-15</sup>F]: + + + + + + + + + + C1 [10<sup>-27</sup>F/Hz]: + + + + + + + + + + C2 [10<sup>-36</sup>F/Hz<sup>2</sup>]: + + + + + + + + + + <html><head/><body><p>C3 [10<span style=" vertical-align:super;">-45</span>F/Hz<span style=" vertical-align:super;">3</span>]:</p></body></html> + + + + + + + + + + true + + + + + + + Z0: + + + + + + + + + + + + + + + + + + + + + Female + + + + + + + + Coefficients + + + OpenType_f + + + + + + + Measurement file + + + OpenType_f + + + + + + + + + 0 + + + + + + + + + Offset delay [ps]: + + + + + + + + + + Offset loss [GΩ/s]: + + + + + + + + + + C0 [10<sup>-15</sup>F]: + + + + + + + + + + C1 [10<sup>-27</sup>F/Hz]: + + + + + + + + + + C2 [10<sup>-36</sup>F/Hz<sup>2</sup>]: + + + + + + + + + + <html><head/><body><p>C3 [10<span style=" vertical-align:super;">-45</span>F/Hz<span style=" vertical-align:super;">3</span>]:</p></body></html> + + + + + + + + + + true + + + + + + + Z0: + + + + + + + + + + + + + + + + + + + - - + + + + + + Qt::Vertical + + + + + + + + + + 16 + + - <html><head/><body><p>L0 [10<span style=" vertical-align:super;">-12</span>H]:</p></body></html> + Load + + + Qt::AlignCenter - - - - - - - <html><head/><body><p>L1 [10<span style=" vertical-align:super;">-24</span>H/Hz]:</p></body></html> - - - - - - - - - - <html><head/><body><p>L2 [10<span style=" vertical-align:super;">-33</span>H/Hz<span style=" vertical-align:super;">2</span>]:</p></body></html> - - - - - - - - - - <html><head/><body><p>L3 [10<span style=" vertical-align:super;">-42</span>H/Hz<span style=" vertical-align:super;">3</span>]:</p></body></html> - - - - - - - - - - true - - - - - - - Z0: + + + + 1 + + + Male + + + + + + + + Coefficients + + + LoadType + + + + + + + Measurement file + + + LoadType + + + + + + + + + 0 + + + + + + + + + Z0: + + + + + + + true + + + + + + + Offset delay [ps]: + + + + + + + + + + Parallel C: + + + + + + + + + + Series L: + + + + + + + + + + + + + + + + + + + + + + + + Female + + + + + + + + Coefficients + + + LoadType_f + + + + + + + Measurement file + + + LoadType_f + + + + + + + + + 0 + + + + + + + + + Z0: + + + + + + + true + + + + + + + Offset delay [ps]: + + + + + + + + + + Parallel C: + + + + + + + + + + Series L: + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - Qt::Vertical - - - - - - - - - - 16 - - - - Open - - - Qt::AlignCenter - - - - - - - - - Coefficients - - - OpenType - - - - - - - Measurement file - - - OpenType - - - - - 0 - - - - - - - - - Offset delay [ps]: - - - - - - - - - - Offset loss [GΩ/s]: - - - - - - - - - - C0 [10<sup>-15</sup>F]: - - - - - - - - - - C1 [10<sup>-27</sup>F/Hz]: - - - - - - - - - - C2 [10<sup>-36</sup>F/Hz<sup>2</sup>]: - - - - - - - - - - <html><head/><body><p>C3 [10<span style=" vertical-align:super;">-45</span>F/Hz<span style=" vertical-align:super;">3</span>]:</p></body></html> - - - - - - - - - - true - - - - - - - Z0: - - - - - - - - - - - - - - - - - - - - - - Qt::Vertical - - - - - - - - - - 16 - - - - Load - - - Qt::AlignCenter + + + Qt::Vertical - + - - - Coefficients + + + + 16 + + + + Through + + + Qt::AlignCenter - - LoadType - - - - Measurement file + + + + + Coefficients + + + ThroughType + + + + + + + Measurement file + + + ThroughType + + + + + + + + + 0 - - LoadType - + + + + + + + + Z0: + + + + + + + false + + + + + + + Delay [ps]: + + + + + + + + + + Loss [GΩ/s]: + + + + + + + + + + + + + + + + + - - - - 0 - - - - - - - - - Z0: - - - - - - - true - - - - - - - Offset delay [ps]: - - - - - - - - - - Parallel C: - - - - - - - Series L: - - - - - - - - - - - - - - - - - - - - - - - - - - - - Qt::Vertical - - - - - - - - - - 16 - - - - Through - - - Qt::AlignCenter - - - - - - - - - Coefficients - - - ThroughType - - - - - - - Measurement file - - - ThroughType - - - - - - - - - 0 - - - - - - - - - Z0: - - - - - - - false - - - - - - - Delay [ps]: - - - - - - - - - - Loss [GΩ/s]: - - - - - - - - - - - - - - - - - - - @@ -607,7 +1040,7 @@ TRL - + @@ -867,19 +1300,6 @@ - - - - Qt::Horizontal - - - - 40 - 20 - - - - @@ -986,12 +1406,63 @@ + + ShortType_f + buttonClicked(int) + short_stack_f + setCurrentIndex(int) + + + -1 + -1 + + + 165 + 442 + + + + + OpenType_f + buttonClicked(int) + open_stack_f + setCurrentIndex(int) + + + -1 + -1 + + + 466 + 442 + + + + + LoadType_f + buttonClicked(int) + load_stack_f + setCurrentIndex(int) + + + -1 + -1 + + + 767 + 442 + + + - - - + + + + + + From dced1732d6811cb81134089c9ba8ec046e31e8c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Wed, 1 Dec 2021 22:11:50 +0100 Subject: [PATCH 14/24] median option for averaging --- .../SpectrumAnalyzer/spectrumanalyzer.cpp | 12 ++ .../SpectrumAnalyzer/spectrumanalyzer.h | 1 + Software/PC_Application/VNA/vna.cpp | 11 ++ Software/PC_Application/VNA/vna.h | 1 + Software/PC_Application/appwindow.cpp | 8 ++ Software/PC_Application/averaging.cpp | 114 +++++++++++++++--- Software/PC_Application/averaging.h | 9 ++ Software/PC_Application/preferences.cpp | 2 + Software/PC_Application/preferences.h | 4 +- Software/PC_Application/preferencesdialog.ui | 32 ++++- 10 files changed, 172 insertions(+), 22 deletions(-) diff --git a/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp b/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp index 1f497d4..14c21ed 100644 --- a/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp +++ b/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.cpp @@ -265,6 +265,13 @@ SpectrumAnalyzer::SpectrumAnalyzer(AppWindow *window) // Set initial sweep settings auto pref = Preferences::getInstance(); + + if(pref.Acquisition.useMedianAveraging) { + average.setMode(Averaging::Mode::Median); + } else { + average.setMode(Averaging::Mode::Mean); + } + if(pref.Startup.RememberSweepSettings) { LoadSweepSettings(); } else { @@ -1075,6 +1082,11 @@ void SpectrumAnalyzer::updateGraphColors() emit graphColorsChanged(); } +void SpectrumAnalyzer::setAveragingMode(Averaging::Mode mode) +{ + average.setMode(mode); +} + QString SpectrumAnalyzer::WindowToString(SpectrumAnalyzer::Window w) { switch(w) { diff --git a/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.h b/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.h index 8d2c7f9..a4eacca 100644 --- a/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.h +++ b/Software/PC_Application/SpectrumAnalyzer/spectrumanalyzer.h @@ -26,6 +26,7 @@ public: virtual void fromJSON(nlohmann::json j) override; void updateGraphColors(); + void setAveragingMode(Averaging::Mode mode); private: diff --git a/Software/PC_Application/VNA/vna.cpp b/Software/PC_Application/VNA/vna.cpp index 40073fa..dd5c5b8 100644 --- a/Software/PC_Application/VNA/vna.cpp +++ b/Software/PC_Application/VNA/vna.cpp @@ -530,6 +530,12 @@ VNA::VNA(AppWindow *window) // Set initial sweep settings auto pref = Preferences::getInstance(); + if(pref.Acquisition.useMedianAveraging) { + average.setMode(Averaging::Mode::Median); + } else { + average.setMode(Averaging::Mode::Mean); + } + if(pref.Startup.RememberSweepSettings) { LoadSweepSettings(); } else { @@ -1466,6 +1472,11 @@ void VNA::updateGraphColors() emit graphColorsChanged(); } +void VNA::setAveragingMode(Averaging::Mode mode) +{ + average.setMode(mode); +} + QString VNA::SweepTypeToString(VNA::SweepType sw) { switch(sw) { diff --git a/Software/PC_Application/VNA/vna.h b/Software/PC_Application/VNA/vna.h index c78f30a..dcb03d4 100644 --- a/Software/PC_Application/VNA/vna.h +++ b/Software/PC_Application/VNA/vna.h @@ -29,6 +29,7 @@ public: virtual void fromJSON(nlohmann::json j) override; void updateGraphColors(); + void setAveragingMode(Averaging::Mode mode); enum class SweepType { Frequency = 0, diff --git a/Software/PC_Application/appwindow.cpp b/Software/PC_Application/appwindow.cpp index ede5e71..dc01824 100644 --- a/Software/PC_Application/appwindow.cpp +++ b/Software/PC_Application/appwindow.cpp @@ -204,6 +204,14 @@ AppWindow::AppWindow(QWidget *parent) vna->updateGraphColors(); } + // averaging mode may have changed, update for all relevant modes + if(p.Acquisition.useMedianAveraging) { + spectrumAnalyzer->setAveragingMode(Averaging::Mode::Median); + vna->setAveragingMode(Averaging::Mode::Median); + } else { + spectrumAnalyzer->setAveragingMode(Averaging::Mode::Mean); + vna->setAveragingMode(Averaging::Mode::Mean); + } }); connect(ui->actionAbout, &QAction::triggered, [=](){ diff --git a/Software/PC_Application/averaging.cpp b/Software/PC_Application/averaging.cpp index 8a5cffa..1eeb004 100644 --- a/Software/PC_Application/averaging.cpp +++ b/Software/PC_Application/averaging.cpp @@ -5,6 +5,7 @@ using namespace std; Averaging::Averaging() { averages = 1; + mode = Mode::Mean; } void Averaging::reset(unsigned int points) @@ -45,18 +46,57 @@ Protocol::Datapoint Averaging::process(Protocol::Datapoint d) deque->pop_front(); } - // calculate average - complex sum[4]; - for(auto s : *deque) { - sum[0] += s[0]; - sum[1] += s[1]; - sum[2] += s[2]; - sum[3] += s[3]; + switch(mode) { + case Mode::Mean: { + // calculate average + complex sum[4]; + for(auto s : *deque) { + sum[0] += s[0]; + sum[1] += s[1]; + sum[2] += s[2]; + sum[3] += s[3]; + } + S11 = sum[0] / (double) (deque->size()); + S12 = sum[1] / (double) (deque->size()); + S21 = sum[2] / (double) (deque->size()); + S22 = sum[3] / (double) (deque->size()); + } + break; + case Mode::Median: { + auto size = deque->size(); + // create sorted arrays + std::vector> S11sorted, S12sorted, S21sorted, S22sorted; + S11sorted.reserve(size); + S12sorted.reserve(size); + S21sorted.reserve(size); + S22sorted.reserve(size); + + auto comp = [=](const complex&a, const complex&b){ + return abs(a) < abs(b); + }; + + for(auto d : *deque) { + S11sorted.insert(upper_bound(S11sorted.begin(), S11sorted.end(), d[0], comp), d[0]); + S12sorted.insert(upper_bound(S12sorted.begin(), S12sorted.end(), d[1], comp), d[1]); + S21sorted.insert(upper_bound(S21sorted.begin(), S21sorted.end(), d[2], comp), d[2]); + S22sorted.insert(upper_bound(S22sorted.begin(), S22sorted.end(), d[3], comp), d[3]); + } + if(size & 0x01) { + // odd number of samples + S11 = S11sorted[size / 2]; + S12 = S12sorted[size / 2]; + S21 = S21sorted[size / 2]; + S22 = S22sorted[size / 2]; + } else { + // even number, use average of middle samples + S11 = (S11sorted[size / 2 - 1] + S11sorted[size / 2]) / 2.0; + S12 = (S12sorted[size / 2 - 1] + S12sorted[size / 2]) / 2.0; + S21 = (S21sorted[size / 2 - 1] + S21sorted[size / 2]) / 2.0; + S22 = (S22sorted[size / 2 - 1] + S22sorted[size / 2]) / 2.0; + } + } + break; } - S11 = sum[0] / (double) (deque->size()); - S12 = sum[1] / (double) (deque->size()); - S21 = sum[2] / (double) (deque->size()); - S22 = sum[3] / (double) (deque->size()); } d.real_S11 = S11.real(); @@ -90,16 +130,40 @@ Protocol::SpectrumAnalyzerResult Averaging::process(Protocol::SpectrumAnalyzerRe deque->pop_front(); } - // calculate average - complex sum[4]; - for(auto s : *deque) { - sum[0] += s[0]; - sum[1] += s[1]; - sum[2] += s[2]; - sum[3] += s[3]; + switch(mode) { + case Mode::Mean: { + // calculate average + complex sum[2]; + for(auto s : *deque) { + sum[0] += s[0]; + sum[1] += s[1]; + } + d.port1 = abs(sum[0] / (double) (deque->size())); + d.port2 = abs(sum[1] / (double) (deque->size())); + } + break; + case Mode::Median: { + auto size = deque->size(); + // create sorted arrays + std::vector port1, port2; + port1.reserve(size); + port2.reserve(size); + for(auto d : *deque) { + port1.insert(upper_bound(port1.begin(), port1.end(), abs(d[0])), abs(d[0])); + port2.insert(upper_bound(port2.begin(), port2.end(), abs(d[0])), abs(d[0])); + } + if(size & 0x01) { + // odd number of samples + d.port1 = port1[size / 2]; + d.port2 = port1[size / 2]; + } else { + // even number, use average of middle samples + d.port1 = (port1[size / 2 - 1] + port1[size / 2]) / 2; + d.port2 = (port2[size / 2 - 1] + port2[size / 2]) / 2; + } + } + break; } - d.port1 = abs(sum[0] / (double) (deque->size())); - d.port2 = abs(sum[1] / (double) (deque->size())); } return d; @@ -122,3 +186,13 @@ unsigned int Averaging::currentSweep() return 0; } } + +Averaging::Mode Averaging::getMode() const +{ + return mode; +} + +void Averaging::setMode(const Mode &value) +{ + mode = value; +} diff --git a/Software/PC_Application/averaging.h b/Software/PC_Application/averaging.h index 7f5a768..84dc68e 100644 --- a/Software/PC_Application/averaging.h +++ b/Software/PC_Application/averaging.h @@ -10,6 +10,11 @@ class Averaging { public: + enum class Mode { + Mean, + Median + }; + Averaging(); void reset(unsigned int points); void setAverages(unsigned int a); @@ -21,10 +26,14 @@ public: // Returns the number of the currently active sweep. Value is incremented whenever the the first point of the sweep is added // Returned values are in range 0 (when no data has been added yet) to averages unsigned int currentSweep(); + Mode getMode() const; + void setMode(const Mode &value); + private: std::vector, 4>>> avg; int maxPoints; unsigned int averages; + Mode mode; }; #endif // AVERAGING_H diff --git a/Software/PC_Application/preferences.cpp b/Software/PC_Application/preferences.cpp index e7b6696..492f38c 100644 --- a/Software/PC_Application/preferences.cpp +++ b/Software/PC_Application/preferences.cpp @@ -134,6 +134,7 @@ PreferencesDialog::PreferencesDialog(Preferences *pref, QWidget *parent) : p->Acquisition.harmonicMixing = ui->AcquisitionUseHarmonic->isChecked(); p->Acquisition.useDFTinSAmode = ui->AcquisitionUseDFT->isChecked(); p->Acquisition.RBWLimitForDFT = ui->AcquisitionDFTlimitRBW->value(); + p->Acquisition.useMedianAveraging = ui->AcquisitionAveragingMode->currentIndex() == 1; p->Graphs.Color.background = ui->GraphsColorBackground->getColor(); p->Graphs.Color.axis = ui->GraphsColorAxis->getColor(); p->Graphs.Color.Ticks.Background.enabled = ui->GraphsColorTicksBackgroundEnabled->isChecked(); @@ -199,6 +200,7 @@ void PreferencesDialog::setInitialGUIState() ui->AcquisitionUseHarmonic->setChecked(p->Acquisition.harmonicMixing); ui->AcquisitionUseDFT->setChecked(p->Acquisition.useDFTinSAmode); ui->AcquisitionDFTlimitRBW->setValue(p->Acquisition.RBWLimitForDFT); + ui->AcquisitionAveragingMode->setCurrentIndex(p->Acquisition.useMedianAveraging ? 1 : 0); ui->GraphsColorBackground->setColor(p->Graphs.Color.background); ui->GraphsColorAxis->setColor(p->Graphs.Color.axis); diff --git a/Software/PC_Application/preferences.h b/Software/PC_Application/preferences.h index 698b8f8..5ef0ede 100644 --- a/Software/PC_Application/preferences.h +++ b/Software/PC_Application/preferences.h @@ -66,6 +66,7 @@ public: bool harmonicMixing; bool useDFTinSAmode; double RBWLimitForDFT; + bool useMedianAveraging; } Acquisition; struct { struct { @@ -100,7 +101,7 @@ private: QString name; QVariant def; }; - const std::array descr = {{ + const std::array descr = {{ {&Startup.ConnectToFirstDevice, "Startup.ConnectToFirstDevice", true}, {&Startup.RememberSweepSettings, "Startup.RememberSweepSettings", false}, {&Startup.DefaultSweep.type, "Startup.DefaultSweep.type", "Frequency"}, @@ -128,6 +129,7 @@ private: {&Acquisition.harmonicMixing, "Acquisition.harmonicMixing", false}, {&Acquisition.useDFTinSAmode, "Acquisition.useDFTinSAmode", true}, {&Acquisition.RBWLimitForDFT, "Acquisition.RBWLimitForDFT", 3000.0}, + {&Acquisition.useMedianAveraging, "Acquisition.useMedianAveraging", false}, {&Graphs.Color.background, "Graphs.Color.background", QColor(Qt::black)}, {&Graphs.Color.axis, "Graphs.Color.axis", QColor(Qt::white)}, {&Graphs.Color.Ticks.Background.enabled, "Graphs.Color.Ticks.Background.enabled", true}, diff --git a/Software/PC_Application/preferencesdialog.ui b/Software/PC_Application/preferencesdialog.ui index 23b3fb5..fff4aa0 100644 --- a/Software/PC_Application/preferencesdialog.ui +++ b/Software/PC_Application/preferencesdialog.ui @@ -78,7 +78,7 @@ - 2 + 1 @@ -637,6 +637,36 @@ + + + + Common + + + + + + Averaging mode: + + + + + + + + Mean + + + + + Median + + + + + + + From 93ad894de6e88a393aeafa426f2052558a4d8e44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Wed, 1 Dec 2021 23:21:13 +0100 Subject: [PATCH 15/24] use json for calibration file format, allow male/female standard selection --- .../Calibration/calibration.cpp | 149 ++++++++++++- .../PC_Application/Calibration/calibration.h | 27 ++- .../Calibration/calibrationtracedialog.cpp | 39 ++++ .../Calibration/calibrationtracedialog.ui | 211 ++++++++++++------ .../PC_Application/Calibration/calkit.cpp | 5 + Software/PC_Application/Calibration/calkit.h | 1 + .../Calibration/calkitdialog.ui | 14 +- 7 files changed, 354 insertions(+), 92 deletions(-) diff --git a/Software/PC_Application/Calibration/calibration.cpp b/Software/PC_Application/Calibration/calibration.cpp index 3fce2b9..6774260 100644 --- a/Software/PC_Application/Calibration/calibration.cpp +++ b/Software/PC_Application/Calibration/calibration.cpp @@ -26,6 +26,7 @@ Calibration::Calibration() measurements[Measurement::Line].datapoints = vector(); type = Type::None; + port1Standard = port2Standard = PortStandard::Male; } Calibration::Standard Calibration::getPort1Standard(Calibration::Measurement m) @@ -124,7 +125,7 @@ bool Calibration::constructErrorTerms(Calibration::Type type) bool isTRL = type == Type::TRL; bool uses_male = true; bool uses_female = true; - if(kit.checkIfValid(minFreq, maxFreq, isTRL, uses_male, uses_female)) { + if(!kit.checkIfValid(minFreq, maxFreq, isTRL, uses_male, uses_female)) { // TODO adjust for male/female standards // Calkit does not support complete calibration range QString msg = QString("The calibration kit does not support the complete span.\n\n") @@ -135,6 +136,11 @@ bool Calibration::constructErrorTerms(Calibration::Type type) qWarning() << msg; return false; } + // check calkit standards and adjust if necessary + if(!kit.hasSeparateMaleFemaleStandards()) { + port1Standard = PortStandard::Male; + port2Standard = PortStandard::Male; + } switch(type) { case Type::Port1SOL: constructPort1SOL(); break; case Type::Port2SOL: constructPort2SOL(); break; @@ -182,7 +188,7 @@ void Calibration::construct12TermPoints() auto S22_through = complex(measurements[Measurement::Through].datapoints[i].real_S22, measurements[Measurement::Through].datapoints[i].imag_S22); auto S12_through = complex(measurements[Measurement::Through].datapoints[i].real_S12, measurements[Measurement::Through].datapoints[i].imag_S12); - auto actual = kit.toSOLT(p.frequency); + auto actual = kit.toSOLT(p.frequency, port1Standard == PortStandard::Male); // Forward calibration computeSOL(S11_short, S11_open, S11_load, p.fe00, p.fe11, p.fe10e01, actual.Open, actual.Short, actual.Load); p.fe30 = S21_isolation; @@ -193,6 +199,7 @@ void Calibration::construct12TermPoints() / ((S11_through - p.fe00)*(actual.ThroughS22-p.fe11*deltaS)-deltaS*p.fe10e01); p.fe10e32 = (S21_through - p.fe30)*(1.0 - p.fe11*actual.ThroughS11 - p.fe22*actual.ThroughS22 + p.fe11*p.fe22*deltaS) / actual.ThroughS21; // Reverse calibration + actual = kit.toSOLT(p.frequency, port2Standard == PortStandard::Male); computeSOL(S22_short, S22_open, S22_load, p.re33, p.re22, p.re23e32, actual.Open, actual.Short, actual.Load); p.re03 = S12_isolation; p.re11 = ((S22_through - p.re33)*(1.0 - p.re22 * actual.ThroughS22)-actual.ThroughS22*p.re23e32) @@ -213,7 +220,7 @@ void Calibration::constructPort1SOL() auto S11_short = complex(measurements[Measurement::Port1Short].datapoints[i].real_S11, measurements[Measurement::Port1Short].datapoints[i].imag_S11); auto S11_load = complex(measurements[Measurement::Port1Load].datapoints[i].real_S11, measurements[Measurement::Port1Load].datapoints[i].imag_S11); // OSL port1 - auto actual = kit.toSOLT(p.frequency); + auto actual = kit.toSOLT(p.frequency, port1Standard == PortStandard::Male); // See page 13 of https://www.rfmentor.com/sites/default/files/NA_Error_Models_and_Cal_Methods.pdf computeSOL(S11_short, S11_open, S11_load, p.fe00, p.fe11, p.fe10e01, actual.Open, actual.Short, actual.Load); // All other calibration coefficients to ideal values @@ -241,7 +248,7 @@ void Calibration::constructPort2SOL() auto S22_short = complex(measurements[Measurement::Port2Short].datapoints[i].real_S22, measurements[Measurement::Port2Short].datapoints[i].imag_S22); auto S22_load = complex(measurements[Measurement::Port2Load].datapoints[i].real_S22, measurements[Measurement::Port2Load].datapoints[i].imag_S22); // OSL port2 - auto actual = kit.toSOLT(p.frequency); + auto actual = kit.toSOLT(p.frequency, port1Standard == PortStandard::Male); // See page 19 of https://www.rfmentor.com/sites/default/files/NA_Error_Models_and_Cal_Methods.pdf computeSOL(S22_short, S22_open, S22_load, p.re33, p.re22, p.re23e32, actual.Open, actual.Short, actual.Load); // All other calibration coefficients to ideal values @@ -775,11 +782,23 @@ bool Calibration::openFromFile(QString filename) } try { - file >> *this; + nlohmann::json j; + file >> j; + fromJSON(j); } catch(exception e) { - InformationBox::ShowError("File parsing error", e.what()); - qWarning() << "Calibration file parsing failed: " << e.what(); - return false; + // json parsing failed, probably using a legacy file format + try { + file.clear(); + file.seekg(0); + file >> *this; + InformationBox::ShowMessage("Loading calibration file", "The file \"" + filename + "\" is stored in a deprecated" + " calibration format. Future versions of this application might not support" + " it anymore. Please save the calibration kit to update to the new format"); + } catch(exception e) { + InformationBox::ShowError("File parsing error", e.what()); + qWarning() << "Calibration file parsing failed: " << e.what(); + return false; + } } this->currentCalFile = filename; // if all ok, remember this @@ -803,7 +822,7 @@ bool Calibration::saveToFile(QString filename) auto calibration_file = filename + ".cal"; ofstream file; file.open(calibration_file.toStdString()); - file << *this; + file << setw(1) << toJSON(); auto calkit_file = filename + ".calkit"; qDebug() << "Saving associated calibration kit to file" << calkit_file; @@ -849,6 +868,95 @@ double Calibration::getMaxFreq(){ int Calibration::getNumPoints(){ return this->points.size(); } + +nlohmann::json Calibration::toJSON() +{ + nlohmann::json j; + nlohmann::json j_measurements; + for(auto m : measurements) { + if(m.second.datapoints.size() > 0) { + nlohmann::json j_measurement; + j_measurement["name"] = MeasurementToString(m.first).toStdString(); + j_measurement["timestamp"] = m.second.timestamp.toSecsSinceEpoch(); + nlohmann::json j_points; + for(auto p : m.second.datapoints) { + nlohmann::json j_point; + j_point["frequency"] = p.frequency; + j_point["S11_real"] = p.real_S11; + j_point["S11_imag"] = p.imag_S11; + j_point["S12_real"] = p.real_S12; + j_point["S12_imag"] = p.imag_S12; + j_point["S21_real"] = p.real_S21; + j_point["S21_imag"] = p.imag_S21; + j_point["S22_real"] = p.real_S22; + j_point["S22_imag"] = p.imag_S22; + j_points.push_back(j_point); + } + j_measurement["points"] = j_points; + j_measurements.push_back(j_measurement); + } + } + j["measurements"] = j_measurements; + j["type"] = TypeToString(getType()).toStdString(); + j["port1StandardMale"] = port1Standard == PortStandard::Male; + j["port2StandardMale"] = port2Standard == PortStandard::Male; + + return j; +} + +void Calibration::fromJSON(nlohmann::json j) +{ + clearMeasurements(); + resetErrorTerms(); + port1Standard = j.value("port1StandardMale", true) ? PortStandard::Male : PortStandard::Female; + port2Standard = j.value("port2StandardMale", true) ? PortStandard::Male : PortStandard::Female; + if(j.contains("measurements")) { + // grab measurements + for(auto j_m : j["measurements"]) { + if(!j_m.contains("name")) { + throw runtime_error("Measurement without name given"); + } + auto m = MeasurementFromString(QString::fromStdString(j_m["name"])); + if(m == Measurement::Last) { + throw runtime_error("Measurement name unknown: "+std::string(j_m["name"])); + } + // get timestamp + measurements[m].timestamp = QDateTime::fromSecsSinceEpoch(j_m.value("timestamp", 0)); + // extract points + if(!j_m.contains("points")) { + throw runtime_error("Measurement "+MeasurementToString(m).toStdString()+" does not contain any points"); + } + int pointNum = 0; + for(auto j_p : j_m["points"]) { + Protocol::Datapoint p; + p.pointNum = pointNum++; + p.frequency = j_p.value("frequency", 0.0); + p.real_S11 = j_p.value("S11_real", 0.0); + p.imag_S11 = j_p.value("S11_imag", 0.0); + p.real_S12 = j_p.value("S12_real", 0.0); + p.imag_S12 = j_p.value("S12_imag", 0.0); + p.real_S21 = j_p.value("S21_real", 0.0); + p.imag_S21 = j_p.value("S21_imag", 0.0); + p.real_S22 = j_p.value("S22_real", 0.0); + p.imag_S22 = j_p.value("S22_imag", 0.0); + measurements[m].datapoints.push_back(p); + } + } + } + // got all measurements, construct calibration according to type + if(j.contains("type")) { + auto t = TypeFromString(QString::fromStdString(j["type"])); + if(t == Type::Last) { + throw runtime_error("Calibration type unknown: "+std::string(j["type"])); + } + if(calculationPossible(t)) { + constructErrorTerms(t); + } else { + throw runtime_error("Incomplete calibration data, the requested calibration could not be performed."); + } + } +} + QString Calibration::getCurrentCalibrationFile(){ return this->currentCalFile; } @@ -912,6 +1020,9 @@ istream& operator >>(istream &in, Calibration &c) } } } + // old file format did not contain port standard gender, set default + c.port1Standard = Calibration::PortStandard::Male; + c.port2Standard = Calibration::PortStandard::Male; qDebug() << "Calibration file parsing complete"; return in; } @@ -1018,6 +1129,26 @@ void Calibration::setCalibrationKit(const Calkit &value) kit = value; } +void Calibration::setPortStandard(int port, Calibration::PortStandard standard) +{ + if(port == 1) { + port1Standard = standard; + } else if(port == 2) { + port2Standard = standard; + } +} + +Calibration::PortStandard Calibration::getPortStandard(int port) +{ + if(port == 1) { + return port1Standard; + } else if(port == 2) { + return port2Standard; + } else { + return PortStandard::Male; + } +} + Calibration::Type Calibration::getType() const { return type; diff --git a/Software/PC_Application/Calibration/calibration.h b/Software/PC_Application/Calibration/calibration.h index 50f0234..f465790 100644 --- a/Software/PC_Application/Calibration/calibration.h +++ b/Software/PC_Application/Calibration/calibration.h @@ -11,8 +11,9 @@ #include #include #include +#include -class Calibration +class Calibration : public Savable { public: Calibration(); @@ -108,6 +109,21 @@ public: Calkit& getCalibrationKit(); void setCalibrationKit(const Calkit &value); + enum class PortStandard { + Male, + Female, + }; + void setPortStandard(int port, PortStandard standard); + PortStandard getPortStandard(int port); + + QString getCurrentCalibrationFile(); + double getMinFreq(); + double getMaxFreq(); + int getNumPoints(); + + nlohmann::json toJSON() override; + void fromJSON(nlohmann::json j) override; + private: void construct12TermPoints(); void constructPort1SOL(); @@ -157,14 +173,9 @@ private: Calkit kit; QString descriptiveCalName(); - -private: QString currentCalFile; -public: - QString getCurrentCalibrationFile(); - double getMinFreq(); - double getMaxFreq(); - int getNumPoints(); + + PortStandard port1Standard, port2Standard; }; #endif // CALIBRATION_H diff --git a/Software/PC_Application/Calibration/calibrationtracedialog.cpp b/Software/PC_Application/Calibration/calibrationtracedialog.cpp index d1d8260..74f8775 100644 --- a/Software/PC_Application/Calibration/calibrationtracedialog.cpp +++ b/Software/PC_Application/Calibration/calibrationtracedialog.cpp @@ -26,6 +26,45 @@ CalibrationTraceDialog::CalibrationTraceDialog(Calibration *cal, double f_min, d ui->tableView->setColumnWidth(3, 160); UpdateCalibrationStatus(); + connect(ui->port1Group, qOverload(&QButtonGroup::buttonClicked), [=](){ + if(ui->port1Male->isChecked()) { + cal->setPortStandard(1, Calibration::PortStandard::Male); + } else { + cal->setPortStandard(1, Calibration::PortStandard::Female); + } + UpdateCalibrationStatus(); + }); + + connect(ui->port2Group, qOverload(&QButtonGroup::buttonClicked), [=](){ + if(ui->port2Male->isChecked()) { + cal->setPortStandard(2, Calibration::PortStandard::Male); + } else { + cal->setPortStandard(2, Calibration::PortStandard::Female); + } + UpdateCalibrationStatus(); + }); + + // hide selector if calkit does not have separate male/female standards + if(!cal->getCalibrationKit().hasSeparateMaleFemaleStandards()) { + ui->port1Standards->hide(); + ui->port2Standards->hide(); + // default selection is male + ui->port1Male->click(); + ui->port2Male->click(); + } else { + // separate standards defined + if(cal->getPortStandard(1) == Calibration::PortStandard::Male) { + ui->port1Male->setChecked(true); + } else { + ui->port1Female->setChecked(true); + } + if(cal->getPortStandard(2) == Calibration::PortStandard::Male) { + ui->port2Male->setChecked(true); + } else { + ui->port2Female->setChecked(true); + } + } + // Check calibration kit span if(type != Calibration::Type::None) { auto kit = cal->getCalibrationKit(); diff --git a/Software/PC_Application/Calibration/calibrationtracedialog.ui b/Software/PC_Application/Calibration/calibrationtracedialog.ui index 33c30c5..0a4dea5 100644 --- a/Software/PC_Application/Calibration/calibrationtracedialog.ui +++ b/Software/PC_Application/Calibration/calibrationtracedialog.ui @@ -16,80 +16,151 @@ true - + - + - - - QAbstractItemView::SelectRows + + + Port 1 Standards - - true - - - false - - - true - - - false - - - false - + + + + + Male + + + port1Group + + + + + + + Female + + + port1Group + + + + - - - - - Measure - - - - :/icons/play.png:/icons/play.png - - - - - - - Delete - - - - :/icons/trash.png:/icons/trash.png - - - - - - - Qt::Horizontal - - - - 40 - 20 - - - - - - - - Apply Calibration - - - - :/icons/ok.png:/icons/ok.png - - - - + + + Port 2 Standards + + + + + + Male + + + port2Group + + + + + + + Female + + + port2Group + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + QAbstractItemView::SelectRows + + + true + + + false + + + true + + + false + + + false + + + + + + + + + Measure + + + + :/icons/play.png:/icons/play.png + + + + + + + Delete + + + + :/icons/trash.png:/icons/trash.png + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Apply Calibration + + + + :/icons/ok.png:/icons/ok.png + + @@ -99,4 +170,8 @@ + + + + diff --git a/Software/PC_Application/Calibration/calkit.cpp b/Software/PC_Application/Calibration/calkit.cpp index 8a6ecbc..e039593 100644 --- a/Software/PC_Application/Calibration/calkit.cpp +++ b/Software/PC_Application/Calibration/calkit.cpp @@ -204,6 +204,11 @@ void Calkit::edit(std::function done) dialog->show(); } +bool Calkit::hasSeparateMaleFemaleStandards() +{ + return SOLT.separate_male_female; +} + class Calkit::SOLT Calkit::toSOLT(double frequency, bool male_standards) { auto addTransmissionLine = [](complex termination_reflection, double offset_impedance, double offset_delay, double offset_loss, double frequency) -> complex { diff --git a/Software/PC_Application/Calibration/calkit.h b/Software/PC_Application/Calibration/calkit.h index 21eb063..c75d253 100644 --- a/Software/PC_Application/Calibration/calkit.h +++ b/Software/PC_Application/Calibration/calkit.h @@ -41,6 +41,7 @@ public: void toFile(QString filename); static Calkit fromFile(QString filename); void edit(std::function done = nullptr); + bool hasSeparateMaleFemaleStandards(); SOLT toSOLT(double frequency, bool male_standards = true); TRL toTRL(double frequency); double minFreqTRL(); diff --git a/Software/PC_Application/Calibration/calkitdialog.ui b/Software/PC_Application/Calibration/calkitdialog.ui index a85820d..b6b29fc 100644 --- a/Software/PC_Application/Calibration/calkitdialog.ui +++ b/Software/PC_Application/Calibration/calkitdialog.ui @@ -148,7 +148,7 @@ - 1 + 0 @@ -435,7 +435,7 @@ - 1 + 0 @@ -722,7 +722,7 @@ - 1 + 0 @@ -1457,12 +1457,12 @@ - - - - + + + + From ecf994cf4a13b140e6fa72548b770ba64ddc8b22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Thu, 2 Dec 2021 22:41:51 +0100 Subject: [PATCH 16/24] option for zero-length through --- .../Calibration/calibration.cpp | 33 +++++++++++++++++-- .../PC_Application/Calibration/calibration.h | 3 ++ .../Calibration/calibrationtracedialog.cpp | 32 ++++++++++++++++++ .../Calibration/calibrationtracedialog.ui | 32 +++++++++++++++++- 4 files changed, 96 insertions(+), 4 deletions(-) diff --git a/Software/PC_Application/Calibration/calibration.cpp b/Software/PC_Application/Calibration/calibration.cpp index 8a6738e..75d460d 100644 --- a/Software/PC_Application/Calibration/calibration.cpp +++ b/Software/PC_Application/Calibration/calibration.cpp @@ -27,6 +27,7 @@ Calibration::Calibration() type = Type::None; port1Standard = port2Standard = PortStandard::Male; + throughZeroLength = false; } Calibration::Standard Calibration::getPort1Standard(Calibration::Measurement m) @@ -141,6 +142,10 @@ bool Calibration::constructErrorTerms(Calibration::Type type) port1Standard = PortStandard::Male; port2Standard = PortStandard::Male; } + if(port1Standard == port2Standard) { + // unable to use zero-length through + throughZeroLength = false; + } switch(type) { case Type::Port1SOL: constructPort1SOL(); break; case Type::Port2SOL: constructPort2SOL(); break; @@ -194,6 +199,14 @@ void Calibration::construct12TermPoints() p.fe30 = S21_isolation; // See page 18 of https://www.rfmentor.com/sites/default/files/NA_Error_Models_and_Cal_Methods.pdf // Formulas for S11M and S21M solved for e22 and e10e32 + if (throughZeroLength) { + // use ideal through + actual.ThroughS11 = 0.0; + actual.ThroughS12 = 1.0; + actual.ThroughS21 = 1.0; + actual.ThroughS22 = 0.0; + } + auto deltaS = actual.ThroughS11*actual.ThroughS22 - actual.ThroughS21 * actual.ThroughS12; p.fe22 = ((S11_through - p.fe00)*(1.0 - p.fe11 * actual.ThroughS11)-actual.ThroughS11*p.fe10e01) / ((S11_through - p.fe00)*(actual.ThroughS22-p.fe11*deltaS)-deltaS*p.fe10e01); @@ -859,6 +872,16 @@ QString Calibration::descriptiveCalName(){ return tmp; } +bool Calibration::getThroughZeroLength() const +{ + return throughZeroLength; +} + +void Calibration::setThroughZeroLength(bool value) +{ + throughZeroLength = value; +} + double Calibration::getMinFreq(){ return this->minFreq; } @@ -900,6 +923,7 @@ nlohmann::json Calibration::toJSON() j["type"] = TypeToString(getType()).toStdString(); j["port1StandardMale"] = port1Standard == PortStandard::Male; j["port2StandardMale"] = port2Standard == PortStandard::Male; + j["throughZeroLength"] = throughZeroLength; return j; } @@ -910,6 +934,7 @@ void Calibration::fromJSON(nlohmann::json j) resetErrorTerms(); port1Standard = j.value("port1StandardMale", true) ? PortStandard::Male : PortStandard::Female; port2Standard = j.value("port2StandardMale", true) ? PortStandard::Male : PortStandard::Female; + throughZeroLength = j.value("throughZeroLength", false); if(j.contains("measurements")) { // grab measurements for(auto j_m : j["measurements"]) { @@ -981,6 +1006,11 @@ ostream& operator<<(ostream &os, const Calibration &c) istream& operator >>(istream &in, Calibration &c) { + // old file format did not contain port standard gender, set default + c.port1Standard = Calibration::PortStandard::Male; + c.port2Standard = Calibration::PortStandard::Male; + c.throughZeroLength = false; + std::string line; while(getline(in, line)) { QString qLine = QString::fromStdString(line).simplified(); @@ -1020,9 +1050,6 @@ istream& operator >>(istream &in, Calibration &c) } } } - // old file format did not contain port standard gender, set default - c.port1Standard = Calibration::PortStandard::Male; - c.port2Standard = Calibration::PortStandard::Male; qDebug() << "Calibration file parsing complete"; return in; } diff --git a/Software/PC_Application/Calibration/calibration.h b/Software/PC_Application/Calibration/calibration.h index f465790..f3f2717 100644 --- a/Software/PC_Application/Calibration/calibration.h +++ b/Software/PC_Application/Calibration/calibration.h @@ -115,6 +115,8 @@ public: }; void setPortStandard(int port, PortStandard standard); PortStandard getPortStandard(int port); + bool getThroughZeroLength() const; + void setThroughZeroLength(bool value); QString getCurrentCalibrationFile(); double getMinFreq(); @@ -176,6 +178,7 @@ private: QString currentCalFile; PortStandard port1Standard, port2Standard; + bool throughZeroLength; }; #endif // CALIBRATION_H diff --git a/Software/PC_Application/Calibration/calibrationtracedialog.cpp b/Software/PC_Application/Calibration/calibrationtracedialog.cpp index 74f8775..55c2f08 100644 --- a/Software/PC_Application/Calibration/calibrationtracedialog.cpp +++ b/Software/PC_Application/Calibration/calibrationtracedialog.cpp @@ -26,12 +26,26 @@ CalibrationTraceDialog::CalibrationTraceDialog(Calibration *cal, double f_min, d ui->tableView->setColumnWidth(3, 160); UpdateCalibrationStatus(); + auto updateThroughStandardUI = [=](){ + if(cal->getPortStandard(1) == cal->getPortStandard(2)) { + // same gender on both ports, can't use zero length through + ui->throughCalkit->click(); + ui->throughZero->setEnabled(false); + ui->throughCalkit->setEnabled(false); + } else { + // user may select option for through + ui->throughZero->setEnabled(true); + ui->throughCalkit->setEnabled(true); + } + }; + connect(ui->port1Group, qOverload(&QButtonGroup::buttonClicked), [=](){ if(ui->port1Male->isChecked()) { cal->setPortStandard(1, Calibration::PortStandard::Male); } else { cal->setPortStandard(1, Calibration::PortStandard::Female); } + updateThroughStandardUI(); UpdateCalibrationStatus(); }); @@ -41,6 +55,16 @@ CalibrationTraceDialog::CalibrationTraceDialog(Calibration *cal, double f_min, d } else { cal->setPortStandard(2, Calibration::PortStandard::Female); } + updateThroughStandardUI(); + UpdateCalibrationStatus(); + }); + + connect(ui->throughGroup, qOverload(&QButtonGroup::buttonClicked), [=](){ + if(ui->throughZero->isChecked()) { + cal->setThroughZeroLength(true); + } else { + cal->setThroughZeroLength(false); + } UpdateCalibrationStatus(); }); @@ -48,9 +72,11 @@ CalibrationTraceDialog::CalibrationTraceDialog(Calibration *cal, double f_min, d if(!cal->getCalibrationKit().hasSeparateMaleFemaleStandards()) { ui->port1Standards->hide(); ui->port2Standards->hide(); + ui->throughStandard->hide(); // default selection is male ui->port1Male->click(); ui->port2Male->click(); + ui->throughCalkit->click(); } else { // separate standards defined if(cal->getPortStandard(1) == Calibration::PortStandard::Male) { @@ -63,6 +89,12 @@ CalibrationTraceDialog::CalibrationTraceDialog(Calibration *cal, double f_min, d } else { ui->port2Female->setChecked(true); } + if(cal->getThroughZeroLength()) { + ui->throughZero->setChecked(true); + } else { + ui->throughCalkit->setChecked(true); + } + updateThroughStandardUI(); } // Check calibration kit span diff --git a/Software/PC_Application/Calibration/calibrationtracedialog.ui b/Software/PC_Application/Calibration/calibrationtracedialog.ui index 0a4dea5..58dd531 100644 --- a/Software/PC_Application/Calibration/calibrationtracedialog.ui +++ b/Software/PC_Application/Calibration/calibrationtracedialog.ui @@ -18,7 +18,7 @@ - + @@ -77,6 +77,35 @@ + + + + Through Standard + + + + + + From calibration kit + + + throughGroup + + + + + + + Zero-length through + + + throughGroup + + + + + + @@ -173,5 +202,6 @@ + From 45cf2200b8c6d463adc31dff015c2f59e44b5521 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sat, 4 Dec 2021 21:16:45 +0100 Subject: [PATCH 17/24] Show gender of required standard in additional column --- .../Calibration/calibration.cpp | 19 ++++++ .../PC_Application/Calibration/calibration.h | 13 +++- .../Calibration/calibrationtracedialog.cpp | 9 ++- .../Calibration/measurementmodel.cpp | 66 ++++++++++++++----- .../Calibration/measurementmodel.h | 17 +++-- 5 files changed, 95 insertions(+), 29 deletions(-) diff --git a/Software/PC_Application/Calibration/calibration.cpp b/Software/PC_Application/Calibration/calibration.cpp index 75d460d..bf0710e 100644 --- a/Software/PC_Application/Calibration/calibration.cpp +++ b/Software/PC_Application/Calibration/calibration.cpp @@ -218,6 +218,9 @@ void Calibration::construct12TermPoints() p.re11 = ((S22_through - p.re33)*(1.0 - p.re22 * actual.ThroughS22)-actual.ThroughS22*p.re23e32) / ((S22_through - p.re33)*(actual.ThroughS11-p.re22*deltaS)-deltaS*p.re23e32); p.re23e01 = (S12_through - p.re03)*(1.0 - p.re11*actual.ThroughS11 - p.re22*actual.ThroughS22 + p.re11*p.re22*deltaS) / actual.ThroughS12; + + + points.push_back(p); } } @@ -237,6 +240,7 @@ void Calibration::constructPort1SOL() // See page 13 of https://www.rfmentor.com/sites/default/files/NA_Error_Models_and_Cal_Methods.pdf computeSOL(S11_short, S11_open, S11_load, p.fe00, p.fe11, p.fe10e01, actual.Open, actual.Short, actual.Load); // All other calibration coefficients to ideal values + p.fex = 0.0; p.fe30 = 0.0; p.fe22 = 0.0; p.fe10e32 = 1.0; @@ -244,6 +248,7 @@ void Calibration::constructPort1SOL() p.re22 = 0.0; p.re23e32 = 1.0; p.re03 = 0.0; + p.rex = 0.0; p.re11 = 0.0; p.re23e01 = 1.0; points.push_back(p); @@ -265,6 +270,7 @@ void Calibration::constructPort2SOL() // See page 19 of https://www.rfmentor.com/sites/default/files/NA_Error_Models_and_Cal_Methods.pdf computeSOL(S22_short, S22_open, S22_load, p.re33, p.re22, p.re23e32, actual.Open, actual.Short, actual.Load); // All other calibration coefficients to ideal values + p.fex = 0.0; p.fe30 = 0.0; p.fe22 = 0.0; p.fe10e32 = 1.0; @@ -272,6 +278,7 @@ void Calibration::constructPort2SOL() p.fe11 = 0.0; p.fe10e01 = 1.0; p.re03 = 0.0; + p.rex = 0.0; p.re11 = 0.0; p.re23e01 = 1.0; points.push_back(p); @@ -292,11 +299,13 @@ void Calibration::constructTransmissionNormalization() p.re23e01 = S12_through / actual.ThroughS12; // All other calibration coefficients to ideal values p.fe30 = 0.0; + p.fex = 0.0; p.fe22 = 0.0; p.fe00 = 0.0; p.fe11 = 0.0; p.fe10e01 = 1.0; p.re03 = 0.0; + p.rex = 0.0; p.re11 = 0.0; p.re33 = 0.0; p.re22 = 0.0; @@ -396,6 +405,7 @@ void Calibration::constructTRL() p.fe10e32 = S_B.m21; // no isolation measurement available p.fe30 = 0.0; + p.fex = 0.0; // Reverse coefficients, normalize for S12 = 1.0 // => det(T)/T22 = 1.0 @@ -416,6 +426,7 @@ void Calibration::constructTRL() p.re33 = S_B.m22; // no isolation measurement available p.re03 = 0.0; + p.rex = 0.0; points.push_back(p); } @@ -1117,7 +1128,9 @@ Calibration::Point Calibration::getCalibrationPoint(Protocol::Datapoint &d) ret.fe11 = low->fe11 * (1 - alpha) + high->fe11 * alpha; ret.fe22 = low->fe22 * (1 - alpha) + high->fe22 * alpha; ret.fe30 = low->fe30 * (1 - alpha) + high->fe30 * alpha; + ret.fex = low->fex * (1 - alpha) + high->fex * alpha; ret.re03 = low->re03 * (1 - alpha) + high->re03 * alpha; + ret.rex = low->rex * (1 - alpha) + high->rex * alpha; ret.re11 = low->re11 * (1 - alpha) + high->re11 * alpha; ret.re22 = low->re22 * (1 - alpha) + high->re22 * alpha; ret.re33 = low->re33 * (1 - alpha) + high->re33 * alpha; @@ -1141,6 +1154,12 @@ void Calibration::computeSOL(std::complex s_m, std::complex o_m, tracking = directivity * match - delta; } +void Calibration::computeIsolation(std::complex x0_m, std::complex x1_m, std::complex reverse_match, std::complex reverse_tracking, std::complex reverse_directivity, std::complex x0, std::complex x1, std::complex &internal_isolation, std::complex &external_isolation) +{ + external_isolation = (x1_m - x0_m)*(1.0 - reverse_match * (x1 - x0) + x1*x0*reverse_match*reverse_match) / (reverse_tracking * (x1 - x0)); + internal_isolation = x0_m - external_isolation*(reverse_directivity + reverse_tracking*x0 / (1.0 - x0*reverse_match)); +} + std::complex Calibration::correctSOL(std::complex measured, std::complex directivity, std::complex match, std::complex tracking) { return (measured - directivity) / (measured * match - directivity * match + tracking); diff --git a/Software/PC_Application/Calibration/calibration.h b/Software/PC_Application/Calibration/calibration.h index f3f2717..fba9643 100644 --- a/Software/PC_Application/Calibration/calibration.h +++ b/Software/PC_Application/Calibration/calibration.h @@ -138,9 +138,9 @@ private: public: double frequency; // Forward error terms - std::complex fe00, fe11, fe10e01, fe10e32, fe22, fe30; + std::complex fe00, fe11, fe10e01, fe10e32, fe22, fe30, fex; // Reverse error terms - std::complex re33, re11, re23e32, re23e01, re22, re03; + std::complex re33, re11, re23e32, re23e01, re22, re03, rex; }; Point getCalibrationPoint(Protocol::Datapoint &d); /* @@ -158,6 +158,15 @@ private: std::complex o_c = std::complex(1.0, 0), std::complex s_c = std::complex(-1.0, 0), std::complex l_c = std::complex(0, 0)); + void computeIsolation(std::complex x0_m, + std::complex x1_m, + std::complex reverse_match, + std::complex reverse_tracking, + std::complex reverse_directivity, + std::complex x0, + std::complex x1, + std::complex &internal_isolation, + std::complex &external_isolation); std::complex correctSOL(std::complex measured, std::complex directivity, std::complex match, diff --git a/Software/PC_Application/Calibration/calibrationtracedialog.cpp b/Software/PC_Application/Calibration/calibrationtracedialog.cpp index 55c2f08..db639ee 100644 --- a/Software/PC_Application/Calibration/calibrationtracedialog.cpp +++ b/Software/PC_Application/Calibration/calibrationtracedialog.cpp @@ -21,9 +21,10 @@ CalibrationTraceDialog::CalibrationTraceDialog(Calibration *cal, double f_min, d model = new MeasurementModel(cal, measurements); ui->tableView->setModel(model); ui->tableView->setColumnWidth(0, 100); - ui->tableView->setColumnWidth(1, 350); - ui->tableView->setColumnWidth(2, 320); - ui->tableView->setColumnWidth(3, 160); + ui->tableView->setColumnWidth(1, 80); + ui->tableView->setColumnWidth(2, 350); + ui->tableView->setColumnWidth(3, 320); + ui->tableView->setColumnWidth(4, 160); UpdateCalibrationStatus(); auto updateThroughStandardUI = [=](){ @@ -37,6 +38,7 @@ CalibrationTraceDialog::CalibrationTraceDialog(Calibration *cal, double f_min, d ui->throughZero->setEnabled(true); ui->throughCalkit->setEnabled(true); } + model->genderUpdated(); }; connect(ui->port1Group, qOverload(&QButtonGroup::buttonClicked), [=](){ @@ -73,6 +75,7 @@ CalibrationTraceDialog::CalibrationTraceDialog(Calibration *cal, double f_min, d ui->port1Standards->hide(); ui->port2Standards->hide(); ui->throughStandard->hide(); + ui->tableView->hideColumn((int) MeasurementModel::ColIndex::Gender); // default selection is male ui->port1Male->click(); ui->port2Male->click(); diff --git a/Software/PC_Application/Calibration/measurementmodel.cpp b/Software/PC_Application/Calibration/measurementmodel.cpp index 4b17b4c..0f382b2 100644 --- a/Software/PC_Application/Calibration/measurementmodel.cpp +++ b/Software/PC_Application/Calibration/measurementmodel.cpp @@ -19,21 +19,46 @@ int MeasurementModel::rowCount(const QModelIndex &) const int MeasurementModel::columnCount(const QModelIndex &) const { - return ColIndexLast; + return (int) ColIndex::Last; } QVariant MeasurementModel::data(const QModelIndex &index, int role) const { auto info = cal->getMeasurementInfo(measurements[index.row()]); if(role == Qt::DisplayRole) { - switch(index.column()) { - case ColIndexName: + switch((ColIndex) index.column()) { + case ColIndex::Name: return info.name; break; - case ColIndexDescription: + case ColIndex::Gender: + switch(measurements[index.row()]) { + case Calibration::Measurement::Port1Load: + case Calibration::Measurement::Port1Open: + case Calibration::Measurement::Port1Short: + if(cal->getPortStandard(1) == Calibration::PortStandard::Male) { + return "Male"; + } else { + return "Female"; + } + break; + case Calibration::Measurement::Port2Load: + case Calibration::Measurement::Port2Open: + case Calibration::Measurement::Port2Short: + if(cal->getPortStandard(2) == Calibration::PortStandard::Male) { + return "Male"; + } else { + return "Female"; + } + break; + default: + return ""; + } + + break; + case ColIndex::Description: return info.prerequisites; break; - case ColIndexData: + case ColIndex::Data: if(info.points > 0) { QString data = QString::number(info.points); data.append(" points from "); @@ -45,16 +70,17 @@ QVariant MeasurementModel::data(const QModelIndex &index, int role) const return "Not available"; } break; - case ColIndexDate: + case ColIndex::Date: return info.timestamp.toString("dd.MM.yyyy hh:mm:ss"); break; } } else if(role == Qt::SizeHintRole) { - switch(index.column()) { - case ColIndexName: return 200; break; - case ColIndexDescription: return 500; break; - case ColIndexData: return 300; break; - case ColIndexDate: return 300; break; + switch((ColIndex) index.column()) { + case ColIndex::Name: return 200; break; + case ColIndex::Gender: return 150; break; + case ColIndex::Description: return 500; break; + case ColIndex::Data: return 300; break; + case ColIndex::Date: return 300; break; default: return QVariant(); break; } } @@ -65,11 +91,12 @@ QVariant MeasurementModel::data(const QModelIndex &index, int role) const QVariant MeasurementModel::headerData(int section, Qt::Orientation orientation, int role) const { if(orientation == Qt::Horizontal && role == Qt::DisplayRole) { - switch(section) { - case ColIndexName: return "Type"; break; - case ColIndexDescription: return "Prerequisites"; break; - case ColIndexData: return "Statistics"; break; - case ColIndexDate: return "Timestamp"; break; + switch((ColIndex) section) { + case ColIndex::Name: return "Type"; break; + case ColIndex::Gender: return "Gender"; break; + case ColIndex::Description: return "Prerequisites"; break; + case ColIndex::Data: return "Statistics"; break; + case ColIndex::Date: return "Timestamp"; break; default: return QVariant(); break; } } else { @@ -83,6 +110,11 @@ void MeasurementModel::measurementUpdated(Calibration::Measurement m) auto it = std::find(measurements.begin(), measurements.end(), m); if(it != measurements.end()) { int row = it - measurements.begin(); - emit dataChanged(index(row, 0), index(row, ColIndexLast - 1)); + emit dataChanged(index(row, 0), index(row, (int) ColIndex::Last - 1)); } } + +void MeasurementModel::genderUpdated() +{ + emit dataChanged(index(0, (int) ColIndex::Gender), index(rowCount() - 1, (int) ColIndex::Gender)); +} diff --git a/Software/PC_Application/Calibration/measurementmodel.h b/Software/PC_Application/Calibration/measurementmodel.h index 6674c27..edbba05 100644 --- a/Software/PC_Application/Calibration/measurementmodel.h +++ b/Software/PC_Application/Calibration/measurementmodel.h @@ -11,6 +11,15 @@ class MeasurementModel : public QAbstractTableModel { Q_OBJECT public: + enum class ColIndex { + Name, + Gender, + Description, + Data, + Date, + Last + }; + MeasurementModel(Calibration *cal, std::vector measurements); int rowCount(const QModelIndex &parent = QModelIndex()) const override; @@ -20,15 +29,9 @@ public: public slots: void measurementUpdated(Calibration::Measurement m); + void genderUpdated(); private: - enum { - ColIndexName, - ColIndexDescription, - ColIndexData, - ColIndexDate, - ColIndexLast - }; Calibration *cal; std::vector measurements; }; From aba0650d25fee70ce751fb89841a93d3cc46e31e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sun, 5 Dec 2021 18:26:32 +0100 Subject: [PATCH 18/24] Add preference option for trace line width --- .../PC_Application/Traces/tracesmithchart.cpp | 2 +- Software/PC_Application/Traces/tracexyplot.cpp | 2 +- Software/PC_Application/preferences.cpp | 3 +++ Software/PC_Application/preferences.h | 4 +++- Software/PC_Application/preferencesdialog.ui | 16 +++++++++++++++- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/Software/PC_Application/Traces/tracesmithchart.cpp b/Software/PC_Application/Traces/tracesmithchart.cpp index 949430e..70213ed 100644 --- a/Software/PC_Application/Traces/tracesmithchart.cpp +++ b/Software/PC_Application/Traces/tracesmithchart.cpp @@ -186,7 +186,7 @@ void TraceSmithChart::draw(QPainter &p) { // trace marked invisible continue; } - pen = QPen(trace->color(), 1); + pen = QPen(trace->color(), pref.Graphs.lineWidth); pen.setCosmetic(true); p.setPen(pen); int nPoints = trace->size(); diff --git a/Software/PC_Application/Traces/tracexyplot.cpp b/Software/PC_Application/Traces/tracexyplot.cpp index 4f59d7e..a6d5277 100644 --- a/Software/PC_Application/Traces/tracexyplot.cpp +++ b/Software/PC_Application/Traces/tracexyplot.cpp @@ -457,7 +457,7 @@ void TraceXYPlot::draw(QPainter &p) if(!t->isVisible()) { continue; } - pen = QPen(t->color(), 1); + pen = QPen(t->color(), pref.Graphs.lineWidth); pen.setCosmetic(true); if(i == 1) { pen.setStyle(Qt::DotLine); diff --git a/Software/PC_Application/preferences.cpp b/Software/PC_Application/preferences.cpp index 492f38c..81db186 100644 --- a/Software/PC_Application/preferences.cpp +++ b/Software/PC_Application/preferences.cpp @@ -143,6 +143,7 @@ PreferencesDialog::PreferencesDialog(Preferences *pref, QWidget *parent) : p->Graphs.domainChangeBehavior = (GraphDomainChangeBehavior) ui->GraphsDomainChangeBehavior->currentIndex(); p->Graphs.markerBehavior.showDataOnGraphs = ui->GraphsShowMarkerData->isChecked(); p->Graphs.markerBehavior.showAllData = ui->GraphsShowAllMarkerData->isChecked(); + p->Graphs.lineWidth = ui->GraphsLineWidth->value(); p->SCPIServer.enabled = ui->SCPIServerEnabled->isChecked(); p->SCPIServer.port = ui->SCPIServerPort->value(); accept(); @@ -210,6 +211,8 @@ void PreferencesDialog::setInitialGUIState() ui->GraphsDomainChangeBehavior->setCurrentIndex((int) p->Graphs.domainChangeBehavior); ui->GraphsShowMarkerData->setChecked(p->Graphs.markerBehavior.showDataOnGraphs); ui->GraphsShowAllMarkerData->setChecked(p->Graphs.markerBehavior.showAllData); + ui->GraphsLineWidth->setValue(p->Graphs.lineWidth); + ui->SCPIServerEnabled->setChecked(p->SCPIServer.enabled); ui->SCPIServerPort->setValue(p->SCPIServer.port); diff --git a/Software/PC_Application/preferences.h b/Software/PC_Application/preferences.h index 5ef0ede..35f0444 100644 --- a/Software/PC_Application/preferences.h +++ b/Software/PC_Application/preferences.h @@ -85,6 +85,7 @@ public: bool showDataOnGraphs; bool showAllData; } markerBehavior; + double lineWidth; } Graphs; struct { bool enabled; @@ -101,7 +102,7 @@ private: QString name; QVariant def; }; - const std::array descr = {{ + const std::array descr = {{ {&Startup.ConnectToFirstDevice, "Startup.ConnectToFirstDevice", true}, {&Startup.RememberSweepSettings, "Startup.RememberSweepSettings", false}, {&Startup.DefaultSweep.type, "Startup.DefaultSweep.type", "Frequency"}, @@ -138,6 +139,7 @@ private: {&Graphs.domainChangeBehavior, "Graphs.domainChangeBehavior", GraphDomainChangeBehavior::AdjustGraphs}, {&Graphs.markerBehavior.showDataOnGraphs, "Graphs.markerBehavior.ShowDataOnGraphs", true}, {&Graphs.markerBehavior.showAllData, "Graphs.markerBehavior.ShowAllData", false}, + {&Graphs.lineWidth, "Graphs.lineWidth", 1.0}, {&SCPIServer.enabled, "SCPIServer.enabled", true}, {&SCPIServer.port, "SCPIServer.port", 19542}, }}; diff --git a/Software/PC_Application/preferencesdialog.ui b/Software/PC_Application/preferencesdialog.ui index fff4aa0..1cd937a 100644 --- a/Software/PC_Application/preferencesdialog.ui +++ b/Software/PC_Application/preferencesdialog.ui @@ -78,7 +78,7 @@ - 1 + 2 @@ -769,6 +769,20 @@ + + + + Line Width: + + + + + + + 0.100000000000000 + + + From b2d3c407fcf4fa20e27bb535529f9a329ebc8984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sun, 5 Dec 2021 18:30:08 +0100 Subject: [PATCH 19/24] allow deletion of multiple measurements at once --- .../PC_Application/Calibration/calibrationtracedialog.cpp | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Software/PC_Application/Calibration/calibrationtracedialog.cpp b/Software/PC_Application/Calibration/calibrationtracedialog.cpp index db639ee..6aefba5 100644 --- a/Software/PC_Application/Calibration/calibrationtracedialog.cpp +++ b/Software/PC_Application/Calibration/calibrationtracedialog.cpp @@ -145,9 +145,11 @@ void CalibrationTraceDialog::UpdateCalibrationStatus() void CalibrationTraceDialog::on_bDelete_clicked() { - auto measurement = measurements[ui->tableView->currentIndex().row()]; - cal->clearMeasurement(measurement); - model->measurementUpdated(measurement); + auto selected = ui->tableView->selectionModel()->selectedRows(); + for(auto s : selected) { + cal->clearMeasurement(measurements[s.row()]); + model->measurementUpdated(measurements[s.row()]); + } UpdateCalibrationStatus(); } From 8246e80d6991b35092acb56d7534f65a5c654c41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sun, 5 Dec 2021 19:28:36 +0100 Subject: [PATCH 20/24] shortcut for creating linked markers for all traces --- .../Traces/Marker/markerwidget.cpp | 24 ++++++++++++++++++- .../Traces/Marker/markerwidget.h | 2 ++ .../Traces/Marker/markerwidget.ui | 24 +++++++++++++++++-- 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/Software/PC_Application/Traces/Marker/markerwidget.cpp b/Software/PC_Application/Traces/Marker/markerwidget.cpp index bc4bfde..eda6645 100644 --- a/Software/PC_Application/Traces/Marker/markerwidget.cpp +++ b/Software/PC_Application/Traces/Marker/markerwidget.cpp @@ -5,6 +5,7 @@ #include #include +#include MarkerWidget::MarkerWidget(MarkerModel &model, QWidget *parent) : QWidget(parent), @@ -12,6 +13,16 @@ MarkerWidget::MarkerWidget(MarkerModel &model, QWidget *parent) : model(model) { ui->setupUi(this); + + // some image magic to create a button with three "add" icons (not available as standard icon) + QImage image(44, 44, QImage::Format_ARGB32); + auto origImage = ui->bAddAll->icon().pixmap(22).toImage().convertToFormat(QImage::Format_ARGB32); + QPainter painter(&image); + painter.drawImage(0, 0, origImage); + painter.drawImage(origImage.width(), 0, origImage); + painter.drawImage(origImage.width()/2, origImage.height(), origImage); + ui->bAddAll->setIcon(QIcon(QPixmap::fromImage(image))); + ui->treeView->setModel(&model); ui->treeView->setItemDelegateForColumn(MarkerModel::ColIndexTrace, new MarkerTraceDelegate); ui->treeView->setItemDelegateForColumn(MarkerModel::ColIndexType, new MarkerTypeDelegate); @@ -123,6 +134,18 @@ void MarkerWidget::on_bAdd_clicked() model.addMarker(marker); } +void MarkerWidget::on_bAddAll_clicked() +{ + // add a marker for every trace and link them + auto group = model.createMarkerGroup(); + for(auto trace : model.getModel().getTraces()) { + auto m = model.createDefaultMarker(); + m->assignTrace(trace); + group->add(m); + model.addMarker(m); + } +} + bool MarkerWidget::eventFilter(QObject *, QEvent *event) { if (event->type() == QEvent::KeyPress) { @@ -152,4 +175,3 @@ void MarkerWidget::updatePersistentEditors() } } } - diff --git a/Software/PC_Application/Traces/Marker/markerwidget.h b/Software/PC_Application/Traces/Marker/markerwidget.h index 29b2aca..7e8f293 100644 --- a/Software/PC_Application/Traces/Marker/markerwidget.h +++ b/Software/PC_Application/Traces/Marker/markerwidget.h @@ -22,6 +22,8 @@ private slots: void on_bAdd_clicked(); void updatePersistentEditors(); + void on_bAddAll_clicked(); + private: bool eventFilter(QObject *obj, QEvent *event) override; Ui::MarkerWidget *ui; diff --git a/Software/PC_Application/Traces/Marker/markerwidget.ui b/Software/PC_Application/Traces/Marker/markerwidget.ui index e7265b2..7caf113 100644 --- a/Software/PC_Application/Traces/Marker/markerwidget.ui +++ b/Software/PC_Application/Traces/Marker/markerwidget.ui @@ -44,7 +44,27 @@ - Add + Add marker + + + + + + + :/icons/add.png:/icons/add.png + + + + + + + + 0 + 0 + + + + Add markers to all traces @@ -64,7 +84,7 @@ - Delete + Delete marker From 2ea668a715a05933cb086103a9b31209a04c6511 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sun, 5 Dec 2021 19:35:34 +0100 Subject: [PATCH 21/24] Fix: prevent crash when deleting marker whose group was already deleted --- Software/PC_Application/Traces/Marker/markergroup.cpp | 4 +--- Software/PC_Application/Traces/Marker/markergroup.h | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Software/PC_Application/Traces/Marker/markergroup.cpp b/Software/PC_Application/Traces/Marker/markergroup.cpp index 8be2695..7df0baa 100644 --- a/Software/PC_Application/Traces/Marker/markergroup.cpp +++ b/Software/PC_Application/Traces/Marker/markergroup.cpp @@ -22,9 +22,7 @@ bool MarkerGroup::add(Marker *m) connect(m, &Marker::positionChanged, this, &MarkerGroup::markerMoved); connect(m, &Marker::typeChanged, this, &MarkerGroup::checkMarker); connect(m, &Marker::domainChanged, this, &MarkerGroup::checkMarker); - connect(m, &Marker::deleted, [=](){ - remove(m); - }); + connect(m, &Marker::deleted, this, &MarkerGroup::remove); if(markers.size() > 0) { m->setPosition((*markers.begin())->getPosition()); diff --git a/Software/PC_Application/Traces/Marker/markergroup.h b/Software/PC_Application/Traces/Marker/markergroup.h index 4883573..5b7dc8c 100644 --- a/Software/PC_Application/Traces/Marker/markergroup.h +++ b/Software/PC_Application/Traces/Marker/markergroup.h @@ -17,7 +17,6 @@ public: ~MarkerGroup(); bool add(Marker *m); - bool remove(Marker *m); unsigned int getNumber() const; bool applicable(Marker *m); @@ -25,6 +24,9 @@ public: signals: void emptied(MarkerGroup*); +public slots: + bool remove(Marker *m); + private: void markerMoved(double newpos); From 1c6a1ab6fdc98e9d62f9e1cdf2a87c371ce5fc97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Sun, 5 Dec 2021 19:53:14 +0100 Subject: [PATCH 22/24] Version number increased, added changelog --- CHANGELOG.md | 26 +++++++++++++++++++ Software/PC_Application/LibreVNA-GUI.pro | 2 +- Software/VNA_embedded/.cproject | 2 +- .../.settings/language.settings.xml | 4 +-- Software/VNA_embedded/Makefile | 2 +- 5 files changed, 31 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e1aa3f..8b5e845 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,31 @@ # Changelog +## v1.2.1 + +Mostly bugfixes along with the occasional new feature. + +- Calibration: + - File format changed to json + - Multiple measurements can be taken/deleted at the same time + - Calibration kit allows separate male/female standards + - configurable Z0 for short/open +- SCPI commands: + - load/save calibration files + - export to touchstone file format directly + - additional command for reading trace data + - fix typo in documentation +- UI improvements: + - Additional Y-axis options: Reactance/Real/Imaginary + - Graphs look a bit nicer + - Configurable line width for graphs + - finally added an application logo +- General bugfixes, among others: + - PLL divider calculation fixed for certain frequencies + - Improved USB buffer handling + - Better error handling when opening invalid files + - Various bugs when adding/deleting markers + - graph autoscaling with invisible traces + ## v1.2.0 - Additional SCPI commands diff --git a/Software/PC_Application/LibreVNA-GUI.pro b/Software/PC_Application/LibreVNA-GUI.pro index 2f02f04..04a72c0 100644 --- a/Software/PC_Application/LibreVNA-GUI.pro +++ b/Software/PC_Application/LibreVNA-GUI.pro @@ -304,5 +304,5 @@ RESOURCES += \ CONFIG += c++17 REVISION = $$system(git rev-parse HEAD) DEFINES += GITHASH=\\"\"$$REVISION\\"\" -DEFINES += FW_MAJOR=1 FW_MINOR=2 FW_PATCH=0 FW_SUFFIX=""#\\"\"-alpha.2\\"\" +DEFINES += FW_MAJOR=1 FW_MINOR=2 FW_PATCH=1 FW_SUFFIX=""#\\"\"-alpha.2\\"\" DEFINES -= _UNICODE UNICODE diff --git a/Software/VNA_embedded/.cproject b/Software/VNA_embedded/.cproject index c9b8014..41f3bde 100644 --- a/Software/VNA_embedded/.cproject +++ b/Software/VNA_embedded/.cproject @@ -189,7 +189,7 @@ - + diff --git a/Software/VNA_embedded/.settings/language.settings.xml b/Software/VNA_embedded/.settings/language.settings.xml index aadbc97..17b53d6 100644 --- a/Software/VNA_embedded/.settings/language.settings.xml +++ b/Software/VNA_embedded/.settings/language.settings.xml @@ -11,7 +11,7 @@ - + @@ -33,7 +33,7 @@ - + diff --git a/Software/VNA_embedded/Makefile b/Software/VNA_embedded/Makefile index 8f06c5a..ec8f4f3 100644 --- a/Software/VNA_embedded/Makefile +++ b/Software/VNA_embedded/Makefile @@ -101,7 +101,7 @@ MCU = $(CPU) -mthumb $(FLOAT-ABI) $(FPU) C_DEFS = \ -DFW_MAJOR=1 \ -DFW_MINOR=2 \ --DFW_PATCH=0 \ +-DFW_PATCH=1 \ -DUSE_FULL_LL_DRIVER \ -DHW_REVISION="'B'" \ -D__weak="__attribute__((weak))" \ From 75f4ee245fe9e8120b22a711283d7391c7f87729 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Fri, 10 Dec 2021 20:46:04 +0100 Subject: [PATCH 23/24] Fix valgrind issues --- .../PC_Application/Calibration/amplitudecaldialog.cpp | 6 ++++++ Software/PC_Application/Traces/Marker/marker.cpp | 10 +++++----- Software/PC_Application/Traces/Marker/markerwidget.cpp | 4 ++-- Software/PC_Application/Traces/Math/dft.cpp | 6 ++++++ Software/PC_Application/Traces/Math/expression.cpp | 6 ++++++ Software/PC_Application/Traces/Math/medianfilter.cpp | 6 ++++++ Software/PC_Application/Traces/Math/tdr.cpp | 6 ++++++ Software/PC_Application/Traces/Math/timegate.cpp | 6 ++++++ Software/PC_Application/Traces/Math/tracemath.cpp | 3 +++ Software/PC_Application/Traces/traceeditdialog.cpp | 3 +++ .../PC_Application/VNA/Deembedding/deembedding.cpp | 6 +++++- .../PC_Application/VNA/Deembedding/matchingnetwork.cpp | 3 +++ .../PC_Application/VNA/Deembedding/portextension.cpp | 3 +++ Software/PC_Application/VNA/Deembedding/twothru.cpp | 3 +++ Software/PC_Application/VNA/tracewidgetvna.cpp | 3 +++ Software/PC_Application/VNA/vna.cpp | 4 ++++ 16 files changed, 70 insertions(+), 8 deletions(-) diff --git a/Software/PC_Application/Calibration/amplitudecaldialog.cpp b/Software/PC_Application/Calibration/amplitudecaldialog.cpp index 50f89d9..766171b 100644 --- a/Software/PC_Application/Calibration/amplitudecaldialog.cpp +++ b/Software/PC_Application/Calibration/amplitudecaldialog.cpp @@ -289,6 +289,9 @@ void AmplitudeCalDialog::AddPointDialog() auto d = new QDialog(); auto ui = new Ui::AddAmplitudePointsDialog(); ui->setupUi(d); + connect(d, &QDialog::finished, [=](){ + delete ui; + }); ui->frequency->setUnit("Hz"); ui->frequency->setPrefixes(" kMG"); ui->startFreq->setUnit("Hz"); @@ -356,6 +359,9 @@ void AmplitudeCalDialog::AutomaticMeasurementDialog() automatic.dialog = new QDialog(this); auto ui = new Ui::AutomaticAmplitudeDialog(); ui->setupUi(automatic.dialog); + connect(automatic.dialog, &QDialog::finished, [=](){ + delete ui; + }); automatic.progress = ui->progress; ui->explanation->setText(info); ui->status->setText("Gathering information about "+otherCal+" Calibration..."); diff --git a/Software/PC_Application/Traces/Marker/marker.cpp b/Software/PC_Application/Traces/Marker/marker.cpp index 1e76a93..6ca8730 100644 --- a/Software/PC_Application/Traces/Marker/marker.cpp +++ b/Software/PC_Application/Traces/Marker/marker.cpp @@ -674,7 +674,7 @@ void Marker::updateContextmenu() auto typemenu = contextmenu.addMenu("Type"); auto typegroup = new QActionGroup(&contextmenu); for(auto t : getSupportedTypes()) { - auto setTypeAction = new QAction(typeToString(t)); + auto setTypeAction = new QAction(typeToString(t), typemenu); setTypeAction->setCheckable(true); if(t == type) { setTypeAction->setChecked(true); @@ -689,7 +689,7 @@ void Marker::updateContextmenu() auto table = contextmenu.addMenu("Data Format in Table"); auto tablegroup = new QActionGroup(&contextmenu); for(auto f : applicableFormats()) { - auto setFormatAction = new QAction(formatToString(f)); + auto setFormatAction = new QAction(formatToString(f), table); setFormatAction->setCheckable(true); if(f == formatTable) { setFormatAction->setChecked(true); @@ -703,7 +703,7 @@ void Marker::updateContextmenu() auto graph = contextmenu.addMenu("Show on Graph"); for(auto f : applicableFormats()) { - auto setFormatAction = new QAction(formatToString(f)); + auto setFormatAction = new QAction(formatToString(f), graph); setFormatAction->setCheckable(true); if(formatGraph.count(f)) { setFormatAction->setChecked(true); @@ -753,7 +753,7 @@ void Marker::updateContextmenu() } if(group != nullptr) { // "remove from group" available - auto removeGroup = new QAction("Remove from linked group"); + auto removeGroup = new QAction("Remove from linked group", &contextmenu); connect(removeGroup, &QAction::triggered, [=](){ group->remove(this); }); @@ -765,7 +765,7 @@ void Marker::updateContextmenu() } - auto deleteAction = new QAction("Delete"); + auto deleteAction = new QAction("Delete", &contextmenu); connect(deleteAction, &QAction::triggered, this, &Marker::deleteLater); contextmenu.addAction(deleteAction); } diff --git a/Software/PC_Application/Traces/Marker/markerwidget.cpp b/Software/PC_Application/Traces/Marker/markerwidget.cpp index eda6645..2274d10 100644 --- a/Software/PC_Application/Traces/Marker/markerwidget.cpp +++ b/Software/PC_Application/Traces/Marker/markerwidget.cpp @@ -58,7 +58,7 @@ MarkerWidget::MarkerWidget(MarkerModel &model, QWidget *parent) : } // multiple markers selected, execute group context menu QMenu menu; - auto createGroup = new QAction("Link selected"); + auto createGroup = new QAction("Link selected", &menu); connect(createGroup, &QAction::triggered, [&](){ auto g = model.createMarkerGroup(); // assign markers to group @@ -68,7 +68,7 @@ MarkerWidget::MarkerWidget(MarkerModel &model, QWidget *parent) : }); menu.addAction(createGroup); if(anyInGroup) { - auto removeGroup = new QAction("Break Links"); + auto removeGroup = new QAction("Break Links", &menu); connect(removeGroup, &QAction::triggered, [&](){ // remove selected markers from groups if they are already assigned to one for(auto m : selected) { diff --git a/Software/PC_Application/Traces/Math/dft.cpp b/Software/PC_Application/Traces/Math/dft.cpp index b2d3990..6e4d3c6 100644 --- a/Software/PC_Application/Traces/Math/dft.cpp +++ b/Software/PC_Application/Traces/Math/dft.cpp @@ -42,6 +42,9 @@ void Math::DFT::edit() auto d = new QDialog(); auto ui = new Ui::DFTDialog; ui->setupUi(d); + connect(d, &QDialog::finished, [=](){ + delete ui; + }); ui->windowBox->setLayout(new QVBoxLayout); ui->windowBox->layout()->addWidget(window.createEditor()); @@ -76,6 +79,9 @@ QWidget *Math::DFT::createExplanationWidget() auto w = new QWidget(); auto ui = new Ui::DFTExplanationWidget; ui->setupUi(w); + connect(w, &QWidget::destroyed, [=](){ + delete ui; + }); return w; } diff --git a/Software/PC_Application/Traces/Math/expression.cpp b/Software/PC_Application/Traces/Math/expression.cpp index 8d05357..0f26417 100644 --- a/Software/PC_Application/Traces/Math/expression.cpp +++ b/Software/PC_Application/Traces/Math/expression.cpp @@ -37,6 +37,9 @@ void Math::Expression::edit() auto d = new QDialog(); auto ui = new Ui::ExpressionDialog; ui->setupUi(d); + connect(d, &QDialog::finished, [=](){ + delete ui; + }); ui->expEdit->setText(exp); connect(ui->buttonBox, &QDialogButtonBox::accepted, [=](){ exp = ui->expEdit->text(); @@ -57,6 +60,9 @@ QWidget *Math::Expression::createExplanationWidget() auto w = new QWidget(); auto ui = new Ui::ExpressionExplanationWidget; ui->setupUi(w); + connect(w, &QWidget::destroyed, [=](){ + delete ui; + }); return w; } diff --git a/Software/PC_Application/Traces/Math/medianfilter.cpp b/Software/PC_Application/Traces/Math/medianfilter.cpp index e72a3aa..6e84cbb 100644 --- a/Software/PC_Application/Traces/Math/medianfilter.cpp +++ b/Software/PC_Application/Traces/Math/medianfilter.cpp @@ -29,6 +29,9 @@ void MedianFilter::edit() auto d = new QDialog(); auto ui = new Ui::MedianFilterDialog(); ui->setupUi(d); + connect(d, &QDialog::finished, [=](){ + delete ui; + }); ui->kernelSize->setValue(kernelSize); ui->sortingMethod->setCurrentIndex((int) order); @@ -52,6 +55,9 @@ QWidget *MedianFilter::createExplanationWidget() auto w = new QWidget(); auto ui = new Ui::MedianFilterExplanationWidget; ui->setupUi(w); + connect(w, &QWidget::destroyed, [=](){ + delete ui; + }); return w; } diff --git a/Software/PC_Application/Traces/Math/tdr.cpp b/Software/PC_Application/Traces/Math/tdr.cpp index 9ce2231..48111e4 100644 --- a/Software/PC_Application/Traces/Math/tdr.cpp +++ b/Software/PC_Application/Traces/Math/tdr.cpp @@ -52,6 +52,9 @@ void TDR::edit() auto d = new QDialog(); auto ui = new Ui::TDRDialog; ui->setupUi(d); + connect(d, &QDialog::finished, [=](){ + delete ui; + }); ui->windowBox->setLayout(new QVBoxLayout); ui->windowBox->layout()->addWidget(window.createEditor()); @@ -119,6 +122,9 @@ QWidget *TDR::createExplanationWidget() auto w = new QWidget(); auto ui = new Ui::TDRExplanationWidget; ui->setupUi(w); + connect(w, &QWidget::destroyed, [=](){ + delete ui; + }); return w; } diff --git a/Software/PC_Application/Traces/Math/timegate.cpp b/Software/PC_Application/Traces/Math/timegate.cpp index ebbe22f..c2f3af8 100644 --- a/Software/PC_Application/Traces/Math/timegate.cpp +++ b/Software/PC_Application/Traces/Math/timegate.cpp @@ -58,6 +58,9 @@ void Math::TimeGate::edit() auto d = new QDialog(); auto ui = new Ui::TimeGateDialog(); ui->setupUi(d); + connect(d, &QDialog::finished, [=](){ + delete ui; + }); ui->graph->setGate(this); ui->windowBox->setLayout(new QVBoxLayout); ui->windowBox->layout()->addWidget(window.createEditor()); @@ -113,6 +116,9 @@ QWidget *Math::TimeGate::createExplanationWidget() auto w = new QWidget(); auto ui = new Ui::TimeGateExplanationWidget; ui->setupUi(w); + connect(w, &QWidget::destroyed, [=](){ + delete ui; + }); return w; } diff --git a/Software/PC_Application/Traces/Math/tracemath.cpp b/Software/PC_Application/Traces/Math/tracemath.cpp index 59e5695..b15c8ba 100644 --- a/Software/PC_Application/Traces/Math/tracemath.cpp +++ b/Software/PC_Application/Traces/Math/tracemath.cpp @@ -74,6 +74,9 @@ TraceMath::TypeInfo TraceMath::getInfo(TraceMath::Type type) ret.explanationWidget = new QWidget(); auto ui = new Ui::TimeDomainGatingExplanationWidget; ui->setupUi(ret.explanationWidget); + connect(ret.explanationWidget, &QWidget::destroyed, [=](){ + delete ui; + }); } break; default: diff --git a/Software/PC_Application/Traces/traceeditdialog.cpp b/Software/PC_Application/Traces/traceeditdialog.cpp index 0f9e21d..15f15d1 100644 --- a/Software/PC_Application/Traces/traceeditdialog.cpp +++ b/Software/PC_Application/Traces/traceeditdialog.cpp @@ -169,6 +169,9 @@ TraceEditDialog::TraceEditDialog(Trace &t, QWidget *parent) : auto d = new QDialog(); auto ui = new Ui::NewTraceMathDialog(); ui->setupUi(d); + connect(d, &QDialog::finished, [=](){ + delete ui; + }); for(int i = 0; i < (int) TraceMath::Type::Last;i++) { auto info = TraceMath::getInfo(static_cast(i)); ui->list->addItem(info.name); diff --git a/Software/PC_Application/VNA/Deembedding/deembedding.cpp b/Software/PC_Application/VNA/Deembedding/deembedding.cpp index 8dac4a0..ba9a166 100644 --- a/Software/PC_Application/VNA/Deembedding/deembedding.cpp +++ b/Software/PC_Application/VNA/Deembedding/deembedding.cpp @@ -34,6 +34,9 @@ void Deembedding::startMeasurementDialog(bool S11, bool S12, bool S21, bool S22) auto ui = new Ui_DeembeddingMeasurementDialog; measurementUI = ui; ui->setupUi(measurementDialog); + connect(measurementDialog, &QDialog::finished, [=](){ + delete ui; + }); // add the trace selector set skip; @@ -106,7 +109,8 @@ void Deembedding::startMeasurementDialog(bool S11, bool S12, bool S21, bool S22) Deembedding::Deembedding(TraceModel &tm) : tm(tm), - measuring(false) + measuring(false), + sweepPoints(0) { } diff --git a/Software/PC_Application/VNA/Deembedding/matchingnetwork.cpp b/Software/PC_Application/VNA/Deembedding/matchingnetwork.cpp index 3b08796..78151ae 100644 --- a/Software/PC_Application/VNA/Deembedding/matchingnetwork.cpp +++ b/Software/PC_Application/VNA/Deembedding/matchingnetwork.cpp @@ -71,6 +71,9 @@ void MatchingNetwork::edit() auto dialog = new QDialog(); auto ui = new Ui::MatchingNetworkDialog(); ui->setupUi(dialog); + connect(dialog, &QDialog::finished, [=](){ + delete ui; + }); dialog->setModal(true); graph = new QWidget(); diff --git a/Software/PC_Application/VNA/Deembedding/portextension.cpp b/Software/PC_Application/VNA/Deembedding/portextension.cpp index 8edbb80..b66556a 100644 --- a/Software/PC_Application/VNA/Deembedding/portextension.cpp +++ b/Software/PC_Application/VNA/Deembedding/portextension.cpp @@ -81,6 +81,9 @@ void PortExtension::edit() auto dialog = new QDialog(); ui = new Ui::PortExtensionEditDialog(); ui->setupUi(dialog); + connect(dialog, &QDialog::finished, [=](){ + delete ui; + }); // set initial values ui->P1Enabled->setChecked(port1.enabled); diff --git a/Software/PC_Application/VNA/Deembedding/twothru.cpp b/Software/PC_Application/VNA/Deembedding/twothru.cpp index ed1e9e8..22ed767 100644 --- a/Software/PC_Application/VNA/Deembedding/twothru.cpp +++ b/Software/PC_Application/VNA/Deembedding/twothru.cpp @@ -126,6 +126,9 @@ void TwoThru::edit() auto dialog = new QDialog(); ui = new Ui::TwoThruDialog(); ui->setupUi(dialog); + connect(dialog, &QDialog::finished, [=](){ + delete ui; + }); ui->Z0->setUnit("Ω"); ui->Z0->setPrecision(4); ui->Z0->setValue(Z0); diff --git a/Software/PC_Application/VNA/tracewidgetvna.cpp b/Software/PC_Application/VNA/tracewidgetvna.cpp index 2c9a824..725b985 100644 --- a/Software/PC_Application/VNA/tracewidgetvna.cpp +++ b/Software/PC_Application/VNA/tracewidgetvna.cpp @@ -83,6 +83,9 @@ void TraceWidgetVNA::importDialog() auto dialog = new QDialog(); auto ui = new Ui::s2pImportOptions; ui->setupUi(dialog); + connect(dialog, &QDialog::finished, [=](){ + delete ui; + }); ui->applyCal->setEnabled(calAvailable); ui->deembed->setEnabled(deembedAvailable); bool applyCal = false; diff --git a/Software/PC_Application/VNA/vna.cpp b/Software/PC_Application/VNA/vna.cpp index dd5c5b8..07fa0e7 100644 --- a/Software/PC_Application/VNA/vna.cpp +++ b/Software/PC_Application/VNA/vna.cpp @@ -53,6 +53,7 @@ VNA::VNA(AppWindow *window) : Mode(window, "Vector Network Analyzer"), SCPINode("VNA"), deembedding(traceModel), + deembedding_active(false), central(new TileWidget(traceModel)) { averages = 1; @@ -60,6 +61,7 @@ VNA::VNA(AppWindow *window) calMeasuring = false; calDialog.reset(); calEdited = false; + settings.sweepType = SweepType::Frequency; // Create default traces auto tS11 = new Trace("S11", Qt::yellow); @@ -551,6 +553,8 @@ VNA::VNA(AppWindow *window) SetPoints(pref.Startup.DefaultSweep.points); if(pref.Startup.DefaultSweep.type == "Power Sweep") { SetSweepType(SweepType::Power); + } else { + SetSweepType(SweepType::Frequency); } } From 0d6dac496960dc86ccd82602991ef28ba0cc793a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20K=C3=A4berich?= Date: Fri, 10 Dec 2021 23:36:28 +0100 Subject: [PATCH 24/24] group delay option for Y axis --- Software/PC_Application/LibreVNA-GUI.pro | 1 + .../PC_Application/Traces/tracexyplot.cpp | 94 ++++++++++++------- Software/PC_Application/Traces/tracexyplot.h | 4 +- .../Traces/xyplotaxisdialog.cpp | 4 +- Software/PC_Application/Util/util.cpp | 26 +++++ Software/PC_Application/Util/util.h | 6 ++ 6 files changed, 99 insertions(+), 36 deletions(-) create mode 100644 Software/PC_Application/Util/util.cpp diff --git a/Software/PC_Application/LibreVNA-GUI.pro b/Software/PC_Application/LibreVNA-GUI.pro index 04a72c0..cb38875 100644 --- a/Software/PC_Application/LibreVNA-GUI.pro +++ b/Software/PC_Application/LibreVNA-GUI.pro @@ -218,6 +218,7 @@ SOURCES += \ Traces/tracewidget.cpp \ Traces/tracexyplot.cpp \ Traces/xyplotaxisdialog.cpp \ + Util/util.cpp \ VNA/Deembedding/deembedding.cpp \ VNA/Deembedding/deembeddingdialog.cpp \ VNA/Deembedding/deembeddingoption.cpp \ diff --git a/Software/PC_Application/Traces/tracexyplot.cpp b/Software/PC_Application/Traces/tracexyplot.cpp index a6d5277..bbfcf8d 100644 --- a/Software/PC_Application/Traces/tracexyplot.cpp +++ b/Software/PC_Application/Traces/tracexyplot.cpp @@ -17,22 +17,6 @@ using namespace std; -const set TraceXYPlot::YAxisTypes = {TraceXYPlot::YAxisType::Disabled, - TraceXYPlot::YAxisType::Magnitude, - TraceXYPlot::YAxisType::Phase, - TraceXYPlot::YAxisType::VSWR, - TraceXYPlot::YAxisType::Real, - TraceXYPlot::YAxisType::Imaginary, - TraceXYPlot::YAxisType::SeriesR, - TraceXYPlot::YAxisType::Reactance, - TraceXYPlot::YAxisType::Capacitance, - TraceXYPlot::YAxisType::Inductance, - TraceXYPlot::YAxisType::QualityFactor, - TraceXYPlot::YAxisType::ImpulseReal, - TraceXYPlot::YAxisType::ImpulseMag, - TraceXYPlot::YAxisType::Step, - TraceXYPlot::YAxisType::Impedance}; - TraceXYPlot::TraceXYPlot(TraceModel &model, QWidget *parent) : TracePlot(model, parent) { @@ -333,8 +317,11 @@ void TraceXYPlot::updateContextMenu() bool TraceXYPlot::dropSupported(Trace *t) { - Q_UNUSED(t) - // all kind of traces can be dropped, the graph will be reconfigured to support the dropped trace if required + if(domainMatch(t) && !supported(t)) { + // correct domain configured but Y axis do not match, prevent drop + return false; + } + // either directly compatible or domain change required return true; } @@ -823,12 +810,14 @@ QString TraceXYPlot::AxisTypeToName(TraceXYPlot::YAxisType type) case YAxisType::Capacitance: return "Capacitance"; case YAxisType::Inductance: return "Inductance"; case YAxisType::QualityFactor: return "Quality Factor"; + case YAxisType::GroupDelay: return "Group delay"; case YAxisType::ImpulseReal: return "Impulse Response (Real)"; case YAxisType::ImpulseMag: return "Impulse Response (Magnitude)"; case YAxisType::Step: return "Step Response"; case YAxisType::Impedance: return "Impedance"; - default: return "Unknown"; + case YAxisType::Last: return "Unknown"; } + return "Missing case"; } void TraceXYPlot::enableTraceAxis(Trace *t, int axis, bool enabled) @@ -861,27 +850,26 @@ void TraceXYPlot::enableTraceAxis(Trace *t, int axis, bool enabled) } } -bool TraceXYPlot::supported(Trace *t, TraceXYPlot::YAxisType type) +bool TraceXYPlot::domainMatch(Trace *t) { switch(XAxis.type) { case XAxisType::Frequency: - if(t->outputType() != Trace::DataType::Frequency) { - return false; - } - break; + return t->outputType() == Trace::DataType::Frequency; case XAxisType::Distance: case XAxisType::Time: - if(t->outputType() != Trace::DataType::Time) { - return false; - } - break; + return t->outputType() == Trace::DataType::Time; case XAxisType::Power: - if(t->outputType() != Trace::DataType::Power) { - return false; - } - break; - default: - break; + return t->outputType() == Trace::DataType::Power; + case XAxisType::Last: + return false; + } + return false; +} + +bool TraceXYPlot::supported(Trace *t, TraceXYPlot::YAxisType type) +{ + if(!domainMatch(t)) { + return false; } switch(type) { @@ -897,6 +885,11 @@ bool TraceXYPlot::supported(Trace *t, TraceXYPlot::YAxisType type) return false; } break; + case YAxisType::GroupDelay: + if(t->isReflection()) { + return false; + } + break; default: break; } @@ -946,6 +939,38 @@ QPointF TraceXYPlot::traceToCoordinate(Trace *t, unsigned int sample, TraceXYPlo case YAxisType::QualityFactor: ret.setY(Util::SparamToQualityFactor(data.y)); break; + case YAxisType::GroupDelay: { + constexpr int requiredSamples = 5; + if(t->size() < requiredSamples) { + // unable to calculate + ret.setY(0.0); + break; + } + // needs at least some samples before/after current sample for calculating the derivative. + // For samples too far at either end of the trace, return group delay of "inner" trace sample instead + if(sample < requiredSamples / 2) { + return traceToCoordinate(t, requiredSamples / 2, type); + } else if(sample >= t->size() - requiredSamples / 2) { + return traceToCoordinate(t, t->size() - requiredSamples / 2 - 1, type); + } else { + // got enough samples at either end to calculate derivative. + // acquire phases of the required samples + std::vector phases; + phases.reserve(requiredSamples); + for(unsigned int index = sample - requiredSamples / 2;index <= sample + requiredSamples / 2;index++) { + phases.push_back(arg(t->sample(index).y)); + } + // make sure there are no phase jumps + Util::unwrapPhase(phases); + // calculate linearRegression to get derivative + double B_0, B_1; + Util::linearRegression(phases, B_0, B_1); + // B_1 now contains the derived phase vs. the sample. Scale by frequency to get group delay + double freq_step = t->sample(sample).x - t->sample(sample - 1).x; + ret.setY(-B_1 / (2.0*M_PI * freq_step)); + } + } + break; case YAxisType::ImpulseReal: ret.setY(real(data.y)); break; @@ -1103,6 +1128,7 @@ QString TraceXYPlot::AxisUnit(TraceXYPlot::YAxisType type) case TraceXYPlot::YAxisType::ImpulseMag: return "db"; case TraceXYPlot::YAxisType::Step: return ""; case TraceXYPlot::YAxisType::Impedance: return "Ohm"; + case TraceXYPlot::YAxisType::GroupDelay: return "s"; default: return ""; } } diff --git a/Software/PC_Application/Traces/tracexyplot.h b/Software/PC_Application/Traces/tracexyplot.h index 0bfd030..6d553a1 100644 --- a/Software/PC_Application/Traces/tracexyplot.h +++ b/Software/PC_Application/Traces/tracexyplot.h @@ -26,6 +26,7 @@ public: Capacitance, Inductance, QualityFactor, + GroupDelay, // TDR options ImpulseReal, ImpulseMag, @@ -33,7 +34,7 @@ public: Impedance, Last, }; - static const std::set YAxisTypes; + enum class XAxisType { Frequency, Time, @@ -80,6 +81,7 @@ private: YAxisType YAxisTypeFromName(QString name); XAxisMode AxisModeFromName(QString name); void enableTraceAxis(Trace *t, int axis, bool enabled); + bool domainMatch(Trace *t); bool supported(Trace *t) override; bool supported(Trace *t, YAxisType type); QPointF traceToCoordinate(Trace *t, unsigned int sample, YAxisType type); diff --git a/Software/PC_Application/Traces/xyplotaxisdialog.cpp b/Software/PC_Application/Traces/xyplotaxisdialog.cpp index 5c57bf7..46fbfdb 100644 --- a/Software/PC_Application/Traces/xyplotaxisdialog.cpp +++ b/Software/PC_Application/Traces/xyplotaxisdialog.cpp @@ -151,7 +151,8 @@ void XYplotAxisDialog::XAxisTypeChanged(int XAxisIndex) { auto type = (TraceXYPlot::XAxisType) XAxisIndex; auto supported = supportedYAxis(type); - for(auto t : TraceXYPlot::YAxisTypes) { + for(unsigned int i=0;i<(int) TraceXYPlot::YAxisType::Last;i++) { + auto t = (TraceXYPlot::YAxisType) i; auto enable = supported.count(t) > 0; auto index = (int) t; enableComboBoxItem(ui->Y1type, index, enable); @@ -197,6 +198,7 @@ std::set XYplotAxisDialog::supportedYAxis(TraceXYPlot::X ret.insert(TraceXYPlot::YAxisType::Capacitance); ret.insert(TraceXYPlot::YAxisType::Inductance); ret.insert(TraceXYPlot::YAxisType::QualityFactor); + ret.insert(TraceXYPlot::YAxisType::GroupDelay); break; case TraceXYPlot::XAxisType::Time: case TraceXYPlot::XAxisType::Distance: diff --git a/Software/PC_Application/Util/util.cpp b/Software/PC_Application/Util/util.cpp new file mode 100644 index 0000000..90baaef --- /dev/null +++ b/Software/PC_Application/Util/util.cpp @@ -0,0 +1,26 @@ +#include "util.h" + +void Util::unwrapPhase(std::vector &phase) +{ + for (unsigned int i = 1; i < phase.size(); i++) { + double d = phase[i] - phase[i-1]; + d = d > M_PI ? d - 2 * M_PI : (d < -M_PI ? d + 2 * M_PI : d); + phase[i] = phase[i-1] + d; + } +} + +void Util::linearRegression(const std::vector &input, double &B_0, double &B_1) +{ + double x_mean = (input.size() - 1.0) / 2.0; + double y_mean = std::accumulate(input.begin(), input.end(), 0.0) / input.size(); + double ss_xy = 0.0; + for(unsigned int i=0;i #include #include +#include #include @@ -55,6 +56,11 @@ namespace Util { auto brightness = q.redF() * 0.299 + q.greenF() * 0.587 + q.blueF() * 0.114; return brightness > 0.6 ? Qt::black : Qt::white; } + + void unwrapPhase(std::vector &phase); + + // input values are Y coordinates, assumes evenly spaced linear X values from 0 to input.size() - 1 + void linearRegression(const std::vector &input, double &B_0, double &B_1); } #endif // UTILH_H