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.
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.
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.
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.
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.