Skip to content

PuLPEngine class

Bases: Engine

Concrete engine implementation using PuLP.

This class provides a PuLP-based implementation of the abstract Engine interface for formulating and solving linear and integer optimization models.

Source code in pyorlib/engines/pulp/pulp_engine.py
class PuLPEngine(Engine):
    """
    Concrete engine implementation using PuLP.

    This class provides a PuLP-based implementation of the abstract Engine interface for formulating
    and solving linear and integer optimization models.
    """

    class _Variable(Variable):
        """
        Represents a PuLP variable in an optimization model.

        The `PuLPVariable` class is a concrete implementation of the abstract `Variable` class.
        It represents a variable that is compatible with the PuLP solver.
        """

        # Strict class attributes.
        __slots__ = ["_pulp_var"]

        @property
        def name(self) -> str:
            return str(self._pulp_var.name)

        @property
        def lower_bound(self) -> float:
            lb = self._pulp_var.lowBound
            return float(lb) if lb is not None else -inf

        @property
        def upper_bound(self) -> float:
            ub = self._pulp_var.upBound
            return float(ub) if ub is not None else inf

        @property
        def value(self) -> float:
            val = self._pulp_var.value()
            return float(val) if val else -0.0

        @property
        def raw(self) -> Any:
            return self._pulp_var

        def __init__(
            self,
            name: str,
            solver: LpProblem,
            value_type: ValueType,
            lower_bound: float = 0,
            upper_bound: float = inf,
        ):
            """
            Initializes a new `PuLPVariable` object with the specified attributes and creates a corresponding PuLP
            variable in the PuLP solver.
            :param name: The name of the variable.
            :param solver: A reference to the PuLP solver.
            :param value_type: An enumeration representing the type of the variable's value.
            :param lower_bound: The lower bound of the variable. Default is 0.
            :param upper_bound: The upper bound of the variable. Default is infinity.
            """
            # Calls the super init method and its validations
            super().__init__(name=name, value_type=value_type, lower_bound=lower_bound, upper_bound=upper_bound)

            # Applies new validations
            if solver is None:
                raise PuLPException("The 'solver' argument cannot be None.")

            # Creates the PuLP variable according to the value type
            pulp_var: LpVariable | None

            if self.value_type == ValueType.BINARY:
                pulp_var = LpVariable(name=name, cat=LpBinary, lowBound=0, upBound=1)
            elif self.value_type == ValueType.INTEGER:
                pulp_var = LpVariable(
                    name=name,
                    cat=LpInteger,
                    lowBound=lower_bound,
                    upBound=upper_bound if upper_bound < inf else None,
                )
            elif self.value_type == ValueType.CONTINUOUS:
                pulp_var = LpVariable(
                    name=name,
                    cat=LpContinuous,
                    lowBound=lower_bound,
                    upBound=upper_bound if upper_bound < inf else None,
                )
            else:
                raise PuLPException("Unknown ValueType.")

            # Applies new validations
            if pulp_var is None:  # pragma: no cover
                raise PuLPException("Failed to create the PuLP variable.")

            # Instance attributes
            self._pulp_var: LpVariable = pulp_var
            """ A LpVariable object representing the variable in the PuLP solver. """

    @property
    def name(self) -> str:  # pragma: no cover
        return "PuLP Engine"

    @property
    def constraints(self) -> List[Element]:
        return [Expression(expression=constraint) for constraint in self._solver.constraints.values()]

    @property
    def objective_value(self) -> float | None:
        if self.solution_status in [SolutionStatus.OPTIMAL, SolutionStatus.FEASIBLE]:
            return float(value(self._solver.objective))
        return None

    @property
    def objective_expr(self) -> Element | None:
        return Expression(expression=self._objective) if self._objective is not None else None

    @property
    def solution_status(self) -> SolutionStatus:  # pragma: no cover
        if self._status == 0:
            return SolutionStatus.NOT_SOLVED
        elif self._status == 1:
            return SolutionStatus.OPTIMAL
        elif self._status == -1:
            return SolutionStatus.INFEASIBLE
        elif self._status in [-2, -3]:
            return SolutionStatus.ERROR
        else:
            StdOutLogger.error(action="Solution status: ", msg=f"{self._status}")
            raise PuLPException("Unhandled PuLP status code.")

    def __init__(self, solver: LpProblem | None = None):
        """
        Initializes the PuLPEngine instance.

        The solver parameter enables the user to pass a pre-configured PuLP solver with custom parameters
        instead of using the default solver. This allows greater flexibility in specifying solver options.
        :param solver: A pre-configured PuLP LpProblem
            object to use as the solver. This allows custom configuration
            of the solver before passing to the engine. If None, a default
            solver will be instantiated with default settings.
            Defaults to None.
        """

        # Instance attributes
        self._solver: LpProblem = solver if solver else LpProblem()
        """ A reference to the PuLP solver. """

        if self._solver is None or not isinstance(self._solver, LpProblem):
            raise PuLPException("The PuLP solver cannot be None.")

        self._objective: Any = None
        """ An object representing the optimization function of the problem. """

        self._status: int = 0
        """ Represents the state of the solution. """

    def add_variable(
        self,
        name: str,
        value_type: ValueType,
        lower_bound: float = 0,
        upper_bound: float = inf,
    ) -> Variable:
        return PuLPEngine._Variable(
            name=name,
            solver=self._solver,
            value_type=value_type,
            lower_bound=lower_bound,
            upper_bound=upper_bound,
        )

    def add_constraint(self, expression: Element) -> Element:
        self._solver += expression.raw
        return expression

    def set_objective(self, opt_type: OptimizationType, expression: Element) -> Element:
        if opt_type == OptimizationType.MINIMIZE:
            self._solver.sense = LpMinimize
        elif opt_type == OptimizationType.MAXIMIZE:
            self._solver.sense = LpMaximize
        else:
            raise PuLPException("Optimization type not supported.")
        self._solver.setObjective(expression.raw)
        self._objective = expression.raw
        return expression

    def solve(self) -> None:
        solve_param = LpSolverDefault.msg = False
        self._status = self._solver.solve(solve_param)

