mirror of
https://github.com/jankae/LibreVNA.git
synced 2026-04-08 16:03:43 +00:00
Waterfall display
This commit is contained in:
parent
a7fcaf7d97
commit
5897705f32
18 changed files with 1706 additions and 902 deletions
441
Software/PC_Application/Traces/traceaxis.cpp
Normal file
441
Software/PC_Application/Traces/traceaxis.cpp
Normal file
|
|
@ -0,0 +1,441 @@
|
|||
#include "traceaxis.h"
|
||||
#include "Util/util.h"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
using namespace std;
|
||||
|
||||
static void createEvenlySpacedTicks(vector<double>& ticks, double start, double stop, double step) {
|
||||
ticks.clear();
|
||||
if(start > stop) {
|
||||
swap(start, stop);
|
||||
}
|
||||
step = abs(step);
|
||||
constexpr unsigned int maxTicks = 100;
|
||||
for(double tick = start; tick - stop < numeric_limits<double>::epsilon() && ticks.size() <= maxTicks;tick+= step) {
|
||||
ticks.push_back(tick);
|
||||
}
|
||||
}
|
||||
|
||||
static double createAutomaticTicks(vector<double>& ticks, double start, double stop, int minDivisions) {
|
||||
Q_ASSERT(stop > start);
|
||||
ticks.clear();
|
||||
double max_div_step = (stop - start) / minDivisions;
|
||||
int zeros = floor(log10(max_div_step));
|
||||
double decimals_shift = pow(10, zeros);
|
||||
max_div_step /= decimals_shift;
|
||||
if(max_div_step >= 5) {
|
||||
max_div_step = 5;
|
||||
} else if(max_div_step >= 2) {
|
||||
max_div_step = 2;
|
||||
} else {
|
||||
max_div_step = 1;
|
||||
}
|
||||
auto div_step = max_div_step * decimals_shift;
|
||||
// round min up to next multiple of div_step
|
||||
auto start_div = ceil(start / div_step) * div_step;
|
||||
for(double tick = start_div;tick <= stop;tick += div_step) {
|
||||
ticks.push_back(tick);
|
||||
}
|
||||
return div_step;
|
||||
}
|
||||
|
||||
static void createLogarithmicTicks(vector<double>& ticks, double start, double stop, int minDivisions) {
|
||||
// enforce usable log settings
|
||||
if(start <= 0) {
|
||||
start = 1.0;
|
||||
}
|
||||
if(stop <= start) {
|
||||
stop = start + 1.0;
|
||||
}
|
||||
ticks.clear();
|
||||
|
||||
auto decades = log10(stop) - log10(start);
|
||||
double max_div_decade = minDivisions / decades;
|
||||
int zeros = floor(log10(max_div_decade));
|
||||
double decimals_shift = pow(10, zeros);
|
||||
max_div_decade /= decimals_shift;
|
||||
if(max_div_decade < 2) {
|
||||
max_div_decade = 2;
|
||||
} else if(max_div_decade < 5) {
|
||||
max_div_decade = 5;
|
||||
} else {
|
||||
max_div_decade = 10;
|
||||
}
|
||||
auto step = pow(10, floor(log10(start))+1) / (max_div_decade * decimals_shift);
|
||||
// round min up to next multiple of div_step
|
||||
auto div = ceil(start / step) * step;
|
||||
if(floor(log10(div)) != floor(log10(start))) {
|
||||
// first div is already at the next decade
|
||||
step *= 10;
|
||||
}
|
||||
do {
|
||||
ticks.push_back(div);
|
||||
if(ticks.size() > 1 && div != step && floor(log10(div)) != floor(log10(div - step))) {
|
||||
// reached a new decade with this switch
|
||||
step *= 10;
|
||||
div = step;
|
||||
} else {
|
||||
div += step;
|
||||
}
|
||||
} while(div <= stop);
|
||||
}
|
||||
|
||||
double YAxis::sampleToCoordinate(Trace::Data data, Trace *t, unsigned int sample)
|
||||
{
|
||||
switch(type) {
|
||||
case YAxis::Type::Magnitude:
|
||||
return Util::SparamTodB(data.y);
|
||||
case YAxis::Type::MagnitudedBuV:
|
||||
return Util::dBmTodBuV(Util::SparamTodB(data.y));
|
||||
case YAxis::Type::MagnitudeLinear:
|
||||
return abs(data.y);
|
||||
case YAxis::Type::Phase:
|
||||
return Util::SparamToDegree(data.y);
|
||||
case YAxis::Type::UnwrappedPhase:
|
||||
if(!t) {
|
||||
return 0.0;
|
||||
}
|
||||
return t->getUnwrappedPhase(sample) * 180.0 / M_PI;
|
||||
case YAxis::Type::VSWR:
|
||||
return Util::SparamToVSWR(data.y);
|
||||
case YAxis::Type::Real:
|
||||
return data.y.real();
|
||||
case YAxis::Type::Imaginary:
|
||||
return data.y.imag();
|
||||
case YAxis::Type::SeriesR:
|
||||
return Util::SparamToResistance(data.y);
|
||||
case YAxis::Type::Reactance:
|
||||
return Util::SparamToImpedance(data.y).imag();
|
||||
case YAxis::Type::Capacitance:
|
||||
return Util::SparamToCapacitance(data.y, data.x);
|
||||
case YAxis::Type::Inductance:
|
||||
return Util::SparamToInductance(data.y, data.x);
|
||||
case YAxis::Type::QualityFactor:
|
||||
return Util::SparamToQualityFactor(data.y);
|
||||
case YAxis::Type::GroupDelay: {
|
||||
constexpr int requiredSamples = 5;
|
||||
if(!t || t->size() < requiredSamples) {
|
||||
// unable to calculate
|
||||
return 0.0;
|
||||
|
||||
}
|
||||
// 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 sampleToCoordinate(data, t, requiredSamples / 2);
|
||||
} else if(sample >= t->size() - requiredSamples / 2) {
|
||||
return sampleToCoordinate(data, t, t->size() - requiredSamples / 2 - 1);
|
||||
} else {
|
||||
// got enough samples at either end to calculate derivative.
|
||||
// acquire phases of the required samples
|
||||
std::vector<double> 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;
|
||||
return -B_1 / (2.0*M_PI * freq_step);
|
||||
}
|
||||
}
|
||||
case YAxis::Type::ImpulseReal:
|
||||
return real(data.y);
|
||||
case YAxis::Type::ImpulseMag:
|
||||
return Util::SparamTodB(data.y);
|
||||
case YAxis::Type::Step:
|
||||
if(!t) {
|
||||
return 0.0;
|
||||
}
|
||||
return t->sample(sample, true).y.real();
|
||||
case YAxis::Type::Impedance: {
|
||||
if(!t) {
|
||||
return 0.0;
|
||||
}
|
||||
double step = t->sample(sample, true).y.real();
|
||||
if(abs(step) < 1.0) {
|
||||
return Util::SparamToImpedance(step).real();
|
||||
}
|
||||
}
|
||||
break;
|
||||
case YAxis::Type::Disabled:
|
||||
case YAxis::Type::Last:
|
||||
// no valid axis
|
||||
break;
|
||||
}
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
void YAxis::set(Type type, bool log, bool autorange, double min, double max, double div)
|
||||
{
|
||||
this->type = type;
|
||||
this->log = log;
|
||||
this->autorange = autorange;
|
||||
this->rangeMin = min;
|
||||
this->rangeMax = max;
|
||||
this->rangeDiv = div;
|
||||
if(type != Type::Disabled) {
|
||||
updateTicks();
|
||||
}
|
||||
}
|
||||
|
||||
QString YAxis::TypeToName(Type type)
|
||||
{
|
||||
switch(type) {
|
||||
case Type::Disabled: return "Disabled";
|
||||
case Type::Magnitude: return "Magnitude (dB/dBm)";
|
||||
case Type::MagnitudedBuV: return "Magnitude (dBuV)";
|
||||
case Type::MagnitudeLinear: return "Magnitude (linear)";
|
||||
case Type::Phase: return "Phase";
|
||||
case Type::UnwrappedPhase: return "Unwrapped Phase";
|
||||
case Type::VSWR: return "VSWR";
|
||||
case Type::Real: return "Real";
|
||||
case Type::Imaginary: return "Imaginary";
|
||||
case Type::SeriesR: return "Resistance";
|
||||
case Type::Reactance: return "Reactance";
|
||||
case Type::Capacitance: return "Capacitance";
|
||||
case Type::Inductance: return "Inductance";
|
||||
case Type::QualityFactor: return "Quality Factor";
|
||||
case Type::GroupDelay: return "Group delay";
|
||||
case Type::ImpulseReal: return "Impulse Response (Real)";
|
||||
case Type::ImpulseMag: return "Impulse Response (Magnitude)";
|
||||
case Type::Step: return "Step Response";
|
||||
case Type::Impedance: return "Impedance";
|
||||
case Type::Last: return "Unknown";
|
||||
}
|
||||
return "Missing case";
|
||||
}
|
||||
|
||||
YAxis::Type YAxis::TypeFromName(QString name)
|
||||
{
|
||||
for(unsigned int i=0;i<(int) Type::Last;i++) {
|
||||
if(TypeToName((Type) i) == name) {
|
||||
return (Type) i;
|
||||
}
|
||||
}
|
||||
// not found, use default
|
||||
return Type::Magnitude;
|
||||
|
||||
}
|
||||
|
||||
QString YAxis::Unit(Type type, TraceModel::DataSource source)
|
||||
{
|
||||
if(source == TraceModel::DataSource::VNA) {
|
||||
switch(type) {
|
||||
case Type::Magnitude: return "dB";
|
||||
case Type::MagnitudeLinear: return "";
|
||||
case Type::Phase: return "°";
|
||||
case Type::UnwrappedPhase: return "°";
|
||||
case Type::VSWR: return "";
|
||||
case Type::ImpulseReal: return "";
|
||||
case Type::ImpulseMag: return "dB";
|
||||
case Type::Step: return "";
|
||||
case Type::Impedance: return "Ω";
|
||||
case Type::GroupDelay: return "s";
|
||||
case Type::Disabled:
|
||||
case Type::Real:
|
||||
case Type::Imaginary:
|
||||
case Type::QualityFactor:
|
||||
return "";
|
||||
case Type::SeriesR: return "Ω";
|
||||
case Type::Reactance: return "Ω";
|
||||
case Type::Capacitance: return "F";
|
||||
case Type::Inductance: return "H";
|
||||
case Type::Last: return "";
|
||||
}
|
||||
} else if(source == TraceModel::DataSource::SA) {
|
||||
switch(type) {
|
||||
case Type::Magnitude: return "dBm";
|
||||
case Type::MagnitudedBuV: return "dBuV";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
QString YAxis::Prefixes(Type type, TraceModel::DataSource source)
|
||||
{
|
||||
if(source == TraceModel::DataSource::VNA) {
|
||||
switch(type) {
|
||||
case Type::Magnitude: return " ";
|
||||
case Type::MagnitudeLinear: return "num ";
|
||||
case Type::Phase: return " ";
|
||||
case Type::UnwrappedPhase: return " ";
|
||||
case Type::VSWR: return " ";
|
||||
case Type::ImpulseReal: return "pnum kMG";
|
||||
case Type::ImpulseMag: return " ";
|
||||
case Type::Step: return "pnum kMG";
|
||||
case Type::Impedance: return "m kM";
|
||||
case Type::GroupDelay: return "pnum ";
|
||||
case Type::Disabled: return " ";
|
||||
case Type::Real: return "pnum ";
|
||||
case Type::Imaginary: return "pnum ";
|
||||
case Type::QualityFactor: return " ";
|
||||
case Type::SeriesR: return "m kM";
|
||||
case Type::Reactance: return "m kM";
|
||||
case Type::Capacitance: return "pnum ";
|
||||
case Type::Inductance: return "pnum ";
|
||||
case Type::Last: return " ";
|
||||
}
|
||||
} else if(source == TraceModel::DataSource::SA) {
|
||||
switch(type) {
|
||||
case Type::Magnitude: return " ";
|
||||
case Type::MagnitudedBuV: return " ";
|
||||
default: return " ";
|
||||
}
|
||||
}
|
||||
return " ";
|
||||
}
|
||||
|
||||
QString YAxis::TypeToName()
|
||||
{
|
||||
return TypeToName(type);
|
||||
}
|
||||
|
||||
QString YAxis::Unit(TraceModel::DataSource source)
|
||||
{
|
||||
return Unit(type, source);
|
||||
}
|
||||
|
||||
QString YAxis::Prefixes(TraceModel::DataSource source)
|
||||
{
|
||||
return Prefixes(type, source);
|
||||
}
|
||||
|
||||
YAxis::Type YAxis::getType() const
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
void Axis::updateTicks()
|
||||
{
|
||||
if(log) {
|
||||
createLogarithmicTicks(ticks, rangeMin, rangeMax, 20);
|
||||
} else if(autorange) {
|
||||
if(rangeMin >= rangeMax) {
|
||||
// problem, min must be less than max
|
||||
rangeMin -= 1.0;
|
||||
rangeMax += 1.0;
|
||||
}
|
||||
rangeDiv = createAutomaticTicks(ticks, rangeMin, rangeMax, 8);
|
||||
} else {
|
||||
createEvenlySpacedTicks(ticks, rangeMin, rangeMax, rangeDiv);
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<double> &Axis::getTicks() const
|
||||
{
|
||||
return ticks;
|
||||
}
|
||||
|
||||
double Axis::getRangeDiv() const
|
||||
{
|
||||
return rangeDiv;
|
||||
}
|
||||
|
||||
double Axis::getRangeMax() const
|
||||
{
|
||||
return rangeMax;
|
||||
}
|
||||
|
||||
double Axis::getRangeMin() const
|
||||
{
|
||||
return rangeMin;
|
||||
}
|
||||
|
||||
bool Axis::getAutorange() const
|
||||
{
|
||||
return autorange;
|
||||
}
|
||||
|
||||
bool Axis::getLog() const
|
||||
{
|
||||
return log;
|
||||
}
|
||||
|
||||
double XAxis::sampleToCoordinate(Trace::Data data, Trace *t, unsigned int sample)
|
||||
{
|
||||
switch(type) {
|
||||
case Type::Distance:
|
||||
if(!t) {
|
||||
return 0.0;
|
||||
}
|
||||
return t->timeToDistance(data.x);
|
||||
default:
|
||||
return data.x;
|
||||
}
|
||||
}
|
||||
|
||||
void XAxis::set(Type type, bool log, bool autorange, double min, double max, double div)
|
||||
{
|
||||
this->type = type;
|
||||
this->log = log;
|
||||
this->autorange = autorange;
|
||||
this->rangeMin = min;
|
||||
this->rangeMax = max;
|
||||
this->rangeDiv = div;
|
||||
updateTicks();
|
||||
}
|
||||
|
||||
QString XAxis::TypeToName(Type type)
|
||||
{
|
||||
switch(type) {
|
||||
case Type::Frequency: return "Frequency";
|
||||
case Type::Time: return "Time";
|
||||
case Type::Distance: return "Distance";
|
||||
case Type::Power: return "Power";
|
||||
default: return "Unknown";
|
||||
}
|
||||
}
|
||||
|
||||
XAxis::Type XAxis::TypeFromName(QString name)
|
||||
{
|
||||
for(unsigned int i=0;i<(int) Type::Last;i++) {
|
||||
if(TypeToName((Type) i) == name) {
|
||||
return (Type) i;
|
||||
}
|
||||
}
|
||||
// not found, use default
|
||||
return Type::Frequency;
|
||||
}
|
||||
|
||||
QString XAxis::Unit(Type type)
|
||||
{
|
||||
switch(type) {
|
||||
case Type::Frequency: return "Hz";
|
||||
case Type::Time: return "s";
|
||||
case Type::Distance: return "m";
|
||||
case Type::Power: return "dBm";
|
||||
default: return "";
|
||||
}
|
||||
}
|
||||
|
||||
QString XAxis::TypeToName()
|
||||
{
|
||||
return TypeToName(type);
|
||||
}
|
||||
|
||||
QString XAxis::Unit()
|
||||
{
|
||||
return Unit(type);
|
||||
}
|
||||
|
||||
XAxis::Type XAxis::getType() const
|
||||
{
|
||||
return type;
|
||||
}
|
||||
|
||||
double Axis::transform(double value, double to_low, double to_high)
|
||||
{
|
||||
return Util::Scale(value, rangeMin, rangeMax, to_low, to_high, log);
|
||||
}
|
||||
|
||||
double Axis::inverseTransform(double value, double to_low, double to_high)
|
||||
{
|
||||
return Util::Scale(value, to_low, to_high, rangeMin, rangeMax, false, log);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue