Program Listing for File DataLogger.h

Program Listing for File DataLogger.h#

Return to documentation for file (include/Karana/KUtils/DataLogger.h)

/*
 * Copyright (c) 2024-2026 Karana Dynamics Pty Ltd. All rights reserved.
 *
 * NOTICE TO USER:
 *
 * This source code and/or documentation (the "Licensed Materials") is
 * the confidential and proprietary information of Karana Dynamics Inc.
 * Use of these Licensed Materials is governed by the terms and conditions
 * of a separate software license agreement between Karana Dynamics and the
 * Licensee ("License Agreement"). Unless expressly permitted under that
 * agreement, any reproduction, modification, distribution, or disclosure
 * of the Licensed Materials, in whole or in part, to any third party
 * without the prior written consent of Karana Dynamics is strictly prohibited.
 *
 * THE LICENSED MATERIALS ARE PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND.
 * KARANA DYNAMICS DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING
 * BUT NOT LIMITED TO WARRANTIES OF MERCHANTABILITY, NON-INFRINGEMENT, AND
 * FITNESS FOR A PARTICULAR PURPOSE.
 *
 * IN NO EVENT SHALL KARANA DYNAMICS BE LIABLE FOR ANY DAMAGES WHATSOEVER,
 * INCLUDING BUT NOT LIMITED TO LOSS OF PROFITS, DATA, OR USE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGES, WHETHER IN CONTRACT, TORT,
 * OR OTHERWISE ARISING OUT OF OR IN CONNECTION WITH THE LICENSED MATERIALS.
 *
 * U.S. Government End Users: The Licensed Materials are a "commercial item"
 * as defined at 48 C.F.R. 2.101, and are provided to the U.S. Government
 * only as a commercial end item under the terms of this license.
 *
 * Any use of the Licensed Materials in individual or commercial software must
 * include, in the user documentation and internal source code comments,
 * this Notice, Disclaimer, and U.S. Government Use Provision.
 */

/**
 * @file
 * @brief Contains the declarations for the DataLogger class.
 */

#pragma once
// Turn formatting off to avoid changing the order of these includes.
// clang-format off
#include "hdf5.h"
// clang-format on
#include "H5PTpublic.h"
#include "Karana/KCore/LockingBase.h"
#include "Karana/KCore/UsageTrackingMap.h"
#include "Karana/KCore/Var_T.h"
#include "Karana/KUtils/Defs.h"
#include "Karana/Math/Defs.h"
#include <Eigen/Core>
#include <cstring>
#include <format>
#include <functional>
#include <typeindex>

namespace Karana::KUtils {
    namespace kc = Karana::Core;
    namespace km = Karana::Math;

    // Forward declare H5Writer
    class H5Writer;

    /**
     * @class PacketTableConfig
     * @brief Define the columns and associated update functions for a PacketTable.
     *        The PacketTable is the table that stores data in the H5 log file.
     */
    class PacketTableConfig : public kc::LockingBase {
        friend H5Writer;

      public:
        /**
         * @brief PacketTableConfig constructor. The constructor is not meant to be called directly.
         *        Please use the create(...) method instead to create an instance.
         *
         * @param name Name of the table. A / will be interpreted as a new group.
         *             Groups will be added as needed.
         */
        PacketTableConfig(std::string_view name);

        /**
         * @brief Create a packet table config.
         *
         * @param name Name of the table. A / will be interpreted as a new group.
         *             Groups will be added as needed.
         * @return A ks_ptr to the newly created instance of PacketTableConfig.
         */
        // Adding suppression, as CodeChecker complains this method has
        // the same name as a method in the base version. This is just a
        // CodeChecker false positive.
        // codechecker_suppress [cppcheck-duplInheritedMember]
        static kc::ks_ptr<PacketTableConfig> create(std::string_view name);

        /** @brief Destroy packet table config. */
        ~PacketTableConfig();

