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.
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.
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
#> # A tibble: 4 × 2
#> tag community
#> <chr> <chr>
#> 1 mild FRaME's shipped example community (4 strata, flat leaves) — control w…
#> 2 extreme FRaME's shipped example community — severe weather
#> 3 alt Hand-authored round/flat-leaf community (tuart_traits species)
#> 4 tuart Survey-derived, non-layered community (frame's k-means strata from tu…How each maps to FRaME:
paramBuilder input format; its golden output is a real
frame run, so it exercises the round/flat-leaf paths
against FRaME.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.
ref <- readRDS(system.file("extdata", "ref_extreme.rds", package = "framecpp"))
names(ref)
#> [1] "params" "db" "run"
head(ref$db$ROS, 4) # frame's own rate-of-spread output
#> repId runIndex level ros
#> 1 1 1 Surface 0.097200
#> 2 1 1 NearSurface 0.197637
#> 3 1 1 Elevated 0.133488
#> 4 1 1 MidStorey 0.000000For 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).
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
#> # A tibble: 4 × 3
#> tag comparisons worst_rel_err
#> <chr> <int> <dbl>
#> 1 mild 16 3.81e-16
#> 2 extreme 16 2.00e-16
#> 3 alt 16 2.73e-15
#> 4 tuart 16 2.69e-16stopifnot(all(results$worst_rel_err < 1e-9))
sprintf("framecpp reproduces frame across the corpus — worst relative error %.1e",
max(results$worst_rel_err))
#> [1] "framecpp reproduces frame across the corpus — worst relative error 2.7e-15"The residuals are at the level of floating-point rounding (~1e-15), not model disagreement: framecpp is classic FRaME, recomputed in C++.
The tuart community crowns hard. Side by side, framecpp
against frame’s own flame-height output:
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))
#> level frame framecpp rel_err
#> 1 NearSurface 0.365881 0.365881 1.51719e-16
#> 2 Elevated 2.036800 2.036800 0.00000e+00
#> 3 MidStorey 2.163215 2.163215 0.00000e+00
#> 4 Canopy 19.769518 19.769518 0.00000e+00The 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.