Directional C++ Library

repository·master·Indexed 42 days ago

https://github.com/avaxman/directional

A C++ library for creating, manipulating, and visualizing directional fields on 3D meshes. The project utilizes the Eigen library for high-performance linear algebra, including support for dynamic and fixed-size tensors, lazy evaluation of tensor operations, and multiple execution backends such as ThreadPoolDevice and GpuDevice.

Tokens
20.6K
Snippets
61
Records
88
Agent score
53%

What's inside Directional

  1. Overview of the Eigen library

    master

    Eigen is a C++ template library designed for linear algebra operations. It provides high-performance implementations for:

    • Matrices and Vectors: Core data structures for linear algebra.
    • Numerical Solvers: Algorithms for solving linear systems and other numerical problems.
    • Related Algorithms: A wide range of mathematical algorithms built on top of the matrix/vector primitives.

    Because it is a template library, it is designed for high performance and can be integrated into C++ projects to handle complex mathematical computations.

  2. What is a Discrete Tangent Bundle?

    master

    In Directional, a Discrete Tangent Bundle is implemented via the TangentBundle class. It represents a finite graph $G_{TB} = (V_{TB},E_{TB})$ where each node is an independent $d$-dimensional vector space (a tangent space).

    A TangentBundle provides several essential properties for geometric algorithms:

    1. Intrinsic parameterization: Vectors are represented using coordinates in a fixed local basis.
    2. Adjacency: A graph structure encoding the local environment and topology of the manifold.
    3. Connection: A directed-edge-based quantity (often an orthogonal matrix) that defines how to transport vectors between adjacent tangent spaces (parallelity).
    4. Metric: Defined by connectionMass (edge weights) and tangentSpaceMass (inner product weights on vectors).
    5. Cycles: Closed chains of spaces used to define topology and singularities via holonomy (the failure of a vector to return to itself after transport around a cycle).
    6. Extrinsic Data: Includes point locations (sources), surface normals, and the ability to project between intrinsic and extrinsic spaces.

    Common Implementations

    • PCFaceTangentBundle: Implements face-based tangent spaces for 2D triangle meshes. It uses dual edges for adjacency and face barycenters as sources.
    • IntrinsicVertexTangentBundle: Implements vertex-based intrinsic tangent spaces, parameterizing the 1-ring environment directly.
  3. How to handle deferred operations with TensorRef

    master

    Most tensor operations return an <Operation> (a deferred expression) rather than a result. You cannot call .dimensions() or operator() directly on these expressions.

    To work with a deferred operation as if it were a real tensor, wrap it in a TensorRef. This is particularly useful in templated code where you want to handle both raw Tensor objects and slices/expressions agnostically.

    Workflow:

    1. Perform an operation (returns <Operation>).
    2. Wrap the result in a TensorRef.
    3. Use TensorRef to access dimensions or evaluate the expression into a concrete tensor.
    // If 'op' is a deferred operation (e.g. from a slice or math op):
    // Eigen::TensorRef<Eigen::Tensor<float, 2>> ref = op;
    // Now you can use ref.dimensions() or ref(i, j)
  4. Apply linear reductions to integration

    master

    You can reduce the degrees of freedom in the integrated functions by enforcing linear relations $F = U imes f$. This is done by providing a matrix $U$ to the linRed field in IntegrationData.

    Commonly used reductions include:

    • Triangular symmetry: Use set_triangular_symmetry(int N).
    • Sign symmetry: Automatically assumed when $N$ is even ($U = [Id_{n imes n}; -Id_{n imes n}]$).

    Warning: The reduction must commute with the permutation around singularities to avoid algorithm failure.

  5. Perform Hodge Decomposition

    master

    Hodge decomposition splits an $i$-form $z_i$ into three unique components: an exact component ($d_{i-1}f_{i-1}$), a coexact component ($\delta_i g_{i+1}$), and a harmonic component ($h_i$).

    To perform this decomposition, use the following logic (conceptually implemented via Hodge_decomposition()):

    1. Exact component: Use project_exact(d, M, cochain, prevCochain, exactCochain). If the operator $d$ has a trivial null-space, it solves the Poisson equation. If it has a non-trivial null-space, use the GagueFixing flag to solve for the minimum $2$-norm solution.
    2. Coexact component: Use project_coexact(dNext, M, MNext, cochain, k, coexactCochain, nextCochain).
    3. Harmonic component: Calculated as harmCochain = cochain - exactCochain - coexactCochain.
  6. How Eigen Tensor operations work (Lazy Evaluation)

    master

    All Tensor methods (except for direct assignments or specific evaluation calls) return non-evaluated Operations. These operations can be chained together to form complex expressions.

    Crucially, the chain of operations is evaluated lazily. The actual computation typically only occurs when the operation is assigned to a tensor. This allows the library to optimize the expression before execution.

  7. How directional field representations work

    master

    A directional field is represented by its encoding in each discrete tangent plane. The number of vectors in each plane is called the degree ($N$). Directional uses the CartesianField class to manage these representations.

    Supported Representations

    TypeDescriptionMemory Complexity
    RawAn explicit vector of $d \times N$ entries. For $d=3, N=4$, it uses $xyzxyzxyzxyz$ ordering. Set CartesianField::fieldType to directional::RAW_FIELD.$dN
    Power Field(2D only) Encodes an $N$-rotational-symmetric object as a single complex number $y=u^N$.$2
    PolyVector(2D only) Represents an $N$-directional object as coefficients $a$ of a monic complex polynomial $f(z)=z^N+\sum_{i=0}^{N-1}{a_iz^i}$.$2N

    Conversion

    Directional provides conversion functions to switch between these types, following the pattern rep1_to_rep2 (e.g., polyvector_to_raw()).

    Note: Not all conversions are possible. For example, you cannot convert from PolyVector to Power Field because PolyVector contains more expressive information.

  8. Use Discrete Tangent Bundles (Vertex vs Face)

    master

    Directional provides flexibility in discretization by allowing you to compute fields on either a vertex-based or a face-based tangent bundle.

    Available types:

    • directional::IntrinsicVertexTangentBundle vtb (Vertex-based)
    • directional::PCFaceTangentBundle ftb (Face-based)

    To visualize both, initialize both bundles with the mesh, set them as Cartesian fields with unique names and ordinal numbers, and use viewer.toggle_cartesian_field(ordinal, name) or viewer.toggle_singularities(ordinal, name) to switch between them in the UI.

    directional::IntrinsicVertexTangentBundle vtb;
    directional::PCFaceTangentBundle ftb;
    
    // ... setup mesh ...
    
    ftb.init(mesh);
    vtb.init(mesh);
    
    // ... set fields with unique ordinals ...
    viewer.set_cartesian_field(rawFaceField, "Face-Based Field", 0);
    viewer.set_cartesian_field(rawVertexField, "Vertex-Based Field", 1);
    viewer.launch();
  9. Understand Eigen Tensor Datatypes

    master

    When working with Eigen Tensors, the documentation uses several pseudo-types to describe methods and operations:

    • <Tensor-Type>::Dimensions: An array-like object of ints representing the tensor's dimensions. It has a size attribute (the rank) and can be indexed like an array.
    • <Tensor-Type>::Index: An integer type used for indexing along dimensions (returned by dimension(), size(), or used in operator()).
    • <Tensor-Type>::Scalar: The underlying data type of the tensor elements (e.g., float for Tensor<float, 2>).
    • <Operation>: Indicates that a method returns a deferred operation rather than a concrete tensor. An <Operation> must be evaluated (e.g., by assigning it to a tensor) before its values can be accessed directly.
  10. How Tensor operations and lazy evaluation work

    master

    Eigen Tensor operations (like +, *, .exp()) do not perform computations immediately. Instead, they construct a lightweight "tensor operator" object (e.g., TensorCwiseBinaryOp). This allows for lazy evaluation, where a complex tree of operations is built and only executed when the result is assigned to a concrete Tensor type. This mechanism enables significant compiler optimizations and performance gains.

    Example of building an expression tree:

    // This constructs a tree of operators, it does NOT perform addition.
    auto t4 = t1 + t2;
    
    // This constructs a nested tree: TensorCwiseBinaryOp(t1, TensorCwiseUnaryOp(t2, 0.3f))
    auto t5 = t1 + t2 * 0.3f;
    Tensor<float, 3> t1(2, 3, 4);
    Tensor<float, 3> t2(2, 3, 4);
    // Set t3 to the element wise sum of t1 and t2
    Tensor<float, 3> t3 = t1 + t2;