        /**
         * @brief Add data to the table via a Var_T<km::Vec>.
         *
         * @param var The Var_T<km::Vec> to add to the table.
         * @param as_scalars Rather than using one column for the vector, use multiple. This logs
         *                   each vector component as a scalar. The column names will be appended
         *                   with a _X where "X" is an integer that corresponds with the index in
         *                   the vector.
         */
        void addData(const kc::ks_ptr<kc::Var_T<km::Vec>> &var, bool as_scalars = false) {

            // Ensure this is not healthy.
            ensureNotHealthy();

            // Add type, size, and offset info.
            _addVector(var->name(), var->getVectorSize(), as_scalars);

            // Add the function
            _funcs.push_back(var->getLoggingFunction());
        }

        /**
         * @brief Add data to the table via a Var_T<km::Mat>.
         *
         * @param var The Var_T<km::Mat> to add to the table.
         * @param as_scalars Rather than using one column for the vector, use multiple. This logs
         *                   each vector component as a scalar. The column names will be appended
         *                   with a _X where "X" is an integer that corresponds with the index in
         *                   the vector.
         */
        void addData(const kc::ks_ptr<kc::Var_T<km::Mat>> &var, bool as_scalars = false) {

            // Ensure this is not healthy.
            ensureNotHealthy();

            auto dark = var->getMatrixSize();

            // Add type, size, and offset info.
            _addMatrix(var->name(), dark.first, dark.second, as_scalars);

            // Add the function
            _funcs.push_back(var->getLoggingFunction());
        }

        /**
         * @brief Add data to the table via a vector-valued Var_T with fixed size,
         *        e.g., Var_T<km::Vec3>.
         *
         * @param var The fixed-size, vector-valued Var_T to add to the table.
         * @param as_scalars Rather than using one column for the vector, use multiple. This logs
         *                   each vector component as a scalar. The column names will be appended
         *                   with a _X where "X" is an integer that corresponds with the index in
         *                   the vector.
         */
        template <int rows>
        void addData(const kc::ks_ptr<kc::Var_T<Eigen::Matrix<double, rows, 1>>> &var,
                     bool as_scalars = false) {
            static_assert(rows != Eigen::Dynamic,
                          "Vector must have a fixed size (no dynamic rows). For a dynamic vector, "
                          "use the addData signature that takes in an integer for the number rows "
                          "as an extra argument.");

            // Ensure this is not healthy.
            ensureNotHealthy();

            // Add type, size, and offset info.
            _addVector(var->name(), rows, as_scalars);

            // Add the function
            _funcs.push_back(var->getLoggingFunction());
        }

        /**
         * @brief Add data to the table via a matrix-valued Var_T with fixed size,
         *        e.g., Var_T<km::Mat33>.
         *
         * @param var The fixed-size, matrix-valued Var_T to add to the table.
         * @param as_scalars Rather than using one column for the matrix, use multiple. This logs
         *                   each matrix component as a scalar. The column names will be appended
         *                   with a _X_Y where "X" and "Y" are integers that correspond with the
         *                   row and index of the element in the matrix.
         */
        template <int rows, int cols>
        void addData(
            const kc::ks_ptr<kc::Var_T<Eigen::Matrix<double, rows, cols, Eigen::RowMajor>>> &var,
            bool as_scalars = false) {

            static_assert(rows != Eigen::Dynamic and cols != Eigen::Dynamic,
                          "Matrix must have a fixed size (no dynamic rows or columns). For a "
                          "dynamic matrix, use the addData signature that takes in integers for "
                          "the rows and columns of a dynamic matrix as extra arguments.");

            // Ensure this is not healthy.
            ensureNotHealthy();

            // Add type, size, and offset info.
            _addMatrix(var->name(), rows, cols, as_scalars);

            // Add the function
            _funcs.push_back(var->getLoggingFunction());
        }

