Introducción a la Programación Lineal y Pyomo en Python

Tutorial Pyomo (I) — Nociones básicas, instalación de Pyomo y resolución de un ejemplo práctico

RikiSot
13 min readApr 23, 2024

Este es el primer artículo de una serie de tutoriales dedicados a comprender los beneficios de la programación lineal y su aplicación en Python. Todo el código del proyecto puede encontrarse en:

RikiSot/pyomo-tutorial: Tutorial series to understand linear programming and Pyomo basics (github.com)

Si ya conoces lo básico sobre programación lineal, te recomiendo echar un ojo a 2. Instalación de Pyomo y solvers y pasar directamente a la segunda parte del tutorial, Tutorial Pyomo (II) — Parámetros, Sets y variables indexadas

1. Introducción

Pyomo (Python Optimization Modeling Objects) es un paquete de software de código abierto para la modelización y resolución de problemas de optimización matemática en Python. Es una herramienta potente y muy flexible que facilita la creación de modelos de optimización matemática.

Beneficios de usar Pyomo

  • Facilidad de uso: Sintaxis intuitiva y similar a Python.
  • Flexibilidad: Permite modelar una amplia variedad de problemas de optimización.
  • Potente: Integración con solvers de última generación.
  • Extensible: Amplia biblioteca de módulos y herramientas adicionales.

Aplicaciones de Pyomo:

  • Optimización financiera: Planificación financiera, gestión de carteras, análisis de riesgo.
  • Ingeniería: Diseño de productos, planificación de producción, optimización de procesos.
  • Logística: Planificación de rutas, gestión de inventario, scheduling.
  • Y muchas más!

Alternativas

Pyomo no es la única herramienta existente para modelar en Python. Personalmente, recomiendo Pyomo por ser una de las más completas y extendidas, pero no está de más nombrar algunas alternativas

  • Pulp: mucho más sencilla y perfecta para problemas sencillos. Cuando el modelo crece en complejidad deja de ser tan cómoda. De todas formas, es perfecta para aprender con ejemplos sencillos (yo mismo aprendí con Pulp antes que Pyomo).
  • SciPy: súper potente pero sigue un lenguaje demasiado matemático y menos “Pythonico”. Es difícil no perderse construyendo un modelo grande, pero tiene muchísimas funcionalidades y trabaja bien con modelos no lineales.

Público objetivo

Este tutorial está dirigido a programadores en Python con interés en aprender a modelar y resolver problemas de optimización lineal utilizando Pyomo. Se asume que el lector tiene conocimientos básicos de Python, matemáticas y programación.

2. Instalación de Pyomo y solvers

Antes de empezar, necesitamos instalar Pyomo. Como es un paquete público de PyPi, podemos hacer uso de pip para instalarlo:

Requisitos previos:

  • Python 3.6 o superior

Pasos para instalar Pyomo

  1. Abre una terminal o ventana de comandos.
  2. Ejecuta el siguiente comando:
pip install pyomo

Solvers

Pyomo únicamente es un lenguaje de modelado, no tiene capacidad para resolver los modelos que creamos haciendo uso de él. Para ello, necesitamos un *solver*, una herramienta que es capaz de obtener la solución numérica del problema que hayamos planteado.

Los solvers son herramientas complicadas y depende del solver que utilicemos podremos resolver un tipo de problema u otro, además de que puede haber diferencias significativas de rendimiento entre unos y otros.

Como el ámbito de este artículo es formativo, nos centraremos en la soluciones de modelado **lineal** y de código abierto:

CBC

Ventajas:

  • Open source
  • Alto rendimiento para problemas MILP
  • Desarrollo activo
  • Aceptado por la mayoría de frameworks

Desventajas:

  • Otras soluciones comerciales ofrecen mejor rendimiento, estabilidad y velocidad (Gurobi, CPLEX)
  • Soporte limitado para problemas no lineales
  • Configuración compleja (no necesario con Pyomo)

GLPK

Ventajas:

  • Open source
  • Fácil de usar
  • Integración en Python
  • Portable en todos los OS

Desventajas:

  • Rendimiento: mucho peor rendimiento que las soluciones comerciales o CBC para problemas grandes
  • Opciones algorítmicas limitadas
  • Sin soporte para programación no lineal

Instalación de solvers

