3. Introducción.

Como ya hemos visto en apartados anteriores, un modelo de Pyomo está construido sobre componentes del modelo ( modeling components, en términos anglosajones ), que son los que se encargan de generar los componentes necesarios para construir el modelo que se necesita para resolver el problema de optimización que en cada momento se precise.

A continuación se relacionan las componentes que son proporcionadas por Pyomo.

  • BuildAction.

  • BuildCheck.

  • Constraint.

  • ConstraintList.

  • Objective.

  • Param.

  • Piecewise.

  • RangeSet.

  • SOSConstraint.

  • Set.

  • Variables.

En los apartados que siguen, se va a detallar la utilización de las componentes Var, Objetive, Cosntraint, Set y Param, el resto de los presentados en la lista anterior son menos utilizados y al lector interesado en ellos, se le remite a la documentación oficial de Pymo.

3.1. Variables.

Una variable representa a un valor numérico, que es determinado por Pymo una vez finaliza el programa de optimización. Esta variables, son manejadas por Pyomo mediante la clase denominada Var, y estas variables van a definir el espacio de búsqueda de la optimización. Los parámetros que soporta, son los siguientes:

  • bounds. Una función (o un objeto de Python) que atribuye los límites (lower, upper) de la variable.

  • domain. Es el conjunto de valores que la variable puede tomar.

  • initialize. Un función o un objeto de Python para establecer los valores iniciales de las variables.

  • within. Es un sinónimo de domain.

3.1.1. Declaración de variables.

Python ofrece muchas alternativas para poder declarar variables. La más sencilla es la que se muestra en el siguiente código:

model.z = Var()

Otras formas posibles de declarar variables, consiste por ejemplo, en unas declaraciones como las que se muestran a continuación:

B = [1.5, 2.5, 3.5]
model.u = Var(B)
model.C = Set()
model.t = Var(B, model.C)

Con las opciones domain o within, se puede definir el dominio en el que las variables puede tomar los valores, como se pueden ver en los siguientes ejemplos (por defecto el valor que toma para el dominio de definición es Any ):

model.A = Set(initialize=[1,2,3])
model.y = Var(within=model.A)
model.r = Var(domain=Reals)
model.w = Var(within=Boolean)

Se les puede dar un intervalo de definición de sus valores, mediante el atributo bounds.

model.a = Var(bounds=(0.0,None))
lower = {1:2, 2:4, 3:6}
upper = {1:3, 2:4, 3:7}
def f(model, i):
return (lower[i], upper[i])
model.b = Var(model.A, bounds=f)

Si se quieres dar un valor inicial a las variables, se deberá uilizar el atributo initialize, como puede verse en el siguiente ejemplo:

model.za = Var(initialize=9, within=NonNegativeReals)
model.zb = Var(model.A, initialize={1:1, 2:4, 3:9})
model.zc = Var(model.A, initialize=2)

También se puede utilizar funciones, para hacer esta inicialización de la variable, como puede verse en el siguiente ejemplo:

def g(model, i):
return 3*i
model.m = Var(model.A, initialize=g)

Si durante el programa de optimización se necesita que una variable vuelva a sus valores iniciales, se puede utilizar el método reset:

model.za = 8.5
print model.za.value # 8.5
model.za.reset()
print model.za.value # 9

Igualmente se les puede asignar un valor concreto mediante el operador de asgnacion “=”.

model.za = 8.5
model.zb[2] = 7

Las variables de Pyomo, tienen una buena cantidad de funciones de tipo helper, que facilitan su uso. A continuación de relacionan algunas de estas funciones:

  • float. Se utiliza para forzar a una variable a tomar valores de punto flotante.

  • value. Se utiliza para volver a una variable al tipo de valores que inicialmente soporta.

  • len. Devuelve el número de variables de una variable de tipo array.

Los objetos derivados de la clase Var, poseen una serie de atributos de entre los cuales se pueden destacar los siguientes

  • value. Devuelve el valor actual de la variable.

  • lb y ub. Devuelve las cotas inferior y superior de la variables.

  • fixed. Si fixed toma el valor True, entonces la variable tiene un valor fijo.

3.2. Objetives.

