Source code for discrete_optimization.lotsizing.costs

#  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.
"""Cost structure mixins for lot sizing problems.

This module provides mixins for different cost components:
- Setup costs (fixed cost when production occurs)
- Production costs (variable cost per unit produced)
- Inventory costs (holding cost per unit in stock)
"""

from __future__ import annotations

from abc import abstractmethod
from typing import Generic

import numpy as np

from discrete_optimization.lotsizing.base import Item
from discrete_optimization.lotsizing.demands import DemandsProblem, DemandsSolution


[docs] class SetupCostsProblem(DemandsProblem[Item], Generic[Item]): """Mixin for problems with setup costs. Setup cost s_it is the fixed cost incurred when producing item i in period t. This cost is paid if Y_it = 1 (setup occurs), regardless of production quantity. """
[docs] @abstractmethod def get_setup_cost(self, item: Item, period: int) -> float: """Get setup cost for producing item i in period t. Args: item: Item identifier period: Time period Returns: Setup cost s_it (non-negative) """ ...
[docs] class SetupCostsSolution(DemandsSolution[Item], Generic[Item]): """Solution mixin for setup cost computation.""" problem: SetupCostsProblem[Item]
[docs] @abstractmethod def has_setup(self, item: Item, period: int) -> bool: """Check if setup occurs (Y_it = 1). Args: item: Item identifier period: Time period Returns: True if setup occurs, False otherwise """ ...
[docs] def compute_total_setup_cost(self) -> float: """Compute total setup cost across all items and periods. Returns: Sum of all setup costs s_it * Y_it """ total_cost = 0.0 for item in self.problem.items_list: for t in range(self.problem.horizon): if self.has_setup(item, t): total_cost += self.problem.get_setup_cost(item, t) return total_cost
[docs] class ProductionCostsProblem(DemandsProblem[Item], Generic[Item]): """Mixin for problems with variable production costs. Production cost v_it is the variable cost per unit of item i produced in period t. Total production cost = v_it * X_it where X_it is production quantity. """
[docs] @abstractmethod def get_production_cost_per_unit(self, item: Item, period: int) -> float: """Get variable production cost per unit. Args: item: Item identifier period: Time period Returns: Production cost v_it per unit (non-negative) """ ...
[docs] class ProductionCostsSolution(DemandsSolution[Item], Generic[Item]): """Solution mixin for production cost computation.""" problem: ProductionCostsProblem[Item]
[docs] def compute_total_production_cost(self) -> float: """Compute total variable production cost. Returns: Sum of v_it * X_it across all items and periods """ total_cost = 0.0 for item in self.problem.items_list: for t in range(self.problem.horizon): qty = self.get_production_quantity(item, t) if qty > 0: cost_per_unit = self.problem.get_production_cost_per_unit(item, t) total_cost += cost_per_unit * qty return total_cost
[docs] class InventoryCostsProblem(DemandsProblem[Item], Generic[Item]): """Mixin for problems with inventory holding costs. Inventory cost c_it is the cost per unit of item i held in stock at end of period t. Total inventory cost = c_it * I_it where I_it is inventory level. """
[docs] @abstractmethod def get_inventory_cost_per_unit(self, item: Item, period: int) -> float: """Get inventory holding cost per unit. Args: item: Item identifier period: Time period Returns: Inventory cost c_it per unit (non-negative) """ ...
[docs] class InventoryCostsSolution(DemandsSolution[Item], Generic[Item]): """Solution mixin for inventory cost computation.""" problem: InventoryCostsProblem[Item]
[docs] def compute_total_inventory_cost(self) -> float: """Compute total inventory holding cost. Returns: Sum of c_it * I_it across all items and periods """ total_cost = 0.0 for item in self.problem.items_list: for t in range(self.problem.horizon): inv = self.get_inventory_level(item, t) if inv > 0: cost_per_unit = self.problem.get_inventory_cost_per_unit(item, t) total_cost += cost_per_unit * inv return total_cost
# Concrete array-based implementations
[docs] class CostsArrayProblem( SetupCostsProblem[Item], ProductionCostsProblem[Item], InventoryCostsProblem[Item], Generic[Item], ): """Concrete implementation using numpy arrays for all cost components. This helper mixin stores costs as 2D arrays for efficient access. Can be used as: class MyProblem(CostsArrayProblem[int], OtherMixins...): def __init__(self, setup_costs, production_costs, inventory_costs, ...): CostsArrayProblem.__init__( self, setup_costs, production_costs, inventory_costs ) ... """ def __init__( self, setup_costs: np.ndarray | list[list[float]], production_costs: np.ndarray | list[list[float]], inventory_costs: np.ndarray | list[list[float]], ): """Initialize with cost arrays. Args: setup_costs: 2D array of setup_costs[item_idx][period] production_costs: 2D array of production_costs[item_idx][period] inventory_costs: 2D array of inventory_costs[item_idx][period] """ self._setup_costs = ( np.array(setup_costs, dtype=np.float64) if not isinstance(setup_costs, np.ndarray) else setup_costs ) self._production_costs = ( np.array(production_costs, dtype=np.float64) if not isinstance(production_costs, np.ndarray) else production_costs ) self._inventory_costs = ( np.array(inventory_costs, dtype=np.float64) if not isinstance(inventory_costs, np.ndarray) else inventory_costs )
[docs] def get_setup_cost(self, item: Item, period: int) -> float: """Get setup cost from array storage.""" idx = self.get_index_from_item(item) return float(self._setup_costs[idx, period])
[docs] def get_production_cost_per_unit(self, item: Item, period: int) -> float: """Get production cost per unit from array storage.""" idx = self.get_index_from_item(item) return float(self._production_costs[idx, period])
[docs] def get_inventory_cost_per_unit(self, item: Item, period: int) -> float: """Get inventory cost per unit from array storage.""" idx = self.get_index_from_item(item) return float(self._inventory_costs[idx, period])
[docs] class SingleItemCostsArrayProblem( SetupCostsProblem[int], ProductionCostsProblem[int], InventoryCostsProblem[int], ): """Concrete implementation for single-item problems with 1D cost arrays. Convenience class for single-item problems where costs are stored as 1D arrays. The items_list is fixed to [0]. """ def __init__( self, setup_costs: np.ndarray | list[float], production_costs: np.ndarray | list[float], inventory_costs: np.ndarray | list[float], ): """Initialize with 1D cost arrays. Args: setup_costs: 1D array of setup_costs[period] production_costs: 1D array of production_costs[period] inventory_costs: 1D array of inventory_costs[period] """ self._setup_costs = ( np.array(setup_costs, dtype=np.float64) if not isinstance(setup_costs, np.ndarray) else setup_costs ) self._production_costs = ( np.array(production_costs, dtype=np.float64) if not isinstance(production_costs, np.ndarray) else production_costs ) self._inventory_costs = ( np.array(inventory_costs, dtype=np.float64) if not isinstance(inventory_costs, np.ndarray) else inventory_costs ) @property def items_list(self) -> list[int]: """Single item with index 0.""" return [0] @property def horizon(self) -> int: """Horizon is length of cost arrays.""" return len(self._setup_costs)
[docs] def get_setup_cost(self, item: int, period: int) -> float: """Get setup cost from 1D array.""" if item != 0: raise ValueError(f"Single item problem only has item 0, got {item}") return float(self._setup_costs[period])
[docs] def get_production_cost_per_unit(self, item: int, period: int) -> float: """Get production cost per unit from 1D array.""" if item != 0: raise ValueError(f"Single item problem only has item 0, got {item}") return float(self._production_costs[period])
[docs] def get_inventory_cost_per_unit(self, item: int, period: int) -> float: """Get inventory cost per unit from 1D array.""" if item != 0: raise ValueError(f"Single item problem only has item 0, got {item}") return float(self._inventory_costs[period])
# "Without" variants for problems without specific cost components
[docs] class WithoutSetupCostsProblem(SetupCostsProblem[Item], Generic[Item]): """Mixin for problems without setup costs. Use this when there is no fixed cost to start production. All setup costs return 0. """
[docs] def get_setup_cost(self, item: Item, period: int) -> float: """No setup costs - always returns 0.""" return 0.0
[docs] class WithoutSetupCostsSolution(SetupCostsSolution[Item], Generic[Item]): """Solution mixin for problems without setup costs.""" problem: WithoutSetupCostsProblem[Item]
[docs] def compute_total_setup_cost(self) -> float: """No setup costs - always returns 0.""" return 0.0
[docs] class WithoutProductionCostsProblem(ProductionCostsProblem[Item], Generic[Item]): """Mixin for problems without per-unit production costs. Use this when production is limited only by capacity, not by per-unit costs. All production costs return 0. """
[docs] def get_production_cost_per_unit(self, item: Item, period: int) -> float: """No production costs - always returns 0.""" return 0.0
[docs] class WithoutProductionCostsSolution(ProductionCostsSolution[Item], Generic[Item]): """Solution mixin for problems without production costs.""" problem: WithoutProductionCostsProblem[Item]
[docs] def compute_total_production_cost(self) -> float: """No production costs - always returns 0.""" return 0.0
[docs] class WithoutInventoryCostsProblem(InventoryCostsProblem[Item], Generic[Item]): """Mixin for problems without inventory holding costs. Use this when inventory can be held without cost (rare in practice). All inventory costs return 0. """
[docs] def get_inventory_cost_per_unit(self, item: Item, period: int) -> float: """No inventory costs - always returns 0.""" return 0.0
[docs] class WithoutInventoryCostsSolution(InventoryCostsSolution[Item], Generic[Item]): """Solution mixin for problems without inventory costs.""" problem: WithoutInventoryCostsProblem[Item]
[docs] def compute_total_inventory_cost(self) -> float: """No inventory costs - always returns 0.""" return 0.0