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:

  1. Read each patch's committed state (color or a custom Store's get()).
  2. Calculate this patch's result.
  3. Assign this patch's pending state (nextColor or store.next).
  4. 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:

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:

  1. Pause mixing. Change MIX to 0 during the run, then Step. Every heat value stays fixed. Time still advances.
  2. 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.
  3. 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.
  4. 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.