Agents

Many agents kept as arrays, with senses, steering forces and a vectorized behaviour tree. Import with from kiln import agents.

agents.Population

agents.Population(n, dim=2, kinds=('agent',), intent=(), **fields)

Agents kept as arrays: positions, velocities, ids, kinds and the sketch’s own fields.

Every per-agent value is a numpy array whose first axis is the agent, so pop.pos[i] is agent i’s position and pop.battery[idx] the batteries of the agents idx::

pop = agents.Population(500, kinds=("prey", "predator"), intent=("goal",),
                        goal=np.full((500, 2), np.nan), battery=np.ones(500))
pop.pos[:] = rng.uniform(0, 800, (500, 2))
pop.kind[:50] = pop.kinds.index("predator")

Agents are spawned at the end and killed anywhere: kill closes the gaps, keeping the survivors in order, so an agent’s index can change. Its id never does, and an id is never given to another agent, so code outside the frame loop refers to agents by id and turns ids into indices with index_of. A Tree follows pop.id by itself.

Intent fields, declared with intent=, are the ones set from outside the simulation (a mind, a network message) through write, which is safe to call from any thread. The simulation reads them and leaves them alone: it keeps its own decisions in ordinary fields.

Units are the sketch’s own: positions in world units (window points, say), velocities in world units per second, times in seconds.

Parameters

Name Type Description Default
n int The number of agents to start with. required
dim int The number of dimensions. 2
kinds Sequence[str] The names of the kinds of agent; pop.kind holds an index into them. ('agent',)
intent Sequence[str] The names of the intent fields. A field that is not given a starting value is an (n,) float array of NaN, for “no intent yet”. ()
**fields ArrayLike Starting values, each with a first axis of n and any shape and dtype after it (a scalar fills an (n,) array). pos and vel default to zeros of shape (n, dim). kind defaults to the first kind; it takes kind indices, a kind name for all agents, or a name per agent. add_field adds a field later. {}

Attributes

Name Type Description
pos np.ndarray (n, dim) float64 positions.
vel np.ndarray (n, dim) float64 velocities.
id np.ndarray (n,) int64 stable ids, read-only, in increasing order.
kind np.ndarray (n,) int64 kind of each agent, an index into kinds.
kinds The kind names, a tuple.
intent The intent field names, a tuple.
dim The number of dimensions.
senses Senses | None What the agents perceived at the last sense, or None before the first and after a spawn or kill.

Raises

Name Type Description
ValueError If a field has the wrong length or a reserved name, kinds is empty or repeats a name, or n or dim is out of range.

Methods

Name Description
add_field Adds a field after the population was made, as if it had been given to it then.
has_field Whether name is a field: pos, vel, kind or one the sketch added.
index_of Turns stable ids into the agents’ current indices.
intent_age Seconds since each agent’s last write of an intent field, on Kiln’s clock.
kill Removes agents and closes the gaps, keeping the survivors in order.
sense Finds every agent’s neighbours within radius and keeps them in pop.senses.
spawn Adds k agents at the end, with new ids.
step Moves every agent along its velocity for dt seconds, capping its speed first.
write Sets intent fields of some agents, from any thread, at the start of the next frame.

add_field

agents.Population.add_field(name, value)

Adds a field after the population was made, as if it had been given to it then.

Parts that keep their own per-agent state use it (a Genome adds genome), and so can a sketch that grows a field later::

pop.add_field("age", np.zeros(len(pop)))

From then on the field is like any other: spawn and kill grow and shrink it with the agents. The array is taken as it is, through np.asarray, without a copy, so value itself becomes the field until the next spawn or kill replaces it with a new array.

Parameters
Name Type Description Default
name str The field’s name: an identifier not starting with _, and not already a field or another attribute of the population. required
value ArrayLike The values, with a first axis of len(pop) and any shape and dtype after it; a scalar fills an (n,) array. required
Raises
Name Type Description
ValueError If the name is reserved or taken, or value does not have one row per agent.

has_field

agents.Population.has_field(name)

Whether name is a field: pos, vel, kind or one the sketch added.

Fields are the per-agent arrays that spawn and kill keep in step with the agents. A plain attribute set on the population is not one.

index_of

agents.Population.index_of(ids)

Turns stable ids into the agents’ current indices.

