R-CMD-check CRAN GitHubLicenseDownloadsBuild Status rFDS status badge

rFDS: An R Package for Fire Dynamics Simulation from LiDAR Data

Authors: Carlos Alberto Silva and Nadeem Fareed

rFDS provides an interface between LiDAR-based vegetation structure and the Fire Dynamics Simulator (FDS), enabling:


1. Getting Started

1.1 Installation of the rFDS package

# CRAN release
install.packages("rFDS")

# Dev (r-universe; recommended for the latest)
install.packages(
  "rFDS",
  repos = c("https://carlos-alberto-silva.r-universe.dev/rFDS", "https://cloud.r-project.org")
)

library(rFDS)

1.2 Install Fire Dynamics Simulator (FDS)

You can install FDS manually from the official FireModels project or automatically via rFDS.


a) Manual install (official FireModels)

i) Download

ii) Windows

iii) Linux

iv) macOS

If the commands aren’t found after installation, ensure the folder containing the executables (e.g., fds, fds_local, smokeview) is on your system PATH, or pass an explicit path to rFDS::run_fds() (see below).


b) One-line install via rFDS

rFDS includes a helper that fetches the official FDS distribution appropriate for your OS (when available) and registers it so you can call it from R:

# Installs/updates FDS from the official FireModels distribution
# Windows (no sudo; installer .bat is used)
install_fds(version = "6.10.1",
            dest_dir = "C:/FDS",    # choose a persistent folder
            sudo = FALSE,
            quiet_download = FALSE)

# Linux/macOS, user-local install (no sudo, writable dest_dir):
install_fds(version = "6.10.1",
            dest_dir = "~/opt/fds",
            sudo = FALSE,
            quiet_download = TRUE)

# Linux/macOS, system-wide (requires sudo):
install_fds(version = "6.10.1",
            dest_dir = "/opt/fds",
            sudo = TRUE,
            quiet_download = TRUE)

1.3 Loading additional required packages

if (!requireNamespace(c("lidR","rgl"), quietly = TRUE)) install.packages(c("lidR","rgl"))
library(lidR)
library(rgl)

2. Examples

2.1 Example 1 — Longleaf pine plot (single LAS, two ignition strips, devices)


# 2.1.1) Load LAS from the package extdata
las_path <- system.file("extdata", "acf_longleaf_pine.laz", package = "rFDS")
stopifnot(nzchar(las_path), file.exists(las_path))
las <- readLAS(las_path)

# Quick bounds
summary(las@data)
# 2.1.2) Voxelization
vox <- laz_to_voxels(
  las,
  grid_res  = 0.5,
  class_col = "Classification",
  local     = FALSE
)
# 2.1.3) Quick visualization
plot_voxels(vox = vox, style = "points", color_by = "class", to_local = FALSE)
# 2.1.4) Domain + meshes + open boundaries
XB <- domain_from_lidar(vox, pad_xyz = c(0.5,0.5,0.5), centers = TRUE)
tiles <- tile_meshes_from_extent(XB, cell_size = c(0.5,0.5,0.5), max_ijk_per_mesh = 120)
mesh_lines <- meshes_to_fds(tiles)
vent_lines <- open_boundary_vents(sides = c("XMIN","XMAX","YMIN","YMAX","ZMAX"), surf_id = "OPEN")
# 2.1.5) Chemistry & materials
specs_reac <- c(
  spec_fds(ID = "CELLULOSE", FORMULA = "C6H10O5"),
  spec_fds(ID = "WATER VAPOR"),
  reac_fds(FUEL = "CELLULOSE", C = 2.10, H = 6.20, O = 2.16,
           SOOT_YIELD = 0.01, HEAT_OF_COMBUSTION = 17425., IDEAL = TRUE),

  matl_fds("GENERIC VEGETATION", DENSITY = 500., CONDUCTIVITY = 0.2,
           SPECIFIC_HEAT_RAMP = "c_v", A = 1040., E = 61041.,
           NU_SPEC = 0.75, SPEC_ID = "CELLULOSE", NU_MATL = 0.25,
           MATL_ID = "CHAR", HEAT_OF_REACTION = 418.),

  matl_fds("CHAR", DENSITY = 300., CONDUCTIVITY = 0.052,
           SPECIFIC_HEAT_RAMP = "c_v", SURFACE_OXIDATION_MODEL = TRUE,
           A = 465., E = 68000., SPEC_ID = c("PRODUCTS","AIR"),
           NU_SPEC = c(8.13, -7.17), MATL_ID = "ASH", NU_MATL = 0.04,
           HEAT_OF_REACTION = -25000.),

  matl_fds("ASH", DENSITY = 67., CONDUCTIVITY = 0.1, SPECIFIC_HEAT_RAMP = "c_v"),

  ramp_fds("c_v", T = 0.,   F = 1.1),
  ramp_fds("c_v", T = 200., F = 2.0),
  ramp_fds("c_v", T = 800., F = 2.0)
)

