├── .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 |
--------------------------------------------------------------------------------