La función objetivo queda constituida por un conjunto de valores de los parámetros y variables que formando una expresión lineal, es sobre la que la Pyomo debe buscar su máximo o mínimo, dependiendo del problema planteado. Un formato sencillo de aplicar este elemento es el que se muestra a continuación:

model.a = Objective()

Algunos solvers sobre los que se puede trabajar con Pyomo, permiten incluso definir más de una variable objetivo, y éstas se pueden declarar de forma separada o mediante un array de funciones objetivos:

model.b = Objective()
model.c = Objective([1,2,3])

Si se han definido previamente las variables \(x[1]\) y \(x[2]\), una forma más concreta de definir una función objetivo es de la siguiente manera:

model.d = Objective(expr=model.x[1] + 2*model.x[2])

Cuando no se declara expresamente el sentido de la optimización, se entiende que por defecto se pretende minimizar la función objetivo. Si el objetivo fuera maximizar, se utilizaría el parámetro \(sense\), tal y como podemos ver en el siguiente ejemplo:

model.e = Objective(expr=model.x[1], sense=maximize)

3.2.1. Declaración de la función objetivo mediante rules.

Una forma un tanto diferente para definir la función objetivo tal y como se ha hecho en el apartado anterior, es mediante el uso de funciones denominadas \(rules\). Para ver cómo definir la función objetivo de esta manera, a continuación se muestra un ejemplo, en el que se puede ver cómo se construye esa función objetivo de tres formas diferentes, pero con el mismo resultado.

model.f = Objective(expr=model.x[1] + 2*model.x[2])

def TheObjective(model):
    return model.x[1] + 2*model.x[2]
model.g = Objective(rule=TheObjective)

def gg_rule(model):
    return model.x[1] + 2*model.x[2]
model.gg = Objective()

La ventaja que ofrece la utilización de funciones de tipo \(rule\) para conseguir la función objetivo, es que cuando el objeto Objetive se declara con un conjunto de valores como argumento, Pyomo itera sobre todos los elementos de ese conjunto de valores para construir esa función objetivo:

def h_rule(model, i):
    return i*model.x[1] + i*i*model.x[2]
model.h = Objective([1, 2, 3, 4])

Si en lugar de pasar un solo conjunto de valores, se pasan varios, lo que se hace es el cruce de todos estos valores, generando un índice para cada uno de estos cruces.

Otra de las ventajas que ofrece la construcción de funciones objetivo mediante este tipo de funciones, es que se pueden construir estas funciones objetivos de una forma mucho más flexible. Por ejemplo, en el ejemplo que sigue, se obtiene una función objetivo, de forma incremental:

def m_rule(model):
    expr = model.x[1]
    expr += 2*model.x[2]
    return expr
model.m = Objective()

Igualmente el siguiente ejemplo es una muestra de cómo construir la función objetivo, dependiendo se cumplan unas u otras condiciones:

p = 0.6
def n_rule(model):
    if p > 0.5:
        return model.x[1] + 2*model.x[2]
    else:
        return model.x[1] + 3*model.x[2]
    return expr
model.n = Objective()

Lo anterior, igualmente se puede conseguir, sin necesidad de utilizar esas funciones de tipo \(rule\):

p = 0.6
if p > 0.5:
    model.p = Objective(expr=model.x[1] + 2*model.x[2])
else:
    model.p = Objective(expr=model.x[1] + 3*model.x[2])

3.3. Constraints.

Las restricciones están constituidas por expresiones que generan limitaciones a los valores que pueden tomar las variables independientes.

Las declaraciones de estas restricciones, son muy similares a la forma en que se define la función objetivo, pero difieren en que para la declaración de las instrucciones además se utilizan igualdades o desigualdades.

Estas restricciones, por regla general están indexadas, lo que facilita su manipulación y construcción.

A continuación se pasan a exponer diferentes metodologías que se pueden utilizar para construir estas restricciones.

Supóngase que se han definido las variables model.x[1] y model.x[2], entonces una sencilla declaración de una restricción puede ser la siguiente:

model.Diff= Constraint(expr=model.x[2]-model.x[1] <= 7.5)

Esta misma restricción puede ser definida utilizando una función de tipo rule, de la siguiente manera:

