On this page
6. Programming patches and their environment
Manual contents | Previous: Programming Main | Next: Programming turtles | Neighborhood reference | NLPatch reference
A patch is one fixed site in the environment. It can hold a state such as burning/not burning, a quantity such as food or heat, and a rule for changing that state. Turtles move between patches; patches do not move.
Start with Fire if you have not yet built a patch model.
This chapter assumes lifecycle: "aligned" and a square grid unless noted.
It explains the choices behind a patch rule, then builds a small heat-spreading
model with an exact check. No knowledge of differential equations is needed.
6.1 Give each rule an owner
| Question | Home |
|---|---|
| How much food or heat exists at one site? | Custom state on Patch |
| How does that site change each tick? | Patch patchStep() |
| Which patch should a turtle move toward? | Turtle turtleStep() |
| Which sites should be active? | Patch setup() for an always-active field; Main for a shared activation policy |
| What is the total across the environment? | Main setup() and update() |
| How big is the grid, and what do colors mean? | Project cell |
Keep environmental quantities separate from their drawing when the quantity needs more precision than a few palette states. Fire can use color as its entire state. Heat below uses a numeric Store and derives a color from it. Do not make another copy of the same quantity in Main.
A patch's hooks, in plain language
| Hook | Runs when | Put here |
|---|---|---|
constructor(col, row, main) |
The patch is created | Preserve super(col, row, main); allocate stable custom fields/Stores |
init() |
Once, after Project's initial patch colors are applied | Register custom Stores, public updaters, and one-time listeners |
setup() |
After populations exist, initially and after world reset | Activate the patch; rebuild population-dependent relationships; restore derived appearance |
patchStep() |
The scheduler visits this patch | Read current state and propose changes; preserve super.patchStep() |
patchShift() |
Only when Main enables doPatchShift |
Optional extra work at the patch shift phase, not ordinary Store commitment |
reset() |
During world reset or an explicit local patch reset | Preserve super.reset(); restore custom local state without reallocating it |
Omit hooks you do not need. Base init/setup/patchShift are empty, but base
step evaluates colorUpdater and base reset restores color.
The runtime commits registered Stores independently of patchShift():
you do not need that hook, doPatchShift, or a manual shift() for a normal
synchronous rule.
Patches survive world reset. Allocate a custom Store once, register it once,
and reinitialize it in reset. Setup repeats; do not install another listener
or create another Store there. A local patch.reset() does not reset time,
reset neighbors, or rerun coordinated setup. Use the browser Reset command or
main.resetWorld() for a whole-world restart.
6.2 Coordinates, lookup, and appearance
In a square grid:
| Property | Meaning |
|---|---|
col, row |
Integer patch address, starting at zero |
location |
Coordinate object at that address; read location.x, location.y |
centerX, centerY |
Drawing/movement center: col + 0.5, row + 0.5 |
main |
The model-wide Main object |
color |
Committed numeric patch-palette value |
Columns increase to the right, rows downward. A turtle at (2.5, 3.5) is
at the center of square patch (2, 3). Cell size is a drawing size in pixels;
it does not change those world coordinates. Treat patch coordinates as fixed.
Hex grids have different address/center conventions; use
hex-aware neighborhoods
and a HexPatch subclass rather than copying square offsets.
Inside a patch rule, this.main.patch(2.7, 3.2) looks up patch (2, 3).
It floors the coordinates. Edge lookup clamps; it does not return null
outside the world. Wrap lookup maps across the opposite edge. If an outside
site should mean "no neighbor," use a boundary-aware neighborhood operator
or test bounds before lookup. For example, manually looking up the left
neighbor of an Edge patch at column zero would return that same edge patch.
Project's cell.palette contains drawing colors; patch code normally uses
numeric values, not CSS strings:
| Assignment | Effect |
|---|---|
this.color = 1 |
Initialize current/next color and replace its reset baseline |
this.currentColor = 1 |
Change current/next immediately, retaining the baseline |
this.nextColor = 1 |
Propose color for commitment |
this.colorUpdater = () => ... |
Install a function evaluated by the aligned base patch step |
Use the first for deliberate initialization, the second for an immediate edit or setup-derived appearance, and the third for a synchronous rule. An updater is another way to propose color, not a second independent rule: if both it and your step assign nextColor, the last assignment wins. Keep the base-step call at the point where its updater should run.
6.3 Read the old world; propose the new world
Imagine two neighboring patches, one hot and one cold. Each should calculate its change from the same starting temperatures. If the first patch changes its current heat before the second reads it, the second sees a different world. The answer can then depend on traversal order.
For a synchronous patch rule:
- Read each patch's committed state (
coloror a custom Store'sget()). - Calculate this patch's result.
- Assign this patch's pending state (
nextColororstore.next). - Let the runtime commit after the individual rules.
Do not write a neighbor's current state as a shortcut. Ordinary JavaScript fields are useful for constants and scratch calculations, but do not supply this current/next separation automatically.
Reading pending state is a different modeling choice. In Grazers, turtles deliberately share a pending food ledger so they cannot claim the same food twice. That ordered allocation is not the independent synchronous calculation used for Fire or the heat field below.
6.4 Make sure the patches actually step
Project visitMode: "C" enables patch rules, but does not select all patches.
For an always-active environment, put this.main.visitAlways(this) in Patch
setup. This is the policy used in all three patch tutorials.
The persistent collection is a Set, so repeating the registration after reset
does not add the patch twice. However, the scheduler visits persistent and
transient collections separately. Do not also use touchPatch() or
addToNextSites() on those patches unless you intend another visit.
Likewise, main.visitAll = true is not a once-per-tick guarantee.
See Main visitation before designing
sparse activation.
Store registration and patch activation do different jobs:
this.main.synchronize(this.heat)arranges commitment when heat is proposed.this.main.visitAlways(this)arranges repeated patch steps.super.patchStep()evaluates a configured built-in color updater.this.heat.reinitialize()restores custom heat on reset.
None of these replaces the other three.
6.5 Choose neighbors and query resident turtles
Use the smallest neighborhood that expresses the rule:
| Need | Operation inside Patch | Center included? |
|---|---|---|
| Nearest surrounding patches | this.neighbors() |
No |
| Four square-grid side neighbors | this.main.nhood4(this) |
No |
| Outer ring at grid distance d | this.ring(d) |
No |
| Filled region through distance d | this.block(d) |
Yes |
| Same region without this patch | this.blockX(d) |
No |
The patch ring/block forms return Sets; Main's nhood4() returns an array.
Both support for...of. neighborVisit(fn) calls a function for each nearest
neighbor without making you manage a result collection.
Grid distances must be nonnegative safe integers.
Neighborhoods return distinct patches, respecting Edge/Wrap boundaries.
On an ordinary square grid a corner has three surrounding neighbors or two
side neighbors with Edge boundaries. On a sufficiently large Wrap grid it
has eight or four. Tiny wrapped worlds can have fewer distinct neighbors.
See the full contracts.
For turtles at a site, this.myTurtles() returns the visible indexed residents
and this.occupied tests whether that list is nonempty. These require the spatial
index; configure agent.agentGrid: true when querying an agent population.
The standard runtime also supplies an empty index in a patch-only world.
Query after population insertion, in setup or later. They read committed
occupancy, not proposed movement. Newborn/dying visibility also has timing
rules; see local agent queries.
Avoid caching a list of turtle objects across reset: turtles are recreated. A patch can ask for its current residents when needed.
6.6 Worked example: spread heat without losing it
We will start with 8 units at the center of a three-by-three square. Each tick, heat flows between side neighbors from warmer to cooler sites. No heat enters or leaves the world. This is a simple discrete mixing rule, not a calibrated physical heat simulation.
Create a Cellular Automaton app named Heat in Studio, as in the
Fire instructions, backing up your current
app first. Replace its Project, Patch, and Main sources with the following
definitions. Keep the generated shell/import wiring. There is no Turtle source
needed for this model; omit Project's agent section and Main's initAgents().
Project: a small closed world
const Project = {
version: 0.2,
name: "Heat",
title: "Heat spreading between patches",
lifecycle: "aligned",
frameWidth: 1170,
frameHeight: 1000,
connect: true,
visitMode: "C",
dt: 1,
cell: {
cellSize: 70,
rowDim: 3,
colDim: 3,
boundary: "Edge",
palette: [
"#101020", "#25345d", "#354f86", "#456db0", "#588cce",
"#80afd8", "#b2cdd5", "#e7d29a", "#ffb347"
]
},
inputs: [
{
code: "MIX", type: "slider", legend: "Mixing per tick",
min: 0, max: 1, step: 0.1, value: 0.5, vtype: "Double"
}
],
displays: [
{
code: "HEAT", site: "display_01", type: "linechart",
dom: [0, 30], ran: [0, 9], width: 460, height: 275,
legend: ["Total heat", "Center heat"],
palette: ["#ff7f0e", "#1f77b4"]
}
]
};
Patch: propose heat, then derive its color
import {NLPatch} from "@nwk/api/nlpatch.js";
import {Store} from "@nwk/api/store.js";
class HeatPatch extends NLPatch {
constructor(col, row, main) {
super(col, row, main);
this.heat = new Store(col === 1 && row === 1 ? 8 : 0);
}
init() {
this.main.synchronize(this.heat);
this.colorUpdater = () => Math.round(this.heat.next);
}
setup() {
this.currentColor = Math.round(this.heat.get());
this.main.visitAlways(this);
}
reset() {
super.reset();
this.heat.reinitialize();
}
patchStep() {
const here = this.heat.get();
let difference = 0;
for (const neighbor of this.main.nhood4(this)) {
difference += neighbor.heat.get() - here;
}
this.heat.next = here + this.main.MIX * difference / 4;
super.patchStep();
}
}
export default HeatPatch;
Read the rule as: "For each side neighbor, add one fourth of MIX times the difference between its heat and mine." A hotter neighbor contributes a gain; a colder neighbor contributes a loss. Each neighbor pair calculates equal and opposite transfers from current values, so total heat stays 8.
Keep the divisor 4, even at an edge or corner. A missing neighbor transfers
nothing; its share stays here. Dividing by each patch's actual neighbor count
would give opposite sides of an edge different transfer amounts and break
conservation. For the supplied MIX range 0 through 1, each new value is a
weighted average of current values, so heat also stays between 0 and 8.
This is a per-tick rule with dt: 1; changing dt alone does not rescale it.
The color updater reads this patch's pending heat, after we calculate it,
so drawing and heat commit together. It does not read neighbors' pending heat.
Using heat.get() in that updater would make the color lag one tick.
Rounding affects only drawing: the Store retains fractional heat.
The base call belongs at the end of this rule because it evaluates the updater.
Do not also call a built-in Store's autoNext() yourself.
Main: report the committed field
import {NLSimMain} from "@nwk/api/nlsimmain.js";
class Heat extends NLSimMain {
constructor(Patch) {
super();
this.Patch = Patch;
}
get MIX() { return this.getParam("MIX"); }
initCells() {
this.initializePatches(this.Patch);
}
setup() {
this.publishHeat();
}
update() {
this.publishHeat();
}
publishHeat() {
const total = this.patches.toList()
.reduce((sum, patch) => sum + patch.heat.get(), 0);
this.show("HEAT", total, this.patch(1, 1).heat.get());
}
}
export default Heat;
reduce adds each patch's heat to a running sum starting at zero.
Main publishes after patch setup at time zero and after commitment on later
ticks. The two numbers match the chart's legend order.
The center seed is custom heat, not Project cell.start color: adding a color
start alone would not initialize the heat Store.
6.7 Predict, step, and reset
Save All and Launch. With MIX 0.5, check the values below. "Side" means any of the four patches adjacent to the center; all four corners also agree.
| Time | Center heat | Each side heat | Each corner heat | Total heat |
|---|---|---|---|---|
| 0 | 8 | 0 | 0 | 8 |
| 1 | 4 | 1 | 0 | 8 |
| 2 | 2.5 | 1.125 | 0.25 | 8 |
At time 1, the center loses 1 to each of four neighbors. Corners are still cold:
heat must not cross two neighbor links in one tick. At time 2 each corner
receives 0.125 from each of its two side neighbors.
The chart shows total and center, not every patch's precise heat; rounded
colors cannot distinguish all the fractional values. For a full numeric
check, temporarily log (col, row, heat.get()) from Main update.
See phase-aware inspection.
Try these experiments one at a time:
- Pause mixing. Change MIX to 0 during the run, then Step. Every heat value stays fixed. Time still advances.
- Restart with today's input. Leave MIX at 0 and Reset. The same patches and Stores survive, heat returns to the center seed, and colors are restored by setup. A further step leaves that seed unchanged. Reset does not restore the slider to 0.5.
- Mix faster. Set MIX to 1, Reset, and Step once. Center heat is 0, each side has 2, corners have 0, and total remains 8.
- Join opposite edges. Restore MIX 0.5 and change boundary to
"Wrap"; save and relaunch. This symmetric center seed actually gives the same values as Edge: sites joined across each seam always have equal heat. To expose the difference, move the seed to the corner by replacing the Store allocation in the Patch constructor with:
this.heat = new Store(col === 0 && row === 0 ? 8 : 0);
Save and relaunch after each boundary change. After one step with Edge,
corner (0, 0) retains 6 and its two side neighbors each have 1.
With Wrap it retains 4 and four side neighbors each have 1, including
(2, 0) and (0, 2) across the seams. Main still charts (1, 1) as
"Center heat"; that is no longer the seed.
Total heat should still be 8, apart from floating-point roundoff.
Restore the center seed and Edge boundaries, save, and relaunch before rechecking the table. Repeat Reset and the first two steps at MIX 0.5. The table must still match. If total drifts substantially, check current-versus-next reads and the fixed divisor before adding a correction or clamping values. Do not sum rounded colors to measure heat.
The guide tests execute these exact sources through the runtime dispatcher. They check the table, simultaneous proposals, colors, conservation/bounds under Edge and Wrap, live input changes, and repeated reset without replacing patches or Stores.
Continue with Programming turtles for movement and sensing, or Grazers and food for turtle claims on a patch resource. Use the neighborhood reference for exact query contracts and state management for more on Store ownership.