diff --git a/README.md b/README.md index c3ccd97..5586758 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Every structure occupies only one cell. A structure with an operational mind governing the body is called a creature or machine. There are four kinds of blocks: -- __neuron blocks__ make the calculations +- __neuron blocks__ make calculations - __action blocks__ let a structure do something - __sensor blocks__ let a structure know something about itself or anything else - __resource blocks__ can be interacted with, causing some effects @@ -74,6 +74,19 @@ There are four kinds of blocks: - if the connection is `-1` then the exact opposite of the input value passes to the destination - the highest `abs(val) where val is the connection/weight` is, the stronger the connection + + \ | / + v v v + ######### + -> # block # <- + ######### + ^ ^ ^ + / | \ + + +- assuming the block in the drawing is at (2,2), it will be connected with: (1,1),(1,2),(1,3),(2,1),(2,2),(2,3),(3,1),(3,2),(3,3) +- this drawing doesn't include the recursive connection the block has with itself + A signal can be any floating point value between -1 and 1. - any signal higher than 1 will be considered to be 1 @@ -167,24 +180,22 @@ This method to calculate fitness has many problems: - What if a creature donates energy to others of the same species? It gets counted as energy consumption, thus decreasing fitness -- What is a creature couldn't harvest energy because the circumstances weren't favorable? +- What if a creature couldn't harvest energy because the environment wasn't ideal? +- What if a creatures scored hugely because the environment was just perfect for huge scores? The conclusion is that this system is far too complex to consider fitness evaluation. ### Complexity of a structure +The point of structure complexity is to try and approximately figure out how computationally expensive it is +to compute an itearation of the simulation for the structure. + The complexity of a structure is: - if the fitness is `> 0`: the sum of the complexity of all its blocks. -- if the fitness is `<= 0`: 0 +- if the fitness is `<= 0`: 0, because there is nothing to compute if the structure has no energy. -Complexity of a block starts at 0, then: - -- increases by 1 for every not null connection of the block, if the block is an input block -- increases by x for every output block, depending on the specific block - -The point of structure complexity is to try and approximately figure out how computationally expensive it is -to compute an itearation of the simulation for the structure +Block complexity evaluation is covered in the Parallelism section. ## Primordial world @@ -252,8 +263,10 @@ However, it has also some issues: - I don't know the rust programming language very well (not a big problem though) - its lower level nature (compared to java) would make development slower - it's very hard to write a multithreaded program in rust due to the compiler being extremely limiting by refusing to compile programs with a chance to cause segfaults or race conditions +- no structure/object/class inheritance +- I honestly don't think I can implement this simulation in a multithreaded and/or parallel computing friendly way using rust. -Java would solve those issues, also maintaining most of rust's advantages. I'll eventually decide what to do. +That's why I ultimately chose to use Java. It's a language I know very well, that has most of rust's features necessary for this project without the problems. ### Simulation iteration evaluation algorithm @@ -290,35 +303,28 @@ It's probably not as fast as it gets, but it should be decent __Note:__ the bigger a machine, the slower it reacts to inputs because signals need more time to travel through the body -## Rust implementation +## Multithreading and Parallelism -Quick and dirty scheme to keep track of things. +The first AIrium wasn't built with concurrency in mind, it was only added later with average results. -- world (class), keeps all information about a world -- structure (trait) - - compute(), computes an iteration of the simulation for this structure - - get_blocks(), returns an immutable reference to the blocks - - clean(), used to clear event queue, also calls clean() on blocks -- runnable (trait) - - run(), does stuff, mostly in another thread -- manager (impls runnable), manages a world -- worker (impls runnable), operates a list of creatures in a world -- applier (impls runnable), operates on a list of creatures in a world, also calls clean() on structures -- block (trait) - - get_connections(), returns an immutable reference to the connection vector - - get_value(), returns an immuatable reference to the current stored signal - - get_name(), returns an immutable reference to the block's name - - compute(), computes an iteration of the simulation for this block, then stores it in the new stored signal. - also returns a list of actions that the block wants to perform - - clean(), clears event queue and writes new stored signal into the current stored signal -- action (trait) - - get_source(), get the source structure (immutable ref) - - get_target(), get the target (immutable ref) - - apply(), applies the action +This time, concurrency was baked into the project since its first design: when evaluating an iteration of the simulation for a structure +its _output_ is just a list of _actions_ that the structure wants to perform on itself or the environment. -- PROBLEM: apply() requires a mutable reference to the target, but it's not possible to pass it in any way. Only an immutable ref can be passed. -- SOLUTION: use an immutable reference and call as_mut on it? -- thread comms: use channels? +The world simulation can be parallelized down to the single _block_ of a structure, then their actions can be applied to the world sequentally. -TODO: figure out where to put UI logic. We probably can't do it in parallel with the simulation, so it needs - to be executed by the manager thread. +__Note:__ Even the action application could be evaluated in parallel by grouping structures together, putting each action into a group. +if an action concerns both Structure A and Structure B, then they are related and actions concerning one of A or B should go in the same group. +The problem is that this kind of classification is already a big problem of its own, expecially considering that a _fast_ grouping algorithm is +essential to fullfull the high performance needed in this implementation. Thus, I think it's better to avoid trying to parallelize action +evaluation for now. + +__Another problem__ the first version of AIrium had was when new creatures where created by the artificial selector. That was because computing a child +creature requires a lot of iteration, memory accessing and random number generation. This is slow and a big problem that will need to be addressed +expecially in environments with structures producing a high amount of eggs. The solution I'm thinking of using is to not try to compute the child +structure right away, but save a copy of the parent structures' inside the egg, then slowly build a child when evaluating the egg blocks. + +There's also __the Block complexity (evaluation speed) variety__. Ideally, to be able to compute how complex a structure is, +we have to know how complex blocks are. But, the higher the complexity, the higher the computational time. +So ideally, we need to know, approximately, how much time does a block need to be evaluated. + +A possible improvement is __Smart block evaluation__. It consists in not evaluating a circuit if no _action blocks_ are affected by it. diff --git a/src/action.rs b/src/action.rs deleted file mode 100644 index 2a3fa33..0000000 --- a/src/action.rs +++ /dev/null @@ -1,11 +0,0 @@ -use util::{Point,Directions}; -use structure::Structure; - -pub enum Actions { - Movement { dir: Directions } -} - -pub struct Action { - pub source: Structure, - pub action_type: Actions -} diff --git a/src/applier.rs b/src/applier.rs deleted file mode 100644 index e69de29..0000000 diff --git a/src/block.rs b/src/block.rs deleted file mode 100644 index 0a2e2ed..0000000 --- a/src/block.rs +++ /dev/null @@ -1,45 +0,0 @@ -use action::Action; -use structure::Structure; - -pub enum Blocks { - Neuron, - Leg { dir: i32 } -} - -pub struct Block { - structure: Structure, - connections: Vec, - current_value: f64, - new_value: f64, - block_type: Blocks -} - -impl Block { - pub fn new(structure: Structure, btype: Blocks) -> Block { - Block { - structure: structure, - connections: vec![0 as f64;9], - current_value: 0 as f64, - new_value: 0 as f64, - block_type: btype - } - } - pub fn get_connections(&self) -> &Vec { - &self.connections - } - pub fn get_value(&self) -> &f64 { - &self.current_value - } - pub fn get_name(&self) -> &'static str { - match self.block_type { - Blocks::Neuron => "Neuron", - Blocks::Leg { dir } => "Leg" - } - } - pub fn clean(&mut self) { - self.current_value = self.new_value; - } - pub fn compute(&mut self) -> Option> { - None - } -} diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index f922af9..0000000 --- a/src/main.rs +++ /dev/null @@ -1,14 +0,0 @@ -mod util; -mod action; -mod block; -mod structure; -mod world; - -use world::World; - -fn main() { - let mut world = World::new(10); - loop { - world.compute() - } -} diff --git a/src/structure.rs b/src/structure.rs deleted file mode 100644 index 7414604..0000000 --- a/src/structure.rs +++ /dev/null @@ -1,40 +0,0 @@ -use util::{Point,Directions}; -use action::Action; -use block::Block; - -pub struct Structure { - pub blocks: Vec, - pub position: Point -} - -impl Structure { - pub fn compute(&mut self) -> Vec { - let mut v = Vec::new() as Vec; - // Iterate blocks - for b in &mut self.blocks { - // Compute and save result - let ret = match b.compute() { - Some(x) => x, - None => Vec::new() as Vec - }; - // push actions to the structure's array - for i in ret { - v.push(i) - } - } - v - } - pub fn move_to(&mut self, dest : Point){ - self.position = dest; - } - pub fn move_dir(&mut self, dir : Directions){ - let mut deltaX = 0; let mut deltaY = 0; - match dir { - Directions::Left => deltaX = -1, - Directions::Right => deltaX = 1, - Directions::Up => deltaY = -1, - Directions::Down => deltaY = 1 - } - self.position = Point { x: self.position.x + deltaX, y: self.position.y + deltaY } - } -} diff --git a/src/util.rs b/src/util.rs deleted file mode 100644 index 0e2ab39..0000000 --- a/src/util.rs +++ /dev/null @@ -1,32 +0,0 @@ -use std::ops::Add; - -pub struct Point { - pub x: i32, - pub y: i32 -} - -impl Add for Point { - type Output = Point; - - fn add(self, other: Point) -> Point { - Point { x: self.x + other.x, y: self.y + other.y } - } -} - -pub enum Directions { - Right, - Left, - Up, - Down -} - -pub fn dir_to_point(dir : Directions) -> Point { - let mut p = Point { x: 0, y: 0 }; - match dir { - Directions::Left => p.x = -1, - Directions::Right => p.x = 1, - Directions::Up => p.y = -1, - Directions::Down => p.y = 1 - } - p -} diff --git a/src/world.rs b/src/world.rs deleted file mode 100644 index bbc8b14..0000000 --- a/src/world.rs +++ /dev/null @@ -1,60 +0,0 @@ -use structure::Structure; -use action::{Action,Actions}; -use util::{Point,Directions,dir_to_point}; -use std::sync::mpsc::{Sender,Receiver,channel}; - -pub struct World { - size: i32, // width and height of the grid - grid: Vec>, // access a structure using coordinates - structures: Vec // list of all structures -} - -impl World { - pub fn new(size: i32) -> World { - let mut v = Vec::new() as Vec>; - v.reserve((size*size) as usize); - World { - size: size, - grid: v, - structures: Vec::new() as Vec, - } - } - pub fn compute(&mut self) { - let mut actions = Vec::new() as Vec; - for s in &mut self.structures { - for a in s.compute() { - actions.push(a); - } - } - for action in actions { - self.apply_action(action) - } - } - pub fn apply_action(&mut self, action : Action){ - let src = &mut action.source; - match action.action_type { - Actions::Movement { dir } => { - let p = src.position + dir_to_point(dir); - match self.get(p) { - &mut Some(x) => {}, - &mut None => src.move_to(p) - } - } - } - } - pub fn get(&mut self, p: Point) -> &mut Option { - &mut self.grid[(p.y+p.x) as usize] - } - pub fn fix_coords(&self, p: Point) -> Point { - fn fix_coord(c: i32, limit: i32) -> i32 { - match c < 0 { - true => (c * -1) % limit, - false => match c >= limit { - true => c % limit, - false => c - } - } - }; - Point { x: fix_coord(p.x,self.size), y: fix_coord(p.y,self.size) } - } -}