Skip to main content
geeks have feelings

Templated numerical integrators in C++

I find myself writing numerical integrators for physics solvers often enough* that I wished I didn’t have to keep rewriting them every time I used a different linear algebra library or changed the dimensions what I was solving.

This is exactly what C++ templates are great for—writing code that works with multiple types with similar interfaces. So I whipped up Euler, midpoint, and Runge-Kutta order 4 (RK4) integrators with a generic C++ template interface.

Now I know templates are scary and all, but you don’t need to know them to use my code. Here’s a simple example of a physics simulator loop for solving 1D Newtonian physics with constant acceleration:

#include "Integrators.h"

// acceleration evaluation function - just return a constant
float constAccel(float pos, float vel, float t) {
    return 1.f;
}

void simLoop() {
    IntegratorRK4 integrator;

    // 1D particle states (positions and velocities) & buffers for next state
    std::vector<float> poss, vels, nextPoss, nextVels;

    // system time state and timestep size
    float t = 0.f, dt = 1.f / 100;
    while (true) {
        // integrate accelerations to velocities & velocities to positions
        for (size_t i = 0; i < poss.size(); i++) {
            integrator.evaluate(poss[i], vels[i], constAccel, t, dt, nextPoss[i], nextVels[i]);
        }
        // take a timestep
        t += dt;
        // advance state to the next state
        poss.swap(nextPoss);
        vels.swap(nextVels);
    }
}

It’s pretty straightforward: every physics iteration, make the following call for every particle i: integrator.evaluate(poss[i], vels[i], constAccel, t, dt, nextPoss[i], nextVels[i]). Thanks to type inference, the actual templates are hidden from you. But what are those arguments?

Of course, those arguments don’t have to be limited to those in that example. For example, we can do:

template<typename Vector, typename Scalar>
struct DragAndConstAccel {
    const Vector accel;
    const Scalar drag;

    DragAndConstAccel(const Vector &accel, double drag) :
            accel(accel), drag(drag) {
    }

    Vector operator()(const Vector &pos, const Vector &vel, Scalar t) {
        return accel - vel * drag;
    }
};

void simLoop() {
    IntegratorRK4 integrator;
    std::vector<std::complex<double> > poss, vels, nextPoss, nextVels;
    double t = 0.0, dt = 1.0 / 100;
    while (true) {
        for (size_t i = 0; i < poss.size(); i++) {
            integrator.evaluate(poss[i],
                    vels[i],
                    DragAndConstAccel<std::complex<double>, double>(1.0, 0.25),
                    t,
                    dt,
                    nextPoss[i],
                    nextVels[i]);
        }
        t += dt;
        poss.swap(nextPoss);
        vels.swap(nextVels);
    }
}

Here I made our system positions and velocities be std::complex<double>s (why would you ever do this?), the time and timestep be double, and replaced the constant acceleration function with a neat functor that can maintain state. In this case, the acceleration functor applies a constant acceleration with a drag acceleration that’s linear to the velocity.

But look how the integrator.evaluate() call and the simulation loop look almost the same as before: that’s the beauty of templates. You can change around your vector & time types and still keep the same loop code.

You wanna see something wild? Let’s stick in Eigen Vector3f and make our acceleration function be a C++11 lambda:

void simLoop() {
    using namespace Eigen;
    IntegratorRK4 integrator;
    std::vector<Vector3f> poss, vels, nextPoss, nextVels;
    double t = 0.0, dt = 1.0 / 100;
    while (true) {
        for (size_t i = 0; i < poss.size(); i++) {
            integrator.evaluate(poss[i], vels[i], [](const Vector3f &pos, const Vector3f &vel, double t) {
                return Vector3f(1.f, 0.f, -1.f) - vel * 0.25f;
            }, t, dt, nextPoss[i], nextVels[i]);
        }
        t += dt;
        poss.swap(nextPoss);
        vels.swap(nextVels);
    }
}

Neat, innit? Anyways, all the integrators are available in a single header file under the MIT/X11 license. If you plan to use it, it would be nice to give me an email shout.

You may have noticed that the integrators don’t handle mass at all—that’s why you pass it an acceleration evaluation function rather than a force evaluation function, as it has no knowledge of mass. Extending the integrators to use mass is a trivial exercise left up to the reader. 🙂

Also, weathered C++ coders may notice that I specify the integrators’ interface using a CRTP base class. There’s no compelling software engineering reason for this; I actually find CRTP static polymorphism to be a mostly useless construct, as you can’t swap around your derived classes without recompiling. But it does make for a good interface spec.

Every Post by Year

  1. 2023
    1. Ducati Timing Belt Replacement
    2. C++ Corrections
  2. 2016
    1. Liftlord
    2. Sensorless Brushless Can’t Even
  3. 2015
    1. Big Data: Test & Refresh
  4. 2014
    1. The Orange Involute
    2. Big Data EVT
  5. 2013
    1. Integer Arithmetic Continued
    2. Real Talk: Integer Arithmetic
    3. Why Microsoft’s 3D Printing Rocks
    4. Flapjack Stator Thoughts
    5. Delicious Axial Flux Flapjack
  6. 2012
    1. How to teach how to PCB?
    2. Fixed-point atan2
    3. It Was Never About the Mileage
    4. Trayrace
    5. BabyCorntrolling
    6. Conkers
    7. BabyCorntroller
    8. Templated numerical integrators in C++
  7. 2011
    1. Bringing up Corntroller
    2. Assembly-izing Tassel
    3. Corn-Troller: Tassel
    4. 5 V to 3.3 V with Preferred Resistors
  8. 2010
    1. HÄRDBÖRD: Interesting Bits
    2. HÄRDBÖRD: Hardcore Electric Longboard
    3. Mistakes to Make on a Raytracer
    4. US International Dvorak
  9. 2009
    1. Raxo
    2. Better Spheres, Fewer Triangles
    3. Donald Knuth Finally Sells Out
    4. Harpy – Sumo Bots 2009