Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

cube_galaxy

physim is a versatile pipeline-based framework for running N-Body simulations. At its core, physim creates a system composed of entities and the state of the system is evolved through modular components at regular timesteps. On each timestep, the state of the system is rendered. physim’s functionality come from plugins. These plugins provide components which let users customise how:

  • entities are created
  • forces are calculated
  • numerical integration is performed
  • the system is rendered

For example, you may want to simulate a globular cluster with 10,000 stars. You can compose how your system evolves through configuring which elements will be used in your simulation.

# simulation of a plummer sphere with gravity
physim global dt=0.01 iterations=1000! plummer n=10000 ! \
    astro theta=0.3 e=0.02  ! rk4 ! glrender

# add collisions
physim global dt=0.01 iterations=1000! plummer n=10000 ! \
    astro theta=0.3 e=0.02 ! collisions ! rk4 ! glrender

# make everything act like a there's also a spring connecting 
# it to the centre of the universe
physim global dt=0.01 iterations=1000! plummer n=10000 ! \ 
    astro theta=0.3 e=0.02 ! shm ! collisions ! rk4 ! glrender

If you are interested in studying the universe through simulation, then I’d recommend using more well established software for your domain (e.g. NEMO). However, if you want to have fun and render cool videos, then physim might be suit your needs.

The first chapter will focus on how to use physim to run simulations and how to render videos. The second chapter will give an example of how a plugin can be made with Rust.

Installation

macOS

The only platform that physim has been pre-built for is macOS (ARM64). The latest release can be found on the GitHub release page. To download, install and add physim to your PATH, run

$ curl -L https://github.com/jhb123/physim/releases/latest/download/physim-macos.tar.gz \
  -o physim-macos.tar.gz && tar -xzf physim-macos.tar.gz && bash physim-macos/install.sh

Other platforms

If you are planning to use physim on other platforms, you will need to compile the binaries and plugins yourself. This can be done by installing Rust and then building physim from source. The compiled binaries and plugins will be in physim/target/release.

$ git clone https://github.com/jhb123/physim.git
$ cd physim
$ cargo build -r

Adding Plugins

physim will load plugins dynamically at run time. They will be discovered if they are in the same directory as physim. You can specify additional directories that will be searched for plugins, each one separated by :, with the PHYSIM_PLUGIN_DIR environment variable.

$ ls 
libastro.dylib            # astro plugin 
libglrender.dylib         # glrender plugin 
libintegrators.dylib      # integrator plugin 
libmechanics.dylib        # classical mechanics plugin 
libphysim_attribute.dylib
libphysim_core.dylib      # core library
libutilities.dylib        # utilities plugin
physcan                   # binary for inspecting plugins
physim                    # binary for running simulations

Usage

In physim,the state of a system is represented by entities, and entities have various parameters like position, velocity, and mass. The way the system evolves in physim simulations is determined by elements. Elements are configurable, reusable blocks which usually perform one kind of action on the entities, and elements are made available to physim through plugins. The state of the system is updated at fixed timesteps and the number of timesteps can be configured. Each kind of element is summarised below.

element kinddescription
initialiserCreates the initial state of the simulation
synthCreates new entities during the simulation
transformCalculate a force to apply to the entities. These can be chained together.
integratorA numerical integrator that uses the force calculated by the chain of transforms to update the entities
transmuteDirectly manipulate the entities
rendererPost processing of the state e.g. render to a window or save to file

Simulations have the following requirements:

  1. One integrator must be specified.
  2. One renderer must be specified.
  3. At least one transform or one transmute must be specified.

A pipeline can use a mixture of transforms and transmutes. For example, astro is a transform which calculates the gravitational force acting on entities. collision is a transmute which calculates elastic collisions. astro indirectly changes each entity through the integrator selected for the simulation whereas collision directly modifies the velocities of the entities.

TOML configuration

The easiest way to construct your simulations is with a TOML file.

$ physim -f /path/to/simulation.toml

The syntax of this file is as follows:

# simulation.toml
# global parameters that control the length and timestep of the simulation
[global]
dt = 0.002
iterations = 2000

# You need a map of named elements which will be in included in your simulation
[elements]
# each element's parameters can be configured
[[elements.foo]]
a = 1
[[elements.bar]]
b = 2
...

The next example will produce a cube of 10,000 stars using the cube element. The force on each star is calculated using the Barnes-Hut algorithm with the astro element. A 4th order Runge-Kutta numerical integrator, rk4, is used to calculate the new location of each star at each time step. The simulation is rendered to a window with the glrender element.

