more design, choosing java
This commit is contained in:
parent
353bf3820d
commit
1bd5c388be
84
README.md
84
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.
|
||||
|
@ -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
|
||||
}
|
45
src/block.rs
45
src/block.rs
@ -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
|
||||
}
|
||||
}
|
14
src/main.rs
14
src/main.rs
@ -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()
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
32
src/util.rs
32
src/util.rs
@ -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
|
||||
}
|
60
src/world.rs
60
src/world.rs
@ -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) }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user