        /**
         * @brief Add data to the table via a Var_T with loggable data.
         *
         * @param var The Var_T to add to the table.
         */
        template <class T>
#ifndef PYBIND11_MKDOC_SKIP
            requires(isLoggingDataType<T> and not is_eigen_type_v<T>)
#endif
        void addData(const kc::ks_ptr<kc::Var_T<T>> &var) {

            // Ensure this is not healthy.
            ensureNotHealthy();

            auto it = _h5TypeMap.find(typeid(T));
            if (it == _h5TypeMap.end()) {
                throw std::runtime_error(
                    std::format("Cannot store H5 data for \"{}\".", var->name()));
            }
            _h5_types.push_back(it->second);
            _names.emplace_back(var->name());

            // Create a copy to store in the new function
            _sizes.push_back(var->getSize());
            _offsets.push_back(_offset);

            // Add the function
            _funcs.push_back(var->getLoggingFunction());

            // Increment the offset for the next function.
            _offset += _sizes.back();
        }

        /**
         * @brief Add data to the table via function.
         *
         * @param name The name of the column in the table.
         * @param f The function that produces the data (dynamic vector). This vector must always
         *          be the same size.
         * @param vector_size The size of the vector that the function produces. This vector must
         *                    always be the same size.
         * @param as_scalars Rather than using one column for the vector, use multiple. This logs
         *                   each vector component as a scalar. The column names will be appended
         *                   with a _X where "X" is an integer that corresponds with the index in
         *                   the vector.
         */
        template <class T>
            requires std::same_as<std::invoke_result_t<T>, km::Vec>
        void addData(std::string_view name, const T &f, int vector_size, bool as_scalars = false) {

            // Ensure this is not healthy.
            ensureNotHealthy();

            // Add type, size, and offset info.
            _addVector(name, vector_size, as_scalars);

            _funcs.push_back([f, size = _sizes.back()](char *buf) {
                km::Vec val = f();
                void *dark = val.data();
                std::memcpy(buf, dark, size);
            });
        }

        /**
         * @brief Add data to the table via function.
         *
         * @param name The name of the column in the table.
         * @param f The function that produces the data (dynamic matrix). This matrix must always
         *          be the same size.
         * @param matrix_rows The number of rows in the matrix that the function produces. This
         *                    matrix must always be the same size.
         * @param matrix_cols The number of columns in the matrix that the function produces. This
         *                    matrix must always be the same size.
         * @param as_scalars Rather than using one column for the matrix, use multiple. This logs
         *                   each matrix component as a scalar. The column names will be appended
         *                   with a _X_Y where "X" and "Y" are integers that correspond with the
         *                   row and index of the element in the matrix.
         */
        template <class T>
            requires std::same_as<std::invoke_result_t<T>, km::Mat>
        void addData(std::string_view name,
                     const T &f,
                     int matrix_rows,
                     int matrix_cols,
                     bool as_scalars = false) {

            // Ensure this is not healthy.
            ensureNotHealthy();

            // Add type, size, and offset info.
            _addMatrix(name, matrix_rows, matrix_cols, as_scalars);

            _funcs.push_back([f, size = _sizes.back()](char *buf) {
                km::Mat val = f();
                void *dark = val.data();
                std::memcpy(buf, dark, size);
            });
        }

        /**
         * @brief Add data to the table via function.
         *
         * @param name The name of the column in the table.
         * @param f The function that produces the data.
         * @param as_scalars Rather than using one column for the vector, use multiple. This logs
         *                   each vector component as a scalar. The column names will be appended
         *                   with a _X where "X" is an integer that corresponds with the index in
         *                   the vector.
         */
        template <class T, class R = typename std::invoke_result<T>::type>
            requires(is_eigen_type_v<R> and is_eigen_vector_v<R>)
        void addData(std::string_view name, const T &f, bool as_scalars = false) {

            // Ensure this is not healthy.
            ensureNotHealthy();

            constexpr int rows = extract_rows_v<R>;
            static_assert(rows != Eigen::Dynamic,
                          "Vector must have a fixed size (no dynamic rows). For a dynamic vector, "
                          "use the addData signature that takes in an integer for the number rows "
                          "as an extra argument.");

            // Add type, size, and offset info.
            _addVector(name, rows, as_scalars);

            _funcs.push_back([f, size = _sizes.back()](char *buf) {
                Eigen::Matrix<double, rows, 1> val = f();
                void *dark = val.data();
                std::memcpy(buf, dark, size);
            });
        }