[global]
dt = 0.01
iterations = 2500

[elements]

[[elements.cube]]
n = 10000
seed = 2
a = 2.0 # side length of the cube in screen-coordinates

[[elements.astro]]
theta = 0.4
e = 0.01

[[elements.rk4]]


[[elements.glrender]]
resolution="1080p"
shader="velocity"

From the CLI

physim simulations can be configured directly in the CLI. Each element is delimited by !, and the properties of the element can be configured as shown below. The same simulation above can be launched with

$ physim global dt=0.01 iterations=2500 ! \
    cube n=10000 seed=2 a=2.0 !  astro theta=0.4 e=0.01 ! \
    rk4 ! glrender resolution="1080p" shader="velocity"

Physcan

physcan is for checking what elements you have available in physim. To inspect an element’s documentation, you can run physim <element>, e.g. physcan astro.

Using stdout and FFmpeg

This guide demonstrates using the stdout with FFmpeg. Once you’re happy that a simulation looks good with glrender, you may want to encode it. stdout renders each frame of the simulation to stdout as 8bit, RGBA pixels. FFmpeg can read this via a pipe. stdout’s can produce 720p, 1080p and 4K (3840 × 2160) video. The

Rendering a video

Since physim simulations can have lots of small moving particles, you may need a high bitrate to reduce the artefacts due to compression.

$ physim global iterations=100 dt=0.1 ! cube ! astro theta=1.3 ! rk4 ! \
 stdout zoom=1.5 resolution=1080p | \
 ffmpeg -y -f rawvideo -pixel_format rgba -video_size 1920x1080 -framerate 60 -i pipe:0 -c:v libx265 -preset slow -crf 16 -x265-params "no-sao=1:deblock=-6,-6:aq-mode=3:keyint=30:level-idc=5.1:tier=high" -pix_fmt yuv420p10le -vf "eq=saturation=1.2" -b:v 50M cube.mp4

Making a thumbnail

To make a thumbnail, you can use the frame parameter. The example below makes a screenshot of the 50th iteration of the simulation.

$ physim global iterations=100 dt=0.1 ! cube ! astro theta=1.3 ! rk4 ! \
 stdout zoom=1.5 resolution=1080p frame=50 | \
ffmpeg -f rawvideo -pix_fmt rgba -s 1920x1080 -i - -frames:v 1 -vf format=rgb24 cube.png

Other handy ffmpeg commands

Add audio with ffmpeg -i input.mp4 -i input.mp3 -c:v copy -c:a aac -shortest output.mp4

Introduction

This chapter will guide you through making a transform element which applies drag to all entities in the simulation. By the end of this chapter, you will know about all the fundamental parts of physim and you’ll be able to create new, interesting simulations.

For more examples of elements, see the physim repository

The boiler plate

This Cargo project contains the dependencies needed to build a plugin for physim. physim-core provides traits and types. physim-attribute provides macros that generate the code which lets physim use the plugin. serde_json is used to parse an element’s configuration at run time. The plugin needs a build.rs script and the rustc_version crate to expose compiler information which physim checks for compatibility. Because the plugin is a dynamically loaded library, you should specify crate-type = ["dylib"].

[package]
name = "example"
edition = "2024"
authors = ["Joseph Briggs"]
license = "MIT"
repository = "https://github.com/jhb123/physim"
version = "0.1.0"

[lib]
crate-type = ["dylib"]

[dependencies]
physim-core = { git = "https://github.com/jhb123/physim" }
physim-attribute = { git = "https://github.com/jhb123/physim" }
serde_json = "1.0.140"


[build-dependencies]
rustc_version = "0.4.1"

The build.rs script should contain

build.rs
use rustc_version::version;

fn main() {
    let rustc_version = version().expect("Failed to get rustc version");
    let target =
        std::env::var("TARGET").expect("Cargo did not set TARGET (this should never happen)");
    let abi_info = format!("rustc:{}|target:{}", rustc_version, target);
    println!("cargo:rustc-env=ABI_INFO={}", abi_info);
}

Your plugin project can be laid out like this

example_plugin/
├── Cargo.toml
├── build.rs
└── src/
    └── lib.rs

Creating a Transform

A plugin contains one or more elements. register_plugin! must name all the elements in a plugin. In this example we are creating a single element, ex_drag, so we write register_plugin!("ex_drag");. If the plugin had more elements, you would list them all in the macro. Besides declaring the elements, register_plugin! sets up the message bus between them and lets the plugin use the same logger as physim.

