Quick Start

Requirements

  • A working C++17 compiler (currently gcc and clang are tested but MSVC will come soon)
  • If using GCC, you will get better results with a trunk build of GCC 10.0
  • CMake 3.14+ if building the tests
  • (optional) Ninja Backend build system

Optional Requirements

Refer to the optimizations page to see recommended compiler flags for your application.

Get the source code

  • git clone [email protected]:jeremyong/gal.git - Clone the project (directly, as a submodule in your project, via CMake's FetchContent module, etc)
  • gal - Link the gal interface target if incorporating the library into your own code (will disable test and sample compilation)
  • mkdir build && cd build && cmake .. -G Ninja && ninja - Build the tests
  • ./bin/gal_test - From the build folder, run the tests

When building as a standalone project, the following CMake options are available:

Option Default Description
GAL_TESTS_ENABLED ON Compiles the tests
GAL_SAMPLES_ENABLED ON Compiles the samples (none as of yet, stay tuned!)
GAL_PROFILE_COMPILATION_ENABLED OFF Enables timing data generation (traces if using clang, reports if using gcc)

If using CMake to integrate GAL into your project, here's a quick snippet you can use (requires CMake 3.14 or above):

include(FetchContent)
FetchContent_Declare(
    gal
    GIT_REPOSITORY https://github.com/jeremyong/gal.git
    GIT_TAG master # or specific commit hash
)
FetchContent_MakeAvailable(gal)

target_link_libraries(your_target PRIVATE gal)

Usage

Pick A Model

To use GAL, you need to minimally include a single header corresponding to the model you wish to work with. The model defines the metric signature as well as the dimensionality of the space you wish to represent. Typically, this will either be the Euclidean plane or the fundamental Euclidean 3-space. If you installed or linked to the library through conventional means, you would include things with the gal path prefix (as in, #include <gal/pga.hpp> for example).

Header Space Namespace Description
gal/ega.hpp gal::ega Canonical 3D Euclidean space
gal/pga2.hpp gal::pga2 Projectivized 2D Euclidean plane
gal/pga.hpp gal::pga Projectivized 3D Euclidean space
gal/cga2.hpp gal::cga2 Conformalized 2D Euclidean plane
gal/cga.hpp gal::cga Conformalized 3D Euclidean plane

All functions and entities specific to the model will be defined in the namespace according to the table above. General functions and operations will be defined in the gal namespace.

Create some entities

Regardless of the space you choose, you will generally have access to the geometric entities you would expect, each templatized based on the precision you wish to work with.

Entity Class
Points point<T>
Lines line<T>
Planes plane<T>
Rotors rotor<T>
Translator translator<T>
Motor motor<T>

As these entities have representations that are model-dependent, they reside in the model namespace from the table above. If a class is missing, it is likely not implemented as of yet (most work will happen in the ) so please file an issue if there's something you need (or even better, submit a PR!).

All the entities above are representations of Euclidean entities and take on a different shape internally (at compile-time) depending on the space and model you're working in.

All entities support initialization the way you'd expect. For example point<double> p{0, 1, 2} defines the point at when using any of the models that represent Euclidean 3-space. They also support indexed access (i.e. p[1] == 1 will evaluate true) and in some cases, accessors are aliased using nested unions for convience. The point coordinates, for example, may be referred to via u, v, and w, or even s and t depending on the model dimensionality.

Where appropriate, entities may support the normalize method, which mutates the entity as appropriate for its type. A divide-by-zero check is not performed. In all cases in fact, the library adopts the philosophy that the user should not pay for what the user does not use.

Entities are stored sparsely regardless of the model they are in with no additional overhead for multivector basis-index bookkeeping. For example, sizeof(point<float>) is 12 (3 floats) regardless of whether you are working with CGA (where the point is represented as ) or in EGA (where the point is simply ).

Evaluating expressions

Expressions are evaluated lazily at compile-time in a manner that maximally promotes CSE. Thus, we sacrifice a little bit of code-authorship convenience to gain a significant speedup at runtime.

A computation will take the form compute([](...) { ... }, entities...) where the first argument is a lambda and the second argument are the entities you created in the prior step.

A simple calculation might look like:

Line Construction

using namespace gal::pga;
using gal::compute;

point<> p1{1, 0, 0}; // The default value type is `float`
point<> p2{0, 1, 1};

// Take the regressive product of the two points to construct
// the line passing through them.
line<> l = compute([](auto p1, auto p2) {
        return p1 & p2;
    }, p1, p2);

// You can verify that l defines Plucker coordinates that pass
// through (1, 0, 0) and (0, 1, 1). Taking the regressive product
// with a collinear point will result in 0.

In the lambda (the first argument to compute), we do not capture variables, and we do not produce side-effects. The lambda itself is never actually evaluated (instead, decltype is used to compute the final result based solely on the type the lambda returns). In the body of the lambda, we shadow the outside variable names for convenience. The actual types that are passed to the lambda are building blocks of a template expression tree. Thus, operations that are supported in the body of a compute lambda are not supported outside the lambda.

Bug

point<> p1{1, 0, 0};
point<> p2{0, 1, 1};

line<> l = p1 & p2; // Fails to compile!

This restriction may be relaxed in the future so that such operations create an immediate computation context for prototyping convenience.

For convience, computations may return multiple results that will be reified to the same number of results.

Variadic Return

point<> p1{1, 0, 0};
point<> p2{0, 1, 1};
point<> p3{1, 0, 1};

// Make a line from p1 to p2, and a plane containing p1, p2, and p3
auto&& [p12, p123] = compute(
    [](auto p1, auto p2, auto p3) {
        auto p12 = p1 & p2;
        auto p123 = p12 & p3;
        return std::make_tuple(p12, p123);
    }, p1, p2, p3);

The binary operations supported in the compute lambda are as follows:

Symbol Operation Description
* The fundamental geometric product
^ The exterior (wedge or outer) product
& The regressive product
>> The left contraction
| The symmetric inner product
% The sandwich operator
+ Multivector sum
- Multivector difference
scalar_product Scalar product

Be mindful of operator precedence!

Please do use parenthesis when using operators so as to avoid bugs associated with operator precedence.

And the following table describes the unary operations

Symbol Operation Description
~ Reversion
! The Poincare dual map
- Multivector negation
extract<uint8_t... E>(a) Extract a specified set of components into a new multivector

Generally, the operations above work with multivectors, The main exception is the use of +, -, *, and / in order to shift or scale a multivector by a compile-time constant. For this, one (and at most one) of the operands must be of type frac<int, int>. For example frac<1, 2> * a would divide the multivector a by 2. Equivalently, this could be done with a / frac<2> as you would expect (the denominator defaults to 1).

Reading results

As we saw in the above example, we can either return from a computation a single result, or just as easily a variadic number of results. What is happening behind the scenes here is an implicit cast whenever you specify the type. It starts as a generic entity type which is templatized on the data it contains. For example, if we did a computation which produced a result of the form , the computation would produce an entity of type entity<A, T, 0, 0b11> where A is the algebraic model we are using, T is the value type, and 0 and 0b11 (3 expressed as binary) are bitfields encoding the coordinates of the multivector. The entity type can be worked with directly using the index operator [size_t]. Alternatively, it can be cast at any point to a concrete type like so:

point<float> p{an_entity};

There is no runtime cost associated with the cast unless the source entity is aliased in a manner the compiler must do a copy. No RTTI or inheritance is used. You cannot cast the type to a different model or precision (at this time).

Entities, do not need to be casted into concrete types before being used in a computation. For example:

Auto return deduction

point<> p1{1, 0, 0};
point<> p2{0, 1, 1};
point<> p3{1, 0, 1};

// This returns a line, but lets say we don't know that
auto p12 = compute(
    [](auto p1, auto p2) {
        return p1 & p2;
    }, p1, p2);

static_assert(p12.size() == 6, "p12 is a 6 dimensional bivector");

// We use p12 without caring about its shape (same efficiency as above) before finally
// storing it as a plane. Note that we could have continued to proceed without storing
// it as a concrete type.
plane<> p123 = compute(
    [](auto p12, auto p3)
        return p12 & p3;
    }, p12, p3);

If needed, a custom multivector can be easily created manually in the geometric model of your choosing. For example:

User-defined multivector

using algebra = gal::pga;
entity<algebra, double, 0b11, 0b101> my_multivector{3.2, 1.2};

This creates the quantity and can be used in a compute context like any other entity (concrete or otherwise). The basis elements are expressed as a bitfield with the lower indices corresponding to the least significant bits. It is important that they be specified in ascending lexicographic order as this is not currently checked for compilation efficiency. Internally, all multivectors, polynomials, and indeterminates are kept sorted to achieve optimal compiler throughput and many algorithms may break if this total ordering is not respected.

Roadmap

(not ordered)

  • Support MSVC
  • Support transcendental function usage in lazily evaluated contexts
  • Provide samples and visualization framework
  • Provide examples using dual and complex numbers as the field type
  • Provide more instructional documentation for newer practitioners of GA
  • Support much longer evaluation contexts (compile-time acceleration)
  • Integrate CI/automated testing

Project layout

docs/
    index.md  # This documentation homepage.
    ...       # Other markdown pages, images and other files.
public/                     # Added to your include path (public headers)
    gal/
        algebra.hpp         # Routines for manipulating multivectors (product, sum, negation, etc)
        algorithm.hpp       # Compile-time routines (i.e. sorting, rearrangement)
        cga.hpp             # Provides conformal geometric algebra
        cga2.hpp            # Provides 2D conformal geometric algebra (aka compass ruler algebra)
        ega.hpp             # Provides 3D geometric algebra
        engine.hpp          # Defines various mechanisms for evaluating expressions at runtime
        entity.hpp          # Describes the statically-typed representation of runtime multivectors
        expression_debug.hpp    # Debug facilities
        expression.hpp      # Expression template interface
        format.hpp          # Various string-conversion routines
        geometric_algebra.hpp   # Implements the various products and operations defined in GA
        null_algebra.hpp    # Routines for converting to and from the null-basis
        numeric.hpp         # Compile time numeric facilities (rational numbers, fast pow, etc)
        pga.hpp             # Provides the 3D projective geometric algebra P(R3*)
        pga2.hpp            # Provides the 2D projective geometric algebra P(R2*)
samples/
    main.cpp    # Primary entrypoint (coming soon!)
test/
    ...         # Various files to test different functionality

Comments