Small neural networks per agent, their weights read from the genes and run in one batched pass, the rays that feed them, and a generational genetic algorithm that evolves them.
The shape of a small neural network that every agent of a population runs.
The network is fully connected: each layer’s outputs are the activation of a weighted sum of the layer before, plus a bias. Brains holds only the shape; the weights are per agent, an (n, weight_count) array of genes in [0, 1] (usually Genome.weights), each gene g standing for the weight 2 g - 1. For each layer of a inputs and b outputs, an agent’s genes hold the a * b weights (input by input, so the weights from input i are genes i * b to i * b + b - 1) and then the b biases.
Parameters
Name
Type
Description
Default
layers
Sequence[int]
The number of values in each layer, inputs first and outputs last, such as (16, 12, 2): 16 inputs, a hidden layer of 12, 2 outputs. At least two.
(16, 16, 4)
activation
Activation | str
The activation of the hidden layers, an Activation.
Activation.TANH
output
Activation | str
The activation of the output layer. TANH, the default, gives outputs in (-1, 1), as drive wants.
Activation.TANH
Attributes
Name
Type
Description
layers
The layer sizes, a tuple.
activation
The hidden layers’ Activation.
output
The output layer’s Activation.
Raises
Name
Type
Description
ValueError
If there are fewer than two layers, a size is below 1, or an activation is not an Activation.
Runs every agent’s network on its inputs, all at once.
Each layer is one batched matrix product over the agents. The weights are read as 2 g - 1 from the genes without making a copy of them: the sum x . (2 g - 1) is worked out as 2 (x . g) - sum(x).
dtype=np.float32 runs the network in single precision, about twice as fast for thousands of agents, to within about 1e-5 of float64. Genes of another dtype are copied to it on every call, so for the speed cast them once a generation, when they change, and pass the copy::
w32 = genome.weights.astype(np.float32) # after each next_generation
outputs = brains.forward(w32, inputs, dtype=np.float32)
Parameters
Name
Type
Description
Default
weights
ArrayLike
(n, weight_count) genes in [0, 1], one row per agent, such as genome.weights.
required
inputs
ArrayLike
(n, layers[0]) inputs, one row per agent.
required
dtype
DTypeLike
The precision to run in: np.float64 (the default) or np.float32.
np.float64
Returns
Name
Type
Description
np.ndarray
(n, layers[-1]) outputs, a new array of dtype.
Raises
Name
Type
Description
ValueError
If the shapes don’t fit the network or each other, or dtype is neither float64 nor float32.
agents.Activation
agents.Activation()
What a layer of a Brains does to its sums.
Attributes
Name
Type
Description
TANH
tanh, in (-1, 1). The default.
RELU
max(0, x).
SIGMOID
1 / (1 + exp(-x)), in (0, 1), worked out as (1 + tanh(x / 2)) / 2 so large sums don’t overflow.
What each agent reads along k rays: how close agents of some kinds are, and fields.
The rays fan out from each agent’s position across fov radians centred on its heading, as ray_directions gives them: ray 0 is turned furthest toward -y, and an agent with no velocity looks along +x. Rays are thin: a body between two rays is not seen, so give small bodies a larger radii or use more rays.
For each kind in kinds, a ray reads its closeness to the body of the first agent of that kind it meets (a disc of radius radii): 1 - distance / reach, so 0 is nothing within reach and 1 is touching. An agent standing inside a body reads 1, and never sees its own. The search is in plain space, as pop.sense is: rays don’t wrap around the edges of the world.
For each field in fields, a ray reads the field at its tip, reach from the agent (Field.sample), as it is. A tip past the field’s world follows the field’s edge: it wraps round with FieldEdge.WRAP and reads the edge cells with FieldEdge.CLOSED.
Parameters
Name
Type
Description
Default
pop
Population
A 2D population.
required
k
int
The number of rays per agent, a whole number, 1 or more.
required
fov
float
The width of the fan in radians, above 0 and at most 2 * pi.
required
reach
float
The length of each ray in world units, above 0.
required
kinds
Sequence[str]
The kinds of agent the rays see, each read separately, each once.
()
fields
Sequence[Field]
The Fields read at the rays’ tips (not FlowFields).
()
radii
ArrayLike
The radius of every agent’s body, as the rays see it: one for all, or (n,). 0 or more.
1.0
who
ArrayLike | None
A boolean mask, one per agent, of the agents that cast rays; None for all. Agents left out are still seen, and cost nothing to sense for.
None
Returns
Name
Type
Description
np.ndarray
An (n, k * (len(kinds) + len(fields))) float64 array, or one row per agent
np.ndarray
who marks, in order: k columns per kind, in the order given, then k
np.ndarray
per field.
Raises
Name
Type
Description
ValueError
If the population is not 2D, k, fov, reach or radii is out of range, a kind is not one of the population’s or is given twice, a field is a FlowField, who is not a boolean mask of one per agent, or a position is not finite.
agents.ray_directions
agents.ray_directions(pop, k, fov, *, who=None)
The unit direction of each of the k rays that rays casts from each agent.
The rays fan out across fov radians centred on each agent’s heading (the way its velocity points; +x for an agent with no velocity), each in the middle of an equal slice of the view, so k=1 looks straight ahead, an odd k always has a ray straight ahead, and fov=2 * np.pi looks evenly all around. Ray 0 is the one turned furthest toward -y (counter-clockwise on screen, where y points down), and the rest follow in order. Take them in the same step as rays to draw the rays an agent sensed with: its heading changes once it is driven.
Parameters
Name
Type
Description
Default
pop
Population
A 2D population.
required
k
int
The number of rays per agent, a whole number, 1 or more.
required
fov
float
The width of the fan in radians, above 0 and at most 2 * pi.
required
who
ArrayLike | None
A boolean mask, one per agent, of the agents whose rays to give; None for all.
None
Returns
Name
Type
Description
np.ndarray
An (n, k, 2) float64 array of unit vectors, or one row per agent who marks.
Raises
Name
Type
Description
ValueError
If the population is not 2D, k or fov is out of range, or who is not a boolean mask of one per agent.
New velocities from two outputs per agent: one turns it, the other sets its speed.
The first output, in [-1, 1], turns each agent’s heading (the way its velocity points) by up to max_turn radians per second, positive toward +y. The second, in [-1, 1], sets its speed from 0 (at -1) to max_speed (at 1). Outputs past [-1, 1] are clipped. Set them with pop.vel[:] = drive(...).
The heading lives in the velocity: there is no separate heading field. An agent told to stop keeps a crawl of a millionth of max_speed, so its velocity still holds its heading for the next step; but one whose velocity is zeroed (by a wall, or by your code) loses its heading and turns from +x again.
Parameters
Name
Type
Description
Default
pop
Population
A 2D population.
required
outputs
ArrayLike
(n, 2) or wider: the turn and the speed, as from Brains.forward with a TANH output. Columns after the first two are ignored.
required
dt
float
The step length in seconds, 0 or more.
required
max_speed
ArrayLike
The top speed, 0 or more: one for all, or (n,).
required
max_turn
float
The fastest turn, in radians per second, 0 or more.
required
Returns
Name
Type
Description
np.ndarray
(n, 2) velocities.
Raises
Name
Type
Description
ValueError
If the population is not 2D, outputs is not (n, 2) or wider, or dt, max_speed or max_turn is negative, not finite or (for max_speed) neither one number nor one per agent.
The genes of the next generation, bred from this one’s by fitness.
The fittest elite share of the population is copied as it is, best first; that is ceil(elite * n) agents (with no rounding up from float error, so 0.07 of 100 is 7), and any elite above 0 keeps at least the best. Every other row is a child: two parents picked by selection, their genes mixed by crossover (or one parent’s copied, with None), then mutated: each gene, with odds mutation, moves by a normal step of sigma and is reflected back into [0, 1], as in reproduce. A parent can be picked twice, even as its own mate.
Parameters
Name
Type
Description
Default
genomes
ArrayLike
(n, g) genes in [0, 1], one row per agent, such as pop.genome.
required
fitness
ArrayLike
(n,) scores, higher is better; any finite numbers.
required
rng
np.random.Generator
The random generator for the picks, the crossover and the mutation.
required
elite
float
The share of the population kept unchanged, 0 to 1.
0.05
selection
Selection | str
How each parent is picked, a Selection.
Selection.TOURNAMENT
crossover
Crossover | str | None
How two parents’ genes are mixed, a Crossover; None for one parent.
Crossover.UNIFORM
mutation
float
The odds that each gene of a child changes, 0 to 1.
0.03
sigma
float
How far a changed gene moves: the standard deviation of its step.
0.15
tournament_size
int
How many agents each TOURNAMENT pick compares, 1 or more.
3
Returns
Name
Type
Description
np.ndarray
An (n, g) float64 array of new genes: the elite first, best first, then the
np.ndarray
children. The rows are no longer in the population’s order.
Raises
Name
Type
Description
ValueError
If genomes is not 2-D, fitness is not (n,) or not finite, or elite, mutation, sigma or tournament_size is out of range, or a mode is unknown.
agents.Selection
agents.Selection()
How next_generation picks each parent from the scored population.
Attributes
Name
Type
Description
TOURNAMENT
The fittest of a few agents drawn at random (tournament_size). It only compares fitnesses, so their scale and sign don’t matter. The default.
ROULETTE
An agent drawn with odds in proportion to its fitness, above the lowest one: the least fit is never picked, unless all are equal, when all are as likely. It suits fitness that is 0 or more, such as food eaten: one very low score (a large penalty) squeezes everyone else’s odds together, so for signed fitness, or fitness with outliers, use TOURNAMENT.