def Diff_rule(model):
    return model.x[2] - model.x[1] <= 7.5
model.Diff = Constraint()

Como ya se ha dicho anteriormente, las restricciones suelen expresarse mediante índices, y esos índices suelen generase al construir las restricciones correspondientes. Para ver esto mejor, veamos al siguiente ejemplo.

N = [1,2,3]
a = {1:1, 2:3.1, 3:4.5}
b = {1:1, 2:2.9, 3:3.1}
model.y = Var(N, within=NonNegativeReals, initialize=0.0)
def CoverConstr_rule(model, i):
    return a[i] * model.y[i] >= b[i]
model.CoverConstr= Constraint(N)

En este ejemplo, se construyen tantas restricciones, como valores tenga el parámetro N que se le pasa a la función de tipo rules que sirve para construir las restricciones. Si se pasan varios parámetros se hace el cruce de todos los valores de esos parámetros. En el ejemplo anterior, se construirían las restricciones dadas por el siguiente modelo:

\[\begin{split} a_iy_i\geq b_i\,\forall i \epsilon \{1,2,3\}\\ y_i \geq 0\, \forall i \epsilon \{1,2,3\} \end{split}\]

En el siguiente ejemplo, se muestra una forma de construir restricciones mediante índices.

from pyomo.environ import *

model = ConcreteModel()

model.N = Set(initialize=[1,2])
model.M = Set(initialize=[1,2])
model.c = Param(model.N, initialize={1:1, 2:2})
model.a = Param(model.N, model.M,
initialize={(1,1):3, (2,1):4, (1,2):2, (2,2):5})
model.b = Param(model.M, initialize={1:1, 2:2})
model.x = Var(model.N, within=NonNegativeReals)

def obj_rule(model):
    return sum(model.c[i]*model.x[i] for i in model.N)
model.obj = Objective(rule=obj_rule)

def con_rule(model, m):
    return sum(model.a[i,m]*model.x[i] for i in model.N) \
    >= model.b[m]
model.con = Constraint(model.M, rule=con_rule)

3.3.1. Declarar restricciones con tuplas.

Hasta estos momentos se han planteado restricciones con un sólo operador de comparación, es decir expresiones que son iguales a algo o que son mayores o iguales o bien menores o iguales. Sin embargo también se pueden presentar situaciones en las que debemos definir acotaciones por arriba y por abajo de nuestra expresión de restricción. En estos caso, si por ejemplo tenemos una restricción de la forma:

\[ l \leq f \leq u\]

En Pyomo, la podremos definir como una tupla de la forma \((l,f,u)\), o también se puede utilizar la tupla \((l,f)\), si la restricción es de la forma \(l == f\).

Veamos esto con un ejemplo concreto. Supongamos que queremos expresar la siguiente restricción:

\[ 0.25 \leq \frac{a_i x_i} { b_i} \leq 1 \, \forall \epsilon \{1,2,3\} \]

Entonces se podrá utilizar una tupla para definir de una sola vez esta restricción:

def CapacityIneq_rule(model, i):
    return (0.25, (a[i] * model.y[i])/b[i], 1.0)
model.CapacityIneq = Constraint(N)

De la misma manera el siguiente código:

def CapacityEq_rule(model, i):
    return (0, a[i] * model.y[i] - b[i])
model.CapacityEq = Constraint(N)

refleja una restricción como la que a continuación sigue:

\[ 0 \leq a_ix_i-b_i\leq 0 \,\forall i \epsilon \{1,2,3\} \]

Dentro de la función rule, se pueden utilizar el valor de las constantes Constraint.Skip o Constraint.NoConstraint para indicar que las restricciones están asociadas a determinados índices. A continuación se muestra un ejemplo de esto.

TimePeriods = [1,2,3,4,5]
LastTimePeriod = 5

model.StartTime = Var(TimePeriods, initialize=1.0)

def Pred_rule(model, t):
    if t == LastTimePeriod:
        return Constraint.Skip
    else:
        return model.StartTime[t] <= model.StartTime[t+1]
model.Pred = Constraint(TimePeriods)

3.4. lista de restircciones (Constraint List).

Este procedimiento es otro disponible en Pyomo para añadir restricciones.