framecpp
reimplements the FRaME forest-flammability model
(Zylstra et al.) in C++/Rcpp. It is a faithful port of classic,
discrete-strata FRaME: given the same inputs it reproduces the original
frame package’s outputs bit-for-bit (see
vignette("reference-tracking")), while running far faster —
no JVM. This vignette walks through the everyday workflow: run the model
on a plant community and read its outputs.
The package ships golden references in inst/extdata/ —
complete frame model runs captured from
frame v0.5.3, kept as the validation corpus. Here
we use one purely as convenient input: the community shipped as FRaME’s
own worked example — four strata of eucalypt-forest plants (all
flat-leaved) — under two weather regimes:
Everything here maps to original FRaME. The parameter table is exactly what
frame::paramBuilder()produces, and the outputs below match a realframemodel run to floating-point precision.vignette("reference-tracking")shows that validation explicitly, across the whole reference corpus.
read_ref <- function(tag) {
readRDS(system.file("extdata", paste0("ref_", tag, ".rds"), package = "framecpp"))
}
mild_params <- read_ref("mild")$params
extreme_params <- read_ref("extreme")$paramsThe parameter table is the long format
frame::paramBuilder() produces — one row per stratum ×
species × parameter:
mild_params |> as_tibble() |> head(8)
#> # A tibble: 8 × 5
#> stratum species param value units
#> <chr> <chr> <chr> <chr> <chr>
#> 1 1 1 name Poa sieberiana <NA>
#> 2 1 1 clumpDiameter 0.26 m
#> 3 1 1 clumpSeparation 0 m
#> 4 1 1 composition 50 <NA>
#> 5 1 1 deadLeafMoisture 0.1 <NA>
#> 6 1 1 hc 0 m
#> 7 1 1 he 0 m
#> 8 1 1 hp 0.26 mrun_fire_model() runs the entire single-site model
in-process: it derives species and stratum properties, grows the surface
fire, runs the ignition-path model for every plant and stratum,
assembles flames, and computes rate of spread.
ros_tbl <- function(res, regime) {
tibble(level = names(res$run1$ros), ros = unname(res$run1$ros), regime = regime)
}
bind_rows(ros_tbl(mild, "mild"), ros_tbl(extreme, "extreme")) |>
pivot_wider(names_from = regime, values_from = ros)
#> # A tibble: 5 × 3
#> level mild extreme
#> <chr> <dbl> <dbl>
#> 1 Surface 0.00682 0.0972
#> 2 NearSurface 0.0123 0.198
#> 3 Elevated 0 0.133
#> 4 MidStorey 0 0
#> 5 Canopy NA NAThe same forest barely creeps under control weather but spreads through the near-surface and elevated strata under severe weather.
Why is the Canopy row NA?
The per-stratum run1$ros map reports spread within
the standing vegetation (surface → mid-storey). Canopy rate of
spread is a separate quantity: it comes from FRaME’s two-run
crown-fire calculation and lives in res$canopyROS (with
res$hasSecondRun and res$run2 describing the
second pass). When there is no crown fire, canopyROS is
0 and the Canopy entry of
run1$ros is flagged absent — reported as
NA, which is deliberately distinct from a genuine zero.
This matches frame, whose rate-of- spread table likewise
has no canopy row here. Neither community crowns:
Flame length, tilt angle (radians) and vertical height (m). The mid-storey flame develops only under the extreme regime:
flame_tbl <- function(res) {
fs <- res$run1$flameSummaries
tibble(
level = names(fs),
length = vapply(fs, `[[`, numeric(1), "flameLength"),
angle = vapply(fs, `[[`, numeric(1), "flameAngle"),
height = vapply(fs, `[[`, numeric(1), "flameHeight")
)
}
flame_tbl(extreme)
#> # A tibble: 4 × 4
#> level length angle height
#> <chr> <dbl> <dbl> <dbl>
#> 1 NearSurface 0.503 0.139 0.0697
#> 2 Elevated 3.27 0.413 2.33
#> 3 MidStorey 4.37 0.284 7.23
#> 4 Canopy 0 0 0run_fire_model() returns a fire_result
object: the full run. Its $tables element holds
all seven frame result tables’ worth as tidy data frames —
Sites, Runs, Strata,
FlameSummaries, SurfaceResults,
IgnitionPaths, ROS — while $run1
/ $run2 give a per-run convenience view (surface detail,
flameSummaries, a ros map, ignition paths and
combined flames), and $canopyROS /
$hasSecondRun are the crown-fire scalars. run2
is the crown-fire second pass (empty when there is no crown fire).
extreme
#> <fire_result>
#> canopyROS: 0 m/s crown-fire second run: no
#> strata: NearSurface, Elevated, MidStorey, Canopy
#> run 1 surface ROS: 0.0972 m/s ROS levels: Surface, NearSurface, Elevated, MidStorey
#> tables: Sites, Runs, Strata, FlameSummaries, SurfaceResults, IgnitionPaths, ROS
#> ignition-path segments: 64 (run 1) + 0 (run 2)The ignition-path detail — every ignited segment, per stratum and
species — is now surfaced too. Species are identified by the engine’s
own speciesId (names are a data-source concern that lives
with the consumer):
head(extreme$tables$IgnitionPaths)
#> repId runIndex level pathType speciesId speciesRank segIndex x0 y0
#> 1 1 1 NearSurface plant 1 0 0 -0.235 0
#> 2 1 1 NearSurface plant 1 0 1 -0.235 0
#> 3 1 1 NearSurface plant 2 1 0 -0.235 0
#> 4 1 1 NearSurface plant 2 1 1 -0.235 0
#> 5 1 1 NearSurface plant 2 1 2 -0.235 0
#> 6 1 1 NearSurface plant 2 1 3 -0.235 0
#> x1 y1 length flameLength
#> 1 0.180608 0.259988 0.490228 0.626976
#> 2 0.180608 0.259988 0.490228 0.626976
#> 3 0.180608 0.259988 0.490228 0.627113
#> 4 0.180608 0.259988 0.490228 0.627113
#> 5 0.180608 0.259988 0.490228 0.627113
#> 6 0.180608 0.259988 0.490228 0.627113The compiled core reports its version with
framecpp_version():
run_fire_model() is a thin wrapper.
scenario_from_params() converts the frame
parameter table into the scenario the compiled model consumes (applying
FRaME’s unit conventions — wind km/h → m/s, fuel load t/ha → kg/m²,
slope deg → rad), and parse_fire_result() assembles the
fire_result from the compiled run. Calling them separately
lets you inspect or adjust the scenario:
scn <- scenario_from_params(mild_params)
scn$wind # site wind speed (m/s), converted from the table's km/h
#> [1] 1.38889
length(scn$strata) # number of strata
#> [1] 4See vignette("reference-tracking") for how the parameter
table is produced from frame and how framecpp is validated
against it.
The model is assembled from many small, individually validated primitives; a curated set is exported as a documented low-level API.
Crown geometry. crown_polygon() turns
the five crown height/width parameters into the convex polygon FRaME
represents a plant crown as:
crown <- crown_polygon(hc = 0, he = 0.05, ht = 0.5, hp = 0.6, w = 0.5)
crown$area
#> [1] 0.2625
crown$vertices
#> [,1] [,2]
#> [1,] -0.25 0.05
#> [2,] -0.25 0.50
#> [3,] 0.00 0.60
#> [4,] 0.25 0.50
#> [5,] 0.25 0.05
#> [6,] 0.00 0.00
#> [7,] -0.25 0.05crown_ray_intersection() returns the chord a ray cuts
through that crown — the core operation of the ignition-path geometry
(here a vertical ray from below):
crown_ray_intersection(0, 0.05, 0.5, 0.6, 0.5,
origin_x = 0, origin_y = -1, angle = pi / 2)
#> [1] 0.0 0.0 0.0 0.6Surface fire. surface_fire() gives head
rate of spread and flame length from slope, moisture, fuel load and
wind:
sf <- surface_fire(slope = 0, dead_fuel_moisture = 0.1, fuel_load = 0.5,
mean_fuel_diameter = 0.005, mean_fineness_leaves = 0.0004,
wind_speed = 3)
sf[c("ros", "flameLength")]
#> $ros
#> [1] 0.0972
#>
#> $flameLength
#> [1] 2Flame geometry. flame_angle() gives the
wind/slope tilt of a flame:
Species properties. species_derived()
computes the leaf-trait quantities FRaME derives per species (live
proportion, leaf area, moisture, ignitability, leaves per clump,
leaf-area index):
species_derived(hc = 0, he = 0.05, ht = 0.5, hp = 0.6, w = 0.5,
live_moisture = 1.0, dead_moisture = 0.1, prop_dead = 0.1,
ignition_temp = 260, leaf_form = "Flat",
leaf_thickness = 0.0002, leaf_width = 0.003, leaf_length = 0.05,
leaf_separation = 0.01, stem_order = 5,
clump_diameter = 0.1, clump_separation = 0.2)
#> $propLive
#> [1] 0.9
#>
#> $leafArea
#> [1] 7.5e-05
#>
#> $leafMoisture
#> [1] 0.91
#>
#> $ignitabilityCoef
#> [1] 9.1
#>
#> $leavesPerClump
#> [1] 88.9749
#>
#> $leafAreaIndex
#> [1] 0.236013Everything here is classic, discrete-strata FRaME, ported exactly.
The larger goal is to generalise it to continuous plant size
distributions while keeping this behaviour as an exact special case —
run_fire_model() is the classic-mode entry point that
generalisation must reduce to. See the code-derived model description in
vignettes/articles/frame-model.qmd and the roadmap in
dev/plan.md. ```