# 2.1.6) Vegetation SURF/PART/INIT helpers
class_to_bdf <- function(i) file.path("bdf", sprintf("bulk_density_class_%s.bdf", i))

surf_grass <- surf_fds("grass surface",
  `MATL_ID(1,1)` = "GENERIC VEGETATION",
  `MATL_MASS_FRACTION(1,1)` = 1.0,
  MOISTURE_FRACTION = 0.15, SURFACE_VOLUME_RATIO = 9770.,
  LENGTH = 0.10, GEOMETRY = "CYLINDRICAL"
)
part_grass <- part_fds("grass", DRAG_COEFFICIENT = 2.8, SAMPLING_FACTOR = 1,
  SURF_ID = "grass surface",
  QUANTITIES = c("PARTICLE TEMPERATURE","PARTICLE BULK DENSITY"),
  STATIC = TRUE, COLOR = "CHARTREUSE",
  EMBER_PARTICLE = TRUE, EMBER_DENSITY_THRESHOLD = 70,
  EMBER_VELOCITY_THRESHOLD = 0., TRACK_EMBERS = FALSE
)
init_grass <- init_fds(PART_ID = "grass", BULK_DENSITY_FACTOR = 2,
  BULK_DENSITY_FILE = class_to_bdf(0)
)

surf_leaf <- surf_fds("leaf surface",
  `MATL_ID(1,1)` = "GENERIC VEGETATION",
  `MATL_MASS_FRACTION(1,1)` = 1.0,
  MOISTURE_FRACTION = 1.0, SURFACE_VOLUME_RATIO = 3588.,
  LENGTH = 0.10, GEOMETRY = "CYLINDRICAL"
)
part_leaf <- part_fds("leaf", DRAG_COEFFICIENT = 2.8, SAMPLING_FACTOR = 1,
  SURF_ID = "leaf surface",
  QUANTITIES = c("PARTICLE TEMPERATURE","PARTICLE BULK DENSITY"),
  STATIC = TRUE, COLOR = "FOREST GREEN"
)
init_leaf <- init_fds(PART_ID = "leaf", CELL_CENTERED = FALSE,
  BULK_DENSITY_FILE = class_to_bdf(1)
)

surf_wood <- surf_fds("wood surface",
  `MATL_ID(1,1)` = "GENERIC VEGETATION",
  `MATL_MASS_FRACTION(1,1)` = 1.0,
  MOISTURE_FRACTION = 1.0, SURFACE_VOLUME_RATIO = 13.,
  LENGTH = 0.10, GEOMETRY = "CYLINDRICAL"
)
part_wood <- part_fds("wood", DRAG_COEFFICIENT = 2.8, SAMPLING_FACTOR = 1,
  SURF_ID = "wood surface",
  QUANTITIES = c("PARTICLE TEMPERATURE","PARTICLE BULK DENSITY"),
  STATIC = TRUE, COLOR = "BURLY WOOD 4"
)
init_wood <- init_fds(PART_ID = "wood", CELL_CENTERED = FALSE,
  BULK_DENSITY_FILE = class_to_bdf(2)
)
# 2.1.7) Ignition (two thin OBST strips near ZMIN) + devices at strip centers
surf_fire <- surf_fds("IGN FIRE", HRRPUA = 800, COLOR = "RED", RAMP_Q = "ign_ramp")
r_ign <- list(
  ramp_fds("ign_ramp", T = 0.0,  F = 0.0),
  ramp_fds("ign_ramp", T = 1.0,  F = 1.0),
  ramp_fds("ign_ramp", T = 120., F = 0.0)
)
yc  <- min(XB[3:4]); dy <- 0.5; zmin <- XB[5]
dz  <- min(0.01, (XB[6]-XB[5])*0.01)

