309 lines
13 KiB
Markdown
309 lines
13 KiB
Markdown
# NewAIrium
|
|
|
|
AIrium is an effort to create a working, complex artificial life simulation environment.
|
|
|
|
This is a design paper for the project and its implementation.
|
|
|
|
### Description
|
|
|
|
This new iteration of the project focuses on solving the problems of the first:
|
|
|
|
- variety in creature body and mind structure
|
|
- removing evolution limits (such as brain size)
|
|
- allowing different species to mate and produce possibly stable children
|
|
- speed: trying to get the simulation to be as fast as possible on consumer grade hardware
|
|
- documentation: having an actual model to follow
|
|
- concurrency: try to enable the maximum concurrency when simulating the world, to the point that multiple
|
|
networked computers may collaborate and work together to simulate a world faster or help each other into
|
|
achieving meaningful results faster.
|
|
- java being used to write the implementation
|
|
|
|
#### Possible Applications
|
|
|
|
- studying evolution dynamics
|
|
- discovering new approaches at common problems by looking at how the simulated machines solved them
|
|
- learning how to create the environment necessary for a specific kind of evolution to take place
|
|
- having fun designing such a world and implementing a simulator for it
|
|
- having fun manually designing and creating structures
|
|
- having fun looking at machines interacting with the world and each other
|
|
- bragging about having designed and implemented such a simulation
|
|
- figuring out wether it would actually work
|
|
- distributing a creative alternative to a fish tank
|
|
|
|
### World
|
|
|
|
The world is a matrix where each cell is either empty or occupied by a structure.
|
|
Every structure occupies only one cell.
|
|
|
|
### Structures and Creatures
|
|
|
|
- a structure is a matrix where each cell is either empty or occupied by a Block.
|
|
- each adiacent Block can be connected.
|
|
- the blocks make up both the body and, if the blocks are connected, the mind.
|
|
- structures can move in the world, but can't occupy the same space of another structure.
|
|
|
|
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
|
|
- __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
|
|
|
|
### Signals
|
|
|
|
- a signal is a floating point value. Each block stores a signal.
|
|
- a signal is always between -1 and 1 included.
|
|
|
|
### Connection
|
|
|
|
- Each block can be connected to every other adiacent block (diagonals too),
|
|
so each block has 9 input connection weights: one for each neighbor and one for itself.
|
|
- A connection _is_ a weight, which is a floating point number. All connections are _inputs_.
|
|
- A neuron block's output is its adiacent blocks's current stored signals multiplied by the appropriate weight.
|
|
- the weight (or connection) is a floating point number between -1 and 1 included.
|
|
- If the connection is `0`, then there's no connection
|
|
- if the connection is `not 0` then there is a connection
|
|
- if the connection is `1` then there is a full positive connection, meaning the source value passes
|
|
intact to the destination
|
|
- 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
|
|
|
|
A signal can be any floating point value between -1 and 1.
|
|
|
|
- any signal higher than 1 will be considered to be 1
|
|
- any signal lower than -1 will be considered to be -1
|
|
|
|
### Neuron (Processing) Blocks
|
|
|
|
- new neuron blocks contain the `0` signal (null signal).
|
|
- neuron blocks store a signal until it is changed. The signal is calculated again at every iteration of the
|
|
simulation
|
|
- a neuron block's signal is the sum of all the incoming values, divided then by the sum of all the weights.
|
|
- an incoming value is the signal coming from a connection multiplied by the connection weight.
|
|
|
|
### Action (Output) blocks
|
|
|
|
They take an input signal and act by doing something, for example:
|
|
|
|
- they move the body in space
|
|
- they eject something
|
|
- they change color
|
|
- they interact with other structures
|
|
- they "eat" a block
|
|
- nothing (can be useful to stop signals from passing through)
|
|
|
|
### Sensor (Input) blocks
|
|
|
|
They give adiacent blocks a signal.
|
|
|
|
Some examples:
|
|
|
|
- they communicate how far is the first structure in a straight line
|
|
- they communicate the amount of structures in relation to blank spaces in an area (popularity in an area)
|
|
- they communicate the color/type/activity of the first block in a straight line
|
|
- they communicate information about the presence of a particular kind of structure in a direction
|
|
- they communicate information about the structure itself, like integrity of adiacent blocks
|
|
|
|
Generated signals are floating point numbers ranging from -1 to 1 included.
|
|
|
|
### Combined (Input+Output) blocks
|
|
|
|
They act as Sensor blocks and Action blocks at the same time.
|
|
|
|
Examples:
|
|
|
|
- a "mouth" block that can communicate wether food is available to eat and can receive eat
|
|
commands at the same time
|
|
|
|
### Reproduction and evolution
|
|
|
|
This requires two action blocks:
|
|
|
|
- the egg block
|
|
- the fertilizer block
|
|
|
|
machines can of course have both blocks, or even multiples of them.
|
|
|
|
- the egg block leaves a structure behind, an egg. This egg will either be fertilized or deplete energy and die.
|
|
- the fertilizer block can be used on egg structure to fertilize them
|
|
|
|
#### Hatching
|
|
|
|
When the egg hatches, a structure is born. The structure is built using a combination of the father's and the mother's blocks, then a mutation is applied to the result.
|
|
|
|
## Energy
|
|
|
|
This requires some new blocks:
|
|
|
|
- the battery block (first mentioned resource block)
|
|
- the energy drain block
|
|
- the generator block
|
|
- the stomach block
|
|
- the energy drain block sucks energy from a structure's batteries.
|
|
- the battery block stores energy and emits the current level to its connections
|
|
- the generator block uses up a lot of energy, then gives back slightly more
|
|
- the stomach block can be used to eat blocks from another structure. It then uses some energy to digest
|
|
the blocks, providing it back with an extra amount after it's done.
|
|
|
|
#### Consumption
|
|
|
|
- each action block uses up a moderate amount of energy when activated
|
|
- each sensor block uses up a small amount of energy when activated
|
|
|
|
### Fitness of a structure
|
|
|
|
The fitness is calculated by dividing the creature's total harvested energy by the creature's total consumed energy.
|
|
|
|
A creature with a fitness lower than or equal to 0 has all its batteries depleted. Then, it is
|
|
either revived by being charged somehow, or it can't do anything and no mind activity can take place.
|
|
|
|
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?
|
|
|
|
The conclusion is that this system is far too complex to consider fitness evaluation.
|
|
|
|
### Complexity of a 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
|
|
|
|
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
|
|
|
|
## Primordial world
|
|
|
|
The world should be composed of a variety of huge, small and anything in between energy farming or storage
|
|
structures.
|
|
|
|
Even better, there should be structures generating energy and shipping it off in space.
|
|
|
|
### Primordial creature
|
|
|
|
Since we can't expect that interesting machines pop up from nothing in our simulation, we need to design one.
|
|
On Earth, it took billions of years for something to show up, but we want to speed things up a little bit.
|
|
|
|
This machine needs:
|
|
|
|
- to search for energy sources
|
|
- to find energy sources and get there
|
|
- to use energy sources to fill up batteries
|
|
- to successfully create fertilized eggs
|
|
|
|
From there, maybe something smarter can show up. It is __essential__ that the user can save a machine at any time,
|
|
even early in an implementation. We __don't__ want to lose good machines!
|
|
|
|
## Interface
|
|
|
|
It is very important to give the user the ability to see the world in motion.
|
|
|
|
The interface should be structured akin to the following guidelines:
|
|
|
|
- a world navigation screen
|
|
- lets the user create an empty structure (needs at least 1 block)
|
|
- structure navigation screen
|
|
- ability to save the structure to file
|
|
- has a small _selected block_ sidebar with block informaton
|
|
- lets the user clear cells or set a block to them
|
|
- block navigation screen
|
|
- provides all info about the block and signal activity information
|
|
- lets the user view all connections and edit them
|
|
- lets the user edit the block type, preserving the connections
|
|
|
|
A sidebar has controls to handle simulation speed and pausing and other information such as:
|
|
|
|
- current CPU load.
|
|
- available energy in the world and amount being generated
|
|
- population information
|
|
|
|
## Implementation
|
|
|
|
The rust programming language is the best suited for a variety of reasons, namely:
|
|
|
|
- it is a fast language, well suited for performance intensive tasks
|
|
- it has a package manager that allows a programmer to focus on writing the code, not
|
|
desiging infrastructure, build systems, or coding functions already available in popular libraries
|
|
- it produces native, multiplatform binaries
|
|
- it obviously supports all the required features for the implementation of the simulation
|
|
|
|
But most importantly:
|
|
|
|
- it is built with a focus on type safety, concurrency and thread safety
|
|
- it's impossible to compile a thread unsafe concurrent program with the rust reference compiler
|
|
|
|
### Simulation iteration evaluation algorithm
|
|
|
|
- __MT__: manager/main thread, controls the flow, HIGHEST PRIORITY
|
|
- __WT__: worker thread, evaluates a simulation iteration for one or more structures
|
|
- __AT__: applier threads, applies actions and writes the `new stored signal` into the `current stored signal`
|
|
for all blocks
|
|
|
|
One tick is evaluated using the following algorithm:
|
|
|
|
1. __MT__
|
|
- rebalances the __WTs__ work load, evaluating it based on structure complexity
|
|
- assignes newborn structures to the workers
|
|
- starts all the workers
|
|
1. __WTs__ work, __MT__ waits until they are done
|
|
1. __MT__ assigns actions returned by all the __WTs__ to a number of __ATs__
|
|
- tries to balance the __ATs__' work
|
|
- groups events from creatures that interact with each other together, keeping it thread safe but as distributed
|
|
across threads as possible
|
|
1. __ATs__ work, __MT__ waits untile they are done
|
|
|
|
It's probably not as fast as it gets, but it should be decent
|
|
|
|
#### Structure iteration evaluation algorithm
|
|
|
|
1. the `new stored signal` of each block is calculated
|
|
- if the block is a neuron block, calculate its `new stored signal` using immutable references to the other blocks
|
|
and the connection values.
|
|
- if the block is an input block, run its logic to figure out what its output should be
|
|
- if the block is an output block and it should perform an action, add it to the action queue
|
|
- an action is a function or method and a list of parameters to pass to it (if it takes parameters)
|
|
- This `new stored signal` is stored in a special variable without overwriting the `current stored signal`
|
|
so that the process can be easily accomplished in multithreading execution environments.
|
|
|
|
__Note:__ the bigger a machine, the slower it reacts to inputs because signals need more time to travel through the body
|
|
|
|
### Structs, traits and methods
|
|
|
|
- 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
|
|
|
|
- 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?
|
|
|
|
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.
|