Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ members = [
"flockers_mpi",
"forestfire",
"forestfire_bayesian",
"n_bodies",
"schelling",
"sir_ga_exploration",
"sugarscape",
Expand Down
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@ This repository hosts working examples of agent-based simulations, based on the
| [Flockers](flockers) | :heavy_check_mark: | <img src="flockers/flock.gif" alt="drawing" width="200" height="200"/> |
| [ForestFire](forestfire) | :heavy_check_mark: | <img src="forestfire/ff.gif" alt="drawing" width="200" height="200"/> |
| [ForestFire_bayesian](forestfire_bayesian) | :heavy_check_mark: | :x: |
| [N Bodies](n_bodies) | :heavy_check_mark: | <img src="n_bodies/nbodies1.gif" alt="drawing" width="200" height="200"/> |
| [Schelling](schelling) | :heavy_check_mark: | <img src="schelling/schelling.gif" alt="drawing" width="200" height="200"/> |
| [Sir_bayesian](sir_bayesian) | :heavy_check_mark: | :x: |
| [Sir_ga_exploration](sir_ga_exploration) | :heavy_check_mark: | :x: |
| [Sugarscape](sugarscape) | :heavy_check_mark: | <img src="sugarscape/sugarscape.gif" alt="drawing" width="200" height="200"/> |
| [Sir_bayesian](sir_bayesian) | :heavy_check_mark: | :x: |
| [Sir_ga_exploration](sir_ga_exploration) | :heavy_check_mark: | :x: |
| [Sugarscape](sugarscape) | :heavy_check_mark: | <img src="sugarscape/sugarscape.gif" alt="drawing" width="200" height="200"/> |
| [Template](template) | :heavy_check_mark: | <img src="template/template.gif" alt="drawing" width="200" height="200"/> |
| [Virus on a Network](virusnetwork) | :heavy_check_mark: | <img src="virusnetwork/virus.gif" alt="drawing" width="200" height="200"/> |
| [Wolf Sheep Grass](wolfsheepgrass) | :heavy_check_mark: | <img src="wolfsheepgrass/wsg.gif" alt="drawing" width="200" height="200"/> |
4 changes: 4 additions & 0 deletions n_bodies/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/target
Cargo.lock
.idea
/output
19 changes: 19 additions & 0 deletions n_bodies/Cargo.toml
Comment thread
C-JeanDev marked this conversation as resolved.
Resolved
Comment thread
C-JeanDev marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "n_bodies"
version = "0.1.0"
authors = [
"Jean Claude Coppola <[email protected]>"
]
edition = "2021"
license = "MIT"
readme = "README.md"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
krabmaga = "0.6.*"
bevy_egui = { version = "0.39", optional = true }

[features]
visualization = ["krabmaga/visualization", "bevy_egui"]
visualization_wasm = ["krabmaga/visualization_wasm", "bevy_egui"]
49 changes: 49 additions & 0 deletions n_bodies/Makefile.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
[env]
ENV_DIR = {source="${CARGO_MAKE_PROFILE}", default_value = "debug", mapping = {release = "release"}}
CARGO_TARGET_DIR = {value = "target", condition = {env_not_set = ["CARGO_TARGET_DIR"]}}
CARGO_WASM_PATH = "${CARGO_TARGET_DIR}/wasm32-unknown-unknown/${ENV_DIR}/${CARGO_MAKE_CRATE_NAME}.wasm"
CARGO_PROFILE = "dev"
TARGET_DIR = "target"
CARGO_MANIFEST_DIR = "${CARGO_MAKE_WORKING_DIRECTORY}"

[env.release]
CARGO_RELEASE_ARGS = "--release"

[tasks.show-env]
command = "env"

[tasks.install-target-wasm32-unknown-unknown]
args = ["target", "install", "wasm32-unknown-unknown"]

command = "rustup"
[tasks.basic-http-server]
install_crate = {crate_name = "basic-http-server", binary = "basic-http-server", test_arg="--help"}

[tasks.wasm-bindgen-cli]
install_crate = {crate_name = "wasm-bindgen-cli", binary = "wasm-bindgen", test_arg="--help"}

[tasks.cargo-build-web]
args = ["build", "--target", "wasm32-unknown-unknown", "--features", "visualization_wasm", "@@split(CARGO_RELEASE_ARGS, )"]
command = "cargo"
dependencies = ["install-target-wasm32-unknown-unknown"]

[tasks.build-web]
args = ["--out-dir", "${TARGET_DIR}", "--out-name", "wasm", "--target", "web", "--no-typescript", "${CARGO_WASM_PATH}"]
command = "wasm-bindgen"
dependencies = ["cargo-build-web", "wasm-bindgen-cli"]

[tasks.build-native]
args = ["build", "--features", "visualization", "@@split(CARGO_RELEASE_ARGS, )"]
command = "cargo"

[tasks.run]
command = "${CARGO_TARGET_DIR}/${ENV_DIR}/${CARGO_MAKE_BINARY_EXECUTABLE_NAME}"
dependencies = ["build-native"]

[tasks.serve]
command = "basic-http-server"
args = ["-x"]
dependencies = ["build-web", "basic-http-server"]

[tasks.test]
disabled = true
127 changes: 127 additions & 0 deletions n_bodies/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# N-Bodies

A 2D gravitational N-body simulation written in Rust with the
[krABMaga](https://github.com/krABMaga/krABMaga) agent-based modeling framework
(Bevy-based visualization).
[N-Bodies](https://www.netlogoweb.org/launch#http://ccl.northwestern.edu/netlogo/models/models/Sample%20Models/Chemistry%20&%20Physics/Mechanics/Unverified/N-Bodies.nlogox)
model.

![](nbodies1.gif)
![](nbodies2.gif)
![](nbodies3.gif)

## Features

- Every body is a krABMaga **agent** attracted by all the others (Newtonian gravity, O(N²)).
- **Trails**: the path of the first *N* bodies is drawn as fading polylines
(Bevy gizmos, no extra sprites), colored like the body they belong to.
- **Control window** (egui) to change the parameters live or regenerate the system.
- Four **initial configurations**: star with circular orbits, stable two-planet
system, random with zero momentum, symmetrical (zero-sum).
- Optional **keep centered** mode that pins the center of mass to the middle of the view.
- Negative `G` turns gravity into a repulsive force.

## Requirements

- Rust (stable) with `cargo`
- The system libraries Bevy needs on your platform
(see the [Bevy setup guide](https://bevy.org/learn/quick-start/getting-started/setup/))

## Run

```bash
# Visualization (window with the simulation and the menus)
cargo run --release --features visualization

# Headless (no window, runs 1000 steps)
cargo run --release
```
Press **▶ Start** in the left panel to begin.

`Cargo.toml` needs the egui dependency used by the menu:

```toml
[dependencies]
krabmaga = "0.6"
bevy_egui = { version = "0.39", optional = true }

[features]
visualization = ["krabmaga/visualization", "bevy_egui"]
visualization_wasm = ["krabmaga/visualization_wasm", "bevy_egui"]
```

Use the same `bevy_egui` version as krABMaga (check with
`cargo tree --features visualization -i bevy_egui`: only one version must appear).

## How it works

**Physics.** For each body *i* the acceleration is

```
a_i = Σ_j G · m_j · (r_j − r_i) / (|r_j − r_i|² + ε²)^(3/2)
```

where `ε` is the *softening* length, which avoids huge forces in close
encounters. Integration uses the semi-implicit (symplectic) Euler method:
`v ← v + a·dt`, then `x ← x + v·dt` with the new `v`.

**Synchronous update.** All the physical data live in `NBodyState`
(single source of truth). During a step each `Body` agent reads the snapshot
`state.bodies` and writes its new data in `state.next`; in `after_step` the
state copies `next` into `bodies`. Every body therefore sees the same instant,
independently of the agents' execution order. The same hook recenters the
system (if enabled) and records the trail points.

**Rendering.** Sprites read their position and size from the state every
frame. The planets are tinted with a per-body color (`body_color`), and the
trail of a body uses the same color with a fading alpha.

## Menu

The default krABMaga panel (left) provides Start / Pause / Stop and the steps
per second. **Stop** regenerates the system using the current parameters.
The floating **N-Bodies** window adds:

| Parameter | Effect | Applied |
|---|---|---|
| Configuration | Initial layout (see below) | Setup |
| Number of bodies | Total number of bodies (2–300) | Setup |
| Central mass | Mass of the star (or mass scale in random setups) | Setup |
| Max planet mass | Upper bound of planet masses | Setup |
| Initial speed | Maximum initial speed in random setups | Setup |
| Bodies with trail | How many bodies get a trail | Setup |
| G | Gravitational constant (negative = repulsive) | Live |
| dt | Integration time step | Live |
| Softening | Smoothing length ε of the force | Live |
| Keep centered | Keep the center of mass in the middle of the view | Live |
| Show trails / Trail length | Toggle and length of the trails | Live |

The window also shows how many bodies are currently **outside the view**.
After pressing **Setup** the simulation is paused: press ▶ Start again.

### Configurations

| Name | Description |
|---|---|
| Star + circular orbits | Heavy star at the center, planets on a spiral (golden angle) with circular-orbit speed |
| Two planets (stable) | Star and two planets on opposite sides, at different radii |
| Random (zero momentum) | Random positions, velocities and masses; net momentum removed so the system doesn't drift |
| Symmetrical (zero-sum) | Half of the bodies are random, the other half are their mirror image (opposite position and velocity) |

## Tips

- Bodies that "escape" are usually the effect of close encounters: increase
*Softening*, decrease *dt*, or lower *Max planet mass*.
- If nothing seems to move, check that the simulation is started and try a
larger `G` or `dt`.
- The simulation area is `980 × 720`: Bevy's default window is 1280 × 720 and
krABMaga reserves 300 px on the left for its panel, so this keeps the scale
1:1. If you resize the window the scene is stretched accordingly.
- Sprites: `Emoji("sun")` and `Emoji("planet")` are loaded from
`assets/emojis/sun.png` and `assets/emojis/planet.png`. If you change the
image size, update `SPRITE_PX` in `visualization/body_vis.rs`.
- The default values (`G = 100`, `dt = 0.05`, softening `5`) are defined in
`Params::default()` in `model/state.rs`.

## References
- http://ccl.northwestern.edu/netlogo/models/N-Bodies
Binary file added n_bodies/assets/emojis/planet.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added n_bodies/assets/emojis/sun.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added n_bodies/assets/fonts/FiraSans-Bold.ttf
Binary file not shown.
26 changes: 26 additions & 0 deletions n_bodies/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<html>
<head>
<meta charset="utf-8"/>
<style>
body {
background: linear-gradient(
135deg,
white 0%,
white 49%,
black 49%,
black 51%,
white 51%,
white 100%
) repeat;
background-size: 20px 20px;
}
canvas {
background-color: white;
}
</style>
</head>
<script type="module">
import init from './target/wasm.js'
init()
</script>
</html>
Binary file added n_bodies/nbodies1.gif
Comment thread
C-JeanDev marked this conversation as resolved.
Resolved
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added n_bodies/nbodies2.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added n_bodies/nbodies3.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
55 changes: 55 additions & 0 deletions n_bodies/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
mod model;
#[cfg(any(feature = "visualization", feature = "visualization_wasm"))]
mod visualization;

// Simulation area. The default window of Bevy is 1280x720 and krABMaga
// reserves 300 px on the left for its panel, so (1280-300) x 720 gives a
// 1:1 scale (circles stay circles).
const DIM: (f32, f32) = (980.0, 720.0);

#[cfg(not(any(feature = "visualization", feature = "visualization_wasm")))]
use krabmaga::*;

#[cfg(not(any(feature = "visualization", feature = "visualization_wasm")))]
fn main() {
use crate::model::state::{NBodyState, SetupMode};

let mut state = NBodyState::new(DIM); // starts from Params::default()

// Change whatever you want here, like the menu does in the window...
state.p.mode = SetupMode::Random;
state.p.number = 100;
state.p.g = 100.0;
state.p.dt = 0.05;

// ...then regenerate the bodies: setup() reads the parameters above.
state.setup();

// simulate!(state, steps, repetitions)
simulate!(state, 1000, 1);
}

#[cfg(any(feature = "visualization", feature = "visualization_wasm"))]
fn main() {
use bevy_egui::EguiPrimaryContextPass;
use krabmaga::bevy::prelude::{Color, Update};
use krabmaga::visualization::visualization::Visualization;

use crate::model::state::NBodyState;
use crate::visualization::{menu::menu_system, trails::draw_trails, vis_state::NBodyVis};

let state = NBodyState::new(DIM);

// `setup` (instead of `start`) returns the Bevy App, so we can add our own systems.
let mut app = Visualization::default()
.with_window_dimensions(1280., 720.)
.with_simulation_dimensions(DIM.0, DIM.1)
.with_background_color(Color::srgb(0.02, 0.02, 0.08))
.with_name("N-Bodies")
.setup::<NBodyVis, NBodyState>(NBodyVis, state);

app.add_systems(Update, draw_trails)
.add_systems(EguiPrimaryContextPass, menu_system);

app.run();
}
67 changes: 67 additions & 0 deletions n_bodies/src/model/body.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
use krabmaga::engine::agent::Agent;
use krabmaga::engine::location::Real2D;
use krabmaga::engine::state::State;

use crate::model::state::NBodyState;

//Physical Data of one body -> it lives in the state so that it can be modified and read by the menu and visualization
#[derive(Clone, Copy)]
pub struct BodyData {
pub mass: f32,
pub radius: f32,
pub pos: Real2D,
pub vel: Real2D,
pub is_star: bool,
}

/// The agent only knows its index: it reads the snapshot `state.bodies`
/// (positions at the beginning of the step) and writes its new data into
/// `state.next`. `NBodyState::after_step` then publishes `next` -> `bodies`,
/// so every body sees the same instant (synchronous update).
#[derive(Clone, Copy)]
pub struct Body {
pub id: u32,
}

impl Agent for Body {
fn step(&mut self, state: &mut dyn State) {
let s = state
.as_any_mut()
.downcast_mut::<NBodyState>()
.expect("state is not NBodyState");

let i = self.id as usize;
let me = s.bodies[i];
let eps2 = s.p.softening * s.p.softening;

// Acceleration = sum_j G * m_j * (r_j - r_i) / (|r|^2 + eps^2)^(3/2)
// (my own mass cancels out: F = m*a)
let mut ax = 0.0f32;
let mut ay = 0.0f32;

for (j, other) in s.bodies.iter().enumerate() {
if j == i {
continue;
}
let dx = other.pos.x - me.pos.x;
let dy = other.pos.y - me.pos.y;
let d2 = dx * dx + dy * dy + eps2;
if d2 < 1e-9 {
continue;
}
let k = s.p.g * other.mass / (d2 * d2.sqrt());
ax += k * dx;
ay += k * dy;
}

// Semi-implicit Euler: v first, then x with the NEW v.
let dt = s.p.dt;
let mut new = me;
new.vel.x += ax * dt;
new.vel.y += ay * dt;
new.pos.x += new.vel.x * dt;
new.pos.y += new.vel.y * dt;

s.next[i] = new;
}
}
2 changes: 2 additions & 0 deletions n_bodies/src/model/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod body;
pub mod state;
Loading
Loading