energyRt 0.80 (development) — the time dimension is now timeslice
Object autoplots actually plot: points + interpolation, defaults on demand
- Fixed the defect that made
autoplot()on supply / import / export / technology / storage objects report “No year-indexed data to plot” for virtually every object: the plot passedyear = NULLintogetData()‘s..., where a NULL selector matched nothing and dropped every row. NULL/empty filters are now ignored (a fix togetData()for objects generally), and the plots’yearargument is routed as the interpolation grid (getData(years = )), soautoplot(x, year = 2020:2050)extends the lines beyond the given years. - The intended display — points for given data, lines for the interpolated series — now actually renders; new argument
interpolate = TRUE(setFALSEfor given data only). Also on the tax/subsidy/constraint and demand plots. - New
show_defaults = FALSEargument (process classes): whenTRUE, parameters mapped to the model but not set in the object are drawn as dotted lines at their default values (e.g.ava.lo = 0); non-finite defaults (ava.up = Inf) are listed in the caption. - Fixed the silently-empty parameter registry behind
getData(interpolate = TRUE): it read.modInp@parameters, but the baked-in.modInpis the plain YAML list — every lookup fell back to generic interpolation. The registry is now built from the list (with bounds params expanded to.lo/.up/.fxcolumns and stale YAML slot names aliased: supply/import/exportavailability, demanddem), so object interpolation uses each parameter’s own rule and default. Interpolation expands only given columns — defaults never materialise uninvited. - Also fixed:
getData(object, interpolate = TRUE)crashed on any empty data-frame slot; demand plots dropped region-NA rows on aggregation. - New test suite
test-autoplot-objects.R; the autoplot vignette’syears =example corrected toyear =(it was silently ignored).
Technology reports: vintages, consistent layout, working PDF and docx
report() and the tech templates were overhauled around the vehicle datasheet layout:
-
Per-vintage levelized costs. A
levcost_variantsresult is no longer silently reduced to its first variant: the generic report gains a “Levelized Cost by Vintage” section (component-stacked comparison chart + per-vintage NPV table), a Vintages table, and per-vintage cost tables (a per-vintageinvcostused to display only its first row). The detail figures (components, frontier) show ONE instance — newest vintage, first cluster, same convention as the designer — re-priced with the same arguments (analytic, instant) and labeled in the section header. Key-parameter scalars (olife/start/end) go blank when several vintages make them ambiguous. -
report_generic.Rmdrebuilt on the vehicle two-column layout with ONE sizing system: fractions of the text width shared by HTML and LaTeX (no more px/\textwidth/inch mix), every ggplot rasterised at 150 dpi through one helper, LCOE labels incost/activityunits. - docx works: a Word-safe branch (markdown headings, pipe tables, embedded figures) replaces the raw-TeX output pandoc used to discard.
-
Empty columns are dropped everywhere (
.report_drop_empty_colsinreport()+ template-side pruning); NA cells render as a dash (na.string=was never a realkableargument). - PDF soundness fixes shared with the vehicle/summary templates: correct LaTeX escaping (the old
fixed = TRUEpatterns never matched$ ^ { }; brace escaping now ordered around backslash), forward-slashed image paths in HTML output, single (not double) kable escaping, the vehicle share-panel px/pt bug, aligned HTML/LaTeX column fractions, and the input/outputrbindcolumn mismatch. -
report()now passes only the params a template declares, so older or user-supplied custom templates keep rendering asreport()grows new params; the container levcost whitelist was synced with the technology one (repo,method,full_output). - New test suite
test-report.R(31 assertions; pandoc/LaTeX-gated).
levcost() computes analytically — no solver required
The unit-demand annual mini-model that levcost() builds has a closed-form optimum for most technologies, and levcost() now computes it directly. New argument method = c("auto", "analytic", "solve"):
-
"auto"(the new default) prices the technology analytically — no GLPK or any other solver needed — whenever it qualifies, and falls back to the solver otherwise with a message naming the reason."analytic"refuses non-qualifying technologies instead of falling back;"solve"forces the previous behaviour. - The analytic engine (
R/levcost_analytic.R) mirrors the model equations one for one — activity/output/input chains (cact2cout,use2cact,cinp2use,ginp2use,cinp2ginp), annual availability (af/afs, weather collapsed to a CF), the greedy build–retire–rebuild capacity schedule, EAC annuities (wacc/payback/olife), fixom/varom/cvarom/avarom, auxiliary flows, and supply pricing — and reproduces the solver’s numbers to the GLPK output precision (parity-tested intest-levcost-analytic.R). -
Group shares report every corner solution. The optimum over a share polytope is a vertex; the analytic result evaluates all vertices of the input and output share polytopes and returns them in
$frontier_vertices(share vector, per-activity NPV cost breakdown,optimalflag), with the cost-minimal corner as the headline levcost. The classic$frontier/$levcost_by_*corner tables are produced as before. - A vintaged/clustered technology is priced per cell directly — no artificial-region isolation, no per-cell LP — which makes many-variant technologies essentially instant.
- The analytic result has the same fields (plus
$method = "analytic");$scenarioisNULL. Not representable analytically (solver still used): technology chains,timeframe = "native",afc.*bounds, availability lower bounds,optimizeRetirement, year-varyinginvcost, constrained supplies, and group substitution combined with year-varying prices. - The process designer and
report(levcost = TRUE)usemethod = "auto", so the levcost tab and report sections work on machines with no solver installed. - Fixed in passing: on the solve path the annual capacity factor collapsed from
@weathernever reached the solver (the mini-model was built from the pre-collapse objects), so weather-driven technologies were priced at full availability. Both paths now apply the CF.
techspec containers: YAML and JSON
The techspec format (read_techspec(), tech_from_spec(), tech_to_spec(), the process designer) now reads and writes JSON (.json) as an alternative container for the same validated structure — tech_to_spec(tech, file = "x.json") writes it, read_techspec() and the designer’s spec upload/gallery accept it, and the designer gained a “Save JSON” button. Full numeric precision is preserved. (A fuller NEWS section for the process designer itself is pending.)
Stack-wide rename slice -> timeslice (paired with the timescales package; matches the TIMES/OSeMOSYS vocabulary and reads unambiguously next to the spatial region dimension):
- The set/index family in all four model backends (GAMS, GLPK/MathProg, Pyomo, JuMP):
slice/slicep/slicepp/slice2->timeslice/timeslicep/…, everymSlice*/pSlice*symbol ->mTimeslice*/pTimeslice*(e.g.mTimesliceParentChild,pTimesliceShare), and theANYSLICEwildcard ->ANYTIMESLICE. Equation-level short aliases (s,sp, …) are unchanged. - Solution output CSVs now carry a
timeslicecolumn. - S4
calendarslots:slice_share/slice_family/slice_ancestry/slices_in_frame->timeslice_*/timeslices_in_frame. - Input-data columns are
timeslice; a compatibility shim accepts the pre-renameslicename in user data (new*constructors andupdate(),newCalendar()timetables,newConstraint()for.each/for.sum,fold_/unfold_scenario_parameters()dims) – renamed with a once-per-session warning. External datasets (IDEEA) keep working through the shim. - Bundled data (
calendars,utopia_*,model_structure) andsysdataregenerated with the new vocabulary. - multimod’s matching update is a recorded follow-up; until then it pairs with pre-v0.80 generated models.
energyRt 0.74.0.9000-dev
The user config moved out of the home directory
en_config_write() now writes to tools::R_user_dir("energyRt", "config") rather than ~/.energyRt/config.yml. CRAN policy does not permit a package to write in the user’s home filespace, and the R >= 4.0 user directories are the sanctioned alternative. The old location is still read, so existing setups keep working; when you next call en_config_write() the legacy files (~/.energyRt/config.yml and the deprecated ~/.energyRt.R) are renamed to *.bak, since the latter is sourced at attach and would otherwise keep overriding the new config. Pass backup = FALSE to leave them alone.
An unservable demand no longer aborts interpolation
interpolate_model() warns instead of stopping when a demand commodity has no supply, production, trade or import, so an incomplete model can be inspected and handed to the solver. Restore the previous behaviour with options(en.model_checks_stop = TRUE).
Settings — one registry, one prefix, one config file
Package options were spread over four unrelated mechanisms: the options registry, bare getOption() calls, load-time side effects, and an R script sourced from the home directory. They are now a single documented layer.
Option names are prefixed en. (breaking). Every option is declared with the same naming scheme instead of one hand-written name per declaration, so options(verbose = ) becomes options(en.verbose = ), options(solver = ) becomes options(en.solver = ), and so on. The old names were un-prefixed and collided with base R — verbose in particular. The documented API is the get_*() / set_*() functions, which are unchanged.
Environment variables are prefixed ENERGYRT_ — ENERGYRT_GAMS_PATH, ENERGYRT_JULIA_PATH, and so on. This also fixes glpk_path, which was the only lower-case one. The old un-prefixed variables (GAMS_PATH, JULIA_PATH, …) still work for one release and warn once. NEOS_EMAIL deliberately keeps its bare name: set_neos_email() exports it so the Pyomo subprocess inherits it.
New: a persisted configuration file. en_config_write() saves the current settings to ~/.energyRt/config.yml (or ./.energyRt.yml for one project), en_config_read() reads it back, and energyRt applies it when the package loads — without overriding anything already set through an R option or an environment variable. Sourcing ~/.energyRt.R still works but is deprecated.
New: en_config_show() prints every option with its current value and where that value came from — option, environment variable, config file, or package default. This is the first thing to run when a solver is not being found.
New: set_solver_path() / get_solver_path() — the generic form of set_gams_path(), set_julia_path() and the other four, which are now thin wrappers over it rather than five copies of the same body. A path that does not exist is still rejected at the point of the mistake.
New: ?energyRt-options documents all seventeen options with their defaults, option names and environment variables. It is generated from the declarations, so it cannot drift.
Verbosity is one setting, not two. en.verbose is a level (0, 1, 2, …; TRUE/FALSE are read as 1/0), tested with isVerbose(level). The separate energyRt.verbose option is deprecated and honoured for one release. en.debug gained a matching isDebug() and now actually does something: it gates internal consistency warnings that used to be silent — or, in one case, used to call browser().
Other option changes. en.neos_endpoint is now a declared option rather than a bare getOption(). en.progress_bar is wired to set_progress_bar() / show_progress_bar() instead of being declared and never read. The internal “GDX library already loaded” flag is no longer a user-visible option. data.table::setNumericRounding(2) moved from source time into .onLoad().
Dead model code removed — LEC and the trade-cost aggregators
An audit of all 237 mapping parameters (dev/audit-dead-maps.R, measuring “populated in any of six models” against “referenced by any live solver template”) found four that nothing could ever fill. They are gone, along with the equations they gated:
-
LEC (
eqLECActivity,meqLECActivity,mLECRegion,pLECLoACT). Themodel@LECdataslot had already been commented out, so there was no way to supply data; the map builders were identity no-ops and the constraint iterated an empty set in all four backends. Removed from GLPK, GAMS, Julia and both Pyomo templates. -
mvTradeCost/mvTradeRowCost, which gatedeqCostTradeandeqCostRowTrade— equations already inside a$ontextblock in the GAMS template. Trade costs reach the objective throughvTradeEac,vTradeFixom,vImportIrCostandvExportIrCostinstead. (There is novTradeVarom: the activity-side cost arrives viaeqImport/ExportIrCostfrompTradeIrCost+ markup.) These followmvTradeIrCost, retired the same way earlier.
Every written .dat loses five now-meaningless lines (set mvTradeCost := ; and friends). Objectives are unchanged — the removed equations were vacuous, verified by solving single- and multi-region UTOPIA before and after and comparing to the last decimal.
Not removed, despite looking similar: mTechAfUp / mTechAfcUp are also forced empty, but af.up still binds through meqTechAfUp + pTechAf. They are redundant domain maps, not the binding mechanism.
Fixes
-
get_scenarios_path()was defined twice, inR/options.RandR/utils.R; the second silently shadowed the first. Both it andset_scenarios_path()now live inR/options.R. -
.call_solver()restored the working directory only from its error handlers, so a non-error early return left the session inside the solver run folder. It now restores on every exit path. -
interpolate_model(ondisk = TRUE)referencedmi_pathbefore it was assigned — it was only initialised on the in-memory branch. - The default solver reported
lang = "glpk"whilesolver_options$glpkreportedlang = "GLPK", so the two compared unequal; only a case-insensitive dispatch hid it. - Two test scripts changed the default solver without restoring it, leaking into every test that ran after them.
energyRt 0.70.5.9000-dev
Renames — main data slots, the sub class, and the plotting vocabulary
All of the below are breaking. Nothing in this group changes the generated solver models: every GAMS/GLPK file written before and after the renames is byte-identical, because the modInp set and parameter names were deliberately left alone.
Main data slot of class X is now named X. The four commodity/flow classes carried an abbreviation instead of the class name, so an object printed as SUP_COA@availability rather than the predictable @supply:
| class | slot | constructor argument |
|---|---|---|
demand |
@dem → @demand
|
dem = → demand =
|
supply |
@availability → @supply
|
availability = → supply =
|
import |
@imp → @import
|
imp = → import =
|
export |
@exp → @export
|
exp = → export =
|
demand’s value column is renamed too (dem→demand), since it shares the slot’s name. The other three keep their short column prefixes (ava.lo/up/fx,imp.*,exp.*), andprice/costare unchanged — they encode a real distinction (market price for import/export vs. internal extraction cost for supply).The break is not symmetric, which is worth knowing before you rely on it.
dem =,imp =andexp =still work silently: each is a unique prefix of its new name and sits before..., so R partial-matches it and the data lands in the right slot.availability =is not a prefix ofsupplyand fails loudly withUnidentified slot(s): "availability". Ademcolumn inside the data frame does fail, withUnknown column "dem" in the slot "demand".
The sub class is now subsidy. It was the only class whose name was an abbreviation, it collided with base::sub(), and its own parameter metadata already called it subsidy:
- S4 class
sub→subsidy; its main data slot@sub→@subsidy(columnsinp/out/balunchanged). -
newSubsidy()is the constructor;newSub()remains as an alias, andsub =still partial-matchessubsidy =. - The modInp set dimension
"sub"and the parameter namespSubCostInp/pSubCostOut/pSubCostBalare unchanged.
One vocabulary across the plotting layer.
-
geo_map()→plot_map().geo_*isgeoscales’ prefix; energyRt’s map entry points are nowplot_map()andplot_trade_map(). - The first argument of every
plot_*()function and of thedraw()generic is nowobject(was a mix ofobject,x,objandscen).plot()keepsx, y— base R fixes those. -
years→year, matchinggetData(),getMix()and theyearset. -
typenow always means the quantity shown (getMix(),plot_map(): generation/capacity/new_capacity/fuel). The chart shape is nowstyle:autoplot(dem, style = "area"),autoplot(wthr, style = "heatmap"). -
plot_demand(),plot_weather()andplot_process_windows()are no longer exported — all three are reachable throughautoplot(), and two public names for one chart is what this pass removed. Still exported are the plotters no generic reaches:plot_heatmap()(accepts a data frame or a named vector),plot_map(),plot_trade_map()andplot_share_frontier().
What replaces the un-exported functions. plot() now delegates to autoplot() for every class that has one — 17 S4 classes plus levcost and levcost_list — so whichever verb you reach for works and autoplot() stays the single implementation. draw() keeps the schematic-diagram role and still needs no ggplot2. New theme_energyRt() is the one place the package’s ggplot look is set.
-
autoplot(),autoplot(model)andautoplot(repository)now fail with energyRt’s own message when ggplot2 (a Suggests) is absent, instead of R’s barethere is no package called 'ggplot2'.
levcost() prices vintages and clusters separately
levcost()on a technology declaring@vintageor@clusterwas silently wrong. Its mini-model prices one process against a unit demand, butinterpolate_model()expands a vintaged technology into one process per cell, so the cells competed for that single unit and the extraction summed across them. No error — just one number where there should have been one per vintage.Each cell is now priced in its own region (
IND_VIN2030), so the variants cannot serve each other’s demand, and results are extracted by filtering to that region. Region is the only axis that works:vTotalCosthas notechdimension but is on a region × year grid, so per-cell cost can only be attributed regionally.Such a technology now returns a
levcost_variantsobject — a named list of ordinarylevcostresults, one per variant, whichautoplot()compares directly.levcost_by_variant(x, what =)stacks the per-variant tables withvintage/clusterkeys. A technology with no vintages or clusters is unaffected and still returns a singlelevcostobject.New
run = c("single", "sequential")argument."single"(default) prices every variant in one model; if that does not solve it retries with the dummy-import slack and then falls back to one model per variant, so one bad cell cannot take the rest down."sequential"goes straight to per-variant models.max_failures(default 10) bounds the fallback.$frontierisNULLon the per-variant path: the frontier corners are a per-commodity sweep, orthogonal to variants, and are not fanned out per cell.
Fixes
-
size()regained its@export: a helper inserted between its roxygen block and the function had silently taken over the block, so the nextdocument()would have droppedsizefromNAMESPACEand exported.instance_slotsinstead.
Mixed-resolution commodities (commodity@geolevel)
A commodity can now be balanced at a coarser geoscale level than the model’s own regions — steel nationally while electricity stays per-state, in one model.
newCommodity("STEEL", geolevel = "nation"). This is the spatial twin ofcommodity@timeframe, and it removes the old workaround of modelling the commodity per region plus trade routes between every pair purely to let it move.Commodities remain region-invariant objects: there is still no region slot.
@geoleveldeclares only at what spatial resolution the balance is written. It defaults to the finest level, which is exactly today’s behaviour.Balancing a commodity at a coarse level asserts free, unlimited transport of it within that level. That suits a good with a genuinely integrated market; it is never right for a network-constrained carrier such as electricity. The test suite pins this: a coarse balance must give the same objective as the same system modelled with per-region commodities plus a zero-cost, unlimited trade route.
Implementation reuses the balance layer the aggregation rewrite already built for time. Two new maps —
mRegionFamily(immediate parent→child regions, the spatialmSliceFamily) andmCommRegion(the level a commodity is balanced at) — plus one extra term ineqOutTot/eqInpTot. There is deliberately nopRegionAgg:pSliceAggexists only because slice values are intensive rates needing renormalisation, whereas regional quantities are extensive and simply add up.With a geoscale attached,
sets$regionholds every level at once — the regions, the zones and the nation together — mirroring howsets$slicealready holds the slices of every timeframe. The coarse members are inert until a commodity names one. A geoscale covering more ground than the model (a world map for a two-region model) is pruned, so only ancestors of declared regions enter the set.A process may sit at a coarse region (national demand for a nationally-balanced commodity), but never coarser than a commodity it uses — its flows could not reach that commodity’s balance and would silently vanish. This is now an error, mirroring the finest-timeframe rule for processes.
GLPK and GAMS only. Julia and Pyomo do not carry the aggregation term, so writing a model with a non-default
@geolevelfor them raises rather than silently solving a different problem — the same treatmentpaybackalready gets.Note: attaching a geoscale makes
pWacc,pSdrandpDiscountFactorslightly larger on disk. TheirANYREGIONwildcard would now also cover the coarse regions, so folding correctly declines and the values stay explicit. Same values, same LP.
Geographic information for model regions
A model can now carry a geoscale — a
geoscales::Geoscaledescribing how its regions nest into coarser levels, what they weigh, and where they are on a map. Attach one withnewModel(..., geoscale = gs)orsetGeoscale(), and read it back withgetGeoscale().config@regionstays authoritative and is always the finest level. A region the geoscale does not cover is a warning at interpolation time, not an error. (When this feature first landed a geoscale was presentation-only;commodity@geolevelabove now also gives it a role in the optimisation model.)geoscalesis an optional (Suggests) dependency. Storing, printing, saving, interpolating and solving a model that carries a geoscale all work without it installed; only maps and region-level aggregation need the package, and they say so.geo_map()draws a choropleth of results —"generation","capacity","new_capacity"or"fuel"— over the model’s geography, optionally aggregated to a coarser level.plot_trade_map()now accepts aGeoscaleas itsmap, falls back to the model’s own geoscale when none is given, and can draw at a coarserlevel(routes internal to an aggregate region drop out).Bug fix:
plot_trade_map()drew routes withgeom_segment()on rawx/yalongside ageom_sf()layer. That works only when the map has no CRS, which is true of the referenceutopia$maplayouts but not of real data — with a CRS,geom_sf()installs acoord_sf()that reprojects the polygons and leaves the routes behind. Routes, centroids and labels are now drawn assflayers whenever the map is projected.Aggregation across regions is delegated to
geoscales::geo_recast(), with the rules read off the variable catalogue rather than hardcoded — the same approach.is_state_var()takes for the temporal roll-up.vTradeIris identified by its declaredrole: flowand is netted, not summed: a flow between two regions that end up in the same aggregate is internal to it and cancels. Noterole: stockis a temporal exclusion only — a storage level must not be summed over slices, but summing it across regions is meaningful and is done.utopia_geoscale()builds a geoscale for the UTOPIA reference model (nation -> zone -> regionoverR1…R11), with geometry from any of the fourutopia$maplayouts. The hierarchy itself ships as the plain tableutopia$geo, sodata/carries no class from an optional package.getCalendar()was declared as a generic but had no methods, sogetCalendar(mod)failed. Methods added forconfig,modelandscenario, alongside the newgetGeoscale().
energyRt 0.70.4.9000-dev
A variable class, mirroring parameter
Model variables are now S4 objects.
scenario@modOut@variablesis a named list ofvariableobjects, exactly asscenario@modInp@parametersis a list ofparameterobjects, and both now extend a shared virtual classmodelDatathat carries the in-memory / on-disk storage contract.A
variableknows what it is: its model dimensions, the column names it is written out with, its gating map, whether it is declared positive, itsrole(source,sink,flow,activity,balance,stock,capacity,cost), aunitkind, and whether it comes from the solver or is computed in R. The specification is composed at build time from the GAMS source, the GLPK template and the newdata-raw/variables.ymloverlay, which is validated for completeness so it cannot fall behind the model.modOutpre-populates every declared variable, so a variable the solver skipped (no non-zero values) still reports its column names instead of being absent entirely.Facts that used to be hard-coded now come from the specification: the “never sum this over slices” rule (was
.timeframe_state_vars <- c("vStorageStore")) and the sign of each series in a generation mix.getData()is unchanged for callers.
Breaking changes
scenario@modOut@variables$vTechCapreturns avariable, not a data.frame.$,[[,dim(),names()andas.data.frame()work on it, so reading...$vObjective$valuestill works;merge(),rbind()andcolnames<-do not. UsegetData()as before, orget_variable(scen, "vTechCap").The on-disk scenario layout changed: a variable’s data moved from
variables/<name>/tovariables/<name>/data/. Scenario directories now carry alayoutfile, andload_scenario()refuses an older one rather than silently reading it as empty. Re-save affected scenarios.vTechRetiredNewCapread from a GDX now has its second year column namedyearp, matching every other engine. Thesrc/dstrenaming forvTradeIris likewise applied to all engines rather than to GDX alone.vDummyImportCost/vDummyExportCostare the solver’s own values. An R recomputation used to overwrite them after every solve with an unweighted, slice-resolved version whose shape did not match the declaration; it has been removed.parameter@misc$nValues(a cached row count) is gone. It was used to truncate a parameter’s data in the GAMS, Pyomo and Julia writers while GLPK never truncated, so a stale count made those engines emit less data than GLPK for the same scenario.
Bug fixes
summary()on a solved scenario never reported dummy import/export costs, because it read a variable name that has never existed. Dummy flows mask infeasibility, so this was invisible to every user.model_structureattached each variable’s dimensions and gating map to the wrong variable: it took names from one generated table and dims from another, and the two carry the same names in a different order.model_size()counted a gating map’s rows once however many variables it gated, understating models whose maps gate several variables (mvStorageStoregates three)..get_data_slot()read a parameter’s data slot directly, so all of its call sites saw zero rows for an on-disk parameter. It is now the same on-disk-aware reader asget_data_slot().update_parameter()built an on-disk path missing itsparameterssegment, pointing at a directory that never exists.
energyRt 0.70.3.9000-dev
Two discount rates: WACC and SDR
A model now has two rates rather than one.
wacc(weighted average cost of capital) annuitises investment into the equivalent annual cost;sdr(social discount rate) discounts the stream of system costs in the objective. They are independent, with no fallback from one to the other.discountsurvives as an argument only, the shorthand for the simple case where one rate plays both roles:newModel(discount = 0.05)is exactlydata.frame(wacc = 0.05, sdr = 0.05)and every existing model is unaffected. Per row you supply eitherdiscount, or bothwaccandsdr; a partial pair or a mix of the two forms is an error.Technologies, storages and trades can carry their own
@invcost$wacc, which overrides the model-wide rate when annuitising that process. There is deliberately no per-processsdr— a social discount rate is a property of the model, not of a technology.@invcost$eacis now honoured: supply the annuity directly and it is used verbatim, in place of annuitisinginvcost. Rows left empty are computed as before.New
@invcost$payback, the cost-recovery period. Where given it replaces the operational life both in the annuity and in the years over which the annuity is charged, so the investment is repaid overpaybackyears while the capacity keeps operating for its full@vintage$olife. Must be positive and no longer thanolife. Implemented for GLPK only; the GAMS, Pyomo and Julia writers refuse a model that sets it. A payback that does not land on a milestone boundary is reported with the span actually charged.
Breaking changes
config@discountno longer has adiscountcolumn; the table holdsregion,year,waccandsdr. Code that read the column should readwaccorsdraccording to which job it means.The
pDiscountparameter is replaced bypWaccandpSdr.pDiscountFactoris unchanged in name and meaning but is now built fromsdr.pTechEac,pStorageEacandpTradeEacnow read@invcost$eacinstead of@invcost$invcost. This also resolves a latent collision in whichpTechInvcostandpTechEacregistered the same class/slot/column key.levcost()annuitises at thewaccrather than at whichever ofsdr/wacc/discounthappened to be set first;report()prints both rates under their own names.
Capacity vintage and cluster
Process classes (
technology,storage,trade) can now describe a group of similar processes whose characteristics evolve over time (vintage) or across space/type (cluster). Add avintageand/orclustercolumn to the data slots (capacity,invcost,fixom,varom,af,ceff,geff,aeff, …) and the object is replicated into one ordinary process per cell before the model is built. Equations and set dimensions are unchanged, so vintaging costs nothing in model structure.New
@vintageslot on all three classes, with columns(vintage, region, cluster, start, end, olife). It replaces@start,@endand@olife, which are removed as slots. The constructors andupdate()still acceptstart=,end=andolife=arguments and fold them into@vintage, so existing code keeps working; saved objects from earlier versions must be rebuilt. All three ofstart,endandolifenow carry aregiondimension.New
@clusterslot ontechnologyandstorage, declaring cluster names with an optional description, region and ordering. Clusters used only in the data slots are still picked up, so the slot is optional.tradehas no cluster dimension –@routesalready provides the multiplicity.Variant names are formed by suffixing the base name, e.g.
ECOA_VIN2030_CLnorth. The prefixes are controlled by@config@variant_prefix(defaultc(vintage = "_VIN", cluster = "_CL")), inherited by@settings, with a collision check across all process names and a consistency check if both copies are set.getData()gainsvariants = TRUE(the default), attachingbase,class,vintageandclustercolumns to results keyed on a process. NewgetVariants(scen, class = )andvariantSummary()report the expansion; the provenance table lives inmodInp@sets$variant.A
vintage = "TOTAL"(orcluster = "TOTAL") row in@capacitybounds the sum over the variants of that process rather than each one, generating a single group constraint.storage@cap2stgis promoted from a scalar to a data.frame(vintage, cluster, region, year, cap2stg).
Breaking changes
trade@capacityVariablehas been removed: a trade’s capacity is always a decision variable, as for technology and storage. In this version the flag gated nothing — the capacity variable, its equations, and the investment and fixed O&M charges were built either way; its only remaining effect widened the flow domain. For a fixed transfer limit with no investment, drop@capacity/@invcostand settrade = data.frame(src =, dst =, ava.up = <limit>). PassingcapacityVariabletonewTrade()orupdate()now errors with that remedy. ThemTradeCapacityVariablemapping is gone.@start,@endand@olifeare no longer slots (see@vintageabove); read sites must use@vintageor the.proc_lifespan()helper.Passing an unrecognised slot name to a constructor now errors instead of being silently dropped.
Bug fixes
A per-region group capacity bound was applied to each region’s variants separately instead of to their sum, letting a region build far past its declared cap.
getData()returned no rows for parameters selected with a wildcardslice = NA.check_name()had inverted guards, so some invalid names passed and some valid ones were rejected.pTradeIrEffdeclaredslot: teff, butteffis a column of@trade, so the parameter was never populated.
energyRt 0.50.9-dev
New features & critical changes in the model code:
Weighting of time-slices in a subset has been revised and rewritten. Since this version, all variables with ‘slice’ dimension are not weighed for consistency of slice-level operation across sampled and non-sampled model runs.
Slice-weights can vary across model years.
-
All variables with ‘year’ dimension are not weighted to the interval lengths.
Exception:cumulative variables (
vSupReserveCum, etc.) which have to account the interval length for every milestone year.capacity variables (
vTechCap, etc.) represent the state of the variable by the end of the period, including any accumulation or retirement of capacity over the interval of the milestone year.
New capacity variables (
vTechNewCap, etc.) are given for a year. Period length (pPeriodLen) must be applied to the annual capacity additions (such asvTechNewCap) to get total new capacity of a process by the end of each period.System costs have been regrouped by type (capital, fixed O&M, variable O&M, supply, taxes, subsidies) and by process type (technology, trade, storage, etc.) to facilitate the analysis of the cost structure. Total Costs equation has been rewritten to reflect the new cost structure.
Bug fixesEarly retirement option (
optimizeRetirment = TRUE) is corrected to exclude retirement of “new” technologies at the same time as their installation.draw()method for ‘trade’ is fixed to exclude repeated arrows in the plot.newCosts()is debugged, an example is added to the Utopia tutorial.tsl2hour()fixed to be able identify n-digits hours (previously worked for 2 only).
MiscellaneousA new version (3) of the logo design idea (by DALL-E).
The code clean-up, testing, and documentation are in progress to comply with CRAN requirements.
add_weightsandadd_intervalsarguments will be added togetData()function to add time-slice weights and interval lengths to the requested data if applicable.
energyRt 0.50.7-dev
- Fixed a few stability issues in the
draw()method. - Added “Hello World” example to the tutorial.
- A new version of the logo design idea (by DALL-E).
- The code clean-up and documentation are in progress to comply with CRAN requirements.
- The very first draft of the package CRAN-like manual is added.
- The version might be unstable due to ongoing changes.
energyRt 0.50.6-dev
- draw() is drafted for all processes: ‘technology’, ‘export’, ‘import’, ‘supply’, ‘demand’, ‘trade’, ’storage
- docs completed for main classes with examples.
- code clean-up and documentation in progress.
energyRt 0.50.5-dev
- draw() is rewritten based on ‘grid’ package, and is now a generic method.
- added draw() methods for ‘technology’, ‘export’, and ‘import’ classes.
- fixed several interface-level bugs introduced in 0.50.4-dev during clean-up and documentation.
energyRt 0.50.4-dev
- Documentation of classes is in progress (~70% docs completed).
- Logo-search has started! “logo” page added.
- Website is reshaped, added new, not populated yet “articles”.
- !!! Not Tested!!! Due to the ongoing changes in both documentation and functions/methods clean-up, the version may have “surprises” - tests are in progress.
