Compare commits

...

No commits in common. "main" and "rust-sticking-probability" have entirely different histories.

25 changed files with 4482 additions and 531 deletions

9
.drone.yml Normal file
View File

@ -0,0 +1,9 @@
kind: pipeline
name: default
steps:
- name: test
image: rust:1.67
commands:
- cargo build --verbose --all
- cargo test --verbose --all

8
.gitignore vendored
View File

@ -1,9 +1,3 @@
# Final executable
/run
# IDE files
/target
.idea
# Data files
*.csv
*.o

4
AIM.md Normal file
View File

@ -0,0 +1,4 @@
Make a gridded structure which can support:
- 3D
- Hexagons

3683
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

43
Cargo.toml Normal file
View File

@ -0,0 +1,43 @@
[package]
name = "rust-codebase"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
name = "dla"
crate-type = ["staticlib"]
path = "src/clib.rs"
[[bin]]
name = "model"
path = "src/main.rs"
[[bin]]
name = "ui"
path = "src/ui.rs"
# Set the default for crate.
[profile.dev]
opt-level = 1
# Set the default for crate.
[profile.release]
debug = true
# Set the default for dependencies.
[profile.dev.package."*"]
opt-level = 3
[dependencies]
clap = { version = "4.1.8", features = ["derive"] }
bevy = { version = "0.9.1" }
nd_array = "0.1.0"
num-integer = "0.1.45"
rand = { version = "0.8.5", features = ["default", "small_rng"] }
csv = "1.1"
serde = { version = "1.0.152", features = ["derive"] }
[build-dependencies]
cbindgen = "0.24.3"

View File

