Skip to content

General Utility Functions

Logging

Lagrange uses spdlog for logging purposes. Spdlog provides feature-rich formatting via the excellent fmt library. This provides several advantages over a traditional std::cout messaging:

  • Python-like formatting syntax improves code readability
  • 6 levels of logging: trace, debug, info, warn, error, critical
  • Fast, thread-safe formatting
  • Multiple sinks (file, stdout, etc.)

The default Lagrange logger can be accessed as follows:

#include <lagrange/Logger.h>

lagrange::logger().debug("This mesh has {} vertices and {} facets", v, f);

To control the logging level, or set additional sinks (e.g. to write logs to a file), please read the spdlog documentation.

A common pattern we use in example applications is to set the logging level/sink via command-line arguments. E.g. using CLI11 to parse cli args:

struct
{
    std::string log_file;
    int log_level = 2;
    bool quiet = false;
} args;

// Parse command-line args with CLI11
CLI::App app{argv[0]};
app.option_defaults()->always_capture_default();
app.add_flag("-q,--quiet", args.quiet, "Hide logger on stdout.");
app.add_option("-l,--level", args.log_level, "Log level (0 = most verbose, 6 = off).");
app.add_option("-f,--log-file", args.log_file, "Log file.");
CLI11_PARSE(app, argc, argv)

if (args.quiet) {
    // Hide stdout default sink
    lagrange::logger().sinks().clear();
}
if (!args.log_file.empty()) {
    // Add a new file sink
    lagrange::logger().sinks().emplace_back(
        std::make_shared<spdlog::sinks::basic_file_sink_mt>(args.log_file, true));
}

// Set log level
args.log_level = std::max(0, std::min(6, args.log_level));
spdlog::set_level(static_cast<spdlog::level::level_enum>(args.log_level));

Timing

Lagrange provides handy timing functions based on modern C++'s chrono package. Here is an example use case:

#include <lagrange/utils/timing.h>

auto start_time = lagrange::get_timestamp();
...
auto finish_time = lagrange::get_timestamp();

auto duration = lagrange::timestamp_diff_in_seconds(start_time, finish_time);
lagrange::logger().info("Duration: {}s", duration);

Assertion

Lagrange provide two types of assertion macros:

  • Runtime assertions, using la_runtime_assert(). Those are used to check the validity of user inputs as a pre-condition to a function. For example, providing a function with an empty, or a mesh with incorrect dimension, could result in such an assertion being raised. A runtime assert is executed even in Release configuration, and indicates a problem with the function input.

  • Debug assertions using la_debug_assert(). Those are only checked in debug code (macro NDEBUG is undefined). A debug assert is used to check internal code validity, and should not be triggered under any circumstance by client code. A failed debug assert indicates an error in the programmer's code, and should be fixed accordingly.

Usage In Expressions

Our assert macros behave as functions, meaning they expand to a void expression and can be used in an expression such as y = (la_debug_assert(true), x).

Our assertion macros can take either 1 or 2 arguments, the second argument being an optional error message. To conveniently format assertion messages with a printf-like syntax, use fmt::format:

#include <lagrange/utils/assert.h>
#include <spdlog/fmt/fmt.h>

la_debug_assert(x == 3);
la_debug_assert(x == 3, "Error message");
la_debug_assert(x == 3, fmt::format("Incorrect value of x: {}", x));

Break Into Debugger

It is possible to have assert macros break into the debugger upon failure. To do that, enable the CMake option LAGRANGE_ASSERT_DEBUG_BREAK when compiling Lagrange into your project.

Type Cast

It is often necessary to cast a variable from one type to another. To ensure such cast is safe, Lagrange provides an explicit casting function:

#include <lagrange/utils/safe_cast.h>

// Cast variable y into variable x of type `type1`
type1 x = lagrange::safe_cast<type1>(y);

Implementation Detail

safe_cast checks for type compatibility, sign consistency and truncation error. We recommended to use code that avoids casting in the first place.

Pointer Type Conversion

