diff options
author | Martin Braun <martin.braun@ettus.com> | 2020-03-03 16:10:06 -0800 |
---|---|---|
committer | Aaron Rossetto <aaron.rossetto@ni.com> | 2020-04-02 11:55:17 -0500 |
commit | 3fe5ccf700a0c6f27dca9511386460194dcc593c (patch) | |
tree | 19d394faac0581601d3d6e5d35a7a82e325bf245 | |
parent | 1e016e49e82c430197a90a018fbc7613cc654088 (diff) | |
download | uhd-3fe5ccf700a0c6f27dca9511386460194dcc593c.tar.gz uhd-3fe5ccf700a0c6f27dca9511386460194dcc593c.tar.bz2 uhd-3fe5ccf700a0c6f27dca9511386460194dcc593c.zip |
uhd: cal: Add iq_cal calibration data container class
This class can be used to store calibration coefficients for the X300
DC offset and IQ imbalance calibration.
Note: This also modifies Doxyfile.in to not document files generated by
flatc.
-rw-r--r-- | host/docs/Doxyfile.in | 2 | ||||
-rw-r--r-- | host/include/uhd/cal/CMakeLists.txt | 8 | ||||
-rw-r--r-- | host/include/uhd/cal/cal_metadata.fbs | 15 | ||||
-rw-r--r-- | host/include/uhd/cal/cal_metadata_generated.h | 111 | ||||
-rw-r--r-- | host/include/uhd/cal/container.hpp | 9 | ||||
-rw-r--r-- | host/include/uhd/cal/iq_cal.fbs | 32 | ||||
-rw-r--r-- | host/include/uhd/cal/iq_cal.hpp | 78 | ||||
-rw-r--r-- | host/include/uhd/cal/iq_cal_generated.h | 162 | ||||
-rw-r--r-- | host/lib/cal/CMakeLists.txt | 2 | ||||
-rw-r--r-- | host/lib/cal/cal_python.hpp | 38 | ||||
-rw-r--r-- | host/lib/cal/iq_cal.cpp | 197 | ||||
-rw-r--r-- | host/python/pyuhd.cpp | 5 | ||||
-rw-r--r-- | host/python/uhd/usrp/__init__.py | 2 | ||||
-rw-r--r-- | host/python/uhd/usrp/cal/__init__.py | 16 | ||||
-rw-r--r-- | host/python/uhd/usrp/cal/libtypes.py | 2 | ||||
-rw-r--r-- | host/tests/CMakeLists.txt | 1 | ||||
-rw-r--r-- | host/tests/cal_data_iq_test.cpp | 81 |
17 files changed, 760 insertions, 1 deletions
diff --git a/host/docs/Doxyfile.in b/host/docs/Doxyfile.in index 36ae2208c..45b65b399 100644 --- a/host/docs/Doxyfile.in +++ b/host/docs/Doxyfile.in @@ -702,7 +702,7 @@ EXCLUDE_SYMLINKS = NO # against the file with absolute path, so to exclude all test directories # for example use the pattern */test/* -EXCLUDE_PATTERNS = +EXCLUDE_PATTERNS = */*_generated.h # The EXCLUDE_SYMBOLS tag can be used to specify one or more symbol names # (namespaces, classes, functions, etc.) that should be excluded from the diff --git a/host/include/uhd/cal/CMakeLists.txt b/host/include/uhd/cal/CMakeLists.txt index b3f724d2e..2aba1a91c 100644 --- a/host/include/uhd/cal/CMakeLists.txt +++ b/host/include/uhd/cal/CMakeLists.txt @@ -8,7 +8,15 @@ UHD_INSTALL(FILES database.hpp container.hpp interpolation.hpp + iq_cal.hpp + iq_cal_generated.h DESTINATION ${INCLUDE_DIR}/uhd/cal COMPONENT headers ) +UHD_INSTALL(FILES + iq_cal.fbs + DESTINATION ${PKG_DATA_DIR}/cal + COMPONENT headers +) + diff --git a/host/include/uhd/cal/cal_metadata.fbs b/host/include/uhd/cal/cal_metadata.fbs new file mode 100644 index 000000000..ce380c141 --- /dev/null +++ b/host/include/uhd/cal/cal_metadata.fbs @@ -0,0 +1,15 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +// Common metadata for all calibration containers +table Metadata +{ + name: string; // Can be given arbitrary, based on calibration + serial: string; // Device serial + timestamp: uint64; // Unix timestamp + version_major: int; // Cal data version (major) + version_minor: int; // Cal data version (minor) +} diff --git a/host/include/uhd/cal/cal_metadata_generated.h b/host/include/uhd/cal/cal_metadata_generated.h new file mode 100644 index 000000000..3b4a6a8e7 --- /dev/null +++ b/host/include/uhd/cal/cal_metadata_generated.h @@ -0,0 +1,111 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_CALMETADATA_H_ +#define FLATBUFFERS_GENERATED_CALMETADATA_H_ + +#include "flatbuffers/flatbuffers.h" + +struct Metadata; + +struct Metadata FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_NAME = 4, + VT_SERIAL = 6, + VT_TIMESTAMP = 8, + VT_VERSION_MAJOR = 10, + VT_VERSION_MINOR = 12 + }; + const flatbuffers::String *name() const { + return GetPointer<const flatbuffers::String *>(VT_NAME); + } + const flatbuffers::String *serial() const { + return GetPointer<const flatbuffers::String *>(VT_SERIAL); + } + uint64_t timestamp() const { + return GetField<uint64_t>(VT_TIMESTAMP, 0); + } + int32_t version_major() const { + return GetField<int32_t>(VT_VERSION_MAJOR, 0); + } + int32_t version_minor() const { + return GetField<int32_t>(VT_VERSION_MINOR, 0); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_NAME) && + verifier.VerifyString(name()) && + VerifyOffset(verifier, VT_SERIAL) && + verifier.VerifyString(serial()) && + VerifyField<uint64_t>(verifier, VT_TIMESTAMP) && + VerifyField<int32_t>(verifier, VT_VERSION_MAJOR) && + VerifyField<int32_t>(verifier, VT_VERSION_MINOR) && + verifier.EndTable(); + } +}; + +struct MetadataBuilder { + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_name(flatbuffers::Offset<flatbuffers::String> name) { + fbb_.AddOffset(Metadata::VT_NAME, name); + } + void add_serial(flatbuffers::Offset<flatbuffers::String> serial) { + fbb_.AddOffset(Metadata::VT_SERIAL, serial); + } + void add_timestamp(uint64_t timestamp) { + fbb_.AddElement<uint64_t>(Metadata::VT_TIMESTAMP, timestamp, 0); + } + void add_version_major(int32_t version_major) { + fbb_.AddElement<int32_t>(Metadata::VT_VERSION_MAJOR, version_major, 0); + } + void add_version_minor(int32_t version_minor) { + fbb_.AddElement<int32_t>(Metadata::VT_VERSION_MINOR, version_minor, 0); + } + explicit MetadataBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + MetadataBuilder &operator=(const MetadataBuilder &); + flatbuffers::Offset<Metadata> Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset<Metadata>(end); + return o; + } +}; + +inline flatbuffers::Offset<Metadata> CreateMetadata( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset<flatbuffers::String> name = 0, + flatbuffers::Offset<flatbuffers::String> serial = 0, + uint64_t timestamp = 0, + int32_t version_major = 0, + int32_t version_minor = 0) { + MetadataBuilder builder_(_fbb); + builder_.add_timestamp(timestamp); + builder_.add_version_minor(version_minor); + builder_.add_version_major(version_major); + builder_.add_serial(serial); + builder_.add_name(name); + return builder_.Finish(); +} + +inline flatbuffers::Offset<Metadata> CreateMetadataDirect( + flatbuffers::FlatBufferBuilder &_fbb, + const char *name = nullptr, + const char *serial = nullptr, + uint64_t timestamp = 0, + int32_t version_major = 0, + int32_t version_minor = 0) { + auto name__ = name ? _fbb.CreateString(name) : 0; + auto serial__ = serial ? _fbb.CreateString(serial) : 0; + return CreateMetadata( + _fbb, + name__, + serial__, + timestamp, + version_major, + version_minor); +} + +#endif // FLATBUFFERS_GENERATED_CALMETADATA_H_ diff --git a/host/include/uhd/cal/container.hpp b/host/include/uhd/cal/container.hpp index c3035c610..14f28d5ff 100644 --- a/host/include/uhd/cal/container.hpp +++ b/host/include/uhd/cal/container.hpp @@ -25,6 +25,15 @@ class UHD_API container public: virtual ~container() = default; + //! Return the name of this calibration table + virtual std::string get_name() const = 0; + + //! Return the device serial of this calibration table + virtual std::string get_serial() const = 0; + + //! Timestamp of acquisition time + virtual uint64_t get_timestamp() const = 0; + //! Return a serialized version of this container virtual std::vector<uint8_t> serialize() = 0; diff --git a/host/include/uhd/cal/iq_cal.fbs b/host/include/uhd/cal/iq_cal.fbs new file mode 100644 index 000000000..55c9ae3de --- /dev/null +++ b/host/include/uhd/cal/iq_cal.fbs @@ -0,0 +1,32 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +// Calibration Table for I/Q Balance correction coefficients +// This is a very simple calibration coefficient table, and +// works for Gen-3 cals (IQ balance and DC offset) which have +// a single real/imag pair of coefficients per frequency. + +include "cal_metadata.fbs"; + +namespace uhd.usrp.cal; + +struct IQCalCoeff +{ + freq: double; + coeff_real: double; + coeff_imag: double; + suppression_abs: double; + suppression_delta: double; +} + +table IQCalCoeffs +{ + metadata: Metadata; + coeffs: [IQCalCoeff]; +} + +root_type IQCalCoeffs; +file_identifier "IQ/f"; // I/Q data per frequency diff --git a/host/include/uhd/cal/iq_cal.hpp b/host/include/uhd/cal/iq_cal.hpp new file mode 100644 index 000000000..ded082698 --- /dev/null +++ b/host/include/uhd/cal/iq_cal.hpp @@ -0,0 +1,78 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#ifndef INCLUDED_LIBUHD_CAL_IQ_DATA_HPP +#define INCLUDED_LIBUHD_CAL_IQ_DATA_HPP + +#include <uhd/cal/container.hpp> +#include <uhd/cal/interpolation.hpp> +#include <uhd/config.hpp> +#include <complex> +#include <memory> +#include <string> + +namespace uhd { namespace usrp { namespace cal { + +/*! Class that stores IQ cal data per frequency + * + * The following calibrations use this: + * - Gen-2 and Gen-3 TX DC Offset + * - Gen-2 and Gen-3 RX,TX IQ Imbalance + */ +class UHD_API iq_cal : public container +{ +public: + using sptr = std::shared_ptr<iq_cal>; + + //! Choose interpolation mode + // + // This class supports two kinds of interpolation: Nearest-neighbour, and + // linear. + // + // \param interp The new interpolation mode + // \throws uhd::value_error if the given interpolation mode is not + // supported. + virtual void set_interp_mode(const interp_mode interp) = 0; + + //! Return a calibration coefficient for a given frequency + // + // This function will interpolate to return a valid coefficient for any + // given frequency. + virtual std::complex<double> get_cal_coeff(const double freq) const = 0; + + //! Update / set a calbration coefficient + // + // This usually only needs to called by calibration utilities. + // + // \param freq The frequency at which this coefficient is measured + // \param coeff The value that is stored + // \param suppression_abs The amount of impairment suppression this + // coefficient provides, in dB. + // \param suppression_delta The difference of impairment power between + // applying this coefficient and applying none, in + // dB. + virtual void set_cal_coeff(const double freq, + const std::complex<double> coeff, + const double suppression_abs = 0, + const double suppression_delta = 0) = 0; + + //! Clear the list of coefficients + // + // This can be useful in order to drop existing cal data, and load an + // entirely new set with deserialize(). + virtual void clear() = 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 + +#endif /* INCLUDED_LIBUHD_CAL_IQ_DATA_HPP */ diff --git a/host/include/uhd/cal/iq_cal_generated.h b/host/include/uhd/cal/iq_cal_generated.h new file mode 100644 index 000000000..f0dfa0b82 --- /dev/null +++ b/host/include/uhd/cal/iq_cal_generated.h @@ -0,0 +1,162 @@ +// automatically generated by the FlatBuffers compiler, do not modify + + +#ifndef FLATBUFFERS_GENERATED_IQCAL_UHD_USRP_CAL_H_ +#define FLATBUFFERS_GENERATED_IQCAL_UHD_USRP_CAL_H_ + +#include "flatbuffers/flatbuffers.h" + +#include "cal_metadata_generated.h" + +namespace uhd { +namespace usrp { +namespace cal { + +struct IQCalCoeff; + +struct IQCalCoeffs; + +FLATBUFFERS_MANUALLY_ALIGNED_STRUCT(8) IQCalCoeff FLATBUFFERS_FINAL_CLASS { + private: + double freq_; + double coeff_real_; + double coeff_imag_; + double suppression_abs_; + double suppression_delta_; + + public: + IQCalCoeff() { + memset(static_cast<void *>(this), 0, sizeof(IQCalCoeff)); + } + IQCalCoeff(double _freq, double _coeff_real, double _coeff_imag, double _suppression_abs, double _suppression_delta) + : freq_(flatbuffers::EndianScalar(_freq)), + coeff_real_(flatbuffers::EndianScalar(_coeff_real)), + coeff_imag_(flatbuffers::EndianScalar(_coeff_imag)), + suppression_abs_(flatbuffers::EndianScalar(_suppression_abs)), + suppression_delta_(flatbuffers::EndianScalar(_suppression_delta)) { + } + double freq() const { + return flatbuffers::EndianScalar(freq_); + } + double coeff_real() const { + return flatbuffers::EndianScalar(coeff_real_); + } + double coeff_imag() const { + return flatbuffers::EndianScalar(coeff_imag_); + } + double suppression_abs() const { + return flatbuffers::EndianScalar(suppression_abs_); + } + double suppression_delta() const { + return flatbuffers::EndianScalar(suppression_delta_); + } +}; +FLATBUFFERS_STRUCT_END(IQCalCoeff, 40); + +struct IQCalCoeffs FLATBUFFERS_FINAL_CLASS : private flatbuffers::Table { + enum FlatBuffersVTableOffset FLATBUFFERS_VTABLE_UNDERLYING_TYPE { + VT_METADATA = 4, + VT_COEFFS = 6 + }; + const Metadata *metadata() const { + return GetPointer<const Metadata *>(VT_METADATA); + } + const flatbuffers::Vector<const IQCalCoeff *> *coeffs() const { + return GetPointer<const flatbuffers::Vector<const IQCalCoeff *> *>(VT_COEFFS); + } + bool Verify(flatbuffers::Verifier &verifier) const { + return VerifyTableStart(verifier) && + VerifyOffset(verifier, VT_METADATA) && + verifier.VerifyTable(metadata()) && + VerifyOffset(verifier, VT_COEFFS) && + verifier.VerifyVector(coeffs()) && + verifier.EndTable(); + } +}; + +struct IQCalCoeffsBuilder { + flatbuffers::FlatBufferBuilder &fbb_; + flatbuffers::uoffset_t start_; + void add_metadata(flatbuffers::Offset<Metadata> metadata) { + fbb_.AddOffset(IQCalCoeffs::VT_METADATA, metadata); + } + void add_coeffs(flatbuffers::Offset<flatbuffers::Vector<const IQCalCoeff *>> coeffs) { + fbb_.AddOffset(IQCalCoeffs::VT_COEFFS, coeffs); + } + explicit IQCalCoeffsBuilder(flatbuffers::FlatBufferBuilder &_fbb) + : fbb_(_fbb) { + start_ = fbb_.StartTable(); + } + IQCalCoeffsBuilder &operator=(const IQCalCoeffsBuilder &); + flatbuffers::Offset<IQCalCoeffs> Finish() { + const auto end = fbb_.EndTable(start_); + auto o = flatbuffers::Offset<IQCalCoeffs>(end); + return o; + } +}; + +inline flatbuffers::Offset<IQCalCoeffs> CreateIQCalCoeffs( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset<Metadata> metadata = 0, + flatbuffers::Offset<flatbuffers::Vector<const IQCalCoeff *>> coeffs = 0) { + IQCalCoeffsBuilder builder_(_fbb); + builder_.add_coeffs(coeffs); + builder_.add_metadata(metadata); + return builder_.Finish(); +} + +inline flatbuffers::Offset<IQCalCoeffs> CreateIQCalCoeffsDirect( + flatbuffers::FlatBufferBuilder &_fbb, + flatbuffers::Offset<Metadata> metadata = 0, + const std::vector<IQCalCoeff> *coeffs = nullptr) { + auto coeffs__ = coeffs ? _fbb.CreateVectorOfStructs<IQCalCoeff>(*coeffs) : 0; + return uhd::usrp::cal::CreateIQCalCoeffs( + _fbb, + metadata, + coeffs__); +} + +inline const uhd::usrp::cal::IQCalCoeffs *GetIQCalCoeffs(const void *buf) { + return flatbuffers::GetRoot<uhd::usrp::cal::IQCalCoeffs>(buf); +} + +inline const uhd::usrp::cal::IQCalCoeffs *GetSizePrefixedIQCalCoeffs(const void *buf) { + return flatbuffers::GetSizePrefixedRoot<uhd::usrp::cal::IQCalCoeffs>(buf); +} + +inline const char *IQCalCoeffsIdentifier() { + return "IQ/f"; +} + +inline bool IQCalCoeffsBufferHasIdentifier(const void *buf) { + return flatbuffers::BufferHasIdentifier( + buf, IQCalCoeffsIdentifier()); +} + +inline bool VerifyIQCalCoeffsBuffer( + flatbuffers::Verifier &verifier) { + return verifier.VerifyBuffer<uhd::usrp::cal::IQCalCoeffs>(IQCalCoeffsIdentifier()); +} + +inline bool VerifySizePrefixedIQCalCoeffsBuffer( + flatbuffers::Verifier &verifier) { + return verifier.VerifySizePrefixedBuffer<uhd::usrp::cal::IQCalCoeffs>(IQCalCoeffsIdentifier()); +} + +inline void FinishIQCalCoeffsBuffer( + flatbuffers::FlatBufferBuilder &fbb, + flatbuffers::Offset<uhd::usrp::cal::IQCalCoeffs> root) { + fbb.Finish(root, IQCalCoeffsIdentifier()); +} + +inline void FinishSizePrefixedIQCalCoeffsBuffer( + flatbuffers::FlatBufferBuilder &fbb, + flatbuffers::Offset<uhd::usrp::cal::IQCalCoeffs> root) { + fbb.FinishSizePrefixed(root, IQCalCoeffsIdentifier()); +} + +} // namespace cal +} // namespace usrp +} // namespace uhd + +#endif // FLATBUFFERS_GENERATED_IQCAL_UHD_USRP_CAL_H_ diff --git a/host/lib/cal/CMakeLists.txt b/host/lib/cal/CMakeLists.txt index 5c2c8a617..fb1f4b0e3 100644 --- a/host/lib/cal/CMakeLists.txt +++ b/host/lib/cal/CMakeLists.txt @@ -7,7 +7,9 @@ ######################################################################## # This file included, use CMake directory variables ######################################################################## +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 ) diff --git a/host/lib/cal/cal_python.hpp b/host/lib/cal/cal_python.hpp index 0fe87046f..39a13d94c 100644 --- a/host/lib/cal/cal_python.hpp +++ b/host/lib/cal/cal_python.hpp @@ -8,6 +8,8 @@ #define INCLUDED_UHD_CAL_PYTHON_HPP #include <uhd/cal/database.hpp> +#include <uhd/cal/interpolation.hpp> +#include <uhd/cal/iq_cal.hpp> std::vector<uint8_t> pybytes_to_vector(const py::bytes& data) { @@ -56,6 +58,42 @@ void export_cal(py::module& m) [](const std::string& key, const std::string& serial, const py::bytes data) { database::write_cal_data(key, serial, pybytes_to_vector(data)); }); + + py::enum_<interp_mode>(m, "interp_mode") + .value("NEAREST_NEIGHBOR", interp_mode::NEAREST_NEIGHBOR) + .value("LINEAR", interp_mode::LINEAR); + + py::class_<container, std::shared_ptr<container>>(m, "container") + .def("get_name", &container::get_name) + .def("get_serial", &container::get_serial) + .def("get_timestamp", &container::get_name) + .def("serialize", + [](std::shared_ptr<container>& self) { + return vector_to_pybytes(self->serialize()); + }) + .def("deserialize", [](std::shared_ptr<container>& self, const py::bytes data) { + self->deserialize(pybytes_to_vector(data)); + }); + + py::class_<iq_cal, container, iq_cal::sptr>(m, "iq_cal") + .def(py::init([](const std::string& name, + const std::string& serial, + const uint64_t timestamp) { + return iq_cal::make(name, serial, timestamp); + })) + .def(py::init([]() { return iq_cal::make(); })) + .def(py::init([](const py::bytes data) { + return container::make<iq_cal>(pybytes_to_vector(data)); + })) + .def("set_interp_mode", &iq_cal::set_interp_mode) + .def("get_cal_coeff", &iq_cal::get_cal_coeff) + .def("set_cal_coeff", + &iq_cal::set_cal_coeff, + py::arg("freq"), + py::arg("coeff"), + py::arg("suppression_abs") = 0, + py::arg("suppression_delta") = 0) + .def("clear", &iq_cal::clear); } #endif /* INCLUDED_UHD_CAL_PYTHON_HPP */ diff --git a/host/lib/cal/iq_cal.cpp b/host/lib/cal/iq_cal.cpp new file mode 100644 index 000000000..e1ed8c9cb --- /dev/null +++ b/host/lib/cal/iq_cal.cpp @@ -0,0 +1,197 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include <uhd/cal/iq_cal.hpp> +#include <uhd/cal/iq_cal_generated.h> +#include <uhd/exception.hpp> +#include <uhd/utils/math.hpp> +#include <map> +#include <string> + +using namespace uhd::usrp::cal; + +constexpr int VERSION_MAJOR = 1; +constexpr int VERSION_MINOR = 0; + +/*********************************************************************** + * Helper routines + **********************************************************************/ +class iq_cal_impl : public iq_cal +{ +public: + iq_cal_impl(const std::string& name = "", + const std::string& serial = "", + const uint64_t timestamp = 0) + : _name(name) + , _serial(serial) + , _timestamp(timestamp) + , _interp(interp_mode::LINEAR) + { + } + + std::string get_name() const + { + return _name; + } + + //! Return the name of this calibration table + std::string get_serial() const + { + return _serial; + } + + //! Timestamp of acquisition time + uint64_t get_timestamp() const + { + return _timestamp; + } + + void set_interp_mode(const interp_mode interp) + { + UHD_ASSERT_THROW( + interp == interp_mode::LINEAR || interp == interp_mode::NEAREST_NEIGHBOR); + _interp = interp; + } + + std::complex<double> get_cal_coeff(const double freq) const + { + UHD_ASSERT_THROW(!_coeffs.empty()); + // Find the first coefficient in the map that maps to a larger frequency + // than freq (or equal) + auto next_coeff = _coeffs.lower_bound(freq); + if (next_coeff == _coeffs.end()) { + // This means freq is larger than our biggest key, and thus we + // can't interpolate. We return the coeffs of the largest key. + return _coeffs.rbegin()->second; + } + if (next_coeff == _coeffs.begin()) { + // This means freq is smaller than our smallest key, and thus we + // can't interpolate. We return the coeffs of the smallest key. + return _coeffs.begin()->second; + } + // Stash away freqs and coeffs for easier code + const auto hi_freq = next_coeff->first; + const auto hi_coeff = next_coeff->second; + next_coeff--; + const auto lo_coeff = next_coeff->second; + const auto lo_freq = next_coeff->first; // lo == low, not LO + // Now, we're guaranteed to be between two points + if (_interp == interp_mode::NEAREST_NEIGHBOR) { + return (hi_freq - freq) < (freq - lo_freq) ? hi_coeff : lo_coeff; + } + using uhd::math::linear_interp; + return std::complex<double>( + linear_interp<double>( + freq, lo_freq, lo_coeff.real(), hi_freq, hi_coeff.real()), + linear_interp<double>( + freq, lo_freq, lo_coeff.imag(), hi_freq, hi_coeff.imag())); + } + + void set_cal_coeff(const double freq, + const std::complex<double> coeff, + const double suppression_abs = 0, + const double suppression_delta = 0) + { + _coeffs[freq] = coeff; + _supp[freq] = {suppression_abs, suppression_delta}; + } + + void clear() + { + _coeffs.clear(); + _supp.clear(); + } + + /************************************************************************** + * Container API (Serialization/Deserialization) + *************************************************************************/ + std::vector<uint8_t> serialize() + { + // This is a magic value to estimate the amount of space the builder will + // have to reserve on top of the coeff data. + // Worst case is we get this too low, and the builder will have to do a + // single reallocation later. + constexpr size_t RESERVE_HDR_BYTES = 20; + const size_t initial_size_bytes = + sizeof(IQCalCoeff) * _coeffs.size() + RESERVE_HDR_BYTES; + flatbuffers::FlatBufferBuilder builder(initial_size_bytes); + // Convert the coefficients to a vector of IQCalCoeff + std::vector<IQCalCoeff> fb_coeffs; + fb_coeffs.reserve(_coeffs.size()); + std::transform(_coeffs.cbegin(), + _coeffs.cend(), + std::back_inserter(fb_coeffs), + [&](const coeffs_type::value_type& coeff) { + const double freq = coeff.first; + const auto this_coeff = coeff.second; + return IQCalCoeff(freq, + this_coeff.real(), + this_coeff.imag(), + _supp.count(freq) ? _supp.at(freq).first : 0.0, + _supp.count(freq) ? _supp.at(freq).second : 0.0); + }); + // Now load it all into the FlatBuffer + const auto metadata = CreateMetadataDirect(builder, + _name.c_str(), + _serial.c_str(), + _timestamp, + VERSION_MAJOR, + VERSION_MINOR); + auto cal_table = CreateIQCalCoeffsDirect(builder, metadata, &fb_coeffs); + FinishIQCalCoeffsBuffer(builder, cal_table); + 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 (!VerifyIQCalCoeffsBuffer(verifier)) { + throw uhd::runtime_error("iq_cal: Invalid data provided!"); + } + auto cal_table = GetIQCalCoeffs(static_cast<const void*>(data.data())); + // TODO we can handle this more nicely + UHD_ASSERT_THROW(cal_table->metadata()->version_major() == VERSION_MAJOR); + _name = std::string(cal_table->metadata()->name()->c_str()); + _serial = std::string(cal_table->metadata()->serial()->c_str()); + _timestamp = cal_table->metadata()->timestamp(); + auto coeffs = cal_table->coeffs(); + for (auto it = coeffs->begin(); it != coeffs->end(); ++it) { + _coeffs[it->freq()] = {it->coeff_real(), it->coeff_imag()}; + // Suppression levels are really not necessary for runtime. + // TODO: Come up with a way to skip this step when loading data for + // runtime (and not future storage) + _supp[it->freq()] = {it->suppression_abs(), it->suppression_delta()}; + } + } + + +private: + std::string _name; + std::string _serial; + uint64_t _timestamp; + using coeffs_type = std::map<double, std::complex<double>>; + coeffs_type _coeffs; + // Abs suppression, delta suppression + std::map<double, std::pair<double, double>> _supp; + + interp_mode _interp; +}; + + +iq_cal::sptr iq_cal::make() +{ + return std::make_shared<iq_cal_impl>(); +} + +iq_cal::sptr iq_cal::make( + const std::string& name, const std::string& serial, const uint64_t timestamp) +{ + return std::make_shared<iq_cal_impl>(name, serial, timestamp); +} diff --git a/host/python/pyuhd.cpp b/host/python/pyuhd.cpp index 9ce72c476..5e8c6424c 100644 --- a/host/python/pyuhd.cpp +++ b/host/python/pyuhd.cpp @@ -13,6 +13,7 @@ namespace py = pybind11; +#include "cal/cal_python.hpp" #include "rfnoc/rfnoc_python.hpp" #include "stream_python.hpp" #include "types/filters_python.hpp" @@ -73,4 +74,8 @@ PYBIND11_MODULE(libpyuhd, m) // Register RFNoC submodule auto rfnoc_module = m.def_submodule("rfnoc", "RFNoC Objects"); export_rfnoc(rfnoc_module); + + // Register calibration submodule + auto cal_module = m.def_submodule("cal", "Calibration Objects"); + export_cal(cal_module); } diff --git a/host/python/uhd/usrp/__init__.py b/host/python/uhd/usrp/__init__.py index c7a630258..6f76066ef 100644 --- a/host/python/uhd/usrp/__init__.py +++ b/host/python/uhd/usrp/__init__.py @@ -15,3 +15,5 @@ from .multi_usrp import MultiUSRP # pylint: disable=wildcard-import from .libtypes import * # pylint: enable=wildcard-import + +from . import cal diff --git a/host/python/uhd/usrp/cal/__init__.py b/host/python/uhd/usrp/cal/__init__.py new file mode 100644 index 000000000..77cc3ca35 --- /dev/null +++ b/host/python/uhd/usrp/cal/__init__.py @@ -0,0 +1,16 @@ +# +# Copyright 2020 Ettus Research, a National Instruments Brand +# +# SPDX-License-Identifier: GPL-3.0-or-later +# +""" +Python UHD Module: Calibration sub-module +""" + +# Disable PyLint because the entire libtypes modules is a list of renames. It is +# thus less redundant to do a wildcard import, even if generally discouraged. +# We could also paste the contents of libtypes.py into here, but by leaving it +# separate we avoid importing the lib module in this __init__ file. +# pylint: disable=wildcard-import +from .libtypes import * +# pylint: enable=wildcard-import diff --git a/host/python/uhd/usrp/cal/libtypes.py b/host/python/uhd/usrp/cal/libtypes.py index 754028c24..b75165965 100644 --- a/host/python/uhd/usrp/cal/libtypes.py +++ b/host/python/uhd/usrp/cal/libtypes.py @@ -16,4 +16,6 @@ from ... import libpyuhd as lib # database is a class, but we treat it like a namespace, i.e., a submodule database = lib.cal.database Source = lib.cal.source +IQCal = lib.cal.iq_cal +InterpMode = lib.cal.interp_mode # pylint: enable=invalid-name diff --git a/host/tests/CMakeLists.txt b/host/tests/CMakeLists.txt index f987aa194..2742c0385 100644 --- a/host/tests/CMakeLists.txt +++ b/host/tests/CMakeLists.txt @@ -26,6 +26,7 @@ set(test_sources byteswap_test.cpp cast_test.cpp cal_database_test.cpp + cal_data_iq_test.cpp chdr_test.cpp constrained_device_args_test.cpp convert_test.cpp diff --git a/host/tests/cal_data_iq_test.cpp b/host/tests/cal_data_iq_test.cpp new file mode 100644 index 000000000..97b35a331 --- /dev/null +++ b/host/tests/cal_data_iq_test.cpp @@ -0,0 +1,81 @@ +// +// Copyright 2020 Ettus Research, a National Instruments Brand +// +// SPDX-License-Identifier: GPL-3.0-or-later +// + +#include <uhd/cal/iq_cal.hpp> +#include <uhd/exception.hpp> +#include <boost/test/unit_test.hpp> +#include <iostream> + +using namespace uhd::usrp::cal; + +BOOST_AUTO_TEST_CASE(test_iq_cal_api) +{ + const std::string name = "Mock IQ Data"; + const std::string serial = "ABC1234"; + const uint64_t timestamp = 0x12340000; + + auto iq_cal_data = iq_cal::make(name, serial, timestamp); + BOOST_CHECK_EQUAL(iq_cal_data->get_name(), name); + BOOST_CHECK_EQUAL(iq_cal_data->get_serial(), serial); + BOOST_CHECK_EQUAL(iq_cal_data->get_timestamp(), timestamp); + // Nothing there yet + BOOST_REQUIRE_THROW(iq_cal_data->get_cal_coeff(0.0), uhd::assertion_error); + + iq_cal_data->set_cal_coeff(1.0, {1.0, 1.0}); + iq_cal_data->set_cal_coeff(2.0, {2.0, 2.0}); + + // We're in linear interpolation mode + auto lo_coeff = iq_cal_data->get_cal_coeff(0.5); + auto hi_coeff = iq_cal_data->get_cal_coeff(2.5); + BOOST_CHECK_EQUAL(lo_coeff, std::complex<double>(1.0, 1.0)); + BOOST_CHECK_EQUAL(hi_coeff, std::complex<double>(2.0, 2.0)); + auto mid_coeff = iq_cal_data->get_cal_coeff(1.75); + BOOST_CHECK_EQUAL(mid_coeff, std::complex<double>(1.75, 1.75)); + + iq_cal_data->set_interp_mode(interp_mode::NEAREST_NEIGHBOR); + lo_coeff = iq_cal_data->get_cal_coeff(0.5); + hi_coeff = iq_cal_data->get_cal_coeff(2.5); + BOOST_CHECK_EQUAL(lo_coeff, std::complex<double>(1.0, 1.0)); + BOOST_CHECK_EQUAL(hi_coeff, std::complex<double>(2.0, 2.0)); + mid_coeff = iq_cal_data->get_cal_coeff(1.75); + BOOST_CHECK_EQUAL(mid_coeff, std::complex<double>(2.0, 2.0)); + + iq_cal_data->clear(); + BOOST_REQUIRE_THROW(iq_cal_data->get_cal_coeff(0.0), uhd::assertion_error); +} + +BOOST_AUTO_TEST_CASE(test_iq_cal_serdes) +{ + const std::string name = "Mock IQ Data"; + const std::string serial = "ABC1234"; + const uint64_t timestamp = 0x12340000; + + auto iq_cal_data_blueprint = iq_cal::make(name, serial, timestamp); + for (double d = 0; d < 5.0; d += 1.0) { + iq_cal_data_blueprint->set_cal_coeff(d, {d, d}, d * 10, d * 20); + } + + const auto serialized = iq_cal_data_blueprint->serialize(); + + auto iq_cal_data = container::make<iq_cal>(serialized); + + BOOST_CHECK_EQUAL(iq_cal_data->get_name(), name); + BOOST_CHECK_EQUAL(iq_cal_data->get_serial(), serial); + BOOST_CHECK_EQUAL(iq_cal_data->get_timestamp(), timestamp); + + iq_cal_data->set_interp_mode(interp_mode::NEAREST_NEIGHBOR); + for (double d = 0; d < 5.0; d += 1.0) { + BOOST_CHECK_EQUAL(iq_cal_data->get_cal_coeff(d), std::complex<double>(d, d)); + } +} + +BOOST_AUTO_TEST_CASE(test_iq_cal_des_fail) +{ + std::vector<uint8_t> not_actual_data(42, 23); + + // FIXME: Re-enable this test when the verification works + // BOOST_REQUIRE_THROW(container::make<iq_cal>(not_actual_data), uhd::runtime_error); +} |