Production Planning Example

Overview

This example demonstrates LumiX’s single-model indexing feature, which allows variables and constraints to be indexed directly by data model instances rather than manual integer indices.

The production planning problem is a fundamental optimization problem in operations research, making it an ideal introduction to data-driven modeling with LumiX.

Problem Description

A manufacturing company produces multiple products, each requiring different amounts of limited resources (labor hours, machine hours, raw materials).

Objective: Maximize total profit from production.

Constraints:

  • Resource capacity limits (can’t exceed available labor, machine time, materials)

  • Minimum production requirements (must meet customer orders)

  • Non-negativity (can’t produce negative quantities)

Mathematical Formulation

Decision Variables:

\[x_p \in \mathbb{R}_+, \quad \forall p \in \text{Products}\]

where \(x_p\) represents the production quantity for product \(p\).

Objective Function:

\[\text{Maximize} \quad \sum_{p \in \text{Products}} \text{profit}_p \cdot x_p\]

where \(\text{profit}_p = \text{selling\_price}_p - \text{unit\_cost}_p\).

Constraints:

  1. Resource Capacity:

    \[\sum_{p \in \text{Products}} \text{usage}_{p,r} \cdot x_p \leq \text{capacity}_r, \quad \forall r \in \text{Resources}\]
  2. Minimum Production:

    \[x_p \geq \text{min\_production}_p, \quad \forall p \in \text{Products}\]

Key Features

Single-Model Indexing

Variables are indexed directly by Product instances:

        - Production planning and scheduling
        - Resource allocation problems
        - Portfolio optimization
        - Supply chain optimization
        - Any problem with homogeneous decision variables indexed by entities

Learning Objectives:

Key Points:

  • LXVariable[Product, float] creates a variable family

  • .indexed_by(lambda p: p.id) specifies the index key

  • .from_data(PRODUCTS) auto-creates one variable per product

  • No manual loops or index management needed

Data-Driven Coefficients

Coefficients are extracted from data using lambda functions:

See Also:
    - Example 02 (driver_scheduling): Multi-model indexing with cartesian products
    - Example 04 (basic_lp): Simpler introduction to LumiX basics
    - User Guide: Single-Model Indexing section
"""

The lambda p: p.selling_price - p.unit_cost extracts profit per unit directly from each Product instance.

Automatic Expression Expansion

Expressions automatically sum over all indexed data:



def build_production_model() -> LXModel:
    """Build the production planning optimization model.

    This function constructs a linear programming model to maximize profit
    from production while respecting resource capacity constraints and minimum

The expression sums resource usage across all products automatically.

Constraint Families

Similar constraints can be created as families indexed by data:

Example:
    >>> model = build_production_model()
    >>> print(model.summary())
    >>> optimizer = LXOptimizer().use_solver("ortools")
    >>> solution = optimizer.solve(model)

Notes:
    The data-driven approach means all coefficients are extracted from
    the PRODUCTS and RESOURCES data using lambda functions, making the

This creates one minimum production constraint per product.

Type-Safe Solution Access

Solutions are accessed using the same indices as the original data:

for product in PRODUCTS:
    qty = solution.variables["production"][product.id]
    profit = (product.selling_price - product.unit_cost) * qty

Running the Example

Prerequisites:

pip install lumix[cplex]  # or ortools, gurobi, glpk

Run:

cd examples/01_production_planning
python production_planning.py

Expected Output:

============================================================
OptiXNG Example: Production Planning
============================================================

Status: optimal
Optimal Profit: $3,465.00

Production Plan:
------------------------------------------------------------
  Widget A       :   10.0 units  (profit: $500.00)
  Widget B       :    8.6 units  (profit: $600.28)
  Gadget X       :    8.0 units  (profit: $520.00)
  Gadget Y       :   23.7 units  (profit: $1,186.50)
  Premium Z      :    3.0 units  (profit: $300.00)

Complete Code Walkthrough

Step 1: Define Data Models

