Support multiple populations.
This commit is contained in:
166
src/model.rs
166
src/model.rs
@@ -1,7 +1,9 @@
|
||||
use crate::grid::Grid;
|
||||
use crate::grid::{combine, Grid, PopulationConfig};
|
||||
|
||||
use rand::{seq::SliceRandom, Rng};
|
||||
use rand_distr::{Distribution, Normal};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use std::f32::consts::TAU;
|
||||
|
||||
/// A single Physarum agent. The x and y positions are continuous, hence we use floating point
|
||||
@@ -11,16 +13,18 @@ struct Agent {
|
||||
x: f32,
|
||||
y: f32,
|
||||
angle: f32,
|
||||
population_id: usize,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
/// Construct a new agent with random parameters.
|
||||
fn new<R: Rng + ?Sized>(width: usize, height: usize, rng: &mut R) -> Self {
|
||||
fn new<R: Rng + ?Sized>(width: usize, height: usize, id: usize, rng: &mut R) -> Self {
|
||||
let (x, y, angle) = rng.gen::<(f32, f32, f32)>();
|
||||
Agent {
|
||||
x: x * width as f32,
|
||||
y: y * height as f32,
|
||||
angle: angle * TAU,
|
||||
population_id: id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,50 +45,6 @@ impl Agent {
|
||||
}
|
||||
}
|
||||
|
||||
/// A model configuration. We make it into a separate type, because we will eventually have multiple
|
||||
/// configurations in one model.
|
||||
#[derive(Debug)]
|
||||
pub struct PopulationConfig {
|
||||
sensor_distance: f32,
|
||||
step_distance: f32,
|
||||
decay_factor: f32,
|
||||
sensor_angle: f32,
|
||||
rotation_angle: f32,
|
||||
deposition_amount: f32,
|
||||
}
|
||||
|
||||
impl PopulationConfig {
|
||||
const SENSOR_ANGLE_MIN: f32 = 0.0;
|
||||
const SENSOR_ANGLE_MAX: f32 = 120.0;
|
||||
const SENSOR_DISTANCE_MIN: f32 = 0.0;
|
||||
const SENSOR_DISTANCE_MAX: f32 = 64.0;
|
||||
const ROTATION_ANGLE_MIN: f32 = 0.0;
|
||||
const ROTATION_ANGLE_MAX: f32 = 120.0;
|
||||
const STEP_DISTANCE_MIN: f32 = 0.2;
|
||||
const STEP_DISTANCE_MAX: f32 = 2.0;
|
||||
const DEPOSITION_AMOUNT_MIN: f32 = 5.0;
|
||||
const DEPOSITION_AMOUNT_MAX: f32 = 5.0;
|
||||
const DECAY_FACTOR_MIN: f32 = 0.1;
|
||||
const DECAY_FACTOR_MAX: f32 = 0.1;
|
||||
|
||||
/// Construct a random configuration.
|
||||
pub fn new<R: Rng + ?Sized>(rng: &mut R) -> Self {
|
||||
PopulationConfig {
|
||||
sensor_distance: rng.gen_range(Self::SENSOR_DISTANCE_MIN..=Self::SENSOR_DISTANCE_MAX),
|
||||
step_distance: rng.gen_range(Self::STEP_DISTANCE_MIN..=Self::STEP_DISTANCE_MAX),
|
||||
decay_factor: rng.gen_range(Self::DECAY_FACTOR_MIN..=Self::DECAY_FACTOR_MAX),
|
||||
sensor_angle: rng
|
||||
.gen_range(Self::SENSOR_ANGLE_MIN..=Self::SENSOR_ANGLE_MAX)
|
||||
.to_radians(),
|
||||
rotation_angle: rng
|
||||
.gen_range(Self::ROTATION_ANGLE_MIN..=Self::ROTATION_ANGLE_MAX)
|
||||
.to_radians(),
|
||||
deposition_amount: rng
|
||||
.gen_range(Self::DEPOSITION_AMOUNT_MIN..=Self::DEPOSITION_AMOUNT_MAX),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level simulation class.
|
||||
#[derive(Debug)]
|
||||
pub struct Model {
|
||||
@@ -92,32 +52,71 @@ pub struct Model {
|
||||
agents: Vec<Agent>,
|
||||
|
||||
// The grid they move on.
|
||||
grid: Grid,
|
||||
grids: Vec<Grid>,
|
||||
|
||||
// Simulation parameters.
|
||||
// Attraction table governs interaction across populations
|
||||
attraction_table: Vec<Vec<f32>>,
|
||||
|
||||
// Global grid diffusivity.
|
||||
diffusivity: usize,
|
||||
pub config: PopulationConfig,
|
||||
|
||||
// Current model iteration.
|
||||
iteration: i32,
|
||||
width: usize,
|
||||
height: usize,
|
||||
}
|
||||
|
||||
impl Model {
|
||||
const ATTRACTION_FACTOR_MEAN: f32 = 1.0;
|
||||
const ATTRACTION_FACTOR_STD: f32 = 0.1;
|
||||
const REPULSION_FACTOR_MEAN: f32 = -1.0;
|
||||
const REPULSION_FACTOR_STD: f32 = 0.1;
|
||||
|
||||
pub fn print_configurations(&self) {
|
||||
for (i, grid) in self.grids.iter().enumerate() {
|
||||
println!("Grid {}: {}", i, grid.config);
|
||||
}
|
||||
println!("Attraction table: {:#?}", self.attraction_table);
|
||||
}
|
||||
|
||||
/// Construct a new model with random initial conditions and random configuration.
|
||||
pub fn new(width: usize, height: usize, n_particles: usize, diffusivity: usize) -> Self {
|
||||
pub fn new(
|
||||
width: usize,
|
||||
height: usize,
|
||||
n_particles: usize,
|
||||
n_populations: usize,
|
||||
diffusivity: usize,
|
||||
) -> Self {
|
||||
let particles_per_grid = (n_particles as f64 / n_populations as f64).ceil() as usize;
|
||||
let n_particles = particles_per_grid * n_populations;
|
||||
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
let attraction_distr =
|
||||
Normal::new(Self::ATTRACTION_FACTOR_MEAN, Self::ATTRACTION_FACTOR_STD).unwrap();
|
||||
let repulstion_distr =
|
||||
Normal::new(Self::REPULSION_FACTOR_MEAN, Self::REPULSION_FACTOR_STD).unwrap();
|
||||
|
||||
let mut attraction_table = Vec::with_capacity(n_populations);
|
||||
for i in 0..n_populations {
|
||||
attraction_table.push(Vec::with_capacity(n_populations));
|
||||
for j in 0..n_populations {
|
||||
attraction_table[i].push(if i == j {
|
||||
attraction_distr.sample(&mut rng)
|
||||
} else {
|
||||
repulstion_distr.sample(&mut rng)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Model {
|
||||
agents: (0..n_particles)
|
||||
.map(|_| Agent::new(width, height, &mut rng))
|
||||
.map(|i| Agent::new(width, height, i / particles_per_grid, &mut rng))
|
||||
.collect(),
|
||||
grid: Grid::new(width, height),
|
||||
grids: (0..n_populations)
|
||||
.map(|_| Grid::new(width, height, &mut rng))
|
||||
.collect(),
|
||||
attraction_table,
|
||||
diffusivity,
|
||||
config: PopulationConfig::new(&mut rng),
|
||||
iteration: 0,
|
||||
width,
|
||||
height,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,19 +136,22 @@ impl Model {
|
||||
|
||||
/// Perform a single simulation step.
|
||||
pub fn step(&mut self) {
|
||||
// To avoid borrow-checker errors inside the parallel loop.
|
||||
let PopulationConfig {
|
||||
sensor_distance,
|
||||
sensor_angle,
|
||||
rotation_angle,
|
||||
step_distance,
|
||||
..
|
||||
} = self.config;
|
||||
let (width, height) = (self.width, self.height);
|
||||
let grid = &self.grid;
|
||||
// Combine grids
|
||||
let grids = &mut self.grids;
|
||||
let attraction_table = &self.attraction_table;
|
||||
combine(grids, attraction_table);
|
||||
|
||||
self.agents.par_iter_mut().for_each(|agent| {
|
||||
let mut rng = rand::thread_rng();
|
||||
let grid = &grids[agent.population_id];
|
||||
let PopulationConfig {
|
||||
sensor_distance,
|
||||
sensor_angle,
|
||||
rotation_angle,
|
||||
step_distance,
|
||||
..
|
||||
} = grid.config;
|
||||
let (width, height) = (grid.width, grid.height);
|
||||
|
||||
let xc = agent.x + agent.angle.cos() * sensor_distance;
|
||||
let yc = agent.y + agent.angle.sin() * sensor_distance;
|
||||
let xl = agent.x + (agent.angle - sensor_angle).cos() * sensor_distance;
|
||||
@@ -158,34 +160,38 @@ impl Model {
|
||||
let yr = agent.y + (agent.angle + sensor_angle).sin() * sensor_distance;
|
||||
|
||||
// Sense
|
||||
let trail_c = grid.get(xc, yc);
|
||||
let trail_l = grid.get(xl, yl);
|
||||
let trail_r = grid.get(xr, yr);
|
||||
let trail_c = grid.get_buf(xc, yc);
|
||||
let trail_l = grid.get_buf(xl, yl);
|
||||
let trail_r = grid.get_buf(xr, yr);
|
||||
|
||||
// Rotate and move
|
||||
let mut rng = rand::thread_rng();
|
||||
let direction = Model::pick_direction(trail_c, trail_l, trail_r, &mut rng);
|
||||
agent.rotate_and_move(direction, rotation_angle, step_distance, width, height);
|
||||
});
|
||||
|
||||
// Deposit
|
||||
for agent in self.agents.iter() {
|
||||
self.grid
|
||||
.add(agent.x, agent.y, self.config.deposition_amount);
|
||||
self.grids[agent.population_id].deposit(agent.x, agent.y);
|
||||
}
|
||||
|
||||
// Diffuse + Decay
|
||||
self.grid
|
||||
.diffuse(self.diffusivity, self.config.decay_factor);
|
||||
let diffusivity = self.diffusivity;
|
||||
self.grids.par_iter_mut().for_each(|grid| {
|
||||
grid.diffuse(diffusivity);
|
||||
});
|
||||
self.iteration += 1;
|
||||
}
|
||||
|
||||
/// Output the current trail layer as a grayscale image.
|
||||
pub fn save_to_image(&self, name: &str) {
|
||||
let mut img = image::GrayImage::new(self.width as u32, self.height as u32);
|
||||
let max_value = self.grid.quantile(0.999);
|
||||
let mut img =
|
||||
image::GrayImage::new(self.grids[0].width as u32, self.grids[0].height as u32);
|
||||
let max_value = self.grids[0].quantile(0.999);
|
||||
|
||||
for (i, value) in self.grid.data().iter().enumerate() {
|
||||
let x = (i % self.width) as u32;
|
||||
let y = (i / self.width) as u32;
|
||||
for (i, value) in self.grids[0].data().iter().enumerate() {
|
||||
let x = (i % self.grids[0].width) as u32;
|
||||
let y = (i / self.grids[0].width) as u32;
|
||||
let c = (value / max_value).clamp(0.0, 1.0) * 255.0;
|
||||
img.put_pixel(x, y, image::Luma([c as u8]));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user