Como los solvers son una herramienta externa a Pyomo, hay que descargarlos por separado. En máquinas Linux es muy sencillo, por lo que recomiendo usar WSL para seguir este tutorial si estamos en Windows.

Unix:

  1. Abre la terminal y actualiza el package manager
sudo apt-get update

2. Instala el solver correspondiente (o ambos). Recomiendo instalar ambos para comparar el funcionamiento de uno con otro más adelante (puede haber diferencias sustanciales)

sudo apt-get install glpk-utils # GLPK
sudo apt-get install coinor-cbc coinor-libcbc-dev # CBC

Windows

Igualmente, si queremos utilizar Pyomo en Windows (yo mismo lo hago muy a menudo) tenemos que descargar los binarios del solver correspondiente.

1. Descargar los binarios de CBC o GLPK para Windows.
2. Extraer los archivos en una carpeta.
3. Añadir la ruta de la carpeta a la variable de entorno `PATH`.
4. Si no queremos añadir los ejecutables a la `PATH`, podemos copiar el ejecutable del solver (.exe) a la carpeta en la que vayamos a trabajar.

Verificación de la instalación:

1. Abrir una terminal o ventana de comandos.
2. Escribir el comando cbco glpsoly verificar que no se produce ningún error.

Nota: Es posible que necesites permisos de administrador para instalar los solvers.

3. Conceptos básicos de programación lineal

En este tutorial nos centraremos en resolver problemas de optimización con modelado lineal, es decir sin multiplicaciones o divisiones entre variables.

3.1. ¿Qué es la programación lineal?

La programación lineal (LP) es una rama de la optimización matemática que se encarga de resolver problemas donde se busca maximizar o minimizar una función lineal (llamada objetivo) sujeta a un conjunto de restricciones lineales.

Las variables de la LP pueden ser continuas (pueden tomar cualquier valor dentro de un intervalo) o enteras (solo pueden tomar valores enteros).

Ejemplo:

Supongamos que queremos determinar la cantidad de producto A y B que debemos producir para maximizar nuestro beneficio. Las variables serían las cantidades de A y B (por ejemplo, X e Y), las restricciones serían las limitaciones de producción (por ejemplo, la disponibilidad de materia prima) y el objetivo sería maximizar el beneficio total (por ejemplo, 2X + 3Y).

3.2. Diferencias entre programación lineal y no lineal

La principal diferencia entre la LP y la programación no lineal (NLP) es que la función objetivo en la LP es lineal, mientras que en la NLP puede ser no lineal.

Problemas lineales:

  • Más fáciles de resolver.
  • Existen algoritmos eficientes para encontrar soluciones óptimas.
  • Se pueden aplicar a una amplia gama de problemas.
  • Su modelado puede ser complejo y requiere de técnicas adicionales para cubrir situaciones no lineales

Problemas no lineales:

  • Más complejos de resolver.
  • No siempre se puede encontrar una solución óptima ya que pueden devolver un mínimo/máximo local
  • Se requieren algoritmos más especializados.
  • Modelado más intuitivo

3.3. Tipos de problemas de programación lineal

  • Programación lineal pura (LP): Todas las variables son continuas.
  • Programación lineal entera (IP): Las variables solo pueden tomar valores enteros.
  • Programación lineal mixta (MILP): Algunas variables son continuas y otras son enteras.

3.4. Ventajas y desventajas de la programación lineal

Ventajas:

  • Eficiencia: Existen algoritmos eficientes para resolver problemas de LP. Además, son mucho más fáciles de resolver que los problemas no lineales.
  • Interpretabilidad: Las soluciones a los problemas de LP son fáciles de entender.
  • Solución concreta: Las soluciones responden a los puntos más eficientes (o cercano) de todo el dominio de la función, sea máximo o mínimo

Desventajas:

  • Limitaciones: Solo se pueden modelar problemas con funciones lineales. Para modelar ciertas restricciones no lineales, tendremos que usar “truquitos” con variables enteras, lo que complica el modelado.
  • Dificultad para encontrar soluciones óptimas: En algunos casos, puede ser difícil encontrar la solución óptima a un problema de programación lineal, en concreto en modelos muy grandes.

3.5. ¿Dónde se usa la programación lineal?

