Skip to content

Fire Simulator

The embrs.fire_simulator package contains the core runtime components of EMBRS. FireSim drives the main simulation loop — advancing time steps, propagating fire, and coordinating weather, logging, and visualization. Cell represents a single hexagonal unit in the grid and stores all per-cell fire behavior state. RealTimeVisualizer provides a live rendering interface that runs alongside a simulation.

Most users interact with FireSim indirectly through a ControlClass subclass, using the methods inherited from BaseFireSim. The classes below are primarily useful for understanding simulation internals or building custom tooling.

FireSim

The main simulation class. Extends BaseFireSim with the Rothermel fire spread loop, crown fire checks, logging, and visualization hooks. User control code receives a FireSim instance as the fire argument in process_state().

Core fire simulation model.

This module defines the FireSim class, which implements wildfire simulation based on fire spread dynamics, wind conditions, and terrain influences. It extends BaseFireSim and incorporates hexagonal grid modeling to track fire behavior at a cellular level.

Classes:

Name Description
- FireSim

The main wildfire simulation model.

.. autoclass:: FireSim :members:

FireSim

Bases: BaseFireSim

A hexagonal grid-based wildfire simulation model.

Extends BaseFireSim and models wildfire spread using Rothermel's fire spread equations. Handles core fire behavior simulation while inheriting grid management, weather, and terrain functionality from the base class.

Attributes:

Name Type Description
logger Logger

Logging utility for storing simulation outputs.

progress_bar tqdm

Progress bar for tracking simulation steps.

agent_list list

Agents interacting with the fire.

agents_added bool

Whether agents have been added to the simulation.

Source code in embrs/fire_simulator/fire.py
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
class FireSim(BaseFireSim):
    """A hexagonal grid-based wildfire simulation model.

    Extends BaseFireSim and models wildfire spread using Rothermel's fire spread
    equations. Handles core fire behavior simulation while inheriting grid management,
    weather, and terrain functionality from the base class.

    Attributes:
        logger (Logger): Logging utility for storing simulation outputs.
        progress_bar (tqdm): Progress bar for tracking simulation steps.
        agent_list (list): Agents interacting with the fire.
        agents_added (bool): Whether agents have been added to the simulation.
    """
    def __init__(self, sim_params: SimParams):
        """Initialize the wildfire simulation.

        Args:
            sim_params (SimParams): Simulation parameters including terrain, fuel,
                wind conditions, and ignition points.
        """
        print("Fire Simulation Initializing...")

        # Variable to store tqdm progress bar
        self.progress_bar = None

        # Variables to keep track of agents in sim
        self._agent_list = []
        self._agents_added = False

        # Reference to visualizer
        self._visualizer = None

        # Reference to active prediction
        self.curr_prediction = None

        super().__init__(sim_params)

        # Log frequency (set to 1 hour by default)
        self._log_freq = int(np.floor(3600 / self._time_step))

        # Dynamic GSI tracking (None if use_gsi is False)
        self._gsi_tracker = self._weather_stream.gsi_tracker
        self._last_gsi_weather_idx = -1
        self._live_mf_changed = False

        self._init_iteration(True)

    def iterate(self):
        """Advance the fire simulation by one time step.

        Updates fire propagation, weather conditions, and burning cell states.
        Handles new ignitions, calculates rate of spread, and transitions fully
        burned cells to BURNT state.

        Side Effects:
            - Updates cell states and fire spread distances.
            - May ignite neighboring cells.
            - Logs changes if logger is configured.
            - Updates visualizer if configured.
        """
        # Set-up iteration
        if self._init_iteration():
            self._finished = True
            return

        # Loop over surface fires
        # Cache frequently-accessed attributes and methods for tight loop
        weather_changed = self.weather_changed
        live_mf_changed = self._live_mf_changed
        weather_idx = self._curr_weather_idx
        weather_stream = self._weather_stream
        ts = self.time_step
        updated_cells = self._updated_cells
        burnt_cells = self._burnt_cells
        update_steady = self.update_steady_state
        propagate = self.propagate_fire
        remove_nbrs = self.remove_neighbors

        # Track cells to remove (avoids copying entire list)
        cells_to_remove = []
        for cell in self._burning_cells:
            if cell.fully_burning:
                cell._set_state(CellStates.BURNT)
                burnt_cells.add(cell)
                cells_to_remove.append(cell)
                updated_cells[cell.id] = cell
                continue

            # Check if conditions have changed
            needs_update = weather_changed or not cell.has_steady_state or live_mf_changed

            if needs_update:
                cell._update_moisture(weather_idx, weather_stream)

            # Van Wagner moisture injection (after DFM update, before surface_fire)
            if cell.water_applied_kJ > 0:
                cell.apply_vw_suppression()
                needs_update = True

            if needs_update:
                update_steady(cell)

            # Set real time ROS and fireline intensity
            accelerate(cell, ts)

            # Update extent of fire along each direction and check for ignition
            propagate(cell)

            # Remove any neighbors that are no longer burnable
            remove_nbrs(cell)

            updated_cells[cell.id] = cell

        # Remove fully burned cells after iteration completes
        if cells_to_remove:
            remove_set = set(cells_to_remove)
            self._burning_cells = [c for c in self._burning_cells if c not in remove_set]

        # Remove suppressed cells (transitioned back to FUEL via partial suppression)
        if self._suppressed_cells:
            suppressed_set = set(self._suppressed_cells)
            self._burning_cells = [c for c in self._burning_cells if c not in suppressed_set]
            for cell in self._suppressed_cells:
                self._updated_cells[cell.id] = cell
            self._suppressed_cells.clear()

        # Get set of spot fires started in this time step
        if self.model_spotting and self._spot_ign_prob > 0:
            self.propagate_embers()

        self.update_control_interface_elements()

        if self.logger:
            self._log_changes()

            if self._iters % self._log_freq == 0:
                self.logger.flush()

        if self._visualizer:
            self._visualizer.cache_changes(self._get_cell_updates())

        self._updated_cells.clear()
        self._iters += 1

    def _log_changes(self):
        """Cache all simulation updates for logging.

        Collects cell state changes, agent positions, prediction data, and
        action entries, then sends them to the logger for buffered output.

        Side Effects:
            - Caches cell updates via logger.cache_cell_updates.
            - Caches agent updates if agents are registered.
            - Caches current prediction if one exists (then clears it).
            - Caches action entries from the control interface.
        """
        self.logger.cache_cell_updates(self._get_cell_updates())

        if self.agents_added:
            self.logger.cache_agent_updates(self._get_agent_updates())

        if self.curr_prediction is not None:
            self.logger.cache_prediction(self.get_prediction_entry())
            self.curr_prediction = None

        self.logger.cache_action_updates(self.get_action_entries(logger=True))

    def _init_iteration(self, in_constructor: bool = False) -> bool:
        """Initialize or update simulation state for the current iteration.

        Handles first-time initialization and subsequent iteration updates including
        progress bar, weather conditions, new ignitions, and fire spread parameters.

        Args:
            in_constructor (bool): True when called from __init__, False otherwise.

        Returns:
            bool: True if simulation should terminate, False otherwise.
        """
        if in_constructor:
            if self.progress_bar is None:
                self.progress_bar = tqdm(total=self._sim_duration/self.time_step,
                                     desc='Current sim ', position=0, leave=False)

            self.weather_changed = True
            self._new_ignitions = []
            for cell, loc in self.starting_ignitions:
                cell._arrival_time = self.curr_time_m
                cell.get_ign_params(loc)

                if not cell.fuel.burnable:
                    continue

                cell._update_moisture(self._curr_weather_idx, self._weather_stream)
                cell._set_state(CellStates.FIRE)
                surface_fire(cell)
                crown_fire(cell, self.fmc)

                cell.has_steady_state = True
                accelerate(cell, self.time_step)

                self._updated_cells[cell.id] = cell
                self._new_ignitions.append(cell)

        else:
            for cell in self._new_ignitions:
                cell._arrival_time = self.curr_time_m
                surface_fire(cell)
                crown_fire(cell, self.fmc)

                for neighbor in list(cell.burnable_neighbors.keys()):
                    self._frontier.add(neighbor)

                if cell._break_width > 0:
                    flame_len_ft = calc_flame_len(cell)
                    flame_len_m = ft_to_m(flame_len_ft)

                    hold_prob = cell.calc_hold_prob(flame_len_m)

                    rand = np.random.random()

                    cell.breached = rand > hold_prob

                else:
                    cell.breached = True

                cell.has_steady_state = True

                if self.model_spotting:
                    if not cell.lofted and cell._crown_status != CrownStatus.NONE and self._spot_ign_prob > 0:
                        self.embers.loft(cell, self.curr_time_m)

                accelerate(cell, self.time_step)

        # Update current time
        self._curr_time_s = self.time_step * self._iters
        if self.progress_bar:
            self.progress_bar.update()

        # Add any new ignitions to the current set of burning cells
        self._burning_cells.extend(self._new_ignitions)
        # Reset new ignitions
        self._new_ignitions = []

        if self._curr_time_s >= self._sim_duration or (self._iters != 0 and len(self._burning_cells) == 0):
            if self.progress_bar:
                self.progress_bar.close()

            return True

        # Update wind if necessary
        self.weather_changed = self._update_weather()

        # Reset live moisture flag and check for GSI day boundary
        self._live_mf_changed = False
        if self._gsi_tracker is not None:
            self._update_gsi()

        return False

    def _update_gsi(self):
        """Feed hourly weather to the GSI tracker and trigger daily update.

        Called each iteration from :meth:`_init_iteration`. Only acts
        when the weather index has advanced (new hourly entry). If the
        calendar day changed, finalizes the previous day's summary and
        triggers :meth:`_apply_gsi_update` to recompute live moistures.
        """
        weather_idx = self._curr_weather_idx

        if weather_idx == self._last_gsi_weather_idx:
            return

        entry = self._weather_stream.stream[weather_idx]
        sim_dt = self._start_datetime + timedelta(seconds=self._curr_time_s)

        day_changed = self._gsi_tracker.ingest_hourly(entry, sim_dt)
        self._last_gsi_weather_idx = weather_idx

        if day_changed:
            self._apply_gsi_update()

    def _apply_gsi_update(self):
        """Recompute GSI and update fuel curing and sim-level live moistures.

        Does NOT iterate over cells. Sets :attr:`_live_mf_changed` so
        that burning cells sync their ``fmois`` lazily via
        :meth:`Cell._update_moisture`. Cold cells pick up new values
        when they ignite.
        """
        gsi = self._gsi_tracker.compute_gsi()
        if gsi < 0:
            return

        new_h, new_w = self._weather_stream.set_live_moistures(gsi)

        if abs(new_h - self._live_h_mf) < 1e-6 and abs(new_w - self._live_w_mf) < 1e-6:
            return

        self._live_h_mf = new_h
        self._live_w_mf = new_w

        # Update shared fuel objects (~40 cache entries)
        for fuel in self._fuel_cache.values():
            fuel.update_curing(new_h)

        self._live_mf_changed = True

    def _get_agent_updates(self) -> list:
        """Collect log entries for all registered agents.

        Returns:
            list: List of AgentLogEntry objects, one per agent, containing
                position and display properties at the current simulation time.
        """
        agent_data = [agent.to_log_entry(self.curr_time_s) for agent in self.agent_list]
        return agent_data

    def _get_cell_updates(self) -> list:
        """Collect log entries for all cells modified this iteration.

        Returns:
            list: List of CellLogEntry objects for cells in _updated_cells.
        """
        cell_data = [cell.to_log_entry(self.curr_time_s) for cell in list(self._updated_cells.values())]

        return cell_data

    def set_visualizer(self, visualizer):
        """Set the visualizer reference for this simulation.

        Args:
            visualizer (RealTimeVisualizer): Visualizer instance to use.
        """
        self._visualizer = visualizer

    def visualize_prediction(self, prediction_grid: dict):
        """Display a prediction grid on the visualization.

        Args:
            prediction_grid (dict): Prediction data mapping timestamps to
                coordinate lists.

        Side Effects:
            - Passes prediction to visualizer if available.
            - Stores prediction in self.curr_prediction for logging.
        """
        if self._visualizer is not None:
            self._visualizer.visualize_prediction(prediction_grid)

        self.curr_prediction = prediction_grid

    def visualize_ensemble_prediction(self, prediction_grid: dict):
        """Visualize ensemble prediction results on the current visualization.

        Args:
            prediction_grid (dict): Ensemble prediction data structure containing
                multiple forecast scenarios.

        Side Effects:
            - Passes prediction to visualizer if available.
            - Stores prediction in self.curr_prediction for logging.
        """
        if self._visualizer is not None:
            self._visualizer.visualize_ensemble_prediction(prediction_grid)

        self.curr_prediction = prediction_grid

    @property
    def agent_list(self) -> list:
        """List of agents registered with the simulation."""
        return self._agent_list

    @property
    def agents_added(self) -> bool:
        """True if agents have been registered, False otherwise."""
        return self._agents_added

agent_list property

List of agents registered with the simulation.

agents_added property

True if agents have been registered, False otherwise.

__init__(sim_params)

Initialize the wildfire simulation.

Parameters:

Name Type Description Default
sim_params SimParams

Simulation parameters including terrain, fuel, wind conditions, and ignition points.

required
Source code in embrs/fire_simulator/fire.py
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
def __init__(self, sim_params: SimParams):
    """Initialize the wildfire simulation.

    Args:
        sim_params (SimParams): Simulation parameters including terrain, fuel,
            wind conditions, and ignition points.
    """
    print("Fire Simulation Initializing...")

    # Variable to store tqdm progress bar
    self.progress_bar = None

    # Variables to keep track of agents in sim
    self._agent_list = []
    self._agents_added = False

    # Reference to visualizer
    self._visualizer = None

    # Reference to active prediction
    self.curr_prediction = None

    super().__init__(sim_params)

    # Log frequency (set to 1 hour by default)
    self._log_freq = int(np.floor(3600 / self._time_step))

    # Dynamic GSI tracking (None if use_gsi is False)
    self._gsi_tracker = self._weather_stream.gsi_tracker
    self._last_gsi_weather_idx = -1
    self._live_mf_changed = False

    self._init_iteration(True)

iterate()

Advance the fire simulation by one time step.

Updates fire propagation, weather conditions, and burning cell states. Handles new ignitions, calculates rate of spread, and transitions fully burned cells to BURNT state.

Side Effects
  • Updates cell states and fire spread distances.
  • May ignite neighboring cells.
  • Logs changes if logger is configured.
  • Updates visualizer if configured.
Source code in embrs/fire_simulator/fire.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
def iterate(self):
    """Advance the fire simulation by one time step.

    Updates fire propagation, weather conditions, and burning cell states.
    Handles new ignitions, calculates rate of spread, and transitions fully
    burned cells to BURNT state.

    Side Effects:
        - Updates cell states and fire spread distances.
        - May ignite neighboring cells.
        - Logs changes if logger is configured.
        - Updates visualizer if configured.
    """
    # Set-up iteration
    if self._init_iteration():
        self._finished = True
        return

    # Loop over surface fires
    # Cache frequently-accessed attributes and methods for tight loop
    weather_changed = self.weather_changed
    live_mf_changed = self._live_mf_changed
    weather_idx = self._curr_weather_idx
    weather_stream = self._weather_stream
    ts = self.time_step
    updated_cells = self._updated_cells
    burnt_cells = self._burnt_cells
    update_steady = self.update_steady_state
    propagate = self.propagate_fire
    remove_nbrs = self.remove_neighbors

    # Track cells to remove (avoids copying entire list)
    cells_to_remove = []
    for cell in self._burning_cells:
        if cell.fully_burning:
            cell._set_state(CellStates.BURNT)
            burnt_cells.add(cell)
            cells_to_remove.append(cell)
            updated_cells[cell.id] = cell
            continue

        # Check if conditions have changed
        needs_update = weather_changed or not cell.has_steady_state or live_mf_changed

        if needs_update:
            cell._update_moisture(weather_idx, weather_stream)

        # Van Wagner moisture injection (after DFM update, before surface_fire)
        if cell.water_applied_kJ > 0:
            cell.apply_vw_suppression()
            needs_update = True

        if needs_update:
            update_steady(cell)

        # Set real time ROS and fireline intensity
        accelerate(cell, ts)

        # Update extent of fire along each direction and check for ignition
        propagate(cell)

        # Remove any neighbors that are no longer burnable
        remove_nbrs(cell)

        updated_cells[cell.id] = cell

    # Remove fully burned cells after iteration completes
    if cells_to_remove:
        remove_set = set(cells_to_remove)
        self._burning_cells = [c for c in self._burning_cells if c not in remove_set]

    # Remove suppressed cells (transitioned back to FUEL via partial suppression)
    if self._suppressed_cells:
        suppressed_set = set(self._suppressed_cells)
        self._burning_cells = [c for c in self._burning_cells if c not in suppressed_set]
        for cell in self._suppressed_cells:
            self._updated_cells[cell.id] = cell
        self._suppressed_cells.clear()

    # Get set of spot fires started in this time step
    if self.model_spotting and self._spot_ign_prob > 0:
        self.propagate_embers()

    self.update_control_interface_elements()

    if self.logger:
        self._log_changes()

        if self._iters % self._log_freq == 0:
            self.logger.flush()

    if self._visualizer:
        self._visualizer.cache_changes(self._get_cell_updates())

    self._updated_cells.clear()
    self._iters += 1

set_visualizer(visualizer)

Set the visualizer reference for this simulation.

Parameters:

Name Type Description Default
visualizer RealTimeVisualizer

Visualizer instance to use.

required
Source code in embrs/fire_simulator/fire.py
355
356
357
358
359
360
361
def set_visualizer(self, visualizer):
    """Set the visualizer reference for this simulation.

    Args:
        visualizer (RealTimeVisualizer): Visualizer instance to use.
    """
    self._visualizer = visualizer

visualize_ensemble_prediction(prediction_grid)

Visualize ensemble prediction results on the current visualization.

Parameters:

Name Type Description Default
prediction_grid dict

Ensemble prediction data structure containing multiple forecast scenarios.

required
Side Effects
  • Passes prediction to visualizer if available.
  • Stores prediction in self.curr_prediction for logging.
Source code in embrs/fire_simulator/fire.py
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
def visualize_ensemble_prediction(self, prediction_grid: dict):
    """Visualize ensemble prediction results on the current visualization.

    Args:
        prediction_grid (dict): Ensemble prediction data structure containing
            multiple forecast scenarios.

    Side Effects:
        - Passes prediction to visualizer if available.
        - Stores prediction in self.curr_prediction for logging.
    """
    if self._visualizer is not None:
        self._visualizer.visualize_ensemble_prediction(prediction_grid)

    self.curr_prediction = prediction_grid

visualize_prediction(prediction_grid)

Display a prediction grid on the visualization.

Parameters:

Name Type Description Default
prediction_grid dict

Prediction data mapping timestamps to coordinate lists.

required
Side Effects
  • Passes prediction to visualizer if available.
  • Stores prediction in self.curr_prediction for logging.
Source code in embrs/fire_simulator/fire.py
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
def visualize_prediction(self, prediction_grid: dict):
    """Display a prediction grid on the visualization.

    Args:
        prediction_grid (dict): Prediction data mapping timestamps to
            coordinate lists.

    Side Effects:
        - Passes prediction to visualizer if available.
        - Stores prediction in self.curr_prediction for logging.
    """
    if self._visualizer is not None:
        self._visualizer.visualize_prediction(prediction_grid)

    self.curr_prediction = prediction_grid

Fuel

Base fuel model for Rothermel fire spread calculations.

Encapsulate the physical properties of a fuel type and precompute derived constants used in the Rothermel (1972) equations. Non-burnable fuel types (e.g., water, urban) store only name and model number.

All internal units follow the Rothermel convention: loading in lb/ft², surface-area-to-volume ratio in 1/ft, fuel depth in ft, heat content in BTU/lb.

Attributes:

Name Type Description
name str

Human-readable fuel model name.

model_num int

Numeric fuel model identifier.

burnable bool

Whether this fuel can sustain fire.

dynamic bool

Whether herbaceous fuel transfer is applied.

load ndarray

Fuel loading per class (tons/acre), shape (6,). Order: [1h, 10h, 100h, dead herb, live herb, live woody].

s ndarray

Surface-area-to-volume ratio per class (1/ft), shape (6,).

sav_ratio int

Characteristic SAV ratio (1/ft).

dead_mx float

Dead fuel moisture of extinction (fraction).

fuel_depth_ft float

Fuel bed depth (feet).

heat_content float

Heat content (BTU/lb), default 8000.

rho_p float

Particle density (lb/ft³), default 32.

