Source code for skcriteria.utils.lp

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# License: BSD-3 (https://tldrlegal.com/license/bsd-3-clause-license-(revised))
# Copyright (c) 2016-2021, Cabral, Juan; Luczywo, Nadia
# Copyright (c) 2022, 2023, 2024 QuatroPe
# All rights reserved.

# =============================================================================
# DOCS
# =============================================================================

"""Utilities for linnear programming based on PuLP.

This file contains an abstraction class to manipulate in a more OOP way
the underlining PuLP model

"""


# =============================================================================
# IMPORTS
# =============================================================================

import numpy as np

import pulp

from .bunch import Bunch
from .doctools import doc_inherit


# =============================================================================
# UTILITIES
# =============================================================================


[docs] def is_solver_available(solver): """Return True if the solver is available.""" return solver is None or solver.upper() in ["PULP"] + pulp.listSolvers( onlyAvailable=True )
# ============================================================================= # VARIABLES # ============================================================================= class _Var(pulp.LpVariable): def __init__(self, name, low=None, up=None, *args, **kwargs): super(_Var, self).__init__( name=name, lowBound=low, upBound=up, cat=self.var_type, *args, **kwargs, )
[docs] class Float(_Var): """:class:`pulp.LpVariable` with :class:`pulp.LpContinuous` category. Example ------- This two codes are equivalent. .. code-block:: python x = pulp.LpVariable("x", cat=pulp.LpContinuous) # pure PuLP x = lp.Float("x") # skcriteria.utils.lp version """ var_type = pulp.LpContinuous
[docs] class Int(_Var): """:class:`pulp.LpVariable` with :class:`pulp.LpInteger` category. Example ------- This two codes are equivalent. .. code-block:: python x = pulp.LpVariable("x", cat=pulp.LpInteger) # pure PuLP x = lp.Int("x") # skcriteria.utils.lp version """ var_type = pulp.LpInteger
[docs] class Bool(_Var): """:class:`pulp.LpVariable` with :class:`pulp.LpBinary` category. Example ------- This two codes are equivalent. .. code-block:: python x = pulp.LpVariable("x", cat=pulp.LpBinary) # pure PuLP x = lp.Bool("x") # skcriteria.utils.lp version """ var_type = pulp.LpBinary
# ============================================================================= # PROBLEM ABSTRACT CLASS # ============================================================================= class _LPBase: """Creates a LP problem with a way better sintax than PuLP. Parameters ---------- z: :class:`LpAffineExpression` A linear combination of :class:`LpVariables<LpVariable>`. name: str (default="no-name") Name of the problem. solver: None, str or any :class:`pulp.LpSolver` instance (default=None) Solver of the problem. If it's None, the default solver is used. PULP is an alias os None. solver_kwds: dict Dictionary of keyword arguments for the solver. Example ------- .. code-block:: python # variable declaration x0 = lp.Float("x0", low=0) x1 = lp.Float("x1", low=0) x2 = lp.Float("x2", low=0) # model model = lp.Maximize( # or lp.Minimize z=250 * x0 + 130 * x1 + 350 * x2 ) # constraints model.subject_to( 120 * x0 + 200 * x1 + 340 * x2 <= 500, -20 * x0 + -40 * x1 + -15 * x2 <= -15, 800 * x0 + 1000 * x1 + 600 * x2 <= 1000, ) Also you can create the model and the constraints in one "line". .. code-block:: python model = lp.Maximize( # or lp.Minimize z=250 * x0 + 130 * x1 + 350 * x2, solver=solver ).subject_to( 120 * x0 + 200 * x1 + 340 * x2 <= 500, -20 * x0 + -40 * x1 + -15 * x2 <= -15, 800 * x0 + 1000 * x1 + 600 * x2 <= 1000, ) """ def __init__(self, z, name="no-name", solver=None, **solver_kwds): """Create an instance of problem solver.""" problem = pulp.LpProblem(name, self.sense) if solver is not None and solver.upper() != "PULP": # None == PULP if isinstance(solver, str): solver = pulp.getSolver(solver.upper(), **solver_kwds) problem.setSolver(solver) problem += z, "Z" self._problem = problem def __repr__(self): """model.__repr__() <==> repr(model).""" name = type(self).__name__ objective = self._problem.objective constraints = ",\n ".join( map(str, self._problem.constraints.values()) ) return f"{name}({objective}).subject_to(\n {constraints}\n)" @property def v(self): """Access to underlining variables.""" return Bunch("variables", self._problem.variablesDict()) def subject_to(self, *args): """Add a constraint to a underliying puLP problem. Parameters ---------- args: tuple Multiple :class:`LpAffineExpression` Returns ------- self: Return the same instance. """ for c in args: self._problem += c return self def solve(self): """Solve the underlying problem and create a report as a dict. The method copy the original problem, and then solve it. Returns ------- result: dict-like. Report of the problem as dict-like. """ problem = self._problem.copy() problem.solve() objective = pulp.value(problem.objective) variables, values = [], [] for v in problem.variables(): variables.append(v.name) values.append(v.varValue) status = pulp.LpStatus[problem.status] result_dict = { "lp_status_code": problem.status, "lp_status": status, "lp_objective": objective, "lp_variables": np.asarray(variables), "lp_values": np.asarray(values), "lp_problem": problem, } return Bunch(name="result", data=result_dict) # ============================================================================= # CONCRETE CLASS # =============================================================================
[docs] @doc_inherit(_LPBase, warn_class=False) class Minimize(_LPBase): """Creates a Minimize LP problem with a way better sintax than PuLP.""" sense = pulp.LpMinimize
[docs] @doc_inherit(_LPBase, warn_class=False) class Maximize(_LPBase): """Creates a Maximize LP problem with a way better sintax than PuLP.""" sense = pulp.LpMaximize