Parameters
Name Type Description Default
ids ArrayLike One id or an array of ids. required
Returns
Name Type Description
np.ndarray An int64 array of the shape of ids: each agent’s index, or -1 for an id
np.ndarray whose agent has died (or never existed).

intent_age

agents.Population.intent_age(field)

Seconds since each agent’s last write of an intent field, on Kiln’s clock.

A tree can fall back to its own behaviour when an agent’s intent is stale::

@agents.condition
def has_fresh_goal(pop, idx):
    return pop.intent_age("goal")[idx] < 2.0
Parameters
Name Type Description Default
field str An intent field. required
Returns
Name Type Description
np.ndarray An (n,) float array: seconds since the write landed, inf if it never did.
Raises
Name Type Description
ValueError If field is not an intent field.

kill

agents.Population.kill(mask)

Removes agents and closes the gaps, keeping the survivors in order.

Their ids are not given out again. Kill between ticks of a Tree, never from inside one of its leaves: mark the agents in a field there and kill them after the tick. The tree then drops the dead agents’ state at its next tick, without calling any halt hook.

Parameters
Name Type Description Default
mask ArrayLike A boolean array of len(pop), True for the agents to remove, or an array of their indices (integers, so an int array of 0s and 1s is indices). required
Raises
Name Type Description
ValueError If mask is boolean but not of len(pop), or neither boolean nor integer.
IndexError If an index is negative or not below len(pop).

sense

agents.Population.sense(radius, *, fov=None)

Finds every agent’s neighbours within radius and keeps them in pop.senses.

Call it once per simulation step, before anything that reads neighbours: tree conditions read pop.senses, and separate, align and cohere reuse its neighbours for any radius up to this one instead of searching again. The search is in plain space: neighbours don’t wrap around the edges of a world whose positions do.

Parameters
Name Type Description Default
radius float The perception radius in world units, above 0. required
fov float | None The field of view: the width in radians of a cone around each agent’s heading (its velocity), outside which neighbours are not seen. None, the default, sees all around. See Senses. None
Returns
Name Type Description
Senses The new Senses, also stored in pop.senses.
Raises
Name Type Description
ValueError If radius is not above 0, fov is not above 0 and at most 2 * pi, or a position is not finite.

spawn

agents.Population.spawn(k, kind=None, **values)

Adds k agents at the end, with new ids.

Fields not given start at zero (False for booleans, “” for strings), and intent fields at NaN (float) or zero, with no write yet (intent_age is inf).

Parameters
Name Type Description Default
k int How many agents to add. required
kind ArrayLike | None Their kind, a name (or an index into kinds), or one per agent; the first kind when omitted. None
**values ArrayLike Starting values of any fields, each one value for all the new agents or one per agent: pop.spawn(3, pos=(0, 0)) or pos=rng.uniform(0, 9, (3, 2)). {}
Returns
Name Type Description
np.ndarray The new agents’ indices, len(pop) - k to len(pop) - 1.
Raises
Name Type Description
ValueError If k is negative, kind is unknown, values names a field the population does not have, or a value can’t be cast to its field’s dtype.

step

agents.Population.step(dt, max_speed)

Moves every agent along its velocity for dt seconds, capping its speed first.

A velocity faster than max_speed is scaled down to it, keeping its direction; then pos += vel * dt. Both arrays change in place.

Parameters
Name Type Description Default
dt float The step length in seconds, usually from kiln.clock.fixed_steps. required
max_speed ArrayLike The speed limit in world units per second: one for all agents, or an (n,) array (predators faster than prey). math.inf for none. required
Raises
Name Type Description
ValueError If a max_speed is negative.

write

agents.Population.write(ids, **intent)

Sets intent fields of some agents, from any thread, at the start of the next frame.

The values are checked and copied now, on the calling thread, and land on the main thread at the start of the next frame (through kiln.call_soon), before update: a simulation step never sees half a write. Agents that die before the write lands are skipped. Each landing records the time, on Kiln’s clock, for intent_age::

# goal is an (n, 2) intent field. In a thread that listens to a model:
pop.write([17, 42], goal=[[100, 80], [300, 220]])
Parameters
Name Type Description Default
ids ArrayLike The agents’ stable ids (pop.id), one or an array. required
**intent ArrayLike New values of intent fields, each one value for all the agents or one per agent. They are cast to the field’s dtype within their kind of number (an int into a float field, not a float into an int field). {}
Raises
Name Type Description
ValueError If a name is not an intent field of the population, no field is given, or a value does not fit the field’s shape or dtype.