@ -1,244 +0,0 @@
//
// DLASystem.cpp
//
#include "DLASystem.h"
// this function gets called every step,
// if there is an active particle then it gets moved,
// if not then add a particle
void DLASystem::update() {
if (lastParticleIsActive == 1) {
moveLastParticle();
} else if (numParticles < endNum) {
addParticleOnAddCircle();
setParticleActive();
} else {
this->running = false;
}
}
void DLASystem::clearParticles() {
// delete particles and the particle list
for (int i = 0; i < numParticles; i++) {
delete particleList[i];
}
particleList.clear();
numParticles = 0;
}
// remove any existing particles and setup initial condition
void DLASystem::Reset() {
// stop running
this->running = false;
clearParticles();
lastParticleIsActive = 0;
// set the grid to zero
for (int i = 0; i < gridSize; i++) {
for (int j = 0; j < gridSize; j++) {
grid[i][j] = 0;
}
}
// setup initial condition and parameters
addCircle = 10;
killCircle = 2.0 * addCircle;
clusterRadius = 0.0;
// add a single particle at the origin
double pos[] = {0.0, 0.0};
addParticle(pos);
}
// set the value of a grid cell for a particular position
// note the position has the initial particle at (0,0)
// but this corresponds to the middle of the grid array ie grid[ halfGrid ][ halfGrid ]
void DLASystem::setGrid(double pos[], int val) {
int halfGrid = gridSize / 2;
grid[(int) (pos[0] + halfGrid)][(int) (pos[1] + halfGrid)] = val;
}
// read the grid cell for a given position
int DLASystem::readGrid(double pos[]) {
int halfGrid = gridSize / 2;
return grid[(int) (pos[0] + halfGrid)][(int) (pos[1] + halfGrid)];
}
// check if the cluster is big enough and we should stop:
// to be safe, we need the killCircle to be at least 2 less than the edge of the grid
int DLASystem::checkStop() {
if (killCircle + 2 >= gridSize / 2) {
this->running = false;
cerr << "STOP" << endl;
return 1;
} else return 0;
}
// add a particle to the system at a specific position
void DLASystem::addParticle(double pos[]) {
// create a new particle
Particle *p = new Particle(pos);
// push_back means "add this to the end of the list"
particleList.push_back(p);
numParticles++;
// pos coordinates should be -gridSize/2 < x < gridSize/2
setGrid(pos, 1);
}
// add a particle to the system at a random position on the addCircle
// if we hit an occupied site then we do nothing except print a message
// (this should never happen)
void DLASystem::addParticleOnAddCircle() {
double pos[2];
double theta = rgen.random01() * 2 * M_PI;
pos[0] = ceil(addCircle * cos(theta));
pos[1] = ceil(addCircle * sin(theta));
if (readGrid(pos) == 0)
addParticle(pos);
else
cerr << "FAIL " << pos[0] << " " << pos[1] << endl;
}
// send back the position of a neighbour of a given grid cell
// NOTE: there is no check that the neighbour is inside the grid,
// this has to be done separately...
void DLASystem::setPosNeighbour(double setpos[], double pos[], int val) {
switch (val) {
case 0:
setpos[0] = pos[0] + 1.0;
setpos[1] = pos[1];
break;
case 1:
setpos[0] = pos[0] - 1.0;
setpos[1] = pos[1];
break;
case 2:
setpos[0] = pos[0];
setpos[1] = pos[1] + 1.0;
break;
case 3:
setpos[0] = pos[0];
setpos[1] = pos[1] - 1.0;
break;
}
}
// when we add a particle to the cluster, we should update the cluster radius
// and the sizes of the addCircle and the killCircle
void DLASystem::updateClusterRadius(double pos[]) {
double rr = distanceFromOrigin(pos);
if (rr > clusterRadius) {
clusterRadius = rr;
// this is how big addCircle is supposed to be:
// either 20% more than cluster radius, or at least 5 bigger.
double check = clusterRadius * addRatio;
if (check < clusterRadius + 5)
check = clusterRadius + 5;
// if it is smaller then update everything...
if (addCircle < check) {
addCircle = check;
killCircle = killRatio * addCircle;
}
checkStop();
}
}
// make a random move of the last particle in the particleList
void DLASystem::moveLastParticle() {
int rr = rgen.randomInt(4); // pick a random number in the range 0-3, which direction do we hop?
double newpos[2];
Particle *lastP = particleList[numParticles - 1];
setPosNeighbour(newpos, lastP->pos, rr);
if (distanceFromOrigin(newpos) > killCircle) {
//cerr << "#deleting particle" << endl;
setGrid(lastP->pos, 0);
particleList.pop_back(); // remove particle from particleList
numParticles--;
setParticleInactive();
} else if (readGrid(newpos) == 0) {
setGrid(lastP->pos, 0); // set the old grid site to empty
// update the position
particleList[numParticles - 1]->pos[0] = newpos[0];
particleList[numParticles - 1]->pos[1] = newpos[1];
setGrid(lastP->pos, 1); // set the new grid site to be occupied
// check if we stick
if (checkStick()) {
//cerr << "stick" << endl;
setParticleInactive(); // make the particle inactive (stuck)
updateClusterRadius(lastP->pos); // update the cluster radius, addCircle, etc.
}
}
}
// check if the last particle should stick (to a neighbour)
int DLASystem::checkStick() {
Particle *lastP = particleList[numParticles - 1];
// loop over neighbours
for (int i = 0; i < 4; i++) {
double checkpos[2];
setPosNeighbour(checkpos, lastP->pos, i);
// if the neighbour is occupied and the particle will stick probabilistically.
if (readGrid(checkpos) == 1 && rgen.random01() < stickProbability) {
return 1;
}
}
return 0;
}
// constructor
DLASystem::DLASystem(const int maxParticles, const string &csvPath, const double stickProbability) {
cerr << "creating system, gridSize " << gridSize << endl;
numParticles = 0;
endNum = maxParticles;
this->stickProbability = stickProbability;
// allocate memory for the grid, remember to free the memory in destructor
grid = new int *[gridSize];
for (int i = 0; i < gridSize; i++) {
grid[i] = new int[gridSize];
}
// reset initial parameters
Reset();
addRatio = 1.2; // how much bigger the addCircle should be, compared to cluster radius
killRatio = 1.7; // how much bigger is the killCircle, compared to the addCircle
csv.open(csvPath);
}
// destructor
DLASystem::~DLASystem() {
// strictly we should not print inside the destructor but never mind...
cerr << "deleting system" << endl;
// delete the particles
clearParticles();
// delete the grid
for (int i = 0; i < gridSize; i++)
delete[] grid[i];
delete[] grid;
if (csv.is_open()) {
csv.flush();
csv.close();
}
}
void DLASystem::exportData() {
csv << "x,y" << endl;
for (auto particle: particleList) {
csv << particle->pos[0] << "," << particle->pos[1] << endl;
}
}

View File

