Forest fire on hexagonal lattices and graphs
The Drossel-Schwabl forest fire on a triangular lattice of hexagonal cells, or on the nodes of a graph.
- Name:
hexagonal_forest_fire - Field:
physics/criticality - Formalism: lattice dynamics
- References: Drossel and Schwabl 1992; Argun et al. 2021
Factory
@register_model(
field='physics/criticality',
tags=('lattice', 'network', 'fire', 'stochastic', 'power law'),
references=('Drossel and Schwabl 1992', 'Argun et al. 2021'),
defaults={'until': 400, 'dt': 2},
)
def hexagonal_forest_fire(
shape: Sequence[int] = (80, 80),
*,
graph: nx.Graph | None = None,
growth: float = 0.02,
lightning: float = 1e-5,
density: float = 0.5,
cover: Sequence[float] | None = None,
random_state: int | np.random.Generator | None = 0,
) -> System:
"""Return the Drossel-Schwabl forest fire on a triangular lattice of hexagonal cells, or on the nodes of a graph.
Every site of the periodic triangular lattice has six neighbours, and every node of a graph has the neighbours
along its edges. Slow growth and rare lightning give fires of every size.
Args:
shape:
The number of rows and columns of the triangular lattice, unused on a graph.
graph:
The graph whose nodes are the sites, or `None` for the triangular lattice.
growth:
The probability that a tree grows on an empty site per step.
lightning:
The probability that lightning strikes a tree per step.
density:
The initial fraction of sites with a tree.
cover:
The initial cover of every site, 0 for empty, 1 for a tree, and 2 for burning, in lattice shape or node
order, or `None` for random trees at the given density.
random_state:
The seed or generator of the initial forest.
Returns:
system:
The model, with the covers `forest.cover`.
Examples:
>>> import networkx as nx
>>> from skcomplex.models.physics.criticality import hexagonal_forest_fire
>>> hexagonal_forest_fire((10, 10)).elements[0].check().prepare().size_
100
>>> hexagonal_forest_fire(graph=nx.cycle_graph(12)).elements[0].check().prepare().size_
12
"""
space = (
(LatticeSpace(tuple(shape), geometry='triangular') if graph is None else NetworkSpace(graph)).check().prepare()
)
size = int(space.size_ or 0)
initial = (
(np.random.default_rng(random_state).random(size) < density).astype(float)
if cover is None
else np.ravel(np.asarray(cover, dtype=float))
)
forest = LatticeElement(
'forest',
element=ForestElement,
space=space,
values={'cover': initial, 'growth': growth, 'lightning': lightning},
)
neighborhood = SITE_NEIGHBORHOODS['triangular' if graph is None else 'graph']
return System(elements=[forest], interactions=[FireInteraction('fire', target='forest', neighborhood=neighborhood)])
Components
class ForestElement(Element):
"""Site of the Drossel-Schwabl forest fire: empty at 0, a tree at 1, or burning at 2.
A `FireInteraction` grows trees on empty sites with probability `growth`, sets them on fire from a burning
neighbour or from lightning with probability `lightning`, and empties burning sites.
Args:
label:
The label of the site or the lattice group.
visual:
How the element is drawn, or `None` for `default_visual_`.
cover:
The initial cover, 0, 1, or 2.
growth:
The probability that a tree grows on an empty site per step.
lightning:
The probability that lightning strikes a tree per step.
Examples:
>>> from skcomplex.models.physics.criticality import ForestElement
>>> ForestElement('site').growth
0.05
"""
def __init__(
self,
label: str,
visual: VisualSpec | None = None,
*,
cover: Annotated[float, StateSpec(domain=NN)] = 0,
growth: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.05,
lightning: Annotated[float, ParameterSpec(domain=RealSet.closed_open(0, oo))] = 0.0001,
) -> None:
super().__init__(label, visual=visual)
self.cover = cover
self.growth = growth
self.lightning = lightning
class FireInteraction(LocalRuleInteraction):
"""Stochastic rule of the Drossel-Schwabl forest fire on a cover of 0 for empty, 1 for a tree, and 2 for burning.
Every step a tree catches fire from a burning neighbour or from lightning with the probability `lightning` of the
site, a burning site becomes empty, and a tree grows on an empty site with the probability `growth` of the site.
Args:
label:
The label of the interaction, unique among the parts of its system.
target:
The path of the lattice or network group of sites.
neighborhood:
The neighbours a fire spreads to, the von Neumann neighbourhood of a square lattice, the six neighbours of
a triangular lattice, or the neighbours of a node, each with the site itself.
Examples:
>>> from skcomplex.models.physics.criticality import FireInteraction
>>> FireInteraction('fire', target='forest').stochastic
True
"""
def __init__(
self,
label: str,
*,
target: Annotated[str, RoleSpec()],
neighborhood: BaseNeighborhood = NEAREST,
) -> None:
super().__init__(
label,
target=target,
state='cover',
rule=self._burn,
neighborhood=neighborhood,
stochastic=True,
)
@staticmethod
def _burn(cover: Array, *around: Array, growth: float | Array, lightning: float | Array, random: Array) -> Array:
"""Return the covers after trees catch fire, fires burn out, and trees grow."""
burning = np.any(np.stack(around) == BURNING, axis=0) if around else np.zeros(np.shape(cover), dtype=bool)
ignites = burning | (random < lightning)
tree = np.where(ignites, float(BURNING), 1.0)
empty = np.where(random < growth, 1.0, 0.0)
return np.where(cover == BURNING, 0.0, np.where(cover == 1, tree, empty))