"""
Fire definition module for CFAST simulations.
This module provides the Fires class for defining fire sources in a
compartment. Fires are characterized by their heat release rate over time,
chemical composition, and physical properties.
"""
from __future__ import annotations
from typing import Any
import numpy as np
import pandas as pd
from .utils.namelist import NamelistRecord
from .utils.theme import build_card
[docs]
class Fires:
"""
Represents a fire source in a CFAST simulation.
A fire in CFAST is specified via a time-dependent heat release rate (HRR). The
specified heat of combustion is used to calculate the mass loss rate of fuel,
from which the production rate of combustion products can be calculated using
specified product yields. The heat release and the corresponding product generation
rates go to zero when the lower oxygen limit is reached, and are replaced by the
appropriate production rate of unburned fuel gas which is transported from zone
to zone until there is sufficient oxygen and a high enough temperature to support
combustion. The model can simulate multiple fires in one or more compartments. These
fires are treated as totally separate entities, with no interaction of the plumes.
These fires can be ignited at a prescribed time, or when a corresponding target reaches a
specified temperature or heat flux. Fires in CFAST are defined in two parts: a
"Fire Definition" that specifies the fuel composition, heat release rate, and
species yields for the fire, and a "Fire Instance" that specifies the placement of
a defined fire within a compartment in the simulation. A single fire definition may
be associated with more than one fire instance in a simulation if desired. The
combustion model is defined by a one-step reaction where burning fuels in CFAST
are assumed to be hydrocarbon fuels that contain at least carbon and hydrogen and
optionally oxygen, nitrogen, and chlorine. All of the specified nitrogen and chlorine
is assumed to completely react to form HCN and HCl. Fire properties are linearly
interpolated between specified time points. If the simulation time is longer than
the total duration of the fire, the final values specified for the fire are
continued until the end of the simulation.
Parameters
----------
id : str
The selected name must be unique (i.e., not the same as another fire instance
in the same simulation).
comp_id : str
Name of the compartment where the fire occurs.
fire_id : str
The selected name must be unique (i.e., not the same as another fire definition
in the same simulation). IDs for fire definitions can be the same as ones for
fire instances.
location : list[float]
Position of the center of the base of the fire relative to the front left corner
of the compartment. Format: [x, y]. Default units: m, default value: compartment center.
ignition_criterion : str, optional
The time of ignition can be controlled by a user-specified time, or by a
user-specified target's surface temperature or incident heat flux.
Options: "TIME", "TEMPERATURE", "FLUX".
set_point : float, optional
The critical value at which ignition will occur. If it is less than or equal
to zero, the default value of zero is taken. Can be temperature (°C) or flux
(kW/m²) depending on criterion.
device_id : str, optional
User-specified target used to calculate surface temperature or incident heat
flux to ignite fire. Target is typically placed at the base of the fire to be ignited.
carbon : float
The number of carbon atoms in the fuel molecule. Burning fuels in CFAST are
assumed to be hydrocarbon fuels that contain at least carbon and hydrogen.
Default value: 1.
chlorine : float
The number of chlorine atoms in the fuel molecule. All of the specified chlorine
is assumed to completely react to form HCl. Default value: 0.
hydrogen : float
The number of hydrogen atoms in the fuel molecule. Burning fuels in CFAST are
assumed to be hydrocarbon fuels that contain at least carbon and hydrogen.
Default value: 4.
nitrogen : float
The number of nitrogen atoms in the fuel molecule. All of the specified nitrogen
is assumed to completely react to form HCN. Default value: 0.
oxygen : float
The number of oxygen atoms in the fuel molecule. Default value: 0.
heat_of_combustion : float
The energy released per unit mass of fuel consumed. Default units: kJ/kg,
default value: 50000 kJ/kg.
radiative_fraction : float
The fraction of the combustion energy that is emitted in the form of thermal
radiation. Default units: none, default value: 0.35.
data_table : list[list[float]], np.ndarray, or pd.DataFrame
Time-dependent fire properties with columns for TIME, HRR, HEIGHT, AREA,
CO_YIELD, SOOT_YIELD, HCN_YIELD, HCL_YIELD, TRACE_YIELD. Properties are linearly
interpolated between specified points. Each row must contain exactly 9 values
corresponding to the LABELS columns.
Examples
--------
Create a simple growing fire:
>>> fire_data = [
... [0, 0, 0.5, 0.1, 0.01, 0.01, 0, 0, 0], # t=0s, no heat release
... [60, 100, 0.5, 0.5, 0.01, 0.01, 0, 0, 0], # t=60s, 100 kW
... [300, 500, 1.0, 1.0, 0.01, 0.01, 0, 0, 0] # t=300s, 500 kW peak
... ]
>>> fire = Fires(
... id="FIRE1",
... comp_id="ROOM1",
... fire_id="POLYURETHANE",
... location=[2.0, 2.0], # Center of 4x4m room
... carbon=27, hydrogen=36, oxygen=2, nitrogen=2, chlorine=0,
... heat_of_combustion=23600, # kJ/kg for polyurethane
... radiative_fraction=0.35, # 35% radiant fraction
... data_table=fire_data
... )
Create a fire with target-based ignition:
>>> steady_fire = Fires(
... id="IGNITED_FIRE",
... comp_id="BEDROOM",
... fire_id="WOOD",
... location=[1.5, 2.0], # Near bed location
... ignition_criterion="TEMPERATURE",
... set_point=200, # Ignite at 200°C
... device_id="BED_TARGET", # Temperature measured at bed
... carbon=1, hydrogen=4, oxygen=0, nitrogen=0, chlorine=0,
... heat_of_combustion=50000, # Default combustion heat
... radiative_fraction=0.35, # Default radiant fraction
... data_table=[[0, 1000, 0, 1.0, 0.01, 0.01, 0, 0, 0]] # Steady 1 MW
... )
"""
LABELS = [
"TIME",
"HRR",
"HEIGHT",
"AREA",
"CO_YIELD",
"SOOT_YIELD",
"HCN_YIELD",
"HCL_YIELD",
"TRACE_YIELD",
]
def __init__(
self,
id: str,
comp_id: str,
fire_id: str,
location: list[float],
ignition_criterion: str | None = None,
set_point: float | None = None,
device_id: str | None = None,
carbon: float = 1,
chlorine: float = 0,
hydrogen: float = 4,
nitrogen: float = 0,
oxygen: float = 0,
heat_of_combustion: float = 50000,
radiative_fraction: float = 0.35,
data_table: list[list[float]] | np.ndarray | pd.DataFrame | None = None,
):
self.id = id
self.comp_id = comp_id
self.fire_id = fire_id
self.location = location
self.ignition_criterion = ignition_criterion
self.set_point = set_point
self.device_id = device_id
self.carbon = carbon
self.chlorine = chlorine
self.hydrogen = hydrogen
self.nitrogen = nitrogen
self.oxygen = oxygen
self.heat_of_combustion = heat_of_combustion
self.radiative_fraction = radiative_fraction
self.data_table = self._process_data_table(data_table)
self._validate()
def _validate(self) -> None:
"""Validate the current state of the fire attributes.
Raises
------
ValueError
If any attribute violates the constraints.
"""
if len(self.location) != 2:
raise ValueError("Location must be a list of two floats [x, y].")
def __repr__(self) -> str:
"""Return a detailed string representation of the Fires."""
location_str = f"[{', '.join(map(str, self.location))}]"
data_rows = len(self.data_table) if self.data_table else 0
return (
f"Fires("
f"id='{self.id}', comp_id='{self.comp_id}', fire_id='{self.fire_id}', "
f"location={location_str}, "
f"heat_of_combustion={self.heat_of_combustion}, "
f"radiative_fraction={self.radiative_fraction}, "
f"data_rows={data_rows}"
f")"
)
def __str__(self) -> str:
"""Return a user-friendly string representation of the Fires."""
location_str = f"({self.location[0]}, {self.location[1]})"
peak_hrr: float = 0
duration: float = 0
if self.data_table:
hrr_values = [
row[1] for row in self.data_table if len(row) > 1
] # HRR is column 1
time_values = [
row[0] for row in self.data_table if len(row) > 0
] # TIME is column 0
peak_hrr = max(hrr_values) if hrr_values else 0
duration = max(time_values) if time_values else 0
fire_info = f"Fire '{self.id}' ({self.fire_id})"
details = []
if peak_hrr > 0:
if peak_hrr >= 1000000: # >= 1 MW
details.append(f"peak: {peak_hrr / 1000000:.1f} MW")
elif peak_hrr >= 1000: # >= 1 kW
details.append(f"peak: {peak_hrr / 1000:.0f} kW")
else:
details.append(f"peak: {peak_hrr:.0f} W")
if duration > 0:
if duration >= 3600: # >= 1 hour
details.append(f"duration: {duration / 3600:.1f}h")
elif duration >= 60: # >= 1 minute
details.append(f"duration: {duration / 60:.0f}min")
else:
details.append(f"duration: {duration:.0f}s")
details.append(f"χr: {self.radiative_fraction}")
details_str = f" ({', '.join(details)})" if details else ""
return f"{fire_info} in '{self.comp_id}' at {location_str}{details_str}"
def _repr_html_(self) -> str:
"""Return an HTML representation for Jupyter/interactive environments."""
location_str = f"({self.location[0]}, {self.location[1]})"
# Calculate peak HRR and duration for display
peak_hrr = 0.0
duration = 0.0
data_points = 0
if self.data_table:
hrr_values = [row[1] for row in self.data_table if len(row) > 1]
time_values = [row[0] for row in self.data_table if len(row) > 0]
peak_hrr = max(hrr_values) if hrr_values else 0
duration = max(time_values) if time_values else 0
data_points = len(self.data_table)
# Format HRR display
if peak_hrr >= 1000000: # >= 1 MW
hrr_display = f"{peak_hrr / 1000000:.1f} MW"
elif peak_hrr >= 1000: # >= 1 kW
hrr_display = f"{peak_hrr / 1000:.0f} kW"
else:
hrr_display = f"{peak_hrr:.0f} W"
# Format duration display
if duration >= 3600: # >= 1 hour
duration_display = f"{duration / 3600:.1f}h"
elif duration >= 60: # >= 1 minute
duration_display = f"{duration / 60:.0f}min"
else:
duration_display = f"{duration:.0f}s"
body_html = f"""
<div class="pycfast-card-grid">
<div><strong>Peak HRR:</strong> {hrr_display}</div>
<div><strong>Duration:</strong> {duration_display}</div>
<div><strong>Location:</strong> {location_str}</div>
<div><strong>χᵣ:</strong> {self.radiative_fraction}</div>
<div><strong>ΔHc:</strong> {self.heat_of_combustion} kJ/kg</div>
<div><strong>Data points:</strong> {data_points}</div>
</div>
<details class="pycfast-inline-detail">
<summary>Chemical Composition</summary>
<div class="pycfast-detail-content">
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); gap: 5px;">
<div>C: {getattr(self, "carbon", 0)}</div>
<div>H: {getattr(self, "hydrogen", 0)}</div>
<div>O: {getattr(self, "oxygen", 0)}</div>
<div>N: {getattr(self, "nitrogen", 0)}</div>
<div>Cl: {getattr(self, "chlorine", 0)}</div>
</div>
</div>
</details>
"""
return build_card(
icon="🔥",
gradient="linear-gradient(135deg, #ff4757, #ff3838)",
title=f"Fire: {self.id}",
subtitle=f"<strong>{self.fire_id}</strong> in compartment <strong>{self.comp_id}</strong>",
accent_color="#ff4757",
body_html=body_html,
)
def __getitem__(self, key: str) -> Any:
"""Get fire property by name for dictionary-like access."""
if not hasattr(self, key):
raise KeyError(f"Property '{key}' not found in Fires.")
return getattr(self, key)
def __setitem__(self, key: str, value: Any) -> None:
"""Set fire property by name for dictionary-like assignment.
Validates the object state after setting the attribute to ensure
all constraints are still satisfied.
Raises
------
KeyError
If the property does not exist.
ValueError
If setting this value would violate object constraints.
"""
if not hasattr(self, key):
raise KeyError(f"Cannot set '{key}'. Property does not exist in Fires.")
old_value = getattr(self, key)
setattr(self, key, value)
try:
self._validate()
except Exception:
setattr(self, key, old_value)
raise
[docs]
def to_dataframe(self) -> pd.DataFrame:
"""
Convert fire data table to pandas DataFrame with proper column labels.
Returns
-------
pd.DataFrame
DataFrame with columns matching LABELS for easy analysis and plotting.
Examples
--------
>>> fire = Fires(id="FIRE1", comp_id="ROOM1", fire_id="WOOD",
... location=[1.0, 1.0],
... data_table=[[0, 1000, 0.5, 1.0, 0.01, 0.01, 0, 0, 0]])
>>> df = fire.to_dataframe()
>>> df['HRR'].max() # Find peak heat release rate
1000.0
"""
return pd.DataFrame(self.data_table, columns=self.LABELS)
def _process_data_table(
self, data_table: list[list[float]] | np.ndarray | pd.DataFrame | None
) -> list[list[float]]:
"""
Process and validate data_table input from various formats.
Parameters
----------
data_table : list[list[float]], np.ndarray, pd.DataFrame, or None
Fire data in various formats. Must have shape (n_rows, 9) where columns
correspond to LABELS: ["TIME", "HRR", "HEIGHT", "AREA", "CO_YIELD",
"SOOT_YIELD", "HCN_YIELD", "HCL_YIELD", "TRACE_YIELD"].
Returns
-------
list[list[float]]
Standardized data table as list of lists.
"""
if data_table is None:
return [[0, 0, 0, 0, 0, 0, 0, 0, 0]]
if isinstance(data_table, pd.DataFrame):
if list(data_table.columns) == self.LABELS:
array_data = data_table.values
elif data_table.shape[1] == len(self.LABELS):
array_data = data_table.values
else:
raise ValueError(
f"DataFrame must have {len(self.LABELS)} columns. "
f"Got {data_table.shape[1]} columns. "
f"Expected columns: {self.LABELS}"
)
elif isinstance(data_table, np.ndarray):
if data_table.ndim != 2:
raise ValueError("NumPy array must be 2-dimensional.")
if data_table.shape[1] != len(self.LABELS):
raise ValueError(
f"NumPy array must have exactly {len(self.LABELS)} columns."
)
array_data = data_table
elif isinstance(data_table, list):
if not data_table or len(data_table) < 1:
raise ValueError("data_table must contain at least one row.")
array_data = np.array(data_table)
else:
raise TypeError(
"data_table must be a list of lists, NumPy array, pandas DataFrame, or None."
)
if array_data.shape[1] != len(self.LABELS):
raise ValueError(
f"data_table must have exactly {len(self.LABELS)} columns. "
f"Got {array_data.shape[1]} columns. "
f"Expected columns: {self.LABELS}"
)
try:
array_data = array_data.astype(float)
except (ValueError, TypeError) as e:
raise ValueError("All values in data_table must be numeric.") from e
# Type cast to satisfy mypy - we know this is list[list[float]] due to validation above
result: list[list[float]] = array_data.tolist()
return result