Fields

Values on a grid over the world, which agents sense, eat and leave behind, and flow fields to steer along.

agents.Field

agents.Field(
    shape,
    extent,
    *,
    diffusion=0.0,
    decay=0.0,
    regrowth=0.0,
    cap=None,
    edge=FieldEdge.WRAP,
)

Amounts on a grid over the world: food, pheromone, heat. Agents sense and change it.

values[i, j] is the amount in the cell whose centre is at ((i + 0.5) * cell[0], (j + 0.5) * cell[1]), so values.sum() is the total in the world. The grid wraps around its edges, or is walled in with edge=FieldEdge.CLOSED (see kiln.agents.fields). pop.sense doesn’t wrap, even when the field does.

step(dt) changes it over time, in this order:

  • diffusion spreads it like heat: the 5-point Laplacian, with as many substeps as keep it stable, so any dt is safe. It moves amounts between cells and never changes the total, whatever the edge.
  • decay fades every cell by exp(-decay * dt): the total halves every ln 2 / decay seconds.
  • regrowth moves every cell toward its cap by 1 - exp(-regrowth * dt) of the gap, so an eaten patch grows back, fast at first and slower as it fills.

Decay and regrowth together are solved exactly, so one step of dt gives the same field as ten steps of dt / 10; a cell left alone settles at cap * regrowth / (regrowth + decay).

cap can be one number for every cell or one per cell, an (nx, ny) array: a fertility map, on which barren cells (cap 0) hold nothing for long. A deposit there is cut to the cell’s cap, and regrowth relaxes each cell toward its own cap, from above as well as from below, so food left on barren ground fades at the regrowth rate rather than at once.

Cost. Diffusion takes ceil(diffusion * dt * (1 / cell[0]**2 + 1 / cell[1]**2) / 0.25) substeps, each one pass over the grid (about 0.2 ms at 256 x 256). At 60 steps a second over square cells, one substep covers diffusion up to about 7.5 * cell[0]**2; past that, the cost grows in proportion to diffusion, so a fast spread is cheaper on a coarser grid.

The rates and cap are properties that a sketch can change at any time; they are checked as they are set.

Parameters

Name Type Description Default
shape tuple[int, int] The number of cells across and down, (nx, ny). required
extent tuple[float, float] The world’s width and height, in world units. required
diffusion float How fast it spreads, in world units squared per second: a spot spreads over about sqrt(4 * diffusion * t) units in t seconds. 0 for none. 0.0
decay float How fast it fades, per second. 0 for never. 0.0
regrowth float How fast it grows back toward cap, per second. 0 for never; needs a cap. 0.0
cap float | ArrayLike | None The most a cell holds: one number above 0, or an (nx, ny) array of numbers 0 or more, one per cell. A deposit past it is lost, and regrowth stops there. None for no limit. None
edge FieldEdge | str What happens at the edges of the world: FieldEdge.WRAP (the default) or FieldEdge.CLOSED. FieldEdge.WRAP

Attributes

Name Type Description
shape tuple[int, int] (nx, ny), the number of cells across and down.
extent tuple[float, float] (width, height) of the world in world units.

Raises

Name Type Description
ValueError If the shape or extent is not a 2D grid over a world of some size, a rate is negative or not finite, cap is not a number above 0 or an (nx, ny) array of finite numbers 0 or more, regrowth has no cap, or edge is not a FieldEdge.

Methods

Name Description
deposit Adds amounts at positions, each split between the four nearest cells.
draw Draws the field as a picture, one pixel per cell, in the tint colour (kiln.tint).
gradient The slope of the field at each position: which way it rises, and how steeply.
sample The field at each position, interpolated bilinearly between cell centres.
step Advances the field by dt seconds: diffusion, then decay and regrowth.
take Removes up to amount at each position, from the cell it lies in.

deposit

agents.Field.deposit(pos, amount)

Adds amounts at positions, each split between the four nearest cells.

The split is bilinear, so an agent moving smoothly leaves a smooth trail, and the cells gain exactly amount in all, less what a cap cuts off: a cell that a deposit takes past its cap is set to its cap. Cells the deposit doesn’t reach are left as they are, even above their cap.

Parameters
Name Type Description Default
pos ArrayLike (n, 2) positions in world units. required
amount ArrayLike The amount each position adds: one for all, or (n,). Finite, 0 or more. required
Raises
Name Type Description
ValueError If the positions are not (n, 2) or not finite, or an amount is negative or not finite.

draw

agents.Field.draw(x=0.0, y=0.0, w=None, h=None, *, scale=None)

Draws the field as a picture, one pixel per cell, in the tint colour (kiln.tint).

Each cell is as opaque as its amount is to scale: empty cells are clear and cells at scale or more are the full colour, so a field draws over the scene like a stain. Cell (i, j) lands i cells right and j cells down from the top-left corner. The pixels are smoothed when drawn larger than the grid. It uploads the cells to a texture at every call; draw it once per frame.

It draws as Image.draw does: (x, y) is the top-left corner, or the centre after rect_mode(RectMode.CENTER), and the current transform applies.