@ -1,122 +0,0 @@
#pragma once
#include <iostream>
#include <fstream>
#include <stdio.h>
#include <vector>
#define _USE_MATH_DEFINES
#include <math.h>
#include <random>
#include <string>
#include <sstream>
#include "Particle.h"
#include "rnd.h"
using namespace std;
class DLASystem {
private:
// these are private variables and functions that the user will not see
// list of particles
vector<Particle*> particleList;
int numParticles;
/*
* The probability that a particle will stick at a given site when adjacent.
* */
double stickProbability;
// delete particles and clear the particle list
void clearParticles();
// size of cluster
double clusterRadius;
// these are related to the DLA algorithm
double addCircle;
double killCircle;
// size of grid
static const int gridSize = 1600;
int **grid; // this will be a 2d array that stores whether each site is occupied
// random number generator, class name is rnd, instance is rgen
rnd rgen;
// CSV ouput
ofstream csv;
// number of particles at which the simulation will stop
// (the value is set in constructor)
int endNum;
// the values of these variables are set in the constructor
double addRatio; // how much bigger the addCircle should be, compared to cluster radius
double killRatio; // how much bigger is the killCircle, compared to the addCircle
public:
// these are public variables and functions
// update the system: if there is an active particle then move it,
// else create a new particle (on the adding circle)
void update();
// is the simulation running
bool running;
// lastParticleIsActive is +1 if there is an active particle in the system, otherwise 0
int lastParticleIsActive;
// constructor
explicit DLASystem(int maxParticles, const string &csvPath, double stickProbability);
// destructor
~DLASystem();
// delete all particles and reset
void Reset();
// this sets the seed for the random numbers
void setSeed(int s) { rgen.setSeed(s); }
// check whether we should stop (eg the cluster has reached the edge of the grid)
int checkStop();
// if pos is outside the cluster radius then set clusterRadius to be the distance to pos.
void updateClusterRadius( double pos[] );
// set and read grid entries associated with a given position
void setGrid(double pos[], int val);
int readGrid(double pos[]);
// return the distance of a given point from the origin
double distanceFromOrigin(double pos[]) {
return sqrt( pos[0]*pos[0] + pos[1]*pos[1] );
}
// set whether there is an active particle in the system or not
void setParticleActive() { lastParticleIsActive = 1; }
void setParticleInactive() { lastParticleIsActive = 0; }
// add a particle at pos
void addParticle(double pos[]);
// add a particle at a random point on the addCircle
void addParticleOnAddCircle();
// assign setpos to the position of a neighbour of pos
// which neighbour we look at is determined by val (=0,1,2,3)
void setPosNeighbour(double setpos[], double pos[], int val);
// this attempts to move the last particle in the List to a random neighbour
// if the neighbour is occupied then nothing happens
// the function also checks if the moving particle should stick.
void moveLastParticle();
// check whether the last particle should stick
// currently it sticks whenever it touches another particle
int checkStick();
void exportData();
};

View File