obst_ign_1 <- obst_fds(c(XB[1], XB[2], yc - dy/2, yc + dy/2, zmin, zmin + dz), SURF_ID = "IGN FIRE")
obst_ign_2 <- obst_fds(c(XB[1], XB[2], yc + 30 - dy/2, yc + 30 + dy/2, zmin, zmin + dz), SURF_ID = "IGN FIRE")
obst_ign   <- c(obst_ign_1, obst_ign_2)

# Parse &OBST to centers for devices
get_center_from_obst <- function(obst_line) {
  m <- regexpr("XB\\s*=\\s*([0-9eE+\\-\\.,\\s]+)", obst_line, perl = TRUE)
  xb <- regmatches(obst_line, m)
  xb <- sub(".*XB\\s*=\\s*", "", xb)
  xb <- sub(",?\\s*(SURF_ID|/).*", "", xb, ignore.case = TRUE)
  v  <- as.numeric(strsplit(gsub("\\s+", "", xb), ",", fixed = TRUE)[[1]])
  c(mean(v[1:2]), mean(v[3:4]), mean(v[5:6]))
}
centers <- rbind(get_center_from_obst(obst_ign_1), get_center_from_obst(obst_ign_2))

dev_temp  <- devc_fds(centers[1, , drop = FALSE], ID = "TC-01", QUANTITY = "TEMPERATURE")
dev_flux  <- devc_fds(centers[2, , drop = FALSE], ID = "HF-01",
                      QUANTITY = "RADIATIVE HEAT FLUX GAS", IOR = 1)
dev_lines <- c(dev_temp, dev_flux)

# Visual check with ignition lines + devices
plot_voxels(
  vox_df        = vox,
  style         = "points",
  color_by      = "class",
  to_local      = FALSE,
  ignition      = obst_ign,       # character vector of &OBST lines
  devices       = dev_lines,      # character vector of &DEVC lines
  device_color  = "black",
  device_radius = 0.3,
  label_devices = TRUE
)
# 2.1.8) Outputs
outputs <- list(
  dump_fds(DT_HRR = 1., DT_DEVC = 1.),
  slcf_fds(PBZ = (XB[5] + XB[6]) / 2, QUANTITY = "TEMPERATURE"),
  slcf_fds(PBZ = (XB[5] + XB[6]) / 2, QUANTITY = "HRRPUV"),
  bndf_fds("RADIATIVE HEAT FLUX"),
  bndf_fds("WALL TEMPERATURE")
)
# 2.1.9) Assemble .fds lines
fds_lines <- build_fds(
  head_fds(CHID = "simulation_lidar", TITLE = "LiDAR-driven domain"),
  misc_fds_literal(TMPA = 300., HUMIDITY = 10.50, VERBOSE = "TRUE"),
  radi_fds(RADIATION_ITERATIONS = 2),
  wind_fds(SPEED = 1.5, DIRECTION = 180.0),
  time_fds(T_END = 2),

  specs_reac,
  mesh_lines,
  vent_lines,

  surf_fire, r_ign, obst_ign,

  surf_grass, part_grass, init_grass,
  surf_leaf,  part_leaf,  init_leaf,
  surf_wood,  part_wood,  init_wood,

  dev_lines,
  outputs
)
# 2.1.10) Stage run folder + write BDFs/.fds
run_dir <- file.path(tempdir(), "fds_demo")
dir.create(file.path(run_dir, "bdf"), recursive = TRUE, showWarnings = FALSE)

voxel_to_bdf(
  vox_df        = vox,
  bdf_base_path = file.path(run_dir, "bdf", "bulk_density.bdf"),
  class_col     = "class_id",
  value_col     = "bulk_density",
  include_na    = FALSE,
  normalize_xyz = FALSE
)


fds_path <- file.path(run_dir, "simulation_lidar.fds")

