Pyomo Optimization Modeling Best Practices
Pyomo Optimization Modeling Best Practices
with Pyomo
All You Wanted to Know About Practical Optimization but Were Afraid to Ask
Andres Ramos
[Link]
[Link]@[Link]
arght@[Link]
Good Optimization Modeling Practices with Pyomo. May 2024 1
Do not confuse the ingredients of the recipe
• Mathematical formulation
• LP, MIP, NLP, QCP, MCP
• Modeling language
• GAMS, Pyomo
• Solver
• CPLEX, Gurobi
• Optimization algorithm
• Primal simplex, dual simplex,
interior point
• Input/output interface
• Text file, CSV, Microsoft Excel
• Operating system
• Windows, Linux, macOS
Good Optimization Modeling Practices with Pyomo. May 2024 2
What is relevant in an optimization model?
Appealing Name
Documentation
Maintainability
and reusability
Code attributes:
• Clarity
• Modularity
• Completeness
• Interoperability
• Maintainability
• Standardization
Developer's Principle
• Pros: • Pros:
• Consistency • Flexibility, multiple choices
• Maturity. Everything has • Powerful Python libraries to
already been written be used (e.g., input data,
• Documentation output results, visualization)
• Customer support • Cons:
• Documentation is a Babel
tower, mined when using
ChatGPT
• Getting the duals
GAMS “Performance in Optimization Models: A Comparative Analysis of GAMS, Pyomo, GurobiPy,
and JuMP” July 2023
Good Optimization Modeling Practices with Pyomo. May 2024 6
Python vs. Julia
1. GAMS has been around for longer than Pyomo, which means 1. Pyomo is open-source software, which means it is free to use
it has a more established user community and more extensive and modify, while GAMS is a commercial software that requires
documentation and support. a license to use.
2. GAMS is specifically designed for mathematical programming, 2. Pyomo is based on Python, a popular general-purpose
programming language that has a large and active user
whereas Pyomo is a general-purpose modeling tool that
community, while GAMS has its own proprietary modeling
includes mathematical programming as one of its features.
language that can have a steeper learning curve.
3. GAMS provides a powerful, integrated modeling language that 3. Pyomo provides more flexibility than GAMS, allowing users to
allows you to specify models using a concise syntax, while integrate their optimization models with other Python libraries
Pyomo requires you to write Python code to define your and tools.
models. 4. Pyomo can be used for a wide range of optimization problems,
4. GAMS has a wide range of solvers available, including not just mathematical programming, such as stochastic
commercial and open-source options, and it can seamlessly programming, optimization under uncertainty, and mixed-
switch between solvers, whereas Pyomo requires more effort integer nonlinear programming.
to switch between solvers. 5. Pyomo has a more modular design that makes it easier to add
new features or extensions, while GAMS has a more monolithic
5. GAMS includes built-in functionality for handling linear,
architecture that can be harder to customize.
nonlinear, and mixed-integer programming problems,
whereas Pyomo requires additional libraries to handle some
types of optimization problems.
• Data manipulation
• pandas - Python Data Analysis Library ([Link]
• Plotting
• Plotly Open Source Graphing Library for Python ([Link]
• Matplotlib Visualization with Python ([Link]
• Vega-Altair Declarative Visualization in Python ([Link]
• Documentation
• reStructuredText ([Link]
• Sphinx makes it easy to create intelligent and beautiful documentation ([Link]
• ReadtheDocs. Build, host, and share documentation, all with a single platform
([Link]
My first example
Code conventions
• Must be defined in blocks. For example, a set and all its subsets
should constitute one block in the set section.
• Names are intended to be meaningful. Follow conventions
• Items with the same name represent the same concept in different
models
• Units should be used in all definitions
• Parameters are named pParameterName (e.g., pOperReserveDw)
• Variables are named vVariableName (e.g., vReserveDown)
• Equations are named eEquationName (e.g., eOperReserveDw)
• Use short set names (one or two letters) for easier reading
• Equations are laid out as clearly as possible
sets min 𝑐 𝑥
I origins / VIGO, ALGECIRAS /
J destinations / MADRID, BARCELONA, VALENCIA /
parameters 𝑥 ≤ 𝑎 ∀𝑖
pA(i) origin capacity
/ VIGO 350
ALGECIRAS 700 /
𝑥 ≥ 𝑏 ∀𝑗
pB(j) destination demand
/ MADRID 400 A. Mizielinska y D. Mizielinski Atlas del mundo: Un insólito viaje por las
BARCELONA 450 𝑥 ≥0 mil curiosidades y maravillas del mundo Ed. Maeva 2015
VALENCIA 150 /
variables
vX(i,j) units transported
vCost transportation cost
positive variable vX
equations
eCost transportation cost
eCapacity(i) maximum capacity of each origin
eDemand (j) demand supply at destination ;
TransportationCost = {
('Vigo',
('Vigo',
'Madrid' ):
'Barcelona'):
0.06,
0.12,
𝑥 ≥ 𝑏 ∀𝑗
('Vigo', 'Valencia' ): 0.09,
('Algeciras', 'Madrid' ): 0.05,
('Algeciras', 'Barcelona'):
('Algeciras', 'Valencia' ):
0.15,
0.11,
𝑥 ≥0
}
def eCost(mTransport):
return sum([Link][i,j]*[Link][i,j] for i,j in mTransport.i*mTransport.j)
[Link] = Objective(rule=eCost, sense=minimize, doc='transportation cost')
[Link] = Suffix(direction=[Link])
Solver = SolverFactory('gurobi')
[Link]['LogFile'] = '[Link]'
SolverResults = [Link](mTransport, tee=True)
[Link]()
[Link]()
[Link]()
for j in mTransport.j:
print([Link][[Link][j]])
import pandas as pd
capacities = [Link](
[["seattle", 350], ["san-diego", 600]], columns=["city", "capacity"] c = Parameter(
).set_index("city") container=m,
demands = [Link]( name="c",
[["new-york", 325], ["chicago", 300], ["topeka", 275]], columns=["city", "demand"] domain=[i, j],
).set_index("city") description="cost per unit of shipment between plant i and market j",
distances = [Link]( )
[ cost = freight_cost * distances / 1000
["seattle", "new-york", 2.5], [Link](cost.reset_index())
["seattle", "chicago", 1.7], x = Variable(
["seattle", "topeka", 1.8], container=m,
["san-diego", "new-york", 2.5], name="x",
["san-diego", "chicago", 1.8], domain=[i, j],
["san-diego", "topeka", 1.4], type="Positive",
], description="amount of commodity to ship from plant i to market j",
columns=["from", "to", "distance"], )
).set_index(["from", "to"]) supply = Equation(
freight_cost = 90 container=m, name="supply", domain=i, description="observe supply limit at plant i"
from gamspy import Container, Set, Parameter, Variable, Equation, Model, Sum, Sense )
m = Container() demand = Equation(
i = Set(container=m, name="i", description="plants") container=m, name="demand", domain=j, description="satisfy demand at market j"
[Link]([Link]) )
j = Set(container=m, name="j", description="markets", records=[Link]) obj = Sum((i, j), c[i, j] * x[i, j])
a = Parameter( transport = Model(
container=m, m,
name="a", name="transport",
domain=i, equations=[supply, demand],
description="supply of commodity at plant i (in cases)", problem="LP",
records=capacities.reset_index(), sense=[Link],
) objective=obj,
b = Parameter( )
container=m, import sys
name="b", [Link](output=[Link])
domain=j, [Link].set_index(["i", "j"])
description="demand for commodity at market j (in cases)", transport.objective_value
records=demands.reset_index(),
)
Additional Features
Sets
• Subsets
mSDUC.g = Set(initialize=[Link], ordered=False, doc='generating units', filter=lambda mSDUC,gg: gg in [Link] and pRatedMaxPower[gg] > 0.0)
mSDUC.t = Set(initialize=mSDUC.g , ordered=False, doc='thermal units', filter=lambda mSDUC,g : g in mSDUC.g and pLinearVarCost [g] > 0.0)
.fix
# fixing the ESS inventory at the last load level .unfix
for sc,p,es in [Link]*mTEPES.p*[Link]: .free
[Link][sc,p,[Link](),es].fix(pESSInitialInventory[es]) .setlb
.setub
• Counting constraints
print('eBalance ... ', len([Link]), ' rows’)
• Counting time
StartTime = [Link]()
# constraint creation
• Distributed computing
• Create the problems and send them to be solved in parallel
• Retrieve the solution once solved
[Link]()
model.del_component([Link])
[Link]()
# Developed by
#
# Andres Ramos
# Instituto de Investigacion Tecnologica
# Escuela Tecnica Superior de Ingenieria - ICAI
# UNIVERSIDAD PONTIFICIA COMILLAS
# Alberto Aguilera 23
# 28015 Madrid, Spain
# [Link]@[Link]
# [Link]
#
# May 8, 2023
[Link] = Param(mTransport.i, initialize={'Vigo' : 350, 'Algeciras': 700 }, doc='origin capacity' ) [Link] = Var (mTransport.i, mTransport.j, bounds=(0.0,None), doc='units transported',
[Link] = Param(mTransport.j, initialize={'Madrid': 400, 'Barcelona': 450, 'Valencia': 150}, doc='destination demand', mutable=True) within=NonNegativeReals)
[Link] = Param(mTransport.j, initialize={'Madrid': 400, 'Barcelona': 450, 'Valencia': 150}, doc='destination demand', mutable=True)
def eCapacity(mTransport, i):
TransportationCost = { return sum([Link][i,j] for j in mTransport.j) <= [Link][i]
('Vigo', 'Madrid' ): 0.06, [Link] = Constraint(mTransport.i, rule=eCapacity, doc='maximum capacity of each origin')
('Vigo', 'Barcelona'): 0.12,
('Vigo', 'Valencia' ): 0.09, def eDemand (mTransport, j):
('Algeciras', 'Madrid' ): 0.05, return sum([Link][i,j] for i in mTransport.i) >= [Link][j]
('Algeciras', 'Barcelona'): 0.15, [Link] = Constraint(mTransport.j, rule=eDemand, doc='demand supply at destination' )
('Algeciras', 'Valencia' ): 0.11,
} def eCost(mTransport):
return sum([Link][i,j]*[Link][i,j] for i,j in mTransport.i*mTransport.j)
[Link] = Objective(rule=eCost, sense=minimize, doc='transportation cost')
[Link] = Suffix(direction=[Link])
opt = [Link]()
timer = HierarchicalTimer()
for p_val in [Link](0.8, 1):
for j in mTransport.j:
[Link][j] = float(p_val)*[Link][j]
res = [Link](mTransport, timer=timer)
assert res.termination_condition == [Link]
Good Optimization Modeling Practices with Pyomo. May 2024
print([Link]())
print(timer)
32
Hashi puzzle ([Link]
Connect bridges between islands, according to their values, to form
one interconnecting path
7
1 2 3 4 5 6 7 8 9 10 11 12 13
Good Optimization Modeling Practices with Pyomo. May 2024 33
# Developed by
# Andres Ramos
[Link]
# Instituto de Investigacion Tecnologica
# Escuela Tecnica Superior de Ingenieria - ICAI
# UNIVERSIDAD PONTIFICIA COMILLAS
# Alberto Aguilera 23
# 28015 Madrid, Spain
# [Link]@[Link]
import pandas as pd
from [Link] import ConcreteModel, NonNegativeIntegers, Set, Param, Var, Constraint, Objective, minimize
from [Link] import SolverFactory
from collections import defaultdict
SolverName = 'gurobi'
# model declaration
mHP = ConcreteModel('Hashi puzzle')
mHP.x = Set(initialize=['x00', 'x01', 'x02', 'x03', 'x04', 'x05', 'x06', 'x07', 'x08', 'x09', 'x10', 'x11'], ordered=True, doc='abscises' )
mHP.y = Set(initialize=['y00', 'y01', 'y02', 'y03', 'y04', 'y05', 'y06', 'y07', 'y08', 'y09', 'y10', 'y11'], ordered=True, doc='ordinates')
Nodes = {
('x05', 'y01'): 1,
('x02', 'y02'): 3,
('x04', 'y02'): 3,
('x06', 'y02'): 4, Arcs = set()
('x03', 'y03'): 7, Neighbors = defaultdict(list)
('x05', 'y03'): 6, for x,y,xx,yy in mHP.x*mHP.y*mHP.x*mHP.y:
('x09', 'y03'): 1, if (x,y) in Nodes and (xx,yy) in Nodes:
('x04', 'y04'): 7, if [Link](x) > 2 and [Link](x) < len(mHP.x)-2 and [Link](y) > 1 and [Link](y) < len(mHP.y)-1:
('x06', 'y04'): 6, if ((xx,yy) == ([Link](x),[Link](y)) or (xx,yy) == ([Link](x,2),y) or (xx,yy) == ([Link](x),[Link](y)) or
('x10', 'y04'): 7, (xx,yy) == ([Link](x),[Link](y)) or (xx,yy) == ([Link](x,2),y) or (xx,yy) == ([Link](x),[Link](y))):
('x07', 'y05'): 5, # add the arc and its neighbors to the list
('x09', 'y05'): 7, [Link]((x, y, xx, yy))
('x11', 'y05'): 4, [Link]((xx, yy, x, y))
('x06', 'y06'): 5, Neighbors[x,y].append((xx,yy))
('x08', 'y06'): 6,
} # parameters
[Link] = Param(mHP.x, mHP.y, initialize=Nodes, doc='number of connections per node')
# Variables
[Link] = Var(Arcs, within=NonNegativeIntegers, doc='connections between two neighbor nodes')
def eTotalConnections(mHP):
return sum([Link][x,y,xx,yy] for x,y,xx,yy in Arcs)
[Link] = Objective(rule=eTotalConnections, sense=minimize, doc='total number of connections')
Solver = SolverFactory(SolverName)
[Link]('[Link]', io_options={'symbolic_solver_labels': True})
SolverResults = [Link](mHP, tee=True)
[Link]() # summary of the solver results
[Link]()
if __name__ == "__main__":
plain_run(mHP, SolverName)
Good Optimization Modeling Practices with Pyomo. May 2024 34
Picking the right campsite
[Link]
• Selecting the ideal camping site requires careful consideration of many factors
such as the proximity to water, availability of firewood and protection from the
elements. Your potential camping sites are shown in the corresponding map.
The map consists of 64 sites each with varying characteristics, including water,
wood, swamp and mosquitoes.
• The quality of a site is determined by a points system. A site that contains water
receives +3 points and a site that is near water receives +1 point. A site that
contains wood receives +2 points and a site that is near wood receives +1 point.
A site that contains a swamp is -2 points and a site that is near a swamp is -1
point. A site that contains mosquitoes receives -3 points and a site that is near
mosquitoes is -2 points.
• “Near” is defined as adjacent sites including diagonals. For example, site B5 is
worth 1 point (based on +3 on water, +1 near wood, -2 near mosquitoes, -1
near swamp). Note that you only count points once for each type of
characteristic.
• Where is the best campsite?
I I I
300
250
200 Lower Bound
150 Upper Bound
100
50
0
7 8 9 10 11 12
Iterations
import pandas as pd
import [Link] as pyo
from [Link] import ConcreteModel, Set, Param, Var, Binary, NonNegativeReals, RealSet, Constraint, Objective, minimize, Suffix, TerminationCondition
from [Link] import SolverFactory
mMaster_Bd.l = Set(initialize=['it1', 'it2', 'it3', 'it4', 'it5', 'it6', 'it7', 'it8', 'it9', 'it10'], ordered=True, doc='iterations')
mMaster_Bd.ll = Set( doc='iterations')
[Link] = Param(mFCTP.i, initialize={'i1': 20, 'i2': 30, 'i3': 40, 'i4': 20}, doc='origin capacity' )
[Link] = Param(mFCTP.j, initialize={'j1': 20, 'j2': 50, 'j3':30 }, doc='destination demand')
FixedCost = {
('i1', 'j1'): 10,
('i1', 'j2'): 20,
('i1', 'j3'): 30,
('i2', 'j1'): 20,
('i2', 'j2'): 30,
('i2', 'j3'): 40,
('i3', 'j1'): 30,
('i3', 'j2'): 40,
('i3', 'j3'): 50,
('i4', 'j1'): 40,
('i4', 'j2'): 50,
('i4', 'j3'): 60,
}
TransportationCost = {
('i1', 'j1'): 1,
('i1', 'j2'): 2,
('i1', 'j3'): 3,
('i2', 'j1'): 3,
('i2', 'j2'): 2,
('i2', 'j3'): 1,
('i3', 'j1'): 2,
('i3', 'j2'): 3,
('i3', 'j3'): 4,
('i4', 'j1'): 4,
('i4', 'j2'): 3,
('i4', 'j3'): 2,
} Good Optimization Modeling Practices with Pyomo. May 2024 39
FCTP solved by Benders decomposition (ii)
[Link] = Param(mFCTP.i, mFCTP.j, initialize=FixedCost, doc='fixed investment cost' )
[Link] = Param(mFCTP.i, mFCTP.j, initialize=TransportationCost, doc='per unit transportation cost')
def eCostMst(mMaster_Bd):
return sum([Link][i,j]*mMaster_Bd.vY[i,j] for i,j in mFCTP.i*mFCTP.j) + mMaster_Bd.vTheta
mMaster_Bd.eCostMst = Objective(rule=eCostMst, sense=minimize, doc='total cost')
def eCostSubp(mFCTP):
return sum([Link][i,j]*[Link][i,j] for i,j in mFCTP.i*mFCTP.j) + sum([Link][j]*1000 for j in mFCTP.j)
[Link] = Objective(rule=eCostSubp, sense=minimize, doc='transportation cost')
Solver = SolverFactory('gurobi')
[Link]['LogFile'] = '[Link]'
[Link] = Suffix(direction=[Link])
# initialization
Z_Lower = float('-inf')
Z_Upper = float(' inf')
BdTol = 1e-6
# solving subproblem
SolverResultsSbp = [Link](mFCTP)
Z2 = [Link]()
Z2_L[l] = Z2
mMaster_Bd.[Link]()
Delta[l] = 1
mMaster_Bd.[Link]()
[Link]()
[Link]()
def eCost(mFCTP):
return sum([Link][i,j]*[Link][i,j] for i,j in mFCTP.i*mFCTP.j) + sum([Link][i,j]*[Link][i,j] for i,j in mFCTP.i*mFCTP.j) + sum([Link][j]*1000 for j in mFCTP.j)
[Link] = Objective(rule=eCost, sense=minimize, doc='total cost')
Disclaimer:
This model is a work in progress and will be
updated accordingly.
# Developed by
# Andres Ramos
# Instituto de Investigacion Tecnologica
# Escuela Tecnica Superior de Ingenieria - ICAI
# UNIVERSIDAD PONTIFICIA COMILLAS
# Alberto Aguilera 23
# 28015 Madrid, Spain
# [Link]@[Link]
# [Link]
# with the very valuable collaboration from David Dominguez ([Link]@[Link]) and Alejandro Rodriguez (argallego@[Link]), our local Python gurus
#%% Libraries
import argparse
import os
import pandas as pd
import time # count clock time
import psutil # access the number of CPUs
import [Link] as pyo
from [Link] import Set, Var, Binary, NonNegativeReals, RealSet, Constraint, ConcreteModel, Objective, minimize, Suffix, DataPortal
from [Link] import SolverFactory
print('\n #### Academic research license - for non-commercial use only #### \n')
StartTime = [Link]()
DIR = [Link](__file__)
CASE = '16g'
SOLVER = 'gurobi'
# compute the demand as the mean over the time step load levels and assign it to active load levels. Idem for operating reserve, variable max power, variable min and max storage
capacity and inflows
pDemand = [Link] (pTimeStep).mean()
pOperReserveUp = [Link] (pTimeStep).mean()
pOperReserveDw = [Link] (pTimeStep).mean()
pVariableMinPower = [Link] (pTimeStep).mean()
pVariableMaxPower = [Link] (pTimeStep).mean()
pVariableMinStorage = [Link](pTimeStep).mean()
pVariableMaxStorage = [Link](pTimeStep).mean()
pEnergyInflows = [Link] (pTimeStep).mean()
if pTimeStep > 1:
# assign duration 0 to load levels not being considered, active load levels are at the end of every pTimeStep
for i in range(pTimeStep-2,-1,-1):
pDuration[range(i,len([Link]),pTimeStep)] = 0
#%% defining subsets: active load levels (n), thermal units (t), ESS units (es), all the lines (la), candidate lines (lc) and lines with losses (ll)
mSDUC.n = Set(initialize=[Link], ordered=True , doc='load levels' , filter=lambda mSDUC,nn: nn in [Link] and pDuration [nn] > 0 )
mSDUC.n2 = Set(initialize=[Link], ordered=True , doc='load levels' , filter=lambda mSDUC,nn: nn in [Link] and pDuration [nn] > 0 )
mSDUC.g = Set(initialize=[Link], ordered=False, doc='generating units', filter=lambda mSDUC,gg: gg in [Link] and pRatedMaxPower[gg] > 0.0)
mSDUC.t = Set(initialize=mSDUC.g , ordered=False, doc='thermal units', filter=lambda mSDUC,g : g in mSDUC.g and pLinearVarCost [g] > 0.0)
mSDUC.r = Set(initialize=mSDUC.g , ordered=False, doc='RES units', filter=lambda mSDUC,g : g in mSDUC.g and pLinearVarCost [g] == 0.0 and pRatedMaxStorage[g] == 0.0)
[Link] = Set(initialize=mSDUC.g , ordered=False, doc='ESS units', filter=lambda mSDUC,g : g in mSDUC.g and pRatedMaxStorage[g] > 0.0)
# non-RES units
[Link] = mSDUC.g - mSDUC.r
if pTimeStep > 1:
# drop levels with duration 0
pDuration = [Link] [[Link]*mSDUC.n]
pDemand = [Link] [[Link]*mSDUC.n]
pOperReserveUp = [Link] [[Link]*mSDUC.n]
pOperReserveDw = [Link] [[Link]*mSDUC.n]
pVariableMinPower = [Link] [[Link]*mSDUC.n]
pVariableMaxPower = [Link] [[Link]*mSDUC.n]
pVariableMinStorage = [Link][[Link]*mSDUC.n]
pVariableMaxStorage = [Link][[Link]*mSDUC.n]
pEnergyInflows = [Link] [[Link]*mSDUC.n]
# values < 1e-5 times the maximum system demand are converted to 0
pEpsilon = [Link]()*1e-5
# these parameters are in GW
pDemand [pDemand < pEpsilon] = 0.0
pOperReserveUp [pOperReserveUp < pEpsilon] = 0.0
pOperReserveDw [pOperReserveDw < pEpsilon] = 0.0
pMinPower [pMinPower < pEpsilon] = 0.0
pMaxPower [pMaxPower < pEpsilon] = 0.0
pMaxPower2ndBlock[pMaxPower2ndBlock < pEpsilon] = 0.0
pMaxCharge [pMaxCharge < pEpsilon] = 0.0
pEnergyInflows [pEnergyInflows < pEpsilon/pTimeStep] = 0.0
# these parameters are in GWh
pMinStorage [pMinStorage < pEpsilon] = 0.0
pMaxStorage [pMaxStorage < pEpsilon] = 0.0
# fixing the ESS inventory at the last load level at the end of the time scope
for sc,es in [Link]*[Link]:
[Link][sc,[Link](),es].fix(pInitialInventory[es])
#%% definition of the time-steps leap to observe the stored energy at ESS
pCycleTimeStep = pUpTime*0
for es in [Link]:
if pStorageType[es] == 'Daily' :
pCycleTimeStep[es] = 1
if pStorageType[es] == 'Weekly' :
pCycleTimeStep[es] = int( 24/pTimeStep)
if pStorageType[es] == 'Monthly' :
pCycleTimeStep[es] = int( 168/pTimeStep)
# fixing the ESS inventory at the end of the following pCycleTimeStep (weekly, yearly), i.e., for daily ESS is fixed at the end of the week, for weekly/monthly ESS is fixed at the end of the year
for sc,n,es in [Link]*mSDUC.n*[Link]:
if pStorageType[es] == 'Daily' and [Link](n) % ( 168/pTimeStep) == 0:
[Link][sc,n,es].fix(pInitialInventory[es])
if pStorageType[es] == 'Weekly' and [Link](n) % (8736/pTimeStep) == 0:
[Link][sc,n,es].fix(pInitialInventory[es])
if pStorageType[es] == 'Monthly' and [Link](n) % (8736/pTimeStep) == 0:
[Link][sc,n,es].fix(pInitialInventory[es])
def eTotalECost(mSDUC):
return [Link] == sum(pScenProb[sc] * pCO2Cost * pCO2EmissionRate[nr] * [Link][sc,n,nr] for sc,n,nr in [Link]*mSDUC.n*[Link])
[Link] = Constraint(rule=eTotalECost, doc='total system emission cost [MEUR]')
def eTotalTCost(mSDUC):
return [Link] + [Link]
[Link] = Objective(rule=eTotalTCost, sense=minimize, doc='total system cost [MEUR]')
#%% constraints
def eOperReserveUp(mSDUC,sc,n):
if pOperReserveUp[sc,n]:
return sum([Link] [sc,n,nr] for nr in [Link]) >= pOperReserveUp[sc,n]
else:
return [Link]
[Link] = Constraint([Link], mSDUC.n, rule=eOperReserveUp, doc='up operating reserve [GW]')
def eOperReserveDw(mSDUC,sc,n):
if pOperReserveDw[sc,n]:
return sum([Link][sc,n,nr] for nr in [Link]) >= pOperReserveDw[sc,n]
else:
return [Link]
[Link] = Constraint([Link], mSDUC.n, rule=eOperReserveDw, doc='down operating reserve [GW]')
def eBalance(mSDUC,sc,n):
return sum([Link][sc,n,g] for g in mSDUC.g) - sum([Link][sc,n,es] for es in [Link]) + [Link][sc,n] == pDemand[sc,n]
[Link] = Constraint([Link], mSDUC.n, rule=eBalance, doc='load generation balance [GW]')
def eESSInventory(mSDUC,sc,n,es):
if [Link](n) == pCycleTimeStep[es]:
return pInitialInventory[es] + sum(pDuration[n2]*(pEnergyInflows[es][sc,n2] - [Link][sc,n2,es] + pEfficiency[es]*[Link][sc,n2,es]) for n2
in list(mSDUC.n2)[[Link](n)-pCycleTimeStep[es]:[Link](n)]) == [Link][sc,n,es] + [Link][sc,n,es]
elif [Link](n) > pCycleTimeStep[es] and [Link](n) % pCycleTimeStep[es] == 0:
return [Link][sc,[Link](n,pCycleTimeStep[es]),es] + sum(pDuration[n2]*(pEnergyInflows[es][sc,n2] - [Link][sc,n2,es] + pEfficiency[es]*[Link][sc,n2,es]) for n2
in list(mSDUC.n2)[[Link](n)-pCycleTimeStep[es]:[Link](n)]) == [Link][sc,n,es] + [Link][sc,n,es]
else:
return [Link]
[Link] = Constraint([Link], mSDUC.n, [Link], rule=eESSInventory, doc='ESS inventory balance [GWh]')
def eMinOutput2ndBlock(mSDUC,sc,n,nr):
if pOperReserveDw[sc,n] and pMaxPower2ndBlock[nr][sc,n]:
return (mSDUC.vOutput2ndBlock[sc,n,nr] + [Link][sc,n,nr]) / pMaxPower2ndBlock[nr][sc,n] >= 0.0
else:
return [Link]
mSDUC.eMinOutput2ndBlock = Constraint([Link], mSDUC.n, [Link], rule=eMinOutput2ndBlock, doc='min output of the second block of a committed unit [p.u.]')
def eTotalOutput(mSDUC,sc,n,nr):
if pMinPower[nr][sc,n] == 0.0:
return [Link][sc,n,nr] == mSDUC.vOutput2ndBlock[sc,n,nr]
else:
return [Link][sc,n,nr] / pMinPower[nr][sc,n] == [Link][n,nr] + mSDUC.vOutput2ndBlock[sc,n,nr] / pMinPower[nr][sc,n]
[Link] = Constraint([Link], mSDUC.n, [Link], rule=eTotalOutput, doc='total output of a unit [GW]')
def eUCStrShut(mSDUC,n,nr):
if n == [Link]():
return [Link][n,nr] - pInitialUC[nr] == [Link][n,nr] - [Link][n,nr]
else:
return [Link][n,nr] - [Link][[Link](n),nr] == [Link][n,nr] - [Link][n,nr]
[Link] = Constraint(mSDUC.n, [Link], rule=eUCStrShut, doc='relation among commitment startup and shutdown')
#%%
def eRampUp(mSDUC,sc,n,t):
if pRampUp[t] and pRampUp[t] < pMaxPower2ndBlock[t][sc,n] and n == [Link]():
return (mSDUC.vOutput2ndBlock[sc,n,t] - max(pInitialOutput[t]-pMinPower[t][sc,n],0.0) + [Link] [sc,n,t]) / pDuration[n] / pRampUp[t] <= [Link][n,t] - [Link][n,t]
elif pRampUp[t] and pRampUp[t] < pMaxPower2ndBlock[t][sc,n]:
return (mSDUC.vOutput2ndBlock[sc,n,t] - mSDUC.vOutput2ndBlock[sc,[Link](n),t] + [Link] [sc,n,t]) / pDuration[n] / pRampUp[t] <= [Link][n,t] - [Link][n,t]
else:
return [Link]
[Link] = Constraint([Link], mSDUC.n, mSDUC.t, rule=eRampUp, doc='maximum ramp up [p.u.]')
def eRampDw(mSDUC,sc,n,t):
if pRampDw[t] and pRampDw[t] < pMaxPower2ndBlock[t][sc,n] and n == [Link]():
return (mSDUC.vOutput2ndBlock[sc,n,t] - max(pInitialOutput[t]-pMinPower[t][sc,n],0.0) - [Link][sc,n,t]) / pDuration[n] / pRampDw[t] >= - pInitialUC[t] + [Link][n,t]
elif pRampDw[t] and pRampDw[t] < pMaxPower2ndBlock[t][sc,n]:
return (mSDUC.vOutput2ndBlock[sc,n,t] - mSDUC.vOutput2ndBlock[sc,[Link](n),t] - [Link][sc,n,t]) / pDuration[n] / pRampDw[t] >= - [Link][[Link](n),t] + [Link][n,t]
else:
return [Link]
[Link] = Constraint([Link], mSDUC.n, mSDUC.t, rule=eRampDw, doc='maximum ramp down [p.u.]')
def eMinDownTime(mSDUC,n,t):
if pDwTime[t] > 1 and [Link](n) >= pDwTime[t]:
return sum([Link][n2,t] for n2 in list(mSDUC.n2)[[Link](n)-pDwTime[t]:[Link](n)]) <= 1 - [Link][n,t]
else:
return [Link]
[Link] = Constraint(mSDUC.n, mSDUC.t, rule=eMinDownTime, doc='minimum down time [h]')
#%% fix values of binary variables to get dual variables and solve it again
for n,nr in mSDUC.n*[Link]:
[Link][n,nr].fix(round([Link][n,nr]()))
[Link] [n,nr].fix(round([Link] [n,nr]()))
[Link] [n,nr].fix(round([Link] [n,nr]()))
[Link] = Suffix(direction=[Link])
SolverResults = [Link](mSDUC, tee=True) # tee=True displays the output of the solver
[Link]() # summary of the solver results
fig, fg = [Link]()
for r in mSDUC.r:
[Link](range(len([Link]*mSDUC.n)), RESCurtailment[:,:,r], label=r)
[Link](xlabel='Hours', ylabel='MW')
fg.set_ybound(lower=0)
[Link]('RES Curtailment')
fg.tick_params(axis='x', rotation=90)
[Link]()
plt.tight_layout()
#[Link]()
[Link](_path+'/oUC_Plot_RESCurtailment_'+CaseName+'.png', bbox_inches=None)
OutputResults = [Link](data=[sum(OutputResults[sc,n,es] for es in [Link] if (gt,es) in mSDUC.t2g) for sc,n,gt in [Link]*mSDUC.n*[Link] if sum(1 for es in [Link] if (gt,es) in mSDUC.t2g)], index=[Link].from_tuples([(sc,n,gt) for
sc,n,gt in [Link]*mSDUC.n*[Link] if sum(1 for es in [Link] if (gt,es) in mSDUC.t2g)]))
OutputResults.to_frame(name='MW' ).reset_index().pivot_table(index=['level_0','level_1'], columns='level_2', values='MW' ).rename_axis(['Scenario','LoadLevel'], axis=0).rename_axis([None],
axis=1).to_csv(_path+'/oUC_Result_TechnologyCharge_'+CaseName+'.csv', sep=',')
TechnologyCharge = [Link][:,:,:]
OutputResults = [Link](data=[sum(OutputResults[sc,n,es] for es in [Link] if (gt,es) in mSDUC.t2g) for sc,n,gt in [Link]*mSDUC.n*[Link] if sum(1 for es in [Link] if (gt,es) in mSDUC.t2g)], index=[Link].from_tuples([(sc,n,gt) for
sc,n,gt in [Link]*mSDUC.n*[Link] if sum(1 for es in [Link] if (gt,es) in mSDUC.t2g)]))
OutputResults.to_frame(name='GWh').reset_index().pivot_table(index=['level_0','level_1'], columns='level_2', values='GWh').rename_axis(['Scenario','LoadLevel'], axis=0).rename_axis([None],
axis=1).to_csv(_path+'/oUC_Result_ESSTechnologyEnergy_'+CaseName+'.csv', sep=',')
TechnologyOutput = [Link][:,:,:]
fig, fg = [Link]()
for sc in [Link]:
[Link](range(len(mSDUC.n)), SRMC[sc], label=sc)
[Link](xlabel='Hours', ylabel='EUR/MWh')
fg.set_ybound(lower=0, upper=100)
[Link]('SRMC')
fg.tick_params(axis='x', rotation=90)
[Link]()
plt.tight_layout()
#[Link]()
[Link](_path+'/oUC_Plot_SRMC_'+CaseName+'.png', bbox_inches=None)
if __name__ == '__main__':
main()
Disclaimer:
This model is a work in progress and will be
updated accordingly.