--- title: "Getting started with framecpp" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Getting started with framecpp} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") options(digits = 6) ``` **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. ```{r setup, message = FALSE} library(framecpp) library(dplyr) library(tidyr) library(tibble) ``` ## An example community 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: * **mild** — control weather (wind 5 km/h, temp 15 °C, dead-fuel moisture 0.10) * **extreme** — severe weather (wind 60 km/h, temp 40 °C, dead-fuel moisture 0.04) > **Everything here maps to original FRaME.** The parameter table is exactly what > `frame::paramBuilder()` produces, and the outputs below match a real `frame` > model run to floating-point precision. `vignette("reference-tracking")` shows > that validation explicitly, across the whole reference corpus. ```{r load} 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")$params ``` The parameter table is the long format `frame::paramBuilder()` produces — one row per stratum × species × parameter: ```{r params} mild_params |> as_tibble() |> head(8) ``` ## Running the model `run_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. ```{r run} mild <- run_fire_model(mild_params) extreme <- run_fire_model(extreme_params) ``` ### Rate of spread per stratum ```{r ros} 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) ``` The 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: ```{r canopy} tibble( regime = c("mild", "extreme"), canopyROS = c(mild$canopyROS, extreme$canopyROS), hasSecondRun = c(mild$hasSecondRun, extreme$hasSecondRun) ) ``` ### Flame summaries per stratum Flame length, tilt angle (radians) and vertical height (m). The mid-storey flame develops only under the extreme regime: ```{r flames} 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) ``` ### The result object `run_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). ```{r structure} extreme ``` 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): ```{r ignition-paths} head(extreme$tables$IgnitionPaths) ``` The compiled core reports its version with `framecpp_version()`: ```{r version} framecpp_version() ``` ## Building inputs yourself `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: ```{r scenario} scn <- scenario_from_params(mild_params) scn$wind # site wind speed (m/s), converted from the table's km/h length(scn$strata) # number of strata ``` See `vignette("reference-tracking")` for how the parameter table is produced from `frame` and how framecpp is validated against it. ## Under the hood: the mechanistic primitives 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: ```{r crown} crown <- crown_polygon(hc = 0, he = 0.05, ht = 0.5, hp = 0.6, w = 0.5) crown$area crown$vertices ``` `crown_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): ```{r ray} crown_ray_intersection(0, 0.05, 0.5, 0.6, 0.5, origin_x = 0, origin_y = -1, angle = pi / 2) ``` **Surface fire.** `surface_fire()` gives head rate of spread and flame length from slope, moisture, fuel load and wind: ```{r surface} 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")] ``` **Flame geometry.** `flame_angle()` gives the wind/slope tilt of a flame: ```{r angle} flame_angle(flame_length = 2, wind_speed = 5, slope = 0, fireline_length = 100) ``` **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): ```{r species} 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) ``` ## Where this is going Everything 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`. ```