Each element is defined by adding a macro to a struct. Because we are making a transform element, we need the transform_element macro. This macro also takes a short description which is used by physcan to show users what the element does. With register_plugin! and the transform_element macro, physim can load your element.

register_plugin!("ex_drag");

#[transform_element(
    name = "ex_drag",
    blurb = "Applies a drag force which scales with the square of velocity"
)]
pub struct Drag {
    alpha: f64,
}

A simple model of acceleration due to drag is to scale it with the square of the entity’s velocity. The acceleration, \(\vec{A}\), can be expressed as \[ \vec{A} = \frac{- \alpha v^2 }{m} \frac{\vec{v}}{\left|\vec{v}\right|} \] where \(\vec{v}\) is velocity, \(\alpha\) is the coefficient of drag and \(m\) is the mass of the entity.

To implement this force, you need to implement the TransformElement for Drag.

#![allow(unused)]
fn main() {
impl TransformElement for Drag {
    fn transform(&self, state: &[Entity], accelerations: &mut [Acceleration]) {
        todo!()
    }
    
    fn new(properties: HashMap<String, Value>) -> Self {
        todo!()
    }

    fn get_property_descriptions(&self) -> HashMap<String, String> {
        todo!()
    }
}
}

On each step of the simulation, the state of the system will be passed to this element. A transform in physim has read-only access to each entity in the simulation. Transforms use this state to calculate an acceleration to apply to each entity. You can use the velocity and mass of each entity to calculate how to update the entity’s acceleration.

    fn transform(&self, state: &[Entity], accelerations: &mut [Acceleration]) {
        for (acc, entity) in accelerations.iter_mut().zip(state) {
            *acc += Acceleration {
                x: -self.alpha * entity.vx * entity.vx.abs() / entity.mass,
                y: -self.alpha * entity.vy * entity.vy.abs() / entity.mass,
                z: -self.alpha * entity.vz * entity.vz.abs() / entity.mass,
            };
        }
    }

new is called when an instance of the element is being created by physim. The element’s configuration comes as a hash map, and this can be parsed with serde. get_property_descriptions serves purely as documentation for your plugin.

    fn new(properties: HashMap<String, Value>) -> Self {
        Drag {
            alpha: properties
                .get("alpha")
                .and_then(|x| x.as_f64())
                .unwrap_or(0.0),
        }
    }

    fn get_property_descriptions(&self) -> HashMap<String, String> {
        HashMap::from([(String::from("alpha"), String::from("Coefficient of drag"))])
    }

Finally, you should implement the MessageClient trait. We aren’t interested in using physim’s inter-element communication bus, so you can leave it empty.

Running cargo build -r will generate a dynamic library. Place this library in the same directory as your physim installation and you will be able to include it in your simulations, e.g.

$ physim ex_drag alpha=0.01 ! cube n=1000 seed=2 a=2.0 ! \
    simple_astro ! rk4 ! glrender ! global dt=0.01 iterations=2500

The whole plugin

#![feature(str_from_raw_parts)]
use std::collections::HashMap;

use serde_json::Value;

use physim_attribute::transform_element;
use physim_core::messages::MessageClient;
use physim_core::plugin::transform::TransformElement;
use physim_core::register_plugin;
use physim_core::{Acceleration, Entity};

// ANCHOR: element_declaration
register_plugin!("ex_drag");

#[transform_element(
    name = "ex_drag",
    blurb = "Applies a drag force which scales with the square of velocity"
)]
pub struct Drag {
    alpha: f64,
}
// ANCHOR_END: element_declaration

impl TransformElement for Drag {
    // ANCHOR: element_transform
    fn transform(&self, state: &[Entity], accelerations: &mut [Acceleration]) {
        for (acc, entity) in accelerations.iter_mut().zip(state) {
            *acc += Acceleration {
                x: -self.alpha * entity.vx * entity.vx.abs() / entity.mass,
                y: -self.alpha * entity.vy * entity.vy.abs() / entity.mass,
                z: -self.alpha * entity.vz * entity.vz.abs() / entity.mass,
            };
        }
    }
    // ANCHOR_END: element_transform
    // ANCHOR: element_props
    fn new(properties: HashMap<String, Value>) -> Self {
        Drag {
            alpha: properties
                .get("alpha")
                .and_then(|x| x.as_f64())
                .unwrap_or(0.0),
        }
    }

    fn get_property_descriptions(&self) -> HashMap<String, String> {
        HashMap::from([(String::from("alpha"), String::from("Coefficient of drag"))])
    }
    // ANCHOR_END: element_props
}

impl MessageClient for Drag {}