La programación lineal se utiliza en una amplia variedad de áreas, incluyendo:

  • Optimización financiera: Planificación financiera, gestión de carteras, análisis de riesgo.
  • Ingeniería: Diseño de productos, planificación de producción, optimización de procesos.
  • Logística: Planificación de rutas, gestión de inventario, scheduling.
  • Ciencia de datos: Análisis de regresión, selección de características, clustering.
  • Transporte: Optimización de rutas de transporte, planificación de flotas.
  • Manufactura: Planificación de la producción, gestión de inventario, scheduling.
  • Telecomunicaciones: Diseño de redes, planificación de recursos.
  • Energía: Optimización de la generación y distribución de energía.
  • Medio ambiente: Gestión de recursos naturales, planificación de la conservación.

Aprender programación lineal puede darte una ventaja competitiva en tu sector, ya que mejora la gestión de recursos y optimización de planes. Es una herramienta muy demandada en todo tipo de perfiles como por ejemplo:

  • Ingenieros industriales
  • Ingenieros de procesos
  • Ingenieros químicos
  • Científicos de datos
  • Economistas

4. Conceptos básicos de Pyomo

Pyomo es un Lenguaje de Modelado Algebraico (AML) de código abierto para la optimización matemática. Un AML es un lenguaje que permite expresar modelos de optimización de forma concisa y matemáticamente natural, sin necesidad de preocuparse por los detalles de la implementación.

Pyomo nació con el objetivo de facilitar la creación de modelos de optimización en Python, independientemente del tipo de problema (lineal, no lineal, entera, etc.) o del solver que se quiera utilizar.

Pyomo responde a la necesidad de homogenizar la forma de modelar problemas de optimización, lo que facilita el intercambio de modelos entre diferentes usuarios y comunidades.

Elementos básicos de un modelo de programación lineal con Pyomo:

Variables:

  • Representan las decisiones que se deben tomar en el problema de optimización.
  • En Pyomo, se definen utilizando la clase Var.
  • Pueden ser de diferentes tipos: continuas (NonNegativeReals), enteras (Integers), binarias (Binary), etc.

Restricciones:

  • Limitaciones que deben cumplir las variables.
  • En Pyomo, se definen utilizando la clase Constraint.
  • Se expresan como ecuaciones o desigualdades.

Objetivo:

  • Función que se desea optimizar (maximizar o minimizar).
  • En Pyomo, se define utilizando la función Objective.
  • Debe ser una expresión lineal (mientras estemos en el ámbito de programación lineal)

5. Nuestro primer problema

Vamos a resolver el problema de la sección 3.1. ¿Qué es la programación lineal? con Pyomo. Para ello, primero tenemos que construir el modelo. A fin de hacer el ejemplo lo más sencillo posible, en esta primera versión no entraremos en detalles sobre buenas prácticas de modelado.

Este es el código empleado para construir el modelo del ejemplo. Vamos a explicarlo paso a paso

from pyomo.environ import *

model = ConcreteModel()

# Definición de variables
model.x = Var(domain=NonNegativeReals)
model.y = Var(domain=NonNegativeReals)

# Definición de restricciones
model.res1 = Constraint(expr=x + y <= 10)
model.res2 = Constraint(expr=2*x + 3*y <= 15)

# Definición del objetivo
model.obj = Objective(expr=2*x + 3*y, sense=maximize)

# Resolución del modelo
solver = SolverFactory("glpk")
solver.solve(model)

# Impresión de la solución
print("Solución óptima:")
print("x:", model.x.value)
print("y:", model.y.value)
print("Beneficio:", model.obj.value)

El primer paso es importar los componentes de la librería AML de Pyomo. Todos los componentes que usaremos están en la ruta pyomo.environ así que los podemos importar con from pyomo.environ import *

A continuación tenemos que crear la instancia del modelo. El modelo es el elemento que contiene las variables, restricciones y función objetivo necesarias para definir el problema. En Pyomo podemos crear un modelo de dos formas:

  • ConcreteModel: Este tipo de modelo se define directamente en el código Python. Es la opción más sencilla y la utilizaremos en este tutorial.
  • AbstractModel: Este tipo de modelo se define en un archivo separado y se carga en el código Python. Es una opción más flexible pero requiere conocimientos adicionales.

En este tutorial, optamos por la simplicidad del ConcreteModel debido a que el problema que vamos a resolver es sencillo.

model = ConcreteModel()

Definición de variables:

El siguiente paso es definir las variables del problema. Las variables representan las decisiones que se deben tomar para optimizar el problema. En Pyomo, se definen utilizando la clase `Var`.