Source code in embrs/models/fuel_models.py
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
class Fuel:
    """Base fuel model for Rothermel fire spread calculations.

    Encapsulate the physical properties of a fuel type and precompute
    derived constants used in the Rothermel (1972) equations. Non-burnable
    fuel types (e.g., water, urban) store only name and model number.

    All internal units follow the Rothermel convention:
    loading in lb/ft², surface-area-to-volume ratio in 1/ft, fuel depth in
    ft, heat content in BTU/lb.

    Attributes:
        name (str): Human-readable fuel model name.
        model_num (int): Numeric fuel model identifier.
        burnable (bool): Whether this fuel can sustain fire.
        dynamic (bool): Whether herbaceous fuel transfer is applied.
        load (np.ndarray): Fuel loading per class (tons/acre), shape (6,).
            Order: [1h, 10h, 100h, dead herb, live herb, live woody].
        s (np.ndarray): Surface-area-to-volume ratio per class (1/ft),
            shape (6,).
        sav_ratio (int): Characteristic SAV ratio (1/ft).
        dead_mx (float): Dead fuel moisture of extinction (fraction).
        fuel_depth_ft (float): Fuel bed depth (feet).
        heat_content (float): Heat content (BTU/lb), default 8000.
        rho_p (float): Particle density (lb/ft³), default 32.
    """

    def __init__(self, name: str, model_num: int, burnable: bool, dynamic: bool, w_0: np.ndarray,
                 s: np.ndarray, s_total: int, dead_mx: float, fuel_depth: float):
        """Initialize a fuel model.

        Args:
            name (str): Human-readable fuel model name.
            model_num (int): Numeric identifier for the fuel model.
            burnable (bool): Whether this fuel can sustain fire.
            dynamic (bool): Whether herbaceous transfer applies.
            w_0 (np.ndarray): Fuel loading per class (tons/acre), shape (6,).
                None for non-burnable models.
            s (np.ndarray): SAV ratio per class (1/ft), shape (6,).
                None for non-burnable models.
            s_total (int): Characteristic SAV ratio (1/ft).
            dead_mx (float): Dead fuel moisture of extinction (fraction).
            fuel_depth (float): Fuel bed depth (feet).
        """
        self.name = name
        self.model_num = model_num
        self.burnable = burnable
        self.dynamic = dynamic
        self.rel_indices = []

        if self.burnable:
            # Standard constants
            self.s_T = 0.055
            self.s_e = 0.010
            self.rho_p = 32
            self.heat_content = 8000 # btu/lb (used for live and dead)

            # Load data defining the fuel type
            self.load = w_0
            self.s = s
            self.sav_ratio = s_total
            self.fuel_depth_ft = fuel_depth
            self.dead_mx = dead_mx

            # Compute weighting factors
            self.compute_f_and_g_weights()

            # Compute f live and f dead
            self.f_dead_arr = self.f_ij[0, 0:4]
            self.f_live_arr = self.f_ij[1, 4:]

            # Compute g live and g dead
            self.g_dead_arr = self.g_ij[0, 0:4]
            self.g_live_arr = self.g_ij[1, 4:]

            # Compute the net fuel loading
            self.w_0 = TPA_to_Lbsft2(self.load)
            w_n = self.w_0 * (1 - self.s_T)
            self.set_fuel_loading(w_n)

            # Store nominal net dead loading
            self.w_n_dead_nominal = self.w_n_dead

            # Compute helpful constants for rothermel equations
            self.beta = np.sum(self.w_0) / 32 / self.fuel_depth_ft
            self.beta_op = 3.348 / (self.sav_ratio ** 0.8189)
            self.rat = self.beta / self.beta_op
            self.A = 133 * self.sav_ratio ** (-0.7913)
            self.gammax = (self.sav_ratio ** 1.5) / (495 + 0.0594 * self.sav_ratio ** 1.5)
            self.gamma = self.gammax * (self.rat ** self.A) * math.exp(self.A*(1-self.rat))
            self.rho_b = np.sum(self.w_0) / self.fuel_depth_ft
            self.flux_ratio = self.calc_flux_ratio()
            self.E, self.B, self.C = self.calc_E_B_C()
            self.W = self.calc_W(w_0)

            # Mark indices that are relevant for this fuel model
            for i in range(6):
                if self.w_0[i] > 0:
                    self.rel_indices.append(i)

            self.rel_indices = np.array(self.rel_indices)
            self.num_classes = len(self.rel_indices)

    def calc_flux_ratio(self) -> float:
        """Compute propagating flux ratio for the Rothermel equation.

        Returns:
            float: Propagating flux ratio (dimensionless).
        """
        packing_ratio = self.rho_b / self.rho_p
        flux_ratio = (192 + 0.2595*self.sav_ratio)**(-1) * math.exp((0.792 + 0.681*math.sqrt(self.sav_ratio))*(packing_ratio + 0.1))

        return flux_ratio

    def calc_E_B_C(self) -> tuple:
        """Compute wind factor coefficients E, B, and C.

        These coefficients parameterize the wind factor equation in the
        Rothermel model as a function of the characteristic SAV ratio.

        Returns:
            Tuple[float, float, float]: ``(E, B, C)`` wind factor
                coefficients (dimensionless).
        """

        sav_ratio = self.sav_ratio

        E = 0.715 * math.exp(-3.59e-4 * sav_ratio)
        B = 0.02526 * sav_ratio ** 0.54
        C = 7.47 * math.exp(-0.133 * sav_ratio**0.55)

        return E, B, C

    def compute_f_and_g_weights(self):
        """Compute fuel class weighting factors f_ij, g_ij, and category fractions f_i.

        Derive weighting arrays from fuel loading and SAV ratios. ``f_ij``
        gives fractional area weights within dead/live categories. ``g_ij``
        gives SAV-bin-based moisture weighting factors. ``f_i`` gives the
        dead vs. live category fractions.

        Side Effects:
            Sets ``self.f_ij`` (2×6), ``self.g_ij`` (2×6), and
            ``self.f_i`` (2,) arrays.
        """
        f_ij = np.zeros((2, 6))
        g_ij = np.zeros((2, 6))
        f_i = np.zeros(2)

        # ── Class grouping ─────────────────────────────────────────────
        dead_indices = [0, 1, 2, 3]  # 1h, 10h, 100h, dead herb
        live_indices = [4, 5]       # live herb, live woody

        # ── Compute a[i] = load[i] * SAV[i] / 32 ──────────────────────
        a_dead = np.array([
            self.load[i] * self.s[i] / 32.0 for i in dead_indices
        ])
        a_live = np.array([
            self.load[i] * self.s[i] / 32.0 for i in live_indices
        ])

        a_sum_dead = np.sum(a_dead)
        a_sum_live = np.sum(a_live)

        # ── f_ij (fractional weights) ─────────────────────────────────
        for idx in dead_indices:
            f_ij[0, idx] = a_dead[dead_indices.index(idx)] / a_sum_dead if a_sum_dead > 0 else 0.0
        for idx in live_indices:
            f_ij[1, idx] = a_live[live_indices.index(idx)] / a_sum_live if a_sum_live > 0 else 0.0

        # ── g_ij (SAV bin-based moisture weighting) ───────────────────
        def sav_bin(sav):
            if sav >= 1200.0: return 0
            elif sav >= 192.0: return 1
            elif sav >= 96.0: return 2
            elif sav >= 48.0: return 3
            elif sav >= 16.0: return 4
            else: return -1

        # Dead gx bin sums
        gx_dead = np.zeros(5)
        for i in dead_indices:
            bin_idx = sav_bin(self.s[i])
            if bin_idx >= 0:
                gx_dead[bin_idx] += f_ij[0, i]

        # Live gx bin sums
        gx_live = np.zeros(5)
        for i in live_indices:
            bin_idx = sav_bin(self.s[i])
            if bin_idx >= 0:
                gx_live[bin_idx] += f_ij[1, i]

        for i in dead_indices:
            bin_idx = sav_bin(self.s[i])
            g_ij[0, i] = gx_dead[bin_idx] if bin_idx >= 0 else 0.0

        for i in live_indices:
            bin_idx = sav_bin(self.s[i])
            g_ij[1, i] = gx_live[bin_idx] if bin_idx >= 0 else 0.0

        f_i[0] = a_sum_dead / (a_sum_dead + a_sum_live)
        f_i[1] = 1.0 - f_i[0]

        self.f_ij = f_ij
        self.g_ij = g_ij
        self.f_i = f_i

    def set_fuel_loading(self, w_n: np.ndarray):
        """Set net fuel loading and recompute weighted dead/live net loadings.

        Args:
            w_n (np.ndarray): Net fuel loading per class (lb/ft²), shape (6,).

        Side Effects:
            Updates ``self.w_n``, ``self.w_n_dead``, and ``self.w_n_live``.
        """
        self.w_n = w_n
        self.w_n_dead = np.dot(self.g_dead_arr, self.w_n[0:4])
        self.w_n_live = np.dot(self.g_live_arr, self.w_n[4:])

    def calc_W(self, w_0_tpa: np.ndarray) -> float:
        """Compute dead-to-live fuel loading ratio W.

        W is used to determine live fuel moisture of extinction. Returns
        ``np.inf`` when there is no live fuel loading (denominator is zero).

        Args:
            w_0_tpa (np.ndarray): Fuel loading per class (tons/acre),
                shape (6,).

        Returns:
            float: Dead-to-live loading ratio (dimensionless), or ``np.inf``
                if no live fuel is present.
        """
        w = w_0_tpa
        s = self.s

        num = 0

        for i in range(4):
            if s[i] != 0:
                num += w[i] * math.exp(-138/s[i])

        den = 0
        for i in range(4, 6):
            if s[i] != 0:
                den += w[i] * math.exp(-500/s[i])

        if den == 0:
            W = math.inf # Live moisture does not apply here

        else:
            W = num/den

        return W

__init__(name, model_num, burnable, dynamic, w_0, s, s_total, dead_mx, fuel_depth)

Initialize a fuel model.

Parameters:

Name Type Description Default
name str

Human-readable fuel model name.

required
model_num int

Numeric identifier for the fuel model.

required
burnable bool

Whether this fuel can sustain fire.

required
dynamic bool

Whether herbaceous transfer applies.

required
w_0 ndarray

Fuel loading per class (tons/acre), shape (6,). None for non-burnable models.

required
s ndarray

SAV ratio per class (1/ft), shape (6,). None for non-burnable models.

required
s_total int

Characteristic SAV ratio (1/ft).

required
dead_mx float

Dead fuel moisture of extinction (fraction).

required
fuel_depth float

Fuel bed depth (feet).

required
Source code in embrs/models/fuel_models.py
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
def __init__(self, name: str, model_num: int, burnable: bool, dynamic: bool, w_0: np.ndarray,
             s: np.ndarray, s_total: int, dead_mx: float, fuel_depth: float):
    """Initialize a fuel model.

    Args:
        name (str): Human-readable fuel model name.
        model_num (int): Numeric identifier for the fuel model.
        burnable (bool): Whether this fuel can sustain fire.
        dynamic (bool): Whether herbaceous transfer applies.
        w_0 (np.ndarray): Fuel loading per class (tons/acre), shape (6,).
            None for non-burnable models.
        s (np.ndarray): SAV ratio per class (1/ft), shape (6,).
            None for non-burnable models.
        s_total (int): Characteristic SAV ratio (1/ft).
        dead_mx (float): Dead fuel moisture of extinction (fraction).
        fuel_depth (float): Fuel bed depth (feet).
    """
    self.name = name
    self.model_num = model_num
    self.burnable = burnable
    self.dynamic = dynamic
    self.rel_indices = []

    if self.burnable:
        # Standard constants
        self.s_T = 0.055
        self.s_e = 0.010
        self.rho_p = 32
        self.heat_content = 8000 # btu/lb (used for live and dead)

        # Load data defining the fuel type
        self.load = w_0
        self.s = s
        self.sav_ratio = s_total
        self.fuel_depth_ft = fuel_depth
        self.dead_mx = dead_mx

        # Compute weighting factors
        self.compute_f_and_g_weights()

        # Compute f live and f dead
        self.f_dead_arr = self.f_ij[0, 0:4]
        self.f_live_arr = self.f_ij[1, 4:]

        # Compute g live and g dead
        self.g_dead_arr = self.g_ij[0, 0:4]
        self.g_live_arr = self.g_ij[1, 4:]

        # Compute the net fuel loading
        self.w_0 = TPA_to_Lbsft2(self.load)
        w_n = self.w_0 * (1 - self.s_T)
        self.set_fuel_loading(w_n)

        # Store nominal net dead loading
        self.w_n_dead_nominal = self.w_n_dead

        # Compute helpful constants for rothermel equations
        self.beta = np.sum(self.w_0) / 32 / self.fuel_depth_ft
        self.beta_op = 3.348 / (self.sav_ratio ** 0.8189)
        self.rat = self.beta / self.beta_op
        self.A = 133 * self.sav_ratio ** (-0.7913)
        self.gammax = (self.sav_ratio ** 1.5) / (495 + 0.0594 * self.sav_ratio ** 1.5)
        self.gamma = self.gammax * (self.rat ** self.A) * math.exp(self.A*(1-self.rat))
        self.rho_b = np.sum(self.w_0) / self.fuel_depth_ft
        self.flux_ratio = self.calc_flux_ratio()
        self.E, self.B, self.C = self.calc_E_B_C()
        self.W = self.calc_W(w_0)

        # Mark indices that are relevant for this fuel model
        for i in range(6):
            if self.w_0[i] > 0:
                self.rel_indices.append(i)

        self.rel_indices = np.array(self.rel_indices)
        self.num_classes = len(self.rel_indices)

calc_E_B_C()

Compute wind factor coefficients E, B, and C.

These coefficients parameterize the wind factor equation in the Rothermel model as a function of the characteristic SAV ratio.

Returns:

Type Description
tuple

Tuple[float, float, float]: (E, B, C) wind factor coefficients (dimensionless).

Source code in embrs/models/fuel_models.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
def calc_E_B_C(self) -> tuple:
    """Compute wind factor coefficients E, B, and C.

    These coefficients parameterize the wind factor equation in the
    Rothermel model as a function of the characteristic SAV ratio.

    Returns:
        Tuple[float, float, float]: ``(E, B, C)`` wind factor
            coefficients (dimensionless).
    """

    sav_ratio = self.sav_ratio

    E = 0.715 * math.exp(-3.59e-4 * sav_ratio)
    B = 0.02526 * sav_ratio ** 0.54
    C = 7.47 * math.exp(-0.133 * sav_ratio**0.55)

    return E, B, C

calc_W(w_0_tpa)

Compute dead-to-live fuel loading ratio W.

W is used to determine live fuel moisture of extinction. Returns np.inf when there is no live fuel loading (denominator is zero).

Parameters:

Name Type Description Default
w_0_tpa ndarray

Fuel loading per class (tons/acre), shape (6,).

required

Returns:

Name Type Description
float float

Dead-to-live loading ratio (dimensionless), or np.inf if no live fuel is present.

Source code in embrs/models/fuel_models.py
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
def calc_W(self, w_0_tpa: np.ndarray) -> float:
    """Compute dead-to-live fuel loading ratio W.

    W is used to determine live fuel moisture of extinction. Returns
    ``np.inf`` when there is no live fuel loading (denominator is zero).

    Args:
        w_0_tpa (np.ndarray): Fuel loading per class (tons/acre),
            shape (6,).

    Returns:
        float: Dead-to-live loading ratio (dimensionless), or ``np.inf``
            if no live fuel is present.
    """
    w = w_0_tpa
    s = self.s

    num = 0

    for i in range(4):
        if s[i] != 0:
            num += w[i] * math.exp(-138/s[i])

    den = 0
    for i in range(4, 6):
        if s[i] != 0:
            den += w[i] * math.exp(-500/s[i])

    if den == 0:
        W = math.inf # Live moisture does not apply here

    else:
        W = num/den

    return W

calc_flux_ratio()

Compute propagating flux ratio for the Rothermel equation.

Returns:

Name Type Description
float float

Propagating flux ratio (dimensionless).

Source code in embrs/models/fuel_models.py
132
133
134
135
136
137
138
139
140
141
def calc_flux_ratio(self) -> float:
    """Compute propagating flux ratio for the Rothermel equation.

    Returns:
        float: Propagating flux ratio (dimensionless).
    """
    packing_ratio = self.rho_b / self.rho_p
    flux_ratio = (192 + 0.2595*self.sav_ratio)**(-1) * math.exp((0.792 + 0.681*math.sqrt(self.sav_ratio))*(packing_ratio + 0.1))

    return flux_ratio

compute_f_and_g_weights()

Compute fuel class weighting factors f_ij, g_ij, and category fractions f_i.

Derive weighting arrays from fuel loading and SAV ratios. f_ij gives fractional area weights within dead/live categories. g_ij gives SAV-bin-based moisture weighting factors. f_i gives the dead vs. live category fractions.

Side Effects

Sets self.f_ij (2×6), self.g_ij (2×6), and self.f_i (2,) arrays.

Source code in embrs/models/fuel_models.py
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
def compute_f_and_g_weights(self):
    """Compute fuel class weighting factors f_ij, g_ij, and category fractions f_i.

    Derive weighting arrays from fuel loading and SAV ratios. ``f_ij``
    gives fractional area weights within dead/live categories. ``g_ij``
    gives SAV-bin-based moisture weighting factors. ``f_i`` gives the
    dead vs. live category fractions.

    Side Effects:
        Sets ``self.f_ij`` (2×6), ``self.g_ij`` (2×6), and
        ``self.f_i`` (2,) arrays.
    """
    f_ij = np.zeros((2, 6))
    g_ij = np.zeros((2, 6))
    f_i = np.zeros(2)

    # ── Class grouping ─────────────────────────────────────────────
    dead_indices = [0, 1, 2, 3]  # 1h, 10h, 100h, dead herb
    live_indices = [4, 5]       # live herb, live woody

    # ── Compute a[i] = load[i] * SAV[i] / 32 ──────────────────────
    a_dead = np.array([
        self.load[i] * self.s[i] / 32.0 for i in dead_indices
    ])
    a_live = np.array([
        self.load[i] * self.s[i] / 32.0 for i in live_indices
    ])

    a_sum_dead = np.sum(a_dead)
    a_sum_live = np.sum(a_live)

    # ── f_ij (fractional weights) ─────────────────────────────────
    for idx in dead_indices:
        f_ij[0, idx] = a_dead[dead_indices.index(idx)] / a_sum_dead if a_sum_dead > 0 else 0.0
    for idx in live_indices:
        f_ij[1, idx] = a_live[live_indices.index(idx)] / a_sum_live if a_sum_live > 0 else 0.0

    # ── g_ij (SAV bin-based moisture weighting) ───────────────────
    def sav_bin(sav):
        if sav >= 1200.0: return 0
        elif sav >= 192.0: return 1
        elif sav >= 96.0: return 2
        elif sav >= 48.0: return 3
        elif sav >= 16.0: return 4
        else: return -1

    # Dead gx bin sums
    gx_dead = np.zeros(5)
    for i in dead_indices:
        bin_idx = sav_bin(self.s[i])
        if bin_idx >= 0:
            gx_dead[bin_idx] += f_ij[0, i]

    # Live gx bin sums
    gx_live = np.zeros(5)
    for i in live_indices:
        bin_idx = sav_bin(self.s[i])
        if bin_idx >= 0:
            gx_live[bin_idx] += f_ij[1, i]

    for i in dead_indices:
        bin_idx = sav_bin(self.s[i])
        g_ij[0, i] = gx_dead[bin_idx] if bin_idx >= 0 else 0.0

    for i in live_indices:
        bin_idx = sav_bin(self.s[i])
        g_ij[1, i] = gx_live[bin_idx] if bin_idx >= 0 else 0.0

    f_i[0] = a_sum_dead / (a_sum_dead + a_sum_live)
    f_i[1] = 1.0 - f_i[0]

    self.f_ij = f_ij
    self.g_ij = g_ij
    self.f_i = f_i

set_fuel_loading(w_n)

Set net fuel loading and recompute weighted dead/live net loadings.

Parameters:

Name Type Description Default
w_n ndarray

Net fuel loading per class (lb/ft²), shape (6,).

required
Side Effects

Updates self.w_n, self.w_n_dead, and self.w_n_live.

Source code in embrs/models/fuel_models.py
237
238
239
240
241
242
243
244
245
246
247
248
def set_fuel_loading(self, w_n: np.ndarray):
    """Set net fuel loading and recompute weighted dead/live net loadings.

    Args:
        w_n (np.ndarray): Net fuel loading per class (lb/ft²), shape (6,).

    Side Effects:
        Updates ``self.w_n``, ``self.w_n_dead``, and ``self.w_n_live``.
    """
    self.w_n = w_n
    self.w_n_dead = np.dot(self.g_dead_arr, self.w_n[0:4])
    self.w_n_live = np.dot(self.g_live_arr, self.w_n[4:])

BTU_ft2_min_to_kW_m2(f_btu_ft2_min)

Convert heat flux from BTU/(ft^2*min) to kW/m^2.

Parameters:

Name Type Description Default
f_btu_ft2_min float

Heat flux in BTU/(ft^2*min).

required

Returns:

Name Type Description
float float

Heat flux in kW/m^2.

Source code in embrs/utilities/unit_conversions.py
220
221
222
223
224
225
226
227
228
229
def BTU_ft2_min_to_kW_m2(f_btu_ft2_min: float) -> float:
    """Convert heat flux from BTU/(ft^2*min) to kW/m^2.

    Args:
        f_btu_ft2_min (float): Heat flux in BTU/(ft^2*min).

    Returns:
        float: Heat flux in kW/m^2.
    """
    return f_btu_ft2_min * _BTU_FT2_MIN_TO_KW_M2

BTU_ft_min_to_kW_m(f_btu_ft_min)

Convert fireline intensity from BTU/(ft*min) to kW/m.

Parameters:

Name Type Description Default
f_btu_ft_min float

Fireline intensity in BTU/(ft*min).

required

Returns:

Name Type Description
float float

Fireline intensity in kW/m.

Source code in embrs/utilities/unit_conversions.py
232
233
234
235
236
237
238
239
240
241
def BTU_ft_min_to_kW_m(f_btu_ft_min: float) -> float:
    """Convert fireline intensity from BTU/(ft*min) to kW/m.

    Args:
        f_btu_ft_min (float): Fireline intensity in BTU/(ft*min).

    Returns:
        float: Fireline intensity in kW/m.
    """
    return f_btu_ft_min * _BTU_FT_MIN_TO_KW_M

BTU_ft_min_to_kcal_s_m(f_btu_ft_min)

Convert fireline intensity from BTU/(ftmin) to kcal/(sm).

Parameters:

Name Type Description Default
f_btu_ft_min float

Fireline intensity in BTU/(ft*min).

required

Returns:

Name Type Description
float float

Fireline intensity in kcal/(s*m).

Source code in embrs/utilities/unit_conversions.py
244
245
246
247
248
249
250
251
252
253
def BTU_ft_min_to_kcal_s_m(f_btu_ft_min: float) -> float:
    """Convert fireline intensity from BTU/(ft*min) to kcal/(s*m).

    Args:
        f_btu_ft_min (float): Fireline intensity in BTU/(ft*min).

    Returns:
        float: Fireline intensity in kcal/(s*m).
    """
    return f_btu_ft_min * _BTU_FT_MIN_TO_KCAL_S_M

BTU_lb_to_cal_g(f_btu_lb)

Convert heat content from BTU/lb to cal/g.

Parameters:

Name Type Description Default
f_btu_lb float

Heat content in BTU/lb.

required

Returns:

Name Type Description
float float

Heat content in cal/g.

Source code in embrs/utilities/unit_conversions.py
272
273
274
275
276
277
278
279
280
281
def BTU_lb_to_cal_g(f_btu_lb: float) -> float:
    """Convert heat content from BTU/lb to cal/g.

    Args:
        f_btu_lb (float): Heat content in BTU/lb.

    Returns:
        float: Heat content in cal/g.
    """
    return f_btu_lb * _BTU_LB_TO_CAL_G

F_to_C(f_f)

Convert temperature from Fahrenheit to Celsius.

Parameters:

Name Type Description Default
f_f float

Temperature in degrees Fahrenheit.

required

Returns:

Name Type Description
float float

Temperature in degrees Celsius.

Source code in embrs/utilities/unit_conversions.py
48
49
50
51
52
53
54
55
56
57
def F_to_C(f_f: float) -> float:
    """Convert temperature from Fahrenheit to Celsius.

    Args:
        f_f (float): Temperature in degrees Fahrenheit.

    Returns:
        float: Temperature in degrees Celsius.
    """
    return (5.0 / 9.0) * (f_f - 32.0)

KiSq_to_Lbsft2(f_kisq)

Convert fuel loading from kg/m^2 to lb/ft^2.

Parameters:

Name Type Description Default
f_kisq float

Fuel loading in kg/m^2.

required

Returns:

Name Type Description
float float

Fuel loading in lb/ft^2.

Source code in embrs/utilities/unit_conversions.py
156
157
158
159
160
161
162
163
164
165
def KiSq_to_Lbsft2(f_kisq: float) -> float:
    """Convert fuel loading from kg/m^2 to lb/ft^2.

    Args:
        f_kisq (float): Fuel loading in kg/m^2.

    Returns:
        float: Fuel loading in lb/ft^2.
    """
    return f_kisq * _KISQ_TO_LBSFT2

KiSq_to_TPA(f_kisq)

Convert fuel loading from kg/m^2 to tons per acre.

Parameters:

Name Type Description Default
f_kisq float

Fuel loading in kg/m^2.

required

Returns:

Name Type Description
float float

Fuel loading in tons per acre.

