13 KiB
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 thecurrent stored signal
for all blocks
One tick is evaluated using the following algorithm:
- MT
- rebalances the WTs work load, evaluating it based on structure complexity
- assignes newborn structures to the workers
- starts all the workers
- WTs work, MT waits until they are done
- 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
- 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
- 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 thecurrent stored signal
so that the process can be easily accomplished in multithreading execution environments.
- if the block is a neuron block, calculate its
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.