        /**
         * @brief Add data to the table via function.
         *
         * @param name The name of the column in the table.
         * @param f The function that produces the data.
         * @param as_scalars Rather than using one column for the matrix, use multiple. This logs
         *                   each matrix component as a scalar. The column names will be appended
         *                   with a _X_Y where "X" and "Y" are integers that correspond with the
         *                   row and index of the element in the matrix.
         */
        template <class T, class R = typename std::invoke_result<T>::type>
            requires(is_eigen_type_v<R> and not is_eigen_vector_v<R>)
        void addData(std::string_view name, const T &f, bool as_scalars = false) {

            // Ensure this is not healthy.
            ensureNotHealthy();
            constexpr int rows = extract_rows_v<R>;
            constexpr int cols = extract_cols_v<R>;

            static_assert(rows != Eigen::Dynamic and cols != Eigen::Dynamic,
                          "Matrix must have a fixed size (no dynamic rows or columns). For a "
                          "dynamic matrix, use the addData signature that takes in integers for "
                          "the rows and columns of a dynamic matrix as extra arguments.");

            // Add type, size, and offset info.
            _addMatrix(name, rows, cols, as_scalars);

            _funcs.push_back([f, size = _sizes.back()](char *buf) {
                Eigen::Matrix<double, rows, cols, Eigen::RowMajor> val = f();
                void *dark = val.data();
                std::memcpy(buf, dark, size);
            });
        }

        /**
         * @brief Add data to the table via function.
         * @param name The name of the column in the table.
         * @param f The function that produces the data.
         */
        template <class T, class R = typename std::invoke_result<T>::type>
            requires(not is_eigen_type_v<R>)
        void addData(std::string_view name, const T &f) {

            // Ensure this is not healthy.
            ensureNotHealthy();

            auto it = _h5TypeMap.find(typeid(R));
            if (it == _h5TypeMap.end()) {
                throw std::runtime_error(std::format("Cannot store H5 data for \"{}\".", name));
            }
            _h5_types.push_back(it->second);
            _names.emplace_back(name);

            // Create a copy to store in the new function
            size_t offset = _offset;
            _sizes.push_back(sizeof(R));
            _offsets.push_back(offset);

            _funcs.push_back([f, size = _sizes.back()](char *buf) {
                R val = f();
                void *dark = &val;
                std::memcpy(buf, dark, size);
            });

            // Increment the offset for the next function.
            _offset += _sizes.back();
        }

        /**
         * @brief Fill the buffer by calling all the functions.
         */
        void fillBuf();

      protected:
        /**
         * @brief Add a vector type to this PacketTableConfig.
         *
         * @param name The name of the data to log.
         * @param rows The number of rows the vector has.
         * @param as_scalars Whether to add the vector as individual scalars or as one vector.
         */
        void _addVector(std::string_view name, int rows, bool as_scalars);

        /**
         * @brief Add a matrix type to this PacketTableConfig.
         *
         * @param name The name of the data to log.
         * @param rows The number of rows the matrix has.
         * @param cols The number of cols the matrix has.
         * @param as_scalars Whether to add the matrix as individual scalars or as one matrix.
         */
        void _addMatrix(std::string_view name, int rows, int cols, bool as_scalars);

        /**
         * @brief Create the data type and buffer for the data. This can only be done once.
         */
        void _makeHealthy();

