Interventions
An intervention changes the ecosystem: it destroys habitat, moves individuals, converts land, or alters what a layer will do next. That is a different thing from a layer change, which changes one layer's values as a pure function of elapsed time - and the two are deliberately separate mechanisms.
| changes | is a function of | applied | |
|---|---|---|---|
AbstractLayerChange | one layer's values | elapsed time only | redundantly on every MPI rank |
Intervention | the ecosystem | a schedule | once, identically everywhere |
An intervention answers three questions, and each is a type rather than a callback - so what one does is visible without running it:
eco = makeeco()
simulate!(eco, 5year, 1month_mean_duration,
intervention = Intervention(AtTime(2year), RandomCells(20), Deactivate()))
count(parent(eco.habitat.active)) # 20 cells destroyed80When - the schedule
EveryStep, AtTime, AtTimes, BetweenTimes, EveryInterval (every whole multiple of an interval, counted from the start of the run), AtDates and EveryYear (real dates, placed through the run's epoch and calendar, so a run needs an epoch to use them) and NeverScheduled (for disabling one without removing it).
A one-off schedule fires on the step that reaches its instant, not on one that equals it: elapsed time accumulates as a float and a run's steps need not land on the instant exactly, so an equality test would silently never fire. An instant at or before the start of the run acts on the starting state, before the first step's births and deaths. EveryStep and BetweenTimes act over the steps taken, so they first act after the first step.
A date schedule is held to the steps more strictly, because a date is meant to be a date. Each date the run reaches must fall at the end of a step, or the step must be no longer than a day; otherwise simulate! refuses before the first step, naming the date and the one it would have acted on. So EveryYear() at steps of month_mean_duration needs calendar = MeanMonths() - under exact dates 1 January drifts against the steps and would be acted on in late January in three years out of four
- and
AtDates([Date(2000, 3, 15)])needs daily steps, since no monthly step ends on the fifteenth.
Where - the region
AllCells, ActiveCells, CellMask (your own boolean grid), RandomCells and SpreadingCells (a contiguous cluster).
A count may be exact or a rate. A rate means each candidate is taken independently over the step - a binomial draw - which is how habitat is actually lost:
eco = makeeco()
simulate!(eco, 10year, 1month_mean_duration,
intervention = Intervention(EveryStep(), RandomCells(0.05 / year), Deactivate()))
100 - count(parent(eco.habitat.active)) # cells lost, drawn not dictated34What - the operations
A closed set: Deactivate, Reactivate, SetLandCover, SetChange, AddAbundance, RemoveAbundance, AddAbundanceTable and AddSpecies. Closed on purpose - a callback could do anything, including the things that break reproducibility and MPI, whereas named operations can each be checked once and then trusted.
Deactivate kills what lives there. Destroying a cell is not a pause: a deactivated cell is skipped by the simulation entirely, so anything left in it would neither breed nor die - a frozen population with no ecological meaning. Reactivate does not bring them back either; it makes the cell habitable again so that dispersal can recolonise it, as vegetation returns to a slag heap once it stops being used. To restock deliberately, add AddAbundance alongside it.
Several operations, one region
Operations after the first share the same resolved region, applied in order - clear ground and plant a crop on exactly the ground you cleared:
eco = makeeco()
simulate!(eco, 5year, 1month_mean_duration,
intervention = Intervention(EveryStep(), RandomCells(0.02 / year),
Deactivate(), AddAbundance(5, 500)))
converted = findall(.!vec(parent(eco.habitat.active)))
all(eco.abundances.matrix[5, converted] .>= 500)trueTwo interventions could not do this: each resolves its own region, and a random one draws its own cells, so the crop would be planted somewhere other than the cleared ground.
Adding individuals from a table
AddAbundanceTable adds individuals as a table lists them - which species, which cell, how many and when - so a population can be seeded from dated records by one operation rather than by an intervention per species and month. Its rows say when, so it goes in an intervention of its own with EveryStep(), and the region filters the rows: ActiveCells() leaves out any in an inactive cell. build_abundance_table turns species names and (y, x) grid indices into the indexed, time-sorted table it reads best:
eco = makeeco()
records = (species = fill(eco.spplist.names[1], 3), y = [1, 1, 2], x = [1, 1, 3],
time = [1.0, 1.0, 6.0] .* month_mean_duration)
table = build_abundance_table(eco, records)(species = [1, 1], cell = [1, 22], count = [2, 1], time = Unitful.Quantity{Float64, 𝐓, Unitful.FreeUnits{(s,), 𝐓, nothing}}[2.6298e6 s, 1.57788e7 s])simulate!(eco, 1year, 1month_mean_duration,
intervention = Intervention(EveryStep(), ActiveCells(), AddAbundanceTable(table)))A table offering whole columns - a named tuple of vectors, a DataFrame, a memory-mapped Arrow file - is searched for the rows each step needs, and can serve any number of runs. One read row by row, such as a CSV.Rows or a generator, is read once as the run proceeds, so its rows must be in time order.
Several interventions
InterventionSet applies them in the order written, every step.
Reproducibility
Selections come from a counter-based stream, seeded from (seed, :intervention, k, step), which generalises the per-species scheme. So a run replays exactly, every MPI rank and thread computes the same selection without communicating, and species streams stay reserved for birth, death and dispersal, meaning adding an intervention cannot re-phase the demography.
Ordering
Interventions run inside update!: after the population dynamics, after the clock advances, and before the layer update. So a SetChange installed on a step takes effect that same step rather than one step late.
Worked examples
examples/interventions/ recreates a published set of scenarios - steady warming and drying, a seasonal cycle, random and clustered habitat loss, land conversion, and generalist and specialist invasions - and examples/interventions.jl runs them as part of the test suite. examples/models.jl reuses the same scenarios verbatim, following them through time with a full suite of diversity measures.
Contrast with a layer change, which is the other mechanism: Time in EcoSISTEM and examples/VaryingClimate.jl cover it. The distinction is not stylistic. A layer change is a pure function of elapsed time, so it can be applied redundantly on every MPI rank and still agree; an intervention mutates the ecosystem, so it must be applied once and identically everywhere. That is also why the operation set here is closed rather than a callback - a callback could draw from the global RNG or write a layer's matrix, and either would silently desynchronise ranks.