# Now resolve to long path with backslashes (or use winslash = "/")
fds_path <- normalizePath(fds_path, winslash = "\\", mustWork = TRUE)
run_dir  <- normalizePath(run_dir,  winslash = "\\", mustWork = TRUE)
# 2.1.11) Run FDS
run_fds(fds_path)

# 2.1.12) Run smokeview
open_smv(file.path(run_dir, "simulation_lidar.smv"))

Demo

2.2 Example 2 — Conifer forest - Western US (four LAS, single ignition)


# 2.2.1) Read LAS files from package extdata
grass_path <- system.file("extdata", "grass_crop.laz", package = "rFDS")
leaf_path  <- system.file("extdata", "leaf_crop.laz",  package = "rFDS")
shrub_path <- system.file("extdata", "shrub_crop.laz", package = "rFDS")
wood_path  <- system.file("extdata", "wood_crop.laz",  package = "rFDS")
stopifnot(all(file.exists(c(grass_path, leaf_path, shrub_path, wood_path))))
# 2.2.2) Load LAS and assign classes
grass_crop <- readLAS(grass_path); grass_crop@data$Classification <- 1L
leaf_crop  <- readLAS(leaf_path);  leaf_crop@data$Classification  <- 2L
shrub_crop <- readLAS(shrub_path); shrub_crop@data$Classification <- 3L
wood_crop  <- readLAS(wood_path);  wood_crop@data$Classification  <- 4L
# 2.2.3) Combine into one LAS and sanity-check bounds
las2 <- rbind(grass_crop, leaf_crop, shrub_crop, wood_crop)
range(las2@data$Z); range(las2@data$X); range(las2@data$Y)
# 2.2.4) Voxelization
vox2 <- laz_to_voxels(las2, grid_res = 0.5, class_col = "Classification", local = FALSE)
# 2.2.5) Quick visualization
plot_voxels(vox = vox2, style = "points", color_by = "class", to_local = FALSE)
# 2.2.6) Domain, meshes, and open boundaries
XB2    <- domain_from_lidar(vox2, pad_xyz = c(0.5,0.5,0.5), centers = TRUE)
tiles2 <- tile_meshes_from_extent(XB2, cell_size = c(0.5,0.5,0.5), max_ijk_per_mesh = 120)
mesh2  <- meshes_to_fds(tiles2)
vents2 <- open_boundary_vents(sides = c("XMIN","XMAX","YMIN","YMAX","ZMAX"), surf_id = "OPEN")
# 2.2.7) (Chemistry/Materials) Reuse `specs_reac` from Example 1
# 2.2.8) SURF/PART/INIT for 4 classes (1=grass, 2=leaf, 3=shrub, 4=wood)
init_grass2 <- init_fds(PART_ID = "grass", BULK_DENSITY_FACTOR = 2, BULK_DENSITY_FILE = class_to_bdf(1))
init_leaf2  <- init_fds(PART_ID = "leaf",  CELL_CENTERED = FALSE, BULK_DENSITY_FILE = class_to_bdf(2))

surf_shrub  <- surf_fds("shrub surface",
                        `MATL_ID(1,1)` = "GENERIC VEGETATION",
                        `MATL_MASS_FRACTION(1,1)` = 1.0,
                        MOISTURE_FRACTION = 0.15,
                        SURFACE_VOLUME_RATIO = 8000.,
                        LENGTH = 0.10,
                        GEOMETRY = "CYLINDRICAL")
part_shrub  <- part_fds("shrub",
                        DRAG_COEFFICIENT = 2.8,
                        SAMPLING_FACTOR = 1,
                        SURF_ID = "shrub surface",
                        QUANTITIES = c("PARTICLE TEMPERATURE","PARTICLE BULK DENSITY"),
                        STATIC = TRUE,
                        COLOR = "DARK GREEN",
                        EMBER_PARTICLE = TRUE,
                        EMBER_DENSITY_THRESHOLD = 70,
                        EMBER_VELOCITY_THRESHOLD = 0.,
                        TRACK_EMBERS = FALSE)
init_shrub2 <- init_fds(PART_ID = "shrub", BULK_DENSITY_FACTOR = 2, BULK_DENSITY_FILE = class_to_bdf(3))