agents.Senses

agents.Senses(pop, radius, *, fov=None)

The neighbours each agent perceives within a radius, at the time of sense.

Made by Population.sense, which keeps the latest in pop.senses. An agent’s neighbours are the other agents closer than radius; an agent never senses itself. The arrays hold one value per agent, in the population’s order, and its kinds, at the time of sense: they stay as they are when agents move or change kind, and stop applying when agents are spawned or killed (pop.senses is then None until the next sense). Neighbours are found in plain space: they don’t wrap around the edges of a world whose positions do.

Each array is worked out the first time it is read, so a step pays only for what it uses.

With a field of view (fov), an agent sees only the neighbours inside a cone fov radians wide, centred on the way it was heading at sense (its velocity then): fov=np.pi / 2 sees 45 degrees either side, and nothing behind. Seeing is one way, so an agent may see a neighbour that does not see it. An agent standing still has no heading and sees all around, and so does every agent with fov of 2 * pi (which is the same as None). count, nearest and nearest_of count only the neighbours seen, and so do separate, align and cohere when they reuse these senses.

Attributes

Name Type Description
radius The radius the neighbours were sensed within, in world units.
fov The width of the view cone in radians, or None for all around.

Methods

Name Description
nearest_of Each agent’s nearest neighbour of one kind, and the distance to it.

nearest_of

agents.Senses.nearest_of(kind)

Each agent’s nearest neighbour of one kind, and the distance to it.

Prey look for the nearest predator with pop.senses.nearest_of("predator"). Only neighbours within radius count. A predator does not find itself, but does find the other predators.

Parameters
Name Type Description Default
kind str One of the population’s kinds. required
Returns
Name Type Description
np.ndarray (index, distance), two (n,) arrays: -1 and inf for an agent with no
np.ndarray neighbour of that kind.
Raises
Name Type Description
ValueError If kind is not one of the population’s kinds.

agents.seek

agents.seek(pop, target, max_speed)

Steers each agent toward a target at full speed.

The force is desired - vel, where the desired velocity points at the target with length max_speed. Agents overshoot and turn back; arrive slows down instead.

Parameters

Name Type Description Default
pop Population The population. required
target ArrayLike One point for all agents, shape (dim,), or one per agent, (n, dim). required
max_speed ArrayLike The desired speed, world units per second: one, or (n,). required

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

Raises

Name Type Description
ValueError If a max_speed is negative.

agents.arrive

agents.arrive(pop, target, max_speed, slow_radius)

Steers each agent toward a target, slowing down to stop on it.

Like seek, but within slow_radius of the target the desired speed falls in proportion to the distance, reaching zero at the target.

Parameters

Name Type Description Default
pop Population The population. required
target ArrayLike One point for all agents, shape (dim,), or one per agent, (n, dim). required
max_speed ArrayLike The desired speed far from the target, world units per second. required
slow_radius ArrayLike The distance at which agents start slowing down, world units, above 0: one, or (n,). required

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

Raises

Name Type Description
ValueError If a max_speed is negative, a slow_radius is not above 0, or slow_radius is an array whose length is not len(pop).

agents.flee

agents.flee(pop, threat, max_speed, radius=np.inf)

Steers each agent within radius of a threat straight away from it at full speed.

The opposite of seek. Agents at radius or further get no force.

Parameters

Name Type Description Default
pop Population The population. required
threat ArrayLike One point for all agents, shape (dim,), or one per agent, (n, dim). required
max_speed ArrayLike The speed to flee at, world units per second: one, or (n,). required
radius float How close the threat must be to be fled, world units; no limit by default. np.inf

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

Raises

Name Type Description
ValueError If a max_speed is negative.

agents.wander

agents.wander(pop, strength, rng)

Turns each agent a little at random, so it meanders.

The force pushes sideways to the agent’s velocity (a still agent, in any direction), by a random amount drawn afresh from rng at every call, so headings drift in a random walk while speeds stay as they are. The same rng state gives the same forces.

Parameters

