# 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.
"""Base classes for lot sizing problems.
This module provides minimal base classes following the generic_tasks_tools pattern.
Each problem variant is composed of mixins that add specific features.
"""
from __future__ import annotations
from abc import abstractmethod
from collections.abc import Hashable
from typing import Generic, Optional, TypeVar
from discrete_optimization.generic_tools.do_problem import Problem, Solution
# Generic type for items/products
# Usually int, but could be string, enum, or any hashable type
Item = TypeVar("Item", bound=Hashable)
[docs]
class LotSizingProblem(Problem, Generic[Item]):
"""Minimal base class for all lot sizing problems.
This class only defines the essential structure common to ALL lot sizing variants:
- Time horizon (number of periods)
- Items/products to produce
All other features (demands, costs, capacity, etc.) are added via mixins.
Similar to TasksProblem in generic_tasks_tools.
"""
_map_item_to_index: Optional[dict[Item, int]] = None
@property
@abstractmethod
def horizon(self) -> int:
"""Number of time periods T.
Periods are indexed from 0 to horizon-1.
"""
...
@property
@abstractmethod
def items_list(self) -> list[Item]:
"""List of all items (product types) to schedule production for.
Returns:
List of unique item identifiers
"""
...
@property
def nb_items(self) -> int:
"""Number of different items/products."""
return len(self.items_list)
[docs]
def get_index_from_item(self, item: Item) -> int:
"""Get index of item in items_list.
This is cached for efficiency when items_list doesn't change.
Args:
item: Item identifier
Returns:
Index in items_list (0 to nb_items-1)
"""
if self._map_item_to_index is None:
self._map_item_to_index = {
item: i for i, item in enumerate(self.items_list)
}
return self._map_item_to_index[item]
[docs]
def get_item_from_index(self, i: int) -> Item:
"""Get item from index.
Args:
i: Index in items_list
Returns:
Item identifier
"""
return self.items_list[i]
[docs]
def update_items_list(self) -> None:
"""Call when items_list is updated to reset cache."""
self._map_item_to_index = None
[docs]
class LotSizingSolution(Solution, Generic[Item]):
"""Minimal base class for lot sizing solutions.
This is the base for all solution types. Specific solution representations
are added by mixins and concrete implementations.
Similar to TasksSolution in generic_tasks_tools.
"""
problem: LotSizingProblem[Item]
[docs]
@abstractmethod
def get_inventory_level(self, item: Item, period: int):
"""Get inventory level at end of period.
Args:
item: Item identifier
period: Time period
Returns:
Inventory level I_it (non-negative integer)
"""
...
[docs]
@abstractmethod
def get_delivery_quantity(self, item: Item, period: int) -> int:
"""Get quantity of item delivered to satisfy demand in period.
This may differ from production quantity due to inventory.
Args:
item: Item identifier
period: Time period
Returns:
Delivery quantity (amount used to satisfy demand in this period)
"""
...
[docs]
def get_total_production_quantity(self, item: Item) -> int:
return sum(
[
self.get_production_quantity(item=item, period=t)
for t in range(self.problem.horizon)
]
)
[docs]
def get_total_delivery_quantity(self, item: Item) -> int:
return sum(
[
self.get_delivery_quantity(item=item, period=t)
for t in range(self.problem.horizon)
]
)
[docs]
@abstractmethod
def get_production_quantity(self, item: Item, period: int) -> int:
"""Get production quantity X_it.
Args:
item: Item identifier
period: Time period
Returns:
Production quantity (non-negative integer)
"""
...