Attributes

name property

name: str

constraints property

constraints: List[Element]

objective_value property

objective_value: float | None

objective_expr property

objective_expr: Element | None

solution_status property

solution_status: SolutionStatus

Functions

__init__

__init__(solver: LpProblem | None = None)

Initializes the PuLPEngine instance.

The solver parameter enables the user to pass a pre-configured PuLP solver with custom parameters instead of using the default solver. This allows greater flexibility in specifying solver options.

PARAMETER DESCRIPTION
solver

A pre-configured PuLP LpProblem object to use as the solver. This allows custom configuration of the solver before passing to the engine. If None, a default solver will be instantiated with default settings. Defaults to None.

TYPE: LpProblem | None DEFAULT: None

Source code in pyorlib/engines/pulp/pulp_engine.py
def __init__(self, solver: LpProblem | None = None):
    """
    Initializes the PuLPEngine instance.

    The solver parameter enables the user to pass a pre-configured PuLP solver with custom parameters
    instead of using the default solver. This allows greater flexibility in specifying solver options.
    :param solver: A pre-configured PuLP LpProblem
        object to use as the solver. This allows custom configuration
        of the solver before passing to the engine. If None, a default
        solver will be instantiated with default settings.
        Defaults to None.
    """

    # Instance attributes
    self._solver: LpProblem = solver if solver else LpProblem()
    """ A reference to the PuLP solver. """

    if self._solver is None or not isinstance(self._solver, LpProblem):
        raise PuLPException("The PuLP solver cannot be None.")

    self._objective: Any = None
    """ An object representing the optimization function of the problem. """

    self._status: int = 0
    """ Represents the state of the solution. """

add_variable

add_variable(name: str, value_type: ValueType, lower_bound: float = 0, upper_bound: float = inf) -> Variable
Source code in pyorlib/engines/pulp/pulp_engine.py
def add_variable(
    self,
    name: str,
    value_type: ValueType,
    lower_bound: float = 0,
    upper_bound: float = inf,
) -> Variable:
    return PuLPEngine._Variable(
        name=name,
        solver=self._solver,
        value_type=value_type,
        lower_bound=lower_bound,
        upper_bound=upper_bound,
    )

add_constraint

add_constraint(expression: Element) -> Element
Source code in pyorlib/engines/pulp/pulp_engine.py
def add_constraint(self, expression: Element) -> Element:
    self._solver += expression.raw
    return expression

set_objective

set_objective(opt_type: OptimizationType, expression: Element) -> Element
Source code in pyorlib/engines/pulp/pulp_engine.py
def set_objective(self, opt_type: OptimizationType, expression: Element) -> Element:
    if opt_type == OptimizationType.MINIMIZE:
        self._solver.sense = LpMinimize
    elif opt_type == OptimizationType.MAXIMIZE:
        self._solver.sense = LpMaximize
    else:
        raise PuLPException("Optimization type not supported.")
    self._solver.setObjective(expression.raw)
    self._objective = expression.raw
    return expression

solve

solve() -> None
Source code in pyorlib/engines/pulp/pulp_engine.py
def solve(self) -> None:
    solve_param = LpSolverDefault.msg = False
    self._status = self._solver.solve(solve_param)