Life and evolution

Genes that set each agent’s traits, and the life cycle that hands them down, so traits evolve.

agents.Genome

agents.Genome(pop, genes, *, weight_count=0)

The genes of every agent of a population, and the traits they map to.

It adds a genome field to the population (pop.add_field), an (n, gene_count) float64 array of genes in [0, 1], or adopts one the population was made with (Population(n, genome=np.zeros((n, gene_count)))). New agents spawned without genes start with all zeros; agents.reproduce gives children their parent’s genes, mutated.

Each named gene maps to a trait through its range: lo + gene * (hi - lo), so a gene of 0 gives lo and 1 gives hi (a range can run downward). genome[name] is the gene itself, a writable view of its column, and genome.trait(name) the trait. The columns after the named genes are weights, for a brain to read.

Parameters

Name Type Description Default
pop Population The population whose agents carry the genes. required
genes Mapping[str, tuple[float, float]] The named genes in column order, each with the (lo, hi) range of its trait: {"size": (2, 9), "speed": (20, 90)}. required
weight_count int How many more genes to keep after the named ones, for a brain’s weights. 0 for none. 0

Attributes

Name Type Description
pop The population.

Raises

Name Type Description
ValueError If there are no genes at all, a range is not two finite numbers, weight_count is negative, the population has a genome field of another shape or a non-float dtype, or pop.genome is an attribute that is not a field. The population is left as it was.

Methods

Name Description
randomize Gives every agent fresh genes, each uniform in [0, 1], weights included.
trait Every agent’s trait from one gene, mapped through its range: an (n,) array.

randomize

agents.Genome.randomize(rng)

Gives every agent fresh genes, each uniform in [0, 1], weights included.

Parameters
Name Type Description Default
rng np.random.Generator The random generator, such as np.random.default_rng(seed). required

trait

agents.Genome.trait(name)

Every agent’s trait from one gene, mapped through its range: an (n,) array.

Parameters
Name Type Description Default
name str One of names. required
Raises
Name Type Description
ValueError If name is not a gene of this genome.

agents.Crossover

agents.Crossover()

How a child’s genes are mixed from two parents.

Attributes

Name Type Description
UNIFORM Each gene comes from one parent or the other, at even odds (Syswerda’s uniform crossover, 1989).

agents.metabolize

agents.metabolize(pop, dt, cost)

Takes the cost of living out of every agent’s energy: energy -= cost * dt.

The cost can be one rate for all agents or one each, built from their traits and how they live: a big, fast agent burns more than a small, slow one::

speed = np.linalg.norm(pop.vel, axis=1)
agents.metabolize(pop, dt, cost=0.02 + 4e-6 * size * speed**2)

Parameters

Name Type Description Default
pop Population A population with an energy field. required
dt float The step length in seconds, 0 or more. required
cost ArrayLike Energy per second: one for all agents, or (n,). Finite, 0 or more. required

Raises

Name Type Description
ValueError If the population has no energy field, dt is negative, or a cost is negative, not finite, or not one per agent.

agents.reproduce

agents.reproduce(
    pop,
    mask,
    *,
    rng,
    share=0.5,
    mutation=0.05,
    sigma=0.1,
    crossover=None,
    mates=None,
    spread=0.0,
)

Each agent the mask picks has one child, and gives it a share of its energy.

The child gets share of its parent’s energy and the parent keeps the rest, so their energies add up to the parent’s before. The energy is split as it is: a parent at 0 or below has a child at 0 or below, which the usual energy <= 0 death rule takes at once, so pick parents with energy to spare. The child is spawned at the end of the population (pop.spawn) near its parent, of its kind and moving with its velocity; its other fields start at zero, as with spawn, so set them from the returned indices. Spawning clears pop.senses.

When the population carries genes (a Genome’s genome field), the child inherits its parent’s, mutated: each gene, with odds mutation, moves by a normal step of sigma and is reflected back into [0, 1]. With a crossover, the genes are first mixed with the parent’s mate, who pays nothing. The mate is its nearest neighbour of its own kind as seen at the last pop.sense (pop.senses.nearest_of): one in its field of view, if the sense had one, and of the kind and at the place it had then. A parent with no such neighbour breeds alone.

spawn, kill and die clear pop.senses, so a crossover after any of them in the same step needs the mates chosen before. Keep their ids, and turn them back into indices once the dead are gone (a dead mate’s -1 means breeding alone)::

near = pop.senses.nearest_of("grazer")[0]
mate = np.where(near >= 0, pop.id[near], -1)[~dead]
agents.die(pop, dead)
agents.reproduce(pop, ready, rng=rng, crossover="uniform", mates=pop.index_of(mate))

Parameters

Name Type Description Default
pop Population A population with a float energy field. required
mask ArrayLike The parents: a boolean mask of len(pop), or their indices. required
rng np.random.Generator The random generator for the positions and the genes. required
share float The fraction of a parent’s energy its child gets, between 0 and 1. 0.5
mutation float The odds that each gene changes, 0 to 1. 0.05
sigma float How far a changed gene moves: the standard deviation of the step, in gene units (the whole range is 1). 0.1
crossover Crossover | str | None How the genes are mixed with the mate’s, a Crossover; None for no mate. None
mates ArrayLike | None Each agent’s mate, as one index per agent of the population, (n,), of which the parents’ are used; -1 for none. None takes the mates from pop.senses. Needs a crossover. None
spread float How far from the parent the child appears: the standard deviation of its offset on each axis, in world units. 0 puts it on its parent. 0.0

Returns

Name Type Description
np.ndarray The children’s indices, at the end of the population.

Raises

Name Type Description
ValueError If the population has no float energy field, its genome field is not float, share, mutation, sigma or spread is out of range, crossover is not a Crossover, mates is not one index per agent or comes without a crossover, or a crossover of genes needs mates from pop.senses and there are none. Nothing is changed then.
IndexError If a parent’s index or a mate’s index is out of range.

agents.die

agents.die(pop, mask, *, leave=None, amount=1.0)

Kills the agents the mask picks, and leaves their bodies in a field.

Each body adds amount to leave where it died (Field.deposit), so the dead feed the living: what a body is worth is the sketch’s choice, such as its size. The agents are removed with pop.kill, which keeps the survivors in order and clears pop.senses: a crossover later in the step needs its mates chosen before (see reproduce).

Parameters

Name Type Description Default
pop Population The population. required
mask ArrayLike The agents to kill: a boolean mask of len(pop), or their indices. required
leave Field | None The field their bodies go into; None to leave nothing. None
amount ArrayLike What each body adds to leave: one for all, or one per agent of the population, (n,), of which the dead ones’ are used. Finite, 0 or more; checked even with no leave. 1.0

Returns

Name Type Description
np.ndarray Where they died: a (k, dim) array of the dead agents’ positions.

Raises

Name Type Description
ValueError If mask is neither a boolean mask of len(pop) nor indices, or an amount is negative, not finite, or not one per agent.
IndexError If an index is out of range.