Name Type Description Default
pop Population The population. required
strength float The size of the push, world units per second squared (the standard deviation along each axis). required
rng np.random.Generator The random generator, such as np.random.default_rng(seed). required

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

agents.separate

agents.separate(pop, radius, mask=None)

Pushes neighbours apart, harder the closer they are.

Each neighbour closer than radius pushes the agent straight away from it by radius - distance: a soft contact, zero at radius, radius units per second squared when they touch. The pushes add up. Agents at the same point don’t push.

With a mask, only the agents it selects take part: a pair pushes only when both of its agents are in the mask, and an agent outside the mask gets no force and gives none. The result is the force the masked agents would get if they were a population of their own. For an agent in the mask it equals the full separate minus the pushes from neighbours outside the mask; for an agent outside it is zero.

Parameters

Name Type Description Default
pop Population The population. required
radius float The personal space, world units. required
mask ArrayLike | None Which agents take part: a boolean array of len(pop). All of them by default. The neighbour search still covers the whole population, so the cost follows len(pop), not the number of agents in the mask. None

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

Raises

Name Type Description
ValueError If mask is not a boolean array of len(pop).

agents.align

agents.align(pop, radius)

Steers each agent toward the mean velocity of its neighbours.

The force is mean_vel - vel over the neighbours closer than radius.

Parameters

Name Type Description Default
pop Population The population. required
radius float How far an agent sees its neighbours, world units. required

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

agents.cohere

agents.cohere(pop, radius)

Pulls each agent toward the centre of its neighbours.

The force is centre - pos over the neighbours closer than radius: a spring of one per second squared.

Parameters

Name Type Description Default
pop Population The population. required
radius float How far an agent sees its neighbours, world units. required

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

agents.follow

agents.follow(pop, flow, max_speed)

Steers each agent along a flow field, as fast as the flow is strong.

The desired velocity points along the flow where the agent is; the force is desired - vel, as in seek. A flow of length 1 or more asks for max_speed, and a weaker one for that fraction of it: where the flow is 0.5 long, agents go at half speed, and where it is zero, they slow down to a stop. After Reynolds, Steering Behaviors For Autonomous Characters (1999), flow field following.

Parameters

Name Type Description Default
pop Population The population, in 2D. required
flow FlowField The FlowField. required
max_speed ArrayLike The desired speed, world units per second: one, or (n,). required

Returns

Name Type Description
np.ndarray (n, 2) forces in world units per second squared.

Raises

Name Type Description
ValueError If a max_speed is negative or the population is not 2D.

agents.pursue

agents.pursue(pop, target, target_vel, max_speed, *, max_lead=None)

Steers each agent toward where a moving target will be, at full speed.

Like seek, aimed at the target’s predicted position: target + target_vel * t, where t is the time the agent takes to cover the distance at max_speed. Far targets are led a lot and near ones hardly at all, so a pursuer cuts a corner that a seeker follows round. max_lead caps t, so a far or slow pursuer doesn’t aim at a point the target won’t reach for ages. After Reynolds, Steering Behaviors For Autonomous Characters (1999).

Parameters

Name Type Description Default
pop Population The population. required
target ArrayLike Where the target is: one point for all agents, (dim,), or one per agent, (n, dim), such as pop.pos[prey]. required
target_vel ArrayLike The target’s velocity, world units per second, shaped as target. required
max_speed ArrayLike The desired speed, world units per second: one, or (n,). required
max_lead float | None The furthest ahead to aim, in seconds of the target’s velocity; None, the default, for no limit. None

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

Raises

Name Type Description
ValueError If a max_speed or max_lead is negative.

agents.evade

agents.evade(
    pop,
    threat,
    threat_vel,
    max_speed,
    radius=np.inf,
    *,
    max_lead=None,
)

Steers each agent within radius of a moving threat away from where it will be.

The opposite of pursue: like flee, from the threat’s predicted position, threat + threat_vel * t, where t is the time the agent would take to cover the distance at max_speed, at most max_lead. Agents at radius or further from the threat (where it is now) get no force. After Reynolds, Steering Behaviors For Autonomous Characters (1999).

Parameters