Parameters
Name Type Description Default
x float Left (or centre) x. 0.0
y float Top (or centre) y. 0.0
w float | None Drawn width; defaults to the world’s width. None
h float | None Drawn height; defaults to the world’s height. None
scale float | None The amount drawn fully opaque; defaults to cap (the largest, for a cap per cell), or to the field’s largest value when it has no cap. None
Raises
Name Type Description
RuntimeError If no App is running.

gradient

agents.Field.gradient(pos)

The slope of the field at each position: which way it rises, and how steeply.

Central differences between neighbouring cells, interpolated bilinearly, in amount per world unit. agents.seek toward pos + gradient climbs toward more food. Across the edges, the neighbours are those of the diffusion: wrapped, or with closed edges the edge cell itself, so the slope there is half the one-sided one.

Parameters
Name Type Description Default
pos ArrayLike (n, 2) positions in world units. required
Returns
Name Type Description
np.ndarray (n, 2) slopes along x and y.
Raises
Name Type Description
ValueError If the positions are not (n, 2) or not finite.

sample

agents.Field.sample(pos)

The field at each position, interpolated bilinearly between cell centres.

Parameters
Name Type Description Default
pos ArrayLike (n, 2) positions in world units. required
Returns
Name Type Description
np.ndarray (n,) values.
Raises
Name Type Description
ValueError If the positions are not (n, 2) or not finite.

step

agents.Field.step(dt)

Advances the field by dt seconds: diffusion, then decay and regrowth.

Parameters
Name Type Description Default
dt float The step length in seconds, 0 or more. required
Raises
Name Type Description
ValueError If dt is negative.

take

agents.Field.take(pos, amount)

Removes up to amount at each position, from the cell it lies in.

Agents never get more than is there. When the agents in a cell ask for more than it holds, they share what it holds in proportion to what each asked for, and the cell is left empty; a cell holding 0 or less gives nothing and is left as it is. The field loses exactly what the agents got::

pop.energy += food.take(pop.pos, bite * dt)
Parameters
Name Type Description Default
pos ArrayLike (n, 2) positions in world units. required
amount ArrayLike How much each agent tries to take: one for all, or (n,). Finite, 0 or more. required
Returns
Name Type Description
np.ndarray (n,) what each agent got: at most its amount, and at most what its cell
np.ndarray held.
Raises
Name Type Description
ValueError If the positions are not (n, 2) or not finite, or an amount is negative or not finite.

agents.FlowField

agents.FlowField(shape, extent, *, edge=FieldEdge.WRAP)

A direction on a grid over the world, for agents.follow to steer agents along.

vectors[i, j] is the flow in the cell whose centre is at ((i + 0.5) * cell[0], (j + 0.5) * cell[1]). Fill it with fill_noise or by hand (flow.vectors[..., 0] = 1 for a wind blowing right). Between cell centres it is interpolated bilinearly. The grid wraps around its edges, or with edge=FieldEdge.CLOSED a position outside the world samples the nearest point inside it.

Parameters

Name Type Description Default
shape tuple[int, int] The number of cells across and down, (nx, ny). required
extent tuple[float, float] The world’s width and height, in world units. required
edge FieldEdge | str What happens at the edges of the world: FieldEdge.WRAP (the default) or FieldEdge.CLOSED. FieldEdge.WRAP

Attributes

Name Type Description
shape tuple[int, int] (nx, ny), the number of cells across and down.
extent tuple[float, float] (width, height) of the world in world units.

Raises

Name Type Description
ValueError If the shape or extent is not a 2D grid over a world of some size, or edge is not a FieldEdge.

Methods

Name Description
fill_noise Fills the field, in place, with unit vectors that turn smoothly across it.
sample The flow at each position, interpolated bilinearly between cell centres.

fill_noise

agents.FlowField.fill_noise(scale, t=0.0)

Fills the field, in place, with unit vectors that turn smoothly across it.

Each cell’s angle is 2 * pi * signed_noise(x * scale, y * scale, t) at its centre, in radians: scale sets the size of the swirls (0.01 turns over about a hundred world units), and moving t slowly animates them. The same arguments give the same field.

Parameters
Name Type Description Default
scale float Noise frequency per world unit. required
t float The time coordinate of the noise. 0.0

sample

agents.FlowField.sample(pos)

The flow at each position, interpolated bilinearly between cell centres.

Parameters
Name Type Description Default
pos ArrayLike (n, 2) positions in world units. required
Returns
Name Type Description
np.ndarray (n, 2) flow vectors.
Raises
Name Type Description
ValueError If the positions are not (n, 2) or not finite.

agents.FieldEdge

agents.FieldEdge()

What a Field or FlowField does at the edges of its world.

Attributes

Name Type Description
WRAP The edges wrap round, like a torus: what leaves on the right comes back on the left, and positions outside the world wrap back into it. The default.
CLOSED The edges are walls: nothing diffuses out through them (each edge cell’s missing neighbour is the cell itself), and positions outside the world are moved to the nearest point inside it.