├── .gitignore ├── .gitlab-ci.yml ├── .github └── workflows │ └── main.yml ├── test.sh ├── dcflow_edge.mod ├── dcflow_arc.mod ├── equilibrium.mod ├── intertemporal.mod ├── unitcommitment.mod ├── nminusone.mod ├── soforsg.mod ├── dhmnl.mod ├── README.md └── startupandpartial.mod /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | \#*\# -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: ubuntu:latest 2 | 3 | before_script: 4 | - apt-get update -qq && apt-get install -y -qq glpk-utils 5 | 6 | test: 7 | script: 8 | - chmod +x ./test.sh && ./test.sh -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Install GLPK 13 | run: sudo apt-get install glpk-utils 14 | - name: Run basic tests 15 | run: chmod +x ./test.sh && ./test.sh 16 | 17 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # small check that all models still compile 4 | error_code=0 5 | for model_file in *.mod; 6 | do 7 | glpk_output=$(glpsol -m $model_file 2>&1) 8 | if [ $? -ne 0 ]; then 9 | error_code=1 10 | printf "Error in '$model_file':\n" 11 | echo "----------------------------------------------------------------------------------" 12 | echo "${glpk_output}" 13 | echo "----------------------------------------------------------------------------------" 14 | fi 15 | done 16 | exit $error_code 17 | -------------------------------------------------------------------------------- /dcflow_edge.mod: -------------------------------------------------------------------------------- 1 | set vertex; 2 | set edge within vertex cross vertex; 3 | 4 | param demand{vertex} >= 0; 5 | param cap_gen{vertex} >= 0; 6 | param costs{vertex}; 7 | param cap_tra{edge} >= 0; 8 | param admittance{edge}; # MW/deg 9 | 10 | var generation{v in vertex} >= 0, <= cap_gen[v]; 11 | var flow{(v1,v2) in edge} >= -cap_tra[v1,v2], <= cap_tra[v1,v2]; 12 | var angle{vertex}; 13 | 14 | minimize obj: sum{v in vertex} costs[v] * generation[v]; 15 | 16 | s.t. demand_satisfaction{v in vertex}: 17 | demand[v] <= generation[v] 18 | + sum{(other, v) in edge} flow[other, v] 19 | - sum{(v, other) in edge} flow[v, other]; 20 | 21 | s.t. dc_flow{(v1, v2) in edge}: 22 | flow[v1, v2] = admittance[v1, v2] * (angle[v1] - angle[v2]); 23 | 24 | solve; 25 | 26 | printf "\nVERTEX BALANCE\n"; 27 | printf " %-3s: %8s %8s %8s %8s\n", "v", "gen", "dem", "angle", "nodal"; 28 | printf " %3s %8s %8s %8s %8s\n", "", "MW", "MW", "deg", "EUR/MWh"; 29 | printf "----------------------------------------------\n"; 30 | printf{v in vertex}: 31 | " %-3s: %8g %8g %8g %8g\n", 32 | v, generation[v], demand[v], angle[v], -demand_satisfaction[v].dual; 33 | 34 | printf "\nEDGE BALANCE\n"; 35 | printf " %-3s, %-3s: %8s\n", "v1", "v2", "flow"; 36 | printf "---------------------\n"; 37 | printf{(i,j) in edge: flow[i,j] <> 0}: 38 | " %-3s, %-3s: %8g\n", i, j, flow[i,j]; 39 | printf "\n"; 40 | 41 | data; 42 | 43 | 44 | param: vertex: demand cap_gen costs := 45 | v1 0 10 10 46 | v2 0 5 20 47 | v3 8 0 0; 48 | 49 | param: edge : cap_tra admittance:= 50 | v1 v2 10 1 51 | v2 v3 10 1 52 | v1 v3 10 1; 53 | 54 | end; 55 | 56 | -------------------------------------------------------------------------------- /dcflow_arc.mod: -------------------------------------------------------------------------------- 1 | set vertex; 2 | set arc within vertex cross vertex; 3 | 4 | param demand{vertex} >= 0; 5 | param cap_gen{vertex} >= 0; 6 | param costs{vertex}; 7 | param cap_tra{arc} >= 0; 8 | param admittance{arc}; # MW/deg 9 | 10 | var generation{v in vertex} >= 0, <= cap_gen[v]; 11 | var flow{(v1,v2) in arc} >= 0, <= cap_tra[v1,v2]; 12 | var angle{vertex}; 13 | 14 | minimize obj: sum{v in vertex} costs[v] * generation[v]; 15 | 16 | s.t. demand_satisfaction{v in vertex}: 17 | demand[v] <= generation[v] 18 | + sum{(other, v) in arc} flow[other, v] 19 | - sum{(v, other) in arc} flow[v, other]; 20 | 21 | s.t. dc_flow{(v1, v2) in arc}: 22 | flow[v1, v2] - flow[v2, v1] = admittance[v1, v2] 23 | * (angle[v1] - angle[v2]); 24 | 25 | solve; 26 | 27 | printf "\nVERTEX BALANCE\n"; 28 | printf " %-3s: %8s %8s %8s %8s\n", "v", "gen", "dem", "angle", "nodal"; 29 | printf " %3s %8s %8s %8s %8s\n", "", "MW", "MW", "deg", "EUR/MWh"; 30 | printf "----------------------------------------------\n"; 31 | printf{v in vertex}: 32 | " %-3s: %8g %8g %8g %8g\n", 33 | v, generation[v], demand[v], angle[v], -demand_satisfaction[v].dual; 34 | 35 | printf "\nARC BALANCE\n"; 36 | printf " %-3s, %-3s: %8s\n", "v1", "v2", "flow"; 37 | printf "---------------------\n"; 38 | printf{(i,j) in arc: flow[i,j] <> 0}: 39 | " %-3s, %-3s: %8g\n", i, j, flow[i,j]; 40 | printf "\n"; 41 | 42 | data; 43 | 44 | 45 | param: vertex: demand cap_gen costs := 46 | v1 0 10 10 47 | v2 0 5 20 48 | v3 8 0 0; 49 | 50 | param: arc : cap_tra admittance:= 51 | # original edges 52 | v1 v2 10 1 53 | v2 v3 10 1 54 | v1 v3 10 1 55 | # reversed copies 56 | v2 v1 10 1 57 | v3 v2 10 1 58 | v3 v1 10 1; 59 | 60 | end; 61 | 62 | -------------------------------------------------------------------------------- /equilibrium.mod: -------------------------------------------------------------------------------- 1 | # EQUILIBRIUM: a minimal market equilibrium as LP model 2 | # Last updated: 11 Nov 2016 3 | # Author: johannes.dorfner@tum.de 4 | # License: CC0 5 | 6 | # USAGE 7 | # glpsol -m equilibrium.mod 8 | 9 | # OVERVIEW 10 | # 11 | # This LP model finds the maximum welfare solution for a given set of 12 | # a) a discretised production cost curve (i.e. a merit order curve) and 13 | # b) a discretised utility function of customers (i.e. a price-demand curve). 14 | # 15 | # An additional parameter can penalise certain power production types by 16 | # increasing their production costs relative to their emissions. This can change 17 | # the order of the producers in the merit order and the total demand that is to 18 | # be satisfied in the case of maximum welfare. 19 | 20 | 21 | 22 | # SETS 23 | set consumer; # energy demands 24 | set producer; # power generator 25 | 26 | # PARAMETERS 27 | param amount{c in consumer}; # quantity of demand (MWh) 28 | param util{c in consumer}; # utility of demand (EUR/MWh) 29 | 30 | param capacity{p in producer}; # maximum power possible generation (MWh) 31 | param varcost{p in producer}; # marginal costs (EUR/MWh) 32 | param emissions{p in producer}; # specific GHG emissions (t/MWh) 33 | 34 | param ghg_tax, default 0; # additional price for emissions (EUR/t) 35 | 36 | # VARIABLES 37 | var satisfied{c in consumer}, >= 0, <= 1; # fraction [0-1] of amount c that is satisfied 38 | var generated{p in producer}, >= 0, <= 1; # part [0-1] of capacity p that is used 39 | var production_cost; 40 | var total_utility; 41 | 42 | # OBJECTIVE 43 | # minimize the sum of production costs minus total utility 44 | minimize objective_function: 45 | production_cost - total_utility; 46 | 47 | s.t. def_production_cost: 48 | production_cost = sum{p in producer} 49 | generated[p] * capacity[p] * 50 | (varcost[p] + ghg_tax * emissions[p]); 51 | 52 | s.t. def_total_utility: 53 | total_utility = sum{c in consumer} 54 | satisfied[c] * amount[c] * 55 | util[c]; 56 | 57 | s.t. supply_equals_demand: 58 | sum{p in producer} generated[p] * capacity[p] = 59 | sum{c in consumer} satisfied[c] * amount[c]; 60 | 61 | # SOLVE 62 | solve; 63 | 64 | # OUTPUT 65 | printf "\nRESULT\n"; 66 | printf "------\n"; 67 | printf "Objective val: %8.1g EUR\n", sum{p in producer} generated[p] * capacity[p] * (varcost[p] + ghg_tax * emissions[p]) - sum{c in consumer} satisfied[c] * amount[c] * util[c]; 68 | printf "Variable cost: %8.1g EUR (%4g MWh total production)\n", sum{p in producer} generated[p] * capacity[p] * varcost[p], sum{p in producer} generated[p] * capacity[p]; 69 | printf "Emission tax: %8.1g EUR (%4g EUR/MWh)\n", sum{p in producer} generated[p] * capacity[p] * ghg_tax * emissions[p], ghg_tax; 70 | printf "Utility: %8.1g EUR (negative=good)\n", -sum{c in consumer} satisfied[c] * amount[c] * util[c]; 71 | 72 | printf "\nENERGY PRODUCED\n"; 73 | printf "---------------\n"; 74 | printf{p in producer: generated[p] > 0}: "%-3s: %3.0f\n", p, generated[p] * capacity[p]; 75 | printf "\nSATISFIED DEMAND\n"; 76 | printf "----------------\n"; 77 | printf{c in consumer: satisfied[c] > 0}: "%-3s: %3.0f\n", c, satisfied[c] * amount[c]; 78 | printf "\n"; 79 | 80 | # DATA 81 | data; 82 | param ghg_tax := 0; # GHG tax (interesting value range: 0 to 4) 83 | 84 | param: consumer: amount util:= 85 | c1 50 10 # necessary consumption 86 | c2 10 9 # basic convenience 87 | c3 20 8 # intermediate value 88 | c4 5 5 89 | c5 900 4; # 'luxury' consumption; 90 | 91 | param: producer: capacity varcost emissions:= 92 | p1 65 1 3 # cheap & dirt plant 93 | p2 50 2 2 # more expensive, cleaner 94 | p3 60 3 1 # high cost, almost GHG free 95 | p4 80 5 0; # highest cost, GHG free 96 | -------------------------------------------------------------------------------- /intertemporal.mod: -------------------------------------------------------------------------------- 1 | # INTERTEMPORAL: minimal intertemporal capacity expansion problem 2 | # Last updated: 18 Nov 2016 3 | # Author: johannes.dorfner@tum.de 4 | # License: CC0 5 | # 6 | # USAGE 7 | # glpsol -m intertemporal.mod 8 | # 9 | # OVERVIEW 10 | # This LP model finds the minimum cost investment plan for for a set of two 11 | # power plant technologies over multiple decades, allowing investment decisions 12 | # every five years. Old investments phase out of the power plant fleet after 13 | # their lifetime is over. A discount factor incentivises later investment, while 14 | # demand must be satisfied throughout the years. 15 | # 16 | 17 | 18 | # SETS 19 | set year; # all years 20 | set yearM within year; # modelled years (investment decisions) 21 | set year0 within year; # initial years (no investments allowed) 22 | set season; # time steps within year 23 | set process; # power plant technologies 24 | 25 | 26 | # PARAMETERS 27 | param interest_rate; # price change of invest/fuel over the years 28 | 29 | param demand{s in season}; # seasonal demand 30 | param weight{s in season}; # seasonal weight 31 | 32 | param discount{yM in yearM} # investment cost reduction for later invest 33 | default interest_rate**((yM - 2010)/5); # exponential decay (ir < 1) or 34 | # exponential growth (ir > 1) 35 | 36 | param invcost{p in process}; # plant investment costs (EUR/MW) 37 | param fuelcost{p in process}; # plant fuel costs (EUR/MWh) 38 | param efficiency{p in process}; # plant efficiency (MWh_out/MWh_in) 39 | param lifetime{p in process}; # plant lifetime (years) 40 | 41 | 42 | # helper data structure: contains all tuples (process, year1, year2) for each 43 | # technology p in process, for which a new-built capacity from year1 is still 44 | # available in year2 (as in: plant lifetime not exceeded yet) 45 | set is_operational within process cross year cross year := setof{ 46 | p in process, y1 in year, y2 in year: 47 | y1 <= y2 and y1 + lifetime[p] >= y2 48 | } (p, y1, y2); 49 | 50 | 51 | # VARIABLES 52 | var costs; 53 | var new_capacity{year, process} >= 0; 54 | var available_capacity{yearM, process} >= 0; 55 | var production{yearM, season, process} >= 0; 56 | 57 | # OBJECTIVE 58 | minimize obj: costs; 59 | 60 | # CONSTRAINTS 61 | s.t. def_costs: 62 | costs = sum{yM in yearM, p in process} 63 | new_capacity[yM, p] * 64 | invcost[p] * 65 | discount[yM] 66 | + sum{yM in yearM, s in season, p in process} 67 | production[yM, s, p] * 68 | fuelcost[p] / 69 | efficiency[p] * 70 | weight[s] * 71 | discount[yM]; 72 | 73 | s.t. res_no_investment_in_initial_years{y0 in year0, p in process}: 74 | new_capacity[y0, p] = 0; 75 | 76 | s.t. def_available_capacity{this_year in yearM, p in process}: 77 | available_capacity[this_year, p] = sum{ 78 | (p, earlier_year, this_year) in is_operational 79 | } new_capacity[earlier_year, p]; 80 | 81 | s.t. res_demand{yM in yearM, s in season}: 82 | demand[s] <= sum{p in process} production[yM, s, p]; 83 | 84 | s.t. res_production_available_capacity{yM in yearM, s in season, p in process}: 85 | production[yM, s, p] <= available_capacity[yM, p]; 86 | 87 | # SOLVE 88 | solve; 89 | 90 | # OUTPUT 91 | printf "\nRESULT\n"; 92 | printf "Total cost: %g kEUR\n", costs/1e3; 93 | printf "Interest rate: %g\n", interest_rate; 94 | 95 | printf "\nCAPACITIES\n"; 96 | printf "%-9s %4s: %6s\n", "Process", "Year", "New Cap"; 97 | printf "-----------------------\n"; 98 | printf{yM in yearM, p in process: new_capacity[yM,p] > 0}: 99 | "%-6s in %4s: %4.0f MW\n", p, yM, new_capacity[yM,p]; 100 | 101 | printf "\nPRODUCTION\n"; 102 | printf "%14s %-6s\n", " ", "Season"; 103 | printf "%-9s %3s:", "Process", "Year"; 104 | printf{s in season}: " %2s", s; 105 | printf "\n----------------------------------\n"; 106 | for {yM in yearM, p in process}: { 107 | printf "%-6s in %2s:", p, yM; 108 | printf{s in season}: 109 | (if production[yM, s, p] > 0 then " %2.0f" else " %2s"), 110 | (if production[yM, s, p] > 0 then production[yM, s, p] else " "); 111 | printf "\n"; 112 | } 113 | printf "\n"; 114 | 115 | # DATA 116 | data; 117 | 118 | set year := 2000 2005 2010 2015 2020 2025 2030 2035 2040; 119 | set year0 := 2000 2005 2010; 120 | set yearM := 2015 2020 2025 2030 2035 2040; 121 | 122 | param interest_rate := 0.8; 123 | 124 | param: season: demand weight:= 125 | S1 10 7300 126 | S2 20 7300 127 | S3 40 7300 128 | S4 70 7300 129 | S5 50 7300 130 | S6 40 7300; 131 | 132 | param: process: invcost fuelcost efficiency lifetime := 133 | stcoal 1000 .01 .45 15 134 | gtgas 450 .02 .40 15; 135 | -------------------------------------------------------------------------------- /unitcommitment.mod: -------------------------------------------------------------------------------- 1 | # UNITCOMMITMENT: a minimal MILP power plant scheduling with startup costs 2 | # Last updated: 21 Nov 2016 3 | # Author: johannes.dorfner@tum.de 4 | # License: CC0 5 | # 6 | # USAGE 7 | # glpsol -m unitcommitment.mod 8 | 9 | 10 | # SETS & PARAMETERS 11 | set time; 12 | set plant; # power plant technologies 13 | set cost_types := {'fixed', 'variable', 'startup', 'shutdown'}; 14 | 15 | # economic 16 | param c_var{plant}; # variable operational cost (EUR/MWh) by production 17 | param c_fix{plant}; # fix operational cost (EUR) per step if plant on 18 | param c_start{plant}; # startup cost of plant (EUR) per occurence 19 | param c_stop{plant}; # shutdown cost of plant (EUR) per occurence 20 | 21 | # plant 22 | param cap_min{plant}; # minimum production capacity of plant (MW) 23 | param cap_max{plant}; # maximum production capacity of plant (MW) 24 | 25 | # time 26 | param demand{time}; # demand timeseries (MW) 27 | 28 | # VARIABLES 29 | var costs{cost_types}; # objective value (EUR) 30 | 31 | var production{time, plant}, >= 0; # power production of plant (MW) 32 | 33 | var on_off{time, plant}, binary; # 1 if plant is on at time, 0 else 34 | var startup{time, plant}, binary; # 1 if plant has started at time, 0 else 35 | var shutdwn{time, plant}, binary; # 1 if plant has shut down at time, 0 else 36 | 37 | 38 | # OBJECTIVE 39 | minimize obj: sum{ct in cost_types} costs[ct]; 40 | 41 | s.t. def_costs_variable: 42 | costs['variable'] = 43 | sum{t in time, p in plant} 44 | (c_var[p] * production[t, p]); 45 | 46 | s.t. def_costs_fixed: 47 | costs['fixed'] = 48 | sum{t in time, p in plant} 49 | (c_fix[p] * on_off[t, p]); 50 | 51 | s.t. def_costs_startup: 52 | costs['startup'] = 53 | sum{t in time, p in plant} 54 | (c_start[p] * startup[t, p]); 55 | 56 | s.t. def_costs_shutdown: 57 | costs['shutdown'] = 58 | sum{t in time, p in plant} 59 | (c_stop[p] * shutdwn[t, p]); 60 | 61 | 62 | # CONSTRAINTS 63 | 64 | s.t. res_demand_satisfaction{t in time}: 65 | demand[t] = sum{p in plant} production[t, p]; 66 | 67 | 68 | s.t. def_startup_shutdown{t in time, p in plant}: 69 | startup[t, p] - shutdwn[t, p] 70 | = 71 | on_off[t, p] - (if t>1 then on_off[t-1,p] else 0); 72 | 73 | 74 | s.t. res_production_minimum{t in time, p in plant}: 75 | production[t, p] >= on_off[t, p] * cap_min[p]; 76 | 77 | 78 | s.t. res_production_maximum{t in time, p in plant}: 79 | production[t, p] <= on_off[t, p] * cap_max[p]; 80 | 81 | 82 | # SOLVE 83 | solve; 84 | 85 | # REPORTING PARAMETERS 86 | param plant_share{p in plant} := (sum{t in time} production[t, p]) / 87 | (sum{t in time} demand[t]) * 100; 88 | param no_of_starts{p in plant} := sum{t in time} startup[t, p]; 89 | param no_of_stops{p in plant} := sum{t in time} shutdwn[t, p]; 90 | 91 | # OUTPUT 92 | printf "RESULT\n"; 93 | printf "\n\nCOSTS\n\n"; 94 | printf " %-12s %8s\n", "type", "kEUR"; 95 | printf " ---------------------\n"; 96 | printf{ct in cost_types} " %-12s %8.1f\n", ct, costs[ct]/1000; 97 | printf " ---------------------\n"; 98 | printf " %-12s %8.1f\n", "total", sum{ct in cost_types} costs[ct]/1000; 99 | 100 | printf "\n\nSCHEDULE\n\n"; 101 | printf " t "; # header line 102 | printf{p in plant}: " %8.8s", p; 103 | printf " %8.8s\n", "Demand"; 104 | printf " "; printf{p in plant}: "---------"; printf "--------------"; 105 | printf "\n"; # table body 106 | for{t in time} { 107 | printf " %2s ", t; # timestep number 108 | printf{p in plant}: " %8g", production[t, p]; # production by plant 109 | printf " %8g ", demand[t]; # final column: demand 110 | printf{d in 500..demand[t] by 1000} "="; # demand barchart idea 111 | printf "\n"; 112 | } 113 | printf " "; printf{p in plant}: "---------"; printf "--------------"; 114 | printf "\n"; # footer line 115 | printf " %% "; # share 116 | printf{p in plant}: " %8.1f", plant_share[p]; 117 | printf "\n"; 118 | printf " #on "; # number of starts 119 | printf{p in plant}: " %8.0f", no_of_starts[p]; 120 | printf "\n"; 121 | printf " #off"; # number of stops 122 | printf{p in plant}: " %8.0f", no_of_stops[p]; 123 | printf "\n"; 124 | printf "\n"; 125 | 126 | 127 | # DATA 128 | data; 129 | 130 | param: 131 | plant: c_var c_fix c_start c_stop cap_min cap_max := 132 | Nuclear 9 360 3600 1 400 1000 133 | Lignite 13 532 5320 1 400 1000 134 | Coal 20 800 8000 1 400 1000 135 | "Gas CombCycle" 33 1320 13200 1 400 1000 136 | "Gas Turbine" 50 2000 20000 1 400 1000; 137 | 138 | param: 139 | time: demand := 140 | 1 500 141 | 2 1000 142 | 3 2000 143 | 4 2000 144 | 5 2000 145 | 6 2000 146 | 7 2000 147 | 8 2000 148 | 9 3000 149 | 10 4000 150 | 11 4000 151 | 12 5000 152 | 13 4000 153 | 14 3000 154 | 15 2000 155 | 16 2000 156 | 17 3000 157 | 18 4000 158 | 19 5000 159 | 20 4000 160 | 21 3000 161 | 22 2000 162 | 23 1000 163 | 24 1000; 164 | 165 | end; 166 | -------------------------------------------------------------------------------- /nminusone.mod: -------------------------------------------------------------------------------- 1 | # N minus 1: a MILP minimum network flow problem with (n-1) redundancy 2 | # Last updated: 18 Nov 2016 3 | # Author: johannes.dorfner@tum.de 4 | # License: CC0 5 | # 6 | # USAGE 7 | # glpsol -m nminusone.mod 8 | # 9 | # OVERVIEW 10 | # This model finds the minimum cost network within a graph to redundantly 11 | # connect a set of source to a set of demand points. 12 | # 13 | # NETWORK GRAPH 14 | # (for input data, see bottom of file) 15 | # vi's denote vertices, lines show edges. Numbers show costs for building 16 | # a pipe on that edge. 17 | # 18 | # Source Sink 19 | # ,----. 2 ,----. 20 | # | v1 |-----------------------| v3 | 21 | # `----´ `----´ 22 | # | ,-----------------´ | 23 | # 1| ,----. 3 | 24 | # `-----| v2 | |1 25 | # `----´ ,----. | 26 | # `--------| v4 |-----´ 27 | # 1 `----´ 28 | # 29 | 30 | # SETS 31 | 32 | # Vertices and edges: 33 | # The network is represented by a set of vertex labels, and a set of unordered 34 | # pairs of these vertices. 35 | set vertex; # {v1, v2, v3, v4} 36 | set edge within vertex cross vertex; # {{v1, v2}, {v1, v3}, {v2, v3}, ...} 37 | 38 | # Arcs: 39 | # Each undirected edge {v1, v2} corresponds to a pair directed arcs (v1, v2) 40 | # and (v2, v1). 41 | set arc within vertex cross vertex := setof{ 42 | v1 in vertex, v2 in vertex: (v1, v2) in edge or (v2, v1) in edge} (v1, v2); 43 | 44 | # Timesteps: 45 | # for each edge in the graph, one contigency timestep 'cont_vi_vj' is created, 46 | # plus one additional timestep 'all', in which all edges are available. In this 47 | # model, this timestep does not change the result, but with an additional 48 | # parameter timestep duration, it could be used to correctly assess operational 49 | # costs over the year. 50 | set time := {'all'} union setof{(v1, v2) in edge} ('cont_' & v1 & '_' & v2); 51 | 52 | 53 | # PARAMS 54 | 55 | # Pipe building costs: 56 | # In this simplified model, only pipe construction incures building costs, 57 | # proportional to the pipe capacity. More generally, source vertices could have 58 | # differing operational costs, pipe construction costs could be split into a 59 | # fixed base for earth works (triggered by variable is_used) and a 60 | # capacity-dependent part (proportional to variable pipe_capacity). 61 | param costs{edge} >= 0; # (EUR/MW) 62 | 63 | # Capacity limit: 64 | # Maximum allowable thermal pipe capacity that can be constructed in an edge; 65 | # for simple case studies, this parameter corresponds to the maximum allowable 66 | # pipe diameter 67 | param capacity_upper_limit{edge} >= 0; # (MW) 68 | 69 | # Sink or source vertices: 70 | # Each vertex in the graph can either have a (positive) source or a (negative) 71 | # sink or demand of energy. As this simple model does not include losses, the 72 | # sum of source and sink must add up to exactly zero. 73 | param sinksource{vertex}, default 0; # (MW, +=source, -=sink) 74 | 75 | # Pipe availability: 76 | # For each contigency timestep, exactly one edge is not available (0), while all 77 | # other pipes are operational. In larger models, this strategy would lead to too 78 | # many contigency cases. In that case, only crucial/central edges might have a 79 | # contigency set. The idea of this parameter can be generalised directly to 80 | # source vertices to require (n-1) redundancy also for all sources. 81 | param is_available{(v1,v2) in edge, t in time}, binary := 82 | if t == ('cont_' & v1 & '_' & v2) then 0 else 1; # (1=yes, 0=no) 83 | 84 | # VARIABLES 85 | var flow{arc, time} >= 0; # actual power flow through pipe (MW) per timestep 86 | var is_used{arc, time} binary; # 1=pipe used in that direction, 0=not used 87 | var pipe_capacity{edge} >= 0; # built pipe capacity 88 | 89 | # OBJECTIVE 90 | minimize obj: sum{(v1, v2) in edge} costs[v1, v2] * pipe_capacity[v1, v2]; 91 | 92 | # CONSTRAINTS 93 | 94 | # power flow in and out of vertices is conserved. sinksource is the fixed demand 95 | # or supply of a vertex. 96 | s.t. vertex_balance{v in vertex, t in time}: 97 | sinksource[v] 98 | + sum{(other, v) in arc} flow[other, v, t] 99 | - sum{(v, other) in arc} flow[v, other, t] 100 | = 0; 101 | 102 | # the flow is limited by the edge's installed capacity, which must be chosen 103 | # high enough so that the flows in all time steps can be covered. Parameter 104 | # is_available reduces the effective capacity to zero, once for each edge's 105 | # contigency moment. Thanks to the last two constraints, only one of the 106 | # left-hand side terms can be greater than zero at any time. 107 | s.t. flow_limit{(v1, v2) in edge, t in time}: 108 | flow[v1, v2, t] + flow[v2, v1, t] <= pipe_capacity[v1, v2] * 109 | is_available[v1, v2, t]; 110 | 111 | 112 | s.t. one_direction_only{(v1, v2) in edge, t in time}: 113 | is_used[v1, v2, t] + is_used[v2, v1, t] <= 1; 114 | 115 | s.t. limit_flow_by_direction{(v1, v2) in arc, t in time}: 116 | flow[v1, v2, t] <= 117 | is_used[v1, v2, t] * ( 118 | if (v1, v2) in edge then capacity_upper_limit[v1, v2] 119 | else capacity_upper_limit[v2, v1]); 120 | 121 | # SOLVE 122 | solve; 123 | 124 | # OUTPUT 125 | printf "\nCAPACITIES\n"; 126 | printf " %-3s, %-3s: %8s\n", "v1", "v2", "cap"; 127 | printf "---------------------\n"; 128 | printf{(i,j) in edge: pipe_capacity[i,j] <> 0}: 129 | " %-3s, %-3s: %8g\n", i, j, pipe_capacity[i,j]; 130 | printf "\n"; 131 | 132 | printf "\nFLOWS PER TIMESTEP\n"; 133 | printf " %-3s, %-3s: %8s\n", "v1", "v2", "flow"; 134 | printf "---------------------\n"; 135 | for{t in time} { 136 | printf "%s:\n", t; 137 | printf{(i,j) in arc: flow[i,j,t] <> 0}: 138 | " %-3s, %-3s: %8g\n", i, j, flow[i,j,t]; 139 | printf "\n"; 140 | } 141 | 142 | # DATA 143 | data; 144 | 145 | param: vertex: sinksource := 146 | v1 1 147 | v2 0 148 | v3 -1 149 | v4 0; 150 | 151 | param: edge: costs capacity_upper_limit := 152 | v1 v2 1 10 153 | v1 v3 2 10 154 | v2 v3 3 10 155 | v2 v4 1 10 156 | v3 v4 1 10; 157 | 158 | end; 159 | -------------------------------------------------------------------------------- /soforsg.mod: -------------------------------------------------------------------------------- 1 | # SOforSG: Storage Optimization for Smart Grids 2 | # Last updated: 14 November 2013 3 | # Author: johannes.dorfner@tum.de 4 | # License: CC0 5 | # 6 | # USAGE 7 | # glpsol -m soforsg.mod 8 | # 9 | # OVERVIEW 10 | # This model optimizes size (storage_capacity) and operation (storage_level[t]) 11 | # of a hypothetical lossless storage technology for electric energy. A given 12 | # electricity demand[t] must be satisfied from 13 | # a) a cost-free (renewable) energy supply[t] with intermittent characteristic 14 | # (RES) or from 15 | # b) electricity_purchase[t], i.e. buying of electricity from the grid for a 16 | # time-dependent electricity_price[t]. 17 | # 18 | # Fig. 1 Power system diagram 19 | # 20 | # RES Grid Electricity 21 | # | | | 22 | # | | +--------+ | 23 | # +-----------| Supply |----->+ 24 | # | | +--------+ | ,-----------------. 25 | # | | +----->( Demand timeseries ) 26 | # | | +------------+ | `-----------------' 27 | # | +<--| Buy / Sell |--->+ 28 | # | | +------------+ | ,<=======>. 29 | # | | +<---->| Storage | 30 | # | | | `-,......-´ 31 | # | | | 32 | # 33 | 34 | # SETS & PARAMETERS 35 | set time; 36 | param demand{time} >= 0; # (kWh/h) 37 | param supply{time} >= 0; # (kWh/h) 38 | param electricity_price{time}; # (EUR/kWh) 39 | param storage_cost; # (EUR/kWh) 40 | param selling_price_ratio; # (1) for sold energy, relative to electricity_price 41 | 42 | # VARIABLES 43 | var energy_balance{time}; # (kWh) 44 | var storage_capacity >= 0; # (kWh) 45 | var storage_level{time} >= 0; # (kWh) 46 | var energy_purchase{time} >= 0; # (kWh) 47 | var energy_sold{time} >= 0; # (kWh) 48 | var costs; 49 | 50 | # OBJECTIVE 51 | minimize obj: costs; 52 | 53 | # CONSTRAINTS 54 | 55 | # total costs = investment for storage + purchased electricity 56 | s.t. def_costs: 57 | costs = 58 | storage_cost * storage_capacity + 59 | sum{t in time} electricity_price[t] * energy_purchase[t] - 60 | sum{t in time} electricity_price[t] * energy_sold[t] * selling_price_ratio; 61 | 62 | # balance = supply - demand + purchase - sold 63 | s.t. def_balance{t in time}: 64 | energy_balance[t] = supply[t] - demand[t] + 65 | energy_purchase[t] - energy_sold[t]; 66 | 67 | # new storage level = old storage level + balance 68 | s.t. def_storage_state{t in time: t>1}: 69 | storage_level[t] = storage_level[t-1] + energy_balance[t]; 70 | 71 | # storage is filled 50% at beginning 72 | s.t. def_storage_initial{t in time: t=1}: 73 | storage_level[t] = 0.5 * storage_capacity; 74 | 75 | # storage must be filled at least 50% in the end 76 | s.t. res_storage_final{t in time: t=card(time)}: 77 | storage_level[t] >= 0.5 * storage_capacity; 78 | 79 | # storage may be filled at most to storage capacity 80 | s.t. res_storage_capacity{t in time}: 81 | storage_level[t] <= storage_capacity; 82 | 83 | # limit sold energy to prevent unbounded model 84 | s.t. res_energy_sold{t in time}: 85 | energy_sold[t] <= 999; 86 | 87 | # SOLVE 88 | solve; 89 | 90 | # OUTPUT 91 | printf "RESULT\n\n"; 92 | printf "Costs: %+5.1f EUR\n", costs; 93 | printf " ( %+5.1f EUR for %g kWh storage at %g EUR/kWh, \n", storage_cost*storage_capacity, storage_capacity, storage_cost; 94 | printf " %+5.1f EUR for purchasing %g kWh,\n", sum{t in time} electricity_price[t] * energy_purchase[t], sum{t in time} energy_purchase[t]; 95 | printf " %+5.1f EUR from selling %g kWh)\n\n", - sum{t in time} selling_price_ratio * electricity_price[t] * energy_sold[t], sum{t in time} energy_sold[t]; 96 | printf "%2s:\t%6s\t%6s\t%5s | %5s\t%5s\t%5s\n", 97 | "t", "demand", "supply", "price", "Level", "Purch", "Sold"; 98 | printf "------------------------------+----------------------\n"; 99 | printf{t in time}: "%2i:\t%6g\t%6g\t%5g | %5g\t%5g\t%5g\n", 100 | t, demand[t], supply[t], electricity_price[t], storage_level[t], 101 | energy_purchase[t], energy_sold[t]; 102 | printf "------------------------------+----------------------\n"; 103 | printf "%s:\t%6g\t%6g\t%5s | %5s\t%5g\t%5g\n", "Sum", 104 | sum{t in time} demand[t], sum{t in time} supply[t], "---", "---", 105 | sum{t in time} energy_purchase[t], sum{t in time} energy_sold[t]; 106 | printf "%s:\t%6s\t%6s\t%5.1f | %5.1f\t%5s\t%5s\n", "Avg", 107 | "---", "---", sum{t in time} electricity_price[t]/card(time), 108 | sum{t in time} storage_level[t]/card(time), "---", "---"; 109 | printf "%s:\t%6i\t%6i\t%5i | %5i\t%5i\t%5i\n", "Min", 110 | min{t in time} demand[t], min{t in time} supply[t], 111 | min{t in time} electricity_price[t], min{t in time} storage_level[t], 112 | min{t in time} energy_purchase[t], min{t in time} energy_sold[t]; 113 | printf "%s:\t%6i\t%6i\t%5i | %5i\t%5i\t%5i\n", "Max", 114 | max{t in time} demand[t], max{t in time} supply[t], 115 | max{t in time} electricity_price[t], max{t in time} storage_level[t], 116 | max{t in time} energy_purchase[t], max{t in time} energy_sold[t]; 117 | printf "\n\n"; 118 | 119 | # DATA 120 | data; 121 | 122 | param storage_cost := 3; # storage capacity cost (EUR/kWh) 123 | param selling_price_ratio := 0.5; # ratio of electricity price (1) for sold energy 124 | 125 | param: time: supply demand electricity_price := 126 | 1 0 0 0 127 | 2 0 1 2 128 | 3 0 1 2 129 | 4 0 1 2 130 | 5 0 1 2 131 | 6 0 1 2 132 | 7 1 2 1 # supply begins 133 | 8 3 5 2 134 | 9 6 2 2 135 | 10 5 1 1 136 | 11 8 1 1 137 | 12 9 5 2 138 | 13 9 0 1 139 | 14 6 0 1 140 | 15 8 0 1 141 | 16 7 0 1 142 | 17 6 6 5 # demand peak, price peak begins 143 | 18 3 9 5 144 | 19 2 6 5 # price peak ends 145 | 20 1 3 1 # supply ends 146 | 21 0 4 1 147 | 22 0 4 1 148 | 23 0 1 1 149 | 24 0 0 1; 150 | end; 151 | 152 | -------------------------------------------------------------------------------- /dhmnl.mod: -------------------------------------------------------------------------------- 1 | # DHMNL: A minimal (mnl) district heating (DH) network optimization model 2 | # Last updated: 03 August 2016 3 | # Author: johannes.dorfner@tum.de 4 | # License: CC0 5 | 6 | # USAGE 7 | # glpsol -m dhmnl.mod 8 | 9 | # OVERVIEW 10 | # This mixed-integer linear programming (MILP) model finds the maximum revenue 11 | # topology and size of a district heating network for a given set of source 12 | # and demand vertices. The model can decide which demands to connect and 13 | # consequently decide over the location and size of the built network. 14 | # 15 | # This example comes with the following (ASCII-art sketch ahead) 16 | # network graph pre-coded into the data section at the end of the file. The 17 | # corners (A, D, H, K) house big heating stations, supported by a smaller 18 | # station in the center (F). All remaining vertices contain either big (E, G), 19 | # medium (I) and small (B, C, J) demands, which may (do not have to be) 20 | # satisfied, if profitable. 21 | # 22 | # Costs occur for a) constructing pipes and b) generating heat in heating 23 | # stations. Revenue is generated through satisfying demands. Pipe 24 | # construction costs consist of a capacity-independent part (earthworks) and 25 | # a capacity-dependent part (additional price of larger diamter, additional 26 | # earthworks for a wider hole), both multiplied with the segment length. Heat 27 | # generation costs may vary among heating stations. Here, the big heating 28 | # stations are cheapest, but located furthest away from the demand centers. 29 | 30 | # NETWORK GRAPH 31 | # (for input data, see bottom of file) 32 | # Letters denote vertices, lines show edges. Numbers show edge lengths. 33 | # 34 | # 5 4 3 35 | # A----------B-------C------D 36 | # | \ 37 | # /3 \3 38 | # | \ 39 | # E------F------G 40 | # | 2 / 2 41 | # 2\ /2 42 | # 4 _,-I--´ __,----K 43 | # _--´ \_____,--J--´ 4 44 | # H´ 4 45 | # 46 | 47 | 48 | 49 | # SETS 50 | set vertex; 51 | set edge within vertex cross vertex; # undirected {v1, v2} pairs 52 | set cost_type := {'network', 'generation', 'revenue'}; 53 | 54 | 55 | 56 | # PARAMETERS 57 | param cost_investment_fix >= 0; # (EUR/m) 58 | param cost_investment_var >= 0; # (EUR/MW/m) 59 | param capacity_upper_limit >= 0; # maximum possible pipe capcity (MW) 60 | param revenue_heat; # (EUR/MW) 61 | 62 | param length{edge} >= 0; # edge length (m) 63 | 64 | param source{vertex} >= 0, default 0; # generation capacity (MW) 65 | param demand{vertex} >= 0, default 0; # 66 | param cost_heat{vertex} >= 0, default 0; # (EUR/MW) 67 | 68 | 69 | 70 | 71 | # VARIABLES 72 | var is_built{edge} binary; # 1 = pipe is built in edge {v1, v2} , 0 = not 73 | var capacity{edge} >= 0; # built pipe capacity (MW) 74 | var flow{edge}; # pipe power flow (MW, positive: v1->v2, negative:v1<-v2) 75 | 76 | var is_satisfied{vertex} binary; # 1 = demand in vertex is satisfied, 0 = not 77 | var generation{vertex} >= 0; # generation power in source vertex (MW) 78 | 79 | var costs{cost_type}; # costs and revenues (EUR) 80 | 81 | 82 | 83 | # OBJECTIVE 84 | # sum of network investment (capacity-independent fix plus capacity-dependent 85 | # variable) and heat generation costs in heating stations 86 | minimize obj: 87 | costs['network'] 88 | + costs['generation'] 89 | - costs['revenue']; 90 | 91 | 92 | s.t. obj_network_costs: 93 | costs['network'] = 94 | sum{(v1, v2) in edge} 95 | length[v1, v2] * ( 96 | cost_investment_fix * is_built[v1, v2] + 97 | cost_investment_var * capacity[v1, v2]); 98 | 99 | s.t. obj_generation_costs: 100 | costs['generation'] = 101 | sum{v in vertex} 102 | cost_heat[v] * generation[v]; 103 | 104 | s.t. obj_revenue: 105 | costs['revenue'] = 106 | sum{v in vertex} 107 | revenue_heat * demand[v] * is_satisfied[v]; 108 | 109 | 110 | 111 | 112 | # CONSTRAINTS 113 | 114 | # power flow conservation in nodes: ingoing power flow and generation count 115 | # positive, outgoing power flow and satisfied demand count negative 116 | s.t. vertex_balance{v in vertex}: 117 | generation[v] 118 | - demand[v] * is_satisfied[v] 119 | + sum{(a,b) in edge: v=b} flow[a, v] 120 | - sum{(a,b) in edge: v=a} flow[v, b] 121 | = 0; 122 | 123 | # limit forward (v1 -> v2) power flow through pipe by built capacity 124 | s.t. flow_limit_upper{(v1, v2) in edge}: 125 | flow[v1, v2] <= capacity[v1, v2]; 126 | 127 | # limit reverse (v1 <- v2) power flow through pipe by negative pipe capacity 128 | s.t. flow_limit_lower{(v1, v2) in edge}: 129 | -capacity[v1, v2] <= flow[v1, v2]; 130 | 131 | # limit built pipe capacity to upper limit. Side effect: binary decision 132 | # variable is_built is forced to take value 1 to allow non-zero value 133 | s.t. limit_capacity_by_is_built{(v1, v2) in edge}: 134 | capacity[v1, v2] <= is_built[v1, v2] * capacity_upper_limit; 135 | 136 | # limit generation in vertices to heating station capacity 137 | s.t. generation_cap{v in vertex}: 138 | generation[v] <= source[v]; 139 | 140 | 141 | 142 | 143 | solve; 144 | 145 | # OUTPUT 146 | printf "\nRESULT\n\n"; 147 | 148 | printf "COSTS\n"; 149 | printf "Network: %8.1f EUR (%s m total length in %s edges)\n", 150 | costs['network'], 151 | sum{(v1, v2) in edge} length[v1,v2] * is_built[v1,v2], 152 | sum{(v1, v2) in edge} is_built[v1,v2]; 153 | printf "Generation: %8.1f EUR\n", costs['generation']; 154 | printf "Revenue: %8.1f EUR\n", -costs['revenue']; 155 | printf "---------------------\n"; 156 | printf "Total: %8.1f\n", costs['network'] + costs['generation'] - costs['revenue']; 157 | 158 | printf "\nVERTEX\n"; 159 | printf "gen: generation (MW)\nsat: satisfied demand (MW)\nnot: unsatisfied demand (MW)\n\n"; 160 | printf " %-3s | %4s %4s %4s\n", "v", "gen", "sat", "not"; 161 | printf "----------------------\n"; 162 | printf{v in vertex}: 163 | " %-3s | %4s %4s %4s\n", 164 | v, 165 | if generation[v] > 0 then generation[v] else '', 166 | if is_satisfied[v] > 0 then demand[v] * is_satisfied[v] else '', 167 | if demand[v] * (1-is_satisfied[v]) > 0 then demand[v] * (1-is_satisfied[v]) else ''; 168 | printf "----------------------\n"; 169 | printf " %-3s | %4s %4s %4s\n", 170 | "Sum", 171 | sum{v in vertex} generation[v], 172 | sum{v in vertex} demand[v] * is_satisfied[v], 173 | sum{v in vertex} demand[v] * (1 - is_satisfied[v]); 174 | 175 | printf "\nEDGE\n"; 176 | printf "cap: capacity (MW)\nflow: heat flow (MW, >0:v1->v2, <0:v2->v1)\n\n"; 177 | printf " %-2s, %-2s |%4s %4s\n", "v1", "v2", "cap", "flow"; 178 | printf "-------------------\n"; 179 | printf{(v1, v2) in edge: capacity[v1, v2] > 0}: 180 | " %-2s%2s%2s |%4g %4g\n", 181 | v1, 182 | if flow[v1,v2] > 0 then '->' else '<-', 183 | v2, 184 | capacity[v1, v2], 185 | flow[v1, v2]; 186 | printf "\n"; 187 | 188 | 189 | 190 | # DATA 191 | data; 192 | 193 | param revenue_heat := 80; # (EUR/MW) 194 | param cost_investment_fix := 50; # (EUR/m) 195 | param cost_investment_var := 4; # (EUR/MW/m) 196 | param capacity_upper_limit := 15; # (MW) 197 | 198 | 199 | # (MW) (MW) (EUR/MW) 200 | param: vertex: source demand cost_heat := 201 | A 9 . 1 202 | B . 2 . 203 | C . 2 . 204 | D 9 . 2 205 | E . 9 . 206 | F 3 . 3 207 | G . 9 . 208 | H 2 . 2 209 | I . 4 . 210 | J . 1 . 211 | K 9 . 1; 212 | 213 | # (m) 214 | param: edge: length := 215 | A B 5 216 | B C 4 217 | B E 3 218 | C D 3 219 | C F 3 220 | C G 3 221 | E F 2 222 | E I 2 223 | F G 2 224 | F I 2 225 | G J 3 226 | H I 4 227 | I J 3 228 | J K 4; 229 | 230 | end; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MathProg models for energy system planning and operation 2 | 3 | This repository contains a collection of mathematical optimisation models that have been developed for educational purposes for several lectures. They are collected here for easier maintenance and better visibility of what has been implemented already. 4 | 5 | The common theme among these models is **capacity expansion**, **power flow** and **plant scheduling** for **minimum total system cost** (or - in the case of DHMNL - maximum revenue), each model stressing another aspect of common tasks in modelling of energy systems. 6 | 7 | ## Models 8 | 9 | ### DCFLOW 10 | 11 | This linear programming (LP) model finds the minimum cost generation and network flow for a lossless electricity network, while obeying **linearised DC powerflow** equations (consult a textbook, or [The DC Power Flow Equations](http://home.eng.iastate.edu/~jdm/ee553/DCPowerFlowEquations.pdf) for a quick primer). It is present in two semantically identical, but mathematically different formulations: `dcflow_arc` uses directed arcs (two per edge) and contains only positive flow variables, the direction directly corresponding to that of its containing arc. `dcflow_edge` only contains one edge between each pair of connected vertices, and an unconstrained flow variable instead, encoding direction in its sign. While the edge formulation uses less variables and thus is more efficient for only this problem, the arc formulation can make it easier to add further variables and constraints that interact with the modelled power flow. 12 | 13 | ### DHMNL 14 | 15 | This mixed-integer linear programming (MILP) model finds the **maximum revenue topology and size** of a district heating (DH) network for a given set of source and demand vertices. The model **can decide which demands to connect** and consequently plans the location and size of the built network. Different from most other models here, this implies that the system under design does not have to satisfy a demand, but can select only a (possibly empty) subset of profitable customers. 16 | 17 | ### Equilibrium 18 | 19 | This linear programming (LP) model finds the **maximum welfare** solution for a given set of *a) a discretised production cost curve* (i.e. a **merit order** curve) and *b) a discretised utility function* of customers (i.e. a **price-demand** curve). It thus finds the [economic equilibrium](https://en.wikipedia.org/wiki/Economic_equilibrium) of the market situation encoded by its inputs. 20 | 21 | ### Intertemporal 22 | 23 | This linear programming (LP) model finds the minimum cost **investment plan** for for a set of two power plant technologies over multiple decades, allowing investment decisions every five years. Old investments phase out of the power plant fleet after the parameterised **lifetime** of each investment is over. 24 | 25 | ### N minus 1 26 | 27 | This mixed-integer linear programming (MILP) model finds the minimum cost network within a graph to **redundantly connect a set of source to a set of demand points**. "Redundantly" means that the resulting network is resilient against the failure of any single edge in the network, i.e. satisfying the [N-1 Criterion](https://www.entsoe.eu/fileadmin/user_upload/_library/publications/entsoe/Operation_Handbook/glossary_v22.pdf#page=9) common in electric grid design. 28 | 29 | ### SOforSG 30 | 31 | This linear programming (LP) model is an abbreviation of *Storage Optimisation for Smart Grid*. This model determines optimal **size and operation** of a hypothetical lossless **storage technology** for electric energy. A given electricity demand must be satisfied from *either* a cost-free (renewable) energy supply with intermittent characteristic *or* from purchase of electricity from the grid for a time-dependent price. Surplus renewable energy can be sold either at generation time, or stored to yield a higher revenue later. This model is the core idea behind [urbs](https://github.com/tum-ens/urbs), which generalises the size and operation optimisation to an arbitrary number of energy conversion, transmission and storage processes (at aan rbitrary number of conceptual nodes, called *sites*). The central idea, the energy balance constraint, storage state equation and a cost minimisation objective function, are all present here. 32 | 33 | ### Startup and partial 34 | 35 | This linear programming (LP) optimisation model finds a **minimum-cost capacity expansion and unit commitment** solution to a given demand timeseries for a combination of a fluctuating feed-in (renewables) and a controllable technology (power plant). This model focuses on correctly depicting the trade-off in sizing the power plant with respect to its operation point, which can exhibit less efficiency than when operating below its nominal size. These properties are approximated by a formulation that combines **startup costs** with a linearily increasing or decreasing **conversion efficiency**, depending on the **operation point**. 36 | 37 | ### Unit commitment 38 | 39 | This mixed-integer linear programming (MILP) model finds a cost-minimal power plant **operation schedule** for a given demand timeseries. Modelled power plant attributes are minimum and maximum output capacity, startup and shutdown costs, operational fixed (when switched on) and variable (by power production) costs. This formulation is computationally more complex than the one used in model *Startup and partial*, but more accurate in that it depicts the discrete nature of the on-off decision. Also, it allows for more extensions similar to state-of-the-art unit commitment models, e.g.: minimum runtimes, minimum cooldown times, hot or warm start capabilities. 40 | 41 | 42 | ## Installation 43 | 44 | All models require the standalone solver `glpsol` from the GNU Linear Programming Toolkit (GLPK). 45 | 46 | ### Web 47 | 48 | For short experiments, there is [GLPK Online](https://cocoto.github.io/glpk-online/), a Javascript port of GLPK and MathProg. Just copy & paste the model code into the text box, press *Solve*, inspect model ouptut in tab *Output*. 49 | 50 | ### Windows 51 | 52 | Binary builds for Windows are available through the [WinGLPK](https://sourceforge.net/projects/winglpk/). Just extract the contents of the ZIP file to a convenient location, e.g. `C:\GLPK`. You then can either call a model using: 53 | 54 | C:\GLPK\bin\glpsol.exe -m model.mod 55 | 56 | However, it is recommended to add the subdirectory `w64`, which contains the file `glpsol.exe`, to the system path ([how](http://geekswithblogs.net/renso/archive/2009/10/21/how-to-set-the-windows-path-in-windows-7.aspx)), so that the command `glpsol` is available on the command prompt from any directory directly: 57 | 58 | glpsol -m model.mod 59 | 60 | ### Linux packages 61 | 62 | Most distributions offer GLPK as a ready-made packages. If unsure, please consult your distribution's package index. On Debian or Debian-based (e.g. Ubuntu) distributions, executing the following command on the terminal (excluding the `$ `): 63 | 64 | $ sudo apt-get install glpk-utils 65 | 66 | Note that package maintainers could potentially lag behind new versions up to several releases. To check which version you have installed, you can use: 67 | 68 | $ glpsol --version 69 | 70 | ### Building from source 71 | 72 | If you want the most recent version, you might consider downloading the source code from the [project homepage](https://www.gnu.org/software/glpk/) and build the solver from source by following the instructions in the accompanying `INSTALL` file. Typically, this boils down to the following steps, where `X-Y` must be replaced by the version number, e.g. `4-60`: 73 | 74 | $ tar -xzvf glpk-X-Y.tar.gz 75 | $ cd glpk-X-Y 76 | $ ./configure 77 | $ make 78 | $ make check 79 | $ sudo make install 80 | 81 | To check whether (and where) the solver was installed, you can use: 82 | 83 | $ which glpsol 84 | /usr/bin/glpsol 85 | 86 | ## Run tests 87 | 88 | To check whether all models work as intended, run included script `test.sh`: 89 | 90 | $ ./test.sh 91 | 92 | If it returns without output (and returns no error code), all models are syntactically fine. If there is a compilation error, it will be printed and the script returns an error code. 93 | 94 | ## Copyright 95 | 96 | Each model has its own author and license statement in the file header. Most models so far have the Creative Commons Public Domain Dedication, short [CC0](https://creativecommons.org/publicdomain/zero/1.0). In other words, *you can copy, modify, distribute and perform the work, even for commercial purposes, all without asking permission.* Some minor conditions still apply, most notably: *When using or citing the work, you should not imply endorsement by the author or the affirmer.* But that's about it. But when in doubt, read the [full license text](https://creativecommons.org/publicdomain/zero/1.0/legalcode). 97 | -------------------------------------------------------------------------------- /startupandpartial.mod: -------------------------------------------------------------------------------- 1 | # STARTUP & PARTIAL: a simple LP formulation of variable conversion efficiency 2 | # Last updated: 18 Nov 2016 3 | # Author: johannes.dorfner@tum.de 4 | # License: CC0 5 | # 6 | # USAGE 7 | # glpsol -m startupandpartial.mod 8 | # 9 | # OVERVIEW 10 | # This linear programming (LP) optimisation model finds a minimum-cost 11 | # capacity expansion and unit commitment solution to a given demand 12 | # timeseries for two sources of energy: 13 | # 14 | # A. fluctuating renewable energy source (RES), e.g. a wind park or solar farm 15 | # with investment, but without variable costs 16 | # B. controllable power-plant (PP), e.g. coal or gas plant 17 | # with investment, fuel and startup costs and partial load efficiency 18 | # 19 | # 20 | # Fig. 1 Power system diagram 21 | # 22 | # Wind Fuel Electricity 23 | # | | | 24 | # | | +-----+ | 25 | # +------------>| RES |----->+ 26 | # | | +-----+ | ,-----------------. 27 | # | | +--->( Demand timeseries ) 28 | # | | +-----+ | `-----------------' 29 | # | +------>| PP |----->+ 30 | # | | +-----+ | 31 | # | | | 32 | # 33 | # The output of the RES power source is determined by the parameter wind[t] and 34 | # the variable wind park capacity capacity_res. 35 | # The output of the power-plant PP is controllable by the solver, but incurs 36 | # costs for 37 | # - installing a certain capacity in the first place (cost_invest, EUR/MW) 38 | # - buying fuel to burn (cost_fuel, EUR/MWh) and 39 | # - starting up the plant (cost_startup, EUR/MW). 40 | # 41 | # The latter cost is coupled to a strict LP formulation of the power plant 42 | # conversion efficiency which depends on the operation point, i.e. which 43 | # fraction of the "online capacity" is actually used. It bundles together the 44 | # following behaviours: 45 | # - minimum partial load: PP must output at least a fraction partial_min of 46 | # its online capacity 47 | # - startup costs: increasing the online capacity leads to startup costs, while 48 | # decreasing is for free 49 | # - partial load efficiency: efficiency at full operation (output equals online 50 | # capacity) happens at full efficiency (efficiency_max) while operation at 51 | # minimum partial load (output equals partial_min of current online capacity) 52 | # happens with minimum efficiency (efficiency_min). In between, a linear 53 | # interpolation is valid which leads to a non-linear effective efficiency 54 | # behaviour. 55 | # 56 | # In practice, probably only the two extreme operation points "maximum load" and 57 | # "minimum partial load" are to be used. While the partial load efficiency makes 58 | # it cheaper (fuel-wise) to always have the online capacity follow the PP 59 | # output, the startup cost provide an incentive in the other direction. 60 | # 61 | 62 | 63 | 64 | # SETS & PARAMETERS 65 | 66 | # time 67 | param N; # number of time steps 68 | set time within 1..N; # time steps 69 | 70 | # economics 71 | set cost_types := {'invest (res)', 'invest (pp)', 'startup', 'fuel'}; 72 | 73 | param cost_invest; # (EUR/MW) power-plant investment costs 74 | param cost_fuel; # (EUR/MWh) power-plant fuel costs 75 | param cost_startup; # (EUR/MW) power-plant startup costs 76 | param cost_res; # (EUR/MW) renewable investment costs 77 | 78 | # power-plant parameters 79 | param efficiency_min; # (1) power-plant efficiency at minimum operation point 80 | param efficiency_max; # (1) power-plant efficiency at maximum operation point 81 | param partial_min; # (1) minimum operation point, relative to plant capacity 82 | 83 | # timeseries 84 | param demand{t in time} >= 0; # (MW) electricity demand 85 | param wind{t in time} >= 0; # (1) normalised wind capacity factor 86 | 87 | 88 | 89 | # VARIABLES 90 | 91 | # capacities 92 | var capacity_pp >= 0; 93 | var capacity_res >= 0; 94 | 95 | # power plant timeseries 96 | var pp_inp{t in time} >= 0; # (MW) power-plant input power, i.e. fuel consumption 97 | var pp_out{t in time} >= 0; # (MW) power-plant output power, i.e. elec generation 98 | var cap_online{t in time} >= 0; # (MW) power-plant online capacity, i.e. activity 99 | var cap_start{t in time} >= 0; # (MW) power-plant startup capacity 100 | var res_out{t in time} >= 0; # (MW) RES fluctuating electricity output 101 | 102 | # costs 103 | var costs{ct in cost_types}; # (EUR) cost by type (invest, startup, fuel) 104 | 105 | 106 | 107 | # OBJECTIVE 108 | 109 | # total cost: sum of all costs by cost type 110 | minimize obj: sum{ct in cost_types} costs[ct]; 111 | 112 | # invest (pp): PP capacity multiplied by PP investment cost parameter 113 | s.t. def_costs_invest_pp: 114 | costs['invest (pp)'] = cost_invest * capacity_pp; 115 | 116 | # invest (res): RES capacity multiplied by res investment cost parameter 117 | s.t. def_costs_invest_res: 118 | costs['invest (res)'] = capacity_res * cost_res; 119 | 120 | # startup cost: total startup capacity multiplied with startup cost parameter 121 | s.t. def_costs_startup: 122 | costs['startup'] = cost_startup * sum{t in time} cap_start[t]; 123 | 124 | # fuel costs: total power-plant input multiplied with fuel cost parameter 125 | s.t. def_costs_fuel: 126 | costs['fuel'] = cost_fuel * sum{t in time} pp_inp[t]; 127 | 128 | 129 | 130 | # CONSTRAINTS 131 | 132 | # demand must be satisfied from either power-plant or RES output 133 | s.t. res_demand{t in time}: 134 | demand[t] <= pp_out[t] + res_out[t]; 135 | 136 | # RES output is determined by product of RES timeseries and RES capacity 137 | s.t. def_res_out{t in time}: 138 | res_out[t] = wind[t] * capacity_res; 139 | 140 | # Power-plant output can in principle be chosen as desired, but is interlocked 141 | # into multiple constraints... 142 | 143 | # ... the first: the output is limited by the 'online' capacity, roughly 144 | # corresponding to the power-plant temperature 145 | s.t. res_pp_out_max_by_capacity_online{t in time}: 146 | pp_out[t] <= cap_online[t]; 147 | 148 | # second, the output may not be lower than the minimum partial load fraction of 149 | # the online capacity 150 | s.t. res_pp_out_min_by_capacity_online{t in time}: 151 | pp_out[t] >= cap_online[t] * partial_min; 152 | 153 | # changes in the online capacity are to cause startup costs. These are triggered 154 | # by the startup capacity variable, which must become positive whenever the 155 | # online capacity increases (but not when it decreases!) 156 | s.t. def_pp_startup{t in time}: 157 | cap_start[t] >= cap_online[t] - (if t > 1 then cap_online[t-1] else 0); 158 | 159 | # the online capacity is limited by the total installed power-plant capacity 160 | s.t. res_pp_cap_online{t in time}: 161 | cap_online[t] <= capacity_pp; 162 | 163 | # finally, the fuel consumption of the power-plant is determined. This unhandy 164 | # expression is a linear interpolation to achieve a non-constant fuel 165 | # efficiency that ranges from efficiency_min when in partial operation 166 | # (pp_out == partial_min * cap_online ) up to efficiency_max when in full 167 | # operation (pp_out == cap_online): 168 | # 169 | # 170 | # Fig. 2 Power-plant input over power-plant output for partial load efficiency 171 | # 172 | # ^ pp_inp 173 | # cap_online | 174 | # ---------- + ,+ 175 | # eff_max | ,--' 176 | # | ,--' 177 | # p_min*c_onl | ,--' 178 | # ----------- + +' 179 | # eff_min | 180 | # | 181 | # | 182 | # +-----+--------------+-------> pp_out 183 | # 184 | # partial_min * cap_online 185 | # cap_online 186 | # 187 | # 188 | # This expression is the "compiled" version of the resulting interpolation 189 | # expression, followed by some algebra to simplify the expression. 190 | s.t. res_pp_partial_efficiency{t in time}: 191 | pp_inp[t] = ( 192 | (efficiency_max - efficiency_min) * partial_min * cap_online[t] + 193 | (efficiency_min - partial_min * efficiency_max) * pp_out[t] 194 | ) / ( 195 | (1 - partial_min) * efficiency_min * efficiency_max 196 | ); 197 | 198 | 199 | 200 | 201 | # SOLVE 202 | solve; 203 | 204 | 205 | 206 | # REPORTING PARAMETERS 207 | param total_demand := sum{t in time} demand[t]; 208 | param total_cost := sum{ct in cost_types} costs[ct]; 209 | param share_pp := (sum{t in time} pp_out[t]) / total_demand; 210 | param total_overprod := ( 211 | sum{t in time} ( 212 | pp_out[t] + res_out[t] - demand[t]) 213 | ) / total_demand; 214 | param pp_efficiency{t in time} := 215 | if pp_inp[t] > 0 then pp_out[t] / pp_inp[t] else 0; 216 | 217 | 218 | # REPORT PRINTING 219 | printf "\n\nCOSTS\n\n"; 220 | printf " %-17s %8s\n", "type", "EUR"; 221 | printf " --------------------------\n"; 222 | printf {ct in cost_types} 223 | " %-17s %8.1f\n", ct, costs[ct]; 224 | printf " --------------------------\n"; 225 | printf " %-17s %8.1f\n", "sum", total_cost; 226 | 227 | printf "\n\nSTATISTICS\n\n"; 228 | printf " %-24s %-5s %-12s\n", "indicator", "value", "unit"; 229 | printf " -----------------------------------\n"; 230 | printf " %-24s %5.1f %-12s\n", "peak demand", max{t in time} demand[t], "(MW)"; 231 | printf " %-24s %5.1f %-12s\n", "power-plant cap", capacity_pp, "(MW)"; 232 | printf " %-24s %5.1f %-12s\n", "res capacity", capacity_res, "(MW)"; 233 | printf " %-24s %5.1f %-12s\n", "share power-plant", 100 * share_pp, "(%)"; 234 | printf " %-24s %5.1f %-12s\n", "share res", 100 * (1 - share_pp), "(%)"; 235 | printf " %-24s %5.1f %-12s\n", "overproduction res", 100 * total_overprod, "(%)"; 236 | 237 | printf "\n\nSCHEDULE\n\n"; 238 | printf " %-2s %3s = %4s + %-4s %4s %3s %3s\n", 239 | "t", "dem", "res", "pp_o", "pp_i", "onl", "eff"; 240 | printf " ------------------------------------\n"; 241 | printf {t in time} 242 | " %-2s %3g %1s %4g + %4g %4g %3g %3d\n", 243 | t, 244 | demand[t], 245 | if demand[t] < res_out[t] + pp_out[t] then "<" else "=", 246 | round(res_out[t], 1), 247 | round(pp_out[t], 1), 248 | round(pp_inp[t], 1), 249 | round(cap_online[t], 1), 250 | 100 * pp_efficiency[t]; 251 | printf "\n\n"; 252 | 253 | 254 | 255 | # DATA 256 | data; 257 | 258 | # economic parameters 259 | param cost_invest := 100; # (EUR/MW) power-plant investment costs 260 | param cost_fuel := 80; # (EUR/MWh) power-plant fuel costs 261 | param cost_startup := 120; # (EUR/MW) power-plant startup costs 262 | param cost_res := 150; # (EUR/MW) renewable investment costs 263 | 264 | # power-plant parameters 265 | param efficiency_max := 0.50; # (1) power-plant efficiency at minimum operation point 266 | param efficiency_min := 0.40; # (1) power-plant efficiency at maximum operation point 267 | param partial_min := 0.25; # (1) minimum operation point, relative to plant capacity 268 | 269 | # timeseries 270 | param N := 6; # number of time steps; must match with the table below 271 | param : time : wind demand := 272 | 1 0.2 2 273 | 2 0.2 1 274 | 3 0.4 5 # peak demand 275 | 4 0.2 1 276 | 5 0.3 3 277 | 6 0.1 5; # peak demand again 278 | 279 | # END 280 | end; 281 | 282 | --------------------------------------------------------------------------------