It is often necessary to convert a std::unique_ptr to std::shared_ptr. Lagrange provides a handy function that avoids repeated type specification:

#include <lagrange/common.h>

std::unique_ptr<T> unique_val = ...;
auto shared_val = lagrange::to_shared_ptr(std::move(unique_val));
assert(unique_val == nullptr);
assert(shared_val.use_count() == 1);

Range

Lagrange provides a handy range() function to enable a python-like range-based for loop:

#include <lagrange/range.h>

for (auto i : lagrange::range(num_vertices)) {
    // i goes from 0 to num_vertices-1
}

The advantage of using range() is that the type of the index variable i is automatically inferred from the type of num_vertices. This reduces the amount of unnecessary implicit casts and compiler warnings.

Lagrange also provide range_sparse() to loop through an active subset of the range:

#include <lagrange/range.h>

std::vector<Index> active_set;

// Populate active_set... (optional)

for (auto i : lagrange::range_sparse(N, active_set)) {
    // i will loop through only the active entries.
}

The iterator behaves as follows:

  • If active_set is empty, range_sparse(N, active_set) will loop from 0 to N (same behavior as range(N)).
  • However, if active_set is non-empty, range_sparse(N, active_set) will loop through the entries of active_set.

Disjoint-Set / Union-Find

Lagrange provide a simple Disjoint-Set data-structure that can be used to implement algorithms such as Kruskal's minimum spanning tree.

#include <lagrange/utils/DisjointSets.h>

using Edge = std::array<Index, 2>;

std::vector<Edge> min_spanning_tree(
    Index num_vertices,
    const std::vector<Edge>& sorted_edges)
{
    // Disjoint-sets for efficient union-find operations
    DisjointSets<Index> union_find(num_vertices);
    std::vector<Edge> output_edges;

    // Iterate over all input edges in ascending order
    for (auto [x, y] : sorted_edges) {
        // If vertices belong to disconnected unrooted trees,
        // merge them and add edge (x, y) to the solution
        if (union_find.find(x) != union_find.find(y)) {
            output_edges.emplace_back(x, y);
            union_find.merge(x, y);
        }
    }
    return output_edges;
}

Implementation Detail

For simplicity, our Disjoint-Set data structure currently only implements path compression, but not union by rank.

BitField

The BitField class can be used to implement convenient bitwise boolean operations over an enum type.

#include <lagrange/utils/BitField.h>
#include <lagrange/Logger.h>

enum Operation : int {
    Translation = (1 << 0),
    Rotation = (1 << 1),
    Scaling = (1 << 2),
}
BitField<Operation> op;
op.set(Operation::Translation);
op.set(Operation::Scaling);
if (op.test(Operation::Scaling)) {
    lagrange::logger().info("Scaling bit is set");
}

Floating Point Exceptions

On certain platforms, it is possible to trap floating point exceptions using compiler-specific functions. This is useful for debugging purposes, e.g. to detect when a NaN is emitted during numerical computation.

Lagrange provide a convenient platform-agnostic function called enable_fpe() / disable_fpe() to enable/disable trapping floating point exceptions. Our implementation currently supports Linux and macOS. On non-supported platform, calling the function is a no-op.

#include <lagrange/utils/fpe.h>

void my_main() {
    lagrange::enable_fpe();
    // call problematic operation [...]
    lagrange::disable_fpe();
}

Compilation Issues

If our implementation of enable_fpe() causes compilation issues on your target platform (e.g. macOS M1, Emscripten, etc.), it is possible to disable the feature explicitly by setting the CMake option LAGRANGE_DISABLE_FPE=ON. In this case, calling enable_fpe() will do nothing.

Scope Guard

Sometimes it is necessary to ensure that certain resources are properly deallocated when leaving the scope of a function. This typically happens when dealing with C API that provide abstract object handling functions such as:

struct MyObject;

MyObject * create_object();

void delete_object(MyObject *);

To make object deletion exception-safe in C++, one must ensure that the delete_object() function is called even if an exception is raised. One possibility is to use a RAII class. Lagrange provides a simple make_scope_guard() function that wraps an arbitrary callable using RAII:

