|
| 1 | +from coopr.opt.base import SolverFactory |
| 2 | +from coopr.pyomo import * |
| 3 | +from datetime import datetime |
| 4 | +from numpy import arange |
| 5 | +import os |
| 6 | +import pandas as pd |
| 7 | +import pdb |
| 8 | + |
| 9 | + |
| 10 | +def create_model(filename, timesteps): |
| 11 | + |
| 12 | + m = ConcreteModel() |
| 13 | + m.name = 'URPS' |
| 14 | + m.settings = { |
| 15 | + 'dateformat': '%Y%m%dT%H%M%S', |
| 16 | + 'basename': os.path.basename(os.path.splitext(filename)[0]), |
| 17 | + 'timesteps': timesteps, |
| 18 | + } |
| 19 | + m.created = datetime.now().strftime(m.settings['dateformat']) |
| 20 | + |
| 21 | + # Preparations |
| 22 | + # ============ |
| 23 | + # Excel import |
| 24 | + # use Pandas DataFrames instead of Pyomo parameters for entity |
| 25 | + # attributes. Syntax to access a value: |
| 26 | + # |
| 27 | + # m.process.loc[pro, coin, cout][attribute] |
| 28 | + # |
| 29 | + xls = pd.ExcelFile(filename) |
| 30 | + m.site = xls.parse('Site', index_col=[0]) |
| 31 | + m.commodity = xls.parse('Commodity', index_col=[0,1,2]) |
| 32 | + m.process = xls.parse('Process', index_col=[0,1,2]) |
| 33 | + m.transmission = xls.parse('Transmission', index_col=[0,1,2]) |
| 34 | + m.storage = xls.parse('Storage', index_col=[0,1]) |
| 35 | + m.demand = xls.parse('Demand', index_col=[0]) |
| 36 | + m.supim = xls.parse('SupIm', index_col=[0]) |
| 37 | + |
| 38 | + # derive annuity factor for process and storage |
| 39 | + m.process['annuity_factor'] = annuity_factor(m.process['depreciation'], m.process['wacc']) |
| 40 | + m.transmission['annuity_factor'] = annuity_factor(m.transmission['depreciation'], m.transmission['wacc']) |
| 41 | + m.storage['annuity_factor'] = annuity_factor(m.storage['depreciation'], m.storage['wacc']) |
| 42 | + |
| 43 | + # Sets |
| 44 | + # ==== |
| 45 | + # Syntax: m.{name} = Set({domain}, initialize={values}) |
| 46 | + # where name: set name |
| 47 | + # domain: set domain for tuple sets, a cartesian set product |
| 48 | + # values: set values, a list or array of element tuples |
| 49 | + m.t = Set(initialize=m.settings['timesteps']) |
| 50 | + m.tm = Set(within=m.t, initialize=m.settings['timesteps'][1:]) |
| 51 | + m.sit = Set(initialize=m.site.index.levels[0]) |
| 52 | + m.com = Set(initialize=m.commodity.index.levels[1]) |
| 53 | + m.com_type = Set(initialize=m.commodity.index.levels[2]) |
| 54 | + m.pro = Set(initialize=m.process.index.levels[0]) |
| 55 | + m.sto = Set(initialize=m.storage.index.levels[0]) |
| 56 | + m.cost_type = Set(initialize=['Inv', 'Fix', 'Var', 'Fuel']) |
| 57 | + |
| 58 | + # sets of existing tuples: |
| 59 | + # co_tuples = [('Coal', 'Stock'), ('Wind', 'SupIm'), ('ElecAC', 'Demand')...] |
| 60 | + # pro_tuples = [('pp', 'Coal', 'ElecAC'), ('wt', 'Wind', 'ElecAC')...] |
| 61 | + # sto_tuples = [('bat', 'ElecDC'), ('pst', 'ElecAC')...] |
| 62 | + m.co_tuples = Set(within=m.sit*m.com*m.com_type, initialize=m.commodity.index) |
| 63 | + m.pro_tuples = Set(within=m.sit*m.pro*m.com*m.com, initialize=m.process.index) |
| 64 | + m.tra_tuples = Set(within=m.sit*m.sit*m.com, initialize=m.transport.index) |
| 65 | + m.sto_tuples = Set(within=m.sit*m.sto*m.com, initialize=m.storage.index) |
| 66 | + |
| 67 | + # subsets of commodities by type |
| 68 | + # for shorter equations that apply to only one commodity type |
| 69 | + m.com_supim = Set(within=m.com, initialize=(c[0] for c in m.com_tuples if c[1] == 'SupIm')) |
| 70 | + m.com_stock = Set(within=m.com, initialize=(c[0] for c in m.com_tuples if c[1] == 'Stock')) |
| 71 | + m.com_demand = Set(within=m.com, initialize=(c[0] for c in m.com_tuples if c[1] == 'Demand')) |
| 72 | + |
| 73 | + # Parameters |
| 74 | + # ========== |
| 75 | + # for model entities (commodity, process, storage) no Pyomo parames |
| 76 | + # are needed, just use the DataFrames m.commodity, m.process and |
| 77 | + # m.storage directly. |
| 78 | + # Syntax: m.{name} = Param({domain}, initialize={values}) |
| 79 | + # where name: param name |
| 80 | + # domain: one or multiple model sets; empty for scalar parameters |
| 81 | + # values: dict of parameter values, addressed by elements of domain sets |
| 82 | + m.weight = Param(initialize=float(8760) / len(m.t)) |
| 83 | + |
| 84 | + # Variables |
| 85 | + # ========= |
| 86 | + # listed alphabetically |
| 87 | + # Syntax: m.{name} = Var({domain}, within={range}) |
| 88 | + # where name: variable name |
| 89 | + # domain: variable domain, consisting of one or multiple sets |
| 90 | + # range: variable values, like Binary, Integer, NonNegativeReals |
| 91 | + m.cap_pro = Var(m.pro_tuples, within=NonNegativeReals) |
| 92 | + m.cap_pro_new = Var(m.pro_tuples, within=NonNegativeReals) |
| 93 | + m.cap_sto_c = Var(m.sto_tuples, within=NonNegativeReals) |
| 94 | + m.cap_sto_c_new = Var(m.sto_tuples, within=NonNegativeReals) |
| 95 | + m.cap_sto_p = Var(m.sto_tuples, within=NonNegativeReals) |
| 96 | + m.cap_sto_p_new = Var(m.sto_tuples, within=NonNegativeReals) |
| 97 | + m.co2_pro_out = Var(m.tm, m.pro_tuples, within=NonNegativeReals) |
| 98 | + m.costs = Var(m.cost_type, within=NonNegativeReals) |
| 99 | + m.e_pro_in = Var(m.tm, m.pro_tuples, within=NonNegativeReals) |
| 100 | + m.e_pro_out = Var(m.tm, m.pro_tuples, within=NonNegativeReals) |
| 101 | + m.e_sto_in = Var(m.tm, m.sto_tuples, within=NonNegativeReals) |
| 102 | + m.e_sto_out = Var(m.tm, m.sto_tuples, within=NonNegativeReals) |
| 103 | + m.e_sto_con = Var(m.t, m.sto_tuples, within=NonNegativeReals) |
| 104 | + |
| 105 | + # Equation definition |
| 106 | + # =================== |
| 107 | + # listed by topic. All equations except the Objective function are |
| 108 | + # of type Constraint, although there are two semantics for those, |
| 109 | + # indicated by the name prefix (def, res). |
| 110 | + # - def: definition, usually equations, defining variable values |
| 111 | + # - res: restriction, usually inequalities, limiting variable values |
| 112 | + # topics |
| 113 | + # - commodity |
| 114 | + # - process |
| 115 | + # - storage |
| 116 | + # - emissions |
| 117 | + # - costs |
| 118 | + |
| 119 | + # commodity |
| 120 | + def res_demand_rule(m, tm, com, com_type): |
| 121 | + if co not in m.com_demand: |
| 122 | + return Constraint.Skip |
| 123 | + else: |
| 124 | + provided_energy = - commodity_balance(m, tm, com) |
| 125 | + return provided_energy >= \ |
| 126 | + m.demand.loc[tm][com] * \ |
| 127 | + m.commodity.loc[com, com_type]['peak'] |
| 128 | + |
| 129 | + def res_stock_hour_rule(m, tm, com, com_type): |
| 130 | + if com not in m.com_stock: |
| 131 | + return Constraint.Skip |
| 132 | + else: |
| 133 | + # calculate total consumption of commodity co in timestep tm |
| 134 | + total_consumption = commodity_balance(m, tm, com) |
| 135 | + return total_consumption <= m.commodity.loc[com, com_type]['maxperhour'] |
| 136 | + |
| 137 | + |
| 138 | + def res_stock_total_rule(m, com, com_type): |
| 139 | + if com not in m.com_stock: |
| 140 | + return Constraint.Skip |
| 141 | + else: |
| 142 | + # calculate total consumption of commodity co |
| 143 | + total_consumption = 0 |
| 144 | + for tm in m.tm: |
| 145 | + total_consumption += commodity_balance(m, tm, com) |
| 146 | + |
| 147 | + return total_consumption <= m.commodity.loc[com, com_type]['max'] |
| 148 | + |
| 149 | + |
| 150 | + # process |
| 151 | + def def_process_capacity_rule(m, tm, pro, coin, cout): |
| 152 | + return m.cap_pro[pro,coin,cout] == \ |
| 153 | + m.cap_pro_new[pro,coin,cout] + \ |
| 154 | + m.process.loc[pro,coin,cout]['inst-cap'] |
| 155 | + |
| 156 | + def def_process_output_rule(m, tm, pro, coin, cout): |
| 157 | + return m.e_pro_out[tm, pro, coin, cout] == \ |
| 158 | + m.e_pro_in[tm, pro, coin, cout] * \ |
| 159 | + m.process.loc[pro, coin, cout]['eff'] |
| 160 | + |
| 161 | + def def_intermittent_supply_rule(m, tm, pro, coin, cout): |
| 162 | + if coin in m.com_supim: |
| 163 | + return m.e_pro_in[tm, pro, coin, cout] == \ |
| 164 | + m.cap_pro[pro, coin, cout] * m.supim.loc[tm][coin] |
| 165 | + else: |
| 166 | + return Constraint.Skip |
| 167 | + |
| 168 | + def def_co2_emissions_rule(m, tm, pro, coin, cout): |
| 169 | + return m.co2_pro_out[tm, pro, coin, cout] == \ |
| 170 | + m.e_pro_in[tm, pro, coin, cout] * \ |
| 171 | + m.process.loc[pro, coin, cout]['co2'] * \ |
| 172 | + m.weight |
| 173 | + |
| 174 | + def res_process_output_by_capacity_rule(m, tm, pro, coin, cout): |
| 175 | + return m.e_pro_out[tm, pro, coin, cout] <= m.cap_pro[pro, coin, cout] |
| 176 | + |
| 177 | + def res_process_capacity_rule(m, pro, coin, cout): |
| 178 | + return (m.process.loc[pro, coin, cout]['cap-lo'], |
| 179 | + m.cap_pro[pro, coin, cout], |
| 180 | + m.process.loc[pro, coin, cout]['cap-up']) |
| 181 | + |
| 182 | + # storage |
| 183 | + def def_storage_state_rule(m, t, sto, com): |
| 184 | + return m.e_sto_con[t, sto, com] == \ |
| 185 | + m.e_sto_con[t-1, sto, com] + \ |
| 186 | + m.e_sto_in[t, sto, com] * m.storage.loc[sto, com]['eff-in'] - \ |
| 187 | + m.e_sto_out[t, sto, com] / m.storage.loc[sto, com]['eff-out'] |
| 188 | + |
| 189 | + def def_storage_power_rule(m, sto, com): |
| 190 | + return m.cap_sto_p[sto, com] == \ |
| 191 | + m.cap_sto_p_new[sto, com] + \ |
| 192 | + m.storage.loc[sto, com]['inst-cap-p'] |
| 193 | + |
| 194 | + def def_storage_capacity_rule(m, sto, com): |
| 195 | + return m.cap_sto_c[sto, com] == \ |
| 196 | + m.cap_sto_c_new[sto, com] + \ |
| 197 | + m.storage.loc[sto, com]['inst-cap-p'] |
| 198 | + |
| 199 | + def res_storage_input_by_power_rule(m, t, sto, com): |
| 200 | + return m.e_sto_in[t, sto, com] <= m.cap_sto_p[sto, com] |
| 201 | + |
| 202 | + def res_storage_output_by_power_rule(m, t, sto, co): |
| 203 | + return m.e_sto_out[t, sto, co] <= m.cap_sto_p[sto, co] |
| 204 | + |
| 205 | + def res_storage_state_by_capacity_rule(m, t, sto, com): |
| 206 | + return m.e_sto_con[t, sto, com] <= m.cap_sto_c[sto, com] |
| 207 | + |
| 208 | + def res_storage_power_rule(m, sto, com): |
| 209 | + return (m.storage.loc[sto, com]['cap-lo-p'], |
| 210 | + m.cap_sto_p[sto, com], |
| 211 | + m.storage.loc[sto, com]['cap-up-p']) |
| 212 | + |
| 213 | + def res_storage_capacity_rule(m, sto, com): |
| 214 | + return (m.storage.loc[sto, com]['cap-lo-c'], |
| 215 | + m.cap_sto_c[sto, com], |
| 216 | + m.storage.loc[sto, com]['cap-up-c']) |
| 217 | + |
| 218 | + def res_initial_and_final_storage_state_rule(m, t, sto, com): |
| 219 | + if t == m.t[1]: # first timestep (Pyomo uses 1-based indexing) |
| 220 | + return m.e_sto_con[t, sto, com] == \ |
| 221 | + m.cap_sto_c[sto, com] * \ |
| 222 | + m.storage.loc[sto, com]['init'] |
| 223 | + elif t == m.t[-1]: # last timestep |
| 224 | + return m.e_sto_con[t, sto, com] >= \ |
| 225 | + m.cap_sto_c[sto, com] * \ |
| 226 | + m.storage.loc[sto, com]['init'] |
| 227 | + else: |
| 228 | + return Constraint.Skip |
| 229 | + |
| 230 | + # emissions |
| 231 | + def res_co2_emission_rule(m): |
| 232 | + return summation(m.co2_pro_out) <= m.commodity.loc['CO2','Env']['max'] |
| 233 | + |
| 234 | + # costs |
| 235 | + def def_costs_rule(m, cost_type): |
| 236 | + """ calculate total costs by cost type |
| 237 | + |
| 238 | + This functions sums up process activity and capacity expansions |
| 239 | + and sums them in the cost types that are specified in the set |
| 240 | + m.cost_type. To change or add cost types, add/change entries |
| 241 | + there and modify the if/elif cases in this function accordingly. |
| 242 | + |
| 243 | + Cost types are |
| 244 | + |
| 245 | + - Investment costs for process power, storage power and |
| 246 | + storage capacity. They are multiplied by the annuity |
| 247 | + factors, which in turn are derived from the attributes |
| 248 | + 'depreciation' and 'wacc'. |
| 249 | + |
| 250 | + - Fixed costs for process power, storage power and storage |
| 251 | + capacity. |
| 252 | + """ |
| 253 | + if cost_type == 'Inv': |
| 254 | + return m.costs['Inv'] == \ |
| 255 | + sum(m.cap_pro_new[p] * |
| 256 | + m.process.loc[p]['inv-cost'] * |
| 257 | + m.process.loc[p]['annuity_factor'] |
| 258 | + for p in m.pro_tuples) + \ |
| 259 | + sum(m.cap_sto_p_new[s] * |
| 260 | + m.storage.loc[s]['inv-cost-p'] * |
| 261 | + m.storage.loc[s]['annuity_factor'] + |
| 262 | + m.cap_sto_c_new[s] * |
| 263 | + m.storage.loc[s]['inv-cost-c'] * |
| 264 | + m.storage.loc[s]['annuity_factor'] |
| 265 | + for s in m.sto_tuples) |
| 266 | + |
| 267 | + elif cost_type == 'Fix': |
| 268 | + return m.costs['Fix'] == \ |
| 269 | + sum(m.cap_pro[p] * m.process.loc[p]['fix-cost'] |
| 270 | + for p in m.pro_tuples) + \ |
| 271 | + sum(m.cap_sto_p[s] * m.storage.loc[s]['fix-cost-p'] + |
| 272 | + m.cap_sto_c[s] * m.storage.loc[s]['fix-cost-c'] |
| 273 | + for s in m.sto_tuples) |
| 274 | + |
| 275 | + elif cost_type == 'Var': |
| 276 | + return m.costs['Var'] == \ |
| 277 | + sum(m.e_pro_out[(tm,) + p] * |
| 278 | + m.process.loc[p]['var-cost'] * |
| 279 | + m.weight |
| 280 | + for tm in m.tm for p in m.pro_tuples) + \ |
| 281 | + sum(m.e_sto_con[(tm,) + s] * |
| 282 | + m.storage.loc[s]['var-cost-c'] * m.weight + |
| 283 | + (m.e_sto_in[(tm,) + s] + m.e_sto_out[(tm,) + s]) * |
| 284 | + m.storage.loc[s]['var-cost-p'] * m.weight |
| 285 | + for tm in m.tm for s in m.sto_tuples) |
| 286 | + |
| 287 | + elif cost_type == 'Fuel': |
| 288 | + return m.costs['Fuel'] == \ |
| 289 | + sum(m.e_pro_in[p] * m.commodity[c]['price'] * m.weight |
| 290 | + for p in m.pro_tuples |
| 291 | + for c in m.com_tuples |
| 292 | + if c[1] == p[1]) |
| 293 | + |
| 294 | + else: |
| 295 | + raise NotImplementedError("Unknown cost type!") |
| 296 | + |
| 297 | + def obj_rule(m): |
| 298 | + return summation(m.costs) |
| 299 | + |
| 300 | + |
| 301 | + # Equation declaration |
| 302 | + # ==================== |
| 303 | + # declarations connect rule functions to the model, specifying |
| 304 | + # the model sets for which the constraints are enforced |
| 305 | + |
| 306 | + # commodity |
| 307 | + m.res_demand = Constraint(m.tm, m.com_tuples) |
| 308 | + m.res_stock_hour = Constraint(m.tm, m.com_tuples) |
| 309 | + m.res_stock_total = Constraint(m.com_tuples) |
| 310 | + |
| 311 | + # process |
| 312 | + m.def_process_capacity = Constraint(m.tm, m.pro_tuples) |
| 313 | + m.def_process_output = Constraint(m.tm, m.pro_tuples) |
| 314 | + m.def_intermittent_supply = Constraint(m.tm, m.pro_tuples) |
| 315 | + m.def_co2_emissions = Constraint(m.tm, m.pro_tuples) |
| 316 | + m.res_process_output_by_capacity = Constraint(m.tm, m.pro_tuples) |
| 317 | + m.res_process_capacity = Constraint(m.pro_tuples) |
| 318 | + |
| 319 | + # storage |
| 320 | + m.def_storage_state = Constraint(m.tm, m.sto_tuples) |
| 321 | + m.def_storage_power = Constraint(m.sto_tuples) |
| 322 | + m.def_storage_capacity = Constraint(m.sto_tuples) |
| 323 | + m.res_storage_input_by_power = Constraint(m.tm, m.sto_tuples) |
| 324 | + m.res_storage_output_by_power = Constraint(m.tm, m.sto_tuples) |
| 325 | + m.res_storage_state_by_capacity = Constraint(m.t, m.sto_tuples) |
| 326 | + m.res_storage_power = Constraint(m.sto_tuples) |
| 327 | + m.res_storage_capacity = Constraint(m.sto_tuples) |
| 328 | + m.res_initial_and_final_storage_state = Constraint(m.t, m.sto_tuples) |
| 329 | + |
| 330 | + # emissions |
| 331 | + m.res_co2_emission = Constraint() |
| 332 | + |
| 333 | + # costs |
| 334 | + m.def_costs = Constraint(m.cost_type) |
| 335 | + m.obj = Objective(sense=minimize) |
| 336 | + |
| 337 | + return m |
| 338 | + |
| 339 | + |
| 340 | + |
| 341 | +def annuity_factor(n, i): |
| 342 | + """ return annuity factor |
| 343 | + |
| 344 | + Evaluates the annuity factor formula for depreciation duration |
| 345 | + and interest rate. Works also well for equally sized numpy arrays |
| 346 | + of values for n and i. |
| 347 | + Args: |
| 348 | + n: depreciation period (years) |
| 349 | + i: interest rate (percent, e.g. 0.06 means 6 %) |
| 350 | + |
| 351 | + Returns: |
| 352 | + Value of the expression |
| 353 | + (1+i)**n * i / ((1+i)**n - 1) |
| 354 | + |
| 355 | + """ |
| 356 | + return (1+i)**n * i / ((1+i)**n - 1) |
| 357 | + |
| 358 | + |
| 359 | +def commodity_balance(m, tm, com): |
| 360 | + """ calculate commodity balance at given timestep. |
| 361 | + |
| 362 | + For a given commodity co and timestep tm, calculate the balance of |
| 363 | + consumed (to process/storage, counts positive) and provided (from |
| 364 | + process/storage, counts negative) energy. Used as helper function |
| 365 | + in create_model for constraints on demand and stock commodities. |
| 366 | + |
| 367 | + Args: |
| 368 | + m: the model object |
| 369 | + tm: the timestep |
| 370 | + co: the commodity |
| 371 | + |
| 372 | + Returns |
| 373 | + balance: net value of consumed (+) or provided (-) energy |
| 374 | + |
| 375 | + """ |
| 376 | + balance = 0 |
| 377 | + for p in m.pro_tuples: |
| 378 | + if p[1] == com: |
| 379 | + # usage as input for process increases balance |
| 380 | + balance += m.e_pro_in[(tm,)+p] |
| 381 | + if p[2] == com: |
| 382 | + # output from processes decreases balance |
| 383 | + balance -= m.e_pro_out[(tm,)+p] |
| 384 | + for s in m.sto_tuples: |
| 385 | + # usage as input for storage increases consumption |
| 386 | + # output from storage decreases consumption |
| 387 | + if s[1] == com: |
| 388 | + balance += m.e_sto_in[(tm,)+s] |
| 389 | + balance -= m.e_sto_out[(tm,)+s] |
| 390 | + return balance |
0 commit comments