energyRt (development version)
License
- energyRt is relicensed from AGPL-3 to Apache-2.0. Releases up to and including v0.89 remain available under AGPL-3.
Breaking changes
The test tier
nightlyis renamedfull(check < fast < cross < full).ENERGYRT_TEST_TIER=nightlyis rejected rather than aliased, and the report of a full run is written totmp/test-reports/full-<timestamp>-<pid>.md.The parameter behind
trade@varom$varomis renamedpTradeIrCost->pTradeIrVarom, with no alias: amodInpsaved under the old name must be rebuilt from source.pTradeIrMarkupand the import/export cost variables keep their names, because each of those variables combines both parameters.Auxiliary flows (
@aux/@aeffon technologies and storages) sit at the AUXILIARY commodity’s timeslices rather than the process’s, so an ANNUAL aux commodity gets one row per year. Capacity-type couplings (cap2ainp,ncap2ainp,pho2a*,ret2a*, and the storage part couplings) are now per-year coefficients booked into each aux slice as its share of the year; a coupling that named a singletimesliceto work around the old per-slice division should drop it.The scales core is
nestedscales(formerlymultiscales),clusterscalesis merged into it, andgeoscales/timescales(>= 0.6.0) are required.get_process_groups(),process_cluster_sweep()andmodel_clusters()are unchanged for callers.The timeslice label conversions are gone from energyRt.
tsl2hour(),tsl2yday(),tsl2month(),tsl2dtm(),dtm2tsl(),hour2HOUR()andyday2YDAY()now live in timescales, which reads the timeframe from the CALENDAR instead of parsing the label text – so they take the calendar:timescales::tsl2hour(tsl, "d365_h24"). timescales moves from Suggests to Imports. A hard break, no aliases.tsl2year(),tsl_guess_format()and thetsl_formatsdataset are removed and not replaced. Guessing a layout from the label text is what the calendar replaces, and no timescales calendar carries aYEARtimeframe – the year belongs in a column, not in the label. They are kept for reference indrafts/deprecated-timeslice-conversions-2026-09-26.R.calendaris now required wherever a timeslice layout is needed.plot_timeslices(),plot_heatmap()andautoplot()on a timeslice-indexed object take acalendarobject or the name of one ("d365_h24"); without it the timeslices are drawn in order rather than laid out by timeframe.storage_duration()takes the calendar from the scenario, so it needs nothing new.The three unrelated things called
shareare now named for what they are a share OF.@cluster$shareiscap.share.fx(the capacity ratio, an equality),@ceff$share.lo/up/fxisgrp.share.lo/up/fx(the input-group mix for co-firing), and the newact.share.lo/up/fxbounds a cluster’s share of its family’s throughput.lossTranches()emits the new name. The timeslice share (calendar@timetable$share) is untouched.-
The
topiadataset now carriesgeoscales, a named list of four [geoscales::Geoscale] objects (squares,honeycomb,island,continent) holding the region hierarchy and that layout’s polygons.topia$map,topia$geo,topia$modules$mapsand thetopia_geoscale()function are removed: the dataset used to store the same polygons three times and the hierarchy twice. Replacements:was now topia_geoscale()topia$geoscales$honeycombtopia_geoscale(layout = "island")topia$geoscales$islandtopia_geoscale(region = r)geoscales::filter_geoscale(gs, "region", r)topia$geogeoscales::geoscale_leaftable(gs)topia$map$honeycombgeoscales::geoscale_geometry(gs, "region")geoscalesmoves from Suggests to Imports: deserialising an S7Geoscaleloads thegeoscalesnamespace, so it can no longer be optional.timescalescomes along as a transitive dependency ofgeoscales. TOPIA regions are renamed
W1,W2,C1..C6,E1..E3(wasR1..R11), in west-to-east order. Each layout now assigns the codes so that every zone is one contiguous block on the picture; the hierarchy, the model kits (R1/R3/R7/R11) and the regional endowments are unchanged. Trade objects follow the region names (TBD_ELC_R1_R2is nowTBD_ELC_W1_W2), and the TOPIA goldens are rebaselined: all objectives are unchanged.getData(process =)now accepts a character value, which SELECTS the named process(es) rather than being read as a logical. A character value is always a selection, soprocess = "TRUE"is a process namedTRUE, notTRUE. The logical form is unchanged.calendargains two slots (year_start,utc_offset_minutes) andhorizon@intervalsalabelcolumn. Objects saved earlier still load – every read is guarded and falls back to the January-1 default – but a calendar written now cannot be read by an older energyRt.The teaching model is renamed UTOPIA -> TOPIA (Latin topia, from Greek topos, “place”): the dataset
utopiais nowtopia,utopia_geoscale()/utopia_profile()/utopia_profiles()aretopia_*, and the calendarutopia_seasonsistopia_seasons. There are no aliases. Article URLsarticles/utopia-build.htmlandutopia-use.htmlredirect.calendarsships GENERIC calendars only.season_dn,topia_seasons,unit_s4andunit_s4h4are no longer in it: a model’s own calendars now travel with the model, intopia$modules$calendarsandtopia$modules$unit$calendars. Pass those as objects – thecalendar =name lookup resolves against the shipped list only.New
calendars$s4_hp3(four seasons xDAY/NIGHT/PEAK, 12 timeslices), the catalog’s regular twin of the retiredseason_dn/topia_seasons.d365andd365_h24are now imported from the timescales catalog rather than rebuilt locally; labels, shares and sums are unchanged.The timeslice-decomposition helpers are no longer exported:
tsl2dtm(),tsl2year(),tsl2yday(),tsl2hour(),tsl2month()andtsl_guess_format(). The time dimension istimescales’ domain; these stay as internals only because the plotting and storage-duration code still needs them, and move out once timescales provides equivalents.The deprecation layer is removed, with its
?energyRt-deprecatedhelp page. These names warned through the 0.8x series and are now gone:solve_mod()/solve_scen()(usesolve_model()/solve_scenario()),register()(add_to_registry()+save_registry()),get_registry()(load_registry()),get_entry()/find_registry()(find_in_registry()),get_entry_object()(getScenario()),registry_exists()/registry.exists()(file.exists(get_registry_file())),set_default_registry()/use_registry()(set_registry_file()),which_registry()(get_registry_file()),tech_designer()/tech_from_spec()/tech_to_spec()/tech_spec_code()/tech_spec_issues()(theprocess_*()equivalents),read_techspec()/read_procspec()(read_process_spec()),write.sc()(write_sc()),make_scenario_dirname()(set_path_builder(scenario_dir = )),levcost_by_variant(x, what)(levcost(x, by_variant = what)),get_data()(getData()) andget_units()(getUnits()).The four standalone TOPIA datasets are removed:
topia_weather,topia_demand,topia_stockandtopia_modulesaretopia$weather,$demand,$stockand$modules.The mosox back-end experiment is gone; it was never functional and now lives in
drafts/.Two rows of one object slot that set the same parameter at the same key are refused at construction, naming the column, the key and both values. An
NAin a key column means “all members of that dimension”, so such rows claimed one cell: nothing chose between them and the interpolation join repeated the row once per duplicate, handing the solver a multiplied parameter. Keys are per parameter, so two columns of one slot can key differently. A repeated key is refused whatever the values, equal ones included.A repeated id tuple in an interpolated parameter is a structural finding and stops the interpolation, where it used to warn.
interpolate_model()also errors if a value series multiplies while being interpolated, rather than writing out the multiplied result.trade@vintageno longer carries aregioncolumn: a trade’s lifespan belongs to the route, which is howpTradeOlifeand the span maps have always been indexed. A per-regionstart/end/olifeon a trade is now refused at construction, naminginvcost/fixomas where per-region trade costs go; an all-NAcolumn is dropped, so objects written earlier still load.The solve drivers refuse to write over a variant that a different configuration produced. Variant labels are unit-derived, so re-running
solve_by_sample()with another seed, orsolve_myopic()with another window, used to replace the first sequence’s results silently; the error names what differs, andoverwrite = TRUEproceeds. Same method, same settings is still a redo.Decoded
vTradeIrnames its endpoint columnssrcanddston every backend. Pyomo and JuMP emittedregion/regionp; GAMS and GLPK already usedsrc/dst. Code readingregionpfrom a trade result needs updating.scenario_artifacts()gains akindcolumn and adds scenario-level rows for the derived tiers ("reports","levcost") beside the"run"rows;drop_solver_outputs()acts onkind == "run"only.prepare_for_sharing()drops rendered reports unlesskeep_reports = TRUE: they re-render, and a PDF or DOCX can carry a path or user name where byte-level scrubbing is not safe.delete_marked()skips an entry that something still references, reportingskipped_referencedbeside the existingskipped_sealed;ignore_refs = TRUEdeletes it anyway, and an interactive session names the dependents and asks.save_scenario(embed_model = NULL), the default, saves the model to the model store and references it instead of embedding a copy in every scenario; a name already holding different content is left alone and the model embedded.Storage aux capacity couplings are part-prefixed:
cap2ainp/cap2aout/ncap2ainp/ncap2aouton a storage@aeffare nowout.cap2ainpetc. The bare names error with a rename hint; technology@aeffkeeps them.A run’s solver files are written directly into
runs/<solve>/, besiderun.yml, instead of asolver/subfolder, and the per-solve metadata file issolver.csv. Existing scenarios still open.The Julia and Python backends exchange data as Arrow files.
data.RDataandinput/data.dbare opt-in throughsolver_options$julia_highs_rdataand$pyomo_cbc_sqlite; the*_arrowpresets are retired.Arrow exchange needs
Arrow.jl(Julia) andpyarrow(Python). Both are installed and checked byen_install_julia_pkgs()/en_check_pyomo();CSVandSQLiteare dropped from the Julia install.arrow_formatsplits intostorage_formatandexchange_format, likewise*_compressionand*_compression_level. Storage keeps zstd-15; exchange defaults tolz4.46 machinery helpers are no longer exported (314 → 268 exports) — the interpolation/mapping engine, the NEOS plumbing below
neos_ping()andneos_list_solvers(), and small utilities. They remain asenergyRt:::.print.levcost(),print.levcost_list(),print.levcost_variants()andprint.share_frontier_plots()are registered as S3 methods;print(x)is unchanged, the directprint.levcost(x)call form is gone.The
ertprefix is retired foren: registry class tagen_registry, report CSS.en-*, LaTeX coloursen_blue/en_gray; custom report templates written against the old names need updating.Store folders are named by the object, not its hash (
models/TOPIA/), and updated in place. Old hash-named folders keep loading;rehash = FALSEkeeps a recorded hash through a change you declare insignificant.Object names are validated at construction — letters, digits and underscore, starting with a letter. Scenario folders join their parts with dashes (
BASE-TOPIA-s4_h24).problem.RDatais retired;scen.RDatais the base problem’s one home. Legacy files are read and folded in byupgrade_scenario_layout().A storage’s availability columns take the part prefixes:
@af$cinp.*→inp.af.*,cout.*→out.af.*;@weather$wcinp.*→inp.waf.*,wcout.*→out.waf.*. Old names error and name their replacement.commodity@geolevelandsummand@geolevelare now@geoframe, as are thegeolevel =arguments ofnewCommodity(),newSummand()andgetData();map_comm_geolevel()is nowmap_comm_geoframe().The variable-catalogue role
flowis nowinterregional.Scenario-management operations are verb-first with no aliases:
load/save/add_to/find/refresh_registry(),drop_scenario_run(),upgrade_scenario_layout(),apply_ledger().modInp@setis removed (superseded by@sets);@gams.equationis now@user_constraintsand@costs.equationis@user_costs. Scenarios saved with the old slots migrate on load.interpolate_model()errors on a weather profile named but not declared, and on ageffrow naming an input group no commodity belongs to. Both used to be dropped silently.The TOPIA world reuses the shared calendars:
topia_annual,topia_s4h24andtopia_m12h24are retired forannual,s4_h24andm12_h24;topia_seasonsstays, relabelledAUT→FAL. Objectives shift slightly.report_tbl()enforces a 200-row cap in PDF/Word when nomax_rowsis given.The sampled daily calendar
calendars$d365_h24_subset_1day_per_monthis renamedd365_h24_1dpm(solver working paths embed the calendar name); existing scenario folders keep their names.
Deprecations
-
en_install_python_deps()is deprecated. The Python environment is set up by multimod, which energyRt and multimod now share.
New features
A
weatherobject can be declared at a COARSER region than the processes that use it — one profile peradm1feeding all of itsadm2children — and is stored once instead of copied per child. A process reads the series at its own region when the object serves it, otherwise at the nearest ancestor that does (mWeatherRegionAt); a flat model resolves through identity rows to exactly the previous lookup. Verified identical on GLPK, GAMS, JuMP and Pyomo. A link that resolves to nothing is now an error: the factor is multiplicative andpWeatherdefaults to 0, so it used to shut the process down silently.subset_model_regions()keeps a parent profile its children still need,aggregate_model_regions()passes one through when it already sits at the target level (and refuses between two non-atom levels), andlevcost()follows it down to the technology — it previously lost the capacity factor and reported a 33% lower levelised cost with no warning.interpolate_model(fold = TRUE)now foldsyearas well asregionandtimeslice. A weather series repeated across milestone years is usually the largest parameter in the model, and the previous pair never touched it: on a 4-regiond365_h24model over 7 milestonespWeatherdrops from 245,259 to 35,037 rows (-85.7%) and all value parameters from 443,545 to 71,215 (-83.9%), with a bit-identical objective on GLPK and Pyomo/HiGHS. A dimension folds only where the value is uniform across the whole set, so a model whose weather genuinely varies by year is unchanged. Pass an explicit character vector to choose the dimensions yourself; theFALSEdefault is unaffected.On-disk stores write ~1M-row row groups instead of one per 32k-row record batch. Stored data.frames are smaller (1.3x on model-shaped data, 2.2x on a sorted hourly series) and scan faster; existing stores are unaffected until rewritten.
multimod_cloudruns persist the COMPLETE job handle incloud_job.yml(viammcloud::cloud_handle_write()), including the output-bucket reference, so a finished job’s solution is fetchable from any later session.print()shows the formatted summary fortechnology,demand,supply,config,modelandscenarioobjects. The printers existed but were never registered, soprint(obj)fell back to a raw slot dump.autoplot()on a variant fan-out gainsxandfacet, so a vintage x cluster x region grid can be compared along any axis, e.g.autoplot(lc, x = "cluster", facet = "vintage"). A dimension left off both is averaged, never summed, and the caption says so.report()on a technology with variants adds a figure per varying dimension beside the combined chart – clusters within each vintage, vintages within each cluster, and regions where they vary. Dimensions that do not vary are skipped, panel counts are capped, and x labels rotate before they collide.levcost(timeframe = )gains"auto", now the default. A process that cannot be annualised without misrepresenting it – two@weatherfactors, or rows pinned to a named sub-annual timeslice – is priced on its native timeframe instead, and an explicittimeframe = "ANNUAL"declines with the reason.timeframe = "native"requires an explicitcalendar =.process– the union ofsupply,demand,technology,storage,export,importandtrade– is a declared set on every backend, so GAMS domain-checks class membership. Nothing is indexed over it yet, so no solution changes.A commodity with a coarse
@geoframeis available in every region of the cell it is supplied or produced in. A plant in a NUTS3 region burning a fuel supplied at NUTS0 used to be judged to have no input and dropped from the model.Discount rates can be declared at any geoscale level: a
discountrow naming a country applies to every region below it, and a region’s own rows override its parent’s. A process declared at a coarse cell takes that cell’s rate, else the rate its children share; where they differ and the cell declares none, the process is refused by name.The
lang = "mm"backend forwards a preset’shighs_options(a named list, e.g.list(presolve = "off")) to HiGHS.The TOPIA transmission corridors carry a 2 EUR/MWh flow cost. They had none, so a built corridor was free to use and regional prices converged further than they should; the solved reference values move by +0.03% or less.
en_install_deps()anden_check_packages()read the package list fromDESCRIPTIONinstead of a hard-coded copy, and install or report the dependencies that are not on CRAN (nestedscales,timescales,geoscales,glpsol,multimod,mmcloud) with their source. The one-line installer does the same, so a fresh install now has a solver to work with.en_install_julia_pkgs()installs the solvers the shippedjulia_*presets name –HiGHS,GLPKandCbc– and no longer installs four packages the model code never loads.CPLEXandcuOptstay opt-in, one being licensed and the other GPU-only.plot_weather(datetime = TRUE)produces a real datetime axis where it previously fell back to the categorical one: dating a timeslice needs a year, and the year is now taken from the data’syearcolumn.Every energyRt calendar converts, including the sampled and subset ones (
d365_h24_1dps,m12_h24_subset_4months, …) that have no entry in timescales’ catalog: the calendar is rebuilt from its own@timetable. energyRt’s timeframe names are matched by what their labels ARE, so a timeframe namedDAYlands on timescales’YDAY.An unregioned trade cost now says so more firmly, recommending that the regions be named, while noting what the unregioned form is good for: the rate follows whichever endpoints survive, so a sampled sub-model bears its own share.
subset_model_regions()warns when sampling splits a geoscale cell that a trade cost is declared at. Such a cost is charged once at the cell whatever remains beneath it, so the sub-model keeps the whole corridor’s cost instead of its share and its objective is not comparable with the full model’s.levcost(by_region = TRUE)prices a process once per region it spans and returns alevcost_listnamed by region, rather than once in one of them. On a container the entries are named<process>@<region>.tradeis unaffected: it spans two regions by construction and is priced across both.@clusteris declared the same way on every process class:cluster,desc,orderalways;regionwhere the class has regional scope;cap.share.fxwhere it has a capacity to tie;act.share.*everywhere.technologyandstoragegaincap.share.fx,importandexportgainregion.supply,importandexporthave nocap.share.fx: they have no capacity variable, so the column is absent rather than accepted and ignored – declaring one is now an error that points atact.share.act.share.lo/up/fxbounds a cluster’s share of its family’s activity. Unlikeafs, which bounds activity against the cluster’s OWN capacity and can be satisfied by building less, a share of the family can only be met by keeping capacity. This is what stops a clustered family collapsing to its cheapest member once regional borders are aggregated away. No new equation: it rides thenewConstraint()path, and binds on GLPK and multimod alike.Cluster shares are guarded rather than silently skipped. A cluster missing a variant in one vintage used to leave that vintage untied with no warning; stock out of proportion used to give a bare infeasibility. Both now error and name the cluster.
aggregate_model_regions(clusters = list(TRD = k))partitions a family of corridors intokmerged objects instead of collapsing every corridor between a pair of coarse regions into one. Merging unlike corridors replaces a fill-the-best-first delivery curve with its chord – on a 100-unit link at 0.99 beside a 20-unit link at 0.90 that costs 1.5% of the energy delivered below saturation.kruns from one part per coarse pair (plain aggregation) to one per corridor; parts become separate objects, because a trade’s@clusteris a loss tranche.kabove the floor is refused when a corridor carries a reactance: parallel AC circuits split flow by impedance, not by optimisation.get_process_groups()reports corridor families, withncounting corridors rather than regions.model_clusters()returns their crosswalk and no geoscale – a corridor has no territory to colour.aggregate_model_regions(clusters = )keeps the regional spread astechnology@clustervariants instead of averaging it away. Settings are per technology family –clusters = list(ECOA = list(k = 3), EWIN = list(k = 8))– because onekfor the model is meaningless: a coal fleet groups on cost and efficiency, a wind fleet on resource quality and profile shape. A family not named is aggregated plainly, soclusters = NULLis the old behaviour exactly. A bare number is accepted only when the model has one family; with more it is an error naming them.kruns from one cluster per coarse region (identical to plain aggregation) to one per fine region (nothing averaged);k = "auto"picks the best average silhouette and returns the sweep, but sweeping and looking is the documented workflow. Clusters cannot straddle a coarse region: the adjacency graph has no edge crossing one.aggregate_model_regions(as = "objects")returns one technology per (family x cluster), named with the cluster’s label (ECOA_W1), instead of one object carrying clusters. Same LP; what differs is how results are keyed.process_cluster_sweep()clusters one family over the admissible range ofkand reports dispersion, silhouette and the cluster sizes, over the same features and contiguity graph the aggregation would use – so the table you read is the grouping you would get.nestedscalesships nobest_k()deliberately, and neither does this.model_clusters()reports what a clustered aggregation did: thek, the region-to-cluster crosswalk, the sweep, and a geoscale carrying aclustergeoframe, soplot_geoscale(type = "map", geoframe = "cluster")andtype = "icicle"show the grouping with no new plotting code.get_process_groups()reports which processes may be merged into one clustered object, grouped by structure – inputs, outputs, aux commodities and input groups – so a coal plant and a gas plant are never merged. It accepts a technology that already spans several regions, one object whose slots carry aregioncolumn, and clusters it exactly as it does a family of one-object-per-region technologies.A technology that already has clusters keeps them when its regions are coarsened: the two groupings compose,
GOODinW1becomingGOOD_W1. Previously the incomingclustercolumn was dropped, so a fleet with two site grades over four regions came back with four averaged variants instead of eight – silently. Clustering features are read per (cluster, region) for the same reason.A cluster of one region is labelled with that region’s code, so the 1:1 end reads
ECOA_CLW1rather thanECOA_CLc03; several regions join with_while that stays a legal name.@cluster$desccarries the membership either way.A clustered aggregation resolves a link column (
weather,transform) to the cluster medoid’s value. These name another object, so they can be neither summed nor averaged, and treating them as keys gave one cluster a row per member. A medoid is a real member, so the merged cluster gets a profile some region actually had.kis checked against the geoframe beforenestedscalessees it, so an out-of-rangeknames the level and its bounds instead of reporting “the units fall into 3 group(s)”.plot_trade_map()derives thex/ylabel anchors from the polygons when a plainsfmap does not carry them, so anysfworks as amapargument.data-raw/topia_maps.Randdata-raw/topia_data.Rare idempotent: rebuilding the dataset no longer rotates the honeycomb rings or adds a duplicate provenance tag to the weathersourceattribute.Milestone years can carry a display label.
newHorizon()accepts alabelcolumn inintervals, and the newyear_label()returns one label per milestone. The label is presentation only: the model key for a period stays the integermid, and nothing reaches the solver.calendargainsyear_start(list(month = , day = ), default January 1) andutc_offset_minutes. A non-January anchor makes the default milestone labels fiscal (FY2025-26), following thetimescalesconvention that the anchored year is the starting Gregorian year. The anchor is carried and reported; it does not yet drive timeslice-to-timestamp alignment. Calendars imported fromtimescaleskeep both values instead of dropping them.getData(yearsAsFactors = TRUE)returnsyearas an ordered factor of labels, with levels in horizon order. It previously left an integeryearuntouched.audit_coefficients()reports the coefficient range of a written model per equation family (widest row, the variable at each end of it, counts per decade), from the free-MPS formglpsolre-emits.dev/coefficient-ranges.mdholds the table for the test fixtures and TOPIA.eqTechAInp/eqTechAOutare multiplied through bypTechCap2actin all four backends, so no coefficient in them is a division. Parameters keep their meaning; the LP is the same problem with those rows scaled.validate_scenario_parameters()adds the advisorycoefficient_scalecheck: a technology, trade or storage whosecap2act x finest timeslice shareis outside[1e-4, 1e4]is named with thecap2actthat would put the product near 1. On an hourly calendar withcap2act = 1the capacity coefficient in the availability rows is 1.1e-4 against 1 on the activity.verify_solution()gainschecks = "default"/"all", averbose =progress report, andprint(detail = c("full", "issues")). New checks:positivity(no negative value in a variable declared positive),inputs_present(every declared object reaches the sets and the parameters),units(unresolved-unit report, never fails), and the opt-ininputs_values/inputs_bounds, which re-derive each object’s declared data independently and compare it againstmodInpand the solution.Further opt-in checks:
storage_dynamics(eqStorageLevel, including thefullYearcycle closure),capacity_accumulation(eqTechCap, the arithmetic theperiodLendefect lived in),eacandflow_chain(input to output through the efficiency chain, grouped inputs included).The
unitsreport summarises coverage per class ($coverage), so it says which classes are missing unit declarations rather than only how many parameters are unresolved.$divergencegainsmax_scaled, the divergence relative to the series scale.max_reldegenerates to 1 wherever a value legitimately reaches zero – an emptying storage level – somax_scaledis the figure that says whether a check is drifting.storage_dynamicsis toleranced against that series scale for the same reason.verify_checks()lists the checks with their group, tier and data path.pathsays whether a check readsmodInpor the model objects – only the latter can catch a mapping that was never built.promote_solution()makes a run’s solution the scenario’s own, copying it to<scenario>/modOut/besidemodInp/and clearing the active run — after whichruns/is scratch and can be deleted without losing the solution.import_solution(promote = TRUE)chains both steps. The store carries amodOut.ymlrecording the solve’s provenance (solver, objective, stage, timings), which otherwise lives only inrun.ymland would go with the run folder.read_sequence()rebuilds the result ofsolve_myopic(),solve_by_sample(),solve_by_region()orsolve_guided()from the variants on disk, sogetData(),sample_summary(),myopic_objective()andguided_gap()work on a reloaded scenario and not only on the live result. A driver’s working state (the carry ledger, capacity targets, a sampling spec) is not stored and is listed in$missing: a rebuilt sequence can be read, not resumed.getData(run = )reads one or more runs of a scenario in a single call, orrun = "all", tagging the rows with aruncolumn. Each run is read on a copy, so the object keeps its active run, and a variant brings its own problem with it. Withoutrunthe result is unchanged.A variant’s
variant.ymlrecords aparams:block — how the driver was configured (solve_myopic()’soverlapandcarry,solve_by_sample()’sseedand sampling spec,solve_by_region()’snstepsandprice,solve_guided()’sslack,basisandmode) alongside the existing keys saying which step, sample, region or stage the variant is.scenario_solutions()lists what each solver attempt produced — status, objective, solver and backend — recovering the objective and solution status from a run’soutput/for runs that were never imported, wherescenario_runs()can only reportNA. Read-only.import_solution()imports one run’s solution and saves it, so a solved scenario keeps its results after the session ends. It restores the run’s solver settings (never the command line), leaves the solver’soutput/alone, and refuses to save a read that produced no variables.read_solution(ondisk = )writes each variable into the run’smodOut/store as it is read, so the solution is on disk when the read returns andsave_scenario()no longer rewrites it — about 30% faster end to end on a full-year model. On by default for a scenario that is itself on disk.interpolate_model()is about 6x faster on large models (a 41-node vintaged multi-year interpolation dropped from ~16 to under 3 minutes) and runsdata.tableon all cores for the duration of the call (options(energyRt.threads = )to override). Interpolated content is unchanged.Every
interpolate_model()call appends its per-stage / per-map / per-parameter timing and memory readings (R heap, per-stage heap peak, process RSS and peak viaps) to a profile log —interp_runs.csv,interp_stages.csv,interp_maps.csv,interp_params.csv— underset_profile_dir(), else next to the operation log, else<project>/logs/; with no sink resolvable nothing is written.On-disk interpolation writes each parameter’s store once at the end of the object loop instead of rewriting it on every append, removing the quadratic cost that made
ondisk = TRUEslower than in-memory on large models.clear_report_cache()removes rendered reports — an object’s ownreports/folder, or the project tier for in-memory objects — listing them by default and deleting only withdry_run = FALSE. A sealed owner refuses.store_dependents()lists what references a stored model, repository or dataset instead of embedding a copy of it — the entries that would break if it were deleted, and whether each is pinned to a superseded version.New solver presets for GPU-accelerated LP.
solver_options$julia_cuopt,julia_cuopt_concurrent,julia_cuopt_pdlpandjulia_cuopt_crossoversolve via NVIDIA cuOpt throughcuOpt.jl. Linux only – cuOpt ships no Windows build, andlibcuopt.somust be onLD_LIBRARY_PATHbefore Julia starts.julia_cuopt_pdlpuses a first-order method with no factorization, so GPU memory grows linearly with the number of non-zeros; add crossover (julia_cuopt_crossover) when duals are needed, as a first-order solution alone carries a ~1e-4 duality gap.solver_options$julia_highs_pdlpandjulia_highs_hipdlpselect the HiGHS first-order solvers. GPU execution requires alibhighsbuilt with-DCUPDLP_GPU=ON; a stock build runs them on the CPU. Neither returns a basic solution or supports crossover — use the simplex or interior-point presets when duals are needed.The JuMP backend records the solver’s
termination statusinoutput/log.csvand skips result export when no primal solution exists, instead of failing while reading variable values.New sampled calendar
calendars$d365_h24_1dps: one representative day per season at hourly resolution (96 timeslices).solver_options$pyomo_mpswrites the problem as an MPS file with energyRt’s own variable and constraint names and stops before solving — for handing a model to an external solver (a GPU LP solver, a remote machine) whose solution comes back later.read_solution()reads a run solved outside energyRt: a solution decoded into the run’soutput/is picked up with the exchange format recorded by the run itself, and the run record’s status and objective are updated. A run with no primal solution reports the recorded termination status instead of a missing-file error.Weather transforms: one shipped data stream can serve many derived series. A weather object carries named functions in
misc$transform; a technology/storage/supply weather link selects one in itstransformcolumn.materialize_weather()inspects a derived series andregister_weather_transform()adds session-wide transforms. Region aggregation refuses transform-bearing weather.scenario_artifacts()lists what each solve left on disk — whether its solution was imported, what the solver scratch costs, and which runs are candidates for clean-up.drop_solver_outputs()removes the regenerable part of a solve — model source, exchange input, solver logs, and the raw output once the solution has been imported — keeping the run record and the solution. Dry-run by default.strip_user_info()removes the machine a scenario was made on — stored absolute paths, the solver command line, and, with the defaultscope = "share", the host and user of every run;scope = "store"keeps it.prepare_for_sharing()writes a cleaned, trimmed copy of a stored scenario and reports anything that will not travel with it.Folding works with GAMS: a dense build (
sparse = FALSE) may be folded, and the GAMS writer substitutes the artificial member like the other backends.validate_scenario_parameters()is exported and also checks that every variable has its governing constraints, that availability bounds are present, and that a calendar’s row order agrees with its timeframe columns.Storage aux flows can couple every part’s capacity:
inp.cap2a*/inp.ncap2a*(charger) andstg.cap2a*/stg.ncap2a*(reservoir) join theout.*couplings on all four backends.draw()of a storage shows the charger, reservoir and discharger as boxes with their own parameters, the ratio triangle (inp2stg,duration,inp2out) as labelled links, andfullYearin the header.A storage gains
@inp2stg, the charging C-rate (charging per unit of storing capacity, 1/hours); with@durationand@inp2outany two ratios fix the third, andnewStorage()refuses a contradictory fixed triple.newImport()andnewExport()takeregion =, like every other process class;importandexportobjects gain the matching@regionslot.solve_by_sample()solves a model on samples of its calendar — consecutive blocks tiling the year, disjoint random draws, or bootstrap draws — storing each as a variant of one scenario.Calendar samples are replicates, not pieces: each is annualised and estimates the whole year, so
sample_summary()reduces them to quantiles and their objectives must not be summed.calendar_samples()builds the sample specification by drawing whole members of a calendar level (seasons, weeks, days); the table can be edited and passed back.aggregate_model_regions()builds a coarser model by mapping regions onto a geoframe of ageoscales::Geoscale: extensive quantities sum, intensive ones take a size-weighted mean, intra-region trade corridors drop.solve_by_region()solves a multi-region model one region or group at a time, each run a variant of one scenario; withtrade = "none"the objectives add up to the full model’s (the result’sadditivefield says so).A severed trade route can be replaced by a stepped price curve instead of a flat price:
boundary_pricesgainsnsteps,price_lo,price_hiandimp.lo/exp.locolumns.boundary_window()builds aboundary_pricestable whose quantity isshareof each region’s demand per timeslice;priceis required.solve_guided()solves a too-large model in stages (endpoint problems seed capacity targets for one final full solve);guided_gap()reports the gap to perfect foresight; primitivessolution_targets(),apply_targets().Every energyRt object has a
report()method: 14 element classes render datasheets from per-class templates;report(mod, name = )finds an element of any class (class =narrows);misc$reportsets the default template.calendarsships the mainstream timescales designs —m12,m12a,q4,s4,s4_h24,m12_h24,wd7_h24,w52_h24— plus three sampled calendars whoseyear_fraction < 1solves partial years natively.The registry gained
newRegistry()(load_registry()no longer invents a missing registry), avariantcolumn, andgetScenario()/getObject()methods fetching bytype/namewithrun =selecting the run.levcost(x, by_variant = )replaceslevcost_by_variant(), takingTRUE,"npv"or"components", either while computing or on a result in hand.Store entries have a lifecycle:
seal_*()/unseal_*()freeze an entry,mark_delete(x, importance =)queues it anddelete_marked()(dry-run by default) removes marks up to a threshold.set_path_builder()overrides how folder names are derived —scenario_dir,store_entry,run_label, or theslugprimitive. See?path_builders.topia_profile()generates deterministic synthetic shapes on any calendar — step staircase, sine, cosine or hexagonal trapezoid — with per-region phase or amplitude variation.The “unit model”:
topia$modules$unitkits (U1,U3) where every input is 1 on the symmetricunit_s4calendar, so each variant’s objective is a small hand-checkable integer.TOPIA add-on modules in every
electricitykit —GAS_CURVE(3-step supply curve),EWIN_SITES(two wind site grades),ENUC_VINT(two nuclear vintages) — replace their base counterpart viaadd(mod, ., overwrite = TRUE).solve_myopic()solves a horizon window by window. The primitives are composable:horizon_windows(),solution_ledger(),apply_ledger().A comparison layer:
compare_scenarios()(scenarios or recorded runs, withprint(),autoplot()andreport()methods),compare_models()(declaration diff) andcompare_inputs()(interpolated-input diff).report(scen, template = "full")renders a page-broken document with branding, time and geography pages, result choropleths and stacked bars.report(mod/scen, template = "summary")renders one-page glimpses.Container reports split by role: the model report documents assumptions and data, the scenario report the results of a solved scenario. Both render to HTML, PDF and Word.
Model and scenario reports follow user-defined process groups (
report(groups = list(Coal = "_coal_"))ormisc$report_groups), else a sample of up to 12 processes;groups = "topology"groups by structure.Faceted autoplots cap at 16 panels (a caption says how many were dropped); report figures size their height by the facet layout (
report_fig_height()).Report figures carry pandoc captions instead of in-plot titles: new
report_fig()emitter (stripstitle/subtitle, auto height, caption) andreport_img(caption = ).Supply charts:
autoplot(supply, style = "bar")draws availability and cost by region (curve steps stacked in order);style = "regions"draws region bars, unlimited availability as a translucent full-height bar.Levelized-cost comparison charts:
report(model)andreport(scenario)withlevcost = TRUEadd overall and per-group comparisons; a process datasheet reported from a container compares it with its structural peers.Model reports summarise weather factors (mean/min/max per factor and region) and draw calendar heatmaps per factor family (6 best + 6 last clusters) and per demand;
autoplot(demand, style = "heatmap")draws the latter directly.Report branding:
misc$logos,misc$figureand scenariomisc$badges, withreport(logos = , figure = , badges = )overrides. New helpersreport_img_row()andreport_pagebreak().The report-template helpers are exported as the
report_*family for use in custom templates.report_templates()lists shipped templates; resolution is class-scoped, so containers and processes can share template names.report()works inside a knitr chunk — the nested render no longer collides with the outer document’s chunk labels.report_tbl()gainsmax_rowsandscroll: scrollable tables in HTML, a capped table with a “… K more rows” footer in PDF/Word.getMix(top_n = )keeps the N largest processes and lumps the rest into"Other", mass-preserving.autoplot()defaults totop_n = 12.plot_map()maps any solved variable carrying a region dimension vianame =and facets by year. Newplot_geoscale()draws a geoscale as a membership map, a layered cabinet stack, or an icicle.levcost()pricesstorage(LCOS) andtrade(LCOT) as well astechnology, closed-form and solver engines held together by a parity test. Containers price all three;classes =narrows.asSupplyCurve(),asImportCurve()andasExportCurve()turn a single price per(region, year, timeslice)into a stepped curve.Transmission losses can be quadratic, approximated by capacity tranches.
newACLine()andnewDCLink(), with an opt-in Kirchhoff voltage law.@trade$afrates a route’s flow relative to its capacity, alongside the absoluteava.lo/up/fx.A filtered geoscale passed to
interpolate_model()produces a sub-territory model — the spatial mirror of calendar sampling."TOTAL"in avintageorclustercolumn works on flow bounds and availability factors, not only@capacity.vTradeIrand the*RetiredNewCapvariables can be used in custom constraints.Scenario storage: a persisted per-project registry, one folder per solve under
runs/<variant>/<solve>/, a content-addressed model store, a shared repository store, and own-problem variants viasolve_scen(variant = ).Registered objects are accessible by name —
getScenario("base"),getModel(),getRepository(),getDataset(),load_scenarios();getData()takes names and environments;open_project(path)anchors a session.A dataset store completes the storage tiers:
save_dataset()/load_dataset()keep a large table, a geoscale map or a recorded generating call in a content-addressed folder; the other savers gainembed_datasets =.Reports render into the object’s own folder; an unchanged report is not re-rendered (
force = TRUEoverrides).levcost()results are cached on disk, keyed by object content and assumptions;report()shares the cache.clear_levcost_cache()empties it.object_hash()works for any energyRt object.An optional operation log:
set_log_file()makesinterpolate_model(),solve_scenario()andsolve_myopic()append one CSV line each;read_log()reads it back. Off by default.add()dispatches on a loaded registry, as a shorthand foradd_to_registry().New
get_weather(), the weather-side counterpart toget_region().summary()on a model reports its regions and its objects by class.run.ymlrecords a solve’s memory footprint (mem_mb/peak_mb) andsaved/updatedstamps, surfaced byscenario_runs().
Bug fixes
A region whose code repeats at two adjacent geoframes no longer poisons its own balance. The self-pair reached
mRegionFamily, soeqOutTotreadvOutTot[c,r] = <terms> + vOutTot[c,r]and forced every real term to zero; such pairs are dropped with a message, and the region balances like a padded one (LUvs Eurostat’sLU0).A geoscale whose
geoframesdo not nest is refused instead of silently double-counting. Two cross-cutting frames in one chain give a region more than one parent, and the roll-up is a plain unweighted sum, so the child was added into both; the error names the frames and the straddling regions.An infeasible or unbounded model is reported as a result rather than an error.
solve_scenario()raised “Solution files not found” when the solver had concluded there is no solution; it now returns the scenario with an emptymodOut, anNAobjective and the solver’s own verdict as a warning. Missing output with no verdict is still an error. On GLPK the verdict is kept inoutput/solver.log, the only place it survives.levcost()returnedNAfor every variant of a technology whose@invcost,@fixomor@varomcarried aregioncolumn: the cost rows kept the real region name and matched nothing, so the variant was costed at zero. Any vintage or cluster fan-out over a technology translated from another framework was affected.A vintage whose build window falls outside the priced horizon is priced instead of being reported at a levelised cost of zero.
levcost()on atradeobject priced a corridor below what the model pays:@varom$varomwas ignored by both engines and is now charged on the quantity sent.@varom$markupis deliberately not priced – it is a transfer between the two regions and cancels in the objective – andlevcost()says so once instead of warning.levcost(method = "solve")could not price a technology declaring a sub-annual@timeframe, failing with an out-of-domain error: the annual collapse rewrote the weather profile and the commodity timeframe but left the process on its sub-annual one.A technology’s
@invcostgiven without a region was dropped from the annualised cost whenever another technology’s cost named a region, leaving that capital free in the solver.verify_solution()no longer double-counts the totals of a coarse region at a timeslice coarser than the commodity’s own.A non-interactive run no longer stops at a debugger prompt inside the package. Internal consistency problems are reported as warnings under
options(en.debug = 1), and an assertion that had been silently swallowing a failed conversion now reports the original error.Year columns are no longer coerced on every pass through interpolation; the check that was meant to skip the work was testing the wrong object.
Exogenous stock records commissioning only. The level at the first milestone is an initial condition and is read as one, rather than folded into the commissioning series. Solutions are unchanged.
This fixes wrong answers rather than only moving code. The old guesser read
"s1_h01"as hour 1 and silently discarded the"s1", and readh168hour-of-week labels ("h000".."h167") as hours of the day. It also could not readm12a("JAN".."DEC"),wd7("MON".."SUN"),q4ors4labels at all, returningNULL. All of those work now.A trade
invcostwith noregionwas charged once per region OF THE MODEL, not once per endpoint of the route – six charges on a six-region model for a two-ended corridor, growing with the model rather than the route.fixomwas always correct. A trade’s investment window carries no region, so the rate reached the annuity step with nothing to carry it and the densepTradeEacmaterialised a row per region; it now expands over the process’s own regions, i.e. the route endpoints.levcost()on a repository or model priced every process as if it lived in the container’s first region. A technology anywhere else had itsinvcost,fixomandceffrows subset away and reported fuel cost only – around 25x too cheap, with no warning.aggregate_model_regions()lost data when it merged trade corridors.@capacitywas not recombined, so only the first corridor’s bounds survived;@vintagecame back empty, losingolifeon every aggregated corridor, merged or not; costs took an unweighted mean; the merge key saw tranche labels but not their shares, so corridors whose shares differed merged and silently adopted the first one’s;reactancewas averaged instead of combined as1/x_eq = sum(1/x_i); a one-way corridor became bidirectional; and two corridors minting the same name overwrote one another. No test had ever merged two corridors.aggregate_model_regions()read only the model’s first repository.add()appends a repository rather than growing one, so a model built up object by object kept its later objects out of@data[[1]]– and those came back declared over the FINE regions in a model whose regions were now coarse, silently. It now reads every repository and returns one.A
supplyno longer reaches regions it was never declared in. A supply givenregion = "R1"also producedmSupSpan,mSupAvaandmSupOutTotentries for the model’s other regions, so its commodity appeared available where no supply existed. The interpolation goldens are re-frozen for this and for the upper-level rows that multi-level timeframes and parent-geoframe totals now add.interpolate()andsolve()work on an installed package. Both generics were documented as the recommended API but never exported, so the pipelines in the README and the vignettes failed for anyone who had not loaded the source tree;read()was already exported.The shipped
topiastorage objects can be read and printed again. They were stored beforestorage@inp2stgexisted, soprint(),o@inp2stgandprocess_to_spec()all failed on them; models built from the kits still interpolated, which is why it went unnoticed.drop_scenario_run()says so when a run holds the only copy of the scenario’s solution, and namespromote_solution(). Dropping it used to leave the scenario pointing at a store that was gone, so every read failed with “On-disk data expected but not found”.Switching to a run whose solver
output/is gone no longer returns the previously active run’s solution.read_solution()reads the run’s importedmodOut/store instead, and errors when neither is available — it used to hand back the unchanged scenario, so a cleaned-up or shared scenario served another run’s numbers with no warning.solve_myopic(store = "scenarios")runs: it composed each step’s scenario name with dashes, which the object-name rule rejects, so every step failed — reported as an infeasible solve.A solution reconstructed from a solver
.solfile wroteyear(andyearp,yeare,yearn,year2) as text, where every other route writes integers. Output from the two routes could not be joined on the year, in nearly every table, and the mismatch was silent.Value-map domains (
mTechVaromand siblings) treat aNAkey as a per-row wildcard: a technology’s year-unkeyed cost row is no longer dropped from the map when another technology keys the same cost by year, which silently removed the cost from the model.save_scenario()no longer errors on a scenario interpolated withondisk = TRUEand never solved (@modOutisNULLthere).interpolate_model(ondisk = TRUE)writes each parameter store undermodInp/parameters/<name>, the locationload_scenario()rebuilds paths to; previously the tables landed flat undermodInp/<name>and were unreachable after a reload. Existing flat stores still load (the rebase falls back to the flat location).Multi-year models understated capital charges by the milestone length: annuities (from
eacorinvcost) were charged on the annual build rate instead of each vintage’s standing capacity — one fifth of their value on 5-year milestones. Fixed in all backends; single-year and overnight models are unchanged. Re-solve multi-year scenarios solved with earlier versions. Seedev/multiyear-capital-charge-bug.md.On a sampled calendar, ANNUAL-timeframe quantities are now full-year magnitude: annual caps and emission totals bind at face value, and every commodity gains totals at each coarser timeslice level and, with a geoscale attached, each coarser region level. Full calendars are unchanged. A serialized calendar built under the old convention is refused at interpolation — rebuild it with
newCalendar(). Seedev/annualized-annual-convention.md.Solution CSVs written by the GLPK backend carry 10 significant digits (was 6 decimal places).
fold = TRUEcould silently drop capital cost from the objective: a parameter whose folded dimension was only partially wildcard kept a rawNA, which is not a set member, so lookups missed and took the parameter’s default. Partial columns are now materialised to explicit members before the write, andapply_fold_artificial()refuses to write a surviving rawNA. Seedev/fold-partial-wildcard-bug.md.interpolate_model()rehydrates a model whose large slots live in the model store (obj2mem()) instead of silently interpolating the empty placeholders — a store-loaded model produced a plausible-looking scenario with no demand and no weather.verify_solution()’sobjectivecheck no longer errors out (and is no longer reported as skipped) when a parameter and its gating map disagree on a key column’s type, aspDiscountFactorandmvTotalCostdo onyear.save_model(),model_hash()and the other store hashes accept objects serialized before a slot was added to their class; a model holding a pre-inp2stgstorageused to fail to save.subset_model_regions()(and sosolve_by_region()) prunes@weatherreferences whose object is dropped with the regions outside the sample; narrowing a clustered-resource model to one region used to fail validation.A solved or written folded scenario no longer carries the artificial set members (
ANYREGION,0);getData()on such a scenario unfolds every folded dimension, includingyear.Folding no longer depends on the order of the folded dimensions, and a parameter folded on both
techandregionreads back correctly.getData()andverify_solution()unfold every foldable dimension.Folding
tradeno longer empties a trade parameter whose route endpoints are wildcards.A user constraint’s
rhsis interpolated over years per region (and any other key), not as one column across regions.User-constraint and user-cost parameters (
pCns*,pCosts*) are never folded.write_script()substitutes the folded wildcard with the artificial set member, assolve_scenario()already did; a written folded scenario no longer silently takes parameter defaults.autoplot()of ademanddraws its"line"and"heatmap"styles with the same profile engine asweather; the old line view errored on demands with years and faceted one row per day on daily calendars.Profile lines and areas (
demand,weather) colour the coarse time level with a viridis gradient, continuous when the level is numeric.plot_process_windows()no longer draws a process backwards: a process not investable within the horizon shows its exogenous stock instead, and a missingoliferuns the operating tail to the end of the horizon.A zero discount rate with no operational life is refused instead of silently charging nothing for capital. Supplying
@invcost$eacdirectly is unaffected.The solver exchange checks its longest prospective file path before writing and stops with the directory and symbol at fault; exchange directories are created recursively.
A process declared in a region where a commodity it consumes is unavailable is dropped there instead of producing from nothing; the dropped cells are reported and listed in
scenario@misc$region_gaps.A
supplyscoped by theregioncolumn of its data, rather than by@region, is no longer available in every region of the model.A
weatherobject scoped the same way no longer zeroes availability in regions it was never declared for.An interpolation that failed part-way left bulk parameter writing on, and every later solve in the session silently returned objective 0; the
en.bulk_param_writeoption is now restored on every exit path.verify_solution()$okno longer reportsTRUEwhen every check was skipped; newn_ran/n_skippedfields, andprint()says “NOTHING VERIFIED”.validate_scenario_parameters()issues carry a severity: a populated map whose source parameter is empty is structural and errors whateveractionsays; the issue count is always announced withmessage().interpolate_model()asserts no parameter is left unmaterialised after the final flush, naming the offenders.read_solution()errors when none of the declared variables produced an output file, naming the extension and the activeimport_format.add()accepts a repository holding more than one object.report(levcost = TRUE, by_variant = )no longer drops the levelised-cost section:FALSEreports the display instance alone, the default keeps the per-variant tables.A user
constraintwhosefor.eachyears all fall outside the solved horizon is dropped with a warning instead of generating unusable solver code.A
supplyat a commodity’s coarse@geoframelevel was silently costless; it is now priced, and rest-of-world import/export at a coarse level are refused loudly.newCommodity()withouttimeframe =no longer crashes interpolation; the empty slot resolves to the calendar’s finest timeframe.add(mod, x, overwrite = TRUE)replaces an object of the same class and name instead of appending a duplicate next to it; withoutoverwritethe collision is an error.Calendar chronology follows the timetable’s row order; mid-level timeslices are no longer ordered alphabetically.
On-disk parameter stores no longer default to CSV; they follow
storage_format. Existing CSV stores keep loading.A parameter write-back could make its store unreadable by writing CSV beside
.arrowfiles; the store’s recorded format is now authoritative.An atomic (single-column) slot was always written as CSV, mixing codecs inside a store.
import_format = "parquet"returned an empty scenario silently. Pyomo honours parquet on both legs; Julia refuses it at write time.Tables with no rows are no longer written.
obj2mem()is quiet by default and reports a progress bar;verboseis passed down the recursion.subset_model_regions()reports one summary line — regions kept, objects and routes dropped — instead of a message per item;verbose = TRUElists them.Interpolation no longer re-deduplicates a parameter’s whole table on every object that writes to it. Set
options(en.bulk_param_write = FALSE)for the previous path.The process/commodity level check no longer refuses models whose objects span many regions.
Conflicting-bounds detection names the parameter again instead of dying on “comparison of these types is not implemented”.
Parameter writes no longer re-coerce every year and character column.
Variant expansion respects
verbose; becauseverbosedefaults to off, the generated-constraint and variant-expansion messages no longer appear by default. When shown, constraints are counted by family rather than listed.get_region()returns a model’s declared regions.load_scenario()on a saved-but-never-solved scenario no longer warns about rebasing on-disk paths.getData()no longer returns an empty frame for a scenario whose solve was not proven optimal: a stored incumbent is served with a warning naming the stage.solve_scenario(transient = TRUE)deletes the throwaway solver directory.levcost()’s mini-models solve in a scratch dir instead of creating scenario folders thatrefresh_registry()indexed as real scenarios.inp.eac/stg.eacgive a storage part its own capacity — they priced the charging or storing part without bounding it, so it came out free.inp.fixom/stg.fixomreach the objective; a storage priced only on its charger or reservoir paid no fixed O&M.eqTechPhaseOut/eqStoragePhaseOutare gated onmTechNew/mStorageNew; a phaseout window past the investment window no longer crashes Pyomo. Objectives unchanged.scenario@status$solvedis set when the solution is read.Per-part
inp./stg.waccandpaybackare honoured; the annuity always read theout.*columns.technology@af$rampup/$rampdownreach the solver on all four backends;cap2actis no longer applied twice and the Up/Down equations are no longer orientation-swapped.Per-column
config@defVal/config@interpolationoverrides are read at interpolation instead of being copied onto the scenario and ignored.The
costsclass is wired end to end; user cost terms reach the objective viaeqTotalUserCosts.An unknown summand field in
newConstraint()is an error instead of being dropped — it quietly turned a per-technology cap into a global one.A storage flow into a coarser-timeframe commodity reaches the balance; the totals summed only at identical timeslices.
A one-sided
inp2outordurationrange no longer resurrects the binding default of 1 on its open side, which madeinp2out.lo = 2infeasible.A supply restricted to one region no longer leaks into every other region.
Multi-level regions were inert — the hierarchy was read under the old
parent_level/child_levelcolumn names.A cost declared at a coarser geoscale level never reached the objective.
add()on a model worked exactly once.A scalar
multon a custom constraint summand was silently discarded.Exogenous stock could phase out and retire at the same time;
capacity$stockreads as the fleet still standing.Trade phase-out is reported, not only computed; GAMS declares retirement non-negative.
A moved scenario folder loads: stored paths are rebased onto the folder being read.
Documentation
New article “Space resolution: geoscales and geoframes” — what
commodity@geoframeasserts, which classes may be declared at a coarse level and which may not, the nesting requirement, and when a coarse balance replaces atraderoute.New article, Testing strategy, on a new Development navbar menu beside the roadmap: why coverage is tracked over model vocabulary rather than lines, why test tiers replaced
skip_on_cran(), why reference solutions are gated onverify_solution(), and what the suite deliberately does not test.Help pages gained the argument descriptions they were missing, and the
model_structure,prefix,prefix_longname,solver_optionsandtsl_levelsdatasets are now documented.About 9,300 lines of superseded code and specification files left the package, archived rather than deleted. No exported function changed.
New article,
vignette("region-aggregation"): five experiments on one model, measuring when aggregating regions changes the answer and when it does not. Two settings leave it untouched, three move it – including one where the aggregated model comes out cheaper than the model it came from, because pooling granted transmission that did not exist.nestedscalesandnestedscalesare Suggests;dev/region-aggregation-model.Rholds the shared model anddev/verify-region-aggregation.Rchecks every number the article prints.Three shipped statements that contradicted the multi-level region feature are corrected:
setGeoscale()andconfig@geoscaleno longer claim a geoscale “never changes the optimisation model” (it is inert only until a commodity names a@geoframe);commodity@geoframeno longer says “GLPK and GAMS only” (all four back-ends carry the roll-up) and now states that the coarse balance is the plain, unweighted SUM of its children and that geoframes must nest; and theregioncolumn oftrade@invcost/@fixomno longer says a coarse level “never reaches the objective” — it is charged once at that cell. The roadmap no longer lists nested regions as unstarted.read_solution()has a help page. Its roxygen block was detached by a stray#comment, so the function documented nothing — the page now coversrun,ondisk, and why the importedmodOut/store is not a copy of the solver’soutput/.The scenario-management article gains a section on getting data out of a variant, where a variant’s solution lives, how to remove one, and the two unrelated meanings of “variant”. Stale diagrams showing a nested
solver/folder are corrected to the flat run layout.New article Reports and levelized costs tours the reporting layer and the report/levcost caches.
One
tradeobject is one shared throughput budget —eqTradeCapFlowsums every route against its single capacity, so a network in one object is not a set of independently rated lines.Trade costs are a rate per endpoint region: capacity is region-free, but
invcost,fixom,retcostandeacare region-indexed and each named region pays its own rate on the whole capacity.
energyRt 0.80 (development) — the time dimension is now timeslice
Breaking changes
- Stack-wide rename
slice→timeslice, matching the TIMES/OSeMOSYS vocabulary and reading unambiguously next toregion. It covers the set/index family in all four backends, everymSlice*/pSlice*symbol, theANYSLICEwildcard, the solution CSV column, and the S4calendarslots. A shim accepts the oldslicename in user data with a once-per-session warning. -
getData(timeframe =)defaults to"highest"(native resolution), not"lowest". An 8760-slice series used to come back as a single annual number with nothing to signal it. Code relying on annual totals must passtimeframe = "lowest". -
storagerole slots declare, parameter slots parameterise.@input,@outputand@storagekeepcomm,unitandcap2act; every capacity and cost lives in@capacity,@invcostor@fixomunder a part prefix —out.(discharger, power),inp.(charger, power),stg.(reservoir, energy). Old spellings are errors naming the column to write. - Output-side parameters are renamed to agree with the variables:
pStorageCap→pStorageOutCap,pStorageInvcost→pStorageOutInvcost, and so on for all ten families. -
vStorageCap/vStorageNewCapare nowvStorageOutCap/vStorageOutNewCap; the old names resolve to nothing rather than returning power where energy is meant. -
vStorageStoreis nowvStorageLevel, witheqStorageStore→eqStorageLeveland the maps following. Numbers are unchanged. -
storage@cap2stgis now@duration(pStorageCap2stg→pStorageDuration);cap2stg =warns once per session.ncap2stgin@aeffis unrelated and unchanged. -
storage@chargeis now@startLeveland the value is annual, added once per cycle at the cycle’s first timeslice. It carries notimeslicecolumn; one on a deprecated spelling is dropped with a warning. -
eqStorageClearis noweqStorageOutLevel; the deadeqStorageCleanplaceholder is removed. -
plot_demand(),plot_weather()andplot_process_windows()are no longer exported — all three are reachable throughautoplot(). - Pyomo-Abstract is retired to
drafts/; asking for it raises an error naming a Concrete option. No shipped solver option ever selected it, and it had fallen behind three refactors. -
@sharedThroughputis removed, having been added and withdrawn in the same cycle. It never bound for a single-commodity storage and compared incommensurable units for a multi-commodity one. Archived with the measurements indrafts/storage-shared-throughput.R. -
data/topia_modules.rdawas regenerated: bundled datasets serialise S4 objects with the class definition of their time, so objects saved before the storage renames must be rebuilt fromdata-raw/.
New features
- A storage names a commodity per role:
@input$commfills the store,@storage$commis what it holds,@output$commis what it releases. One shape covers a battery, a hydrogen store and a reservoir;@seff$inpeff/$outeffbecome cross-commodity conversion factors. The fused hydrogen storage reproduces the three-object decomposition exactly in 149 variables / 151 constraints instead of 251 / 257. - A storage’s energy capacity is its own variable,
vStorageStgCap(withvStorageStgNewCap), measured in the commodity’s unit, with its own stock, bounds and costs on@storage.@durationbecomes a bound on(storage, region, year)—.fxties energy to power,.lo/.uplet the model choose. - A storage sizes its charger separately:
vStorageInpCap, with@inp2outlinking the two ratings the way@durationlinks energy to power. The default[1, 1]keeps the two sides symmetric. -
@outputaccepts the same capacity and cost columns as the other two parts and folds them into@capacity/@invcost/@fixomat construction; supplying both forms is an error. -
@input$cap2act/@output$cap2actmake a storage capacity a rate. It defaults to 8760, so a capacity reads as commodity per hour on any calendar; the flow bounds previously meant “per timeslice”, so refining an hourly model to 4-hourly silently quartered every store. The storing side deliberately has nocap2act. - The charger and reservoir gained the parameters only the discharger had (
ret.*,wacc,payback,retcost). These are declared and inert — storage retirement has no equation in any backend. -
storage_duration(scen, width = )splits a storage level into duration bands by nested floors, so the bands partition the level with no remainder and sum back exactly. Verified against a brute-force definition. -
@invcost$paybackworks on GAMS, Julia and Pyomo-Concrete, not just GLPK.eqXCapkeepspXOlifeeverywhere — only the cost window moves. Pyomo-Abstract still refuses it. -
commodity@property: a tidy table of physical properties — heating values, density, molar mass, composition — with uncertainty columns. It is reference data; no solver template sees it.commodity_properties()lists recognised names,commodity_property()reads one value. -
newCommodity()gainsimage =andicon =;object_image()resolves either.report()defaultsimage_filefrom the object’s ownmisc$image. -
levcost()computes analytically — no solver required. Newmethod = c("auto", "analytic", "solve");"auto"prices analytically whenever the technology qualifies and falls back with a message naming the reason. A vintaged or clustered technology is priced per cell directly. - Group shares report every corner solution: the analytic result evaluates all vertices of the input and output share polytopes and returns them in
$frontier_vertices. - Per-vintage levelized costs in reports: a “Levelized Cost by Vintage” section, a Vintages table and per-vintage cost tables. Detail figures show one instance (newest vintage, first cluster), labelled in the section header.
-
report_generic.Rmdis rebuilt on the vehicle two-column layout with one sizing system — fractions of the text width shared by HTML and LaTeX, ggplots rasterised at 150 dpi through one helper. Word output renders through a Word-safe branch. -
autoplot()on objects draws points for given data and lines for the interpolated series, withinterpolate = TRUEand a newshow_defaults = FALSEthat draws mapped-but-unset parameters at their defaults. -
autoplot.weather()/plot_weather()gainregion— pass a count or names to keep a legible subset of a 41-region model. -
techspecreads and writes JSON as well as YAML; the process designer gained a “Save JSON” button. -
storage@input,@outputand@storagetake aunitcolumn, carried for reporting andconvert(). It never reaches the solver. - New data
vre_cf— 8760 hourly capacity factors for one wind and one solar resource from MERRA-2 viamerra2ools, keyed in thed365_h24vocabulary — andvre_storage_duration, the precomputed decomposition of a battery in a full-year hourly model. - New
theme_energyRt(), andplot()delegates toautoplot()for every class that has one.
Bug fixes
-
storage@fullYearhad no effect — every storage cycled within its parent timeframe.mStorageFullYearwas built and declared in all four backends but referenced by no equation, so multi-day and seasonal storage were not representable. Results change for any model with a calendar three or more levels deep; setfullYear = FALSEto keep the old behaviour. - A storage requesting
fullYear = TRUEon a calendar with no year-wide successor map now falls back to the parent-timeframe cycle with a warning rather than dropping out of the balance entirely — which left its level unconstrained by history. - Each
@seffcoefficient reaches its own role’s commodity.ob2mi()assigned into a shared frame, so whichever parameter went first stampedcommand the other two inherited it — an EV’s kilometres-per-kWh was filed under electricity and the motor ran at efficiency 1. -
mStorageInpTot/mStorageOutTotfollowed the level’s commodity, so a hydrogen store never appeared in theELCbalance and sat unused at a valid-looking optimum. Each total now follows its own flow. - The stored commodity never reached
mCommReg, somvStorageLevelemptied and the storage lost its level variable altogether — built, solved and reported, storing nothing. -
vStorageInp/vStorageOutwere declared overmvStorageLeveland reported zero flow for any storage whose level commodity differed from what it exchanged. -
storage_duration()returned zero rows with no message on month-based calendars — it calledtsl2dtm()withoutmday. It now stops with the format it read when a level genuinely cannot be dated. -
tsl2dtm()died withobject 'dtm' not foundfor formats it has no branch for; it returnsNULLinstead. -
draw()on a storage drew no arrows unless@seffwas populated — the commodity frame was cross-joined with a slot whose prototype has zero rows. Arrows now come from the roles. -
draw()printed one label per region: a technology’s@ceffholds a row per region, so a 41-node model drew 36 stacked labels on one arrow. Values collapse to a single number or amin-maxrange. -
draw()showed no duration label once@durationbecame a bound. -
autoplot()on process objects reported “No year-indexed data to plot” for virtually every object:year = NULLwas passed intogetData()’s..., where a NULL selector matched nothing. NULL/empty filters are now ignored. - The parameter registry behind
getData(interpolate = TRUE)was silently empty — it read.modInp@parameters, but the baked-in.modInpis a plain YAML list, so every lookup fell back to generic interpolation. -
getData(object, interpolate = TRUE)crashed on any empty data-frame slot; demand plots dropped region-NA rows on aggregation. -
print()on agetUnits()result and on a commodity was dead code:@exportalone does not register an S3 method on an S4 generic. - Empty columns are dropped throughout the report templates, and NA cells render as a dash (
na.string=was never akableargument). - PDF soundness: correct LaTeX escaping (the old
fixed = TRUEpatterns never matched$ ^ { }), forward-slashed image paths in HTML, single kable escaping, and aligned HTML/LaTeX column fractions. -
report()passes only the params a template declares, so custom templates keep rendering asreport()grows new ones. - On the
levcost()solve path the annual capacity factor collapsed from@weathernever reached the solver, so weather-driven technologies were priced at full availability. -
newStorage(commodity = , storage = list(invcost = ))silently dropped theinvcost: the commodity shorthand replaced a supplied part frame instead of adding to it. -
cap2stgcould never work — the formal default was a bareduration = 1, so the deprecation path always fired. -
prod()over several weather factors is verified on GLPK and Julia/HiGHS; the deadwrite_jump()guard refusing multi-factor models is retired.
Documentation
- New article Units: where each unit is declared, how
convert()moves between dimensions, howcommodity@propertylets a conversion cross them, and why money needs a year attached. - The Storage article is restyled to the modelling convention and extended to the slots it previously skipped, with a slot map of the whole object.
-
Model bricks corrected:
cap2act = 8.76was documented as 8.76 GWh per GW per year, wrong by a factor of 1000. The text now states the unit basis explicitly. - Slot documentation for
storage@fullYearrewritten — theTRUEandFALSEbranches were described with the same sentence.technology@fullYear, documented as ignored, in fact governs ramping. - A coarse calendar remains inadequate for storage even with rate-based capacities: on slices longer than the discharge duration the power bound goes slack and the cycle count collapses. A store whose cycle is shorter than one timeslice is understated, not approximated.
energyRt 0.74.0.9000-dev
Breaking changes
- Package options are prefixed
en.—options(verbose = )becomesoptions(en.verbose = ), and so on. The old names collided with base R. The documented API is theget_*()/set_*()functions, which are unchanged. - Environment variables are prefixed
ENERGYRT_. The old unprefixed names work for one release and warn once;NEOS_EMAILdeliberately keeps its bare name. -
en.verboseis a level (0,1,2, …) tested withisVerbose(level); the separateenergyRt.verboseoption is deprecated for one release. - Dead model code removed: the LEC family (
eqLECActivity,meqLECActivity,mLECRegion,pLECLoACT) andmvTradeCost/mvTradeRowCost. An audit of all 237 mapping parameters found nothing could ever fill them. Objectives are unchanged; every written.datloses five meaningless lines.
New features
-
en_config_write()/en_config_read()persist settings to a config file, applied at load without overriding an option or environment variable already set. It writes totools::R_user_dir("energyRt", "config")— CRAN policy does not permit writing in the user’s home filespace — and backs up legacy files. -
en_config_show()prints every option with its value and where that value came from. The first thing to run when a solver is not found. -
set_solver_path()/get_solver_path(), the generic form of the six per-solver setters, which are now thin wrappers. -
?energyRt-optionsdocuments all seventeen options, generated from the declarations so it cannot drift. -
en.debuggainedisDebug()and now gates internal consistency warnings that used to be silent — or, in one case, calledbrowser().
Bug fixes
- An unservable demand warns instead of stopping interpolation, so an incomplete model can be inspected. Restore the old behaviour with
options(en.model_checks_stop = TRUE). -
get_scenarios_path()was defined twice, the second silently shadowing the first. -
.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. -
interpolate_model(ondisk = TRUE)referencedmi_pathbefore assignment. - The default solver reported
lang = "glpk"whilesolver_options$glpkreported"GLPK", so the two compared unequal. - Two test scripts changed the default solver without restoring it.
energyRt 0.70.5.9000-dev
Breaking changes
- A class’s main data slot is now named after the class:
demand@dem→@demand(and its value column),supply@availability→@supply,import@imp→@import,export@exp→@export.dem =,imp =andexp =still partial-match;availability =fails loudly. - The
subclass is nowsubsidy, with@sub→@subsidyandnewSubsidy()as the constructor (newSub()remains an alias). The modInp set dimension and thepSub*parameters are unchanged. -
geo_map()is nowplot_map();geo_*isgeoscales’ prefix. - The first argument of every
plot_*()function and ofdraw()isobject;yearsis nowyear, matchinggetData()andgetMix(). -
typealways means the quantity shown; the chart shape is nowstyle. - Nothing in this group changes the generated solver models — every GAMS/GLPK file written before and after is byte-identical.
New features
- A commodity can be balanced at a coarser geoscale level than the model’s regions —
newCommodity("STEEL", geolevel = "nation")— the spatial twin ofcommodity@timeframe. It asserts free unlimited transport within that level, so it suits an integrated market and never a network-constrained carrier. GLPK and GAMS only; Julia and Pyomo raise rather than solve a different problem. - A model can carry a
geoscales::Geoscaledescribing how its regions nest, what they weigh and where they are. Attach withnewModel(geoscale = )orsetGeoscale(), read withgetGeoscale().geoscalesis a Suggests — storing, printing, interpolating and solving all work without it. -
plot_trade_map()accepts aGeoscaleas itsmap, falls back to the model’s own, and can draw at a coarserlevel. - Aggregation across regions is delegated to
geoscales::geo_recast()with the rules read off the variable catalogue.vTradeIris netted, not summed. -
topia_geoscale()builds a geoscale for the TOPIA model (nation → zone → region), with geometry from any of the fourtopia$maplayouts. The hierarchy ships as the plain tabletopia$geo. -
levcost()prices vintages and clusters separately, returning alevcost_variantsobject thatautoplot()compares directly. Each cell is priced in its own region so the variants cannot serve each other’s demand. Newrun = c("single", "sequential")andmax_failures. -
getCalendar()gained methods forconfig,modelandscenario— it was declared as a generic with none.
Bug fixes
-
levcost()on a technology declaring@vintageor@clusterwas silently wrong: the expanded cells competed for one unit of demand and the extraction summed across them. -
plot_trade_map()drew routes withgeom_segment()on rawx/ybeside ageom_sf()layer, which works only for a map with no CRS. Routes, centroids and labels are nowsflayers whenever the map is projected. -
tech_from_spec()read@input$combustionback as character, so any technology setting it wrote a valid techspec that then failed to load. -
size()regained its@export: a helper inserted between its roxygen block and the function had silently taken over the block. -
autoplot()on models and repositories fails with energyRt’s own message when ggplot2 is absent.
energyRt 0.70.4.9000-dev
Breaking changes
-
scenario@modOut@variables$vTechCapreturns avariableobject, not a data.frame.$,[[,dim(),names()andas.data.frame()work on it;merge(),rbind()andcolnames<-do not. UsegetData()orget_variable(). - The on-disk scenario layout changed — a variable’s data moved to
variables/<name>/data/. Directories carry alayoutfile andload_scenario()refuses an older one rather than reading it as empty. -
vTechRetiredNewCapread from a GDX names its second year columnyearp, matching every other engine; thesrc/dstrenaming forvTradeIrapplies to all engines. -
vDummyImportCost/vDummyExportCostare the solver’s own values; the R recomputation that overwrote them after every solve is removed. -
parameter@misc$nValuesis gone. It truncated 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 for the same scenario.
New features
- Model variables are S4 objects.
scenario@modOut@variablesis a list ofvariableobjects asmodInp@parametersis a list ofparameterobjects, both extending a shared virtual classmodelData. - A
variableknows its dimensions, output columns, gating map, positivity,role,unitkind and origin. The specification is composed at build time from the GAMS source, the GLPK template anddata-raw/variables.yml, and validated for completeness. -
modOutpre-populates every declared variable, so one the solver skipped still reports its column names. - The “never sum this over slices” rule and the sign of each series in a generation mix now come from the specification instead of being hard-coded.
Bug fixes
-
summary()on a solved scenario never reported dummy import/export costs — it read a variable name that has never existed. Dummy flows mask infeasibility. -
model_structureattached each variable’s dimensions and gating map to the wrong variable: names came from one generated table and dims from another. -
model_size()counted a gating map’s rows once however many variables it gated. -
.get_data_slot()read a parameter’s data slot directly, so every call site saw zero rows for an on-disk parameter. -
update_parameter()built an on-disk path missing itsparameterssegment.
energyRt 0.70.3.9000-dev
Breaking changes
-
config@discountholdsregion,year,waccandsdr— there is nodiscountcolumn.pDiscountis replaced bypWaccandpSdr;pDiscountFactorkeeps its name and is built fromsdr. -
pTechEac,pStorageEacandpTradeEacread@invcost$eacinstead of@invcost$invcost. -
levcost()annuitises at thewacc;report()prints both rates under their own names. -
trade@capacityVariableis removed — a trade’s capacity is always a decision variable. For a fixed transfer limit with no investment, drop@capacity/@invcostand settrade = data.frame(src =, dst =, ava.up = ). -
@start,@endand@olifeare no longer slots; they are columns of the new@vintage. The constructors still accept them as arguments, but saved objects from earlier versions must be rebuilt. - Passing an unrecognised slot name to a constructor errors instead of being silently dropped.
New features
- A model has two rates:
waccannuitises investment,sdrdiscounts the cost stream, with no fallback between them.discountsurvives as an argument —newModel(discount = 0.05)sets both. - Technologies, storages and trades can carry their own
@invcost$wacc. There is deliberately no per-processsdr. -
@invcost$eacis honoured: supply the annuity directly and it is used verbatim. - New
@invcost$payback, the cost-recovery period — the investment is repaid overpaybackyears while capacity operates for its full@vintage$olife. GLPK only in this version; the other writers refuse a model that sets it. - Process classes can describe a group of similar processes evolving over time (vintage) or across space/type (cluster). Add a
vintageand/orclustercolumn to the data slots and the object is replicated into one ordinary process per cell before the model is built — equations and set dimensions are unchanged. - New
@vintageslot ontechnology,storageandtrade, and@clusteron the first two. Variant names suffix the base name (ECOA_VIN2030_CLnorth), controlled byconfig@variant_prefix. -
getData()gainsvariants = TRUE, attachingbase,class,vintageandclustercolumns. NewgetVariants()andvariantSummary()report the expansion. - A
vintage = "TOTAL"orcluster = "TOTAL"row in@capacitybounds the sum over that process’s variants, generating a single group constraint. -
storage@cap2stgis promoted from a scalar to a data.frame.
Bug fixes
- A per-region group capacity bound was applied to each region’s variants separately instead of to their sum.
-
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
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.
