diff options
-rw-r--r-- | host/include/uhd/cal/CMakeLists.txt | 5 | ||||
-rw-r--r-- | host/include/uhd/cal/pwr_cal.fbs | 46 | ||||
-rw-r--r-- | host/include/uhd/cal/pwr_cal.hpp | 151 | ||||
-rw-r--r-- | host/include/uhd/cal/pwr_cal_generated.h | 340 | ||||
-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 | ||||
-rw-r--r-- | host/python/uhd/usrp/cal/libtypes.py | 1 | ||||
-rw-r--r-- | host/tests/CMakeLists.txt | 1 | ||||
-rw-r--r-- | host/tests/cal_data_gain_pwr_test.cpp | 134 |
10 files changed, 1071 insertions, 1 deletions
diff --git a/host/include/uhd/cal/CMakeLists.txt b/host/include/uhd/cal/CMakeLists.txt index 8d8dfa11e..38b4885ef 100644 --- a/host/include/uhd/cal/CMakeLists.txt +++ b/host/include/uhd/cal/CMakeLists.txt @@ -5,16 +5,19 @@ # UHD_INSTALL(FILES - database.hpp container.hpp + database.hpp iq_cal.hpp + pwr_cal.hpp iq_cal_generated.h + pwr_cal_generated.h DESTINATION ${INCLUDE_DIR}/uhd/cal COMPONENT headers ) UHD_INSTALL(FILES iq_cal.fbs + pwr_cal.fbs DESTINATION ${PKG_DATA_DIR}/cal COMPONENT headers ) diff --git a/host/include/uhd/cal/pwr_cal.fbs b/host/include/uhd/cal/pwr_cal.fbs new file mode 100644 index 000000000..7cf66feee --- /dev/null +++ b/host/include/uhd/cal/pwr_cal.fbs @@ -0,0 +1,46 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +// Calibration Table for power calibration coefficents. +// See also pwr_cal.hpp + +include "cal_metadata.fbs"; + +namespace uhd.usrp.cal; + +// Maps a gain value (dB) to a power value (dBm) +struct PowerMap +{ + gain: double (key); + power_dbm: double; +} + +// For a given frequency, stores all gain -> power measurements, as well as a +// max and min power level the device can reasonably produce. +table FreqPowerMap +{ + freq: ulong (key); + powers: [PowerMap]; + min_power: double; // Minimum power achievable at this frequency + max_power: double; // Maximum power achievable at this frequency +} + +// Given a temperature, store a gain table +table TempFreqMap +{ + temperature: int (key); // Temperature in C + freqs: [FreqPowerMap]; +} + +table PowerCal +{ + metadata: Metadata; + temp_freq_map: [TempFreqMap]; + ref_gain: int = -1; +} + +root_type PowerCal; +file_identifier "dB/m"; // dB per dBm diff --git a/host/include/uhd/cal/pwr_cal.hpp b/host/include/uhd/cal/pwr_cal.hpp new file mode 100644 index 000000000..b30c6dfc2 --- /dev/null +++ b/host/include/uhd/cal/pwr_cal.hpp @@ -0,0 +1,151 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#pragma once + +#include <uhd/cal/container.hpp> +#include <uhd/config.hpp> +#include <uhd/types/ranges.hpp> +#include <boost/optional.hpp> +#include <map> + +namespace uhd { namespace usrp { namespace cal { + +/*! Class that stores power levels (in dBm) at various gains, frequencies, and + * temperatures. + * + * This container class is suitable for all cases where devices want to store a + * gain-to-power mapping from a single, overall gain value. + * + * The underlying data structure stores the power level for every gain/frequency + * point that is part of this data set. It can also do a reverse look-up of gain + * values for a given power value. + * + * The interpolation algorithms assume a monotonic gain/power profile, i.e., + * f(gain) = power is monotonically increasing. If the power is not monotonically + * increasing, the interpolation algorithms still work, but get_gain() might + * produce a greater than necessary interpolation error. For that case, this + * class provides get_gain_coerced(), which helps with both coercion of the + * interpolated gain into a gain value that can be used at the call site, as + * well as minimizing the effect on the application using this container. + * + * All interpolation algorithms first interpolate temperature by finding the + * nearest available temperature data. For example, if the data set includes + * calibration data for 40C and 50C, and the actual temperature is measured at + * 48C, then the data set for 50C is used, and the data set for 40C is not + * considered at all. + * Within a data set, frequency and gain are interpolated in two dimensions (the + * same is true for frequency and power for get_gain() and get_gain_coerced()) + * using a bilinear interpolation. + */ +class UHD_API pwr_cal : public container +{ +public: + using sptr = std::shared_ptr<pwr_cal>; + + /*! Add or update a power level data point + * + * Note: Power measurements can only be written atomically. It is not + * possible to add individual gain/power points using this method. + * + * \param gain_power_map A mapping gain -> power (dB -> dBm) for all + * measured gain points for this frequency. + * \param max_power The maximum available power for this frequency. + * \param min_power The minimum available power for this frequency. + * \param freq The frequency at which this power level was measured + * \param temperature The temperature at which this power level was measured, + * in Celsius. This parameter is optional, the return + * value for get_temperature() is used if no temperature + * is provided here. + */ + virtual void add_power_table(const std::map<double, double>& gain_power_map, + const double max_power, + const double min_power, + const double freq, + const boost::optional<int> temperature = boost::none) = 0; + + /*! Clear all stored values + * + * Note: All of the getters will throw a uhd::assertion_error if called after + * clearing the data. + */ + virtual void clear() = 0; + + /*! Set the current default temperature + * + * Some of the API calls have a optional temperature argument. However, in + * practice, it is often useful to set an ambient temperature once, or only + * when it has changed, and then use a default temperature instead of + * providing the temperature on every call. This sets the default + * temperature. + */ + virtual void set_temperature(const int temperature) = 0; + + //! Return the current default temperature + virtual int get_temperature() const = 0; + + /*! Set the reference gain point, i.e., the gain value where by definition + * the difference between linearized power and measured power is zero. + * + * Currently unused. + */ + virtual void set_ref_gain(const double gain) = 0; + + /*! Return the current reference gain point. + * + * Currently unused. + */ + virtual double get_ref_gain() const = 0; + + /*! Return the min and max power available for this frequency + */ + virtual uhd::meta_range_t get_power_limits(const double freq, + const boost::optional<int> temperature = boost::none) const = 0; + + /*! Returns the power at a gain value. + * + * This will interpolate from the given data set to obtain a power value if + * gain and frequency are not exactly within the given data set. + * + * \param gain The gain at which we are checking the power + * \param freq The frequency at which we are checking the power + * \param temperature The temperature at which we are checking the power. If + * none is given, uses the current default temperature + * (see set_temperature()). + */ + virtual double get_power(const double gain, + const double freq, + const boost::optional<int> temperature = boost::none) const = 0; + + /*! Look up a gain value from a power value. + * + * This is the reverse function to get_power(). Like get_power(), it will + * interpolate in two dimensions (linearly) if the requested power value is + * not part of the measurement data. + * + * Note: \p power_dbm is coerced into the available power range using + * get_power_limits(). + * + * \param power_dbm The power (in dBm) at which we are getting the gain value for + * \param freq The frequency at which we are finding the gain + * \param temperature The temperature at which we are finding the gain. If + * none is given, uses the current default temperature + * (see set_temperature()). + */ + virtual double get_gain(const double power_dbm, + const double freq, + const boost::optional<int> temperature = boost::none) const = 0; + + //! Factory for new cal data sets + static sptr make( + const std::string& name, const std::string& serial, const uint64_t timestamp); + + //! Default factory + static sptr make(); +}; + +}}} // namespace uhd::usrp::cal + diff --git a/host/include/uhd/cal/pwr_cal_generated.h b/host/include/uhd/cal/pwr_cal_generated.h new file mode 100644 index 000000000..f7ea8d8a7 --- /dev/null +++ b/host/include/uhd/cal/pwr_cal_generated.h @@ -0,0 +1,340 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_PWRCAL_UHD_USRP_CAL_H_ +#define FLATBUFFERS_GENERATED_PWRCAL_UHD_USRP_CAL_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "cal_metadata_generated.h" + +namespace uhd { +namespace usrp { +namespace cal { + +struct PowerMap; + +struct FreqPowerMap; +struct FreqPowerMapBuilder; + +struct TempFreqMap; +struct TempFreqMapBuilder; + +struct PowerCal; +struct PowerCalBuilder; + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(8) PowerMap FLATBUFFERS_FINAL_CLASS { + private: + double gain_; + double power_dbm_; + + public: + PowerMap() { + memset(static_cast<void *>(this), 0, sizeof(PowerMap)); + } + PowerMap(double _gain, double _power_dbm) + : gain_(flatbuffers::EndianScalar(_gain)), + power_dbm_(flatbuffers::EndianScalar(_power_dbm)) { + } + double gain() const { + return flatbuffers::EndianScalar(gain_); + } + bool KeyCompareLessThan(const PowerMap *o) const { + return gain() < o->gain(); + } + int KeyCompareWithValue(double val) const { + return static_cast<int>(gain() > val) - static_cast<int>(gain() < val); + } + double power_dbm() const { + return flatbuffers::EndianScalar(power_dbm_); + } +}; +FLATBUFFERS_STRUCT_END(PowerMap, 16); + +struct FreqPowerMap FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef FreqPowerMapBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_FREQ = 4, + VT_POWERS = 6, + VT_MIN_POWER = 8, + VT_MAX_POWER = 10 + }; + uint64_t freq() const { + return GetField<uint64_t>(VT_FREQ, 0); + } + bool KeyCompareLessThan(const FreqPowerMap *o) const { + return freq() < o->freq(); + } + int KeyCompareWithValue(uint64_t val) const { + return static_cast<int>(freq() > val) - static_cast<int>(freq() < val); + } + const flatbuffers::Vector<const uhd::usrp::cal::PowerMap *> *powers() const { + return GetPointer<const flatbuffers::Vector<const uhd::usrp::cal::PowerMap *> *>(VT_POWERS); + } + double min_power() const { + return GetField<double>(VT_MIN_POWER, 0.0); + } + double max_power() const { + return GetField<double>(VT_MAX_POWER, 0.0); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField<uint64_t>(verifier, VT_FREQ) && + VerifyOffset(verifier, VT_POWERS) && + verifier.VerifyVector(powers()) && + VerifyField<double>(verifier, VT_MIN_POWER) && + VerifyField<double>(verifier, VT_MAX_POWER) && + verifier.EndTable(); + } +}; + +struct FreqPowerMapBuilder { + typedef FreqPowerMap Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_freq(uint64_t freq) { + fbb_.AddElement<uint64_t>(FreqPowerMap::VT_FREQ, freq, 0); + } + void add_powers(flatbuffers::Offset<flatbuffers::Vector<const uhd::usrp::cal::PowerMap *>> powers) { + fbb_.AddOffset(FreqPowerMap::VT_POWERS, powers); + } + void add_min_power(double min_power) { + fbb_.AddElement<double>(FreqPowerMap::VT_MIN_POWER, min_power, 0.0); + } + void add_max_power(double max_power) { + fbb_.AddElement<double>(FreqPowerMap::VT_MAX_POWER, max_power, 0.0); + } + explicit FreqPowerMapBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + FreqPowerMapBuilder &operator=(const FreqPowerMapBuilder &); + flatbuffers::Offset<FreqPowerMap> Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset<FreqPowerMap>(end); + return o; + } +}; + +inline flatbuffers::Offset<FreqPowerMap> CreateFreqPowerMap( + flatbuffers::FlatBufferBuilder &_fbb, + uint64_t freq = 0, + flatbuffers::Offset<flatbuffers::Vector<const uhd::usrp::cal::PowerMap *>> powers = 0, + double min_power = 0.0, + double max_power = 0.0) { + FreqPowerMapBuilder builder_(_fbb); + builder_.add_max_power(max_power); + builder_.add_min_power(min_power); + builder_.add_freq(freq); + builder_.add_powers(powers); + return builder_.Finish(); +} + +inline flatbuffers::Offset<FreqPowerMap> CreateFreqPowerMapDirect( + flatbuffers::FlatBufferBuilder &_fbb, + uint64_t freq = 0, + std::vector<uhd::usrp::cal::PowerMap> *powers = nullptr, + double min_power = 0.0, + double max_power = 0.0) { + auto powers__ = powers ? _fbb.CreateVectorOfSortedStructs<uhd::usrp::cal::PowerMap>(powers) : 0; + return uhd::usrp::cal::CreateFreqPowerMap( + _fbb, + freq, + powers__, + min_power, + max_power); +} + +struct TempFreqMap FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef TempFreqMapBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_TEMPERATURE = 4, + VT_FREQS = 6 + }; + int32_t temperature() const { + return GetField<int32_t>(VT_TEMPERATURE, 0); + } + bool KeyCompareLessThan(const TempFreqMap *o) const { + return temperature() < o->temperature(); + } + int KeyCompareWithValue(int32_t val) const { + return static_cast<int>(temperature() > val) - static_cast<int>(temperature() < val); + } + const flatbuffers::Vector<flatbuffers::Offset<uhd::usrp::cal::FreqPowerMap>> *freqs() const { + return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<uhd::usrp::cal::FreqPowerMap>> *>(VT_FREQS); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyField<int32_t>(verifier, VT_TEMPERATURE) && + VerifyOffset(verifier, VT_FREQS) && + verifier.VerifyVector(freqs()) && + verifier.VerifyVectorOfTables(freqs()) && + verifier.EndTable(); + } +}; + +struct TempFreqMapBuilder { + typedef TempFreqMap Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_temperature(int32_t temperature) { + fbb_.AddElement<int32_t>(TempFreqMap::VT_TEMPERATURE, temperature, 0); + } + void add_freqs(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<uhd::usrp::cal::FreqPowerMap>>> freqs) { + fbb_.AddOffset(TempFreqMap::VT_FREQS, freqs); + } + explicit TempFreqMapBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + TempFreqMapBuilder &operator=(const TempFreqMapBuilder &); + flatbuffers::Offset<TempFreqMap> Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset<TempFreqMap>(end); + return o; + } +}; + +inline flatbuffers::Offset<TempFreqMap> CreateTempFreqMap( + flatbuffers::FlatBufferBuilder &_fbb, + int32_t temperature = 0, + flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<uhd::usrp::cal::FreqPowerMap>>> freqs = 0) { + TempFreqMapBuilder builder_(_fbb); + builder_.add_freqs(freqs); + builder_.add_temperature(temperature); + return builder_.Finish(); +} + +inline flatbuffers::Offset<TempFreqMap> CreateTempFreqMapDirect( + flatbuffers::FlatBufferBuilder &_fbb, + int32_t temperature = 0, + std::vector<flatbuffers::Offset<uhd::usrp::cal::FreqPowerMap>> *freqs = nullptr) { + auto freqs__ = freqs ? _fbb.CreateVectorOfSortedTables<uhd::usrp::cal::FreqPowerMap>(freqs) : 0; + return uhd::usrp::cal::CreateTempFreqMap( + _fbb, + temperature, + freqs__); +} + +struct PowerCal FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + typedef PowerCalBuilder Builder; + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_METADATA = 4, + VT_TEMP_FREQ_MAP = 6, + VT_REF_GAIN = 8 + }; + const Metadata *metadata() const { + return GetPointer<const Metadata *>(VT_METADATA); + } + const flatbuffers::Vector<flatbuffers::Offset<uhd::usrp::cal::TempFreqMap>> *temp_freq_map() const { + return GetPointer<const flatbuffers::Vector<flatbuffers::Offset<uhd::usrp::cal::TempFreqMap>> *>(VT_TEMP_FREQ_MAP); + } + int32_t ref_gain() const { + return GetField<int32_t>(VT_REF_GAIN, -1); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_METADATA) && + verifier.VerifyTable(metadata()) && + VerifyOffset(verifier, VT_TEMP_FREQ_MAP) && + verifier.VerifyVector(temp_freq_map()) && + verifier.VerifyVectorOfTables(temp_freq_map()) && + VerifyField<int32_t>(verifier, VT_REF_GAIN) && + verifier.EndTable(); + } +}; + +struct PowerCalBuilder { + typedef PowerCal Table; + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_metadata(flatbuffers::Offset<Metadata> metadata) { + fbb_.AddOffset(PowerCal::VT_METADATA, metadata); + } + void add_temp_freq_map(flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<uhd::usrp::cal::TempFreqMap>>> temp_freq_map) { + fbb_.AddOffset(PowerCal::VT_TEMP_FREQ_MAP, temp_freq_map); + } + void add_ref_gain(int32_t ref_gain) { + fbb_.AddElement<int32_t>(PowerCal::VT_REF_GAIN, ref_gain, -1); + } + explicit PowerCalBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + PowerCalBuilder &operator=(const PowerCalBuilder &); + flatbuffers::Offset<PowerCal> Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset<PowerCal>(end); + return o; + } +}; + +inline flatbuffers::Offset<PowerCal> CreatePowerCal( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset<Metadata> metadata = 0, + flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<uhd::usrp::cal::TempFreqMap>>> temp_freq_map = 0, + int32_t ref_gain = -1) { + PowerCalBuilder builder_(_fbb); + builder_.add_ref_gain(ref_gain); + builder_.add_temp_freq_map(temp_freq_map); + builder_.add_metadata(metadata); + return builder_.Finish(); +} + +inline flatbuffers::Offset<PowerCal> CreatePowerCalDirect( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset<Metadata> metadata = 0, + std::vector<flatbuffers::Offset<uhd::usrp::cal::TempFreqMap>> *temp_freq_map = nullptr, + int32_t ref_gain = -1) { + auto temp_freq_map__ = temp_freq_map ? _fbb.CreateVectorOfSortedTables<uhd::usrp::cal::TempFreqMap>(temp_freq_map) : 0; + return uhd::usrp::cal::CreatePowerCal( + _fbb, + metadata, + temp_freq_map__, + ref_gain); +} + +inline const uhd::usrp::cal::PowerCal *GetPowerCal(const void *buf) { + return flatbuffers::GetRoot<uhd::usrp::cal::PowerCal>(buf); +} + +inline const uhd::usrp::cal::PowerCal *GetSizePrefixedPowerCal(const void *buf) { + return flatbuffers::GetSizePrefixedRoot<uhd::usrp::cal::PowerCal>(buf); +} + +inline const char *PowerCalIdentifier() { + return "dB/m"; +} + +inline bool PowerCalBufferHasIdentifier(const void *buf) { + return flatbuffers::BufferHasIdentifier( + buf, PowerCalIdentifier()); +} + +inline bool VerifyPowerCalBuffer( + flatbuffers::Verifier &verifier) { + return verifier.VerifyBuffer<uhd::usrp::cal::PowerCal>(PowerCalIdentifier()); +} + +inline bool VerifySizePrefixedPowerCalBuffer( + flatbuffers::Verifier &verifier) { + return verifier.VerifySizePrefixedBuffer<uhd::usrp::cal::PowerCal>(PowerCalIdentifier()); +} + +inline void FinishPowerCalBuffer( + flatbuffers::FlatBufferBuilder &fbb, + flatbuffers::Offset<uhd::usrp::cal::PowerCal> root) { + fbb.Finish(root, PowerCalIdentifier()); +} + +inline void FinishSizePrefixedPowerCalBuffer( + flatbuffers::FlatBufferBuilder &fbb, + flatbuffers::Offset<uhd::usrp::cal::PowerCal> root) { + fbb.FinishSizePrefixed(root, PowerCalIdentifier()); +} + +} // namespace cal +} // namespace usrp +} // namespace uhd + +#endif // FLATBUFFERS_GENERATED_PWRCAL_UHD_USRP_CAL_H_ 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); +} + diff --git a/host/python/uhd/usrp/cal/libtypes.py b/host/python/uhd/usrp/cal/libtypes.py index b75165965..1eb9ff360 100644 --- a/host/python/uhd/usrp/cal/libtypes.py +++ b/host/python/uhd/usrp/cal/libtypes.py @@ -17,5 +17,6 @@ from ... import libpyuhd as lib database = lib.cal.database Source = lib.cal.source IQCal = lib.cal.iq_cal +PwrCal = lib.cal.pwr_cal InterpMode = lib.cal.interp_mode # pylint: enable=invalid-name diff --git a/host/tests/CMakeLists.txt b/host/tests/CMakeLists.txt index 4ed94de2c..3723119fa 100644 --- a/host/tests/CMakeLists.txt +++ b/host/tests/CMakeLists.txt @@ -27,6 +27,7 @@ set(test_sources cast_test.cpp cal_database_test.cpp cal_data_iq_test.cpp + cal_data_gain_pwr_test.cpp chdr_test.cpp constrained_device_args_test.cpp convert_test.cpp diff --git a/host/tests/cal_data_gain_pwr_test.cpp b/host/tests/cal_data_gain_pwr_test.cpp new file mode 100644 index 000000000..e066a96f3 --- /dev/null +++ b/host/tests/cal_data_gain_pwr_test.cpp @@ -0,0 +1,134 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include <uhd/cal/pwr_cal.hpp> +#include <uhd/exception.hpp> +#include <boost/test/unit_test.hpp> +#include <iostream> + +using namespace uhd::usrp::cal; + +BOOST_AUTO_TEST_CASE(test_pwr_cal_api) +{ + const std::string name = "Mock Gain/Power Data"; + const std::string serial = "ABC1234"; + const uint64_t timestamp = 0x12340000; + + auto gain_power_data = pwr_cal::make(name, serial, timestamp); + BOOST_CHECK_EQUAL(gain_power_data->get_name(), name); + BOOST_CHECK_EQUAL(gain_power_data->get_serial(), serial); + BOOST_CHECK_EQUAL(gain_power_data->get_timestamp(), timestamp); + + constexpr int NEW_TEMP = 25; + gain_power_data->set_temperature(NEW_TEMP); + BOOST_CHECK_EQUAL(gain_power_data->get_temperature(), NEW_TEMP); + + // Write a table... + gain_power_data->add_power_table({{0.0, -30.0}, {10.0, -20.0}}, -40.0, -10.0, 1e9); + // ...read it back... + BOOST_CHECK_EQUAL(gain_power_data->get_power(0.0, 1e9), -30.0); + BOOST_CHECK_EQUAL(gain_power_data->get_power(10.0, 1e9), -20.0); + // ...and make sure any other index will do the same + BOOST_CHECK_EQUAL(gain_power_data->get_power(47.2, 23e6), -20.0); + + // Let's say power goes down 10 dB per octave + gain_power_data->add_power_table({{0.0, -40.0}, {10.0, -30.0}}, -40.0, -10.0, 2e9); + // Do uninterpolated readbacks again + BOOST_CHECK_EQUAL(gain_power_data->get_power(10.0, 1e9), -20.0); + BOOST_CHECK_EQUAL(gain_power_data->get_power(47.2, 23e6), -20.0); + // Now interpolate + BOOST_CHECK_CLOSE(gain_power_data->get_power(5.0, 1.5e9), -30.0, 1e-6); + + // Some ref gain checks + BOOST_CHECK_EQUAL(gain_power_data->get_ref_gain(), 0.0); + gain_power_data->set_ref_gain(5.0); + BOOST_CHECK_EQUAL(gain_power_data->get_ref_gain(), 5.0); + + // Clear, make sure the container doesn't work anymore, and add the value + // back for future tests + gain_power_data->clear(); + BOOST_REQUIRE_THROW(gain_power_data->get_power(47.2, 23e6), uhd::exception); + gain_power_data->add_power_table({{10.0, -20.0}}, -20.0, -20.0, 1e9); + + constexpr int ROOM_TEMP = 20; + gain_power_data->set_temperature(ROOM_TEMP); + BOOST_CHECK_EQUAL(gain_power_data->get_temperature(), ROOM_TEMP); + // Shouldn't change anything yet + BOOST_CHECK_EQUAL(gain_power_data->get_power(10.0, 1e9), -20.0); + // Power shall go down 5 dB at this new temperature (whoa) + gain_power_data->add_power_table({{10.0, -25.0}}, -20.0, -20.0, 1e9); + BOOST_CHECK_EQUAL(gain_power_data->get_power(10.0, 1e9), -25.0); + // And if we have to interpolate, temp compensation uses NN: + BOOST_CHECK_EQUAL(gain_power_data->get_power(10.0, 1e9, 21), -25.0); + + // Now clear, then add a more useful data set. We can stay at ROOM_TEMP. + gain_power_data->clear(); + constexpr double power_offset = -20.0; // 0 dB shall map to -20 dBm + constexpr double lin_error = 0.1; // The linearization error shall increase by 0.1 + // dB per dB gain + constexpr double ref_freq = 1e9; + std::map<double, double> test_gain_power; + constexpr double max_gain = 10.0; + for (double gain = 0.0; gain <= max_gain; gain += 1.0) { + test_gain_power[gain] = gain + power_offset + lin_error * gain; + } + const double min_power = test_gain_power.cbegin()->second; + const double max_power = test_gain_power.crbegin()->second; + gain_power_data->add_power_table(test_gain_power, min_power, max_power, ref_freq); + // Quick check + BOOST_CHECK_EQUAL(gain_power_data->get_power(0.0, ref_freq), power_offset); + const std::pair<double, double> expected_limits{min_power, max_power}; + const auto limits = gain_power_data->get_power_limits(ref_freq); + BOOST_CHECK_EQUAL(limits.start(), min_power); + BOOST_CHECK_EQUAL(limits.stop(), max_power); + + BOOST_CHECK_CLOSE(gain_power_data->get_gain(-20.0, ref_freq), 0.0, 1e-6); + BOOST_CHECK_CLOSE( + gain_power_data->get_gain(test_gain_power.crbegin()->second, ref_freq), + test_gain_power.crbegin()->first, + 1e-6); + BOOST_CHECK_CLOSE(gain_power_data->get_gain(-19.0, ref_freq), 1/1.1, 1e-6); +} + +BOOST_AUTO_TEST_CASE(test_pwr_cal_serdes) +{ + const std::string name = "Mock Gain/Power Data"; + const std::string serial = "ABC1234"; + const uint64_t timestamp = 0x12340000; + auto gain_power_data_blueprint = pwr_cal::make(name, serial, timestamp); + + constexpr double power_offset = -20.0; + constexpr double lin_error = 0.1; + constexpr double ref_freq = 1e9; + std::map<double, double> test_gain_power; + for (double gain = 0.0; gain < 10.0; gain += 1.0) { + test_gain_power[gain] = gain + power_offset + lin_error * gain; + } + const double min_power = test_gain_power[0.0]; + const double max_power = test_gain_power[9.0]; + gain_power_data_blueprint->add_power_table( + test_gain_power, min_power, max_power, ref_freq); + + const auto serialized = gain_power_data_blueprint->serialize(); + auto pwr_cal_data = container::make<pwr_cal>(serialized); + + BOOST_CHECK_EQUAL(pwr_cal_data->get_name(), name); + BOOST_CHECK_EQUAL(pwr_cal_data->get_serial(), serial); + BOOST_CHECK_EQUAL(pwr_cal_data->get_timestamp(), timestamp); + + for (auto& gp : test_gain_power) { + BOOST_CHECK_EQUAL( + pwr_cal_data->get_power(gp.first, ref_freq), + gp.second); + } +} + +BOOST_AUTO_TEST_CASE(test_pwr_cal_des_fail) +{ + std::vector<uint8_t> not_actual_data(42, 23); + + BOOST_REQUIRE_THROW(container::make<pwr_cal>(not_actual_data), uhd::runtime_error); +} |