@ -1,72 +0,0 @@
# ====================================================================================== #
# From the Author #
# ====================================================================================== #
# ! The purpose of this Makefile is to build the DLASystem project
# ! This makefile was adapted to work with any cpp project on OSX
# ====================================================================================== #
# Variables of the Makefile #
# ====================================================================================== #
CXX = clang++
CXXFLAGS = -Wall -Wextra -g -O0 --std=c++20
IFLAGS = -I/usr/local/include -I/usr/include
LFLAGS = -L/usr/local/lib -lm
# ------------------------------------------
# FOR GENERIC MAKEFILE:
# 1 - Binary directory
# 2 - Source directory
# 3 - Executable name
# 4 - Sources names
# 5 - Dependencies names
# ------------------------------------------
BIN = .
SOURCE = .
EXEC = ./run
SOURCES = $(wildcard $(SOURCE)/*.cpp)
OBJECTS = $(SOURCES:.cpp=.o)
# ====================================================================================== #
# Targets of the Makefile #
# target_name : dependency #
# <tabulation> command #
# ====================================================================================== #
# ------------------------------------------
# ! - all : Compiles everything
# ! - help : Shows this help
# ! - clean : erases all object files *.o
# ! and all binary executables
# ------------------------------------------
all : $(BIN)/run
test: $(BIN)/hllc_test
help :
@grep -E "^# !" Makefile | sed -e 's/# !/ /g'
clean:
rm -f $(EXEC) $(OBJECTS)
# ------------------------------------------
# Executable
# ------------------------------------------
$(EXEC): $(OBJECTS)
$(CXX) $(OBJECTS) -o $(EXEC) $(IFLAGS) $(LFLAGS)
# ------------------------------------------
# Temorary files (*.o) (IFLAGS should be added here)
# ------------------------------------------
$(SOURCE)/%.o: $(SOURCE)/%.cpp
$(CXX) $(CXXFLAGS) -c $< -o $@ $(IFLAGS) $(LFLAGS)

View File

@ -1,20 +0,0 @@
#pragma once
class Particle {
public:
static const int dim = 2; // we are in two dimensions
double *pos; // pointer to an array of size dim, to store the position
// default constructor
Particle() {
pos = new double[dim];
}
// constructor, with a specified initial position
Particle(double set_pos[]) {
pos = new double[dim];
for (int d=0;d<dim;d++)
pos[d]=set_pos[d];
}
// destructor
~Particle() { delete[] pos; }
};

View File

@ -1,4 +0,0 @@
# DLA Model
This currently contains The Initially Provided Code for the DLA model which will be used as the source of truth for
further alterations once verified.

16
build.rs Normal file
View File

@ -0,0 +1,16 @@
extern crate cbindgen;
use std::env;
use std::path::Path;
use cbindgen::{Config, Builder};
fn main() {
let crate_env = env::var("CARGO_MANIFEST_DIR").unwrap();
let crate_path = Path::new(&crate_env);
let config = Config::from_root_or_default(crate_path);
Builder::new().with_crate(crate_path.to_str().unwrap())
.with_config(config)
.generate()
.expect("Cannot generate header file!")
.write_to_file("libdla.h");
}

24
libdla.h Normal file
View File

@ -0,0 +1,24 @@
#include <cstdarg>
#include <cstdint>
#include <cstdlib>
#include <ostream>
#include <new>
struct CStorage;
struct CPosition {
int32_t _0;
int32_t _1;
};
extern "C" {
CStorage *storage_new(uint32_t grid_size);
bool storage_at(const CStorage *storage, int32_t i, int32_t j);
void storage_deposit(CStorage *storage, int32_t i, int32_t j, uint8_t val);
CPosition walk(uint32_t d, int32_t i, int32_t j);
} // extern "C"

View File

@ -1,29 +0,0 @@
#include <iostream>
#include "DLASystem.h"
int main(int argc, char **argv) {
if (argc != 5) {
cerr << "Usage " << argv[0] << " <seed> <maxParticles> <stickProbability> <csvPath>" << endl;
return 1;
}
int seed = std::stoi(argv[1]);
int maxParticles = std::stoi(argv[2]);
double stickProbability = std::stod(argv[3]);
string csvPath = argv[4];
std::cerr << "Setting seed " << seed << endl;
// create the system
auto *sys = new DLASystem(maxParticles, csvPath, stickProbability);
sys->setSeed(seed);
sys->running = true;
while (sys->running) {
sys->update();
}
sys->exportData();
return 0;
}

33
rnd.h
View File

@ -1,33 +0,0 @@
#pragma once
#include <random>
// ... don't worry how this all works
// ... member functions that you may want to use:
// random01() returns a random double between 0 and 1
// randomInt(max) returns a random int between 0 and max-1 (inclusive)
class rnd {
private:
// nuts and bolts.. should not need to touch this.
std::default_random_engine generator;
int genMax;
std::uniform_int_distribution<int> *intmax;
std::uniform_real_distribution<double> *real01;
public:
// constructor
rnd() {
genMax = 0x7fffffff;
//cout << "genMax is " << generator.max() << endl;
intmax = new std::uniform_int_distribution<int>(0, genMax);
real01 = new std::uniform_real_distribution<double>(0.0, 1.0);
}
// destructor
~rnd() { delete intmax; delete real01; }
// set the random seed
void setSeed(int seed) { generator.seed(seed); }
// member functions for generating random double in [0,1] and random integer in [0,max-1]
double random01() { return (*real01)(generator); }
int randomInt(int max) { return (*intmax)(generator) % max; }
};

2
rust-toolchain.toml Normal file
View File

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

75
src/clib.rs Normal file
View File

@ -0,0 +1,75 @@
#![feature(array_zip)]
use system::Storage;
use crate::system::grid::{Position, VectorStorage};
mod system;
#[derive(Eq, PartialEq, Debug)]
#[repr(C)]
pub struct CPosition(i32, i32);
pub struct CStorage(VectorStorage);
#[no_mangle]
pub extern "C" fn storage_new(grid_size: u32) -> &'static mut CStorage {
let mut pntr = Box::new(CStorage(VectorStorage::new(grid_size)));
Box::leak(pntr)
}
#[no_mangle]
pub extern "C" fn storage_at(storage: &CStorage, i: i32, j: i32) -> bool {
storage.0.at(&Position { x: i, y: j })
}
#[no_mangle]
pub extern "C" fn storage_deposit(storage: &mut CStorage, i: i32, j: i32, val: u8) {
storage.0.write(&Position { x: i, y: j }, val == 1);
}
#[no_mangle]
pub extern "C" fn walk(d: u32, i: i32, j: i32) -> CPosition {
return test::b(d, i, j);
}
mod test {
use num_integer::Integer;
use crate::CPosition;
use crate::system::grid::Position;
pub(crate) fn a(d: u32, i: i32, j: i32) -> CPosition {
match d {
0 => CPosition(i + 1, j),
1 => CPosition(i - 1, j),
2 => CPosition(i, j + 1),
3 => CPosition(i, j - 1),
_ => panic!("Ahh"),
}
}
pub(crate) fn b(d: u32, i: i32, j: i32) -> CPosition {
let (dim, sign) = d.div_rem(&2);
let sign = if sign == 0 { 1 } else { -1 };
// HACK: Our conventin and the MVA are different, since we are trying to strangle fig this, quick hack.
let offset = Position::in_direction(dim, sign);
let next = Position { x: i, y: j } + offset;
CPosition(next.x, next.y)
}
#[test]
fn test() {
let d = [0, 1, 2, 3];
d.iter()
.map(|d| d.div_rem(&2))
.for_each(|p| println!("{p:?}"));
}
#[test]
fn alignment() {
let d = [0, 1, 2, 3];
d.iter()
.map(|d| (a(*d, 0, 0), b(*d, 0, 0)))
.for_each(|p| assert_eq!(p.0, p.1));
}
}

36
src/example_systems.rs Normal file
View File

@ -0,0 +1,36 @@
use rand::rngs::SmallRng;
use rand::SeedableRng;
use crate::system::grid::{Position, VectorStorage};
use crate::system::model::DLASystem;
use crate::system::nd::{NDPosition, NDVectorStorage};
use crate::system::walker::LocalRandomWalker;
pub fn initial_config(seed: u64, max_particles: usize) -> DLASystem<SmallRng, Position, VectorStorage, LocalRandomWalker> {
DLASystem::new_g(
SmallRng::seed_from_u64(seed),
VectorStorage::new(1600),
LocalRandomWalker,
1.0,
max_particles,
)
}
pub fn stick_probability(seed: u64, max_particles: usize, stick_probability: f32) -> DLASystem<SmallRng, Position, VectorStorage, LocalRandomWalker> {
DLASystem::new_g(
SmallRng::seed_from_u64(seed),
VectorStorage::new(1600),
LocalRandomWalker,
stick_probability,
max_particles,
)
}
pub fn three_dimensional(seed: u64, max_particles: usize, stick_probability: f32) -> DLASystem<SmallRng, NDPosition<3>, NDVectorStorage<3>, LocalRandomWalker> {
DLASystem::new_g(
SmallRng::seed_from_u64(seed),
NDVectorStorage::new(1600),
LocalRandomWalker,
stick_probability,
max_particles,
)
}

36
src/main.rs Normal file
View File

@ -0,0 +1,36 @@
#![feature(array_zip)]
use std::path::PathBuf;
mod system;
mod example_systems;
use clap::Parser;
use crate::example_systems::stick_probability;
#[derive(Parser, Debug)]
struct Cli {
seed: u64,
max_particles: usize,
stick_probability: f32,
csv_path: PathBuf,
}
fn main() {
let cli = Cli::parse();
println!("Running: {:?}", cli);
let mut sys = stick_probability(
cli.seed,
cli.max_particles,
cli.stick_probability
);
while sys.running {
sys.update();
}
sys.export_data(&cli.csv_path)
.expect("Failed to write");
}

92
src/system/grid.rs Normal file
View File

@ -0,0 +1,92 @@
use std::f32::consts::PI;
use std::ops::Add;
use num_integer::Integer;
use rand::Rng;
use crate::system::{GriddedPosition, Storage};
use serde::{Serialize, Deserialize};
pub struct VectorStorage {
backing: Vec<bool>,
grid_size: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Position {
pub x: i32,
pub y: i32,
}
impl Position {
pub fn in_direction(direction: u32, value: i32) -> Self {
if direction == 0 { Position { x: value, y: 0 } } else { Position { x: 0, y: value } }
}
}
impl Add for Position {
type Output = Position;
fn add(self, rhs: Self) -> Self::Output {
Position { x: self.x + rhs.x, y: self.y + rhs.y }
}
}
impl GriddedPosition for Position {
const NEIGHBOURS: u32 = 4;
fn zero() -> Position {
Position { x: 0, y: 0 }
}
fn spawn<R: Rng>(rng: &mut R, radius: f32) -> Self {
let theta = rng.gen_range(0f32..1.0) * 2.0 * PI;
let (x, y) = (radius * theta.cos(), radius * theta.sin());
Position { x: x as i32, y: y as i32 }
}
fn abs(&self) -> f32 {
((self.x.pow(2) + self.y.pow(2)) as f32).powf(0.5)
}
fn neighbour(&self, neighbour_index: u32) -> Self {
let (dim, sign) = neighbour_index.div_rem(&2);
let sign = if sign == 0 { 1 } else { -1 };
let offset = Position::in_direction(dim, sign);
self.clone() + offset
}
fn linear_index(&self, grid_size: u32) -> usize {
let grid_size = grid_size as i32;
assert!(self.x <= grid_size && -(grid_size) <= self.x);
assert!(self.y <= grid_size && -(grid_size) <= self.y);
let x = (self.x + (grid_size) / 2) as usize;
let y = (self.y + (grid_size) / 2) as usize;
return grid_size as usize * y + x;
}
}
impl VectorStorage {
pub fn new(grid_size: u32) -> VectorStorage {
VectorStorage { grid_size, backing: vec![false; grid_size.pow(2) as usize] }
}
/*
* Convenience function for c-binding
* */
pub fn write(&mut self, position: &Position, val: bool) {
self.backing[position.linear_index(self.grid_size)] = val;
}
}
impl Storage<Position> for VectorStorage {
fn at(&self, position: &Position) -> bool {
return self.backing[position.linear_index(self.grid_size)]
}
fn deposit(&mut self, position: &Position) {
self.backing[position.linear_index(self.grid_size)] = true;
}
}