#include <lagrange/utils/scope_guard.h>

void my_function() {
    MyObject *obj = create_object();

    // Deferred called to `delete_object()` destructor.
    // Called when `guard` goes out of scope.
    auto guard = lagrange::make_scope_guard([&]() { delete_object(obj); });

    // Use `obj` ...
}

This paradigm is also useful when using imperative programming, e.g. when implementing an "editable state" in an object, and you want to ensure the begin_edit() and end_edit() methods are always called together. It can also be use to wrap calls to glBegin() and glEnd() in OpenGL, ensuring the state machine stays consistent.

Stack-Allocated Containers

Lagrange provides different implementations of stack-allocated containers. Such containers are extremely useful when dealing with many small buffers with a known upper-limit size, or at least a good initial guess. For example if the nodes of a graph have at most 6 neighbors, you can use a stack-allocated containers to store the neighbor indices. Another example is storing a quad-dominant mesh, where facets can have a size 3 or 4.

Lagrange currently provide the following implementations:

  • SmallVector. An implementation of a std::vector<> with a pre-allocated stack buffer that grow on the heap beyond the initial upper limit.
  • StackVector. An implementation of a std::vector<> with a fixed upper size provided at compile-time. Inserting new elements beyond the limit will throw an exception.
  • StackSet. An implementation of a std::set<> with a fixed upper size provided at compile-time. Inserting new elements beyond the limit will throw an exception.

Shared Span

Lagrange's SurfaceMesh class relies heavily on a span<> implementation to provide bounds-safe view over raw pointers. Since our mesh attribute supports copy-on-write and wrapping of external buffers, it can be desirable to wrap external buffers as attribute while managing their lifetime.

To this end, Lagrange implements a SharedSpan object that offers a bounds-safe view over a raw pointer, while tracking the actual owner object via a different std::shared_ptr<>.

Example 1: Buffer Created via a C-style API

struct MyMesh {
    std::vector<float> vertices;
    std::vector<uint32_t> facets;
};

// Create a new mesh instance
MyMesh * mesh_new();

// Free previously allocated mesh instance
void mesh_free(MyMesh* obj);

void wrap_mesh(lagrange::SurfaceMesh<float, uint32_t> &mesh)
{
    // Create vertex buffer via abstract C API
    auto owner_obj = std::shared_ptr<MyObject>(mesh_new(), &mesh_free);

    // Number of vertices is number of coordinates divided by 3
    uint32_t num_vertices = owner_obj->vertices.size() / 3;

    // Create a "view" of the raw vertices pointer while tracking ownership information
    auto vertices_view = make_shared_span(
        owner_obj,
        owner_obj->vertices.data(),
        owner_obj->vertices.size());

    mesh.wrap_as_vertices(vertices_view, num_vertices);

    // Wrap facet buffer sharing the same owner object
    uint32_t num_facets = owner_obj->facets.size() / 3;
    auto facets_view = make_shared_span(
        owner_obj,
        owner_obj->facets.data(),
        owner_obj->facets.size());
    mesh.wrap_as_facets(facets_view, num_facets, 3);
}

Example 2: Buffer Stored as an Eigen Matrix

using RowMatrixXd = Eigen::Matrix<double, Eigen::Dynamic, Eigen::Dynamic, Eigen::RowMajor>;

// Moves a N x 3 matrix of vertex positions into a Lagrange SurfaceMesh
// without copying the underlying buffer
void wrap_vertices(lagrange::SurfaceMesh32d& mesh, RowMatrixXd&& V)
{
    // Take ownership of the input matrix V
    auto eigen_matrix = std::make_shared<RowMatrixXd>(std::move(V));

    // Create a safe view of the raw position data
    auto vertices_view =
        lagrange::make_shared_span(eigen_matrix, eigen_matrix->data(), eigen_matrix->size());

    // Wrap raw position data as vertices without any copy
    mesh.wrap_as_vertices(vertices_view, eigen_matrix->rows());
}