--- title: "Tracking the frame reference" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Tracking the frame reference} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") options(digits = 6) ``` framecpp is a *reimplementation* of FRaME, so its worth rests entirely on reproducing the original model. To keep that honest, the package ships a **corpus of golden references — real `frame` model runs — and checks itself against them**. This vignette documents that corpus and re-runs the check live. ```{r setup, message = FALSE} library(framecpp) library(dplyr) library(tidyr) ``` ## Provenance: everything traces to original FRaME The references live in `inst/extdata/ref_*.rds`. Each was produced by **`frame` v0.5.3 itself** — the parameter table was fed to `frame`'s JVM runner (`ffm.runner.CSVRunner`) and the resulting model output saved. The capture scripts are in `data-raw/`. So every golden output is genuine FRaME; framecpp must match it bit-for-bit. ```{r corpus} corpus <- tribble( ~tag, ~community, "mild", "FRaME's shipped example community (4 strata, flat leaves) — control weather", "extreme", "FRaME's shipped example community — severe weather", "alt", "Hand-authored round/flat-leaf community (tuart_traits species)", "tuart", "Survey-derived, non-layered community (frame's k-means strata from tuart_survey)" ) corpus ``` How each maps to FRaME: * **mild / extreme** — the community FRaME distributes as its own worked example, run under two weather regimes. The most direct "does framecpp equal frame?" test. * **alt** — a structurally different community (round-leaf species, non-degenerate crowns) authored in FRaME's `paramBuilder` input format; its golden output is a real `frame` run, so it exercises the round/flat-leaf paths against FRaME. * **tuart** — strata derived by **frame's own k-means stratifier** from the `tuart_survey` transect data (`data-raw/tuartref_reference.R`), then run through `frame`. A genuinely non-layered community, again validated against FRaME. Each `ref_*.rds` holds the `frame` parameter table (`params`) and the full model output tables (`db`), plus the captured run metadata. ```{r peek} ref <- readRDS(system.file("extdata", "ref_extreme.rds", package = "framecpp")) names(ref) head(ref$db$ROS, 4) # frame's own rate-of-spread output ``` ## The check For each reference we run framecpp on the **same parameter table `frame` consumed** (`run_fire_model()` chains `scenario_from_params()` → the compiled model → `parse_fire_result()`), then compare every per-stratum rate of spread and flame summary against `frame`'s recorded output (`runIndex == 1`). ```{r check} relerr <- function(a, b) abs(a - b) / pmax(abs(b), 1e-9) check_tag <- function(tag) { ref <- readRDS(system.file("extdata", paste0("ref_", tag, ".rds"), package = "framecpp")) res <- run_fire_model(ref$params) # rate of spread (frame reports levels that spread; framecpp keyed by name) ros_ref <- ref$db$ROS |> filter(runIndex == 1) ros_err <- relerr(res$run1$ros[as.character(ros_ref$level)], ros_ref$ros) # flame summaries: length / angle / height per stratum fl_ref <- ref$db$FlameSummaries |> filter(runIndex == 1) fs <- res$run1$flameSummaries fl_err <- unlist(lapply(seq_len(nrow(fl_ref)), function(i) { s <- fs[[as.character(fl_ref$level[i])]] relerr(c(s$flameLength, s$flameAngle, s$flameHeight), c(fl_ref$flameLength[i], fl_ref$flameAngle[i], fl_ref$flameHeight[i])) })) tibble(tag = tag, comparisons = length(ros_err) + length(fl_err), worst_rel_err = max(ros_err, fl_err, na.rm = TRUE)) } results <- bind_rows(lapply(corpus$tag, check_tag)) results ``` ```{r verdict} stopifnot(all(results$worst_rel_err < 1e-9)) sprintf("framecpp reproduces frame across the corpus — worst relative error %.1e", max(results$worst_rel_err)) ``` The residuals are at the level of floating-point rounding (~1e-15), not model disagreement: framecpp **is** classic FRaME, recomputed in C++. ## A concrete comparison The `tuart` community crowns hard. Side by side, framecpp against `frame`'s own flame-height output: ```{r detail} ref <- readRDS(system.file("extdata", "ref_tuart.rds", package = "framecpp")) res <- run_fire_model(ref$params) fl_ref <- ref$db$FlameSummaries |> filter(runIndex == 1) |> select(level, frame = flameHeight) fs <- res$run1$flameSummaries fl_am <- tibble(level = names(fs), framecpp = vapply(fs, `[[`, numeric(1), "flameHeight")) inner_join(fl_ref, fl_am, by = "level") |> mutate(rel_err = relerr(framecpp, frame)) ``` ## Beyond the golden references The golden corpus checks framecpp against complete `frame` *model runs*. A separate differential harness (`tools/parity/`, needs a JDK + `frame`) drives the real `frame_scala` classes **stage by stage** over thousands of random inputs, and the testthat suite mirrors FRaME's own ScalaTest specs. Run the golden check any time with `make parity`.