A model’s data can be large. multimod keeps the model structure and
the data apart on disk: save_model() writes a
workspace, a directory with the R object for the structure and
one data file per set, mapping and parameter, in CSV, Arrow IPC or
Parquet. load_model() reads it back, with or without the
data, and get_data() fetches a table from disk on demand.
The generated JuMP and Pyomo code reads the same files at run time,
which is why write_jump() and write_pyomo()
create a workspace when they are given an empty
model_dir.
Layout
<model_dir>/
model.rds the model structure, as saved
metadata.json counts and format, for readers outside R
format.txt "csv", "ipc" or "parquet"
sets/<name>/data.* one file per set, column `member`
mappings/<name>/data.* one file per mapping, one column per dimension
parameters/<name>/data.* dimension columns plus `value`
parameters/<name>/folded_data/data.* if the parameter is folded
variables/<name>/data.* solution values, after a solve
solution_log.txt one line per solution load
solvers/<backend>/ generated code and the backend's output
Save and load
toy <- example_models$toy
ws <- tempfile("toy-")
toy <- save_model(toy, ws, format = "csv", verbose = FALSE)
list.files(ws, recursive = TRUE)[1:8]
#> [1] "format.txt" "mappings/mappings.csv"
#> [3] "mappings/mR/data.csv" "mappings/mTR/data.csv"
#> [5] "metadata.json" "model.rds"
#> [7] "parameters/parameters.csv" "parameters/pAvail/data.csv"
toy$base_path == ws
#> [1] TRUEsave_model() returns the model with
base_path set to the workspace. With
format = "ipc" or "parquet" the data files are
Arrow files, which are typically smaller than CSV and faster to read;
both need the ‘arrow’ package, and the generated Julia and Python read
them as readily as CSV.
ws_ipc <- tempfile("toy-ipc-")
save_model(toy, ws_ipc, format = "ipc", verbose = FALSE)
list.files(file.path(ws_ipc, "parameters"), recursive = TRUE)
#> [1] "parameters.csv" "pAvail/data.arrow" "pCapCost/data.arrow"
#> [4] "pDemand/data.arrow" "pVarCost/data.arrow"load_model() reads the structure and, by default, every
data table:
m <- load_model(ws, verbose = FALSE)
head(m$parameters$pAvail$data)
#> tech region value
#> 1 coal north 0.85
#> 2 gas north 0.85
#> 3 wind north 0.90
#> 4 coal south 0.90
#> 5 gas south 0.35
#> 6 wind south 0.25Data on demand
A large model can be loaded without its data and its tables read one
at a time. get_data() reads the table from the workspace
and caches it in the model object it returns from.
m <- load_model(ws, load_data = FALSE, verbose = FALSE)
is.null(m$parameters$pAvail$data)
#> [1] FALSE
get_data(m, "pAvail", type = "parameter")
#> tech region value
#> 1 coal north 0.85
#> 2 gas north 0.85
#> 3 wind north 0.90
#> 4 coal south 0.90
#> 5 gas south 0.35
#> 6 wind south 0.25
get_data(m, "mTR", type = "mapping")
#> tech region
#> 1 coal north
#> 2 gas north
#> 3 wind north
#> 4 coal south
#> 5 gas south
#> 6 wind south
get_data(m, "tech", type = "set")
#> [1] "coal" "gas" "wind"Changing data
update_parameter() replaces a parameter’s table; with
auto_save = TRUE the file in the workspace is rewritten at
the same time.
d <- get_data(m, "pDemand", type = "parameter")
d$value <- d$value * 1.1
m <- update_parameter(m, "pDemand", d, auto_save = TRUE)
read.csv(file.path(ws, "parameters", "pDemand", "data.csv"))
#> region value
#> 1 north 1100
#> 2 south 660The structure is unchanged, so the model can be re-solved on the new data through any route, for example in R:
m <- load_model(ws, verbose = FALSE)
solve_highs(m, attach = FALSE)$objective
#> [1] 217800Solutions
The code-generation routes write their results under
solvers/<backend>/solution/. With
load_results = TRUE (the default when a model is passed),
solve_gmpl(), solve_jump() and
solve_pyomo() read the variable values into
model$variables[[name]]$solution, copy them to
variables/, and append a line to
solution_log.txt, so the workspace holds the latest loaded
solution and a record of when it was loaded.
r <- solve_jump(m, ws)
r$model$variables$vGen$solution
r$model$solution_metadata$variables_loaded
readLines(file.path(ws, "solution_log.txt"))Folded data
A folded parameter (see Folding and
trimming) keeps both tables: the original under data.*
and the reduced one under folded_data/data.*.
save_model() writes both, load_model() reads
both, and the writers take the folded table when the model is
folded.
Reading the files elsewhere
The data files are plain CSV or Arrow and need nothing from multimod:
metadata.json lists the counts and the format so a
reader can orient itself without R.
cat(readLines(file.path(ws, "metadata.json")), sep = "\n")
#> {
#> "name": "toy",
#> "desc": "two-region capacity expansion toy model",
#> "saved": "2026-10-21 21:39:26",
#> "format": "csv",
#> "compression": null,
#> "compression_level": null,
#> "n_sets": 2,
#> "n_parameters": 4,
#> "n_mappings": 2,
#> "n_equations": 3,
#> "n_variables": 3
#> }