Basic usage

This section showcases the main components of the library by detailing the contents of examples/basics.py.

1import os
2
3from abrain import Genome, Point3D as Pt, ANN3D
4from examples.common import example_path

We start by importing the essential components, aliased directly under the main package:

  • Genome abstracts the evolvable part of the library

  • ANN3D is the callable object representing an Artificial Neural Network of emergent topology in 3 dimensions

  • Point3D describes a coordinate in the substrate (“the brain”)

Note

While we illustrate here the use of a 3D ANN, everything (but the 3D rendering) works identically for abrain.ANN2D

11data = Genome.Data.create_for_eshn_cppn(dimension=3, seed=seed)
12g = Genome.random(data)
13for _ in range(mutations):
14    g.mutate(data)

To create a Genome we first need a Data object to store all the shared parameters (labels, random number generator, id managers, …). This is done either via create_for_generic_cppn(), when using the CPPN directly, or with create_for_eshn_cppn(), when using this genome to create ANN3D through ES-HyperNEAT. We can then generate a random Genome by providing this structure to random().

To simulate an evolutionary process, we subject this Genome g to a number of undirected mutations (see Mutations). Again, the shared Data must be provided.

17inputs = [Pt(-1, -1, -1), Pt(-1, -1, 1), Pt(1, -1, -1), Pt(1, -1, 1)]
18outputs = [Pt(0, 1, -1), Pt(0, 1, 0), Pt(0, 1, 1)]

Before instantiating the ANN, we define the coordinates of the neural inputs (sensors) and outputs (effectors). Thanks to the ES-HyperNEAT algorithms the topology (hidden neurons & connections) will be automatically determined. As much as possible, the provided coordinates should respect the geometrical relationships (i.e. bilateral symmetry, front-back …).

Warning

It is essential that neurons are placed at unique coordinates including hidden ones. Safe coordinates for inputs/outputs are of the form

\[\{(x,y,z) /\ \exists c \in \{x,y,z\} /\ \not\exists \{i_1,...,i_n\} /\ c = \sum_{j=1}^n 2^{-i_j}\}\]

with \(1 \leq i_j \leq\) maxDepth

In particular, this means that all outside planes (e.g. \(y=\pm 1\)) can never contain hidden neurons and are thus safe for user-defined inputs/outputs

21ann = ANN3D.build(inputs, outputs, g)
22print(f"empty ANN: {ann.empty()}")
23print(f"maximal depth: {ann.stats().depth}")

Creating the ANN is then as trivial as calling the static build() function with the set of inputs/outputs and the evolved genome. Various statistics can be queried on the resulting object including whether the build procedure resulted in a functional network.

26ann.render3D().write_html(example_path("./sample_ann.html"))

Optionally, one can produce a 3D rendering of the network through the utility function render3D().

29inputs, outputs = ann.buffers()
30inputs[0] = 1
31inputs[1:3] = [data.rng.uniform(-1, 1) for _ in range(2)]
32inputs[3] = -1

Actually using the ANN requires defining the neural inputs at a given time step, which can be done by direct assignment (line 29) or through slices (line 30). At the same time we also retrieve the output buffer which will store the neural responses computed in the next step.

Note

The default activation function for every hidden and output neurons maps 0 to 0. By contrast input neurons expose the exact same value as that provided. This means that providing constant, small values might result in the whole network staying in a quiescent state.

35n = 5
36ann(inputs, outputs, substeps=n)

Following that, we can query the outbound activity by invoking the ANN with both buffers. An optional parameter substeps can be provided if more than a single activation step is desired, e.g. deep networks with a low update rate.

39print("Outputs:", outputs[0], outputs[1:3])

As with the input buffer, the results can be queried individually or in bulk to set the robot’s outputs (motors…).