diff options
author | Martin Braun <martin.braun@ettus.com> | 2020-04-01 16:44:08 -0700 |
---|---|---|
committer | Aaron Rossetto <aaron.rossetto@ni.com> | 2020-04-17 07:58:19 -0500 |
commit | 1a19bc653ee499545addb2a8718d12069bfd6fcd (patch) | |
tree | fa9c0b16432d559163e1e93dde836efd2fe3a1de /host/lib/cal | |
parent | 760abdf70315be16943496a1a1375f78660d96d8 (diff) | |
download | uhd-1a19bc653ee499545addb2a8718d12069bfd6fcd.tar.gz uhd-1a19bc653ee499545addb2a8718d12069bfd6fcd.tar.bz2 uhd-1a19bc653ee499545addb2a8718d12069bfd6fcd.zip |
cal: Add pwr_cal container
This is a cal container for all types of power cal (RX or TX) that rely
on a single, overall gain value.
Includes Python API.
Diffstat (limited to 'host/lib/cal')
-rw-r--r-- | host/lib/cal/CMakeLists.txt | 1 | ||||
-rw-r--r-- | host/lib/cal/cal_python.hpp | 46 | ||||
-rw-r--r-- | host/lib/cal/pwr_cal.cpp | 347 |
3 files changed, 394 insertions, 0 deletions
diff --git a/host/lib/cal/CMakeLists.txt b/host/lib/cal/CMakeLists.txt index fb1f4b0e3..a50d654ff 100644 --- a/host/lib/cal/CMakeLists.txt +++ b/host/lib/cal/CMakeLists.txt @@ -11,5 +11,6 @@ include_directories(${CMAKE_SOURCE_DIR}/lib/deps/flatbuffers/include) LIBUHD_APPEND_SOURCES( ${CMAKE_CURRENT_SOURCE_DIR}/database.cpp ${CMAKE_CURRENT_SOURCE_DIR}/iq_cal.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/pwr_cal.cpp ) diff --git a/host/lib/cal/cal_python.hpp b/host/lib/cal/cal_python.hpp index e8f19eef9..5de9e8288 100644 --- a/host/lib/cal/cal_python.hpp +++ b/host/lib/cal/cal_python.hpp @@ -9,7 +9,16 @@ #include <uhd/cal/database.hpp> #include <uhd/cal/iq_cal.hpp> +#include <uhd/cal/pwr_cal.hpp> #include <uhd/utils/interpolation.hpp> +#include <pybind11/stl.h> + +namespace pybind11 { namespace detail { +template <typename T> +struct type_caster<boost::optional<T>> : optional_caster<boost::optional<T>> +{ +}; +}} std::vector<uint8_t> pybytes_to_vector(const py::bytes& data) { @@ -94,6 +103,43 @@ void export_cal(py::module& m) py::arg("suppression_abs") = 0, py::arg("suppression_delta") = 0) .def("clear", &iq_cal::clear); + + py::class_<pwr_cal, container, pwr_cal::sptr>(m, "pwr_cal") + .def(py::init([](const std::string& name, + const std::string& serial, + const uint64_t timestamp) { + return pwr_cal::make(name, serial, timestamp); + })) + .def(py::init([]() { return pwr_cal::make(); })) + .def(py::init([](const py::bytes data) { + return container::make<pwr_cal>(pybytes_to_vector(data)); + })) + .def("add_power_table", + &pwr_cal::add_power_table, + py::arg("gain_power_map"), + py::arg("max_power"), + py::arg("min_power"), + py::arg("freq"), + py::arg("temperature") = boost::optional<int>()) + .def("clear", &pwr_cal::clear) + .def("set_temperature", &pwr_cal::set_temperature) + .def("get_temperature", &pwr_cal::get_temperature) + .def("set_ref_gain", &pwr_cal::set_ref_gain) + .def("get_ref_gain", &pwr_cal::get_ref_gain) + .def("get_power_limits", + &pwr_cal::get_power_limits, + py::arg("freq"), + py::arg("temperature") = boost::optional<int>()) + .def("get_power", + &pwr_cal::get_power, + py::arg("gain"), + py::arg("freq"), + py::arg("temperature") = boost::optional<int>()) + .def("get_gain", + &pwr_cal::get_gain, + py::arg("power_dbm"), + py::arg("freq"), + py::arg("temperature") = boost::optional<int>()); } #endif /* INCLUDED_UHD_CAL_PYTHON_HPP */ diff --git a/host/lib/cal/pwr_cal.cpp b/host/lib/cal/pwr_cal.cpp new file mode 100644 index 000000000..3ff199598 --- /dev/null +++ b/host/lib/cal/pwr_cal.cpp @@ -0,0 +1,347 @@ +// +// 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; + // Frequency is out of bounds + if (f1i == f2i) { + return at_lin_interp(table.at(f1i).p2g, power_coerced); + } + const double f1 = static_cast<double>(f1i); + const double f2 = static_cast<double>(f2i); + const auto pwr_iters = get_bounding_iterators(table.at(f1).p2g, power_coerced); + const double pwr1 = pwr_iters.first->first; + const double pwr2 = pwr_iters.second->first; + // 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, + table.at(f1i).p2g.at(pwr1), + f2, + table.at(f2i).p2g.at(pwr1)); + } + + // Both gain and freq are within bounds: Bi-Linear interpolation + // Find power values + const auto gain11 = table.at(f1i).p2g.at(pwr1); + const auto gain12 = table.at(f1i).p2g.at(pwr2); + const auto gain21 = table.at(f2i).p2g.at(pwr1); + const auto gain22 = table.at(f2i).p2g.at(pwr2); + + 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); +} + |