51
src/system/hexagonal.rs Normal file
View File

@ -0,0 +1,51 @@
use std::ops::Add;
use bevy::render::color::HexColorError::Hex;
use rand::Rng;
use crate::system::GriddedPosition;
use serde::{Serialize, Deserialize};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct HexPosition {
pub q: i32,
pub r: i32,
}
impl Add for HexPosition {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
HexPosition { q: self.q + rhs.q, r: self.r + rhs.r }
}
}
impl GriddedPosition for HexPosition {
const NEIGHBOURS: u32 = 6;
fn zero() -> Self {
HexPosition { q: 0, r: 0 }
}
fn spawn<R: Rng>(rng: &mut R, radius: f32) -> Self {
todo!()
}
fn abs(&self) -> f32 {
((self.q.pow(2) + self.r.pow(2) + self.q * self.r) as f32).sqrt()
}
fn neighbour(&self, neighbour_index: u32) -> Self {
let neighbour_index = neighbour_index as usize;
const OFFSETS: [(i32, i32); 6] = [
(1, 0), (1, -1), (0, -1),
(-1, 0), (-1, 1), (0, 1),
];
self.clone() + HexPosition { q: OFFSETS[neighbour_index].0, r: OFFSETS[neighbour_index].0 }
}
fn linear_index(&self, grid_size: u32) -> usize {
todo!()
// ((self.q + grid_size / 2) + grid_size * self.r) as usize
}
}