Ejemplo:

En nuestro problema de producción, tenemos dos variables:

- x: Cantidad de producto A a producir.
- y: Cantidad de producto B a producir.

Ambas variables son continuas no negativas, lo que significa que pueden tomar cualquier valor real positivo o cero.

model.x = Var(domain=NonNegativeReals)
model.y = Var(domain=NonNegativeReals)

Definición de restricciones:

Las restricciones son las limitaciones que deben cumplir las variables. En Pyomo, se definen utilizando la clase Constraint. Las restricciones se pueden expresar como ecuaciones o desigualdades.

Ejemplo:

En nuestro problema, tenemos dos restricciones:

  • La cantidad total de producto A y B no puede superar las 10 unidades.
  • La cantidad de producto A multiplicada por 2 más la cantidad de producto B multiplicada por 3 no puede superar las 15 unidades.
# Definición de restricciones
model.res1 = Constraint(expr=x + y <= 10)
model.res2 = Constraint(expr=2*x + 3*y <= 15)

Definición del objetivo:

Finalmente, tenemos que definir el objetivo, que es la función que se desea optimizar. En nuestro caso, queremos maximizar el beneficio total. El beneficio se calcula como la suma del beneficio por unidad de producto A multiplicada por la cantidad de producto A y el beneficio por unidad de producto B multiplicada por la cantidad de producto B.

# Definición del objetivo
model.obj = Objective(expr=2*x + 3*y, sense=maximize)

Depuración del modelo

Pyomo dispone de la función pprint() para la gran mayoría de componentes, que nos permite visualizar de forma rápida los componentes del modelo. Esto nos permite verificar que las restricciones son correctas de una forma más visual que desde el propio código:

model.pprint()
2 Var Declarations
x : Size=1, Index=None
Key : Lower : Value : Upper : Fixed : Stale : Domain
None : 0 : None : None : False : True : NonNegativeReals
y : Size=1, Index=None
Key : Lower : Value : Upper : Fixed : Stale : Domain
None : 0 : None : None : False : True : NonNegativeReals
1 Objective Declarations
obj : Size=1, Index=None, Active=True
Key : Active : Sense : Expression
None : True : maximize : 2*x + 3*y
2 Constraint Declarations
restriccion1 : Size=1, Index=None, Active=True
Key : Lower : Body : Upper : Active
None : -Inf : x + y : 10.0 : True
restriccion2 : Size=1, Index=None, Active=True
Key : Lower : Body : Upper : Active
None : -Inf : 2*x + 3*y : 15.0 : True

Se pueden comprobar las expresiones de las restricciones y del objetivo, así como todas las variables declaradas. Llama la atención que el valor de las variables es None. Como el modelo todavía no ha sido resuelto, no tenemos disponibles los resultados.

También podríamos comprobar restricciones de forma individual con el comando pprint(), por ejemplo mediante model.res1.pprint(). Esto es muy útil para debuggear modelos muy grandes.

En este punto, hemos completado la construcción del modelo de programación lineal. En la siguiente sección, explicaremos cómo resolver el modelo y obtener la solución óptima.

6. (BONUS) Mejorando el modelo

Ya hemos visto como crear un modelo básico en Pyomo. Si bien el código es funcional, ya habrás comprobado que añadir restricciones de la manera que hemos visto no es nada práctico para problemas grandes

model.res1 = Constraint(expr=x + y <= 10)
model.res2 = Constraint(expr=2*x + 3*y <= 15)

Se supone que tengo que añadir así todas las restricciones? Y si tengo que modelar condiciones complejas?

La verdad es que la legibilidad del modelo se nos puede ir de las manos. Por suerte, podemos hacer el código mucho más legible haciendo uso de los componentes AML de Pyomo. Vamos a ver cómo quedaría en la versión mejorada:

En lugar de definir la restricción directamente en el modelo, podemos crear una función que la represente. Esto puede ser útil para mejorar la legibilidad del código, especialmente cuando las restricciones son complejas.

def restriccion1(model):
return model.x + model.y <= 10

def restriccion2(model):
return 2*model.x + 3*model.y <= 15

Ahora, podemos asignar la función a la restricción `model.res1` utilizando el argumento `rule`. De esta manera, la función se utiliza para evaluar la restricción durante la resolución del modelo.

model.res1 = Constraint(rule=restriccion1)
model.res2 = Constraint(rule=restriccion2)

