energyRt 0.90.0 (pre-release)
The first release since 0.50-beta, consolidating the unreleased 0.60-0.80 cycles. The modelling API is the one planned for 1.0; testing and debugging continue. Upgrading from 0.50 is not a drop-in: see Breaking changes.
License
- The per-object timeslice maps (
mTechTimesliceand its siblings,mCommTimeslice,mWeatherTimeslice), the technology activity and output domains (mvTechAct,mvTechOut),meqTechActSngand the availability-factor maps (meqTechAfLo,meqTechAfUp,meqTechAfFx, where the bound is one number per technology, region and year) are defined by a rule, the join of the small tables they are made of, and their tables are dropped at the end ofinterpolate_model(). Readers see the same tuples; the multimod route enumerates the rule itself and the saved scenario no longer carries the tables. The output and single-output domains keep their tables in a model with coarse processes.options(energyRt.map_rules = FALSE)keeps every table;options(energyRt.map_rules_verify = TRUE)checks each rule against the table it replaces. The availability-factor maps are decided per technology, region and year and recorded as rules without building their tables, in a sparse, unfolded interpolation; a full-year one-country model builds them about 3.5 times faster, with a quarter less peak memory. - Relicensed from AGPL-3 to Apache-2.0. Releases up to v0.89 remain AGPL-3.
New features
solve_by_sample()accepts a saved scenario as its first argument, or an explicitbase =: the scenario’s model drives the sequence and the scenario becomes the base, so each sample’s interpolated tables are stored as a row filter over it instead of a copy. The base must have its inputs sealed (seal_scenario()), which leaves it solvable, and only tables at or aboveenergyRt.param_ref_min_rowsqualify. A base that cannot be used falls back to copying and says why.solve_by_sample(ncap_floor =)solves the ensemble twice: freely, then against a floor ofncap_floortimes the samples’ own new capacity, so the spread becomes period variation at one plan rather than variation between plans.ncap_statpicks the mean or the median. The consensus is taken across all samples, so it does not depend on the order they were solved, and a sample that built nothing counts as a zero rather than being skipped. It is a floor, not a fix: a sample may still build above it, which marks a period the consensus plan cannot serve. The floored solves are stored as<sample>_fvariants beside the free ones and the free pass stays in$scenarios_free;$runsgains apasscolumn.ncap_floor = 0is the default and changes nothing.
Model building
- Three-part storage: charger, reservoir and discharger with their own capacities, costs and availability, coupled by
@duration,@inp2outand@inp2stg. - Vintages (
@vintage) and clusters (@cluster) expand a process into variants at interpolation;getVariants()lists them. - Resource tiers: a
commodityorweatherobject with@clustergets one balance per tier. - Weather transforms derive many series from one, and a weather object may be declared at a coarser region than the processes reading it.
- Transmission:
newACLine(),newDCLink(), an optional Kirchhoff voltage law, loss tranches (lossTranches()), route ratings (@trade$af), route ratings over a parent timeslice (@trade$afs) and weather factors of the ratings (@weather, read at the route’s source region). - Supply, import and export curves:
asSupplyCurve(),asImportCurve(),asExportCurve(). - Taxes and subsidies are indexed by instrument and can be levied on processes.
capandncapcharge technologies and trades (a trade in each region the levy is placed in); a storage’s capacity is levied by part withstg.cap/stg.ncap(reservoir),inp.cap/inp.ncap(charger) andout.cap/out.ncap(discharger). -
commodity@propertyfor physical properties; discount rates at any geoscale level; a WACC per technology withpaybackas its cost-recovery life. - Portable process definitions:
process_to_spec()/process_from_spec()(YAML/JSON),process_designer(),read_technology_xlsx()/write_technology_xlsx().
Time and space
- Calendars from timescales, including irregular calendars (months of 28-31 days), sampled calendars (representative days) and the new
m12_d365_h24andm12_d365_h24_1dpm. - Multi-level geography from geoscales: commodities and processes may sit at a coarser
@geoframethan the flows they touch, with auxiliary flows held in the finer cells beneath. - Region aggregation with clustering:
aggregate_model_regions(clusters = ),get_process_groups(),process_cluster_sweep(),model_clusters(). - Sub-territory solves from a filtered geoscale, and
subset_model_regions(). - Auxiliary flows sit at the auxiliary commodity’s timeframe: an annual emission is one variable per year, not one per hour.
Solving
Solve drivers:
solve_recursive(),solve_by_sample(),solve_by_region()andsolve_guided().First-order LP solvers (cuOpt, HiGHS PDLP), and
lang = "mm", which builds the problem matrix with multimod. Remote solving on the NEOS server and on cloud GPUs.Matrix presets (
backend = "mm") are solved bymultimod::solve_mps(): HiGHS in the R session or through Julia, cuOpt on a local GPU through WSL (mm_cuopt_wsl) or on a rented GPU (mm_cuopt_cloud,pyomo_cuopt_cloud).On the
lang = "mm"route the activity and output definitions (eqTechAfFx,eqTechActSng) and the per-process totals (eqTechOutTot,eqTechInpTot, the storage, supply, import, export and demand totals) are substituted while the matrix is built: their rows and the columns they define are not written, which reduces a full-year model several-fold. The preset fieldsubstitutenames the equations;character()builds every row. With a multimod that composes roll-ups, the commodity output and input totals (eqOutTot,eqInpTot) are substituted too.The matrix route removes
model.mps(and its compressed copy) once the solution is decoded; the index tables and the solution file stay. The preset fieldkeep_mps = TRUEkeeps it. A preset’smm_solverandwherechoose the solver and the place. A Pyomo- or JuMP-written matrix carries index tables (from Pyomo’s symbol map, from JuMP’s containers) and is decoded by multimod like its own;julia_mpswrites the JuMP matrix andjulia_cuopt_cloudsolves it on a rented GPU.interpolate_model()is about 6x faster on large models;fx_equ = TRUEwrites fixed bounds as single equality rows (about 25% fewer rows on hourly VRE models).MPS output (
solver_options$pyomo_mps),read_solution()for solutions produced elsewhere, anden_check_solve(),en_check_packages(),en_install_deps(),en_install_julia_pkgs()to check and set up the toolchains.interpolate_model(model, ..., base = REF)builds a scenario on an interpolated base when the objects added are constraints, cost terms, taxes or subsidies: only what they add is computed, the base’s tables are reused and its store is never written to, and the result equals a full interpolation. Anything else is interpolated in full, with a message saying why. The saved scenario names its base inscenario.yml.A scenario keeps the model as given in
@model: the variants, tiers and transform clones interpolation builds are rebuilt when a reader needs them. A saved scenario references its sealed model instead of copying it, also for models with vintages, clusters or a spatial sample.getObject()on a scenario returns the objects as defined.save_dataset(rehash = FALSE)andsave_dataset(overwrite = TRUE)stop when another store entry references the version they would rewrite, and name the dependents.base = REFalso takes new processes, commodities and other model objects: the per-object tables of the objects REF holds are reused and every map is rebuilt.base = REFtakes a sample of REF’s calendar or regions (a calendar of some of its timeslices, a filtered geoscale withboundary_prices): REF’s tables are reused filtered to the sample. Saved, such a scenario references the sealed REF’s tables through a row filter.
Results and project management
- A project registry keyed on path, with a lifecycle (
seal_*(),mark_delete(), …); objects are reachable by name (getScenario("base")). - Runs and artifacts:
scenario_runs(),scenario_solutions(),promote_solution(),import_solution(),drop_scenario_run(). - Results in Arrow stores, readable when larger than memory;
getData(run = )reads several runs at once. -
prepare_for_sharing()andstrip_user_info(). - The dataset store keeps versions by content under one name (
datasets/<name>/<hash>/): saving other content under a name adds a version instead of overwriting,load_dataset(name, hash = )pins one, and the registry lists every version. A references table beside the registry records which scenario points at which version. -
save_scenario()keeps a scenario’s large interpolated tables (numeric parameters, bounds and maps fromenergyRt.param_ref_min_rowsrows, 1e6 by default; weather and demand series fromenergyRt.ondisk_min_rows) in its own store and records their hashes inscenario.yml. A scenario built withinterpolate_model(base = )on a saved and sealed base references every table identical to the base’s instead of storing a copy, and keeps the rest.load_scenario()resolves the references; the lazy readers follow them; a writer that changes a table drops its reference.unseal_scenario()refuses while scenarios reference the base’s tables (force = TRUEto unseal anyway). References into the dataset store held by scenarios saved earlier still load; a weather or demand table whose version is gone is recomputed from the model. - A scenario seals its inputs and its outputs separately:
seal_scenario(x, inputs = TRUE, outputs = FALSE). With sealed inputs and open outputs it can still be solved under a new run label;save_scenario()then writes that run only, so scenarios built on it keep reading the same tables.unseal_scenario(x, inputs = FALSE, outputs = TRUE)reopens the runs alone. A scenario sealed before reads as both parts sealed. -
save_model()andsave_scenario()takemode = "ref"(default,options(energyRt.save_mode)) or"full"."ref"references a repository or model only when its exact content is a SEALED version, and copies it in otherwise;"full"copies everything in.save_scenario()no longer stores an unsealed model in the model store to reference it. Changed: a scenario whose model is stored but not sealed now carries its own copy. - A scenario whose model is a sealed version plus repositories added by
interpolate_model()references the sealed version and keeps only the added repositories (model: addedin its manifest);load_scenario()adds them back. -
list_versions()lists the versions of models, repositories and datasets with a status (current,in use,marked,unused), when they were sealed, how many entries reference them and their size (details = TRUEfor more).mark_delete(version = )marks superseded versions (sealed ones of models and repositories; a hash, or"unused"), anddelete_marked()deletes them when still unreferenced.delete_marked()also keeps an entry while any of its sealed versions has a dependent. -
list_dependents()replacesstore_dependents(): the verified dependents recorded on sealed versions (withrecursive = TRUE, transitively) plus the references found in manifests;prune = TRUEdrops stale records. - Unsealed saves of models and repositories record a save id instead of a content hash (
hashed: falsein the manifest), so saving a large model no longer serializes it for hashing. The content hash is computed when a decision needs it: saving onto a sealed entry, comparing with the sealed versions of a name, and sealing (which now loads and hashes the payload). An identical re-save of an unsealed entry is written again.options(energyRt.lazy_hash = FALSE)hashes every save. - A model whose repository holds unchanged objects of one sealed repository version (taken with
getObject()from a loaded sealed repository) references that version with the object names and the hash of the selected objects, verified byload_model(). Sealing a repository records each object’s hash for this. - Sealing a model or repository freezes its content as a version (
<entry>/<hash>/): unsealed saves update the entry in place and leave the sealed versions untouched, and references andload_*(hash = )reach a sealed version exactly. A sealed version records the models and scenarios that reference it, andunseal_model()/unseal_repository()list them and refuse while they exist unlessforce = TRUE. - Referenced tables read back into memory are dropped before the scenario shell (
scen.RData) is written. A shell aboveoptions(energyRt.shell_max_mb = 256)warns and names its largest slots. - The
mmroute no longer copies the scenario’s tables into the multimod model: multimod reads them from the scenario’s store or the dataset store as the matrix is built (multimod_from_energyRt(inMemory = FALSE)), a table held in memory is attached rather than dropped, and the memo size is the preset’sdata_cache_gb(32 by default). Once the matrix is written, the model, the memo and the scenario’s tables that are persisted on disk or in the store are released, so a solve, local or in the cloud, waits with the memory free.
Validation and comparison
-
validate_scenario_parameters()(structural versus advisory findings),verify_solution()with check groups,audit_coefficients(). -
compare_scenarios(),compare_models(),compare_inputs().
Reporting and visualisation
-
report()for every class and for models and scenarios (HTML, PDF, Word, TeX), with spatial and solved context (geoscale =,model =,scenario =). -
levcost(): analytic levelized cost of technologies, storage (LCOS) and trade (LCOT) per vintage, cluster and region; ex-post per variant on a solved scenario. -
draw()process diagrams;autoplot()for objects, calendars, horizons, scenarios and comparisons;plot_map(),plot_trade_map(),plot_trade_network(). -
sequence_coverage()reports what a solve sequence covered, one row per unit and element, andautoplot()draws it: a sampled calendar as a tile grid or a wall calendar, a region sample on its geoscale, a recursive walk as a horizon Gantt with the look-ahead years marked apart from the decided ones. Elements no unit covered stay visible as gaps. -
autoplot(<by_sample>, "histogram")draws a result’s spread across the samples, weighted by each sample’s coverage, with the free pass overlaid on the floored one whenncap_floor > 0. - The solve drivers report progress through progressr:
set_progress_bar()for a bar,verbose = TRUEfor a line per unit, either or both. - TOPIA, the teaching model (was UTOPIA), rebuilt around concentrated resources traded between zones.
Breaking changes
-
solve_myopic()is renamedsolve_recursive(), withmyopic_objective()torecursive_objective()and the result class frommyopictorecursive. The driver steps through the horizon’s milestones carrying capacity forward, which is recursive dynamic; it is only myopic in its default special case (step = 1,overlap = 0), sinceoverlapgives a step look-ahead andstep > 1solves several milestones jointly. There are no aliases.Sequences already on disk are unaffected:
variant.ymlnow recordstype: recursive_step, andread_sequence()reads the oldermyopic_stepas the same driver. sliceistimeslice, and acalendaris required; thetsl2*()helpers moved to timescales.Storage:
vStorageStoreisvStorageLevel;@cap2stg,@chargeand@infloware refused (use@duration,@startLevelor asupply); flows are bounded by converter capacity (inp.af.up/out.af.updefault to 1).technology@aeffno longer takesstg2ainp,stg2aout,sinp2ainp,sinp2aout,sout2ainporsout2aout; they were never read for a technology. A storage’s own@aeffkeeps its couplings.@start,@endand@olifeare merged into@vintage; vintage and cluster labels must be declared.@geolevelis@geoframe, and a model carries aGeoscale.Taxes and subsidies are indexed by instrument;
newConstraintS()andnewSub()are removed.Storage auxiliary couplings are part-prefixed (
inp.cap2ainp, …).The first argument is
objectin 66 functions; the scenario API is verb-first (save_scenario(),load_scenario(), …) withen_registry; 46 helpers are no longer exported; the 0.5-era deprecations are removed.The Julia and Python backends exchange Arrow files (
Arrow.jl,pyarrow);arrow_formatis split intostorage_formatandexchange_format.UTOPIA is TOPIA (regions
W1/W2,C1-C6,E1-E3); the test tiernightlyisfull.The cloud presets are renamed:
mmcloudismm_cuopt_cloud,mmcloud_crossoverismm_cuopt_cloud_crossover,mmcloud_pyomoispyomo_cuopt_cloud,mmcloud_pyomo_crossoverispyomo_cuopt_cloud_crossover; a solve with a former name stops and names its replacement.mm_engineis replaced bymm_solverandwhere; settings stored by earlier versions are still read.get_hf_token()andset_hf_token()are removed: usemmcloud::get_hf_token()andmmcloud::set_hf_token().
Bug fixes
interpolate_model(base =)refused every base when the caller also passedpath =. The base is dispatched before the control arguments are read out of..., sopathreached the addition check and was reported as “an object of class character is added; interpolating in full.” Every solve driver passespath, so none of them could build on a base, and because the refusal is a message it was invisible behind theirsuppressMessages(). An argument now counts as a model addition only if it is an energyRt object.A reloaded
solve_by_region()sequence served every variant the FIRST unit’s model. The scenario stored the model it was interpolated from, which the driver had already cut, while@modellives on the shell and is written only on the first save – so region R2’s results came back beside a model declaring R1, whose own R2 rows were empty.verify_solution()re-derives identities from that model, andlevcost()andreport()read its declarations. The driver now stores the full model with each unit’s region set, and the cut is rebuilt per variant.solve_by_region()accepted overlappinggroup =entries, or a repeated region inregions =, and solved and counted that region twice – including in the “sum of objectives” its print method presents as the whole-system cost. Overlaps are refused, naming the regions.sample_summary()’smeanis weighted by each sample’syear_fraction. Samples need not cover equal shares of the year –method = "sequential"folds a trailing singleton block into its predecessor – and the mean previously gave a larger block the same single vote. The quantiles remain unweighted, and a message says so when coverage varies.subset_model_regions()clears the parent’s recorded content hash. The subset keeps the model’s name on purpose andsave_model()updates an entry in place by name, so saving one could overwrite the full model’s store entry.read_sequence()no longer reports aby_regionsequence as complete while returning itsboundarystub counts asNA; they are declared missing.A spatial sample survives a variant switch: the problem cartridge records it, where before only the shell held it and a reload restored another unit’s regions.
A filtered geoscale announces the sub-territory solve unconditionally. The mode is accepted silently, so a geoscale covering only some declared regions used to yield a smaller solve – a different answer from the full model’s – with nothing said unless
verbose.About 130 fixes, most of which change results: unwired parameters now reach the solver, equations are no longer instantiated without data,
waf.fxbinds from both sides,aeff$cout2ainp/cinp2aoutbind, sub-region emissions are counted, coarse supply keeps its cost, andget_process_*(process = )is honoured.A tax or subsidy without
@regioncharged the coarse regions of a geoscale too, i.e. the roll-up of their sub-regions a second time. A commodity rate now applies at the commodity’s own regions and a process rate at the regions the process is declared in; a region off that level is an error.levcost()of a storage sized a charger with noinp2outorinp2stglike the discharger, and a discharger with nodurationat one hour of storage. Each is now sized by its own flow, as in the solved model.Auxiliary flows (
aeff) of a process were lost when it gave the coefficient for all timeslices and another process gave the same coefficient for named timeslices.Capacity taxes and subsidies on a storage charged nothing unless it had discharger data. They are now levied by part (see above); a levy that selects storages and sets only
cap/ncapis an error.A bound declared per region on one object and for all regions on another lost the second object’s bound equation (e.g. a fixed storage duration).
Content hashes of models with a geoscale are stable between R sessions (the S7 class object was hashed), so such models match their sealed versions in a new session.
Documentation
- A rebuilt pkgdown site with tutorial articles, and a runnable example on every public help page.
- The testing strategy (tiers, the
@coversmatrix, goldens) is documented intests/README.mdandtests/TESTING.md.
energyRt 0.50.9-dev
Breaking changes
- Time-slice weighting is rewritten: variables with a
slicedimension are no longer weighted, for consistency between sampled and non-sampled runs. Slice weights can vary across model years. - Variables with a
yeardimension are no longer weighted to interval lengths, except cumulative variables (vSupReserveCum) and capacity variables, which represent the state at the end of the period. - New-capacity variables (
vTechNewCap) are given per year — applypPeriodLento get total new capacity by the end of each period. - System costs are regrouped by type (capital, fixed O&M, variable O&M, supply, taxes, subsidies) and by process type, and the Total Costs equation rewritten to match.
Bug fixes
-
optimizeRetirment = TRUEno longer retires new technologies at the same time as their installation. -
draw()on a trade no longer repeats arrows. -
newCosts()is debugged, with an example in the Topia tutorial. -
tsl2hour()identifies n-digit hours; it previously worked for two only.
energyRt 0.50.7-dev
Bug fixes
- Several stability issues in
draw().
energyRt 0.50.6-dev
New features
-
draw()is drafted for every process class —technology,export,import,supply,demand,trade,storage.
energyRt 0.50.5-dev
New features
-
draw()is rewritten on thegridpackage and is now a generic, with methods fortechnology,exportandimport.
energyRt 0.50.3-dev
Documentation
- A
NEWS.mdfile to track changes;technology-classandnewTechnology()documented. Development version in preparation for CRAN submission.