24
src/system/mod.rs Normal file
View File

@ -0,0 +1,24 @@
use std::ops::Add;
use rand::Rng;
use serde::Serialize;
pub mod walker;
pub mod grid;
pub mod model;
pub mod nd;
mod hexagonal;
pub trait GriddedPosition: Add<Output=Self> + Serialize + Clone {
const NEIGHBOURS: u32;
fn zero() -> Self;
fn spawn<R: Rng>(rng: &mut R, radius: f32) -> Self;
fn abs(&self) -> f32;
fn neighbour(&self, neighbour_index: u32) -> Self;
fn linear_index(&self, grid_size: u32) -> usize;
}
pub trait Storage<P: GriddedPosition> {
fn at(&self, position: &P) -> bool;
fn deposit(&mut self, position: &P);
}

128
src/system/model.rs Normal file
View File

@ -0,0 +1,128 @@
use std::io;
use std::path::Path;
use rand::prelude::*;
use crate::system::{GriddedPosition, Storage};
use crate::system::walker::Walker;
pub struct DLASystem<R: Rng, P: GriddedPosition, S: Storage<P>, W: Walker<P>> {
rng: R,
space: S,
walker: W,
stick_probability: f32,
max_particles: usize,
pub running: bool,
particles: Vec<P>,
active_particle: Option<P>,
add_ratio: f32,
add_circle: f32,
kill_ratio: f32,
kill_circle: f32,
cluster_radius: f32,
}
impl<R: Rng, P: GriddedPosition, S: Storage<P>, W: Walker<P>> DLASystem<R, P, S, W> {
pub fn new_g(rng: R, space: S, walker: W, stick_probability: f32, max_particles: usize) -> Self {
let mut sys = DLASystem {
rng,
stick_probability,
max_particles,
running: true,
space,
walker,
particles: vec![],
active_particle: None,
add_ratio: 1.2,
kill_ratio: 1.7,
add_circle: 10.0,
kill_circle: 20.0,
cluster_radius: 0.0,
};
sys.deposit(&P::zero());
sys
}
pub fn update(&mut self) {
if self.active_particle.is_some() {
self.move_particle();
} else if self.particles.len() < self.max_particles {
self.spawn_particle();
} else {
self.running = false;
}
}
fn move_particle(&mut self) {
let current_position = &self
.active_particle
.clone()
.expect("No active particle");
let next_position = self.walker.walk(&mut self.rng, current_position);
let distance = next_position.abs();
if distance > self.kill_circle {
self.active_particle = None;
} else if !self.space.at(&next_position) {
if self.check_stick(&next_position) {
self.deposit(&next_position);
self.active_particle = None;
return;
} else {
self.active_particle.replace(next_position);
}
}
}
fn check_stick(&mut self, position: &P) -> bool {
(0..P::NEIGHBOURS)
.map(|n| position.neighbour(n))
.any(|neighbour|
self.space.at(&neighbour)
&& self.rng.gen_range(0.0f32..=1.0) < self.stick_probability
)
}
fn spawn_particle(&mut self) {
let position = P::spawn(&mut self.rng, self.add_circle);
if !self.space.at(&position) {
self.active_particle = Some(position);
}
}
fn deposit(&mut self, p0: &P) {
self.particles.push(p0.clone());
self.space.deposit(p0);
let distance = p0.abs();
if distance > self.cluster_radius {
self.cluster_radius = distance;
let new_add_circle = (self.cluster_radius * self.add_ratio).max(self.cluster_radius + 5.0);
if self.add_circle < new_add_circle {
self.add_circle = new_add_circle;
self.kill_circle = self.kill_ratio * self.add_circle;
}
}
}
pub fn export_data(&self, path: &Path) -> io::Result<()> {
let mut wtr = csv::Writer::from_path(path)?;
for particle in &self.particles {
wtr.serialize(particle)?;
}
wtr.flush()?;
Ok(())
}
}