Name Type Description Default
pop Population The population. required
threat ArrayLike Where the threat is: one point for all agents, (dim,), or one per agent, (n, dim). required
threat_vel ArrayLike The threat’s velocity, world units per second, shaped as threat. required
max_speed ArrayLike The speed to evade at, world units per second: one, or (n,). required
radius float How close the threat must be to be evaded, world units; no limit by default. np.inf
max_lead float | None The furthest ahead to look, in seconds of the threat’s velocity; None, the default, for no limit. None

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

Raises

Name Type Description
ValueError If a max_speed or max_lead is negative.

agents.follow_path

agents.follow_path(
    pop,
    path,
    radius,
    max_speed,
    *,
    closed=False,
    look_ahead=0.25,
)

Steers each agent along a path of line segments, keeping within radius of it.

Each agent looks where it will be in look_ahead seconds and finds the nearest point of the path to there. While that is within radius, it cruises along the path, in the order of its points, at max_speed. Once it would stray further, it steers toward a point radius further along the path from that nearest point. Past the end of an open path, agents turn back round its last point. After Reynolds, Steering Behaviors For Autonomous Characters (1999), path following.

It costs one distance per agent and segment, so keep paths to tens of points.

Parameters

Name Type Description Default
pop Population The population. required
path ArrayLike The path’s points in order, (m, dim) with m of 2 or more. required
radius float The path’s half width, world units, above 0. required
max_speed ArrayLike The desired speed, world units per second: one, or (n,). required
closed bool Whether the path loops from its last point back to its first. False
look_ahead float How far ahead agents look, in seconds of their current velocity. 0.25

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

Raises

Name Type Description
ValueError If the path has fewer than 2 points or another dimension than the population, radius is not above 0, or a max_speed is negative.

agents.contain

agents.contain(pop, bounds, margin, max_speed)

Turns each agent back from the walls of a box before it reaches them.

Within margin of a wall, an agent wants to move away from it, along that axis, at a speed that grows from 0 at the margin’s inner edge to max_speed at the wall: max_speed * (1 - gap / margin), where gap is its distance from the wall. It keeps any faster speed away from the wall, and its velocity along the other axes. Elsewhere it gets no force.

The walls push like springs, so other forces that push agents outward settle them inside the margin, where the two balance, rather than jittering at its edge; a target inside the margin is reached only as far as the forces balance. An agent heading straight at a wall at max_speed turns round about halfway into a margin of max_speed / weight world units, and just short of the wall in one of half that. After Reynolds, Steering Behaviors For Autonomous Characters (1999), containment.

Parameters

Name Type Description Default
pop Population The population. required
bounds ArrayLike The box, as its lowest and highest corners: ((0, 0), (width, height)). required
margin float How far from a wall agents start to turn, world units: 0 or more, and at most half the box’s narrowest side. With 0, only agents outside the box turn back, at max_speed. required
max_speed ArrayLike The speed to turn back at, world units per second: one, or (n,). required

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

Raises

Name Type Description
ValueError If bounds is not two finite corners of the population’s dimension with the lowest below the highest, the margin is negative or more than half the box, or a max_speed is negative.

agents.avoid

agents.avoid(pop, centers, radii, *, look_ahead=1.0)

Steers each agent round the round obstacles ahead of it.

Each agent looks along its velocity as far as it will go in look_ahead seconds. If that line runs into obstacles, it turns, at its current speed, to graze the one the line enters first on the side it is already heading for: along the tangent from the agent to the obstacle’s edge. Head on, it picks a side. An agent inside an obstacle heads straight out (out of the one it is deepest in), or, at its very centre, keeps its heading. Agents standing still get no force. After Reynolds, Steering Behaviors For Autonomous Characters (1999), obstacle avoidance.

Agents graze the edge, and may cut a hair inside it while they turn: add their own size to the radii to keep them clear. Give avoiding priority over other forces (np.where(dodge.any(axis=1, keepdims=True), dodge, others)) rather than adding them up, or the others pull agents into the obstacles.

It costs one test per agent and obstacle.

Parameters

Name Type Description Default
pop Population The population, in 2 or more dimensions. required
centers ArrayLike The obstacles’ centres, (k, dim). required
radii ArrayLike Their radii, world units: one for all, or (k,). required
look_ahead float How far ahead agents look, in seconds of their current velocity. 1.0

Returns

Name Type Description
np.ndarray (n, dim) forces in world units per second squared.

Raises

Name Type Description
ValueError If centers is not (k, dim) or a radius is negative.