Once a model is built (see Model bricks and TOPIA I) and solved (see Solver backends and TOPIA II), a handful of utility functions do the rest of the day-to-day work: pull objects and results out, edit a piece and re-solve, organize scenarios on disk, and compare runs. This article tours that layer.
We use the packaged single-region TOPIA kit as a running example, and
keep every store — scenarios, models, repositories and the registry —
under a temporary folder. All four matter: the stores and the registry
are written beside whatever path is redirected, so redirecting only the
scenario path leaves models/ and a registry beside the
article.
wf <- file.path(tempdir(), "wf")
set_scenarios_path(wf) # where scenarios go
set_models_path(file.path(wf, "models")) # ... and models
set_repositories_path(file.path(wf, "repositories")) # ... and repositories
set_registry_file(file.path(wf, "energyRt_registry.csv"))
um <- topia$modules$electricity$R1
mod <- newModel("TOPIA", data = um$repo,
calendar = topia$modules$calendars$s4_h24,
region = um$regions,
horizon = topia$modules$horizons$base, discount = 0.05)
mod <- setGeoscale(mod, um$geoscale)
# interpolate + solve on the bundled GLPK; echo = FALSE keeps the log quiet
scen <- interpolate_model(mod, name = "BASE") |>
solve_scenario(solver = solver_options$glpk, echo = FALSE)Selecting objects: getObject()
getObject() returns the building-block objects
held in a repository, model or scenario, filtered by
class, name,
description, region or any object
slot. It is the object-level counterpart of getData()
(which returns their data). By default it returns a named list keyed by
object name; drop = TRUE unwraps a single match into the
object itself.
repo <- um$repo
names(getObject(repo, class = "technology")) # all technologies
#> [1] "ECOA" "EGAS" "ESOL" "EWIN" "EBIO"
names(getObject(repo, class = "supply", region = "W1"))
#> [1] "SUP_BIO" "RES_SOL" "RES_WIN"
getObject(repo, name = "ECOA", drop = TRUE)@invcost # the ECOA object itself
#> vintage cluster region year invcost wacc payback eac retcost
#> 1 <NA> <NA> <NA> NA 2000 NA NA NA NARegion matching reads @region slots and the
region/src/dst columns of any
data.frame slot, so it works for every class; region-agnostic objects
(e.g. commodities) match every region. getObject() accepts
a scenario too, so the same query works before or after
solving.
Extracting data: getData()
getData() pulls parameter and
variable data out of a scenario as tidy data.frames.
With merge = TRUE it returns one long frame (a named list
otherwise); every frame carries a scenario and a
name column.
getData(scen, "vObjective", merge = TRUE)$value # total system cost, MEUR
#> [1] 18351.9
gen <- getData(scen, "vTechOut", comm = "ELC", merge = TRUE)
head(gen[, c("scenario", "tech", "region", "year", "timeslice", "value")], 4)
#> # A tibble: 4 × 6
#> scenario tech region year timeslice value
#> <chr> <chr> <chr> <int> <chr> <dbl>
#> 1 BASE ECOA W1 2025 WIN_h00 0.799
#> 2 BASE ECOA W1 2025 WIN_h01 0.736
#> 3 BASE ECOA W1 2025 WIN_h02 0.698
#> 4 BASE ECOA W1 2025 WIN_h03 0.673The ... accept set filters, exact
(comm = "ELC") or regex (comm_ = "^EL").
timeframe = c("lowest","highest","all") controls how
timeslice-indexed values are returned. Rename dimensions or recode
values on the way out with newNames = /
newValues = (or afterwards with renameSets() /
revalueSets()), and discover which parameters carry a given
set with findData():
names(findData(scen, setsNames_ = "tech")) # parameters indexed by 'tech'
#> [1] "pTechWeatherAf" "pTechWeatherAfs" "pTechWeatherAfc"
#> [4] "pTechCap2act" "pTechEac" "pTechWacc"
#> [7] "pTechPayback" "pTechEmisComm" "pTechOlife"
#> [10] "pTechFixom" "pTechInvcost" "pTechStock"
#> [13] "pTechStockNew" "pTechStockSurv" "pTechVarom"
#> [16] "pTechAf" "pTechRampUp" "pTechRampDown"
#> [19] "pTechAfs" "pTechGinp2use" "pTechCinp2ginp"
#> [22] "pTechUse2cact" "pTechAct2AInp" "pTechCap2AInp"
#> [25] "pTechAct2AOut" "pTechCap2AOut" "pTechNCap2AInp"
#> [28] "pTechNCap2AOut" "pTechPho2AInp" "pTechPho2AOut"
#> [31] "pTechRet2AInp" "pTechRet2AOut" "pTechCinp2AInp"
#> [34] "pTechCout2AInp" "pTechCinp2AOut" "pTechCout2AOut"
#> [37] "pTechCact2cout" "pTechCinp2use" "pTechCvarom"
#> [40] "pTechAvarom" "pTechShare" "pTechAfc"
#> [43] "pTechCap" "pTechNewCap" "pTechRet"
#> [46] "pTechRetCost" "vTechAInp" "vTechAOut"
#> [49] "vTechAct" "vTechCap" "vTechEac"
#> [52] "vTechFixom" "vTechInp" "vTechInv"
#> [55] "vTechNewCap" "vTechOut" "vTechRetCost"
#> [58] "vTechRetiredNewCap" "vTechRetiredStock" "vTechPhaseOut"
#> [61] "vTechStockCap" "vTechStockPhaseOut" "vTechVarom"
#> [64] "vTechEmsFuel"find_in_model(mod, "ECOA") text-searches every object
slot for a value — handy for locating where a name is used.
Editing an object: update()
update() edits the slots of a single
model object (a technology, commodity, demand, …). It does
not operate on a whole model or scenario — you update
the object, put it back into the repository with
add(..., overwrite = TRUE), and re-interpolate/solve.
ECOA <- getObject(repo, name = "ECOA", drop = TRUE)
ECOA <- update(ECOA, invcost = data.frame(invcost = 2500)) # pricier coal capex
repo_hi <- add(repo, ECOA, overwrite = TRUE) # swap it back in
mod_hi <- newModel("TOPIA_HI", data = repo_hi,
calendar = topia$modules$calendars$s4_h24,
region = um$regions, horizon = topia$modules$horizons$base,
discount = 0.05)(For an already-interpolated scenario,
update_parameter(scen, param, data) writes rows straight
into its parameter store.)
Scenario folders and structure
(The Scenario management article explains this layer in depth — stores, manifests, the registry, runs, and variants; this section is the quick tour.)
The scenarios directory is an option — read it with
get_scenarios_path(), set it with
set_scenarios_path() (default "scenarios/").
Each scenario gets its own folder, named automatically as
name-model-calendar-horizon (parts joined by dashes, which
valid names cannot contain; duplicate parts are dropped, so a scenario
and model sharing a name appear once):
get_scenarios_path()
#> [1] "C:\\Users\\admin\\AppData\\Local\\Temp\\RtmpKs1y8G/wf"
basename(scen@path)
#> [1] "BASE-TOPIA-s4_h24-base"Saving a scenario writes an Arrow-backed folder that
mirrors the scenario’s structure — a thin scen.RData shell,
a scenario.yml manifest, each large data slot as a dataset,
and one folder per solver run:
<scenarios-path>/<name-model-calendar-horizon>/
├── scenario.yml # manifest: model ref/hash, calendar, horizon,
│ # `default:` — the default run (user-changeable)
├── scen.RData # thinned S4 scenario shell
├── class · format · layout # "scenario" · "parquet" · "3"
├── logfile.csv # timestamped save log
├── model/data/<repo>/<object>/<slot>/ # model data (embedded scenarios only)
├── modInp/parameters/<param>/ # interpolated input parameters
└── runs/ # one folder per solve
├── glpk/ # base-problem run, named by its solve label
│ ├── run.yml # provenance: solver, cmdline, times, objective
│ ├── input/ # the exchange tables handed to the solver
│ ├── output/ # the solver's raw dump (read_solution() reads it)
│ └── modOut/variables/<var>/ # the solved variables
└── julia_highs/ # another backend's run of the same problem
Beyond the per-scenario traces above, an optional operation
log records the sequence of a working session:
set_log_file("energyRt_log.csv") makes
interpolate_model(), solve_scenario() and
solve_recursive() append one line each (operation, object,
status, objective, duration), and read_log() returns the
sequence as a tibble. It is off by default.
Solving the same scenario with several backends (or option sets —
pass run = "<label>" to
solve_scenario()) keeps every run side by side;
scenario_runs(scen) lists them with status and objective,
and read_solution(scen, run = "<label>") switches the
active solution. A saved scenario references its model in the model
store when the model is a sealed version (seal_model()),
and carries a copy of it otherwise (see ?save_scenario,
mode, and the Scenario management article).
A scenario knows whether its data is in RAM or on disk via
isInMemory(); when on disk, the slots are empty and read
lazily from the folder on demand. Folders from older energyRt versions
are still readable and migrate in place with
upgrade_scenario_layout(path).
Saving and loading
save_scenario() spills the scenario to its folder and
returns the (now on-disk) object;
load_scenario(path, env = NULL) reads the shell back. Its
data stays on disk until requested — getData() reads it
lazily, or obj2mem() pulls the whole scenario back into
memory.
saved <- save_scenario(scen, verbose = FALSE)
ld <- load_scenario(saved@path, env = NULL, verbose = FALSE)A saved scenario is also reachable by its registered
name — load_scenario("BASE", env = NULL)
resolves it through the project registry, and the cached accessor
getScenario("BASE") is the everyday short form (see the
Scenario management article):
identical(getScenario("BASE")@name, ld@name)
#> [1] TRUE
basename(saved@path) # the scenario folder
#> [1] "BASE-TOPIA-s4_h24-base"
isInMemory(saved) # FALSE -- data is on disk
#> [1] TRUE
getData(ld, "vObjective", merge = TRUE)$value # lazy read, no full load
#> [1] 18351.9
ld <- obj2mem(ld) # rehydrate fully into RAM
isInMemory(ld)
#> [1] TRUEEvery save_scenario() / save_model() /
save_repository() also records its object in the project
registry — a single CSV (default
energyRt_registry.csv at the project root, see
get_registry_file()) indexing saved models, repositories,
scenarios, and runs. Repositories shared by several models can be stored
once with save_repository() and referenced by each model
(save_model(embed_repos = )), just as scenarios can
reference a stored model. load_registry() reads it,
find_in_registry() filters it (by type, name, or model
hash), and refresh_registry() rebuilds it by rescanning the
scenarios/ and models/ stores — the on-disk
manifests are the source of truth, so the registry can always be
reconstructed.
Comparing scenarios
Layer a policy lever onto the base model to get a second scenario,
then pass a named list of scenarios to
getData() — the scenario column makes the
comparison a one-liner. (Scenarios already saved compare by
name alone: getData(c("BASE", "CO2CAP"), ...) loads them
from the registry on the fly.)
scen_cap <- interpolate_model(mod, "CO2CAP", um$CO2_CAP) |> # add the CO2 cap lever
solve_scenario(solver = solver_options$glpk, echo = FALSE)
emis <- getData(list(BASE = scen, CO2CAP = scen_cap), "vEmsFuelTot",
comm = "CO2", merge = TRUE)
emis |>
group_by(scenario, year) |> summarise(ktCO2 = sum(value), .groups = "drop") |>
ggplot(aes(factor(year), ktCO2, fill = scenario)) +
geom_col(position = "dodge") +
labs(x = "year", title = "CO2 emissions by scenario") + theme_bw()
sapply(list(BASE = scen, CO2CAP = scen_cap),
function(s) round(getData(s, "vObjective", merge = TRUE)$value[1]))
#> BASE CO2CAP
#> 18352 32669A scenario that adds a lever to an interpolated one can be
built on it: base = scen reuses BASE’s
interpolated tables and computes only what the cap adds, and the result
equals interpolating from scratch:
scen_cap2 <- interpolate_model(mod, "CO2CAP", um$CO2_CAP, base = scen)
sum(compare_inputs(scen_cap2, scen_cap)$summary$status != "identical")
#> [1] 0The same holds for new technologies or commodities and for a sample of the base’s calendar or regions; anything else is interpolated in full, with a message saying why. Saved on a sealed base, such a scenario references the base’s tables instead of copying them (see Scenario management, “Scenarios built on a base”).
To reason about model size rather than results,
model_size() estimates the parameter/variable/constraint
counts of an interpolated scenario, and size() reports its
in-memory footprint:
model_size(scen)
#> model_size: BASE
#> parameters : 200 value, 359 maps, 15 sets
#> param rows : 5,972
#> estimate : ~20,012 variables, ~20,704 constraints (from gating maps)
#> top parameters by rows:
#> pTimesliceAgg 1,188
#> pTechCinp2use 1,152
#> pWeather 1,004
#> pTimesliceWeight 404
#> pDemand 384
#> pExportRowPrice 384
#> pExportRow 384
#> pStorageInpEff 384
#> pStorageOutEff 384
#> pTimesliceShare 101
#> pTechFixom 20
#> pTechInvcost 20
#> pTechEac 17
#> pTechStock 14
#> pWacc 12
size(scen)
#> [1] "10.8 Mb"For a systematic check that a build is correct and
efficient, compare_interp_settings(mod) and
compare_solve_settings(mod, solvers = ...) interpolate (and
solve) the model under different storage settings and tabulate build
size, time and — crucially — confirm the objective is invariant across
fold/sparse/prune. These run
several builds, so they are best run interactively:
compare_interp_settings(mod) # size/time by setting
compare_solve_settings(mod, solvers = list(solver_options$glpk))Sampling: solving a subset of the model
Interpolation can solve a sample of a model without editing
the model itself — in time (a sampled calendar) and in space (a sampled
geoscale). Both work the same way: build the sample object, pass it to
interpolate_model(), and the scenario is declared,
validated and solved on the sample.
Sampled calendars
A sampled calendar is a row subset of the full calendar’s
timetable plus an explicit year_fraction — the
fraction of the year the sample covers. Timeslice weights annualise the
sample inside the solver (the top slice carries weight
1/year_fraction), so the objective is an estimate
of the full model’s — and exactly equals it when the sampled slices are
representative:
tt <- topia$modules$calendars$s4_h24@timetable
tt2 <- tt[tt$SEASON %in% c("WIN", "SUM"), ] # two of the four seasons
cal2 <- newCalendar(timetable = tt2, name = "s2h24",
year_fraction = sum(tt2$share))
scen_t <- interpolate_model(mod, "S2", cal2) |>
solve_scenario(solver = solver_options$glpk, echo = FALSE)
sapply(list(full = scen, sampled = scen_t),
function(s) round(getData(s, "vObjective", merge = TRUE)$value[1]))
#> full sampled
#> 18352 20427Two traps: pass year_fraction
explicitly (it is not recomputed from the subset), and
subset the existing timetable rather than rebuilding
one with make_timetable() (which would renormalise the
shares to a full year). Declarations are validated against the
model’s calendar, so objects naming out-of-sample timeslices
interpolate cleanly — their out-of-sample rows are simply filtered.
A sample of an already interpolated full-calendar scenario can be
built on it,
interpolate_model(mod, "S2", cal2, base = scen): the full
scenario’s tables are taken filtered to the sampled timeslices instead
of being recomputed.
Spatial sampling
The spatial mirror: pass a filtered geoscale — a
geoscales::filter_geoscale() subset of the model’s regions
— and the scenario becomes a sub-territory model: only the
sampled regions are declared, out-of-sample parameter rows are filtered,
and trade routes crossing the sample boundary are dropped (with a
message). Unlike calendar sampling there is no
reweighting: regional quantities are extensive, so the
objective is the sub-territory’s own, and disjoint samples add
up to the full model (absent cross-boundary trade).
TOPIA is single-region, so a compact three-region demo:
library(geoscales)
r3 <- c("R1", "R2", "R3")
gs <- geoscale_from_leaftable(
data.frame(zone = c("Z12", "Z12", "Z3"), region = r3, km2 = 1),
geoframes = c("zone", "region"), name = "demo")
m3 <- newModel(
"M3", region = r3, horizon = newHorizon(2020), discount = 0.05,
repo = newRepository(
"r",
newCommodity("COA", timeframe = "ANNUAL"),
newCommodity("ELC", timeframe = "ANNUAL"),
newSupply("SUP_COA", commodity = "COA",
supply = data.frame(region = r3, cost = 5)),
newTechnology("ECOA", input = list(comm = "COA"),
output = list(comm = "ELC"),
af = data.frame(af.up = 0.9),
invcost = data.frame(invcost = 1000),
olife = list(olife = 30), cap2act = 1),
newDemand("DEM_ELC", commodity = "ELC",
demand = data.frame(region = r3, demand = c(10, 20, 30))),
newTrade("TR23", commodity = "ELC", cap2act = 1,
routes = data.frame(src = "R2", dst = "R3"),
trade = data.frame(src = "R2", dst = "R3", ava.up = 4),
invcost = data.frame(invcost = 10))))Filtering the geoscale to R1 + R2 and interpolating
drops the R2 -> R3 route on the way:
gs12 <- filter_geoscale(gs, "region", c("R1", "R2"),
drop_empty_geoframes = TRUE)
scen12 <- interpolate_model(m3, "S12", gs12)
#> spatial sample: sub-territory solve on 2 of 3 regions
#> spatial sample: kept 2 region(s); dropped 1 object(s) and 1 trade route(s) outside the sample (verbose = TRUE lists them)
as.character(scen12@settings@region)
#> [1] "R1" "R2"A dropped route can optionally leave a priced boundary
stub at the kept endpoint — an export (kept source) or import
(kept destination) object bounded by cap.up (or, failing
that, the route’s ava.up) — via
boundary_prices; routes without a price row are just
dropped:
bp <- data.frame(src = "R2", dst = "R3", price = 50, cap.up = 4)
scen12p <- interpolate_model(m3, "S12P", gs12, boundary_prices = bp)
#> spatial sample: sub-territory solve on 2 of 3 regions
#> spatial sample: kept 2 region(s); dropped 1 object(s) and 1 trade route(s) outside the sample (verbose = TRUE lists them)
grep("^EXP", as.character(scen12p@modInp@sets$expp), value = TRUE)
#> [1] "EXP_TR23_R22R3"The same surgery is available up front, as a model-to-model verb:
m12 <- subset_model_regions(m3, c("R1", "R2"), boundary_prices = bp)
as.character(m12@config@region)
#> [1] "R1" "R2"The filtered geoscale records its coverage (fraction
of each weight’s parent total — see
geoscales::geoscale_coverage()), and its name is mangled
("demo[region:R1+R2]") so different samples of one parent
never collide in caches or joins. A pruned geoscale (a
coarser level of the model’s own hierarchy) requests a full-territory
solve at the parent level instead. That is a different operation – it
aggregates the model’s data rather than subsetting it – so
interpolate_model() refuses a pruned geoscale and points at
aggregate_model_regions(), which builds the coarser model
directly.
A trade window instead of a flat price
A stub priced with a single number is a perfectly elastic
counterparty: the region can buy or sell any amount the bound allows
without moving the price. For supply-curve work that is usually too
generous. boundary_window() builds a stepped
curve instead — a rising cost of imports, a falling revenue on
exports — and sizes its quantity from the region’s own demand:
bw <- boundary_window(m3, regions = c("R1", "R2"),
share = 0.1, nsteps = 3, price = 40)
head(bw, 2)
#> src dst year timeslice price cap.up nsteps price_lo price_hi
#> 1 R2 R3 NA <NA> 40 2 3 20 40share = 0.1 caps the boundary at 10% of the region’s
demand in each timeslice, which is where the
import/export bound lives; summed over the year that is 10% of annual
demand. The window deliberately refuses to guess a price — it can size
the quantity from the model, but what the neighbours would charge is a
judgement, so price is required.
The result is an ordinary boundary_prices table with
three extra columns (nsteps, price_lo,
price_hi), so it goes where a flat one goes; omit them and
you get exactly the flat stub as before.
m12w <- subset_model_regions(m3, c("R1", "R2"), boundary_prices = bw)
stub <- m12w@data[["boundary_stubs"]]@data[[1]]
# `@cluster` declares WHAT the steps are; the split itself is enforced in the
# quantity bound, so read it there rather than from a descriptive copy
as.data.frame(stub@cluster)[, c("cluster", "desc", "order")]
#> cluster desc order
#> 1 S1 step 1 of 3: 0-0.3333 of quantity at 36.6667 1
#> 2 S2 step 2 of 3: 0.3333-0.6667 of quantity at 30 2
#> 3 S3 step 3 of 3: 0.6667-1 of quantity at 23.3333 3Region by region
solve_by_region() runs the whole sweep — one region at a
time by default, or one group at a time — storing each as a variant of a
single scenario:
br <- solve_by_region(m3, name = "sweep") # autarky
br <- solve_by_region(m3, name = "sweep_w", # with a trade window
trade = "auto", price = 40, share = 0.1)
br$runs
getData(br, "vTechCap", merge = TRUE) # stacked, region columnWith trade = "none" the regions are disjoint and nothing
crosses, so the per-region objectives add up to the
full model’s — the same property the sample objectives have above. Any
trade window breaks it: each region then faces an exterior the others
cannot see. br$additive records which case you are in, and
print() says so rather than leaving it to be
discovered.
on_error = "continue" keeps the sweep going past a
region that fails to solve, which matters at 30-odd regions: a single
infeasible one should not cost you the rest.
Sampling the calendar repeatedly
A single sampled calendar gives one cheap estimate.
solve_by_sample() runs a whole ensemble of them —
consecutive blocks tiling the year, disjoint random draws, or bootstrap
draws — and keeps every run:
ens <- solve_by_sample(m3, name = "ens", sample_size = 2,
method = "sequential") # two-season blocks
ens$runs
sample_summary(ens, "vTechCap", probs = c(0.05, 0.5, 0.95))Samples are drawn as whole members of a calendar level — seasons,
weeks, days. A hand-edited sample need not be a complete cross: its
calendar is irregular and keeps the full calendar’s timeslice names.
calendar_samples() builds the specification on its own, so
it can be inspected or edited first.
Two rules follow from how a sampled calendar works, and both are
enforced rather than left to be discovered. A sample must keep
at least two members at every level: a single season
(or week, or day) collapses its level, and the only way to build such a
calendar renames the leaf timeslices so the model’s own data would stop
matching them. And every sample is annualised —
pTimesliceWeight = 1/year_fraction — so each one estimates
the whole year. The ensemble is therefore a set of replicates
whose spread is the sampling uncertainty; summing them is meaningless,
unlike the region sweep above, whose disjoint pieces do add up.
Combining a sequence’s results
Each driver stores one variant per unit of work and nothing merged is written to disk. Combination happens in memory, on read, and the rule differs by driver because the units mean different things.
getData() on a sequence stacks and does not
aggregate. It binds every unit’s rows and adds a provenance
column — region for solve_by_region(),
sample for solve_by_sample() — so the caller
decides what to do with them. Two methods are exceptions:
getData() on a recursive sequence stitches, taking each
year from the step that decided it, and on a guided sequence it
delegates to the final full-horizon solve.
rc <- solve_recursive(m3, name = "rec") # milestone by milestone
getData(br, "vTechCap", merge = TRUE) # stacked, with a `region` column
getData(ens, "vTechCap", merge = TRUE) # stacked, with a `sample` column
sample_summary(ens, "vTechCap", by = "tech")
recursive_objective(rc)| driver | what a unit is | how results combine |
|---|---|---|
solve_by_region() |
a disjoint piece of the territory | quantities add; objectives sum only under
trade = "none", which the result reports as
additive
|
solve_by_sample() |
a replicate estimate of the whole year | a distribution — mean and quantiles via
sample_summary(). Summing is wrong,
because every sample is already annualised |
solve_recursive() |
a milestone window | stitched, each year contributed once. Per-step objectives are not
comparable, so recursive_objective() recomputes a
discounted total |
solve_guided() |
a stage of a ladder | the answer is $final; guided_gap() prices
the guidance against perfect foresight |
Two things the combination does not do. Regional objectives sum to
the whole system only in autarky: with transmission the per-region sum
exceeds the full model, and the difference is the
network’s value, so additive = TRUE means “may be summed”,
not “equals the full model”. And under trade = "auto" each
region faces an exterior the others cannot see — one region’s export and
another’s import are independent decisions at independently set prices,
and nothing reconciles them.
Seeing what a sequence covered
A sequence records its design per unit, and in a different shape for
each driver. sequence_coverage() puts all four on one frame
— one row per unit and element covered, with dim naming the
scale the units were drawn on — and autoplot() draws that
frame on the scale itself.
sequence_coverage(ens) # unit, dim, key, role, count
autoplot(ens) # the calendar, tiled by sample
autoplot(ens, "wall", year = 2020) # the same, as a wall calendar
autoplot(br) # the geoscale, filled by unit
autoplot(rc) # the horizon, decided against look-aheadThree properties show in the drawing that a table of runs leaves
implicit. Elements no unit covered keep a grey fill, so a gap in an
ensemble is a hole in the picture rather than an absent row. An element
two units claimed is filled as (shared) instead of being
given to the first of them, and the subtitle counts both that and the
within-unit multiplicity a bootstrap draw carries. And a recursive
step’s look-ahead milestones are filled apart from the years it decided,
which is the overlap between steps seen for what it is.
The wall calendar needs a day-resolution timeframe (YDAY
or MDAY) and rolls each unit up to whole days; the tile
grid, over the calendar’s two finest timeframes, works for any calendar.
A region sample is mapped on the finest geoframe that contains every
solved region — a model’s regions need not sit at the finest frame of
its geoscale — and falls back to a plain list of regions where the
geoscale carries no geometry.
The spread of a result across an ensemble is a histogram rather than a map:
autoplot(ens, "histogram") # the objective
autoplot(ens, "histogram", var = "vTechCap", by = "tech")Bars are weighted by each sample’s year_fraction, since
sequential sampling can leave one block covering more of
the year than its siblings, and the dashed rule is the weighted mean
sample_summary() reports — one weighting rule, so the plot
and the table agree about the same ensemble. Where
ncap_floor > 0 the free pass is overlaid on the floored
one. A variable must reduce to one value per sample within each group; a
dimension left inside a bar is named rather than summed away, since
totalling capacity over years would answer a different question. There
is no histogram for solve_by_region(), whose units are
shards of one system: their values are addends, and their spread would
report only that regions differ in size.
Watching a sequence run
A sequence solve is the longest-running operation in the package and
reports as one. set_progress_bar() registers a
progressr handler, after which each driver advances a
bar one unit at a time; verbose = TRUE prints a line per
unit, as an alternative or in addition. Only the outermost sequence
claims the bar, so the interpolation inside a driver’s loop does not
open a second one over the first.
set_progress_bar()
ens2 <- solve_by_sample(m3, name = "ens_bar", sample_size = 2,
verbose = FALSE)See also
- Model bricks and TOPIA I — building the objects and the model.
- Solver backends and TOPIA II — interpolating and solving.
-
?getData,?getObject,?update,?save_scenario,?load_scenario,?model_size,?compare_solve_settings,?subset_model_regions.