107
src/system/nd.rs Normal file
View File

@ -0,0 +1,107 @@
use std::ops::Add;
use num_integer::Integer;
use rand::Rng;
use serde::{Serialize, Serializer};
use serde::ser::SerializeMap;
use crate::system::GriddedPosition;
use crate::system::Storage;
pub struct NDVectorStorage<const DIM: usize> {
backing: Vec<bool>,
grid_size: u32,
}
impl<const DIM: usize> NDVectorStorage<DIM> {
pub fn new(grid_size: u32) -> Self {
Self { grid_size, backing: vec![false; grid_size.pow(DIM as u32) as usize] }
}
}
impl<const DIM: usize> Storage<NDPosition<DIM>> for NDVectorStorage<DIM> {
fn at(&self, position: &NDPosition<DIM>) -> bool {
return self.backing[position.linear_index(self.grid_size)];
}
fn deposit(&mut self, position: &NDPosition<DIM>) {
self.backing[position.linear_index(self.grid_size)] = true;
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct NDPosition<const DIM: usize>([i32; DIM]);
impl<const DIM: usize> NDPosition<DIM> {
pub fn in_direction(direction: usize, value: i32) -> Self {
let mut arr = [0; DIM];
arr[direction] = value;
NDPosition(arr)
}
}
impl<const DIM: usize> Add for NDPosition<DIM> {
type Output = Self;
fn add(self, rhs: Self) -> Self::Output {
Self(self.0.zip(rhs.0).map(|(a, b)| a + b))
}
}
impl<const DIM: usize> Serialize for NDPosition<DIM> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> where S: Serializer {
let mut map = serializer.serialize_map(Some(DIM))?;
for (i, v) in self.0.iter().enumerate() {
map.serialize_entry(&format!("r{}", i), v)?;
}
map.end()
}
}
impl<const DIM: usize> GriddedPosition for NDPosition<DIM> {
const NEIGHBOURS: u32 = { 2u32.pow(DIM as u32) } as u32;
fn zero() -> Self {
NDPosition([0; DIM])
}
fn spawn<R: Rng>(rng: &mut R, radius: f32) -> Self {
let mut a: [f32; DIM] = [0f32; DIM];
let mut b: [i32; DIM] = [0i32; DIM];
for i in 0..DIM {
a[i] = rng.gen_range(0f32..1f32);
}
let norm = a.iter().sum::<f32>()
.sqrt();
for i in 0..DIM {
a[i] = a[i] * radius / norm;
b[i] = a[i] as i32;
}
return Self(b);
}
fn abs(&self) -> f32 {
let a: i32 = self.0.iter()
.map(|r| r.pow(2))
.sum();
(a as f32).powf(0.5)
}
fn neighbour(&self, neighbour_index: u32) -> Self {
let (dim, sign) = neighbour_index.div_rem(&(DIM as u32));
let sign = if sign == 0 { 1 } else { -1 };
let offset = Self::in_direction(dim as usize, sign);
self.clone() + offset
}
fn linear_index(&self, grid_size: u32) -> usize {
self.0.iter()
.enumerate()
.map(|(i, v)| (grid_size.pow(i as u32) as usize) * (v + ((grid_size / 2) as i32)) as usize)
.sum()
}
}

57
src/system/walker.rs Normal file
View File

@ -0,0 +1,57 @@
use rand::prelude::Rng;
use crate::system::GriddedPosition;
pub trait Walker<P: GriddedPosition> {
fn walk<R: Rng>(&self, rng: &mut R, position: &P) -> P;
}
pub struct LocalRandomWalker;
impl<Position: GriddedPosition> Walker<Position> for LocalRandomWalker {
fn walk<R: Rng>(&self, rng: &mut R, position: &Position) -> Position {
position.neighbour(rng.gen_range(0u32..Position::NEIGHBOURS))
}
}
mod test {
use rand::rngs::SmallRng;
use rand::{SeedableRng, thread_rng};
use crate::system::{GriddedPosition, grid::Position};
use crate::system::walker::{LocalRandomWalker, Walker};
#[test]
fn uniformity() {
let walker = LocalRandomWalker;
let mut rng = SmallRng::from_rng(thread_rng()).unwrap();
let mut results: Vec<Position> = vec![];
let origin = &Position::zero();
let x: u32 = (1_000_000);
for i in 0..x {
results.push(walker.walk(&mut rng, origin));
}
let a = results
.iter()
.filter(|a| **a == Position { x: 0, y: 1 })
.count();
let b = results
.iter()
.filter(|a| **a == Position { x: 0, y: -1 })
.count();
let c = results
.iter()
.filter(|a| **a == Position { x: 1, y: 0 })
.count();
let d = results
.iter()
.filter(|a| **a == Position { x: -1, y: 0 })
.count();
println!("{} {} {} {}", a as f32 / x as f32, b as f32 / x as f32, c as f32 / x as f32, d as f32 / x as f32);
}
}

94
src/ui.rs Normal file
View File

@ -0,0 +1,94 @@
#![feature(array_zip)]
use std::default::Default;
use std::error::Error;
use bevy::{prelude::*, sprite::MaterialMesh2dBundle};
use csv::{Position, Reader, ReaderBuilder};
use clap::Parser;
#[derive(Parser, Resource)] // requires `derive` feature
enum UICli {
CSV(CSVArgs)
}
#[derive(clap::Args)]
struct CSVArgs {
path: std::path::PathBuf,
}
struct Position2D(i32, i32);
fn main() -> Result<(), Box<dyn Error>> {
let cli = UICli::parse();
App::new()
.insert_resource(cli)
.add_plugins(DefaultPlugins.set(WindowPlugin {
window: WindowDescriptor {
title: "DLA Static 2D Render".to_string(),
width: 800.,
height: 800.,
..default()
},
..default()
}))
.add_startup_system(setup_ui)
.add_startup_system(read_csv)
.run();
Ok(())
}
fn read_csv(
cli: Res<UICli>,
mut commands: Commands
) {
let csv_path = match &cli.into_inner() {
UICli::CSV(CSVArgs { path }) => path,
_ => panic!("Ahh"),
};
let mut reader = ReaderBuilder::new()
.has_headers(true)
.from_path(csv_path)
.expect("Failed to read csv");
let headers = reader.headers()
.expect("Failed to read headers");
let x_column = headers.iter().position(|name| name.trim() == "x")
.expect("Failed to find x column");
let y_column = headers.iter().position(|name| name.trim() == "y")
.expect("Failed to find x column");
let positions = reader
.records()
.map(|record| {
let record = record.expect("Failed to read position");
let x: i32 = record[x_column].trim().parse::<i32>().expect("Failed to read x");
let y: i32 = record[y_column].trim().parse::<i32>().expect("Failed to read y");
Position2D(x, y)
});
for Position2D(x, y) in positions {
let rect_size = 5.0;
commands.spawn(SpriteBundle {
sprite: Sprite {
color: Color::rgb(0.25, 0.25, 0.75),
custom_size: Some(Vec2::new(rect_size, rect_size)),
..default()
},
transform: Transform::from_translation(Vec3::new((x as f32) * rect_size, (y as f32) * rect_size, 0.)),
..default()
});
}
}
fn setup_ui(
mut commands: Commands,
) {
commands.spawn(Camera2dBundle::default());
}