        /**
         * @brief This should never be called. If it is, then there is an issue.
         */
        void _makeNotHealthy();

      private:
        /// The name of the group for this PacketTableConfig
        std::string _group_name;

        // The data type of the packet table.
        hid_t _data_type;

        // Buffer to hold the table data
        char *_buf = nullptr;

        /** This vector holds the functions that add memory to the void* data buffer
         * used by the packet table. */
        std::vector<std::function<void(char *buf)>> _funcs;

        /// The sizes for each function return type.
        std::vector<size_t> _sizes;

        /// The names of the columns in the packet table.
        std::vector<std::string> _names;

        /// The h5 types of the columns in the table.
        std::vector<hid_t> _h5_types;

        /// The offsets for each type.
        std::vector<size_t> _offsets;

        /// Memory offset used by the next function.
        size_t _offset = 0;

        /// Maps C++ types to H5 types.
        static const std::unordered_map<std::type_index, hid_t> _h5TypeMap;
    };

    /**
     * @class H5Writer
     * @brief Logs data to an HDF5 file. The data to be logged is configured
     *        via `PacketTableConfig`s.
     */
    class H5Writer : public kc::BaseWithVars {
      public:
        /**
         * @brief H5Writer constructor. The constructor is not meant to be called directly.
         *        Please use the create(...) method instead to create an instance.
         *
         * @param filename The name of the h5 file to create.
         */
        H5Writer(std::string_view filename);

        /**
         * @brief Create an H5Writer class.
         *
         * @param filename The name of the h5 file to create.
         * @return A ks_ptr to the newly created instance of H5Writer.
         */
        // Adding suppression, as CodeChecker complains this method
        // has the same name as a method in the base version. This is
        // just a CodeChecker false positive.
        // codechecker_suppress [cppcheck-duplInheritedMember]
        static kc::ks_ptr<H5Writer> create(std::string_view filename);

        /**
         * @brief Destroy the H5Writer. This closes the H5 file.
         */
        ~H5Writer();

        /**
         * @brief Create a packet table to log.
         *
         * This creates the table and adds it to the active tables. Activate tables
         * will have a data entry added whenever log is called. To deactivate a table
         * use the deactivateTable method. The name of the tame will be the same as
         * the name of the PacketTableConfig.
         *
         * @param config The config that specifies the data of the packet table.
         */
        void createTable(const kc::ks_ptr<PacketTableConfig> &config);

        /**
         * @brief Log data for all the active tables.
         */
        void log();

        /**
         * @brief Create a log entry for just the given table.
         *
         * This will log the table regardless of whether it is active or not.
         *
         * @param name The name of the table to log.
         */
        void logTable(const std::string &name);

        /**
         * @brief Activate the table with the given name.
         *
         * @param name The name of the table to activate.
         */
        void activateTable(const std::string &name);

        /**
         * @brief Deactivate the table with the given name.
         *
         * @param name The name of the table to deactivate.
         */
        void deactivateTable(const std::string &name);

        /**
         * @brief Get a vector of the active table names.
         *
         * @return A vector of the active table names.
         */
        std::vector<std::string> getActiveTableNames();

        /**
         * @brief Get a vector of all the table names.
         *
         * @return A vector of the table names.
         */
        std::vector<std::string> getTableNames();

      private:
        /// Holds the id of the H5 file.
        hid_t _file;

        /// Usage tracking map for all the packet tables.
        kc::UsageTrackingMap<hid_t, PacketTableConfig> _tables;

        /// A map for all the active tables.
        std::map<hid_t, kc::ks_ptr<PacketTableConfig>> _active_tables;

        /// A map of all the table names to their H5 IDs
        std::unordered_map<std::string, hid_t> _table_names;

        /// Functions to run when closing.
        std::vector<std::function<void()>> _fns_on_close;
        std::unordered_set<hid_t> _ids_to_close;
    };
} // namespace Karana::KUtils