Julia library for solving the generation and transmission capacity expansion problem, implemented as a linear program using JuMP. On top of the optimisation model, the library provides:
- Time series aggregation (
src/aggreg.jl) — chronological clustering of demand and renewable availability series, used to reduce the number of time periods in an instance while keeping the temporal order. - Plotting (
src/plotting.jl) — a set of standard result plots (dispatch, capacities, storage levels, transmission flows, loss of load). - Data generation (
src/Data_generation/) — building instance datasets for European regions from GlobalEnergyGIS output.
The documentation contains the full mathematical formulation of the model.
The library is authored by Lucas Condeixa, Fabricio Oliveira, and Jaan Tollander de Balsch in the Systems Analysis Laboratory at Aalto University.
The constraints included in the model are selected through the Specs struct. All fields default to false except transmission.
| Field | Description |
|---|---|
transmission |
Transmission lines between nodes, with investment and flow limits |
renewable_target |
Minimum share of generation coming from renewable technologies |
carbon_cap |
Emission reduction target relative to a reference year |
nuclear_limit |
Maximum allowed share of nuclear generation |
storage |
Storage technologies (charge/discharge, capacity investment) |
ramping |
Ramp-up and ramp-down limits on generation |
voltage_angles |
DC power flow voltage angle constraints |
hydro |
Hydro reservoirs and run-of-river, including environmental flow |
hydro_simple |
Simplified hydro representation (use instead of hydro) |
Time periods may represent more than one hour (τ_t), which is what makes the aggregated instances produced by src/aggreg.jl usable directly in the model.
src/ Library source
model.jl Specs, Params, EnergySystemModel, Expressions
io.jl Instance loading, JSON/JLD2 serialisation, clustered instances
aggreg.jl Chronological time series aggregation
plotting.jl Result plots
Data_generation/ Instance generation from GlobalEnergyGIS data
examples/
constants/ constants.json shared by all instances
structures/ Node structures (8nodes, 11nodes, ...) and their instances
run.jl, plot.jl Example driver scripts
docs/ Documenter.jl documentation sources
.triton/exe/ Slurm driver scripts used on the Triton cluster
This library can be installed directly from GitHub
pkg> add https://github.com/gamma-opt/EnergySystemModeling.jl
Julia 1.10 or newer is required (see [compat] in Project.toml).
An instance is defined by two paths: the shared constants directory and the instance directory.
constants/constants.json holds the general parameters kappa, mu, C, C_bar, C_E, R_E, r and Fmin.
The instance directory contains:
indices.json— fieldsG,G_r,N,L,L_ind,T,S,Hnodes/— per-node time series (1.csv,2.csv, ...) with demand and availability columnsgen_technology.csv,gen_capacity.csv— generation technology costs, lifetimes, efficiencies, emissions, ramp limits and initial capacitiestransmission.csv— line cost, distance, lifetime, capacity limits and susceptancestorage.csv,sto_capacity.csv— storage technology parameters and capacity limitshydro.csv,hydro_technology.csv,hydro_capacity.csv— hydro reservoir and run-of-river parametersnodes_specs.csv— node-level specifications
Each instance directory carries a README.md listing the node, technology and line indexing for that instance (indices start at 1, following the Julia convention).
examples/run.jl demonstrates the workflow on the instances shipped in examples/structures.
using EnergySystemModeling
constants_path = joinpath("examples", "constants")
instance_path = joinpath("examples", "structures", "8nodes", "instances", "ftr", "08n8760h_ftr")
parameters = Params(constants_path, instance_path)
specs = Specs(
transmission=true,
renewable_target=true,
carbon_cap=true,
nuclear_limit=false,
storage=true,
ramping=true,
voltage_angles=false,
hydro=true,
hydro_simple=false
)
# EnergySystemModel returns the JuMP model together with dictionaries
# describing the variables and objective terms that were created.
(model, VariablesDict, ObjectivesDict) = EnergySystemModel(parameters, specs)using Gurobi, JuMP
optimizer = optimizer_with_attributes(Gurobi.Optimizer,
"TimeLimit" => 60*60*2,
"LogFile" => joinpath(output_path, "gurobi.log"))
set_optimizer(model, optimizer)
set_optimizer_attributes(model, "Method" => 2)
set_optimizer_attributes(model, "Crossover" => 0)
set_optimizer_attributes(model, "NumericFocus" => 1)
optimize!(model)Barrier without crossover (Method => 2, Crossover => 0) is a good default for the large instances; NumericFocus and ScaleFlag help with the numerically harder ones.
JuMPVar and JuMPObj turn the solved model into plain dictionaries of arrays, and Expressions computes derived quantities (renewable share κ′, hydro share μ′, emission reduction C′_E).
variables = JuMPVar(model, VariablesDict)
objectives = JuMPObj(model, ObjectivesDict)
expressions = Expressions(parameters, specs, variables)JSON, for small instances and for inspecting results by hand:
save_json(specs, joinpath(output_path, "specs.json"))
save_json(parameters, joinpath(output_path, "parameters.json"))
save_json(variables, joinpath(output_path, "variables.json"))
save_json(objectives, joinpath(output_path, "objectives.json"))
save_json(expressions, joinpath(output_path, "expressions.json"))
specs = load_json(Specs, joinpath(output_path, "specs.json"))
parameters = load_json(Params, joinpath(output_path, "parameters.json"))JLD2, which is what the cluster runs use — considerably faster and smaller for full-year instances:
using JLD2
JLD2.save(joinpath(output_path, "variables.jld2"), variables; compress = true)
JLD2.save(joinpath(output_path, "objectives.jld2"), objectives; compress = true)
JLD2.save(joinpath(output_path, "expressions.jld2"), expressions; compress = true)Parsing the CSV instance data is expensive, so it is worth doing once with perform_Params, which builds the Params object and stores it as parameters.jld2:
perform_Params(constants_path, instance_path, params_path)Individual plotting functions (plot_objective_values, plot_generation_dispatch, plot_generation_capacities, plot_generation_capacities_stacked, plot_transmission_flow, plot_transmission_capacities, plot_transmission_bars, plot_storage_level, plot_storage_capacities, plot_loss_of_load, plot_box, plot_box_all, plot_dispatch_bars) each return a Plots object.
perform_plotting runs a selected subset of them and writes both PDF and PNG into <plots_output_path>/pdf and <plots_output_path>/png:
Plots_specs = Dict{String,Bool}(
"p1" => true, # objective function values
"p2" => true, # dispatch and storage levels, per node
"p3" => true, # storage capacities
"p4" => true, # generation dispatch levels (box plots)
"p5" => true, # generation capacities, stacked
"p6" => false, # consolidated dispatch vs demand
"p7" => false, # transmission flow, per line
"p8" => false, # transmission capacities
"p9" => false, # consolidated transmission flow
"p10" => false, # loss of load
"p11" => false # in development
)
perform_plotting(Plots_specs, parameters, variables, objectives, expressions, plots_output_path)The pdf and png subdirectories must exist beforehand.
Solving a full 8760-hour instance is expensive, so the library provides chronological aggregation that merges neighbouring time steps into clusters while preserving their order. Each resulting period keeps a weight, which the model reads as τ_t.
The aggregation runs on a matrix series whose columns are the individual time series (demand per node, followed by availability per technology and node) and whose rows are time steps:
# Instance carrying the series and the aggregation settings
_SeriesInstance = load_series_instance(
series,
block_size, # clusters merged per iteration
current_k, # starting number of clusters
stopping_k, # target number of clusters
dm, # discrepancy metric, e.g. :ed (euclidean) or :wd (ward)
rep_value # cluster representative value, e.g. :mean
)
# Initial clustering state (one cluster per time step)
_ClustInstance = load_clust_instance(k_cent, series_clust)
# Aggregate down to stopping_k, recording every intermediate clustering
find_clusters!(_SeriesInstance, _ClustInstance, _DistUpdate)write_clust_instance! then materialises the clustered instances as instance directories that Params/change_time_parameters can read back, and read_clust_instance / read_clusters load them again.
To solve an aggregated instance, load the pre-computed full-resolution parameters and swap in the clustered time dimension:
parameters = change_time_parameters(params_path_ftr, instance_path_clust)
# optionally: nosun = true to zero out solar availabilitybreak_FTR_parameters_in_periods does the same for a sub-range of periods, which is used for the representative-day runs.
.triton/exe/aggregation/ contains the driver scripts for this workflow: form_aggreg_instances_new.jl runs the aggregation itself, and generate_clust_instances.jl writes out the resulting instance folders.
src/Data_generation/ builds instance datasets for European countries and regions from GlobalEnergyGIS output. run_data_generation.jl is the entry point — set the SSP scenario, target year, ERA year, region list, time horizon and number of technologies at the top of the file, then call create_data_sets, which writes a new instance directory under examples/structures. See docs/src/datageneration.md for details.
.triton/exe/ holds the Slurm scripts used to run the model on Aalto's Triton cluster, split into aggregation/ (building clustered instances), opt/ (solving them) and plot/ (post-processing). The opt directory contains variants of the driver script for the different experiment setups: run.jl, run_FTR.jl (full time resolution), run_clust.jl (aggregated instances), run_FTR_days.jl (representative days), run_fix.jl and run_min_cap.jl. These scripts contain absolute paths and are meant to be adapted rather than run as-is.
Install the Julia programming language.
Clone the repository
git clone https://github.com/gamma-opt/EnergySystemModeling.jl.gitIn the project root directory, install packages locally using Julia's package manager.
pkg> dev .
Install a solver such as Gurobi.
It's up to the user to choose a suitable solver for solving the JuMP model. For small instances, GLPK is sufficient, but for large instances, we recommend commercial solvers such as Gurobi or CPLEX.
Gurobi is a powerful commercial optimizer that provides a free academic license. We can interface with Gurobi in Julia using Gurobi.jl. Here are the steps to install Julia and Gurobi to run the program:
-
Obtain a license of Gurobi and install Gurobi solver by following the instructions on Gurobi's website.
-
Make sure the
GUROBI_HOMEenvironmental variable is set to the path of the Gurobi directory. This is part of standard installation. The Gurobi library will be searched for inGUROBI_HOME/libon Unix platforms andGUROBI_HOME\binon Windows. If the library is not found, check that your version is listed indeps/build.jl. The environmental variable can be set by appendingexport GUROBI_HOME="<path>/gurobi811/linux64"to.bashrcfile. Replace the<path>, platformlinux64and version number811with the values of your Gurobi installation. -
Install
Gurobi.jlin Julia's package manager by running commandspkg> add Gurobi pkg> build Gurobi
The project documentation is created using Documenter.jl. It covers the mathematical formulation (index.md), the aggregation method (aggreg.md), plotting (plotting.md), data generation (datageneration.md) and the full API reference (api.md).
To build the documentation, navigate inside the docs directory and run the command
julia make.jl