Source code for discrete_optimization.lotsizing.uncapacitatedsingleitem.solvers.lp

#  Copyright (c) 2026 AIRBUS and its affiliates.
#  This source code is licensed under the MIT license found in the
#  LICENSE file in the root directory of this source tree.
import logging
from typing import Any, Callable

try:
    import gurobipy
except ImportError:
    gurobi_available = False
else:
    gurobi_available = True

from discrete_optimization.generic_tools.lp_tools import (
    GurobiMilpSolver,
    InequalitySense,
    MilpSolver,
    OrtoolsMathOptMilpSolver,
)
from discrete_optimization.lotsizing.uncapacitatedsingleitem.problem import (
    UncapacitatedSingleItemLSP,
    UncapacitatedSingleItemSolution,
)

logger = logging.getLogger(__name__)


class _BaseLpUncapacitatedSingleItemSolver(MilpSolver):
    problem: UncapacitatedSingleItemLSP
    variables: dict

    def init_model(self, **kwargs: Any) -> None:
        super().init_model(**kwargs)
        self.model = self.create_empty_model("uncapacitated_single_item")
        self.create_vars()
        self._set_objective()

    def create_vars(self):
        self.variables = {}
        bool_production_per_time = {
            t: self.add_binary_variable(name=f"is_producing_per_time_{t}")
            for t in range(self.problem.horizon)
        }
        production_per_time = {
            t: self.add_integer_variable(
                lb=0,
                ub=self.problem.get_total_demand(self.problem.items_list[0]),
                name=f"production_per_time_{t}",
            )
            for t in range(self.problem.horizon)
        }
        inventory_per_time = {
            t: self.add_integer_variable(
                lb=0,
                ub=self.problem.get_total_demand(self.problem.items_list[0]),
                name=f"stock_{t}",
            )
            for t in range(self.problem.horizon)
        }
        for t in range(self.problem.horizon):
            self.add_linear_constraint_with_indicator(
                binvar=bool_production_per_time[t],
                binval=1,
                lhs=production_per_time[t],
                sense=InequalitySense.GREATER_OR_EQUAL,
                rhs=1,
                penalty_coeff=self.problem.get_total_demand(self.problem.items_list[0]),
            )
            self.add_linear_constraint_with_indicator(
                binvar=bool_production_per_time[t],
                binval=0,
                lhs=production_per_time[t],
                sense=InequalitySense.EQUAL,
                rhs=0,
                penalty_coeff=self.problem.get_total_demand(self.problem.items_list[0]),
            )
        for t in range(self.problem.horizon):
            if t == 0:
                self.add_linear_constraint(
                    inventory_per_time[t]
                    == production_per_time[t]
                    - self.problem.get_demand(self.problem.items_list[0], t)
                )
            else:
                self.add_linear_constraint(
                    inventory_per_time[t]
                    == inventory_per_time[t - 1]
                    + production_per_time[t]
                    - self.problem.get_demand(self.problem.items_list[0], t)
                )
        self.variables["inventory"] = inventory_per_time
        self.variables["production_per_time"] = production_per_time
        self.variables["bool_production_per_time"] = bool_production_per_time

    def _set_objective(self):
        """Set the objective function."""
        self.variables["objectives"] = {}
        horizon = self.problem.horizon
        item = 0
        objectives = []
        for obj, weight in zip(
            self.params_objective_function.objectives,
            self.params_objective_function.weights,
        ):
            if obj == "setup_cost":
                # Setup cost: sum_t (s_t * Y_t)
                setup_cost_terms = []
                for t in range(horizon):
                    cost = int(self.problem.get_setup_cost(item, t))
                    if cost > 0:
                        setup_cost_terms.append(
                            cost * self.variables["bool_production_per_time"][t]
                        )
                self.variables["objectives"]["setup_cost"] = self.construct_linear_sum(
                    setup_cost_terms
                )
                objectives.append(weight * self.variables["objectives"]["setup_cost"])

            if obj == "production_cost":
                # Production cost: sum_t (v_t * X_t)
                production_cost_terms = []
                for t in range(horizon):
                    cost_per_unit = int(
                        self.problem.get_production_cost_per_unit(item, t)
                    )
                    if cost_per_unit > 0:
                        production_cost_terms.append(
                            cost_per_unit * self.variables["production_per_time"][t]
                        )
                self.variables["objectives"]["production_cost"] = (
                    self.construct_linear_sum(production_cost_terms)
                )
                objectives.append(
                    weight * self.variables["objectives"]["production_cost"]
                )

            if obj == "inventory_cost":
                # Inventory cost: sum_t (c_t * I_t)
                inventory_cost_terms = []
                for t in range(horizon):
                    cost_per_unit = int(
                        self.problem.get_inventory_cost_per_unit(item, t)
                    )
                    if cost_per_unit > 0:
                        inventory_cost_terms.append(
                            cost_per_unit * self.variables["inventory"][t]
                        )
                self.variables["objectives"]["inventory_cost"] = (
                    self.construct_linear_sum(inventory_cost_terms)
                )
                objectives.append(
                    weight * self.variables["objectives"]["inventory_cost"]
                )
        # Minimize total cost
        if objectives:
            self.set_model_objective(
                self.construct_linear_sum(objectives), minimize=True
            )

    def retrieve_current_solution(
        self,
        get_var_value_for_current_solution: Callable[[Any], float],
        get_obj_value_for_current_solution: Callable[[], float],
    ) -> UncapacitatedSingleItemSolution:
        production_periods = []
        production_quantity = []
        for t in self.variables["production_per_time"]:
            val = get_var_value_for_current_solution(
                self.variables["production_per_time"][t]
            )
            if val > 0:
                production_periods.append(t)
                production_quantity.append(val)
        for t in self.variables["inventory"]:
            logger.info(
                "Inventory %s: %s",
                t,
                get_var_value_for_current_solution(self.variables["inventory"][t]),
            )
        return UncapacitatedSingleItemSolution(
            problem=self.problem,
            production_periods=production_periods,
            production_quantities=production_quantity,
        )


[docs] class MathoptUncapacitatedSingleItemSolver(
_BaseLpUncapacitatedSingleItemSolver, OrtoolsMathOptMilpSolver ): ...
[docs] class GurobiUncapacitatedSingleItemSolver( _BaseLpUncapacitatedSingleItemSolver, GurobiMilpSolver ):
[docs] def convert_to_variable_values( self, solution: UncapacitatedSingleItemSolution ) -> dict["gurobipy.Var", float]: dict_variable = {} for t in range(self.problem.horizon): prod_t = solution.get_production_quantity( item=self.problem.items_list[0], period=t ) dict_variable[self.variables["production_per_time"][t]] = prod_t dict_variable[self.variables["bool_production_per_time"][t]] = ( 1 if prod_t > 0 else 0 ) inventory_t = solution.get_inventory_level( item=self.problem.items_list[0], period=t ) dict_variable[self.variables["inventory"][t]] = int(inventory_t) return dict_variable