mirror of
https://github.com/saymrwulf/uhd.git
synced 2026-05-16 21:10:10 +00:00
The pwr_cal::get_gain() method previously held the incorrect assumption that power values per frequency would be equidistant, i.e., be at the same indices as the gain values. Due to the frequency-dependent nature of the hardware, this is not a valid assumption (if that were the case, frequency-dependent calibration would be unnecessary). This changes get_gain() to not rely on that assumption. Note that it requires doing some more rounding: The bilinear interpolation method uses requires coordinates to be on a rectangular grid. This snaps the power values onto a single coordinate.
359 lines
13 KiB
C++
359 lines
13 KiB
C++
//
|
|
// Copyright 2020 Ettus Research, a National Instruments Brand
|
|
//
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//
|
|
|
|
#include <uhd/cal/pwr_cal.hpp>
|
|
#include <uhd/cal/pwr_cal_generated.h>
|
|
#include <uhd/exception.hpp>
|
|
#include <uhd/utils/log.hpp>
|
|
#include <uhd/utils/math.hpp>
|
|
#include <uhdlib/utils/interpolation.hpp>
|
|
#include <map>
|
|
#include <string>
|
|
|
|
using namespace uhd::usrp::cal;
|
|
using namespace uhd::math;
|
|
|
|
namespace {
|
|
|
|
//! We use the NIST normal temperature
|
|
constexpr int NORMAL_TEMPERATURE = 20;
|
|
|
|
constexpr size_t VERSION_MAJOR = 1;
|
|
constexpr size_t VERSION_MINOR = 0;
|
|
|
|
//! Return map with keys as values and vice versa
|
|
template <typename map_type>
|
|
map_type reverse_map(const map_type& map)
|
|
{
|
|
map_type result;
|
|
std::transform(map.cbegin(),
|
|
map.cend(),
|
|
std::inserter(result, result.end()),
|
|
[](const typename map_type::value_type& entry) {
|
|
return std::pair<typename map_type::mapped_type, typename map_type::key_type>(
|
|
entry.second, entry.first);
|
|
});
|
|
return result;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
|
|
class pwr_cal_impl : public pwr_cal
|
|
{
|
|
public:
|
|
pwr_cal_impl(const std::string& name = "",
|
|
const std::string& serial = "",
|
|
const uint64_t timestamp = 0)
|
|
: _name(name), _serial(serial), _timestamp(timestamp)
|
|
{
|
|
}
|
|
|
|
/**************************************************************************
|
|
* Container API (Basics)
|
|
*************************************************************************/
|
|
std::string get_name() const
|
|
{
|
|
return _name;
|
|
}
|
|
|
|
std::string get_serial() const
|
|
{
|
|
return _serial;
|
|
}
|
|
|
|
uint64_t get_timestamp() const
|
|
{
|
|
return _timestamp;
|
|
}
|
|
|
|
/**************************************************************************
|
|
* Specific APIs
|
|
*************************************************************************/
|
|
void add_power_table(const std::map<double, double>& gain_power_map,
|
|
const double min_power,
|
|
const double max_power,
|
|
const double freq,
|
|
const boost::optional<int> temperature = boost::none)
|
|
{
|
|
if (min_power > max_power) {
|
|
throw uhd::runtime_error(
|
|
std::string("Invalid min/max power levels: Min power must be smaller "
|
|
"than max power! (Is: "
|
|
+ std::to_string(min_power) + " dBm, "
|
|
+ std::to_string(max_power) + " dBm)"));
|
|
}
|
|
const int temp = bool(temperature) ? temperature.get() : _default_temp;
|
|
_data[temp][static_cast<uint64_t>(freq)] = {
|
|
gain_power_map, reverse_map(gain_power_map), min_power, max_power};
|
|
}
|
|
|
|
// Note: This is very similar to at_bilin_interp(), but we can't use that
|
|
// because we mix types in the gain tables (we have uint64_t and double, and
|
|
// a struct).
|
|
double get_power(const double gain,
|
|
const double freq,
|
|
const boost::optional<int> temperature = boost::none) const
|
|
{
|
|
UHD_ASSERT_THROW(!_data.empty());
|
|
const uint64_t freqi = static_cast<uint64_t>(freq);
|
|
const auto& table = _get_table(temperature);
|
|
|
|
const auto f_iters = get_bounding_iterators(table, freqi);
|
|
const uint64_t f1i = f_iters.first->first;
|
|
const uint64_t f2i = f_iters.second->first;
|
|
// Frequency is out of bounds
|
|
if (f1i == f2i) {
|
|
return at_lin_interp(table.at(f1i).g2p, gain);
|
|
}
|
|
const double f1 = static_cast<double>(f1i);
|
|
const double f2 = static_cast<double>(f2i);
|
|
const auto gain_iters = get_bounding_iterators(table.at(f1).g2p, gain);
|
|
const double gain1 = gain_iters.first->first;
|
|
const double gain2 = gain_iters.second->first;
|
|
// Gain is out of bounds
|
|
if (gain1 == gain2) {
|
|
return linear_interp(freq,
|
|
f1,
|
|
table.at(f1i).g2p.at(gain1),
|
|
f2,
|
|
table.at(f2i).g2p.at(gain1));
|
|
}
|
|
|
|
// Both gain and freq are within bounds: Bi-Linear interpolation
|
|
// Find power values
|
|
const auto power11 = table.at(f1i).g2p.at(gain1);
|
|
const auto power12 = table.at(f1i).g2p.at(gain2);
|
|
const auto power21 = table.at(f2i).g2p.at(gain1);
|
|
const auto power22 = table.at(f2i).g2p.at(gain2);
|
|
|
|
return bilinear_interp(
|
|
freq, gain, f1, gain1, f2, gain2, power11, power12, power21, power22);
|
|
}
|
|
|
|
void clear()
|
|
{
|
|
_data.clear();
|
|
}
|
|
|
|
void set_temperature(const int temperature)
|
|
{
|
|
_default_temp = temperature;
|
|
}
|
|
|
|
int get_temperature() const
|
|
{
|
|
return _default_temp;
|
|
}
|
|
|
|
void set_ref_gain(const double gain)
|
|
{
|
|
_ref_gain = gain;
|
|
}
|
|
|
|
double get_ref_gain() const
|
|
{
|
|
return _ref_gain;
|
|
}
|
|
|
|
uhd::meta_range_t get_power_limits(
|
|
const double freq, const boost::optional<int> temperature = boost::none) const
|
|
{
|
|
const auto table = at_nearest(_get_table(temperature), uint64_t(freq));
|
|
return uhd::meta_range_t(table.min_power, table.max_power);
|
|
}
|
|
|
|
double get_gain(const double power_dbm,
|
|
const double freq,
|
|
const boost::optional<int> temperature = boost::none) const
|
|
{
|
|
UHD_ASSERT_THROW(!_data.empty());
|
|
const uint64_t freqi = static_cast<uint64_t>(freq);
|
|
const auto& table = _get_table(temperature);
|
|
const double power_coerced = get_power_limits(freq, temperature).clip(power_dbm);
|
|
|
|
const auto f_iters = get_bounding_iterators(table, freqi);
|
|
const uint64_t f1i = f_iters.first->first;
|
|
const uint64_t f2i = f_iters.second->first;
|
|
if (f1i == f2i) {
|
|
// Frequency is out of bounds
|
|
return at_lin_interp(table.at(f1i).p2g, power_coerced);
|
|
}
|
|
|
|
// NOTE: bilinear_interp() does not interpolate on an arbitrary tetragon,
|
|
// but requires the coordinates to be on a rectangular grid. Due to the
|
|
// frequency-dependent nature of power calibration, it is unlikely that
|
|
// the bounding power values for f1 and f2 (respectively) are identical.
|
|
// We therefore not only interpolate the final gain values, but we also
|
|
// nearest-neighbor-interpolate the grid coordinates for the power.
|
|
// This snap-to-grid adds another error, which can be counteracted by
|
|
// good choice of frequency and gain points on which to sample.
|
|
const auto f1pwr_iters = get_bounding_iterators(table.at(f1i).p2g, power_coerced);
|
|
const double f1pwr1 = f1pwr_iters.first->first;
|
|
const double f1pwr2 = f1pwr_iters.second->first;
|
|
const auto f2pwr_iters = get_bounding_iterators(table.at(f2i).p2g, power_coerced);
|
|
const double f2pwr1 = f2pwr_iters.first->first;
|
|
const double f2pwr2 = f2pwr_iters.second->first;
|
|
const double f1 = static_cast<double>(f1i);
|
|
const double f2 = static_cast<double>(f2i);
|
|
const double pwr1 = linear_interp(freq, f1, f1pwr1, f2, f2pwr1);
|
|
const double pwr2 = linear_interp(freq, f1, f1pwr2, f2, f2pwr2);
|
|
// Power is out of bounds (this shouldn't happen after coercing, but this
|
|
// is just another good sanity check on our data)
|
|
if (pwr1 == pwr2) {
|
|
return linear_interp(freq,
|
|
f1,
|
|
at_nearest(table.at(f1i).p2g, pwr1),
|
|
f2,
|
|
at_nearest(table.at(f2i).p2g, pwr2));
|
|
}
|
|
// Both gain and freq are within bounds => Bi-Linear interpolation
|
|
// Find gain values:
|
|
const auto gain11 = table.at(f1i).p2g.at(f1pwr1);
|
|
const auto gain12 = table.at(f1i).p2g.at(f1pwr2);
|
|
const auto gain21 = table.at(f2i).p2g.at(f2pwr1);
|
|
const auto gain22 = table.at(f2i).p2g.at(f2pwr2);
|
|
return bilinear_interp(
|
|
freq, power_coerced, f1, pwr1, f2, pwr2, gain11, gain12, gain21, gain22);
|
|
}
|
|
|
|
/**************************************************************************
|
|
* Container API (Serialization/Deserialization)
|
|
*************************************************************************/
|
|
std::vector<uint8_t> serialize()
|
|
{
|
|
const size_t initial_size_bytes = 1024 * 20; // 20 kiB as an initial guess
|
|
flatbuffers::FlatBufferBuilder builder(initial_size_bytes);
|
|
|
|
std::vector<flatbuffers::Offset<TempFreqMap>> temp_freq_map;
|
|
temp_freq_map.reserve(_data.size());
|
|
for (auto& temp_freq_pair : _data) {
|
|
const int temperature = temp_freq_pair.first;
|
|
std::vector<flatbuffers::Offset<FreqPowerMap>> freq_gain_map;
|
|
for (auto& freq_gain_pair : temp_freq_pair.second) {
|
|
const uint64_t freq = freq_gain_pair.first;
|
|
const double min_power = freq_gain_pair.second.min_power;
|
|
const double max_power = freq_gain_pair.second.max_power;
|
|
std::vector<PowerMap> gain_power_map;
|
|
for (auto& gain_power_pair : freq_gain_pair.second.g2p) {
|
|
gain_power_map.push_back(
|
|
PowerMap(gain_power_pair.first, gain_power_pair.second));
|
|
}
|
|
|
|
freq_gain_map.push_back(CreateFreqPowerMapDirect(
|
|
builder, freq, &gain_power_map, min_power, max_power));
|
|
}
|
|
|
|
temp_freq_map.push_back(
|
|
CreateTempFreqMapDirect(builder, temperature, &freq_gain_map));
|
|
}
|
|
|
|
// Now load it all into the FlatBuffer
|
|
auto metadata = CreateMetadataDirect(builder,
|
|
_name.c_str(),
|
|
_serial.c_str(),
|
|
_timestamp,
|
|
VERSION_MAJOR,
|
|
VERSION_MINOR);
|
|
auto power_cal =
|
|
CreatePowerCalDirect(builder, metadata, &temp_freq_map, get_ref_gain());
|
|
FinishPowerCalBuffer(builder, power_cal);
|
|
const size_t table_size = builder.GetSize();
|
|
const uint8_t* table = builder.GetBufferPointer();
|
|
return std::vector<uint8_t>(table, table + table_size);
|
|
}
|
|
|
|
// This will amend the existing table. If that's not desired, then it is
|
|
// necessary to call clear() ahead of time.
|
|
void deserialize(const std::vector<uint8_t>& data)
|
|
{
|
|
auto verifier = flatbuffers::Verifier(data.data(), data.size());
|
|
if (!VerifyPowerCalBuffer(verifier)) {
|
|
throw uhd::runtime_error("pwr_cal: Invalid data provided!");
|
|
}
|
|
auto cal_table = GetPowerCal(static_cast<const void*>(data.data()));
|
|
if (cal_table->metadata()->version_major() != VERSION_MAJOR) {
|
|
throw uhd::runtime_error("pwr_cal: Compat number mismatch!");
|
|
}
|
|
if (cal_table->metadata()->version_minor() != VERSION_MINOR) {
|
|
UHD_LOG_WARNING("CAL",
|
|
"pwr_cal: Expected compat number "
|
|
<< VERSION_MAJOR << "." << VERSION_MINOR << ", got "
|
|
<< cal_table->metadata()->version_major() << "."
|
|
<< cal_table->metadata()->version_minor());
|
|
}
|
|
_name = std::string(cal_table->metadata()->name()->c_str());
|
|
_serial = std::string(cal_table->metadata()->serial()->c_str());
|
|
_timestamp = cal_table->metadata()->timestamp();
|
|
if (cal_table->ref_gain() >= 0.0) {
|
|
_ref_gain = cal_table->ref_gain();
|
|
}
|
|
|
|
auto temp_freq_map = cal_table->temp_freq_map();
|
|
for (auto it = temp_freq_map->begin(); it != temp_freq_map->end(); ++it) {
|
|
const int temperature = it->temperature();
|
|
auto freq_gain_map = it->freqs();
|
|
for (auto f_it = freq_gain_map->begin(); f_it != freq_gain_map->end();
|
|
++f_it) {
|
|
std::map<double, double> power;
|
|
auto power_map = f_it->powers();
|
|
for (auto g_it = power_map->begin(); g_it != power_map->end(); ++g_it) {
|
|
power.insert({g_it->gain(), g_it->power_dbm()});
|
|
}
|
|
add_power_table(power,
|
|
f_it->min_power(),
|
|
f_it->max_power(),
|
|
f_it->freq(),
|
|
temperature);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
private:
|
|
// We map the gain to power, and power to gain, in different data structures.
|
|
// This is suboptimal w.r.t. memory usage (it duplicates the keys/values),
|
|
// but helps us with the algorithms above.
|
|
// This could also be solved with a Boost.Bimap, but it doesn't seem worth
|
|
// the additional dependency.
|
|
struct pwr_cal_table
|
|
{
|
|
std::map<double, double> g2p; //!< Maps gain to power
|
|
std::map<double, double> p2g; //!< Maps power to gain
|
|
double min_power;
|
|
double max_power;
|
|
};
|
|
|
|
using freq_table_map = std::map<uint64_t /* freq */, pwr_cal_table>;
|
|
|
|
freq_table_map _get_table(const boost::optional<int> temperature) const
|
|
{
|
|
const int temp = bool(temperature) ? temperature.get() : _default_temp;
|
|
return at_nearest(_data, temp);
|
|
}
|
|
|
|
std::string _name;
|
|
std::string _serial;
|
|
uint64_t _timestamp;
|
|
|
|
//! The actual gain table
|
|
std::map<int /* temp */, freq_table_map> _data;
|
|
double _ref_gain = 0.0;
|
|
int _default_temp = NORMAL_TEMPERATURE;
|
|
};
|
|
|
|
|
|
pwr_cal::sptr pwr_cal::make()
|
|
{
|
|
return std::make_shared<pwr_cal_impl>();
|
|
}
|
|
|
|
pwr_cal::sptr pwr_cal::make(
|
|
const std::string& name, const std::string& serial, const uint64_t timestamp)
|
|
{
|
|
return std::make_shared<pwr_cal_impl>(name, serial, timestamp);
|
|
}
|
|
|