# 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.
"""Parser for capacitated multi-item lot sizing problem instances."""
import os
from typing import Optional
from discrete_optimization.datasets import ERROR_MSG_MISSING_DATASETS, get_data_home
from discrete_optimization.lotsizing.capacitatedmultiitem.problem import (
CapacitatedMultiItemLSP,
)
try:
import pymzn
except ImportError:
pymzn = None
[docs]
def get_data_available(
data_folder: Optional[str] = None, data_home: Optional[str] = None
) -> list[str]:
"""Get datasets available for lot sizing.
Args:
data_folder: folder where datasets for lot sizing should be found.
If None, we look in "lotsizing" subdirectory of `data_home`.
data_home: root directory for all datasets. If None, set by
default to "~/discrete_optimization_data"
Returns:
List of available instance file paths
"""
if data_folder is None:
data_home = get_data_home(data_home=data_home)
data_folder = f"{data_home}/lotsizing"
try:
instances_files = []
folders = os.listdir(data_folder)
for f in folders:
folder_path = os.path.join(data_folder, f)
if os.path.isdir(folder_path):
instances_files += [
os.path.join(folder_path, f)
for f in os.listdir(folder_path)
if "psp" in f or "dzn" in f
]
except FileNotFoundError as e:
raise FileNotFoundError(str(e) + ERROR_MSG_MISSING_DATASETS)
return instances_files
[docs]
def parse_dzn_file(file_path: str) -> CapacitatedMultiItemLSP:
"""Parse lot sizing problem from .dzn MiniZinc data file.
Expected format:
Periods = <int>;
Items = <int>;
Demands = [|...|]; % Items x Periods matrix
StockingCosts = [<int>, ...]; % per item
SetupCosts = [|...|]; % Items x Items matrix (changeover costs)
Args:
file_path: Path to .dzn file
Returns:
CapacitatedMultiItemLSP instance
"""
if pymzn is None:
raise ImportError(
"pymzn is required to parse .dzn files. Install it with: pip install pymzn"
)
# Parse the .dzn file
data = pymzn.dzn2dict(file_path)
# Extract data
nb_periods = data["Periods"]
nb_items = data["Items"]
# pymzn.dzn2dict flattens 2D arrays, so we need to reshape them
# Demands is stored as a flat list (Items x Periods) in row-major order
demands_flat = data["Demands"]
demands = [
demands_flat[i * nb_periods : (i + 1) * nb_periods] for i in range(nb_items)
]
# StockingCosts is a list per item
stocking_costs = [float(x) for x in data["StockingCosts"]]
# SetupCosts is stored as a flat list (Items x Items) in row-major order
setup_flat = data["SetupCosts"]
changeover_costs = [
setup_flat[i * nb_items : (i + 1) * nb_items] for i in range(nb_items)
]
# Create the problem
capacity_machine = 1
# High penalty for delays
delay_cost_per_type = [100000.0] * nb_items
problem = CapacitatedMultiItemLSP(
nb_items=nb_items,
horizon=nb_periods,
demands=demands,
capacity_machine=capacity_machine,
changeover_costs=changeover_costs,
stock_cost_per_type=stocking_costs,
stock_capacity=None, # Will be set to sum of demands
allow_delays=True, # Soft constraint
delay_cost_per_type=delay_cost_per_type, # But high penalty in objective
)
return problem
[docs]
def parse_file(file_path: str) -> CapacitatedMultiItemLSP:
"""Parse lot sizing problem from file.
Automatically detects file format based on extension:
- .txt: plain text format
- .dzn: MiniZinc data format
Args:
file_path: Path to problem file
Returns:
CapacitatedMultiItemLSP instance
"""
_, ext = os.path.splitext(file_path)
if ext.lower() == ".dzn":
return parse_dzn_file(file_path)
else:
# Default to txt format
with open(file_path, "r", encoding="utf-8") as f:
input_data = f.read()
return parse_input_data(input_data)