more design, choosing java

This commit is contained in:
Enrico Fasoli 2015-10-16 19:41:59 +02:00
parent 353bf3820d
commit 1bd5c388be
8 changed files with 45 additions and 241 deletions

View File

@ -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.

View File

@ -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
}

View File

View File

@ -1,45 +0,0 @@
use action::Action;
use structure::Structure;
pub enum Blocks {
Neuron,
Leg { dir: i32 }
}
pub struct Block {
structure: Structure,
connections: Vec<f64>,
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<f64> {
&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<Vec<Action>> {
None
}
}

View File

@ -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()
}
}

View File

@ -1,40 +0,0 @@
use util::{Point,Directions};
use action::Action;
use block::Block;
pub struct Structure {
pub blocks: Vec<Block>,
pub position: Point
}
impl Structure {
pub fn compute(&mut self) -> Vec<Action> {
let mut v = Vec::new() as Vec<Action>;
// 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<Action>
};
// 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 }
}
}

View File

@ -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
}

View File

@ -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<Option<Structure>>, // access a structure using coordinates
structures: Vec<Structure> // list of all structures
}
impl World {
pub fn new(size: i32) -> World {
let mut v = Vec::new() as Vec<Option<Structure>>;
v.reserve((size*size) as usize);
World {
size: size,
grid: v,
structures: Vec::new() as Vec<Structure>,
}
}
pub fn compute(&mut self) {
let mut actions = Vec::new() as Vec<Action>;
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<Structure> {
&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) }
}
}