Source code in embrs/utilities/unit_conversions.py
204
205
206
207
208
209
210
211
212
213
def KiSq_to_TPA(f_kisq: float) -> float:
    """Convert fuel loading from kg/m^2 to tons per acre.

    Args:
        f_kisq (float): Fuel loading in kg/m^2.

    Returns:
        float: Fuel loading in tons per acre.
    """
    return f_kisq * _KISQ_TO_TPA

Lbsft2_to_KiSq(f_libsft2)

Convert fuel loading from lb/ft^2 to kg/m^2.

Parameters:

Name Type Description Default
f_libsft2 float

Fuel loading in lb/ft^2.

required

Returns:

Name Type Description
float float

Fuel loading in kg/m^2.

Source code in embrs/utilities/unit_conversions.py
144
145
146
147
148
149
150
151
152
153
def Lbsft2_to_KiSq(f_libsft2: float) -> float:
    """Convert fuel loading from lb/ft^2 to kg/m^2.

    Args:
        f_libsft2 (float): Fuel loading in lb/ft^2.

    Returns:
        float: Fuel loading in kg/m^2.
    """
    return f_libsft2 * _LBSFT2_TO_KISQ

Lbsft2_to_TPA(f_lbsft2)

Convert fuel loading from lb/ft^2 to tons per acre.

Parameters:

Name Type Description Default
f_lbsft2 float

Fuel loading in lb/ft^2.

required

Returns:

Name Type Description
float float

Fuel loading in tons per acre.

Source code in embrs/utilities/unit_conversions.py
192
193
194
195
196
197
198
199
200
201
def Lbsft2_to_TPA(f_lbsft2: float) -> float:
    """Convert fuel loading from lb/ft^2 to tons per acre.

    Args:
        f_lbsft2 (float): Fuel loading in lb/ft^2.

    Returns:
        float: Fuel loading in tons per acre.
    """
    return f_lbsft2 * _LBSFT2_TO_TPA

TPA_to_KiSq(f_tpa)

Convert fuel loading from tons per acre to kg/m^2.

Parameters:

Name Type Description Default
f_tpa float

Fuel loading in tons per acre.

required

Returns:

Name Type Description
float float

Fuel loading in kg/m^2.

Source code in embrs/utilities/unit_conversions.py
168
169
170
171
172
173
174
175
176
177
def TPA_to_KiSq(f_tpa: float) -> float:
    """Convert fuel loading from tons per acre to kg/m^2.

    Args:
        f_tpa (float): Fuel loading in tons per acre.

    Returns:
        float: Fuel loading in kg/m^2.
    """
    return f_tpa / _TPA_TO_KISQ_DIVISOR

TPA_to_Lbsft2(f_tpa)

Convert fuel loading from tons per acre to lb/ft^2.

Parameters:

Name Type Description Default
f_tpa float

Fuel loading in tons per acre.

required

Returns:

Name Type Description
float float

Fuel loading in lb/ft^2.

Source code in embrs/utilities/unit_conversions.py
180
181
182
183
184
185
186
187
188
189
def TPA_to_Lbsft2(f_tpa: float) -> float:
    """Convert fuel loading from tons per acre to lb/ft^2.

    Args:
        f_tpa (float): Fuel loading in tons per acre.

    Returns:
        float: Fuel loading in lb/ft^2.
    """
    return f_tpa * _TPA_TO_LBSFT2

accelerate(cell, time_step)

Apply fire acceleration toward steady-state ROS.

Update the transient rate of spread (cell.r_t) and average ROS (cell.avg_ros) for each spread direction using the exponential acceleration model (McAlpine 1989). Directions already at or above steady-state are clamped.

Uses a JIT-compiled inner loop to avoid numpy dispatch overhead on small (12-element) arrays.

Parameters:

Name Type Description Default
cell Cell

Burning cell with r_ss, r_t, a_a set.

required
time_step float

Simulation time step in seconds.

required
Side Effects

Updates cell.r_t, cell.avg_ros, and cell.I_t in-place.

Source code in embrs/models/rothermel.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
def accelerate(cell: Cell, time_step: float):
    """Apply fire acceleration toward steady-state ROS.

    Update the transient rate of spread (``cell.r_t``) and average ROS
    (``cell.avg_ros``) for each spread direction using the exponential
    acceleration model (McAlpine 1989). Directions already at or above
    steady-state are clamped.

    Uses a JIT-compiled inner loop to avoid numpy dispatch overhead on
    small (12-element) arrays.

    Args:
        cell (Cell): Burning cell with ``r_ss``, ``r_t``, ``a_a`` set.
        time_step (float): Simulation time step in seconds.

    Side Effects:
        Updates ``cell.r_t``, ``cell.avg_ros``, and ``cell.I_t`` in-place.
    """
    _accelerate_core(cell.r_t, cell.r_ss, cell.avg_ros, cell.I_t, cell.I_ss,
                     cell.a_a, float(time_step))

cal_g_to_BTU_lb(f_cal_g)

Convert heat content from cal/g to BTU/lb.

Parameters:

Name Type Description Default
f_cal_g float

Heat content in cal/g.

required

Returns:

Name Type Description
float float

Heat content in BTU/lb.

Source code in embrs/utilities/unit_conversions.py
260
261
262
263
264
265
266
267
268
269
def cal_g_to_BTU_lb(f_cal_g: float) -> float:
    """Convert heat content from cal/g to BTU/lb.

    Args:
        f_cal_g (float): Heat content in cal/g.

    Returns:
        float: Heat content in BTU/lb.
    """
    return f_cal_g * _CAL_G_TO_BTU_LB

calc_I_r(fuel, dead_moist_damping, live_moist_damping)

Compute reaction intensity from fuel properties and moisture damping.

Reaction intensity is the rate of heat release per unit area of the flaming front (Rothermel 1972, Eq. 27).

Parameters:

Name Type Description Default
fuel Fuel

Fuel model with net fuel loadings, heat content, and optimum reaction velocity (gamma).

required
dead_moist_damping float

Dead fuel moisture damping coefficient in [0, 1].

required
live_moist_damping float

Live fuel moisture damping coefficient in [0, 1].

required

Returns:

Name Type Description
float float

Reaction intensity (BTU/ft²/min).

Source code in embrs/models/rothermel.py
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
def calc_I_r(fuel: Fuel, dead_moist_damping: float, live_moist_damping: float) -> float:
    """Compute reaction intensity from fuel properties and moisture damping.

    Reaction intensity is the rate of heat release per unit area of the
    flaming front (Rothermel 1972, Eq. 27).

    Args:
        fuel (Fuel): Fuel model with net fuel loadings, heat content, and
            optimum reaction velocity (``gamma``).
        dead_moist_damping (float): Dead fuel moisture damping coefficient
            in [0, 1].
        live_moist_damping (float): Live fuel moisture damping coefficient
            in [0, 1].

    Returns:
        float: Reaction intensity (BTU/ft²/min).
    """
    mineral_damping = calc_mineral_damping()

    dead_calc = fuel.w_n_dead * fuel.heat_content * dead_moist_damping * mineral_damping
    live_calc = fuel.w_n_live * fuel.heat_content * live_moist_damping * mineral_damping

    I_r = fuel.gamma * (dead_calc + live_calc)

    return I_r

calc_eccentricity(fuel, R_h, R_0)

Compute fire ellipse eccentricity from effective wind speed.

Convert the effective wind speed to m/s, then compute the length-to- breadth ratio z and derive eccentricity. Capped at z = 8.0 following Anderson (1983).

Parameters:

Name Type Description Default
fuel Fuel

Fuel model for effective wind speed calculation.

required
R_h float

Head-fire rate of spread (ft/min).

required
R_0 float

No-wind, no-slope base ROS (ft/min).

required

Returns:

Name Type Description
float float

Fire ellipse eccentricity in [0, 1).

Source code in embrs/models/rothermel.py
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
def calc_eccentricity(fuel: Fuel, R_h: float, R_0: float) -> float:
    """Compute fire ellipse eccentricity from effective wind speed.

    Convert the effective wind speed to m/s, then compute the length-to-
    breadth ratio ``z`` and derive eccentricity. Capped at ``z = 8.0``
    following Anderson (1983).

    Args:
        fuel (Fuel): Fuel model for effective wind speed calculation.
        R_h (float): Head-fire rate of spread (ft/min).
        R_0 (float): No-wind, no-slope base ROS (ft/min).

    Returns:
        float: Fire ellipse eccentricity in [0, 1).
    """
    u_e = calc_effective_wind_speed(fuel, R_h, R_0)
    u_e_ms = ft_min_to_m_s(u_e)
    z = 0.936 * math.exp(0.2566 * u_e_ms) + 0.461 * math.exp(-0.1548 * u_e_ms) - 0.397
    z = min(z, 8.0)
    e = math.sqrt(z**2 - 1) / z

    return e

calc_effective_wind_factor(R_h, R_0)

Compute the effective wind factor from head-fire and base ROS.

The effective wind factor (phi_e) represents the combined influence of wind and slope as if it were a single wind-only factor.

Parameters:

Name Type Description Default
R_h float

Head-fire rate of spread (ft/min).

required
R_0 float

No-wind, no-slope base ROS (ft/min).

required

Returns:

Name Type Description
float float

Effective wind factor (dimensionless).

Source code in embrs/models/rothermel.py
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
def calc_effective_wind_factor(R_h: float, R_0: float) -> float:
    """Compute the effective wind factor from head-fire and base ROS.

    The effective wind factor (phi_e) represents the combined influence of
    wind and slope as if it were a single wind-only factor.

    Args:
        R_h (float): Head-fire rate of spread (ft/min).
        R_0 (float): No-wind, no-slope base ROS (ft/min).

    Returns:
        float: Effective wind factor (dimensionless).
    """
    phi_e = (R_h / R_0) - 1

    return phi_e

calc_effective_wind_speed(fuel, R_h, R_0)

Compute the effective wind speed from the effective wind factor.

Invert the wind factor equation to recover the equivalent wind speed that produces the same effect as the combined wind and slope.

Parameters:

Name Type Description Default
fuel Fuel

Fuel model with wind coefficients B, C, E, and packing ratio rat.

required
R_h float

Head-fire rate of spread (ft/min).

required
R_0 float

No-wind, no-slope base ROS (ft/min).

required

Returns:

Name Type Description
float float

Effective wind speed (ft/min). Returns 0 when R_h <= R_0.

Source code in embrs/models/rothermel.py
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
def calc_effective_wind_speed(fuel: Fuel, R_h: float, R_0: float) -> float:
    """Compute the effective wind speed from the effective wind factor.

    Invert the wind factor equation to recover the equivalent wind speed
    that produces the same effect as the combined wind and slope.

    Args:
        fuel (Fuel): Fuel model with wind coefficients ``B``, ``C``, ``E``,
            and packing ratio ``rat``.
        R_h (float): Head-fire rate of spread (ft/min).
        R_0 (float): No-wind, no-slope base ROS (ft/min).

    Returns:
        float: Effective wind speed (ft/min). Returns 0 when ``R_h <= R_0``.
    """


    if R_h <= R_0:
        phi_e = 0

    else: 
        phi_e = calc_effective_wind_factor(R_h, R_0)

    u_e = ((phi_e * (fuel.rat**fuel.E))/fuel.C) ** (1/fuel.B)

    return u_e

calc_flame_len(cell)

Estimate flame length from maximum fireline intensity.

For surface fires, uses Brown and Davis (1973) correlation. For crown fires, uses Thomas (1963) correlation.

Parameters:

Name Type Description Default
cell Cell

Cell with I_ss (BTU/ft/min) and _crown_status.

required

Returns:

Name Type Description
float float

Flame length in feet.

Source code in embrs/models/rothermel.py
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
def calc_flame_len(cell: Cell) -> float:
    """Estimate flame length from maximum fireline intensity.

    For surface fires, uses Brown and Davis (1973) correlation. For crown
    fires, uses Thomas (1963) correlation.

    Args:
        cell (Cell): Cell with ``I_ss`` (BTU/ft/min) and ``_crown_status``.

    Returns:
        float: Flame length in feet.
    """
    # Fireline intensity in Btu/ft/min
    fli = float(np.max(cell.I_ss))
    fli /= 60 # convert to Btu/ft/s

    if cell._crown_status == CrownStatus.NONE:
        # Surface fire
        # Brown and Davis 1973 pg. 175
        flame_len_ft = 0.45 * fli ** (0.46)

    else:
        flame_len_ft = (0.2 * (fli ** (2/3))) # in feet

    return flame_len_ft

calc_heat_sink(fuel, m_f)

Compute heat sink term for the Rothermel spread equation.

The heat sink represents the energy required to raise the fuel ahead of the fire front to ignition temperature, weighted by fuel class properties and moisture contents.

Parameters:

Name Type Description Default
fuel Fuel

Fuel model with bulk density, weighting factors, and surface-area-to-volume ratios.

required
m_f ndarray

Fuel moisture content array of shape (6,) as fractions.

required

Returns:

Name Type Description
float float

Heat sink (BTU/ft³).

Source code in embrs/models/rothermel.py
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
def calc_heat_sink(fuel: Fuel, m_f: np.ndarray) -> float:
    """Compute heat sink term for the Rothermel spread equation.

    The heat sink represents the energy required to raise the fuel ahead
    of the fire front to ignition temperature, weighted by fuel class
    properties and moisture contents.

    Args:
        fuel (Fuel): Fuel model with bulk density, weighting factors, and
            surface-area-to-volume ratios.
        m_f (np.ndarray): Fuel moisture content array of shape (6,) as
            fractions.

    Returns:
        float: Heat sink (BTU/ft³).
    """
    Q_ig = 250 + 1116 * m_f


    # Compute the heat sink term as per the equation
    heat_sink = 0

    dead_sum = 0
    for j in range(4):
        if fuel.s[j] != 0:
            dead_sum += fuel.f_dead_arr[j] * math.exp(-138/fuel.s[j]) * Q_ig[j]

    heat_sink += fuel.f_i[0] * dead_sum

    live_sum = 0
    for j in range(2):
        if fuel.s[4+j] != 0:
            live_sum += fuel.f_live_arr[j] * math.exp(-138/fuel.s[4+j]) * Q_ig[4+j]

    heat_sink += fuel.f_i[1] * live_sum
    heat_sink *= fuel.rho_b

    return heat_sink

calc_live_mx(fuel, m_f)

Compute live fuel moisture of extinction.

Determine the threshold moisture content above which live fuels will not sustain combustion, based on the ratio of dead-to-live fuel loading (fuel.W) and the dead characteristic moisture.

Parameters:

Name Type Description Default
fuel Fuel

Fuel model with loading ratio W and dead_mx.

required
m_f float

Weighted characteristic dead fuel moisture (fraction).

required

Returns:

Name Type Description
float float

Live fuel moisture of extinction (fraction). Clamped to be at least fuel.dead_mx.

Source code in embrs/models/rothermel.py
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
def calc_live_mx(fuel: Fuel, m_f: float) -> float:
    """Compute live fuel moisture of extinction.

    Determine the threshold moisture content above which live fuels will
    not sustain combustion, based on the ratio of dead-to-live fuel loading
    (``fuel.W``) and the dead characteristic moisture.

    Args:
        fuel (Fuel): Fuel model with loading ratio ``W`` and ``dead_mx``.
        m_f (float): Weighted characteristic dead fuel moisture (fraction).

    Returns:
        float: Live fuel moisture of extinction (fraction). Clamped to be
            at least ``fuel.dead_mx``.
    """
    W = fuel.W

    if W == math.inf:
        return fuel.dead_mx

    num = 0
    den = 0
    for i in range(4):
        if fuel.s[i] != 0:
            num += m_f * fuel.load[i] * math.exp(-138/fuel.s[i])
            den += fuel.load[i] * math.exp(-138/fuel.s[i])

    mf_dead = num/den

    mx = 2.9 * W * (1 - mf_dead / fuel.dead_mx) - 0.226

    return max(mx, fuel.dead_mx)

calc_mineral_damping(s_e=0.01)

Compute mineral damping coefficient.

Parameters:

Name Type Description Default
s_e float

Effective mineral content (fraction). Defaults to 0.010 (standard value for wildland fuels).

0.01

Returns:

Name Type Description
float float

Mineral damping coefficient (dimensionless).

Source code in embrs/models/rothermel.py
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
def calc_mineral_damping(s_e: float = 0.010) -> float:
    """Compute mineral damping coefficient.

    Args:
        s_e (float): Effective mineral content (fraction). Defaults to
            0.010 (standard value for wildland fuels).

    Returns:
        float: Mineral damping coefficient (dimensionless).
    """
    if s_e == 0.010:
        return _MINERAL_DAMPING_DEFAULT

    mineral_damping = 0.174 * s_e ** (-0.19)

    return mineral_damping

calc_moisture_damping(m_f, m_x)

Compute moisture damping coefficient for dead or live fuel.

Evaluates a cubic polynomial in the moisture ratio m_f / m_x (Rothermel 1972, Eq. 29). Returns 0 when moisture of extinction is zero or when the polynomial evaluates to a negative value.

Parameters:

Name Type Description Default
m_f float

Characteristic fuel moisture content (fraction).

required
m_x float

Moisture of extinction (fraction).

required

Returns:

Name Type Description
float float

Moisture damping coefficient in [0, 1].

Source code in embrs/models/rothermel.py
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
def calc_moisture_damping(m_f: float, m_x: float) -> float:
    """Compute moisture damping coefficient for dead or live fuel.

    Evaluates a cubic polynomial in the moisture ratio ``m_f / m_x``
    (Rothermel 1972, Eq. 29). Returns 0 when moisture of extinction is
    zero or when the polynomial evaluates to a negative value.

    Args:
        m_f (float): Characteristic fuel moisture content (fraction).
        m_x (float): Moisture of extinction (fraction).

    Returns:
        float: Moisture damping coefficient in [0, 1].
    """
    if m_x == 0:
        return 0

    r_m = m_f / m_x

    # Horner's form: fewer multiplications than expanded polynomial
    moist_damping = 1 + r_m * (-2.59 + r_m * (5.11 - 3.52 * r_m))

    return max(0, moist_damping)

calc_r_0(fuel, m_f)

Compute no-wind, no-slope base rate of spread and reaction intensity.

Evaluate the Rothermel (1972) equations for base ROS using fuel properties and moisture content. This is the fundamental spread rate before wind and slope adjustments.

Parameters:

Name Type Description Default
fuel Fuel

Fuel model with precomputed constants.

required
m_f ndarray

Fuel moisture content array of shape (6,) with entries [1h, 10h, 100h, dead herb, live herb, live woody] as fractions (g water / g fuel).

required

Returns:

Type Description
Tuple[float, float]

Tuple[float, float]: (R_0, I_r) where R_0 is base ROS (ft/min) and I_r is reaction intensity (BTU/ft²/min).

Source code in embrs/models/rothermel.py
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
def calc_r_0(fuel: Fuel, m_f: np.ndarray) -> Tuple[float, float]:
    """Compute no-wind, no-slope base rate of spread and reaction intensity.

    Evaluate the Rothermel (1972) equations for base ROS using fuel
    properties and moisture content. This is the fundamental spread rate
    before wind and slope adjustments.

    Args:
        fuel (Fuel): Fuel model with precomputed constants.
        m_f (np.ndarray): Fuel moisture content array of shape (6,) with
            entries [1h, 10h, 100h, dead herb, live herb, live woody] as
            fractions (g water / g fuel).

    Returns:
        Tuple[float, float]: ``(R_0, I_r)`` where ``R_0`` is base ROS
            (ft/min) and ``I_r`` is reaction intensity (BTU/ft²/min).
    """
    # Calculate moisture damping constants
    dead_mf, live_mf = get_characteristic_moistures(fuel, m_f)
    live_mx = calc_live_mx(fuel, dead_mf)
    live_moisture_damping = calc_moisture_damping(live_mf, live_mx)
    dead_moisture_damping = calc_moisture_damping(dead_mf, fuel.dead_mx)

    I_r = calc_I_r(fuel, dead_moisture_damping, live_moisture_damping)
    heat_sink = calc_heat_sink(fuel, m_f)

    R_0 = (I_r * fuel.flux_ratio)/heat_sink

    return R_0, I_r # ft/min, BTU/ft^2-min

calc_r_h(cell, R_0=None, I_r=None)

Compute head-fire rate of spread combining wind and slope effects.

Resolve the wind and slope vectors to determine the maximum spread direction (alpha) and head-fire ROS (R_h). Wind speed is capped at 0.9 × reaction intensity per Rothermel's wind limit.

Parameters:

Name Type Description Default
cell Cell

Cell with wind, slope, fuel, and moisture data.

required
R_0 float

Pre-computed no-wind, no-slope ROS (ft/min). Computed internally if None.

None
I_r float

Pre-computed reaction intensity (BTU/ft²/min). Computed internally if None.

None

Returns:

Type Description
Tuple[float, float, float, float]

Tuple[float, float, float, float]: (R_h, R_0, I_r, alpha) where R_h is head-fire ROS (ft/min), R_0 is base ROS (ft/min), I_r is reaction intensity (BTU/ft²/min), and alpha is the combined wind/slope heading (radians).

Side Effects

May update cell.aspect when slope is zero (set to wind direction).

Source code in embrs/models/rothermel.py
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
def calc_r_h(cell: Cell, R_0: float = None, I_r: float = None) -> Tuple[float, float, float, float]:
    """Compute head-fire rate of spread combining wind and slope effects.

    Resolve the wind and slope vectors to determine the maximum spread
    direction (``alpha``) and head-fire ROS (``R_h``). Wind speed is capped
    at 0.9 × reaction intensity per Rothermel's wind limit.

    Args:
        cell (Cell): Cell with wind, slope, fuel, and moisture data.
        R_0 (float, optional): Pre-computed no-wind, no-slope ROS (ft/min).
            Computed internally if None.
        I_r (float, optional): Pre-computed reaction intensity
            (BTU/ft²/min). Computed internally if None.

    Returns:
        Tuple[float, float, float, float]: ``(R_h, R_0, I_r, alpha)`` where
            ``R_h`` is head-fire ROS (ft/min), ``R_0`` is base ROS (ft/min),
            ``I_r`` is reaction intensity (BTU/ft²/min), and ``alpha`` is the
            combined wind/slope heading (radians).

    Side Effects:
        May update ``cell.aspect`` when slope is zero (set to wind direction).
    """
    wind_speed_m_s, wind_dir_deg = cell.curr_wind()

    wind_speed_ft_min = m_s_to_ft_min(wind_speed_m_s)

    wind_speed_ft_min *= cell.wind_adj_factor

    slope_angle_deg = cell.slope_deg
    slope_dir_deg = cell.aspect

    if slope_angle_deg == 0:
        rel_wind_dir_deg = 0
        cell.aspect = wind_dir_deg

    elif wind_speed_m_s == 0:
        rel_wind_dir_deg = 0
        wind_dir_deg = cell.aspect

    else:
        rel_wind_dir_deg = wind_dir_deg - slope_dir_deg
        if rel_wind_dir_deg < 0:
            rel_wind_dir_deg += 360

    rel_wind_dir = math.radians(rel_wind_dir_deg)
    slope_angle = math.radians(slope_angle_deg)

    fuel = cell.fuel
    m_f = cell.fmois

    if R_0 is None or I_r is None:
        R_0, I_r = calc_r_0(fuel, m_f)

    if R_0 <= 1e-12:
        # No spread in this cell
        return 0, 0, 0, 0

    # Enforce maximum wind speed
    U_max = 0.9 * I_r
    wind_speed_ft_min = min(U_max, wind_speed_ft_min)

    phi_w = calc_wind_factor(fuel, wind_speed_ft_min)
    phi_s = calc_slope_factor(fuel, slope_angle)

    vec_speed, alpha = calc_wind_slope_vec(R_0, phi_w, phi_s, rel_wind_dir)

    R_h = R_0 + vec_speed

    return R_h, R_0, I_r, alpha