También podemos añadir las restricciones haciendo uso de un @decorator en la definición de la regla

@model.Constraint()
def restriccion1(model):
return model.x + model.y <= 10

@model.Constraint()
def restriccion2(model):
return 2*model.x + 3*model.y <= 15

Esta forma de añadir componentes con reglas también la podemos usar para la función objetivo (ya que al fin y al cabo es el mismo componente pyo.Expression)

# En lugar de 
model.obj = Objective(expr=2*x + 3*y, sense=maximize)

# Podemos usar
def regla_objetivo(model):
return 2*model.x + 3*model.y

model.obj = Objective(rule=regla_objetivo, sense=maximize)

# o bien
@model.Objective(sense=maximize)
def regla_objetivo(model):
return 2*model.x + 3*model.y

Código completo:

Ventajas de esta técnica:

  • Mejora la legibilidad del código: Las restricciones complejas se pueden expresar en funciones separadas, lo que facilita la comprensión del modelo.
  • Reutilización de código: Las funciones se pueden reutilizar en diferentes modelos.
  • Facilidad de mantenimiento: Es más fácil modificar o actualizar las restricciones si están definidas en funciones.
  • Permite la creación de reglas complejas: quizás en este ejemplo no pueda apreciarse, pero cuando trabajamos con variables indexadas en un set podemos crear restricciones complejas haciendo uso de estas funciones.

7. Resolver el modelo

Una vez creado el modelo podemos pasárselo al solver para obtener su solución numérica. Es importante destacar que el objeto SolverFactory es distinto al modelo, ya que es el encargado de llamar por debajo al solver especificado, y una vez obtenida la solución nuestro modelo tendrá valores numéricos para cada variable.

Para crear el objeto SolverFactory tenemos que especificar el solver correspondiente. Si hemos añadido nuestro solver al PATH bastará con indicar con glpk o cbc para que sea invocado. Si no es el caso, podemos pasar la ruta completa del .exe del solver (si lo tenemos en la misma carpeta del proyecto será algo así como cbc.exe)

Una vez especificado el solver, le pasamos el modelo para resolverlo. Una vez resuelto, se puede acceder a los valores numéricos de la solución haciendo uso de la función value() o a través del atributo .value (no disponible para el objetivo)

# Solución
solver = SolverFactory("glpk")
solver.solve(model)

# Resultados
print("Valor de x:", model.x.value)
print("Valor de y:", model.y.value)
print("Valor objetivo:", value(model.obj))

# Valor de x: 0.0
# Valor de y: 5.0
# Valor objetivo: 15.0

De hecho, si volvemos a invocar el comando model.pprint() con el modelo ya resuelto podemos ver el valor de las variables y el objetivo:

2 Var Declarations
x : Size=1, Index=None
Key : Lower : Value : Upper : Fixed : Stale : Domain
None : 0 : 0.0 : None : False : False : NonNegativeReals
y : Size=1, Index=None
Key : Lower : Value : Upper : Fixed : Stale : Domain
None : 0 : 5.0 : None : False : False : NonNegativeReals

1 Objective Declarations
obj : Size=1, Index=None, Active=True
Key : Active : Sense : Expression
None : True : maximize : 2*x + 3*y

2 Constraint Declarations
restriccion1 : Size=1, Index=None, Active=True
Key : Lower : Body : Upper : Active
None : -Inf : x + y : 10.0 : True
restriccion2 : Size=1, Index=None, Active=True
Key : Lower : Body : Upper : Active
None : -Inf : 2*x + 3*y : 15.0 : True

5 Declarations: x y restriccion1 restriccion2 obj

8. Conclusión

Recapitulando lo que hemos hecho hasta ahora

  • Hemos aprendido qué es la programación lineal y para qué sirve
  • Vistazo rápido al framework Pyomo y que posibilidades ofrece
  • Guía de instalación de Pyomo y un par de solvers básicos
  • Hemos creado nuestro primer modelo lineal en Pyomo
  • Hemos resuelto nuestro primer modelo haciendo uso del solver glpk

9. Siguientes pasos

El contenido de este artículo cubre lo necesario para conocer e instalar Pyomo, así como resolver un modelo muy básico. Para aprender cómo resolver modelos más complicados seguimos con el siguiente artículo

10. Recursos adicionales

--

--

RikiSot

Data Science and Analytics, Python developer @Idrica