Simulating Lift: Particle-Particle Collisions

In 2020 I complained about superficial explanations of how wings work. That post left me wondering whether a particle-based fluid simulation could manifest lift.

The short answer: yes.


To answer the question I wrote a command-line 2-dimensional simulation of an airfoil shape moving through a swarm of ideal (?) gas particles. I learned a lot in the process, about things such as: modern macOS concurrency facilities; common techniques for modeling particle-particle and particle-polygon collisions; how to model an infinite stream of particles without thrashing the heap; the joys of wind tunnel pressure waves; the very basics of Lattice-Boltzmann fluid flow models; and so on.

Source Code

A version of the code is now available on GitHub under an MIT license. Everything is written in Swift and/or SwiftUI, for macOS.

  • DMCFluidSim is a SwiftUI-based application that controls both the particle-based and the Lattice-Boltzmann fluid flow simulations.
  • DMCLatticeBoltzmann is the D2Q9 Lattice-Boltzmann fluid flow simulation.
  • DMCWingWorks is the original, particle-based 2D simulation.
  • DMCMovieWriter helps record simulations as h.264 movies.
  • DMC2D provides 2D geometry primitives like Vectors and Polygons.

In addition to the code I'd like to write some plain old prose about what I've learned. Hence this post, the first in a series of length > 0. Let's see how far I get. ;)

Representing Particles

The particle simulation code in DMCWingWorks makes a lot of simplifying assumptions.

  1. Space has two dimensions.
  2. All particles have the same mass.
  3. Particles have no charge. Collisions involve momenta exchanges.
  4. The mass of every particle is concentrated at a single point in 2D space. There is no rotational inertia.
  5. Every particle has the same radius of interaction.
  6. If the distance between any two particles is less than or equal to the sum of their radii, then they are in collision.

I think this list can be shortened to, "DMCWingWorks is a 2D simulation of a monoatomic gas."

Collision Resolution

Collisions between particles are resolved in a single time step. The lack of angular momentum simplifies the process:

  1. Find the unit vector for the relative displacement of the two particles.
  2. Find the relative velocity of the particles.
  3. Find the magnitude of the relative velocity along the displacement unit vector.
  4. Compute the total impulse along the displacement direction, based on the masses of the two particles.
  5. Allow energy loss in the collision via a coefficient of restitution.
  6. Accelerate each particle by the impulse force.

This algorithm is derived from the Impulse-based contact model section of the Wikipedia page on Collision Response. Here's a bit of Swift illustrating how to calculate the collision impulse.

    private func calcImpulse(with other: Particle) -> Double {
        // Get the unit vector for the relative offsets of the particles.
        let n = (s - other.s).unit()

        // Coefficient of restitution.  A value < 1.0 represents a loss of
        // collision energy, presumably as heat.
        let e = 1.0

        // Get the relative collision velocity.
        let vr = v - other.v
        // Get the portion of the collision velocity that lies along
        // the direction from self to other.
        let vNormal = vr.dot(n)

        // Calculate the impulse, ignoring rotational inertia.
        let numer = -(1.0 + e) * vNormal
        let denom = 1.0 / mass + 1.0 / other.mass
        return numer / denom
    }