calc_slope_factor(fuel, phi)

Compute the slope factor (phi_s) for the Rothermel spread equation.

Parameters:

Name Type Description Default
fuel Fuel

Fuel model with bulk density rho_b and particle density rho_p.

required
phi float

Slope angle (radians).

required

Returns:

Name Type Description
float float

Dimensionless slope factor (phi_s).

Source code in embrs/models/rothermel.py
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
def calc_slope_factor(fuel: Fuel, phi: float) -> float:
    """Compute the slope factor (phi_s) for the Rothermel spread equation.

    Args:
        fuel (Fuel): Fuel model with bulk density ``rho_b`` and particle
            density ``rho_p``.
        phi (float): Slope angle (radians).

    Returns:
        float: Dimensionless slope factor (phi_s).
    """
    packing_ratio = fuel.rho_b / fuel.rho_p

    phi_s = 5.275 * (packing_ratio ** (-0.3)) * math.tan(phi) ** 2

    return phi_s

calc_vals_for_all_directions(cell, R_h, I_r, alpha, e, I_h=None)

Compute ROS and fireline intensity along all spread directions.

Use the fire ellipse (eccentricity e) and the combined wind/slope heading alpha to resolve the head-fire ROS into each of the cell's spread directions.

Parameters:

Name Type Description Default
cell Cell

Cell providing directions and fuel properties.

required
R_h float

Head-fire rate of spread (ft/min for surface, m/min for crown fire).

required
I_r float

Reaction intensity (BTU/ft²/min). Ignored when I_h is provided.

required
alpha float

Combined wind/slope heading in radians, relative to the cell's aspect (upslope direction).

required
e float

Fire ellipse eccentricity in [0, 1).

required
I_h float

Head-fire fireline intensity (BTU/ft/min). When provided, directional intensities are scaled from this value instead of being computed from I_r.

None

Returns:

Type Description
Tuple[ndarray, ndarray]

Tuple[np.ndarray, np.ndarray]: (r_list, I_list) where r_list is ROS in m/s per direction and I_list is fireline intensity in BTU/ft/min per direction.

Source code in embrs/models/rothermel.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
def calc_vals_for_all_directions(cell: Cell, R_h: float, I_r: float, alpha: float,
                                 e: float, I_h: float = None) -> Tuple[np.ndarray, np.ndarray]:
    """Compute ROS and fireline intensity along all spread directions.

    Use the fire ellipse (eccentricity ``e``) and the combined wind/slope
    heading ``alpha`` to resolve the head-fire ROS into each of the cell's
    spread directions.

    Args:
        cell (Cell): Cell providing directions and fuel properties.
        R_h (float): Head-fire rate of spread (ft/min for surface, m/min
            for crown fire).
        I_r (float): Reaction intensity (BTU/ft²/min). Ignored when
            ``I_h`` is provided.
        alpha (float): Combined wind/slope heading in radians, relative to
            the cell's aspect (upslope direction).
        e (float): Fire ellipse eccentricity in [0, 1).
        I_h (float, optional): Head-fire fireline intensity (BTU/ft/min).
            When provided, directional intensities are scaled from this
            value instead of being computed from ``I_r``.

    Returns:
        Tuple[np.ndarray, np.ndarray]: ``(r_list, I_list)`` where
            ``r_list`` is ROS in m/s per direction and ``I_list`` is
            fireline intensity in BTU/ft/min per direction.
    """
    spread_directions = np.deg2rad(cell.directions)

    gamma = np.abs(((alpha + np.deg2rad(cell.aspect)) - spread_directions) % (2*np.pi))
    gamma = np.minimum(gamma, 2*np.pi - gamma)

    R_gamma = R_h * ((1 - e)/(1 - e * np.cos(gamma)))

    if I_h is None:
        t_r = 384 / cell.fuel.sav_ratio
        H_a = I_r * t_r
        I_gamma = H_a * R_gamma # BTU/ft/min

    else:
        I_gamma = I_h * (R_gamma / R_h) # BTU/ft/min

    r_list = ft_min_to_m_s(R_gamma)
    return r_list, I_gamma

calc_wind_factor(fuel, wind_speed)

Compute the wind factor (phi_w) for the Rothermel spread equation.

Parameters:

Name Type Description Default
fuel Fuel

Fuel model with precomputed wind coefficients B, C, E, and packing ratio rat.

required
wind_speed float

Midflame wind speed (ft/min).

required

Returns:

Name Type Description
float float

Dimensionless wind factor (phi_w).

Source code in embrs/models/rothermel.py
436
437
438
439
440
441
442
443
444
445
446
447
448
449
def calc_wind_factor(fuel: Fuel, wind_speed: float) -> float:
    """Compute the wind factor (phi_w) for the Rothermel spread equation.

    Args:
        fuel (Fuel): Fuel model with precomputed wind coefficients
            ``B``, ``C``, ``E``, and packing ratio ``rat``.
        wind_speed (float): Midflame wind speed (ft/min).

    Returns:
        float: Dimensionless wind factor (phi_w).
    """
    phi_w = fuel.C * (wind_speed ** fuel.B) * fuel.rat ** (-fuel.E)

    return phi_w

calc_wind_slope_vec(R_0, phi_w, phi_s, angle)

Compute the combined wind and slope vector magnitude and direction.

Resolve wind and slope spread factors into a single resultant vector using Rothermel's vector addition method.

Parameters:

Name Type Description Default
R_0 float

No-wind, no-slope ROS (ft/min).

required
phi_w float

Wind factor (dimensionless).

required
phi_s float

Slope factor (dimensionless).

required
angle float

Angle between wind and upslope directions (radians).

required

Returns:

Type Description
Tuple[float, float]

Tuple[float, float]: (vec_mag, vec_dir) where vec_mag is the combined wind/slope spread increment (ft/min) and vec_dir is the direction of the resultant vector (radians).

Source code in embrs/models/rothermel.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
def calc_wind_slope_vec(R_0: float, phi_w: float, phi_s: float, angle: float) -> Tuple[float, float]:
    """Compute the combined wind and slope vector magnitude and direction.

    Resolve wind and slope spread factors into a single resultant vector
    using Rothermel's vector addition method.

    Args:
        R_0 (float): No-wind, no-slope ROS (ft/min).
        phi_w (float): Wind factor (dimensionless).
        phi_s (float): Slope factor (dimensionless).
        angle (float): Angle between wind and upslope directions (radians).

    Returns:
        Tuple[float, float]: ``(vec_mag, vec_dir)`` where ``vec_mag`` is the
            combined wind/slope spread increment (ft/min) and ``vec_dir`` is
            the direction of the resultant vector (radians).
    """
    d_w = R_0 * phi_w
    d_s = R_0 * phi_s

    x = d_s + d_w * math.cos(angle)
    y = d_w * math.sin(angle)
    vec_mag = math.sqrt(x**2 + y**2)

    if vec_mag == 0:
        vec_dir = 0

    else:
        vec_dir = math.atan2(y, x)

    return vec_mag, vec_dir

ft_min_to_m_s(f_ft_min)

Convert speed from feet per minute to meters per second.

Parameters:

Name Type Description Default
f_ft_min float

Speed in ft/min.

required

Returns:

Name Type Description
float float

Speed in m/s.

Source code in embrs/utilities/unit_conversions.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
def ft_min_to_m_s(f_ft_min: float) -> float:
    """Convert speed from feet per minute to meters per second.

    Args:
        f_ft_min (float): Speed in ft/min.

    Returns:
        float: Speed in m/s.
    """
    return f_ft_min * _FT_MIN_TO_M_S

ft_min_to_mph(f_ft_min)

Convert speed from feet per minute to miles per hour.

Parameters:

Name Type Description Default
f_ft_min float

Speed in ft/min.

required

Returns:

Name Type Description
float float

Speed in mph.

Source code in embrs/utilities/unit_conversions.py
116
117
118
119
120
121
122
123
124
125
def ft_min_to_mph(f_ft_min: float) -> float:
    """Convert speed from feet per minute to miles per hour.

    Args:
        f_ft_min (float): Speed in ft/min.

    Returns:
        float: Speed in mph.
    """
    return f_ft_min * _FT_MIN_TO_MPH

ft_to_m(f_ft)

Convert length from feet to meters.

Parameters:

Name Type Description Default
f_ft float

Length in feet.

required

Returns:

Name Type Description
float float

Length in meters.

Source code in embrs/utilities/unit_conversions.py
76
77
78
79
80
81
82
83
84
85
def ft_to_m(f_ft: float) -> float:
    """Convert length from feet to meters.

    Args:
        f_ft (float): Length in feet.

    Returns:
        float: Length in meters.
    """
    return f_ft * _FT_TO_M

get_characteristic_moistures(fuel, m_f)

Compute weighted characteristic dead and live fuel moisture contents.

Use fuel weighting factors (f_dead_arr, f_live_arr) to collapse the per-class moisture array into single dead and live values.

Parameters:

Name Type Description Default
fuel Fuel

Fuel model providing weighting arrays.

required
m_f ndarray

Moisture content array of shape (6,) as fractions.

required

Returns:

Type Description
Tuple[float, float]

Tuple[float, float]: (dead_mf, live_mf) weighted characteristic moisture contents for dead and live fuel categories.

Source code in embrs/models/rothermel.py
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
def get_characteristic_moistures(fuel: Fuel, m_f: np.ndarray) -> Tuple[float, float]:
    """Compute weighted characteristic dead and live fuel moisture contents.

    Use fuel weighting factors (``f_dead_arr``, ``f_live_arr``) to collapse
    the per-class moisture array into single dead and live values.

    Args:
        fuel (Fuel): Fuel model providing weighting arrays.
        m_f (np.ndarray): Moisture content array of shape (6,) as fractions.

    Returns:
        Tuple[float, float]: ``(dead_mf, live_mf)`` weighted characteristic
            moisture contents for dead and live fuel categories.
    """
    dead_mf = np.dot(fuel.f_dead_arr, m_f[0:4])
    live_mf = np.dot(fuel.f_live_arr, m_f[4:])

    return dead_mf, live_mf

m_s_to_ft_min(m_s)

Convert speed from meters per second to feet per minute.

Parameters:

Name Type Description Default
m_s float

Speed in m/s.

required

Returns:

Name Type Description
float float

Speed in ft/min.

Source code in embrs/utilities/unit_conversions.py
104
105
106
107
108
109
110
111
112
113
def m_s_to_ft_min(m_s: float) -> float:
    """Convert speed from meters per second to feet per minute.

    Args:
        m_s (float): Speed in m/s.

    Returns:
        float: Speed in ft/min.
    """
    return m_s * _M_S_TO_FT_MIN

m_to_ft(f_m)

Convert length from meters to feet.

Parameters:

Name Type Description Default
f_m float

Length in meters.

required

Returns:

Name Type Description
float float

Length in feet.

Source code in embrs/utilities/unit_conversions.py
64
65
66
67
68
69
70
71
72
73
def m_to_ft(f_m: float) -> float:
    """Convert length from meters to feet.

    Args:
        f_m (float): Length in meters.

    Returns:
        float: Length in feet.
    """
    return f_m * _M_TO_FT

mph_to_ft_min(f_mph)

Convert speed from miles per hour to feet per minute.

Parameters:

Name Type Description Default
f_mph float

Speed in mph.

required

Returns:

Name Type Description
float float

Speed in ft/min.

Source code in embrs/utilities/unit_conversions.py
128
129
130
131
132
133
134
135
136
137
def mph_to_ft_min(f_mph: float) -> float:
    """Convert speed from miles per hour to feet per minute.

    Args:
        f_mph (float): Speed in mph.

    Returns:
        float: Speed in ft/min.
    """
    return f_mph * _MPH_TO_FT_MIN

njit_if_enabled(**jit_kwargs)

Decorator that applies Numba njit if available and enabled.

Equivalent to jit_if_enabled(nopython=True, **jit_kwargs). Use this for functions that must run in nopython mode.

Parameters:

Name Type Description Default
**jit_kwargs Any

Keyword arguments to pass to numba.njit.

{}

Returns:

Name Type Description
Callable Callable

Decorated function (JIT-compiled if enabled, else unchanged).

Source code in embrs/utilities/numba_utils.py
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def njit_if_enabled(**jit_kwargs: Any) -> Callable:
    """Decorator that applies Numba njit if available and enabled.

    Equivalent to jit_if_enabled(nopython=True, **jit_kwargs).
    Use this for functions that must run in nopython mode.

    Args:
        **jit_kwargs: Keyword arguments to pass to numba.njit.

    Returns:
        Callable: Decorated function (JIT-compiled if enabled, else unchanged).
    """
    def decorator(func: Callable) -> Callable:
        if NUMBA_AVAILABLE and not DISABLE_JIT:
            return njit(**jit_kwargs)(func)
        else:
            return func
    return decorator

surface_fire(cell)

Compute steady-state surface fire ROS and fireline intensity for a cell.

Calculate the head-fire rate of spread (R_h), then resolve spread rates and fireline intensities along all 12 spread directions using fire ellipse geometry. Results are stored directly on the cell object.

Parameters:

Name Type Description Default
cell Cell

Cell to evaluate. Must have fuel, moisture, wind, slope, and direction attributes populated.

required
Side Effects

Sets cell.r_ss (m/s), cell.I_ss (BTU/ft/min), cell.r_h_ss (m/s), cell.reaction_intensity (BTU/ft²/min), cell.alpha (radians), and cell.e (eccentricity) on the cell.

Source code in embrs/models/rothermel.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
def surface_fire(cell: Cell):
    """Compute steady-state surface fire ROS and fireline intensity for a cell.

    Calculate the head-fire rate of spread (R_h), then resolve spread rates and
    fireline intensities along all 12 spread directions using fire ellipse
    geometry. Results are stored directly on the cell object.

    Args:
        cell (Cell): Cell to evaluate. Must have fuel, moisture, wind, slope,
            and direction attributes populated.

    Side Effects:
        Sets ``cell.r_ss`` (m/s), ``cell.I_ss`` (BTU/ft/min),
        ``cell.r_h_ss`` (m/s), ``cell.reaction_intensity`` (BTU/ft²/min),
        ``cell.alpha`` (radians), and ``cell.e`` (eccentricity) on the cell.
    """
    R_h, R_0, I_r, alpha = calc_r_h(cell)

    cell.alpha = alpha
    spread_directions = np.deg2rad(cell.directions)

    if R_h < R_0 or R_0 == 0:
        cell.r_ss = np.zeros_like(spread_directions)
        cell.I_ss = np.zeros_like(spread_directions)
        cell.r_h_ss = 0.0
        cell.reaction_intensity = 0
        cell.e = 0
        return

    cell.reaction_intensity = I_r

    e = calc_eccentricity(cell.fuel, R_h, R_0)
    cell.e = e

    r_list, I_list = calc_vals_for_all_directions(cell, R_h, I_r, alpha, e)

    # r in m/s, I in btu/ft/min
    cell.r_ss = r_list
    cell.I_ss = I_list
    cell.r_h_ss = np.max(r_list)

Cell

Each cell tracks its terrain (elevation, slope, aspect), fuel model, fuel moisture, fire state, and spread geometry. Cells are typically accessed through FireSim lookup methods like get_cell_from_xy() or get_cell_from_indices() rather than constructed directly.

Representation of the discrete cells that make up the fire simulation.

This module defines the Cell class, which represents the fundamental units of the wildfire simulation grid. Each Cell object stores terrain properties, fire spread parameters, and state transitions necessary for modeling fire behavior.

Classes:

Name Description
- Cell

A hexagonal simulation unit with fire propagation attributes.

.. autoclass:: Cell :members:

Cell

Represents a hexagonal simulation cell in the wildfire model.

Each cell maintains its physical properties (elevation, slope, aspect), fuel characteristics, fire state, and interactions with neighboring cells. Cells are structured in a point-up hexagonal grid to model fire spread dynamics.

Attributes:

Name Type Description
id int

Unique identifier for the cell.

col int

Column index of the cell in the simulation grid.

row int

Row index of the cell in the simulation grid.

cell_size float

Edge length of the hexagonal cell (meters).

cell_area float

Area of the hexagonal cell (square meters).

x_pos float

X-coordinate of the cell in the simulation space (meters).

y_pos float

Y-coordinate of the cell in the simulation space (meters).

elevation_m float

Elevation of the cell (meters).

aspect float

Upslope direction in degrees (0° = North, 90° = East, etc.).

slope_deg float

Slope angle of the terrain at the cell (degrees).

fuel Fuel

Fire Behavior Fuel Model (FBFM) for the cell, from either Anderson 13 or Scott-Burgan 40.

state CellStates

Current fire state (FUEL, FIRE, BURNT).

neighbors dict

Dictionary of adjacent cell neighbors.

burnable_neighbors dict

Subset of neighbors that are in a burnable state.

forecast_wind_speeds list

Forecasted wind speeds in m/s.

forecast_wind_dirs list

Forecasted wind directions in degrees (cartesian).

