aboutsummaryrefslogtreecommitdiffstats
path: root/host/lib/cal
diff options
context:
space:
mode:
authorMartin Braun <martin.braun@ettus.com>2020-04-01 16:44:08 -0700
committerAaron Rossetto <aaron.rossetto@ni.com>2020-04-17 07:58:19 -0500
commit1a19bc653ee499545addb2a8718d12069bfd6fcd (patch)
treefa9c0b16432d559163e1e93dde836efd2fe3a1de /host/lib/cal
parent760abdf70315be16943496a1a1375f78660d96d8 (diff)
downloaduhd-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.txt1
-rw-r--r--host/lib/cal/cal_python.hpp46
-rw-r--r--host/lib/cal/pwr_cal.cpp347
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);
+}
+