init_wood2  <- init_fds(PART_ID = "wood",  CELL_CENTERED = FALSE, BULK_DENSITY_FILE = class_to_bdf(4))
# 2.2.9) Ignition
surf_fire2 <- surf_fds("IGN FIRE", HRRPUA = 800, COLOR = "RED", RAMP_Q = "ign_ramp")
r_ign2 <- list(
  ramp_fds("ign_ramp", T = 0.0,  F = 0.0),
  ramp_fds("ign_ramp", T = 1.0,  F = 1.0),
  ramp_fds("ign_ramp", T = 120., F = 0.0)
)
yc2 <- mean(XB2[3:4]); dy2 <- 0.5; zmin2 <- XB2[5]
dz2 <- min(0.01, (XB2[6]-XB2[5]) * 0.01)
obst_ign2 <- obst_fds(c(XB2[1], XB2[2], yc2 - dy2/2, yc2 + dy2/2, zmin2, zmin2 + dz2),
                      SURF_ID = "IGN FIRE")
# 2.2.10) Outputs
outputs2 <- list(
  dump_fds(DT_HRR = 1., DT_DEVC = 1.),
  slcf_fds(PBZ = (XB2[5] + XB2[6]) / 2, QUANTITY = "TEMPERATURE"),
  slcf_fds(PBZ = (XB2[5] + XB2[6]) / 2, QUANTITY = "HRRPUV"),
  bndf_fds("RADIATIVE HEAT FLUX"),
  bndf_fds("WALL TEMPERATURE")
)
# 2.2.11) Assemble the .fds for example 2
fds2 <- build_fds(
  head_fds(CHID = "simulation_lidar", TITLE = "LiDAR-driven domain"),
  misc_fds_literal(TMPA = 300., HUMIDITY = 10.50, VERBOSE = "TRUE"),
  radi_fds(RADIATION_ITERATIONS = 2),
  wind_fds(SPEED = 1.5, DIRECTION = 180.0),
  time_fds(T_END = 20),

  specs_reac,  # from Example 1
  mesh2,
  vents2,

  surf_fire2, r_ign2, obst_ign2,

  # reuse grass/leaf/wood SURF/PART from example 1 objects:
  surf_grass, part_grass, init_grass2,
  surf_leaf,  part_leaf,  init_leaf2,
  surf_shrub, part_shrub, init_shrub2,
  surf_wood,  part_wood,  init_wood2,

  outputs2
)
# 2.2.12) Stage run folder and write BDFs
run_dir2 <- file.path(tempdir(), "fds_demo2")
dir.create(file.path(run_dir2, "bdf"), recursive = TRUE, showWarnings = FALSE)

voxel_to_bdf(
  vox_df        = vox2,
  bdf_base_path = file.path(run_dir2, "bdf", "bulk_density.bdf"),
  class_col     = "class_id",
  value_col     = "bulk_density",
  include_na    = FALSE,
  normalize_xyz = FALSE
)
# 2.2.13) Write .fds and run
fds_path2 <- file.path(run_dir2, "simulation_lidar.fds")
write_fds(fds_path2, fds2)
# 2.2.14) Run and visualize (uncomment to execute)
run_fds(fds_path2)
open_smv(file.path(run_dir2, "simulation_lidar.smv"))

Demo

3. Acknowledgements

We gratefully acknowledge funding from ESTCP for the project FuelsCraft: An innovative wildland fuel mapping tool for prescribed fire decision support on Department of Defense military installations. We also thank undergraduate student Lucas Bielak Rezende for his initial contribution to the project.

4. Reporting Issues

Please report any issue regarding the rGEDI package to c.silva@ufl.edu

5. Citing rFDS

Silva,C.A; Fareed,N; rFDS: An R Package for Fire Dynamics Simulation from LiDAR Data. version 0.0.1, accessed on October. 23 2025, available at: https://CRAN.R-project.org/package=rFDS

6. Disclaimer

rFDS package is developed by the Forest Biometrics, Remote Sensing and Artificial Intelligence Laboratory (Silvalab) at the University of Florida. It comes with no guarantee, expressed or implied, and the authors hold no responsibility for its use or reliability of its outputs.