Source code in embrs/fire_simulator/cell.py
  68
  69
  70
  71
  72
  73
  74
  75
  76
  77
  78
  79
  80
  81
  82
  83
  84
  85
  86
  87
  88
  89
  90
  91
  92
  93
  94
  95
  96
  97
  98
  99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
 127
 128
 129
 130
 131
 132
 133
 134
 135
 136
 137
 138
 139
 140
 141
 142
 143
 144
 145
 146
 147
 148
 149
 150
 151
 152
 153
 154
 155
 156
 157
 158
 159
 160
 161
 162
 163
 164
 165
 166
 167
 168
 169
 170
 171
 172
 173
 174
 175
 176
 177
 178
 179
 180
 181
 182
 183
 184
 185
 186
 187
 188
 189
 190
 191
 192
 193
 194
 195
 196
 197
 198
 199
 200
 201
 202
 203
 204
 205
 206
 207
 208
 209
 210
 211
 212
 213
 214
 215
 216
 217
 218
 219
 220
 221
 222
 223
 224
 225
 226
 227
 228
 229
 230
 231
 232
 233
 234
 235
 236
 237
 238
 239
 240
 241
 242
 243
 244
 245
 246
 247
 248
 249
 250
 251
 252
 253
 254
 255
 256
 257
 258
 259
 260
 261
 262
 263
 264
 265
 266
 267
 268
 269
 270
 271
 272
 273
 274
 275
 276
 277
 278
 279
 280
 281
 282
 283
 284
 285
 286
 287
 288
 289
 290
 291
 292
 293
 294
 295
 296
 297
 298
 299
 300
 301
 302
 303
 304
 305
 306
 307
 308
 309
 310
 311
 312
 313
 314
 315
 316
 317
 318
 319
 320
 321
 322
 323
 324
 325
 326
 327
 328
 329
 330
 331
 332
 333
 334
 335
 336
 337
 338
 339
 340
 341
 342
 343
 344
 345
 346
 347
 348
 349
 350
 351
 352
 353
 354
 355
 356
 357
 358
 359
 360
 361
 362
 363
 364
 365
 366
 367
 368
 369
 370
 371
 372
 373
 374
 375
 376
 377
 378
 379
 380
 381
 382
 383
 384
 385
 386
 387
 388
 389
 390
 391
 392
 393
 394
 395
 396
 397
 398
 399
 400
 401
 402
 403
 404
 405
 406
 407
 408
 409
 410
 411
 412
 413
 414
 415
 416
 417
 418
 419
 420
 421
 422
 423
 424
 425
 426
 427
 428
 429
 430
 431
 432
 433
 434
 435
 436
 437
 438
 439
 440
 441
 442
 443
 444
 445
 446
 447
 448
 449
 450
 451
 452
 453
 454
 455
 456
 457
 458
 459
 460
 461
 462
 463
 464
 465
 466
 467
 468
 469
 470
 471
 472
 473
 474
 475
 476
 477
 478
 479
 480
 481
 482
 483
 484
 485
 486
 487
 488
 489
 490
 491
 492
 493
 494
 495
 496
 497
 498
 499
 500
 501
 502
 503
 504
 505
 506
 507
 508
 509
 510
 511
 512
 513
 514
 515
 516
 517
 518
 519
 520
 521
 522
 523
 524
 525
 526
 527
 528
 529
 530
 531
 532
 533
 534
 535
 536
 537
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
class Cell:
    """Represents a hexagonal simulation cell in the wildfire model.

    Each cell maintains its physical properties (elevation, slope, aspect),
    fuel characteristics, fire state, and interactions with neighboring cells.
    Cells are structured in a **point-up** hexagonal grid to model fire spread dynamics.

    Attributes:
        id (int): Unique identifier for the cell.
        col (int): Column index of the cell in the simulation grid.
        row (int): Row index of the cell in the simulation grid.
        cell_size (float): Edge length of the hexagonal cell (meters).
        cell_area (float): Area of the hexagonal cell (square meters).
        x_pos (float): X-coordinate of the cell in the simulation space (meters).
        y_pos (float): Y-coordinate of the cell in the simulation space (meters).
        elevation_m (float): Elevation of the cell (meters).
        aspect (float): Upslope direction in degrees (0° = North, 90° = East, etc.).
        slope_deg (float): Slope angle of the terrain at the cell (degrees).
        fuel (Fuel): Fire Behavior Fuel Model (FBFM) for the cell, from either Anderson 13 or Scott-Burgan 40.
        state (CellStates): Current fire state (FUEL, FIRE, BURNT).
        neighbors (dict): Dictionary of adjacent cell neighbors.
        burnable_neighbors (dict): Subset of `neighbors` that are in a burnable state.
        forecast_wind_speeds (list): Forecasted wind speeds in m/s.
        forecast_wind_dirs (list): Forecasted wind directions in degrees (cartesian).
    """

    def __init__(self, id: int, col: int, row: int, cell_size: float):
        """Initialize a hexagonal cell with position and geometry.

        Creates a cell at the specified grid position and calculates its spatial
        coordinates based on the hexagonal grid layout. The cell is initialized
        with default values for fire state and fuel properties.

        Args:
            id (int): Unique identifier for this cell.
            col (int): Column index in the simulation grid.
            row (int): Row index in the simulation grid.
            cell_size (float): Edge length of the hexagon in meters.

        Notes:
            - Spatial position is calculated using point-up hexagon geometry.
            - For even rows: x = col * cell_size * sqrt(3)
            - For odd rows: x = (col + 0.5) * cell_size * sqrt(3)
            - y = row * cell_size * 1.5
        """
        self.id = id

        # Deterministic hash based on cell ID (not memory address) so that
        # set iteration order is reproducible across process invocations.
        self._hash = hash(id)

        # Set cell indices
        self._col = col
        self._row = row

        # x_pos, y_pos are the global position of cell in m
        _sqrt3 = 1.7320508075688772  # math.sqrt(3)
        if row % 2 == 0:
            self._x_pos = col * cell_size * _sqrt3
        else:
            self._x_pos = (col + 0.5) * cell_size * _sqrt3

        self._y_pos = row * cell_size * 1.5

        self._cell_size = cell_size # defined as the edge length of hexagon
        self._cell_area = self.calc_cell_area()

        # Flag to track if cell has been treated with long-term fire retardant
        self._retardant = False
        self._retardant_factor = 1.0 # Factor multiplied by rate of spread (0-1)
        self.retardant_expiration_s = -1.0 # Time at which long-term retardant in cell expires

        # Track the total amount of water dropped (if modelled as rain)
        self.local_rain = 0.0

        # Van Wagner energy-balance water suppression (Van Wagner & Taylor 2022)
        self.water_applied_kJ = 0.0   # Cumulative cooling energy from water drops
        self._vw_efficiency = 2.5     # Application efficiency multiplier (Table 4)

        # Width in meters of any fuel discontinuity within cell (road or firebreak)
        self._break_width = 0 

        # Variable to track if fuel discontinuity within cell can be breached
        self.breached = True

        # Track if firebrands have been lofted from cell
        self.lofted = False

        # Source cell coordinates if ignited by ember spotting (set by Embers.flight)
        self.spot_source_xy = None

        # Weak reference to parent BaseFire object
        self._parent = None

        self._arrival_time = -999

    def __hash__(self):
        return self._hash

    def set_parent(self, parent: BaseFireSim) -> None:
        """Sets the parent BaseFire object for this cell.

        Args:
            parent (BaseFireSim): The BaseFire object that owns this cell.
        """
        self._parent = weakref.ref(parent)

    def reset_to_fuel(self):
        """Reset cell to initial FUEL state, preserving terrain/fuel/geometry data.

        Resets all mutable fire-state attributes to their initial values as set
        by ``__init__`` and ``_set_cell_data``. Immutable properties such as
        position, fuel model, elevation, slope, aspect, canopy attributes,
        polygon, wind adjustment factor, and neighbor topology are preserved.

        This is used by FirePredictor to efficiently restore cells to a clean
        state between predictions, avoiding expensive deepcopy operations.

        Side Effects:
            - Resets fire state to CellStates.FUEL
            - Clears all spread tracking arrays
            - Resets suppression effects (retardant, rain, firebreaks)
            - Resets fuel moisture to initial values
            - Restores full neighbor set to _burnable_neighbors
        """
        # Fire state
        self._state = CellStates.FUEL
        self.fully_burning = False
        self._crown_status = CrownStatus.NONE
        self.cfb = 0
        self.reaction_intensity = 0

        # Partial suppression state
        self.disabled_locs = set()
        self._suppression_count = 0

        # Spread arrays — shared read-only constants (replaced when cell ignites)
        self.distances = None
        self.directions = None
        self.end_pts = None
        self._ign_n_loc = None
        self._self_end_points = None
        self.r_h_ss = None
        self.I_h_ss = None
        self.r_t = _RESET_ARRAY_ZERO
        self.fire_spread = _RESET_ARRAY_EMPTY
        self.avg_ros = _RESET_ARRAY_EMPTY
        self.r_ss = _RESET_ARRAY_EMPTY
        self.I_ss = _RESET_ARRAY_ZERO
        self.I_t = _RESET_ARRAY_ZERO
        self.intersections = _RESET_IXN_EMPTY
        self.e = 0
        self.alpha = None
        self.has_steady_state = False

        # Suppression effects — back to __init__ defaults
        self._retardant = False
        self._retardant_factor = 1.0
        self.retardant_expiration_s = -1.0
        self.local_rain = 0.0
        self.water_applied_kJ = 0.0
        self._vw_efficiency = 2.5
        self._break_width = 0
        self.breached = True
        self.lofted = False
        self.spot_source_xy = None
        self._arrival_time = -999

        # Wind forecast
        self.forecast_wind_speeds = []
        self.forecast_wind_dirs = []

        # Moisture — reset to initial values (use cached template if available)
        self.moist_update_time_s = 0
        if self._fuel.burnable:
            if self.dfms:
                for dfm in self.dfms:
                    dfm.initializeStick()
            # Reset moisture array from cached initial values
            self.fmois = self._init_fmois.copy()

        # Restore full neighbor set (remove_neighbors modifies _burnable_neighbors during sim)
        self._burnable_neighbors = dict(self._neighbors)

    def compute_disabled_locs(self):
        """Compute boundary locations consumed by current fire and add to disabled_locs.

        Must be called BEFORE fire-state arrays are cleared. Uses three rules
        to determine which boundary locations (1-12) have been consumed.

        Rule 1: Entry point is consumed.
        Rule 2: Each crossed intersection's exit boundary location is consumed.
        Rule 3: For corner ignitions, adjacent midpoints are consumed if fire
                 has spread past half the distance in that direction.
        """
        n_loc = self._ign_n_loc
        if n_loc is None:
            return

        # Rule 1 — entry point
        if n_loc != 0:
            self.disabled_locs.add(n_loc)

        # Rule 2 — crossed intersections → exit boundary locations
        if self._self_end_points is not None:
            for i in range(len(self.intersections)):
                if self.intersections[i]:
                    self.disabled_locs.add(self._self_end_points[i])

        # Rule 3 — corner adjacent midpoints (even, nonzero n_loc only)
        if n_loc != 0 and n_loc % 2 == 0:
            if len(self.fire_spread) > 0 and self.distances is not None and len(self.distances) > 0:
                if self.fire_spread[0] > self.distances[0] / 2:
                    self.disabled_locs.add((n_loc % 12) + 1)
                if self.fire_spread[-1] > self.distances[-1] / 2:
                    self.disabled_locs.add(((n_loc - 2) % 12) + 1)

    def suppress_to_fuel(self):
        """Suppress cell back to FUEL state, preserving moisture and disabled_locs.

        Similar to reset_to_fuel() but preserves:
        - disabled_locs (accumulated consumed boundary locations)
        - _suppression_count (incremented)
        - Fuel moisture state (fmois, dfms, moist_update_time_s)

        Clears fire-state arrays, VW water state, and crown fire state.
        Restores burnable_neighbors from full neighbor set.
        """
        # Fire state
        self._state = CellStates.FUEL
        self.fully_burning = False
        self._crown_status = CrownStatus.NONE
        self.cfb = 0
        self.reaction_intensity = 0

        # Spread arrays
        self.distances = None
        self.directions = None
        self.end_pts = None
        self._ign_n_loc = None
        self._self_end_points = None
        self.r_h_ss = None
        self.I_h_ss = None
        self.r_t = _RESET_ARRAY_ZERO
        self.fire_spread = _RESET_ARRAY_EMPTY
        self.avg_ros = _RESET_ARRAY_EMPTY
        self.r_ss = _RESET_ARRAY_EMPTY
        self.I_ss = _RESET_ARRAY_ZERO
        self.I_t = _RESET_ARRAY_ZERO
        self.intersections = _RESET_IXN_EMPTY
        self.e = 0
        self.alpha = None
        self.has_steady_state = False

        # Clear VW water suppression state
        self.water_applied_kJ = 0.0
        self._vw_efficiency = 2.5

        # Restore full neighbor set
        self._burnable_neighbors = dict(self._neighbors)

        # Increment suppression count
        self._suppression_count += 1

    @property
    def n_disabled_locs(self) -> int:
        """Number of boundary locations disabled by prior suppression."""
        return len(self.disabled_locs)

    def _set_cell_data(self, cell_data: CellData):
        """Configure cell properties from terrain and fuel data.

        Initializes the cell's physical properties, fuel characteristics, and
        fire spread parameters from a CellData object. This method is called
        during simulation setup after the cell geometry is established.

        Args:
            cell_data (CellData): Container with fuel type, elevation, slope,
                aspect, canopy properties, and initial moisture fractions.

        Side Effects:
            - Sets terrain attributes (elevation, slope, aspect).
            - Sets canopy attributes and calculates wind adjustment factor.
            - Initializes fuel moisture arrays for dead and live fuels.
            - Creates the Shapely polygon representation.
            - Sets cell state to CellStates.FUEL.
        """

        # Set Fuel type
        self._fuel = cell_data.fuel_type

        # z is the elevation of cell in m
        self._elevation_m = cell_data.elevation

        # Upslope direction in degrees - 0 degrees = North
        self.aspect = cell_data.aspect

        # Slope angle in degrees
        self.slope_deg = cell_data.slope_deg

        # Canopy cover as a percentage
        self.canopy_cover = cell_data.canopy_cover

        # Canopy height in meters
        self.canopy_height = cell_data.canopy_height

        # Canopy base height in meters
        self.canopy_base_height = cell_data.canopy_base_height

        # Canopy bulk density in kg/m^3
        self.canopy_bulk_density = cell_data.canopy_bulk_density

        # Check if cell has a canopy
        self.has_canopy = self.canopy_height > 0 and self.canopy_cover > 0

        # Wind adjustment factor based on sheltering condition
        self._set_wind_adj_factor()

        # Set to true when fire has spread across entire cell
        self.fully_burning = False

        self.init_dead_mf = cell_data.init_dead_mf
        self.init_live_h_mf = cell_data.live_h_mf
        self.init_live_w_mf = cell_data.live_w_mf

        self.reaction_intensity = 0

        if self.fuel.burnable:
            self.set_arrays()

        # Default state is fuel
        self._state = CellStates.FUEL

        # Crown fire attribute
        self._crown_status = CrownStatus.NONE

        # Crown fraction burned
        self.cfb = 0

        # Dictionaries to store neighbors
        self._neighbors = {}
        self._burnable_neighbors = {}

        # Last time the moisture was updated in cell
        self.moist_update_time_s = 0

        # Polygon is set externally via batch creation for performance
        self.polygon = None

        # Variables that define spread directions within cell
        self.distances = None
        self.directions = None
        self.end_pts = None
        self._ign_n_loc = None  # Set when cell ignites via get_ign_params()

        # Heading rate of spread and fireline intensity
        self.r_h_ss = None
        self.I_h_ss = None

        # Variables that keep track of elliptical spread within cell
        self.r_t = np.array([0])
        self.fire_spread = np.array([])
        self.avg_ros = np.array([]) # Average rate of spread for current time step
        self.r_ss = np.array([])
        self.I_ss = np.array([0])
        self.I_t = np.array([0])
        self.intersections = np.zeros(0, dtype=np.bool_)
        self.e = 0
        self.alpha = None

        # Partial suppression state
        self.disabled_locs = set()  # boundary locations (1-12) consumed by prior burns
        self._suppression_count = 0
        self._self_end_points = None  # set by get_ign_params, used for disabled-exit checks

        # Boolean defining the cell already has a steady-state ROS
        self.has_steady_state = False

        # Wind forecast and current wind within cell
        self.forecast_wind_speeds = []
        self.forecast_wind_dirs = []

        # Constant defining fire acceleration characteristics
        self.a_a = 0.3 / 60

    def set_arrays(self):
        """Initialize fuel moisture tracking arrays for this cell.

        DFM objects are created lazily on first moisture update to avoid
        allocating ~12 numpy arrays per object for cells that never burn.

        Side Effects:
            - Sets self.wdry and self.sigma from fuel model properties.
            - Initializes self.fmois array with initial moisture fractions.
            - Sets self._dfms_needed tuple for lazy DFM creation.
        """
        indices = self._fuel.rel_indices

        self.wdry = self._fuel.w_n[self._fuel.rel_indices]
        self.sigma = self._fuel.s[self._fuel.rel_indices]

        # Defer DFM creation — store which classes are needed
        self.dfms = []
        self._dfms_needed = (0 in indices, 1 in indices, 2 in indices)

        fmois = np.zeros(6)

        if 0 in indices:
            fmois[0] = self.init_dead_mf[0]
        if 1 in indices:
            fmois[1] = self.init_dead_mf[1]
        if 2 in indices:
            fmois[2] = self.init_dead_mf[2]
        if 3 in indices:
            fmois[3] = self.init_dead_mf[0]
        if 4 in indices:
            fmois[4] = self.init_live_h_mf
        if 5 in indices:
            fmois[5] = self.init_live_w_mf

        self.fmois = np.array(fmois)
        # Cache initial fmois for efficient reset_to_fuel
        self._init_fmois = self.fmois.copy()

    def _ensure_dfms(self):
        """Create DeadFuelMoisture objects on demand (lazy initialization).

        Called before first moisture update. Uses template cloning for fast
        object creation.
        """
        if self.dfms:
            return  # Already initialized

        need_1hr, need_10hr, need_100hr = self._dfms_needed
        if need_1hr:
            self.dfm1 = DeadFuelMoisture.createDeadFuelMoisture1()
            self.dfms.append(self.dfm1)
        if need_10hr:
            self.dfm10 = DeadFuelMoisture.createDeadFuelMoisture10()
            self.dfms.append(self.dfm10)
        if need_100hr:
            self.dfm100 = DeadFuelMoisture.createDeadFuelMoisture100()
            self.dfms.append(self.dfm100)

    def project_distances_to_surf(self, distances: np.ndarray):
        """Project horizontal distances onto the sloped terrain surface.

        Adjusts the flat-ground distances to each cell edge by accounting for
        the slope and aspect of the terrain. This ensures fire spread distances
        are measured along the actual terrain surface.

        Args:
            distances (np.ndarray): Horizontal distances to cell edges in meters.

        Side Effects:
            - Sets self.distances to the slope-adjusted distances in meters.
        """
        slope_rad = np.deg2rad(self.slope_deg)
        aspect = (self.aspect + 180) % 360
        deltas = np.deg2rad(aspect - np.array(self.directions))
        proj = np.sqrt(np.cos(deltas) ** 2 * np.cos(slope_rad) ** 2 + np.sin(deltas) ** 2)
        self.distances = distances / proj

    def get_ign_params(self, n_loc: int):
        """Calculate fire spread directions and distances from an ignition location.

        Computes the radial spread directions from the specified ignition point
        within the cell to each edge or vertex. Initializes arrays for tracking
        rate of spread and fireline intensity in each direction.

        Args:
            n_loc (int): Ignition location index within the cell.
                0=center, 1-6=vertices, 7-12=edge midpoints.

        Side Effects:
            - Sets self.directions: array of compass directions in degrees.
            - Sets self.distances: slope-adjusted distances to cell boundaries.
            - Sets self.end_pts: coordinates of cell boundary points.
            - Initializes self.avg_ros, self.I_t, self.r_t to zero arrays.
        """
        self._ign_n_loc = n_loc
        self.directions, distances, self.end_pts = UtilFuncs.get_ign_parameters(n_loc, self.cell_size)
        self._self_end_points = _derive_self_end_points(n_loc)
        self.project_distances_to_surf(distances)
        self.avg_ros = np.zeros_like(self.directions)
        self.I_t = np.zeros_like(self.directions)
        self.r_t = np.zeros_like(self.directions)

    def _set_wind_forecast(self, wind_speed: np.ndarray, wind_dir: np.ndarray):
        """Store the local forecasted wind speed and direction for the cell.

        Args:
            wind_speed (np.ndarray): Array of forecasted wind speeds in m/s.
            wind_dir (np.ndarray): Array of wind directions in degrees, using
                cartesian convention (0° = blowing toward North/+y,
                90° = blowing toward East/+x).

        Side Effects:
            - Sets self.forecast_wind_speeds with the speed array.
            - Sets self.forecast_wind_dirs with the direction array.
        """ 
        self.forecast_wind_speeds = wind_speed
        self.forecast_wind_dirs = wind_dir

    def _step_moisture(self, weather_stream: WeatherStream, idx: int, update_interval_hr: float = 1):
        """Advance dead fuel moisture calculations by one time interval.

        Updates the moisture content of each dead fuel size class using the
        Nelson dead fuel moisture model. Applies site-specific corrections
        for elevation and calculates local solar radiation.

        Args:
            weather_stream (WeatherStream): Weather data source with temperature,
                humidity, and solar radiation.
            idx (int): Index into the weather stream for current conditions.
            update_interval_hr (float): Time step for moisture update in hours.

        Side Effects:
            - Updates internal state of each DeadFuelMoisture object in self.dfms.
        """
        # Lazy DFM creation — only allocate when first needed
        if not self.dfms:
            self._ensure_dfms()

        elev_ref = weather_stream.ref_elev

        curr_weather = weather_stream.stream[idx]

        bp0 = 0.0218

        t_f_celsius, h_f_frac = apply_site_specific_correction(self, elev_ref, curr_weather)
        solar_radiation = calc_local_solar_radiation(self, curr_weather)

        for i, dfm in enumerate(self.dfms):
            if not dfm.initialized():
                dfm.initializeEnvironment(
                    t_f_celsius, # Intial ambient air temeperature
                    h_f_frac, # Initial ambient air rel. humidity (g/g)
                    solar_radiation, # Initial solar radiation (W/m^2)
                    0, # Initial cumulative rainfall (cm)
                    t_f_celsius, # Initial stick temperature (degrees C)
                    h_f_frac, # Intial stick surface relative humidity (g/g)
                    self.init_dead_mf[0], # Initial stick fuel moisture fraction (g/g)
                    bp0) # Initial stick barometric pressure (cal/cm^3)

            dfm.update_internal(
                update_interval_hr, # Elapsed time since the previous observation (hours)
                t_f_celsius, # Current observation's ambient air temperature (degrees C)
                h_f_frac, # Current observation's ambient air relative humidity (g/g)
                solar_radiation, # Current observation's solar radiation (W/m^2)
                curr_weather.rain + self.local_rain, # Current observation's total cumulative rainfall (cm)
                bp0) # Current observation's stick barometric pressure (cal/cm^3)

    def _update_moisture(self, idx: float, weather_stream: WeatherStream):
        """Update fuel moisture content to the current weather interval.

        Advances the moisture model to the midpoint of the specified weather
        interval and updates the moisture content array. For dynamic fuel models,
        also updates dead herbaceous moisture.

        Args:
            idx (float): Weather interval index (0-based).
            weather_stream (WeatherStream): Weather data source.

        Side Effects:
            - Updates self.fmois array with current moisture fractions.
            - May update dead herbaceous moisture for dynamic fuel models.
        """
        # Make target time the midpoint of the weather interval
        target_time_s = (idx + 0.5) * self._parent().weather_t_step  # Convert index to seconds

        self._catch_up_moisture_to_curr(target_time_s, weather_stream)

        # Update moisture content for each dead fuel moisture class
        self.fmois[0:len(self.dfms)] = [dfm.meanWtdMoisture() for dfm in self.dfms]

        if self.fuel.dynamic:
            # Set dead herbaceous moisture to 1-hr value
            self.fmois[3] = self.fmois[0]

        # Sync live fuel moisture from sim-level GSI-derived values
        # Only when dynamic GSI is active (use_gsi=True); per-fuel-type
        # overrides from fms_has_live are left untouched.
        # Uses getattr for safety when parent is a FirePredictor.
        parent = self._parent()
        if getattr(parent, '_gsi_tracker', None) is not None:
            rel = self._fuel.rel_indices
            if 4 in rel:
                self.fmois[4] = parent._live_h_mf
            if 5 in rel:
                self.fmois[5] = parent._live_w_mf

    def _catch_up_moisture_to_curr(self, target_time_s: float, weather_stream: WeatherStream):
        """Advance moisture calculations from last update time to target time.

        Steps through weather intervals between the last moisture update and
        the target time, calling _step_moisture for each interval. Handles
        partial intervals at the start and end.

        Args:
            target_time_s (float): Target simulation time in seconds.
            weather_stream (WeatherStream): Weather data source.

        Side Effects:
            - Updates self.moist_update_time_s to target_time_s.
            - Updates internal state of DeadFuelMoisture objects.
        """
        if self.moist_update_time_s >= target_time_s:
            return

        curr = self.moist_update_time_s
        weather_t_step = self._parent().weather_t_step # seconds

        # Align to next weather boundary
        interval_end_s = ((curr // weather_t_step) + 1) * weather_t_step
        if interval_end_s > target_time_s:
            interval_end_s = target_time_s

        if curr < interval_end_s:
            idx = int(curr // weather_t_step)
            dt_hr = (interval_end_s - curr) / 3600  # Convert seconds to hours
            self._step_moisture(weather_stream, idx, update_interval_hr=dt_hr)
            curr = interval_end_s

        # Take full interval steps until we reach the target time
        while curr + weather_t_step <= target_time_s:
            idx = int(curr // weather_t_step)
            self._step_moisture(weather_stream, idx, update_interval_hr=weather_t_step / 3600)
            curr += weather_t_step

        if curr < target_time_s:
            idx = int(curr // weather_t_step)
            dt_hr = (target_time_s - curr) / 3600  # Convert seconds to hours
            self._step_moisture(weather_stream, idx, update_interval_hr=dt_hr)
            curr = target_time_s

        self.moist_update_time_s = curr

    def curr_wind(self) -> tuple:
        """Get the current wind speed and direction at this cell.

        Returns the wind conditions for the current weather interval from the
        cell's local wind forecast. For prediction runs, may trigger forecast
        updates if needed.

        Returns:
            tuple: (wind_speed, wind_direction) where speed is in m/s and
                direction is in degrees using cartesian convention
                (0° = blowing toward North/+y, 90° = blowing toward East/+x).
        """
        parent = self._parent()
        w_idx = parent._curr_weather_idx - parent.sim_start_w_idx

        if parent.is_prediction() and len(self.forecast_wind_speeds) <= w_idx:
            parent._set_prediction_forecast(self)

        # Defensive clamp: prevent index-out-of-bounds on forecast arrays
        if w_idx >= len(self.forecast_wind_speeds):
            import warnings
            warnings.warn(
                f"Cell.curr_wind: w_idx ({w_idx}) >= forecast length "
                f"({len(self.forecast_wind_speeds)}) for cell {self.id}. "
                f"Clamping to last available index."
            )
            w_idx = len(self.forecast_wind_speeds) - 1

        curr_wind = (self.forecast_wind_speeds[w_idx], self.forecast_wind_dirs[w_idx])

        return curr_wind

    def _set_elev(self, elevation: float):
        """Sets the elevation of the cell.

        Args:
            elevation (float): Elevation of the terrain at the cell location, in meters.

        Side Effects:
            - Updates the `elevation_m` attribute with the new elevation value.
        """
        self.elevation_m = elevation

    def _set_slope(self, slope: float):
        """Sets the slope of the cell.

        The slope represents the steepness of the terrain at the cell's location.

        Args:
            slope (float): The slope of the terrain in degrees, where 0° represents 
                        flat terrain and higher values indicate steeper inclines.

        Side Effects:
            - Updates the `slope_deg` attribute with the new slope value.
        """
        self.slope_deg = slope

    def _set_aspect(self, aspect: float):
        """Sets the aspect (upslope direction) of the cell.

        Aspect is the compass direction that the upslope direction faces, measured in degrees:
        - 0° = North
        - 90° = East
        - 180° = South
        - 270° = West

        Args:
            aspect (float): The aspect of the terrain in degrees.

        Side Effects:
            - Updates the `aspect` attribute with the new aspect value.
        """
        self.aspect = aspect


    def _set_canopy_cover(self, canopy_cover: float):
        """Sets the canopy cover (as a percentage) of the cell

        Args:
            canopy_cover (float): canopy cover as a percentage
        """
        self.canopy_cover = canopy_cover

    def _set_canopy_height(self, canopy_height: float):
        """Sets the average top of canopy height within the cell in meters

        Args:
            canopy_height (float): average top of canopy height within the cell in meters
        """
        self.canopy_height = canopy_height

    def _set_canopy_base_height(self, canopy_base_height: float):
        """Set the canopy base height for the cell.

        Args:
            canopy_base_height (float): Height from ground to bottom of canopy
                in meters.

        Side Effects:
            - Updates the canopy_base_height attribute.
        """
        self.canopy_base_height = canopy_base_height


    def _set_canopy_bulk_density(self, canopy_bulk_density: float):
        """Set the canopy bulk density for the cell.

        Args:
            canopy_bulk_density (float): Mass of canopy fuel per unit volume
                in kg/m^3.

        Side Effects:
            - Updates the canopy_bulk_density attribute.
        """
        self.canopy_bulk_density = canopy_bulk_density

    def _set_wind_adj_factor(self):
        """Calculate and set the wind adjustment factor for this cell.

        Computes the wind adjustment factor (WAF) based on fuel type and canopy
        characteristics using equations from Albini and Baughman (1979).

        For non-burnable fuels, WAF is set to 1. For burnable fuels:
        - Unsheltered (canopy cover <= 5%): uses fuel depth
        - Sheltered (canopy cover > 5%): uses canopy height and crown fill

        Side Effects:
            - Sets self.wind_adj_factor.
        """

        if not self.fuel.burnable:
            self.wind_adj_factor = 1

        else:
            # Calcuate crown fill portion
            f = (self.canopy_cover / 100) * (np.pi / 12)

            if f <= 0.05:
                # Use un-sheltered WAF equation
                H = self.fuel.fuel_depth_ft
                self.wind_adj_factor = 1.83 / np.log((20 + 0.36*H)/(0.13*H))

            else:
                # Use sheltered WAF equation
                H = self.canopy_height
                self.wind_adj_factor = 0.555/(np.sqrt(f*3.28*H) * np.log((20 + 1.18*H)/(0.43*H)))

    def _set_fuel_type(self, fuel_type: Fuel):
        """Sets the fuel type of the cell based on the selected Fuel Model.

        The fuel type determines the fire spread characteristics of the cell.

        Args:
            fuel_type (Fuel): The fuel classification, based on the Anderson
                                    or Scott Burgan standard fire behavior fuel models.

        Side Effects:
            - Updates the `_fuel` attribute with the specified fuel model.
        """
        self._fuel = fuel_type

        if self._fuel.burnable:
            self.set_arrays()

    def _set_state(self, state: CellStates):
        """Sets the state of the cell.

        The state represents the fire condition of the cell and must be one of:
        - `CellStates.FUEL`: The cell contains unburned fuel.
        - `CellStates.FIRE`: The cell is actively burning.
        - `CellStates.BURNT`: The cell has already burned.

        Args:
            state (CellStates): The new state of the cell.

        Side Effects:
            - Updates the `_state` attribute with the given state.
            - If the state is `CellStates.FIRE`, initializes fire spread-related attributes:
                - `fire_spread`: Tracks fire spread extent in different directions.
                - `r_ss`: Stores steady-state rates of spread.
                - `I_ss`: Stores steady-state fireline intensity values.
        """
        self._state = state

        if self._state == CellStates.FIRE:
            n_dirs = len(self.directions)
            self.fire_spread = np.zeros(n_dirs)
            self.r_ss = np.zeros(n_dirs)
            self.I_ss = np.zeros(n_dirs)
            self.intersections = np.zeros(n_dirs, dtype=np.bool_)

            self.r_h_ss = 0
            self.I_h_ss = 0

    def add_retardant(self, duration_hr: float, effectiveness: float):
        """Apply long-term fire retardant to this cell.

        Marks the cell as treated with retardant, which reduces the rate of
        spread by the effectiveness factor until the retardant expires.

        Args:
            duration_hr (float): Duration of retardant effectiveness in hours.
            effectiveness (float): Reduction factor for rate of spread (0.0-1.0).
                A value of 0.5 reduces ROS by 50%.

        Raises:
            ValueError: If effectiveness is not in range [0, 1].

        Side Effects:
            - Sets self._retardant to True.
            - Sets self._retardant_factor to (1 - effectiveness).
            - Sets self.retardant_expiration_s to expiration time.
        """
        if effectiveness < 0 or effectiveness > 1:
            raise ValueError(f"Retardant effectiveness must be between 0 and 1 ({effectiveness} passed in)")

        self._retardant = True
        self._retardant_factor = 1 - effectiveness

        self.retardant_expiration_s = self._parent().curr_time_s + (duration_hr * 3600)

    def water_drop_as_rain(self, water_depth_cm: float, duration_s: float = 30):
        """Apply a water drop modeled as equivalent rainfall.

        Simulates water delivery by treating the water as cumulative
        rainfall input to the fuel moisture model. Updates moisture state
        immediately.

        Args:
            water_depth_cm (float): Equivalent water depth in centimeters.
            duration_s (float): Duration of the water application in seconds.

        Side Effects:
            - Updates self.local_rain with accumulated water depth.
            - Advances moisture model through the application period.
            - Updates self.fmois with new moisture fractions.

        Notes:
            - No effect on non-burnable fuel types.
        """
        if not self.fuel.burnable:
            return

        parent = self._parent()
        now_idx = parent._curr_weather_idx
        now_s = parent.curr_time_s
        weather_stream = parent._weather_stream

        # 1. Step moisture from last update to the current time
        self._catch_up_moisture_to_curr(now_s, weather_stream)

        # 2. Increment local rain
        self.local_rain += water_depth_cm

        # 3. Step moisture from current time to end of local rain interval
        interval_hr = duration_s / 3600 # Convert seconds to hours
        self._step_moisture(weather_stream, now_idx, update_interval_hr=interval_hr)

        # 4. Store the time so that next update just updates from current time over a weather interval
        self.moist_update_time_s = now_s + duration_s

         # Update moisture content for each dead fuel moisture class
        self.fmois[0:len(self.dfms)] = [dfm.meanWtdMoisture() for dfm in self.dfms]

        if self.fuel.dynamic:
            # Set dead herbaceous moisture to 1-hr value
            self.fmois[3] = self.fmois[0]

        self.has_steady_state = False

    def water_drop_as_moisture_bump(self, moisture_bump: float):
        """Apply a water drop as a direct fuel moisture increase.

        Simulates water delivery by directly increasing the outer node moisture
        of each dead fuel class, then advances the moisture model briefly to
        allow diffusion.

        Args:
            moisture_bump (float): Moisture fraction to add to fuel surface.

        Side Effects:
            - Increases outer node moisture on each DeadFuelMoisture object.
            - Advances moisture model by 30 seconds.
            - Updates self.fmois with new moisture fractions.

        Notes:
            - No effect on non-burnable fuel types.
        """
        if not self.fuel.burnable:
            return

        if not self.dfms:
            self._ensure_dfms()

        # Catch cell up to current time
        parent = self._parent()
        now_idx = parent._curr_weather_idx
        now_s = parent.curr_time_s
        weather_stream = parent._weather_stream
        self._catch_up_moisture_to_curr(now_s, weather_stream)

        for dfm in self.dfms:
            # Add moisture bump to outer node of each dead fuel moisture class
            dfm.m_w[0] += moisture_bump
            dfm.m_w[0] = max(dfm.m_w[0], dfm.m_wmx)

        self._step_moisture(weather_stream, now_idx, update_interval_hr=30/3600)

        # Update moisture content for each dead fuel moisture class
        self.fmois[0:len(self.dfms)] = [dfm.meanWtdMoisture() for dfm in self.dfms]

        if self.fuel.dynamic:
            # Set dead herbaceous moisture to 1-hr value
            self.fmois[3] = self.fmois[0]

        self.has_steady_state = False

    def water_drop_vw(self, volume_L: float, efficiency: float = 2.5,
                       T_a: float = 20.0):
        """Apply water using Van Wagner (2022) energy-balance model.

        Converts water volume to cooling energy (Eq. 1b) and accumulates in
        water_applied_kJ. Moisture injection is applied during iterate() by
        apply_vw_suppression().

        Args:
            volume_L: Water volume in liters (1 L = 1 kg).
            efficiency: Application efficiency multiplier (Table 4, Van Wagner
                2022). Typical range 2.0–4.0. Default 2.5.
            T_a: Ambient air temperature in °C. Default 20.

        Raises:
            ValueError: If volume_L < 0.
        """
        if volume_L < 0:
            raise ValueError(f"Water volume must be >= 0, got {volume_L}")

        if not self.fuel.burnable:
            return

        from embrs.models.van_wagner_water import volume_L_to_energy_kJ

        self._vw_efficiency = efficiency
        energy_kJ = volume_L_to_energy_kJ(volume_L, T_a)
        self.water_applied_kJ += energy_kJ
        self.has_steady_state = False

    def apply_vw_suppression(self):
        """Apply moisture injection from accumulated Van Wagner water energy.

        Called by iterate() for cells with water_applied_kJ > 0. Computes
        suppression ratio from current fire state, then injects moisture
        toward dead_mx proportionally.

        Uses:
            - I_ss (BTU/ft/min) converted to kW/m
            - fuel.w_n_dead + w_n_live (lb/ft²) converted to kg/m²
            - fire_area_m2 property
            - self._vw_efficiency (stored from water_drop_vw call)
            - heat_to_extinguish_kJ() (Eq. 7b + 10b + Table 4)
            - compute_suppression_ratio()
            - compute_moisture_injection()

        Side Effects:
            - Modifies self.fmois
            - Sets self.has_steady_state = False
        """
        from embrs.models.van_wagner_water import (
            heat_to_extinguish_kJ, compute_suppression_ratio,
            compute_moisture_injection
        )
        from embrs.utilities.unit_conversions import BTU_ft_min_to_kW_m, Lbsft2_to_KiSq

        # Convert fireline intensity from BTU/(ft·min) to kW/m
        # I_t is a numpy array; use head-fire value (max)
        I_btu = max(self.I_t) if len(self.I_t) > 0 else 0.0
        I_kW_m = BTU_ft_min_to_kW_m(I_btu)

        # Dead fuel loading in kg/m² (w_n_dead is in lb/ft²)
        W_1_kg_m2 = Lbsft2_to_KiSq(self.fuel.w_n_dead)

        # Current fire area
        area_m2 = self.fire_area_m2

        # Compute energy needed to extinguish
        heat_needed = heat_to_extinguish_kJ(
            I_kW_m, W_1_kg_m2, area_m2,
            efficiency=self._vw_efficiency
        )

        # Suppression ratio
        ratio = compute_suppression_ratio(self.water_applied_kJ, heat_needed)

        # Inject moisture into dead fuel classes toward extinction
        self.fmois = compute_moisture_injection(
            self.fmois, self.fuel.dead_mx, ratio
        )

        self.has_steady_state = False

    def __str__(self) -> str:
        """Returns a formatted string representation of the cell.

        The string includes the cell's ID, coordinates, elevation, fuel type, and state.

        Returns:
            str: A formatted string representing the cell.
        """
        return (f"(id: {self.id}, {self.x_pos}, {self.y_pos}, {self.elevation_m}, "
                f"type: {self.fuel.name}, "
                f"state: {self.state}")

    def calc_hold_prob(self, flame_len_m: float) -> float:
        """Calculate the probability that a fuel break will stop fire spread.

        Uses the Mees et al. (1993) model to estimate the probability that a
        fuel discontinuity (road, firebreak) will prevent fire from crossing.

        Args:
            flame_len_m (float): Flame length at the fire front in meters.

        Returns:
            float: Probability that the fuel break holds (0.0-1.0).
                Returns 0 if no fuel break is present in this cell.

        Notes:
            - Based on Mees, et al. (1993).
        """
        # Mees et. al 1993
        if self._break_width == 0:
            return 0

        x = self._break_width
        m = flame_len_m
        T = 1

        if m == 0:
            return 1

        if m <= 0.61:
            h = 0
        elif 0.61 < m < 2.44:
            h = (m - 0.61)/m
        else:
            h = 0.75

        if x < (h*m):
            prob = 0
        else:
            prob = 1 - np.exp(((x - h*m) * np.log(0.15))/(T*m - h*m))

        return prob

    def calc_cell_area(self) -> float:
        """Calculates the area of the hexagonal cell in square meters.

        The formula for the area of a regular hexagon is:

            Area = (3 * sqrt(3) / 2) * side_length²

        Returns:
            float: The area of the hexagonal cell in square meters.
        """
        area_m2 = (3 * 1.7320508075688772 * self.cell_size ** 2) / 2  # 3*sqrt(3)/2
        return area_m2

    def to_polygon(self) -> Polygon:
        """Generates a Shapely polygon representation of the hexagonal cell.

        The polygon is created in a point-up orientation using the center (`x_pos`, `y_pos`)
        and the hexagon's side length.

        Returns:
            Polygon: A Shapely polygon representing the hexagonal cell.
        """
        l = self.cell_size
        x, y = self.x_pos, self.y_pos

        # Define the vertices for the hexagon in point-up orientation
        hex_coords = [
            (x, y + l),
            (x + (np.sqrt(3) / 2) * l, y + l / 2),
            (x + (np.sqrt(3) / 2) * l, y - l / 2),
            (x, y - l),
            (x - (np.sqrt(3) / 2) * l, y - l / 2),
            (x - (np.sqrt(3) / 2) * l, y + l / 2),
            (x, y + l)  # Close the polygon
        ]

        return Polygon(hex_coords)

    def to_log_entry(self, time: float) -> CellLogEntry:
        """Create a log entry capturing the cell's current state.

        Generates a structured record of the cell's fire behavior, fuel moisture,
        wind conditions, and other properties for logging and playback.

        Args:
            time (float): Current simulation time in seconds.

        Returns:
            CellLogEntry: Dataclass containing cell state for logging.
        """

        if self.fuel.burnable:
            w_n_dead = self.fuel.w_n_dead
            w_n_dead_start = self.fuel.w_n_dead_nominal
            w_n_live = self.fuel.w_n_live
            dfm_1hr = self.fmois[0]
            dfm_10hr = self.fmois[1]
            dfm_100hr = self.fmois[2]
        else:
            w_n_dead = 0
            w_n_dead_start = 0
            w_n_live = 0
            dfm_1hr = 0
            dfm_10hr = 0
            dfm_100hr = 0

        if len(self.r_t) > 0:
            r_t = np.max(self.r_t)
            I_ss = np.max(self.I_ss)
        else:
            r_t = 0
            I_ss = 0

        wind_speed, wind_dir = self.curr_wind()

        entry = CellLogEntry(
            timestamp=time,
            id=self.id,
            x=self.x_pos,
            y=self.y_pos,
            fuel=self.fuel.model_num,
            state=self.state,
            crown_state=self._crown_status,
            w_n_dead=w_n_dead,
            w_n_dead_start=w_n_dead_start,
            w_n_live=w_n_live,
            dfm_1hr=dfm_1hr,
            dfm_10hr=dfm_10hr,
            dfm_100hr=dfm_100hr,
            ros=r_t,
            I_ss=I_ss,
            wind_speed=wind_speed,
            wind_dir=wind_dir,
            retardant=self._retardant,
            arrival_time=self._arrival_time,
            suppression_count=self._suppression_count,
            n_disabled_locs=self.n_disabled_locs
        )

        return entry

    def iter_neighbor_cells(self) -> Iterator[Cell]:
        """Iterate over neighboring Cell objects.

        Yields each adjacent cell by looking up neighbor IDs in the parent
        simulation's cell_dict.

        Yields:
            Cell: Each neighboring cell object.

        Notes:
            - Returns immediately if parent reference is None.
        """
        parent = self._parent()

        if parent is None:
            return

        cell_dict = parent.cell_dict
        for nid in self._neighbors.keys():
            yield cell_dict[nid]

    # ------ Compare operators overloads ------ #
    def __lt__(self, other) -> bool:
        """Compares two cells based on their unique ID.

        This method allows for sorting and comparison of cells using the `<` (less than) operator.

        Args:
            other (Cell): Another cell to compare against.

        Returns:
            bool: `True` if this cell's ID is less than the other cell's ID, `False` otherwise.

        Raises:
            TypeError: If `other` is not an instance of `Cell`.
        """
        if not isinstance(other, type(self)):
            raise TypeError("Comparison must be between two Cell instances.")
        return self.id < other.id

    def __gt__(self, other) -> bool:
        """Compares two cells based on their unique ID.

        This method allows for sorting and comparison of cells using the `>` (greater than) operator.

        Args:
            other (Cell): Another cell to compare against.

        Returns:
            bool: `True` if this cell's ID is greater than the other cell's ID, `False` otherwise.

        Raises:
            TypeError: If `other` is not an instance of `Cell`.
        """
        if not isinstance(other, type(self)):
            raise TypeError("Comparison must be between two Cell instances.")
        return self.id > other.id

    def __getstate__(self) -> dict:
        """Prepare cell state for pickling.

        Excludes the weak reference to parent which cannot be pickled.

        Returns:
            dict: Cell state dictionary with _parent set to None.
        """
        state = self.__dict__.copy()
        # Remove weak reference - will be restored later
        state['_parent'] = None
        return state

    def __setstate__(self, state):
        """Restore cell state after unpickling.

        Args:
            state (dict): Cell state dictionary from __getstate__.
        """
        self.__dict__.update(state)
        # Parent will be set later via cell.set_parent(predictor)

    @property
    def col(self) -> int:
        """Column index of the cell in the simulation grid."""
        return self._col

    @property
    def row(self) -> int:
        """Row index of the cell in the simulation grid."""
        return self._row

    @property
    def cell_size(self) -> float:
        """Size of the cell in meters.

        Measured as the side length of the hexagon.
        """
        return self._cell_size

    @property
    def cell_area(self) -> float:
        """Area of the cell in square meters."""
        return self._cell_area

    @property
    def fire_area_m2(self) -> float:
        """Fire area within cell via trapezoidal polar integration.

        Computes A = (1/2) Σ Δθ_i · (r_i² + r_{i+1}²) / 2 over the
        discretized spread directions.  For center ignition (n_loc=0) the
        sum includes a closing segment from the last direction back to the
        first.  No area_fraction is needed — the angular range of the
        directions already encodes whether the fire covers the full cell,
        a half-cell, or a 60° sector.

        Clamped to cell_area as an upper bound.

        Returns 0.0 for non-burning cells, cells with no spread data, or
        cells where _ign_n_loc has not yet been set.
        """
        if self._state != CellStates.FIRE or len(self.fire_spread) == 0:
            return 0.0
        if self._ign_n_loc is None:
            return 0.0

        fs = self.fire_spread
        dirs = self.directions
        n = len(fs)

        area = 0.0
        for i in range(n - 1):
            dth = (dirs[i + 1] - dirs[i]) % 360
            area += dth * (fs[i] * fs[i] + fs[i + 1] * fs[i + 1])

        if self._ign_n_loc == 0:
            dth = (dirs[0] - dirs[-1] + 360) % 360
            area += dth * (fs[-1] * fs[-1] + fs[0] * fs[0])

        # 0.25 = (1/2 from polar integral) × (1/2 from trapezoidal average)
        # π/180 converts the degree-valued Δθ to radians
        area *= 0.25 * (np.pi / 180)

        return min(area, self._cell_area)

    @property
    def x_pos(self) -> float:
        """X-coordinate of the cell center in meters.

        Increases left to right in the visualization.
        """
        return self._x_pos

    @property
    def y_pos(self) -> float:
        """Y-coordinate of the cell center in meters.

        Increases bottom to top in the visualization.
        """
        return self._y_pos

    @property
    def elevation_m(self) -> float:
        """Elevation of the cell in meters."""
        return self._elevation_m

    @property
    def fuel(self) -> Fuel:
        """Fuel model for this cell.

        Can be any Anderson or Scott-Burgan fuel model.
        """
        return self._fuel

    @property
    def state(self) -> CellStates:
        """Current fire state of the cell (FUEL, FIRE, or BURNT)."""
        return self._state

    @property
    def neighbors(self) -> dict:
        """Dictionary of adjacent cells.

        Keys are neighbor cell IDs, values are (dx, dy) tuples indicating
        the column and row offset from this cell to the neighbor.
        """
        return self._neighbors

    @property
    def burnable_neighbors(self) -> dict:
        """Dictionary of adjacent cells that are in a burnable state.

        Same format as neighbors: keys are cell IDs, values are (dx, dy) offsets.
        """
        return self._burnable_neighbors

burnable_neighbors property

Dictionary of adjacent cells that are in a burnable state.

Same format as neighbors: keys are cell IDs, values are (dx, dy) offsets.

cell_area property

Area of the cell in square meters.

cell_size property

Size of the cell in meters.

Measured as the side length of the hexagon.

col property

Column index of the cell in the simulation grid.

elevation_m property

Elevation of the cell in meters.

fire_area_m2 property

Fire area within cell via trapezoidal polar integration.

Computes A = (1/2) Σ Δθ_i · (r_i² + r_{i+1}²) / 2 over the discretized spread directions. For center ignition (n_loc=0) the sum includes a closing segment from the last direction back to the first. No area_fraction is needed — the angular range of the directions already encodes whether the fire covers the full cell, a half-cell, or a 60° sector.

Clamped to cell_area as an upper bound.

Returns 0.0 for non-burning cells, cells with no spread data, or cells where _ign_n_loc has not yet been set.

fuel property

Fuel model for this cell.

Can be any Anderson or Scott-Burgan fuel model.

n_disabled_locs property

Number of boundary locations disabled by prior suppression.

neighbors property

Dictionary of adjacent cells.

Keys are neighbor cell IDs, values are (dx, dy) tuples indicating the column and row offset from this cell to the neighbor.

row property

Row index of the cell in the simulation grid.

state property

Current fire state of the cell (FUEL, FIRE, or BURNT).

x_pos property

X-coordinate of the cell center in meters.

Increases left to right in the visualization.

y_pos property

Y-coordinate of the cell center in meters.

Increases bottom to top in the visualization.

__getstate__()

Prepare cell state for pickling.

Excludes the weak reference to parent which cannot be pickled.

Returns:

Name Type Description
dict dict

Cell state dictionary with _parent set to None.

Source code in embrs/fire_simulator/cell.py
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
def __getstate__(self) -> dict:
    """Prepare cell state for pickling.

    Excludes the weak reference to parent which cannot be pickled.

    Returns:
        dict: Cell state dictionary with _parent set to None.
    """
    state = self.__dict__.copy()
    # Remove weak reference - will be restored later
    state['_parent'] = None
    return state

__gt__(other)

Compares two cells based on their unique ID.

This method allows for sorting and comparison of cells using the > (greater than) operator.

Parameters:

Name Type Description Default
other Cell

Another cell to compare against.

required

Returns:

Name Type Description
bool bool

True if this cell's ID is greater than the other cell's ID, False otherwise.

Raises:

Type Description
TypeError

If other is not an instance of Cell.

Source code in embrs/fire_simulator/cell.py
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
def __gt__(self, other) -> bool:
    """Compares two cells based on their unique ID.

    This method allows for sorting and comparison of cells using the `>` (greater than) operator.

    Args:
        other (Cell): Another cell to compare against.

    Returns:
        bool: `True` if this cell's ID is greater than the other cell's ID, `False` otherwise.

    Raises:
        TypeError: If `other` is not an instance of `Cell`.
    """
    if not isinstance(other, type(self)):
        raise TypeError("Comparison must be between two Cell instances.")
    return self.id > other.id

__init__(id, col, row, cell_size)

Initialize a hexagonal cell with position and geometry.

Creates a cell at the specified grid position and calculates its spatial coordinates based on the hexagonal grid layout. The cell is initialized with default values for fire state and fuel properties.

Parameters:

Name Type Description Default
id int

Unique identifier for this cell.

required
col int

Column index in the simulation grid.

required
row int

Row index in the simulation grid.

required
cell_size float

Edge length of the hexagon in meters.

required
Notes
  • Spatial position is calculated using point-up hexagon geometry.
  • For even rows: x = col * cell_size * sqrt(3)
  • For odd rows: x = (col + 0.5) * cell_size * sqrt(3)
  • y = row * cell_size * 1.5
Source code in embrs/fire_simulator/cell.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def __init__(self, id: int, col: int, row: int, cell_size: float):
    """Initialize a hexagonal cell with position and geometry.

    Creates a cell at the specified grid position and calculates its spatial
    coordinates based on the hexagonal grid layout. The cell is initialized
    with default values for fire state and fuel properties.

    Args:
        id (int): Unique identifier for this cell.
        col (int): Column index in the simulation grid.
        row (int): Row index in the simulation grid.
        cell_size (float): Edge length of the hexagon in meters.

    Notes:
        - Spatial position is calculated using point-up hexagon geometry.
        - For even rows: x = col * cell_size * sqrt(3)
        - For odd rows: x = (col + 0.5) * cell_size * sqrt(3)
        - y = row * cell_size * 1.5
    """
    self.id = id

    # Deterministic hash based on cell ID (not memory address) so that
    # set iteration order is reproducible across process invocations.
    self._hash = hash(id)

    # Set cell indices
    self._col = col
    self._row = row

    # x_pos, y_pos are the global position of cell in m
    _sqrt3 = 1.7320508075688772  # math.sqrt(3)
    if row % 2 == 0:
        self._x_pos = col * cell_size * _sqrt3
    else:
        self._x_pos = (col + 0.5) * cell_size * _sqrt3

    self._y_pos = row * cell_size * 1.5

    self._cell_size = cell_size # defined as the edge length of hexagon
    self._cell_area = self.calc_cell_area()

    # Flag to track if cell has been treated with long-term fire retardant
    self._retardant = False
    self._retardant_factor = 1.0 # Factor multiplied by rate of spread (0-1)
    self.retardant_expiration_s = -1.0 # Time at which long-term retardant in cell expires

    # Track the total amount of water dropped (if modelled as rain)
    self.local_rain = 0.0

    # Van Wagner energy-balance water suppression (Van Wagner & Taylor 2022)
    self.water_applied_kJ = 0.0   # Cumulative cooling energy from water drops
    self._vw_efficiency = 2.5     # Application efficiency multiplier (Table 4)

    # Width in meters of any fuel discontinuity within cell (road or firebreak)
    self._break_width = 0 

    # Variable to track if fuel discontinuity within cell can be breached
    self.breached = True

    # Track if firebrands have been lofted from cell
    self.lofted = False

    # Source cell coordinates if ignited by ember spotting (set by Embers.flight)
    self.spot_source_xy = None

    # Weak reference to parent BaseFire object
    self._parent = None

    self._arrival_time = -999

__lt__(other)

Compares two cells based on their unique ID.

This method allows for sorting and comparison of cells using the < (less than) operator.

Parameters:

Name Type Description Default
other Cell

Another cell to compare against.

required

Returns:

Name Type Description
bool bool

True if this cell's ID is less than the other cell's ID, False otherwise.

Raises:

Type Description
TypeError

If other is not an instance of Cell.

Source code in embrs/fire_simulator/cell.py
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
def __lt__(self, other) -> bool:
    """Compares two cells based on their unique ID.

    This method allows for sorting and comparison of cells using the `<` (less than) operator.

    Args:
        other (Cell): Another cell to compare against.

    Returns:
        bool: `True` if this cell's ID is less than the other cell's ID, `False` otherwise.

    Raises:
        TypeError: If `other` is not an instance of `Cell`.
    """
    if not isinstance(other, type(self)):
        raise TypeError("Comparison must be between two Cell instances.")
    return self.id < other.id

__setstate__(state)

Restore cell state after unpickling.

Parameters:

Name Type Description Default
state dict

Cell state dictionary from getstate.

required
Source code in embrs/fire_simulator/cell.py
1331
1332
1333
1334
1335
1336
1337
def __setstate__(self, state):
    """Restore cell state after unpickling.

    Args:
        state (dict): Cell state dictionary from __getstate__.
    """
    self.__dict__.update(state)

__str__()

Returns a formatted string representation of the cell.

The string includes the cell's ID, coordinates, elevation, fuel type, and state.

Returns:

Name Type Description
str str

A formatted string representing the cell.

Source code in embrs/fire_simulator/cell.py
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
def __str__(self) -> str:
    """Returns a formatted string representation of the cell.

    The string includes the cell's ID, coordinates, elevation, fuel type, and state.

    Returns:
        str: A formatted string representing the cell.
    """
    return (f"(id: {self.id}, {self.x_pos}, {self.y_pos}, {self.elevation_m}, "
            f"type: {self.fuel.name}, "
            f"state: {self.state}")

add_retardant(duration_hr, effectiveness)

Apply long-term fire retardant to this cell.

Marks the cell as treated with retardant, which reduces the rate of spread by the effectiveness factor until the retardant expires.

Parameters:

Name Type Description Default
duration_hr float

Duration of retardant effectiveness in hours.

required
effectiveness float

Reduction factor for rate of spread (0.0-1.0). A value of 0.5 reduces ROS by 50%.

required

Raises:

Type Description
ValueError

If effectiveness is not in range [0, 1].

Side Effects
  • Sets self._retardant to True.
  • Sets self._retardant_factor to (1 - effectiveness).
  • Sets self.retardant_expiration_s to expiration time.
Source code in embrs/fire_simulator/cell.py
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
def add_retardant(self, duration_hr: float, effectiveness: float):
    """Apply long-term fire retardant to this cell.

    Marks the cell as treated with retardant, which reduces the rate of
    spread by the effectiveness factor until the retardant expires.

    Args:
        duration_hr (float): Duration of retardant effectiveness in hours.
        effectiveness (float): Reduction factor for rate of spread (0.0-1.0).
            A value of 0.5 reduces ROS by 50%.

    Raises:
        ValueError: If effectiveness is not in range [0, 1].

    Side Effects:
        - Sets self._retardant to True.
        - Sets self._retardant_factor to (1 - effectiveness).
        - Sets self.retardant_expiration_s to expiration time.
    """
    if effectiveness < 0 or effectiveness > 1:
        raise ValueError(f"Retardant effectiveness must be between 0 and 1 ({effectiveness} passed in)")

    self._retardant = True
    self._retardant_factor = 1 - effectiveness

    self.retardant_expiration_s = self._parent().curr_time_s + (duration_hr * 3600)

apply_vw_suppression()

Apply moisture injection from accumulated Van Wagner water energy.

Called by iterate() for cells with water_applied_kJ > 0. Computes suppression ratio from current fire state, then injects moisture toward dead_mx proportionally.

Uses
  • I_ss (BTU/ft/min) converted to kW/m
  • fuel.w_n_dead + w_n_live (lb/ft²) converted to kg/m²
  • fire_area_m2 property
  • self._vw_efficiency (stored from water_drop_vw call)
  • heat_to_extinguish_kJ() (Eq. 7b + 10b + Table 4)
  • compute_suppression_ratio()
  • compute_moisture_injection()
Side Effects
  • Modifies self.fmois
  • Sets self.has_steady_state = False
Source code in embrs/fire_simulator/cell.py
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
def apply_vw_suppression(self):
    """Apply moisture injection from accumulated Van Wagner water energy.

    Called by iterate() for cells with water_applied_kJ > 0. Computes
    suppression ratio from current fire state, then injects moisture
    toward dead_mx proportionally.

    Uses:
        - I_ss (BTU/ft/min) converted to kW/m
        - fuel.w_n_dead + w_n_live (lb/ft²) converted to kg/m²
        - fire_area_m2 property
        - self._vw_efficiency (stored from water_drop_vw call)
        - heat_to_extinguish_kJ() (Eq. 7b + 10b + Table 4)
        - compute_suppression_ratio()
        - compute_moisture_injection()

    Side Effects:
        - Modifies self.fmois
        - Sets self.has_steady_state = False
    """
    from embrs.models.van_wagner_water import (
        heat_to_extinguish_kJ, compute_suppression_ratio,
        compute_moisture_injection
    )
    from embrs.utilities.unit_conversions import BTU_ft_min_to_kW_m, Lbsft2_to_KiSq

    # Convert fireline intensity from BTU/(ft·min) to kW/m
    # I_t is a numpy array; use head-fire value (max)
    I_btu = max(self.I_t) if len(self.I_t) > 0 else 0.0
    I_kW_m = BTU_ft_min_to_kW_m(I_btu)

    # Dead fuel loading in kg/m² (w_n_dead is in lb/ft²)
    W_1_kg_m2 = Lbsft2_to_KiSq(self.fuel.w_n_dead)

    # Current fire area
    area_m2 = self.fire_area_m2

    # Compute energy needed to extinguish
    heat_needed = heat_to_extinguish_kJ(
        I_kW_m, W_1_kg_m2, area_m2,
        efficiency=self._vw_efficiency
    )

    # Suppression ratio
    ratio = compute_suppression_ratio(self.water_applied_kJ, heat_needed)

    # Inject moisture into dead fuel classes toward extinction
    self.fmois = compute_moisture_injection(
        self.fmois, self.fuel.dead_mx, ratio
    )

    self.has_steady_state = False

calc_cell_area()

Calculates the area of the hexagonal cell in square meters.

The formula for the area of a regular hexagon is:

Area = (3 * sqrt(3) / 2) * side_length²

Returns:

Name Type Description
float float

The area of the hexagonal cell in square meters.

Source code in embrs/fire_simulator/cell.py
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
def calc_cell_area(self) -> float:
    """Calculates the area of the hexagonal cell in square meters.

    The formula for the area of a regular hexagon is:

        Area = (3 * sqrt(3) / 2) * side_length²

    Returns:
        float: The area of the hexagonal cell in square meters.
    """
    area_m2 = (3 * 1.7320508075688772 * self.cell_size ** 2) / 2  # 3*sqrt(3)/2
    return area_m2

calc_hold_prob(flame_len_m)

Calculate the probability that a fuel break will stop fire spread.

Uses the Mees et al. (1993) model to estimate the probability that a fuel discontinuity (road, firebreak) will prevent fire from crossing.

Parameters:

Name Type Description Default
flame_len_m float

Flame length at the fire front in meters.

required

Returns:

Name Type Description
float float

Probability that the fuel break holds (0.0-1.0). Returns 0 if no fuel break is present in this cell.

Notes
  • Based on Mees, et al. (1993).
Source code in embrs/fire_simulator/cell.py
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
def calc_hold_prob(self, flame_len_m: float) -> float:
    """Calculate the probability that a fuel break will stop fire spread.

    Uses the Mees et al. (1993) model to estimate the probability that a
    fuel discontinuity (road, firebreak) will prevent fire from crossing.

    Args:
        flame_len_m (float): Flame length at the fire front in meters.

    Returns:
        float: Probability that the fuel break holds (0.0-1.0).
            Returns 0 if no fuel break is present in this cell.

    Notes:
        - Based on Mees, et al. (1993).
    """
    # Mees et. al 1993
    if self._break_width == 0:
        return 0

    x = self._break_width
    m = flame_len_m
    T = 1

    if m == 0:
        return 1

    if m <= 0.61:
        h = 0
    elif 0.61 < m < 2.44:
        h = (m - 0.61)/m
    else:
        h = 0.75

    if x < (h*m):
        prob = 0
    else:
        prob = 1 - np.exp(((x - h*m) * np.log(0.15))/(T*m - h*m))

    return prob

compute_disabled_locs()

Compute boundary locations consumed by current fire and add to disabled_locs.

Must be called BEFORE fire-state arrays are cleared. Uses three rules to determine which boundary locations (1-12) have been consumed.

Rule 1: Entry point is consumed. Rule 2: Each crossed intersection's exit boundary location is consumed. Rule 3: For corner ignitions, adjacent midpoints are consumed if fire has spread past half the distance in that direction.

Source code in embrs/fire_simulator/cell.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
def compute_disabled_locs(self):
    """Compute boundary locations consumed by current fire and add to disabled_locs.

    Must be called BEFORE fire-state arrays are cleared. Uses three rules
    to determine which boundary locations (1-12) have been consumed.

    Rule 1: Entry point is consumed.
    Rule 2: Each crossed intersection's exit boundary location is consumed.
    Rule 3: For corner ignitions, adjacent midpoints are consumed if fire
             has spread past half the distance in that direction.
    """
    n_loc = self._ign_n_loc
    if n_loc is None:
        return

    # Rule 1 — entry point
    if n_loc != 0:
        self.disabled_locs.add(n_loc)

    # Rule 2 — crossed intersections → exit boundary locations
    if self._self_end_points is not None:
        for i in range(len(self.intersections)):
            if self.intersections[i]:
                self.disabled_locs.add(self._self_end_points[i])

    # Rule 3 — corner adjacent midpoints (even, nonzero n_loc only)
    if n_loc != 0 and n_loc % 2 == 0:
        if len(self.fire_spread) > 0 and self.distances is not None and len(self.distances) > 0:
            if self.fire_spread[0] > self.distances[0] / 2:
                self.disabled_locs.add((n_loc % 12) + 1)
            if self.fire_spread[-1] > self.distances[-1] / 2:
                self.disabled_locs.add(((n_loc - 2) % 12) + 1)

curr_wind()

Get the current wind speed and direction at this cell.

Returns the wind conditions for the current weather interval from the cell's local wind forecast. For prediction runs, may trigger forecast updates if needed.

Returns:

Name Type Description
tuple tuple

(wind_speed, wind_direction) where speed is in m/s and direction is in degrees using cartesian convention (0° = blowing toward North/+y, 90° = blowing toward East/+x).

Source code in embrs/fire_simulator/cell.py
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
def curr_wind(self) -> tuple:
    """Get the current wind speed and direction at this cell.

    Returns the wind conditions for the current weather interval from the
    cell's local wind forecast. For prediction runs, may trigger forecast
    updates if needed.

    Returns:
        tuple: (wind_speed, wind_direction) where speed is in m/s and
            direction is in degrees using cartesian convention
            (0° = blowing toward North/+y, 90° = blowing toward East/+x).
    """
    parent = self._parent()
    w_idx = parent._curr_weather_idx - parent.sim_start_w_idx

    if parent.is_prediction() and len(self.forecast_wind_speeds) <= w_idx:
        parent._set_prediction_forecast(self)

    # Defensive clamp: prevent index-out-of-bounds on forecast arrays
    if w_idx >= len(self.forecast_wind_speeds):
        import warnings
        warnings.warn(
            f"Cell.curr_wind: w_idx ({w_idx}) >= forecast length "
            f"({len(self.forecast_wind_speeds)}) for cell {self.id}. "
            f"Clamping to last available index."
        )
        w_idx = len(self.forecast_wind_speeds) - 1

    curr_wind = (self.forecast_wind_speeds[w_idx], self.forecast_wind_dirs[w_idx])

    return curr_wind

get_ign_params(n_loc)

Calculate fire spread directions and distances from an ignition location.

Computes the radial spread directions from the specified ignition point within the cell to each edge or vertex. Initializes arrays for tracking rate of spread and fireline intensity in each direction.

Parameters:

Name Type Description Default
n_loc int

Ignition location index within the cell. 0=center, 1-6=vertices, 7-12=edge midpoints.

required
Side Effects
  • Sets self.directions: array of compass directions in degrees.
  • Sets self.distances: slope-adjusted distances to cell boundaries.
  • Sets self.end_pts: coordinates of cell boundary points.
  • Initializes self.avg_ros, self.I_t, self.r_t to zero arrays.
Source code in embrs/fire_simulator/cell.py
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
def get_ign_params(self, n_loc: int):
    """Calculate fire spread directions and distances from an ignition location.

    Computes the radial spread directions from the specified ignition point
    within the cell to each edge or vertex. Initializes arrays for tracking
    rate of spread and fireline intensity in each direction.

    Args:
        n_loc (int): Ignition location index within the cell.
            0=center, 1-6=vertices, 7-12=edge midpoints.

    Side Effects:
        - Sets self.directions: array of compass directions in degrees.
        - Sets self.distances: slope-adjusted distances to cell boundaries.
        - Sets self.end_pts: coordinates of cell boundary points.
        - Initializes self.avg_ros, self.I_t, self.r_t to zero arrays.
    """
    self._ign_n_loc = n_loc
    self.directions, distances, self.end_pts = UtilFuncs.get_ign_parameters(n_loc, self.cell_size)
    self._self_end_points = _derive_self_end_points(n_loc)
    self.project_distances_to_surf(distances)
    self.avg_ros = np.zeros_like(self.directions)
    self.I_t = np.zeros_like(self.directions)
    self.r_t = np.zeros_like(self.directions)

iter_neighbor_cells()

Iterate over neighboring Cell objects.

Yields each adjacent cell by looking up neighbor IDs in the parent simulation's cell_dict.

Yields:

Name Type Description
Cell Cell

Each neighboring cell object.

Notes
  • Returns immediately if parent reference is None.
Source code in embrs/fire_simulator/cell.py
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
def iter_neighbor_cells(self) -> Iterator[Cell]:
    """Iterate over neighboring Cell objects.

    Yields each adjacent cell by looking up neighbor IDs in the parent
    simulation's cell_dict.

    Yields:
        Cell: Each neighboring cell object.

    Notes:
        - Returns immediately if parent reference is None.
    """
    parent = self._parent()

    if parent is None:
        return

    cell_dict = parent.cell_dict
    for nid in self._neighbors.keys():
        yield cell_dict[nid]

project_distances_to_surf(distances)

Project horizontal distances onto the sloped terrain surface.

Adjusts the flat-ground distances to each cell edge by accounting for the slope and aspect of the terrain. This ensures fire spread distances are measured along the actual terrain surface.

Parameters:

Name Type Description Default
distances ndarray

Horizontal distances to cell edges in meters.

required
Side Effects
  • Sets self.distances to the slope-adjusted distances in meters.
Source code in embrs/fire_simulator/cell.py
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
def project_distances_to_surf(self, distances: np.ndarray):
    """Project horizontal distances onto the sloped terrain surface.

    Adjusts the flat-ground distances to each cell edge by accounting for
    the slope and aspect of the terrain. This ensures fire spread distances
    are measured along the actual terrain surface.

    Args:
        distances (np.ndarray): Horizontal distances to cell edges in meters.

    Side Effects:
        - Sets self.distances to the slope-adjusted distances in meters.
    """
    slope_rad = np.deg2rad(self.slope_deg)
    aspect = (self.aspect + 180) % 360
    deltas = np.deg2rad(aspect - np.array(self.directions))
    proj = np.sqrt(np.cos(deltas) ** 2 * np.cos(slope_rad) ** 2 + np.sin(deltas) ** 2)
    self.distances = distances / proj

reset_to_fuel()

Reset cell to initial FUEL state, preserving terrain/fuel/geometry data.

Resets all mutable fire-state attributes to their initial values as set by __init__ and _set_cell_data. Immutable properties such as position, fuel model, elevation, slope, aspect, canopy attributes, polygon, wind adjustment factor, and neighbor topology are preserved.

This is used by FirePredictor to efficiently restore cells to a clean state between predictions, avoiding expensive deepcopy operations.

Side Effects
  • Resets fire state to CellStates.FUEL
  • Clears all spread tracking arrays
  • Resets suppression effects (retardant, rain, firebreaks)
  • Resets fuel moisture to initial values
  • Restores full neighbor set to _burnable_neighbors
Source code in embrs/fire_simulator/cell.py
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
def reset_to_fuel(self):
    """Reset cell to initial FUEL state, preserving terrain/fuel/geometry data.

    Resets all mutable fire-state attributes to their initial values as set
    by ``__init__`` and ``_set_cell_data``. Immutable properties such as
    position, fuel model, elevation, slope, aspect, canopy attributes,
    polygon, wind adjustment factor, and neighbor topology are preserved.

    This is used by FirePredictor to efficiently restore cells to a clean
    state between predictions, avoiding expensive deepcopy operations.

    Side Effects:
        - Resets fire state to CellStates.FUEL
        - Clears all spread tracking arrays
        - Resets suppression effects (retardant, rain, firebreaks)
        - Resets fuel moisture to initial values
        - Restores full neighbor set to _burnable_neighbors
    """
    # Fire state
    self._state = CellStates.FUEL
    self.fully_burning = False
    self._crown_status = CrownStatus.NONE
    self.cfb = 0
    self.reaction_intensity = 0

    # Partial suppression state
    self.disabled_locs = set()
    self._suppression_count = 0

    # Spread arrays — shared read-only constants (replaced when cell ignites)
    self.distances = None
    self.directions = None
    self.end_pts = None
    self._ign_n_loc = None
    self._self_end_points = None
    self.r_h_ss = None
    self.I_h_ss = None
    self.r_t = _RESET_ARRAY_ZERO
    self.fire_spread = _RESET_ARRAY_EMPTY
    self.avg_ros = _RESET_ARRAY_EMPTY
    self.r_ss = _RESET_ARRAY_EMPTY
    self.I_ss = _RESET_ARRAY_ZERO
    self.I_t = _RESET_ARRAY_ZERO
    self.intersections = _RESET_IXN_EMPTY
    self.e = 0
    self.alpha = None
    self.has_steady_state = False

    # Suppression effects — back to __init__ defaults
    self._retardant = False
    self._retardant_factor = 1.0
    self.retardant_expiration_s = -1.0
    self.local_rain = 0.0
    self.water_applied_kJ = 0.0
    self._vw_efficiency = 2.5
    self._break_width = 0
    self.breached = True
    self.lofted = False
    self.spot_source_xy = None
    self._arrival_time = -999

    # Wind forecast
    self.forecast_wind_speeds = []
    self.forecast_wind_dirs = []

    # Moisture — reset to initial values (use cached template if available)
    self.moist_update_time_s = 0
    if self._fuel.burnable:
        if self.dfms:
            for dfm in self.dfms:
                dfm.initializeStick()
        # Reset moisture array from cached initial values
        self.fmois = self._init_fmois.copy()

    # Restore full neighbor set (remove_neighbors modifies _burnable_neighbors during sim)
    self._burnable_neighbors = dict(self._neighbors)

set_arrays()

Initialize fuel moisture tracking arrays for this cell.

DFM objects are created lazily on first moisture update to avoid allocating ~12 numpy arrays per object for cells that never burn.

Side Effects
  • Sets self.wdry and self.sigma from fuel model properties.
  • Initializes self.fmois array with initial moisture fractions.
  • Sets self._dfms_needed tuple for lazy DFM creation.
Source code in embrs/fire_simulator/cell.py
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
def set_arrays(self):
    """Initialize fuel moisture tracking arrays for this cell.

    DFM objects are created lazily on first moisture update to avoid
    allocating ~12 numpy arrays per object for cells that never burn.

    Side Effects:
        - Sets self.wdry and self.sigma from fuel model properties.
        - Initializes self.fmois array with initial moisture fractions.
        - Sets self._dfms_needed tuple for lazy DFM creation.
    """
    indices = self._fuel.rel_indices

    self.wdry = self._fuel.w_n[self._fuel.rel_indices]
    self.sigma = self._fuel.s[self._fuel.rel_indices]

    # Defer DFM creation — store which classes are needed
    self.dfms = []
    self._dfms_needed = (0 in indices, 1 in indices, 2 in indices)

    fmois = np.zeros(6)

    if 0 in indices:
        fmois[0] = self.init_dead_mf[0]
    if 1 in indices:
        fmois[1] = self.init_dead_mf[1]
    if 2 in indices:
        fmois[2] = self.init_dead_mf[2]
    if 3 in indices:
        fmois[3] = self.init_dead_mf[0]
    if 4 in indices:
        fmois[4] = self.init_live_h_mf
    if 5 in indices:
        fmois[5] = self.init_live_w_mf

    self.fmois = np.array(fmois)
    # Cache initial fmois for efficient reset_to_fuel
    self._init_fmois = self.fmois.copy()

set_parent(parent)

Sets the parent BaseFire object for this cell.

Parameters:

Name Type Description Default
parent BaseFireSim

The BaseFire object that owns this cell.

required
Source code in embrs/fire_simulator/cell.py
167
168
169
170
171
172
173
def set_parent(self, parent: BaseFireSim) -> None:
    """Sets the parent BaseFire object for this cell.

    Args:
        parent (BaseFireSim): The BaseFire object that owns this cell.
    """
    self._parent = weakref.ref(parent)

suppress_to_fuel()

Suppress cell back to FUEL state, preserving moisture and disabled_locs.

Similar to reset_to_fuel() but preserves: - disabled_locs (accumulated consumed boundary locations) - _suppression_count (incremented) - Fuel moisture state (fmois, dfms, moist_update_time_s)

Clears fire-state arrays, VW water state, and crown fire state. Restores burnable_neighbors from full neighbor set.

Source code in embrs/fire_simulator/cell.py
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def suppress_to_fuel(self):
    """Suppress cell back to FUEL state, preserving moisture and disabled_locs.

    Similar to reset_to_fuel() but preserves:
    - disabled_locs (accumulated consumed boundary locations)
    - _suppression_count (incremented)
    - Fuel moisture state (fmois, dfms, moist_update_time_s)

    Clears fire-state arrays, VW water state, and crown fire state.
    Restores burnable_neighbors from full neighbor set.
    """
    # Fire state
    self._state = CellStates.FUEL
    self.fully_burning = False
    self._crown_status = CrownStatus.NONE
    self.cfb = 0
    self.reaction_intensity = 0

    # Spread arrays
    self.distances = None
    self.directions = None
    self.end_pts = None
    self._ign_n_loc = None
    self._self_end_points = None
    self.r_h_ss = None
    self.I_h_ss = None
    self.r_t = _RESET_ARRAY_ZERO
    self.fire_spread = _RESET_ARRAY_EMPTY
    self.avg_ros = _RESET_ARRAY_EMPTY
    self.r_ss = _RESET_ARRAY_EMPTY
    self.I_ss = _RESET_ARRAY_ZERO
    self.I_t = _RESET_ARRAY_ZERO
    self.intersections = _RESET_IXN_EMPTY
    self.e = 0
    self.alpha = None
    self.has_steady_state = False

    # Clear VW water suppression state
    self.water_applied_kJ = 0.0
    self._vw_efficiency = 2.5

    # Restore full neighbor set
    self._burnable_neighbors = dict(self._neighbors)

    # Increment suppression count
    self._suppression_count += 1

to_log_entry(time)

Create a log entry capturing the cell's current state.

Generates a structured record of the cell's fire behavior, fuel moisture, wind conditions, and other properties for logging and playback.

Parameters:

Name Type Description Default
time float

Current simulation time in seconds.

required

Returns:

Name Type Description
CellLogEntry CellLogEntry

Dataclass containing cell state for logging.

Source code in embrs/fire_simulator/cell.py
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
def to_log_entry(self, time: float) -> CellLogEntry:
    """Create a log entry capturing the cell's current state.

    Generates a structured record of the cell's fire behavior, fuel moisture,
    wind conditions, and other properties for logging and playback.

    Args:
        time (float): Current simulation time in seconds.

    Returns:
        CellLogEntry: Dataclass containing cell state for logging.
    """

    if self.fuel.burnable:
        w_n_dead = self.fuel.w_n_dead
        w_n_dead_start = self.fuel.w_n_dead_nominal
        w_n_live = self.fuel.w_n_live
        dfm_1hr = self.fmois[0]
        dfm_10hr = self.fmois[1]
        dfm_100hr = self.fmois[2]
    else:
        w_n_dead = 0
        w_n_dead_start = 0
        w_n_live = 0
        dfm_1hr = 0
        dfm_10hr = 0
        dfm_100hr = 0

    if len(self.r_t) > 0:
        r_t = np.max(self.r_t)
        I_ss = np.max(self.I_ss)
    else:
        r_t = 0
        I_ss = 0

    wind_speed, wind_dir = self.curr_wind()

    entry = CellLogEntry(
        timestamp=time,
        id=self.id,
        x=self.x_pos,
        y=self.y_pos,
        fuel=self.fuel.model_num,
        state=self.state,
        crown_state=self._crown_status,
        w_n_dead=w_n_dead,
        w_n_dead_start=w_n_dead_start,
        w_n_live=w_n_live,
        dfm_1hr=dfm_1hr,
        dfm_10hr=dfm_10hr,
        dfm_100hr=dfm_100hr,
        ros=r_t,
        I_ss=I_ss,
        wind_speed=wind_speed,
        wind_dir=wind_dir,
        retardant=self._retardant,
        arrival_time=self._arrival_time,
        suppression_count=self._suppression_count,
        n_disabled_locs=self.n_disabled_locs
    )

    return entry

to_polygon()

Generates a Shapely polygon representation of the hexagonal cell.

The polygon is created in a point-up orientation using the center (x_pos, y_pos) and the hexagon's side length.

Returns:

Name Type Description
Polygon Polygon

A Shapely polygon representing the hexagonal cell.

Source code in embrs/fire_simulator/cell.py
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
def to_polygon(self) -> Polygon:
    """Generates a Shapely polygon representation of the hexagonal cell.

    The polygon is created in a point-up orientation using the center (`x_pos`, `y_pos`)
    and the hexagon's side length.

    Returns:
        Polygon: A Shapely polygon representing the hexagonal cell.
    """
    l = self.cell_size
    x, y = self.x_pos, self.y_pos

    # Define the vertices for the hexagon in point-up orientation
    hex_coords = [
        (x, y + l),
        (x + (np.sqrt(3) / 2) * l, y + l / 2),
        (x + (np.sqrt(3) / 2) * l, y - l / 2),
        (x, y - l),
        (x - (np.sqrt(3) / 2) * l, y - l / 2),
        (x - (np.sqrt(3) / 2) * l, y + l / 2),
        (x, y + l)  # Close the polygon
    ]

    return Polygon(hex_coords)

water_drop_as_moisture_bump(moisture_bump)

Apply a water drop as a direct fuel moisture increase.

Simulates water delivery by directly increasing the outer node moisture of each dead fuel class, then advances the moisture model briefly to allow diffusion.

Parameters:

Name Type Description Default
moisture_bump float

Moisture fraction to add to fuel surface.

required
Side Effects
  • Increases outer node moisture on each DeadFuelMoisture object.
  • Advances moisture model by 30 seconds.
  • Updates self.fmois with new moisture fractions.
Notes
  • No effect on non-burnable fuel types.
Source code in embrs/fire_simulator/cell.py
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
def water_drop_as_moisture_bump(self, moisture_bump: float):
    """Apply a water drop as a direct fuel moisture increase.

    Simulates water delivery by directly increasing the outer node moisture
    of each dead fuel class, then advances the moisture model briefly to
    allow diffusion.

    Args:
        moisture_bump (float): Moisture fraction to add to fuel surface.

    Side Effects:
        - Increases outer node moisture on each DeadFuelMoisture object.
        - Advances moisture model by 30 seconds.
        - Updates self.fmois with new moisture fractions.

    Notes:
        - No effect on non-burnable fuel types.
    """
    if not self.fuel.burnable:
        return

    if not self.dfms:
        self._ensure_dfms()

    # Catch cell up to current time
    parent = self._parent()
    now_idx = parent._curr_weather_idx
    now_s = parent.curr_time_s
    weather_stream = parent._weather_stream
    self._catch_up_moisture_to_curr(now_s, weather_stream)

    for dfm in self.dfms:
        # Add moisture bump to outer node of each dead fuel moisture class
        dfm.m_w[0] += moisture_bump
        dfm.m_w[0] = max(dfm.m_w[0], dfm.m_wmx)

    self._step_moisture(weather_stream, now_idx, update_interval_hr=30/3600)

    # Update moisture content for each dead fuel moisture class
    self.fmois[0:len(self.dfms)] = [dfm.meanWtdMoisture() for dfm in self.dfms]

    if self.fuel.dynamic:
        # Set dead herbaceous moisture to 1-hr value
        self.fmois[3] = self.fmois[0]

    self.has_steady_state = False

water_drop_as_rain(water_depth_cm, duration_s=30)

Apply a water drop modeled as equivalent rainfall.

Simulates water delivery by treating the water as cumulative rainfall input to the fuel moisture model. Updates moisture state immediately.

Parameters:

Name Type Description Default
water_depth_cm float

Equivalent water depth in centimeters.

required
duration_s float

Duration of the water application in seconds.

30
Side Effects
  • Updates self.local_rain with accumulated water depth.
  • Advances moisture model through the application period.
  • Updates self.fmois with new moisture fractions.
Notes
  • No effect on non-burnable fuel types.
Source code in embrs/fire_simulator/cell.py
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
def water_drop_as_rain(self, water_depth_cm: float, duration_s: float = 30):
    """Apply a water drop modeled as equivalent rainfall.

    Simulates water delivery by treating the water as cumulative
    rainfall input to the fuel moisture model. Updates moisture state
    immediately.

    Args:
        water_depth_cm (float): Equivalent water depth in centimeters.
        duration_s (float): Duration of the water application in seconds.

    Side Effects:
        - Updates self.local_rain with accumulated water depth.
        - Advances moisture model through the application period.
        - Updates self.fmois with new moisture fractions.

    Notes:
        - No effect on non-burnable fuel types.
    """
    if not self.fuel.burnable:
        return

    parent = self._parent()
    now_idx = parent._curr_weather_idx
    now_s = parent.curr_time_s
    weather_stream = parent._weather_stream

    # 1. Step moisture from last update to the current time
    self._catch_up_moisture_to_curr(now_s, weather_stream)

    # 2. Increment local rain
    self.local_rain += water_depth_cm

    # 3. Step moisture from current time to end of local rain interval
    interval_hr = duration_s / 3600 # Convert seconds to hours
    self._step_moisture(weather_stream, now_idx, update_interval_hr=interval_hr)

    # 4. Store the time so that next update just updates from current time over a weather interval
    self.moist_update_time_s = now_s + duration_s

     # Update moisture content for each dead fuel moisture class
    self.fmois[0:len(self.dfms)] = [dfm.meanWtdMoisture() for dfm in self.dfms]

    if self.fuel.dynamic:
        # Set dead herbaceous moisture to 1-hr value
        self.fmois[3] = self.fmois[0]

    self.has_steady_state = False

water_drop_vw(volume_L, efficiency=2.5, T_a=20.0)

Apply water using Van Wagner (2022) energy-balance model.

Converts water volume to cooling energy (Eq. 1b) and accumulates in water_applied_kJ. Moisture injection is applied during iterate() by apply_vw_suppression().

Parameters:

Name Type Description Default
volume_L float

Water volume in liters (1 L = 1 kg).

required
efficiency float

Application efficiency multiplier (Table 4, Van Wagner 2022). Typical range 2.0–4.0. Default 2.5.

2.5
T_a float

Ambient air temperature in °C. Default 20.

20.0

Raises:

Type Description
ValueError

If volume_L < 0.

Source code in embrs/fire_simulator/cell.py
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
def water_drop_vw(self, volume_L: float, efficiency: float = 2.5,
                   T_a: float = 20.0):
    """Apply water using Van Wagner (2022) energy-balance model.

    Converts water volume to cooling energy (Eq. 1b) and accumulates in
    water_applied_kJ. Moisture injection is applied during iterate() by
    apply_vw_suppression().

    Args:
        volume_L: Water volume in liters (1 L = 1 kg).
        efficiency: Application efficiency multiplier (Table 4, Van Wagner
            2022). Typical range 2.0–4.0. Default 2.5.
        T_a: Ambient air temperature in °C. Default 20.

    Raises:
        ValueError: If volume_L < 0.
    """
    if volume_L < 0:
        raise ValueError(f"Water volume must be >= 0, got {volume_L}")

    if not self.fuel.burnable:
        return

    from embrs.models.van_wagner_water import volume_L_to_energy_kJ

    self._vw_efficiency = efficiency
    energy_kJ = volume_L_to_energy_kJ(volume_L, T_a)
    self.water_applied_kJ += energy_kJ
    self.has_steady_state = False

RealTimeVisualizer

Renders fire spread, agent positions, and weather data in real-time during a simulation. Created internally by the simulation launcher — most users do not need to instantiate this directly.

Real-time visualization for running fire simulations.

This module provides a visualizer that renders fire simulation state in real-time during simulation execution. It extends the base visualizer with live update capabilities and caching for efficient rendering.

Classes:

Name Description
- RealTimeVisualizer

Live visualization interface for FireSim instances.

.. autoclass:: RealTimeVisualizer :members:

RealTimeVisualizer

Bases: BaseVisualizer

Real-time visualization interface for running fire simulations.

Provides live rendering of fire spread, agent positions, and weather data during simulation execution. Cell updates are cached between visualization frames for efficient batch rendering.

Attributes:

Name Type Description
sim FireSim

Reference to the associated fire simulation.

cell_cache list

Buffer of CellLogEntry objects awaiting visualization.

Source code in embrs/fire_simulator/visualizer.py
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
class RealTimeVisualizer(BaseVisualizer):
    """Real-time visualization interface for running fire simulations.

    Provides live rendering of fire spread, agent positions, and weather data
    during simulation execution. Cell updates are cached between visualization
    frames for efficient batch rendering.

    Attributes:
        sim (FireSim): Reference to the associated fire simulation.
        cell_cache (list): Buffer of CellLogEntry objects awaiting visualization.
    """

    def __init__(self, sim: FireSim):
        """Initialize the real-time visualizer for a simulation.

        Args:
            sim (FireSim): The fire simulation instance to visualize.

        Side Effects:
            - Initializes the base visualizer with simulation parameters.
            - Creates an empty cell cache for buffering updates.
        """
        self.sim = sim
        self.cell_cache = []

        input_params = self.get_input_params()

        super().__init__(input_params)

    def set_sim(self, sim: FireSim):
        """Update the simulation reference for this visualizer.

        Args:
            sim (FireSim): New simulation instance to visualize.
        """
        self.sim = sim

    def get_init_entries(self) -> list:
        """Generate initial log entries for all cells in the simulation.

        Returns:
            list: List of CellLogEntry objects representing the initial state
                of every cell in the simulation grid.
        """
        entries = [cell.to_log_entry(self.sim._curr_time_s) for cell in self.sim.cell_dict.values()]
        return entries

    def cache_changes(self, updated_cells: list):
        """Add cell updates to the visualization cache.

        Buffers cell state changes for batch rendering on the next update call.

        Args:
            updated_cells (list): List of CellLogEntry objects to cache.

        Side Effects:
            - Extends self.cell_cache with new entries.
        """
        self.cell_cache.extend(updated_cells)

    def update(self):
        """Render all cached updates to the visualization.

        Collects agent positions and action entries from the simulation,
        then renders all cached cell changes along with current agent and
        action state.

        Side Effects:
            - Calls update_grid with cached cell data.
            - Clears the cell cache after rendering.
        """
        agents = [agent.to_log_entry(self.sim._curr_time_s) for agent in self.sim.agent_list]
        actions = self.sim.get_action_entries(logger=False)
        self.update_grid(self.sim._curr_time_s, self.cell_cache, agents, actions)

        self.cell_cache = []

    def get_input_params(self) -> VisualizerInputs:
        """Build visualization parameters from the current simulation state.

        Extracts grid geometry, weather forecasts, terrain data, and initial
        cell states from the simulation to configure the base visualizer.

        Returns:
            VisualizerInputs: Configuration dataclass for BaseVisualizer.
        """
        params = VisualizerInputs(
            cell_size=self.sim.cell_size,
            sim_shape=self.sim.shape,
            sim_size=self.sim.size,
            start_datetime=self.sim._start_datetime,
            north_dir_deg=self.sim._north_dir_deg,
            wind_forecast=self.sim.wind_forecast,
            wind_resolution=self.sim._wind_res,
            wind_t_step=self.sim.weather_t_step,
            wind_xpad=self.sim.wind_xpad,
            wind_ypad=self.sim.wind_ypad,
            temp_forecast=np.array([entry.temp for entry in self.sim._weather_stream.stream[self.sim.sim_start_w_idx:]]),
            rh_forecast=np.array([entry.rel_humidity for entry in self.sim._weather_stream.stream[self.sim.sim_start_w_idx:]]),
            forecast_t_step=self.sim.weather_t_step,
            elevation=self.sim.coarse_elevation,
            roads=self.sim.roads,
            fire_breaks=self.sim.fire_breaks,
            init_entries=self.get_init_entries(),
            show_weather_data=True
        )

        return params

__init__(sim)

Initialize the real-time visualizer for a simulation.

Parameters:

Name Type Description Default
sim FireSim

The fire simulation instance to visualize.

required
Side Effects
  • Initializes the base visualizer with simulation parameters.
  • Creates an empty cell cache for buffering updates.
Source code in embrs/fire_simulator/visualizer.py
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
def __init__(self, sim: FireSim):
    """Initialize the real-time visualizer for a simulation.

    Args:
        sim (FireSim): The fire simulation instance to visualize.

    Side Effects:
        - Initializes the base visualizer with simulation parameters.
        - Creates an empty cell cache for buffering updates.
    """
    self.sim = sim
    self.cell_cache = []

    input_params = self.get_input_params()

    super().__init__(input_params)

cache_changes(updated_cells)

Add cell updates to the visualization cache.

Buffers cell state changes for batch rendering on the next update call.

Parameters:

Name Type Description Default
updated_cells list

List of CellLogEntry objects to cache.

required
Side Effects
  • Extends self.cell_cache with new entries.
Source code in embrs/fire_simulator/visualizer.py
67
68
69
70
71
72
73
74
75
76
77
78
def cache_changes(self, updated_cells: list):
    """Add cell updates to the visualization cache.

    Buffers cell state changes for batch rendering on the next update call.

    Args:
        updated_cells (list): List of CellLogEntry objects to cache.

    Side Effects:
        - Extends self.cell_cache with new entries.
    """
    self.cell_cache.extend(updated_cells)

get_init_entries()

Generate initial log entries for all cells in the simulation.

Returns:

Name Type Description
list list

List of CellLogEntry objects representing the initial state of every cell in the simulation grid.

Source code in embrs/fire_simulator/visualizer.py
57
58
59
60
61
62
63
64
65
def get_init_entries(self) -> list:
    """Generate initial log entries for all cells in the simulation.

    Returns:
        list: List of CellLogEntry objects representing the initial state
            of every cell in the simulation grid.
    """
    entries = [cell.to_log_entry(self.sim._curr_time_s) for cell in self.sim.cell_dict.values()]
    return entries

get_input_params()

Build visualization parameters from the current simulation state.

Extracts grid geometry, weather forecasts, terrain data, and initial cell states from the simulation to configure the base visualizer.

Returns:

Name Type Description
VisualizerInputs VisualizerInputs

Configuration dataclass for BaseVisualizer.

Source code in embrs/fire_simulator/visualizer.py
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
def get_input_params(self) -> VisualizerInputs:
    """Build visualization parameters from the current simulation state.

    Extracts grid geometry, weather forecasts, terrain data, and initial
    cell states from the simulation to configure the base visualizer.

    Returns:
        VisualizerInputs: Configuration dataclass for BaseVisualizer.
    """
    params = VisualizerInputs(
        cell_size=self.sim.cell_size,
        sim_shape=self.sim.shape,
        sim_size=self.sim.size,
        start_datetime=self.sim._start_datetime,
        north_dir_deg=self.sim._north_dir_deg,
        wind_forecast=self.sim.wind_forecast,
        wind_resolution=self.sim._wind_res,
        wind_t_step=self.sim.weather_t_step,
        wind_xpad=self.sim.wind_xpad,
        wind_ypad=self.sim.wind_ypad,
        temp_forecast=np.array([entry.temp for entry in self.sim._weather_stream.stream[self.sim.sim_start_w_idx:]]),
        rh_forecast=np.array([entry.rel_humidity for entry in self.sim._weather_stream.stream[self.sim.sim_start_w_idx:]]),
        forecast_t_step=self.sim.weather_t_step,
        elevation=self.sim.coarse_elevation,
        roads=self.sim.roads,
        fire_breaks=self.sim.fire_breaks,
        init_entries=self.get_init_entries(),
        show_weather_data=True
    )

    return params

set_sim(sim)

Update the simulation reference for this visualizer.

Parameters:

Name Type Description Default
sim FireSim

New simulation instance to visualize.

required
Source code in embrs/fire_simulator/visualizer.py
49
50
51
52
53
54
55
def set_sim(self, sim: FireSim):
    """Update the simulation reference for this visualizer.

    Args:
        sim (FireSim): New simulation instance to visualize.
    """
    self.sim = sim

update()

Render all cached updates to the visualization.

Collects agent positions and action entries from the simulation, then renders all cached cell changes along with current agent and action state.

Side Effects
  • Calls update_grid with cached cell data.
  • Clears the cell cache after rendering.
Source code in embrs/fire_simulator/visualizer.py
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def update(self):
    """Render all cached updates to the visualization.

    Collects agent positions and action entries from the simulation,
    then renders all cached cell changes along with current agent and
    action state.

    Side Effects:
        - Calls update_grid with cached cell data.
        - Clears the cell cache after rendering.
    """
    agents = [agent.to_log_entry(self.sim._curr_time_s) for agent in self.sim.agent_list]
    actions = self.sim.get_action_entries(logger=False)
    self.update_grid(self.sim._curr_time_s, self.cell_cache, agents, actions)

    self.cell_cache = []