@dataclass
class Product:
    """Represents a product that can be manufactured.

    This class models a product with its economic and resource consumption
    characteristics. Each product has a profit margin (selling_price - unit_cost)
    and requires various resources for production.

    Attributes:
        id: Unique identifier for the product.
        name: Human-readable product name.
        selling_price: Revenue per unit sold, in dollars.
        unit_cost: Total production cost per unit (materials + labor), in dollars.
        labor_hours: Labor time required per unit, in hours.
        machine_hours: Machine time required per unit, in hours.
        material_units: Raw material quantity required per unit.
        min_production: Minimum production quantity to meet customer orders.

    Example:
        >>> widget = Product(
        ...     id=1, name="Widget A", selling_price=100.0,
        ...     unit_cost=50.0, labor_hours=5.0, machine_hours=3.0,
        ...     material_units=2.0, min_production=10
        ... )
        >>> profit_margin = widget.selling_price - widget.unit_cost
        >>> print(f"Profit: ${profit_margin}")
        Profit: $50.0
    """

    id: int
    name: str
    selling_price: float  # $ per unit
    unit_cost: float  # $ per unit (materials + labor)
    labor_hours: float  # hours per unit
    machine_hours: float  # hours per unit
    material_units: float  # units of raw material per product unit
    min_production: int  # minimum units to produce (customer orders)

Step 2: Create Variables

        - Production planning and scheduling
        - Resource allocation problems
        - Portfolio optimization
        - Supply chain optimization
        - Any problem with homogeneous decision variables indexed by entities

Learning Objectives:

Step 3: Set Objective


See Also:
    - Example 02 (driver_scheduling): Multi-model indexing with cartesian products
    - Example 04 (basic_lp): Simpler introduction to LumiX basics
    - User Guide: Single-Model Indexing section
"""

from lumix import LXConstraint, LXLinearExpression, LXModel, LXOptimizer, LXSolution, LXVariable

Step 4: Add Constraints

Resource Capacity:


solver_to_use = "ortools"

# ==================== MODEL BUILDING ====================


def build_production_model() -> LXModel:
    """Build the production planning optimization model.

    This function constructs a linear programming model to maximize profit
    from production while respecting resource capacity constraints and minimum
    production requirements.

    The model uses single-model indexing where variables are indexed directly
    by Product instances, eliminating the need for manual index management.

    Returns:
        An LXModel instance containing:

Minimum Production:

    Example:
        >>> model = build_production_model()
        >>> print(model.summary())
        >>> optimizer = LXOptimizer().use_solver("ortools")
        >>> solution = optimizer.solve(model)

    Notes:
        The data-driven approach means all coefficients are extracted from
        the PRODUCTS and RESOURCES data using lambda functions, making the
        model automatically adapt to changes in the data.

Step 5: Solve and Access Solution

optimizer = LXOptimizer().use_solver("cplex")
solution = optimizer.solve(model)

if solution.is_optimal():
    for product in PRODUCTS:
        qty = solution.variables["production"][product.id]

Learning Objectives

After completing this example, you should understand:

  1. Variable Families: How one LXVariable expands to multiple solver variables

  2. Indexing: How .indexed_by() and .from_data() work together

  3. Lambda Coefficients: How to extract coefficients from data instances

  4. Automatic Summation: How expressions sum over all indexed instances

  5. Constraint Families: How to create multiple similar constraints

  6. Solution Mapping: How to access results using original indices

Common Patterns

Pattern 1: Variable Family from Data

variable = (
    LXVariable[DataModel, float]("var_name")
    .continuous()  # or .integer() or .binary()
    .bounds(lower=0, upper=100)
    .indexed_by(lambda instance: instance.id)
    .from_data(data_list)
)

Pattern 2: Expression with Lambda Coefficients

expr = (
    LXLinearExpression()
    .add_term(variable, coeff=lambda instance: instance.attribute)
)

Pattern 3: Constraint Family

model.add_constraint(
    LXConstraint[DataModel]("constraint_name")
    .expression(expr)
    .ge()  # or .le() or .eq()
    .rhs(lambda instance: instance.threshold)
    .from_data(data_list)
    .indexed_by(lambda instance: instance.key)
)

Extending the Example

Try These Modifications

  1. Add New Resource: Introduce a storage capacity constraint

  2. Maximum Production: Add upper bounds on production quantities

  3. Product Groups: Group products and add category-level constraints

  4. Time Periods: Extend to multi-period production planning

  5. Inventory: Add inventory variables and balance constraints

Next Steps

After mastering this example:

  1. Example 02 (Driver Scheduling): Learn multi-model indexing with cartesian products

  2. Example 03 (Facility Location): Binary variables and fixed costs

  3. Example 04 (Basic LP): Even simpler introduction if needed

  4. User Guide - Indexing: Deep dive into single and multi-model indexing

See Also

Related Examples:

API Reference:

User Guide:

Files in This Example

  • production_planning.py - Main optimization model and solution display

  • sample_data.py - Data models (Product, Resource) and sample data

  • README.md - Detailed documentation and usage guide